Visitar URL original
Unify competition, achievements, and public sharing by mkernsNCR · Pull Request #15 · mkernsNCR/bowlsense · GitHub
Skip to content

Unify competition, achievements, and public sharing - #15

Merged
mkernsNCR merged 27 commits into
mainfrom
agent/competition
Aug 1, 2026
Merged

mkernsNCR merged 27 commits into
mainfrom
agent/competition

Conversation

@mkernsNCR

@mkernsNCR mkernsNCR commented Jul 28, 2026 •

Copy link
Copy Markdown
Owner

Gives leagues, tournaments, achievements, and public sharing one competition language.

  • Uses grouped private competition views and sheet-based create/edit flows.
  • Keeps perfect-game celebration rare and reduced-motion aware.
  • Gives anonymous public/share routes a minimal shell without private notes or controls.

Stack: agent/competition → agent/gear

Validation: npm --prefix frontend run build

Closes #7

Summary by CodeRabbit

  • New Features

    • Archive and restore leagues and tournaments without deleting records.
    • View active, upcoming, past, and archived competitions in organized sections.
    • Share perfect-game scorecards with improved save and sharing controls.
    • Enjoy consistent public result pages with social previews, sharing tools, and SEO metadata.
    • View streamlined public profile summary statistics.
  • Bug Fixes

    • Preserved archive status during restores and exports.
    • Improved accessibility, responsive layouts, metadata handling, and reduced-motion support.
  • Tests

    • Expanded coverage for archiving, sharing, metadata, scoring, and public competition pages.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@mkernsNCR, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 58bbf598-e94e-4d22-968d-48438d458a2f

📥 Commits

Reviewing files that changed from the base of the PR and between 88a487e and 070f1e2.

⛔ Files ignored due to path filters (1)
  • backend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (46)
  • backend/package.json
  • backend/src/competitionArchive.pr15.test.mjs
  • backend/src/publicMetadata.pr15.test.mjs
  • backend/src/run-tests.mjs
  • backend/src/schema.ts
  • backend/src/server.ts
  • backend/src/test-app.mjs
  • frontend/package.json
  • frontend/src/components/BowlingScorer.tsx
  • frontend/src/components/PerfectGameCelebration.tsx
  • frontend/src/components/QuickAddGame.tsx
  • frontend/src/components/ShareCard.tsx
  • frontend/src/design/Icon.tsx
  • frontend/src/features/competition/CompetitionUI.tsx
  • frontend/src/features/competition/archive.ts
  • frontend/src/features/competition/competition.css
  • frontend/src/features/competition/competition.test.mjs
  • frontend/src/features/competition/publicMetadata.pr15.test.tsx
  • frontend/src/features/competition/publicMetadata.ts
  • frontend/src/features/competition/useCopyLink.test.tsx
  • frontend/src/features/competition/useCopyLink.ts
  • frontend/src/features/scoring/BowlingScorer.test.tsx
  • frontend/src/features/scoring/frameMarks.test.tsx
  • frontend/src/features/scoring/frameMarks.ts
  • frontend/src/features/scoring/scoring.css
  • frontend/src/features/today/QuickLogSheet.tsx
  • frontend/src/pages/CompetitionArchiveOrder.pr15.test.tsx
  • frontend/src/pages/LeagueRecap.tsx
  • frontend/src/pages/LeagueRecapShare.tsx
  • frontend/src/pages/LeagueShare.tsx
  • frontend/src/pages/LeagueWeekShare.tsx
  • frontend/src/pages/Leagues.tsx
  • frontend/src/pages/PerfectGameShare.tsx
  • frontend/src/pages/PerfectGames.tsx
  • frontend/src/pages/PublicLeague.tsx
  • frontend/src/pages/PublicLeagueLeaderboard.tsx
  • frontend/src/pages/PublicProfile.tsx
  • frontend/src/pages/SessionDetail.tsx
  • frontend/src/pages/SessionShare.test.tsx
  • frontend/src/pages/SessionShare.tsx
  • frontend/src/pages/ShareScore.tsx
  • frontend/src/pages/TournamentShare.tsx
  • frontend/src/pages/TournamentStandings.tsx
  • frontend/src/pages/TournamentStandingsShare.tsx
  • frontend/src/pages/Tournaments.tsx
  • frontend/src/utils/gameShare.ts
📝 Walkthrough

Walkthrough

This change adds archive-state support for leagues and tournaments, shared competition and public-result interfaces, server and client metadata handling, perfect-game sharing, scoring utilities, and related backend and frontend tests.

Changes

Competition and sharing

Layer / File(s) Summary
Competition archive backend
backend/src/schema.ts, backend/src/server.ts, backend/src/competitionArchive.pr15.test.mjs
Leagues and tournaments now use active state changes for archive and restore operations. Listing, restore, backup, and CSV flows preserve archive state.
Server-rendered public metadata
backend/src/server.ts, backend/src/publicMetadata.pr15.test.mjs, backend/src/test-app.mjs
The server generates route-specific metadata, escapes injected values, supports SPA fallthrough, and exposes test initialization controls.
Shared competition and scoring primitives
frontend/src/features/competition/*, frontend/src/features/scoring/*, frontend/src/components/BowlingScorer.tsx, frontend/src/components/ShareCard.tsx, frontend/src/design/Icon.tsx
The frontend adds shared shells, archive sheets, public results, metadata hooks, frame-mark utilities, perfect-game sharing, responsive styling, and reduced-motion behavior.
Private competition interfaces
frontend/src/pages/Leagues.tsx, frontend/src/pages/Tournaments.tsx, frontend/src/pages/LeagueRecap.tsx
League and tournament pages now separate active and archived records, use archive and restore sheets, use labeled forms, and pass context into scoring flows. Recaps track request and download outcomes.
Public result pages
frontend/src/pages/*Share.tsx, frontend/src/pages/Public*.tsx, frontend/src/pages/ShareScore.tsx
Public pages now use shared shells, result summaries, client metadata, share endpoints, and clipboard helpers. Profile data uses aggregate statistics.
Validation and test execution
backend/package.json, frontend/package.json, backend/src/*test.mjs, frontend/src/**/*.test.*
Test scripts and coverage now include archive migration, metadata injection, public pages, competition UI, scoring, sharing, and archive ordering flows.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant SPAFallback
  participant MetadataResolver
  participant PublicPage
  Browser->>SPAFallback: Request public route
  SPAFallback->>MetadataResolver: Resolve route metadata
  MetadataResolver-->>SPAFallback: Return page metadata
  SPAFallback-->>Browser: Return injected HTML
  Browser->>PublicPage: Hydrate public result page
  PublicPage->>MetadataResolver: Apply client metadata
  MetadataResolver-->>PublicPage: Restore metadata on cleanup
Loading
sequenceDiagram
  participant User
  participant CompetitionPage
  participant Server
  participant Database
  User->>CompetitionPage: Select archive or restore
  CompetitionPage->>Server: POST archive or unarchive
  Server->>Database: Update active state
  Database-->>Server: Preserve competition data
  Server-->>CompetitionPage: Return updated state
  CompetitionPage-->>User: Refresh active and archived sections
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The frontend requirements are addressed, but the PR violates issue #7's parallel-work contract by modifying backend files outside the owned paths. Move backend changes to the owning workstream or update issue #7 to explicitly include the backend paths.
Out of Scope Changes check ⚠️ Warning Backend schema, server, package, helper, and test changes are outside issue #7's listed frontend-owned files. Remove the backend changes from this PR or expand the linked issue's ownership contract to cover those paths.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the PR's primary changes to competition, achievements, and public sharing.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/competition

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Action performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mkernsNCR

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 34 minutes.

@mkernsNCR

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 22 minutes.

@mkernsNCR

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 17 minutes.

@mkernsNCR
mkernsNCR changed the base branch from agent/gear to main August 1, 2026 06:31
@mkernsNCR

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 41 minutes.

@mkernsNCR

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 25

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
frontend/src/pages/PerfectGames.tsx (1)

163-190: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Wrap or stack the action rows at 320px.

