781 lines
31 KiB
TypeScript
781 lines
31 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useSession } from 'next-auth/react';
|
|
import {
|
|
Mail, Loader2, AlertCircle,
|
|
MoreVertical, Settings, Plus as PlusIcon, Trash2, Edit,
|
|
Inbox, Send, Star, Trash, Plus, ChevronLeft, ChevronRight,
|
|
Search, ChevronDown, Folder, ChevronUp, Reply, Forward, ReplyAll,
|
|
MoreHorizontal, FolderOpen, X, Paperclip, MessageSquare, Copy, EyeOff,
|
|
AlertOctagon, Archive, RefreshCw
|
|
} from 'lucide-react';
|
|
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
import { Label } from '@/components/ui/label';
|
|
import { toast } from '@/components/ui/use-toast';
|
|
|
|
// Import components
|
|
import EmailSidebar from '@/components/email/EmailSidebar';
|
|
import EmailList from '@/components/email/EmailList';
|
|
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';
|
|
|
|
// Import the prefetching function
|
|
import { prefetchFolderEmails } from '@/lib/services/prefetch-service';
|
|
|
|
// Import the RedisCacheStatus component
|
|
import { RedisCacheStatus } from '@/components/debug/RedisCacheStatus';
|
|
|
|
// Simplified version for this component
|
|
function SimplifiedLoadingFix() {
|
|
// In production, don't render anything
|
|
if (process.env.NODE_ENV === 'production') {
|
|
return null;
|
|
}
|
|
|
|
// Simple debugging component
|
|
return (
|
|
<div className="fixed bottom-4 right-4 z-50 p-2 bg-white/80 shadow rounded-lg text-xs">
|
|
Debug: Email app loaded
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface Account {
|
|
id: number;
|
|
name: string;
|
|
email: string;
|
|
color: string;
|
|
folders?: string[];
|
|
}
|
|
|
|
export default function CourrierPage() {
|
|
const router = useRouter();
|
|
const { data: session } = useSession();
|
|
|
|
// Get all the email functionality from the hook
|
|
const {
|
|
emails = [],
|
|
selectedEmail,
|
|
selectedEmailIds,
|
|
currentFolder,
|
|
mailboxes,
|
|
isLoading,
|
|
isSending,
|
|
error,
|
|
searchQuery,
|
|
page,
|
|
totalPages,
|
|
loadEmails,
|
|
handleEmailSelect,
|
|
markEmailAsRead,
|
|
toggleStarred,
|
|
sendEmail,
|
|
deleteEmails,
|
|
toggleEmailSelection,
|
|
toggleSelectAll,
|
|
changeFolder,
|
|
searchEmails,
|
|
formatEmailForAction,
|
|
setPage,
|
|
} = useCourrier();
|
|
|
|
// UI state
|
|
const [showComposeModal, setShowComposeModal] = useState(false);
|
|
const [composeType, setComposeType] = useState<'new' | 'reply' | 'reply-all' | 'forward'>('new');
|
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
const [showLoginNeeded, setShowLoginNeeded] = useState(false);
|
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
|
const [accountsDropdownOpen, setAccountsDropdownOpen] = useState(true);
|
|
const [currentView, setCurrentView] = useState('INBOX');
|
|
const [unreadCount, setUnreadCount] = useState(0);
|
|
const [loading, setLoading] = useState(false);
|
|
const [prefetchStarted, setPrefetchStarted] = useState(false);
|
|
const [showFolders, setShowFolders] = useState(true);
|
|
const [showAddAccountForm, setShowAddAccountForm] = useState(false);
|
|
|
|
// Email accounts for the sidebar
|
|
const [accounts, setAccounts] = useState<Account[]>([
|
|
{ id: 0, name: 'All', email: '', color: 'bg-gray-500' },
|
|
{ id: 1, name: 'Loading...', email: '', color: 'bg-blue-500', folders: mailboxes }
|
|
]);
|
|
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
|
|
|
|
// Update account folders when mailboxes change
|
|
useEffect(() => {
|
|
setAccounts(prev => {
|
|
const updated = [...prev];
|
|
if (updated[1]) {
|
|
updated[1].folders = mailboxes;
|
|
}
|
|
return updated;
|
|
});
|
|
}, [mailboxes]);
|
|
|
|
// Calculate unread count (this would be replaced with actual data in production)
|
|
useEffect(() => {
|
|
// Example: counting unread emails in the inbox
|
|
const unreadInInbox = (emails || []).filter(email => !email.read && currentFolder === 'INBOX').length;
|
|
setUnreadCount(unreadInInbox);
|
|
}, [emails, currentFolder]);
|
|
|
|
// Initialize session and start prefetching
|
|
useEffect(() => {
|
|
// Flag to prevent multiple initialization attempts
|
|
let isMounted = true;
|
|
let initAttempted = false;
|
|
|
|
const initSession = async () => {
|
|
if (initAttempted) return;
|
|
initAttempted = true;
|
|
|
|
try {
|
|
setLoading(true);
|
|
|
|
// First check if Redis is ready before making API calls
|
|
const redisStatus = await fetch('/api/redis/status')
|
|
.then(res => res.json())
|
|
.catch(() => ({ ready: false }));
|
|
|
|
if (!isMounted) return;
|
|
|
|
// Call the session API to check email credentials and start prefetching
|
|
const response = await fetch('/api/courrier/session');
|
|
const data = await response.json();
|
|
|
|
if (!isMounted) return;
|
|
|
|
if (data.authenticated) {
|
|
if (data.hasEmailCredentials) {
|
|
console.log('Session initialized, prefetch status:', data.prefetchStarted ? 'running' : 'not started');
|
|
setPrefetchStarted(Boolean(data.prefetchStarted));
|
|
|
|
// Update the accounts with the actual email address
|
|
if (data.email) {
|
|
setAccounts(prev => {
|
|
const updated = [...prev];
|
|
updated[1] = {
|
|
...updated[1],
|
|
name: data.email,
|
|
email: data.email,
|
|
folders: data.mailboxes || mailboxes
|
|
};
|
|
return updated;
|
|
});
|
|
}
|
|
|
|
// Preload first page of emails for faster initial rendering
|
|
if (session?.user?.id) {
|
|
await loadEmails();
|
|
|
|
// If the user hasn't opened this page recently, trigger a background refresh
|
|
if (data.lastVisit && Date.now() - data.lastVisit > 5 * 60 * 1000) {
|
|
// It's been more than 5 minutes, refresh in background
|
|
try {
|
|
const refreshResponse = await fetch('/api/courrier/refresh', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ folder: currentFolder })
|
|
});
|
|
console.log('Background refresh triggered');
|
|
} catch (error) {
|
|
console.error('Failed to trigger background refresh', error);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// User is authenticated but doesn't have email credentials
|
|
setShowLoginNeeded(true);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error initializing session:', error);
|
|
} finally {
|
|
if (isMounted) {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (session?.user?.id) {
|
|
initSession();
|
|
}
|
|
|
|
return () => {
|
|
isMounted = false;
|
|
};
|
|
}, [session?.user?.id, loadEmails]);
|
|
|
|
// Helper to get folder icons
|
|
const getFolderIcon = (folder: string) => {
|
|
const folderLower = folder.toLowerCase();
|
|
|
|
if (folderLower.includes('inbox')) {
|
|
return Inbox;
|
|
} else if (folderLower.includes('sent')) {
|
|
return Send;
|
|
} else if (folderLower.includes('trash')) {
|
|
return Trash;
|
|
} else if (folderLower.includes('archive')) {
|
|
return Archive;
|
|
} else if (folderLower.includes('draft')) {
|
|
return Edit;
|
|
} else if (folderLower.includes('spam') || folderLower.includes('junk')) {
|
|
return AlertOctagon;
|
|
} else {
|
|
return Folder;
|
|
}
|
|
};
|
|
|
|
// Helper to format folder names
|
|
const formatFolderName = (folder: string) => {
|
|
return folder.charAt(0).toUpperCase() + folder.slice(1).toLowerCase();
|
|
};
|
|
|
|
// Check for more emails
|
|
const hasMoreEmails = page < totalPages;
|
|
|
|
// Handle loading more emails on scroll
|
|
const handleLoadMore = () => {
|
|
if (hasMoreEmails && !isLoading) {
|
|
// Increment the page
|
|
const nextPage = page + 1;
|
|
setPage(nextPage);
|
|
|
|
// Also prefetch additional pages to make scrolling smoother
|
|
if (session?.user?.id) {
|
|
// Prefetch next 2 pages beyond the current next page
|
|
prefetchFolderEmails(session.user.id, currentFolder, 2, nextPage + 1).catch(err => {
|
|
console.error(`Error prefetching additional pages for ${currentFolder}:`, err);
|
|
});
|
|
}
|
|
|
|
// Note: loadEmails will be called automatically due to the page dependency in useEffect
|
|
}
|
|
};
|
|
|
|
// Handle bulk actions
|
|
const handleBulkAction = async (action: 'delete' | 'mark-read' | 'mark-unread' | 'archive') => {
|
|
if (selectedEmailIds.length === 0) return;
|
|
|
|
switch (action) {
|
|
case 'delete':
|
|
setShowDeleteConfirm(true);
|
|
break;
|
|
|
|
case 'mark-read':
|
|
// Mark all selected emails as read
|
|
for (const emailId of selectedEmailIds) {
|
|
await markEmailAsRead(emailId, true);
|
|
}
|
|
break;
|
|
|
|
case 'mark-unread':
|
|
// Mark all selected emails as unread
|
|
for (const emailId of selectedEmailIds) {
|
|
await markEmailAsRead(emailId, false);
|
|
}
|
|
break;
|
|
|
|
case 'archive':
|
|
// Archive functionality would be implemented here
|
|
break;
|
|
}
|
|
};
|
|
|
|
// Handle email actions
|
|
const handleReply = () => {
|
|
if (!selectedEmail) return;
|
|
setComposeType('reply');
|
|
setShowComposeModal(true);
|
|
};
|
|
|
|
const handleReplyAll = () => {
|
|
if (!selectedEmail) return;
|
|
setComposeType('reply-all');
|
|
setShowComposeModal(true);
|
|
};
|
|
|
|
const handleForward = () => {
|
|
if (!selectedEmail) return;
|
|
setComposeType('forward');
|
|
setShowComposeModal(true);
|
|
};
|
|
|
|
const handleComposeNew = () => {
|
|
setComposeType('new');
|
|
setShowComposeModal(true);
|
|
};
|
|
|
|
// Handle mailbox change with prefetching
|
|
const handleMailboxChange = (folder: string) => {
|
|
// Reset to page 1 when changing folders
|
|
setPage(1);
|
|
|
|
// Change folder in the state
|
|
changeFolder(folder);
|
|
setCurrentView(folder);
|
|
|
|
// Start prefetching additional pages for this folder
|
|
if (session?.user?.id && folder) {
|
|
// First two pages are most important - prefetch immediately
|
|
prefetchFolderEmails(session.user.id, folder, 3).catch(err => {
|
|
console.error(`Error prefetching ${folder}:`, err);
|
|
});
|
|
}
|
|
};
|
|
|
|
// Handle sending email
|
|
const handleSendEmail = async (emailData: EmailData) => {
|
|
return await sendEmail(emailData);
|
|
};
|
|
|
|
// Handle delete confirmation
|
|
const handleDeleteConfirm = async () => {
|
|
await deleteEmails(selectedEmailIds);
|
|
setShowDeleteConfirm(false);
|
|
};
|
|
|
|
// Check login on mount
|
|
useEffect(() => {
|
|
// Check if the user is logged in after a short delay
|
|
const timer = setTimeout(() => {
|
|
if (error?.includes('Not authenticated') || error?.includes('No email credentials found')) {
|
|
setShowLoginNeeded(true);
|
|
}
|
|
}, 2000);
|
|
|
|
return () => clearTimeout(timer);
|
|
}, [error]);
|
|
|
|
// Go to login page
|
|
const handleGoToLogin = () => {
|
|
router.push('/courrier/login');
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<SimplifiedLoadingFix />
|
|
<RedisCacheStatus />
|
|
|
|
{/* 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={handleComposeNew}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<PlusIcon className="h-3.5 w-3.5" />
|
|
<span>Compose</span>
|
|
</div>
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-9 w-9 text-gray-400 hover:text-gray-600"
|
|
onClick={() => {
|
|
setLoading(true);
|
|
// Reset to page 1 when manually refreshing
|
|
setPage(1);
|
|
// Trigger a scroll reset
|
|
if (typeof window !== 'undefined') {
|
|
window.dispatchEvent(new CustomEvent('reset-email-scroll'));
|
|
}
|
|
// Load emails
|
|
loadEmails().finally(() => setLoading(false));
|
|
}}
|
|
>
|
|
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Accounts Section */}
|
|
<div className="p-3 border-b border-gray-100">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<Button
|
|
variant="ghost"
|
|
className="w-full justify-between 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>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 w-7 p-0 text-gray-400 hover:text-gray-600"
|
|
onClick={() => setShowAddAccountForm(!showAddAccountForm)}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Form for adding a new account */}
|
|
{showAddAccountForm && (
|
|
<div className="mb-3 p-2 border border-gray-200 rounded-md bg-gray-50">
|
|
<h4 className="text-xs font-medium mb-2 text-gray-700">Add IMAP Account</h4>
|
|
<form onSubmit={async (e) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
|
|
const formData = new FormData(e.currentTarget);
|
|
const newAccount = {
|
|
email: formData.get('email'),
|
|
password: formData.get('password'),
|
|
host: formData.get('host'),
|
|
port: parseInt(formData.get('port') as string),
|
|
secure: formData.get('secure') === 'on',
|
|
display_name: formData.get('display_name') || formData.get('email'),
|
|
color: formData.get('color') || '#0082c9',
|
|
smtp_host: formData.get('smtp_host'),
|
|
smtp_port: formData.get('smtp_port') ? parseInt(formData.get('smtp_port') as string) : undefined,
|
|
smtp_secure: formData.get('smtp_secure') === 'on'
|
|
};
|
|
|
|
try {
|
|
const response = await fetch('/api/courrier/account', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(newAccount)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw new Error(result.error || 'Failed to add account');
|
|
}
|
|
|
|
// Update accounts list
|
|
const newAccountObj = {
|
|
id: Date.now(), // temporary ID
|
|
name: newAccount.display_name as string,
|
|
email: newAccount.email as string,
|
|
color: `bg-blue-500`, // Default color class
|
|
folders: ['INBOX', 'Sent', 'Drafts', 'Trash'] // Default folders
|
|
};
|
|
|
|
setAccounts(prev => [...prev, newAccountObj]);
|
|
setShowAddAccountForm(false);
|
|
toast({
|
|
title: "Account added successfully",
|
|
description: `Your email account ${newAccount.email} has been added.`,
|
|
duration: 5000
|
|
});
|
|
} catch (error) {
|
|
console.error('Error adding account:', error);
|
|
toast({
|
|
title: "Failed to add account",
|
|
description: error instanceof Error ? error.message : 'Unknown error',
|
|
variant: "destructive",
|
|
duration: 5000
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}}>
|
|
<div className="space-y-2">
|
|
<Tabs defaultValue="imap" className="w-full">
|
|
<TabsList className="grid w-full grid-cols-2 h-7">
|
|
<TabsTrigger value="imap" className="text-xs">IMAP Settings</TabsTrigger>
|
|
<TabsTrigger value="smtp" className="text-xs">SMTP Settings</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="imap" className="space-y-2 pt-2">
|
|
<Input
|
|
type="email"
|
|
name="email"
|
|
placeholder="Email address"
|
|
className="h-8 text-xs"
|
|
required
|
|
/>
|
|
<Input
|
|
type="password"
|
|
name="password"
|
|
placeholder="Password"
|
|
className="h-8 text-xs"
|
|
required
|
|
/>
|
|
<Input
|
|
type="text"
|
|
name="display_name"
|
|
placeholder="Display name (optional)"
|
|
className="h-8 text-xs"
|
|
/>
|
|
<Input
|
|
type="text"
|
|
name="host"
|
|
placeholder="IMAP server"
|
|
className="h-8 text-xs"
|
|
required
|
|
/>
|
|
<div className="flex space-x-2">
|
|
<Input
|
|
type="number"
|
|
name="port"
|
|
placeholder="Port"
|
|
className="h-8 text-xs w-1/2"
|
|
defaultValue="993"
|
|
required
|
|
/>
|
|
<div className="flex items-center space-x-2 w-1/2">
|
|
<Checkbox id="secure" name="secure" defaultChecked />
|
|
<Label htmlFor="secure" className="text-xs">SSL/TLS</Label>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="smtp" className="space-y-2 pt-2">
|
|
<Input
|
|
type="text"
|
|
name="smtp_host"
|
|
placeholder="SMTP server (optional)"
|
|
className="h-8 text-xs"
|
|
/>
|
|
<div className="flex space-x-2">
|
|
<Input
|
|
type="number"
|
|
name="smtp_port"
|
|
placeholder="Port"
|
|
className="h-8 text-xs w-1/2"
|
|
defaultValue="587"
|
|
/>
|
|
<div className="flex items-center space-x-2 w-1/2">
|
|
<Checkbox id="smtp_secure" name="smtp_secure" />
|
|
<Label htmlFor="smtp_secure" className="text-xs">SSL/TLS</Label>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
<div className="flex justify-end space-x-2 pt-1">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-7 text-xs"
|
|
onClick={() => setShowAddAccountForm(false)}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
type="submit"
|
|
size="sm"
|
|
className="h-7 text-xs"
|
|
disabled={loading}
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
|
|
Adding...
|
|
</>
|
|
) : "Add Account"}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
{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={() => {
|
|
if (account.id !== 0) {
|
|
// Only toggle folders for non-All accounts
|
|
setShowFolders(!showFolders);
|
|
}
|
|
setSelectedAccount(account);
|
|
}}
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
<div className={`w-2.5 h-2.5 rounded-full ${account.color}`}></div>
|
|
<span className="font-medium text-gray-700 truncate">{account.name}</span>
|
|
</div>
|
|
{account.id !== 0 && (
|
|
<div className="flex items-center">
|
|
{showFolders ?
|
|
<ChevronDown className="h-3.5 w-3.5 text-gray-500" /> :
|
|
<ChevronRight className="h-3.5 w-3.5 text-gray-500" />
|
|
}
|
|
</div>
|
|
)}
|
|
</Button>
|
|
|
|
{/* Show folders for email accounts (not for "All" account) without the "Folders" header */}
|
|
{account.id !== 0 && showFolders && (
|
|
<div className="pl-4 mt-1 mb-2 space-y-0.5 border-l border-gray-200">
|
|
{account.folders && account.folders.length > 0 ? (
|
|
account.folders.map((folder) => (
|
|
<Button
|
|
key={folder}
|
|
variant="ghost"
|
|
className={`w-full justify-start py-1 px-2 text-xs ${
|
|
currentView === folder ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900'
|
|
}`}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleMailboxChange(folder);
|
|
}}
|
|
>
|
|
<div className="flex items-center justify-between w-full gap-1.5">
|
|
<div className="flex items-center gap-1.5">
|
|
{React.createElement(getFolderIcon(folder), { className: "h-3.5 w-3.5" })}
|
|
<span className="truncate">{formatFolderName(folder)}</span>
|
|
</div>
|
|
{folder === 'INBOX' && unreadCount > 0 && (
|
|
<span className="ml-auto bg-blue-600 text-white text-xs px-1.5 py-0.5 rounded-full text-[10px]">
|
|
{unreadCount}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</Button>
|
|
))
|
|
) : (
|
|
<div className="px-2 py-2">
|
|
<div className="flex flex-col space-y-2">
|
|
{/* Create placeholder folder items with shimmer effect */}
|
|
{Array.from({ length: 5 }).map((_, index) => (
|
|
<div key={index} className="flex items-center gap-1.5 animate-pulse">
|
|
<div className="h-3.5 w-3.5 bg-gray-200 rounded-sm"></div>
|
|
<div className="h-3 w-24 bg-gray-200 rounded"></div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Email List and Content View */}
|
|
<div className="flex-1 flex overflow-hidden">
|
|
{/* 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
|
|
className="mt-6 bg-blue-600 hover:bg-blue-700"
|
|
onClick={handleComposeNew}
|
|
>
|
|
<PlusIcon className="mr-2 h-4 w-4" />
|
|
Compose New
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
|
|
{/* Login needed alert */}
|
|
<LoginNeededAlert
|
|
show={showLoginNeeded}
|
|
onLogin={handleGoToLogin}
|
|
onClose={() => setShowLoginNeeded(false)}
|
|
/>
|
|
|
|
{/* Compose Modal */}
|
|
{showComposeModal && (
|
|
<ComposeEmail
|
|
initialEmail={selectedEmail}
|
|
type={composeType}
|
|
onClose={() => setShowComposeModal(false)}
|
|
onSend={async (emailData: EmailData) => {
|
|
await sendEmail(emailData);
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{/* Delete Confirmation Dialog */}
|
|
<DeleteConfirmDialog
|
|
show={showDeleteConfirm}
|
|
selectedCount={selectedEmailIds.length}
|
|
onConfirm={handleDeleteConfirm}
|
|
onCancel={() => setShowDeleteConfirm(false)}
|
|
/>
|
|
</>
|
|
);
|
|
}
|