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%
3.0 KB · 68 lines tsx
Raw Blame History
1"use client";23import * as React from "react";4import { useAdminQuery } from "@/components/admin/use-query";5import type { MissionsResponse } 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, pct } from "@/components/admin/format";9import { cn } from "@/lib/utils";1011export default function AdminMissionsPage() {12  const q = useAdminQuery<MissionsResponse>("/api/admin/missions");13  const d = q.data;1415  const items = React.useMemo<ProgressionItem[]>(() => {16    if (!d) return [];17    const c = new Map(d.completions.map((r) => [r.mission_key, r]));18    return d.missions.map((m) => {19      const s = c.get(m.key);20      const started = num(s?.started ?? 0);21      const completed = num(s?.completed ?? 0);22      return {23        key: m.key,24        name: m.name,25        description: m.description,26        target: num(m.target),27        rewardCredits: m.rewardCredits,28        rewardXp: m.rewardXp,29        enabled: m.enabled,30        metric: m.metric,31        tag: m.period,32        stats: [33          { label: "Started", value: int(started) },34          { label: "Completed", value: int(completed) },35          { label: "Rate", value: started ? pct(completed / started, 0) : "—" },36        ],37      };38    });39  }, [d]);4041  const totals = d ? { daily: d.missions.filter((m) => m.period === "daily").length, weekly: d.missions.filter((m) => m.period === "weekly").length, enabled: d.missions.filter((m) => m.enabled).length, completed: d.completions.reduce((a, r) => a + num(r.completed), 0) } : null;4243  return (44    <>45      <PageHeader title="Missions" description="Daily and weekly missions. Edit inline, then save each row; toggles apply immediately. Completion counts cover periods active in the last 7 days." actions={<RefreshButton onClick={() => void q.refresh()} loading={q.refreshing} />} />46      {totals ? (47        <StatGrid cols={4} className="mb-4">48          <StatTile label="Daily missions" value={totals.daily} compact />49          <StatTile label="Weekly missions" value={totals.weekly} compact />50          <StatTile label="Enabled" value={`${totals.enabled} / ${totals.daily + totals.weekly}`} compact tone={totals.enabled === 0 ? "danger" : "neutral"} />51          <StatTile label="Completions · 7d" value={int(totals.completed)} compact tone="accent" />52        </StatGrid>53      ) : null}54      <Panel padded={false} className={cn(q.stale && "opacity-70")}>55        {q.error && !d ? (56          <div className="p-4">57            <ErrorState error={q.error} onRetry={() => void q.refresh()} />58          </div>59        ) : !d ? (60          <TableSkeleton rows={8} cols={8} />61        ) : (62          <ProgressionTable items={items} endpoint="/api/admin/missions" onSaved={() => void q.refresh()} statHeaders={["Started", "Completed", "Rate"]} />63        )}64      </Panel>65    </>66  );67}68