courrier multi account restore compose

This commit is contained in:
alma 2025-04-29 09:31:57 +02:00
parent 2fb4fcd069
commit c2bb904fde
2 changed files with 456 additions and 483 deletions

View File

@ -102,6 +102,18 @@ interface EmailMessage {
}; };
} }
interface AccountData {
email: string;
password: string;
host: string;
port: number;
secure: boolean;
display_name: string;
smtp_host?: string;
smtp_port?: number;
smtp_secure?: boolean;
}
// Define a color palette for account circles // Define a color palette for account circles
const colorPalette = [ const colorPalette = [
'bg-blue-500', 'bg-blue-500',
@ -654,11 +666,16 @@ export default function CourrierPage() {
// Handle sending email // Handle sending email
const handleSendEmail = async (emailData: EmailData) => { const handleSendEmail = async (emailData: EmailData) => {
const result = await sendEmail(emailData); try {
if (!result.success) { const result = await sendEmail(emailData);
throw new Error(result.error); if (!result.success) {
throw new Error(result.error);
}
return result;
} catch (error) {
// Handle any errors
throw error;
} }
return result;
}; };
// Handle delete confirmation // Handle delete confirmation
@ -742,325 +759,123 @@ export default function CourrierPage() {
<main className="w-full h-screen bg-black"> <main className="w-full h-screen bg-black">
<div className="w-full h-full px-4 pt-12 pb-4"> <div className="w-full h-full px-4 pt-12 pb-4">
<div className="flex h-full bg-carnet-bg"> <div className="flex h-full bg-carnet-bg">
{/* Panel 1: Sidebar - Always visible */} {/* Use EmailSidebar component instead of inline sidebar */}
<div className="w-60 bg-white/95 backdrop-blur-sm border-r border-gray-100 flex flex-col md:flex" style={{display: "flex !important"}}> <EmailSidebar
{/* Courrier Title */} accounts={accounts}
<div className="p-3 border-b border-gray-100"> selectedAccount={selectedAccount}
<div className="flex items-center gap-2"> selectedFolders={selectedFolders}
<Mail className="h-6 w-6 text-gray-600" /> currentFolder={currentFolder}
<span className="text-xl font-semibold text-gray-900">COURRIER</span> expandedAccounts={expandedAccounts}
</div> loading={loading}
</div> unreadCount={unreadCount}
showAddAccountForm={showAddAccountForm}
{/* Compose button and refresh button */} onFolderChange={handleMailboxChange}
<div className="p-2 border-b border-gray-100 flex items-center gap-2"> onRefresh={() => {
<Button setLoading(true);
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" setPage(1);
onClick={handleComposeNew} loadEmails().finally(() => setLoading(false));
> }}
<div className="flex items-center gap-2"> onComposeNew={handleComposeNew}
<PlusIcon className="h-3.5 w-3.5" /> onAccountSelect={handleAccountSelect}
<span>Compose</span> onToggleExpand={(accountId, expanded) => {
</div> setExpandedAccounts(prev => ({ ...prev, [accountId]: expanded }));
</Button> }}
<Button onShowAddAccountForm={setShowAddAccountForm}
variant="ghost" onAddAccount={async (formData) => {
size="icon" setLoading(true);
className="h-9 w-9 text-gray-400 hover:text-gray-600"
onClick={() => { // Pull values from form with proper type handling
setLoading(true); const formValues = {
// Reset to page 1 when manually refreshing email: formData.get('email')?.toString() || '',
setPage(1); password: formData.get('password')?.toString() || '',
// Load emails host: formData.get('host')?.toString() || '',
loadEmails().finally(() => setLoading(false)); port: parseInt(formData.get('port')?.toString() || '993'),
}} secure: formData.get('secure') === 'on',
> display_name: formData.get('display_name')?.toString() || '',
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} /> smtp_host: formData.get('smtp_host')?.toString() || '',
</Button> smtp_port: formData.get('smtp_port')?.toString() ?
</div> parseInt(formData.get('smtp_port')?.toString() || '587') : undefined,
smtp_secure: formData.get('smtp_secure') === 'on'
{/* Scrollable area for accounts and folders */} };
<div className="flex-1 overflow-y-auto">
{/* Accounts Section */} // If display_name is empty, use email
<div className="p-3 border-b border-gray-100"> if (!formValues.display_name) {
<div className="flex items-center justify-between mb-2"> formValues.display_name = formValues.email;
<span className="text-sm font-medium text-gray-500">Accounts</span> }
<Button
variant="ghost" try {
size="sm" // First test the connection
className="h-7 w-7 p-0 text-gray-400 hover:text-gray-600" const testResponse = await fetch('/api/courrier/test-connection', {
onClick={() => setShowAddAccountForm(!showAddAccountForm)} method: 'POST',
> headers: {
<Plus className="h-4 w-4" /> 'Content-Type': 'application/json'
</Button> },
</div> body: JSON.stringify({
email: formValues.email,
password: formValues.password,
host: formValues.host,
port: formValues.port,
secure: formValues.secure
})
});
{/* Display all accounts */} const testResult = await testResponse.json();
<div className="mt-1">
{/* Form for adding a new account */} if (!testResponse.ok) {
{showAddAccountForm && ( throw new Error(testResult.error || 'Connection test failed');
<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={async (e) => { console.log('Connection test successful:', testResult);
e.preventDefault();
setLoading(true); // Only declare realAccounts once before using for color assignment
const realAccounts = accounts.filter(a => a.id !== 'loading-account');
const formData = new FormData(e.currentTarget); const saveResponse = await fetch('/api/courrier/account', {
method: 'POST',
// Pull values from form with proper type handling headers: {
const formValues = { 'Content-Type': 'application/json'
email: formData.get('email')?.toString() || '', },
password: formData.get('password')?.toString() || '', body: JSON.stringify(formValues)
host: formData.get('host')?.toString() || '', });
port: parseInt(formData.get('port')?.toString() || '993'), const saveResult = await saveResponse.json();
secure: formData.get('secure') === 'on', if (!saveResponse.ok) {
display_name: formData.get('display_name')?.toString() || '', throw new Error(saveResult.error || 'Failed to add account');
smtp_host: formData.get('smtp_host')?.toString() || '', }
smtp_port: formData.get('smtp_port')?.toString() ? const realAccount = saveResult.account;
parseInt(formData.get('smtp_port')?.toString() || '587') : undefined, realAccount.color = colorPalette[realAccounts.length % colorPalette.length];
smtp_secure: formData.get('smtp_secure') === 'on' realAccount.folders = testResult.details.sampleFolders || ['INBOX', 'Sent', 'Drafts', 'Trash'];
}; setAccounts(prev => [...prev, realAccount]);
setShowAddAccountForm(false);
// If display_name is empty, use email toast({
if (!formValues.display_name) { title: "Account added successfully",
formValues.display_name = formValues.email; description: `Your email account ${formValues.email} has been added.`,
} duration: 5000
});
try { } catch (error) {
// First test the connection console.error('Error adding account:', error);
const testResponse = await fetch('/api/courrier/test-connection', { toast({
method: 'POST', title: "Failed to add account",
headers: { description: error instanceof Error ? error.message : 'Unknown error',
'Content-Type': 'application/json' variant: "destructive",
}, duration: 5000
body: JSON.stringify({ });
email: formValues.email, } finally {
password: formValues.password, setLoading(false);
host: formValues.host, }
port: formValues.port, }}
secure: formValues.secure onEditAccount={(account) => {
}) setAccountToEdit(account);
}); setShowEditModal(true);
}}
const testResult = await testResponse.json(); onDeleteAccount={(account) => {
setAccountToDelete(account);
if (!testResponse.ok) { setShowDeleteDialog(true);
throw new Error(testResult.error || 'Connection test failed'); }}
} onSelectEmail={(emailId, accountId, folder) => {
if (typeof emailId === 'string') {
console.log('Connection test successful:', testResult); handleEmailSelect(emailId, accountId || '', folder || currentFolder);
}
// Only declare realAccounts once before using for color assignment }}
const realAccounts = accounts.filter(a => a.id !== 'loading-account'); />
const saveResponse = await fetch('/api/courrier/account', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formValues)
});
const saveResult = await saveResponse.json();
if (!saveResponse.ok) {
throw new Error(saveResult.error || 'Failed to add account');
}
const realAccount = saveResult.account;
realAccount.color = colorPalette[realAccounts.length % colorPalette.length];
realAccount.folders = testResult.details.sampleFolders || ['INBOX', 'Sent', 'Drafts', 'Trash'];
setAccounts(prev => [...prev, realAccount]);
setShowAddAccountForm(false);
toast({
title: "Account added successfully",
description: `Your email account ${formValues.email} has been added.`,
duration: 5000
});
} catch (error) {
console.error('Error adding account:', error);
toast({
title: "Failed to add account",
description: error instanceof Error ? error.message : 'Unknown error',
variant: "destructive",
duration: 5000
});
} finally {
setLoading(false);
}
}}>
<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
/>
</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
/>
</div>
<div>
<Input
id="display_name"
name="display_name"
placeholder="John Doe"
className="h-7 text-xs bg-white border-gray-300 mb-0.5 text-gray-900"
/>
</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
/>
</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"
defaultValue="993"
required
/>
</div>
<div className="flex items-center pl-1">
<div className="flex items-center space-x-1">
<Checkbox id="secure" name="secure" defaultChecked />
<Label htmlFor="secure" 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="smtp_host"
placeholder="smtp.example.com"
className="h-7 text-xs bg-white border-gray-300 mb-0.5 text-gray-900"
/>
</div>
<div className="flex gap-1">
<div className="flex-1">
<Input
id="smtp_port"
name="smtp_port"
placeholder="587"
className="h-7 text-xs bg-white border-gray-300 text-gray-900"
defaultValue="587"
/>
</div>
<div className="flex items-center pl-1">
<div className="flex items-center space-x-1">
<Checkbox id="smtp_secure" name="smtp_secure" defaultChecked />
<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={loading}
>
{loading ? <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={() => setShowAddAccountForm(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={() => handleAccountSelect(account)}
tabIndex={0}
role="button"
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') handleAccountSelect(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(); setAccountToEdit(account); setShowEditModal(true); }}>
Edit
</DropdownMenuItem>
<DropdownMenuItem onClick={e => { e.stopPropagation(); setAccountToDelete(account); setShowDeleteDialog(true); }}>
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{/* Expand/collapse arrow */}
{account.id !== 'loading-account' && (
<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(); setExpandedAccounts(prev => ({ ...prev, [account.id]: !prev[account.id] })); }}
>
{expandedAccounts[account.id] ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
</button>
)}
</div>
{/* Show folders for any expanded account */}
{expandedAccounts[account.id] && account.folders && account.folders.length > 0 && (
<div className="pl-4">
{account.folders.map((folder) => renderFolderButton(folder, account.id))}
</div>
)}
</div>
))}
</div>
</div>
</div>
</div>
{/* Panel 2: Email List - Always visible */} {/* Panel 2: Email List - Always visible */}
<div className="w-80 flex flex-col border-r border-gray-100 overflow-hidden"> <div className="w-80 flex flex-col border-r border-gray-100 overflow-hidden">
@ -1160,7 +975,7 @@ export default function CourrierPage() {
emails={emails} emails={emails}
selectedEmailIds={selectedEmailIds} selectedEmailIds={selectedEmailIds}
selectedEmail={selectedEmail} selectedEmail={selectedEmail}
onSelectEmail={handleEmailSelect} onSelectEmail={(emailId) => handleEmailSelect(emailId, selectedAccount?.id || '', currentFolder)}
onToggleSelect={toggleEmailSelection} onToggleSelect={toggleEmailSelection}
onToggleSelectAll={toggleSelectAll} onToggleSelectAll={toggleSelectAll}
onToggleStarred={toggleStarred} onToggleStarred={toggleStarred}
@ -1184,9 +999,9 @@ export default function CourrierPage() {
<div className="flex-1 overflow-hidden bg-white"> <div className="flex-1 overflow-hidden bg-white">
{selectedEmail ? ( {selectedEmail ? (
<EmailDetailView <EmailDetailView
email={selectedEmail} email={selectedEmail as any}
onBack={() => { onBack={() => {
handleEmailSelect(''); handleEmailSelect('', '', '');
// Ensure sidebar stays visible // Ensure sidebar stays visible
setSidebarOpen(true); setSidebarOpen(true);
}} }}
@ -1234,7 +1049,10 @@ export default function CourrierPage() {
<ComposeEmail <ComposeEmail
type={composeType} type={composeType}
initialEmail={composeType !== 'new' ? selectedEmail : undefined} initialEmail={composeType !== 'new' ? selectedEmail : undefined}
onSend={handleSendEmail} onSend={(emailData) => {
const result = sendEmail(emailData);
return result;
}}
onClose={() => setShowComposeModal(false)} onClose={() => setShowComposeModal(false)}
/> />
</DialogContent> </DialogContent>

View File

@ -3,206 +3,361 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { import {
Inbox, Send, Trash, Archive, Star, Inbox, Send, Trash, Archive, Star,
File, RefreshCw, Plus, MailOpen, Settings, File, RefreshCw, Plus as PlusIcon, Edit,
ChevronDown, ChevronRight, Mail ChevronDown, ChevronUp, Mail, Menu,
Settings, Loader2, AlertOctagon, MessageSquare
} from 'lucide-react'; } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { Badge } from '@/components/ui/badge'; 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: string;
name: string;
email: string;
color: string;
folders: string[];
}
interface EmailSidebarProps { interface EmailSidebarProps {
accounts: Account[];
selectedAccount: Account | null;
selectedFolders: Record<string, string>;
currentFolder: string; currentFolder: string;
currentAccount: string; expandedAccounts: Record<string, boolean>;
accounts: Array<{ loading: boolean;
id: string; unreadCount: number;
email: string; showAddAccountForm: boolean;
folders: string[]; // Actions
}>;
onFolderChange: (folder: string, accountId: string) => void; onFolderChange: (folder: string, accountId: string) => void;
onRefresh: () => void; onRefresh: () => void;
onCompose: () => void; onComposeNew: () => void;
isLoading: boolean; onAccountSelect: (account: Account) => void;
onToggleExpand: (accountId: string, expanded: boolean) => void;
onShowAddAccountForm: (show: boolean) => void;
onAddAccount: (formData: FormData) => Promise<void>;
onEditAccount: (account: Account) => void;
onDeleteAccount: (account: Account) => void;
onSelectEmail?: (emailId: string, accountId: string, folder: string) => void;
} }
export default function EmailSidebar({ export default function EmailSidebar({
currentFolder,
currentAccount,
accounts, accounts,
selectedAccount,
selectedFolders,
currentFolder,
expandedAccounts,
loading,
unreadCount,
showAddAccountForm,
onFolderChange, onFolderChange,
onRefresh, onRefresh,
onCompose, onComposeNew,
isLoading onAccountSelect,
onToggleExpand,
onShowAddAccountForm,
onAddAccount,
onEditAccount,
onDeleteAccount
}: EmailSidebarProps) { }: EmailSidebarProps) {
const [showAccounts, setShowAccounts] = useState(true);
const [expandedAccount, setExpandedAccount] = useState<string | null>(currentAccount);
// Get the appropriate icon for a folder // Get the appropriate icon for a folder
const getFolderIcon = (folder: string) => { const getFolderIcon = (folder: string) => {
const folderLower = folder.toLowerCase(); const folderLower = folder.toLowerCase();
switch (folderLower) { if (folderLower.includes('inbox')) {
case 'inbox': return <Inbox className="h-4 w-4 text-gray-500" />;
return <Inbox className="h-4 w-4" />; } else if (folderLower.includes('sent')) {
case 'sent': return <Send className="h-4 w-4 text-gray-500" />;
case 'sent items': } else if (folderLower.includes('trash')) {
return <Send className="h-4 w-4" />; return <Trash className="h-4 w-4 text-gray-500" />;
case 'drafts': } else if (folderLower.includes('archive')) {
return <File className="h-4 w-4" />; return <Archive className="h-4 w-4 text-gray-500" />;
case 'trash': } else if (folderLower.includes('draft')) {
case 'deleted': return <Edit className="h-4 w-4 text-gray-500" />;
case 'bin': } else if (folderLower.includes('spam') || folderLower.includes('junk')) {
return <Trash className="h-4 w-4" />; return <AlertOctagon className="h-4 w-4 text-gray-500" />;
case 'archive': } else {
case 'archived': return <MessageSquare className="h-4 w-4 text-gray-500" />;
return <Archive className="h-4 w-4" />;
case 'starred':
case 'important':
return <Star className="h-4 w-4" />;
default:
return <MailOpen className="h-4 w-4" />;
} }
}; };
// Group folders into standard and custom // Format folder names
const getStandardFolders = (folders: string[]) => { const formatFolderName = (folder: string) => {
const standardFolders = ['INBOX', 'Sent', 'Drafts', 'Trash', 'Archive', 'Junk']; return folder.charAt(0).toUpperCase() + folder.slice(1).toLowerCase();
return standardFolders.filter(f =>
folders.includes(f) || folders.some(folder => folder.toLowerCase() === f.toLowerCase())
);
};
const getCustomFolders = (folders: string[]) => {
const standardFolders = ['INBOX', 'Sent', 'Drafts', 'Trash', 'Archive', 'Junk'];
return folders.filter(f =>
!standardFolders.some(sf => sf.toLowerCase() === f.toLowerCase())
);
}; };
const handleAccountClick = (accountId: string) => { // Render folder button with exact same styling as in courrier page
setExpandedAccount(accountId); const renderFolderButton = (folder: string, accountId: string) => {
// Get the account prefix from the folder name
const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId;
// Only show folders that belong to this account
if (folderAccountId !== accountId) return null;
const isSelected = selectedFolders[accountId] === folder;
// Get the base folder name for display
const baseFolder = folder.includes(':') ? folder.split(':')[1] : folder;
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 > 0 && (
<span className="ml-auto bg-blue-500 text-white text-[10px] px-1.5 rounded-full">
{unreadCount}
</span>
)}
</div>
</Button>
);
}; };
return ( return (
<aside className="w-64 border-r h-full flex flex-col bg-white/95 backdrop-blur-sm"> <div className="w-60 bg-white/95 backdrop-blur-sm border-r border-gray-100 flex flex-col md:flex" style={{display: "flex !important"}}>
{/* Compose button area */} {/* Courrier Title */}
<div className="p-4"> <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 <Button
className="w-full bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center justify-center py-2" 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={onCompose} onClick={onComposeNew}
> >
<Plus className="h-4 w-4 mr-2" /> <div className="flex items-center gap-2">
Compose <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> </Button>
</div> </div>
{/* Accounts and folders navigation */} {/* Scrollable area for accounts and folders */}
<ScrollArea className="flex-1"> <div className="flex-1 overflow-y-auto">
<div className="p-2 space-y-1"> {/* Accounts Section */}
{/* Accounts header with toggle and add button */} <div className="p-3 border-b border-gray-100">
<div className="flex items-center justify-between px-2 py-2 text-sm font-medium text-gray-600"> <div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2"> <span className="text-sm font-medium text-gray-500">Accounts</span>
<button <Button
onClick={() => setShowAccounts(!showAccounts)} variant="ghost"
className="text-gray-400 hover:text-gray-600" size="sm"
> className="h-7 w-7 p-0 text-gray-400 hover:text-gray-600"
{showAccounts ? ( onClick={() => onShowAddAccountForm(!showAddAccountForm)}
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</button>
<span>Accounts</span>
</div>
<button
onClick={() => {/* Add account logic here */}}
className="text-gray-400 hover:text-gray-600"
> >
<Plus className="h-4 w-4" /> <PlusIcon className="h-4 w-4" />
</button> </Button>
</div> </div>
{/* Accounts list */} {/* Display all accounts */}
{showAccounts && ( <div className="mt-1">
<div className="space-y-1"> {/* Form for adding a new account - Content is identical to courrier page */}
{accounts.map((account) => ( {showAddAccountForm && (
<div key={account.id} className="space-y-1"> <div className="mb-2 p-2 border border-gray-200 rounded-md bg-white">
{/* Account button */} <h4 className="text-xs font-medium mb-0.5 text-gray-700">Add IMAP Account</h4>
<Button <form onSubmit={async (e) => {
variant="ghost" e.preventDefault();
className={`w-full justify-between p-2 text-gray-600 hover:text-gray-900 hover:bg-gray-100 ${ await onAddAccount(new FormData(e.currentTarget));
expandedAccount === account.id ? 'bg-gray-100' : '' }}>
}`} <div>
onClick={() => handleAccountClick(account.id)} <Tabs defaultValue="imap" className="w-full">
> <TabsList className="grid w-full grid-cols-2 h-6 mb-0.5 bg-gray-100">
<div className="flex items-center"> <TabsTrigger value="imap" className="text-xs h-5 data-[state=active]:bg-blue-500 data-[state=active]:text-white">IMAP</TabsTrigger>
<Mail className="h-4 w-4 mr-2" /> <TabsTrigger value="smtp" className="text-xs h-5 data-[state=active]:bg-blue-500 data-[state=active]:text-white">SMTP</TabsTrigger>
<span className="truncate">{account.email}</span> </TabsList>
</div>
</Button>
{/* Account folders - shown when account is selected */}
{expandedAccount === account.id && (
<div className="pl-6 space-y-1">
{getStandardFolders(account.folders).map((folder) => (
<Button
key={folder}
variant={currentFolder === folder && currentAccount === account.id ? "secondary" : "ghost"}
className={`w-full justify-start ${
currentFolder === folder && currentAccount === account.id ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900'
}`}
onClick={() => onFolderChange(folder, account.id)}
>
<div className="flex items-center w-full">
<span className="flex items-center">
{getFolderIcon(folder)}
<span className="ml-2 capitalize">{folder.toLowerCase()}</span>
</span>
{folder === 'INBOX' && (
<span className="ml-auto bg-blue-600 text-white text-xs px-2 py-0.5 rounded-full">
{/* Unread count would go here */}
</span>
)}
</div>
</Button>
))}
{/* Custom folders */} <TabsContent value="imap" className="mt-0.5 space-y-0.5">
{getCustomFolders(account.folders).map(folder => ( <div>
<Button <Input
key={folder} id="email"
variant={currentFolder === folder && currentAccount === account.id ? "secondary" : "ghost"} name="email"
className={`w-full justify-start ${ placeholder="email@example.com"
currentFolder === folder && currentAccount === account.id ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900' className="h-7 text-xs bg-white border-gray-300 mb-0.5 text-gray-900"
}`} required
onClick={() => onFolderChange(folder, account.id)} />
> </div>
<div className="flex items-center"> <div>
{getFolderIcon(folder)} <Input
<span className="ml-2 truncate">{folder}</span> id="password"
name="password"
type="password"
placeholder="•••••••••"
className="h-7 text-xs bg-white border-gray-300 mb-0.5 text-gray-900"
required
/>
</div>
<div>
<Input
id="display_name"
name="display_name"
placeholder="John Doe"
className="h-7 text-xs bg-white border-gray-300 mb-0.5 text-gray-900"
/>
</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
/>
</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"
defaultValue="993"
required
/>
</div> </div>
</Button> <div className="flex items-center pl-1">
))} <div className="flex items-center space-x-1">
<Checkbox id="secure" name="secure" defaultChecked />
<Label htmlFor="secure" 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="smtp_host"
placeholder="smtp.example.com"
className="h-7 text-xs bg-white border-gray-300 mb-0.5 text-gray-900"
/>
</div>
<div className="flex gap-1">
<div className="flex-1">
<Input
id="smtp_port"
name="smtp_port"
placeholder="587"
className="h-7 text-xs bg-white border-gray-300 text-gray-900"
defaultValue="587"
/>
</div>
<div className="flex items-center pl-1">
<div className="flex items-center space-x-1">
<Checkbox id="smtp_secure" name="smtp_secure" defaultChecked />
<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={loading}
>
{loading ? <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>
</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>
)}
{/* Expand/collapse arrow */}
{account.id !== 'loading-account' && (
<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(); onToggleExpand(account.id, !expandedAccounts[account.id]); }}
>
{expandedAccounts[account.id] ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
</button>
)} )}
</div> </div>
))} {/* Show folders for any expanded account */}
</div> {expandedAccounts[account.id] && account.folders && account.folders.length > 0 && (
)} <div className="pl-4">
{account.folders.map((folder) => renderFolderButton(folder, account.id))}
</div>
)}
</div>
))}
</div>
</div> </div>
</ScrollArea>
{/* Settings button (bottom) */}
<div className="p-2 border-t border-gray-100">
<Button
variant="ghost"
size="sm"
className="w-full justify-start text-gray-600 hover:text-gray-900"
>
<Settings className="h-4 w-4 mr-2" />
<span>Email settings</span>
</Button>
</div> </div>
</aside> </div>
); );
} }