mail page fix design

This commit is contained in:
alma 2025-04-21 19:46:51 +02:00
parent 69d4a69713
commit 127765069f
2 changed files with 181 additions and 169 deletions

View File

@ -328,61 +328,93 @@ const initialSidebarItems = [
} }
]; ];
function getReplyBody(email: any, type: 'reply' | 'reply-all' | 'forward' = 'reply'): string { function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward'): string {
let content = ''; if (!email.body) return '';
if (email.body) { try {
// Handle multipart emails // Split email into headers and body
if (email.body.includes('Content-Type: multipart/alternative')) { const [headersPart, ...bodyParts] = email.body.split('\r\n\r\n');
const parts = email.body.split('--'); if (!headersPart || bodyParts.length === 0) {
for (const part of parts) { throw new Error('Invalid email format: missing headers or body');
if (part.includes('Content-Type: text/html')) {
content = part.split('\n\n')[1] || '';
break;
}
}
} else {
content = email.body;
} }
// Clean and structure the content const body = bodyParts.join('\r\n\r\n');
content = content
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<p>/gi, '\n')
.replace(/<\/p>/gi, '\n')
.replace(/<div>/gi, '\n')
.replace(/<\/div>/gi, '\n')
.trim();
// Convert plain text to HTML while preserving formatting // Parse headers using Infomaniak MIME decoder
content = content const headerInfo = parseEmailHeaders(headersPart);
.split('\n') const boundary = extractBoundary(headersPart);
.map(line => `<p>${line}</p>`)
.join('');
// Add proper quoting structure let content = '';
const quotedContent = `
<blockquote style="border-left: 2px solid #ccc; padding-left: 10px; margin: 10px 0 0 0">
${content}
</blockquote>
`;
// Add metadata based on type // If it's a multipart email
const metadata = ` if (boundary) {
<div style="color: #666; font-size: 0.9em; margin-bottom: 10px;"> const parts = body.split(`--${boundary}`);
${type === 'forward' ? 'Forwarded message' : 'Original message'}<br/>
From: ${email.from}<br/>
Date: ${new Date(email.date).toLocaleString()}<br/>
Subject: ${email.subject}
</div>
`;
return type === 'forward' // Find HTML part first, fallback to text part
? `<div>${metadata}${quotedContent}</div>` const htmlPart = parts.find(part => part.toLowerCase().includes('content-type: text/html'));
: `<div><br/><br/>${metadata}${quotedContent}</div>`; const textPart = parts.find(part => part.toLowerCase().includes('content-type: text/plain'));
const selectedPart = htmlPart || textPart;
if (selectedPart) {
const [partHeaders, ...partBodyParts] = selectedPart.split('\r\n\r\n');
const partBody = partBodyParts.join('\r\n\r\n');
const partHeaderInfo = parseEmailHeaders(partHeaders);
content = partHeaderInfo.encoding === 'quoted-printable'
? decodeQuotedPrintable(partBody, partHeaderInfo.charset)
: partBody;
}
} else {
content = headerInfo.encoding === 'quoted-printable'
? decodeQuotedPrintable(body, headerInfo.charset)
: body;
}
// Convert plain text to HTML if needed
if (!headerInfo.contentType.includes('text/html')) {
content = content
.split('\n')
.map(line => {
if (!line.trim()) return '<br>';
if (line.startsWith('>')) {
return `<p class="text-gray-600" dir="ltr" style="unicode-bidi: bidi-override; direction: ltr;">${line}</p>`;
}
return `<p dir="ltr" style="unicode-bidi: bidi-override; direction: ltr;">${line}</p>`;
})
.join('');
}
// Clean HTML content
content = cleanHtml(content);
const date = new Date(email.date).toLocaleString();
if (type === 'forward') {
return `
<div class="prose max-w-none" dir="ltr" style="unicode-bidi: bidi-override; direction: ltr;">
<div class="border-l-4 border-gray-300 pl-4 my-4">
<p class="text-sm text-gray-600 mb-2" dir="ltr" style="unicode-bidi: bidi-override; direction: ltr;"><strong>From:</strong> ${email.from}</p>
<p class="text-sm text-gray-600 mb-2" dir="ltr" style="unicode-bidi: bidi-override; direction: ltr;"><strong>Date:</strong> ${date}</p>
<p class="text-sm text-gray-600 mb-2" dir="ltr" style="unicode-bidi: bidi-override; direction: ltr;"><strong>Subject:</strong> ${email.subject}</p>
<p class="text-sm text-gray-600 mb-2" dir="ltr" style="unicode-bidi: bidi-override; direction: ltr;"><strong>To:</strong> ${Array.isArray(email.to) ? email.to.join(', ') : email.to}</p>
<div class="mt-4 prose-sm" dir="ltr" style="unicode-bidi: bidi-override; direction: ltr;">${content}</div>
</div>
</div>
`;
} else {
return `
<div class="prose max-w-none" dir="ltr" style="unicode-bidi: bidi-override; direction: ltr;">
<div class="border-l-4 border-gray-300 pl-4 my-4">
<p class="text-sm text-gray-600 mb-2" dir="ltr" style="unicode-bidi: bidi-override; direction: ltr;">On ${date}, ${email.from} wrote:</p>
<div class="mt-4 prose-sm" dir="ltr" style="unicode-bidi: bidi-override; direction: ltr;">${content}</div>
</div>
</div>
`;
}
} catch (error) {
console.error('Error processing email body:', error);
return '';
} }
return '';
} }
export default function CourrierPage() { export default function CourrierPage() {
@ -1492,92 +1524,92 @@ export default function CourrierPage() {
<main className="w-full h-screen bg-black"> <main className="w-full h-screen bg-black">
<div className="w-full h-full px-4 pt-12 pb-4"> <div className="w-full h-full px-4 pt-12 pb-4">
<div className="flex h-full bg-carnet-bg"> <div className="flex h-full bg-carnet-bg">
{/* Sidebar */} {/* 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 <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`}> ${mobileSidebarOpen ? 'fixed inset-y-0 left-0 z-40' : 'hidden'} md:block`}>
{/* Courrier Title */} {/* Courrier Title */}
<div className="p-3 border-b border-gray-100"> <div className="p-3 border-b border-gray-100">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Mail className="h-6 w-6 text-gray-600" /> <Mail className="h-6 w-6 text-gray-600" />
<span className="text-xl font-semibold text-gray-900">COURRIER</span> <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={() => {
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>
{/* 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> </div>
{/* Main content area */}
<div className="flex-1 flex overflow-hidden">
{/* Email list panel */}
{renderEmailListWrapper()}
</div>
</div>
</div>
</main> </main>
{/* Compose Email Modal */} {/* Compose Email Modal */}

View File

@ -1,18 +1,12 @@
'use client'; 'use client';
import { useRef, useEffect, useState } from 'react'; import { useRef, useEffect } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Paperclip, X } from 'lucide-react'; import { Paperclip, X } from 'lucide-react';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
// Direction detection utility
function detectDirection(text: string): 'rtl' | 'ltr' {
const rtlChars = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/;
return rtlChars.test(text) ? 'rtl' : 'ltr';
}
interface ComposeEmailProps { interface ComposeEmailProps {
showCompose: boolean; showCompose: boolean;
setShowCompose: (show: boolean) => void; setShowCompose: (show: boolean) => void;
@ -56,22 +50,17 @@ export default function ComposeEmail({
setAttachments, setAttachments,
handleSend handleSend
}: ComposeEmailProps) { }: ComposeEmailProps) {
const editorRef = useRef<HTMLDivElement>(null); const composeBodyRef = useRef<HTMLDivElement>(null);
const [direction, setDirection] = useState<'ltr' | 'rtl'>('ltr');
useEffect(() => { useEffect(() => {
if (editorRef.current) { if (composeBodyRef.current) {
editorRef.current.innerHTML = composeBody; composeBodyRef.current.innerHTML = composeBody;
const plainText = editorRef.current.textContent || '';
setDirection(detectDirection(plainText));
} }
}, [composeBody]); }, [composeBody]);
const handleInput = () => { const handleInput = (e: React.FormEvent<HTMLDivElement>) => {
if (editorRef.current) { if (composeBodyRef.current) {
const plainText = editorRef.current.textContent || ''; setComposeBody(composeBodyRef.current.innerHTML);
setDirection(detectDirection(plainText));
setComposeBody(editorRef.current.innerHTML);
} }
}; };
@ -223,23 +212,14 @@ export default function ComposeEmail({
</div> </div>
{/* Message Body */} {/* Message Body */}
<div className="flex-1 min-h-[200px] overflow-auto"> <div className="flex-1">
<div <Label htmlFor="message" className="block text-sm font-medium text-gray-700">Message</Label>
ref={editorRef} <Textarea
contentEditable id="message"
className="prose max-w-none min-h-[200px] p-4 border border-gray-300 rounded-lg bg-white" value={composeBody}
style={{ onChange={(e) => setComposeBody(e.target.value)}
color: '#000000', placeholder="Write your message..."
cursor: 'text', className="w-full h-full mt-1 bg-white border-gray-300 text-gray-900 resize-none"
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
fontFamily: 'inherit',
fontSize: 'inherit',
lineHeight: 'inherit',
textAlign: 'left'
}}
dir={direction}
onInput={handleInput}
/> />
</div> </div>
</div> </div>