brackt/app/components/scoring/TabbedBracketLayout.tsx
2026-04-21 13:39:43 -07:00

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>
);
}