courrier refactor rebuild 2

This commit is contained in:
alma 2025-04-27 09:56:52 +02:00
parent 3c738f179a
commit c993fe738e
10 changed files with 955 additions and 1008 deletions

View File

@ -29,9 +29,10 @@ import { Button } from '@/components/ui/button';
// Import components
import EmailSidebar from '@/components/email/EmailSidebar';
import EmailList from '@/components/email/EmailList';
import EmailContent from '@/components/email/EmailContent';
import EmailHeader from '@/components/email/EmailHeader';
import EmailSidebarContent from '@/components/email/EmailSidebarContent';
import EmailDetailView from '@/components/email/EmailDetailView';
import ComposeEmail from '@/components/email/ComposeEmail';
import { DeleteConfirmDialog, LoginNeededAlert } from '@/components/email/EmailDialogs';
// Import the custom hook
import { useCourrier, EmailData } from '@/hooks/use-courrier';
@ -89,35 +90,13 @@ export default function CourrierPage() {
setPage,
} = useCourrier();
// Local state
// UI state
const [showComposeModal, setShowComposeModal] = useState(false);
const [composeData, setComposeData] = useState<EmailData | null>(null);
const [composeType, setComposeType] = useState<'new' | 'reply' | 'reply-all' | 'forward'>('new');
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showLoginNeeded, setShowLoginNeeded] = useState(false);
// States to match the provided implementation
const [sidebarOpen, setSidebarOpen] = useState(true);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const [isReplying, setIsReplying] = useState(false);
const [isForwarding, setIsForwarding] = useState(false);
const [accountsDropdownOpen, setAccountsDropdownOpen] = useState(false);
const [showCompose, setShowCompose] = useState(false);
const [composeTo, setComposeTo] = useState('');
const [composeCc, setComposeCc] = useState('');
const [composeBcc, setComposeBcc] = useState('');
const [composeSubject, setComposeSubject] = useState('');
const [composeBody, setComposeBody] = useState('');
const [showCc, setShowCc] = useState(false);
const [showBcc, setShowBcc] = useState(false);
const [attachments, setAttachments] = useState<any[]>([]);
// Mock accounts
const [accounts, setAccounts] = useState<Account[]>([
{ id: 0, name: 'All', email: '', color: 'bg-gray-500' },
{ id: 1, name: 'Mail', email: 'user@example.com', color: 'bg-blue-500' }
]);
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
// Check for more emails
const hasMoreEmails = page < totalPages;
@ -158,77 +137,33 @@ export default function CourrierPage() {
}
};
// Handle email reply or forward
const handleReplyOrForward = (type: 'reply' | 'reply-all' | 'forward') => {
// Handle email actions
const handleReply = () => {
if (!selectedEmail) return;
const formattedEmail = formatEmailForAction(selectedEmail, type);
if (!formattedEmail) return;
setComposeTo(formattedEmail.to || '');
setComposeCc(formattedEmail.cc || '');
setComposeSubject(formattedEmail.subject || '');
setComposeBody(formattedEmail.body || '');
if (type === 'forward') {
setIsForwarding(true);
} else {
setIsReplying(true);
}
setShowCompose(true);
setShowCc(type === 'reply-all');
setComposeType('reply');
setShowComposeModal(true);
};
// Handle compose new email
const handleReplyAll = () => {
if (!selectedEmail) return;
setComposeType('reply-all');
setShowComposeModal(true);
};
const handleForward = () => {
if (!selectedEmail) return;
setComposeType('forward');
setShowComposeModal(true);
};
const handleComposeNew = () => {
setComposeTo('');
setComposeCc('');
setComposeBcc('');
setComposeSubject('');
setComposeBody('');
setShowCc(false);
setShowBcc(false);
setIsReplying(false);
setIsForwarding(false);
setShowCompose(true);
setComposeType('new');
setShowComposeModal(true);
};
// Handle sending email
const handleSend = async () => {
if (!composeTo) {
alert('Please specify at least one recipient');
return;
}
try {
await sendEmail({
to: composeTo,
cc: composeCc,
bcc: composeBcc,
subject: composeSubject,
body: composeBody,
});
// Clear compose form and close modal
setComposeTo('');
setComposeCc('');
setComposeBcc('');
setComposeSubject('');
setComposeBody('');
setAttachments([]);
setShowCompose(false);
setIsReplying(false);
setIsForwarding(false);
// Refresh the Sent folder if we're currently viewing it
if (currentFolder.toLowerCase() === 'sent') {
loadEmails();
}
} catch (error) {
console.error('Error sending email:', error);
alert('Failed to send email. Please try again.');
}
const handleSendEmail = async (emailData: EmailData) => {
return await sendEmail(emailData);
};
// Handle delete confirmation
@ -254,406 +189,115 @@ export default function CourrierPage() {
router.push('/courrier/login');
};
// Handle mailbox change
const handleMailboxChange = (newMailbox: string) => {
changeFolder(newMailbox);
};
// Format date for display
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
if (date.toDateString() === now.toDateString()) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} else {
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
}
};
// Render sidebar navigation
const renderSidebarNav = () => (
<nav className="p-3">
<ul className="space-y-0.5 px-2">
{mailboxes.map((folder) => (
<li key={folder}>
<Button
variant={currentFolder === folder ? 'secondary' : 'ghost'}
className={`w-full justify-start py-2 ${
currentFolder === folder ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900'
}`}
onClick={() => handleMailboxChange(folder)}
>
<div className="flex items-center">
{getFolderIcon(folder)}
<span className="ml-2">{formatFolderName(folder)}</span>
</div>
</Button>
</li>
))}
</ul>
</nav>
);
// Helper to format folder names
const formatFolderName = (folder: string) => {
return folder.charAt(0).toUpperCase() + folder.slice(1).toLowerCase();
};
// Get account color
const getAccountColor = (accountId: number) => {
const account = accounts.find(acc => acc.id === accountId);
return account ? account.color : 'bg-gray-500';
};
// Helper to get folder icons
const getFolderIcon = (folder: string) => {
const folderLower = folder.toLowerCase();
if (folderLower.includes('inbox')) {
return <Inbox className="h-4 w-4" />;
} else if (folderLower.includes('sent')) {
return <Send className="h-4 w-4" />;
} else if (folderLower.includes('trash')) {
return <Trash className="h-4 w-4" />;
} else if (folderLower.includes('archive')) {
return <Archive className="h-4 w-4" />;
} else if (folderLower.includes('draft')) {
return <Edit className="h-4 w-4" />;
} else if (folderLower.includes('spam') || folderLower.includes('junk')) {
return <AlertOctagon className="h-4 w-4" />;
} else {
return <Folder className="h-4 w-4" />;
}
};
// Render email list component
const renderEmailList = () => (
<EmailList
emails={emails}
selectedEmailIds={selectedEmailIds}
selectedEmail={selectedEmail}
currentFolder={currentFolder}
isLoading={isLoading}
totalEmails={emails.length}
hasMoreEmails={hasMoreEmails}
onSelectEmail={handleEmailSelect}
onToggleSelect={toggleEmailSelection}
onToggleSelectAll={toggleSelectAll}
onBulkAction={handleBulkAction}
onToggleStarred={toggleStarred}
onLoadMore={handleLoadMore}
onSearch={searchEmails}
/>
);
// Render email content based on the email body
const renderEmailContent = (email: any) => {
try {
// For simple rendering in this example, we'll just display the content directly
return <div dangerouslySetInnerHTML={{ __html: email.content || email.html || email.body || '' }} />;
} catch (e) {
console.error('Error rendering email:', e);
return <div className="text-gray-500">Failed to render email content</div>;
}
};
// Email list wrapper with preview panel
const renderEmailListWrapper = () => (
<div className="flex-1 flex overflow-hidden">
{/* Email list panel */}
{renderEmailList()}
{/* Preview panel - will automatically take remaining space */}
<div className="flex-1 bg-white/95 backdrop-blur-sm flex flex-col">
{selectedEmail ? (
<>
{/* Email actions header */}
<div className="flex-none px-4 py-3 border-b border-gray-100">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 min-w-0 flex-1">
<Button
variant="ghost"
size="icon"
onClick={() => handleEmailSelect('')}
className="md:hidden flex-shrink-0"
>
<ChevronLeft className="h-5 w-5" />
</Button>
<div className="min-w-0 max-w-[500px]">
<h2 className="text-lg font-semibold text-gray-900 truncate">
{selectedEmail.subject}
</h2>
</div>
</div>
<div className="flex items-center gap-1 flex-shrink-0 ml-auto">
<div className="flex items-center border-l border-gray-200 pl-4">
<Button
variant="ghost"
size="icon"
className="text-gray-400 hover:text-gray-900 h-9 w-9"
onClick={() => handleReplyOrForward('reply')}
>
<Reply className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="text-gray-400 hover:text-gray-900 h-9 w-9"
onClick={() => handleReplyOrForward('reply-all')}
>
<ReplyAll className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="text-gray-400 hover:text-gray-900 h-9 w-9"
onClick={() => handleReplyOrForward('forward')}
>
<Forward className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="text-gray-400 hover:text-gray-900 h-9 w-9"
onClick={() => toggleStarred(selectedEmail.id)}
>
<Star className={`h-4 w-4 ${selectedEmail.starred ? 'fill-yellow-400 text-yellow-400' : ''}`} />
</Button>
</div>
</div>
</div>
</div>
{/* Scrollable content area */}
<ScrollArea className="flex-1 p-6">
<div className="flex items-center gap-4 mb-6">
<Avatar className="h-10 w-10">
<AvatarFallback>
{(selectedEmail.from?.[0]?.name || '').charAt(0) || (selectedEmail.from?.[0]?.address || '').charAt(0) || '?'}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className="font-medium text-gray-900">
{selectedEmail.from?.[0]?.name || ''} <span className="text-gray-500">&lt;{selectedEmail.from?.[0]?.address || ''}&gt;</span>
</p>
<p className="text-sm text-gray-500">
to {selectedEmail.to?.[0]?.address || ''}
</p>
{selectedEmail.cc && selectedEmail.cc.length > 0 && (
<p className="text-sm text-gray-500">
cc {selectedEmail.cc.map(c => c.address).join(', ')}
</p>
)}
</div>
<div className="text-sm text-gray-500 whitespace-nowrap">
{formatDate(selectedEmail.date)}
</div>
</div>
<div className="prose max-w-none">
{renderEmailContent(selectedEmail)}
</div>
</ScrollArea>
</>
) : (
<div className="flex flex-col items-center justify-center h-full">
<Mail className="h-12 w-12 text-gray-400 mb-4" />
<p className="text-gray-500">Select an email to view its contents</p>
</div>
)}
</div>
</div>
);
// Delete confirmation dialog
const renderDeleteConfirmDialog = () => (
<AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Emails</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete {selectedEmailIds.length} selected email{selectedEmailIds.length > 1 ? 's' : ''}? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDeleteConfirm}>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
// If there's a critical error, show error dialog
if (error && !isLoading && emails.length === 0 && !showLoginNeeded) {
return (
<div className="flex h-screen items-center justify-center">
<Alert variant="destructive" className="max-w-md">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error loading emails</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
</div>
);
}
return (
<>
<div className="flex h-screen flex-col">
{/* Loading Fix for development */}
<SimplifiedLoadingFix />
{/* Login required dialog */}
<AlertDialog open={showLoginNeeded} onOpenChange={setShowLoginNeeded}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Login Required</AlertDialogTitle>
<AlertDialogDescription>
You need to configure your email account credentials before you can access your emails.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleGoToLogin}>Setup Email</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Login needed alert */}
<LoginNeededAlert
show={showLoginNeeded}
onLogin={handleGoToLogin}
onClose={() => setShowLoginNeeded(false)}
/>
{/* Main layout */}
<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>
{/* Main Content */}
<div className="flex flex-1 overflow-hidden">
{/* Sidebar (Desktop) */}
{sidebarOpen && (
<aside className="hidden md:flex md:w-64 bg-gray-50 border-r border-gray-200 flex-col">
{/* Account switching and compose button */}
<div className="px-4 py-3 border-b border-gray-200">
<Button
className="w-full bg-blue-600 hover:bg-blue-700"
onClick={handleComposeNew}
>
<PlusIcon className="mr-2 h-4 w-4" />
Compose
</Button>
</div>
{/* Folder Navigation */}
<EmailSidebarContent
mailboxes={mailboxes}
currentFolder={currentFolder}
onFolderChange={changeFolder}
/>
</aside>
)}
{/* Email List and Content View */}
<div className="flex-1 flex">
{/* Email List */}
<EmailList
emails={emails}
selectedEmailIds={selectedEmailIds}
selectedEmail={selectedEmail}
currentFolder={currentFolder}
isLoading={isLoading}
totalEmails={emails.length}
hasMoreEmails={hasMoreEmails}
onSelectEmail={handleEmailSelect}
onToggleSelect={toggleEmailSelection}
onToggleSelectAll={toggleSelectAll}
onBulkAction={handleBulkAction}
onToggleStarred={toggleStarred}
onLoadMore={handleLoadMore}
onSearch={searchEmails}
/>
{/* Email Content View */}
<div className="flex-1 bg-white/95 backdrop-blur-sm flex flex-col">
{selectedEmail ? (
<EmailDetailView
email={selectedEmail}
onBack={() => handleEmailSelect('')}
onReply={handleReply}
onReplyAll={handleReplyAll}
onForward={handleForward}
onToggleStar={() => toggleStarred(selectedEmail.id)}
/>
) : (
<div className="flex-1 flex flex-col items-center justify-center text-center p-8">
<Mail className="h-12 w-12 text-gray-300 mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-1">Select an email to read</h3>
<p className="text-sm text-gray-500 max-w-sm">
Choose an email from the list or compose a new message to get started.
</p>
<Button
variant="ghost"
size="icon"
onClick={() => handleMailboxChange('INBOX')}
className="text-gray-600 hover:text-gray-900 hover:bg-gray-100"
className="mt-6 bg-blue-600 hover:bg-blue-700"
onClick={handleComposeNew}
>
<RefreshCw className="h-4 w-4" />
<PlusIcon className="mr-2 h-4 w-4" />
Compose New
</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 */}
<ComposeEmail
showCompose={showCompose}
setShowCompose={setShowCompose}
composeTo={composeTo}
setComposeTo={setComposeTo}
composeCc={composeCc}
setComposeCc={setComposeCc}
composeBcc={composeBcc}
setComposeBcc={setComposeBcc}
composeSubject={composeSubject}
setComposeSubject={setComposeSubject}
composeBody={composeBody}
setComposeBody={setComposeBody}
showCc={showCc}
setShowCc={setShowCc}
showBcc={showBcc}
setShowBcc={setShowBcc}
attachments={attachments}
setAttachments={setAttachments}
handleSend={handleSend}
replyTo={isReplying ? selectedEmail : null}
forwardFrom={isForwarding ? selectedEmail : null}
onSend={async (email: EmailData) => {
console.log('Email sent:', email);
setShowCompose(false);
setIsReplying(false);
setIsForwarding(false);
return Promise.resolve();
}}
onCancel={() => {
setShowCompose(false);
setComposeTo('');
setComposeCc('');
setComposeBcc('');
setComposeSubject('');
setComposeBody('');
setShowCc(false);
setShowBcc(false);
setAttachments([]);
setIsReplying(false);
setIsForwarding(false);
}}
</div>
{/* Compose Modal */}
<Dialog open={showComposeModal} onOpenChange={setShowComposeModal}>
<DialogContent className="max-w-4xl p-0">
<ComposeEmail
initialEmail={selectedEmail}
type={composeType}
onClose={() => setShowComposeModal(false)}
onSend={async (emailData: EmailData) => {
await sendEmail(emailData);
}}
/>
</DialogContent>
</Dialog>
{/* Delete Confirmation Dialog */}
<DeleteConfirmDialog
show={showDeleteConfirm}
selectedCount={selectedEmailIds.length}
onConfirm={handleDeleteConfirm}
onCancel={() => setShowDeleteConfirm(false)}
/>
{renderDeleteConfirmDialog()}
</>
</div>
);
}

