Conversation
|
No actionable comments were generated in the recent review. 🎉 📝 WalkthroughWalkthroughAdds a tabbed Tournament Tree view (Graph + Ranking), a BracketRankingTable with Swiss-style ranking and clickable match-history chips, BracketTabs UI, a useTabParam hook for URL tab state, MatchHistoryBadges, small GraphView/MatchNode UI tweaks, and trims docs lint/test guidance. Changes
Sequence DiagramsequenceDiagram
actor User
participant BracketTabs
participant GraphView
participant BracketRankingTable
participant Router as NextRouter
User->>BracketTabs: Load bracket page (teams, matches, teamCount)
Note over BracketTabs: Reads tab param from URL (useTabParam)
alt tab = graph
BracketTabs->>GraphView: Render matches & teamCount
GraphView-->>BracketTabs: Display graph (nodes with hideScore)
else tab = ranking
BracketTabs->>BracketRankingTable: Render teams & matches
Note over BracketRankingTable: Compute Swiss ranks and histories
BracketRankingTable-->>BracketTabs: Display ranking table
end
User->>BracketTabs: Click tab
BracketTabs->>Router: Update URL query param (tab=...) without scroll
User->>BracketRankingTable: Click team row
BracketRankingTable->>Router: Navigate to /events/{eventId}/teams/{teamId}
User->>BracketRankingTable: Click history chip
BracketRankingTable->>Router: Navigate to /events/{eventId}/match/{matchId}
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ation when not all the matches are revealed
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/app/events/[id]/bracket/page.tsx (1)
11-14:⚠️ Potential issue | 🟡 MinorMetadata title doesn't match the page heading.
The
metadata.titleis"Tournament Bracket"(Line 12) but the<h1>reads"Tournament Tree"(Line 40). These should be consistent for SEO and user experience.Proposed fix
export const metadata = { - title: "Tournament Bracket", + title: "Tournament Tree", description: "View the tournament bracket and match results.", };Also applies to: 39-41
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/events/`[id]/bracket/page.tsx around lines 11 - 14, The page has inconsistent titles: the exported const metadata.title is "Tournament Bracket" while the rendered <h1> displays "Tournament Tree"; update one of them so they match (either change metadata.title to "Tournament Tree" or change the <h1> text to "Tournament Bracket")—edit the export const metadata object and/or the <h1> element in the page component to use the same string so SEO and headings are consistent.
🧹 Nitpick comments (3)
frontend/app/events/[id]/bracket/BracketRankingTable.tsx (2)
39-78:getTeamStatsis re-created and re-executed on every render.This function is declared inline and called per-team inside a
.map()(Line 94). Each call filters and sortsrevealedMatches. For typical tournament sizes this is fine, but consider memoizing withuseMemoif performance becomes a concern with larger datasets.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/events/`[id]/bracket/BracketRankingTable.tsx around lines 39 - 78, getTeamStats is being re-created and re-run for every render and for each team, causing repeated filters/sorts over revealedMatches; move the heavy work out of the inline function by memoizing computed stats per team (e.g., useMemo or precompute a Map keyed by teamId) so you compute teamMatches, highestRound, actualRank, and history once per revealedMatches/maxRound change, then have getTeamStats (or the map lookup) return the precomputed entry when iterating teams (referencing getTeamStats, revealedMatches, maxRound and the .map usage that calls it).
180-208: Significant duplication withRankingTable.tsxmatch history badges.The history badge rendering (styling, click handler, result-to-color mapping) is nearly identical between this component and
RankingTable.tsx(lines 97–114). Consider extracting a sharedMatchHistoryBadgescomponent to reduce duplication across both ranking tables.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/events/`[id]/bracket/BracketRankingTable.tsx around lines 180 - 208, The match-history badge rendering in BracketRankingTable.tsx duplicates logic found in RankingTable.tsx; extract a shared functional component (e.g., MatchHistoryBadges) that accepts props { history: Match[], eventId: string, router } (or a click callback) and encapsulates the mapping of match.result to className, the onClick router.push(`/events/${eventId}/match/${match.id}`) behavior, keying, and the "No matches" fallback; replace the inline JSX in both BracketRankingTable and RankingTable with this new component to remove duplication while preserving styling, result-to-color mapping, and stopPropagation behavior.frontend/app/events/[id]/bracket/BracketTabs.tsx (1)
26-36: Tab state logic is duplicated withGroupPhaseTabs.tsx.The URL-param-based tab switching logic (lines 26–36) is identical to
GroupPhaseTabs.tsx(lines 26–36). If more tab views are introduced, consider extracting a shareduseTabParamhook. Not urgent for two instances.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/events/`[id]/bracket/BracketTabs.tsx around lines 26 - 36, The tab state and URL-param logic used by currentTab and onTabChange (which use useRouter, usePathname, useSearchParams) are duplicated in GroupPhaseTabs.tsx; extract this into a shared hook (e.g., useTabParam) that encapsulates reading the "tab" param and updating it via router.push, then replace the inline logic in BracketTabs.tsx and GroupPhaseTabs.tsx with calls to useTabParam to return the currentTab value and an onTabChange handler; ensure the hook accepts an optional defaultTab and maintains the same behavior (preserve existing pathname?scroll:false) so behavior is identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/app/events/`[id]/bracket/BracketRankingTable.tsx:
- Around line 64-70: The history array maps teamMatches to objects using m.id,
but Match.id is optional so this can produce undefined IDs used later by the
navigation handler; fix by filtering out matches with no id before mapping
(e.g., add a check like filter(m => m.id != null) before the map that builds
history) so only matches with a defined m.id are included, and keep existing
filters for state and round; reference symbols: teamMatches, history, m.id,
teamId and the navigation handler that consumes the history entries.
In `@frontend/app/events/`[id]/groups/RankingTable.tsx:
- Around line 97-103: The history mapping in RankingTable.tsx uses
history.map((match, i) => ...) and calls
router.push(`/events/${eventId}/match/${match.id}`) without guarding that
match.id (from the Match type) may be undefined; update the code so only matches
with a defined id are rendered or navigable (e.g., filter history to entries
where match.id is truthy or early-return/disable the onClick when match.id is
missing) and ensure the key and router.push use the guaranteed id (or fallback
behavior) so navigation to /match/undefined cannot occur.
---
Outside diff comments:
In `@frontend/app/events/`[id]/bracket/page.tsx:
- Around line 11-14: The page has inconsistent titles: the exported const
metadata.title is "Tournament Bracket" while the rendered <h1> displays
"Tournament Tree"; update one of them so they match (either change
metadata.title to "Tournament Tree" or change the <h1> text to "Tournament
Bracket")—edit the export const metadata object and/or the <h1> element in the
page component to use the same string so SEO and headings are consistent.
---
Nitpick comments:
In `@frontend/app/events/`[id]/bracket/BracketRankingTable.tsx:
- Around line 39-78: getTeamStats is being re-created and re-run for every
render and for each team, causing repeated filters/sorts over revealedMatches;
move the heavy work out of the inline function by memoizing computed stats per
team (e.g., useMemo or precompute a Map keyed by teamId) so you compute
teamMatches, highestRound, actualRank, and history once per
revealedMatches/maxRound change, then have getTeamStats (or the map lookup)
return the precomputed entry when iterating teams (referencing getTeamStats,
revealedMatches, maxRound and the .map usage that calls it).
- Around line 180-208: The match-history badge rendering in
BracketRankingTable.tsx duplicates logic found in RankingTable.tsx; extract a
shared functional component (e.g., MatchHistoryBadges) that accepts props {
history: Match[], eventId: string, router } (or a click callback) and
encapsulates the mapping of match.result to className, the onClick
router.push(`/events/${eventId}/match/${match.id}`) behavior, keying, and the
"No matches" fallback; replace the inline JSX in both BracketRankingTable and
RankingTable with this new component to remove duplication while preserving
styling, result-to-color mapping, and stopPropagation behavior.
In `@frontend/app/events/`[id]/bracket/BracketTabs.tsx:
- Around line 26-36: The tab state and URL-param logic used by currentTab and
onTabChange (which use useRouter, usePathname, useSearchParams) are duplicated
in GroupPhaseTabs.tsx; extract this into a shared hook (e.g., useTabParam) that
encapsulates reading the "tab" param and updating it via router.push, then
replace the inline logic in BracketTabs.tsx and GroupPhaseTabs.tsx with calls to
useTabParam to return the currentTab value and an onTabChange handler; ensure
the hook accepts an optional defaultTab and maintains the same behavior
(preserve existing pathname?scroll:false) so behavior is identical.
…utilized in the bracket view.
…`MatchHistoryBadges` component for match history display.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/app/events/[id]/dashboard/dashboard.tsx (1)
648-668:⚠️ Potential issue | 🟠 Major
confirm()is blocked by ESLintno-alert— replace with shadcnAlertDialog.Both destructive cleanup buttons (Group Phase at Line 652, Tournament at Line 694) use
confirm(), which is flagged by the pipeline asno-alertviolations. The coding guidelines also require using shadcn/ui exclusively for UI interactions.🛠️ Suggested approach using
AlertDialogAdd state to control the dialog and replace the inline
confirm()with anAlertDialog:+import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; // Replace the Button + confirm block: -<Button - disabled={cleanupMatchesMutation.isPending} - onClick={() => { - if (confirm("Are you sure you want to delete ALL Group Phase matches? This will also reset team scores!")) { - cleanupMatchesMutation.mutate("SWISS"); - } - }} - variant="destructive" -> - ... - Clean Up Group Phase -</Button> +<AlertDialog> + <AlertDialogTrigger asChild> + <Button disabled={cleanupMatchesMutation.isPending} variant="destructive"> + {cleanupMatchesMutation.isPending ? ( + <Loader2 className="mr-2 h-4 w-4 animate-spin" /> + ) : ( + <Trash2 className="mr-2 h-4 w-4" /> + )} + Clean Up Group Phase + </Button> + </AlertDialogTrigger> + <AlertDialogContent> + <AlertDialogHeader> + <AlertDialogTitle>Delete all Group Phase matches?</AlertDialogTitle> + <AlertDialogDescription> + This will also reset team scores and cannot be undone. + </AlertDialogDescription> + </AlertDialogHeader> + <AlertDialogFooter> + <AlertDialogCancel>Cancel</AlertDialogCancel> + <AlertDialogAction onClick={() => cleanupMatchesMutation.mutate("SWISS")}> + Confirm + </AlertDialogAction> + </AlertDialogFooter> + </AlertDialogContent> +</AlertDialog>Apply the same pattern for the Tournament cleanup button around Line 694.
As per coding guidelines: "ONLY use shadcn/ui components for building UIs in Next.js."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/events/`[id]/dashboard/dashboard.tsx around lines 648 - 668, Replace the inline confirm() usage in the Group Phase cleanup Button with a shadcn AlertDialog: add local state (e.g., isGroupCleanupOpen) or use AlertDialog's controlled API to open the dialog from the Button's onClick, render AlertDialog with title/body warning about deleting matches, and call cleanupMatchesMutation.mutate("SWISS") from the AlertDialogConfirm handler; ensure the original Button still shows Loader2 vs Trash2 based on cleanupMatchesMutation.isPending and remains disabled when isPending; repeat the exact same pattern for the Tournament cleanup Button (cleanupMatchesMutation.mutate("TOURNAMENT") or the appropriate key) so both no longer use window.confirm and use shadcn/ui AlertDialog components.
🧹 Nitpick comments (5)
frontend/components/match/MatchNode.tsx (1)
183-185: Redundantmatch.state === MatchState.FINISHEDguard inside already-gated branch.The outer ternary at line 157 already ensures
match.state === MatchState.FINISHEDbefore enteringmatch.teams.map(…), so the re-check at line 184 is alwaystrueand can be removed.♻️ Proposed simplification
- {!hideScore - && match.state === MatchState.FINISHED - && team.score !== undefined && ( + {!hideScore && team.score !== undefined && (🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/match/MatchNode.tsx` around lines 183 - 185, Remove the redundant state check inside the team render branch: since the outer conditional already ensures match.state === MatchState.FINISHED before iterating match.teams.map, eliminate the inner "&& match.state === MatchState.FINISHED" guard and rely only on "!hideScore && team.score !== undefined" (or equivalent) to decide whether to render the score; update the conditional near match.teams.map in MatchNode.tsx to reference hideScore and team.score only, keeping MatchState.FINISHED enforced by the outer ternary.frontend/components/match/MatchHistoryBadges.tsx (1)
29-30: Prefermatch.idover array index as the Reactkey.Using
ias the key means React cannot distinguish badge identity across re-renders if the history order changes. Sincematch.idis a stable unique string on everyMatchResult, it's a straightforward drop-in.♻️ Proposed fix
- <div - key={i} + <div + key={match.id}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/components/match/MatchHistoryBadges.tsx` around lines 29 - 30, The div currently uses the array index i as its React key which can break identity when history order changes; in the MatchHistoryBadges component replace key={i} with key={match.id} (using the stable unique string on each MatchResult) and remove reliance on the index; also ensure the MatchResult type/prop supplies a non-null id (add a simple fallback or type assertion if necessary) so every badge has a stable unique key.frontend/hooks/useTabParam.ts (1)
1-1: Add'use client'directive to this hook file.The hook exclusively uses client-side Next.js primitives (
useRouter,usePathname,useSearchParams). Without'use client', accidentally importing it into a Server Component produces a cryptic runtime error rather than a clear build-time signal.✨ Proposed fix
+"use client"; + import { usePathname, useRouter, useSearchParams } from "next/navigation";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/hooks/useTabParam.ts` at line 1, Add the "use client" directive at the very top of the file to mark this hook as a client component; this file uses client-only Next primitives (useRouter, usePathname, useSearchParams) so prepend 'use client' on the first line before any imports in useTabParam.ts to ensure it's treated as a client module.frontend/app/events/[id]/bracket/BracketRankingTable.tsx (1)
103-126:swissRankMapandsortedTeamsrecompute on every render — wrap inuseMemo.
teamStatsMapis correctly memoized, but the dependent computationsswissRankMapandsortedTeams(which sort and transform the entireteamsarray) are plain variable declarations that re-run on every render.♻️ Proposed fix
- const swissRankMap = new Map( + const swissRankMap = useMemo(() => new Map( [...teams] .sort((a, b) => b.score !== a.score ? b.score - a.score : b.buchholzPoints - a.buchholzPoints, ) .map((t, i) => [t.id, i + 1]), - ); + ), [teams]); - const sortedTeams = teams + const sortedTeams = useMemo(() => teams .map(team => ({ ...team, swissRank: swissRankMap.get(team.id), ...getTeamStats(team.id), })) .filter(team => team.hasMatches) .sort((a, b) => { if (a.actualRank !== b.actualRank) return a.actualRank - b.actualRank; if (b.score !== a.score) return b.score - a.score; return b.buchholzPoints - a.buchholzPoints; - }); + }), [teams, swissRankMap, teamStatsMap]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/events/`[id]/bracket/BracketRankingTable.tsx around lines 103 - 126, Wrap the expensive derived computations in useMemo: compute swissRankMap inside a useMemo that depends on teams (so it only re-sorts when teams change), and compute sortedTeams inside another useMemo that depends on teams, swissRankMap and getTeamStats/teamStatsMap (or whatever stable identifier provides team stats) so the mapping/filter/sort only runs when inputs change; also ensure useMemo is imported from React and that dependencies include any functions/objects used (e.g., teams, swissRankMap, getTeamStats or teamStatsMap) to avoid stale values.frontend/app/events/[id]/bracket/BracketTabs.tsx (1)
28-61: Tab shell is structurally identical toGroupPhaseTabs— consider extracting a shared wrapper.The
TabsListwith itsNetwork/BarChart3triggers,className, and layout div are copy-pasted verbatim fromGroupPhaseTabs.tsx. If the icon set or tab labels ever change, both files need updating in sync. A lightweight shared component (e.g.,GraphRankingTabShell) acceptingchildrenfor the tab contents could eliminate this duplication.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/events/`[id]/bracket/BracketTabs.tsx around lines 28 - 61, This file duplicates the tab shell UI from GroupPhaseTabs; extract the shared structure (Tabs, the surrounding div, TabsList and TabsTrigger with Network/BarChart3 and their classNames) into a new reusable component (e.g., GraphRankingTabShell) that accepts props for currentTab, onTabChange and children for tab contents; then replace the duplicated JSX in BracketTabs (where GraphView and BracketRankingTable are used) and in GroupPhaseTabs to render GraphRankingTabShell with the appropriate children and pass through isEventAdmin/team/match props as needed so styling and icon/label changes are centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/app/events/`[id]/groups/GroupPhaseTabs.tsx:
- Line 50: The change replaced responsive padding "p-1 md:p-4" with fixed "px-2
py-1" in the container div inside GroupPhaseTabs component; decide whether
medium+ screens should retain larger padding—if so, restore responsive classes
(e.g., use p-1 md:p-4 or equivalent) on the element with className
"overflow-hidden rounded-xl border bg-card/50 px-2 py-1 text-card-foreground
shadow-sm md:rounded-2xl" to match the bracket view spacing, otherwise confirm
the non-responsive px/py values are intentional and remove the review note.
In `@frontend/components/match/MatchHistoryBadges.tsx`:
- Line 8: The prop type for match result currently declared as `result: "W" |
"L" | "T" | string` collapses to plain string; update the type to a strict union
(e.g. `type MatchResult = "W" | "L" | "T"` and use `result: MatchResult` in the
MatchHistoryBadges props) and update any callers to satisfy this stricter type
(or provide a converter/validator where external DTOs may contain arbitrary
strings); ensure the discriminant checks in the cn(...) expression (and any
switch/if branches that style based on `result`) now operate on the narrowed
`MatchResult` type so unknown values are handled explicitly (e.g. map/validation
before rendering).
In `@frontend/hooks/useTabParam.ts`:
- Around line 6-8: Pages that render the client components BracketTabs and
GroupPhaseTabs must wrap those components in a React.Suspense boundary because
useTabParam calls useSearchParams (a Client hook) and requires suspense when a
Client Component is rendered from a Server Component; update the two parent
pages (the components that render BracketTabs and GroupPhaseTabs) to import
Suspense from React and wrap each usage like <Suspense
fallback={...}><BracketTabs .../></Suspense> and <Suspense
fallback={...}><GroupPhaseTabs .../></Suspense> respectively, choosing a minimal
fallback (spinner or null) so the page does not fully opt into client rendering
and to avoid production build/runtime failures.
---
Outside diff comments:
In `@frontend/app/events/`[id]/dashboard/dashboard.tsx:
- Around line 648-668: Replace the inline confirm() usage in the Group Phase
cleanup Button with a shadcn AlertDialog: add local state (e.g.,
isGroupCleanupOpen) or use AlertDialog's controlled API to open the dialog from
the Button's onClick, render AlertDialog with title/body warning about deleting
matches, and call cleanupMatchesMutation.mutate("SWISS") from the
AlertDialogConfirm handler; ensure the original Button still shows Loader2 vs
Trash2 based on cleanupMatchesMutation.isPending and remains disabled when
isPending; repeat the exact same pattern for the Tournament cleanup Button
(cleanupMatchesMutation.mutate("TOURNAMENT") or the appropriate key) so both no
longer use window.confirm and use shadcn/ui AlertDialog components.
---
Duplicate comments:
In `@frontend/app/events/`[id]/bracket/BracketRankingTable.tsx:
- Around line 83-89: The history mapping uses m.id! unsafely: update the
pipeline that constructs history from teamMatches (the filter/sort/map that
produces history) to exclude matches with undefined ids before the map (or add a
guard in the map) so you never assert m.id; e.g., ensure the filter checks m.id
truthiness (m => m.state === "FINISHED" && m.id) or otherwise skip entries
without id so MatchHistoryBadges / navigation never receives an "undefined" id.
In `@frontend/app/events/`[id]/groups/RankingTable.tsx:
- Around line 40-53: The getMatchHistory function currently uses m.id! in the
map, risking runtime "undefined" IDs; update the filter to include !!m.id so
only matches with an id pass (e.g., filter m => m.state === "FINISHED" &&
m.teams.some(t => t.id === teamId) && !!m.id) and introduce a type predicate or
narrow the type so the subsequent map sees m.id as defined (ensuring the mapped
objects returned by getMatchHistory always use a non-optional id), which
prevents navigation to /match/undefined and removes the need for the non-null
assertion.
---
Nitpick comments:
In `@frontend/app/events/`[id]/bracket/BracketRankingTable.tsx:
- Around line 103-126: Wrap the expensive derived computations in useMemo:
compute swissRankMap inside a useMemo that depends on teams (so it only re-sorts
when teams change), and compute sortedTeams inside another useMemo that depends
on teams, swissRankMap and getTeamStats/teamStatsMap (or whatever stable
identifier provides team stats) so the mapping/filter/sort only runs when inputs
change; also ensure useMemo is imported from React and that dependencies include
any functions/objects used (e.g., teams, swissRankMap, getTeamStats or
teamStatsMap) to avoid stale values.
In `@frontend/app/events/`[id]/bracket/BracketTabs.tsx:
- Around line 28-61: This file duplicates the tab shell UI from GroupPhaseTabs;
extract the shared structure (Tabs, the surrounding div, TabsList and
TabsTrigger with Network/BarChart3 and their classNames) into a new reusable
component (e.g., GraphRankingTabShell) that accepts props for currentTab,
onTabChange and children for tab contents; then replace the duplicated JSX in
BracketTabs (where GraphView and BracketRankingTable are used) and in
GroupPhaseTabs to render GraphRankingTabShell with the appropriate children and
pass through isEventAdmin/team/match props as needed so styling and icon/label
changes are centralized.
In `@frontend/components/match/MatchHistoryBadges.tsx`:
- Around line 29-30: The div currently uses the array index i as its React key
which can break identity when history order changes; in the MatchHistoryBadges
component replace key={i} with key={match.id} (using the stable unique string on
each MatchResult) and remove reliance on the index; also ensure the MatchResult
type/prop supplies a non-null id (add a simple fallback or type assertion if
necessary) so every badge has a stable unique key.
In `@frontend/components/match/MatchNode.tsx`:
- Around line 183-185: Remove the redundant state check inside the team render
branch: since the outer conditional already ensures match.state ===
MatchState.FINISHED before iterating match.teams.map, eliminate the inner "&&
match.state === MatchState.FINISHED" guard and rely only on "!hideScore &&
team.score !== undefined" (or equivalent) to decide whether to render the score;
update the conditional near match.teams.map in MatchNode.tsx to reference
hideScore and team.score only, keeping MatchState.FINISHED enforced by the outer
ternary.
In `@frontend/hooks/useTabParam.ts`:
- Line 1: Add the "use client" directive at the very top of the file to mark
this hook as a client component; this file uses client-only Next primitives
(useRouter, usePathname, useSearchParams) so prepend 'use client' on the first
line before any imports in useTabParam.ts to ensure it's treated as a client
module.
Summary by CodeRabbit
New Features
Style
Documentation