373 lines
13 KiB
TypeScript
373 lines
13 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";
|
|
|
|
interface EmailLayoutProps {
|
|
className?: string;
|
|
}
|
|
|
|
export default function EmailLayout({ className = '' }: EmailLayoutProps) {
|
|
// Email state
|
|
const [emails, setEmails] = useState<EmailMessage[]>([]);
|
|
const [selectedEmailId, setSelectedEmailId] = useState<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);
|
|
setSelectedEmailId(null);
|
|
setPage(1);
|
|
setSearchQuery('');
|
|
};
|
|
|
|
// Handle email selection
|
|
const handleEmailSelect = (id: string) => {
|
|
setSelectedEmailId(id);
|
|
};
|
|
|
|
// 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 = () => {
|
|
setSelectedEmailId(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={`flex h-full bg-background ${className}`}>
|
|
{/* Sidebar */}
|
|
<div className="w-64 border-r h-full flex flex-col">
|
|
{/* 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>
|
|
|
|
{/* Main content */}
|
|
<div className="flex-1 flex flex-col lg:flex-row h-full">
|
|
{/* Email list */}
|
|
<div className="w-full lg:w-96 border-r h-full flex flex-col overflow-hidden">
|
|
{/* Search and refresh */}
|
|
<div className="p-2 border-b flex items-center gap-2">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
placeholder="Search emails..."
|
|
className="pl-8"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
|
/>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={handleRefresh}
|
|
disabled={loading}
|
|
>
|
|
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Email list */}
|
|
<ScrollArea className="flex-1">
|
|
{loading && emails.length === 0 ? (
|
|
<div className="flex items-center justify-center h-32">
|
|
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
|
</div>
|
|
) : emails.length === 0 ? (
|
|
<div className="flex items-center justify-center h-32 text-muted-foreground">
|
|
No emails found
|
|
</div>
|
|
) : (
|
|
<div className="divide-y">
|
|
{emails.map((email) => (
|
|
<div
|
|
key={email.id}
|
|
className={`p-3 hover:bg-secondary/20 cursor-pointer transition-colors ${
|
|
selectedEmailId === email.id ? 'bg-secondary/30' : ''
|
|
} ${!email.flags.seen ? 'bg-primary/5' : ''}`}
|
|
onClick={() => handleEmailSelect(email.id)}
|
|
>
|
|
<div className="flex items-start gap-2">
|
|
<div className="pt-0.5">
|
|
{email.flags.seen ? (
|
|
<MailOpen className="h-4 w-4 text-muted-foreground" />
|
|
) : (
|
|
<Mail className="h-4 w-4 text-primary" />
|
|
)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex justify-between items-baseline">
|
|
<p className={`text-sm truncate font-medium ${!email.flags.seen ? 'text-primary' : ''}`}>
|
|
{email.from[0]?.name || email.from[0]?.address || 'Unknown'}
|
|
</p>
|
|
<span className="text-xs text-muted-foreground whitespace-nowrap ml-2">
|
|
{formatDate(email.date.toString())}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm font-medium truncate">{email.subject}</p>
|
|
<p className="text-xs text-muted-foreground truncate">{email.preview}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Email indicators */}
|
|
<div className="flex items-center mt-1 gap-1">
|
|
{email.hasAttachments && (
|
|
<Badge variant="outline" className="text-xs py-0 h-5">
|
|
<span className="flex items-center">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="w-3 h-3 mr-1">
|
|
<path fillRule="evenodd" d="M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z" clipRule="evenodd" />
|
|
</svg>
|
|
Attachment
|
|
</span>
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</ScrollArea>
|
|
</div>
|
|
|
|
{/* Email preview */}
|
|
<div className="flex-1 h-full overflow-hidden">
|
|
<EmailPanel
|
|
selectedEmailId={selectedEmailId}
|
|
folder={currentFolder}
|
|
onSendEmail={handleSendEmail}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|