brackt/app/routes/admin.tournaments._index.tsx
Chris Parsons 3a4fe41eb5
feat(admin): canonical tournament list + result entry routes
New routes /admin/tournaments and /admin/tournaments/:id. The detail
page lets admins enter tournament results once via paste-and-parse,
then calls syncTournamentResults to fan out to every linked window.

Phase 3 Task 4 of canonical tournament layer migration.
2026-05-01 22:03:39 +00:00

138 lines
4.6 KiB
TypeScript

import { Link } from "react-router";
import type { Route } from "./+types/admin.tournaments._index";
import { findAllTournamentsGroupedBySport } from "~/models/tournament";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "~/components/ui/table";
import { Badge } from "~/components/ui/badge";
import { Button } from "~/components/ui/button";
import { Trophy } from "lucide-react";
export function meta(): Route.MetaDescriptors {
return [{ title: "Tournaments - Brackt Admin" }];
}
export async function loader() {
const grouped = await findAllTournamentsGroupedBySport();
return { grouped };
}
export default function AdminTournamentsIndex({
loaderData,
}: Route.ComponentProps) {
const { grouped } = loaderData;
const totalCount = grouped.reduce(
(acc, g) => acc + g.tournaments.length,
0
);
return (
<div className="p-8">
<div className="flex items-center justify-between mb-6">
<div>
<h1 className="text-3xl font-bold">Tournaments</h1>
<p className="text-muted-foreground mt-1">
Canonical tournaments across all sports. Enter results once here
and they fan out to every linked scoring window.
</p>
</div>
</div>
{grouped.length === 0 ? (
<Card>
<CardContent className="py-12 text-center">
<Trophy className="mx-auto h-12 w-12 text-muted-foreground" />
<h3 className="mt-4 text-lg font-semibold">
No tournaments yet
</h3>
<p className="text-muted-foreground mt-2">
Canonical tournaments are created automatically by the backfill
or data-sync flows.
</p>
</CardContent>
</Card>
) : (
<div className="space-y-6">
<p className="text-sm text-muted-foreground">
{totalCount} {totalCount === 1 ? "tournament" : "tournaments"}{" "}
across {grouped.length}{" "}
{grouped.length === 1 ? "sport" : "sports"}
</p>
{grouped.map((group) => (
<Card key={group.sport.id}>
<CardHeader>
<CardTitle>{group.sport.name}</CardTitle>
<CardDescription>
{group.tournaments.length}{" "}
{group.tournaments.length === 1
? "tournament"
: "tournaments"}
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Year</TableHead>
<TableHead>Starts</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{group.tournaments.map((t) => (
<TableRow key={t.id}>
<TableCell className="font-medium">{t.name}</TableCell>
<TableCell>{t.year}</TableCell>
<TableCell className="text-muted-foreground">
{t.startsAt
? new Date(t.startsAt).toLocaleDateString()
: "—"}
</TableCell>
<TableCell>
<Badge
variant={
t.status === "completed"
? "default"
: t.status === "in_progress"
? "secondary"
: "outline"
}
>
{t.status}
</Badge>
</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm" asChild>
<Link to={`/admin/tournaments/${t.id}`}>
Open
</Link>
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
))}
</div>
)}
</div>
);
}