courrier refactor rebuild 2
This commit is contained in:
parent
3c738f179a
commit
c993fe738e
@ -29,9 +29,10 @@ import { Button } from '@/components/ui/button';
|
|||||||
// Import components
|
// Import components
|
||||||
import EmailSidebar from '@/components/email/EmailSidebar';
|
import EmailSidebar from '@/components/email/EmailSidebar';
|
||||||
import EmailList from '@/components/email/EmailList';
|
import EmailList from '@/components/email/EmailList';
|
||||||
import EmailContent from '@/components/email/EmailContent';
|
import EmailSidebarContent from '@/components/email/EmailSidebarContent';
|
||||||
import EmailHeader from '@/components/email/EmailHeader';
|
import EmailDetailView from '@/components/email/EmailDetailView';
|
||||||
import ComposeEmail from '@/components/email/ComposeEmail';
|
import ComposeEmail from '@/components/email/ComposeEmail';
|
||||||
|
import { DeleteConfirmDialog, LoginNeededAlert } from '@/components/email/EmailDialogs';
|
||||||
|
|
||||||
// Import the custom hook
|
// Import the custom hook
|
||||||
import { useCourrier, EmailData } from '@/hooks/use-courrier';
|
import { useCourrier, EmailData } from '@/hooks/use-courrier';
|
||||||
@ -89,35 +90,13 @@ export default function CourrierPage() {
|
|||||||
setPage,
|
setPage,
|
||||||
} = useCourrier();
|
} = useCourrier();
|
||||||
|
|
||||||
// Local state
|
// UI state
|
||||||
const [showComposeModal, setShowComposeModal] = useState(false);
|
const [showComposeModal, setShowComposeModal] = useState(false);
|
||||||
const [composeData, setComposeData] = useState<EmailData | null>(null);
|
|
||||||
const [composeType, setComposeType] = useState<'new' | 'reply' | 'reply-all' | 'forward'>('new');
|
const [composeType, setComposeType] = useState<'new' | 'reply' | 'reply-all' | 'forward'>('new');
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
const [showLoginNeeded, setShowLoginNeeded] = useState(false);
|
const [showLoginNeeded, setShowLoginNeeded] = useState(false);
|
||||||
|
|
||||||
// States to match the provided implementation
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
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
|
// Check for more emails
|
||||||
const hasMoreEmails = page < totalPages;
|
const hasMoreEmails = page < totalPages;
|
||||||
@ -158,77 +137,33 @@ export default function CourrierPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle email reply or forward
|
// Handle email actions
|
||||||
const handleReplyOrForward = (type: 'reply' | 'reply-all' | 'forward') => {
|
const handleReply = () => {
|
||||||
if (!selectedEmail) return;
|
if (!selectedEmail) return;
|
||||||
|
setComposeType('reply');
|
||||||
const formattedEmail = formatEmailForAction(selectedEmail, type);
|
setShowComposeModal(true);
|
||||||
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');
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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 = () => {
|
const handleComposeNew = () => {
|
||||||
setComposeTo('');
|
setComposeType('new');
|
||||||
setComposeCc('');
|
setShowComposeModal(true);
|
||||||
setComposeBcc('');
|
|
||||||
setComposeSubject('');
|
|
||||||
setComposeBody('');
|
|
||||||
setShowCc(false);
|
|
||||||
setShowBcc(false);
|
|
||||||
setIsReplying(false);
|
|
||||||
setIsForwarding(false);
|
|
||||||
setShowCompose(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle sending email
|
// Handle sending email
|
||||||
const handleSend = async () => {
|
const handleSendEmail = async (emailData: EmailData) => {
|
||||||
if (!composeTo) {
|
return await sendEmail(emailData);
|
||||||
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.');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle delete confirmation
|
// Handle delete confirmation
|
||||||
@ -254,406 +189,115 @@ export default function CourrierPage() {
|
|||||||
router.push('/courrier/login');
|
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"><{selectedEmail.from?.[0]?.address || ''}></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 (
|
return (
|
||||||
<>
|
<div className="flex h-screen flex-col">
|
||||||
|
{/* Loading Fix for development */}
|
||||||
<SimplifiedLoadingFix />
|
<SimplifiedLoadingFix />
|
||||||
|
|
||||||
{/* Login required dialog */}
|
{/* Login needed alert */}
|
||||||
<AlertDialog open={showLoginNeeded} onOpenChange={setShowLoginNeeded}>
|
<LoginNeededAlert
|
||||||
<AlertDialogContent>
|
show={showLoginNeeded}
|
||||||
<AlertDialogHeader>
|
onLogin={handleGoToLogin}
|
||||||
<AlertDialogTitle>Login Required</AlertDialogTitle>
|
onClose={() => setShowLoginNeeded(false)}
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* Main layout */}
|
{/* Main Content */}
|
||||||
<main className="w-full h-screen bg-black">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
<div className="w-full h-full px-4 pt-12 pb-4">
|
{/* Sidebar (Desktop) */}
|
||||||
<div className="flex h-full bg-carnet-bg">
|
{sidebarOpen && (
|
||||||
{/* Sidebar */}
|
<aside className="hidden md:flex md:w-64 bg-gray-50 border-r border-gray-200 flex-col">
|
||||||
<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
|
{/* Account switching and compose button */}
|
||||||
${mobileSidebarOpen ? 'fixed inset-y-0 left-0 z-40' : 'hidden'} md:block`}>
|
<div className="px-4 py-3 border-b border-gray-200">
|
||||||
{/* Courrier Title */}
|
<Button
|
||||||
<div className="p-3 border-b border-gray-100">
|
className="w-full bg-blue-600 hover:bg-blue-700"
|
||||||
<div className="flex items-center gap-2">
|
onClick={handleComposeNew}
|
||||||
<Mail className="h-6 w-6 text-gray-600" />
|
>
|
||||||
<span className="text-xl font-semibold text-gray-900">COURRIER</span>
|
<PlusIcon className="mr-2 h-4 w-4" />
|
||||||
</div>
|
Compose
|
||||||
</div>
|
</Button>
|
||||||
|
</div>
|
||||||
{/* Compose button and refresh button */}
|
|
||||||
<div className="p-2 border-b border-gray-100 flex items-center gap-2">
|
{/* Folder Navigation */}
|
||||||
<Button
|
<EmailSidebarContent
|
||||||
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"
|
mailboxes={mailboxes}
|
||||||
onClick={() => {
|
currentFolder={currentFolder}
|
||||||
setShowCompose(true);
|
onFolderChange={changeFolder}
|
||||||
setComposeTo('');
|
/>
|
||||||
setComposeCc('');
|
</aside>
|
||||||
setComposeBcc('');
|
)}
|
||||||
setComposeSubject('');
|
|
||||||
setComposeBody('');
|
{/* Email List and Content View */}
|
||||||
setShowCc(false);
|
<div className="flex-1 flex">
|
||||||
setShowBcc(false);
|
{/* Email List */}
|
||||||
}}
|
<EmailList
|
||||||
>
|
emails={emails}
|
||||||
<div className="flex items-center gap-2">
|
selectedEmailIds={selectedEmailIds}
|
||||||
<PlusIcon className="h-3.5 w-3.5" />
|
selectedEmail={selectedEmail}
|
||||||
<span>Compose</span>
|
currentFolder={currentFolder}
|
||||||
</div>
|
isLoading={isLoading}
|
||||||
</Button>
|
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
|
<Button
|
||||||
variant="ghost"
|
className="mt-6 bg-blue-600 hover:bg-blue-700"
|
||||||
size="icon"
|
onClick={handleComposeNew}
|
||||||
onClick={() => handleMailboxChange('INBOX')}
|
|
||||||
className="text-gray-600 hover:text-gray-900 hover:bg-gray-100"
|
|
||||||
>
|
>
|
||||||
<RefreshCw className="h-4 w-4" />
|
<PlusIcon className="mr-2 h-4 w-4" />
|
||||||
|
Compose New
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{/* Accounts Section */}
|
|
||||||
<div className="p-3 border-b border-gray-100">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="w-full justify-between mb-2 text-sm font-medium text-gray-500"
|
|
||||||
onClick={() => setAccountsDropdownOpen(!accountsDropdownOpen)}
|
|
||||||
>
|
|
||||||
<span>Accounts</span>
|
|
||||||
{accountsDropdownOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{accountsDropdownOpen && (
|
|
||||||
<div className="space-y-1 pl-2">
|
|
||||||
{accounts.map(account => (
|
|
||||||
<div key={account.id} className="relative group">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
className="w-full justify-between px-2 py-1.5 text-sm group"
|
|
||||||
onClick={() => setSelectedAccount(account)}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-start">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className={`w-2.5 h-2.5 rounded-full ${account.color}`}></div>
|
|
||||||
<span className="font-medium">{account.name}</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-gray-500 ml-4">{account.email}</span>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Navigation */}
|
|
||||||
{renderSidebarNav()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main content area */}
|
|
||||||
<div className="flex-1 flex overflow-hidden">
|
|
||||||
{/* Email list panel */}
|
|
||||||
{renderEmailListWrapper()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</div>
|
||||||
|
|
||||||
{/* Compose Email Modal */}
|
{/* Compose Modal */}
|
||||||
<ComposeEmail
|
<Dialog open={showComposeModal} onOpenChange={setShowComposeModal}>
|
||||||
showCompose={showCompose}
|
<DialogContent className="max-w-4xl p-0">
|
||||||
setShowCompose={setShowCompose}
|
<ComposeEmail
|
||||||
composeTo={composeTo}
|
initialEmail={selectedEmail}
|
||||||
setComposeTo={setComposeTo}
|
type={composeType}
|
||||||
composeCc={composeCc}
|
onClose={() => setShowComposeModal(false)}
|
||||||
setComposeCc={setComposeCc}
|
onSend={async (emailData: EmailData) => {
|
||||||
composeBcc={composeBcc}
|
await sendEmail(emailData);
|
||||||
setComposeBcc={setComposeBcc}
|
}}
|
||||||
composeSubject={composeSubject}
|
/>
|
||||||
setComposeSubject={setComposeSubject}
|
</DialogContent>
|
||||||
composeBody={composeBody}
|
</Dialog>
|
||||||
setComposeBody={setComposeBody}
|
|
||||||
showCc={showCc}
|
{/* Delete Confirmation Dialog */}
|
||||||
setShowCc={setShowCc}
|
<DeleteConfirmDialog
|
||||||
showBcc={showBcc}
|
show={showDeleteConfirm}
|
||||||
setShowBcc={setShowBcc}
|
selectedCount={selectedEmailIds.length}
|
||||||
attachments={attachments}
|
onConfirm={handleDeleteConfirm}
|
||||||
setAttachments={setAttachments}
|
onCancel={() => setShowDeleteConfirm(false)}
|
||||||
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);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
{renderDeleteConfirmDialog()}
|
</div>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -11,6 +11,11 @@ import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/componen
|
|||||||
import DOMPurify from 'isomorphic-dompurify';
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
import { Label } from '@/components/ui/label';
|
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 ONLY from the centralized formatter
|
||||||
import {
|
import {
|
||||||
formatForwardedEmail,
|
formatForwardedEmail,
|
||||||
@ -148,10 +153,6 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
|
|||||||
type: string;
|
type: string;
|
||||||
}>>([]);
|
}>>([]);
|
||||||
|
|
||||||
// Refs
|
|
||||||
const editorRef = useRef<HTMLDivElement>(null);
|
|
||||||
const attachmentInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
// Initialize the form when replying to or forwarding an email
|
// Initialize the form when replying to or forwarding an email
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (initialEmail && type !== 'new') {
|
if (initialEmail && type !== 'new') {
|
||||||
@ -187,148 +188,47 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
|
|||||||
setSubject(subject);
|
setSubject(subject);
|
||||||
setEmailContent(content);
|
setEmailContent(content);
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
// For type safety
|
console.error('Error formatting email for reply/forward:', err);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [initialEmail, type]);
|
}, [initialEmail, type]);
|
||||||
|
|
||||||
// Handle attachment selection
|
// Handle file attachments
|
||||||
const handleAttachmentClick = () => {
|
const handleAttachmentAdd = async (files: FileList) => {
|
||||||
attachmentInputRef.current?.click();
|
const newAttachments = Array.from(files).map(file => ({
|
||||||
};
|
name: file.name,
|
||||||
|
type: file.type,
|
||||||
// Process selected files
|
content: URL.createObjectURL(file)
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset file input
|
setAttachments(prev => [...prev, ...newAttachments]);
|
||||||
if (e.target) {
|
|
||||||
e.target.value = '';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Remove attachment
|
const handleAttachmentRemove = (index: number) => {
|
||||||
const removeAttachment = (index: number) => {
|
setAttachments(prev => prev.filter((_, i) => i !== index));
|
||||||
setAttachments(current => current.filter((_, i) => i !== index));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle editor input
|
// Handle sending email
|
||||||
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
|
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
if (!to) {
|
if (!to) {
|
||||||
alert('Please specify at least one recipient');
|
alert('Please specify at least one recipient');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSending(true);
|
||||||
|
|
||||||
try {
|
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({
|
await onSend({
|
||||||
to,
|
to,
|
||||||
cc: cc || undefined,
|
cc: cc || undefined,
|
||||||
bcc: bcc || undefined,
|
bcc: bcc || undefined,
|
||||||
subject,
|
subject,
|
||||||
body: finalContent,
|
body: emailContent,
|
||||||
attachments
|
attachments
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Reset form and close
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending email:', error);
|
console.error('Error sending email:', error);
|
||||||
@ -337,201 +237,46 @@ export default function ComposeEmail(props: ComposeEmailAllProps) {
|
|||||||
setSending(false);
|
setSending(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full max-w-4xl mx-auto">
|
<Card className="w-full max-w-3xl mx-auto flex flex-col min-h-[60vh] max-h-[80vh]">
|
||||||
<CardHeader>
|
<ComposeEmailHeader
|
||||||
<CardTitle className="flex items-center justify-between">
|
type={type}
|
||||||
<span>{type === 'new' ? 'New Message' : type === 'forward' ? 'Forward Email' : 'Reply to Email'}</span>
|
onClose={onClose}
|
||||||
<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>
|
|
||||||
|
|
||||||
<CardFooter className="border-t p-3 flex justify-between">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<div>
|
<ComposeEmailForm
|
||||||
<input
|
to={to}
|
||||||
type="file"
|
setTo={setTo}
|
||||||
ref={attachmentInputRef}
|
cc={cc}
|
||||||
className="hidden"
|
setCc={setCc}
|
||||||
onChange={handleFileSelection}
|
bcc={bcc}
|
||||||
multiple
|
setBcc={setBcc}
|
||||||
/>
|
subject={subject}
|
||||||
<Button
|
setSubject={setSubject}
|
||||||
variant="outline"
|
emailContent={emailContent}
|
||||||
size="sm"
|
setEmailContent={setEmailContent}
|
||||||
onClick={handleAttachmentClick}
|
showCc={showCc}
|
||||||
>
|
setShowCc={setShowCc}
|
||||||
<Paperclip className="h-4 w-4 mr-1" />
|
showBcc={showBcc}
|
||||||
Attach
|
setShowBcc={setShowBcc}
|
||||||
</Button>
|
attachments={attachments}
|
||||||
</div>
|
onAttachmentAdd={handleAttachmentAdd}
|
||||||
|
onAttachmentRemove={handleAttachmentRemove}
|
||||||
<Button
|
/>
|
||||||
size="sm"
|
</div>
|
||||||
onClick={handleSend}
|
|
||||||
disabled={sending}
|
<ComposeEmailFooter
|
||||||
>
|
sending={sending}
|
||||||
{sending ? (
|
onSend={handleSend}
|
||||||
<>
|
onCancel={onClose}
|
||||||
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
/>
|
||||||
Sending...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<SendHorizontal className="h-4 w-4 mr-1" />
|
|
||||||
Send
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adapter component for legacy props
|
// Legacy adapter to maintain backward compatibility
|
||||||
function LegacyAdapter({
|
function LegacyAdapter({
|
||||||
showCompose,
|
showCompose,
|
||||||
setShowCompose,
|
setShowCompose,
|
||||||
@ -558,224 +303,113 @@ function LegacyAdapter({
|
|||||||
replyTo,
|
replyTo,
|
||||||
forwardFrom
|
forwardFrom
|
||||||
}: LegacyComposeEmailProps) {
|
}: LegacyComposeEmailProps) {
|
||||||
// Create a ref for the contentEditable div
|
const [sending, setSending] = useState(false);
|
||||||
const composeBodyRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// Set the content of the contentEditable div on mount
|
// Determine the type based on legacy props
|
||||||
useEffect(() => {
|
const determineType = (): 'new' | 'reply' | 'reply-all' | 'forward' => {
|
||||||
if (composeBodyRef.current && composeBody) {
|
if (originalEmail?.type === 'forward') return 'forward';
|
||||||
composeBodyRef.current.innerHTML = composeBody;
|
if (originalEmail?.type === 'reply-all') return 'reply-all';
|
||||||
}
|
if (originalEmail?.type === 'reply') return 'reply';
|
||||||
}, [composeBody, showCompose]);
|
if (replyTo) return 'reply';
|
||||||
|
if (forwardFrom) return 'forward';
|
||||||
// Handle clicking the content editable area
|
return 'new';
|
||||||
const handleComposeAreaClick = () => {
|
|
||||||
composeBodyRef.current?.focus();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle input events on the contentEditable div
|
// Converts attachments to the expected format
|
||||||
const handleInput = () => {
|
const convertAttachments = () => {
|
||||||
if (composeBodyRef.current) {
|
return attachments.map(att => ({
|
||||||
setComposeBody(composeBodyRef.current.innerHTML);
|
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
|
// Handle file selection for legacy interface
|
||||||
const handleFileAttachment = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileSelection = (files: FileList) => {
|
||||||
if (!e.target.files || e.target.files.length === 0) return;
|
const newAttachments = Array.from(files).map(file => ({
|
||||||
|
|
||||||
// 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 => ({
|
|
||||||
name: file.name,
|
name: file.name,
|
||||||
type: file.type,
|
type: file.type,
|
||||||
size: file.size,
|
content: URL.createObjectURL(file),
|
||||||
// In a real implementation you'd use FileReader to read the file content
|
size: file.size
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Add to existing attachments
|
|
||||||
setAttachments([...attachments, ...newAttachments]);
|
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;
|
if (!showCompose) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-gray-600/30 backdrop-blur-sm z-50 flex items-center justify-center">
|
<Card className="w-full max-w-3xl mx-auto flex flex-col min-h-[60vh] max-h-[80vh]">
|
||||||
<div className="w-full max-w-2xl h-[90vh] bg-white rounded-xl shadow-xl flex flex-col mx-4">
|
<ComposeEmailHeader
|
||||||
{/* Modal Header */}
|
type={determineType()}
|
||||||
<div className="flex-none flex items-center justify-between px-6 py-3 border-b border-gray-200">
|
onClose={() => {
|
||||||
<h3 className="text-lg font-semibold text-gray-900">
|
if (onCancel) onCancel();
|
||||||
{replyTo ? 'Reply' : forwardFrom ? 'Forward' : 'New Message'}
|
setShowCompose(false);
|
||||||
</h3>
|
}}
|
||||||
<Button
|
/>
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
<div className="flex-1 overflow-y-auto">
|
||||||
className="hover:bg-gray-100 rounded-full"
|
<ComposeEmailForm
|
||||||
onClick={onCancel}
|
to={composeTo}
|
||||||
>
|
setTo={setComposeTo}
|
||||||
<X className="h-5 w-5 text-gray-500" />
|
cc={composeCc}
|
||||||
</Button>
|
setCc={setComposeCc}
|
||||||
</div>
|
bcc={composeBcc}
|
||||||
|
setBcc={setComposeBcc}
|
||||||
{/* Modal Body */}
|
subject={composeSubject}
|
||||||
<div className="flex-1 overflow-hidden">
|
setSubject={setComposeSubject}
|
||||||
<div className="h-full flex flex-col p-6 space-y-4 overflow-y-auto">
|
emailContent={composeBody}
|
||||||
{/* To Field */}
|
setEmailContent={setComposeBody}
|
||||||
<div className="flex-none">
|
showCc={showCc}
|
||||||
<Label htmlFor="to" className="block text-sm font-medium text-gray-700">To</Label>
|
setShowCc={setShowCc}
|
||||||
<Input
|
showBcc={showBcc}
|
||||||
id="to"
|
setShowBcc={setShowBcc}
|
||||||
value={composeTo}
|
attachments={convertAttachments()}
|
||||||
onChange={(e) => setComposeTo(e.target.value)}
|
onAttachmentAdd={handleFileSelection}
|
||||||
placeholder="recipient@example.com"
|
onAttachmentRemove={(index) => {
|
||||||
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
|
setAttachments(attachments.filter((_, i) => i !== index));
|
||||||
/>
|
}}
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<ComposeEmailFooter
|
||||||
|
sending={sending}
|
||||||
|
onSend={handleLegacySend}
|
||||||
|
onCancel={() => {
|
||||||
|
if (onCancel) onCancel();
|
||||||
|
setShowCompose(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
53
components/email/ComposeEmailFooter.tsx
Normal file
53
components/email/ComposeEmailFooter.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
227
components/email/ComposeEmailForm.tsx
Normal file
227
components/email/ComposeEmailForm.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
components/email/ComposeEmailHeader.tsx
Normal file
41
components/email/ComposeEmailHeader.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
163
components/email/EmailDetailView.tsx
Normal file
163
components/email/EmailDetailView.tsx
Normal 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"><{email.from?.[0]?.address || ''}></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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
74
components/email/EmailDialogs.tsx
Normal file
74
components/email/EmailDialogs.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -90,13 +90,22 @@ export default function EmailPanel({
|
|||||||
hasAttachments: email.hasAttachments || false
|
hasAttachments: email.hasAttachments || false
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use the formatter to get properly formatted content
|
// Try both formatting approaches to match what ComposeEmail would display
|
||||||
const { content } = formatReplyEmail(formatterEmail, 'reply');
|
// 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 a new email object with the formatted content
|
||||||
return {
|
return {
|
||||||
...email,
|
...email,
|
||||||
formattedContent: content
|
formattedContent
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error formatting email content:', error);
|
console.error('Error formatting email content:', error);
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect, useMemo } from 'react';
|
||||||
import { Loader2, Paperclip, User } from 'lucide-react';
|
import { Loader2, Paperclip, User } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import {
|
import {
|
||||||
formatReplyEmail,
|
formatReplyEmail,
|
||||||
EmailMessage as FormatterEmailMessage
|
formatForwardedEmail,
|
||||||
|
formatEmailForReplyOrForward,
|
||||||
|
EmailMessage as FormatterEmailMessage,
|
||||||
|
sanitizeHtml
|
||||||
} from '@/lib/utils/email-formatter';
|
} from '@/lib/utils/email-formatter';
|
||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||||
import { AvatarImage } 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
|
// Get sender initials for avatar
|
||||||
const getSenderInitials = (name: string) => {
|
const getSenderInitials = (name: string) => {
|
||||||
|
if (!name) return '';
|
||||||
return name
|
return name
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.map((n) => n[0])
|
.map((n) => n?.[0] || '')
|
||||||
.join("")
|
.join("")
|
||||||
.toUpperCase()
|
.toUpperCase()
|
||||||
.slice(0, 2);
|
.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
|
// Display loading state
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
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;
|
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 (
|
return (
|
||||||
<Card className="flex flex-col h-full overflow-hidden border-0 shadow-none">
|
<Card className="flex flex-col h-full overflow-hidden border-0 shadow-none">
|
||||||
{/* Email header */}
|
{/* Email header */}
|
||||||
@ -215,7 +249,7 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
|
|||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
contentEditable={false}
|
contentEditable={false}
|
||||||
className="w-full p-4 min-h-[300px] focus:outline-none email-content-display"
|
className="w-full p-4 min-h-[300px] focus:outline-none email-content-display"
|
||||||
dangerouslySetInnerHTML={{ __html: displayContent }}
|
dangerouslySetInnerHTML={{ __html: formattedContent }}
|
||||||
dir="rtl"
|
dir="rtl"
|
||||||
style={{
|
style={{
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
|
|||||||
68
components/email/EmailSidebarContent.tsx
Normal file
68
components/email/EmailSidebarContent.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user