brackt/app/routes/sports-seasons.$sportsSeasonId.tournament.tsx

136 lines
4.9 KiB
TypeScript
Raw Permalink Normal View History

import { useEffect } from "react";
import { Link, useRevalidator } from "react-router";
import { eq, inArray } from "drizzle-orm";
import type { Route } from "./+types/sports-seasons.$sportsSeasonId.tournament";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import { type PlayoffMatch } from "~/models/playoff-match";
import { findSeasonMatchesBySportsSeasonId } from "~/models/season-match";
import { getBracketTemplate, getOrderedRoundsFromMatches } from "~/lib/bracket-templates";
import { Cs2TournamentBracket } from "~/components/scoring/Cs2TournamentBracket";
import { MatchSchedule } from "~/components/scoring/MatchSchedule";
import { ArrowLeft } from "lucide-react";
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
return [{ title: `${data?.sportsSeason?.name ?? "Tournament"} — Schedule & Results` }];
}
export async function loader({ params }: Route.LoaderArgs) {
const { sportsSeasonId } = params;
const db = database();
const sportsSeason = await db.query.sportsSeasons.findFirst({
where: eq(schema.sportsSeasons.id, sportsSeasonId),
with: { sport: true },
});
if (!sportsSeason) throw new Response("Sports season not found", { status: 404 });
const simulatorType = sportsSeason.sport?.simulatorType ?? null;
const seasonMatches = await findSeasonMatchesBySportsSeasonId(sportsSeasonId);
// Load playoff matches for bracket sports
let playoffMatches: (PlayoffMatch & { participant1: { id: string; name: string } | null; participant2: { id: string; name: string } | null; winner: { id: string; name: string } | null; loser: { id: string; name: string } | null; })[] = [];
let playoffRounds: string[] = [];
let bracketTemplateId: string | null = null;
if (simulatorType?.endsWith("_bracket") || simulatorType === "cs2_major_qualifying_points") {
const events = await db.query.scoringEvents.findMany({
where: eq(schema.scoringEvents.sportsSeasonId, sportsSeasonId),
});
if (events.length > 0) {
const eventIds = events.map((e) => e.id);
const matches = await db.query.playoffMatches.findMany({
where: (pm) => inArray(pm.scoringEventId, eventIds),
with: {
participant1: true,
participant2: true,
winner: true,
loser: true,
},
});
playoffMatches = matches as typeof playoffMatches;
const templateId = events.find((e) => e.bracketTemplateId)?.bracketTemplateId ?? undefined;
bracketTemplateId = templateId ?? null;
const template = templateId ? getBracketTemplate(templateId) : undefined;
playoffRounds = getOrderedRoundsFromMatches(matches, template);
}
}
const hasLiveMatches =
seasonMatches.some((m) => m.status === "in_progress") ||
playoffMatches.some((m) => !m.isComplete && (m.participant1Id !== null || m.participant2Id !== null));
return {
sportsSeason,
simulatorType,
seasonMatches,
playoffMatches,
playoffRounds,
bracketTemplateId,
hasLiveMatches,
};
}
export default function TournamentPage({ loaderData }: Route.ComponentProps) {
const {
sportsSeason,
simulatorType,
seasonMatches,
playoffMatches,
playoffRounds,
bracketTemplateId,
hasLiveMatches,
} = loaderData;
const { revalidate } = useRevalidator();
// Poll every 30 seconds when any match is live
useEffect(() => {
if (!hasLiveMatches) return;
const interval = setInterval(() => revalidate(), 30_000);
return () => clearInterval(interval);
}, [hasLiveMatches, revalidate]);
const isCs2 = simulatorType === "cs2_major_qualifying_points";
return (
<div className="container mx-auto px-4 py-8 max-w-6xl">
<div className="mb-6">
<Link
to="/"
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground mb-4"
>
<ArrowLeft className="h-4 w-4" />
Back
</Link>
<h1 className="text-3xl font-bold">{sportsSeason.name}</h1>
{sportsSeason.sport && (
<p className="text-muted-foreground mt-1">{sportsSeason.sport.name}</p>
)}
{hasLiveMatches && (
<div className="mt-2 flex items-center gap-2 text-sm text-amber-400">
<span className="inline-block w-2 h-2 rounded-full bg-amber-400 animate-pulse" />
Live updating every 30 seconds
</div>
)}
</div>
{isCs2 ? (
<Cs2TournamentBracket
swissMatches={seasonMatches as Parameters<typeof Cs2TournamentBracket>[0]["swissMatches"]}
playoffMatches={playoffMatches as Parameters<typeof Cs2TournamentBracket>[0]["playoffMatches"]}
playoffRounds={playoffRounds}
bracketTemplateId={bracketTemplateId}
/>
) : (
<MatchSchedule matches={seasonMatches as Parameters<typeof MatchSchedule>[0]["matches"]} />
)}
</div>
);
}