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 {
|
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 */}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user