275 lines
8.3 KiB
TypeScript
275 lines
8.3 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 { 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) {
|
|
console.log('EmailPreview: No email provided');
|
|
return '';
|
|
}
|
|
|
|
try {
|
|
console.log('EmailPreview: Raw email content:', {
|
|
content: email.content,
|
|
html: email.html,
|
|
text: email.text,
|
|
formattedContent: email.formattedContent
|
|
});
|
|
|
|
// Get the content in order of preference
|
|
let content = '';
|
|
|
|
// If content is an object with html/text
|
|
if (email.content && typeof email.content === 'object') {
|
|
console.log('EmailPreview: Using object content:', email.content);
|
|
content = email.content.html || email.content.text || '';
|
|
}
|
|
// If content is a string
|
|
else if (typeof email.content === 'string') {
|
|
console.log('EmailPreview: Using direct string content');
|
|
content = email.content;
|
|
}
|
|
// Fallback to html/text properties
|
|
else {
|
|
console.log('EmailPreview: Using html/text properties');
|
|
content = email.html || email.text || '';
|
|
}
|
|
|
|
console.log('EmailPreview: Content before sanitization:', content);
|
|
|
|
// Sanitize the content for display
|
|
const sanitizedContent = DOMPurify.sanitize(content, {
|
|
ADD_TAGS: ['style', 'table', 'thead', 'tbody', 'tr', 'td', 'th'],
|
|
ADD_ATTR: ['class', 'style', 'dir', 'colspan', 'rowspan'],
|
|
ALLOW_DATA_ATTR: false
|
|
});
|
|
|
|
console.log('EmailPreview: Final sanitized content:', sanitizedContent);
|
|
|
|
return sanitizedContent;
|
|
} catch (error) {
|
|
console.error('EmailPreview: Error formatting email content:', error);
|
|
return '';
|
|
}
|
|
}, [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="space-y-2 p-6">
|
|
<div className="border rounded-md overflow-hidden">
|
|
<div
|
|
ref={editorRef}
|
|
contentEditable={false}
|
|
className="w-full p-4 min-h-[300px] focus:outline-none email-content-display"
|
|
dangerouslySetInnerHTML={{ __html: formattedContent }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</ScrollArea>
|
|
</Card>
|
|
);
|
|
}
|