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 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"><{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 (
|
||||
<>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
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
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
@ -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',
|
||||
|
||||
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