import { useEffect, useState } from 'react'; import { api, type StatBucket, type Stats as StatsData } from '../api'; import { useI18n } from '../i18n/I18nProvider'; // The traffic screen. One fetch per range; every figure below is drawn from it. // No charting dependency: two polylines and a list of styled divs are what an // admin actually reads here, and they cost a few hundred bytes instead of a // library's worth. const RANGES = [7, 30, 90]; export function Stats() { const { t } = useI18n(); const [days, setDays] = useState(30); const [data, setData] = useState(null); const [err, setErr] = useState(null); useEffect(() => { let live = true; setErr(null); api.adminStats(days).then( (d) => { if (live) setData(d); }, (e: unknown) => { if (live) setErr(e instanceof Error ? e.message : String(e)); }, ); return () => { live = false; }; }, [days]); return (
{RANGES.map((n) => ( ))} {t('stats.hint')}
{err ?

{err}

: null} {data ? : err ? null :

{t('auth.busy')}

}
); } function Body({ data }: { data: StatsData }) { const { t } = useI18n(); return ( <>
); } function Kpi({ k, label, n }: { k: string; label: string; n: number }) { return (
{n} {label}
); } // Views and clicks per day. `viewBox` plus `preserveAspectRatio="none"` lets the // SVG stretch to the pane's width without measuring the DOM. function Chart({ series }: { series: StatsData['series'] }) { const { t } = useI18n(); const W = 600; const H = 100; const max = Math.max(1, ...series.map((d) => Math.max(d.views, d.clicks))); const step = series.length > 1 ? W / (series.length - 1) : 0; const line = (pick: (d: StatsData['series'][number]) => number) => series.map((d, i) => `${(i * step).toFixed(1)},${(H - (pick(d) / max) * H).toFixed(1)}`).join(' '); const first = series[0]?.date ?? ''; const last = series[series.length - 1]?.date ?? ''; return (

{t('stats.timeline')}

d.views)} ${W},${H}`} /> d.views)} /> d.clicks)} />
{t('stats.views')} {t('stats.clicks')}

{first} → {last} · {t('stats.peak')} {max}

); } // One grouped count, longest bar first. The bar is proportional to the block's // own largest row, so a small dimension stays readable next to a big one. function Bars({ k, title, rows }: { k: string; title: string; rows: StatBucket[] }) { const { t } = useI18n(); const max = Math.max(1, ...rows.map((r) => r.n)); return (

{title}

{rows.length === 0 ? (

{t('stats.empty')}

) : (
    {rows.map((r) => (
  • {r.key} {r.n}
  • ))}
)}
); }