From facb0e358ec1c1bb45854ac671b31efc47e5752b Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Fri, 12 Jun 2026 17:26:36 +0300 Subject: [PATCH] Label underground bracket in the FactCard --- .../tournament/tournament-utils.test.ts | 57 ++++++++++++++++--- app/features/tournament/tournament-utils.ts | 45 +++++++++++---- 2 files changed, 82 insertions(+), 20 deletions(-) diff --git a/app/features/tournament/tournament-utils.test.ts b/app/features/tournament/tournament-utils.test.ts index 7309a3672..bd3a83c88 100644 --- a/app/features/tournament/tournament-utils.test.ts +++ b/app/features/tournament/tournament-utils.test.ts @@ -797,17 +797,26 @@ describe("tournamentNameParts", () => { }); describe("bracketProgressionLabel", () => { + const bracket = ( + bracket: Partial & Pick, + ): ParsedBracket => ({ + name: bracket.type, + requiresCheckIn: false, + settings: {}, + ...bracket, + }); + it("returns the short code for a single stage", () => { - expect(bracketProgressionLabel([{ type: "single_elimination" }])).toBe( - "SE", - ); + expect( + bracketProgressionLabel([bracket({ type: "single_elimination" })]), + ).toBe("SE"); }); it("joins stages with an arrow", () => { expect( bracketProgressionLabel([ - { type: "round_robin" }, - { type: "single_elimination" }, + bracket({ type: "round_robin" }), + bracket({ type: "single_elimination" }), ]), ).toBe("RR → SE"); }); @@ -815,13 +824,45 @@ describe("bracketProgressionLabel", () => { it("collapses consecutive duplicate stages", () => { expect( bracketProgressionLabel([ - { type: "single_elimination" }, - { type: "single_elimination" }, - { type: "double_elimination" }, + bracket({ type: "single_elimination" }), + bracket({ type: "single_elimination" }), + bracket({ type: "double_elimination" }), ]), ).toBe("SE → DE"); }); + it("appends an underground bracket of a new type with a plus and (UG) tag", () => { + expect( + bracketProgressionLabel([ + bracket({ type: "double_elimination" }), + bracket({ + type: "single_elimination", + sources: [{ bracketIdx: 0, placements: [-1] }], + }), + ]), + ).toBe("DE + SE (UG)"); + }); + + it("collapses repeated underground brackets of an already-shown type", () => { + expect( + bracketProgressionLabel([ + bracket({ type: "round_robin" }), + bracket({ + type: "single_elimination", + sources: [{ bracketIdx: 0, placements: [1] }], + }), + bracket({ + type: "single_elimination", + sources: [{ bracketIdx: 0, placements: [2] }], + }), + bracket({ + type: "single_elimination", + sources: [{ bracketIdx: 0, placements: [3] }], + }), + ]), + ).toBe("RR → SE (UG)"); + }); + it("returns empty string for empty progression", () => { expect(bracketProgressionLabel([])).toBe(""); }); diff --git a/app/features/tournament/tournament-utils.ts b/app/features/tournament/tournament-utils.ts index 210b3b50e..c9d32071c 100644 --- a/app/features/tournament/tournament-utils.ts +++ b/app/features/tournament/tournament-utils.ts @@ -500,30 +500,51 @@ const STAGE_TYPE_TO_SHORT_CODE: Record< }; /** - * Builds a compact arrow-separated label describing the bracket progression of a tournament, + * Builds a compact label describing the bracket progression of a tournament, * derived from `settings.bracketProgression`. * - * Each stage type is rendered as a short code (`RR`, `SE`, `DE`, `SW`) and consecutive duplicates - * are collapsed so e.g. two single-elimination stages still render as a single `SE`. + * Each stage type is rendered as a short code (`RR`, `SE`, `DE`, `SW`). Main progression stages are + * arrow-separated with consecutive duplicates collapsed (e.g. two single-elimination stages still + * render as a single `SE`). + * + * Underground brackets are not part of the main progression — they give early losers another chance + * to play. When present the whole label is tagged `(UG)` once. An underground bracket type that does + * not already appear in the main progression is also surfaced with `+ {code}`, while repeated + * underground brackets of an already-shown type are collapsed away to keep the label compact. * * @example - * // [{type: "round_robin"}, {type: "single_elimination"}] - * bracketProgressionLabel(progression) // "RR → SE" + * // [{type: "round_robin"}, {type: "single_elimination"}, ...underground SE brackets] + * bracketProgressionLabel(progression) // "RR → SE (UG)" + * // [{type: "double_elimination"}, {type: "single_elimination", sources: [...]}] + * bracketProgressionLabel(progression) // "DE + SE (UG)" */ -export function bracketProgressionLabel( - progression: Pick[], -): string { +export function bracketProgressionLabel(progression: ParsedBracket[]): string { if (progression.length === 0) return ""; - const codes: string[] = []; - for (const bracket of progression) { - const code = STAGE_TYPE_TO_SHORT_CODE[bracket.type]; + const mainCodes: string[] = []; + const undergroundCodes: string[] = []; + for (let i = 0; i < progression.length; i++) { + const code = STAGE_TYPE_TO_SHORT_CODE[progression[i].type]; + const codes = Progression.isUnderground(i, progression) + ? undergroundCodes + : mainCodes; if (codes.at(-1) !== code) { codes.push(code); } } - return codes.join(" → "); + const mainLabel = mainCodes.join(" → "); + + if (undergroundCodes.length === 0) return mainLabel; + + const extraCodes = undergroundCodes.filter( + (code) => !mainCodes.includes(code), + ); + + const label = + extraCodes.length > 0 ? [mainLabel, ...extraCodes].join(" + ") : mainLabel; + + return `${label} (UG)`; } /**