courrier refactor rebuild 2

This commit is contained in:
alma 2025-04-27 12:05:51 +02:00
parent 45bbb8229f
commit 46d8220466
2 changed files with 157 additions and 199 deletions

View File

@ -98,23 +98,53 @@ export default function CourrierPage() {
const [sidebarOpen, setSidebarOpen] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(true);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const [accountsDropdownOpen, setAccountsDropdownOpen] = useState(true); const [accountsDropdownOpen, setAccountsDropdownOpen] = useState(true);
const [foldersOpen, setFoldersOpen] = useState(true);
const [currentView, setCurrentView] = useState('INBOX'); const [currentView, setCurrentView] = useState('INBOX');
const [unreadCount, setUnreadCount] = useState(0); const [unreadCount, setUnreadCount] = useState(0);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [userEmail, setUserEmail] = useState('');
// Mock accounts for the sidebar // Mock accounts for the sidebar
const [accounts, setAccounts] = useState<Account[]>([ const [accounts, setAccounts] = useState<Account[]>([
{ id: 0, name: 'All', email: '', color: 'bg-gray-500' }, { id: 1, name: 'Mail', email: userEmail || 'Loading...', color: 'bg-blue-500', folders: mailboxes }
{ id: 1, name: 'Mail', email: 'user@example.com', color: 'bg-blue-500', folders: mailboxes }
]); ]);
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null); const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
// Fetch user email from credentials when component mounts
useEffect(() => {
async function fetchUserEmail() {
try {
const response = await fetch('/api/courrier/credentials');
if (response.ok) {
const data = await response.json();
if (data.credentials?.email) {
setUserEmail(data.credentials.email);
// Update account with the email address
setAccounts(prev => {
const updated = [...prev];
if (updated[0]) {
updated[0].email = data.credentials.email;
updated[0].name = data.credentials.email;
}
return updated;
});
}
}
} catch (error) {
console.error('Error fetching user email:', error);
}
}
fetchUserEmail();
}, []);
// Update account folders when mailboxes change // Update account folders when mailboxes change
useEffect(() => { useEffect(() => {
setAccounts(prev => { setAccounts(prev => {
const updated = [...prev]; const updated = [...prev];
if (updated[1]) { if (updated[0]) {
updated[1].folders = mailboxes; updated[0].folders = mailboxes;
} }
return updated; return updated;
}); });
@ -311,19 +341,30 @@ export default function CourrierPage() {
<Button <Button
variant="ghost" variant="ghost"
className="w-full justify-between px-2 py-1.5 text-sm group" className="w-full justify-between px-2 py-1.5 text-sm group"
onClick={() => setSelectedAccount(account)} onClick={() => {
setSelectedAccount(account);
setFoldersOpen(!foldersOpen);
}}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2 truncate">
<div className={`w-2.5 h-2.5 rounded-full ${account.color}`}></div> <div className={`w-2.5 h-2.5 rounded-full ${account.color}`}></div>
<span className="font-medium text-gray-700">{account.name}</span> <span className="font-medium text-gray-700 truncate">{account.email}</span>
</div> </div>
{/* Arrow to indicate toggle */}
{account.folders && account.folders.length > 0 && (
<div className="ml-1 flex-shrink-0">
{foldersOpen ?
<ChevronUp className="h-3.5 w-3.5 text-gray-400" /> :
<ChevronDown className="h-3.5 w-3.5 text-gray-400" />
}
</div>
)}
</Button> </Button>
{/* Show folders for email accounts (not for "All" account) without the "Folders" header */} {/* Show folders for email accounts without the "Folders" header */}
{account.id !== 0 && ( {account.folders && account.folders.length > 0 && foldersOpen && (
<div className="pl-4 mt-1 mb-2 space-y-0.5 border-l border-gray-200"> <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) => (
account.folders.map((folder) => (
<Button <Button
key={folder} key={folder}
variant="ghost" variant="ghost"
@ -347,8 +388,11 @@ export default function CourrierPage() {
)} )}
</div> </div>
</Button> </Button>
)) ))}
) : ( </div>
)}
{account.folders && account.folders.length === 0 && (
<div className="px-2 py-2"> <div className="px-2 py-2">
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
{/* Create placeholder folder items with shimmer effect */} {/* Create placeholder folder items with shimmer effect */}
@ -362,8 +406,6 @@ export default function CourrierPage() {
</div> </div>
)} )}
</div> </div>
)}
</div>
))} ))}
</div> </div>
)} )}

