Neah/components/email/EmailSidebar.tsx

443 lines
18 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import {
Inbox, Send, Trash, Archive, Star,
File, RefreshCw, Plus as PlusIcon, Edit,
ChevronDown, ChevronUp, Mail, Menu,
Settings, Loader2, AlertOctagon, MessageSquare
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Badge } from '@/components/ui/badge';
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { Input } from '@/components/ui/input';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
interface Account {
id: number | string;
name: string;
email: string;
color: string;
folders?: string[];
}
interface EmailSidebarProps {
accounts: Account[];
selectedAccount: Account | null;
selectedFolders: Record<string, string>;
currentFolder: string;
loading: boolean;
unreadCount: Record<string, Record<string, number>>;
showAddAccountForm: boolean;
showFolders?: boolean;
onFolderChange: (folder: string, accountId: string) => void;
onRefresh: () => void;
onComposeNew: () => void;
onAccountSelect: (account: Account) => void;
onShowAddAccountForm: (show: boolean) => void;
onAddAccount: (formData: any) => Promise<void>;
onEditAccount: (account: Account) => void;
onDeleteAccount: (account: Account) => void;
onSelectEmail?: (emailId: string, accountId: string, folder: string) => void;
onShowFoldersToggle?: (show: boolean) => void;
}
export default function EmailSidebar({
accounts,
selectedAccount,
selectedFolders,
currentFolder,
loading,
unreadCount,
showAddAccountForm,
showFolders = true,
onFolderChange,
onRefresh,
onComposeNew,
onAccountSelect,
onShowAddAccountForm,
onAddAccount,
onEditAccount,
onDeleteAccount,
onSelectEmail,
onShowFoldersToggle
}: EmailSidebarProps) {
const [isSaving, setIsSaving] = useState(false);
const [formData, setFormData] = useState({
email: '',
password: '',
displayName: '',
host: '',
port: '993',
useSSL: true,
smtpHost: '',
smtpPort: '587',
smtpUseSSL: false
});
const [activeTab, setActiveTab] = useState('imap');
// Handle form submission
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsSaving(true);
try {
await onAddAccount(formData);
setFormData({
email: '',
password: '',
displayName: '',
host: '',
port: '993',
useSSL: true,
smtpHost: '',
smtpPort: '587',
smtpUseSSL: false
});
onShowAddAccountForm(false);
} catch (err) {
console.error('Failed to add account:', err);
} finally {
setIsSaving(false);
}
};
// Handle input changes
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
};
// Get the appropriate icon for a folder
const getFolderIcon = (folder: string) => {
const folderLower = folder.toLowerCase();
if (folderLower.includes('inbox')) {
return <Inbox className="h-4 w-4 text-gray-500" />;
} else if (folderLower.includes('sent')) {
return <Send className="h-4 w-4 text-gray-500" />;
} else if (folderLower.includes('trash')) {
return <Trash className="h-4 w-4 text-gray-500" />;
} else if (folderLower.includes('archive')) {
return <Archive className="h-4 w-4 text-gray-500" />;
} else if (folderLower.includes('draft')) {
return <Edit className="h-4 w-4 text-gray-500" />;
} else if (folderLower.includes('spam') || folderLower.includes('junk')) {
return <AlertOctagon className="h-4 w-4 text-gray-500" />;
} else {
return <MessageSquare className="h-4 w-4 text-gray-500" />;
}
};
// Format folder names
const formatFolderName = (folder: string) => {
return folder.charAt(0).toUpperCase() + folder.slice(1).toLowerCase();
};
// Improve the renderFolderButton function to ensure consistent handling
const renderFolderButton = (folder: string, accountId: string) => {
// Ensure folder always has accountId prefix for consistency
const prefixedFolder = folder.includes(':') ? folder : `${accountId}:${folder}`;
// Extract the base folder name and account ID for display and checking
const [folderAccountId, baseFolder] = prefixedFolder.includes(':')
? prefixedFolder.split(':')
: [accountId, folder];
// Only show folders that belong to this account
if (folderAccountId !== accountId) return null;
const isSelected = selectedFolders[accountId] === prefixedFolder;
return (
<Button
key={folder}
variant="ghost"
className={`w-full justify-start text-xs py-1 h-7 ${isSelected ? 'bg-gray-100' : ''}`}
onClick={() => onFolderChange(folder, accountId)}
>
<div className="flex items-center w-full">
{getFolderIcon(baseFolder)}
<span className="ml-2 truncate text-gray-700">{formatFolderName(baseFolder)}</span>
{baseFolder === 'INBOX' && unreadCount[accountId]?.[folder] > 0 && (
<span className="ml-auto bg-blue-500 text-white text-[10px] px-1.5 rounded-full">
{unreadCount[accountId][folder]}
</span>
)}
</div>
</Button>
);
};
return (
<div className="w-60 bg-white/95 backdrop-blur-sm border-r border-gray-100 flex flex-col md:flex" style={{display: "flex !important"}}>
{/* Courrier Title */}
<div className="p-3 border-b border-gray-100">
<div className="flex items-center gap-2">
<Mail className="h-6 w-6 text-gray-600" />
<span className="text-xl font-semibold text-gray-900">COURRIER</span>
</div>
</div>
{/* Compose button and refresh button */}
<div className="p-2 border-b border-gray-100 flex items-center gap-2">
<Button
className="flex-1 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center justify-center transition-all py-1.5 text-sm"
onClick={onComposeNew}
>
<div className="flex items-center gap-2">
<PlusIcon className="h-3.5 w-3.5" />
<span>Compose</span>
</div>
</Button>
<Button
variant="ghost"
size="icon"
className="h-9 w-9 text-gray-400 hover:text-gray-600"
onClick={onRefresh}
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
</Button>
</div>
{/* Scrollable area for accounts and folders */}
<div className="flex-1 overflow-y-auto">
{/* Accounts Section */}
<div className="p-3 border-b border-gray-100">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-500">Accounts</span>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0 text-gray-400 hover:text-gray-600"
onClick={() => onShowFoldersToggle?.(showFolders ? false : true)}
>
{showFolders ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0 text-gray-400 hover:text-gray-600"
onClick={() => onShowAddAccountForm(!showAddAccountForm)}
>
<PlusIcon className="h-4 w-4" />
</Button>
</div>
</div>
{/* Display all accounts */}
<div className="mt-1">
{/* Form for adding a new account - Content is identical to courrier page */}
{showAddAccountForm && (
<div className="mb-2 p-2 border border-gray-200 rounded-md bg-white">
<h4 className="text-xs font-medium mb-0.5 text-gray-700">Add IMAP Account</h4>
<form onSubmit={handleSubmit}>
<div>
<Tabs defaultValue="imap" className="w-full">
<TabsList className="grid w-full grid-cols-2 h-6 mb-0.5 bg-gray-100">
<TabsTrigger value="imap" className="text-xs h-5 data-[state=active]:bg-blue-500 data-[state=active]:text-white">IMAP</TabsTrigger>
<TabsTrigger value="smtp" className="text-xs h-5 data-[state=active]:bg-blue-500 data-[state=active]:text-white">SMTP</TabsTrigger>
</TabsList>
<TabsContent value="imap" className="mt-0.5 space-y-0.5">
<div>
<Input
id="email"
name="email"
placeholder="email@example.com"
className="h-7 text-xs bg-white border-gray-300 mb-0.5 text-gray-900"
required
value={formData.email}
onChange={handleChange}
/>
</div>
<div>
<Input
id="password"
name="password"
type="password"
placeholder="•••••••••"
className="h-7 text-xs bg-white border-gray-300 mb-0.5 text-gray-900"
required
value={formData.password}
onChange={handleChange}
/>
</div>
<div>
<Input
id="display_name"
name="displayName"
placeholder="John Doe"
className="h-7 text-xs bg-white border-gray-300 mb-0.5 text-gray-900"
value={formData.displayName}
onChange={handleChange}
/>
</div>
<div>
<Input
id="host"
name="host"
placeholder="imap.example.com"
className="h-7 text-xs bg-white border-gray-300 mb-0.5 text-gray-900"
required
value={formData.host}
onChange={handleChange}
/>
</div>
<div className="flex gap-1">
<div className="flex-1">
<Input
id="port"
name="port"
placeholder="993"
className="h-7 text-xs bg-white border-gray-300 text-gray-900"
value={formData.port}
onChange={handleChange}
/>
</div>
<div className="flex items-center pl-1">
<div className="flex items-center space-x-1">
<Checkbox
id="useSSL"
name="useSSL"
checked={formData.useSSL}
onCheckedChange={(checked) => {
setFormData(prev => ({
...prev,
useSSL: checked === true
}));
}}
/>
<Label htmlFor="useSSL" className="text-xs">SSL</Label>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="smtp" className="mt-0.5 space-y-0.5">
<div>
<Input
id="smtp_host"
name="smtpHost"
placeholder="smtp.example.com"
className="h-7 text-xs bg-white border-gray-300 mb-0.5 text-gray-900"
value={formData.smtpHost}
onChange={handleChange}
/>
</div>
<div className="flex gap-1">
<div className="flex-1">
<Input
id="smtp_port"
name="smtpPort"
placeholder="587"
className="h-7 text-xs bg-white border-gray-300 text-gray-900"
value={formData.smtpPort}
onChange={handleChange}
/>
</div>
<div className="flex items-center pl-1">
<div className="flex items-center space-x-1">
<Checkbox
id="smtp_secure"
name="smtpUseSSL"
checked={formData.smtpUseSSL}
onCheckedChange={(checked) => {
setFormData(prev => ({
...prev,
smtpUseSSL: checked === true
}));
}}
/>
<Label htmlFor="smtp_secure" className="text-xs">SSL</Label>
</div>
</div>
</div>
<div className="text-xs text-gray-500 italic">
Note: SMTP settings needed for sending emails
</div>
</TabsContent>
</Tabs>
<div className="flex gap-1 mt-1">
<Button
type="submit"
className="flex-1 h-6 text-xs bg-blue-500 hover:bg-blue-600 text-white rounded-md px-2 py-0"
disabled={isSaving}
>
{isSaving ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : null}
Test & Add
</Button>
<Button
type="button"
className="h-6 text-xs bg-gray-200 text-gray-800 hover:bg-gray-300 rounded-md px-2 py-0"
onClick={() => onShowAddAccountForm(false)}
>
Cancel
</Button>
</div>
</div>
</form>
</div>
)}
{accounts.map((account) => (
<div key={account.id} className="mb-1">
<div className={`flex items-center w-full px-1 py-1 rounded-md cursor-pointer ${selectedAccount?.id === account.id ? 'bg-gray-100' : ''}`}
onClick={() => onAccountSelect(account)}
tabIndex={0}
role="button"
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') onAccountSelect(account); }}
>
<div className={`w-3 h-3 rounded-full ${account.color?.startsWith('#') ? 'bg-blue-500' : account.color || 'bg-blue-500'} mr-2`}></div>
<span className="truncate text-gray-700 flex-1">{account.name}</span>
{/* More options button (⋮) */}
{account.id !== 'loading-account' && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className="ml-1 text-gray-400 hover:text-gray-600 cursor-pointer flex items-center justify-center h-5 w-5"
tabIndex={-1}
onClick={e => e.stopPropagation()}
aria-label="Account options"
>
<span style={{ fontSize: '18px', lineHeight: 1 }}></span>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={e => { e.stopPropagation(); onEditAccount(account); }}>
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={e => { e.stopPropagation(); onDeleteAccount(account); }}>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
{/* Show folders for each account when selected and when folders are visible */}
{selectedAccount?.id === account.id && showFolders && account.folders && account.folders.length > 0 && (
<div className="pl-4">
{account.folders.map((folder: string) => renderFolderButton(folder, account.id.toString()))}
</div>
)}
</div>
))}
</div>
</div>
</div>
</div>
);
}