courrier refactor rebuild 2

This commit is contained in:
alma 2025-04-27 10:02:33 +02:00
parent c993fe738e
commit 1c4b38ec8f
2 changed files with 653 additions and 165 deletions

View File

@ -97,6 +97,61 @@ export default function CourrierPage() {
const [showLoginNeeded, setShowLoginNeeded] = useState(false); const [showLoginNeeded, setShowLoginNeeded] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(true);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const [accountsDropdownOpen, setAccountsDropdownOpen] = useState(false);
const [currentView, setCurrentView] = useState('INBOX');
const [unreadCount, setUnreadCount] = useState(0);
const [loading, setLoading] = useState(false);
// Mock accounts for the sidebar
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', folders: mailboxes }
]);
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
// Update account folders when mailboxes change
useEffect(() => {
setAccounts(prev => {
const updated = [...prev];
if (updated[1]) {
updated[1].folders = mailboxes;
}
return updated;
});
}, [mailboxes]);
// Calculate unread count (this would be replaced with actual data in production)
useEffect(() => {
// Example: counting unread emails in the inbox
const unreadInInbox = emails.filter(email => !email.read && currentFolder === 'INBOX').length;
setUnreadCount(unreadInInbox);
}, [emails, currentFolder]);
// Helper to get folder icons
const getFolderIcon = (folder: string) => {
const folderLower = folder.toLowerCase();
if (folderLower.includes('inbox')) {
return Inbox;
} else if (folderLower.includes('sent')) {
return Send;
} else if (folderLower.includes('trash')) {
return Trash;
} else if (folderLower.includes('archive')) {
return Archive;
} else if (folderLower.includes('draft')) {
return Edit;
} else if (folderLower.includes('spam') || folderLower.includes('junk')) {
return AlertOctagon;
} else {
return Folder;
}
};
// Helper to format folder names
const formatFolderName = (folder: string) => {
return folder.charAt(0).toUpperCase() + folder.slice(1).toLowerCase();
};
// Check for more emails // Check for more emails
const hasMoreEmails = page < totalPages; const hasMoreEmails = page < totalPages;
@ -161,6 +216,12 @@ export default function CourrierPage() {
setShowComposeModal(true); setShowComposeModal(true);
}; };
// Handle mailbox change
const handleMailboxChange = (folder: string) => {
changeFolder(folder);
setCurrentView(folder);
};
// Handle sending email // Handle sending email
const handleSendEmail = async (emailData: EmailData) => { const handleSendEmail = async (emailData: EmailData) => {
return await sendEmail(emailData); return await sendEmail(emailData);
@ -172,6 +233,30 @@ export default function CourrierPage() {
setShowDeleteConfirm(false); setShowDeleteConfirm(false);
}; };
// 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">
{React.createElement(getFolderIcon(folder), { className: "h-4 w-4" })}
<span className="ml-2">{formatFolderName(folder)}</span>
</div>
</Button>
</li>
))}
</ul>
</nav>
);
// Check login on mount // Check login on mount
useEffect(() => { useEffect(() => {
// Check if the user is logged in after a short delay // Check if the user is logged in after a short delay
@ -190,44 +275,130 @@ export default function CourrierPage() {
}; };
return ( return (
<div className="flex h-screen flex-col"> <>
{/* Loading Fix for development */}
<SimplifiedLoadingFix /> <SimplifiedLoadingFix />
{/* Login needed alert */} {/* Main layout */}
<LoginNeededAlert <main className="w-full h-screen bg-black">
show={showLoginNeeded} <div className="w-full h-full px-4 pt-12 pb-4">
onLogin={handleGoToLogin} <div className="flex h-full bg-carnet-bg">
onClose={() => setShowLoginNeeded(false)} {/* 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>
</div>
{/* Main Content */} {/* Compose button and refresh button */}
<div className="flex flex-1 overflow-hidden"> <div className="p-2 border-b border-gray-100 flex items-center gap-2">
{/* Sidebar (Desktop) */}
{sidebarOpen && (
<aside className="hidden md:flex md:w-64 bg-gray-50 border-r border-gray-200 flex-col">
{/* Account switching and compose button */}
<div className="px-4 py-3 border-b border-gray-200">
<Button <Button
className="w-full bg-blue-600 hover:bg-blue-700" 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} onClick={handleComposeNew}
> >
<PlusIcon className="mr-2 h-4 w-4" /> <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={() => {
setLoading(true);
loadEmails().finally(() => setLoading(false));
}}
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
</Button> </Button>
</div> </div>
{/* Folder Navigation */} {/* Accounts Section */}
<EmailSidebarContent <div className="p-3 border-b border-gray-100">
mailboxes={mailboxes} <Button
currentFolder={currentFolder} variant="ghost"
onFolderChange={changeFolder} className="w-full justify-between mb-2 text-sm font-medium text-gray-500"
/> onClick={() => setAccountsDropdownOpen(!accountsDropdownOpen)}
</aside> >
<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 items-center gap-2">
<div className={`w-2.5 h-2.5 rounded-full ${account.color}`}></div>
<span className="font-medium text-gray-700">{account.name}</span>
</div>
</Button>
{/* Show folders for email accounts (not for "All" account) without the "Folders" header */}
{account.id !== 0 && (
<div className="pl-4 mt-1 mb-2 space-y-0.5 border-l border-gray-200">
{account.folders && account.folders.length > 0 ? (
account.folders.map((folder) => (
<Button
key={folder}
variant="ghost"
className={`w-full justify-start py-1 px-2 text-xs ${
currentView === folder ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900'
}`}
onClick={(e) => {
e.stopPropagation();
handleMailboxChange(folder);
}}
>
<div className="flex items-center justify-between w-full gap-1.5">
<div className="flex items-center gap-1.5">
{React.createElement(getFolderIcon(folder), { className: "h-3.5 w-3.5" })}
<span className="truncate">{folder}</span>
</div>
{folder === 'INBOX' && unreadCount > 0 && (
<span className="ml-auto bg-blue-600 text-white text-xs px-1.5 py-0.5 rounded-full text-[10px]">
{unreadCount}
</span>
)} )}
</div>
</Button>
))
) : (
<div className="px-2 py-2">
<div className="flex flex-col space-y-2">
{/* Create placeholder folder items with shimmer effect */}
{Array.from({ length: 5 }).map((_, index) => (
<div key={index} className="flex items-center gap-1.5 animate-pulse">
<div className="h-3.5 w-3.5 bg-gray-200 rounded-sm"></div>
<div className="h-3 w-24 bg-gray-200 rounded"></div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
))}
</div>
)}
</div>
{/* Navigation */}
{renderSidebarNav()}
</div>
{/* Email List and Content View */} {/* Email List and Content View */}
<div className="flex-1 flex"> <div className="flex-1 flex overflow-hidden">
{/* Email List */} {/* Email List */}
<EmailList <EmailList
emails={emails} emails={emails}
@ -276,10 +447,18 @@ export default function CourrierPage() {
</div> </div>
</div> </div>
</div> </div>
</div>
</main>
{/* Login needed alert */}
<LoginNeededAlert
show={showLoginNeeded}
onLogin={handleGoToLogin}
onClose={() => setShowLoginNeeded(false)}
/>
{/* Compose Modal */} {/* Compose Modal */}
<Dialog open={showComposeModal} onOpenChange={setShowComposeModal}> {showComposeModal && (
<DialogContent className="max-w-4xl p-0">
<ComposeEmail <ComposeEmail
initialEmail={selectedEmail} initialEmail={selectedEmail}
type={composeType} type={composeType}
@ -288,8 +467,7 @@ export default function CourrierPage() {
await sendEmail(emailData); await sendEmail(emailData);
}} }}
/> />
</DialogContent> )}
</Dialog>
{/* Delete Confirmation Dialog */} {/* Delete Confirmation Dialog */}
<DeleteConfirmDialog <DeleteConfirmDialog
@ -298,6 +476,6 @@ export default function CourrierPage() {
onConfirm={handleDeleteConfirm} onConfirm={handleDeleteConfirm}
onCancel={() => setShowDeleteConfirm(false)} onCancel={() => setShowDeleteConfirm(false)}
/> />
</div> </>
); );
} }

View File

@ -239,40 +239,196 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
}; };
return ( return (
<Card className="w-full max-w-3xl mx-auto flex flex-col min-h-[60vh] max-h-[80vh]"> <div className="fixed inset-0 bg-gray-600/30 backdrop-blur-sm z-50 flex items-center justify-center">
<ComposeEmailHeader <div className="w-full max-w-2xl h-[90vh] bg-white rounded-xl shadow-xl flex flex-col mx-4">
type={type} {/* Modal Header */}
onClose={onClose} <div className="flex-none flex items-center justify-between px-6 py-3 border-b border-gray-200">
/> <h3 className="text-lg font-semibold text-gray-900">
{type === 'reply' ? 'Reply' : type === 'forward' ? 'Forward' : type === 'reply-all' ? 'Reply All' : 'New Message'}
</h3>
<Button
variant="ghost"
size="icon"
className="hover:bg-gray-100 rounded-full"
onClick={onClose}
>
<X className="h-5 w-5 text-gray-500" />
</Button>
</div>
<div className="flex-1 overflow-y-auto"> {/* Modal Body */}
<ComposeEmailForm <div className="flex-1 overflow-hidden">
to={to} <div className="h-full flex flex-col p-6 space-y-4 overflow-y-auto">
setTo={setTo} {/* To Field */}
cc={cc} <div className="flex-none">
setCc={setCc} <Label htmlFor="to" className="block text-sm font-medium text-gray-700">To</Label>
bcc={bcc} <Input
setBcc={setBcc} id="to"
subject={subject} value={to}
setSubject={setSubject} onChange={(e) => setTo(e.target.value)}
emailContent={emailContent} placeholder="recipient@example.com"
setEmailContent={setEmailContent} className="w-full mt-1 bg-white border-gray-300 text-gray-900"
showCc={showCc}
setShowCc={setShowCc}
showBcc={showBcc}
setShowBcc={setShowBcc}
attachments={attachments}
onAttachmentAdd={handleAttachmentAdd}
onAttachmentRemove={handleAttachmentRemove}
/> />
</div> </div>
<ComposeEmailFooter {/* CC/BCC Toggle Buttons */}
sending={sending} <div className="flex-none flex items-center gap-4">
onSend={handleSend} <button
onCancel={onClose} type="button"
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
onClick={() => setShowCc(!showCc)}
>
{showCc ? 'Hide Cc' : 'Add Cc'}
</button>
<button
type="button"
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
onClick={() => setShowBcc(!showBcc)}
>
{showBcc ? 'Hide Bcc' : 'Add Bcc'}
</button>
</div>
{/* CC Field */}
{showCc && (
<div className="flex-none">
<Label htmlFor="cc" className="block text-sm font-medium text-gray-700">Cc</Label>
<Input
id="cc"
value={cc}
onChange={(e) => setCc(e.target.value)}
placeholder="cc@example.com"
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
/> />
</Card> </div>
)}
{/* BCC Field */}
{showBcc && (
<div className="flex-none">
<Label htmlFor="bcc" className="block text-sm font-medium text-gray-700">Bcc</Label>
<Input
id="bcc"
value={bcc}
onChange={(e) => setBcc(e.target.value)}
placeholder="bcc@example.com"
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
/>
</div>
)}
{/* Subject Field */}
<div className="flex-none">
<Label htmlFor="subject" className="block text-sm font-medium text-gray-700">Subject</Label>
<Input
id="subject"
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder="Enter subject"
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
/>
</div>
{/* Message Body */}
<div className="flex-1 min-h-[200px] flex flex-col">
<Label htmlFor="message" className="flex-none block text-sm font-medium text-gray-700 mb-2">Message</Label>
<Textarea
value={emailContent}
onChange={(e) => setEmailContent(e.target.value)}
placeholder="Write your message here..."
className="flex-1 w-full bg-white border border-gray-300 rounded-md p-4 text-black overflow-y-auto focus:outline-none focus:ring-1 focus:ring-blue-500"
style={{
minHeight: '200px',
maxHeight: 'calc(100vh - 400px)',
resize: 'none'
}}
/>
</div>
{/* Attachments */}
{attachments.length > 0 && (
<div className="border rounded-md p-3 mt-4">
<h3 className="text-sm font-medium mb-2 text-gray-700">Attachments</h3>
<div className="space-y-2">
{attachments.map((file, index) => (
<div key={index} className="flex items-center justify-between text-sm border rounded p-2">
<span className="truncate max-w-[200px] text-gray-800">{file.name}</span>
<Button
variant="ghost"
size="sm"
onClick={() => handleAttachmentRemove(index)}
className="h-6 w-6 p-0 text-gray-500 hover:text-gray-700"
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
</div>
)}
</div>
</div>
{/* Modal Footer */}
<div className="flex-none flex items-center justify-between px-6 py-3 border-t border-gray-200 bg-white">
<div className="flex items-center gap-2">
{/* File Input for Attachments */}
<input
type="file"
id="file-attachment"
className="hidden"
multiple
onChange={(e) => {
if (e.target.files && e.target.files.length > 0) {
handleAttachmentAdd(e.target.files);
}
}}
/>
<label htmlFor="file-attachment">
<Button
variant="outline"
size="icon"
className="rounded-full bg-white hover:bg-gray-100 border-gray-300"
onClick={(e) => {
e.preventDefault();
document.getElementById('file-attachment')?.click();
}}
>
<Paperclip className="h-4 w-4 text-gray-600" />
</Button>
</label>
{sending && <span className="text-xs text-gray-500 ml-2">Preparing attachment...</span>}
</div>
<div className="flex items-center gap-3">
<Button
variant="ghost"
className="text-gray-600 hover:text-gray-700 hover:bg-gray-100"
onClick={onClose}
disabled={sending}
>
Cancel
</Button>
<Button
className="bg-blue-600 text-white hover:bg-blue-700"
onClick={handleSend}
disabled={sending}
>
{sending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Sending...
</>
) : (
<>
<SendHorizontal className="mr-2 h-4 w-4" />
Send
</>
)}
</Button>
</div>
</div>
</div>
</div>
); );
} }
@ -369,47 +525,201 @@ function LegacyAdapter({
if (!showCompose) return null; if (!showCompose) return null;
return ( return (
<Card className="w-full max-w-3xl mx-auto flex flex-col min-h-[60vh] max-h-[80vh]"> <div className="fixed inset-0 bg-gray-600/30 backdrop-blur-sm z-50 flex items-center justify-center">
<ComposeEmailHeader <div className="w-full max-w-2xl h-[90vh] bg-white rounded-xl shadow-xl flex flex-col mx-4">
type={determineType()} {/* Modal Header */}
onClose={() => { <div className="flex-none flex items-center justify-between px-6 py-3 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">
{determineType() === 'reply' ? 'Reply' : determineType() === 'forward' ? 'Forward' : determineType() === 'reply-all' ? 'Reply All' : 'New Message'}
</h3>
<Button
variant="ghost"
size="icon"
className="hover:bg-gray-100 rounded-full"
onClick={() => {
if (onCancel) onCancel(); if (onCancel) onCancel();
setShowCompose(false); setShowCompose(false);
}} }}
/> >
<X className="h-5 w-5 text-gray-500" />
</Button>
</div>
<div className="flex-1 overflow-y-auto"> {/* Modal Body */}
<ComposeEmailForm <div className="flex-1 overflow-hidden">
to={composeTo} <div className="h-full flex flex-col p-6 space-y-4 overflow-y-auto">
setTo={setComposeTo} {/* To Field */}
cc={composeCc} <div className="flex-none">
setCc={setComposeCc} <Label htmlFor="to" className="block text-sm font-medium text-gray-700">To</Label>
bcc={composeBcc} <Input
setBcc={setComposeBcc} id="to"
subject={composeSubject} value={composeTo}
setSubject={setComposeSubject} onChange={(e) => setComposeTo(e.target.value)}
emailContent={composeBody} placeholder="recipient@example.com"
setEmailContent={setComposeBody} className="w-full mt-1 bg-white border-gray-300 text-gray-900"
showCc={showCc} />
setShowCc={setShowCc} </div>
showBcc={showBcc}
setShowBcc={setShowBcc} {/* CC/BCC Toggle Buttons */}
attachments={convertAttachments()} <div className="flex-none flex items-center gap-4">
onAttachmentAdd={handleFileSelection} <button
onAttachmentRemove={(index) => { type="button"
setAttachments(attachments.filter((_, i) => i !== index)); className="text-blue-600 hover:text-blue-700 text-sm font-medium"
onClick={() => setShowCc(!showCc)}
>
{showCc ? 'Hide Cc' : 'Add Cc'}
</button>
<button
type="button"
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
onClick={() => setShowBcc(!showBcc)}
>
{showBcc ? 'Hide Bcc' : 'Add Bcc'}
</button>
</div>
{/* CC Field */}
{showCc && (
<div className="flex-none">
<Label htmlFor="cc" className="block text-sm font-medium text-gray-700">Cc</Label>
<Input
id="cc"
value={composeCc}
onChange={(e) => setComposeCc(e.target.value)}
placeholder="cc@example.com"
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
/>
</div>
)}
{/* BCC Field */}
{showBcc && (
<div className="flex-none">
<Label htmlFor="bcc" className="block text-sm font-medium text-gray-700">Bcc</Label>
<Input
id="bcc"
value={composeBcc}
onChange={(e) => setComposeBcc(e.target.value)}
placeholder="bcc@example.com"
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
/>
</div>
)}
{/* Subject Field */}
<div className="flex-none">
<Label htmlFor="subject" className="block text-sm font-medium text-gray-700">Subject</Label>
<Input
id="subject"
value={composeSubject}
onChange={(e) => setComposeSubject(e.target.value)}
placeholder="Enter subject"
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
/>
</div>
{/* Message Body */}
<div className="flex-1 min-h-[200px] flex flex-col">
<Label htmlFor="message" className="flex-none block text-sm font-medium text-gray-700 mb-2">Message</Label>
<Textarea
value={composeBody}
onChange={(e) => setComposeBody(e.target.value)}
placeholder="Write your message here..."
className="flex-1 w-full bg-white border border-gray-300 rounded-md p-4 text-black overflow-y-auto focus:outline-none focus:ring-1 focus:ring-blue-500"
style={{
minHeight: '200px',
maxHeight: 'calc(100vh - 400px)',
resize: 'none'
}} }}
/> />
</div> </div>
<ComposeEmailFooter {/* Attachments */}
sending={sending} {attachments.length > 0 && (
onSend={handleLegacySend} <div className="border rounded-md p-3 mt-4">
onCancel={() => { <h3 className="text-sm font-medium mb-2 text-gray-700">Attachments</h3>
<div className="space-y-2">
{attachments.map((file, index) => (
<div key={index} className="flex items-center justify-between text-sm border rounded p-2">
<span className="truncate max-w-[200px] text-gray-800">{file.name || file.filename}</span>
<Button
variant="ghost"
size="sm"
onClick={() => setAttachments(attachments.filter((_, i) => i !== index))}
className="h-6 w-6 p-0 text-gray-500 hover:text-gray-700"
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
</div>
)}
</div>
</div>
{/* Modal Footer */}
<div className="flex-none flex items-center justify-between px-6 py-3 border-t border-gray-200 bg-white">
<div className="flex items-center gap-2">
{/* File Input for Attachments */}
<input
type="file"
id="file-attachment-legacy"
className="hidden"
multiple
onChange={(e) => {
if (e.target.files && e.target.files.length > 0) {
handleFileSelection(e.target.files);
}
}}
/>
<label htmlFor="file-attachment-legacy">
<Button
variant="outline"
size="icon"
className="rounded-full bg-white hover:bg-gray-100 border-gray-300"
onClick={(e) => {
e.preventDefault();
document.getElementById('file-attachment-legacy')?.click();
}}
>
<Paperclip className="h-4 w-4 text-gray-600" />
</Button>
</label>
{sending && <span className="text-xs text-gray-500 ml-2">Preparing attachment...</span>}
</div>
<div className="flex items-center gap-3">
<Button
variant="ghost"
className="text-gray-600 hover:text-gray-700 hover:bg-gray-100"
onClick={() => {
if (onCancel) onCancel(); if (onCancel) onCancel();
setShowCompose(false); setShowCompose(false);
}} }}
/> disabled={sending}
</Card> >
Cancel
</Button>
<Button
className="bg-blue-600 text-white hover:bg-blue-700"
onClick={handleLegacySend}
disabled={sending}
>
{sending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Sending...
</>
) : (
<>
<SendHorizontal className="mr-2 h-4 w-4" />
Send
</>
)}
</Button>
</div>
</div>
</div>
</div>
); );
} }