From b4b4fc7e0625a096df0ffa1b1d287808207dbc0b Mon Sep 17 00:00:00 2001 From: Chris Parsons Date: Mon, 16 Mar 2026 12:25:03 -0700 Subject: [PATCH] Fix First Four appearing out of order in NCAA bracket display Use bracket template's canonical round order instead of sorting by match count, which placed First Four (4 matches) after Elite Eight (also 4 matches). Extract getOrderedRoundsFromMatches() into bracket-templates.ts to eliminate identical logic duplicated in the admin bracket component. Co-Authored-By: Claude Sonnet 4.6 --- app/lib/bracket-templates.ts | 26 +++++++++++++++++++ ...ts-seasons.$id.events.$eventId.bracket.tsx | 16 +++--------- ...d.sports-seasons.$sportsSeasonId.server.ts | 11 +++----- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/app/lib/bracket-templates.ts b/app/lib/bracket-templates.ts index 0399a14..e1fba39 100644 --- a/app/lib/bracket-templates.ts +++ b/app/lib/bracket-templates.ts @@ -582,6 +582,32 @@ export function getAllBracketTemplates(): BracketTemplate[] { return Object.values(BRACKET_TEMPLATES); } +/** + * Returns round names in chronological order (earliest first) for the given matches. + * When a template is provided its defined order is used; otherwise rounds are sorted + * by descending match count (more matches = earlier round). + */ +export function getOrderedRoundsFromMatches( + matches: Array<{ round: string }>, + template?: BracketTemplate +): string[] { + const roundsInMatches = new Set(matches.map((m) => m.round)); + + if (template) { + return template.rounds + .map((r) => r.name) + .filter((name) => roundsInMatches.has(name)); + } + + const roundMatchCounts = new Map(); + for (const m of matches) { + roundMatchCounts.set(m.round, (roundMatchCounts.get(m.round) || 0) + 1); + } + return Array.from(roundMatchCounts.keys()).sort( + (a, b) => (roundMatchCounts.get(b) || 0) - (roundMatchCounts.get(a) || 0) + ); +} + /** * Helper to determine scoring round type based on match count in scoring rounds * Used for determining placement sharing (5-8th, 3-4th, 1-2nd) diff --git a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx index 59b61a0..6eac747 100644 --- a/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx +++ b/app/routes/admin.sports-seasons.$id.events.$eventId.bracket.tsx @@ -22,7 +22,7 @@ import { SelectValue, } from "~/components/ui/select"; import { ArrowLeft, Plus, Trophy, X, Check, ChevronDown, ChevronRight, Calendar, TrendingUp, Trash2 } from "lucide-react"; -import { getAllBracketTemplates, getBracketTemplate, buildNCAA68SlotMap, ALL_16_SEEDS, type BracketRegion } from "~/lib/bracket-templates"; +import { getAllBracketTemplates, getBracketTemplate, getOrderedRoundsFromMatches, buildNCAA68SlotMap, ALL_16_SEEDS, type BracketRegion } from "~/lib/bracket-templates"; import { Table, TableBody, @@ -277,18 +277,8 @@ export default function EventBracket({ // Memoized: Get available rounds in chronological order const availableRounds = useMemo(() => { - const roundsInMatches = Array.from(new Set(matches.map(m => m.round))); - - if (event.bracketTemplateId) { - const eventTemplate = getBracketTemplate(event.bracketTemplateId); - if (eventTemplate) { - return eventTemplate.rounds - .map((r) => r.name) - .filter((roundName: string) => roundsInMatches.includes(roundName)); - } - } - - return roundsInMatches; + const template = event.bracketTemplateId ? getBracketTemplate(event.bracketTemplateId) : undefined; + return getOrderedRoundsFromMatches(matches, template); }, [matches, event.bracketTemplateId]); // Check if all matches are complete diff --git a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts index 0f0df0d..7012a0b 100644 --- a/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts +++ b/app/routes/leagues/$leagueId.sports-seasons.$sportsSeasonId.server.ts @@ -14,6 +14,7 @@ import { getUpcomingScoringEvents, getRecentCompletedEvents, } from "~/models/scoring-event"; +import { getBracketTemplate, getOrderedRoundsFromMatches } from "~/lib/bracket-templates"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; import { eq, and, inArray } from "drizzle-orm"; @@ -160,13 +161,9 @@ export async function loader(args: Route.LoaderArgs) { }); playoffMatches = matches; - const roundMatchCounts = new Map(); - for (const m of matches) { - roundMatchCounts.set(m.round, (roundMatchCounts.get(m.round) || 0) + 1); - } - playoffRounds = Array.from(roundMatchCounts.keys()).sort( - (a, b) => (roundMatchCounts.get(b) || 0) - (roundMatchCounts.get(a) || 0) - ); + const templateId = events.find((e) => e.bracketTemplateId)?.bracketTemplateId; + const template = templateId ? getBracketTemplate(templateId) : undefined; + playoffRounds = getOrderedRoundsFromMatches(matches, template); } // Fetch group-stage losers and all participant results for bracket scoring