import { useCallback, useEffect, useRef, useState } from "react"; import { api, ApiError, type Job } from "./api"; export function useFetch(path: string | null, deps: unknown[] = []) { const [data, setData] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(!!path); const [tick, setTick] = useState(0); const reload = useCallback(() => setTick((t) => t + 1), []); useEffect(() => { if (!path) { setData(null); setLoading(false); return; } const ctl = new AbortController(); setLoading(true); setError(null); api(path, { signal: ctl.signal }).then((d) => { setData(d); setLoading(false); }).catch((e) => { if (ctl.signal.aborted) return; setError(e instanceof ApiError ? e : new ApiError(0, "network", String(e))); setLoading(false); }); return () => ctl.abort(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [path, tick, ...deps]); return { data, error, loading, reload, setData }; } /** Poll a job until it reaches a terminal state. */ export function useJob(jobId: string | null, onDone?: (j: Job) => void) { const [job, setJob] = useState(null); const cb = useRef(onDone); cb.current = onDone; useEffect(() => { if (!jobId) { setJob(null); return; } let stop = false; const poll = async () => { try { const j = await api(`jobs/${jobId}`); if (stop) return; setJob(j); if (["done", "dead", "cancelled", "failed"].includes(j.state)) { cb.current?.(j); return; } } catch { /* retry */ } if (!stop) setTimeout(poll, 1500); }; poll(); return () => { stop = true; }; }, [jobId]); return job; } export function useTheme() { const [theme, setTheme] = useState(() => document.documentElement.getAttribute("data-theme") ?? "auto"); const toggle = () => { const dark = theme === "dark" || (theme === "auto" && window.matchMedia("(prefers-color-scheme: dark)").matches); const next = dark ? "light" : "dark"; document.documentElement.setAttribute("data-theme", next); try { localStorage.setItem("akos-theme", next); } catch { /* ignore */ } setTheme(next); }; return { theme, toggle }; } export function fmtDate(s: string | null | undefined): string { if (!s) return "—"; const d = new Date(s); if (isNaN(d.getTime())) return s; return d.toLocaleString(undefined, { year: "2-digit", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }); } export function ago(s: string | null | undefined): string { if (!s) return "—"; const ms = Date.now() - new Date(s).getTime(); if (isNaN(ms)) return s; const m = Math.round(ms / 60000); if (m < 1) return "just now"; if (m < 60) return `${m} min ago`; const h = Math.round(m / 60); if (h < 48) return `${h} h ago`; return `${Math.round(h / 24)} d ago`; } export function fmtBytes(n: number | null | undefined): string { if (n === null || n === undefined) return "—"; if (n < 1024) return `${n} B`; if (n < 1048576) return `${(n / 1024).toFixed(1)} KB`; if (n < 1073741824) return `${(n / 1048576).toFixed(1)} MB`; return `${(n / 1073741824).toFixed(2)} GB`; } export const short = (s: string | null | undefined, n = 8) => (s ? s.slice(0, n) : "—"); export const num = (v: unknown) => (typeof v === "number" ? v.toLocaleString() : v === null || v === undefined ? "—" : String(v));