Skip to content
File

Blob: src/client/components/email-list.tsx

typescript109 lines
1import { Loader2, Mail, Paperclip } from "lucide-react";
2import { Badge } from "@/client/components/ui";
3import type { EmailSummary } from "@/client/lib/api";
4import { fullDate, relativeTime } from "@/client/lib/time";
5 
6interface EmailListProps {
7 inboxAddress: string;
8 emails: EmailSummary[];
9 selectedEmailId: string | null;
10 loadingEmailId: string | null;
11 loading: boolean;
12 onSelect: (emailId: string) => void;
13}
14 
15export function EmailList({
16 inboxAddress,
17 emails,
18 selectedEmailId,
19 loadingEmailId,
20 loading,
21 onSelect,
22}: EmailListProps) {
23 return (
24 <aside className="flex max-h-[calc(100vh-220px)] min-h-[560px] flex-col rounded-xl border border-zinc-800/60 bg-zinc-900">
25 <div className="flex items-start justify-between gap-3 p-5 pb-0">
26 <div>
27 <span className="mb-1 block text-xs font-semibold uppercase tracking-wider text-zinc-500">Messages</span>
28 <h2 className="text-base font-semibold text-zinc-100">Inbox timeline</h2>
29 </div>
30 <Badge variant="accent">{emails.length}</Badge>
31 </div>
32
33 {loading ? (
34 <div className="flex items-center gap-2 px-5 pt-4 text-sm text-zinc-400" aria-busy="true">
35 <Loader2 className="h-3.5 w-3.5 animate-spin" />
36 Refreshing inbox...
37 </div>
38 ) : null}
39
40 {!loading && emails.length === 0 ? (
41 <div className="flex flex-1 flex-col items-center justify-center gap-3 p-8 text-center">
42 <span className="inline-grid h-12 w-12 place-items-center rounded-full bg-zinc-800/60">
43 <Mail className="h-6 w-6 text-zinc-600" />
44 </span>
45 <p className="text-sm text-zinc-400">No emails have arrived yet.</p>
46 <p className="text-xs text-zinc-500">New messages will appear here automatically.</p>
47 </div>
48 ) : null}
49
50 <div className="mt-3 flex-1 space-y-1.5 overflow-y-auto px-3 pb-3">
51 {emails.map((email, i) => {
52 const active = email.id === selectedEmailId;
53 const unread = !email.isRead;
54 const showRecipientAddress = email.recipientAddress && email.recipientAddress !== inboxAddress;
55
56 return (
57 <button
58 key={email.id}
59 type="button"
60 className={`animate-scale-fade opacity-0 group relative w-full rounded-lg border px-4 py-3 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-500/50 focus-visible:ring-offset-2 focus-visible:ring-offset-canvas ${
61 active
62 ? "border-accent-500/30 bg-accent-500/5"
63 : "border-transparent bg-zinc-800/20 hover:border-zinc-700/60 hover:bg-zinc-800/40"
64 }`}
65 style={{ animationDelay: `${Math.min(i, 8) * 60}ms` }}
66 onClick={() => onSelect(email.id)}
67 >
68 {unread ? (
69 <span className="absolute left-1.5 top-1/2 h-2 w-2 -translate-y-1/2 rounded-full bg-accent-500" />
70 ) : null}
71 <div className="flex items-center justify-between gap-2">
72 <strong
73 className={`truncate text-sm ${unread ? "font-semibold text-zinc-100" : "font-medium text-zinc-300"}`}
74 >
75 {email.fromName || email.fromAddress}
76 </strong>
77 {loadingEmailId === email.id ? (
78 <Loader2 className="h-3 w-3 shrink-0 animate-spin text-accent-400" />
79 ) : (
80 <span className="shrink-0 text-xs text-zinc-500" title={fullDate(email.receivedAt)}>
81 {relativeTime(email.receivedAt)}
82 </span>
83 )}
84 </div>
85 <div className={`mt-0.5 truncate text-sm ${unread ? "text-zinc-200" : "text-zinc-400"}`}>
86 {email.subject}
87 </div>
88 <div className="mt-1.5 flex flex-wrap items-center gap-2 text-xs text-zinc-500">
89 <span className="truncate">{email.fromAddress}</span>
90 {showRecipientAddress ? (
91 <span className="inline-flex items-center rounded-full border border-zinc-700/70 bg-zinc-800 px-2 py-0.5 text-zinc-400">
92 Delivered to {email.recipientAddress}
93 </span>
94 ) : null}
95 {email.hasAttachments ? (
96 <span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-zinc-800 px-2 py-0.5 text-zinc-400">
97 <Paperclip className="h-3 w-3" />
98 <span className="hidden sm:inline">Files</span>
99 </span>
100 ) : null}
101 </div>
102 </button>
103 );
104 })}
105 </div>
106 </aside>
107 );
108}