import { useEffect, useState } from "react"; import { Loader2, RefreshCw } from "lucide-react"; import { useTranslation } from "../i18n"; import { formatAmount, localizeUsageError, planLabel } from "../lib/usage-display"; import type { UsageData, UsageQueryState } from "../hooks/useUsageQuery"; interface UsageFooterProps { /** Query state lifted to the provider card via useUsageQuery. Both the * compact (card header) and detail (footer) variants read the same state, * so a refresh from either updates both. */ usage: UsageQueryState; /** "detail" (default) renders the multi-line footer; "compact" renders a * one-line summary + last-updated + refresh button for the card header. */ variant?: "detail" | "compact"; } export function UsageFooter({ usage, variant = "detail" }: UsageFooterProps) { const { t } = useTranslation(); const { status, data, error, updatedAt, supported, refresh } = usage; const [now, setNow] = useState(() => Date.now()); // Tick relative times once a minute while showing data. useEffect(() => { if (status !== "success") return; const id = setInterval(() => setNow(Date.now()), 60_000); return () => clearInterval(id); }, [status]); if (!supported || status === "idle") return null; const percentOf = (d: UsageData): number | null => { if (d.used != null && d.total != null && d.total > 0) return (d.used / d.total) * 100; if (d.remaining != null && d.total != null && d.total > 0) return (1 - d.remaining / d.total) * 100; return null; }; const formatReset = (resetsAt?: string | null): string | null => { if (!resetsAt) return null; const ms = new Date(resetsAt).getTime() - now; if (Number.isNaN(ms)) return null; if (ms <= 0) return t("usageResetDone"); const mins = Math.floor(ms / 60_000); if (mins < 60) return t("usageResetIn", { time: `${Math.max(mins, 1)}m` }); const hours = Math.floor(mins / 60); if (hours < 24) return t("usageResetIn", { time: `${hours}h` }); return t("usageResetIn", { time: `${Math.floor(hours / 24)}d` }); }; const formatAgo = (ts: number | null): string | null => { if (ts == null) return null; const mins = Math.floor((now - ts) / 60_000); if (mins < 1) return t("usageUpdatedAgo", { time: "1m" }); if (mins < 60) return t("usageMinAgo", { n: mins }); const hours = Math.floor(mins / 60); if (hours < 24) return t("usageHourAgo", { n: hours }); return t("usageDayAgo", { n: Math.floor(hours / 24) }); }; const refreshBtn = ( ); /** One-line summary of the primary entry (first tier / balance). */ const compactSummary = (): { icon: string; text: string; color: string } | null => { const d = data[0]; if (!d) return null; // A "plan" is a quota tier (used/total, e.g. 5h / weekly limit). Balance // entries carry only remaining + unit โ their plan_name is a currency / // brand label and must render as an amount, not a percentage. const isPlan = !!d.planName && d.planName !== "NewAPI" && (d.total != null || d.used != null); if (d.isValid === false) { return { icon: "โก", text: t("usageInvalidKey"), color: "text-red-500 dark:text-red-400" }; } if (d.planName === "NewAPI" && d.remaining == null && d.total == null) { return { icon: "๐ฐ", text: `${t("usageBalance")} โ`, color: "text-green-600 dark:text-green-400" }; } if (isPlan) { const pct = percentOf(d); const color = pct == null ? "text-content-muted" : pct < 70 ? "text-green-600 dark:text-green-400" : pct < 90 ? "text-orange-600 dark:text-orange-400" : "text-red-500 dark:text-red-400"; const label = data.length > 1 && d.planName ? `${planLabel(d.planName, t)} ` : ""; return { icon: "โก", text: pct != null ? `${label}${Math.round(pct)}%` : `${label}โ`, color, }; } return { icon: "๐ฐ", text: formatAmount(d.remaining, d.unit), color: "text-content-primary", }; }; if (variant === "compact") { if (status === "loading" && data.length === 0) { return
; } if (status === "error" && data.length === 0) { return ( {error == null ? t("usageNetworkError") : localizeUsageError(error, t)} {refreshBtn} ); } const s = compactSummary(); const ghost = status !== "success"; const ago = formatAgo(updatedAt); return ( {s && ( <> {s.text} > )} {ago && {ago}} {refreshBtn} ); } // โโ detail variant โโ // Loading with no prior data: skeleton bar. if (status === "loading" && data.length === 0) { return (