The second row has four .btn controls with padding: 1rem and a 0.5rem gap. With the card’s 36px box-horizontal padding, 360px leaves about 77px per flex item, but Download image needs at least ~92px for text alone and the buttons also include horizontal padding/gap. Add a mobile breakpoint-style wrap/stack fallback, or set .btn min-width: 0 with shrinking text/truncation, so UI doesn’t overflow.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/PerfectGames.tsx` around lines 163 - 190, Update the
action-row layout in PerfectGames so the four .btn controls wrap or stack at
320px, preventing overflow from the fixed card padding, button padding, and
gaps; preserve the existing desktop arrangement and ensure labels such as
“Download image” remain usable.
frontend/src/pages/LeagueWeekShare.tsx (1)

91-99: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Rewrite the error copy so it does not instruct an anonymous visitor to log a week.

Line 94 tells the reader to "Log your first week to generate a share card!". This route is public and unauthenticated, so the reader is usually not the account owner. That person cannot log a week, and the sentence sends them toward an action they cannot perform.

There is a second problem. This branch runs whenever the query fails, which includes network errors and server errors, not only an empty league. So the message also states a cause that may not be true.

Keep the copy neutral and factual for a shared page.

✏️ Proposed fix for the public error copy
         {isError && (
           <div style={{ textAlign: 'center', padding: 80 }}>
             <div style={{ color: '`#fc8181`', marginBottom: 16 }}>Failed to load week data.</div>
-            <p style={{ color: 'rgba(255,255,255,0.6)' }}>No league weeks found. Log your first week to generate a share card! 🎳</p>
+            <p style={{ color: 'rgba(255,255,255,0.6)' }}>This recap is not available. The link may be incorrect or the result may no longer be shared.</p>
             <Link to="/" className="btn btn-primary" style={{ marginTop: 20, display: 'inline-block' }}>
               BowlSense home
             </Link>
           </div>
         )}

The change of destination from the leagues page to the home page on line 95 is correct, since /leagues is a private route.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/LeagueWeekShare.tsx` around lines 91 - 99, Update the
error-state copy in the LeagueWeekShare component’s isError branch to be neutral
and factual for unauthenticated visitors: remove the instruction to log a week
and avoid claiming that no league weeks exist, since the branch also covers
network and server failures. Preserve the existing home-page Link destination
and surrounding error-state layout.
frontend/src/pages/LeagueRecapShare.tsx (1)

110-122: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the download button label so it does not claim success while the request runs.

Here is the sequence worth tracing. Line 62 sets downloading to true before the fetch on line 64. Line 121 then renders "Downloaded". So the button claims completion while the request is still in flight.

The error path makes this clearer. If the fetch on line 64 throws, the catch on line 74 swallows the error, and the timeout on line 75 clears the flag after 1200 ms. The user sees "Downloaded" for a download that never happened, and no other error surface exists on this page.

The underlying cause is that one boolean carries two meanings. downloading records "the action started", but the label asserts "the action finished". Split those two states so the label matches reality.

🐛 Proposed fix to separate the in-flight state from the success state
   const [downloading, setDownloading] = useState(false)
+  const [downloaded, setDownloaded] = useState(false)
   const downloadPng = async () => {
     if (!data) return
     setDownloading(true)
     try {
       const res = await fetch(`/api/leagues/${leagueId}/recap/og-image`)
+      if (!res.ok) throw new Error('Failed to fetch image')
       const blob = await res.blob()
       const url = URL.createObjectURL(blob)
       const a = document.createElement('a')
       a.href = url
       a.download = `${(data.league.name || 'league').replace(/\s+/g, '-').toLowerCase()}-week-${data.week.weekNumber}-recap.png`
       document.body.appendChild(a)
       a.click()
       document.body.removeChild(a)
       URL.revokeObjectURL(url)
-    } catch { /* ignore */ }
-    setTimeout(() => setDownloading(false), 1200)
+      setDownloaded(true)
+      setTimeout(() => setDownloaded(false), 1200)
+    } catch { /* ignore */ } finally {
+      setDownloading(false)
+    }
   }
-            {downloading ? 'Downloaded' : 'Download image'}
+            {downloaded ? 'Downloaded' : downloading ? 'Saving…' : 'Download image'}

Note that ShareScore.tsx line 157 already uses 'Saving…' for the in-flight label, so this also aligns the two pages on one vocabulary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/LeagueRecapShare.tsx` around lines 110 - 122, Separate the
in-flight and successful download states in the downloadPng flow and button
rendering in LeagueRecapShare. Keep downloading true only while the request is
active, introduce or reuse a success state that is set only after the download
completes successfully and reset on failure, and render “Downloading…” while
active and “Downloaded” only after success, matching ShareScore’s vocabulary.
frontend/src/pages/TournamentStandings.tsx (1)

171-188: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Four controls kept dark-shell inline colors after moving under the light PublicShell.

These pages moved from the dark app shell into PublicShell, which is the light public theme. Several elements declare only one half of a foreground/background pair inline and inherit the other half. Under the old dark parent that worked. Under the light parent the two halves no longer match, and the text loses contrast. This is the single hazard behind all four sites, in two variants: dark background with an inherited light-theme foreground, and white foreground with an inherited light-theme background.

  • frontend/src/pages/TournamentStandings.tsx#L171-L188: remove the inline color: '#fff' and borderColor: 'rgba(255,255,255,0.2)' from the X-share anchor and the download button, and let btn-ghost own their colors.
  • frontend/src/pages/TournamentStandings.tsx#L191-L192: add an explicit light foreground to the loading card, which currently sets only background: '#121228'.
  • frontend/src/pages/PublicLeagueLeaderboard.tsx#L123-L124: add an explicit light foreground to the loading card, which currently sets only background: '#121228'.
  • frontend/src/pages/TournamentStandingsShare.tsx#L185-L202: delete the inline style block on the X-share anchor so the newly added btn-ghost class actually applies.

A useful rule for the rest of this migration: inside public-legacy-content, an element may set both colors or neither, but never just one.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/TournamentStandings.tsx` around lines 171 - 188, Update
the public-theme controls across frontend/src/pages/TournamentStandings.tsx
lines 171-188 and 191-192, frontend/src/pages/PublicLeagueLeaderboard.tsx lines
123-124, and frontend/src/pages/TournamentStandingsShare.tsx lines 185-202:
remove the inline foreground and border colors from the TournamentStandings
share/download controls so btn-ghost owns both, add an explicit light foreground
to each dark loading card, and remove the TournamentStandingsShare share
anchor’s inline style so btn-ghost applies.
frontend/src/pages/Leagues.tsx (1)

443-455: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Hiding the Expand/Collapse text removes the only state cue for screen readers.

The change swapped the glyph for the words Expand and Collapse, which reads better visually. The aria-hidden="true" then removes that text from the accessibility tree, and the button on Line 443 carries no aria-expanded. The result is that assistive technology announces the week summary and gives no indication that the control toggles a panel, or which state it is currently in.

You already compute expanded on Line 440, so the fix is one attribute. Adding aria-controls also links the button to the panel it reveals.

♿ Suggested fix
               <button
+                aria-expanded={expanded}
+                aria-controls={`week-${week.id}-games`}
                 style={{ width: '100%', background: 'transparent', border: 'none', color: 'inherit', textAlign: 'left', cursor: 'pointer' }}
                 onClick={() => setExpandedWeeks((prev) => prev.includes(week.id) ? prev.filter((w) => w !== week.id) : [...prev, week.id])}
               >

Then give the revealed panel on Line 507 the matching id:

-                <div style={{ marginTop: 10, borderTop: '1px solid var(--border)', paddingTop: 10 }}>
+                <div id={`week-${week.id}-games`} style={{ marginTop: 10, borderTop: '1px solid var(--border)', paddingTop: 10 }}>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/Leagues.tsx` around lines 443 - 455, Add accessibility
state to the week toggle button using the existing expanded value: set
aria-expanded and add aria-controls referencing a unique panel identifier for
that week. Assign the matching id to the revealed panel around the week content
at line 507, and remove aria-hidden from the visible Expand/Collapse state text
so assistive technology receives the state cue.
frontend/src/pages/LeagueRecap.tsx (1)

67-75: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Removing the loading and error reset leaves the page stuck on a stale error.

This one is subtle, so let me trace it. React Router reuses the same component instance when only a route parameter changes. Navigating from /leagues/1/recap to /leagues/2/recap therefore reruns this effect, because leagueId is in the dependency array on Line 75, but it does not remount the component and does not reset state.

Two problems follow.

