mail page rest
This commit is contained in:
parent
d8cab53806
commit
037977b3fb
@ -153,9 +153,29 @@ function processMultipartEmail(emailRaw: string, boundary: string, mainHeaders:
|
||||
const partContent = emailRaw.substring(startPos, endPos).trim();
|
||||
|
||||
if (partContent) {
|
||||
const decoded = processSinglePartEmail(partContent);
|
||||
// Check if this is a nested multipart
|
||||
const nestedBoundaryMatch = partContent.match(/boundary="?([^"\r\n;]+)"?/i);
|
||||
if (nestedBoundaryMatch) {
|
||||
const nestedResult = processMultipartEmail(partContent, nestedBoundaryMatch[1]);
|
||||
result.text = result.text || nestedResult.text;
|
||||
result.html = result.html || nestedResult.html;
|
||||
result.attachments.push(...nestedResult.attachments);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (decoded.contentType.includes('text/plain')) {
|
||||
const decoded = processSinglePartEmail(partContent);
|
||||
const contentDisposition = extractHeader(partContent, 'Content-Disposition');
|
||||
const isAttachment = contentDisposition.toLowerCase().includes('attachment');
|
||||
|
||||
if (isAttachment) {
|
||||
const filename = extractFilename(partContent) || 'attachment';
|
||||
result.attachments.push({
|
||||
filename,
|
||||
contentType: decoded.contentType,
|
||||
encoding: decoded.raw?.headers ? parseEmailHeaders(decoded.raw.headers).encoding : '7bit',
|
||||
content: decoded.raw?.body || ''
|
||||
});
|
||||
} else if (decoded.contentType.includes('text/plain')) {
|
||||
result.text = decoded.text || '';
|
||||
} else if (decoded.contentType.includes('text/html')) {
|
||||
result.html = cleanHtml(decoded.html || '');
|
||||
@ -223,23 +243,37 @@ function parseEmailHeaders(headers: string): { contentType: string; encoding: st
|
||||
charset: 'utf-8'
|
||||
};
|
||||
|
||||
// Extract content type and charset
|
||||
const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*charset=([^;"\r\n]+)|(?:;\s*charset="([^"]+)"))?/i);
|
||||
// Extract content type and charset with better handling of quoted strings
|
||||
const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*([^=]+)=([^;"\r\n]+|"[^"]*"))*/gi);
|
||||
if (contentTypeMatch) {
|
||||
result.contentType = contentTypeMatch[1].trim().toLowerCase();
|
||||
if (contentTypeMatch[2]) {
|
||||
result.charset = contentTypeMatch[2].trim().toLowerCase();
|
||||
} else if (contentTypeMatch[3]) {
|
||||
result.charset = contentTypeMatch[3].trim().toLowerCase();
|
||||
const fullContentType = contentTypeMatch[0];
|
||||
const typeMatch = fullContentType.match(/Content-Type:\s*([^;]+)/i);
|
||||
if (typeMatch) {
|
||||
result.contentType = typeMatch[1].trim().toLowerCase();
|
||||
}
|
||||
|
||||
// Extract charset with better handling of quoted values
|
||||
const charsetMatch = fullContentType.match(/charset=([^;"\r\n]+|"[^"]*")/i);
|
||||
if (charsetMatch) {
|
||||
result.charset = charsetMatch[1].replace(/"/g, '').trim().toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
// Extract content transfer encoding
|
||||
// Extract content transfer encoding with better pattern matching
|
||||
const encodingMatch = headers.match(/Content-Transfer-Encoding:\s*([^\s;\r\n]+)/i);
|
||||
if (encodingMatch) {
|
||||
result.encoding = encodingMatch[1].trim().toLowerCase();
|
||||
}
|
||||
|
||||
// Normalize charset names
|
||||
if (result.charset === 'iso-8859-1' || result.charset === 'latin1') {
|
||||
result.charset = 'iso-8859-1';
|
||||
} else if (result.charset === 'utf-8' || result.charset === 'utf8') {
|
||||
result.charset = 'utf-8';
|
||||
} else if (result.charset === 'windows-1252' || result.charset === 'cp1252') {
|
||||
result.charset = 'windows-1252';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -255,17 +289,34 @@ function decodeMIME(text: string, encoding?: string, charset: string = 'utf-8'):
|
||||
if (encoding === 'quoted-printable') {
|
||||
return decodeQuotedPrintable(text, charset);
|
||||
} else if (encoding === 'base64') {
|
||||
return decodeBase64(text, charset);
|
||||
// Handle line breaks in base64 content
|
||||
const cleanText = text.replace(/[\r\n]/g, '');
|
||||
return decodeBase64(cleanText, charset);
|
||||
} else if (encoding === '7bit' || encoding === '8bit' || encoding === 'binary') {
|
||||
// For these encodings, we still need to handle the character set
|
||||
return convertCharset(text, charset);
|
||||
} else {
|
||||
// Unknown encoding, return as is but still handle charset
|
||||
return convertCharset(text, charset);
|
||||
// Unknown encoding, try to detect and handle
|
||||
if (text.match(/^[A-Za-z0-9+/=]+$/)) {
|
||||
// Looks like base64
|
||||
return decodeBase64(text, charset);
|
||||
} else if (text.includes('=') && text.match(/=[A-F0-9]{2}/)) {
|
||||
// Looks like quoted-printable
|
||||
return decodeQuotedPrintable(text, charset);
|
||||
} else {
|
||||
// Unknown encoding, return as is but still handle charset
|
||||
return convertCharset(text, charset);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error decoding MIME:', error);
|
||||
return text;
|
||||
// Try to recover by returning the original text with charset conversion
|
||||
try {
|
||||
return convertCharset(text, charset);
|
||||
} catch (e) {
|
||||
console.error('Error in fallback charset conversion:', e);
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -277,7 +328,14 @@ function decodeBase64(text: string, charset: string): string {
|
||||
binaryString = atob(cleanText);
|
||||
} catch (e) {
|
||||
console.error('Base64 decoding error:', e);
|
||||
return text;
|
||||
// Try to recover by removing invalid characters
|
||||
const validBase64 = cleanText.replace(/[^A-Za-z0-9+/=]/g, '');
|
||||
try {
|
||||
binaryString = atob(validBase64);
|
||||
} catch (e2) {
|
||||
console.error('Base64 recovery failed:', e2);
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
return convertCharset(binaryString, charset);
|
||||
@ -380,154 +438,44 @@ function cleanHtml(html: string): string {
|
||||
function decodeMimeContent(content: string): string {
|
||||
if (!content) return '';
|
||||
|
||||
try {
|
||||
// First, try to extract the content type and encoding
|
||||
const contentTypeMatch = content.match(/Content-Type:\s*([^;\r\n]+)/i);
|
||||
const encodingMatch = content.match(/Content-Transfer-Encoding:\s*([^\r\n]+)/i);
|
||||
const charsetMatch = content.match(/charset="?([^"\r\n;]+)"?/i);
|
||||
|
||||
const contentType = contentTypeMatch ? contentTypeMatch[1].toLowerCase() : 'text/plain';
|
||||
const encoding = encodingMatch ? encodingMatch[1].toLowerCase() : '7bit';
|
||||
const charset = charsetMatch ? charsetMatch[1].toLowerCase() : 'utf-8';
|
||||
// Check if this is an Infomaniak multipart message
|
||||
if (content.includes('Content-Type: multipart/')) {
|
||||
const boundary = content.match(/boundary="([^"]+)"/)?.[1];
|
||||
if (boundary) {
|
||||
const parts = content.split('--' + boundary);
|
||||
let htmlContent = '';
|
||||
let textContent = '';
|
||||
|
||||
// Handle multipart messages
|
||||
if (contentType.includes('multipart/')) {
|
||||
const boundaryMatch = content.match(/boundary="?([^"\r\n;]+)"?/i);
|
||||
if (boundaryMatch) {
|
||||
const boundary = boundaryMatch[1];
|
||||
const parts = content.split('--' + boundary);
|
||||
|
||||
let htmlContent = '';
|
||||
let textContent = '';
|
||||
|
||||
for (const part of parts) {
|
||||
if (!part.trim()) continue;
|
||||
|
||||
const partContentType = part.match(/Content-Type:\s*([^;\r\n]+)/i)?.[1]?.toLowerCase() || '';
|
||||
const partEncoding = part.match(/Content-Transfer-Encoding:\s*([^\r\n]+)/i)?.[1]?.toLowerCase() || '7bit';
|
||||
const partCharset = part.match(/charset="?([^"\r\n;]+)"?/i)?.[1]?.toLowerCase() || 'utf-8';
|
||||
|
||||
// Extract the actual content (after headers)
|
||||
const contentMatch = part.match(/\r?\n\r?\n([\s\S]+?)(?=\r?\n--)/);
|
||||
if (!contentMatch) continue;
|
||||
|
||||
let partContent = contentMatch[1];
|
||||
|
||||
// Decode based on encoding
|
||||
if (partEncoding === 'quoted-printable') {
|
||||
partContent = decodeQuotedPrintable(partContent, partCharset);
|
||||
} else if (partEncoding === 'base64') {
|
||||
partContent = decodeBase64(partContent, partCharset);
|
||||
parts.forEach(part => {
|
||||
if (part.includes('Content-Type: text/html')) {
|
||||
const match = part.match(/\r?\n\r?\n([\s\S]+?)(?=\r?\n--)/);
|
||||
if (match) {
|
||||
htmlContent = cleanHtml(match[1]);
|
||||
}
|
||||
|
||||
if (partContentType.includes('text/html')) {
|
||||
htmlContent = partContent;
|
||||
} else if (partContentType.includes('text/plain')) {
|
||||
textContent = partContent;
|
||||
} else if (part.includes('Content-Type: text/plain')) {
|
||||
const match = part.match(/\r?\n\r?\n([\s\S]+?)(?=\r?\n--)/);
|
||||
if (match) {
|
||||
textContent = cleanHtml(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer HTML content if available
|
||||
return htmlContent || textContent || content;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle single part messages
|
||||
let decodedContent = content;
|
||||
|
||||
// Find the actual content (after headers)
|
||||
const contentMatch = content.match(/\r?\n\r?\n([\s\S]+)/);
|
||||
if (contentMatch) {
|
||||
decodedContent = contentMatch[1];
|
||||
// Prefer HTML content if available
|
||||
return htmlContent || textContent;
|
||||
}
|
||||
|
||||
// Decode based on encoding
|
||||
if (encoding === 'quoted-printable') {
|
||||
decodedContent = decodeQuotedPrintable(decodedContent, charset);
|
||||
} else if (encoding === 'base64') {
|
||||
decodedContent = decodeBase64(decodedContent, charset);
|
||||
}
|
||||
|
||||
// Clean up the content
|
||||
return cleanHtml(decodedContent);
|
||||
} catch (e) {
|
||||
console.error('Error decoding MIME content:', e);
|
||||
return content;
|
||||
}
|
||||
|
||||
// If not multipart or no boundary found, clean the content directly
|
||||
return cleanHtml(content);
|
||||
}
|
||||
|
||||
// Add this helper function
|
||||
const renderEmailContent = (email: Email) => {
|
||||
try {
|
||||
// First try to decode the MIME content
|
||||
const decodedContent = decodeMimeContent(email.body);
|
||||
|
||||
// Check if the content is HTML
|
||||
const isHtml = decodedContent.includes('<') &&
|
||||
(decodedContent.includes('<html') ||
|
||||
decodedContent.includes('<body') ||
|
||||
decodedContent.includes('<div'));
|
||||
|
||||
if (isHtml) {
|
||||
// Extract the body content if it exists
|
||||
const bodyMatch = decodedContent.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
||||
const content = bodyMatch ? bodyMatch[1] : decodedContent;
|
||||
|
||||
// Sanitize HTML content
|
||||
const sanitizedHtml = content
|
||||
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
|
||||
.replace(/on\w+="[^"]*"/g, '')
|
||||
.replace(/on\w+='[^']*'/g, '')
|
||||
.replace(/javascript:/gi, '')
|
||||
.replace(/data:/gi, '')
|
||||
.replace(/<meta[^>]*>/gi, '')
|
||||
.replace(/<link[^>]*>/gi, '')
|
||||
// Fix common encoding issues
|
||||
.replace(/=C2=A0/g, ' ')
|
||||
.replace(/=E2=80=93/g, '\u2013')
|
||||
.replace(/=E2=80=94/g, '\u2014')
|
||||
.replace(/=E2=80=98/g, '\u2018')
|
||||
.replace(/=E2=80=99/g, '\u2019')
|
||||
.replace(/=E2=80=9C/g, '\u201C')
|
||||
.replace(/=E2=80=9D/g, '\u201D');
|
||||
|
||||
return (
|
||||
<div
|
||||
className="prose prose-sm max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// Format plain text content
|
||||
const formattedText = decodedContent
|
||||
.replace(/\n/g, '<br>')
|
||||
.replace(/\t/g, ' ')
|
||||
.replace(/ /g, ' ')
|
||||
// Fix common encoding issues
|
||||
.replace(/=C2=A0/g, ' ')
|
||||
.replace(/=E2=80=93/g, '\u2013')
|
||||
.replace(/=E2=80=94/g, '\u2014')
|
||||
.replace(/=E2=80=98/g, '\u2018')
|
||||
.replace(/=E2=80=99/g, '\u2019')
|
||||
.replace(/=E2=80=9C/g, '\u201C')
|
||||
.replace(/=E2=80=9D/g, '\u201D');
|
||||
|
||||
return (
|
||||
<div
|
||||
className="prose prose-sm max-w-none whitespace-pre-wrap"
|
||||
dangerouslySetInnerHTML={{ __html: formattedText }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error rendering email content:', e);
|
||||
return (
|
||||
<div className="text-sm text-gray-500">
|
||||
Error rendering email content. Please try refreshing the page.
|
||||
</div>
|
||||
);
|
||||
const decodedContent = decodeMimeContent(email.body);
|
||||
if (email.body.includes('Content-Type: text/html')) {
|
||||
return <div dangerouslySetInnerHTML={{ __html: decodedContent }} />;
|
||||
}
|
||||
return <div className="whitespace-pre-wrap">{decodedContent}</div>;
|
||||
};
|
||||
|
||||
// Add this helper function
|
||||
@ -580,7 +528,7 @@ const initialSidebarItems = [
|
||||
}
|
||||
];
|
||||
|
||||
export default function CourrierPage() {
|
||||
export default function MailPage() {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [accounts, setAccounts] = useState<Account[]>([
|
||||
@ -694,7 +642,7 @@ export default function CourrierPage() {
|
||||
}
|
||||
|
||||
// Process emails keeping exact folder names
|
||||
const processedEmails = (data.emails || []).map((email: any) => ({
|
||||
const processedEmails = data.emails.map((email: any) => ({
|
||||
id: Number(email.id),
|
||||
accountId: 1,
|
||||
from: email.from || '',
|
||||
@ -960,20 +908,20 @@ export default function CourrierPage() {
|
||||
// Update the email count in the header to show filtered count
|
||||
const renderEmailListHeader = () => (
|
||||
<div className="border-b border-gray-100">
|
||||
<div className="px-4 py-1">
|
||||
<div className="px-4 py-2">
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-2 h-4 w-4 text-gray-400" />
|
||||
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-400" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search in folder..."
|
||||
className="pl-8 h-8 bg-gray-50"
|
||||
className="pl-8 h-9 bg-gray-50"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4 h-10">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center justify-between px-4 h-14">
|
||||
<div className="flex items-center gap-3">
|
||||
<Checkbox
|
||||
checked={filteredEmails.length > 0 && selectedEmails.length === filteredEmails.length}
|
||||
onCheckedChange={toggleSelectAll}
|
||||
@ -1138,7 +1086,39 @@ export default function CourrierPage() {
|
||||
</div>
|
||||
|
||||
<div className="prose max-w-none">
|
||||
{renderEmailContent(selectedEmail)}
|
||||
{(() => {
|
||||
try {
|
||||
const parsed = parseFullEmail(selectedEmail.body);
|
||||
return (
|
||||
<div>
|
||||
{/* Display HTML content if available, otherwise fallback to text */}
|
||||
<div dangerouslySetInnerHTML={{
|
||||
__html: parsed.html || parsed.text || decodeMimeContent(selectedEmail.body)
|
||||
}} />
|
||||
|
||||
{/* Display attachments if present */}
|
||||
{parsed.attachments && parsed.attachments.length > 0 && (
|
||||
<div className="mt-6 border-t border-gray-200 pt-6">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-4">Attachments</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{parsed.attachments.map((attachment, index) => (
|
||||
<div key={index} className="flex items-center space-x-2 p-2 border rounded">
|
||||
<Paperclip className="h-4 w-4 text-gray-400" />
|
||||
<span className="text-sm text-gray-600 truncate">
|
||||
{attachment.filename}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Error parsing email:', e);
|
||||
return selectedEmail.body;
|
||||
}
|
||||
})()}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</>
|
||||
@ -1219,32 +1199,43 @@ export default function CourrierPage() {
|
||||
</h3>
|
||||
<div className="text-xs text-gray-500 truncate">
|
||||
{(() => {
|
||||
// Get clean preview of the actual message content
|
||||
let preview = '';
|
||||
try {
|
||||
// First decode the MIME content
|
||||
const decodedContent = decodeMimeContent(email.body);
|
||||
const parsed = parseFullEmail(email.body);
|
||||
|
||||
// Extract preview text
|
||||
let preview = decodedContent
|
||||
// Remove HTML tags
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
// Remove email headers
|
||||
.replace(/^(From|To|Sent|Subject|Date|Cc|Bcc):.*$/gim, '')
|
||||
// Remove quoted text
|
||||
.replace(/^>.*$/gm, '')
|
||||
// Remove multiple spaces
|
||||
.replace(/\s+/g, ' ')
|
||||
// Remove special characters
|
||||
.replace(/ |‌|»|«|>/g, ' ')
|
||||
// Fix common encoding issues
|
||||
.replace(/=C2=A0/g, ' ')
|
||||
.replace(/=E2=80=93/g, '\u2013')
|
||||
.replace(/=E2=80=94/g, '\u2014')
|
||||
.replace(/=E2=80=98/g, '\u2018')
|
||||
.replace(/=E2=80=99/g, '\u2019')
|
||||
.replace(/=E2=80=9C/g, '\u201C')
|
||||
.replace(/=E2=80=9D/g, '\u201D')
|
||||
// Try to get content from parsed email
|
||||
if (parsed.html) {
|
||||
// Extract text from HTML
|
||||
preview = parsed.html
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ |‌|»|«|>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
} else if (parsed.text) {
|
||||
preview = parsed.text;
|
||||
}
|
||||
|
||||
// If no preview from parsed content, try direct body
|
||||
if (!preview) {
|
||||
preview = email.body
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ |‌|»|«|>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
// Remove email artifacts and clean up
|
||||
preview = preview
|
||||
.replace(/^>+/gm, '')
|
||||
.replace(/Content-Type:[^\n]+/g, '')
|
||||
.replace(/Content-Transfer-Encoding:[^\n]+/g, '')
|
||||
.replace(/--[a-zA-Z0-9]+(-[a-zA-Z0-9]+)?/g, '')
|
||||
.replace(/boundary=[^\n]+/g, '')
|
||||
.replace(/charset=[^\n]+/g, '')
|
||||
.replace(/[\r\n]+/g, ' ')
|
||||
.trim();
|
||||
|
||||
// Take first 100 characters
|
||||
@ -1259,11 +1250,12 @@ export default function CourrierPage() {
|
||||
preview += '...';
|
||||
}
|
||||
|
||||
return preview || 'No preview available';
|
||||
} catch (e) {
|
||||
console.error('Error generating preview:', e);
|
||||
return 'Error loading preview';
|
||||
preview = '(Error generating preview)';
|
||||
}
|
||||
|
||||
return preview || 'No preview available';
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
@ -1695,93 +1687,92 @@ export default function CourrierPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<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-background text-gray-900 overflow-hidden">
|
||||
{/* 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">
|
||||
<>
|
||||
{/* Main layout */}
|
||||
<div className="flex h-[calc(100vh-theme(spacing.12))] bg-gray-50 text-gray-900 overflow-hidden mt-12">
|
||||
{/* 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">
|
||||
<Mail className="h-6 w-6 text-gray-600" />
|
||||
<span className="text-xl font-semibold text-gray-900">COURRIER</span>
|
||||
<PlusIcon className="h-3.5 w-3.5" />
|
||||
<span>Compose</span>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
{/* 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>
|
||||
{/* 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>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
{renderSidebarNav()}
|
||||
<span className="text-xs text-gray-500 ml-4">{account.email}</span>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Main content area */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Email list panel */}
|
||||
{renderEmailListWrapper()}
|
||||
</div>
|
||||
{/* Navigation */}
|
||||
{renderSidebarNav()}
|
||||
</div>
|
||||
|
||||
{/* Main content area */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Email list panel */}
|
||||
{renderEmailListWrapper()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1946,6 +1937,6 @@ export default function CourrierPage() {
|
||||
</div>
|
||||
)}
|
||||
{renderDeleteConfirmDialog()}
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user