Part 1 — CS2 playoff matches now appear in upcoming schedule: getUpcomingEventsForDraftedParticipants (all-compete path) now also picks up events with upcoming playoff_match_games.scheduledAt, so CS2 Champions Stage bracket games set via the admin bracket UI surface on both /upcoming-events and the sport_season EventSchedule panel. getUpcomingScoringEvents is updated to sort by earliest bracket game time when eventDate/eventStartsAt are absent or past. Part 2 — Auto floor-score CS2 Champions Stage qualifiers: assignCs2EliminationQP now writes provisional floor event_result rows (placement=5, QP = tie-split of slots 5–8) for the 8 Champions Stage advancing teams once all Stage-3 exits are known. The bracket admin's result entry overwrites these via onConflictDoUpdate. Part 3 — Public event detail view: New route /leagues/:leagueId/sports-seasons/:sportsSeasonId/events/:eventId shows CS2 Swiss stage progress (W-L table + matches per stage) and the Champions Stage bracket, bracket events (tennis/golf bracket + groups), and results tables for QP/racing events. EventSchedule and UpcomingCalendarPanel event rows now link to this detail page. https://claude.ai/code/session_01RYK7NCjcaah545979wupcM
178 lines
6.7 KiB
TypeScript
178 lines
6.7 KiB
TypeScript
import { Link } from "react-router";
|
|
import { ArrowLeft, Calendar } from "lucide-react";
|
|
import { format } from "date-fns";
|
|
import type { Route } from "./+types/$leagueId.sports-seasons.$sportsSeasonId.events.$eventId";
|
|
import { loader } from "./$leagueId.sports-seasons.$sportsSeasonId.events.$eventId.server";
|
|
import { Badge } from "~/components/ui/badge";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "~/components/ui/card";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "~/components/ui/table";
|
|
import { PlayoffBracket } from "~/components/scoring/PlayoffBracket";
|
|
import { GroupStageStandings } from "~/components/sport-season/GroupStageStandings";
|
|
import { Cs2MajorEventView } from "~/components/events/Cs2MajorEventView";
|
|
|
|
export function meta({ data }: Route.MetaArgs) {
|
|
const eventName = data?.scoringEvent?.name ?? "Event";
|
|
const leagueName = data?.league?.name ?? "League";
|
|
return [{ title: `${eventName} — ${leagueName} — Brackt` }];
|
|
}
|
|
|
|
export { loader };
|
|
|
|
function formatEventDate(dateStr: string | null | Date | undefined): string {
|
|
if (!dateStr) return "Date TBD";
|
|
const d = new Date(dateStr);
|
|
if (isNaN(d.getTime())) return String(dateStr);
|
|
return format(d, "MMM d, yyyy");
|
|
}
|
|
|
|
export default function EventDetailPage({ loaderData }: Route.ComponentProps) {
|
|
const { league, sportsSeason, scoringEvent } = loaderData;
|
|
const leagueId = league.id;
|
|
const sportsSeasonId = sportsSeason.id;
|
|
|
|
const ownershipMap = Object.fromEntries(
|
|
loaderData.teamOwnerships.map((o) => [
|
|
o.participantId,
|
|
{ teamName: o.teamName, ownerName: o.ownerName ?? "", teamId: o.teamId },
|
|
])
|
|
);
|
|
|
|
const eventDate = scoringEvent.eventStartsAt
|
|
? formatEventDate(scoringEvent.eventStartsAt)
|
|
: formatEventDate(scoringEvent.eventDate);
|
|
|
|
return (
|
|
<div className="container mx-auto py-8 px-4 max-w-5xl">
|
|
{/* Breadcrumb */}
|
|
<div className="mb-6 space-y-1">
|
|
<Link
|
|
to={`/leagues/${leagueId}/sports-seasons/${sportsSeasonId}`}
|
|
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
|
>
|
|
<ArrowLeft className="h-3.5 w-3.5" />
|
|
{sportsSeason.name}
|
|
</Link>
|
|
<h1 className="text-2xl font-bold">{scoringEvent.name}</h1>
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
<Calendar className="h-3.5 w-3.5" />
|
|
<span suppressHydrationWarning>{eventDate}</span>
|
|
{scoringEvent.isComplete && (
|
|
<Badge variant="outline" className="border-emerald-500/30 text-emerald-400 text-xs">
|
|
Complete
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* CS2 Major view */}
|
|
{loaderData.kind === "cs2" && (
|
|
<Cs2MajorEventView
|
|
cs2StageResults={loaderData.cs2StageResults}
|
|
seasonMatches={loaderData.seasonMatches}
|
|
playoffMatches={loaderData.playoffMatches}
|
|
playoffRounds={loaderData.playoffRounds}
|
|
bracketTemplateId={loaderData.bracketTemplateId}
|
|
teamOwnerships={loaderData.teamOwnerships}
|
|
userParticipantIds={loaderData.userParticipantIds}
|
|
/>
|
|
)}
|
|
|
|
{/* Bracket event (tennis/golf/soccer) */}
|
|
{loaderData.kind === "bracket" && (
|
|
<div className="space-y-6">
|
|
{loaderData.groupStandings.length > 0 && (
|
|
<GroupStageStandings
|
|
groups={loaderData.groupStandings}
|
|
ownershipMap={ownershipMap}
|
|
showEmpty={false}
|
|
/>
|
|
)}
|
|
{loaderData.playoffMatches.length > 0 ? (
|
|
<PlayoffBracket
|
|
matches={loaderData.playoffMatches}
|
|
rounds={loaderData.playoffRounds}
|
|
bracketTemplateId={loaderData.bracketTemplateId}
|
|
preEliminatedParticipants={loaderData.preEliminatedParticipants}
|
|
teamOwnerships={loaderData.teamOwnerships}
|
|
userParticipantIds={loaderData.userParticipantIds}
|
|
showOwnership={true}
|
|
mode="bracket"
|
|
/>
|
|
) : (
|
|
<Card>
|
|
<CardContent className="py-8 text-center">
|
|
<p className="text-muted-foreground text-sm">Bracket not yet available.</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Results table (QP tournaments, racing events) */}
|
|
{loaderData.kind === "results" && (
|
|
loaderData.eventResults.length > 0 ? (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-base">Results</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="pt-0">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-12">#</TableHead>
|
|
<TableHead>Participant</TableHead>
|
|
<TableHead>Manager</TableHead>
|
|
<TableHead className="text-right">QP</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{loaderData.eventResults.map((r) => {
|
|
const ownership = ownershipMap[r.seasonParticipantId];
|
|
const isUser = loaderData.userParticipantIds.includes(r.seasonParticipantId);
|
|
return (
|
|
<TableRow key={r.id} className={isUser ? "bg-electric/5" : undefined}>
|
|
<TableCell className="text-muted-foreground">{r.placement}</TableCell>
|
|
<TableCell className="font-medium">
|
|
{r.participantName ?? "—"}
|
|
{isUser && <span className="ml-1.5 text-xs text-electric">★</span>}
|
|
</TableCell>
|
|
<TableCell className="text-muted-foreground text-sm">
|
|
{ownership?.teamName ?? "—"}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
{r.qualifyingPointsAwarded != null
|
|
? parseFloat(r.qualifyingPointsAwarded).toFixed(2)
|
|
: r.rawScore != null
|
|
? r.rawScore
|
|
: "—"}
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</CardContent>
|
|
</Card>
|
|
) : (
|
|
<Card>
|
|
<CardContent className="py-8 text-center">
|
|
<p className="text-muted-foreground text-sm">Results not yet available for this event.</p>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
)}
|
|
</div>
|
|
);
|
|
}
|