web/src/routes/players.index.tsx
3.7 KB · sha256:53a24978e8df9c9df642bd3f3565421c19bab98a860aa0bfe3ab4190a5868dec
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Trash2 } from "lucide-react";
import { api } from "../lib/api";
import { Badge, PageHeader, PlayerAvatar } from "../components/ui";
import { PageShell } from "../components/layout";
import { DataTable, type Column } from "../components/data-table";
import type { PlayerSummary } from "../lib/types";
export const Route = createFileRoute("/players/")({
component: PlayersIndex,
});
function PlayersIndex() {
const navigate = useNavigate();
const qc = useQueryClient();
const players = useQuery({
queryKey: ["players"],
queryFn: () => api.players.list(),
});
const clearMany = useMutation({
mutationFn: async (rows: PlayerSummary[]) => {
await Promise.all(rows.map((r) => api.players.clear(r.uuid)));
},
meta: { pending: "Clearing nodes…", success: "Player nodes cleared" },
onSuccess: () => qc.invalidateQueries({ queryKey: ["players"] }),
});
const rows = players.data ?? [];
const columns: Column<PlayerSummary>[] = [
{
id: "player",
header: "Player",
accessor: (p) => p.username,
cell: (p) => (
<div className="flex items-center gap-3">
<PlayerAvatar uuid={p.uuid} name={p.username} size={32} />
<div className="leading-tight">
<div className="text-fg font-medium">{p.username}</div>
<div className="text-fg-subtle font-mono text-[11px]">
{p.uuid.slice(0, 8)}…{p.uuid.slice(-4)}
</div>
</div>
</div>
),
},
{
id: "primaryGroup",
header: "Primary group",
accessor: (p) => p.primaryGroup || "",
cell: (p) =>
p.primaryGroup ? (
<Badge tone="brand">{p.primaryGroup}</Badge>
) : (
<span className="text-fg-subtle">—</span>
),
},
{
id: "nodes",
header: "Nodes",
accessor: (p) => p.nodeCount,
align: "right",
cell: (p) => <span className="tabular-nums">{p.nodeCount}</span>,
},
{
id: "status",
header: "Status",
accessor: (p) => (p.online ? "online" : "offline"),
cell: (p) =>
p.online ? (
<Badge tone="success">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-emerald-500" />
Online
</Badge>
) : (
<span className="text-fg-subtle text-sm">Offline</span>
),
},
];
return (
<PageShell>
<PageHeader
title={
<span>
Players{" "}
<span className="text-fg-subtle text-base font-normal">
({rows.length})
</span>
</span>
}
description="Everyone Ward has seen on this server."
/>
<DataTable<PlayerSummary>
data={rows}
columns={columns}
rowKey={(p) => p.uuid}
searchPlaceholder="Search by username, group, UUID…"
pageSize={25}
loading={players.isLoading}
onRowClick={(p) =>
navigate({ to: "/players/$uuid", params: { uuid: p.uuid } })
}
bulkActions={[
{
id: "clear",
label: "Clear nodes",
icon: Trash2,
variant: "danger",
confirm:
"Clear ALL nodes on {n} players? They lose all groups + overrides.",
run: (rows) => clearMany.mutateAsync(rows),
},
]}
emptyState={{
title: "No players known",
description:
"Once a player joins, Ward records their UUID and they'll appear here.",
}}
/>
</PageShell>
);
}