140 lines
4.7 KiB
TypeScript
140 lines
4.7 KiB
TypeScript
import { useState } from "react";
|
|
import { cn } from "~/lib/utils";
|
|
import type { BracketPhase, ConferenceGroup } from "~/lib/bracket-templates";
|
|
import {
|
|
TreeColumns,
|
|
BracketTreePaginated,
|
|
type BracketMatch,
|
|
type BracketOwnership,
|
|
} from "./BracketTreeView";
|
|
|
|
interface TabbedBracketLayoutProps {
|
|
rounds: string[];
|
|
matchesByRound: Map<string, BracketMatch[]>;
|
|
ownershipMap: Map<string, BracketOwnership>;
|
|
userParticipantIds: Set<string>;
|
|
phases: BracketPhase[];
|
|
scoringRoundIdx: number;
|
|
}
|
|
|
|
const CARD_H = 112;
|
|
const CARD_GAP = 14;
|
|
|
|
function groupMatches(
|
|
matchesByRound: Map<string, BracketMatch[]>,
|
|
group: ConferenceGroup
|
|
): Map<string, BracketMatch[]> {
|
|
const out = new Map<string, BracketMatch[]>();
|
|
for (const [round, allowed] of Object.entries(group.roundMatchNumbers)) {
|
|
const filtered = (matchesByRound.get(round) ?? []).filter((m) =>
|
|
allowed.includes(m.matchNumber)
|
|
);
|
|
if (filtered.length > 0) out.set(round, filtered);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function phaseHeight(matchesByRound: Map<string, BracketMatch[]>, rounds: string[]): number {
|
|
const max = Math.max(...rounds.map((r) => matchesByRound.get(r)?.length ?? 0), 1);
|
|
return max * (CARD_H + CARD_GAP);
|
|
}
|
|
|
|
export function TabbedBracketLayout({
|
|
rounds,
|
|
matchesByRound,
|
|
ownershipMap,
|
|
userParticipantIds,
|
|
phases,
|
|
scoringRoundIdx,
|
|
}: TabbedBracketLayoutProps) {
|
|
const [activeIdx, setActiveIdx] = useState(0);
|
|
const phase = phases[activeIdx];
|
|
|
|
const groupRounds = phase.groups
|
|
? rounds.filter((r) => phase.groups?.some((g) => g.roundMatchNumbers[r] !== undefined))
|
|
: [];
|
|
const sharedRounds = (phase.sharedRounds ?? []).filter((r) => rounds.includes(r));
|
|
const simpleRounds = phase.groups ? [] : (phase.rounds ?? []).filter((r) => rounds.includes(r));
|
|
|
|
const phaseRounds = phase.groups ? [...groupRounds, ...sharedRounds] : simpleRounds;
|
|
const phaseMatchesByRound = new Map(phaseRounds.map((r) => [r, matchesByRound.get(r) ?? []]));
|
|
const sharedMatchesByRound = new Map(sharedRounds.map((r) => [r, matchesByRound.get(r) ?? []]));
|
|
|
|
const phaseFirstScoringIdx = phaseRounds.findIndex((r) => rounds.indexOf(r) >= scoringRoundIdx);
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex gap-1 rounded-lg bg-muted p-1 w-fit">
|
|
{phases.map((p, i) => (
|
|
<button
|
|
key={p.name}
|
|
type="button"
|
|
onClick={() => setActiveIdx(i)}
|
|
className={cn(
|
|
"px-3 py-1.5 text-sm font-medium rounded-md transition-colors",
|
|
activeIdx === i
|
|
? "bg-background shadow-sm text-foreground"
|
|
: "text-muted-foreground hover:text-foreground"
|
|
)}
|
|
>
|
|
{p.name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Desktop */}
|
|
<div className="hidden md:block">
|
|
{phase.groups ? (
|
|
<div className="space-y-8">
|
|
{phase.groups.map((group) => {
|
|
const gMatches = groupMatches(matchesByRound, group);
|
|
const gRounds = groupRounds.filter((r) => group.roundMatchNumbers[r] !== undefined);
|
|
return (
|
|
<div key={group.name}>
|
|
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-2">
|
|
{group.name}
|
|
</p>
|
|
<TreeColumns
|
|
visibleRounds={gRounds}
|
|
matchesByRound={gMatches}
|
|
ownershipMap={ownershipMap}
|
|
userParticipantIds={userParticipantIds}
|
|
bracketHeight={phaseHeight(gMatches, gRounds)}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
{sharedRounds.length > 0 && (
|
|
<TreeColumns
|
|
visibleRounds={sharedRounds}
|
|
matchesByRound={sharedMatchesByRound}
|
|
ownershipMap={ownershipMap}
|
|
userParticipantIds={userParticipantIds}
|
|
bracketHeight={phaseHeight(sharedMatchesByRound, sharedRounds)}
|
|
/>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<TreeColumns
|
|
visibleRounds={phaseRounds}
|
|
matchesByRound={phaseMatchesByRound}
|
|
ownershipMap={ownershipMap}
|
|
userParticipantIds={userParticipantIds}
|
|
bracketHeight={phaseHeight(phaseMatchesByRound, phaseRounds)}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{/* Mobile */}
|
|
<div className="md:hidden">
|
|
<BracketTreePaginated
|
|
rounds={phaseRounds}
|
|
matchesByRound={phaseMatchesByRound}
|
|
ownershipMap={ownershipMap}
|
|
userParticipantIds={userParticipantIds}
|
|
firstScoringRoundIdx={phaseFirstScoringIdx >= 0 ? phaseFirstScoringIdx : undefined}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|