If the first league failed, error still holds the old message. The effect refetches, Line 72 sets the new data, and nothing clears error. Line 122 keeps rendering the error block, and Line 132 requires !error, so the successful result never appears. The page is stuck until a full reload.

If the first league succeeded, loading is already false and data still holds league 1. The user sees the previous league's recap presented as the new one, with no loading indicator, until the fetch resolves.

Reset both values at the start of the effect. That restores the previous behavior and costs two lines.

🐛 Suggested fix
   useEffect(() => {
     if (invalidId) return
     let cancelled = false
+    setLoading(true)
+    setError(null)
     fetch(`/api/leagues/${leagueId}/recap`)
       .then(r => { if (!r.ok) throw new Error('Recap not found'); return r.json() })
       .then(d => { if (!cancelled) { setData(d); setLoading(false) } })
       .catch(e => { if (!cancelled) { setError(e.message); setLoading(false) } })
     return () => { cancelled = true }
   }, [leagueId, invalidId])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/LeagueRecap.tsx` around lines 67 - 75, Update the
useEffect tied to leagueId and invalidId to reset loading to true and clear
error at the start of each valid fetch. Preserve the existing cancellation and
fetch handling so route changes show a loading state, discard stale errors, and
render only the newly fetched recap.
frontend/src/pages/Tournaments.tsx (1)

392-444: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The role="menu" promise is not matched by the keyboard behavior.

Adding role="menu" and role="menuitem" is a commitment, not a label. Assistive technology tells the user this is a menu, and the user then expects the standard menu interactions. Right now three of them are missing.

Escape does not close the menu. There is no keydown handler, so a keyboard user who opens it has no dismissal path other than tabbing through all four items.

The only dismissal is onMouseLeave on Line 409. That is mouse-only, and it also fires when the pointer crosses the gap between the trigger and the panel, which closes the menu while the user is still reaching for it.

A click outside the menu does not close it either.

You have two reasonable options. The lighter one is to drop the menu roles and keep a plain container of buttons. The roles add no value here, because the four buttons are already reachable and operable by keyboard, and removing the roles removes the mismatch. The fuller one is to keep the roles and add the expected handlers.

♿ Lighter option
           {shareMenuOpen && (
             <div
-              role="menu"
-              aria-label="Tournament sharing options"
+              aria-label="Tournament sharing options"
+              onKeyDown={(e) => { if (e.key === 'Escape') setShareMenuOpen(false) }}

Then remove role="menuitem" from the four buttons on Line 412, Line 420, Line 428, and Line 436, and change aria-haspopup="menu" on Line 387 to aria-haspopup="true".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/Tournaments.tsx` around lines 392 - 444, Remove the
unsupported menu semantics from the sharing controls: delete role="menu" from
the share-menu container, remove role="menuitem" from each button invoking
handleShare, handleCopyLink, handleDownloadCard, and handleXShare, and change
the trigger’s aria-haspopup value from "menu" to "true".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/package.json`:
- Line 8: Update the package.json test script to use node --test src/ so test
discovery works across all supported Node runtimes, or alternatively add an
engines.node requirement of >=21 to match the quoted glob behavior. Keep the
existing .test.mjs test coverage intact.

In `@backend/src/competitionArchive.pr15.test.mjs`:
- Around line 5-32: Replace the source-text regex tests in the archive test file
with behavioral tests that build the server against a temporary SQLite database
and exercise routes via fastify.inject(). Cover schema migration without data
loss, archive/unarchive behavior and listings for leagues and tournaments, 404
responses for missing resources, and backup restore plus tournament CSV
preservation of active state, including the leagues INSERT restore path.

In `@backend/src/publicMetadata.pr15.test.mjs`:
- Around line 25-43: Expand the HTML fixture in the replacement test around
inject to include stale og:description, og:url, og:type, twitter:title,
twitter:description, and twitter:card tags alongside the existing managed tags.
Add assertions that each managed metadata tag occurs exactly once after
injection, while preserving the existing escaping, replacement-value, and
private.example checks.
- Around line 58-69: Replace the serverSource regex assertions in crawler HTML
fallthrough covers API-colliding public result routes with request-level tests
against the registered server handlers. For each listed route, request a
representative URL and assert an HTML content type plus metadata-injected HTML
containing the resolved title and canonical og:url; retain or update the image
metadata assertion only if it is validated through the response.

In `@backend/src/server.ts`:
- Around line 2461-2465: Align both soft-delete handlers with the archive helper
contract: in backend/src/server.ts lines 2461-2465, update DELETE /leagues/:id
to use the same Number.isInteger validation as setLeagueArchived and verify
callers no longer require permanent deletion; if they do, add a separate
hard-delete route. Apply the same validation and caller-contract check to DELETE
/tournaments/:id at lines 3339-3356, using setTournamentArchived.
- Around line 1381-1390: Extract the shared archive-query logic into helpers
near the existing listing handlers, including a wantsArchived-style parser for
request.query and reusable league and tournament row-selection helpers. Replace
the duplicated activeFilter setup and SQL bodies in the league handlers around
the current code and the corresponding tournament handlers with calls to those
helpers, preserving the existing includeArchived behavior and result shape.
- Around line 5318-5336: Update requestOrigin so unconfigured public origins
never derive URLs from Host or X-Forwarded-Host; return the safe
http://localhost:3003 fallback instead. If forwarded headers remain supported,
configure Fastify trustProxy and validate the resolved host against an explicit
allowlist before constructing the origin, while preserving
BOWLSENSE_PUBLIC_ORIGIN as the configured public-domain source.
- Around line 5524-5527: Update the SPA fallback around
resolvePublicPageMetadata to avoid synchronous per-request work: cache
index.html rather than calling readFileSync for every navigation, and cache the
aggregate result used by the /bowl metadata path for a short interval (or reuse
a maintained summary updated by write paths) so resolvePublicPageMetadata does
not repeatedly scan the games table.

In `@frontend/src/components/ShareCard.tsx`:
- Around line 206-211: Add role="img" to the score-card canvas in ShareCard and
provide an accessible label or equivalent hidden summary describing all rendered
details, including location, date, ball, and frame marks. Keep the existing
visual canvas rendering unchanged.

In `@frontend/src/features/competition/publicMetadata.pr15.test.tsx`:
- Around line 59-78: Add a rerender scenario to the public metadata hook tests
using the existing Harness and pre-rendered metadata setup: render with an
initial title, rerender with a second title, then unmount and assert the
original server-rendered title and metadata are restored, including any relevant
tag removal behavior. This should exercise snapshot recapture across the effect
cleanup and dependency-change cycle.

In `@frontend/src/features/competition/publicMetadata.ts`:
- Around line 65-92: Review usePublicMetadata and determine whether its cleanup
behavior restoring the previous document.title and metadata is a deliberate
requirement; if it is not required, migrate the metadata updates to React 19
native title/meta elements and remove the manual DOM override logic. If restore
semantics are required, retain the existing hook implementation and document or
preserve that requirement rather than switching approaches.

In `@frontend/src/pages/CompetitionArchiveOrder.pr15.test.tsx`:
- Around line 20-33: Replace direct window.location.pathname reads in
LeaguesPage and TournamentsPage with pathname from React Router’s useLocation(),
so route checks follow MemoryRouter navigation. Then remove the
window.history.replaceState call from renderPage and reset the browser history
in afterEach to avoid test-order dependence.

In `@frontend/src/pages/LeagueRecap.tsx`:
- Around line 29-36: Update recapNarrative to use a shared singular/plural
helper for wins, losses, ties, and games, handling “loss” explicitly so singular
output is grammatically correct. Return no narrative when data.stats.totalGames
=== 0, matching the empty-state behavior already handled by the page.
- Around line 201-207: Update the downloadPng state flow and the button label:
use downloading to display an in-progress state while the fetch and download are
active, add a separate downloaded state set only after a.click() succeeds, and
expose the catch failure instead of swallowing it so failed downloads are
reported rather than shown as successful.

In `@frontend/src/pages/Leagues.tsx`:
- Around line 344-369: The archive/restore confirmation UI and mutation wiring
are duplicated between the league and tournament pages. In
frontend/src/features/competition/CompetitionUI.tsx, add a shared component
accepting the resource noun, archived flag, pending/error states, and
confirm/cancel handlers; update frontend/src/pages/Leagues.tsx lines 344-369 to
use it with noun="league" and archived={league.active === 0}, and
frontend/src/pages/Tournaments.tsx lines 452-477 to use it with
noun="tournament" and archived={tournament.active === 0}; also consolidate the
corresponding mutation wiring near Leagues.tsx line 262 and Tournaments.tsx line
259 while preserving each page’s URL segment.
- Around line 93-98: Update the useQuery flow in frontend/src/pages/Leagues.tsx
lines 93-98 to reject non-OK fetch responses before parsing JSON, expose
isError, and render an error state alongside the existing loading state; apply
the same r.ok handling and error-state rendering to the ['tournaments'] query in
frontend/src/pages/Tournaments.tsx lines 114-117.

In `@frontend/src/pages/PerfectGames.tsx`:
- Around line 26-31: Update PerfectGameCard so nullable game.frameData is
handled explicitly: require a non-null frameData before parsing and rendering
the scoreboard, or render a clear empty/null state when it is absent. Ensure the
card does not silently render an incomplete scoreboard through marks.map(...).

In `@frontend/src/pages/PerfectGameShare.tsx`:
- Around line 76-80: Catch rejected copyText calls in both handlers: update
handleCopyLink in frontend/src/pages/PerfectGameShare.tsx lines 76-80 and
copyLink in frontend/src/pages/ShareScore.tsx lines 78-82 with try/catch, and
set copied only after a successful await inside try. Ensure both button callers
no longer produce unhandled promise rejections; optional failure feedback is not
required.
- Around line 101-113: Update the fetch effect in PerfectGameShare so every
non-OK response sets a visible failure state: preserve the notFound state for
404 responses and set the existing failure state for other statuses, including
500 and 503. Keep the loading transition intact and retain the return null guard
only as an unreachable fallback after data and failure states are handled.

In `@frontend/src/pages/SessionShare.tsx`:
- Around line 146-150: Replace the inline flex styling on the action-row div in
SessionShare with the shared public-share-actions class, preserving the existing
buttons and links unchanged so spacing, wrapping, and responsive behavior come
from the shared stylesheet.

In `@frontend/src/pages/Tournaments.tsx`:
- Around line 121-128: Guard the date comparators in the upcomingTournaments,
pastTournaments, and archivedTournaments sorts against null or missing dates so
rendering never calls localeCompare on an invalid value. Update the Tournament
interface date type to string | null and adjust each comparator to use a safe
fallback while preserving the existing sort ordering for valid dates.

In `@frontend/src/pages/TournamentShare.tsx`:
- Line 194: Update the net value formatting in the stats construction to use
Intl.NumberFormat with currency formatting instead of hardcoding the dollar sign
and interpolating the raw number; preserve the existing positive/negative net
behavior while producing locale-aware grouping and two decimal places.

In `@frontend/src/pages/TournamentStandings.tsx`:
- Around line 91-94: Update the date formatting in TournamentStandings to pass
undefined instead of the hardcoded 'en-US' locale to toLocaleDateString,
matching TournamentShare’s viewer-locale behavior. Keep the existing formatting
options and the 'T00:00:00' local-time suffix unchanged.
- Around line 98-100: Guard the image URL passed to usePublicMetadata in
TournamentStandings by passing an empty string when invalidId is true, while
retaining ogImageUrl for valid tournament IDs. Keep the hook call in its current
position so hook order remains consistent, and preserve the existing not-found
rendering flow.

In `@frontend/src/pages/TournamentStandingsShare.tsx`:
- Around line 161-165: Update the render conditions for both the PublicResult
block and the standings table in the TournamentStandingsShare component to
depend on standings בלבד, removing the unnecessary tournamentData requirement.
Preserve the existing fallback values and error handling so successful standings
data still renders primary results when the metadata query fails.

---

Outside diff comments:
In `@frontend/src/pages/LeagueRecap.tsx`:
- Around line 67-75: Update the useEffect tied to leagueId and invalidId to
reset loading to true and clear error at the start of each valid fetch. Preserve
the existing cancellation and fetch handling so route changes show a loading
state, discard stale errors, and render only the newly fetched recap.

In `@frontend/src/pages/LeagueRecapShare.tsx`:
- Around line 110-122: Separate the in-flight and successful download states in
the downloadPng flow and button rendering in LeagueRecapShare. Keep downloading
true only while the request is active, introduce or reuse a success state that
is set only after the download completes successfully and reset on failure, and
render “Downloading…” while active and “Downloaded” only after success, matching
ShareScore’s vocabulary.

In `@frontend/src/pages/Leagues.tsx`:
- Around line 443-455: Add accessibility state to the week toggle button using
the existing expanded value: set aria-expanded and add aria-controls referencing
a unique panel identifier for that week. Assign the matching id to the revealed
panel around the week content at line 507, and remove aria-hidden from the
visible Expand/Collapse state text so assistive technology receives the state
cue.

In `@frontend/src/pages/LeagueWeekShare.tsx`:
- Around line 91-99: Update the error-state copy in the LeagueWeekShare
component’s isError branch to be neutral and factual for unauthenticated
visitors: remove the instruction to log a week and avoid claiming that no league
weeks exist, since the branch also covers network and server failures. Preserve
the existing home-page Link destination and surrounding error-state layout.

In `@frontend/src/pages/PerfectGames.tsx`:
- Around line 163-190: Update the action-row layout in PerfectGames so the four
.btn controls wrap or stack at 320px, preventing overflow from the fixed card
padding, button padding, and gaps; preserve the existing desktop arrangement and
ensure labels such as “Download image” remain usable.

In `@frontend/src/pages/Tournaments.tsx`:
- Around line 392-444: Remove the unsupported menu semantics from the sharing
controls: delete role="menu" from the share-menu container, remove
role="menuitem" from each button invoking handleShare, handleCopyLink,
handleDownloadCard, and handleXShare, and change the trigger’s aria-haspopup
value from "menu" to "true".

In `@frontend/src/pages/TournamentStandings.tsx`:
- Around line 171-188: Update the public-theme controls across
frontend/src/pages/TournamentStandings.tsx lines 171-188 and 191-192,
frontend/src/pages/PublicLeagueLeaderboard.tsx lines 123-124, and
frontend/src/pages/TournamentStandingsShare.tsx lines 185-202: remove the inline
foreground and border colors from the TournamentStandings share/download
controls so btn-ghost owns both, add an explicit light foreground to each dark
loading card, and remove the TournamentStandingsShare share anchor’s inline
style so btn-ghost applies.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c90a6d3b-6bc9-452b-8d14-6b0483f43727

📥 Commits

Reviewing files that changed from the base of the PR and between 88a487e and 8cb295c.

📒 Files selected for processing (39)
  • backend/package.json
  • backend/src/competitionArchive.pr15.test.mjs
  • backend/src/publicMetadata.pr15.test.mjs
  • backend/src/schema.ts
  • backend/src/server.ts
  • frontend/package.json
  • frontend/src/components/BowlingScorer.tsx
  • frontend/src/components/PerfectGameCelebration.tsx
  • frontend/src/components/QuickAddGame.tsx
  • frontend/src/components/ShareCard.tsx
  • frontend/src/design/Icon.tsx
  • frontend/src/features/competition/CompetitionUI.tsx
  • frontend/src/features/competition/competition.css
  • frontend/src/features/competition/competition.test.mjs
  • frontend/src/features/competition/publicMetadata.pr15.test.tsx
  • frontend/src/features/competition/publicMetadata.ts
  • frontend/src/features/scoring/BowlingScorer.test.tsx
  • frontend/src/features/scoring/frameMarks.test.tsx
  • frontend/src/features/scoring/frameMarks.ts
  • frontend/src/features/scoring/scoring.css
  • frontend/src/features/today/QuickLogSheet.tsx
  • frontend/src/pages/CompetitionArchiveOrder.pr15.test.tsx
  • frontend/src/pages/LeagueRecap.tsx
  • frontend/src/pages/LeagueRecapShare.tsx
  • frontend/src/pages/LeagueShare.tsx
  • frontend/src/pages/LeagueWeekShare.tsx
  • frontend/src/pages/Leagues.tsx
  • frontend/src/pages/PerfectGameShare.tsx
  • frontend/src/pages/PerfectGames.tsx
  • frontend/src/pages/PublicLeague.tsx
  • frontend/src/pages/PublicLeagueLeaderboard.tsx
  • frontend/src/pages/PublicProfile.tsx
  • frontend/src/pages/SessionDetail.tsx
  • frontend/src/pages/SessionShare.tsx
  • frontend/src/pages/ShareScore.tsx
  • frontend/src/pages/TournamentShare.tsx
  • frontend/src/pages/TournamentStandings.tsx
  • frontend/src/pages/TournamentStandingsShare.tsx
  • frontend/src/pages/Tournaments.tsx
💤 Files with no reviewable changes (1)
  • frontend/src/components/PerfectGameCelebration.tsx
📜 Review details
🧰 Additional context used
🪛 ast-grep (0.45.0)
backend/src/server.ts

[warning] 5305-5310: Avoid hand-rolled HTML escaping (replacing characters with HTML entities); use a vetted encoder/sanitizer such as DOMPurify or sanitize-html.
Context: value
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, '&#39;')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(manual-sanitization-typescript)


[warning] 5305-5309: Avoid hand-rolled HTML escaping (replacing characters with HTML entities); use a vetted encoder/sanitizer such as DOMPurify or sanitize-html.
Context: value
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(manual-sanitization-typescript)


[warning] 5305-5308: Avoid hand-rolled HTML escaping (replacing characters with HTML entities); use a vetted encoder/sanitizer such as DOMPurify or sanitize-html.
Context: value
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(manual-sanitization-typescript)


[warning] 5305-5307: Avoid hand-rolled HTML escaping (replacing characters with HTML entities); use a vetted encoder/sanitizer such as DOMPurify or sanitize-html.
Context: value
.replace(/&/g, '&')
.replace(/</g, '<')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(manual-sanitization-typescript)


[warning] 5305-5306: Avoid hand-rolled HTML escaping (replacing characters with HTML entities); use a vetted encoder/sanitizer such as DOMPurify or sanitize-html.
Context: value
.replace(/&/g, '&')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(manual-sanitization-typescript)

🔇 Additional comments (77)
frontend/src/pages/PerfectGames.tsx (3)

7-7: LGTM!


372-372: LGTM!


456-462: LGTM!

frontend/src/components/BowlingScorer.tsx (1)

15-15: LGTM!

Also applies to: 36-40, 52-57, 94-101, 111-118, 137-141, 164-164, 182-182, 526-566

frontend/src/components/QuickAddGame.tsx (1)

75-75: LGTM!

frontend/src/components/ShareCard.tsx (1)

2-3: LGTM!

Also applies to: 41-41, 64-64, 81-81, 146-146, 197-205, 213-229

frontend/src/features/scoring/frameMarks.ts (1)

1-66: LGTM!

frontend/src/features/scoring/scoring.css (1)

152-154: LGTM!

Also applies to: 163-163, 196-196

frontend/src/features/competition/competition.test.mjs (1)

1-69: LGTM!

frontend/src/features/scoring/frameMarks.test.tsx (1)

1-25: LGTM!

frontend/src/features/scoring/BowlingScorer.test.tsx (1)

6-9: LGTM!

Also applies to: 33-82

frontend/src/features/today/QuickLogSheet.tsx (1)

104-104: LGTM!

frontend/src/pages/SessionDetail.tsx (1)

262-262: LGTM!

Also applies to: 350-350

frontend/src/design/Icon.tsx (1)

27-27: LGTM!

Also applies to: 67-67

frontend/src/features/competition/CompetitionUI.tsx (5)

8-33: LGTM!


35-45: LGTM!


47-73: LGTM!


75-96: LGTM!


98-134: LGTM!

frontend/src/features/competition/competition.css (5)

1-30: LGTM!


32-37: LGTM!


39-54: LGTM!


56-76: LGTM!


78-102: LGTM!

frontend/src/pages/LeagueRecapShare.tsx (4)

3-5: LGTM!


46-58: LGTM!


78-95: LGTM!


150-157: LGTM!

frontend/src/pages/LeagueWeekShare.tsx (5)

1-6: LGTM!

Also applies to: 45-45


60-66: LGTM!


77-85: LGTM!


101-112: LGTM!


123-123: LGTM!

Also applies to: 156-156, 167-177

frontend/src/pages/PerfectGameShare.tsx (3)

4-6: LGTM!

Also applies to: 74-74


115-122: LGTM!


173-173: LGTM!

Also applies to: 192-192, 210-210, 268-277

frontend/src/pages/PublicLeague.tsx (6)

4-6: LGTM!

Also applies to: 86-97


216-243: LGTM!


245-284: LGTM!


370-370: LGTM!

Also applies to: 385-396, 405-405


449-449: LGTM!

Also applies to: 523-523, 534-535


176-214: 🗄️ Data Integrity & Integration

No change needed. The /api/leagues/:id/stats, standings, and leaderboard routes are public JSON mirrors in backend/src/server.ts, and PublicLeagueLeaderboard also uses the public leaderboard endpoint.

frontend/src/pages/PublicProfile.tsx (3)

1-15: LGTM!


17-60: LGTM!


62-86: LGTM!

frontend/src/pages/ShareScore.tsx (3)

3-6: LGTM!

Also applies to: 70-76


84-89: LGTM!

Also applies to: 113-142


143-169: LGTM!

frontend/src/features/competition/publicMetadata.ts (2)

18-36: LGTM!


38-63: LGTM!

frontend/src/features/competition/publicMetadata.pr15.test.tsx (1)

1-30: LGTM!

frontend/package.json (1)

13-13: LGTM!

frontend/src/pages/LeagueShare.tsx (3)

1-6: LGTM!

Also applies to: 28-38


68-88: LGTM!


90-133: LGTM!

frontend/src/pages/PublicLeagueLeaderboard.tsx (3)

62-65: LGTM!


78-88: LGTM!

Also applies to: 96-103


126-132: LGTM!

Also applies to: 200-201

frontend/src/pages/SessionShare.tsx (3)

8-10: LGTM!

Also applies to: 89-89


118-126: LGTM!


128-145: LGTM!

frontend/src/pages/TournamentShare.tsx (3)

1-7: LGTM!

Also applies to: 87-97


139-167: LGTM!


169-193: LGTM!

Also applies to: 195-208

frontend/src/pages/TournamentStandings.tsx (2)

1-7: LGTM!

Also applies to: 54-55


205-207: LGTM!

Also applies to: 217-217, 226-226, 263-265, 280-280

frontend/src/pages/TournamentStandingsShare.tsx (2)

1-6: LGTM!

Also applies to: 90-90, 117-127


151-159: LGTM!

Also applies to: 217-217, 333-333, 355-357

backend/src/server.ts (4)

5305-5312: 📐 Maintainability & Code Quality

The hand-rolled escaper is acceptable here, and the static analysis hint is a false positive.

The tool flags escapeHtml as manual sanitization and suggests DOMPurify. That advice targets a different problem. DOMPurify sanitizes untrusted HTML that you intend to render as markup. Here you build attribute values and element text, and you want every character neutralized, not a filtered subset of tags.

The five replacements cover the characters that matter for both text content and quoted attribute values, and the & replacement runs first so the other entities are not double-encoded. That ordering is the detail people usually get wrong, and it is correct here.

No change requested. I am recording the reasoning so the hint does not get "fixed" later into something weaker.

Source: Linters/SAST tools


30-34: LGTM!

Also applies to: 46-46, 450-450, 528-538, 2678-2678, 2713-2713, 2754-2754, 2810-2810, 2866-2866, 4329-4341


5354-5379: LGTM!

Also applies to: 5381-5493


3877-3877: 🩺 Stability & Availability

No change needed. The leagues INSERT already declares the active column and @active placeholder, and extra named parameters are permitted on better-sqlite3 run().

backend/src/schema.ts (1)

92-92: LGTM!

frontend/src/pages/CompetitionArchiveOrder.pr15.test.tsx (1)

36-48: LGTM!

Also applies to: 50-68, 70-91, 93-116

frontend/src/pages/Leagues.tsx (1)

5-7: LGTM!

Also applies to: 52-52, 79-90, 135-153, 174-186, 201-201, 220-220, 262-273, 291-342, 371-407, 419-435, 470-470, 487-504, 506-545, 564-564, 614-623, 649-649, 661-667

frontend/src/pages/Tournaments.tsx (1)

5-7: LGTM!

Also applies to: 46-46, 100-107, 155-180, 216-228, 248-248, 259-270, 309-330, 374-391, 445-449, 509-509, 529-545, 551-581

frontend/src/pages/LeagueRecap.tsx (1)

103-103: LGTM!

Also applies to: 113-113, 150-152, 199-199, 208-213

Comment thread backend/package.json Resolved
Comment thread backend/src/server.ts Resolved
Comment thread frontend/src/pages/Tournaments.tsx Resolved
Comment thread frontend/src/pages/TournamentShare.tsx Resolved
@mkernsNCR

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mkernsNCR

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 15 minutes.

@mkernsNCR

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 6 seconds.

@mkernsNCR

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 24

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/pages/LeagueRecap.tsx (1)

214-228: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider announcing the copy/download status changes to screen readers.

These buttons change their visible text to reflect state ("Link copied", "Preparing…", "Downloaded", "Download failed"), but neither carries aria-live. A screen reader user clicking "Copy link" or "Download image" gets no announcement that the action succeeded or failed; they'd have to re-focus the button to discover the new label.

Add aria-live="polite" to both buttons so the label change is announced automatically.

               <button
                 onClick={copyLink}
+                aria-live="polite"
                 className="btn btn-ghost"
                 style={{ borderColor: 'rgba(167,139,250,0.5)', color: '`#fff`', fontWeight: 800, flex: 1, minHeight: 48, borderRadius: 12 }}
               >
                 {copied ? 'Link copied' : 'Copy link'}
               </button>
               <button
                 onClick={downloadPng}