View File

@ -11,6 +11,11 @@ import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/componen
import DOMPurify from 'isomorphic-dompurify';
import { Label } from '@/components/ui/label';
// Import sub-components
import ComposeEmailHeader from './ComposeEmailHeader';
import ComposeEmailForm from './ComposeEmailForm';
import ComposeEmailFooter from './ComposeEmailFooter';
// Import ONLY from the centralized formatter
import {
formatForwardedEmail,
@ -148,10 +153,6 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
type: string;
}>>([]);
// Refs
const editorRef = useRef<HTMLDivElement>(null);
const attachmentInputRef = useRef<HTMLInputElement>(null);
// Initialize the form when replying to or forwarding an email
useEffect(() => {
if (initialEmail && type !== 'new') {
@ -187,148 +188,47 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
setSubject(subject);
setEmailContent(content);
}
// For type safety
const isReplyType = (t: 'new' | 'reply' | 'reply-all' | 'forward'): t is 'reply' | 'reply-all' | 'forward' =>
t === 'reply' || t === 'reply-all' || t === 'forward';
// Focus editor after initializing content
setTimeout(() => {
if (isReplyType(type) && editorRef.current) {
// For replies/forwards, focus contentEditable
editorRef.current.focus();
try {
// Place cursor at the beginning
const selection = window.getSelection();
if (selection) {
const range = document.createRange();
if (editorRef.current.firstChild) {
range.setStart(editorRef.current.firstChild, 0);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
}
}
} catch (e) {
console.error('Error positioning cursor:', e);
}
} else {
// For new emails, focus the textarea
const textarea = document.querySelector('textarea');
textarea?.focus();
}
}, 100);
} catch (error) {
console.error('Error formatting email:', error);
} catch (err) {
console.error('Error formatting email for reply/forward:', err);
}
}
}, [initialEmail, type]);
// Handle attachment selection
const handleAttachmentClick = () => {
attachmentInputRef.current?.click();
};
// Process selected files
const handleFileSelection = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
// Read files as data URLs
for (const file of files) {
const reader = new FileReader();
reader.onload = (event) => {
const content = event.target?.result as string;
setAttachments(current => [
...current,
{
name: file.name,
content: content.split(',')[1], // Remove data:mime/type;base64, prefix
type: file.type
}
]);
};
reader.readAsDataURL(file);
}
// Handle file attachments
const handleAttachmentAdd = async (files: FileList) => {
const newAttachments = Array.from(files).map(file => ({
name: file.name,
type: file.type,
content: URL.createObjectURL(file)
}));
// Reset file input
if (e.target) {
e.target.value = '';
}
setAttachments(prev => [...prev, ...newAttachments]);
};
// Remove attachment
const removeAttachment = (index: number) => {
setAttachments(current => current.filter((_, i) => i !== index));
const handleAttachmentRemove = (index: number) => {
setAttachments(prev => prev.filter((_, i) => i !== index));
};
// Handle editor input
const handleEditorInput = () => {
if (!editorRef.current) return;
// Store the current selection/cursor position
const selection = window.getSelection();
const range = selection?.getRangeAt(0);
const offset = range?.startOffset || 0;
const container = range?.startContainer;
// Capture the content
setEmailContent(editorRef.current.innerHTML);
// Try to restore the cursor position after React updates
setTimeout(() => {
if (!selection || !range || !container || !editorRef.current) return;
try {
if (editorRef.current.contains(container)) {
const newRange = document.createRange();
newRange.setStart(container, offset);
newRange.collapse(true);
selection.removeAllRanges();
selection.addRange(newRange);
}
} catch (e) {
console.error('Error restoring cursor position:', e);
}
}, 0);
};
// Add a handler for textarea changes
const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setEmailContent(e.target.value);
};
// Send email
// Handle sending email
const handleSend = async () => {
if (!to) {
alert('Please specify at least one recipient');
return;
}
setSending(true);
try {
setSending(true);
// For new emails, emailContent is already set via onChange
// For replies/forwards, we need to get content from editorRef
const finalContent = type === 'new'
? emailContent
: editorRef.current?.innerHTML || emailContent;
await onSend({
to,
cc: cc || undefined,
bcc: bcc || undefined,
subject,
body: finalContent,
body: emailContent,
attachments
});
// Reset form and close
onClose();
} catch (error) {
console.error('Error sending email:', error);
@ -337,201 +237,46 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
setSending(false);
}
};
return (
<Card className="w-full max-w-4xl mx-auto">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span>{type === 'new' ? 'New Message' : type === 'forward' ? 'Forward Email' : 'Reply to Email'}</span>
<Button variant="ghost" size="icon" onClick={onClose}>
<X className="h-5 w-5" />
</Button>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Recipients, Subject fields */}
<div className="p-3 border-b space-y-2">
<div className="flex items-center">
<span className="w-16 text-sm font-medium">To:</span>
<Input
value={to}
onChange={(e) => setTo(e.target.value)}
className="flex-1 border-0 shadow-none h-8 focus-visible:ring-0"
placeholder="recipient@example.com"
/>
</div>
{showCc && (
<div className="flex items-center">
<span className="w-16 text-sm font-medium">Cc:</span>
<Input
value={cc}
onChange={(e) => setCc(e.target.value)}
className="flex-1 border-0 shadow-none h-8 focus-visible:ring-0"
placeholder="cc@example.com"
/>
</div>
)}
{showBcc && (
<div className="flex items-center">
<span className="w-16 text-sm font-medium">Bcc:</span>
<Input
value={bcc}
onChange={(e) => setBcc(e.target.value)}
className="flex-1 border-0 shadow-none h-8 focus-visible:ring-0"
placeholder="bcc@example.com"
/>
</div>
)}
{/* CC/BCC controls */}
<div className="flex items-center text-xs">
<button
className="text-primary hover:underline mr-3 flex items-center"
onClick={() => setShowCc(!showCc)}
>
{showCc ? (
<>
<ChevronUp className="h-3 w-3 mr-0.5" />
Hide Cc
</>
) : (
<>
<ChevronDown className="h-3 w-3 mr-0.5" />
Show Cc
</>
)}
</button>
<button
className="text-primary hover:underline flex items-center"
onClick={() => setShowBcc(!showBcc)}
>
{showBcc ? (
<>
<ChevronUp className="h-3 w-3 mr-0.5" />
Hide Bcc
</>
) : (
<>
<ChevronDown className="h-3 w-3 mr-0.5" />
Show Bcc
</>
)}
</button>
</div>
<div className="flex items-center">
<span className="w-16 text-sm font-medium">Subject:</span>
<Input
value={subject}
onChange={(e) => setSubject(e.target.value)}
className="flex-1 border-0 shadow-none h-8 focus-visible:ring-0"
placeholder="Subject"
/>
</div>
</div>
{/* Email Body Editor - different approach based on type */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<label htmlFor="body" className="text-sm font-medium">Message</label>
</div>
{/* For new emails, use textarea for better text direction */}
{type === 'new' ? (
<div className="border rounded-md overflow-hidden">
<textarea
value={emailContent}
onChange={handleTextareaChange}
className="w-full p-4 min-h-[300px] focus:outline-none resize-none"
placeholder="Write your message here..."
disabled={sending}
/>
</div>
) : (
/* For replies and forwards, use contentEditable to preserve formatting */
<div className="border rounded-md overflow-hidden">
<div
ref={editorRef}
contentEditable={!sending}
className="w-full p-4 min-h-[300px] focus:outline-none"
onInput={handleEditorInput}
dangerouslySetInnerHTML={{ __html: emailContent }}
dir="rtl"
style={{
textAlign: 'right',
}}
/>
</div>
)}
</div>
{/* Attachments section */}
{attachments.length > 0 && (
<div className="border-t p-2">
<div className="text-sm font-medium mb-1">Attachments:</div>
<div className="flex flex-wrap gap-2">
{attachments.map((attachment, index) => (
<div key={index} className="flex items-center gap-1 text-sm bg-muted px-2 py-1 rounded">
<Paperclip className="h-3 w-3" />
<span>{attachment.name}</span>
<button
onClick={() => removeAttachment(index)}
className="ml-1 text-muted-foreground hover:text-destructive"
>
<X className="h-3 w-3" />
</button>
</div>
))}
</div>
</div>
)}
</CardContent>
<Card className="w-full max-w-3xl mx-auto flex flex-col min-h-[60vh] max-h-[80vh]">
<ComposeEmailHeader
type={type}
onClose={onClose}
/>
<CardFooter className="border-t p-3 flex justify-between">
<div>
<input
type="file"
ref={attachmentInputRef}
className="hidden"
onChange={handleFileSelection}
multiple
/>
<Button
variant="outline"
size="sm"
onClick={handleAttachmentClick}
>
<Paperclip className="h-4 w-4 mr-1" />
Attach
</Button>
</div>
<Button
size="sm"
onClick={handleSend}
disabled={sending}
>
{sending ? (
<>
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
Sending...
</>
) : (
<>
<SendHorizontal className="h-4 w-4 mr-1" />
Send
</>
)}
</Button>
</CardFooter>
<div className="flex-1 overflow-y-auto">
<ComposeEmailForm
to={to}
setTo={setTo}
cc={cc}
setCc={setCc}
bcc={bcc}
setBcc={setBcc}
subject={subject}
setSubject={setSubject}
emailContent={emailContent}
setEmailContent={setEmailContent}
showCc={showCc}
setShowCc={setShowCc}
showBcc={showBcc}
setShowBcc={setShowBcc}
attachments={attachments}
onAttachmentAdd={handleAttachmentAdd}
onAttachmentRemove={handleAttachmentRemove}
/>
</div>
<ComposeEmailFooter
sending={sending}
onSend={handleSend}
onCancel={onClose}
/>
</Card>
);
}
// Adapter component for legacy props
// Legacy adapter to maintain backward compatibility
function LegacyAdapter({
showCompose,
setShowCompose,
@ -558,224 +303,113 @@ function LegacyAdapter({
replyTo,
forwardFrom
}: LegacyComposeEmailProps) {
// Create a ref for the contentEditable div
const composeBodyRef = useRef<HTMLDivElement>(null);
const [sending, setSending] = useState(false);
// Set the content of the contentEditable div on mount
useEffect(() => {
if (composeBodyRef.current && composeBody) {
composeBodyRef.current.innerHTML = composeBody;
}
}, [composeBody, showCompose]);
// Handle clicking the content editable area
const handleComposeAreaClick = () => {
composeBodyRef.current?.focus();
// Determine the type based on legacy props
const determineType = (): 'new' | 'reply' | 'reply-all' | 'forward' => {
if (originalEmail?.type === 'forward') return 'forward';
if (originalEmail?.type === 'reply-all') return 'reply-all';
if (originalEmail?.type === 'reply') return 'reply';
if (replyTo) return 'reply';
if (forwardFrom) return 'forward';
return 'new';
};
// Handle input events on the contentEditable div
const handleInput = () => {
if (composeBodyRef.current) {
setComposeBody(composeBodyRef.current.innerHTML);
// Converts attachments to the expected format
const convertAttachments = () => {
return attachments.map(att => ({
name: att.name || att.filename || 'attachment',
content: att.content || '',
type: att.type || att.contentType || 'application/octet-stream'
}));
};
// Handle sending in the legacy format
const handleLegacySend = async () => {
setSending(true);
try {
if (onSend) {
// New API
await onSend({
to: composeTo,
cc: composeCc,
bcc: composeBcc,
subject: composeSubject,
body: composeBody,
attachments: convertAttachments()
});
} else if (handleSend) {
// Old API
await handleSend();
}
// Close compose window
setShowCompose(false);
} catch (error) {
console.error('Error sending email:', error);
alert('Failed to send email. Please try again.');
} finally {
setSending(false);
}
};
// Handle file attachment
const handleFileAttachment = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!e.target.files || e.target.files.length === 0) return;
// Here you would normally process the files and add them to attachments
console.log('Files selected:', e.target.files);
// Just a placeholder - in a real implementation you'd convert files to the format needed
const newAttachments = Array.from(e.target.files).map(file => ({
// Handle file selection for legacy interface
const handleFileSelection = (files: FileList) => {
const newAttachments = Array.from(files).map(file => ({
name: file.name,
type: file.type,
size: file.size,
// In a real implementation you'd use FileReader to read the file content
content: URL.createObjectURL(file),
size: file.size
}));
// Add to existing attachments
setAttachments([...attachments, ...newAttachments]);
};
// Handle send email action
const handleSendEmail = async () => {
// Call the provided onSend handler with the email data
await onSend({
to: composeTo,
cc: composeCc,
bcc: composeBcc,
subject: composeSubject,
body: composeBody,
attachments
});
};
if (!showCompose) return null;
return (
<div className="fixed inset-0 bg-gray-600/30 backdrop-blur-sm z-50 flex items-center justify-center">
<div className="w-full max-w-2xl h-[90vh] bg-white rounded-xl shadow-xl flex flex-col mx-4">
{/* Modal Header */}
<div className="flex-none flex items-center justify-between px-6 py-3 border-b border-gray-200">
<h3 className="text-lg font-semibold text-gray-900">
{replyTo ? 'Reply' : forwardFrom ? 'Forward' : 'New Message'}
</h3>
<Button
variant="ghost"
size="icon"
className="hover:bg-gray-100 rounded-full"
onClick={onCancel}
>
<X className="h-5 w-5 text-gray-500" />
</Button>
</div>
{/* Modal Body */}
<div className="flex-1 overflow-hidden">
<div className="h-full flex flex-col p-6 space-y-4 overflow-y-auto">
{/* To Field */}
<div className="flex-none">
<Label htmlFor="to" className="block text-sm font-medium text-gray-700">To</Label>
<Input
id="to"
value={composeTo}
onChange={(e) => setComposeTo(e.target.value)}
placeholder="recipient@example.com"
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
/>
</div>
{/* CC/BCC Toggle Buttons */}
<div className="flex-none flex items-center gap-4">
<button
type="button"
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
onClick={() => setShowCc(!showCc)}
>
{showCc ? 'Hide Cc' : 'Add Cc'}
</button>
<button
type="button"
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
onClick={() => setShowBcc(!showBcc)}
>
{showBcc ? 'Hide Bcc' : 'Add Bcc'}
</button>
</div>
{/* CC Field */}
{showCc && (
<div className="flex-none">
<Label htmlFor="cc" className="block text-sm font-medium text-gray-700">Cc</Label>
<Input
id="cc"
value={composeCc}
onChange={(e) => setComposeCc(e.target.value)}
placeholder="cc@example.com"
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
/>
</div>
)}
{/* BCC Field */}
{showBcc && (
<div className="flex-none">
<Label htmlFor="bcc" className="block text-sm font-medium text-gray-700">Bcc</Label>
<Input
id="bcc"
value={composeBcc}
onChange={(e) => setComposeBcc(e.target.value)}
placeholder="bcc@example.com"
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
/>
</div>
)}
{/* Subject Field */}
<div className="flex-none">
<Label htmlFor="subject" className="block text-sm font-medium text-gray-700">Subject</Label>
<Input
id="subject"
value={composeSubject}
onChange={(e) => setComposeSubject(e.target.value)}
placeholder="Enter subject"
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
/>
</div>
{/* Message Body */}
<div className="flex-1 min-h-[200px] flex flex-col">
<Label htmlFor="message" className="flex-none block text-sm font-medium text-gray-700 mb-2">Message</Label>
<div
ref={composeBodyRef}
contentEditable="true"
onInput={handleInput}
onClick={handleComposeAreaClick}
className="flex-1 w-full bg-white border border-gray-300 rounded-md p-4 text-black overflow-y-auto focus:outline-none focus:ring-1 focus:ring-blue-500"
style={{
direction: 'ltr',
maxHeight: 'calc(100vh - 400px)',
minHeight: '200px',
overflowY: 'auto',
scrollbarWidth: 'thin',
scrollbarColor: '#cbd5e0 #f3f4f6',
cursor: 'text'
}}
dir="ltr"
spellCheck="true"
role="textbox"
aria-multiline="true"
tabIndex={0}
suppressContentEditableWarning={true}
/>
</div>
</div>
</div>
{/* Modal Footer */}
<div className="flex-none flex items-center justify-between px-6 py-3 border-t border-gray-200 bg-white">
<div className="flex items-center gap-2">
{/* File Input for Attachments */}
<input
type="file"
id="file-attachment"
className="hidden"
multiple
onChange={handleFileAttachment}
/>
<label htmlFor="file-attachment">
<Button
variant="outline"
size="icon"
className="rounded-full bg-white hover:bg-gray-100 border-gray-300"
onClick={(e) => {
e.preventDefault();
document.getElementById('file-attachment')?.click();
}}
>
<Paperclip className="h-4 w-4 text-gray-600" />
</Button>
</label>
</div>
<div className="flex items-center gap-3">
<Button
variant="ghost"
className="text-gray-600 hover:text-gray-700 hover:bg-gray-100"
onClick={onCancel}
>
Cancel
</Button>
<Button
className="bg-blue-600 text-white hover:bg-blue-700"
onClick={handleSendEmail}
>
Send
</Button>
</div>
</div>
<Card className="w-full max-w-3xl mx-auto flex flex-col min-h-[60vh] max-h-[80vh]">
<ComposeEmailHeader
type={determineType()}
onClose={() => {
if (onCancel) onCancel();
setShowCompose(false);
}}
/>
<div className="flex-1 overflow-y-auto">
<ComposeEmailForm
to={composeTo}
setTo={setComposeTo}
cc={composeCc}
setCc={setComposeCc}
bcc={composeBcc}
setBcc={setComposeBcc}
subject={composeSubject}
setSubject={setComposeSubject}
emailContent={composeBody}
setEmailContent={setComposeBody}
showCc={showCc}
setShowCc={setShowCc}
showBcc={showBcc}
setShowBcc={setShowBcc}
attachments={convertAttachments()}
onAttachmentAdd={handleFileSelection}
onAttachmentRemove={(index) => {
setAttachments(attachments.filter((_, i) => i !== index));
}}
/>
</div>
</div>
<ComposeEmailFooter
sending={sending}
onSend={handleLegacySend}
onCancel={() => {
if (onCancel) onCancel();
setShowCompose(false);
}}
/>
</Card>
);
}

View File

@ -0,0 +1,53 @@
import React from 'react';
import { SendHorizontal, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
interface ComposeEmailFooterProps {
sending: boolean;
onSend: () => Promise<void>;
onCancel: () => void;
}
export default function ComposeEmailFooter({
sending,
onSend,
onCancel
}: ComposeEmailFooterProps) {
return (
<div className="p-4 border-t border-gray-200 flex justify-between items-center">
<div className="flex space-x-2">
<Button
type="button"
onClick={onSend}
disabled={sending}
className="bg-blue-600 hover:bg-blue-700"
>
{sending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Sending...
</>
) : (
<>
<SendHorizontal className="mr-2 h-4 w-4" />
Send
</>
)}
</Button>
<Button
type="button"
variant="outline"
onClick={onCancel}
disabled={sending}
>
Cancel
</Button>
</div>
<div className="text-xs text-gray-500">
{sending ? 'Sending your email...' : 'Ready to send'}
</div>
</div>
);
}

View File

@ -0,0 +1,227 @@
import React, { useState } from 'react';
import { ChevronDown, ChevronUp, Paperclip, X } from 'lucide-react';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
interface ComposeEmailFormProps {
to: string;
setTo: (value: string) => void;
cc: string;
setCc: (value: string) => void;
bcc: string;
setBcc: (value: string) => void;
subject: string;
setSubject: (value: string) => void;
emailContent: string;
setEmailContent: (value: string) => void;
showCc: boolean;
setShowCc: (value: boolean) => void;
showBcc: boolean;
setShowBcc: (value: boolean) => void;
attachments: Array<{
name: string;
content: string;
type: string;
}>;
onAttachmentAdd: (files: FileList) => void;
onAttachmentRemove: (index: number) => void;
}
export default function ComposeEmailForm({
to,
setTo,
cc,
setCc,
bcc,
setBcc,
subject,
setSubject,
emailContent,
setEmailContent,
showCc,
setShowCc,
showBcc,
setShowBcc,
attachments,
onAttachmentAdd,
onAttachmentRemove
}: ComposeEmailFormProps) {
const fileInputRef = React.useRef<HTMLInputElement>(null);
const handleAttachmentClick = () => {
fileInputRef.current?.click();
};
const handleFileSelection = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
onAttachmentAdd(e.target.files);
}
// Reset the input value so the same file can be selected again
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
return (
<div className="p-4 space-y-4">
<div className="space-y-3">
{/* To field */}
<div className="flex items-center">
<Label htmlFor="to" className="w-16 flex-shrink-0">To:</Label>
<Input
id="to"
value={to}
onChange={(e) => setTo(e.target.value)}
placeholder="Email address..."
className="flex-grow"
/>
</div>
{/* CC field - conditionally shown */}
{showCc && (
<div className="flex items-center">
<Label htmlFor="cc" className="w-16 flex-shrink-0">Cc:</Label>
<Input
id="cc"
value={cc}
onChange={(e) => setCc(e.target.value)}
placeholder="CC address..."
className="flex-grow"
/>
</div>
)}
{/* BCC field - conditionally shown */}
{showBcc && (
<div className="flex items-center">
<Label htmlFor="bcc" className="w-16 flex-shrink-0">Bcc:</Label>
<Input
id="bcc"
value={bcc}
onChange={(e) => setBcc(e.target.value)}
placeholder="BCC address..."
className="flex-grow"
/>
</div>
)}
{/* CC/BCC toggle buttons */}
<div className="flex items-center space-x-2 ml-16">
{!showCc && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setShowCc(true)}
className="h-6 px-2 text-xs text-gray-500 hover:text-gray-700"
>
Add Cc <ChevronDown className="ml-1 h-3 w-3" />
</Button>
)}
{!showBcc && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setShowBcc(true)}
className="h-6 px-2 text-xs text-gray-500 hover:text-gray-700"
>
Add Bcc <ChevronDown className="ml-1 h-3 w-3" />
</Button>
)}
{showCc && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setShowCc(false)}
className="h-6 px-2 text-xs text-gray-500 hover:text-gray-700"
>
Remove Cc <ChevronUp className="ml-1 h-3 w-3" />
</Button>
)}
{showBcc && (
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setShowBcc(false)}
className="h-6 px-2 text-xs text-gray-500 hover:text-gray-700"
>
Remove Bcc <ChevronUp className="ml-1 h-3 w-3" />
</Button>
)}
</div>
{/* Subject field */}
<div className="flex items-center">
<Label htmlFor="subject" className="w-16 flex-shrink-0">Subject:</Label>
<Input
id="subject"
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder="Email subject..."
className="flex-grow"
/>
</div>
</div>
{/* Email content */}
<Textarea
value={emailContent}
onChange={(e) => setEmailContent(e.target.value)}
placeholder="Write your message here..."
className="w-full min-h-[200px]"
/>
{/* Attachments */}
{attachments.length > 0 && (
<div className="border rounded-md p-2">
<h3 className="text-sm font-medium mb-2">Attachments</h3>
<div className="space-y-2">
{attachments.map((file, index) => (
<div key={index} className="flex items-center justify-between text-sm border rounded p-2">
<span className="truncate max-w-[200px]">{file.name}</span>
<Button
variant="ghost"
size="sm"
onClick={() => onAttachmentRemove(index)}
className="h-6 w-6 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
</div>
)}
{/* Attachment input (hidden) */}
<input
type="file"
multiple
ref={fileInputRef}
onChange={handleFileSelection}
className="hidden"
/>
{/* Attachments button */}
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAttachmentClick}
className="flex items-center"
>
<Paperclip className="mr-2 h-4 w-4" />
Attach Files
</Button>
</div>
);
}

View File

@ -0,0 +1,41 @@
import React from 'react';
import { X } from 'lucide-react';
import { Button } from '@/components/ui/button';
interface ComposeEmailHeaderProps {
type: 'new' | 'reply' | 'reply-all' | 'forward';
onClose: () => void;
}
export default function ComposeEmailHeader({
type,
onClose
}: ComposeEmailHeaderProps) {
// Set the header title based on the compose type
const getTitle = () => {
switch (type) {
case 'reply':
return 'Reply';
case 'reply-all':
return 'Reply All';
case 'forward':
return 'Forward';
default:
return 'New Message';
}
};
return (
<div className="flex items-center justify-between p-4 border-b border-gray-200">
<h2 className="text-lg font-semibold">{getTitle()}</h2>
<Button
variant="ghost"
size="icon"
onClick={onClose}
className="h-8 w-8"
>
<X className="h-4 w-4" />
</Button>
</div>
);
}

View File

@ -0,0 +1,163 @@
import React from 'react';
import {
ChevronLeft, Reply, ReplyAll, Forward, Star, MoreHorizontal
} from 'lucide-react';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import { Email } from '@/hooks/use-courrier';
interface EmailDetailViewProps {
email: Email;
onBack: () => void;
onReply: () => void;
onReplyAll: () => void;
onForward: () => void;
onToggleStar: () => void;
}
export default function EmailDetailView({
email,
onBack,
onReply,
onReplyAll,
onForward,
onToggleStar
}: EmailDetailViewProps) {
// Format date for display
const formatDate = (dateString: string) => {
const date = new Date(dateString);
const now = new Date();
if (date.toDateString() === now.toDateString()) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} else {
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
}
};
// Render email content based on the email body
const renderEmailContent = () => {
try {
// For simple rendering in this example, we'll just display the content directly
return <div dangerouslySetInnerHTML={{ __html: email.content || '' }} />;
} catch (e) {
console.error('Error rendering email:', e);
return <div className="text-gray-500">Failed to render email content</div>;
}
};
return (
<>
{/* Email actions header */}
<div className="flex-none px-4 py-3 border-b border-gray-100">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 min-w-0 flex-1">
<Button
variant="ghost"
size="icon"
onClick={onBack}
className="md:hidden flex-shrink-0"
>
<ChevronLeft className="h-5 w-5" />
</Button>
<div className="min-w-0 max-w-[500px]">
<h2 className="text-lg font-semibold text-gray-900 truncate">
{email.subject}
</h2>
</div>
</div>
<div className="flex items-center gap-1 flex-shrink-0 ml-auto">
<div className="flex items-center border-l border-gray-200 pl-4">
<Button
variant="ghost"
size="icon"
className="text-gray-400 hover:text-gray-900 h-9 w-9"
onClick={onReply}
>
<Reply className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="text-gray-400 hover:text-gray-900 h-9 w-9"
onClick={onReplyAll}
>
<ReplyAll className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="text-gray-400 hover:text-gray-900 h-9 w-9"
onClick={onForward}
>
<Forward className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="text-gray-400 hover:text-gray-900 h-9 w-9"
onClick={onToggleStar}
>
<Star className={`h-4 w-4 ${email.starred ? 'fill-yellow-400 text-yellow-400' : ''}`} />
</Button>
</div>
</div>
</div>
</div>
{/* Scrollable content area */}
<ScrollArea className="flex-1 p-6">
<div className="flex items-center gap-4 mb-6">
<Avatar className="h-10 w-10">
<AvatarFallback>
{(email.from?.[0]?.name || '').charAt(0) || (email.from?.[0]?.address || '').charAt(0) || '?'}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className="font-medium text-gray-900">
{email.from?.[0]?.name || ''} <span className="text-gray-500">&lt;{email.from?.[0]?.address || ''}&gt;</span>
</p>
<p className="text-sm text-gray-500">
to {email.to?.[0]?.address || ''}
</p>
{email.cc && email.cc.length > 0 && (
<p className="text-sm text-gray-500">
cc {email.cc.map(c => c.address).join(', ')}
</p>
)}
</div>
<div className="text-sm text-gray-500 whitespace-nowrap">
{formatDate(email.date)}
</div>
</div>
{/* Email content */}
<div className="prose prose-sm max-w-none">
{renderEmailContent()}
</div>
{/* Attachments (if any) */}
{email.hasAttachments && email.attachments && email.attachments.length > 0 && (
<div className="mt-6 border-t border-gray-100 pt-4">
<h3 className="text-sm font-medium text-gray-900 mb-2">Attachments</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{email.attachments.map((attachment, idx) => (
<div
key={idx}
className="flex items-center gap-2 p-2 border border-gray-200 rounded-md"
>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-700 truncate">{attachment.filename}</p>
<p className="text-xs text-gray-500">{(attachment.size / 1024).toFixed(1)} KB</p>
</div>
</div>
))}
</div>
</div>
)}
</ScrollArea>
</>
);
}

