brackt/app/components/events/Cs2MajorEventView.tsx

258 lines
8.7 KiB
TypeScript
Raw Normal View History

2026-06-16 14:11:51 +00:00
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>
);
}