TypeScript 97.6%
SQL 1.4%
JavaScript 0.5%
1"use client";23import * as React from "react";4import { useAdminQuery } from "@/components/admin/use-query";5import type { AchievementsResponse } from "@/components/admin/types";6import { PageHeader, RefreshButton, Panel, ErrorState, TableSkeleton, StatGrid, StatTile } from "@/components/admin/primitives";7import { ProgressionTable, type ProgressionItem } from "@/components/admin/progression-table";8import { int, num, sc } from "@/components/admin/format";9import { cn } from "@/lib/utils";1011export default function AdminAchievementsPage() {12 const q = useAdminQuery<AchievementsResponse>("/api/admin/achievements");13 const d = q.data;1415 const items = React.useMemo<ProgressionItem[]>(() => {16 if (!d) return [];17 const u = new Map(d.unlocks.map((r) => [r.achievement_key, num(r.n)]));18 return d.achievements.map((a) => {19 const unlocks = u.get(a.key) ?? 0;20 return {21 key: a.key,22 name: a.name,23 description: a.description,24 target: num(a.target),25 rewardCredits: a.rewardCredits,26 rewardXp: a.rewardXp,27 enabled: a.enabled,28 metric: a.metric,29 tag: a.category,30 stats: [31 { label: "Unlocks", value: int(unlocks) },32 { label: "SC paid", value: sc(unlocks * a.rewardCredits) },33 ],34 };35 });36 }, [d]);3738 const totals = d ? { total: d.achievements.length, enabled: d.achievements.filter((a) => a.enabled).length, unlocks: d.unlocks.reduce((a, r) => a + num(r.n), 0), paid: d.achievements.reduce((a, r) => a + r.rewardCredits * num(d.unlocks.find((x) => x.achievement_key === r.key)?.n ?? 0), 0) } : null;3940 return (41 <>42 <PageHeader title="Achievements" description="Lifetime achievements. Edit inline, then save each row; toggles apply immediately." actions={<RefreshButton onClick={() => void q.refresh()} loading={q.refreshing} />} />43 {totals ? (44 <StatGrid cols={4} className="mb-4">45 <StatTile label="Achievements" value={totals.total} compact />46 <StatTile label="Enabled" value={`${totals.enabled} / ${totals.total}`} compact tone={totals.enabled === 0 ? "danger" : "neutral"} />47 <StatTile label="Total unlocks" value={int(totals.unlocks)} compact tone="accent" />48 <StatTile label="Credits paid out" value={sc(totals.paid)} compact />49 </StatGrid>50 ) : null}51 <Panel padded={false} className={cn(q.stale && "opacity-70")}>52 {q.error && !d ? (53 <div className="p-4">54 <ErrorState error={q.error} onRetry={() => void q.refresh()} />55 </div>56 ) : !d ? (57 <TableSkeleton rows={10} cols={8} />58 ) : (59 <ProgressionTable items={items} endpoint="/api/admin/achievements" onSaved={() => void q.refresh()} statHeaders={["Unlocks", "SC paid"]} />60 )}61 </Panel>62 </>63 );64}65