View File

@ -0,0 +1,74 @@
import React from 'react';
import { AlertCircle } from 'lucide-react';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
interface DeleteConfirmDialogProps {
show: boolean;
selectedCount: number;
onConfirm: () => Promise<void>;
onCancel: () => void;
}
export function DeleteConfirmDialog({
show,
selectedCount,
onConfirm,
onCancel
}: DeleteConfirmDialogProps) {
return (
<AlertDialog open={show} onOpenChange={(open) => !open && onCancel()}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete {selectedCount} email{selectedCount !== 1 ? 's' : ''}?</AlertDialogTitle>
<AlertDialogDescription>
This will move the selected email{selectedCount !== 1 ? 's' : ''} to the trash folder.
You can restore them later from the trash folder if needed.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={onCancel}>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm}>Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
interface LoginNeededAlertProps {
show: boolean;
onLogin: () => void;
onClose: () => void;
}
export function LoginNeededAlert({
show,
onLogin,
onClose
}: LoginNeededAlertProps) {
if (!show) return null;
return (
<Alert className="mb-4">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Please log in to your email account</AlertTitle>
<AlertDescription>
You need to connect your email account before you can access your emails.
</AlertDescription>
<div className="mt-2 flex gap-2">
<Button size="sm" onClick={onLogin}>Go to Login</Button>
<Button size="sm" variant="outline" onClick={onClose}>Dismiss</Button>
</div>
</Alert>
);
}

View File

@ -90,13 +90,22 @@ export default function EmailPanel({
hasAttachments: email.hasAttachments || false
};
// Use the formatter to get properly formatted content
const { content } = formatReplyEmail(formatterEmail, 'reply');
// Try both formatting approaches to match what ComposeEmail would display
// This handles preview, reply and forward cases
let formattedContent: string;
// ComposeEmail switches based on type - we need to do the same
const { content: replyContent } = formatReplyEmail(formatterEmail, 'reply');
// Set the formatted content
formattedContent = replyContent;
console.log("Generated formatted content for email preview");
// Return a new email object with the formatted content
return {
...email,
formattedContent: content
formattedContent
};
} catch (error) {
console.error('Error formatting email content:', error);

View File

@ -1,13 +1,16 @@
'use client';
import { useState, useRef, useEffect } from 'react';
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,
EmailMessage as FormatterEmailMessage
formatForwardedEmail,
formatEmailForReplyOrForward,
EmailMessage as FormatterEmailMessage,
sanitizeHtml
} from '@/lib/utils/email-formatter';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { AvatarImage } from '@/components/ui/avatar';
@ -93,14 +96,49 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
// Get sender initials for avatar
const getSenderInitials = (name: string) => {
if (!name) return '';
return name
.split(" ")
.map((n) => n[0])
.map((n) => n?.[0] || '')
.join("")
.toUpperCase()
.slice(0, 2);
};
// Format the email content using the same formatter as ComposeEmail
const formattedContent = useMemo(() => {
if (!email) return '';
try {
// Convert to the formatter message format - same as what ComposeEmail does
const formatterEmail: FormatterEmailMessage = {
id: email.id,
messageId: email.messageId,
subject: email.subject,
from: email.from || [],
to: email.to || [],
cc: email.cc || [],
bcc: email.bcc || [],
date: email.date instanceof Date ? email.date : new Date(email.date),
content: email.content || '',
html: email.html,
text: email.text,
hasAttachments: email.hasAttachments || false
};
// Get the formatted content - if already formatted content is provided, use that instead
if (email.formattedContent) {
return email.formattedContent;
}
// Otherwise sanitize the content for display
return sanitizeHtml(email.content || email.html || email.text || '');
} catch (error) {
console.error('Error formatting email content:', error);
return email.content || email.html || email.text || '';
}
}, [email]);
// Display loading state
if (loading) {
return (
@ -126,10 +164,6 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
const sender = email.from && email.from.length > 0 ? email.from[0] : undefined;
// Use the directly formatted content provided by the parent component
// This is now exactly the same content that ComposeEmail would use
const displayContent = email.formattedContent || email.content || email.html || email.text || '';
return (
<Card className="flex flex-col h-full overflow-hidden border-0 shadow-none">
{/* Email header */}
@ -215,7 +249,7 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
ref={editorRef}
contentEditable={false}
className="w-full p-4 min-h-[300px] focus:outline-none email-content-display"
dangerouslySetInnerHTML={{ __html: displayContent }}
dangerouslySetInnerHTML={{ __html: formattedContent }}
dir="rtl"
style={{
textAlign: 'right',

View File

@ -0,0 +1,68 @@
import React from 'react';
import {
Inbox, Send, Star, Trash, Folder,
AlertOctagon, Archive, Edit
} from 'lucide-react';
import { Button } from '@/components/ui/button';
interface EmailSidebarContentProps {
mailboxes: string[];
currentFolder: string;
onFolderChange: (folder: string) => void;
}
export default function EmailSidebarContent({
mailboxes,
currentFolder,
onFolderChange
}: EmailSidebarContentProps) {
// Helper to format folder names
const formatFolderName = (folder: string) => {
return folder.charAt(0).toUpperCase() + folder.slice(1).toLowerCase();
};
// Helper to get folder icons
const getFolderIcon = (folder: string) => {
const folderLower = folder.toLowerCase();
if (folderLower.includes('inbox')) {
return <Inbox className="h-4 w-4" />;
} else if (folderLower.includes('sent')) {
return <Send className="h-4 w-4" />;
} else if (folderLower.includes('trash')) {
return <Trash className="h-4 w-4" />;
} else if (folderLower.includes('archive')) {
return <Archive className="h-4 w-4" />;
} else if (folderLower.includes('draft')) {
return <Edit className="h-4 w-4" />;
} else if (folderLower.includes('spam') || folderLower.includes('junk')) {
return <AlertOctagon className="h-4 w-4" />;
} else {
return <Folder className="h-4 w-4" />;
}
};
return (
<nav className="p-3">
<ul className="space-y-0.5 px-2">
{mailboxes.map((folder) => (
<li key={folder}>
<Button
variant={currentFolder === folder ? 'secondary' : 'ghost'}
className={`w-full justify-start py-2 ${
currentFolder === folder ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900'
}`}
onClick={() => onFolderChange(folder)}
>
<div className="flex items-center">
{getFolderIcon(folder)}
<span className="ml-2">{formatFolderName(folder)}</span>
</div>
</Button>
</li>
))}
</ul>
</nav>
);
}