import { useEffect, useState } from "react"; import { api, type Stats } from "./api.js"; import { useToast } from "./useToast.js"; const monthLabel = (m: string) => new Date(`${m}-15T00:00:00Z`) .toLocaleString(undefined, { month: "short", timeZone: "UTC" }) .toLowerCase(); // "2026-07-05" -> "7/5" export const dayLabel = (day: string) => { const [, m, d] = day.split("-"); return `${Number(m)}/${Number(d)}`; }; function useIsMobile() { const [mobile, setMobile] = useState(() => window.matchMedia("(max-width: 640px)").matches); useEffect(() => { const mq = window.matchMedia("(max-width: 640px)"); const onChange = (e: MediaQueryListEvent) => setMobile(e.matches); mq.addEventListener("change", onChange); return () => mq.removeEventListener("change", onChange); }, []); return mobile; } export function usePersisted(key: string, allowed: number[], fallback: number) { const [value, setValue] = useState(() => { const saved = Number(localStorage.getItem(key)); return allowed.includes(saved) ? saved : fallback; }); const set = (v: number) => { setValue(v); localStorage.setItem(key, String(v)); }; return [value, set] as const; } export function ChartHead({ title, value, options, unit, onChange, }: { title: string; value: number; options: number[]; unit: string; onChange: (v: number) => void; }) { return (
{title}
); } export function Bars({ data, labels, ariaLabel, max: maxOverride, onBar, }: { data: { key: string; n: number; title: string }[]; labels?: string[]; ariaLabel: string; // shared scale when one dataset renders as multiple charts max?: number; onBar?: (title: string, at: { clientX: number; clientY: number }) => void; }) { const max = maxOverride ?? Math.max(...data.map((d) => d.n), 1); // pre-thinned labels (blanks between marks) opt out of the nth-child hiding const sparse = labels?.some((l) => !l) ?? false; return ( <>
40 ? " dense" : ""}`} role="img" aria-label={ariaLabel}> {data.map((d) => (
onBar?.(d.title, e)} /> ))}
{labels && (
12 ? " dense" : ""}${sparse ? " sparse" : ""}`} aria-hidden="true" > {labels.map((l, i) => ( {l} ))}
)} ); } export function StatsPanel({ onSearchAccount }: { onSearchAccount?: (handle: string) => void }) { const [stats, setStats] = useState(null); const [error, setError] = useState(null); const [writeDays, setWriteDays] = usePersisted("statsWriteDays", [7, 30, 90], 30); const [signupMonths, setSignupMonths] = usePersisted("statsSignupMonths", [6, 12, 24], 12); const [activeDays, setActiveDays] = usePersisted("statsActiveDays", [7, 30, 90], 30); const isMobile = useIsMobile(); const [toast, showToast] = useToast(); useEffect(() => { const refresh = () => api .stats(writeDays, signupMonths, activeDays) .then(setStats) .catch((e) => setError(e.message)); refresh(); // same cadence as the relay stats at the top of the page const id = setInterval(refresh, 15_000); return () => clearInterval(id); }, [writeDays, signupMonths, activeDays]); const placeholder = error ? ( — ) : (