import { cn } from "~/lib/utils"; import type { BracketPhase, BracketTemplate, ConferenceGroup } from "~/lib/bracket-templates"; import type { FeederMap } from "~/lib/bracket-layout"; import { TreeColumns, BracketMatchSlot, bracketGeometry, type BracketMatch, type BracketOwnership, } from "./BracketTreeView"; import { BracketTreePaginated } from "./BracketTreePaginated"; /** Card height for the play-in columns, which lay themselves out rather than via TreeColumns. */ const CARD_H = 112; interface TabbedBracketLayoutProps { rounds: string[]; matchesByRound: Map; ownershipMap: Map; userParticipantIds: Set; phases: BracketPhase[]; scoringRoundIdx: number; feeders?: FeederMap; template?: BracketTemplate; } function groupMatches( matchesByRound: Map, group: ConferenceGroup ): Map { const out = new Map(); 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; } // ─── Play-In Layout ─────────────────────────────────────────────────────────── interface PlayInColumnProps { label: string; description: string; match: BracketMatch | undefined; ownershipMap: Map; userParticipantIds: Set; } function PlayInColumn({ label, description, match, ownershipMap, userParticipantIds, }: PlayInColumnProps) { return (

{label}

{match ? ( ) : (
)}

{description}

); } interface PlayInLayoutProps { matchesByRound: Map; ownershipMap: Map; userParticipantIds: Set; } function PlayInLayout({ matchesByRound, ownershipMap, userParticipantIds }: PlayInLayoutProps) { const pir1 = matchesByRound.get("Play-In Round 1") ?? []; const pir2 = matchesByRound.get("Play-In Round 2") ?? []; const conferences = [ { name: "Eastern Conference", match78: pir1.find((m) => m.matchNumber === 1), match910: pir1.find((m) => m.matchNumber === 2), matchFor8: pir2.find((m) => m.matchNumber === 1), }, { name: "Western Conference", match78: pir1.find((m) => m.matchNumber === 3), match910: pir1.find((m) => m.matchNumber === 4), matchFor8: pir2.find((m) => m.matchNumber === 2), }, ]; return (
{conferences.map((conf) => (

{conf.name}

))}
); } // ─── Main component ─────────────────────────────────────────────────────────── export function TabbedBracketLayout({ rounds, matchesByRound, ownershipMap, userParticipantIds, phases, scoringRoundIdx, feeders, template, }: TabbedBracketLayoutProps) { const roundOrder = template?.rounds.map((r) => r.name) ?? rounds; return (
{phases.map((phase) => { 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; // Restrict each round to the match numbers this phase's groups actually claim. // Rounds can be shared across phases (LLWS runs U.S. and International through // the same rounds), so without this the mobile view would merge both sides into // one column. No-op where a phase's groups already cover every match in the // round (NCAA regions, NBA conferences) and for sharedRounds, which have no // group filter. const phaseMatchesByRound = new Map( phaseRounds.map((r) => { const all = matchesByRound.get(r) ?? []; if (!phase.groups || sharedRounds.includes(r)) return [r, all] as const; const allowed = new Set( phase.groups.flatMap((g) => g.roundMatchNumbers[r] ?? []) ); return [r, allowed.size > 0 ? all.filter((m) => allowed.has(m.matchNumber)) : all] as const; }) ); const sharedMatchesByRound = new Map(sharedRounds.map((r) => [r, matchesByRound.get(r) ?? []])); const phaseFirstScoringIdx = phaseRounds.findIndex((r) => rounds.indexOf(r) >= scoringRoundIdx); return (

1 ? "text-muted-foreground" : "hidden" )}> {phase.name}

{/* Desktop */}
{phase.layout === "play-in" ? ( ) : phase.groups ? (
{phase.groups.map((group) => { const gMatches = groupMatches(matchesByRound, group); const gRounds = groupRounds.filter((r) => group.roundMatchNumbers[r] !== undefined); const geometry = bracketGeometry(gRounds, gMatches, feeders, roundOrder); return (

{group.name}

); })} {sharedRounds.length > 0 && ( )}
) : ( )}
{/* Mobile — paged one group at a time, matching the desktop split. Paging a whole phase would merge the winners and elimination brackets into one tree, and a double-elimination phase is a DAG rather than a tree: the same game feeds forward and sideways, so its column placement would be arbitrary. */}
{phase.layout === "play-in" ? ( ) : phase.groups ? ( <> {phase.groups.map((group) => (

{group.name}

group.roundMatchNumbers[r] !== undefined)} matchesByRound={groupMatches(matchesByRound, group)} ownershipMap={ownershipMap} userParticipantIds={userParticipantIds} feeders={feeders} template={template} />
))} {sharedRounds.length > 0 && ( )} ) : ( = 0 ? phaseFirstScoringIdx : undefined} feeders={feeders} template={template} /> )}
); })}
); }