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 ? (
—
) : (
);
const act = stats?.activity;
const noActivityYet = act != null && act.writesByDay.every((d) => d.n === 0);
const tiles: { label: string; value: string | null | undefined }[] = [
{ label: "Active", value: act?.active.toLocaleString() },
{ label: "Dormant", value: act?.dormant.toLocaleString() },
{
label: "Avg Daily Active",
value: act?.avgDailyActive.toLocaleString(undefined, { maximumFractionDigits: 1 }),
},
{ label: "Signups", value: act?.newAccounts.toLocaleString() },
{ label: "Writes", value: act?.writes.toLocaleString() },
];
return (
Stats
{error && (
{error}
)}
{tiles.map((t) => (
{stats ? {t.value ?? "—"} : placeholder}
{t.label}
))}
{stats && act && (
<>
{act.topAccounts.length > 0 && (
Most Active
{act.topAccounts.map((t) => (
))}
)}
{(() => {
const data = stats.signups.map((s) => ({
key: s.month,
n: s.n,
title: `${s.month}: ${s.n.toLocaleString()}`,
}));
const monthLabels = stats.signups.map((s) => monthLabel(s.month));
// 24 bars don't fit a phone screen; split into two stacked years
if (isMobile && signupMonths === 24) {
const shared = Math.max(...stats.signups.map((s) => s.n), 1);
return [0, 12].map((from) => (
0 ? "chart-half" : undefined}>
));
}
return (
);
})()}
{noActivityYet ? (
no activity recorded yet, counting starts now
) : (
({
key: d.day,
n: d.n,
title: `${d.day}: ${d.n.toLocaleString()}`,
}))}
labels={act.writesByDay.map((d, i) =>
i % (writeDays === 7 ? 1 : writeDays === 30 ? 5 : 15) === 0
? dayLabel(d.day)
: "",
)}
/>
)}
{stats.labels.length > 0 && (
Flags By Label
{stats.labels.map((l) => (
))}
)}
>
)}
{toast}
);
}