courrier multi account restore compose
This commit is contained in:
parent
18a17ceaf5
commit
525af4dab3
@ -36,6 +36,7 @@ import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuIte
|
||||
// Import components
|
||||
import EmailSidebar from '@/components/email/EmailSidebar';
|
||||
import EmailList from '@/components/email/EmailList';
|
||||
import EmailSidebarContent from '@/components/email/EmailSidebarContent';
|
||||
import EmailDetailView from '@/components/email/EmailDetailView';
|
||||
import ComposeEmail from '@/components/email/ComposeEmail';
|
||||
import { DeleteConfirmDialog } from '@/components/email/EmailDialogs';
|
||||
@ -742,30 +743,324 @@ export default function CourrierPage() {
|
||||
<div className="w-full h-full px-4 pt-12 pb-4">
|
||||
<div className="flex h-full bg-carnet-bg">
|
||||
{/* Panel 1: Sidebar - Always visible */}
|
||||
<div className={`${sidebarOpen ? 'w-64' : 'w-0'} transition-all duration-300 flex-shrink-0`}>
|
||||
<EmailSidebar
|
||||
currentFolder={currentFolder}
|
||||
currentAccount={selectedAccount?.id || ''}
|
||||
accounts={accounts}
|
||||
onFolderChange={(folder, accountId) => {
|
||||
const account = accounts.find(a => a.id === accountId);
|
||||
if (account) {
|
||||
setSelectedAccount(account);
|
||||
changeFolder(folder);
|
||||
<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={handleComposeNew}
|
||||
>
|
||||
<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={() => {
|
||||
setLoading(true);
|
||||
// Reset to page 1 when manually refreshing
|
||||
setPage(1);
|
||||
// Load emails
|
||||
loadEmails().finally(() => setLoading(false));
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0 text-gray-400 hover:text-gray-600"
|
||||
onClick={() => setShowAddAccountForm(!showAddAccountForm)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Display all accounts */}
|
||||
<div className="mt-1">
|
||||
{/* Form for adding a new account */}
|
||||
{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={async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
|
||||
// Pull values from form with proper type handling
|
||||
const formValues = {
|
||||
email: formData.get('email')?.toString() || '',
|
||||
password: formData.get('password')?.toString() || '',
|
||||
host: formData.get('host')?.toString() || '',
|
||||
port: parseInt(formData.get('port')?.toString() || '993'),
|
||||
secure: formData.get('secure') === 'on',
|
||||
display_name: formData.get('display_name')?.toString() || '',
|
||||
smtp_host: formData.get('smtp_host')?.toString() || '',
|
||||
smtp_port: formData.get('smtp_port')?.toString() ?
|
||||
parseInt(formData.get('smtp_port')?.toString() || '587') : undefined,
|
||||
smtp_secure: formData.get('smtp_secure') === 'on'
|
||||
};
|
||||
|
||||
// If display_name is empty, use email
|
||||
if (!formValues.display_name) {
|
||||
formValues.display_name = formValues.email;
|
||||
}
|
||||
}}
|
||||
onRefresh={() => {
|
||||
if (selectedAccount) {
|
||||
loadEmails(selectedAccount.id, currentFolder);
|
||||
|
||||
try {
|
||||
// First test the connection
|
||||
const testResponse = await fetch('/api/courrier/test-connection', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: formValues.email,
|
||||
password: formValues.password,
|
||||
host: formValues.host,
|
||||
port: formValues.port,
|
||||
secure: formValues.secure
|
||||
})
|
||||
});
|
||||
|
||||
const testResult = await testResponse.json();
|
||||
|
||||
if (!testResponse.ok) {
|
||||
throw new Error(testResult.error || 'Connection test failed');
|
||||
}
|
||||
}}
|
||||
onCompose={() => {
|
||||
setComposeType('new');
|
||||
setShowComposeModal(true);
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
|
||||
console.log('Connection test successful:', testResult);
|
||||
|
||||
// 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 */}
|
||||
<div className="w-80 flex flex-col border-r border-gray-100 overflow-hidden">
|
||||
|
||||
68
components/email/EmailSidebarContent.tsx
Normal file
68
components/email/EmailSidebarContent.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Inbox, Send, Star, Trash, Folder,
|
||||
AlertOctagon, Archive, Edit
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface EmailSidebarContentProps {
|
||||
mailboxes: string[];
|
||||
currentFolder: string;
|
||||
onFolderChange: (folder: string) => void;
|
||||
}
|
||||
|
||||
export default function EmailSidebarContent({
|
||||
mailboxes,
|
||||
currentFolder,
|
||||
onFolderChange
|
||||
}: EmailSidebarContentProps) {
|
||||
|
||||
// Helper to format folder names
|
||||
const formatFolderName = (folder: string) => {
|
||||
return folder.charAt(0).toUpperCase() + folder.slice(1).toLowerCase();
|
||||
};
|
||||
|
||||
// Helper to get folder icons
|
||||
const getFolderIcon = (folder: string) => {
|
||||
const folderLower = folder.toLowerCase();
|
||||
|
||||
if (folderLower.includes('inbox')) {
|
||||
return <Inbox className="h-4 w-4" />;
|
||||
} else if (folderLower.includes('sent')) {
|
||||
return <Send className="h-4 w-4" />;
|
||||
} else if (folderLower.includes('trash')) {
|
||||
return <Trash className="h-4 w-4" />;
|
||||
} else if (folderLower.includes('archive')) {
|
||||
return <Archive className="h-4 w-4" />;
|
||||
} else if (folderLower.includes('draft')) {
|
||||
return <Edit className="h-4 w-4" />;
|
||||
} else if (folderLower.includes('spam') || folderLower.includes('junk')) {
|
||||
return <AlertOctagon className="h-4 w-4" />;
|
||||
} else {
|
||||
return <Folder className="h-4 w-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className="p-3">
|
||||
<ul className="space-y-0.5 px-2">
|
||||
{mailboxes.map((folder) => (
|
||||
<li key={folder}>
|
||||
<Button
|
||||
variant={currentFolder === folder ? 'secondary' : 'ghost'}
|
||||
className={`w-full justify-start py-2 ${
|
||||
currentFolder === folder ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
onClick={() => onFolderChange(folder)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
{getFolderIcon(folder)}
|
||||
<span className="ml-2">{formatFolderName(folder)}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user