courrier formatting
This commit is contained in:
parent
f011bfc43e
commit
e02ee396dc
@ -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>
|
||||||
|
|||||||
@ -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}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user