web/src/routes/index.tsx
4.6 KB · sha256:ff48cdd3c1bebaf830cbf8257879147a4e6bd80f1102bccdc4806ef556006511
import { createFileRoute, Link } from "@tanstack/react-router";
import { useQuery } from "@tanstack/react-query";
import {
Activity,
Compass,
Shield,
Users,
type LucideIcon,
} from "lucide-react";
import { api } from "../lib/api";
import { Badge, Card, CardBody, CardHeader, PageHeader } from "../components/ui";
import { PageShell } from "../components/layout";
export const Route = createFileRoute("/")({
component: DashboardPage,
});
function DashboardPage() {
const overview = useQuery({
queryKey: ["overview"],
queryFn: api.overview,
refetchInterval: 10_000,
});
return (
<PageShell>
<PageHeader
title="Dashboard"
description="Live snapshot of Ward state on this server."
/>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
<StatCard
icon={Activity}
label="Online"
value={
overview.data
? `${overview.data.online} / ${overview.data.maxPlayers}`
: "—"
}
loading={overview.isLoading}
to="/players"
/>
<StatCard
icon={Users}
label="Players known"
value={overview.data?.players ?? "—"}
loading={overview.isLoading}
to="/players"
/>
<StatCard
icon={Shield}
label="Groups"
value={overview.data?.groups ?? "—"}
loading={overview.isLoading}
to="/groups"
/>
<StatCard
icon={Compass}
label="Tracks"
value={overview.data?.tracks ?? "—"}
loading={overview.isLoading}
to="/tracks"
/>
</div>
<div className="mt-8 grid grid-cols-1 gap-4 lg:grid-cols-2">
<Card>
<CardHeader
title="Transaction handlers"
description="Cost handlers available to track rungs."
/>
<CardBody>
{overview.data && overview.data.handlers.length > 0 ? (
<div className="flex flex-wrap gap-2">
{overview.data.handlers.map((h) => (
<Badge key={h} tone="brand">
{h}
</Badge>
))}
</div>
) : (
<p className="text-fg-muted text-sm">
No handlers registered. Defaults depend on Vault / vanilla XP
being detected at boot; additional handlers can be registered
from any Rune script via{" "}
<code className="bg-surface-3 rounded px-1.5 py-0.5 text-xs">
ward.registerTransaction(...)
</code>
.
</p>
)}
</CardBody>
</Card>
<Card>
<CardHeader
title="Quick actions"
description="Jump into common workflows."
/>
<CardBody>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
<QuickLink to="/players" label="Browse players" icon={Users} />
<QuickLink to="/groups" label="Manage groups" icon={Shield} />
<QuickLink to="/tracks" label="Edit tracks" icon={Compass} />
<QuickLink
to="/groups"
label="Set default group"
icon={Shield}
/>
</div>
</CardBody>
</Card>
</div>
</PageShell>
);
}
function StatCard({
icon: Icon,
label,
value,
loading,
to,
}: {
icon: LucideIcon;
label: string;
value: number | string;
loading?: boolean;
to: string;
}) {
return (
<Link
to={to}
className="bg-surface border-border hover:border-brand-300 dark:hover:border-brand-700 block rounded-2xl border p-5 shadow-sm transition"
>
<div className="flex items-center justify-between">
<span className="text-fg-muted text-sm font-medium">{label}</span>
<span className="bg-brand-50 text-brand-600 dark:bg-brand-900/40 dark:text-brand-300 flex h-9 w-9 items-center justify-center rounded-lg">
<Icon size={18} />
</span>
</div>
<div className="text-fg mt-4 text-3xl font-semibold tabular-nums">
{loading ? <span className="text-fg-subtle">…</span> : value}
</div>
</Link>
);
}
function QuickLink({
to,
label,
icon: Icon,
}: {
to: string;
label: string;
icon: LucideIcon;
}) {
return (
<Link
to={to}
className="hover:bg-surface-3 text-fg flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm font-medium transition"
>
<Icon size={16} className="text-fg-muted" />
{label}
</Link>
);
}