Neah/app/courrier/page.tsx

1157 lines
49 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, Menu
} from 'lucide-react';
import { Dialog, DialogContent, DialogTitle } 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';
// 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: string;
name: string;
email: string;
color: string;
folders?: string[];
}
interface EmailWithFlags {
id: string;
read?: boolean;
flags?: {
seen?: boolean;
};
}
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: 'all-accounts', name: 'All', email: '', color: 'bg-gray-500' },
{ id: 'loading-account', name: 'Loading...', email: '', color: 'bg-blue-500', folders: [] }
]);
const [selectedAccount, setSelectedAccount] = useState<Account | null>(null);
// Track expanded folders for each account
const [expandedAccounts, setExpandedAccounts] = useState<Record<string, boolean>>({});
// Update account folders when mailboxes change - update this to maintain account IDs
useEffect(() => {
console.log('Mailboxes updated:', mailboxes);
setAccounts(prev => {
const updated = [...prev];
if (updated.length > 1) {
// Only update folders, preserve other properties including ID
if (updated[1]) {
updated[1] = {
...updated[1],
folders: mailboxes
};
}
console.log('Updated accounts with new mailboxes:', updated);
}
return updated;
});
}, [mailboxes]);
// Debug accounts state
useEffect(() => {
console.log('Current accounts state:', accounts);
}, [accounts]);
// Debug selectedAccount state
useEffect(() => {
console.log('Selected account changed:', selectedAccount);
if (selectedAccount) {
console.log('Selected account folders:', selectedAccount.folders);
}
}, [selectedAccount]);
// Add useEffect for debugging
useEffect(() => {
if (typeof window !== 'undefined') {
console.log('[DEBUG] Rendering UI with:', {
accountsCount: accounts.length,
selectedAccountId: selectedAccount?.id,
showFolders,
currentFolder
});
}
}, [accounts, selectedAccount, showFolders, currentFolder]);
// 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 => {
// Access the 'read' property safely, handling both old and new email formats
const emailWithFlags = email as unknown as EmailWithFlags;
return (!emailWithFlags.read && emailWithFlags.read !== undefined) ||
(emailWithFlags.flags && !emailWithFlags.flags.seen) ||
false;
}).filter(email => currentFolder === 'INBOX').length;
setUnreadCount(unreadInInbox);
}, [emails, currentFolder]);
// Ensure accounts section is never empty
useEffect(() => {
// If accounts array becomes empty (bug), restore default accounts
if (!accounts || accounts.length === 0) {
console.warn('Accounts array is empty, restoring defaults');
setAccounts([
{ id: 'all-accounts', name: 'All', email: '', color: 'bg-gray-500' },
{ id: 'loading-account', name: 'Loading...', email: '', color: 'bg-blue-500', folders: mailboxes }
]);
}
}, [accounts, mailboxes]);
// 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();
// Log the raw API response to inspect structure
console.log('[DEBUG] Raw session API response:', JSON.stringify(data, null, 2));
// Add detailed logging to inspect the accounts and folders structure
console.log('=== SESSION API RESPONSE DETAILED INSPECTION ===');
console.log('Session authenticated:', data.authenticated);
console.log('Has email credentials:', data.hasEmailCredentials);
console.log('Primary email:', data.email);
console.log('Redis status:', data.redisStatus);
// Log mailboxes structure - what the frontend used previously
console.log('=== MAILBOXES STRUCTURE (OLD API FORMAT) ===');
console.log('Global mailboxes exists:', !!data.mailboxes);
console.log('Global mailboxes is array:', Array.isArray(data.mailboxes));
console.log('Global mailboxes:', data.mailboxes);
// Log allAccounts structure - the new per-account folders approach
console.log('=== ALL ACCOUNTS STRUCTURE (NEW API FORMAT) ===');
console.log('allAccounts exists:', !!data.allAccounts);
console.log('allAccounts is array:', Array.isArray(data.allAccounts));
console.log('allAccounts length:', data.allAccounts?.length || 0);
// Inspect each account's structure
if (data.allAccounts && Array.isArray(data.allAccounts)) {
data.allAccounts.forEach((account: any, idx: number) => {
console.log(`Account ${idx + 1}:`, {
id: account.id,
email: account.email,
display_name: account.display_name,
foldersExist: !!account.folders,
foldersIsArray: Array.isArray(account.folders),
foldersLength: account.folders?.length || 0,
folders: account.folders
});
});
}
if (!isMounted) return;
if (data.authenticated) {
if (data.hasEmailCredentials) {
console.log('Session initialized, prefetch status:', data.prefetchStarted ? 'running' : 'not started');
setPrefetchStarted(Boolean(data.prefetchStarted));
// Create a copy of the current accounts to update
const updatedAccounts = [...accounts];
// Check if we have multiple accounts returned
if (data.allAccounts && Array.isArray(data.allAccounts) && data.allAccounts.length > 0) {
console.log('[DEBUG] Multiple accounts found:', data.allAccounts.length);
// First, validate the structure of each account
data.allAccounts.forEach((account: any, index: number) => {
console.log(`[DEBUG] Account ${index+1} structure check:`, {
id: account.id,
email: account.email,
display_name: account.display_name,
hasFolders: !!account.folders,
foldersIsArray: Array.isArray(account.folders),
foldersCount: Array.isArray(account.folders) ? account.folders.length : 0,
folders: account.folders || []
});
});
// Keep the All account at position 0
if (updatedAccounts.length > 0) {
// Replace the loading account with the real one
data.allAccounts.forEach((account: any, index: number) => {
// Ensure folders are valid
const accountFolders = (account.folders && Array.isArray(account.folders))
? account.folders
: (data.mailboxes && Array.isArray(data.mailboxes))
? data.mailboxes
: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk'];
// If we're updating the first real account (index 0 in API, position 1 in our array)
if (index === 0 && updatedAccounts.length > 1) {
// Update the loading account in place to maintain references
updatedAccounts[1] = {
id: account.id, // Use the real account ID
name: account.display_name || account.email,
email: account.email,
color: account.color || 'bg-blue-500',
folders: accountFolders
};
console.log(`[DEBUG] Updated loading account to real account: ${account.email} with ID ${account.id}`);
} else {
// Add additional accounts as new entries
updatedAccounts.push({
id: account.id || `account-${index}`,
name: account.display_name || account.email,
email: account.email,
color: account.color || 'bg-blue-500',
folders: accountFolders
});
}
});
} else {
// Fallback if accounts array is empty for some reason
updatedAccounts.push({ id: 'all-accounts', name: 'All', email: '', color: 'bg-gray-500' });
const firstAccount = data.allAccounts[0];
const accountFolders = (firstAccount.folders && Array.isArray(firstAccount.folders))
? firstAccount.folders
: (data.mailboxes && Array.isArray(data.mailboxes))
? data.mailboxes
: ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk'];
updatedAccounts.push({
id: firstAccount.id,
name: firstAccount.display_name || firstAccount.email,
email: firstAccount.email,
color: firstAccount.color || 'bg-blue-500',
folders: accountFolders
});
}
} else if (data.email) {
// Fallback to single account if allAccounts is not available
console.log(`[DEBUG] Fallback to single account: ${data.email}`);
// Force include some hardcoded folders if none are present
const fallbackFolders = ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk'];
// Prioritize mailboxes from IMAP if available
const folderList = (data.mailboxes && data.mailboxes.length > 0) ?
data.mailboxes : fallbackFolders;
// Update the loading account if it exists
if (updatedAccounts.length > 1) {
updatedAccounts[1] = {
id: 'default-account', // Use consistent ID
name: data.displayName || data.email,
email: data.email,
color: 'bg-blue-500',
folders: folderList
};
} else {
// Fallback if accounts array is empty
updatedAccounts.push({ id: 'all-accounts', name: 'All', email: '', color: 'bg-gray-500' });
updatedAccounts.push({
id: 'default-account',
name: data.displayName || data.email,
email: data.email,
color: 'bg-blue-500',
folders: folderList
});
}
}
console.log('Setting accounts:', updatedAccounts);
// Debug each account's folders
updatedAccounts.forEach((acc: Account, idx: number) => {
console.log(`Account ${idx + 1}: ${acc.name} (${acc.email}) - Folders:`,
acc.folders ? acc.folders.length : 0,
acc.folders || []);
});
// Update the accounts state
setAccounts(updatedAccounts);
// Auto-select the first non-All account if available
if (updatedAccounts.length > 1) {
console.log('Auto-selecting account:', updatedAccounts[1]);
setSelectedAccount(updatedAccounts[1]);
setShowFolders(true);
}
// 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 className="h-4 w-4 text-gray-500" />;
} else if (folderLower.includes('sent')) {
return <Send className="h-4 w-4 text-gray-500" />;
} else if (folderLower.includes('trash')) {
return <Trash className="h-4 w-4 text-gray-500" />;
} else if (folderLower.includes('archive')) {
return <Archive className="h-4 w-4 text-gray-500" />;
} else if (folderLower.includes('draft')) {
return <Edit className="h-4 w-4 text-gray-500" />;
} else if (folderLower.includes('spam') || folderLower.includes('junk')) {
return <AlertOctagon className="h-4 w-4 text-gray-500" />;
} else {
return <Folder className="h-4 w-4 text-gray-500" />;
}
};
// 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,
selectedAccount?.id
).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, accountId?: string) => {
// Reset to page 1 when changing folders
setPage(1);
// If we have a specific accountId, store it with the folder
if (accountId) {
// Store the current account ID with the folder change
console.log(`Changing folder to ${folder} for account ${accountId}`);
}
// 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, 1, accountId).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');
};
// Extra debugging for folder rendering
useEffect(() => {
if (selectedAccount && showFolders) {
console.log('Folder rendering debug:',
'account:', selectedAccount.id,
'folders:', selectedAccount.folders?.length || 0,
'showFolders:', showFolders
);
}
}, [selectedAccount, showFolders]);
return (
<>
<SimplifiedLoadingFix />
{/* 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">
{/* Panel 1: Sidebar - Always visible */}
<div className="w-60 bg-white/95 backdrop-blur-sm border-r border-gray-100 flex flex-col md:flex" style={{display: "flex !important"}}>
{/* 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);
// Load emails
loadEmails().finally(() => setLoading(false));
}}
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
</Button>
</div>
{/* Scrollable area for accounts and folders */}
<div className="flex-1 overflow-y-auto">
{/* 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>
{/* Display all accounts */}
<div className="mt-1">
{/* Form for adding a new account */}
{showAddAccountForm && (
<div className="mb-2 p-2 border border-gray-200 rounded-md bg-white">
<h4 className="text-xs font-medium mb-0.5 text-gray-700">Add IMAP Account</h4>
<form onSubmit={async (e) => {
e.preventDefault();
setLoading(true);
const formData = new FormData(e.currentTarget);
// Pull values from form with proper type handling
const formValues = {
email: formData.get('email')?.toString() || '',
password: formData.get('password')?.toString() || '',
host: formData.get('host')?.toString() || '',
port: parseInt(formData.get('port')?.toString() || '993'),
secure: formData.get('secure') === 'on',
display_name: formData.get('display_name')?.toString() || '',
smtp_host: formData.get('smtp_host')?.toString() || '',
smtp_port: formData.get('smtp_port')?.toString() ?
parseInt(formData.get('smtp_port')?.toString() || '587') : undefined,
smtp_secure: formData.get('smtp_secure') === 'on'
};
// If display_name is empty, use email
if (!formValues.display_name) {
formValues.display_name = formValues.email;
}
try {
// First test the connection
const testResponse = await fetch('/api/courrier/test-connection', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: formValues.email,
password: formValues.password,
host: formValues.host,
port: formValues.port,
secure: formValues.secure
})
});
const testResult = await testResponse.json();
if (!testResponse.ok) {
throw new Error(testResult.error || 'Connection test failed');
}
console.log('Connection test successful:', testResult);
// If connection test is successful, save the account
const saveResponse = await fetch('/api/courrier/account', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formValues)
});
const saveResult = await saveResponse.json();
if (!saveResponse.ok) {
throw new Error(saveResult.error || 'Failed to add account');
}
// Update accounts list
const newAccountObj = {
id: `account-${Date.now()}`, // generate unique string ID
name: formValues.display_name,
email: formValues.email,
color: `bg-blue-500`, // Default color class
folders: testResult.details.sampleFolders || ['INBOX', 'Sent', 'Drafts', 'Trash'] // Use discovered folders or defaults
};
setAccounts(prev => [...prev, newAccountObj]);
setShowAddAccountForm(false);
toast({
title: "Account added successfully",
description: `Your email account ${formValues.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>
<Tabs defaultValue="imap" className="w-full">
<TabsList className="grid w-full grid-cols-2 h-6 mb-0.5 bg-gray-100">
<TabsTrigger value="imap" className="text-xs h-5 data-[state=active]:bg-blue-500 data-[state=active]:text-white">IMAP</TabsTrigger>
<TabsTrigger value="smtp" className="text-xs h-5 data-[state=active]:bg-blue-500 data-[state=active]:text-white">SMTP</TabsTrigger>
</TabsList>
<TabsContent value="imap" className="mt-0.5 space-y-0.5">
<div>
<Input
id="email"
name="email"
placeholder="email@example.com"
className="h-7 text-xs bg-white border-gray-300 mb-0.5 text-gray-900"
required
/>
</div>
<div>
<Input
id="password"
name="password"
type="password"
placeholder="•••••••••"
className="h-7 text-xs bg-white border-gray-300 mb-0.5 text-gray-900"
required
/>
</div>
<div>
<Input
id="display_name"
name="display_name"
placeholder="John Doe"
className="h-7 text-xs bg-white border-gray-300 mb-0.5 text-gray-900"
/>
</div>
<div>
<Input
id="host"
name="host"
placeholder="imap.example.com"
className="h-7 text-xs bg-white border-gray-300 mb-0.5 text-gray-900"
required
/>
</div>
<div className="flex gap-1">
<div className="flex-1">
<Input
id="port"
name="port"
placeholder="993"
className="h-7 text-xs bg-white border-gray-300 text-gray-900"
defaultValue="993"
required
/>
</div>
<div className="flex items-center pl-1">
<div className="flex items-center space-x-1">
<Checkbox id="secure" name="secure" defaultChecked />
<Label htmlFor="secure" className="text-xs">SSL</Label>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="smtp" className="mt-0.5 space-y-0.5">
<div>
<Input
id="smtp_host"
name="smtp_host"
placeholder="smtp.example.com"
className="h-7 text-xs bg-white border-gray-300 mb-0.5 text-gray-900"
/>
</div>
<div className="flex gap-1">
<div className="flex-1">
<Input
id="smtp_port"
name="smtp_port"
placeholder="587"
className="h-7 text-xs bg-white border-gray-300 text-gray-900"
defaultValue="587"
/>
</div>
<div className="flex items-center pl-1">
<div className="flex items-center space-x-1">
<Checkbox id="smtp_secure" name="smtp_secure" defaultChecked />
<Label htmlFor="smtp_secure" className="text-xs">SSL</Label>
</div>
</div>
</div>
<div className="text-xs text-gray-500 italic">
Note: SMTP settings needed for sending emails
</div>
</TabsContent>
</Tabs>
<div className="flex gap-1 mt-1">
<Button
type="submit"
className="flex-1 h-6 text-xs bg-blue-500 hover:bg-blue-600 text-white rounded-md px-2 py-0"
disabled={loading}
>
{loading ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : null}
Test & Add
</Button>
<Button
type="button"
className="h-6 text-xs bg-gray-200 text-gray-800 hover:bg-gray-300 rounded-md px-2 py-0"
onClick={() => setShowAddAccountForm(false)}
>
Cancel
</Button>
</div>
</div>
</form>
</div>
)}
{accounts.map((account) => (
<div key={account.id} className="mb-1">
<Button
variant="ghost"
className={`w-full justify-start text-xs ${selectedAccount?.id === account.id ? 'bg-gray-100' : ''}`}
onClick={() => {
setSelectedAccount(account);
setShowFolders(true);
// Auto-expand this account when selected
if (account.id !== 'all-accounts') {
setExpandedAccounts(prev => ({
...prev,
[account.id]: true
}));
}
if (account.id === 'all-accounts') {
handleMailboxChange('INBOX');
} else {
handleMailboxChange('INBOX', account.id);
}
}}
>
<div className="flex items-center w-full">
<div className={`w-3 h-3 rounded-full ${account.color?.startsWith('#') ? 'bg-blue-500' : account.color || 'bg-blue-500'} mr-2`}></div>
<span className="truncate text-gray-700">{account.name}</span>
{account.id !== 'all-accounts' && (
<span
className="ml-auto text-gray-400 hover:text-gray-600 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
// Toggle this specific account's expanded state
setExpandedAccounts(prev => ({
...prev,
[account.id]: !prev[account.id]
}));
}}
>
{expandedAccounts[account.id] ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
</span>
)}
</div>
</Button>
{/* Show folders for this account if expanded */}
{expandedAccounts[account.id] && account.id !== 'all-accounts' && account.folders && (
<div className="pl-4">
{account.folders.map((folder) => (
<Button
key={folder}
variant="ghost"
className={`w-full justify-start text-xs py-1 h-7 ${currentFolder === folder ? 'bg-gray-100' : ''}`}
onClick={() => handleMailboxChange(folder, account.id !== 'all-accounts' ? account.id : undefined)}
>
<div className="flex items-center w-full">
{getFolderIcon(folder)}
<span className="ml-2 truncate text-gray-700">{formatFolderName(folder)}</span>
{folder === 'INBOX' && unreadCount > 0 && (
<span className="ml-auto bg-blue-500 text-white text-[10px] px-1.5 rounded-full">
{unreadCount}
</span>
)}
</div>
</Button>
))}
</div>
)}
</div>
))}
</div>
</div>
</div>
</div>
{/* Panel 2: Email List - Always visible */}
<div className="w-80 flex flex-col border-r border-gray-100 overflow-hidden">
{/* Header without search bar or profile */}
<div className="p-2 border-b border-gray-100 bg-white flex items-center justify-between">
<Button
variant="ghost"
size="icon"
className="md:hidden h-9 w-9"
onClick={() => setMobileSidebarOpen(!mobileSidebarOpen)}
>
<Menu className="h-5 w-5 text-gray-500" />
</Button>
<div className="flex-1">
<div className="flex items-center">
{getFolderIcon(currentFolder)}
<span className="ml-2 font-medium text-gray-700">{formatFolderName(currentFolder)}</span>
</div>
</div>
<div className="flex items-center">
{selectedEmailIds.length > 0 && (
<div className="flex items-center space-x-1">
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => handleBulkAction('mark-read')}
>
<MessageSquare className="h-4 w-4 text-gray-500" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => handleBulkAction('delete')}
>
<Trash className="h-4 w-4 text-gray-500" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => handleBulkAction('archive')}
>
<Archive className="h-4 w-4 text-gray-500" />
</Button>
</div>
)}
</div>
</div>
{/* Email List - Always visible */}
<div className="flex-1 overflow-hidden bg-white">
{isLoading ? (
<div className="h-full flex items-center justify-center">
<div className="flex flex-col items-center">
<Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-2" />
<p className="text-sm text-gray-500">Loading emails...</p>
</div>
</div>
) : error ? (
<div className="h-full flex items-center justify-center">
<div className="max-w-md p-4">
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
{error}
{(error?.includes('Not authenticated') || error?.includes('No email credentials found')) && (
<Button
variant="outline"
size="sm"
className="mt-2"
onClick={handleGoToLogin}
>
Go to login
</Button>
)}
</AlertDescription>
</Alert>
</div>
</div>
) : (
<div className="h-full overflow-hidden flex flex-col">
{/* Email List */}
<div className="flex-1 overflow-y-auto">
{emails.length === 0 ? (
<div className="h-full flex items-center justify-center">
<div className="text-center p-6">
<Inbox className="h-12 w-12 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-700">No emails found</h3>
<p className="text-sm text-gray-500 mt-1">
{searchQuery
? `No results found for "${searchQuery}"`
: `Your ${currentFolder.toLowerCase()} is empty`}
</p>
</div>
</div>
) : (
<EmailList
emails={emails}
selectedEmailIds={selectedEmailIds}
selectedEmail={selectedEmail}
onSelectEmail={handleEmailSelect}
onToggleSelect={toggleEmailSelection}
onToggleSelectAll={toggleSelectAll}
onToggleStarred={toggleStarred}
onLoadMore={handleLoadMore}
hasMoreEmails={page < totalPages}
currentFolder={currentFolder}
isLoading={isLoading}
totalEmails={emails.length}
onBulkAction={handleBulkAction}
/>
)}
</div>
</div>
)}
</div>
</div>
{/* Panel 3: Email Detail - Always visible */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Content for Panel 3 based on state but always visible */}
<div className="flex-1 overflow-hidden bg-white">
{selectedEmail ? (
<EmailDetailView
email={selectedEmail}
onBack={() => {
handleEmailSelect('');
// Ensure sidebar stays visible
setSidebarOpen(true);
}}
onReply={handleReply}
onReplyAll={handleReplyAll}
onForward={handleForward}
onToggleStar={() => toggleStarred(selectedEmail.id)}
/>
) : (
<div className="h-full flex items-center justify-center">
<div className="text-center text-muted-foreground">
<p>Select an email to view or</p>
<button
className="text-primary mt-2 hover:underline"
onClick={() => {
setComposeType('new');
setShowComposeModal(true);
}}
>
Compose a new message
</button>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</main>
{/* Modals and Dialogs */}
<DeleteConfirmDialog
show={showDeleteConfirm}
selectedCount={selectedEmailIds.length}
onConfirm={handleDeleteConfirm}
onCancel={() => setShowDeleteConfirm(false)}
/>
<LoginNeededAlert
show={showLoginNeeded}
onLogin={handleGoToLogin}
onClose={() => setShowLoginNeeded(false)}
/>
{/* Compose Email Dialog */}
<Dialog open={showComposeModal} onOpenChange={(open) => !open && setShowComposeModal(false)}>
<DialogContent className="sm:max-w-[800px] p-0 h-[80vh]">
<DialogTitle asChild>
<span className="sr-only">New Message</span>
</DialogTitle>
<ComposeEmail
type={composeType}
initialEmail={composeType !== 'new' ? selectedEmail : undefined}
onSend={handleSendEmail}
onClose={() => setShowComposeModal(false)}
/>
</DialogContent>
</Dialog>
</>
);
}