mail page fix design
This commit is contained in:
parent
69d4a69713
commit
127765069f
@ -328,61 +328,93 @@ const initialSidebarItems = [
|
||||
}
|
||||
];
|
||||
|
||||
function getReplyBody(email: any, type: 'reply' | 'reply-all' | 'forward' = 'reply'): string {
|
||||
let content = '';
|
||||
function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward'): string {
|
||||
if (!email.body) return '';
|
||||
|
||||
if (email.body) {
|
||||
// Handle multipart emails
|
||||
if (email.body.includes('Content-Type: multipart/alternative')) {
|
||||
const parts = email.body.split('--');
|
||||
for (const part of parts) {
|
||||
if (part.includes('Content-Type: text/html')) {
|
||||
content = part.split('\n\n')[1] || '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content = email.body;
|
||||
try {
|
||||
// Split email into headers and body
|
||||
const [headersPart, ...bodyParts] = email.body.split('\r\n\r\n');
|
||||
if (!headersPart || bodyParts.length === 0) {
|
||||
throw new Error('Invalid email format: missing headers or body');
|
||||
}
|
||||
|
||||
// Clean and structure the content
|
||||
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
|
||||
content = content
|
||||
.split('\n')
|
||||
.map(line => `<p>${line}</p>`)
|
||||
.join('');
|
||||
|
||||
// Add proper quoting structure
|
||||
const quotedContent = `
|
||||
<blockquote style="border-left: 2px solid #ccc; padding-left: 10px; margin: 10px 0 0 0">
|
||||
${content}
|
||||
</blockquote>
|
||||
`;
|
||||
|
||||
// Add metadata based on type
|
||||
const metadata = `
|
||||
<div style="color: #666; font-size: 0.9em; margin-bottom: 10px;">
|
||||
${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'
|
||||
? `<div>${metadata}${quotedContent}</div>`
|
||||
: `<div><br/><br/>${metadata}${quotedContent}</div>`;
|
||||
const body = bodyParts.join('\r\n\r\n');
|
||||
|
||||
// Parse headers using Infomaniak MIME decoder
|
||||
const headerInfo = parseEmailHeaders(headersPart);
|
||||
const boundary = extractBoundary(headersPart);
|
||||
|
||||
let content = '';
|
||||
|
||||
// If it's a multipart email
|
||||
if (boundary) {
|
||||
const parts = body.split(`--${boundary}`);
|
||||
|
||||
// Find HTML part first, fallback to text part
|
||||
const htmlPart = parts.find(part => part.toLowerCase().includes('content-type: text/html'));
|
||||
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() {
|
||||
@ -1492,92 +1524,92 @@ export default function CourrierPage() {
|
||||
<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>
|
||||
</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()}
|
||||
{/* 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>
|
||||
|
||||
{/* 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>
|
||||
</main>
|
||||
|
||||
{/* Compose Email Modal */}
|
||||
|
||||
@ -1,18 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useEffect, useState } from 'react';
|
||||
import { useRef, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Paperclip, X } from 'lucide-react';
|
||||
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 {
|
||||
showCompose: boolean;
|
||||
setShowCompose: (show: boolean) => void;
|
||||
@ -56,22 +50,17 @@ export default function ComposeEmail({
|
||||
setAttachments,
|
||||
handleSend
|
||||
}: ComposeEmailProps) {
|
||||
const editorRef = useRef<HTMLDivElement>(null);
|
||||
const [direction, setDirection] = useState<'ltr' | 'rtl'>('ltr');
|
||||
const composeBodyRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.innerHTML = composeBody;
|
||||
const plainText = editorRef.current.textContent || '';
|
||||
setDirection(detectDirection(plainText));
|
||||
if (composeBodyRef.current) {
|
||||
composeBodyRef.current.innerHTML = composeBody;
|
||||
}
|
||||
}, [composeBody]);
|
||||
|
||||
const handleInput = () => {
|
||||
if (editorRef.current) {
|
||||
const plainText = editorRef.current.textContent || '';
|
||||
setDirection(detectDirection(plainText));
|
||||
setComposeBody(editorRef.current.innerHTML);
|
||||
const handleInput = (e: React.FormEvent<HTMLDivElement>) => {
|
||||
if (composeBodyRef.current) {
|
||||
setComposeBody(composeBodyRef.current.innerHTML);
|
||||
}
|
||||
};
|
||||
|
||||
@ -223,23 +212,14 @@ export default function ComposeEmail({
|
||||
</div>
|
||||
|
||||
{/* Message Body */}
|
||||
<div className="flex-1 min-h-[200px] overflow-auto">
|
||||
<div
|
||||
ref={editorRef}
|
||||
contentEditable
|
||||
className="prose max-w-none min-h-[200px] p-4 border border-gray-300 rounded-lg bg-white"
|
||||
style={{
|
||||
color: '#000000',
|
||||
cursor: 'text',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
fontFamily: 'inherit',
|
||||
fontSize: 'inherit',
|
||||
lineHeight: 'inherit',
|
||||
textAlign: 'left'
|
||||
}}
|
||||
dir={direction}
|
||||
onInput={handleInput}
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="message" className="block text-sm font-medium text-gray-700">Message</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
value={composeBody}
|
||||
onChange={(e) => setComposeBody(e.target.value)}
|
||||
placeholder="Write your message..."
|
||||
className="w-full h-full mt-1 bg-white border-gray-300 text-gray-900 resize-none"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user