"use client"; import * as React from "react"; import { Plus, RotateCcw, Save, Trash2 } from "lucide-react"; import { DAILY_REWARDS, RESCUE_CREDITS_AMOUNT, RESCUE_CREDITS_COOLDOWN_HOURS, RESCUE_CREDITS_THRESHOLD } from "@spinza/shared"; import { Button } from "@/components/ui"; import { api } from "@/lib/api"; import { toast } from "@/lib/store"; import { cn } from "@/lib/utils"; import { useAdminQuery } from "@/components/admin/use-query"; import type { DailyRewardsSetting, RescueSetting, SettingsResponse } from "@/components/admin/types"; import { PageHeader, RefreshButton, Panel, ErrorState, TableSkeleton, DenseInput, FieldLabel, InlineError, Pill } from "@/components/admin/primitives"; import { describeError, int, sc } from "@/components/admin/format"; function readSchedule(s: Record | undefined): number[] { const v = s?.["dailyRewards"] as Partial | undefined; return Array.isArray(v?.schedule) && v.schedule.length ? v.schedule.map((n) => Math.max(0, Math.trunc(Number(n) || 0))) : [...DAILY_REWARDS]; } function readRescue(s: Record | undefined): RescueSetting { const v = s?.["rescue"] as Partial | undefined; return { amount: Number(v?.amount ?? RESCUE_CREDITS_AMOUNT), cooldownHours: Number(v?.cooldownHours ?? RESCUE_CREDITS_COOLDOWN_HOURS), threshold: Number(v?.threshold ?? RESCUE_CREDITS_THRESHOLD) }; } export default function AdminRewardsPage() { const q = useAdminQuery("/api/admin/settings"); const d = q.data; const flags = d?.flags ?? {}; return ( <> void q.refresh()} loading={q.refreshing} />} /> {q.error && !d ? ( void q.refresh()} /> ) : !d ? (
) : (
void q.refresh()} /> void q.refresh()} />
)} ); } function ScheduleEditor({ initial, enabled, onSaved }: { initial: number[]; enabled: boolean; onSaved: () => void }) { const [schedule, setSchedule] = React.useState(initial); const [busy, setBusy] = React.useState(false); const [error, setError] = React.useState(null); const dirty = JSON.stringify(schedule) !== JSON.stringify(initial); const valid = schedule.length >= 1 && schedule.length <= 30 && schedule.every((n) => Number.isInteger(n) && n >= 0); const total = schedule.reduce((a, n) => a + n, 0); async function save() { if (!valid) return; setBusy(true); setError(null); try { await api("/api/admin/settings/dailyRewards", { json: { value: { schedule } } }); toast({ title: "Daily reward schedule saved", description: `${schedule.length}-day streak · ${sc(total)} per full cycle`, tone: "success" }); onSaved(); } catch (e) { setError(describeError(e)); } finally { setBusy(false); } } return ( {enabled ? "flag on" : "flag off"} } >
{schedule.map((v, i) => (
Day {i + 1} setSchedule((s) => s.map((x, j) => (j === i ? Number(e.target.value.replace(/\D/g, "") || 0) : x)))} aria-label={`Day ${i + 1} reward`} /> SC
))} {schedule.length < 30 ? ( ) : null}
days {schedule.length} full cycle {sc(total)} avg / day {sc(Math.round(total / Math.max(1, schedule.length)))}
Preview
{schedule.map((v, i) => (
{i + 1}
))}
{!valid ?
Schedule must have 1–30 non-negative integer amounts.
: null} {error ?
{error}
: null} ); } function RescueEditor({ initial, enabled, onSaved }: { initial: RescueSetting; enabled: boolean; onSaved: () => void }) { const [v, setV] = React.useState(initial); const [busy, setBusy] = React.useState(false); const [error, setError] = React.useState(null); const dirty = JSON.stringify(v) !== JSON.stringify(initial); const valid = Number.isInteger(v.amount) && v.amount >= 0 && Number.isFinite(v.cooldownHours) && v.cooldownHours >= 0 && Number.isInteger(v.threshold) && v.threshold >= 0; async function save() { if (!valid) return; setBusy(true); setError(null); try { await api("/api/admin/settings/rescue", { json: { value: v } }); toast({ title: "Rescue credits saved", description: `${sc(v.amount)} every ${v.cooldownHours} h under ${sc(v.threshold)}`, tone: "success" }); onSaved(); } catch (e) { setError(describeError(e)); } finally { setBusy(false); } } return ( {enabled ? "flag on" : "flag off"} } >
Amount (SC) setV({ ...v, amount: Number(e.target.value.replace(/\D/g, "") || 0) })} />
Cooldown (hours) setV({ ...v, cooldownHours: Number(e.target.value.replace(/[^\d.]/g, "") || 0) })} />
Threshold (SC) setV({ ...v, threshold: Number(e.target.value.replace(/\D/g, "") || 0) })} />

A player with ≤ {sc(v.threshold)} can claim {sc(v.amount)} at most once every {v.cooldownHours} h.

{!valid ?
Amount and threshold must be non-negative integers; cooldown a non-negative number.
: null} {error ?
{error}
: null}
); }