"use client"; import * as React from "react"; import { useAdminQuery } from "@/components/admin/use-query"; import type { PlayerAnalyticsResponse } from "@/components/admin/types"; import { PageHeader, RefreshButton, Panel, ErrorState, TileSkeleton, ChartSkeleton, StatGrid, StatTile, SegmentedControl } from "@/components/admin/primitives"; import { Bars, ChartFrame, CHART, Histogram } from "@/components/admin/charts"; import { compact, int, isoDay, num, pct, shortDate } from "@/components/admin/format"; import { cn } from "@/lib/utils"; const DAYS = [7, 30, 90] as const; function dayRange(days: number, anchor: number): string[] { return Array.from({ length: days }, (_, i) => new Date(anchor - (days - 1 - i) * 86400_000).toISOString().slice(0, 10)); } export default function AdminPlayerAnalyticsPage() { const [days, setDays] = React.useState<(typeof DAYS)[number]>(30); const q = useAdminQuery(`/api/admin/analytics/players?days=${days}`); const d = q.data; const anchor = q.updatedAt; const series = React.useMemo(() => { const signups = new Map((d?.signups ?? []).map((r) => [isoDay(r.day), num(r.n)])); const act = new Map((d?.activity ?? []).map((r) => [isoDay(r.day), r])); return dayRange(days, anchor).map((day) => ({ day, signups: signups.get(day) ?? 0, players: num(act.get(day)?.players ?? 0), spins: num(act.get(day)?.spins ?? 0) })); }, [d, days, anchor]); const levels = React.useMemo(() => { const m = new Map((d?.levels ?? []).map((r) => [num(r.bucket), num(r.n)])); return Array.from({ length: 10 }, (_, i) => { const b = i + 1; const lo = 1 + (b - 1) * 10; const hi = b * 10; return { bucket: b, label: `${lo}–${hi}`, n: (m.get(b) ?? 0) + (b === 10 ? (m.get(11) ?? 0) : 0) }; }); }, [d]); const totalSignups = series.reduce((a, r) => a + r.signups, 0); const totalSpins = series.reduce((a, r) => a + r.spins, 0); const peakPlayers = series.reduce((a, r) => Math.max(a, r.players), 0); const barSize = days > 30 ? 6 : days > 7 ? 12 : 24; return ( <> ({ value: v, label: `${v}d` }))} /> void q.refresh()} loading={q.refreshing} /> } /> {q.error && !d ? ( void q.refresh()} /> ) : !d ? (
) : (
{pct(d.retention.total ? d.retention.d1 / d.retention.total : null, 1)} / {pct(d.retention.total ? d.retention.d7 / d.retention.total : null, 1)}} sub={`${int(d.retention.d1)} / ${int(d.retention.d7)} of ${int(d.retention.total)} signed in`} tone="accent" />
shortDate(String(v))} barSize={barSize} yFormat={(v) => int(v)} /> shortDate(String(v))} barSize={barSize} yFormat={(v) => int(v)} /> shortDate(String(v))} barSize={barSize} yFormat={(v) => compact(v)} />
int(v)} />
)} ); }