"use client"; import { useMemo, useOptimistic, useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import type { Acc } from "@/lib/types"; import { deleteAccount, updateAccount } from "@/app/actions"; import EditableCell from "./editable-cell"; import ConfirmDialog from "./confirm-dialog"; import CreateAccountDialog from "./create-account-dialog"; import Toast, { type ToastMessage } from "./toast"; type Props = { initial: Acc[]; prefixPattern: string }; type SortDir = "asc" | "desc"; type OptimisticPatch = { username: string; field: keyof Acc; value: string }; function sortAccounts(rows: Acc[], dir: SortDir, prefix: string): Acc[] { return [...rows].sort((a, b) => { const ap = a.username.startsWith(prefix); const bp = b.username.startsWith(prefix); if (ap && !bp) return -1; if (!ap && bp) return 1; return dir === "asc" ? a.username.localeCompare(b.username) : b.username.localeCompare(a.username); }); } function StatusBadge({ status }: { status: string }) { const map: Record = { "": { bg: "bg-zinc-100", fg: "text-zinc-600", label: "available" }, wait: { bg: "bg-amber-100", fg: "text-amber-700", label: "wait" }, done: { bg: "bg-emerald-100", fg: "text-emerald-700", label: "done" }, }; const v = map[status] ?? { bg: "bg-zinc-100", fg: "text-zinc-600", label: status || "—" }; return ( {v.label} ); } function DeleteButton({ label, onClick, }: { label: string; onClick: () => void; }) { return ( ); } export default function AccountsTable({ initial, prefixPattern }: Props) { const router = useRouter(); const [sortDir, setSortDir] = useState("desc"); const [editingKey, setEditingKey] = useState(null); const [, startTransition] = useTransition(); const [refreshing, setRefreshing] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [deleting, setDeleting] = useState(false); const [deleteError, setDeleteError] = useState(null); const [createOpen, setCreateOpen] = useState(false); const [toast, setToast] = useState(null); const [optimistic, applyOptimistic] = useOptimistic( initial, (state, patch) => state.map((row) => row.username === patch.username ? { ...row, [patch.field]: patch.value } : row, ), ); const sorted = useMemo( () => sortAccounts(optimistic, sortDir, prefixPattern), [optimistic, sortDir, prefixPattern], ); function saveCell(username: string, field: keyof Acc, value: string) { return new Promise<{ ok: boolean; error?: string }>((resolve) => { startTransition(async () => { applyOptimistic({ username, field, value }); const row = initial.find((r) => r.username === username); if (!row) return resolve({ ok: false, error: "row not found" }); const next: Acc = { ...row, [field]: value }; const result = await updateAccount(next); resolve(result.ok ? { ok: true } : { ok: false, error: result.error }); }); }); } function refresh() { setRefreshing(true); startTransition(() => { router.refresh(); setTimeout(() => setRefreshing(false), 400); }); } async function confirmDelete() { if (!deleteTarget) return; setDeleting(true); setDeleteError(null); const result = await deleteAccount(deleteTarget); setDeleting(false); if (result.ok) { const deleted = deleteTarget; setDeleteTarget(null); setToast({ type: "success", message: `Account ${deleted} deleted` }); } else { setDeleteError(result.error); } } if (initial.length === 0) { return (
setCreateOpen(true)} />

No accounts yet. Click Add above to create one manually, or wait for the monitor.

setCreateOpen(false)} prefixPattern={prefixPattern} />
); } return (
setCreateOpen(true)} /> {/* Desktop / tablet table */}
{sorted.map((row) => { const k = (f: string) => `${row.username}::${f}`; return ( ); })}
Password Status Link
{row.username} setEditingKey(k("password"))} onEditEnd={() => setEditingKey(null)} onSave={(v) => saveCell(row.username, "password", v)} /> setEditingKey(k("status"))} onEditEnd={() => setEditingKey(null)} onSave={(v) => saveCell(row.username, "status", v)} renderView={(v) => } /> setEditingKey(k("link"))} onEditEnd={() => setEditingKey(null)} onSave={(v) => saveCell(row.username, "link", v)} /> { setDeleteError(null); setDeleteTarget(row.username); }} />
{/* Mobile cards */}
{sorted.map((row) => { const k = (f: string) => `${row.username}::${f}`; return (
{row.username}
setEditingKey(k("status"))} onEditEnd={() => setEditingKey(null)} onSave={(v) => saveCell(row.username, "status", v)} renderView={(v) => } />
{ setDeleteError(null); setDeleteTarget(row.username); }} />
setEditingKey(k("password"))} onEditEnd={() => setEditingKey(null)} onSave={(v) => saveCell(row.username, "password", v)} /> setEditingKey(k("link"))} onEditEnd={() => setEditingKey(null)} onSave={(v) => saveCell(row.username, "link", v)} />
); })}
setCreateOpen(false)} onSuccess={(name) => setToast({ type: "success", message: `Account ${name} created` }) } prefixPattern={prefixPattern} /> setToast(null)} /> { if (!deleting) setDeleteTarget(null); }} onConfirm={confirmDelete} title={`Delete ${deleteTarget ?? ""}?`} message={ <>

Permanently remove{" "} {deleteTarget} {" "} from the accounts table. This action cannot be undone.

{deleteError && (

{deleteError}

)} } confirmLabel="Delete" destructive pending={deleting} />
); } function Th({ children, className = "" }: { children: React.ReactNode; className?: string }) { return ( {children} ); } function CardRow({ label, children }: { label: string; children: React.ReactNode }) { return (
{label}
{children}
); } function PageHead({ count, onRefresh, refreshing, onAdd, }: { count: number; onRefresh: () => void; refreshing: boolean; onAdd: () => void; }) { return (

Table

Accounts {count}

); }