+                aria-live="polite"
                 className="btn btn-ghost"
                 disabled={downloading}
                 style={{ borderColor: 'rgba(167,139,250,0.5)', color: downloaded ? '`#34d399`' : downloadError ? '`#fc8181`' : '`#fff`', fontWeight: 800, flex: 1, minHeight: 48, borderRadius: 12 }}
               >
                 {downloading ? 'Preparing…' : downloaded ? 'Downloaded' : downloadError ? 'Download failed' : 'Download image'}
               </button>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/LeagueRecap.tsx` around lines 214 - 228, Add
aria-live="polite" to both buttons rendered in the copyLink and downloadPng
controls so screen readers announce their changing labels, including copied,
preparing, downloaded, and failure states.
frontend/src/pages/Leagues.tsx (1)

346-383: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Both sheet-based edit forms in this file are missing the required-field/pending-state guard their create-form siblings already have. One root cause: when league and week editing moved into CompetitionSheets during this refactor, the Save buttons didn't carry over the disabled pattern already used by LeagueCreate and LogWeekForm.

  • frontend/src/pages/Leagues.tsx#L346-L383: add disabled={!leagueForm.name.trim() || updateLeague.isPending} to the "Save changes" button, matching LeagueCreate's guard on Line 192.
  • frontend/src/pages/Leagues.tsx#L464-L481: add disabled={!weekForm.date || updateWeek.isPending} to the week-edit "Save" button, matching LogWeekForm's guard on Line 643.

Without these guards, a user can persist a league with a blank name or a week with no date, and can double-fire either mutation by clicking repeatedly while the request is in flight.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/Leagues.tsx` around lines 346 - 383, The edit Save button
in frontend/src/pages/Leagues.tsx lines 346-383 must disable when
leagueForm.name is blank after trimming or updateLeague.isPending; add the
equivalent guard to the week-edit Save button at lines 464-481 using
weekForm.date and updateWeek.isPending, matching the existing LeagueCreate and
LogWeekForm patterns.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/package.json`:
- Around line 5-11: Update the package.json engines.node constraint to require
Node 20.6.0 or newer, matching the minimum version that supports --import, and
change the test script to pass the src directory instead of the shell-expanded
src/*.test.mjs pattern. Preserve the existing test runner and import
configuration.

In `@backend/src/competitionArchive.pr15.test.mjs`:
- Around line 68-73: Update the CSV assertions in the export test to parse the
header, locate the active column by name, and compare each tournament row’s
value at that column instead of matching with cross-row regular expressions.
Preserve the existing status, content-type, and active/archived expectations,
using row splitting for the current comma-free fixture values.
- Around line 20-43: Extend the parameterized test in “league and tournament
archive routes are reversible and validate ids” with a successful DELETE request
for the valid resource ID. Assert the response succeeds, then verify the record
remains available through the archived-inclusive GET and has active equal to 0,
ensuring DELETE archives rather than removes it.

In `@backend/src/publicMetadata.pr15.test.mjs`:
- Around line 88-105: Extend the cases table in the test loop to cover the
remaining resolvePublicPageMetadata routes, prioritizing
/leagues/:id/recap/share with its null-week title path and adding /bowl,
/score/:id, /sessions/:id/share, /perfect-games/:id, and
/leagues/:id/week/:weekId/share. Replace the title and og:url RegExp assertions
with escaped-safe plain string containment checks, while preserving descriptive
failure messages and the attacker-host assertion.
- Around line 10-22: Replace the source-slicing, transpilation, and VM
evaluation in loadHtmlInjector with the real exported injectPublicMetadataHtml
obtained from the existing buildTestApp module harness. Update buildTestApp to
expose the injector, make the two pure-function tests async as needed, and
preserve their current assertions while exercising the shipped implementation
directly.

In `@backend/src/server.ts`:
- Around line 2434-2439: Confirm the frontend’s delete URL, then update the
DELETE handlers in backend/src/server.ts at lines 2434-2439 and 3296-3301
consistently: register the same league handler for /api/leagues/:id and
tournament handler for /api/tournaments/:id when the frontend uses the prefixed
paths; otherwise remove the unused bare routes. Preserve the existing
soft-archive behavior and validation.
- Around line 1376-1386: Update backend/src/server.ts lines 1376-1386 and
2620-2630: refactor selectLeagueRows and selectTournamentRows to use SQL
builders with two module-level prepared statements per helper, one including
archived records and one filtering them, then select the appropriate cached
statement based on the boolean argument instead of calling sqlite.prepare per
request.
- Around line 5441-5443: No code change is required; retain the boot-time
CACHED_INDEX_HTML caching and ensure deployment builds the frontend before
starting or restarting the server, with server restarts on frontend-only
redeploys so asset hashes refresh.
- Around line 5250-5257: Add a brief comment immediately above escapeHtml
documenting that it is intended only for text in <title> and values in
double-quoted attributes used by injectPublicMetadataHtml, and must not be
reused for unquoted attributes, URL attributes, inline scripts, or styles.
- Around line 5288-5313: Guard the final replacement in injectPublicMetadataHtml
so missing </head> markup still preserves the generated metadata. If
withoutManagedMeta.replace does not find a closing head tag, append the
generated tags instead of returning the title-stripped HTML; retain the existing
replacement behavior when </head> is present.

In `@backend/src/test-app.mjs`:
- Around line 40-48: Add a concise comment immediately before the process.env
assignments in buildTestApp documenting that the harness relies on calls
executing sequentially because environment variables are process-global and the
unique server.ts import re-reads them. Do not change the existing setup or
import behavior.
- Around line 50-63: Update the teardown callback around fastify.close,
sqlite.close, environment restoration, and rm so each cleanup step is isolated
with error handling and later steps still execute when an earlier step fails.
Preserve the existing order—close Fastify, close SQLite, restore all environment
variables, then remove root—and ensure cleanup failures remain associated with
teardown without preventing subsequent cleanup.

In `@frontend/src/features/competition/CompetitionUI.tsx`:
- Around line 75-118: Reset the archive mutation whenever
CompetitionArchiveSheet mounts so reopening it does not display a previous
error; accept a reset function in the component’s mutation prop and invoke it on
mount. Update useCompetitionArchive call sites in Leagues.tsx and
Tournaments.tsx to pass through the hook’s reset method alongside the existing
mutation state and mutate function.

In `@frontend/src/pages/LeagueRecapShare.tsx`:
- Around line 89-93: Update the not-found body links rendered by the
invalidId/not-found branches so they use the purpose-specific accessible name
“Browse leagues on BowlSense” instead of duplicating the PublicShell brand link:
apply this in frontend/src/pages/LeagueRecapShare.tsx lines 89-93,
frontend/src/pages/LeagueShare.tsx lines 90-94,
frontend/src/pages/LeagueWeekShare.tsx lines 77-81,
frontend/src/pages/SessionShare.tsx lines 122-126,
frontend/src/pages/ShareScore.tsx lines 123-127, and
frontend/src/pages/TournamentShare.tsx lines 169-173.

In `@frontend/src/pages/LeagueWeekShare.tsx`:
- Around line 91-99: The light PublicShell leaves dark-theme inline colors in
LeagueWeekShare. In frontend/src/pages/LeagueWeekShare.tsx lines 91-99, replace
the error-state white and red values with the shared muted and alert treatments;
in lines 119-124, replace the game-number white value, and update the scoreColor
default band near line 20 so the chip uses light-surface-appropriate background
and text colors.

In `@frontend/src/pages/PerfectGames.tsx`:
- Line 31: Update the marks guard in the PerfectGames rendering flow to treat
both null frame data and an empty result from parseFrameMarks as unavailable.
Use the parsed array’s length when deciding whether to render frame details, so
unparsable data displays the existing “Frame details were not recorded” fallback
instead of an empty grid.
- Around line 35-43: Update the copyGameShareLink helper in gameShare.ts to use
the shared copyText utility from the scoring feature instead of calling
navigator.clipboard.writeText directly, preserving its existing return behavior
so handleCopyLink in PerfectGames.tsx continues to work.

In `@frontend/src/pages/ShareScore.tsx`:
- Line 147: Replace the inline flex styling on the action-row div in ShareScore
with the shared public-share-actions class, matching the wrapper used by
SessionShare, LeagueShare, and TournamentShare; preserve the existing child
actions and rely on the shared stylesheet for spacing and wrapping.
- Around line 78-86: Extract the duplicated copyLink logic from ShareScore and
the other shared-page components into a reusable useCopyLink hook. Have the hook
own copied state, call copyText with window.location.href, apply one consistent
reset delay, handle failures consistently, and clean up its timer; update each
page to use the hook instead of defining its own handler and state.
- Around line 163-169: Resolve the duplicate copy-link controls in ShareScore by
choosing one owner for copy behavior: either remove the secondary button using
copyLink and copied, or change the shell action around the existing action
button to use the native share path while keeping copyLink in this button.
Ensure only one visible control provides copy confirmation for a single action.

In `@frontend/src/pages/Tournaments.tsx`:
- Around line 96-112: Reorder the route checks in the tournament page so the
explicit `/tournaments/new` branch is evaluated before the `if (id)` detail
branch. Preserve the existing create-sheet and detail rendering behavior,
ensuring the `"new"` path opens TournamentForm instead of TournamentDetail.
- Around line 506-515: Update the BowlingScorer edit flow in the onSave block to
pass the existing game state through g.frameData and g.splits, matching the
BowlingScorer usage in SessionDetail.tsx, so editing restores the saved game
before changes are applied.

In `@frontend/src/pages/TournamentShare.tsx`:
- Around line 207-211: Update the games list in TournamentShare around the
data.games.map/GameCard rendering to use the shared public-detail-row structure
and public theme tokens instead of the dark GameCard presentation. Refactor
GameCard’s rendered surface, text, borders, and background to match the light
public-detail-row styling used by LeagueShare, while preserving each game’s
existing content and key.
- Line 204: Update handleDownload in TournamentShare to track an in-flight state
and downloadError, reset the outcome before starting, validate the download
response via res.ok, and catch rejected requests instead of allowing unhandled
errors. Set downloaded only after a successful response, disable the button
while downloading, and render the failure state consistently with the existing
LeagueRecapShare pattern.

---

Outside diff comments:
In `@frontend/src/pages/LeagueRecap.tsx`:
- Around line 214-228: Add aria-live="polite" to both buttons rendered in the
copyLink and downloadPng controls so screen readers announce their changing
labels, including copied, preparing, downloaded, and failure states.

In `@frontend/src/pages/Leagues.tsx`:
- Around line 346-383: The edit Save button in frontend/src/pages/Leagues.tsx
lines 346-383 must disable when leagueForm.name is blank after trimming or
updateLeague.isPending; add the equivalent guard to the week-edit Save button at
lines 464-481 using weekForm.date and updateWeek.isPending, matching the
existing LeagueCreate and LogWeekForm patterns.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9763e9df-6f25-4252-b3ed-2c69b95c291a

📥 Commits

Reviewing files that changed from the base of the PR and between 88a487e and fa09f72.

⛔ Files ignored due to path filters (1)
  • backend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (41)
  • backend/package.json
  • backend/src/competitionArchive.pr15.test.mjs
  • backend/src/publicMetadata.pr15.test.mjs
  • backend/src/schema.ts
  • backend/src/server.ts
  • backend/src/test-app.mjs
  • frontend/package.json
  • frontend/src/components/BowlingScorer.tsx
  • frontend/src/components/PerfectGameCelebration.tsx
  • frontend/src/components/QuickAddGame.tsx
  • frontend/src/components/ShareCard.tsx
  • frontend/src/design/Icon.tsx
  • frontend/src/features/competition/CompetitionUI.tsx
  • frontend/src/features/competition/archive.ts
  • frontend/src/features/competition/competition.css
  • frontend/src/features/competition/competition.test.mjs
  • frontend/src/features/competition/publicMetadata.pr15.test.tsx
  • frontend/src/features/competition/publicMetadata.ts
  • frontend/src/features/scoring/BowlingScorer.test.tsx
  • frontend/src/features/scoring/frameMarks.test.tsx
  • frontend/src/features/scoring/frameMarks.ts
  • frontend/src/features/scoring/scoring.css
  • frontend/src/features/today/QuickLogSheet.tsx
  • frontend/src/pages/CompetitionArchiveOrder.pr15.test.tsx
  • frontend/src/pages/LeagueRecap.tsx
  • frontend/src/pages/LeagueRecapShare.tsx
  • frontend/src/pages/LeagueShare.tsx
  • frontend/src/pages/LeagueWeekShare.tsx
  • frontend/src/pages/Leagues.tsx
  • frontend/src/pages/PerfectGameShare.tsx
  • frontend/src/pages/PerfectGames.tsx
  • frontend/src/pages/PublicLeague.tsx
  • frontend/src/pages/PublicLeagueLeaderboard.tsx
  • frontend/src/pages/PublicProfile.tsx
  • frontend/src/pages/SessionDetail.tsx
  • frontend/src/pages/SessionShare.tsx
  • frontend/src/pages/ShareScore.tsx
  • frontend/src/pages/TournamentShare.tsx
  • frontend/src/pages/TournamentStandings.tsx
  • frontend/src/pages/TournamentStandingsShare.tsx
  • frontend/src/pages/Tournaments.tsx
💤 Files with no reviewable changes (1)
  • frontend/src/components/PerfectGameCelebration.tsx
📜 Review details
🧰 Additional context used
🪛 ast-grep (0.45.0)
backend/src/server.ts

[warning] 5250-5255: Avoid hand-rolled HTML escaping (replacing characters with HTML entities); use a vetted encoder/sanitizer such as DOMPurify or sanitize-html.
Context: value
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, '&#39;')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(manual-sanitization-typescript)


[warning] 5250-5254: Avoid hand-rolled HTML escaping (replacing characters with HTML entities); use a vetted encoder/sanitizer such as DOMPurify or sanitize-html.
Context: value
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(manual-sanitization-typescript)


[warning] 5250-5253: Avoid hand-rolled HTML escaping (replacing characters with HTML entities); use a vetted encoder/sanitizer such as DOMPurify or sanitize-html.
Context: value
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(manual-sanitization-typescript)


[warning] 5250-5252: Avoid hand-rolled HTML escaping (replacing characters with HTML entities); use a vetted encoder/sanitizer such as DOMPurify or sanitize-html.
Context: value
.replace(/&/g, '&')
.replace(/</g, '<')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(manual-sanitization-typescript)


[warning] 5250-5251: Avoid hand-rolled HTML escaping (replacing characters with HTML entities); use a vetted encoder/sanitizer such as DOMPurify or sanitize-html.
Context: value
.replace(/&/g, '&')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(manual-sanitization-typescript)

🔇 Additional comments (56)
frontend/src/pages/LeagueRecapShare.tsx (3)

62-87: LGTM!


101-106: LGTM!

Also applies to: 123-132


48-52: 🗄️ Data Integrity & Integration

No change needed. absoluteUrl('') returns undefined, so the empty image path is handled as “no image.”

frontend/src/pages/LeagueShare.tsx (2)

87-88: LGTM!

Also applies to: 96-133


29-38: 📐 Maintainability & Code Quality

No change needed.

The public custom property and shared public class definitions already exist in the public scorecard CSS scope.

frontend/src/pages/SessionShare.tsx (1)

129-134: 🎯 Functional Correctness

No change needed.

IconProps.name is IconName, and share is included in both IconName and the paths map.

			> Likely an incorrect or invalid review comment.
frontend/src/pages/PerfectGameShare.tsx (1)

4-6: LGTM!

Also applies to: 33-33, 43-57, 77-87, 108-133, 149-149, 184-184, 203-203, 221-221, 281-288

frontend/src/pages/PerfectGames.tsx (1)

7-7: LGTM!

Also applies to: 161-176, 186-194, 203-214, 231-231, 240-256, 376-376, 460-466

frontend/src/pages/PublicLeague.tsx (2)

4-6: LGTM!

Also applies to: 86-96, 216-243, 245-261, 277-284, 370-370, 385-396, 405-405, 449-449, 534-535


174-214: 🗄️ Data Integrity & Integration

No change needed.

/api/leagues/${leagueId}/stats, /standings, and /leaderboard are reachable without authentication, so the public league page can continue to call them.

frontend/src/pages/PublicLeagueLeaderboard.tsx (2)

1-6: LGTM!

Also applies to: 78-88, 96-104, 121-132, 200-201


61-66: 🗄️ Data Integrity & Integration

Accept the real /share payload shape instead of adding a defensive default

The backend returns { league, stats, weeks }, and those stats include average, so the nullish-default read is not needed here. Remove the optional/default handling so the type does not allow invalid API responses, and replace the incorrect PublicLeague.tsx ShareLeaguePayload declaration with the actual public share shape if it is still unused.

			> Likely an incorrect or invalid review comment.
frontend/src/pages/PublicProfile.tsx (2)

1-15: LGTM!

Also applies to: 38-60, 62-86


23-36: 🔒 Security & Privacy

No changes needed. /api/stats is anonymous and returns profileName and generatedAt.

frontend/src/pages/TournamentStandings.tsx (1)

1-7: LGTM!

Also applies to: 54-55, 81-110, 142-153, 171-191, 205-207, 217-217, 226-226, 263-265, 280-280

frontend/src/pages/TournamentStandingsShare.tsx (1)

1-6: LGTM!

Also applies to: 90-90, 104-104, 117-127, 146-165, 183-189, 207-207, 235-236, 323-323, 345-347

backend/src/server.ts (13)

14-17: LGTM!


30-34: LGTM!

Also applies to: 46-46


450-450: LGTM!

Also applies to: 528-537


1388-1417: LGTM!


1711-1711: LGTM!

Also applies to: 2775-2775, 3186-3186, 5018-5018


2421-2432: LGTM!


2632-2662: LGTM!

Also applies to: 2698-2698, 2754-2754, 2810-2810


3283-3294: LGTM!


3835-3837: LGTM!

Also applies to: 4274-4286


5259-5286: LGTM!


5315-5324: LGTM!

Also applies to: 5326-5438


5469-5472: LGTM!

Also applies to: 5489-5496


3822-3822: 🗄️ Data Integrity & Integration

No change needed.

The restore insertLeague statement includes the active column and @active placeholder.

backend/src/competitionArchive.pr15.test.mjs (2)

5-18: LGTM!


45-67: LGTM!

backend/src/test-app.mjs (1)

7-31: LGTM!

backend/src/publicMetadata.pr15.test.mjs (1)

24-75: LGTM!

backend/src/schema.ts (1)

92-92: 🗄️ Data Integrity & Integration

No change needed. leagues already declares active: integer('active').default(1), matching the runtime migration and tournaments.

frontend/src/components/BowlingScorer.tsx (1)

15-15: LGTM!

Also applies to: 36-40, 52-57, 94-101, 111-118, 137-141, 164-164, 182-182, 526-566

frontend/src/components/QuickAddGame.tsx (1)

75-75: LGTM!

frontend/src/components/ShareCard.tsx (1)

2-3: LGTM!

Also applies to: 41-41, 64-64, 81-81, 146-146, 197-231

frontend/src/design/Icon.tsx (1)

27-27: LGTM!

Also applies to: 67-67

frontend/src/features/scoring/frameMarks.ts (1)

1-66: LGTM!

frontend/src/features/scoring/scoring.css (1)

152-154: LGTM!

Also applies to: 163-163, 196-196

frontend/src/features/scoring/BowlingScorer.test.tsx (1)

6-9: LGTM!

Also applies to: 33-81

frontend/src/features/today/QuickLogSheet.tsx (1)

104-104: LGTM!

frontend/src/pages/SessionDetail.tsx (1)

262-262: LGTM!

Also applies to: 350-350

frontend/src/pages/CompetitionArchiveOrder.pr15.test.tsx (1)

1-134: 🎯 Functional Correctness

Past coupling comment looks resolved on the Leagues side; confirm Tournaments.tsx matches.

The earlier review flagged renderPage mixing window.history.replaceState with MemoryRouter.initialEntries, and recommended switching the pages to useLocation() plus resetting history in afterEach instead. Current renderPage (Line 21) no longer touches window.history, and afterEach (Line 14) resets it. Leagues.tsx now reads location.pathname via useLocation() for its create-route check, matching the fix.

This test file drives TournamentsPage through the same renderPage helper, so confirm Tournaments.tsx made the equivalent change (route detection through useLocation(), not window.location.pathname) to keep both pages consistent with this shared test harness.

🔎 Verification script
#!/bin/bash
fd -a 'Tournaments\.tsx$' frontend/src/pages | xargs -I{} rg -n "window\.location\.pathname|useLocation" {}
frontend/src/features/competition/CompetitionUI.tsx (1)

1-6: LGTM!

Also applies to: 8-33, 35-45, 47-73, 125-141, 143-179

frontend/src/features/competition/archive.ts (2)

5-9: LGTM!


11-34: 🎯 Functional Correctness

No change needed. The tournament detail query key uses queryKey: ['tournament', id] with the same id value passed to useCompetitionArchive, so this invalidation matches the active query.

			> Likely an incorrect or invalid review comment.
frontend/src/features/competition/competition.css (1)

1-108: LGTM!

frontend/src/features/competition/publicMetadata.ts (1)

1-94: LGTM!

frontend/package.json (1)

13-13: LGTM!

frontend/src/features/competition/competition.test.mjs (1)

1-69: LGTM!

frontend/src/features/competition/publicMetadata.pr15.test.tsx (1)

1-92: LGTM!

frontend/src/features/scoring/frameMarks.test.tsx (1)

1-25: LGTM!

frontend/src/pages/Leagues.tsx (2)

94-157: LGTM!

Also applies to: 265-265, 283-344, 394-411, 503-541


73-92: 🎯 Functional Correctness

No change needed.

frontend/src/main.tsx defines the static /leagues/new route before /leagues/:id, so LeaguesPage renders the create sheet when id is "new" and the later location.pathname === '/leagues/new' guard is unnecessary but not incorrect.

			> Likely an incorrect or invalid review comment.
frontend/src/pages/LeagueRecap.tsx (1)

29-38: LGTM!

Also applies to: 45-58, 60-85, 87-95, 123-123, 133-133, 170-172

Comment thread backend/package.json Resolved
Comment thread frontend/src/pages/ShareScore.tsx Resolved
Comment thread frontend/src/pages/Tournaments.tsx Resolved
Comment thread frontend/src/pages/Tournaments.tsx Resolved
Comment thread frontend/src/pages/TournamentShare.tsx Resolved
Comment thread frontend/src/pages/TournamentShare.tsx Resolved
@mkernsNCR

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Aug 1, 2026 •

Copy link
Copy Markdown
✅ Action performed

Full review finished.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 36 minutes.

@mkernsNCR
mkernsNCR merged commit 42de2e3 into main Aug 1, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[UI] Unify leagues, tournaments, achievements, and sharing

1 participant