481 lines
20 KiB
TypeScript
481 lines
20 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) => {
|
|
// Add extra logging to debug folder rendering issues
|
|
console.log(`Rendering folder button: ${folder} for account: ${accountId}`);
|
|
|
|
// Ensure folder has a consistent format
|
|
let prefixedFolder = folder;
|
|
let baseFolderName = folder;
|
|
let folderAccountId = accountId;
|
|
|
|
// Extract parts if the folder has a prefix
|
|
if (folder.includes(':')) {
|
|
const parts = folder.split(':');
|
|
folderAccountId = parts[0];
|
|
baseFolderName = parts[1];
|
|
|
|
console.log(`Folder has prefix, extracted: accountId=${folderAccountId}, baseFolder=${baseFolderName}`);
|
|
} else {
|
|
// Add account prefix if missing
|
|
prefixedFolder = `${accountId}:${folder}`;
|
|
console.log(`Added prefix to folder: ${prefixedFolder}`);
|
|
}
|
|
|
|
// Only show folders that belong to this account
|
|
if (folderAccountId !== accountId) {
|
|
console.log(`Skipping folder ${folder} - belongs to different account (${folderAccountId})`);
|
|
return null;
|
|
}
|
|
|
|
// Check if this folder is selected for this account
|
|
// Must handle both prefixed and non-prefixed versions in the selected map
|
|
const isSelected =
|
|
(selectedFolders[accountId] === prefixedFolder) ||
|
|
(selectedFolders[accountId] === baseFolderName) ||
|
|
(selectedFolders[accountId]?.split(':')[1] === baseFolderName);
|
|
|
|
if (isSelected) {
|
|
console.log(`Folder ${baseFolderName} is selected for account ${accountId}`);
|
|
}
|
|
|
|
// Get unread count
|
|
const folderUnreadCount = unreadCount[accountId]?.[baseFolderName] ||
|
|
unreadCount[accountId]?.[prefixedFolder] || 0;
|
|
|
|
return (
|
|
<Button
|
|
key={prefixedFolder}
|
|
variant="ghost"
|
|
className={`w-full justify-start text-xs py-1 h-7 ${isSelected ? 'bg-gray-100' : ''}`}
|
|
onClick={() => {
|
|
console.log(`Folder button clicked: folder=${prefixedFolder}, accountId=${accountId}, normalized=${baseFolderName}`);
|
|
|
|
// Always ensure the folder name includes the account ID prefix
|
|
const fullyPrefixedFolder = folder.includes(':') ? folder : `${accountId}:${folder}`;
|
|
|
|
// Make sure we pass the EXACT accountId parameter here, not the folder's extracted account ID
|
|
console.log(`Calling onFolderChange with folder=${fullyPrefixedFolder}, accountId=${accountId}`);
|
|
onFolderChange(fullyPrefixedFolder, accountId);
|
|
}}
|
|
>
|
|
<div className="flex items-center w-full">
|
|
{getFolderIcon(baseFolderName)}
|
|
<span className="ml-2 truncate text-gray-700">{formatFolderName(baseFolderName)}</span>
|
|
{folderUnreadCount > 0 && (
|
|
<span className="ml-auto bg-blue-500 text-white text-[10px] px-1.5 rounded-full">
|
|
{folderUnreadCount}
|
|
</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>
|
|
);
|
|
}
|