import { ChevronDown, ChevronRight, Loader2, RefreshCw, Search, Trophy } from "lucide-react"; import { useEffect, useState } from "react"; import { api, errorMessage } from "../api"; import { Alert, Badge, EmptyRow, Metric, PageFrame, QueryPanel } from "../components/ui"; import { displayUsername, formatDate, formatQuantity, toNumeric } from "../lib/format"; import type { Navigate } from "../routing"; import type { AccountRatingRow } from "../types"; export function AccountRatingsPage({ navigate }: { navigate: Navigate }) { const [minLevel, setMinLevel] = useState(""); const [search, setSearch] = useState(""); const [limit, setLimit] = useState("50"); const [rows, setRows] = useState([]); const [hasMore, setHasMore] = useState(false); const [cursor, setCursor] = useState(""); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); async function load(next = false) { // One free-text field: the backend matches a username prefix (editable or // collectible), a first/last name prefix, and a bare number as the user id. const wanted = search.trim(); setBusy(true); setError(""); const params = new URLSearchParams({ limit }); if (minLevel.trim()) params.set("min_level", minLevel.trim()); if (wanted) params.set("q", wanted); if (next && cursor) params.set("before_id", cursor); try { const result = await api.accountRatings(params); const page = result.rows ?? []; setRows((current) => (next ? [...current, ...page] : page)); setCursor(result.next_before_id ?? ""); setHasMore(Boolean(result.has_more)); } catch (err) { setError(errorMessage(err)); } finally { setBusy(false); } } useEffect(() => { void load(false); }, []); const topLevel = rows.reduce((max, row) => Math.max(max, row.Level), 0); const pendingCount = rows.filter((row) => toNumeric(row.PendingStars) !== 0).length; const avgLevel = rows.length > 0 ? (rows.reduce((sum, row) => sum + row.Level, 0) / rows.length).toFixed(1) : "0"; return ( load(false)} disabled={busy}> {"Refresh"} } > {error && {error}}
{ event.preventDefault(); void load(false); }}>
{rows.map((row) => ( ))} {rows.length === 0 && }
{"User ID"} {"Username"} {"Level"} {"Points"} {"Progress to next level"} {"Pending"} {"Computed"}
{row.UserID} {displayUsername(row.Username) || row.FirstName || "-"} {formatQuantity(row.Stars)} {toNumeric(row.PendingStars) !== 0 ? formatQuantity(row.PendingStars) : "-"} {formatDate(row.ComputedAt) || "-"}
{hasMore && (
)}
); } export function LevelBadge({ level }: { level: number }) { const tone = level >= 10 ? "good" : level >= 5 ? "warn" : "neutral"; return {`Level ${level}`}; } export function levelProgress(row: AccountRatingRow): { percent: number; remaining: number; target: number; stars: number } { const stars = toNumeric(row.Stars); const current = toNumeric(row.CurrentLevelStars); const target = toNumeric(row.NextLevelStars); const span = target - current; const percent = span > 0 ? Math.min(100, Math.max(0, ((stars - current) / span) * 100)) : 0; return { percent, remaining: Math.max(0, target - stars), target, stars }; } export function RatingProgress({ row }: { row: AccountRatingRow }) { if (!row.HasNextLevel) { return {"Max level reached"}; } const { percent, remaining, target } = levelProgress(row); return (
{`${formatQuantity(String(remaining))} left to reach ${formatQuantity(String(target))}`}
); }