SPB Git forge

spb/spinza

Public
8commits 1branches 0releases
1.6 MBsize
maindefault branch
16 days agolast push
TypeScript 97.6% SQL 1.4% JavaScript 0.5%
2.8 KB · 65 lines tsx
Raw Blame History
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