"use client"; import { AlertTriangleIcon, OctagonAlertIcon, RotateCcwIcon } from "lucide-react"; import { motion, useReducedMotion, useSpring, useTransform } from "motion/react"; import { type ReactNode, useEffect, useMemo, useRef, useState } from "react"; import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; export interface QuotaPart { label: string; value: number; } export interface QuotaRingProps { label: string; used: number; limit: number; unit?: string; breakdown?: QuotaPart[]; resetsAt?: Date | string | number; warnAt?: number; criticalAt?: number; locale?: string; className?: string; } type Level = "ok" | "warn" | "critical"; const LEVEL: Record = { ok: { stroke: "stroke-foreground/80", text: "text-foreground" }, warn: { stroke: "stroke-warning", text: "text-warning-foreground" }, critical: { stroke: "stroke-destructive", text: "text-destructive-foreground" }, }; const SIZE = 40; const STROKE = 4; const R = (SIZE - STROKE) / 2; const CIRC = 2 * Math.PI * R; function useNow(active: boolean, every = 1000) { const [now, setNow] = useState(null); useEffect(() => { setNow(Date.now()); if (!active) return; const id = setInterval(() => setNow(Date.now()), every); return () => clearInterval(id); }, [active, every]); return now; } function until(target: number, now: number, withSeconds: boolean) { const s = Math.max(0, Math.floor((target - now) / 1000)); const d = Math.floor(s / 86400); const h = Math.floor((s % 86400) / 3600); const m = Math.floor((s % 3600) / 60); const sec = s % 60; const pad = (n: number) => String(n).padStart(2, "0"); if (!withSeconds) return d ? `${d}d ${h}h` : h ? `${h}h ${m}m` : `${m}m`; return d ? `${d}d ${pad(h)}h ${pad(m)}m ${pad(sec)}s` : `${pad(h)}h ${pad(m)}m ${pad(sec)}s`; } function Countdown({ target }: { target: number }) { const now = useNow(true); return {now === null ? "–" : until(target, now, true)}; } export function QuotaRing({ label, used, limit, unit = "", breakdown = [], resetsAt, warnAt = 0.75, criticalAt = 0.9, locale = "en-US", className, }: QuotaRingProps) { const reduce = useReducedMotion(); const ratio = limit > 0 ? used / limit : 0; const level: Level = ratio >= criticalAt ? "critical" : ratio >= warnAt ? "warn" : "ok"; const tone = LEVEL[level]; const num = useMemo(() => new Intl.NumberFormat(locale), [locale]); const compact = useMemo(() => new Intl.NumberFormat(locale, { notation: "compact", maximumFractionDigits: 1 }), [locale]); const target = resetsAt === undefined ? null : new Date(resetsAt).getTime(); const [pulse, setPulse] = useState(0); const lastLevel = useRef(level); useEffect(() => { if (lastLevel.current !== level && level !== "ok") setPulse((p) => p + 1); lastLevel.current = level; }, [level]); const progress = useSpring(0, { stiffness: 120, damping: 20 }); useEffect(() => { const v = Math.min(1, ratio); if (reduce) progress.jump(v); else progress.set(v); }, [ratio, reduce, progress]); const dash = useTransform(progress, (p) => `${(p * CIRC).toFixed(2)} ${CIRC.toFixed(2)}`); const pct = Math.round(ratio * 100); const u = unit ? ` ${unit}` : ""; return ( } > {[warnAt, criticalAt].map((t) => ( ))} {pct > 999 ? "999+" : pct} {label} {level === "warn" ? : null} {level === "critical" ? : null} {compact.format(used)} {" "} / {compact.format(limit)} {unit ? {unit} : null}
{label} {pct}%
{num.format(used)} of {num.format(limit)} {u} used {used > limit ? ` · ${num.format(used - limit)}${u} over` : ` · ${num.format(limit - used)}${u} left`}
{breakdown.length ? (
{breakdown.map((p) => (
{p.label} {num.format(p.value)} {u}
))}
) : null} {target !== null ? (
Resets in
) : null}
); } export function QuotaRingGroup({ children, className }: { children: ReactNode; className?: string }) { return (
{children}
); }