brackt/app/components/events/Cs2MajorEventView.tsx
Claude df70ef83c2
Add CS2 schedule fix, Champions Stage floor scoring, and event detail view
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
2026-06-16 00:02:28 +00:00

288 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from "react";
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 { cn } from "~/lib/utils";
import { PlayoffBracket, type Match } from "~/components/scoring/PlayoffBracket";
import { CheckCircle2, XCircle, Clock } from "lucide-react";
import type { Cs2StageResultWithName } from "~/models/cs2-major-stage";
import type { SeasonMatch } from "~/models/season-match";
type StageTab = 1 | 2 | 3 | "champions";
interface Ownership {
participantId: string;
teamName: string;
teamId: string;
ownerName?: string;
}
interface Cs2MatchRow extends Omit<SeasonMatch, "scheduledAt" | "startedAt" | "completedAt"> {
scheduledAt: string | null;
startedAt: string | null;
completedAt: string | null;
}
interface PlayoffMatchWithRelations extends Match {
participant1: { id: string; name: string } | null;
participant2: { id: string; name: string } | null;
winner: { id: string; name: string } | null;
loser: { id: string; name: string } | null;
}
interface Props {
cs2StageResults: Cs2StageResultWithName[];
seasonMatches: Cs2MatchRow[];
playoffMatches: PlayoffMatchWithRelations[];
playoffRounds: string[];
bracketTemplateId: string | null;
teamOwnerships: Ownership[];
userParticipantIds: string[];
}
const STAGE_LABELS: Record<StageTab, string> = {
1: "Stage 1",
2: "Stage 2",
3: "Stage 3",
champions: "Champions",
};
function StageStatusBadge({ result }: { result: Cs2StageResultWithName }) {
if (result.stageEliminated !== null) {
return (
<span className="flex items-center gap-1 text-xs text-red-400">
<XCircle className="h-3 w-3" />
Eliminated
</span>
);
}
const winsNeeded = 3;
const wins = result.stageEliminatedWins ?? 0;
if (result.stageEliminated === null && result.stageEntry !== null) {
// Null stageEliminated = either in progress or advanced
// If their stageEntry is less than the current stage context, they advanced
return (
<span className="flex items-center gap-1 text-xs text-emerald-400">
<CheckCircle2 className="h-3 w-3" />
Advanced
</span>
);
}
return (
<span className="flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="h-3 w-3" />
In Progress
</span>
);
}
function SwissStageTab({
stage,
stageResults,
matches,
teamOwnerships,
userParticipantIds,
}: {
stage: 1 | 2 | 3;
stageResults: Cs2StageResultWithName[];
matches: Cs2MatchRow[];
teamOwnerships: Ownership[];
userParticipantIds: string[];
}) {
const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o]));
const userIds = new Set(userParticipantIds);
const teamsInStage = stageResults.filter((r) => r.stageEntry <= stage && (r.stageEliminated === null || r.stageEliminated >= stage));
const matchesInStage = matches.filter((m) => m.matchStage === stage);
// Group teams by W-L record (approximated from stageEliminatedWins if eliminated, else unknown)
const eliminated = stageResults.filter((r) => r.stageEliminated === stage);
const advanced = stageResults.filter((r) => r.stageEliminated === null && r.stageEntry <= stage);
if (stageResults.length === 0) {
return (
<p className="text-sm text-muted-foreground py-4">No teams assigned to this stage yet.</p>
);
}
return (
<div className="space-y-4">
<Table>
<TableHeader>
<TableRow>
<TableHead>Team</TableHead>
<TableHead>Manager</TableHead>
<TableHead className="text-right">W</TableHead>
<TableHead className="text-right">L</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{stageResults.map((r) => {
const ownership = ownershipMap.get(r.participantId);
const isUser = userIds.has(r.participantId);
const wins = r.stageEliminated === stage ? (r.stageEliminatedWins ?? "—") : "—";
const losses = r.stageEliminated === stage ? (3 - (r.stageEliminatedWins ?? 0)) : "—";
const isElim = r.stageEliminated === stage;
const isAdvanced = r.stageEliminated === null || (r.stageEliminated !== null && r.stageEliminated > stage);
return (
<TableRow
key={r.participantId}
className={cn(
isUser && "bg-electric/5",
isElim && "opacity-50"
)}
>
<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 ? ownership.teamName : "—"}
</TableCell>
<TableCell className="text-right">{wins}</TableCell>
<TableCell className="text-right">{losses}</TableCell>
<TableCell>
{isElim ? (
<span className="flex items-center gap-1 text-xs text-red-400">
<XCircle className="h-3 w-3" /> Eliminated
</span>
) : isAdvanced && r.stageEliminated === null ? (
<span className="flex items-center gap-1 text-xs text-emerald-400">
<CheckCircle2 className="h-3 w-3" /> Advanced
</span>
) : (
<span className="flex items-center gap-1 text-xs text-muted-foreground">
<Clock className="h-3 w-3" /> In Progress
</span>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
{matchesInStage.length > 0 && (
<div>
<h3 className="text-sm font-medium text-muted-foreground mb-2">Matches</h3>
<div className="space-y-1">
{matchesInStage.map((m) => {
const p1Name = m.participant1Id
? stageResults.find((r) => r.participantId === m.participant1Id)?.participantName ?? m.participant1Id.slice(0, 8)
: "TBD";
const p2Name = m.participant2Id
? stageResults.find((r) => r.participantId === m.participant2Id)?.participantName ?? m.participant2Id.slice(0, 8)
: "TBD";
const dateStr = m.scheduledAt
? new Date(m.scheduledAt).toLocaleDateString(undefined, { month: "short", day: "numeric" })
: null;
return (
<div
key={m.id}
className={cn(
"flex items-center justify-between text-sm px-3 py-1.5 rounded-md",
m.status === "complete" ? "bg-muted/30" : "bg-muted/10"
)}
>
<span className={cn(m.winnerId === m.participant1Id && "font-semibold text-emerald-400")}>{p1Name}</span>
<span className="text-xs text-muted-foreground px-2">
{m.status === "complete"
? `${m.participant1Score ?? 0}${m.participant2Score ?? 0}`
: dateStr ?? "vs"}
</span>
<span className={cn("text-right", m.winnerId === m.participant2Id && "font-semibold text-emerald-400")}>{p2Name}</span>
</div>
);
})}
</div>
</div>
)}
</div>
);
}
export function Cs2MajorEventView({
cs2StageResults,
seasonMatches,
playoffMatches,
playoffRounds,
bracketTemplateId,
teamOwnerships,
userParticipantIds,
}: Props) {
const hasChampions = playoffMatches.length > 0;
const [activeTab, setActiveTab] = useState<StageTab>(hasChampions ? "champions" : 3);
const tabs: StageTab[] = [1, 2, 3, "champions"];
const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o]));
return (
<div className="space-y-4">
{/* Tab bar */}
<div className="flex gap-1 rounded-lg bg-muted p-1 w-fit">
{tabs.map((tab) => (
<button
key={tab}
type="button"
onClick={() => setActiveTab(tab)}
className={cn(
"px-3 py-1.5 text-sm font-medium rounded-md transition-colors",
activeTab === tab
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}
>
{STAGE_LABELS[tab]}
</button>
))}
</div>
{/* Swiss stage tabs */}
{activeTab !== "champions" && (
<SwissStageTab
stage={activeTab as 1 | 2 | 3}
stageResults={cs2StageResults.filter(
(r) =>
r.stageEntry <= (activeTab as number) &&
(r.stageEliminated === null || r.stageEliminated >= (activeTab as number))
)}
matches={seasonMatches}
teamOwnerships={teamOwnerships}
userParticipantIds={userParticipantIds}
/>
)}
{/* Champions Stage bracket */}
{activeTab === "champions" && (
hasChampions ? (
<PlayoffBracket
matches={playoffMatches}
rounds={playoffRounds}
bracketTemplateId={bracketTemplateId}
teamOwnerships={teamOwnerships}
userParticipantIds={userParticipantIds}
showOwnership={true}
title="Champions Stage"
mode="bracket"
/>
) : (
<Card>
<CardContent className="py-8 text-center">
<p className="text-muted-foreground text-sm">
Champions Stage bracket will be available once Stage 3 is complete.
</p>
</CardContent>
</Card>
)
)}
</div>
);
}