Fix First Four appearing out of order in NCAA bracket display (#152)

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 <noreply@anthropic.com>
This commit is contained in:
Chris Parsons 2026-03-16 12:27:43 -07:00 committed by GitHub
parent 84787c9a4b
commit b1535b2976
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 33 additions and 20 deletions

View file

@ -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<string, number>();
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)

View file

@ -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

View file

@ -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<string, number>();
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