courrier multi account restore compose
This commit is contained in:
parent
2fb4fcd069
commit
c2bb904fde
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user