import { useEffect, useRef, useState, type ReactNode } from "react"; import { ApiError } from "../api/client"; import type { Job } from "../api/types"; export function errorMessage(e: unknown, fallback = "Something went wrong. Try again."): string { return e instanceof ApiError ? e.message : e instanceof Error && e.message ? e.message : fallback; } /** Launch progress bar for a job. */ export function JobProgress({ job, compact = false }: { job: Job; compact?: boolean }) { const pct = Math.round((job.status === "done" ? 1 : job.progress) * 100); const failed = job.status === "failed"; const label = failed ? job.error ?? "Failed" : job.status === "queued" ? job.message ?? "Queued for launch" : job.message ?? "Working"; return (
{label} {!failed && {pct}%}
); } export function Modal({ title, onClose, children, wide = false, }: { title: string; onClose: () => void; children: ReactNode; wide?: boolean; }) { const ref = useRef(null); useEffect(() => { const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose(); document.addEventListener("keydown", onKey); ref.current?.focus(); return () => document.removeEventListener("keydown", onKey); }, [onClose]); return (
e.target === e.currentTarget && onClose()}>

{title}

{children}
); } export function Drawer({ onClose, children, label }: { onClose: () => void; children: ReactNode; label: string }) { useEffect(() => { const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose(); document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [onClose]); return (
e.target === e.currentTarget && onClose()}>
); } export function Tabs({ tabs, value, onChange, }: { tabs: { id: T; label: string; count?: number }[]; value: T; onChange: (id: T) => void; }) { return (
{tabs.map((t) => ( ))}
); } export function CopyButton({ text, label = "Copy" }: { text: string; label?: string }) { const [copied, setCopied] = useState(false); return ( ); } /** Two step destructive button: first click arms it, second confirms. No browser dialogs. */ export function ConfirmButton({ onConfirm, children, confirmLabel = "Click again to confirm", className = "btn btn-small btn-danger", }: { onConfirm: () => void | Promise; children: ReactNode; confirmLabel?: string; className?: string; }) { const [armed, setArmed] = useState(false); const [busy, setBusy] = useState(false); useEffect(() => { if (!armed) return; const t = setTimeout(() => setArmed(false), 4000); return () => clearTimeout(t); }, [armed]); return ( ); } export function EmptyState({ title, body, action }: { title: string; body?: string; action?: ReactNode }) { return (

{title}

{body &&

{body}

} {action}
); } export function Loading({ label = "Loading" }: { label?: string }) { return (
{label}...
); } export function StatusPill({ status }: { status: string }) { const tone = ["ready", "ok", "done", "posted", "reminded"].includes(status) ? "ok" : ["failed", "error"].includes(status) ? "bad" : "busy"; return {status}; } export function PageHeader({ eyebrow, title, children }: { eyebrow: string; title: string; children?: ReactNode }) { return (

{eyebrow}

{title}

{children &&
{children}
}
); } export function formatDate(iso: string | null | undefined, withTime = false): string { if (!iso) return "Undated"; const d = new Date(iso); return withTime ? d.toLocaleString(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }) : d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" }); } export function formatDuration(seconds: number | undefined): string { if (!seconds) return ""; const m = Math.floor(seconds / 60); const s = Math.round(seconds % 60); return `${m}:${String(s).padStart(2, "0")}`; }