File
Blob: src/client/components/email-list.tsx
| 1 | import { Loader2, Mail, Paperclip } from "lucide-react"; |
| 2 | import { Badge } from "@/client/components/ui"; |
| 3 | import type { EmailSummary } from "@/client/lib/api"; |
| 4 | import { fullDate, relativeTime } from "@/client/lib/time"; |
| 5 | |
| 6 | interface 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 | |
| 15 | export 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 | } |