- activities indexes + client_events/client_errors/rollups/alerts collections - global function middleware via createStart; reads skipped on success - actor stash, denial dedup, control-flow error guards - api route logging, auth event overrides, beacon endpoint, web vitals - centralized admin role checks, shared skeletons, award-badges fix - extracted deriveRunState with tests
142 lines
4.9 KiB
TypeScript
142 lines
4.9 KiB
TypeScript
import { createFileRoute, redirect } from "@tanstack/react-router";
|
|
import {
|
|
tournamentQueries,
|
|
useTournament,
|
|
} from "@/features/tournaments/queries";
|
|
import { ensureServerQueryData } from "@/lib/tanstack-query/utils/ensure";
|
|
import SeedTournament from "@/features/tournaments/components/seed-tournament";
|
|
import SetupGroupStage from "@/features/tournaments/components/setup-group-stage";
|
|
import GroupStageView from "@/features/tournaments/components/group-stage-view";
|
|
import { Container, Stack, Divider, Title, Box, Card, Group, Skeleton, SimpleGrid } from "@mantine/core";
|
|
import BracketView from "@/features/bracket/components/bracket-view";
|
|
import { SpotifyControlsBar } from "@/features/spotify/components";
|
|
import { useIsAdmin } from "@/features/core/hooks/use-is-admin";
|
|
import { useTournamentRunState } from "@/features/tournaments/hooks/use-tournament-run-state";
|
|
import { Trans } from "@lingui/react/macro";
|
|
import { msg } from "@lingui/core/macro";
|
|
|
|
export const Route = createFileRoute("/_authed/admin/tournaments/run/$id")({
|
|
beforeLoad: async ({ context, params }) => {
|
|
const { queryClient } = context;
|
|
const tournament = await ensureServerQueryData(
|
|
queryClient,
|
|
tournamentQueries.details(params.id)
|
|
);
|
|
if (!tournament) throw redirect({ to: "/admin/tournaments" });
|
|
return {
|
|
tournament,
|
|
};
|
|
},
|
|
loader: ({ context }) => ({
|
|
fullWidth: true,
|
|
withPadding: false,
|
|
showSpotifyPanel: true,
|
|
header: {
|
|
withBackButton: true,
|
|
title: msg`Run {name}`,
|
|
titleValues: { name: context.tournament.name },
|
|
},
|
|
}),
|
|
component: RouteComponent,
|
|
pendingComponent: RunTournamentPending,
|
|
});
|
|
|
|
function RunTournamentPending() {
|
|
return (
|
|
<Container size="md" px={0}>
|
|
<Box p="md">
|
|
<Stack gap="md">
|
|
<Group gap={0} grow mb="md">
|
|
{Array.from({ length: 4 }).map((_, index) => (
|
|
<Skeleton
|
|
key={`run-pending-tab-${index}`}
|
|
height={44}
|
|
radius="sm"
|
|
style={{ opacity: 0.85 - index * 0.1 }}
|
|
/>
|
|
))}
|
|
</Group>
|
|
<Card withBorder radius="md" p={0}>
|
|
<Group justify="space-between" p="sm">
|
|
<Skeleton height={16} width={120} radius="sm" />
|
|
<Skeleton height={22} width={22} radius="xl" />
|
|
</Group>
|
|
</Card>
|
|
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="md">
|
|
{Array.from({ length: 6 }).map((_, index) => (
|
|
<Skeleton
|
|
key={`run-pending-match-${index}`}
|
|
height={100}
|
|
radius="md"
|
|
style={{ opacity: Math.max(1 - index * 0.12, 0.4) }}
|
|
/>
|
|
))}
|
|
</SimpleGrid>
|
|
</Stack>
|
|
</Box>
|
|
</Container>
|
|
);
|
|
}
|
|
|
|
function RouteComponent() {
|
|
const { id } = Route.useParams();
|
|
const { data: tournament } = useTournament(id);
|
|
const isAdmin = useIsAdmin();
|
|
|
|
const { hasGroupStage, hasKnockout, knockoutBracketPopulated, nextUpMatchId, bracket } =
|
|
useTournamentRunState(tournament.matches);
|
|
|
|
return (
|
|
<Container size="md" px={0}>
|
|
{ isAdmin && !tournament.regional && <SpotifyControlsBar />}
|
|
{tournament.matches?.length ? (
|
|
hasGroupStage && hasKnockout ? (
|
|
<Stack gap="xl">
|
|
<GroupStageView
|
|
groups={tournament.groups || []}
|
|
matches={tournament.matches}
|
|
showControls
|
|
tournamentId={tournament.id}
|
|
hasKnockoutBracket={knockoutBracketPopulated}
|
|
isRegional={tournament.regional}
|
|
groupConfig={tournament.group_config}
|
|
nextUpMatchId={nextUpMatchId}
|
|
/>
|
|
<Divider />
|
|
<div>
|
|
<Title order={3} ta="center" mb="md"><Trans>Knockout Bracket</Trans></Title>
|
|
<BracketView bracket={bracket} showControls groupConfig={tournament.group_config} nextUpMatchId={nextUpMatchId} />
|
|
</div>
|
|
</Stack>
|
|
) : hasGroupStage ? (
|
|
<GroupStageView
|
|
groups={tournament.groups || []}
|
|
matches={tournament.matches}
|
|
showControls
|
|
tournamentId={tournament.id}
|
|
hasKnockoutBracket={knockoutBracketPopulated}
|
|
isRegional={tournament.regional}
|
|
groupConfig={tournament.group_config}
|
|
nextUpMatchId={nextUpMatchId}
|
|
/>
|
|
) : (
|
|
<BracketView bracket={bracket} showControls groupConfig={tournament.group_config} nextUpMatchId={nextUpMatchId} />
|
|
)
|
|
) : (
|
|
tournament.regional === true ? (
|
|
<SetupGroupStage
|
|
tournamentId={tournament.id}
|
|
teams={tournament.teams || []}
|
|
/>
|
|
) : (
|
|
<SeedTournament
|
|
tournamentId={tournament.id}
|
|
teams={tournament.teams || []}
|
|
isRegional={tournament.regional}
|
|
/>
|
|
)
|
|
)}
|
|
</Container>
|
|
);
|
|
}
|