Neah/components/email/EmailLayout.tsx

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, accountId: string, folder: 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, email.accountId || '', email.folder || '')}
>
<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>
);
}