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 { 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 { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import {
@ -215,7 +215,7 @@ export default function CourrierPage() {
};
return (
<div className="h-full flex flex-col">
<div className="h-screen">
<SimplifiedLoadingFix />
{/* Login required dialog */}
@ -259,7 +259,6 @@ export default function CourrierPage() {
{/* Compose email dialog */}
<Dialog open={showComposeModal} onOpenChange={setShowComposeModal}>
<DialogContent className="sm:max-w-[800px] h-[80vh] p-0">
{/* Using modern props format for ComposeEmail */}
<ComposeEmail
initialEmail={createEmailMessage()}
type={composeType}
@ -271,11 +270,14 @@ export default function CourrierPage() {
{/* Main email interface */}
<div className="h-full flex flex-col">
{/* Email header */}
<EmailHeader
onSearch={searchEmails}
/>
{/* Main content area */}
<div className="flex-1 flex overflow-hidden">
{/* Sidebar */}
<EmailSidebar
currentFolder={currentFolder}
folders={mailboxes}
@ -285,60 +287,72 @@ export default function CourrierPage() {
isLoading={isLoading}
/>
<div className="flex-1 h-full flex overflow-hidden">
<div className={`w-[400px] border-r h-full overflow-hidden ${selectedEmail ? '' : 'flex-1'}`}>
<EmailList
emails={emails}
selectedEmailIds={selectedEmailIds}
selectedEmail={selectedEmail}
currentFolder={currentFolder}
isLoading={isLoading}
totalEmails={emails.length}
hasMoreEmails={hasMoreEmails}
onSelectEmail={handleEmailSelect}
onToggleSelect={toggleEmailSelection}
onToggleSelectAll={toggleSelectAll}
onBulkAction={handleBulkAction}
onToggleStarred={toggleStarred}
onLoadMore={handleLoadMore}
/>
</div>
{selectedEmail && (
<div className="flex-1 h-full overflow-hidden flex flex-col bg-white/95 backdrop-blur-sm">
<div className="border-b p-3 flex items-center justify-between">
<div>
<h2 className="text-xl font-semibold text-gray-900">{selectedEmail.subject || '(No subject)'}</h2>
<div className="text-sm text-gray-500">
From: {selectedEmail.from?.[0]?.name || selectedEmail.from?.[0]?.address || 'Unknown'}
{/* Email list */}
<EmailList
emails={emails}
selectedEmailIds={selectedEmailIds}
selectedEmail={selectedEmail}
currentFolder={currentFolder}
isLoading={isLoading}
totalEmails={emails.length}
hasMoreEmails={hasMoreEmails}
onSelectEmail={handleEmailSelect}
onToggleSelect={toggleEmailSelection}
onToggleSelectAll={toggleSelectAll}
onBulkAction={handleBulkAction}
onToggleStarred={toggleStarred}
onLoadMore={handleLoadMore}
onSearch={searchEmails}
/>
{/* Email content */}
<div className="flex-1 h-full overflow-hidden flex flex-col">
{selectedEmail ? (
<>
{/* Email actions header */}
<div className="flex-none px-4 py-3 border-b border-gray-100">
<div className="flex items-center gap-4">
<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 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>
{/* Email content area */}
<div className="flex-1 overflow-auto">
<EmailContent email={selectedEmail} />
</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>

View File

@ -1,11 +1,12 @@
'use client';
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 { Button } from '@/components/ui/button';
import { Email } from '@/hooks/use-courrier';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { ScrollArea } from '@/components/ui/scroll-area';
interface EmailContentProps {
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>
<div className="grid grid-cols-2 gap-4">
{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" />
<span className="text-sm text-gray-600 truncate">
<span className="text-sm text-gray-600 truncate flex-1">
{attachment.filename}
</span>
<Download className="h-4 w-4 text-gray-400 hover:text-gray-600 cursor-pointer" />
</div>
))}
</div>
@ -92,9 +94,27 @@ export default function EmailContent({ email }: EmailContentProps) {
}
return (
<div className="p-6">
{content}
{renderAttachments()}
</div>
<ScrollArea className="flex-1">
<div className="p-6">
<div className="flex items-center gap-4 mb-6">
<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';
import React, { useState } from 'react';
import { Loader2, Mail } from 'lucide-react';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Loader2, Mail, Search, X } from 'lucide-react';
import { Email } from '@/hooks/use-courrier';
import EmailListItem from './EmailListItem';
import EmailListHeader from './EmailListHeader';
import BulkActionsToolbar from './BulkActionsToolbar';
import { Input } from '@/components/ui/input';
interface EmailListProps {
emails: Email[];
@ -22,6 +22,7 @@ interface EmailListProps {
onBulkAction: (action: 'delete' | 'mark-read' | 'mark-unread' | 'archive') => void;
onToggleStarred: (emailId: string) => void;
onLoadMore: () => void;
onSearch?: (query: string) => void;
}
export default function EmailList({
@ -37,9 +38,11 @@ export default function EmailList({
onToggleSelectAll,
onBulkAction,
onToggleStarred,
onLoadMore
onLoadMore,
onSearch
}: EmailListProps) {
const [scrollPosition, setScrollPosition] = useState(0);
const [searchQuery, setSearchQuery] = useState('');
// Handle scroll to detect when user reaches the bottom
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
if (isLoading && emails.length === 0) {
return (
@ -66,12 +80,14 @@ export default function EmailList({
// Render empty state
if (emails.length === 0) {
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" />
<p className="text-gray-500 text-sm">
{currentFolder === 'INBOX'
? "Your inbox is empty. You're all caught up!"
: `No emails in this folder`}
{searchQuery
? 'No emails match your search'
: currentFolder === 'INBOX'
? "Your inbox is empty. You're all caught up!"
: 'No emails in this folder'}
</p>
</div>
);
@ -84,14 +100,40 @@ export default function EmailList({
const someSelected = selectedEmailIds.length > 0 && selectedEmailIds.length < emails.length;
return (
<div className="flex flex-col h-full bg-white/95 backdrop-blur-sm">
<EmailListHeader
allSelected={allSelected}
someSelected={someSelected}
onToggleSelectAll={onToggleSelectAll}
currentFolder={currentFolder}
totalEmails={totalEmails}
/>
<div className="w-[320px] bg-white/95 backdrop-blur-sm border-r border-gray-100 flex flex-col">
{/* Search header */}
<div className="border-b border-gray-100">
<div className="px-4 py-2">
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-400" />
<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 && (
<BulkActionsToolbar

View File

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

View File

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