TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1"use client";23import * as React from "react";4import { useRouter } from "next/navigation";5import { Search } from "lucide-react";6import { useAdminQuery } from "@/components/admin/use-query";7import type { UserListRow, UsersResponse } from "@/components/admin/types";8import { PageHeader, RefreshButton, Panel, DataTable, ErrorState, TableSkeleton, Pagination, DenseInput, DenseSelect, UserStatusPill, type Column } from "@/components/admin/primitives";9import { dateTime, int, sc } from "@/components/admin/format";10import { timeAgo } from "@/lib/utils";1112const SORTS = [13 { value: "created", label: "Newest" },14 { value: "spins", label: "Most spins" },15 { value: "balance", label: "Highest balance" },16 { value: "level", label: "Highest level" },17] as const;18type Sort = (typeof SORTS)[number]["value"];1920const LIMIT = 50;2122const cols: Column<UserListRow>[] = [23 { key: "username", header: "Username", render: (r) => <span className="font-medium text-fg">{r.username}</span> },24 { key: "level", header: "Level", align: "right", render: (r) => r.level },25 { key: "balance", header: "Balance", align: "right", render: (r) => <span className="text-credit">{sc(r.balance)}</span> },26 { key: "spins", header: "Spins", align: "right", render: (r) => int(r.totalSpins) },27 { key: "wagered", header: "Wagered", align: "right", render: (r) => sc(r.wagered) },28 { key: "won", header: "Won", align: "right", render: (r) => sc(r.won) },29 { key: "status", header: "Status", render: (r) => <UserStatusPill status={r.status} /> },30 { key: "created", header: "Created", render: (r) => <span className="text-fg-3">{dateTime(r.createdAt)}</span> },31 { key: "lastLogin", header: "Last login", render: (r) => <span className="text-fg-3">{r.lastLoginAt ? timeAgo(r.lastLoginAt) : "never"}</span> },32];3334function useDebounced<T>(value: T, delay = 300): T {35 const [v, setV] = React.useState(value);36 React.useEffect(() => {37 const t = setTimeout(() => setV(value), delay);38 return () => clearTimeout(t);39 }, [value, delay]);40 return v;41}4243export default function AdminUsersPage() {44 const router = useRouter();45 const [q, setQ] = React.useState("");46 const [sort, setSort] = React.useState<Sort>("created");47 const [offset, setOffset] = React.useState(0);48 const dq = useDebounced(q.trim());4950 const params = new URLSearchParams({ sort, limit: String(LIMIT), offset: String(offset) });51 if (dq) params.set("q", dq);52 const query = useAdminQuery<UsersResponse>(`/api/admin/users?${params.toString()}`);53 const data = query.data;5455 return (56 <>57 <PageHeader title="Users" description="Search, inspect and moderate player accounts." actions={<RefreshButton onClick={() => void query.refresh()} loading={query.refreshing} />} />5859 <div className="mb-3 flex flex-wrap items-center gap-2">60 <div className="relative w-full sm:w-72">61 <Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-fg-4" />62 <DenseInput63 className="pl-8"64 placeholder="Search username…"65 value={q}66 onChange={(e) => {67 setQ(e.target.value);68 setOffset(0);69 }}70 aria-label="Search users"71 />72 </div>73 <DenseSelect74 value={sort}75 onChange={(e) => {76 setSort(e.target.value as Sort);77 setOffset(0);78 }}79 aria-label="Sort users"80 >81 {SORTS.map((s) => (82 <option key={s.value} value={s.value}>83 {s.label}84 </option>85 ))}86 </DenseSelect>87 {data ? <span className="ml-auto text-[12px] text-fg-3 tabular">{int(data.total)} accounts</span> : null}88 </div>8990 <Panel padded={false}>91 {query.error && !data ? (92 <div className="p-4">93 <ErrorState error={query.error} onRetry={() => void query.refresh()} />94 </div>95 ) : !data ? (96 <TableSkeleton rows={10} cols={9} />97 ) : (98 <>99 <DataTable columns={cols} rows={data.users} rowKey={(r) => r.id} dense stale={query.stale} onRowClick={(r) => router.push(`/admin/users/${r.id}`)} empty={dq ? `No users match "${dq}".` : "No registered users yet."} />100 <Pagination offset={offset} limit={LIMIT} total={data.total} onChange={setOffset} />101 </>102 )}103 </Panel>104 </>105 );106}107