brackt/app/components/scoring/TabbedBracketLayout.tsx

241 lines
8.5 KiB
TypeScript

import { cn } from "~/lib/utils";
import type { BracketPhase, ConferenceGroup } from "~/lib/bracket-templates";
import { TreeColumns, BracketMatchSlot, type BracketMatch, type BracketOwnership } from "./BracketTreeView";
import { BracketTreePaginated } from "./BracketTreePaginated";
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);
}
// ─── Play-In Layout ───────────────────────────────────────────────────────────
interface PlayInColumnProps {
label: string;
description: string;
match: BracketMatch | undefined;
ownershipMap: Map<string, BracketOwnership>;
userParticipantIds: Set<string>;
}
function PlayInColumn({
label,
description,
match,
ownershipMap,
userParticipantIds,
}: PlayInColumnProps) {
return (
<div>
<p className="text-[10px] font-semibold uppercase tracking-wider text-muted-foreground text-center mb-2">
{label}
</p>
{match ? (
<BracketMatchSlot
match={match}
slotHeight={CARD_H}
ownershipMap={ownershipMap}
userParticipantIds={userParticipantIds}
/>
) : (
<div style={{ height: CARD_H }} />
)}
<p className="text-[10px] text-muted-foreground text-center mt-2">{description}</p>
</div>
);
}
interface PlayInLayoutProps {
matchesByRound: Map<string, BracketMatch[]>;
ownershipMap: Map<string, BracketOwnership>;
userParticipantIds: Set<string>;
}
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 (
<div className="space-y-6">
{conferences.map((conf) => (
<div key={conf.name}>
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-3">
{conf.name}
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<PlayInColumn
label="7 VS 8"
description="Winner → 7 seed · Loser plays again"
match={conf.match78}
ownershipMap={ownershipMap}
userParticipantIds={userParticipantIds}
/>
<PlayInColumn
label="9 VS 10"
description="Winner plays again · Loser eliminated"
match={conf.match910}
ownershipMap={ownershipMap}
userParticipantIds={userParticipantIds}
/>
<PlayInColumn
label="For 8 Seed"
description="Winner → 8 seed · Loser eliminated"
match={conf.matchFor8}
ownershipMap={ownershipMap}
userParticipantIds={userParticipantIds}
/>
</div>
</div>
))}
</div>
);
}
// ─── Main component ───────────────────────────────────────────────────────────
export function TabbedBracketLayout({
rounds,
matchesByRound,
ownershipMap,
userParticipantIds,
phases,
scoringRoundIdx,
}: TabbedBracketLayoutProps) {
return (
<div className="space-y-10">
{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;
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 key={phase.name}>
<p className={cn(
"text-sm font-semibold uppercase tracking-wider mb-4",
phases.length > 1 ? "text-muted-foreground" : "hidden"
)}>
{phase.name}
</p>
{/* Desktop */}
<div className="hidden md:block">
{phase.layout === "play-in" ? (
<PlayInLayout
matchesByRound={phaseMatchesByRound}
ownershipMap={ownershipMap}
userParticipantIds={userParticipantIds}
/>
) : 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">
{phase.layout === "play-in" ? (
<PlayInLayout
matchesByRound={phaseMatchesByRound}
ownershipMap={ownershipMap}
userParticipantIds={userParticipantIds}
/>
) : (
<BracketTreePaginated
rounds={phaseRounds}
matchesByRound={phaseMatchesByRound}
ownershipMap={ownershipMap}
userParticipantIds={userParticipantIds}
firstScoringRoundIdx={phaseFirstScoringIdx >= 0 ? phaseFirstScoringIdx : undefined}
/>
)}
</div>
</div>
);
})}
</div>
);
}