courrier refactor
This commit is contained in:
parent
367b79bf0b
commit
b056438814
@ -305,11 +305,11 @@ export default function CourrierPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedEmail && (
|
{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">
|
||||||
<div className="border-b p-3 flex items-center justify-between">
|
<div className="border-b p-3 flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xl font-semibold">{selectedEmail.subject || '(No subject)'}</h2>
|
<h2 className="text-xl font-semibold text-gray-900">{selectedEmail.subject || '(No subject)'}</h2>
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-gray-500">
|
||||||
From: {selectedEmail.from?.[0]?.name || selectedEmail.from?.[0]?.address || 'Unknown'}
|
From: {selectedEmail.from?.[0]?.name || selectedEmail.from?.[0]?.address || 'Unknown'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,14 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Trash, Mail, MailOpen, Archive } from 'lucide-react';
|
import { Trash2, EyeOff, Archive } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipProvider,
|
|
||||||
TooltipTrigger
|
|
||||||
} from '@/components/ui/tooltip';
|
|
||||||
|
|
||||||
interface BulkActionsToolbarProps {
|
interface BulkActionsToolbarProps {
|
||||||
selectedCount: number;
|
selectedCount: number;
|
||||||
@ -20,74 +14,41 @@ export default function BulkActionsToolbar({
|
|||||||
onBulkAction
|
onBulkAction
|
||||||
}: BulkActionsToolbarProps) {
|
}: BulkActionsToolbarProps) {
|
||||||
return (
|
return (
|
||||||
<div className="border-b p-2 flex items-center gap-2 bg-accent/20">
|
<div className="bg-white border-b border-gray-100 px-4 py-2">
|
||||||
<div className="text-xs font-medium flex-1">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
{selectedCount} {selectedCount === 1 ? 'message' : 'messages'} selected
|
<span className="text-sm text-gray-600">
|
||||||
|
{selectedCount} selected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-gray-600 hover:text-gray-900 h-8 px-2"
|
||||||
|
onClick={() => onBulkAction('mark-read')}
|
||||||
|
>
|
||||||
|
<EyeOff className="h-4 w-4 mr-1" />
|
||||||
|
<span className="text-sm">Mark Read</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-gray-600 hover:text-gray-900 h-8 px-2"
|
||||||
|
onClick={() => onBulkAction('archive')}
|
||||||
|
>
|
||||||
|
<Archive className="h-4 w-4 mr-1" />
|
||||||
|
<span className="text-sm">Archive</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="text-red-600 hover:text-red-700 h-8 px-2"
|
||||||
|
onClick={() => onBulkAction('delete')}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-1" />
|
||||||
|
<span className="text-sm">Delete</span>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8"
|
|
||||||
onClick={() => onBulkAction('mark-read')}
|
|
||||||
>
|
|
||||||
<MailOpen className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Mark as read</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8"
|
|
||||||
onClick={() => onBulkAction('mark-unread')}
|
|
||||||
>
|
|
||||||
<Mail className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Mark as unread</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8"
|
|
||||||
onClick={() => onBulkAction('archive')}
|
|
||||||
>
|
|
||||||
<Archive className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Archive</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="h-8 w-8 text-destructive hover:text-destructive"
|
|
||||||
onClick={() => onBulkAction('delete')}
|
|
||||||
>
|
|
||||||
<Trash className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Delete</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -34,7 +34,7 @@ export default function EmailContent({ email }: EmailContentProps) {
|
|||||||
|
|
||||||
setContent(
|
setContent(
|
||||||
<div
|
<div
|
||||||
className="email-content prose prose-sm max-w-none dark:prose-invert"
|
className="email-content prose max-w-none"
|
||||||
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
||||||
dir="auto"
|
dir="auto"
|
||||||
/>
|
/>
|
||||||
@ -58,30 +58,16 @@ export default function EmailContent({ email }: EmailContentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-t mt-4 pt-4">
|
<div className="mt-6 border-t border-gray-200 pt-6">
|
||||||
<h3 className="text-sm font-medium mb-2 flex items-center gap-1">
|
<h3 className="text-sm font-semibold text-gray-900 mb-4">Attachments</h3>
|
||||||
<Paperclip className="h-4 w-4" />
|
<div className="grid grid-cols-2 gap-4">
|
||||||
Attachments ({email.attachments.length})
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{email.attachments.map((attachment, index) => (
|
{email.attachments.map((attachment, index) => (
|
||||||
<Button
|
<div key={index} className="flex items-center space-x-2 p-2 border rounded">
|
||||||
key={index}
|
<Paperclip className="h-4 w-4 text-gray-400" />
|
||||||
variant="outline"
|
<span className="text-sm text-gray-600 truncate">
|
||||||
size="sm"
|
{attachment.filename}
|
||||||
className="flex items-center gap-1 text-xs"
|
</span>
|
||||||
asChild
|
</div>
|
||||||
>
|
|
||||||
<a
|
|
||||||
href={`/api/courrier/${email.id}/attachment/${encodeURIComponent(attachment.filename)}`}
|
|
||||||
download={attachment.filename}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<FileDown className="h-3 w-3 mr-1" />
|
|
||||||
{attachment.filename} ({(attachment.size / 1024).toFixed(0)}KB)
|
|
||||||
</a>
|
|
||||||
</Button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -91,7 +77,7 @@ export default function EmailContent({ email }: EmailContentProps) {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center h-full p-8">
|
<div className="flex justify-center items-center h-full p-8">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -106,7 +92,7 @@ export default function EmailContent({ email }: EmailContentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-6">
|
||||||
{content}
|
{content}
|
||||||
{renderAttachments()}
|
{renderAttachments()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export default function EmailHeader({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b flex flex-col">
|
<div className="border-b bg-white/95 backdrop-blur-sm flex flex-col">
|
||||||
{/* Courrier Title */}
|
{/* Courrier Title */}
|
||||||
<div className="p-3 border-b border-gray-100">
|
<div className="p-3 border-b border-gray-100">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@ -52,13 +52,13 @@ export default function EmailHeader({
|
|||||||
<div className="px-4 py-2 flex items-center">
|
<div className="px-4 py-2 flex items-center">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<form onSubmit={handleSearch} className="relative">
|
<form onSubmit={handleSearch} className="relative">
|
||||||
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search emails..."
|
placeholder="Search emails..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
className="pl-8 pr-8 h-9"
|
className="pl-8 pr-8 h-9 bg-gray-50"
|
||||||
/>
|
/>
|
||||||
{searchQuery && (
|
{searchQuery && (
|
||||||
<button
|
<button
|
||||||
@ -66,7 +66,7 @@ export default function EmailHeader({
|
|||||||
onClick={clearSearch}
|
onClick={clearSearch}
|
||||||
className="absolute right-2 top-1/2 transform -translate-y-1/2"
|
className="absolute right-2 top-1/2 transform -translate-y-1/2"
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4 text-muted-foreground" />
|
<X className="h-4 w-4 text-gray-400" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</form>
|
</form>
|
||||||
@ -80,7 +80,7 @@ export default function EmailHeader({
|
|||||||
type="submit"
|
type="submit"
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8 text-gray-600 hover:text-gray-900"
|
||||||
onClick={handleSearch}
|
onClick={handleSearch}
|
||||||
>
|
>
|
||||||
<Search className="h-4 w-4" />
|
<Search className="h-4 w-4" />
|
||||||
@ -95,7 +95,7 @@ export default function EmailHeader({
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-gray-600 hover:text-gray-900">
|
||||||
<Settings className="h-4 w-4" />
|
<Settings className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Loader2 } from 'lucide-react';
|
import { Loader2, Mail } from 'lucide-react';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
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';
|
||||||
@ -57,8 +57,8 @@ export default function EmailList({
|
|||||||
// Render loading state
|
// Render loading state
|
||||||
if (isLoading && emails.length === 0) {
|
if (isLoading && emails.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center items-center h-full p-8">
|
<div className="flex justify-center items-center h-full p-8 bg-white/95 backdrop-blur-sm">
|
||||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -66,12 +66,12 @@ 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">
|
<div className="flex flex-col justify-center items-center h-full p-8 text-center bg-white/95 backdrop-blur-sm">
|
||||||
<h3 className="text-lg font-semibold mb-2">No emails found</h3>
|
<Mail className="h-8 w-8 text-gray-400 mb-2" />
|
||||||
<p className="text-muted-foreground">
|
<p className="text-gray-500 text-sm">
|
||||||
{currentFolder === 'INBOX'
|
{currentFolder === 'INBOX'
|
||||||
? "Your inbox is empty. You're all caught up!"
|
? "Your inbox is empty. You're all caught up!"
|
||||||
: `The ${currentFolder} folder is empty.`}
|
: `No emails in this folder`}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -84,11 +84,13 @@ 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">
|
<div className="flex flex-col h-full bg-white/95 backdrop-blur-sm">
|
||||||
<EmailListHeader
|
<EmailListHeader
|
||||||
allSelected={allSelected}
|
allSelected={allSelected}
|
||||||
someSelected={someSelected}
|
someSelected={someSelected}
|
||||||
onToggleSelectAll={onToggleSelectAll}
|
onToggleSelectAll={onToggleSelectAll}
|
||||||
|
currentFolder={currentFolder}
|
||||||
|
totalEmails={totalEmails}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{selectedEmailIds.length > 0 && (
|
{selectedEmailIds.length > 0 && (
|
||||||
@ -98,8 +100,11 @@ export default function EmailList({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ScrollArea className="flex-1" onScroll={handleScroll}>
|
<div
|
||||||
<div className="divide-y">
|
className="flex-1 overflow-y-auto"
|
||||||
|
onScroll={handleScroll}
|
||||||
|
>
|
||||||
|
<div className="divide-y divide-gray-100">
|
||||||
{emails.map((email) => (
|
{emails.map((email) => (
|
||||||
<EmailListItem
|
<EmailListItem
|
||||||
key={email.id}
|
key={email.id}
|
||||||
@ -107,11 +112,11 @@ export default function EmailList({
|
|||||||
isSelected={selectedEmailIds.includes(email.id)}
|
isSelected={selectedEmailIds.includes(email.id)}
|
||||||
isActive={selectedEmail?.id === email.id}
|
isActive={selectedEmail?.id === email.id}
|
||||||
onSelect={() => onSelectEmail(email.id)}
|
onSelect={() => onSelectEmail(email.id)}
|
||||||
onToggleSelect={(e) => {
|
onToggleSelect={(e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onToggleSelect(email.id);
|
onToggleSelect(email.id);
|
||||||
}}
|
}}
|
||||||
onToggleStarred={(e) => {
|
onToggleStarred={(e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onToggleStarred(email.id);
|
onToggleStarred(email.id);
|
||||||
}}
|
}}
|
||||||
@ -119,12 +124,12 @@ export default function EmailList({
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{isLoading && emails.length > 0 && (
|
{isLoading && emails.length > 0 && (
|
||||||
<div className="p-4 flex justify-center">
|
<div className="flex items-center justify-center p-4">
|
||||||
<Loader2 className="h-6 w-6 animate-spin text-primary" />
|
<div className="animate-spin rounded-full h-4 w-4 border-t-2 border-b-2 border-blue-500"></div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ChevronDown } 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 { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
@ -15,45 +15,37 @@ interface EmailListHeaderProps {
|
|||||||
allSelected: boolean;
|
allSelected: boolean;
|
||||||
someSelected: boolean;
|
someSelected: boolean;
|
||||||
onToggleSelectAll: () => void;
|
onToggleSelectAll: () => void;
|
||||||
|
currentFolder?: string;
|
||||||
|
totalEmails?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EmailListHeader({
|
export default function EmailListHeader({
|
||||||
allSelected,
|
allSelected,
|
||||||
someSelected,
|
someSelected,
|
||||||
onToggleSelectAll,
|
onToggleSelectAll,
|
||||||
|
currentFolder = 'Inbox',
|
||||||
|
totalEmails = 0
|
||||||
}: EmailListHeaderProps) {
|
}: EmailListHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between border-b px-4 py-2">
|
<div className="border-b border-gray-100 bg-white/95">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center justify-between px-4 h-14">
|
||||||
<Checkbox
|
<div className="flex items-center gap-3">
|
||||||
checked={allSelected}
|
<Checkbox
|
||||||
ref={(input) => {
|
checked={allSelected}
|
||||||
if (input) {
|
ref={(input) => {
|
||||||
(input as unknown as HTMLInputElement).indeterminate = someSelected && !allSelected;
|
if (input) {
|
||||||
}
|
(input as unknown as HTMLInputElement).indeterminate = someSelected && !allSelected;
|
||||||
}}
|
}
|
||||||
onClick={onToggleSelectAll}
|
}}
|
||||||
/>
|
onCheckedChange={onToggleSelectAll}
|
||||||
|
className="mt-0.5"
|
||||||
<DropdownMenu>
|
/>
|
||||||
<DropdownMenuTrigger asChild>
|
<h2 className="text-base font-semibold text-gray-900 capitalize">{currentFolder.toLowerCase()}</h2>
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 p-0">
|
</div>
|
||||||
<ChevronDown className="h-4 w-4" />
|
|
||||||
</Button>
|
<span className="text-sm text-gray-600">
|
||||||
</DropdownMenuTrigger>
|
{totalEmails} emails
|
||||||
<DropdownMenuContent align="start">
|
</span>
|
||||||
<DropdownMenuItem onClick={onToggleSelectAll}>
|
|
||||||
{allSelected ? 'Unselect all' : 'Select all'}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem onClick={onToggleSelectAll} disabled={!someSelected}>
|
|
||||||
Unselect all
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
Select messages to perform actions
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -95,64 +95,69 @@ export default function EmailListItem({
|
|||||||
return `hsl(${h}, 70%, 80%)`;
|
return `hsl(${h}, 70%, 80%)`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get preview text from email content
|
||||||
|
const getPreviewText = () => {
|
||||||
|
if (email.preview) return email.preview;
|
||||||
|
|
||||||
|
let content = email.content || '';
|
||||||
|
|
||||||
|
// Strip HTML tags if present
|
||||||
|
content = content.replace(/<[^>]+>/g, ' ');
|
||||||
|
|
||||||
|
// Clean up whitespace
|
||||||
|
content = content.replace(/\s+/g, ' ').trim();
|
||||||
|
|
||||||
|
// Limit to ~70 chars
|
||||||
|
if (content.length > 70) {
|
||||||
|
return content.substring(0, 70) + '...';
|
||||||
|
}
|
||||||
|
|
||||||
|
return content || 'No preview available';
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex items-start gap-2 p-3 cursor-pointer transition-colors hover:bg-accent/50 relative',
|
'flex items-center gap-3 px-4 py-2 hover:bg-gray-50/80 cursor-pointer',
|
||||||
isActive && 'bg-accent',
|
isActive ? 'bg-blue-50/50' : '',
|
||||||
!email.read && 'font-medium'
|
!email.read ? 'bg-blue-50/20' : ''
|
||||||
)}
|
)}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 min-w-[3rem]">
|
<Checkbox
|
||||||
<Checkbox
|
checked={isSelected}
|
||||||
checked={isSelected}
|
onClick={onToggleSelect}
|
||||||
onClick={onToggleSelect}
|
className="mt-0.5"
|
||||||
className="mt-1"
|
/>
|
||||||
/>
|
|
||||||
<div onClick={onToggleStarred}>
|
|
||||||
<Star
|
|
||||||
className={cn(
|
|
||||||
'h-5 w-5 cursor-pointer transition-colors',
|
|
||||||
email.starred ? 'fill-yellow-400 text-yellow-400' : 'text-muted-foreground hover:text-yellow-400'
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Avatar className="mt-1 h-8 w-8 font-semibold text-sm" style={{ backgroundColor: getAvatarColor() }}>
|
|
||||||
<AvatarFallback>{getSenderInitial()}</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex justify-between items-baseline mb-1">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<div className="font-medium truncate">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
{getSenderName()}
|
<span className={`text-sm truncate ${!email.read ? 'font-semibold text-gray-900' : 'text-gray-600'}`}>
|
||||||
|
{getSenderName()}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground whitespace-nowrap ml-2">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
{formatDate(email.date)}
|
<span className="text-xs text-gray-500 whitespace-nowrap">
|
||||||
|
{formatDate(email.date)}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="h-6 w-6 text-gray-400 hover:text-yellow-400"
|
||||||
|
onClick={onToggleStarred}
|
||||||
|
>
|
||||||
|
<Star className={`h-4 w-4 ${email.starred ? 'fill-yellow-400 text-yellow-400' : ''}`} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-baseline">
|
<h3 className="text-sm text-gray-900 truncate">
|
||||||
<div className="font-medium truncate">
|
{email.subject || '(No subject)'}
|
||||||
{email.subject || '(No subject)'}
|
</h3>
|
||||||
</div>
|
|
||||||
{email.hasAttachments && (
|
|
||||||
<Badge variant="outline" className="ml-2 text-xs">📎</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-sm text-muted-foreground truncate mt-1">
|
<div className="text-xs text-gray-500 truncate">
|
||||||
{email.preview || 'No preview available'}
|
{getPreviewText()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!email.read && (
|
|
||||||
<div className="absolute right-3 top-3">
|
|
||||||
<div className="h-2 w-2 rounded-full bg-primary"></div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -65,23 +65,23 @@ export default function EmailSidebar({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="w-60 border-r h-full flex flex-col">
|
<aside className="w-60 border-r h-full flex flex-col bg-white/95 backdrop-blur-sm">
|
||||||
<div className="p-4">
|
<div className="p-2 border-b border-gray-100">
|
||||||
<Button
|
<Button
|
||||||
className="w-full gap-2"
|
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"
|
size="sm"
|
||||||
onClick={onCompose}
|
onClick={onCompose}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-3.5 w-3.5" />
|
||||||
Compose
|
Compose
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="px-4 py-2">
|
<div className="px-2 py-2 border-b border-gray-100 flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="icon"
|
||||||
className="w-full justify-start gap-2"
|
className="text-gray-600 hover:text-gray-900 hover:bg-gray-100 h-8 w-8"
|
||||||
onClick={onRefresh}
|
onClick={onRefresh}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
@ -89,48 +89,66 @@ export default function EmailSidebar({
|
|||||||
"h-4 w-4",
|
"h-4 w-4",
|
||||||
isLoading && "animate-spin"
|
isLoading && "animate-spin"
|
||||||
)} />
|
)} />
|
||||||
Refresh
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ScrollArea className="flex-1">
|
<ScrollArea className="flex-1">
|
||||||
<div className="space-y-1 p-2">
|
<div className="p-3">
|
||||||
<div className="text-xs font-medium text-muted-foreground px-2 py-1">
|
<div className="text-sm font-medium text-gray-500 mb-2">
|
||||||
FOLDERS
|
Folders
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Standard folders */}
|
{/* Standard folders */}
|
||||||
{visibleStandardFolders.map(folder => (
|
<ul className="space-y-0.5 px-2">
|
||||||
<Button
|
{visibleStandardFolders.map(folder => (
|
||||||
key={folder}
|
<li key={folder}>
|
||||||
variant={currentFolder === folder ? "secondary" : "ghost"}
|
<Button
|
||||||
size="sm"
|
variant={currentFolder === folder ? "secondary" : "ghost"}
|
||||||
className="w-full justify-start gap-2"
|
className={`w-full justify-start py-2 ${
|
||||||
onClick={() => onFolderChange(folder)}
|
currentFolder === folder ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900'
|
||||||
>
|
}`}
|
||||||
{getFolderIcon(folder)}
|
onClick={() => onFolderChange(folder)}
|
||||||
<span className="capitalize">{folder.toLowerCase()}</span>
|
>
|
||||||
</Button>
|
<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>
|
||||||
|
|
||||||
{/* Custom folders section */}
|
{/* Custom folders section */}
|
||||||
{customFolders.length > 0 && (
|
{customFolders.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<div className="text-xs font-medium text-muted-foreground px-2 py-1 mt-4">
|
<div className="text-sm font-medium text-gray-500 mt-4 mb-2">
|
||||||
CUSTOM FOLDERS
|
Custom Folders
|
||||||
</div>
|
</div>
|
||||||
{customFolders.map(folder => (
|
<ul className="space-y-0.5 px-2">
|
||||||
<Button
|
{customFolders.map(folder => (
|
||||||
key={folder}
|
<li key={folder}>
|
||||||
variant={currentFolder === folder ? "secondary" : "ghost"}
|
<Button
|
||||||
size="sm"
|
variant={currentFolder === folder ? "secondary" : "ghost"}
|
||||||
className="w-full justify-start gap-2"
|
className={`w-full justify-start py-2 ${
|
||||||
onClick={() => onFolderChange(folder)}
|
currentFolder === folder ? 'bg-gray-100 text-gray-900' : 'text-gray-600 hover:text-gray-900'
|
||||||
>
|
}`}
|
||||||
{getFolderIcon(folder)}
|
onClick={() => onFolderChange(folder)}
|
||||||
<span className="truncate">{folder}</span>
|
>
|
||||||
</Button>
|
<div className="flex items-center">
|
||||||
))}
|
{getFolderIcon(folder)}
|
||||||
|
<span className="ml-2 truncate">{folder}</span>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user