View File

@ -10,6 +10,7 @@ import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card';
import DOMPurify from 'isomorphic-dompurify'; import DOMPurify from 'isomorphic-dompurify';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import dynamic from 'next/dynamic';
// Import sub-components // Import sub-components
import ComposeEmailHeader from './ComposeEmailHeader'; import ComposeEmailHeader from './ComposeEmailHeader';
@ -108,26 +109,18 @@ function EmailMessageToQuotedContentAdapter({
email: EmailMessage, email: EmailMessage,
type: 'reply' | 'reply-all' | 'forward' type: 'reply' | 'reply-all' | 'forward'
}) { }) {
// Get the email content if (!email) return null;
const content = email.content || email.html || email.text || '';
// Get the sender
const sender = email.from && email.from.length > 0
? {
name: email.from[0].name,
email: email.from[0].address
}
: { email: 'unknown@example.com' };
// Map the type to what QuotedEmailContent expects
const mappedType = type === 'reply-all' ? 'reply' : type;
return ( return (
<QuotedEmailContent <QuotedEmailContent
content={content} content={email.content || email.html || email.text || ''}
sender={sender} sender={{
name: email.from?.[0]?.name || '',
email: email.from?.[0]?.address || ''
}}
date={email.date} date={email.date}
type={mappedType} type={type === 'reply-all' ? 'reply' : type}
className="mt-4"
/> />
); );
} }
@ -146,7 +139,7 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
const [cc, setCc] = useState<string>(''); const [cc, setCc] = useState<string>('');
const [bcc, setBcc] = useState<string>(''); const [bcc, setBcc] = useState<string>('');
const [subject, setSubject] = useState<string>(''); const [subject, setSubject] = useState<string>('');
const [emailContent, setEmailContent] = useState<string>(''); const [emailContent, setEmailContent] = useState<string>('<div></div>');
const [showCc, setShowCc] = useState<boolean>(false); const [showCc, setShowCc] = useState<boolean>(false);
const [showBcc, setShowBcc] = useState<boolean>(false); const [showBcc, setShowBcc] = useState<boolean>(false);
const [sending, setSending] = useState<boolean>(false); const [sending, setSending] = useState<boolean>(false);
@ -156,133 +149,54 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
type: string; type: string;
}>>([]); }>>([]);
// Initialize the form when replying to or forwarding an email // Initialize the form with the provided email data
useEffect(() => { useEffect(() => {
if (initialEmail && type !== 'new') {
try { try {
// Set recipients based on type // Only process if there's an initial email and it's a reply/forward
if (initialEmail && type !== 'new') {
// For replies: set the recipient to the sender of the original email
if (type === 'reply' || type === 'reply-all') { if (type === 'reply' || type === 'reply-all') {
// Reply goes to the original sender // Set recipients and subject using the formatReplyEmail utility
const formattedEmail = formatReplyEmail(initialEmail, type as 'reply' | 'reply-all');
// Set recipients
setTo(formatEmailAddresses(initialEmail.from || [])); setTo(formatEmailAddresses(initialEmail.from || []));
// For reply-all, include all original recipients in CC // For reply-all: add original recipients to CC
if (type === 'reply-all') { if (type === 'reply-all') {
const allRecipients = [ const allRecipients = [
...(initialEmail.to || []), ...(initialEmail.to || []),
...(initialEmail.cc || []) ...(initialEmail.cc || [])
]; ];
// Filter out the current user if they were a recipient
// This would need some user context to properly implement if (allRecipients.length > 0) {
setCc(formatEmailAddresses(allRecipients)); 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 = `
<div><br></div>
<div><br></div>
<div><br></div>
<div><br></div>
<div style="font-weight: 400; color: #555; margin: 20px 0 8px 0; font-size: 13px;">On ${formattedDate}, ${sender} wrote:</div>
<blockquote style="margin: 0; padding: 10px 0 10px 15px; border-left: 2px solid #ddd; color: #505050; background-color: #f9f9f9; border-radius: 4px;">
<div style="font-size: 13px;">
${content}
</div>
</blockquote>
`;
setEmailContent(replyContent);
// Show CC field if there are CC recipients
if (initialEmail.cc && initialEmail.cc.length > 0) {
setShowCc(true); setShowCc(true);
} }
} }
// Set subject with "Re:" prefix if needed
setSubject(formattedEmail.subject);
// Initialize with empty content - we'll use QuotedEmailContent in the render
setEmailContent('<div></div>');
}
// For forwards: set forwarded subject
else if (type === 'forward') { else if (type === 'forward') {
// Set subject with Fwd: prefix // Format the email for forwarding
const subjectBase = initialEmail.subject || '(No subject)'; const formattedEmail = formatForwardedEmail(initialEmail);
const subject = subjectBase.match(/^(Fwd|FW|Forward):/i) ? subjectBase : `Fwd: ${subjectBase}`;
setSubject(subject);
// Format the forward content with the original email included directly // Set subject with "Fwd:" prefix
const content = initialEmail.content || initialEmail.html || initialEmail.text || ''; setSubject(formattedEmail.subject);
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 // Initialize with empty content - we'll use QuotedEmailContent in the render
const formattedDate = date.toLocaleString('en-US', { setEmailContent('<div></div>');
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
// Create forwarded content
const forwardContent = `
<div><br></div>
<div><br></div>
<div><br></div>
<div><br></div>
<div style="border-top: 1px solid #ccc; margin-top: 10px; padding-top: 10px;">
<div style="font-family: Arial, sans-serif; color: #333;">
<div style="margin-bottom: 15px;">
<div>---------- Forwarded message ---------</div>
<div><b>From:</b> ${fromString}</div>
<div><b>Date:</b> ${formattedDate}</div>
<div><b>Subject:</b> ${initialEmail.subject || ''}</div>
<div><b>To:</b> ${toString}</div>
</div>
<div class="email-original-content">
${content}
</div>
</div>
</div>
`;
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);
} }
} }
} catch (error) { } catch (error) {
console.error('Error initializing compose form:', error); console.error('Error initializing compose form:', error);
} }
}
}, [initialEmail, type]); }, [initialEmail, type]);
// Handle file attachments // Handle file attachments
@ -450,18 +364,26 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
/> />
</div> </div>
{/* Message Body */} {/* Email Content Section */}
<div className="flex-1 min-h-[200px] flex flex-col overflow-hidden"> <div className="flex-1 border border-gray-200 rounded-md overflow-hidden">
<Label htmlFor="message" className="flex-none block text-sm font-medium text-gray-700 mb-2">Message</Label>
<div className="flex-1 border border-gray-300 rounded-md overflow-hidden">
<RichEmailEditor <RichEmailEditor
initialContent={emailContent} initialContent={emailContent}
onChange={setEmailContent} onChange={setEmailContent}
minHeight="200px" />
maxHeight="none" {/* Add QuotedEmailContent for replies and forwards */}
preserveFormatting={true} {initialEmail && (type === 'reply' || type === 'reply-all' || type === 'forward') && (
<div className="px-4 pb-4">
<QuotedEmailContent
content={initialEmail.content || initialEmail.html || ''}
sender={{
name: initialEmail.from[0]?.name || '',
email: initialEmail.from[0]?.address || ''
}}
date={initialEmail.date}
type={type === 'forward' ? 'forward' : 'reply'}
/> />
</div> </div>
)}
</div> </div>
{/* Attachments */} {/* Attachments */}
@ -813,19 +735,13 @@ function LegacyAdapter({
/> />
</div> </div>
{/* Message Body */} {/* Email Content Section */}
<div className="flex-1 min-h-[200px] flex flex-col overflow-hidden"> <div className="flex-1 border border-gray-200 rounded-md overflow-hidden">
<Label htmlFor="message" className="flex-none block text-sm font-medium text-gray-700 mb-2">Message</Label>
<div className="flex-1 border border-gray-300 rounded-md overflow-hidden">
<RichEmailEditor <RichEmailEditor
initialContent={composeBody} initialContent={composeBody}
onChange={setComposeBody} onChange={setComposeBody}
minHeight="200px"
maxHeight="none"
preserveFormatting={true}
/> />
</div> </div>
</div>
{/* Attachments */} {/* Attachments */}
{attachments.length > 0 && ( {attachments.length > 0 && (