"use client"; import { useCallback, useEffect, useState } from "react"; import Link from "next/link"; import { ArrowLeft, History, Copy, Check, ChevronRight } from "lucide-react"; import type { RoundHistoryEntry } from "@spinza/shared"; import { formatMultiplier, formatSC, classifyWin, WIN_CLASSES } from "@spinza/shared"; import { api, ApiClientError } from "@/lib/api"; import { toast } from "@/lib/store"; import { cn } from "@/lib/utils"; import { Badge, Button, Card, Empty, Sheet, Skeleton } from "@/components/ui"; import { AppShell } from "@/components/shell/app-shell"; import { RequireAuth } from "@/components/shell/require-auth"; import { ApiErrorState } from "@/components/shell/api-error"; import { useRouter } from "next/navigation"; type Entry = RoundHistoryEntry & { features: string[] }; interface Page { entries: Entry[]; nextBefore: string | null; } interface RoundDetail { roundId: string; game: string; version: string; bet: number; win: number; multiplier: number; balanceAfter: number; createdAt: string; result: unknown; features: string[]; } const LIMIT = 40; function fmtDate(iso: string) { const d = new Date(iso); return d.toLocaleString("en-US", { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }); } function WinTag({ multiplier }: { multiplier: number }) { const cls = classifyWin(multiplier); if (cls === "none" || cls === "regular") return null; const label = WIN_CLASSES.find((w) => w.id === cls)?.label ?? ""; return {label}; } function RoundSheet({ roundId, onClose }: { roundId: string | null; onClose: () => void }) { const [detail, setDetail] = useState(null); const [error, setError] = useState(null); const [copied, setCopied] = useState(false); // Reset when a different round is opened (adjust-state-from-props), then fetch. const [seen, setSeen] = useState(roundId); if (roundId !== seen) { setSeen(roundId); setDetail(null); setError(null); } useEffect(() => { if (!roundId) return; let alive = true; api(`/api/user/rounds/${roundId}`) .then((d) => alive && setDetail(d)) .catch((e: unknown) => alive && setError(e)); return () => { alive = false; }; }, [roundId]); const copy = async () => { if (!roundId) return; try { await navigator.clipboard.writeText(roundId); setCopied(true); setTimeout(() => setCopied(false), 1500); } catch { toast({ title: "Copy unavailable", tone: "danger" }); } }; const gameName = (detail?.game ?? "").replace(/-/g, " "); const resultObj = detail && detail.result && typeof detail.result === "object" ? (detail.result as Record) : null; const summary = resultObj ? Object.entries(resultObj).filter(([, v]) => typeof v === "number" || typeof v === "string" || typeof v === "boolean") : []; return ( {error ? ( ) : !detail ? (
) : (
{gameName}
{fmtDate(detail.createdAt)} · v{detail.version}
{[ { l: "Bet", v: formatSC(detail.bet) }, { l: "Win", v: 0 ? "text-credit" : ""}>{formatSC(detail.win)} }, { l: "Multiplier", v: formatMultiplier(detail.multiplier) }, { l: "Balance after", v: formatSC(detail.balanceAfter) }, ].map((x) => (
{x.l}
{x.v}
))}
Features triggered
{detail.features.length ? (
{detail.features.map((f) => ( {f.replace(/[_-]/g, " ")} ))}
) : (

Base game only — no features this round.

)}
{summary.length ? (
Outcome
{summary.slice(0, 12).map(([k, v]) => (
{k.replace(/([A-Z])/g, " $1").replace(/[_-]/g, " ")}
{String(v)}
))}
) : null}
Round ID
{detail.roundId}

Every round is recorded server-side. Quote this ID if you ever need to reference a specific spin.

)}
); } function fetchPage(before: string | null): Promise { const q = new URLSearchParams({ limit: String(LIMIT) }); if (before) q.set("before", before); return api(`/api/user/history?${q}`); } function HistoryBody() { const router = useRouter(); const [entries, setEntries] = useState(null); // null = first page loading const [nextBefore, setNextBefore] = useState(null); const [more, setMore] = useState(false); const [error, setError] = useState(null); const [open, setOpen] = useState(null); const [tick, setTick] = useState(0); const fail = useCallback( (e: unknown) => { if (e instanceof ApiClientError && e.status === 401) { router.replace("/login?next=%2Fprofile%2Fhistory&reason=expired"); return; } setError(e); }, [router], ); useEffect(() => { let alive = true; fetchPage(null) .then((page) => { if (!alive) return; setEntries(page.entries); setNextBefore(page.nextBefore); setError(null); }) .catch((e: unknown) => alive && fail(e)); return () => { alive = false; }; }, [tick, fail]); const retry = () => { setEntries(null); setError(null); setTick((t) => t + 1); }; const loadMore = async () => { if (!nextBefore || more) return; setMore(true); try { const page = await fetchPage(nextBefore); setEntries((cur) => [...(cur ?? []), ...page.entries]); setNextBefore(page.nextBefore); } catch (e) { fail(e); toast({ title: "Could not load more", tone: "danger" }); } finally { setMore(false); } }; if (error && !entries?.length) return ; if (entries === null) { return (
{Array.from({ length: 8 }).map((_, i) => ( ))}
); } if (!entries.length) return } action={} />; return ( <>
Time Game Bet Win Multi Balance after
    {entries.map((e) => (
  • ))}
{nextBefore ? (
) : (

That's every round on record.

)} setOpen(null)} /> ); } export default function HistoryPage() { return (
Profile

Round history

Every spin, newest first. Tap a round for the full result and the features it triggered.

); }