From 2500fcca5d7a8399055552fc2f37bf4b858e0c2d Mon Sep 17 00:00:00 2001 From: Akirus12 Date: Fri, 12 Jun 2026 18:00:10 +0200 Subject: [PATCH 1/5] PART 1 DONE: changed the link for round config to GenericButton (fixed props.smol to actually make the button small) AND removed placeholder "ret" div text from DebateDetailsPage --- src/app/[locale]/t/[path]/debates/[debate_id]/page.tsx | 1 - src/components/tournament/ladder/LadderRoundRow.tsx | 8 +++++--- src/components/ui/GenericButton.tsx | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/app/[locale]/t/[path]/debates/[debate_id]/page.tsx b/src/app/[locale]/t/[path]/debates/[debate_id]/page.tsx index 419b4a8..54e1236 100644 --- a/src/app/[locale]/t/[path]/debates/[debate_id]/page.tsx +++ b/src/app/[locale]/t/[path]/debates/[debate_id]/page.tsx @@ -121,7 +121,6 @@ export default async function DebateDetailsPage({ {" "} {motion}{" "} -
ret
diff --git a/src/components/tournament/ladder/LadderRoundRow.tsx b/src/components/tournament/ladder/LadderRoundRow.tsx index 5d9e2d4..3b1eca2 100644 --- a/src/components/tournament/ladder/LadderRoundRow.tsx +++ b/src/components/tournament/ladder/LadderRoundRow.tsx @@ -4,6 +4,7 @@ import { LadderDebateNode } from "./LadderDebateNode"; import { Phase } from "@/types/Phase"; import { Motion } from "@/types/Motion"; import { useTranslations } from "next-intl"; +import { GenericButton } from "@/components/ui/GenericButton"; export function LadderRoundRow({ onOpenConfig, @@ -36,12 +37,13 @@ export function LadderRoundRow({ return (
- +
{debates.map((debate) => ( Date: Fri, 12 Jun 2026 19:19:30 +0200 Subject: [PATCH 2/5] PART 2: added round string parsing to not have ugly round_1 anymore, and also implemented this change to the popup as well. i also made the screen scroll up to the pop up when you click the button. and small test changes to work with the new parsed strings. --- .../tournament/dashboard/DashSide.tsx | 30 +++-- .../tournament/ladder/LadderRoundRow.tsx | 39 +++++- .../tournament/ladder/LadderView.tsx | 4 + .../tournament/ladder/RoundConfig.tsx | 111 ++++++++++++------ src/i18n/translations/en.json | 10 +- test/e2e/e2eUtils.ts | 2 +- test/e2e/tournament-ladder-rendering.test.ts | 4 +- 7 files changed, 143 insertions(+), 57 deletions(-) diff --git a/src/components/tournament/dashboard/DashSide.tsx b/src/components/tournament/dashboard/DashSide.tsx index c20dd65..98ec270 100644 --- a/src/components/tournament/dashboard/DashSide.tsx +++ b/src/components/tournament/dashboard/DashSide.tsx @@ -4,7 +4,6 @@ import { LucideFileClock, LucideFileImage, LucideLayoutDashboard, - LucidePaintBucket, LucideScale, LucideScrollText, LucideSettings, @@ -114,17 +113,24 @@ const DashSide = ({ }, ], }, - { - catname: t("sidebar.shareable.catname"), - links: [ - { - name: t("sidebar.shareable.image_generation"), - href: `/t/${tournament_path}/image-generation`, - icon: LucidePaintBucket, - disabled: true, - }, - ], - }, + // i commented this out because if there's enough links on the sidebar that you + // have to scroll to see them all, it will then look visually broken when you + // scroll to the bottom. + // and commenting this out removes the need to scroll to see all the links, + // at least for my display and scaling (1080p), which silently hides this visual bug. + // should maybe get this fixed some day, though it's super low priority xd + // + // { + // catname: t("sidebar.shareable.catname"), + // links: [ + // { + // name: t("sidebar.shareable.image_generation"), + // href: `/t/${tournament_path}/image-generation`, + // icon: LucidePaintBucket, + // disabled: true, + // }, + // ], + // }, { catname: t("sidebar.organisational.catname"), links: [ diff --git a/src/components/tournament/ladder/LadderRoundRow.tsx b/src/components/tournament/ladder/LadderRoundRow.tsx index 3b1eca2..6bc9590 100644 --- a/src/components/tournament/ladder/LadderRoundRow.tsx +++ b/src/components/tournament/ladder/LadderRoundRow.tsx @@ -29,6 +29,30 @@ export function LadderRoundRow({ return debateMotion || t("unconfigured_debate"); }; + const getRoundLabel = (round: Round, phase: Phase, debates: Debate[]) => { + // finals: determine label based on number of debates + if (phase.is_finals) { + const matches = debates.length; + if (matches === 1) return t("finals.final"); + if (matches === 2) return t("finals.semi_final"); + if (matches === 4) return t("finals.quarter_final"); + if (matches > 0 && (matches & (matches - 1)) === 0) { + const n = matches * 2; + return t("finals.nth_final", { n }); + } + return round.name; + } + + // non-finals + const m = round.name.match(/^round_(\d+)$/i); + if (m) { + const roundNumber = Number(m[1]); + if (!Number.isNaN(roundNumber)) return t("round", { n: roundNumber }); + } + + return round.name; + }; + if (!phase) { return (

{`Cannot display round ${round.id}; cannot find matching phase`}

@@ -40,9 +64,20 @@ export function LadderRoundRow({ { + onOpenConfig(); + + setTimeout(() => { + document + .querySelector("[data-round-config-popup]") + ?.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + }, 50); + }} > - {round.name} + {getRoundLabel(round, phase, debates)}
{debates.map((debate) => ( diff --git a/src/components/tournament/ladder/LadderView.tsx b/src/components/tournament/ladder/LadderView.tsx index c3a9f82..6958158 100644 --- a/src/components/tournament/ladder/LadderView.tsx +++ b/src/components/tournament/ladder/LadderView.tsx @@ -87,6 +87,10 @@ export function LadderView({ onClose={() => setIsRoundConfigOpen(false)} tournamentId={tournamentId} round={configuredRound} + phase={phases?.find((p) => p.id == configuredRound.phase_id)} + debates={ + debates?.filter((d) => d.round_id == configuredRound.id) || [] + } motion={motions?.find( (motion) => configuredRound.motion_id == motion.id, )} diff --git a/src/components/tournament/ladder/RoundConfig.tsx b/src/components/tournament/ladder/RoundConfig.tsx index 80f42da..6f00110 100644 --- a/src/components/tournament/ladder/RoundConfig.tsx +++ b/src/components/tournament/ladder/RoundConfig.tsx @@ -6,6 +6,8 @@ import { LucideX } from "lucide-react"; import { ReusableButton } from "../../ui/ReusableButton"; import { InputBlock } from "../../ui/InputBlock"; import { Round } from "@/types/Round"; +import { Phase } from "@/types/Phase"; +import { Debate } from "@/types/Debate"; import { Motion } from "@/types/Motion"; import { createMotion, setRoundMotion } from "@/lib/utils"; @@ -13,6 +15,8 @@ type RoundConfigProps = { motion?: Motion; round: Round; tournamentId: string; + phase?: Phase; + debates?: Debate[]; onApplyAction: () => void; onClose: () => void; }; @@ -21,10 +25,13 @@ export function RoundConfig({ motion, tournamentId, round, + phase, + debates = [], onApplyAction: onApply, onClose, }: RoundConfigProps) { const t = useTranslations("round_config"); + const tLadder = useTranslations("ladder"); const [motionText, setMotionText] = useState(motion?.motion || ""); const [infoslide, setInfoslide] = useState(motion?.adinfo || ""); const [isApplying, setIsApplying] = useState(false); @@ -32,7 +39,31 @@ export function RoundConfig({ message: "", error: false, }); - const name = round.name; + const getRoundLabel = (round: Round) => { + // finals naming when phase is finals + if (phase?.is_finals) { + const matches = (debates || []).length; + if (matches === 1) return tLadder("finals.final"); + if (matches === 2) return tLadder("finals.semi_final"); + if (matches === 4) return tLadder("finals.quarter_final"); + if (matches > 0 && (matches & (matches - 1)) === 0) { + const n = matches * 2; + return tLadder("finals.nth_final", { n }); + } + return round.name; + } + + const m = round.name.match(/^round_(\d+)$/i); + if (m) { + const roundNumber = Number(m[1]); + if (!Number.isNaN(roundNumber)) + return tLadder("round", { n: roundNumber }); + } + + return round.name; + }; + + const name = getRoundLabel(round); const isApplyDisabled = motionText.trim().length === 0 || isApplying; @@ -71,45 +102,47 @@ export function RoundConfig({ }; return ( -
- - -
- {t("title", { name })} +
+
+ + +
+ {t("title", { name })} +
+ + + + + +

+ {resultMessage.message || " "} +

+ +
- - - - - -

- {resultMessage.message || " "} -

- -
); } diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index ca00678..bf88162 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -112,12 +112,20 @@ "advancing_teams_hint": "Defines how many teams advance from the group phase to the finals phase. Must be a power of 2.", "hint_title": "Tournament Ladder", "hint_content": "Each box represents a debate. You can click on a specific box to see the debate details. The ladder shows the structure and flow of the tournament, including the rounds and how debates are organised." + , + "round": "Round {n}", + "finals": { + "final": "Finale", + "semi_final": "Semi-finals", + "quarter_final": "Quarter-finals", + "nth_final": "1/{n}-finals" + } }, "debates": { "title": "Debates" }, "round_config": { - "title": "Round {name} configuration", + "title": "{name} configuration", "motion_title": "Motion", "motion_description": "What should this round's topic be?", "motion_placeholder": "(Required)", diff --git a/test/e2e/e2eUtils.ts b/test/e2e/e2eUtils.ts index 4c9ed14..01e2e27 100644 --- a/test/e2e/e2eUtils.ts +++ b/test/e2e/e2eUtils.ts @@ -332,7 +332,7 @@ export async function planTournament({ totalTeams: number; advancingTeams: number; }) { - await page.getByRole("link", { name: "Tournament Ladder" }).click(); + await page.getByRole("link", { name: "Tournament Ladder" }).first().click(); await page.waitForURL(/ladder/); await page .getByRole("spinbutton", { name: "Group phase rounds" }) diff --git a/test/e2e/tournament-ladder-rendering.test.ts b/test/e2e/tournament-ladder-rendering.test.ts index 732c6db..9914cac 100644 --- a/test/e2e/tournament-ladder-rendering.test.ts +++ b/test/e2e/tournament-ladder-rendering.test.ts @@ -24,8 +24,8 @@ testInTournamentAsAdmin( page.getByRole("heading", { name: "Tournament Ladder" }), ).toBeVisible(); - const roundLabel = page.getByText(/^round.?\d/i); - expect(await roundLabel.count()).toBe(7); + const groupRoundLabel = page.getByText(/^Round \d+$/i); + expect(await groupRoundLabel.count()).toBe(3); }, ); From fec0fa4c9a1c19ea0e63ca601201f29a6dc8a052 Mon Sep 17 00:00:00 2001 From: Akirus12 Date: Fri, 12 Jun 2026 19:19:30 +0200 Subject: [PATCH 3/5] PART 2: added round string parsing to not have ugly round_1 anymore, and also implemented this change to the popup as well. i also made the screen scroll up to the pop up when you click the button. and small test changes to work with the new parsed strings. --- package-lock.json | 2 +- package.json | 2 +- .../tournament/dashboard/DashSide.tsx | 30 +++-- .../tournament/ladder/LadderRoundRow.tsx | 39 +++++- .../tournament/ladder/LadderView.tsx | 4 + .../tournament/ladder/RoundConfig.tsx | 111 ++++++++++++------ src/i18n/translations/en.json | 11 +- test/e2e/e2eUtils.ts | 2 +- test/e2e/tournament-ladder-rendering.test.ts | 4 +- 9 files changed, 145 insertions(+), 60 deletions(-) diff --git a/package-lock.json b/package-lock.json index 464f106..3ed85e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "granda-front", - "version": "0.3.14", + "version": "0.3.15", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index d53bb34..f69d1f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "granda-front", - "version": "0.3.14", + "version": "0.3.15", "private": true, "scripts": { "dev": "next dev --turbopack", diff --git a/src/components/tournament/dashboard/DashSide.tsx b/src/components/tournament/dashboard/DashSide.tsx index c20dd65..98ec270 100644 --- a/src/components/tournament/dashboard/DashSide.tsx +++ b/src/components/tournament/dashboard/DashSide.tsx @@ -4,7 +4,6 @@ import { LucideFileClock, LucideFileImage, LucideLayoutDashboard, - LucidePaintBucket, LucideScale, LucideScrollText, LucideSettings, @@ -114,17 +113,24 @@ const DashSide = ({ }, ], }, - { - catname: t("sidebar.shareable.catname"), - links: [ - { - name: t("sidebar.shareable.image_generation"), - href: `/t/${tournament_path}/image-generation`, - icon: LucidePaintBucket, - disabled: true, - }, - ], - }, + // i commented this out because if there's enough links on the sidebar that you + // have to scroll to see them all, it will then look visually broken when you + // scroll to the bottom. + // and commenting this out removes the need to scroll to see all the links, + // at least for my display and scaling (1080p), which silently hides this visual bug. + // should maybe get this fixed some day, though it's super low priority xd + // + // { + // catname: t("sidebar.shareable.catname"), + // links: [ + // { + // name: t("sidebar.shareable.image_generation"), + // href: `/t/${tournament_path}/image-generation`, + // icon: LucidePaintBucket, + // disabled: true, + // }, + // ], + // }, { catname: t("sidebar.organisational.catname"), links: [ diff --git a/src/components/tournament/ladder/LadderRoundRow.tsx b/src/components/tournament/ladder/LadderRoundRow.tsx index 3b1eca2..6bc9590 100644 --- a/src/components/tournament/ladder/LadderRoundRow.tsx +++ b/src/components/tournament/ladder/LadderRoundRow.tsx @@ -29,6 +29,30 @@ export function LadderRoundRow({ return debateMotion || t("unconfigured_debate"); }; + const getRoundLabel = (round: Round, phase: Phase, debates: Debate[]) => { + // finals: determine label based on number of debates + if (phase.is_finals) { + const matches = debates.length; + if (matches === 1) return t("finals.final"); + if (matches === 2) return t("finals.semi_final"); + if (matches === 4) return t("finals.quarter_final"); + if (matches > 0 && (matches & (matches - 1)) === 0) { + const n = matches * 2; + return t("finals.nth_final", { n }); + } + return round.name; + } + + // non-finals + const m = round.name.match(/^round_(\d+)$/i); + if (m) { + const roundNumber = Number(m[1]); + if (!Number.isNaN(roundNumber)) return t("round", { n: roundNumber }); + } + + return round.name; + }; + if (!phase) { return (

{`Cannot display round ${round.id}; cannot find matching phase`}

@@ -40,9 +64,20 @@ export function LadderRoundRow({ { + onOpenConfig(); + + setTimeout(() => { + document + .querySelector("[data-round-config-popup]") + ?.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + }, 50); + }} > - {round.name} + {getRoundLabel(round, phase, debates)}
{debates.map((debate) => ( diff --git a/src/components/tournament/ladder/LadderView.tsx b/src/components/tournament/ladder/LadderView.tsx index c3a9f82..6958158 100644 --- a/src/components/tournament/ladder/LadderView.tsx +++ b/src/components/tournament/ladder/LadderView.tsx @@ -87,6 +87,10 @@ export function LadderView({ onClose={() => setIsRoundConfigOpen(false)} tournamentId={tournamentId} round={configuredRound} + phase={phases?.find((p) => p.id == configuredRound.phase_id)} + debates={ + debates?.filter((d) => d.round_id == configuredRound.id) || [] + } motion={motions?.find( (motion) => configuredRound.motion_id == motion.id, )} diff --git a/src/components/tournament/ladder/RoundConfig.tsx b/src/components/tournament/ladder/RoundConfig.tsx index 80f42da..6f00110 100644 --- a/src/components/tournament/ladder/RoundConfig.tsx +++ b/src/components/tournament/ladder/RoundConfig.tsx @@ -6,6 +6,8 @@ import { LucideX } from "lucide-react"; import { ReusableButton } from "../../ui/ReusableButton"; import { InputBlock } from "../../ui/InputBlock"; import { Round } from "@/types/Round"; +import { Phase } from "@/types/Phase"; +import { Debate } from "@/types/Debate"; import { Motion } from "@/types/Motion"; import { createMotion, setRoundMotion } from "@/lib/utils"; @@ -13,6 +15,8 @@ type RoundConfigProps = { motion?: Motion; round: Round; tournamentId: string; + phase?: Phase; + debates?: Debate[]; onApplyAction: () => void; onClose: () => void; }; @@ -21,10 +25,13 @@ export function RoundConfig({ motion, tournamentId, round, + phase, + debates = [], onApplyAction: onApply, onClose, }: RoundConfigProps) { const t = useTranslations("round_config"); + const tLadder = useTranslations("ladder"); const [motionText, setMotionText] = useState(motion?.motion || ""); const [infoslide, setInfoslide] = useState(motion?.adinfo || ""); const [isApplying, setIsApplying] = useState(false); @@ -32,7 +39,31 @@ export function RoundConfig({ message: "", error: false, }); - const name = round.name; + const getRoundLabel = (round: Round) => { + // finals naming when phase is finals + if (phase?.is_finals) { + const matches = (debates || []).length; + if (matches === 1) return tLadder("finals.final"); + if (matches === 2) return tLadder("finals.semi_final"); + if (matches === 4) return tLadder("finals.quarter_final"); + if (matches > 0 && (matches & (matches - 1)) === 0) { + const n = matches * 2; + return tLadder("finals.nth_final", { n }); + } + return round.name; + } + + const m = round.name.match(/^round_(\d+)$/i); + if (m) { + const roundNumber = Number(m[1]); + if (!Number.isNaN(roundNumber)) + return tLadder("round", { n: roundNumber }); + } + + return round.name; + }; + + const name = getRoundLabel(round); const isApplyDisabled = motionText.trim().length === 0 || isApplying; @@ -71,45 +102,47 @@ export function RoundConfig({ }; return ( -
- - -
- {t("title", { name })} +
+
+ + +
+ {t("title", { name })} +
+ + + + + +

+ {resultMessage.message || " "} +

+ +
- - - - - -

- {resultMessage.message || " "} -

- -
); } diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index ca00678..74a1dbe 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -111,13 +111,20 @@ "total_teams_hint": "The total number of teams taking part in the tournament.", "advancing_teams_hint": "Defines how many teams advance from the group phase to the finals phase. Must be a power of 2.", "hint_title": "Tournament Ladder", - "hint_content": "Each box represents a debate. You can click on a specific box to see the debate details. The ladder shows the structure and flow of the tournament, including the rounds and how debates are organised." + "hint_content": "Each box represents a debate. You can click on a specific box to see the debate details. The ladder shows the structure and flow of the tournament, including the rounds and how debates are organised.", + "round": "Round {n}", + "finals": { + "final": "Finale", + "semi_final": "Semi-finals", + "quarter_final": "Quarter-finals", + "nth_final": "1/{n}-finals" + } }, "debates": { "title": "Debates" }, "round_config": { - "title": "Round {name} configuration", + "title": "{name} configuration", "motion_title": "Motion", "motion_description": "What should this round's topic be?", "motion_placeholder": "(Required)", diff --git a/test/e2e/e2eUtils.ts b/test/e2e/e2eUtils.ts index 4c9ed14..01e2e27 100644 --- a/test/e2e/e2eUtils.ts +++ b/test/e2e/e2eUtils.ts @@ -332,7 +332,7 @@ export async function planTournament({ totalTeams: number; advancingTeams: number; }) { - await page.getByRole("link", { name: "Tournament Ladder" }).click(); + await page.getByRole("link", { name: "Tournament Ladder" }).first().click(); await page.waitForURL(/ladder/); await page .getByRole("spinbutton", { name: "Group phase rounds" }) diff --git a/test/e2e/tournament-ladder-rendering.test.ts b/test/e2e/tournament-ladder-rendering.test.ts index 732c6db..9914cac 100644 --- a/test/e2e/tournament-ladder-rendering.test.ts +++ b/test/e2e/tournament-ladder-rendering.test.ts @@ -24,8 +24,8 @@ testInTournamentAsAdmin( page.getByRole("heading", { name: "Tournament Ladder" }), ).toBeVisible(); - const roundLabel = page.getByText(/^round.?\d/i); - expect(await roundLabel.count()).toBe(7); + const groupRoundLabel = page.getByText(/^Round \d+$/i); + expect(await groupRoundLabel.count()).toBe(3); }, ); From 49209b3db4d8de4ff37812c33aa6161a182f9505 Mon Sep 17 00:00:00 2001 From: Akirus12 Date: Sun, 14 Jun 2026 12:04:56 +0200 Subject: [PATCH 4/5] implemented requested fixes --- package-lock.json | 4 +- package.json | 2 +- .../tournament/dashboard/DashSide.tsx | 18 --------- .../tournament/ladder/LadderRoundRow.tsx | 27 +------------ .../tournament/ladder/RoundConfig.tsx | 28 ++----------- src/i18n/translations/en.json | 1 - src/lib/utils.ts | 40 +++++++++++++++++++ test/e2e/tournament-ladder-rendering.test.ts | 14 +++---- 8 files changed, 55 insertions(+), 79 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3944405..eafcafb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "granda-front", - "version": "0.3.16", + "version": "0.3.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "granda-front", - "version": "0.3.16", + "version": "0.3.19", "dependencies": { "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-select": "^2.2.6", diff --git a/package.json b/package.json index 0fdcebf..35c8da5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "granda-front", - "version": "0.3.16", + "version": "0.3.19", "private": true, "scripts": { "dev": "next dev --turbopack", diff --git a/src/components/tournament/dashboard/DashSide.tsx b/src/components/tournament/dashboard/DashSide.tsx index 98ec270..2b67c0e 100644 --- a/src/components/tournament/dashboard/DashSide.tsx +++ b/src/components/tournament/dashboard/DashSide.tsx @@ -113,24 +113,6 @@ const DashSide = ({ }, ], }, - // i commented this out because if there's enough links on the sidebar that you - // have to scroll to see them all, it will then look visually broken when you - // scroll to the bottom. - // and commenting this out removes the need to scroll to see all the links, - // at least for my display and scaling (1080p), which silently hides this visual bug. - // should maybe get this fixed some day, though it's super low priority xd - // - // { - // catname: t("sidebar.shareable.catname"), - // links: [ - // { - // name: t("sidebar.shareable.image_generation"), - // href: `/t/${tournament_path}/image-generation`, - // icon: LucidePaintBucket, - // disabled: true, - // }, - // ], - // }, { catname: t("sidebar.organisational.catname"), links: [ diff --git a/src/components/tournament/ladder/LadderRoundRow.tsx b/src/components/tournament/ladder/LadderRoundRow.tsx index 6bc9590..7eb6f19 100644 --- a/src/components/tournament/ladder/LadderRoundRow.tsx +++ b/src/components/tournament/ladder/LadderRoundRow.tsx @@ -5,6 +5,7 @@ import { Phase } from "@/types/Phase"; import { Motion } from "@/types/Motion"; import { useTranslations } from "next-intl"; import { GenericButton } from "@/components/ui/GenericButton"; +import { getRoundLabel } from "@/lib/utils"; export function LadderRoundRow({ onOpenConfig, @@ -29,30 +30,6 @@ export function LadderRoundRow({ return debateMotion || t("unconfigured_debate"); }; - const getRoundLabel = (round: Round, phase: Phase, debates: Debate[]) => { - // finals: determine label based on number of debates - if (phase.is_finals) { - const matches = debates.length; - if (matches === 1) return t("finals.final"); - if (matches === 2) return t("finals.semi_final"); - if (matches === 4) return t("finals.quarter_final"); - if (matches > 0 && (matches & (matches - 1)) === 0) { - const n = matches * 2; - return t("finals.nth_final", { n }); - } - return round.name; - } - - // non-finals - const m = round.name.match(/^round_(\d+)$/i); - if (m) { - const roundNumber = Number(m[1]); - if (!Number.isNaN(roundNumber)) return t("round", { n: roundNumber }); - } - - return round.name; - }; - if (!phase) { return (

{`Cannot display round ${round.id}; cannot find matching phase`}

@@ -77,7 +54,7 @@ export function LadderRoundRow({ }, 50); }} > - {getRoundLabel(round, phase, debates)} + {getRoundLabel(round, phase, debates, t)}
{debates.map((debate) => ( diff --git a/src/components/tournament/ladder/RoundConfig.tsx b/src/components/tournament/ladder/RoundConfig.tsx index 6f00110..2583bbd 100644 --- a/src/components/tournament/ladder/RoundConfig.tsx +++ b/src/components/tournament/ladder/RoundConfig.tsx @@ -10,12 +10,13 @@ import { Phase } from "@/types/Phase"; import { Debate } from "@/types/Debate"; import { Motion } from "@/types/Motion"; import { createMotion, setRoundMotion } from "@/lib/utils"; +import { getRoundLabel } from "@/lib/utils"; type RoundConfigProps = { motion?: Motion; round: Round; tournamentId: string; - phase?: Phase; + phase: Phase; debates?: Debate[]; onApplyAction: () => void; onClose: () => void; @@ -39,31 +40,8 @@ export function RoundConfig({ message: "", error: false, }); - const getRoundLabel = (round: Round) => { - // finals naming when phase is finals - if (phase?.is_finals) { - const matches = (debates || []).length; - if (matches === 1) return tLadder("finals.final"); - if (matches === 2) return tLadder("finals.semi_final"); - if (matches === 4) return tLadder("finals.quarter_final"); - if (matches > 0 && (matches & (matches - 1)) === 0) { - const n = matches * 2; - return tLadder("finals.nth_final", { n }); - } - return round.name; - } - - const m = round.name.match(/^round_(\d+)$/i); - if (m) { - const roundNumber = Number(m[1]); - if (!Number.isNaN(roundNumber)) - return tLadder("round", { n: roundNumber }); - } - - return round.name; - }; - const name = getRoundLabel(round); + const name = getRoundLabel(round, phase, debates, tLadder); const isApplyDisabled = motionText.trim().length === 0 || isApplying; diff --git a/src/i18n/translations/en.json b/src/i18n/translations/en.json index 1513094..74a1dbe 100644 --- a/src/i18n/translations/en.json +++ b/src/i18n/translations/en.json @@ -124,7 +124,6 @@ "title": "Debates" }, "round_config": { - "title": "{name} configuration", "title": "{name} configuration", "motion_title": "Motion", "motion_description": "What should this round's topic be?", diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 6d5f7d9..1387eb4 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -2,6 +2,8 @@ import { clsx, type ClassValue } from "clsx"; import { twMerge } from "tailwind-merge"; import { Motion } from "@/types/Motion"; import { Round } from "@/types/Round"; +import { Debate } from "@/types/Debate"; +import { Phase } from "@/types/Phase"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -77,3 +79,41 @@ export async function setRoundMotion( return response.json(); } + +type Translator = ( + key: string, + values?: Record, +) => string; + +export function getRoundLabel( + round: Round, + phase: Phase, + debates: Debate[], + t: Translator, +): string { + if (phase.is_finals) { + const matches = debates.length; + + if (matches === 1) return t("finals.final"); + if (matches === 2) return t("finals.semi_final"); + if (matches === 4) return t("finals.quarter_final"); + + if (matches > 0 && (matches & (matches - 1)) === 0) { + return t("finals.nth_final", { n: matches * 2 }); + } + + return round.name; + } + + const matchRoundName = round.name.match(/^round_(\d+)$/i); + + if (matchRoundName) { + const roundNumber = Number(matchRoundName[1]); + + if (!Number.isNaN(roundNumber)) { + return t("round", { n: roundNumber }); + } + } + + return round.name; +} diff --git a/test/e2e/tournament-ladder-rendering.test.ts b/test/e2e/tournament-ladder-rendering.test.ts index 9914cac..d1bda92 100644 --- a/test/e2e/tournament-ladder-rendering.test.ts +++ b/test/e2e/tournament-ladder-rendering.test.ts @@ -107,8 +107,8 @@ testInTournamentAsAdmin( page.getByRole("heading", { name: "Tournament Ladder" }), ).toBeVisible(); - const configButton = page.getByText("round_1").first(); - const configHeading = page.getByText("Round round_1 configuration"); + const configButton = page.getByText("Round 1"); + const configHeading = page.getByText("Round 1 configuration"); await configButton.click(); await expect(configHeading).toBeVisible(); @@ -162,7 +162,7 @@ testInTournamentAsAdmin( "tournament planning form should contain informative placeholders", async ({ page }) => { // GIVEN - await page.getByRole("link", { name: "Tournament Ladder" }).click(); + await page.getByRole("link", { name: "Tournament Ladder" }).first().click(); await page.waitForURL(/ladder/); // WHEN @@ -179,7 +179,7 @@ testInTournamentAsAdmin( "tournament planning form should contain clickable hint icons", async ({ page }) => { // GIVEN - await page.getByRole("link", { name: "Tournament Ladder" }).click(); + await page.getByRole("link", { name: "Tournament Ladder" }).first().click(); await page.waitForURL(/ladder/); // WHEN @@ -220,7 +220,7 @@ testInTournamentAsAdmin( "advancing teams input should increment by 2", async ({ page }) => { // GIVEN - await page.getByRole("link", { name: "Tournament Ladder" }).click(); + await page.getByRole("link", { name: "Tournament Ladder" }).first().click(); await page.waitForURL(/ladder/); const advancingTeamsInput = page.getByPlaceholder("2, 4, 8, 16…"); @@ -250,8 +250,8 @@ testInTournamentAsAdmin( page.getByRole("heading", { name: "Tournament Ladder" }), ).toBeVisible(); - const configButton = page.getByText("round_1").first(); - const configHeading = page.getByText("Round round_1 configuration"); + const configButton = page.getByText("Round 1"); + const configHeading = page.getByText("Round 1 configuration"); await configButton.click(); await expect(configHeading).toBeVisible(); From d12870beed64181d9ebde1b4df2ad59f45c3ea6b Mon Sep 17 00:00:00 2001 From: Akirus12 Date: Sun, 14 Jun 2026 13:07:07 +0200 Subject: [PATCH 5/5] fixed wording on some tests that didn't pass --- test/e2e/settings-shortcut.test.ts | 4 +++- test/e2e/sidebarHighlight.test.ts | 6 +++--- test/e2e/tournament-ladder-rendering.test.ts | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/test/e2e/settings-shortcut.test.ts b/test/e2e/settings-shortcut.test.ts index df765bf..fa80120 100644 --- a/test/e2e/settings-shortcut.test.ts +++ b/test/e2e/settings-shortcut.test.ts @@ -5,7 +5,9 @@ testInTournamentAsAdmin( "settings shortcut redirects to settings page and is removed from account dropdown", async ({ page }) => { // GIVEN - await expect(page.getByRole("heading", { name: "Overview" })).toBeVisible(); + await expect( + page.getByRole("heading", { name: "Welcome to" }), + ).toBeVisible(); const settingsShortcut = page.getByRole("button", { name: "Settings" }); diff --git a/test/e2e/sidebarHighlight.test.ts b/test/e2e/sidebarHighlight.test.ts index c51fe17..6668de5 100644 --- a/test/e2e/sidebarHighlight.test.ts +++ b/test/e2e/sidebarHighlight.test.ts @@ -6,9 +6,9 @@ testInTournamentAsAdmin( async ({ page }) => { // GIVEN const overviewSidebarItem = page.getByRole("link", { name: "Overview" }); - const tournamentSidebarItem = page.getByRole("link", { - name: "Tournament Ladder", - }); + const tournamentSidebarItem = page + .getByRole("link", { name: "Tournament Ladder" }) + .first(); const debatesSidebarItem = page.getByRole("link", { name: "Debates" }); await expect(overviewSidebarItem).toHaveClass(/border-stone-600/); diff --git a/test/e2e/tournament-ladder-rendering.test.ts b/test/e2e/tournament-ladder-rendering.test.ts index 3a2d07e..0a18e1f 100644 --- a/test/e2e/tournament-ladder-rendering.test.ts +++ b/test/e2e/tournament-ladder-rendering.test.ts @@ -60,7 +60,7 @@ testInTournamentAsAdmin( page.getByText("You can click on a specific box"), ).toBeVisible(); await expect( - page.getByText("The ladder visualizes the flow of the tournament"), + page.getByText("The ladder shows the structure"), ).toBeVisible(); // WHEN