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()}>
);
}
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 (
);
}
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 (
);
}
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")}`;
}