From a1241a20fa3ea80d3809efe724124b7bf6512c7d Mon Sep 17 00:00:00 2001 From: alma Date: Sun, 27 Apr 2025 11:39:12 +0200 Subject: [PATCH] courrier refactor rebuild 2 --- app/courrier/page.tsx | 150 ++++--- components/email/ComposeEmail.tsx | 631 ++++++++++++++++-------------- 2 files changed, 408 insertions(+), 373 deletions(-) diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index af2061fa..5612c60b 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -58,7 +58,6 @@ interface Account { email: string; color: string; folders?: string[]; - showFolders?: boolean; } export default function CourrierPage() { @@ -103,7 +102,6 @@ export default function CourrierPage() { const [unreadCount, setUnreadCount] = useState(0); const [loading, setLoading] = useState(false); const [userEmail, setUserEmail] = useState(''); - const [showAccountDropdown, setShowAccountDropdown] = useState(false); // Get user's email from session data useEffect(() => { @@ -128,7 +126,7 @@ export default function CourrierPage() { // Accounts for the sidebar (using the actual user email) const [accounts, setAccounts] = useState([ { id: 0, name: 'All', email: '', color: 'bg-gray-500' }, - { id: 1, name: userEmail || 'Loading...', email: userEmail || '', color: 'bg-blue-500', folders: mailboxes, showFolders: true } + { id: 1, name: userEmail || 'Loading...', email: userEmail || '', color: 'bg-blue-500', folders: mailboxes } ]); const [selectedAccount, setSelectedAccount] = useState(null); @@ -142,8 +140,6 @@ export default function CourrierPage() { updated[1].name = userEmail; updated[1].email = userEmail; } - // Preserve the showFolders state - updated[1].showFolders = updated[1].showFolders !== false; } return updated; }); @@ -248,9 +244,7 @@ export default function CourrierPage() { // Handle mailbox change const handleMailboxChange = (folder: string) => { changeFolder(folder); - // You would typically fetch emails for the selected folder here - // For example: - // fetchEmails({ folder }); + setCurrentView(folder); }; // Handle sending email @@ -305,15 +299,6 @@ export default function CourrierPage() { router.push('/courrier/login'); }; - // Function to toggle folder visibility for a specific account - const toggleFolderVisibility = (accountId: number) => { - setAccounts(prev => prev.map(account => - account.id === accountId - ? { ...account, showFolders: !account.showFolders } - : account - )); - }; - return ( <> @@ -357,75 +342,74 @@ export default function CourrierPage() { - {/* Account section */} -
-
-

Accounts

- -
+ {/* Accounts Section */} +
+ - {showAccountDropdown && ( -
- {accounts.map((account) => ( -
-
- - {account.folders && account.folders.length > 0 && ( - - )} -
+ {accountsDropdownOpen && ( +
+ {accounts.map(account => ( +
+ - {account.folders && account.showFolders && ( -
- {/* Folder navigation */} - {account.folders.map((folder) => ( - + )) + ) : ( +
+
+ {/* Create placeholder folder items with shimmer effect */} + {Array.from({ length: 5 }).map((_, index) => ( +
+
+
+
+ ))}
- - ))} +
+ )}
)}
diff --git a/components/email/ComposeEmail.tsx b/components/email/ComposeEmail.tsx index a1255ff3..1c77745c 100644 --- a/components/email/ComposeEmail.tsx +++ b/components/email/ComposeEmail.tsx @@ -2,7 +2,7 @@ import { useState, useRef, useEffect } from 'react'; import { - X, Paperclip, ChevronDown, ChevronUp, SendHorizontal, Loader2, ChevronRight + X, Paperclip, ChevronDown, ChevronUp, SendHorizontal, Loader2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -10,7 +10,6 @@ import { Textarea } from '@/components/ui/textarea'; import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'; import DOMPurify from 'isomorphic-dompurify'; import { Label } from '@/components/ui/label'; -import { sanitizeHtml, formatEmailAddresses } from "@/lib/utils/email-formatter"; // Import sub-components import ComposeEmailHeader from './ComposeEmailHeader'; @@ -23,6 +22,7 @@ import QuotedEmailContent from './QuotedEmailContent'; import { formatReplyEmail, formatForwardedEmail, + formatEmailAddresses, type EmailMessage, type EmailAddress } from '@/lib/utils/email-formatter'; @@ -142,9 +142,9 @@ export default function ComposeEmail(props: ComposeEmailAllProps) { const { initialEmail, type = 'new', onClose, onSend } = props; // Email form state - const [to, setTo] = useState([]); - const [cc, setCc] = useState([]); - const [bcc, setBcc] = useState([]); + const [to, setTo] = useState(''); + const [cc, setCc] = useState(''); + const [bcc, setBcc] = useState(''); const [subject, setSubject] = useState(''); const [emailContent, setEmailContent] = useState(''); const [showCc, setShowCc] = useState(false); @@ -156,105 +156,170 @@ export default function ComposeEmail(props: ComposeEmailAllProps) { type: string; }>>([]); - const fileInputRef = useRef(null); - // Initialize the form when replying to or forwarding an email useEffect(() => { if (initialEmail && type !== 'new') { try { - // Set recipients based on email type - if (type === 'reply') { - // Reply only to sender - setTo(initialEmail.from || []); - setCc([]); - setBcc([]); - } else if (type === 'reply-all') { - // Reply to sender and all recipients, excluding current user - setTo(initialEmail.from || []); - // Set CC to all original recipients - setCc(initialEmail.cc || []); - // Enable CC field if there are recipients - if ((initialEmail.cc && initialEmail.cc.length > 0) || (initialEmail.to && initialEmail.to.length > 0)) { + // Set recipients based on type + if (type === 'reply' || type === 'reply-all') { + // Reply goes to the original sender + setTo(formatEmailAddresses(initialEmail.from || [])); + + // For reply-all, include all original recipients in CC + if (type === 'reply-all') { + const allRecipients = [ + ...(initialEmail.to || []), + ...(initialEmail.cc || []) + ]; + // Filter out the current user if they were a recipient + // This would need some user context to properly implement + setCc(formatEmailAddresses(allRecipients)); + } + + // Set subject with Re: prefix + const subjectBase = initialEmail.subject || '(No subject)'; + const subject = subjectBase.match(/^Re:/i) ? subjectBase : `Re: ${subjectBase}`; + setSubject(subject); + + // Format the reply content with the quoted message included directly + const content = initialEmail.content || initialEmail.html || initialEmail.text || ''; + const sender = initialEmail.from && initialEmail.from.length > 0 + ? initialEmail.from[0].name || initialEmail.from[0].address + : 'Unknown sender'; + const date = initialEmail.date ? + (typeof initialEmail.date === 'string' ? new Date(initialEmail.date) : initialEmail.date) : + new Date(); + + // Format date for display + const formattedDate = date.toLocaleString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + + // Create reply content with quote + const replyContent = ` +

+

+

+

+
On ${formattedDate}, ${sender} wrote:
+
+
+ ${content} +
+
+ `; + + setEmailContent(replyContent); + + // Show CC field if there are CC recipients + if (initialEmail.cc && initialEmail.cc.length > 0) { setShowCc(true); } - } else if (type === 'forward') { - // Forward doesn't preset recipients - setTo([]); - setCc([]); - setBcc([]); + } + else if (type === 'forward') { + // Set subject with Fwd: prefix + const subjectBase = initialEmail.subject || '(No subject)'; + const subject = subjectBase.match(/^(Fwd|FW|Forward):/i) ? subjectBase : `Fwd: ${subjectBase}`; + setSubject(subject); + + // Format the forward content with the original email included directly + const content = initialEmail.content || initialEmail.html || initialEmail.text || ''; + const fromString = formatEmailAddresses(initialEmail.from || []); + const toString = formatEmailAddresses(initialEmail.to || []); + const date = initialEmail.date ? + (typeof initialEmail.date === 'string' ? new Date(initialEmail.date) : initialEmail.date) : + new Date(); + + // Format date for display + const formattedDate = date.toLocaleString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + + // Create forwarded content + const forwardContent = ` +

+

+

+

+
+
+
+
---------- Forwarded message ---------
+
From: ${fromString}
+
Date: ${formattedDate}
+
Subject: ${initialEmail.subject || ''}
+
To: ${toString}
+
+ +
+
+ `; + + setEmailContent(forwardContent); + + // If the original email has attachments, we should include them + if (initialEmail.attachments && initialEmail.attachments.length > 0) { + const formattedAttachments = initialEmail.attachments.map(att => ({ + name: att.filename || 'attachment', + type: att.contentType || 'application/octet-stream', + content: att.content || '' + })); + setAttachments(formattedAttachments); + } } - - // Set subject based on email type - if (type === 'reply' || type === 'reply-all') { - setSubject(initialEmail.subject ? `Re: ${initialEmail.subject.replace(/^Re: /i, '')}` : ''); - } else if (type === 'forward') { - setSubject(initialEmail.subject ? `Fwd: ${initialEmail.subject.replace(/^Fwd: /i, '')}` : ''); - } - - // Set initial content - just an empty div, QuotedEmailContent will be added in the render - setEmailContent('
'); - } catch (error) { console.error('Error initializing compose form:', error); - // Set safe defaults - setTo([]); - setCc([]); - setBcc([]); - setSubject(''); - setEmailContent('
'); } } }, [initialEmail, type]); - - // Handle file input change - const handleAttachmentChange = (event: React.ChangeEvent) => { - if (event.target.files && event.target.files.length > 0) { - handleAttachmentAdd(event.target.files); - } - }; - - // Add attachment + + // Handle file attachments const handleAttachmentAdd = async (files: FileList) => { - // Implementation for adding attachments - // Code omitted for brevity + const newAttachments = Array.from(files).map(file => ({ + name: file.name, + type: file.type, + content: URL.createObjectURL(file) + })); + + setAttachments(prev => [...prev, ...newAttachments]); }; - - // Remove attachment + const handleAttachmentRemove = (index: number) => { setAttachments(prev => prev.filter((_, i) => i !== index)); }; - // Handle send email + // Handle sending email const handleSend = async () => { - if (!to.length) { - // Validation error: no recipients - alert('Please add at least one recipient'); + if (!to) { + alert('Please specify at least one recipient'); return; } + setSending(true); + try { - setSending(true); - - // Format addresses to strings for the API - const formattedTo = to.map(addr => addr.name && addr.name !== addr.address - ? `${addr.name} <${addr.address}>` - : addr.address).join(', '); - const formattedCc = cc.length ? cc.map(addr => addr.name && addr.name !== addr.address - ? `${addr.name} <${addr.address}>` - : addr.address).join(', ') : undefined; - const formattedBcc = bcc.length ? bcc.map(addr => addr.name && addr.name !== addr.address - ? `${addr.name} <${addr.address}>` - : addr.address).join(', ') : undefined; - await onSend({ - to: formattedTo, - cc: formattedCc, - bcc: formattedBcc, + to, + cc: cc || undefined, + bcc: bcc || undefined, subject, body: emailContent, attachments }); + // Reset form and close onClose(); } catch (error) { console.error('Error sending email:', error); @@ -263,237 +328,223 @@ export default function ComposeEmail(props: ComposeEmailAllProps) { setSending(false); } }; - - // Render the modern compose form - return ( -
-
- {/* Header */} -
-

- {type === 'reply' && } - {type === 'reply-all' && } - {type === 'forward' && } - {type === 'reply' ? 'Reply' : - type === 'reply-all' ? 'Reply All' : - type === 'forward' ? 'Forward' : 'New Message'} -

- -
+ + // Additional effect to ensure we scroll to the top and focus the editor + useEffect(() => { + // Focus the editor and ensure it's scrolled to the top + const editorContainer = document.querySelector('.ql-editor') as HTMLElement; + if (editorContainer) { + // Set timeout to ensure DOM is fully rendered + setTimeout(() => { + // Focus the editor + editorContainer.focus(); - {/* Body */} -
- {/* To */} -
- - addr.name && addr.name !== addr.address - ? `${addr.name} <${addr.address}>` - : addr.address).join(', ')} - onChange={(e) => { - // Parse email addresses - const addresses = e.target.value.split(',').map(addr => { - addr = addr.trim(); - const match = addr.match(/(.*)<(.*)>/); - if (match) { - return { name: match[1].trim(), address: match[2].trim() }; - } - return { name: addr, address: addr }; - }); - setTo(addresses); - }} - placeholder="Recipients" - className="w-full" - /> -
- - {/* CC and BCC toggles */} -
- {!showCc && ( - - )} - - {!showBcc && ( - - )} -
- - {/* CC */} - {showCc && ( -
-
- - -
- addr.name && addr.name !== addr.address - ? `${addr.name} <${addr.address}>` - : addr.address).join(', ')} - onChange={(e) => { - // Parse email addresses - const addresses = e.target.value.split(',').map(addr => { - addr = addr.trim(); - const match = addr.match(/(.*)<(.*)>/); - if (match) { - return { name: match[1].trim(), address: match[2].trim() }; - } - return { name: addr, address: addr }; - }); - setCc(addresses); - }} - placeholder="Carbon copy recipients" - className="w-full" + // Make sure all scroll containers are at the top + editorContainer.scrollTop = 0; + + // Find all possible scrollable parent containers + const scrollContainers = [ + document.querySelector('.ql-container') as HTMLElement, + document.querySelector('.rich-email-editor-container') as HTMLElement, + document.querySelector('.h-full.flex.flex-col.p-6') as HTMLElement + ]; + + // Scroll all containers to top + scrollContainers.forEach(container => { + if (container) { + container.scrollTop = 0; + } + }); + }, 100); + } + }, []); + + return ( +
+
+ {/* Modal Header */} +
+

+ {type === 'reply' ? 'Reply' : type === 'forward' ? 'Forward' : type === 'reply-all' ? 'Reply All' : 'New Message'} +

+ +
+ + {/* Modal Body */} +
+
+ {/* To Field */} +
+ + setTo(e.target.value)} + placeholder="recipient@example.com" + className="w-full mt-1 bg-white border-gray-300 text-gray-900" />
- )} - - {/* BCC */} - {showBcc && ( -
-
- - + + {/* CC/BCC Toggle Buttons */} +
+ + +
+ + {/* CC Field */} + {showCc && ( +
+ + setCc(e.target.value)} + placeholder="cc@example.com" + className="w-full mt-1 bg-white border-gray-300 text-gray-900" + />
- addr.name && addr.name !== addr.address - ? `${addr.name} <${addr.address}>` - : addr.address).join(', ')} - onChange={(e) => { - // Parse email addresses - const addresses = e.target.value.split(',').map(addr => { - addr = addr.trim(); - const match = addr.match(/(.*)<(.*)>/); - if (match) { - return { name: match[1].trim(), address: match[2].trim() }; - } - return { name: addr, address: addr }; - }); - setBcc(addresses); - }} - placeholder="Blind carbon copy recipients" - className="w-full" + )} + + {/* BCC Field */} + {showBcc && ( +
+ + setBcc(e.target.value)} + placeholder="bcc@example.com" + className="w-full mt-1 bg-white border-gray-300 text-gray-900" + /> +
+ )} + + {/* Subject Field */} +
+ + setSubject(e.target.value)} + placeholder="Enter subject" + className="w-full mt-1 bg-white border-gray-300 text-gray-900" />
- )} - - {/* Subject */} -
- - setSubject(e.target.value)} - placeholder="Subject" - className="w-full" - /> -
- - {/* Email Content */} -
-
setEmailContent(e.currentTarget.innerHTML)} - /> + + {/* Message Body */} +
+ +
+ +
+
- {/* Quoted content for replies and forwards */} - {initialEmail && (type === 'reply' || type === 'reply-all' || type === 'forward') && ( - - )} -
- - {/* Attachments */} -
+ {/* Attachments */} {attachments.length > 0 && ( -
- {attachments.map((attachment, index) => ( -
- {attachment.name} - -
- ))} +
+

Attachments

+
+ {attachments.map((file, index) => ( +
+ {file.name} + +
+ ))} +
)} - +
+
+ + {/* Modal Footer */} +
+
+ {/* File Input for Attachments */} { + if (e.target.files && e.target.files.length > 0) { + handleAttachmentAdd(e.target.files); + } + }} /> - - + + {sending && Preparing attachment...} +
+
+ +
-
- - {/* Footer */} -
- - -