courrier formatting

This commit is contained in:
alma 2025-04-30 15:44:06 +02:00
parent f011bfc43e
commit e02ee396dc
3 changed files with 143 additions and 117 deletions

View File

@ -15,19 +15,12 @@ import { EmailMessage } from '@/lib/services/email-service';
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import { sendEmail } from '@/lib/services/email-service'; import { sendEmail } from '@/lib/services/email-service';
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { cn } from '@/lib/utils';
import EmailList from './EmailList';
import { Email } from '@/hooks/use-courrier';
interface EmailLayoutProps { interface EmailLayoutProps {
className?: string; className?: string;
previewModeEnabled?: boolean;
} }
export default function EmailLayout({ export default function EmailLayout({ className = '' }: EmailLayoutProps) {
className = '',
previewModeEnabled = false
}: EmailLayoutProps) {
// Email state // Email state
const [emails, setEmails] = useState<EmailMessage[]>([]); const [emails, setEmails] = useState<EmailMessage[]>([]);
const [selectedEmail, setSelectedEmail] = useState<{ const [selectedEmail, setSelectedEmail] = useState<{
@ -250,70 +243,131 @@ export default function EmailLayout({
}; };
return ( return (
<div className={cn("h-full flex flex-col bg-white text-slate-900", className)}> <div className={`flex h-full bg-background ${className}`}>
{/* Main email interface */} {/* Sidebar */}
<div className="flex-1 flex h-full overflow-hidden"> <div className="w-64 border-r h-full flex flex-col">
{/* Sidebar */} {/* New email button */}
<div className="hidden md:flex md:w-56 border-r border-gray-100 flex-col"> <div className="p-4">
{/* Sidebar content */} <Button
{/* New email button */} className="w-full"
<div className="p-4"> onClick={handleComposeNew}
<Button >
className="w-full" <Plus className="h-4 w-4 mr-2" />
onClick={handleComposeNew} 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}
> >
<Plus className="h-4 w-4 mr-2" /> <RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
New Email
</Button> </Button>
</div> </div>
{/* Folder navigation */} {/* Email list */}
<ScrollArea className="flex-1"> <ScrollArea className="flex-1">
<div className="p-2 space-y-1"> {loading && emails.length === 0 ? (
{folders.map((folder) => ( <div className="flex items-center justify-center h-32">
<Button <Loader2 className="h-6 w-6 animate-spin text-primary" />
key={folder} </div>
variant={currentFolder === folder ? "secondary" : "ghost"} ) : emails.length === 0 ? (
className="w-full justify-start" <div className="flex items-center justify-center h-32 text-muted-foreground">
onClick={() => handleFolderChange(folder)} No emails found
> </div>
{getFolderIcon(folder)} ) : (
<span className="ml-2">{folder}</span> <div className="divide-y">
</Button> {emails.map((email) => (
))} <div
</div> key={email.id}
className={`p-3 hover:bg-secondary/20 cursor-pointer transition-colors ${
selectedEmail?.emailId === 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> </ScrollArea>
</div> </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 */} {/* Email preview */}
<div className="flex-1 h-full overflow-hidden"> <div className="flex-1 h-full overflow-hidden">
<EmailPanel <EmailPanel
selectedEmail={selectedEmail} selectedEmail={selectedEmail}
folder={currentFolder}
onSendEmail={handleSendEmail} onSendEmail={handleSendEmail}
/> />
</div> </div>

View File

@ -23,7 +23,6 @@ interface EmailListProps {
onToggleStarred: (emailId: string) => void; onToggleStarred: (emailId: string) => void;
onLoadMore: () => void; onLoadMore: () => void;
onSearch?: (query: string) => void; onSearch?: (query: string) => void;
previewMode?: boolean;
} }
export default function EmailList({ export default function EmailList({
@ -40,44 +39,33 @@ export default function EmailList({
onBulkAction, onBulkAction,
onToggleStarred, onToggleStarred,
onLoadMore, onLoadMore,
onSearch, onSearch
previewMode = false
}: EmailListProps) { }: EmailListProps) {
const [scrollPosition, setScrollPosition] = useState(0); const [scrollPosition, setScrollPosition] = useState(0);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
// Handle scroll to detect when user is near the bottom // Handle scroll to detect when user reaches the bottom
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => { const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
const target = e.target as HTMLDivElement; const target = event.target as HTMLDivElement;
const scrollTop = target.scrollTop; const { scrollTop, scrollHeight, clientHeight } = target;
setScrollPosition(scrollTop); setScrollPosition(scrollTop);
// Check if we're near the bottom // If user scrolls near the bottom and we have more emails, load more
const scrollHeight = target.scrollHeight; if (scrollHeight - scrollTop - clientHeight < 200 && hasMoreEmails && !isLoading) {
const clientHeight = target.clientHeight;
const scrollPosition = scrollTop + clientHeight;
// If we're within 100px of the bottom, load more
if (scrollHeight - scrollPosition < 100 && !isLoading && hasMoreEmails) {
onLoadMore(); onLoadMore();
} }
}; };
// Handle search form submission // Handle search
const handleSearch = (e: React.FormEvent) => { const handleSearch = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
onSearch?.(searchQuery);
if (onSearch && searchQuery.trim()) {
onSearch(searchQuery.trim());
}
}; };
// Clear search
const clearSearch = () => { const clearSearch = () => {
setSearchQuery(''); setSearchQuery('');
if (onSearch) { onSearch?.('');
onSearch('');
}
}; };
// Render loading state // Render loading state
@ -138,27 +126,16 @@ export default function EmailList({
</form> </form>
</div> </div>
</div> </div>
{/* Only show selection UI if not in preview mode */} <EmailListHeader
{!previewMode ? ( allSelected={allSelected}
<EmailListHeader someSelected={someSelected}
allSelected={allSelected} onToggleSelectAll={onToggleSelectAll}
someSelected={someSelected} currentFolder={currentFolder}
onToggleSelectAll={onToggleSelectAll} totalEmails={totalEmails}
currentFolder={currentFolder} />
totalEmails={totalEmails}
/>
) : (
<div className="flex items-center justify-between px-4 h-14">
<h2 className="text-base font-semibold text-gray-900 capitalize">Messages</h2>
<span className="text-sm text-gray-600">
{totalEmails} {totalEmails === 1 ? 'email' : 'emails'}
</span>
</div>
)}
</div> </div>
{/* Only show bulk actions if not in preview mode and there are selected emails */} {selectedEmailIds.length > 0 && (
{!previewMode && selectedEmailIds.length > 0 && (
<BulkActionsToolbar <BulkActionsToolbar
selectedCount={selectedEmailIds.length} selectedCount={selectedEmailIds.length}
onBulkAction={onBulkAction} onBulkAction={onBulkAction}
@ -185,7 +162,6 @@ export default function EmailList({
e.stopPropagation(); e.stopPropagation();
onToggleStarred(email.id); onToggleStarred(email.id);
}} }}
previewMode={previewMode}
/> />
))} ))}

View File

@ -15,7 +15,6 @@ interface EmailListItemProps {
onSelect: () => void; onSelect: () => void;
onToggleSelect: (e: React.MouseEvent) => void; onToggleSelect: (e: React.MouseEvent) => void;
onToggleStarred: (e: React.MouseEvent) => void; onToggleStarred: (e: React.MouseEvent) => void;
previewMode?: boolean;
} }
const PREVIEW_LENGTH = 70; const PREVIEW_LENGTH = 70;
@ -26,8 +25,7 @@ export default function EmailListItem({
isActive, isActive,
onSelect, onSelect,
onToggleSelect, onToggleSelect,
onToggleStarred, onToggleStarred
previewMode = false
}: EmailListItemProps) { }: EmailListItemProps) {
// Format the date in a readable way // Format the date in a readable way
const formatDate = (dateString: string) => { const formatDate = (dateString: string) => {
@ -127,34 +125,32 @@ export default function EmailListItem({
className={cn( className={cn(
'flex items-center gap-3 px-4 py-2 hover:bg-gray-50/80 cursor-pointer', 'flex items-center gap-3 px-4 py-2 hover:bg-gray-50/80 cursor-pointer',
isActive ? 'bg-blue-50/50' : '', isActive ? 'bg-blue-50/50' : '',
!email.flags?.seen ? 'bg-blue-50/20' : '' !email.read ? 'bg-blue-50/20' : ''
)} )}
onClick={onSelect} onClick={onSelect}
> >
{!previewMode && ( <Checkbox
<Checkbox checked={isSelected}
checked={isSelected} onClick={onToggleSelect}
onClick={onToggleSelect} className="mt-0.5"
className="mt-0.5" />
/>
)}
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
<span className={`text-sm truncate ${!email.flags?.seen ? 'font-semibold text-gray-900' : 'text-gray-600'}`}> <span className={`text-sm truncate ${!email.read ? 'font-semibold text-gray-900' : 'text-gray-600'}`}>
{getSenderName()} {getSenderName()}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 flex-shrink-0"> <div className="flex items-center gap-2 flex-shrink-0">
<span className="text-xs text-gray-500 whitespace-nowrap"> <span className="text-xs text-gray-500 whitespace-nowrap">
{formatDate(typeof email.date === 'string' ? email.date : email.date.toString())} {formatDate(email.date)}
</span> </span>
<button <button
className="h-6 w-6 text-gray-400 hover:text-yellow-400" className="h-6 w-6 text-gray-400 hover:text-yellow-400"
onClick={onToggleStarred} onClick={onToggleStarred}
> >
<Star className={`h-4 w-4 ${email.flags?.flagged ? 'fill-yellow-400 text-yellow-400' : ''}`} /> <Star className={`h-4 w-4 ${email.starred ? 'fill-yellow-400 text-yellow-400' : ''}`} />
</button> </button>
</div> </div>
</div> </div>