courrier refactor

This commit is contained in:
alma 2025-04-26 23:11:58 +02:00
parent b056438814
commit dcc2594195
5 changed files with 223 additions and 172 deletions

View File

@ -2,7 +2,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Loader2, AlertCircle } from 'lucide-react'; import { Loader2, AlertCircle, Mail } from 'lucide-react';
import { Dialog, DialogContent } from '@/components/ui/dialog'; import { Dialog, DialogContent } from '@/components/ui/dialog';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { import {
@ -215,7 +215,7 @@ export default function CourrierPage() {
}; };
return ( return (
<div className="h-full flex flex-col"> <div className="h-screen">
<SimplifiedLoadingFix /> <SimplifiedLoadingFix />
{/* Login required dialog */} {/* Login required dialog */}
@ -259,7 +259,6 @@ export default function CourrierPage() {
{/* Compose email dialog */} {/* Compose email dialog */}
<Dialog open={showComposeModal} onOpenChange={setShowComposeModal}> <Dialog open={showComposeModal} onOpenChange={setShowComposeModal}>
<DialogContent className="sm:max-w-[800px] h-[80vh] p-0"> <DialogContent className="sm:max-w-[800px] h-[80vh] p-0">
{/* Using modern props format for ComposeEmail */}
<ComposeEmail <ComposeEmail
initialEmail={createEmailMessage()} initialEmail={createEmailMessage()}
type={composeType} type={composeType}
@ -271,11 +270,14 @@ export default function CourrierPage() {
{/* Main email interface */} {/* Main email interface */}
<div className="h-full flex flex-col"> <div className="h-full flex flex-col">
{/* Email header */}
<EmailHeader <EmailHeader
onSearch={searchEmails} onSearch={searchEmails}
/> />
{/* Main content area */}
<div className="flex-1 flex overflow-hidden"> <div className="flex-1 flex overflow-hidden">
{/* Sidebar */}
<EmailSidebar <EmailSidebar
currentFolder={currentFolder} currentFolder={currentFolder}
folders={mailboxes} folders={mailboxes}
@ -285,60 +287,72 @@ export default function CourrierPage() {
isLoading={isLoading} isLoading={isLoading}
/> />
<div className="flex-1 h-full flex overflow-hidden"> {/* Email list */}
<div className={`w-[400px] border-r h-full overflow-hidden ${selectedEmail ? '' : 'flex-1'}`}> <EmailList
<EmailList emails={emails}
emails={emails} selectedEmailIds={selectedEmailIds}
selectedEmailIds={selectedEmailIds} selectedEmail={selectedEmail}
selectedEmail={selectedEmail} currentFolder={currentFolder}
currentFolder={currentFolder} isLoading={isLoading}
isLoading={isLoading} totalEmails={emails.length}
totalEmails={emails.length} hasMoreEmails={hasMoreEmails}
hasMoreEmails={hasMoreEmails} onSelectEmail={handleEmailSelect}
onSelectEmail={handleEmailSelect} onToggleSelect={toggleEmailSelection}
onToggleSelect={toggleEmailSelection} onToggleSelectAll={toggleSelectAll}
onToggleSelectAll={toggleSelectAll} onBulkAction={handleBulkAction}
onBulkAction={handleBulkAction} onToggleStarred={toggleStarred}
onToggleStarred={toggleStarred} onLoadMore={handleLoadMore}
onLoadMore={handleLoadMore} onSearch={searchEmails}
/> />
</div>
{/* Email content */}
{selectedEmail && ( <div className="flex-1 h-full overflow-hidden flex flex-col">
<div className="flex-1 h-full overflow-hidden flex flex-col bg-white/95 backdrop-blur-sm"> {selectedEmail ? (
<div className="border-b p-3 flex items-center justify-between"> <>
<div> {/* Email actions header */}
<h2 className="text-xl font-semibold text-gray-900">{selectedEmail.subject || '(No subject)'}</h2> <div className="flex-none px-4 py-3 border-b border-gray-100">
<div className="text-sm text-gray-500"> <div className="flex items-center gap-4">
From: {selectedEmail.from?.[0]?.name || selectedEmail.from?.[0]?.address || 'Unknown'} <div className="flex items-center gap-2 min-w-0 flex-1">
<div className="min-w-0 max-w-[500px]">
<h2 className="text-lg font-semibold text-gray-900 truncate">
{selectedEmail.subject || '(No subject)'}
</h2>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<div className="flex items-center border-l border-gray-200 pl-4">
<button
className="text-gray-600 hover:text-gray-900 px-2 py-1"
onClick={() => handleReplyOrForward('reply')}
>
Reply
</button>
<button
className="text-gray-600 hover:text-gray-900 px-2 py-1"
onClick={() => handleReplyOrForward('reply-all')}
>
Reply All
</button>
<button
className="text-gray-600 hover:text-gray-900 px-2 py-1"
onClick={() => handleReplyOrForward('forward')}
>
Forward
</button>
</div>
</div> </div>
</div>
<div className="flex gap-2">
<button
className="text-sm text-primary hover:underline"
onClick={() => handleReplyOrForward('reply')}
>
Reply
</button>
<button
className="text-sm text-primary hover:underline"
onClick={() => handleReplyOrForward('reply-all')}
>
Reply All
</button>
<button
className="text-sm text-primary hover:underline"
onClick={() => handleReplyOrForward('forward')}
>
Forward
</button>
</div> </div>
</div> </div>
{/* Email content area */}
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<EmailContent email={selectedEmail} /> <EmailContent email={selectedEmail} />
</div> </div>
</>
) : (
<div className="flex flex-col items-center justify-center h-full">
<Mail className="h-12 w-12 text-gray-400 mb-4" />
<p className="text-gray-500">Select an email to view its contents</p>
</div> </div>
)} )}
</div> </div>

View File

@ -1,11 +1,12 @@
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Loader2, Paperclip, FileDown } from 'lucide-react'; import { Loader2, Paperclip, FileDown, Download } from 'lucide-react';
import { sanitizeHtml } from '@/lib/utils/email-formatter'; import { sanitizeHtml } from '@/lib/utils/email-formatter';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Email } from '@/hooks/use-courrier'; import { Email } from '@/hooks/use-courrier';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { ScrollArea } from '@/components/ui/scroll-area';
interface EmailContentProps { interface EmailContentProps {
email: Email; email: Email;
@ -62,11 +63,12 @@ export default function EmailContent({ email }: EmailContentProps) {
<h3 className="text-sm font-semibold text-gray-900 mb-4">Attachments</h3> <h3 className="text-sm font-semibold text-gray-900 mb-4">Attachments</h3>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{email.attachments.map((attachment, index) => ( {email.attachments.map((attachment, index) => (
<div key={index} className="flex items-center space-x-2 p-2 border rounded"> <div key={index} className="flex items-center space-x-2 p-2 border rounded hover:bg-gray-50">
<Paperclip className="h-4 w-4 text-gray-400" /> <Paperclip className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-600 truncate"> <span className="text-sm text-gray-600 truncate flex-1">
{attachment.filename} {attachment.filename}
</span> </span>
<Download className="h-4 w-4 text-gray-400 hover:text-gray-600 cursor-pointer" />
</div> </div>
))} ))}
</div> </div>
@ -92,9 +94,27 @@ export default function EmailContent({ email }: EmailContentProps) {
} }
return ( return (
<div className="p-6"> <ScrollArea className="flex-1">
{content} <div className="p-6">
{renderAttachments()} <div className="flex items-center gap-4 mb-6">
</div> <div className="h-10 w-10 bg-gray-100 rounded-full flex items-center justify-center">
<span className="text-gray-600 font-medium">
{email.from?.[0]?.name?.[0] || email.from?.[0]?.address?.[0] || '?'}
</span>
</div>
<div>
<p className="font-medium text-gray-900">
{email.from?.[0]?.name || email.from?.[0]?.address || 'Unknown'}
</p>
<p className="text-sm text-gray-500">
to {email.to?.[0]?.address || 'you'}
</p>
</div>
</div>
{content}
{renderAttachments()}
</div>
</ScrollArea>
); );
} }

View File

@ -1,12 +1,12 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Loader2, Mail } from 'lucide-react'; import { Loader2, Mail, Search, X } from 'lucide-react';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Email } from '@/hooks/use-courrier'; import { Email } from '@/hooks/use-courrier';
import EmailListItem from './EmailListItem'; import EmailListItem from './EmailListItem';
import EmailListHeader from './EmailListHeader'; import EmailListHeader from './EmailListHeader';
import BulkActionsToolbar from './BulkActionsToolbar'; import BulkActionsToolbar from './BulkActionsToolbar';
import { Input } from '@/components/ui/input';
interface EmailListProps { interface EmailListProps {
emails: Email[]; emails: Email[];
@ -22,6 +22,7 @@ interface EmailListProps {
onBulkAction: (action: 'delete' | 'mark-read' | 'mark-unread' | 'archive') => void; onBulkAction: (action: 'delete' | 'mark-read' | 'mark-unread' | 'archive') => void;
onToggleStarred: (emailId: string) => void; onToggleStarred: (emailId: string) => void;
onLoadMore: () => void; onLoadMore: () => void;
onSearch?: (query: string) => void;
} }
export default function EmailList({ export default function EmailList({
@ -37,9 +38,11 @@ export default function EmailList({
onToggleSelectAll, onToggleSelectAll,
onBulkAction, onBulkAction,
onToggleStarred, onToggleStarred,
onLoadMore onLoadMore,
onSearch
}: EmailListProps) { }: EmailListProps) {
const [scrollPosition, setScrollPosition] = useState(0); const [scrollPosition, setScrollPosition] = useState(0);
const [searchQuery, setSearchQuery] = useState('');
// Handle scroll to detect when user reaches the bottom // Handle scroll to detect when user reaches the bottom
const handleScroll = (event: React.UIEvent<HTMLDivElement>) => { const handleScroll = (event: React.UIEvent<HTMLDivElement>) => {
@ -54,6 +57,17 @@ export default function EmailList({
} }
}; };
// Handle search
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
onSearch?.(searchQuery);
};
const clearSearch = () => {
setSearchQuery('');
onSearch?.('');
};
// Render loading state // Render loading state
if (isLoading && emails.length === 0) { if (isLoading && emails.length === 0) {
return ( return (
@ -66,12 +80,14 @@ export default function EmailList({
// Render empty state // Render empty state
if (emails.length === 0) { if (emails.length === 0) {
return ( return (
<div className="flex flex-col justify-center items-center h-full p-8 text-center bg-white/95 backdrop-blur-sm"> <div className="flex flex-col justify-center items-center h-64 p-8 text-center bg-white/95 backdrop-blur-sm">
<Mail className="h-8 w-8 text-gray-400 mb-2" /> <Mail className="h-8 w-8 text-gray-400 mb-2" />
<p className="text-gray-500 text-sm"> <p className="text-gray-500 text-sm">
{currentFolder === 'INBOX' {searchQuery
? "Your inbox is empty. You're all caught up!" ? 'No emails match your search'
: `No emails in this folder`} : currentFolder === 'INBOX'
? "Your inbox is empty. You're all caught up!"
: 'No emails in this folder'}
</p> </p>
</div> </div>
); );
@ -84,14 +100,40 @@ export default function EmailList({
const someSelected = selectedEmailIds.length > 0 && selectedEmailIds.length < emails.length; const someSelected = selectedEmailIds.length > 0 && selectedEmailIds.length < emails.length;
return ( return (
<div className="flex flex-col h-full bg-white/95 backdrop-blur-sm"> <div className="w-[320px] bg-white/95 backdrop-blur-sm border-r border-gray-100 flex flex-col">
<EmailListHeader {/* Search header */}
allSelected={allSelected} <div className="border-b border-gray-100">
someSelected={someSelected} <div className="px-4 py-2">
onToggleSelectAll={onToggleSelectAll} <div className="relative">
currentFolder={currentFolder} <Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-400" />
totalEmails={totalEmails} <form onSubmit={handleSearch}>
/> <Input
type="search"
placeholder="Search in folder..."
className="pl-8 h-9 bg-gray-50"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{searchQuery && (
<button
type="button"
onClick={clearSearch}
className="absolute right-2 top-1/2 transform -translate-y-1/2"
>
<X className="h-4 w-4 text-gray-400" />
</button>
)}
</form>
</div>
</div>
<EmailListHeader
allSelected={allSelected}
someSelected={someSelected}
onToggleSelectAll={onToggleSelectAll}
currentFolder={currentFolder}
totalEmails={totalEmails}
/>
</div>
{selectedEmailIds.length > 0 && ( {selectedEmailIds.length > 0 && (
<BulkActionsToolbar <BulkActionsToolbar

View File

@ -3,13 +3,6 @@
import React from 'react'; import React from 'react';
import { ChevronDown, Inbox } from 'lucide-react'; import { ChevronDown, Inbox } from 'lucide-react';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
interface EmailListHeaderProps { interface EmailListHeaderProps {
allSelected: boolean; allSelected: boolean;
@ -27,26 +20,24 @@ export default function EmailListHeader({
totalEmails = 0 totalEmails = 0
}: EmailListHeaderProps) { }: EmailListHeaderProps) {
return ( return (
<div className="border-b border-gray-100 bg-white/95"> <div className="flex items-center justify-between px-4 h-14">
<div className="flex items-center justify-between px-4 h-14"> <div className="flex items-center gap-3">
<div className="flex items-center gap-3"> <Checkbox
<Checkbox checked={allSelected}
checked={allSelected} ref={(input) => {
ref={(input) => { if (input) {
if (input) { (input as unknown as HTMLInputElement).indeterminate = someSelected && !allSelected;
(input as unknown as HTMLInputElement).indeterminate = someSelected && !allSelected; }
} }}
}} onCheckedChange={onToggleSelectAll}
onCheckedChange={onToggleSelectAll} className="mt-0.5"
className="mt-0.5" />
/> <h2 className="text-base font-semibold text-gray-900 capitalize">{currentFolder.toLowerCase()}</h2>
<h2 className="text-base font-semibold text-gray-900 capitalize">{currentFolder.toLowerCase()}</h2>
</div>
<span className="text-sm text-gray-600">
{totalEmails} emails
</span>
</div> </div>
<span className="text-sm text-gray-600">
{totalEmails} {totalEmails === 1 ? 'email' : 'emails'}
</span>
</div> </div>
); );
} }

View File

@ -3,7 +3,7 @@
import React from 'react'; import React from 'react';
import { import {
Inbox, Send, Trash, Archive, Star, Inbox, Send, Trash, Archive, Star,
File, RefreshCw, Plus, MailOpen File, RefreshCw, Plus, MailOpen, Settings
} from 'lucide-react'; } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@ -65,94 +65,78 @@ export default function EmailSidebar({
); );
return ( return (
<aside className="w-60 border-r h-full flex flex-col bg-white/95 backdrop-blur-sm"> <aside className="w-64 border-r h-full flex flex-col bg-white/95 backdrop-blur-sm">
<div className="p-2 border-b border-gray-100"> {/* Compose button area */}
<div className="p-4">
<Button <Button
className="w-full gap-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center justify-center transition-all py-1.5 text-sm" className="w-full bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center justify-center py-2"
size="sm"
onClick={onCompose} onClick={onCompose}
> >
<Plus className="h-3.5 w-3.5" /> <Plus className="h-4 w-4 mr-2" />
Compose New Email
</Button>
</div>
<div className="px-2 py-2 border-b border-gray-100 flex items-center gap-2">
<Button
variant="ghost"
size="icon"
className="text-gray-600 hover:text-gray-900 hover:bg-gray-100 h-8 w-8"
onClick={onRefresh}
disabled={isLoading}
>
<RefreshCw className={cn(
"h-4 w-4",
isLoading && "animate-spin"
)} />
</Button> </Button>
</div> </div>
{/* Folder navigation */}
<ScrollArea className="flex-1"> <ScrollArea className="flex-1">
<div className="p-3"> <div className="p-2 space-y-1">
<div className="text-sm font-medium text-gray-500 mb-2"> {visibleStandardFolders.map((folder) => (
Folders <Button
</div> key={folder}
variant={currentFolder === folder ? "secondary" : "ghost"}
{/* Standard folders */} className={`w-full justify-start ${
<ul className="space-y-0.5 px-2"> currentFolder === folder ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900'
{visibleStandardFolders.map(folder => ( }`}
<li key={folder}> onClick={() => onFolderChange(folder)}
<Button >
variant={currentFolder === folder ? "secondary" : "ghost"} <div className="flex items-center w-full">
className={`w-full justify-start py-2 ${ <span className="flex items-center">
currentFolder === folder ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900' {getFolderIcon(folder)}
}`} <span className="ml-2 capitalize">{folder.toLowerCase()}</span>
onClick={() => onFolderChange(folder)} </span>
> {folder === 'INBOX' && (
<div className="flex items-center justify-between w-full"> <span className="ml-auto bg-blue-600 text-white text-xs px-2 py-0.5 rounded-full">
<div className="flex items-center"> {/* Unread count would go here */}
{getFolderIcon(folder)} </span>
<span className="ml-2 capitalize">{folder.toLowerCase()}</span> )}
</div> </div>
{folder === 'INBOX' && ( </Button>
<span className="ml-auto bg-blue-600 text-white text-xs px-2 py-0.5 rounded-full"> ))}
{/* Unread count would go here */}
</span>
)}
</div>
</Button>
</li>
))}
</ul>
{/* Custom folders section */} {/* Custom folders section */}
{customFolders.length > 0 && ( {customFolders.length > 0 && (
<> <>
<div className="text-sm font-medium text-gray-500 mt-4 mb-2"> {customFolders.map(folder => (
Custom Folders <Button
</div> key={folder}
<ul className="space-y-0.5 px-2"> variant={currentFolder === folder ? "secondary" : "ghost"}
{customFolders.map(folder => ( className={`w-full justify-start ${
<li key={folder}> currentFolder === folder ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900'
<Button }`}
variant={currentFolder === folder ? "secondary" : "ghost"} onClick={() => onFolderChange(folder)}
className={`w-full justify-start py-2 ${ >
currentFolder === folder ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900' <div className="flex items-center">
}`} {getFolderIcon(folder)}
onClick={() => onFolderChange(folder)} <span className="ml-2 truncate">{folder}</span>
> </div>
<div className="flex items-center"> </Button>
{getFolderIcon(folder)} ))}
<span className="ml-2 truncate">{folder}</span>
</div>
</Button>
</li>
))}
</ul>
</> </>
)} )}
</div> </div>
</ScrollArea> </ScrollArea>
{/* Settings button (bottom) */}
<div className="p-2 border-t border-gray-100">
<Button
variant="ghost"
size="sm"
className="w-full justify-start text-gray-600 hover:text-gray-900"
>
<Settings className="h-4 w-4 mr-2" />
<span>Email settings</span>
</Button>
</div>
</aside> </aside>
); );
} }