brackt/app/components/events/Cs2MajorEventView.tsx
chrisp fe4e1b3f3c
All checks were successful
🚀 Deploy / 🧪 Test (push) Successful in 3m24s
🚀 Deploy / ʦ🔍 Typecheck & Lint (push) Successful in 1m38s
🚀 Deploy / 🐳 Build (push) Successful in 1m29s
🚀 Deploy / 🚀 Deploy (push) Successful in 9s
claude/beautiful-hawking-a72ilq (#92)
2026-06-16 14:11:51 +00:00

257 lines
8.7 KiB
TypeScript
Raw Permalink 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 { Card, CardContent } 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 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 matchesInStage = matches.filter((m) => m.matchStage === 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 ? (
<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 hasChampionsActivity = playoffMatches.some((m) => m.winnerId !== null);
const highestActiveStage: 1 | 2 | 3 =
cs2StageResults.some((r) => r.stageEntry >= 3 || r.stageEliminated === 3) ? 3 :
cs2StageResults.some((r) => r.stageEntry >= 2 || r.stageEliminated === 2) ? 2 : 1;
const [activeTab, setActiveTab] = useState<StageTab>(
hasChampionsActivity ? "champions" : highestActiveStage
);
const tabs: StageTab[] = [1, 2, 3, "champions"];
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" && (
playoffMatches.length > 0 ? (
<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>
);
}