Neah/components/email/EmailPreview.tsx
2025-04-30 20:32:24 +02:00

269 lines
8.4 KiB
TypeScript

'use client';
import { useState, useRef, useEffect, useMemo } from 'react';
import { Loader2, Paperclip, User } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
formatReplyEmail,
formatForwardedEmail,
formatEmailForReplyOrForward,
EmailMessage as FormatterEmailMessage,
sanitizeHtml
} from '@/lib/utils/email-formatter';
import { formatEmailContent } from '@/lib/utils/email-content';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { AvatarImage } from '@/components/ui/avatar';
import { Card } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import { CalendarIcon, PaperclipIcon } from 'lucide-react';
import Link from 'next/link';
import DOMPurify from 'dompurify';
interface EmailAddress {
name: string;
address: string;
}
interface EmailAttachment {
filename: string;
contentType: string;
size: number;
path?: string;
content?: string;
}
interface EmailMessage {
id: string;
uid: number;
from: EmailAddress[];
to: EmailAddress[];
cc?: EmailAddress[];
bcc?: EmailAddress[];
subject: string;
date: string;
flags: string[];
attachments: EmailAttachment[];
content?: string | {
text?: string;
html?: string;
};
html?: string;
text?: string;
formattedContent?: string;
}
interface EmailPreviewProps {
email: EmailMessage | null;
loading?: boolean;
onReply?: (type: 'reply' | 'reply-all' | 'forward') => void;
}
export default function EmailPreview({ email, loading = false, onReply }: EmailPreviewProps) {
// Add editorRef to match ComposeEmail exactly
const editorRef = useRef<HTMLDivElement>(null);
// Format the date
const formatDate = (date: Date | string) => {
if (!date) return '';
const dateObj = date instanceof Date ? date : new Date(date);
return dateObj.toLocaleString('en-US', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
// Format email addresses
const formatEmailAddresses = (addresses: Array<{name: string, address: string}> | undefined) => {
if (!addresses || addresses.length === 0) return '';
return addresses.map(addr =>
addr.name && addr.name !== addr.address
? `${addr.name} <${addr.address}>`
: addr.address
).join(', ');
};
// Get sender initials for avatar
const getSenderInitials = (name: string) => {
if (!name) return '';
return name
.split(" ")
.map((n) => n?.[0] || '')
.join("")
.toUpperCase()
.slice(0, 2);
};
// Format the email content
const formattedContent = useMemo(() => {
if (!email) {
return '';
}
// Use the improved, standardized email content formatter
return formatEmailContent(email);
}, [email]);
// Display loading state
if (loading) {
return (
<div className="flex items-center justify-center h-full p-6">
<div className="text-center">
<Loader2 className="h-8 w-8 animate-spin mx-auto mb-4 text-primary" />
<p>Loading email...</p>
</div>
</div>
);
}
// No email selected
if (!email) {
return (
<div className="flex items-center justify-center h-full p-6">
<div className="text-center text-muted-foreground">
<p>Select an email to view</p>
</div>
</div>
);
}
const sender = email.from && email.from.length > 0 ? email.from[0] : undefined;
// Update the array access to use proper type checking
const hasAttachments = email.attachments && email.attachments.length > 0;
return (
<Card className="flex flex-col h-full overflow-hidden border-0 shadow-none">
{/* Email header */}
<div className="p-6 border-b">
<div className="mb-4">
<h2 className="text-xl font-semibold mb-4">{email.subject}</h2>
<div className="flex items-start gap-3 mb-4">
<Avatar className="h-10 w-10">
<AvatarFallback>{getSenderInitials(sender?.name || '')}</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<div className="font-medium">{sender?.name || sender?.address}</div>
<span className="text-sm text-muted-foreground">{formatDate(email.date)}</span>
</div>
<div className="text-sm text-muted-foreground truncate mt-1">
To: {formatEmailAddresses(email.to)}
</div>
{email.cc && email.cc.length > 0 && (
<div className="text-sm text-muted-foreground truncate mt-1">
Cc: {formatEmailAddresses(email.cc)}
</div>
)}
</div>
</div>
{/* Action buttons */}
{onReply && (
<div className="flex gap-2 mt-4">
<Button
size="sm"
variant="outline"
onClick={() => onReply('reply')}
>
Reply
</Button>
<Button
size="sm"
variant="outline"
onClick={() => onReply('reply-all')}
>
Reply All
</Button>
<Button
size="sm"
variant="outline"
onClick={() => onReply('forward')}
>
Forward
</Button>
</div>
)}
</div>
{/* Attachments */}
{hasAttachments && (
<div className="px-6 py-3 border-b bg-muted/30">
<div className="text-sm font-medium mb-2">Attachments</div>
<div className="flex flex-wrap gap-2">
{email.attachments.map((attachment, index) => (
<Badge key={index} variant="outline" className="flex items-center gap-1 px-2 py-1">
<Paperclip className="h-3.5 w-3.5" />
<span>{attachment.filename}</span>
<span className="text-xs text-muted-foreground ml-1">
({Math.round(attachment.size / 1024)}KB)
</span>
</Badge>
))}
</div>
</div>
)}
</div>
{/* Email content */}
<ScrollArea className="flex-1">
<div className="p-6">
<div
ref={editorRef}
contentEditable={false}
className="w-full email-content-container rounded-md overflow-hidden"
style={{
backgroundColor: '#ffffff',
border: '1px solid #e2e8f0',
boxShadow: '0 1px 3px rgba(0,0,0,0.05)',
minHeight: '300px'
}}
>
<div className="email-content-body p-4 sm:p-6">
{formattedContent ? (
<div
className="email-content-rendered"
dangerouslySetInnerHTML={{ __html: formattedContent }}
/>
) : (
<div className="email-content-empty py-8 text-center text-muted-foreground">
<p>This email does not contain any content.</p>
</div>
)}
</div>
</div>
{process.env.NODE_ENV === 'development' && (
<details className="mt-6 text-xs text-muted-foreground border rounded-md p-2">
<summary className="cursor-pointer">Email Debug Info</summary>
<div className="mt-2 overflow-auto max-h-40">
<p><strong>Email ID:</strong> {email.id}</p>
<p><strong>Content Type:</strong> {
typeof email.content === 'object' && email.content?.html
? 'HTML'
: 'Plain Text'
}</p>
<p><strong>Content Size:</strong> {
typeof email.content === 'object'
? `HTML: ${email.content?.html?.length || 0} chars, Text: ${email.content?.text?.length || 0} chars`
: `${typeof email.content === 'string' ? email.content.length : 0} chars`
}</p>
</div>
</details>
)}
</div>
</ScrollArea>
</Card>
);
}