1315 lines
54 KiB
TypeScript
1315 lines
54 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 { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem } from '@/components/ui/dropdown-menu';
|
|
|
|
// 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 } 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;
|
|
};
|
|
}
|
|
|
|
interface EmailMessage {
|
|
id: string;
|
|
from: { name: string; address: string }[];
|
|
to: { name: string; address: string }[];
|
|
subject: string;
|
|
date: Date;
|
|
flags: {
|
|
seen: boolean;
|
|
flagged: boolean;
|
|
answered: boolean;
|
|
draft: boolean;
|
|
deleted: boolean;
|
|
};
|
|
size: number;
|
|
hasAttachments: boolean;
|
|
folder: string;
|
|
contentFetched: boolean;
|
|
accountId: string;
|
|
content: {
|
|
text: string;
|
|
html: string;
|
|
};
|
|
}
|
|
|
|
// Define a color palette for account circles
|
|
const colorPalette = [
|
|
'bg-blue-500',
|
|
'bg-green-500',
|
|
'bg-red-500',
|
|
'bg-yellow-500',
|
|
'bg-purple-500',
|
|
'bg-pink-500',
|
|
'bg-indigo-500',
|
|
'bg-teal-500',
|
|
'bg-orange-500',
|
|
'bg-cyan-500',
|
|
];
|
|
|
|
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: '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>>({});
|
|
|
|
// Track selected folder per account
|
|
const [selectedFolders, setSelectedFolders] = useState<Record<string, string>>({});
|
|
|
|
// Track folder visibility per account
|
|
const [visibleFolders, setVisibleFolders] = useState<Record<string, string[]>>({});
|
|
|
|
// Add state for modals/dialogs
|
|
const [showEditModal, setShowEditModal] = useState(false);
|
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
|
const [accountToEdit, setAccountToEdit] = useState<Account | null>(null);
|
|
const [accountToDelete, setAccountToDelete] = useState<Account | null>(null);
|
|
const [newPassword, setNewPassword] = useState('');
|
|
const [editLoading, setEditLoading] = useState(false);
|
|
const [deleteLoading, setDeleteLoading] = useState(false);
|
|
|
|
// Debug accounts state
|
|
useEffect(() => {
|
|
console.log('Current accounts state:', accounts);
|
|
console.log('Expanded accounts:', expandedAccounts);
|
|
console.log('Selected account:', selectedAccount);
|
|
console.log('Show folders:', showFolders);
|
|
}, [accounts, expandedAccounts, selectedAccount, showFolders]);
|
|
|
|
// 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 for each account and folder
|
|
useEffect(() => {
|
|
// Create a map to store unread counts per account and folder
|
|
const accountFolderUnreadCounts = new Map<string, Map<string, number>>();
|
|
|
|
// Initialize counts for all accounts and folders
|
|
accounts.forEach(account => {
|
|
if (account.id !== 'loading-account') {
|
|
const folderCounts = new Map<string, number>();
|
|
account.folders.forEach(folder => {
|
|
folderCounts.set(folder, 0);
|
|
});
|
|
accountFolderUnreadCounts.set(account.id, folderCounts);
|
|
}
|
|
});
|
|
|
|
// Count unread emails for each account and folder
|
|
(emails || []).forEach(email => {
|
|
// Check if email is unread based on flags
|
|
const isUnread = email.flags && !email.flags.seen;
|
|
|
|
// Count unread emails for the specific account and folder
|
|
if (isUnread && email.accountId && email.folder) {
|
|
const folderCounts = accountFolderUnreadCounts.get(email.accountId);
|
|
if (folderCounts) {
|
|
const currentCount = folderCounts.get(email.folder) || 0;
|
|
folderCounts.set(email.folder, currentCount + 1);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Update the unread count for the selected account and folder
|
|
if (selectedAccount && selectedAccount.id !== 'loading-account') {
|
|
const folderCounts = accountFolderUnreadCounts.get(selectedAccount.id);
|
|
if (folderCounts) {
|
|
setUnreadCount(folderCounts.get(currentFolder) || 0);
|
|
} else {
|
|
setUnreadCount(0);
|
|
}
|
|
} else {
|
|
// For 'loading-account', sum up all unread counts for the current folder
|
|
let totalUnread = 0;
|
|
accountFolderUnreadCounts.forEach((folderCounts: Map<string, number>) => {
|
|
totalUnread += folderCounts.get(currentFolder) || 0;
|
|
});
|
|
setUnreadCount(totalUnread);
|
|
}
|
|
|
|
// Log the counts for debugging
|
|
console.log('Unread counts per account and folder:',
|
|
Object.fromEntries(
|
|
Array.from(accountFolderUnreadCounts.entries()).map(([accountId, folderCounts]) => [
|
|
accountId,
|
|
Object.fromEntries(folderCounts.entries())
|
|
])
|
|
)
|
|
);
|
|
}, [emails, selectedAccount, currentFolder, accounts]);
|
|
|
|
// 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: '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 retryCount = 0;
|
|
const MAX_RETRIES = 3;
|
|
const RETRY_DELAY = 1000; // 1 second
|
|
|
|
const initSession = async () => {
|
|
try {
|
|
if (!isMounted) return;
|
|
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', {
|
|
credentials: 'include', // Ensure cookies are sent
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
}
|
|
});
|
|
|
|
// Handle 401 Unauthorized with retry logic
|
|
if (response.status === 401) {
|
|
if (retryCount < MAX_RETRIES) {
|
|
retryCount++;
|
|
console.log(`Session request failed (attempt ${retryCount}/${MAX_RETRIES}), retrying in ${RETRY_DELAY}ms...`);
|
|
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
|
|
return initSession();
|
|
} else {
|
|
console.error('Max retries reached for session request');
|
|
// Instead of throwing, redirect to login
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Session request failed with status ${response.status}`);
|
|
}
|
|
|
|
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.authenticated) {
|
|
if (data.hasEmailCredentials) {
|
|
console.log('Session initialized, prefetch status:', data.prefetchStarted ? 'running' : 'not started');
|
|
setPrefetchStarted(Boolean(data.prefetchStarted));
|
|
|
|
let updatedAccounts: Account[] = [];
|
|
|
|
// 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);
|
|
|
|
// Add all accounts from the API response
|
|
data.allAccounts.forEach((account: any) => {
|
|
console.log('[DEBUG] Processing account:', {
|
|
id: account.id,
|
|
email: account.email,
|
|
folders: account.folders
|
|
});
|
|
|
|
// Use exact folders from IMAP without any mapping
|
|
const accountFolders = (account.folders && Array.isArray(account.folders))
|
|
? account.folders
|
|
: [];
|
|
|
|
// Keep the account prefix in folder names
|
|
const validFolders = accountFolders.map((folder: string) => {
|
|
// If folder doesn't have account prefix, add it
|
|
if (!folder.includes(':')) {
|
|
return `${account.id}:${folder}`;
|
|
}
|
|
return folder;
|
|
});
|
|
|
|
updatedAccounts.push({
|
|
id: account.id,
|
|
name: account.display_name || account.email,
|
|
email: account.email,
|
|
color: colorPalette[(updatedAccounts.length - 1) % colorPalette.length],
|
|
folders: validFolders
|
|
});
|
|
console.log(`[DEBUG] Added account with folders:`, {
|
|
id: account.id,
|
|
email: account.email,
|
|
folders: validFolders
|
|
});
|
|
});
|
|
} else {
|
|
// Fallback to single account if allAccounts is not available
|
|
console.log(`[DEBUG] Fallback to single account: ${data.email}`);
|
|
|
|
// Use exact folders from IMAP if available
|
|
const folderList = (data.mailboxes && data.mailboxes.length > 0) ?
|
|
data.mailboxes : [];
|
|
|
|
updatedAccounts.push({
|
|
id: 'default-account',
|
|
name: data.displayName || data.email,
|
|
email: data.email,
|
|
color: colorPalette[(updatedAccounts.length - 1) % colorPalette.length],
|
|
folders: folderList
|
|
});
|
|
}
|
|
|
|
// Update accounts state
|
|
setAccounts(updatedAccounts);
|
|
console.log('[DEBUG] Updated accounts:', 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);
|
|
|
|
// Ensure folders are visible for the selected account
|
|
setExpandedAccounts(prev => ({
|
|
...prev,
|
|
[updatedAccounts[1].id]: true
|
|
}));
|
|
|
|
// Set initial selected folder
|
|
setSelectedFolders(prev => ({
|
|
...prev,
|
|
[updatedAccounts[1].id]: 'INBOX'
|
|
}));
|
|
}
|
|
} else {
|
|
// User is authenticated but doesn't have email credentials
|
|
setShowLoginNeeded(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);
|
|
}
|
|
}
|
|
}
|
|
} 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);
|
|
};
|
|
|
|
// Update handleMailboxChange to properly handle per-account folders
|
|
const handleMailboxChange = (folder: string, accountId?: string) => {
|
|
if (accountId && accountId !== 'loading-account') {
|
|
const account = accounts.find(a => a.id === accountId);
|
|
if (!account) {
|
|
toast({
|
|
title: "Account not found",
|
|
description: `The account ${accountId} could not be found.`,
|
|
variant: "destructive",
|
|
});
|
|
return;
|
|
}
|
|
// Only allow navigation to folders in selectedAccount.folders
|
|
if (!account.folders.includes(folder)) {
|
|
toast({
|
|
title: "Folder not found",
|
|
description: `The folder ${folder} does not exist for this account.`,
|
|
variant: "destructive",
|
|
});
|
|
return;
|
|
}
|
|
setSelectedFolders(prev => ({ ...prev, [accountId]: folder }));
|
|
changeFolder(folder, accountId);
|
|
} else {
|
|
changeFolder(folder, accountId);
|
|
}
|
|
};
|
|
|
|
// Update the folder button rendering to show selected state based on account
|
|
const renderFolderButton = (folder: string, accountId: string) => {
|
|
// Get the account prefix from the folder name
|
|
const folderAccountId = folder.includes(':') ? folder.split(':')[0] : accountId;
|
|
|
|
// Only show folders that belong to this account
|
|
if (folderAccountId !== accountId) return null;
|
|
|
|
const isSelected = selectedFolders[accountId] === folder;
|
|
const account = accounts.find(a => a.id === accountId);
|
|
|
|
// Get the base folder name for display
|
|
const baseFolder = folder.includes(':') ? folder.split(':')[1] : folder;
|
|
|
|
return (
|
|
<Button
|
|
key={folder}
|
|
variant="ghost"
|
|
className={`w-full justify-start text-xs py-1 h-7 ${isSelected ? 'bg-gray-100' : ''}`}
|
|
onClick={() => handleMailboxChange(folder, accountId)}
|
|
>
|
|
<div className="flex items-center w-full">
|
|
{getFolderIcon(baseFolder)}
|
|
<span className="ml-2 truncate text-gray-700">{formatFolderName(baseFolder)}</span>
|
|
{baseFolder === 'INBOX' && unreadCount > 0 && (
|
|
<span className="ml-auto bg-blue-500 text-white text-[10px] px-1.5 rounded-full">
|
|
{unreadCount}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</Button>
|
|
);
|
|
};
|
|
|
|
// Handle sending email
|
|
const handleSendEmail = async (emailData: EmailData) => {
|
|
const result = await sendEmail(emailData);
|
|
if (!result.success) {
|
|
throw new Error(result.error);
|
|
}
|
|
return result;
|
|
};
|
|
|
|
// 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]);
|
|
|
|
const handleAccountSelect = (account: Account) => {
|
|
setSelectedAccount(account);
|
|
setShowFolders(true);
|
|
if (account.id !== 'loading-account') {
|
|
setExpandedAccounts(prev => ({
|
|
...prev,
|
|
[account.id]: true
|
|
}));
|
|
}
|
|
handleMailboxChange('INBOX', account.id);
|
|
};
|
|
|
|
const handleAddAccount = async (accountData: AccountData) => {
|
|
// ... account creation logic ...
|
|
// setAccounts(prev => [...prev, newAccount]);
|
|
// setVisibleFolders(prev => ({
|
|
// ...prev,
|
|
// [newAccount.id]: newAccount.folders
|
|
// }));
|
|
};
|
|
|
|
// Debug folder rendering
|
|
useEffect(() => {
|
|
if (selectedAccount) {
|
|
console.log('Selected account folders:', selectedAccount.folders);
|
|
console.log('Is account expanded:', expandedAccounts[selectedAccount.id]);
|
|
}
|
|
}, [selectedAccount, expandedAccounts]);
|
|
|
|
// On page load/refresh, expand all accounts so their folders are always visible
|
|
useEffect(() => {
|
|
// Expand all real accounts (not loading-account) on load/refresh
|
|
const expanded: Record<string, boolean> = {};
|
|
accounts.forEach(a => {
|
|
if (a.id !== 'loading-account') expanded[a.id] = true;
|
|
});
|
|
setExpandedAccounts(expanded);
|
|
}, [accounts]);
|
|
|
|
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">
|
|
<span className="text-sm font-medium text-gray-500">Accounts</span>
|
|
<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);
|
|
|
|
// Only declare realAccounts once before using for color assignment
|
|
const realAccounts = accounts.filter(a => a.id !== 'loading-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');
|
|
}
|
|
const realAccount = saveResult.account;
|
|
realAccount.color = colorPalette[realAccounts.length % colorPalette.length];
|
|
realAccount.folders = testResult.details.sampleFolders || ['INBOX', 'Sent', 'Drafts', 'Trash'];
|
|
setAccounts(prev => [...prev, realAccount]);
|
|
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">
|
|
<div className={`flex items-center w-full px-1 py-1 rounded-md cursor-pointer ${selectedAccount?.id === account.id ? 'bg-gray-100' : ''}`}
|
|
onClick={() => handleAccountSelect(account)}
|
|
tabIndex={0}
|
|
role="button"
|
|
onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') handleAccountSelect(account); }}
|
|
>
|
|
<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 flex-1">{account.name}</span>
|
|
{/* More options button (⋮) */}
|
|
{account.id !== 'loading-account' && (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<button
|
|
type="button"
|
|
className="ml-1 text-gray-400 hover:text-gray-600 cursor-pointer flex items-center justify-center h-5 w-5"
|
|
tabIndex={-1}
|
|
onClick={e => e.stopPropagation()}
|
|
aria-label="Account options"
|
|
>
|
|
<span style={{ fontSize: '18px', lineHeight: 1 }}>⋮</span>
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={e => { e.stopPropagation(); setAccountToEdit(account); setShowEditModal(true); }}>
|
|
Edit
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem onClick={e => { e.stopPropagation(); setAccountToDelete(account); setShowDeleteDialog(true); }}>
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
)}
|
|
{/* Expand/collapse arrow */}
|
|
{account.id !== 'loading-account' && (
|
|
<button
|
|
type="button"
|
|
className="ml-1 text-gray-400 hover:text-gray-600 cursor-pointer flex items-center justify-center h-5 w-5"
|
|
tabIndex={-1}
|
|
onClick={e => { e.stopPropagation(); setExpandedAccounts(prev => ({ ...prev, [account.id]: !prev[account.id] })); }}
|
|
>
|
|
{expandedAccounts[account.id] ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
|
|
</button>
|
|
)}
|
|
</div>
|
|
{/* Show folders for any expanded account */}
|
|
{expandedAccounts[account.id] && account.folders && account.folders.length > 0 && (
|
|
<div className="pl-4">
|
|
{account.folders.map((folder) => renderFolderButton(folder, account.id))}
|
|
</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)}
|
|
{/* Extract base folder and show email as prefix */}
|
|
<span className="ml-2 font-medium text-gray-700">
|
|
{selectedAccount?.email ? `${selectedAccount.email}: ` : ''}
|
|
{formatFolderName(currentFolder.includes(':') ? currentFolder.split(':')[1] : 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}
|
|
</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)}
|
|
/>
|
|
|
|
{/* 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>
|
|
|
|
{/* Edit Password Modal */}
|
|
<Dialog open={showEditModal} onOpenChange={open => { if (!open) setShowEditModal(false); }}>
|
|
<DialogContent className="sm:max-w-[400px]">
|
|
<DialogTitle>Edit Account Password</DialogTitle>
|
|
<form onSubmit={async e => {
|
|
e.preventDefault();
|
|
if (!accountToEdit) return;
|
|
setEditLoading(true);
|
|
try {
|
|
const res = await fetch('/api/courrier/account', {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ accountId: accountToEdit.id, newPassword }),
|
|
});
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Failed to update password');
|
|
toast({ title: 'Password updated', description: 'Password changed successfully.' });
|
|
setShowEditModal(false);
|
|
setNewPassword('');
|
|
window.location.reload();
|
|
} catch (err) {
|
|
toast({ title: 'Error', description: err instanceof Error ? err.message : 'Failed to update password', variant: 'destructive' });
|
|
} finally {
|
|
setEditLoading(false);
|
|
}
|
|
}}>
|
|
<div className="mb-2">
|
|
<Label htmlFor="new-password">New Password</Label>
|
|
<Input id="new-password" type="password" value={newPassword} onChange={e => setNewPassword(e.target.value)} required className="mt-1" />
|
|
</div>
|
|
<div className="flex justify-end gap-2 mt-4">
|
|
<Button type="button" variant="outline" onClick={() => setShowEditModal(false)}>Cancel</Button>
|
|
<Button type="submit" disabled={editLoading}>{editLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Save'}</Button>
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
{/* Delete Account Dialog */}
|
|
<AlertDialog open={showDeleteDialog} onOpenChange={open => { if (!open) setShowDeleteDialog(false); }}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Delete Account</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Are you sure you want to delete this account? This action cannot be undone.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel onClick={() => setShowDeleteDialog(false)}>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction asChild>
|
|
<Button variant="destructive" disabled={deleteLoading} onClick={async () => {
|
|
if (!accountToDelete) return;
|
|
setDeleteLoading(true);
|
|
try {
|
|
const res = await fetch(`/api/courrier/account?accountId=${accountToDelete.id}`, { method: 'DELETE' });
|
|
const data = await res.json();
|
|
if (!res.ok) throw new Error(data.error || 'Failed to delete account');
|
|
toast({ title: 'Account deleted', description: 'The account was deleted successfully.' });
|
|
setShowDeleteDialog(false);
|
|
window.location.reload();
|
|
} catch (err) {
|
|
toast({ title: 'Error', description: err instanceof Error ? err.message : 'Failed to delete account', variant: 'destructive' });
|
|
} finally {
|
|
setDeleteLoading(false);
|
|
}
|
|
}}>Delete</Button>
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</>
|
|
);
|
|
}
|