"use client"; import { useMemo, useOptimistic, useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import type { Acc } from "@/lib/types"; import { updateAccount } from "@/app/actions"; import EditableCell from "./editable-cell"; 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-800", label: "wait" }, done: { bg: "bg-emerald-100", fg: "text-emerald-800", label: "done" }, }; const v = map[status] ?? { bg: "bg-zinc-100", fg: "text-zinc-600", label: status || "—" }; return ( {v.label} ); } 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 [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) { resolve({ ok: false, error: "row not found" }); return; } const next: Acc = { ...row, [field]: value }; const result = await updateAccount(next); if (result.ok) resolve({ ok: true }); else resolve({ ok: false, error: result.error }); }); }); } function refresh() { setRefreshing(true); startTransition(() => { router.refresh(); setTimeout(() => setRefreshing(false), 400); }); } if (initial.length === 0) { return (

Accounts

No accounts yet. The monitor will create some on the next run.

); } return (

// table

Accounts

count {optimistic.length}
{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)} />
setEditingKey(k("link"))} onEditEnd={() => setEditingKey(null)} onSave={(v) => saveCell(row.username, "link", v)} />
{sorted.map((row) => { const k = (f: string) => `${row.username}::${f}`; return (
{row.username}
password
setEditingKey(k("password"))} onEditEnd={() => setEditingKey(null)} onSave={(v) => saveCell(row.username, "password", v)} />
status
setEditingKey(k("status"))} onEditEnd={() => setEditingKey(null)} onSave={(v) => saveCell(row.username, "status", v)} />
link
setEditingKey(k("link"))} onEditEnd={() => setEditingKey(null)} onSave={(v) => saveCell(row.username, "link", v)} />
); })}
); }