courrier refactor

This commit is contained in:
alma 2025-04-26 23:25:19 +02:00
parent a30198cb2b
commit 4af36d63f9

View File

@ -5,7 +5,7 @@ import { useRouter } from 'next/navigation';
import {
Mail, Loader2, AlertCircle,
ChevronLeft, ChevronRight, Reply, ReplyAll, Forward,
Star, FolderOpen
Star, FolderOpen, Plus as PlusIcon, RefreshCw, ChevronUp, ChevronDown
} from 'lucide-react';
import { Dialog, DialogContent } from '@/components/ui/dialog';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
@ -48,6 +48,14 @@ function SimplifiedLoadingFix() {
);
}
interface Account {
id: number;
name: string;
email: string;
color: string;
folders?: string[];
}
export default function CourrierPage() {
const router = useRouter();
@ -85,6 +93,29 @@ export default function CourrierPage() {
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showLoginNeeded, setShowLoginNeeded] = useState(false);
// States to match the provided implementation
const [sidebarOpen, setSidebarOpen] = useState(true);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const [isReplying, setIsReplying] = useState(false);
const [isForwarding, setIsForwarding] = useState(false);
const [accountsDropdownOpen, setAccountsDropdownOpen] = useState(false);
const [showCompose, setShowCompose] = useState(false);
const [composeTo, setComposeTo] = useState('');
const [composeCc, setComposeCc] = useState('');
const [composeBcc, setComposeBcc] = useState('');
const [composeSubject, setComposeSubject] = useState('');
const [composeBody, setComposeBody] = useState('');
const [showCc, setShowCc] = useState(false);
const [showBcc, setShowBcc] = useState(false);
const [attachments, setAttachments] = useState<any[]>([]);
// Mock accounts
const [accounts, setAccounts] = useState<Account[]>([
{ id: 0, name: 'All', email: '', color: 'bg-gray-500' },
{ id: 1, name: 'Mail', email: 'user@example.com', color: 'bg-blue-500' }
]);
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
// Check for more emails
const hasMoreEmails = page < totalPages;
@ -131,38 +162,69 @@ export default function CourrierPage() {
const formattedEmail = formatEmailForAction(selectedEmail, type);
if (!formattedEmail) return;
setComposeData({
to: formattedEmail.to,
cc: formattedEmail.cc,
subject: formattedEmail.subject,
body: formattedEmail.body,
});
setComposeTo(formattedEmail.to || '');
setComposeCc(formattedEmail.cc || '');
setComposeSubject(formattedEmail.subject || '');
setComposeBody(formattedEmail.body || '');
setComposeType(type);
setShowComposeModal(true);
if (type === 'forward') {
setIsForwarding(true);
} else {
setIsReplying(true);
}
setShowCompose(true);
setShowCc(type === 'reply-all');
};
// Handle compose new email
const handleComposeNew = () => {
setComposeData({
to: '',
subject: '',
body: '',
});
setComposeType('new');
setShowComposeModal(true);
setComposeTo('');
setComposeCc('');
setComposeBcc('');
setComposeSubject('');
setComposeBody('');
setShowCc(false);
setShowBcc(false);
setIsReplying(false);
setIsForwarding(false);
setShowCompose(true);
};
// Handle sending email
const handleSend = async (emailData: EmailData) => {
await sendEmail(emailData);
setShowComposeModal(false);
setComposeData(null);
// Refresh the Sent folder if we're currently viewing it
if (currentFolder.toLowerCase() === 'sent') {
loadEmails();
const handleSend = async () => {
if (!composeTo) {
alert('Please specify at least one recipient');
return;
}
try {
await sendEmail({
to: composeTo,
cc: composeCc,
bcc: composeBcc,
subject: composeSubject,
body: composeBody,
});
// Clear compose form and close modal
setComposeTo('');
setComposeCc('');
setComposeBcc('');
setComposeSubject('');
setComposeBody('');
setAttachments([]);
setShowCompose(false);
setIsReplying(false);
setIsForwarding(false);
// Refresh the Sent folder if we're currently viewing it
if (currentFolder.toLowerCase() === 'sent') {
loadEmails();
}
} catch (error) {
console.error('Error sending email:', error);
alert('Failed to send email. Please try again.');
}
};
@ -189,6 +251,163 @@ export default function CourrierPage() {
router.push('/courrier/login');
};
// Handle mailbox change
const handleMailboxChange = (newMailbox: string) => {
changeFolder(newMailbox);
};
// Format date for display
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
if (date.toDateString() === now.toDateString()) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} else {
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
}
};
// Render sidebar navigation
const renderSidebarNav = () => (
<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={() => handleMailboxChange(folder)}
>
<div className="flex items-center">
{getFolderIcon(folder)}
<span className="ml-2">{formatFolderName(folder)}</span>
</div>
</Button>
</li>
))}
</ul>
</nav>
);
// 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 <Mail className="h-4 w-4" />;
} else if (folderLower.includes('sent')) {
return <Mail className="h-4 w-4" />;
} else if (folderLower.includes('trash')) {
return <Mail className="h-4 w-4" />;
} else {
return <Mail className="h-4 w-4" />;
}
};
// Email list wrapper with preview panel
const renderEmailListWrapper = () => (
<div className="flex-1 flex overflow-hidden">
{/* Email list panel */}
<EmailList
emails={emails}
selectedEmailIds={selectedEmailIds}
selectedEmail={selectedEmail}
currentFolder={currentFolder}
isLoading={isLoading}
totalEmails={emails.length}
hasMoreEmails={hasMoreEmails}
onSelectEmail={handleEmailSelect}
onToggleSelect={toggleEmailSelection}
onToggleSelectAll={toggleSelectAll}
onBulkAction={handleBulkAction}
onToggleStarred={toggleStarred}
onLoadMore={handleLoadMore}
onSearch={searchEmails}
/>
{/* Preview panel - will automatically take remaining space */}
<div className="flex-1 bg-white/95 backdrop-blur-sm flex flex-col">
{selectedEmail ? (
<>
{/* Email actions header */}
<div className="flex-none px-4 py-3 border-b border-gray-100">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 min-w-0 flex-1">
<div className="min-w-0 max-w-[500px]">
<h2 className="text-lg font-semibold text-gray-900 truncate">
{selectedEmail.subject || '(No subject)'}
</h2>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<div className="flex items-center border-l border-gray-200 pl-4">
<Button
variant="ghost"
className="text-gray-600 hover:text-gray-900 px-2 py-1"
onClick={() => handleReplyOrForward('reply')}
>
Reply
</Button>
<Button
variant="ghost"
className="text-gray-600 hover:text-gray-900 px-2 py-1"
onClick={() => handleReplyOrForward('reply-all')}
>
Reply All
</Button>
<Button
variant="ghost"
className="text-gray-600 hover:text-gray-900 px-2 py-1"
onClick={() => handleReplyOrForward('forward')}
>
Forward
</Button>
</div>
</div>
</div>
</div>
{/* Email content with scroll area */}
<ScrollArea className="flex-1">
<EmailContent email={selectedEmail} />
</ScrollArea>
</>
) : (
<div className="flex flex-col items-center justify-center h-full">
<Mail className="h-12 w-12 text-gray-400 mb-4" />
<p className="text-gray-500">Select an email to view its contents</p>
</div>
)}
</div>
</div>
);
// Delete confirmation dialog
const renderDeleteConfirmDialog = () => (
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Emails</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete {selectedEmailIds.length} selected email{selectedEmailIds.length > 1 ? 's' : ''}? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteConfirm}>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
// If there's a critical error, show error dialog
if (error && !isLoading && emails.length === 0 && !showLoginNeeded) {
return (
@ -202,39 +421,8 @@ export default function CourrierPage() {
);
}
// Create a properly formatted email message from composeData for the ComposeEmail component
const createEmailMessage = () => {
if (!composeData) return null;
return {
id: 'temp-id',
messageId: '',
subject: composeData.subject || '',
from: [{ name: '', address: '' }],
to: [{ name: '', address: composeData.to || '' }],
cc: composeData.cc ? [{ name: '', address: composeData.cc }] : [],
bcc: composeData.bcc ? [{ name: '', address: composeData.bcc }] : [],
date: new Date(),
content: composeData.body || '',
html: composeData.body || '',
hasAttachments: false
};
};
// Format date for display
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
if (date.toDateString() === now.toDateString()) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} else {
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
}
};
return (
<div className="h-screen">
<>
<SimplifiedLoadingFix />
{/* Login required dialog */}
@ -253,137 +441,143 @@ export default function CourrierPage() {
</AlertDialogContent>
</AlertDialog>
{/* Delete confirmation dialog */}
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Deletion</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete {selectedEmailIds.length} {selectedEmailIds.length === 1 ? 'email' : 'emails'}?
This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteConfirm}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Compose email dialog */}
<Dialog open={showComposeModal} onOpenChange={setShowComposeModal}>
<DialogContent className="sm:max-w-[800px] h-[80vh] p-0">
<ComposeEmail
initialEmail={createEmailMessage()}
type={composeType}
onClose={() => setShowComposeModal(false)}
onSend={handleSend}
/>
</DialogContent>
</Dialog>
{/* Main email interface */}
<div className="h-full flex flex-col">
{/* Email header */}
<EmailHeader
onSearch={searchEmails}
/>
{/* Main content area - fixed-width sidebar and list panel layout */}
<div className="flex-1 flex overflow-hidden">
{/* Sidebar - fixed width 64px */}
<div className="w-64 flex-shrink-0">
<EmailSidebar
currentFolder={currentFolder}
folders={mailboxes}
onFolderChange={changeFolder}
onRefresh={() => loadEmails(false)}
onCompose={handleComposeNew}
isLoading={isLoading}
/>
</div>
{/* Email list - fixed width 320px */}
<div className="w-[320px] flex-shrink-0">
<EmailList
emails={emails}
selectedEmailIds={selectedEmailIds}
selectedEmail={selectedEmail}
currentFolder={currentFolder}
isLoading={isLoading}
totalEmails={emails.length}
hasMoreEmails={hasMoreEmails}
onSelectEmail={handleEmailSelect}
onToggleSelect={toggleEmailSelection}
onToggleSelectAll={toggleSelectAll}
onBulkAction={handleBulkAction}
onToggleStarred={toggleStarred}
onLoadMore={handleLoadMore}
onSearch={searchEmails}
/>
</div>
{/* Email content - takes remaining space */}
<div className="flex-1 h-full overflow-hidden flex flex-col">
{selectedEmail ? (
<>
{/* Email actions header */}
<div className="flex-none px-4 py-3 border-b border-gray-100">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 min-w-0 flex-1">
<div className="min-w-0 max-w-[500px]">
<h2 className="text-lg font-semibold text-gray-900 truncate">
{selectedEmail.subject || '(No subject)'}
</h2>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<div className="flex items-center border-l border-gray-200 pl-4">
<Button
variant="ghost"
className="text-gray-600 hover:text-gray-900 px-2 py-1"
onClick={() => handleReplyOrForward('reply')}
>
Reply
</Button>
<Button
variant="ghost"
className="text-gray-600 hover:text-gray-900 px-2 py-1"
onClick={() => handleReplyOrForward('reply-all')}
>
Reply All
</Button>
<Button
variant="ghost"
className="text-gray-600 hover:text-gray-900 px-2 py-1"
onClick={() => handleReplyOrForward('forward')}
>
Forward
</Button>
</div>
</div>
</div>
{/* Main layout */}
<main className="w-full h-screen bg-black">
<div className="w-full h-full px-4 pt-12 pb-4">
<div className="flex h-full bg-carnet-bg">
{/* Sidebar */}
<div className={`${sidebarOpen ? 'w-60' : 'w-16'} bg-white/95 backdrop-blur-sm border-r border-gray-100 flex flex-col transition-all duration-300 ease-in-out
${mobileSidebarOpen ? 'fixed inset-y-0 left-0 z-40' : 'hidden'} md:block`}>
{/* 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>
{/* Email content with avatar and proper scroll area */}
<ScrollArea className="flex-1">
<EmailContent email={selectedEmail} />
</ScrollArea>
</>
) : (
<div className="flex flex-col items-center justify-center h-full">
<Mail className="h-12 w-12 text-gray-400 mb-4" />
<p className="text-gray-500">Select an email to view its contents</p>
</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={() => {
setShowCompose(true);
setComposeTo('');
setComposeCc('');
setComposeBcc('');
setComposeSubject('');
setComposeBody('');
setShowCc(false);
setShowBcc(false);
}}
>
<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"
onClick={() => handleMailboxChange('INBOX')}
className="text-gray-600 hover:text-gray-900 hover:bg-gray-100"
>
<RefreshCw className="h-4 w-4" />
</Button>
</div>
{/* Accounts Section */}
<div className="p-3 border-b border-gray-100">
<Button
variant="ghost"
className="w-full justify-between mb-2 text-sm font-medium text-gray-500"
onClick={() => setAccountsDropdownOpen(!accountsDropdownOpen)}
>
<span>Accounts</span>
{accountsDropdownOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</Button>
{accountsDropdownOpen && (
<div className="space-y-1 pl-2">
{accounts.map(account => (
<div key={account.id} className="relative group">
<Button
variant="ghost"
className="w-full justify-between px-2 py-1.5 text-sm group"
onClick={() => setSelectedAccount(account)}
>
<div className="flex flex-col items-start">
<div className="flex items-center gap-2">
<div className={`w-2.5 h-2.5 rounded-full ${account.color}`}></div>
<span className="font-medium">{account.name}</span>
</div>
<span className="text-xs text-gray-500 ml-4">{account.email}</span>
</div>
</Button>
</div>
))}
</div>
)}
</div>
{/* Navigation */}
{renderSidebarNav()}
</div>
{/* Main content area */}
<div className="flex-1 flex overflow-hidden">
{/* Email list panel */}
{renderEmailListWrapper()}
</div>
</div>
</div>
</div>
</div>
</main>
{/* Compose Email Modal */}
<ComposeEmail
showCompose={showCompose}
setShowCompose={setShowCompose}
composeTo={composeTo}
setComposeTo={setComposeTo}
composeCc={composeCc}
setComposeCc={setComposeCc}
composeBcc={composeBcc}
setComposeBcc={setComposeBcc}
composeSubject={composeSubject}
setComposeSubject={setComposeSubject}
composeBody={composeBody}
setComposeBody={setComposeBody}
showCc={showCc}
setShowCc={setShowCc}
showBcc={showBcc}
setShowBcc={setShowBcc}
attachments={attachments}
setAttachments={setAttachments}
handleSend={handleSend}
replyTo={isReplying ? selectedEmail : null}
forwardFrom={isForwarding ? selectedEmail : null}
onSend={async (email: EmailData) => {
console.log('Email sent:', email);
setShowCompose(false);
setIsReplying(false);
setIsForwarding(false);
return Promise.resolve();
}}
onCancel={() => {
setShowCompose(false);
setComposeTo('');
setComposeCc('');
setComposeBcc('');
setComposeSubject('');
setComposeBody('');
setShowCc(false);
setShowBcc(false);
setAttachments([]);
setIsReplying(false);
setIsForwarding(false);
}}
/>
{renderDeleteConfirmDialog()}
</>
);
}