323 lines
9.7 KiB
TypeScript
323 lines
9.7 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import {
|
|
Inbox, Star, Send, File, Trash, RefreshCw, Plus,
|
|
Search, Loader2, MailOpen, Mail, ArchiveIcon
|
|
} from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import EmailPanel from './EmailPanel';
|
|
import { EmailMessage } from '@/lib/services/email-service';
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { sendEmail } from '@/lib/services/email-service';
|
|
import { useSession } from "next-auth/react";
|
|
import { cn } from '@/lib/utils';
|
|
import EmailList from './EmailList';
|
|
import { Email } from '@/hooks/use-courrier';
|
|
|
|
interface EmailLayoutProps {
|
|
className?: string;
|
|
previewModeEnabled?: boolean;
|
|
}
|
|
|
|
export default function EmailLayout({
|
|
className = '',
|
|
previewModeEnabled = false
|
|
}: EmailLayoutProps) {
|
|
// Email state
|
|
const [emails, setEmails] = useState<EmailMessage[]>([]);
|
|
const [selectedEmail, setSelectedEmail] = useState<{
|
|
emailId: string;
|
|
accountId: string;
|
|
folder: string;
|
|
} | null>(null);
|
|
const [currentFolder, setCurrentFolder] = useState<string>('INBOX');
|
|
const [folders, setFolders] = useState<string[]>([]);
|
|
const [mailboxes, setMailboxes] = useState<string[]>([]);
|
|
|
|
// UI state
|
|
const [loading, setLoading] = useState<boolean>(true);
|
|
const [searching, setSearching] = useState<boolean>(false);
|
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
|
const [page, setPage] = useState<number>(1);
|
|
const [hasMore, setHasMore] = useState<boolean>(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isSending, setIsSending] = useState<boolean>(false);
|
|
const [showComposeModal, setShowComposeModal] = useState<boolean>(false);
|
|
|
|
// Get toast and session
|
|
const { toast } = useToast();
|
|
const { data: session } = useSession();
|
|
|
|
// Load emails on component mount and when folder changes
|
|
useEffect(() => {
|
|
loadEmails();
|
|
}, [currentFolder, page]);
|
|
|
|
// Function to load emails
|
|
const loadEmails = async (refresh = false) => {
|
|
if (refresh) {
|
|
setPage(1);
|
|
}
|
|
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
// Construct the API endpoint URL with parameters
|
|
const queryParams = new URLSearchParams({
|
|
folder: currentFolder,
|
|
page: page.toString(),
|
|
perPage: '20'
|
|
});
|
|
|
|
if (searchQuery) {
|
|
queryParams.set('search', searchQuery);
|
|
}
|
|
|
|
const response = await fetch(`/api/courrier?${queryParams.toString()}`);
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || 'Failed to fetch emails');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
if (refresh || page === 1) {
|
|
setEmails(data.emails || []);
|
|
} else {
|
|
// Append emails for pagination
|
|
setEmails(prev => [...prev, ...(data.emails || [])]);
|
|
}
|
|
|
|
// Update available folders if returned from API
|
|
if (data.mailboxes && data.mailboxes.length > 0) {
|
|
setMailboxes(data.mailboxes);
|
|
|
|
// Create a nicer list of standard folders
|
|
const standardFolders = ['INBOX', 'Sent', 'Drafts', 'Trash', 'Junk'];
|
|
const customFolders = data.mailboxes.filter(
|
|
(folder: string) => !standardFolders.includes(folder)
|
|
);
|
|
|
|
// Combine standard folders that exist with custom folders
|
|
const availableFolders = [
|
|
...standardFolders.filter(f => data.mailboxes.includes(f)),
|
|
...customFolders
|
|
];
|
|
|
|
setFolders(availableFolders);
|
|
}
|
|
|
|
// Check if there are more emails to load
|
|
setHasMore(data.emails && data.emails.length >= 20);
|
|
} catch (err) {
|
|
console.error('Error loading emails:', err);
|
|
setError(err instanceof Error ? err.message : 'Failed to load emails');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// Handle folder change
|
|
const handleFolderChange = (folder: string) => {
|
|
setCurrentFolder(folder);
|
|
setSelectedEmail(null);
|
|
setPage(1);
|
|
setSearchQuery('');
|
|
};
|
|
|
|
// Handle email selection
|
|
const handleEmailSelect = (emailId: string, accountId: string, folder: string) => {
|
|
setSelectedEmail({ emailId, accountId, folder });
|
|
};
|
|
|
|
// Handle search
|
|
const handleSearch = () => {
|
|
if (searchQuery.trim()) {
|
|
setSearching(true);
|
|
setPage(1);
|
|
loadEmails(true);
|
|
}
|
|
};
|
|
|
|
// Handle refreshing emails
|
|
const handleRefresh = () => {
|
|
loadEmails(true);
|
|
};
|
|
|
|
// Handle composing a new email
|
|
const handleComposeNew = () => {
|
|
setSelectedEmail(null);
|
|
// The compose functionality will be handled by the EmailPanel component
|
|
};
|
|
|
|
// Handle email sending
|
|
const handleSendEmail = async (emailData: {
|
|
to: string;
|
|
cc?: string;
|
|
bcc?: string;
|
|
subject: string;
|
|
body: string;
|
|
attachments?: Array<{
|
|
name: string;
|
|
content: string;
|
|
type: string;
|
|
}>;
|
|
}): Promise<void> => {
|
|
setIsSending(true);
|
|
try {
|
|
// The sendEmail function requires userId as the first parameter
|
|
const result = await sendEmail(session?.user?.id as string, emailData);
|
|
|
|
if (result.success) {
|
|
toast({
|
|
title: "Success",
|
|
description: "Email sent successfully"
|
|
});
|
|
setShowComposeModal(false);
|
|
} else {
|
|
toast({
|
|
variant: "destructive",
|
|
title: "Error",
|
|
description: `Failed to send email: ${result.error || 'Unknown error'}`
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error sending email:', error);
|
|
toast({
|
|
variant: "destructive",
|
|
title: "Error",
|
|
description: "Failed to send email"
|
|
});
|
|
} finally {
|
|
setIsSending(false);
|
|
}
|
|
};
|
|
|
|
// Format the date in a readable format
|
|
const formatDate = (dateString: string) => {
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
const yesterday = new Date(today);
|
|
yesterday.setDate(yesterday.getDate() - 1);
|
|
|
|
// Check if date is today
|
|
if (date >= today) {
|
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
// Check if date is yesterday
|
|
if (date >= yesterday) {
|
|
return 'Yesterday';
|
|
}
|
|
|
|
// Check if date is this year
|
|
if (date.getFullYear() === now.getFullYear()) {
|
|
return date.toLocaleDateString([], { month: 'short', day: 'numeric' });
|
|
}
|
|
|
|
// Date is from a previous year
|
|
return date.toLocaleDateString([], { year: 'numeric', month: 'short', day: 'numeric' });
|
|
};
|
|
|
|
// Get folder icon
|
|
const getFolderIcon = (folder: string) => {
|
|
switch (folder.toLowerCase()) {
|
|
case 'inbox':
|
|
return <Inbox className="h-4 w-4" />;
|
|
case 'sent':
|
|
case 'sent items':
|
|
return <Send className="h-4 w-4" />;
|
|
case 'drafts':
|
|
return <File className="h-4 w-4" />;
|
|
case 'trash':
|
|
case 'deleted':
|
|
case 'bin':
|
|
return <Trash className="h-4 w-4" />;
|
|
case 'junk':
|
|
case 'spam':
|
|
return <ArchiveIcon className="h-4 w-4" />;
|
|
default:
|
|
return <MailOpen className="h-4 w-4" />;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={cn("h-full flex flex-col bg-white text-slate-900", className)}>
|
|
{/* Main email interface */}
|
|
<div className="flex-1 flex h-full overflow-hidden">
|
|
{/* Sidebar */}
|
|
<div className="hidden md:flex md:w-56 border-r border-gray-100 flex-col">
|
|
{/* Sidebar content */}
|
|
{/* New email button */}
|
|
<div className="p-4">
|
|
<Button
|
|
className="w-full"
|
|
onClick={handleComposeNew}
|
|
>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
New Email
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Folder navigation */}
|
|
<ScrollArea className="flex-1">
|
|
<div className="p-2 space-y-1">
|
|
{folders.map((folder) => (
|
|
<Button
|
|
key={folder}
|
|
variant={currentFolder === folder ? "secondary" : "ghost"}
|
|
className="w-full justify-start"
|
|
onClick={() => handleFolderChange(folder)}
|
|
>
|
|
{getFolderIcon(folder)}
|
|
<span className="ml-2">{folder}</span>
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
|
|
{/* Email list */}
|
|
<div className="w-1/3 border-r border-gray-100 overflow-hidden">
|
|
<EmailList
|
|
emails={emails}
|
|
selectedEmailIds={selectedEmail ? [selectedEmail.emailId] : []}
|
|
selectedEmail={selectedEmail ? {
|
|
id: selectedEmail.emailId,
|
|
accountId: selectedEmail.accountId,
|
|
folder: selectedEmail.folder
|
|
} as Email : null}
|
|
currentFolder={currentFolder}
|
|
isLoading={loading}
|
|
totalEmails={emails.length}
|
|
hasMoreEmails={hasMore}
|
|
onSelectEmail={handleEmailSelect}
|
|
onToggleSelect={() => {}}
|
|
onToggleSelectAll={() => {}}
|
|
onBulkAction={() => {}}
|
|
onToggleStarred={() => {}}
|
|
onLoadMore={loadEmails}
|
|
onSearch={handleSearch}
|
|
previewMode={previewModeEnabled}
|
|
/>
|
|
</div>
|
|
|
|
{/* Email preview */}
|
|
<div className="flex-1 h-full overflow-hidden">
|
|
<EmailPanel
|
|
selectedEmail={selectedEmail}
|
|
onSendEmail={handleSendEmail}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|