From 4eb3eff81d129672d7d8b86d3fc76ac2cebda1cd Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Wed, 15 Apr 2026 10:55:57 +0200 Subject: [PATCH 1/4] Add pointer --- next-frontend/src/components/competitions/TabMenu.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/next-frontend/src/components/competitions/TabMenu.tsx b/next-frontend/src/components/competitions/TabMenu.tsx index 1ec0b414f6..5fa0ea4eac 100644 --- a/next-frontend/src/components/competitions/TabMenu.tsx +++ b/next-frontend/src/components/competitions/TabMenu.tsx @@ -185,6 +185,7 @@ function CollapsibleTabGroup({ py="2" borderRadius="md" _hover={{ bg: "bg.subtle" }} + cursor="pointer" > {t(i18nKey)} From 780630356a6d154b338a3058d37857a9132bf1c5 Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Wed, 15 Apr 2026 10:56:25 +0200 Subject: [PATCH 2/4] add Done badge when round is done --- next-frontend/src/lib/wca/competitions/tabs.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/next-frontend/src/lib/wca/competitions/tabs.ts b/next-frontend/src/lib/wca/competitions/tabs.ts index e1532b9898..3fe771a2ae 100644 --- a/next-frontend/src/lib/wca/competitions/tabs.ts +++ b/next-frontend/src/lib/wca/competitions/tabs.ts @@ -127,10 +127,15 @@ export const duringCompetitionTabs = ( rounds.length, Boolean(round.cutoff), ); + + const roundDone = + round.state === "locked" || + (round.state === "open" && + round.competitors_live_results_entered === round.total_competitors); return { i18nKey: `rounds.${roundTypeId}.name`, menuKey: round.id, - badge: round.state === "locked" ? "Done" : "live", + badge: roundDone ? "Done" : "live", disabled: round.state === "pending" || round.state === "ready", href: route({ pathname: "/competitions/[competitionId]/live/rounds/[roundId]", From 790dfcda66622952464342f583f4f6b15eaa972a Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Wed, 15 Apr 2026 11:55:35 +0200 Subject: [PATCH 3/4] bring back custom tabs --- .../src/components/competitions/LiveMenu.tsx | 2 +- .../src/components/competitions/TabMenu.tsx | 81 +++++++++++++++---- 2 files changed, 65 insertions(+), 18 deletions(-) diff --git a/next-frontend/src/components/competitions/LiveMenu.tsx b/next-frontend/src/components/competitions/LiveMenu.tsx index 12f1474582..52b933afc9 100644 --- a/next-frontend/src/components/competitions/LiveMenu.tsx +++ b/next-frontend/src/components/competitions/LiveMenu.tsx @@ -23,7 +23,7 @@ export default async function LiveMenu({ const tabs = duringCompetitionTabs(competitionInfo, data.rounds); return ( - + {children} ); diff --git a/next-frontend/src/components/competitions/TabMenu.tsx b/next-frontend/src/components/competitions/TabMenu.tsx index 5fa0ea4eac..438322c95d 100644 --- a/next-frontend/src/components/competitions/TabMenu.tsx +++ b/next-frontend/src/components/competitions/TabMenu.tsx @@ -8,6 +8,7 @@ import { Collapsible, Drawer, IconButton, + Separator, Spacer, Tabs, Text, @@ -24,15 +25,18 @@ import { useState } from "react"; import { TFunction } from "i18next"; import { LuAlignJustify } from "react-icons/lu"; import { iconMap } from "@/components/icons/iconMap"; +import { route } from "nextjs-routes"; export default function TabMenu({ competitionInfo, children, tabs, + isLiveMenu = false, }: { children: React.ReactNode; competitionInfo: components["schemas"]["CompetitionInfo"]; tabs: CompetitionNavTab[]; + isLiveMenu?: boolean; }) { const [openGroup, setOpenGroup] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); @@ -56,7 +60,7 @@ export default function TabMenu({ setOpenGroup((prev) => (prev === tab.menuKey ? null : tab.menuKey)) } + isLiveMenu={isLiveMenu} + competitionInfo={competitionInfo} /> @@ -117,6 +123,7 @@ export default function TabMenu({ prev === tab.menuKey ? null : tab.menuKey, ) } + competitionInfo={competitionInfo} /> @@ -136,28 +143,68 @@ function TabList({ t, onToggle, openGroup, + isLiveMenu, + competitionInfo, }: { tabs: CompetitionNavTab[]; t: TFunction; openGroup: string | null; onToggle: (tab: CompetitionNavTab) => void; + isLiveMenu?: boolean; + competitionInfo: components["schemas"]["CompetitionInfo"]; }) { - return tabs.map((tab) => - "href" in tab ? ( - - - {t(tab.i18nKey)} - - - ) : ( - onToggle(tab)} - /> - ), + return ( + <> + {tabs.map((tab) => + "href" in tab ? ( + + + {t(tab.i18nKey)} + + + ) : ( + onToggle(tab)} + /> + ), + )} + {!isLiveMenu && ( + <> + + {competitionInfo.tab_names.map((tabName) => ( + + + + {tabName} + + + + ))} + + )} + ); } From 314cf88a8a7927c9022ce18eb8b1755369926e86 Mon Sep 17 00:00:00 2001 From: FinnIckler Date: Wed, 15 Apr 2026 12:02:40 +0200 Subject: [PATCH 4/4] have submenu open after refresh --- .../src/components/competitions/TabMenu.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/next-frontend/src/components/competitions/TabMenu.tsx b/next-frontend/src/components/competitions/TabMenu.tsx index 438322c95d..006cd97f3d 100644 --- a/next-frontend/src/components/competitions/TabMenu.tsx +++ b/next-frontend/src/components/competitions/TabMenu.tsx @@ -26,6 +26,16 @@ import { TFunction } from "i18next"; import { LuAlignJustify } from "react-icons/lu"; import { iconMap } from "@/components/icons/iconMap"; import { route } from "nextjs-routes"; +import { parseActivityCode } from "@/lib/wca/wcif/rounds"; + +function parseActivityCodeOrNull(path: string) { + try { + const { eventId } = parseActivityCode(path); + return eventId; + } catch { + return null; + } +} export default function TabMenu({ competitionInfo, @@ -38,15 +48,17 @@ export default function TabMenu({ tabs: CompetitionNavTab[]; isLiveMenu?: boolean; }) { - const [openGroup, setOpenGroup] = useState(null); - const [drawerOpen, setDrawerOpen] = useState(false); - const pathName = usePathname(); const { t } = useT(); const path = _.last(pathName.split("/")); const currentPath = path === competitionInfo.id ? "general" : path; + const eventId = parseActivityCodeOrNull(currentPath!); + + const [openGroup, setOpenGroup] = useState(eventId); + const [drawerOpen, setDrawerOpen] = useState(false); + return (