Initial commit: webmail client (M0/M1)
Three-pane webmail client. - Backend: FastAPI + async SQLAlchemy + arq worker (Postgres/Redis), IMAP/SMTP sync, threading, search, drafts/outbox with undo, attachments, 2FA. - Frontend: React 18 + Vite + TypeScript + TanStack Query + Tailwind; three-pane UI with rich-text composer, light/dark themes, resizable panels. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { useT } from "../../i18n";
|
||||
import type { DraftOut } from "../../lib/types";
|
||||
|
||||
function timeLabel(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
const now = new Date();
|
||||
if (d.toDateString() === now.toDateString())
|
||||
return d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
||||
return d.toLocaleDateString([], { month: "short", day: "numeric" });
|
||||
}
|
||||
|
||||
// Local drafts live in the `drafts` table (not an IMAP mailbox), so the Drafts
|
||||
// folder lists them here rather than through the thread list.
|
||||
export function DraftsList({
|
||||
drafts,
|
||||
onOpen,
|
||||
}: {
|
||||
drafts: DraftOut[];
|
||||
onOpen: (draft: DraftOut) => void;
|
||||
}) {
|
||||
const t = useT();
|
||||
if (drafts.length === 0) {
|
||||
return <div className="p-6 text-sm text-app-faint">{t("compose.noDrafts")}</div>;
|
||||
}
|
||||
return (
|
||||
<div className="h-full overflow-y-auto" data-testid="drafts-list">
|
||||
{drafts.map((draft) => {
|
||||
const recipients = draft.payload.to.join(", ") || t("compose.noRecipient");
|
||||
const preview = draft.payload.text.trim();
|
||||
return (
|
||||
<button
|
||||
key={draft.id}
|
||||
onClick={() => onOpen(draft)}
|
||||
className="flex w-full flex-col gap-1 border-b border-app-border px-3 py-2.5 text-left hover:bg-app-hover"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex-1 truncate text-sm text-app-fg">{recipients}</span>
|
||||
<span className="shrink-0 text-[11px] text-app-faint">{timeLabel(draft.updated_at)}</span>
|
||||
</div>
|
||||
<div className="truncate text-[13px] font-medium text-app-fg">
|
||||
{draft.payload.subject || t("compose.noSubject")}
|
||||
</div>
|
||||
{preview && <div className="truncate text-xs text-app-faint">{preview}</div>}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user