Skip to content

refactor: render cohost dashboard routes through shared organizer page components - #506

Open
zacjones93 wants to merge 6 commits into
mainfrom
claude/gallant-gauss-8r3sfo
Open

refactor: render cohost dashboard routes through shared organizer page components#506
zacjones93 wants to merge 6 commits into
mainfrom
claude/gallant-gauss-8r3sfo

Conversation

@zacjones93

@zacjones93 zacjones93 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Cohost dashboard routes were forked copies of the organizer routes (~8,900 duplicated lines) and had drifted — cohosts were seeing outdated UI (e.g. a broken online submissions overview). This PR makes every cohost route render the same page components as its organizer counterpart, with co-host permissions preserved, so organizer changes flow to cohosts automatically.

Architecture

Shared chrome

  • CohostSidebar deleted; CompetitionSidebar now serves both dashboards. Each nav item declares an optional cohostPermission key (divisions, editEvents, scoringConfig, …); cohost mode filters by granted permissions, and items without a key (Invites, Broadcasts, Co-Hosts, Settings, Danger zone, Event divisions) remain organizer-only. New organizer nav items appear for cohosts as soon as a permission key is assigned.
  • New CompetitionDashboardShell rendered by both layout routes — cohosts now get the organizer breadcrumb and CompetitionHeader (with Edit / Go to Series hidden, since those target organizer-only routes).

Shared page bodies

  • All 21 cohost pages (overview, divisions, scoring, athletes, events list/detail, submissions list/review, results, schedule, volunteers, coupons, pricing, revenue, leaderboard preview, locations, waivers, sponsors, submission windows) now render shared page components extracted to organizer/$competitionId/-pages/.
  • Route files are thin shells. Cohost routes keep their own loaders unchanged (cohost server fns, competitionTeamId, FORBIDDEN-swallowing graceful degradation) and inject cohost-permissioned mutation callbacks, link targets, and permission flags as props — the existing shared-component callback pattern, lifted to page level.
  • Sync is enforced by the type system: a new required prop on a shared page breaks the cohost route at compile time instead of silently drifting.

Permission gates (preserved, now prop-driven)

  • editRegistrations gates the Add Registration button, row action menus/column, Form Questions tab, and the tab=registration-rulesathletes coercion
  • editEvents / revenue gate overview quick-action and revenue cards; pricing/revenue/coupons loader redirects unchanged
  • Organizer-only capabilities with no cohost server fn (refunds, manual score entry, video-link editing, series banners/links, registration detail links, sibling video tabs) are optional props that don't render for cohosts
  • No server functions or auth code changed; cohost fns still enforce permissions server-side via requireCohostPermission

Drift fixes cohosts gain

  • Modern online Submissions overview (the cohost copy was an outdated table whose "With Video" column always showed 0)
  • Per-registration submission grouping with review progress
  • Unified adjust-score form with validation (replaces the old percentage-slider penalty flow)
  • Optimistic mark/unmark-reviewed, series-aware affiliate names, and other accumulated organizer improvements

Impact

  • Cohost route tree: 8,907 → 3,520 lines (loaders + wiring only); page bodies exist once (~8,800 lines in -pages/)
  • Net: 70 files changed, +10,411 / −14,298

Verification

  • pnpm type-check clean repo-wide
  • All 2,811 tests pass (122 files)
  • Biome: zero errors on all changed files (remaining warnings are the pre-existing loaderData! assertion pattern)
  • lat check passes; lat.md/ docs updated (new "Shared Page Components" section, layout/sidebar/architecture docs)

https://claude.ai/code/session_01U1fRZH8SG5gmo9Nt49maQZ


Generated by Claude Code


Summary by cubic

Render all cohost dashboard routes through the same page components as organizer routes, with permissions applied, to keep UIs in sync and remove drift. Unifies the dashboard shell (sidebar, breadcrumb, header), reduces duplicated code, fixes cohost submissions grouping and chrome issues, and ports check-in/event-grouping/empty-state updates into the shared pages.

  • Refactors

    • Cohost routes now render shared organizer page bodies from organizer/$competitionId/-pages/, while keeping cohost loaders and permissioned callbacks.
    • Added CompetitionDashboardShell used by both layouts; merged CohostSidebar into CompetitionSidebar with permission-based filtering and organizer-only actions hidden via showOrganizerActions.
    • Permission gates are preserved and prop-driven; organizer-only features (e.g., refunds, manual score entry, video-link editing, registration detail links) don’t render for cohosts. No auth changes.
    • Cohosts gain modern submissions overview, per-registration grouping, unified adjust-score form, optimistic review toggles, and the overview revenue “Details” link when revenue is granted (targets cohost revenue). Cohost pricing loader now runs in parallel to match organizer.
    • Ported day-of check-in, event grouping, and unified empty states into shared pages: check-in breadcrumb lives in the shell (no cohost check-in route), Overview adds organizer-only “Go to Check-In,” Athletes shows “Checked In” (incl. CSV/mobile), Results uses unified empty states; cohost events keeps its grouping override.
    • Duplication trimmed: cohost route tree 8,907 → 3,520 lines; page bodies centralized once under -pages/.
  • Bug Fixes

    • Submissions list: use real videoIndex and division teamSize for stable grouped primary-row selection; partial-partner-videos badge now correct.
    • Submissions loader fetches all statuses and filters client-side so registrationAllReviewed is accurate under filters.
    • Submission review: validate back-link is internal; reset video tab and optimistic state when submissionId changes; cohost verifyScore fails fast if round adjustments are supplied.
    • Review notes: compute modifier-key label after mount to avoid SSR/OS mismatches.
    • Breadcrumb: composite React key prevents collisions; supports custom root for cohost non-linked label.
    • Router Links replace full-page navigations for CompetitionHeader Edit and Overview “Configure registration.”
    • Coupon copy-link now guards missing Clipboard API and shows an error toast on failure.
    • LocationsPage: consolidated duplicate venue invalidation handlers.
    • Typing fixes: narrow submission-windows workout map param to the union element.

Written for commit e88cf60. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Refactor
    • Unified organizer and cohost competition management interfaces by consolidating page components and standardizing layouts across both user types.
    • Simplified competition sidebar with permission-based navigation filtering for cohosts.
    • Streamlined page structure with new dashboard shell wrapper for consistent breadcrumb and header behavior.

claude added 2 commits June 11, 2026 06:00
… pattern

- Merge CohostSidebar into CompetitionSidebar: nav items declare their
  cohost permission key; cohost mode filters by granted permissions
- Add CompetitionDashboardShell shared by both layout routes (breadcrumb +
  CompetitionHeader now render for cohosts, with organizer-only actions hidden)
- Add -pages/ directory for shared page bodies; convert scoring and
  divisions as the pattern exemplars (cohost routes inject cohost server-fn
  overrides, organizer-only sections are optional props)

https://claude.ai/code/session_01U1fRZH8SG5gmo9Nt49maQZ
…e components

Every cohost competition page now renders the same page-body component as
its organizer counterpart (organizer/$competitionId/-pages/), so organizer
UI changes flow to cohosts automatically. Cohost routes keep their own
loaders (cohost server fns, FORBIDDEN graceful degradation) and inject
cohost-permissioned mutation callbacks, link targets, and permission flags
as props.

- Extract shared page components for overview, divisions, scoring, athletes,
  events (list/detail), submissions (list/review), results, schedule,
  volunteers, coupons, pricing, revenue, leaderboard preview, locations,
  waivers, sponsors, and submission windows
- Permission gates preserved: editRegistrations hides registration actions
  and form-questions tab; editEvents/revenue gate overview cards; loader
  redirects for pricing/revenue/coupons unchanged; organizer-only sections
  (series banners, refunds, manual score entry, video link editing,
  registration detail links) hidden for cohosts via optional props
- Cohosts gain previously drifted organizer improvements: modern online
  submissions overview, per-registration submission grouping, unified
  adjust-score form, optimistic review toggling
- Cohost route tree shrinks 8,907 -> 3,520 lines; page bodies live once

https://claude.ai/code/session_01U1fRZH8SG5gmo9Nt49maQZ
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR consolidates organizer and cohost competition dashboard functionality by extracting 16 shared page components and updating both route trees to use them. A new CompetitionDashboardShell unifies layout composition, while cohost routes delegate rendering to shared pages via thin wrappers that inject permission-gated server-function overrides. Sidebar and breadcrumb components are updated to support dual-mode rendering.

Changes

Competition Dashboard Shell and Navigation Foundation

Layer / File(s) Summary
Shell wrapper and navigation mode switching
apps/wodsmith-start/src/components/competition-dashboard-shell.tsx, competition-header.tsx, competition-sidebar.tsx, organizer-breadcrumb.tsx
New CompetitionDashboardShell wraps both organizer and cohost layouts with breadcrumb translation for online/in-person competitions. CompetitionSidebar gains optional cohost mode with permission-filtered nav groups and cohost-aware header. CompetitionHeader adds showOrganizerActions flag to hide organizer-only controls. OrganizerBreadcrumb accepts optional root segment for cohost labeling.
Organizer and cohost parent route layout refactoring
apps/wodsmith-start/src/routes/compete/organizer/$competitionId.tsx, compete/cohost/$competitionId.tsx
Both parent routes switch from inline composition to CompetitionDashboardShell, removing useMatches breadcrumb logic and delegating sidebar/breadcrumb/header to the shell.

Shared Page Components for Organizer and Cohost Dashboards

Layer / File(s) Summary
Overview, divisions, and events pages with series mapping
apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/overview-page.tsx, divisions-page.tsx, events/events-page.tsx
New OverviewPage composes publishing controls, registration status, and revenue cards with cohost-aware permission gating. DivisionsPage includes capacity settings and series-mapping banner. EventsPage passes series template data to OrganizerEventManager.
Event detail and submission review pages with tabbed editing and verification
apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/events/event-detail-page.tsx, events/submission-review-page.tsx, events/submission-review/review-notes.tsx, events/submission-review/verification-controls.tsx
New EventDetailPage dispatches to single-event or parent-event editor (with child tabs and shared judging sheets). SubmissionReviewPage renders community votes, multi-video tabs, verification controls, and review notes. ReviewNoteForm/ReviewNotesList support timestamped, movement-tagged notes. VerificationControls handles score verification, penalty adjustment, and audit logging.
Submissions list and results pages
apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/events/submissions-page.tsx, results-page.tsx
SubmissionsPage groups submissions by registration, applies client-side filtering/sorting, and renders submission groups with review progress. ResultsPage switches between online submissions overview (with event grouping and submission counts) and in-person results entry (with division publish/unpublish and score form).
Schedule, volunteers, and pricing pages
apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/schedule-page.tsx, volunteers-page.tsx, pricing-page.tsx
SchedulePage wraps SchedulePageClient with optional cohost heat-schedule overrides. VolunteersPage renders tabbed roster/shifts/schedule/questions with optional override callbacks for mutations. PricingPage conditionally shows Stripe connection prompt or pricing form with optional cohost/team fee settings.
Coupons, locations, leaderboard, revenue, scoring, sponsors, submission windows, and waivers pages
apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/coupons-page.tsx, locations-page.tsx, leaderboard-preview-page.tsx, revenue-page.tsx, scoring-page.tsx, sponsors-page.tsx, submission-windows-page.tsx, waivers-page.tsx
Lightweight shared page wrappers for coupon management (with status badges and copy-link), venue management (with router invalidation on change), leaderboard preview (with cohost alert variant), revenue reporting, scoring configuration, sponsor management, submission window normalization, and waiver rendering.

Cohost Route Refactoring to Delegate to Shared Pages

Layer / File(s) Summary
Cohost athletes, divisions, and events routes with overrides
apps/wodsmith-start/src/routes/compete/cohost/$competitionId/athletes.tsx, divisions.tsx, events/index.tsx
Cohost routes now render shared pages with cohost-specific DivisionManagerOverrides, ResourceOverrides, and EventManagerOverrides that inject competitionTeamId into mutations. Athletes route disables registration transfer with cohost-specific messaging.
Cohost event-detail and submission routes with complex overrides
apps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/$eventId/index.tsx, events/$eventId/submissions/$submissionId.tsx, events/$eventId/submissions/index.tsx
Cohost event-detail delegates to shared EventDetailPage with cohost resource/judging/publishing overrides. Submission-review route wires cohost note/verification overrides. Submissions-list route adapts cohost submission rows to organizer shape, filters client-side by status.
Cohost overview, results, and schedule routes
apps/wodsmith-start/src/routes/compete/cohost/$competitionId/index.tsx, results.tsx, schedule.tsx
Cohost overview delegates to shared OverviewPage with cohost publishing callbacks. Results route computes online submission counts client-side and wires cohost score-save/publish callbacks. Schedule route injects heatScheduleOverrides for cohost mutations.
Cohost pricing, volunteers, and remaining feature routes
apps/wodsmith-start/src/routes/compete/cohost/$competitionId/pricing.tsx, volunteers.tsx, scoring.tsx, sponsors.tsx, waivers.tsx, coupons.tsx, submission-windows.tsx, leaderboard-preview.tsx, locations.tsx, revenue.tsx
Cohost routes delegate to shared pages (pricing, volunteers, scoring, sponsors, waivers, coupons, submission-windows, leaderboard, locations, revenue) while wiring cohost server-function overrides for mutations. Each route constructs typed override objects matching the shared page's callback interface.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • wodsmith/thewodapp#313: Adds scalingGroupTitle and "Change Scaling Group" behavior to OrganizerDivisionManager, which the shared DivisionsPage threads through as props.
  • wodsmith/thewodapp#359: Introduces the dedicated cohost route-tree and original CohostSidebar that this PR replaces by switching to CompetitionDashboardShell.
  • wodsmith/thewodapp#353: Introduces parent-event resources/judging-sheet behavior that the new EventDetailPage directly implements.

Suggested reviewers

  • theianjones

Poem

🐰 The cohost now shares the organizer's stage,
No duplicated code to clutter the page,
With shell-wrapped dashboards and overrides in place,
Both routes dance together with newfound grace,
A rabbit refactors with careful delight—
One source of truth, shining bright! ✨

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/gallant-gauss-8r3sfo

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0548fd8610

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +179 to +180
verifyScore: async ({ adjustedRoundScores: _unsupported, ...input }) =>
verifyScore({ data: { ...input, competitionTeamId } }),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restore cohost log edits to use update endpoint

When a cohost clicks the pencil on an existing verification/audit log, the shared VerificationControls edit path now calls this verifyScore override. In this route that maps to cohostVerifySubmissionScoreFn, whereas the previous cohost page used cohostUpdateVerificationLogFn for the same UI. That means editing a log applies a fresh score adjustment/new audit entry instead of updating the existing log's penalty/no-rep fields, unexpectedly changing the athlete's score/history; keep a dedicated cohost update-log override or hide/change that edit action.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is intentional organizer parity rather than a regression: the organizer page has always routed audit-log edits through verifySubmissionScoreFn with action: "adjust" (the pre-refactor organizer handleUpdate did exactly this), so the shared VerificationControls reproduces the organizer flow. The old cohost cohostUpdateVerificationLogFn path can't back the new shared edit form — it only updates penalty/no-rep fields and can't persist the form's score value or cap status. cohostVerifySubmissionScoreFn enforces the same results permission server-side, and the adjust flow keeps the prior entry in the audit history (superseded by the new one), matching what organizers see when they edit a log.


Generated by Claude Code

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

10 issues found across 70 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/athletes-page.tsx">

<violation number="1">
P2: Question filters ignore invited teammate answers, causing pending/accepted invite rows to be filtered out incorrectly.</violation>
</file>

<file name="apps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/$eventId/submissions/$submissionId.tsx">

<violation number="1" location="apps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/$eventId/submissions/$submissionId.tsx:179">
P1: Audit-log edits should use a dedicated update-log mutation instead of `verifyScore`; routing edit actions through `verifyScore` can create a new adjustment entry and alter score history rather than updating the existing log fields.</violation>
</file>

<file name="apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/leaderboard-preview-page.tsx">

<violation number="1" location="apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/leaderboard-preview-page.tsx:21">
P3: Leaderboard search schema is duplicated instead of being shared, creating drift risk between preview and public leaderboard filters.</violation>

<violation number="2" location="apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/leaderboard-preview-page.tsx:22">
P2: `division` accepts empty strings, which can short-circuit leaderboard loading for `?division=` URLs.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

const overrides: SubmissionReviewOverrides = {
// The cohost fn has no multi-round support, but cohost submission data
// never includes round scores so the multi-round adjust path never runs.
verifyScore: async ({ adjustedRoundScores: _unsupported, ...input }) =>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Audit-log edits should use a dedicated update-log mutation instead of verifyScore; routing edit actions through verifyScore can create a new adjustment entry and alter score history rather than updating the existing log fields.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/$eventId/submissions/$submissionId.tsx, line 179:

<comment>Audit-log edits should use a dedicated update-log mutation instead of `verifyScore`; routing edit actions through `verifyScore` can create a new adjustment entry and alter score history rather than updating the existing log fields.</comment>

<file context>
@@ -1568,233 +164,52 @@ function SubmissionDetailPage() {
+  const overrides: SubmissionReviewOverrides = {
+    // The cohost fn has no multi-round support, but cohost submission data
+    // never includes round scores so the multi-round adjust path never runs.
+    verifyScore: async ({ adjustedRoundScores: _unsupported, ...input }) =>
+      verifyScore({ data: { ...input, competitionTeamId } }),
+    deleteVerificationLog: async (input) =>
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Checked — intentional, not a bug: the organizer flow has always routed audit-log edits through the verify fn with action: "adjust" (pre-refactor organizer code did the same), so the shared component matches the organizer source of truth. A dedicated update-log mutation can't back the shared edit form because cohostUpdateVerificationLogFn only updates penalty/no-rep fields and can't persist the form's score value or cap status. The adjust flow preserves audit history (the prior entry remains, superseded), identical to organizer behavior.


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the feedback! I've saved this as a new learning to improve future reviews.

@@ -0,0 +1,2329 @@
/**

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Question filters ignore invited teammate answers, causing pending/accepted invite rows to be filtered out incorrectly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/athletes-page.tsx:

<comment>Question filters ignore invited teammate answers, causing pending/accepted invite rows to be filtered out incorrectly.</comment>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Pre-existing organizer behavior carried over verbatim — the question-filter predicate (getAnswersForUser over answersByRegistration) is byte-identical to the pre-refactor athletes/index.tsx, so invited-teammate pendingAnswers were never consulted by filters before this PR either. Worth a follow-up issue, but out of scope for this refactor (whose contract is organizer-behavior preservation).


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Got it — thanks for the clarification. I’ll treat this as out of scope for this refactor.

// Match the public leaderboard search schema so existing filters work.
// Shared by both route shells' validateSearch.
export const leaderboardPreviewSearchSchema = z.object({
division: z.string().optional(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: division accepts empty strings, which can short-circuit leaderboard loading for ?division= URLs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/leaderboard-preview-page.tsx, line 22:

<comment>`division` accepts empty strings, which can short-circuit leaderboard loading for `?division=` URLs.</comment>

<file context>
@@ -0,0 +1,66 @@
+// Match the public leaderboard search schema so existing filters work.
+// Shared by both route shells' validateSearch.
+export const leaderboardPreviewSearchSchema = z.object({
+  division: z.string().optional(),
+  event: z.string().optional(),
+  affiliate: z.string().optional(),
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Pre-existing: this schema is byte-identical to the one both route files contained before the refactor, and it deliberately mirrors the public leaderboard's schema (division: z.string().optional() in compete/$slug/leaderboard.tsx), which has the same empty-string acceptance. Tightening it here would diverge the preview from the public leaderboard; if ?division= handling should change, it should change in both schemas as a separate fix.


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Got it — this is intentional parity with the public leaderboard schema, so I’ll avoid flagging it here.


// Match the public leaderboard search schema so existing filters work.
// Shared by both route shells' validateSearch.
export const leaderboardPreviewSearchSchema = z.object({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: Leaderboard search schema is duplicated instead of being shared, creating drift risk between preview and public leaderboard filters.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/leaderboard-preview-page.tsx, line 21:

<comment>Leaderboard search schema is duplicated instead of being shared, creating drift risk between preview and public leaderboard filters.</comment>

<file context>
@@ -0,0 +1,66 @@
+
+// Match the public leaderboard search schema so existing filters work.
+// Shared by both route shells' validateSearch.
+export const leaderboardPreviewSearchSchema = z.object({
+  division: z.string().optional(),
+  event: z.string().optional(),
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Partially addressed by this PR: the schema previously lived in two route files (organizer + cohost preview) and now has a single source imported by both. Sharing it with the public leaderboard too would mean exporting from (or importing into) the compete/$slug/leaderboard.tsx route module — better done as a small follow-up that lifts the public schema into a shared module both can import.


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Got it — I’ll treat this as intentional parity and avoid flagging it here.

Comment thread apps/wodsmith-start/src/components/competition-header.tsx Outdated
Comment thread apps/wodsmith-start/src/components/competition-dashboard-shell.tsx
claude added 3 commits June 11, 2026 06:56
… loader

- Overview revenue card's Details button now renders for cohosts with the
  revenue permission, targeting the cohost revenue route via revenueLinkTo
- Cohost pricing loader fetches scaling group, fee config, and team fee
  settings in parallel, matching the organizer loader

https://claude.ai/code/session_01U1fRZH8SG5gmo9Nt49maQZ
… chrome

- cohostGetOrganizerSubmissionsFn now returns the real videoIndex column and
  division teamSize, so grouped primary-row selection no longer depends on
  query ordering and the partial-partner-videos badge works for cohosts
- Cohost submissions loader fetches all statuses and filters client-side so
  registrationAllReviewed is computed from the complete set (a partially
  reviewed team registration no longer shows as reviewed under status filters)
- CompetitionHeader Edit action uses router Link instead of a full-page <a>
- Coupon copy-link handles clipboard failures with an error toast
- Consolidate identical venue invalidation handlers in LocationsPage

https://claude.ai/code/session_01U1fRZH8SG5gmo9Nt49maQZ
…to shared pages

Resolves conflicts from main's day-of check-in (#430), event grouping (#504),
and unified empty states (#501) landing on the pre-refactor page bodies:
- check-in breadcrumb label moved to CompetitionDashboardShell; sidebar item
  is organizer-only (no cohost check-in route)
- Go to Check-In button ported to shared OverviewPage, organizer-only gated
- Checked In column/CSV/mobile detail ported to shared AthletesPage (cohost
  registration rows already carry checkedInAt)
- OrganizerEmptyState swaps ported to shared ResultsPage
- cohost events route keeps main's cohostGroupEventsFn override; the grouping
  UI itself arrived via the shared OrganizerEventManager with no extra work

https://claude.ai/code/session_01U1fRZH8SG5gmo9Nt49maQZ

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 17

Caution

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

⚠️ Outside diff range comments (2)
apps/wodsmith-start/src/components/organizer-breadcrumb.tsx (1)

32-37: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use a unique key for breadcrumb segments.

Line 36 uses segment.label as the React key, which can collide when labels repeat (e.g., a competition named “Organizer”), causing unstable list reconciliation.

Suggested fix
-            <Fragment key={segment.label}>
+            <Fragment key={`${segment.href ?? "nolink"}-${index}-${segment.label}`}>
🤖 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 `@apps/wodsmith-start/src/components/organizer-breadcrumb.tsx` around lines 32
- 37, The breadcrumb list uses segment.label as the React key in the
allSegments.map callback (inside organizer-breadcrumb.tsx), which can collide
for repeated labels; change the key to a stable unique identifier (e.g., use
segment.id if available) or fall back to a deterministic composite (like
`${segment.id || segment.label}-${index}`) so each Fragment key is unique and
stable across renders; update the Fragment key reference where the map is
implemented to use that unique value.
apps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/$eventId/submissions/$submissionId.tsx (1)

53-59: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only swallow the explicit FORBIDDEN path here.

This call populates the whole page. catch(() => ({ submission: null })) turns permission regressions, backend failures, and parsing bugs into a fake “Submission not found”, which makes the route lie about the failure mode. As per PR objectives, these wrappers are supposed to preserve FORBIDDEN graceful degradation, not mask every loader failure.

Proposed fix
-    const reviewResult = await cohostGetOrganizerSubmissionDetailFn({
+    const reviewResult = await cohostGetOrganizerSubmissionDetailFn({
       data: {
         competitionTeamId,
         submissionId: params.submissionId,
         competitionId: params.competitionId,
       },
-    }).catch(() => ({ submission: null }))
+    }).catch((error) => {
+      if (isForbiddenError(error)) {
+        return { submission: null }
+      }
+      throw error
+    })
// Reuse the same forbidden classifier your existing cohost loaders use.
function isForbiddenError(error: unknown) {
  return error instanceof Error && error.message.startsWith("FORBIDDEN")
}
🤖 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
`@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/events/$eventId/submissions/$submissionId.tsx
around lines 53 - 59, The current catch on
cohostGetOrganizerSubmissionDetailFn(swallowing all errors) masks non-permission
failures; replace it so only explicit FORBIDDEN errors are translated to {
submission: null } by detecting them (reuse or add an isForbiddenError(error)
helper that checks error instanceof Error &&
error.message.startsWith("FORBIDDEN")), and rethrow any other errors so
reviewResult is assigned only for forbidden cases and other failures surface
normally; keep the call site (reviewResult assignment) and function name
cohostGetOrganizerSubmissionDetailFn unchanged.
🧹 Nitpick comments (14)
apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/overview-page.tsx (3)

112-146: 💤 Low value

Consider consolidating date formatting with existing utilities.

The formatDateTime helper duplicates YYYY-MM-DD parsing logic already present in formatUTCDateFull and formatDateStringFull from @/utils/date-utils. Consider refactoring to reuse existing utilities or extending them to handle timestamps with time display.

🤖 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
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/overview-page.tsx
around lines 112 - 146, The local helper formatDateTime duplicates YYYY-MM-DD
parsing already implemented in formatUTCDateFull and formatDateStringFull from
`@/utils/date-utils`; replace the inline parsing/formatting in formatDateTime with
calls to those utilities (or extend formatUTCDateFull/formatDateStringFull to
accept an option to include time) and use that shared utility to handle both
date-only strings and Date/timestamp inputs so formatting logic is consolidated
and consistent.

344-352: 💤 Low value

Type assertion on athletesLinkTo could be avoided with stricter typing.

The as string cast at line 345 is safe due to the default value, but consider typing athletesLinkTo prop more strictly to match the expected route paths or use a const assertion on the default value.

🤖 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
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/overview-page.tsx
around lines 344 - 352, The cast "as string" on athletesLinkTo should be removed
by tightening its type where it's declared: ensure the prop or variable
athletesLinkTo is explicitly typed as string (or the exact route path string
union) or give its default value a const assertion so its type is string; then
pass athletesLinkTo directly to the Link component (replace the assertion).
Update the prop/interface or the variable declaration where athletesLinkTo is
defined to reflect this stricter string type (referencing the athletesLinkTo
identifier and the Link usage in overview-page.tsx).

310-316: ⚡ Quick win

Prefer typed <Link> over raw anchor for internal navigation.

Replace the anchor tag with a <Link> component for type-safe routing and better integration with TanStack Router.

♻️ Suggested refactor
-              {isOrganizer && (
-                <a href={`/compete/organizer/${competition.id}/edit`}>
-                  <Button variant="outline" size="sm" className="mt-2">
-                    Configure registration
-                  </Button>
-                </a>
-              )}
+              {isOrganizer && (
+                <Link
+                  to="/compete/organizer/$competitionId/edit"
+                  params={{ competitionId: competition.id }}
+                >
+                  <Button variant="outline" size="sm" className="mt-2">
+                    Configure registration
+                  </Button>
+                </Link>
+              )}
🤖 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
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/overview-page.tsx
around lines 310 - 316, The JSX uses a raw <a href=...> for internal navigation
(inside the isOrganizer conditional rendering); replace the anchor with the
framework Link component (e.g., <Link
to={`/compete/organizer/${competition.id}/edit`}> ) so routing is type-safe and
integrated with TanStack Router, and add the appropriate import for Link from
the TanStack Router package used in the project; keep the Button as the child of
Link and otherwise preserve isOrganizer and competition.id usage.
apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/coupons-page.tsx (2)

139-179: ⚖️ Poor tradeoff

Consider adding Zod validation for the coupon creation form.

The form uses manual validation (checking NaN and <= 0) but doesn't leverage Zod schema validation. Per coding guidelines, forms should use React Hook Form with Zod validation for consistent validation and better type safety.

🤖 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
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/coupons-page.tsx
around lines 139 - 179, Replace the manual checks in handleCreate with Zod-based
validation: define a Zod schema (e.g., CouponSchema) that validates
amountDollars (transformed to integer cents > 0), optional codeOverride (trimmed
string), optional maxRedemptions (optional positive integer), and optional
expiresAt (optional date string); use react-hook-form with zodResolver to drive
the form and get typed values instead of reading local state, call schema.parse
or form.handleSubmit to validate before constructing couponInput, and on
validation errors surface them via form.setError or toast; keep the existing
call sites (overrides.createCoupon or createCoupon) and only pass the
validated/parsed couponInput (amountOffCents, code, maxRedemptions, expiresAt).

Source: Coding guidelines


197-207: ⚡ Quick win

Improve clipboard error handling for permission denial.

The clipboard writeText operation currently catches rejections with a generic error toast. However, it won't handle cases where the clipboard API is unavailable (e.g., non-HTTPS contexts) or when permissions are explicitly denied by the browser.

🔧 Add availability check
 function handleCopyLink(code: string) {
   const url = `${appUrl}/compete/${slug}?coupon=${encodeURIComponent(code)}`
+  if (!navigator.clipboard) {
+    toast.error("Clipboard not available")
+    return
+  }
   navigator.clipboard.writeText(url).then(
🤖 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
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/coupons-page.tsx
around lines 197 - 207, handleCopyLink currently assumes
navigator.clipboard.writeText will succeed and shows only a generic error;
update handleCopyLink to first check for navigator.clipboard availability and,
if the Permissions API exists, query "clipboard-write" before attempting copy,
then perform the copy with async/await and a try/catch around
navigator.clipboard.writeText; on catch, inspect the error (e.g.,
DOMException.name or message) and call toast.error with a specific message for
permission denial (e.g., "Permission denied to access clipboard") and a
different message for unsupported environments (e.g., "Clipboard not available —
try HTTPS or use the manual copy fallback"); if clipboard is unavailable, fall
back to a manual copy approach (temporary textarea +
document.execCommand('copy')) and handle success/error similarly, referencing
the function name handleCopyLink, variables appUrl and slug, and the
navigator.clipboard.writeText call to locate where to change the code.
apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/volunteers-page.tsx (1)

290-292: ⚡ Quick win

Validate defaultLaneShiftPattern before type assertion.

The type assertion as "stay" | "shift_right" assumes the database value matches one of these two literals. If the database contains an unexpected value (null, empty string, or another pattern), the assertion will succeed at compile time but pass an invalid value at runtime.

🛡️ Suggested validation
  competitionDefaultPattern={
-   (competition.defaultLaneShiftPattern as "stay" | "shift_right") ??
-   "shift_right"
+   competition.defaultLaneShiftPattern === "stay" || 
+   competition.defaultLaneShiftPattern === "shift_right"
+     ? competition.defaultLaneShiftPattern
+     : "shift_right"
  }
🤖 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
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/volunteers-page.tsx
around lines 290 - 292, The code unsafely asserts
competition.defaultLaneShiftPattern as "stay" | "shift_right"; instead validate
the runtime value before using it: read competition.defaultLaneShiftPattern,
check whether it equals "stay" or "shift_right" (type guard), and only pass that
validated value into competitionDefaultPattern, otherwise fall back to
"shift_right"; update the usage around competitionDefaultPattern to use this
validated variable so unexpected/null/empty DB values never get asserted
through.
apps/wodsmith-start/src/routes/compete/cohost/$competitionId/locations.tsx (1)

51-60: ⚡ Quick win

Consider typing override function parameters for better type safety.

The override callbacks use args: { data: any }, which bypasses TypeScript's type checking. Since the cohost server functions have typed inputs, explicitly typing these parameters would catch mismatches at compile time.

♻️ Example with typed parameters
const venueOverrides: VenueManagerOverrides = {
  createVenueFn: (args: Parameters<typeof cohostCreateVenueFn>[0]) =>
    cohostCreateVenueFn({ data: { ...args.data, competitionTeamId } }),
  updateVenueFn: (args: Parameters<typeof cohostUpdateVenueFn>[0]) =>
    cohostUpdateVenueFn({ data: { ...args.data, competitionTeamId } }),
  // ... etc
}

This approach preserves the input types from the cohost server functions.

🤖 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 `@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/locations.tsx
around lines 51 - 60, The override callbacks in venueOverrides use args: { data:
any } which loses type safety; update the parameter types to mirror the cohost
handlers by using the actual parameter types of cohostCreateVenueFn,
cohostUpdateVenueFn, cohostDeleteVenueFn, and cohostGetVenueHeatCountFn (for
example via Parameters<typeof cohostCreateVenueFn>[0] etc.) so each override
(createVenueFn, updateVenueFn, deleteVenueFn, getVenueHeatCountFn) accepts the
correct typed args and you can still return/invoke the cohost* functions with
the merged data including competitionTeamId.
apps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/index.tsx (1)

110-164: ⚡ Quick win

Don't cast away the shared override contract.

This adapter is now the compile-time sync point between the cohost wrapper and EventsPage, but Record<string, unknown> plus as any removes the checks that would catch signature drift here before runtime. Please type overrides against the shared page's override interface (or prop type) instead of erasing it.

As per PR objectives, the shared-page extraction is supposed to keep organizer/cohost routes in sync at compile time.

🤖 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
`@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/events/index.tsx
around lines 110 - 164, The overrides object is currently cast to any and uses
Record<string, unknown>, which erases compile-time checks; import and use the
shared EventsPage override interface (the prop type/interface exported by the
shared page, e.g., EventsPageOverrides or the EventsPageProps overrides type)
and type the overrides variable with it (e.g., const overrides:
EventsPageOverrides = useMemo(...)). Remove the Record<string, unknown> and as
any casts on createWorkoutFn, removeWorkoutFn, reorderEventsFn, and
groupEventsFn, and instead type each args parameter to the exact override
callback types from the shared interface (or the specific argument types like
CreateWorkoutArgs, RemoveWorkoutArgs, etc.) so the compiler enforces the correct
shape when calling cohostCreateWorkoutFn, cohostRemoveWorkoutFn,
cohostReorderEventsFn, and cohostGroupEventsFn.
apps/wodsmith-start/src/routes/compete/organizer/$competitionId/events/$eventId.tsx (1)

1-15: ⚡ Quick win

Add the required @lat anchors to these new route wrappers.

These TSX files add or retarget substantial organizer/cohost page wiring, but they don't include the repo-standard // @lat: [[section-id]] anchor. That makes the new shared-route delegation harder to trace back to the domain concepts the refactor is organizing around.

As per coding guidelines, **/*.{ts,tsx,js,jsx,rs,go,c,h,py}: Use // @lat: [[section-id]] code reference comments in TypeScript/JavaScript/Rust/Go/C files and # @lat: [[section-id]] in Python files to tie source code to concepts.

🤖 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
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/events/$eventId.tsx
around lines 1 - 15, The file is missing the repo-standard source anchor
comment; add a top-of-file LAT anchor comment using the exact token format `//
`@lat`: [[section-id]]` (choose the appropriate section-id for the
competition/event organizer routing concept) immediately above the file header
or first import so the route wrapper (symbols: createFileRoute, Outlet) is
annotated; ensure the comment uses `//` style and matches other repo anchors so
tracing tools pick it up.

Source: Coding guidelines

apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/events/submission-review-page.tsx (1)

1-75: ⚡ Quick win

Add the required // @lat`` anchors across the extracted shared pages.

All five new TSX modules are missing the section-id markers the repo uses to tie code back to concepts, so the extracted organizer/cohost surface is not traceable through the lat tooling. As per coding guidelines, **/*.{ts,tsx,js,jsx,rs,go,c,h,py}: Use // @lat: [[section-id]] code reference comments in TypeScript/JavaScript/Rust/Go/C files and # @lat: [[section-id]] in Python files.

🤖 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
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/events/submission-review-page.tsx
around lines 1 - 75, This file is missing the required // `@lat` anchors that tie
extracted shared pages to repo concepts; add a top-of-file anchor comment like
// `@lat`: [[submission-review-page]] and, where relevant, anchors for major
sections/components referenced here (e.g., // `@lat`:
[[submission-review-enter-score-form]] near EnterScoreForm usage, // `@lat`:
[[submission-review-video-links-editor]] near OrganizerVideoLinksEditor, and //
`@lat`: [[submission-review-notes]] near
MovementTallyCard/ReviewNoteForm/ReviewNotesList and VerificationControls) so
the lat tooling can trace the organizer/cohost shared surface.

Source: Coding guidelines

apps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/$eventId/submissions/index.tsx (1)

16-19: ⚡ Quick win

Add @lat anchors to this submissions adapter route.

This file now owns the cohost→organizer submissions-shape adaptation, but there are still no // @lat: [[section-id]] comments around that contract. As per coding guidelines, **/*.{ts,tsx,js,jsx,rs,go,c,h,py} files should use // @lat: [[section-id]] code reference comments.

Also applies to: 100-164

🤖 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
`@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/events/$eventId/submissions/index.tsx
around lines 16 - 19, Add code reference anchors for the submissions adapter
contract by surrounding the adaptation code that maps cohost→organizer
submissions shape with LAT comments (e.g., // `@lat`:
[[submissions-adapter:start]] before the block and // `@lat`:
[[submissions-adapter:end]] after it). Apply these same anchors around the
import/usage of SubmissionsPage and submissionsSearchSchema and the related
mapping logic (including the region covering the existing adapter code further
down the file) so the entire contract is bracketed by matching // `@lat` markers.

Source: Coding guidelines

apps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/$eventId/submissions/$submissionId.tsx (1)

10-38: ⚡ Quick win

Add @lat comments to the shared submission-review wrapper.

This route now owns a non-trivial adapter layer into the organizer SubmissionReviewPage, but there are still no // @lat: [[section-id]] anchors around the loader/override contract. As per coding guidelines, **/*.{ts,tsx,js,jsx,rs,go,c,h,py} files should use // @lat: [[section-id]] code reference comments.

Also applies to: 154-215

🤖 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
`@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/events/$eventId/submissions/$submissionId.tsx
around lines 10 - 38, Add // `@lat`: anchors around the loader/override contract
in this route's adapter layer to the organizer SubmissionReviewPage: insert
descriptive markers (e.g. // `@lat`: [[submission-review-loader]] and // `@lat`:
[[submission-review-overrides]]) around the code that builds/consumes
SubmissionReviewPage props and the loader logic that calls
cohostGetSubmissionDetailFn, cohostGetOrganizerSubmissionDetailFn,
cohostGetReviewNotesFn, getCompetitionByIdFn, getSubmissionVoteDetailsFn,
cohostGetVerificationLogsFn, and the SubmissionReviewOverrides wiring (types
SubmissionReviewOverrides and component SubmissionReviewPage) so the mapping
between loader data and overrides is clearly anchored for readers and tooling.

Source: Coding guidelines

apps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/$eventId/index.tsx (1)

27-28: ⚡ Quick win

Add @lat comments around the shared EventDetailPage handoff.

This wrapper is now the cohost-side contract for the shared event page, but there are still no // @lat: [[section-id]] anchors around that delegation and override wiring. As per coding guidelines, **/*.{ts,tsx,js,jsx,rs,go,c,h,py} files should use // @lat: [[section-id]] code reference comments.

Also applies to: 41-129

🤖 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
`@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/events/$eventId/index.tsx
around lines 27 - 28, Add explicit code-reference anchors around the cohost-side
delegation to the shared EventDetailPage: locate the import and the place where
this route component hands off rendering to EventDetailPage (the wrapper/default
export for this route) and insert a pair of comments like // `@lat`:
[[cohost-event-detail-handoff-start]] before the delegation/override wiring and
// `@lat`: [[cohost-event-detail-handoff-end]] after it; also add the same
start/end anchors around any other override logic that spans the shared
implementation in this file (the block covering the EventDetailPage delegation
and the override wiring that currently spans the equivalent of lines 41-129).

Source: Coding guidelines

apps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/$eventId.tsx (1)

16-25: ⚡ Quick win

Add the required @lat anchors to this route wrapper.

This file now documents and owns the cohost→organizer route pairing, but it still has no // @lat: [[section-id]] comments tying that contract back to the relevant concept/docs. As per coding guidelines, **/*.{ts,tsx,js,jsx,rs,go,c,h,py} files should use // @lat: [[section-id]] code reference comments.

🤖 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
`@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/events/$eventId.tsx
around lines 16 - 25, Add the required // `@lat`: [[section-id]] anchor comments
at the top of this route wrapper file (immediately above the import block) to
document the cohost→organizer route pairing; locate the file by the route
wrapper symbols createFileRoute and Outlet and add one or more // `@lat`: [[...]]
lines using the project's LAT naming convention (for example
[[cohost:organizer-route]] or the canonical section-id) so the route contract is
tied back to the relevant docs.

Source: Coding guidelines

🤖 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
`@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/events/$eventId/submissions/$submissionId.tsx:
- Around line 176-180: The wrapper in the overrides object currently swallows
adjustedRoundScores by renaming it to _unsupported and calling verifyScore
without signaling an error; instead, in the
SubmissionReviewOverrides.verifyScore wrapper, detect when adjustedRoundScores
is present and throw a clear error (or return a rejected Promise) rather than
silently dropping them — reference the overrides constant and the verifyScore
wrapper, inspect the adjustedRoundScores parameter (named _unsupported) and fail
fast (including competitionTeamId as needed when delegating) so callers receive
a clear failure when multi-round edits are supplied.

In
`@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/events/$eventId/submissions/index.tsx:
- Around line 68-78: Replace the blanket .catch on
cohostGetOrganizerSubmissionsFn so it only converts non-forbidden errors to the
empty-result fallback; detect FORBIDDEN using the same classifier used by other
cohost loaders (e.g., implement or reuse isForbiddenError(error) that checks
Error.message.startsWith("FORBIDDEN")), rethrow the error when isForbiddenError
returns true, and only return the { submissions: [], totals: { total:0,
reviewed:0, pending:0 } } fallback for other errors; apply this change around
the cohostGetOrganizerSubmissionsFn(...) call so permission failures propagate
while other failures degrade to the empty dataset.

In `@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/index.tsx:
- Around line 52-60: The loader currently swallows all errors from
cohostGetRevenueStatsFn and returns fake zeroed metrics; change the catch so it
only swallows explicit permission-denied/forbidden errors (e.g., check
error.status === 403 or error.name/message indicating FORBIDDEN) and rethrow any
other exceptions so transient/backend failures surface; apply the same change to
the other similar wrapper around cohostGetRevenueStatsFn (the block referenced
at lines 78-86) so only the intended FORBIDDEN case returns the safe zero-state
while all other errors propagate.

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/coupons-page.tsx:
- Around line 1-13: This file (and the other listed components) uses client-side
React hooks (useState, useEffect, useServerFn) but is missing the "use client"
directive; add a single line "use client" at the very top of coupons-page.tsx
(before any imports) to mark the module as a client component so hooks like
useState, useEffect and useServerFn work correctly — apply the same fix to the
other affected files (cohost/divisions.tsx, cohost/pricing.tsx,
cohost/results.tsx, cohost/revenue.tsx, cohost/scoring.tsx, cohost/coupons.tsx)
which use Route.useLoaderData, useRouter, useCallback or useServerFn.

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/events/event-detail-page.tsx:
- Around line 1-13: This file is missing the 'use client' directive required
because it imports and uses React hooks (useState) — add a top-line 'use client'
string directive as the very first line of the module, ensure the existing
import of useState remains, and add the required code reference comment(s) (e.g.
// `@lat`: [[section-id]]) near the module header; verify hook usages referenced
at useState and locations around the event-detail component (usages around lines
where state is created/used) continue to work as client components after the
change.

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/events/submission-review-page.tsx:
- Around line 503-507: The back button renders user-controlled backUrl directly
into an anchor href (inside Button asChild with ArrowLeft), creating an
XSS/open-redirect risk; validate or sanitize backUrl the same way you do for
video links (reuse the existing safe URL check/sanitizer) before rendering, and
only render the <a href=...> when the check passes (otherwise fall back to a
safe internal route or render a non-navigating button), or instead route through
the app Link component to ensure internal navigation handling.
- Around line 375-381: The component initializes activeVideoIndex and
optimisticReviews only once, so when the route/Submission changes the previous
values leak into the new submission; add a useEffect that watches the submission
identifier (e.g., submission.id) and submission.videoIndex and on change call
setActiveVideoIndex(submission.videoIndex ?? 0) and reset per-submission
optimistic state (e.g., setOptimisticReviews({}) or remove keys not matching the
current submission) so activeVideoIndex, setActiveVideoIndex, optimisticReviews,
and setOptimisticReviews are reinitialized for the new submission.

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/events/submission-review/review-notes.tsx:
- Around line 81-245: ReviewNoteForm currently manages form state/validation
manually (content, noteType, timestampSeconds, selectedMovementId) and calls
createNote directly; refactor it to use React Hook Form with a Zod schema for
validation (fields: content string required/non-empty, type enum
"general"|"no-rep", timestampSeconds optional number, movementId optional
string) so submission flows go through RHF's handleSubmit and errors are derived
from Zod; replace local useState hooks for form fields with useForm(), wire
formTextareaRef into RHF's register or controller, update the submit handler
referenced by handleSubmit to call createNote via the existing
defaultCreateNote/useServerFn flow (createReviewNoteFn) and call onNoteCreated
and playerRef.playVideo() on success, and ensure the Add note button and
keyboard shortcut trigger RHF's submit instead of the manual validation.
- Around line 228-233: The render currently branches on navigator.platform
inside the JSX (the span that shows the keyboard shortcut in review-notes.tsx)
which causes SSR/CSR mismatch; change this to a stable server-safe label like
"Cmd/Ctrl+Enter" or detect the platform only on the client by using a
useState/useEffect pair (e.g., create a platformLabel state, set it in useEffect
by reading navigator.platform and updating to "⌘+Enter" or "Ctrl+Enter") and
render platformLabel instead of reading navigator directly during render.

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/events/submission-review/verification-controls.tsx:
- Around line 858-871: The current handleUpdate flow sends only adjustedScore
and adjustedScoreStatus to verify, which flattens multi-round adjustments and
loses per-round breakdowns; update the UI and payload so that when the existing
audit entry or current score contains adjustedRoundScores (multi-round),
handleUpdate (the verify call above and the similar block at lines 952-980)
includes adjustedRoundScores in the request instead of or alongside
adjustedScore/adjustedScoreStatus, and update AuditLogEntry.handleUpdate to
accept and replay adjustedRoundScores (preserving per-round fields like
roundScores/roundStatuses) so edits re-submit the original multi-round shape
rather than a single flattened score. Ensure the functions/handlers that build
the verify payload check for adjustedRoundScores and prefer sending that
structure for multi-round workouts.

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/events/submissions-page.tsx:
- Around line 177-179: The sort select currently uses local state (sortBy,
setSortBy) initialized from initialSort but never writes back to the
route/search state, and it doesn't resync when the search param changes; update
the component so changes to the select update the route search param (e.g.,
write to search.sort via your router/search helper) and add an effect that
watches initialSort (or the search param) to call setSortBy to resync when
navigation or back/forward updates the URL; modify the onChange handler of the
select (where setSortBy is used) to also update the route search state and
ensure initialSort is the single source of truth for initialization/resync.
- Around line 298-302: The score comparator in the case "score" block currently
does sa.score.value - sb.score.value which assumes lower-is-better; update it to
consult the workout direction (e.g., an enum/field like workout.direction or
event.workout.direction) and flip the comparison when higher-is-better: compute
a sign multiplier (e.g., multiplier = directionIndicatesHigherIsBetter ? -1 : 1)
and return multiplier * (sa.score.value - sb.score.value), preserving the
existing null-handling branches in the case "score" block so time-based vs
reps/weight-based events sort correctly.

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/leaderboard-preview-page.tsx:
- Around line 19-25: Add the required // `@lat`: [[section-id]] anchor comments to
this and the other new/updated TSX/TS modules introduced by the refactor (ensure
one pass across the shared/wrapper files) so traceability is restored; for this
file, place a top-of-file marker above the exported symbol
leaderboardPreviewSearchSchema (and do the same in files that define related
route shells/validateSearch utilities), using the exact format "// `@lat`:
[[section-id]]" and ensuring each file gets a unique section-id consistent with
the codebase convention.

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/locations-page.tsx:
- Around line 1-64: This file is missing the "use client" directive required
because it uses the client-side hook useRouter() inside the LocationsPage
component; add the top-level "use client" directive as the very first line of
the module so the component (and VenueManager usage tied to useRouter) runs as a
client component and client hooks like useRouter() work correctly.

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/results-page.tsx:
- Around line 210-217: The Events StatTile is using topLevel.length which
undercounts parent layouts with child events; update the value passed to
StatTile (the one currently using topLevel.length) to reflect the total number
of event queues including children (e.g., use events.length or a computed
flattened count variable) so the Events tile matches the aggregated
pending/review totals shown below; adjust any nearby variable/prop names
(events, topLevel, StatTile value) accordingly and ensure the computed count is
used wherever the summary is rendered.

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/submission-windows-page.tsx:
- Around line 42-49: The map callback currently types the parameter as any;
replace it with a proper union element type so TypeScript preserves type safety
— e.g. type the parameter to the union of the element types from the workouts
collection (such as OrganizerWorkouts[number] | CohostCompetitionWorkout) when
defining workoutsWithType in the workouts.map call; import or reference the
OrganizerWorkouts and CohostCompetitionWorkout types as needed and update the
callback signature (the mapping logic can remain the same).

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/volunteers-page.tsx:
- Around line 1-315: The file is missing the "use client" directive required for
client-side hooks; add the string "use client" as the very first line of the
module (before any imports) so the VolunteersPage component (which uses
useNavigate, useRouter, and useEffect) is treated as a client component; ensure
the directive appears exactly as "use client" on its own line at the top of the
file.

---

Outside diff comments:
In `@apps/wodsmith-start/src/components/organizer-breadcrumb.tsx`:
- Around line 32-37: The breadcrumb list uses segment.label as the React key in
the allSegments.map callback (inside organizer-breadcrumb.tsx), which can
collide for repeated labels; change the key to a stable unique identifier (e.g.,
use segment.id if available) or fall back to a deterministic composite (like
`${segment.id || segment.label}-${index}`) so each Fragment key is unique and
stable across renders; update the Fragment key reference where the map is
implemented to use that unique value.

In
`@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/events/$eventId/submissions/$submissionId.tsx:
- Around line 53-59: The current catch on
cohostGetOrganizerSubmissionDetailFn(swallowing all errors) masks non-permission
failures; replace it so only explicit FORBIDDEN errors are translated to {
submission: null } by detecting them (reuse or add an isForbiddenError(error)
helper that checks error instanceof Error &&
error.message.startsWith("FORBIDDEN")), and rethrow any other errors so
reviewResult is assigned only for forbidden cases and other failures surface
normally; keep the call site (reviewResult assignment) and function name
cohostGetOrganizerSubmissionDetailFn unchanged.

---

Nitpick comments:
In
`@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/events/$eventId.tsx:
- Around line 16-25: Add the required // `@lat`: [[section-id]] anchor comments at
the top of this route wrapper file (immediately above the import block) to
document the cohost→organizer route pairing; locate the file by the route
wrapper symbols createFileRoute and Outlet and add one or more // `@lat`: [[...]]
lines using the project's LAT naming convention (for example
[[cohost:organizer-route]] or the canonical section-id) so the route contract is
tied back to the relevant docs.

In
`@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/events/$eventId/index.tsx:
- Around line 27-28: Add explicit code-reference anchors around the cohost-side
delegation to the shared EventDetailPage: locate the import and the place where
this route component hands off rendering to EventDetailPage (the wrapper/default
export for this route) and insert a pair of comments like // `@lat`:
[[cohost-event-detail-handoff-start]] before the delegation/override wiring and
// `@lat`: [[cohost-event-detail-handoff-end]] after it; also add the same
start/end anchors around any other override logic that spans the shared
implementation in this file (the block covering the EventDetailPage delegation
and the override wiring that currently spans the equivalent of lines 41-129).

In
`@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/events/$eventId/submissions/$submissionId.tsx:
- Around line 10-38: Add // `@lat`: anchors around the loader/override contract in
this route's adapter layer to the organizer SubmissionReviewPage: insert
descriptive markers (e.g. // `@lat`: [[submission-review-loader]] and // `@lat`:
[[submission-review-overrides]]) around the code that builds/consumes
SubmissionReviewPage props and the loader logic that calls
cohostGetSubmissionDetailFn, cohostGetOrganizerSubmissionDetailFn,
cohostGetReviewNotesFn, getCompetitionByIdFn, getSubmissionVoteDetailsFn,
cohostGetVerificationLogsFn, and the SubmissionReviewOverrides wiring (types
SubmissionReviewOverrides and component SubmissionReviewPage) so the mapping
between loader data and overrides is clearly anchored for readers and tooling.

In
`@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/events/$eventId/submissions/index.tsx:
- Around line 16-19: Add code reference anchors for the submissions adapter
contract by surrounding the adaptation code that maps cohost→organizer
submissions shape with LAT comments (e.g., // `@lat`:
[[submissions-adapter:start]] before the block and // `@lat`:
[[submissions-adapter:end]] after it). Apply these same anchors around the
import/usage of SubmissionsPage and submissionsSearchSchema and the related
mapping logic (including the region covering the existing adapter code further
down the file) so the entire contract is bracketed by matching // `@lat` markers.

In
`@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/events/index.tsx:
- Around line 110-164: The overrides object is currently cast to any and uses
Record<string, unknown>, which erases compile-time checks; import and use the
shared EventsPage override interface (the prop type/interface exported by the
shared page, e.g., EventsPageOverrides or the EventsPageProps overrides type)
and type the overrides variable with it (e.g., const overrides:
EventsPageOverrides = useMemo(...)). Remove the Record<string, unknown> and as
any casts on createWorkoutFn, removeWorkoutFn, reorderEventsFn, and
groupEventsFn, and instead type each args parameter to the exact override
callback types from the shared interface (or the specific argument types like
CreateWorkoutArgs, RemoveWorkoutArgs, etc.) so the compiler enforces the correct
shape when calling cohostCreateWorkoutFn, cohostRemoveWorkoutFn,
cohostReorderEventsFn, and cohostGroupEventsFn.

In `@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/locations.tsx:
- Around line 51-60: The override callbacks in venueOverrides use args: { data:
any } which loses type safety; update the parameter types to mirror the cohost
handlers by using the actual parameter types of cohostCreateVenueFn,
cohostUpdateVenueFn, cohostDeleteVenueFn, and cohostGetVenueHeatCountFn (for
example via Parameters<typeof cohostCreateVenueFn>[0] etc.) so each override
(createVenueFn, updateVenueFn, deleteVenueFn, getVenueHeatCountFn) accepts the
correct typed args and you can still return/invoke the cohost* functions with
the merged data including competitionTeamId.

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/coupons-page.tsx:
- Around line 139-179: Replace the manual checks in handleCreate with Zod-based
validation: define a Zod schema (e.g., CouponSchema) that validates
amountDollars (transformed to integer cents > 0), optional codeOverride (trimmed
string), optional maxRedemptions (optional positive integer), and optional
expiresAt (optional date string); use react-hook-form with zodResolver to drive
the form and get typed values instead of reading local state, call schema.parse
or form.handleSubmit to validate before constructing couponInput, and on
validation errors surface them via form.setError or toast; keep the existing
call sites (overrides.createCoupon or createCoupon) and only pass the
validated/parsed couponInput (amountOffCents, code, maxRedemptions, expiresAt).
- Around line 197-207: handleCopyLink currently assumes
navigator.clipboard.writeText will succeed and shows only a generic error;
update handleCopyLink to first check for navigator.clipboard availability and,
if the Permissions API exists, query "clipboard-write" before attempting copy,
then perform the copy with async/await and a try/catch around
navigator.clipboard.writeText; on catch, inspect the error (e.g.,
DOMException.name or message) and call toast.error with a specific message for
permission denial (e.g., "Permission denied to access clipboard") and a
different message for unsupported environments (e.g., "Clipboard not available —
try HTTPS or use the manual copy fallback"); if clipboard is unavailable, fall
back to a manual copy approach (temporary textarea +
document.execCommand('copy')) and handle success/error similarly, referencing
the function name handleCopyLink, variables appUrl and slug, and the
navigator.clipboard.writeText call to locate where to change the code.

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/events/submission-review-page.tsx:
- Around line 1-75: This file is missing the required // `@lat` anchors that tie
extracted shared pages to repo concepts; add a top-of-file anchor comment like
// `@lat`: [[submission-review-page]] and, where relevant, anchors for major
sections/components referenced here (e.g., // `@lat`:
[[submission-review-enter-score-form]] near EnterScoreForm usage, // `@lat`:
[[submission-review-video-links-editor]] near OrganizerVideoLinksEditor, and //
`@lat`: [[submission-review-notes]] near
MovementTallyCard/ReviewNoteForm/ReviewNotesList and VerificationControls) so
the lat tooling can trace the organizer/cohost shared surface.

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/overview-page.tsx:
- Around line 112-146: The local helper formatDateTime duplicates YYYY-MM-DD
parsing already implemented in formatUTCDateFull and formatDateStringFull from
`@/utils/date-utils`; replace the inline parsing/formatting in formatDateTime with
calls to those utilities (or extend formatUTCDateFull/formatDateStringFull to
accept an option to include time) and use that shared utility to handle both
date-only strings and Date/timestamp inputs so formatting logic is consolidated
and consistent.
- Around line 344-352: The cast "as string" on athletesLinkTo should be removed
by tightening its type where it's declared: ensure the prop or variable
athletesLinkTo is explicitly typed as string (or the exact route path string
union) or give its default value a const assertion so its type is string; then
pass athletesLinkTo directly to the Link component (replace the assertion).
Update the prop/interface or the variable declaration where athletesLinkTo is
defined to reflect this stricter string type (referencing the athletesLinkTo
identifier and the Link usage in overview-page.tsx).
- Around line 310-316: The JSX uses a raw <a href=...> for internal navigation
(inside the isOrganizer conditional rendering); replace the anchor with the
framework Link component (e.g., <Link
to={`/compete/organizer/${competition.id}/edit`}> ) so routing is type-safe and
integrated with TanStack Router, and add the appropriate import for Link from
the TanStack Router package used in the project; keep the Button as the child of
Link and otherwise preserve isOrganizer and competition.id usage.

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/volunteers-page.tsx:
- Around line 290-292: The code unsafely asserts
competition.defaultLaneShiftPattern as "stay" | "shift_right"; instead validate
the runtime value before using it: read competition.defaultLaneShiftPattern,
check whether it equals "stay" or "shift_right" (type guard), and only pass that
validated value into competitionDefaultPattern, otherwise fall back to
"shift_right"; update the usage around competitionDefaultPattern to use this
validated variable so unexpected/null/empty DB values never get asserted
through.

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/events/$eventId.tsx:
- Around line 1-15: The file is missing the repo-standard source anchor comment;
add a top-of-file LAT anchor comment using the exact token format `// `@lat`:
[[section-id]]` (choose the appropriate section-id for the competition/event
organizer routing concept) immediately above the file header or first import so
the route wrapper (symbols: createFileRoute, Outlet) is annotated; ensure the
comment uses `//` style and matches other repo anchors so tracing tools pick it
up.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3f713da3-7a4f-42e9-b602-3d8ace6e6d12

📥 Commits

Reviewing files that changed from the base of the PR and between a8f5350 and 2c75d28.

📒 Files selected for processing (71)
  • apps/wodsmith-start/src/components/cohost-sidebar.tsx
  • apps/wodsmith-start/src/components/competition-dashboard-shell.tsx
  • apps/wodsmith-start/src/components/competition-header.tsx
  • apps/wodsmith-start/src/components/competition-sidebar.tsx
  • apps/wodsmith-start/src/components/organizer-breadcrumb.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/athletes.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/coupons.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/divisions.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/$eventId.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/$eventId/index.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/$eventId/submissions/$submissionId.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/$eventId/submissions/index.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/index.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/index.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/leaderboard-preview.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/locations.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/pricing.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/results.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/revenue.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/schedule.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/scoring.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/sponsors.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/submission-windows.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/volunteers.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/waivers.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/athletes-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/coupons-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/divisions-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/events/event-detail-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/events/events-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/events/submission-review-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/events/submission-review/review-notes.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/events/submission-review/verification-controls.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/events/submissions-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/leaderboard-preview-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/locations-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/overview-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/pricing-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/results-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/revenue-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/schedule-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/scoring-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/sponsors-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/submission-windows-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/volunteers-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/waivers-page.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/athletes/index.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/coupons.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/divisions.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/events/$eventId.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/events/$eventId/index.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/events/$eventId/submissions/$submissionId.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/events/$eventId/submissions/index.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/events/index.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/index.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/leaderboard-preview.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/locations.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/pricing.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/results.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/revenue.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/schedule.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/scoring.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/sponsors.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/submission-windows.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/volunteers.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/waivers.tsx
  • apps/wodsmith-start/src/server-fns/cohost/cohost-submission-fns.ts
  • lat.md/architecture.md
  • lat.md/organizer-dashboard.md
💤 Files with no reviewable changes (1)
  • apps/wodsmith-start/src/components/cohost-sidebar.tsx

Comment on lines +68 to +78
cohostGetOrganizerSubmissionsFn({
data: {
trackWorkoutId: params.eventId,
competitionId: params.competitionId,
competitionTeamId,
divisionFilter: deps?.division,
},
}).catch(() => ({
submissions: [],
totals: { total: 0, reviewed: 0, pending: 0 },
})),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't turn submission-fetch failures into an empty event.

This is now the primary dataset for the shared submissions page. The blanket catch makes permission regressions and backend failures look identical to “no submissions yet”, which drops counts to zero and hides the real outage. As per PR objectives, the cohost wrappers should preserve FORBIDDEN graceful degradation, not swallow every server failure.

Proposed fix
-        cohostGetOrganizerSubmissionsFn({
+        cohostGetOrganizerSubmissionsFn({
           data: {
             trackWorkoutId: params.eventId,
             competitionId: params.competitionId,
             competitionTeamId,
             divisionFilter: deps?.division,
           },
-        }).catch(() => ({
-          submissions: [],
-          totals: { total: 0, reviewed: 0, pending: 0 },
-        })),
+        }).catch((error) => {
+          if (isForbiddenError(error)) {
+            return {
+              submissions: [],
+              totals: { total: 0, reviewed: 0, pending: 0 },
+            }
+          }
+          throw error
+        }),
// Reuse the same forbidden classifier your existing cohost loaders use.
function isForbiddenError(error: unknown) {
  return error instanceof Error && error.message.startsWith("FORBIDDEN")
}
🤖 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
`@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/events/$eventId/submissions/index.tsx
around lines 68 - 78, Replace the blanket .catch on
cohostGetOrganizerSubmissionsFn so it only converts non-forbidden errors to the
empty-result fallback; detect FORBIDDEN using the same classifier used by other
cohost loaders (e.g., implement or reuse isForbiddenError(error) that checks
Error.message.startsWith("FORBIDDEN")), rethrow the error when isForbiddenError
returns true, and only return the { submissions: [], totals: { total:0,
reviewed:0, pending:0 } } fallback for other errors; apply this change around
the cohostGetOrganizerSubmissionsFn(...) call so permission failures propagate
while other failures degrade to the empty dataset.

Comment on lines 52 to +60
cohostGetRevenueStatsFn({
data: { competitionId: params.competitionId, competitionTeamId },
}).catch(() => ({ stats: { totalGrossCents: 0, totalOrganizerNetCents: 0, purchaseCount: 0 } })),
}).catch(() => ({
stats: {
totalGrossCents: 0,
totalOrganizerNetCents: 0,
purchaseCount: 0,
},
})),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't turn loader failures into fake zero-state metrics.

These fallbacks now report 0 revenue and no division-results status for any exception, including transient backend failures, so the overview can silently show materially wrong business data instead of surfacing an error. Limit the swallow to the explicit permission-denied case if that's intentional, and rethrow everything else.

As per PR objectives, cohost wrappers are supposed to preserve the existing FORBIDDEN-swallowing behavior rather than hide arbitrary loader failures.

Also applies to: 78-86

🤖 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 `@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/index.tsx
around lines 52 - 60, The loader currently swallows all errors from
cohostGetRevenueStatsFn and returns fake zeroed metrics; change the catch so it
only swallows explicit permission-denied/forbidden errors (e.g., check
error.status === 403 or error.name/message indicating FORBIDDEN) and rethrow any
other exceptions so transient/backend failures surface; apply the same change to
the other similar wrapper around cohostGetRevenueStatsFn (the block referenced
at lines 78-86) so only the intended FORBIDDEN case returns the safe zero-state
while all other errors propagate.

Comment on lines +1 to +13
/**
* Competition Coupons Page
*
* Shared page body for the organizer and cohost coupons routes. The organizer
* route renders it with defaults (organizer server fns); the cohost route
* injects cohost-permissioned overrides for listing, creating, and
* deactivating coupons.
*/

import { useServerFn } from "@tanstack/react-start"
import { Ban, Copy, Plus, Tag } from "lucide-react"
import { useEffect, useState } from "react"
import { toast } from "sonner"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Add "use client" directive to all components using React hooks.

Seven components across this PR use client-side React hooks (useState, useEffect, useServerFn, useCallback, useRouter, Route.useLoaderData()) but are missing the required "use client" directive. Per coding guidelines for **/*.tsx files: "Use Server Components by default, add use client only when necessary." These components require client-side execution and must include the directive at the top of each file.

Affected files:

  • coupons-page.tsx (uses useState, useEffect, useServerFn)
  • cohost/divisions.tsx (uses useServerFn, Route.useLoaderData)
  • cohost/pricing.tsx (uses useServerFn, Route.useLoaderData)
  • cohost/results.tsx (uses useRouter, useServerFn, useCallback, Route.useLoaderData)
  • cohost/revenue.tsx (uses Route.useLoaderData)
  • cohost/scoring.tsx (uses useServerFn, Route.useLoaderData)
  • cohost/coupons.tsx (uses useServerFn, Route.useLoaderData)
🤖 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
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/coupons-page.tsx
around lines 1 - 13, This file (and the other listed components) uses
client-side React hooks (useState, useEffect, useServerFn) but is missing the
"use client" directive; add a single line "use client" at the very top of
coupons-page.tsx (before any imports) to mark the module as a client component
so hooks like useState, useEffect and useServerFn work correctly — apply the
same fix to the other affected files (cohost/divisions.tsx, cohost/pricing.tsx,
cohost/results.tsx, cohost/revenue.tsx, cohost/scoring.tsx, cohost/coupons.tsx)
which use Route.useLoaderData, useRouter, useCallback or useServerFn.

Source: Coding guidelines

Comment on lines +1 to +13
/**
* Competition Event Detail Page
*
* Shared page body for the organizer and cohost event edit routes. The
* organizer route renders it with defaults (including organizer-only
* event-division mappings); the cohost route injects cohost-permissioned
* mutation overrides and the cohost route prefix for navigation links.
*/

import { Link } from "@tanstack/react-router"
import { Plus, Video } from "lucide-react"
import type { ComponentProps } from "react"
import { useState } from "react"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Missing 'use client' directive for Client Component.

This component uses useState (imported at line 13 and used at lines 103 and 227) but lacks the 'use client' directive. Per React 19 and coding guidelines, components using hooks must explicitly declare themselves as Client Components.

🔧 Required fix
+// `@lat`: [[organizer-cohost-event-detail-page]]
+'use client'
+
 /**
  * Competition Event Detail Page

As per coding guidelines, TypeScript files should use 'use client' directive when necessary for Client Components with hooks, and should include code reference comments (// @lat: [[section-id]]) to tie source code to concepts.

🤖 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
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/events/event-detail-page.tsx
around lines 1 - 13, This file is missing the 'use client' directive required
because it imports and uses React hooks (useState) — add a top-line 'use client'
string directive as the very first line of the module, ensure the existing
import of useState remains, and add the required code reference comment(s) (e.g.
// `@lat`: [[section-id]]) near the module header; verify hook usages referenced
at useState and locations around the event-detail component (usages around lines
where state is created/used) continue to work as client components after the
change.

Source: Coding guidelines

Comment on lines +1 to +64
/**
* Competition Locations Page
*
* Shared page body for the organizer and cohost locations routes. The
* organizer route renders it with defaults; the cohost route injects
* cohost-permissioned venue mutation overrides.
*/

import { useRouter } from "@tanstack/react-router"
import type { ComponentProps } from "react"
import { VenueManager } from "@/components/organizer/schedule/venue-manager"

type VenueManagerProps = ComponentProps<typeof VenueManager>

interface LocationsPageProps {
competitionId: string
competitionName: string
venues: VenueManagerProps["venues"]
primaryAddressId: VenueManagerProps["primaryAddressId"]
primaryAddress: VenueManagerProps["primaryAddress"]
/** Cohost routes inject cohost-permissioned venue mutations. */
overrides?: VenueManagerProps["overrides"]
}

export function LocationsPage({
competitionId,
competitionName,
venues,
primaryAddressId,
primaryAddress,
overrides,
}: LocationsPageProps) {
const router = useRouter()

// Refresh loader data after any venue mutation
const handleVenueChange = async () => {
await router.invalidate()
}

return (
<div className="container max-w-4xl py-6 space-y-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">
Locations & Venues
</h1>
<p className="text-muted-foreground">
Manage venues for {competitionName}. Venues are physical locations
like "Main Floor" or "Outside Rig" where heats are scheduled.
</p>
</div>

<VenueManager
competitionId={competitionId}
venues={venues}
primaryAddressId={primaryAddressId}
primaryAddress={primaryAddress}
onVenueCreate={handleVenueChange}
onVenueUpdate={handleVenueChange}
onVenueDelete={handleVenueChange}
overrides={overrides}
/>
</div>
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Add "use client" directive for client-side router hooks.

This component uses useRouter() from @tanstack/react-router, which is a client-side hook. As per coding guidelines, Server Components are the default and any component using client hooks must include the "use client" directive at the top of the file.

🔧 Add the directive
+"use client"
+
 /**
  * Competition Locations Page
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Competition Locations Page
*
* Shared page body for the organizer and cohost locations routes. The
* organizer route renders it with defaults; the cohost route injects
* cohost-permissioned venue mutation overrides.
*/
import { useRouter } from "@tanstack/react-router"
import type { ComponentProps } from "react"
import { VenueManager } from "@/components/organizer/schedule/venue-manager"
type VenueManagerProps = ComponentProps<typeof VenueManager>
interface LocationsPageProps {
competitionId: string
competitionName: string
venues: VenueManagerProps["venues"]
primaryAddressId: VenueManagerProps["primaryAddressId"]
primaryAddress: VenueManagerProps["primaryAddress"]
/** Cohost routes inject cohost-permissioned venue mutations. */
overrides?: VenueManagerProps["overrides"]
}
export function LocationsPage({
competitionId,
competitionName,
venues,
primaryAddressId,
primaryAddress,
overrides,
}: LocationsPageProps) {
const router = useRouter()
// Refresh loader data after any venue mutation
const handleVenueChange = async () => {
await router.invalidate()
}
return (
<div className="container max-w-4xl py-6 space-y-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">
Locations & Venues
</h1>
<p className="text-muted-foreground">
Manage venues for {competitionName}. Venues are physical locations
like "Main Floor" or "Outside Rig" where heats are scheduled.
</p>
</div>
<VenueManager
competitionId={competitionId}
venues={venues}
primaryAddressId={primaryAddressId}
primaryAddress={primaryAddress}
onVenueCreate={handleVenueChange}
onVenueUpdate={handleVenueChange}
onVenueDelete={handleVenueChange}
overrides={overrides}
/>
</div>
)
}
"use client"
/**
* Competition Locations Page
*
* Shared page body for the organizer and cohost locations routes. The
* organizer route renders it with defaults; the cohost route injects
* cohost-permissioned venue mutation overrides.
*/
import { useRouter } from "`@tanstack/react-router`"
import type { ComponentProps } from "react"
import { VenueManager } from "`@/components/organizer/schedule/venue-manager`"
type VenueManagerProps = ComponentProps<typeof VenueManager>
interface LocationsPageProps {
competitionId: string
competitionName: string
venues: VenueManagerProps["venues"]
primaryAddressId: VenueManagerProps["primaryAddressId"]
primaryAddress: VenueManagerProps["primaryAddress"]
/** Cohost routes inject cohost-permissioned venue mutations. */
overrides?: VenueManagerProps["overrides"]
}
export function LocationsPage({
competitionId,
competitionName,
venues,
primaryAddressId,
primaryAddress,
overrides,
}: LocationsPageProps) {
const router = useRouter()
// Refresh loader data after any venue mutation
const handleVenueChange = async () => {
await router.invalidate()
}
return (
<div className="container max-w-4xl py-6 space-y-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">
Locations & Venues
</h1>
<p className="text-muted-foreground">
Manage venues for {competitionName}. Venues are physical locations
like "Main Floor" or "Outside Rig" where heats are scheduled.
</p>
</div>
<VenueManager
competitionId={competitionId}
venues={venues}
primaryAddressId={primaryAddressId}
primaryAddress={primaryAddress}
onVenueCreate={handleVenueChange}
onVenueUpdate={handleVenueChange}
onVenueDelete={handleVenueChange}
overrides={overrides}
/>
</div>
)
}
🤖 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
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/locations-page.tsx
around lines 1 - 64, This file is missing the "use client" directive required
because it uses the client-side hook useRouter() inside the LocationsPage
component; add the top-level "use client" directive as the very first line of
the module so the component (and VenueManager usage tied to useRouter) runs as a
client component and client hooks like useRouter() work correctly.

Source: Coding guidelines

Comment on lines +210 to +217
{events.length > 0 && (
<div className="grid grid-cols-3 gap-3 animate-in fade-in-0 slide-in-from-bottom-2 duration-400 sm:gap-4">
<StatTile
label="Events"
value={topLevel.length}
icon={<Video className="h-4 w-4" />}
delay={0}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

The Events stat undercounts parent/child online layouts.

Pending/reviewed totals are aggregated over child events when a parent has sub-events, but the Events tile still uses topLevel.length. A parent with three child review queues shows 1 event here, which makes the summary inconsistent with the links below.

🤖 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
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/results-page.tsx
around lines 210 - 217, The Events StatTile is using topLevel.length which
undercounts parent layouts with child events; update the value passed to
StatTile (the one currently using topLevel.length) to reflect the total number
of event queues including children (e.g., use events.length or a computed
flattened count variable) so the Events tile matches the aggregated
pending/review totals shown below; adjust any nearby variable/prop names
(events, topLevel, StatTile value) accordingly and ensure the computed count is
used wherever the summary is rendered.

Comment on lines +1 to +315
/**
* Competition Volunteers Page
*
* Shared page body for the organizer and cohost volunteers routes. The
* organizer route renders it with defaults (organizer server fns) and the
* organizer-only waiver status column data; the cohost route injects
* cohost-permissioned callback bundles for the roster, shifts, judge
* scheduling, and signup-question editing.
*/

import { useNavigate, useRouter } from "@tanstack/react-router"
import type { ComponentProps } from "react"
import { useEffect } from "react"
import type { RegistrationQuestionsOverrides } from "@/components/competition-settings/registration-questions-editor"
import { RegistrationQuestionsEditor } from "@/components/competition-settings/registration-questions-editor"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { InvitedVolunteersList } from "../-components/invited-volunteers-list"
import type { JudgeSchedulingOverrides } from "../-components/judges"
import { JudgeSchedulingContainer } from "../-components/judges"
import { ShiftList } from "../-components/shifts/shift-list"
import { VolunteersList } from "../-components/volunteers-list"

type VolunteersListProps = ComponentProps<typeof VolunteersList>
type ShiftListProps = ComponentProps<typeof ShiftList>
type JudgeSchedulingProps = ComponentProps<typeof JudgeSchedulingContainer>

export type VolunteersPageTab =
| "roster"
| "shifts"
| "schedule"
| "registration-rules"

/** Roster mutation callbacks; cohost routes inject cohost server fns. */
export type VolunteersListCallbacks = Pick<
VolunteersListProps,
| "onBulkAssignRole"
| "onInviteVolunteer"
| "onAddRoleType"
| "onRemoveRoleType"
| "onUpdateMetadata"
| "onGrantScoreAccess"
| "onRevokeScoreAccess"
>

/** Shift CRUD/assignment callbacks; cohost routes inject cohost server fns. */
export type ShiftListCallbacks = Pick<
ShiftListProps,
| "onDeleteShift"
| "onCreateShift"
| "onUpdateShift"
| "onGetVolunteers"
| "onAssignVolunteer"
| "onUnassignVolunteer"
>

interface VolunteersPageProps {
competition: {
id: string
slug: string
organizingTeamId: string
competitionType: JudgeSchedulingProps["competitionType"]
defaultHeatsPerRotation: number | null
defaultLaneShiftPattern: string | null
}
competitionTeamId: string
/** Active tab from the route's `tab` search param. */
tab: VolunteersPageTab
/** Selected event id from the route's `event` search param. */
eventFromUrl?: string
invitations: VolunteersListProps["invitations"]
volunteersWithAccess: VolunteersListProps["volunteers"]
events: JudgeSchedulingProps["events"]
pendingDirectInvites: ComponentProps<typeof InvitedVolunteersList>["invites"]
judges: JudgeSchedulingProps["judges"]
heats: JudgeSchedulingProps["heats"]
judgeAssignments: JudgeSchedulingProps["judgeAssignments"]
rotations: JudgeSchedulingProps["rotations"]
eventDefaultsMap: JudgeSchedulingProps["eventDefaultsMap"]
versionHistoryMap: JudgeSchedulingProps["versionHistoryMap"]
activeVersionMap: JudgeSchedulingProps["activeVersionMap"]
shifts: ShiftListProps["shifts"]
volunteerAssignments: VolunteersListProps["volunteerAssignments"]
volunteerQuestions: VolunteersListProps["volunteerQuestions"]
answersByInvitation: VolunteersListProps["answersByInvitation"]
emailToInvitationId: VolunteersListProps["emailToInvitationId"]
/** Organizer-only; cohost loaders cannot fetch waiver statuses, hiding the waiver columns. */
volunteerWaiverStatus?: VolunteersListProps["volunteerWaiverStatus"]
/** Team id handed to the signup questions editor. Defaults to the organizing team; cohosts pass competitionTeamId alongside questionOverrides. */
questionsTeamId?: string
/** Cohost routes inject cohost-permissioned roster mutations. */
volunteersListCallbacks?: VolunteersListCallbacks
/** Cohost routes inject cohost-permissioned shift mutations. */
shiftListCallbacks?: ShiftListCallbacks
/** Cohost routes inject cohost-permissioned question CRUD. */
questionOverrides?: RegistrationQuestionsOverrides
/** Cohost routes inject cohost-permissioned judge rotation mutations. */
judgeSchedulingOverrides?: JudgeSchedulingOverrides
}

export function VolunteersPage({
competition,
competitionTeamId,
tab,
eventFromUrl,
invitations,
volunteersWithAccess,
events,
pendingDirectInvites,
judges,
heats,
judgeAssignments,
rotations,
eventDefaultsMap,
versionHistoryMap,
activeVersionMap,
shifts,
volunteerAssignments,
volunteerQuestions,
answersByInvitation,
emailToInvitationId,
volunteerWaiverStatus,
questionsTeamId,
volunteersListCallbacks,
shiftListCallbacks,
questionOverrides,
judgeSchedulingOverrides,
}: VolunteersPageProps) {
const navigate = useNavigate()
const router = useRouter()

const handleTabChange = (value: string) => {
navigate({
to: ".",
search: (prev) => ({
...prev,
tab: value as VolunteersPageTab,
}),
replace: true,
})
}

const handleEventChange = (eventId: string) => {
navigate({
to: ".",
search: (prev) => ({ ...prev, event: eventId }),
replace: true,
})
}

// Determine selected event - from URL or first event
// Validate eventFromUrl exists in events before using it
const selectedEventId =
eventFromUrl && events.some((event) => event.id === eventFromUrl)
? eventFromUrl
: events[0]?.id || ""

// Check if schedule tab should be available (in-person competitions only)
const isInPerson = competition.competitionType === "in-person"

// Derive effective tab - fall back to roster if schedule isn't allowed
const effectiveTab = !isInPerson && tab === "schedule" ? "roster" : tab

// Sync URL/state when competition type changes and schedule tab is no longer valid
useEffect(() => {
if (!isInPerson && tab === "schedule") {
navigate({
to: ".",
search: { tab: "roster" },
replace: true,
})
}
}, [isInPerson, tab, navigate])

const handleQuestionsChange = () => {
router.invalidate()
}

return (
<Tabs
value={effectiveTab}
onValueChange={handleTabChange}
className="w-full"
>
{/* Mobile: Select dropdown */}
<div className="mb-6 sm:hidden">
<Select value={effectiveTab} onValueChange={handleTabChange}>
<SelectTrigger className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="roster">Roster</SelectItem>
<SelectItem value="shifts">Shifts</SelectItem>
{isInPerson && (
<SelectItem value="schedule">Judge Schedule</SelectItem>
)}
<SelectItem value="registration-rules">Signup Questions</SelectItem>
</SelectContent>
</Select>
</div>

{/* Desktop: Tabs */}
<TabsList className="mb-6 hidden sm:inline-flex">
<TabsTrigger value="roster">Roster</TabsTrigger>
<TabsTrigger value="shifts">Shifts</TabsTrigger>
{isInPerson && (
<TabsTrigger value="schedule">Judge Schedule</TabsTrigger>
)}
<TabsTrigger value="registration-rules">Signup Questions</TabsTrigger>
</TabsList>

{/* Roster Tab - Volunteer Management */}
<TabsContent value="roster" className="flex flex-col gap-8">
{/* Invited Volunteers Section - Only show if there are pending direct invites */}
{pendingDirectInvites.length > 0 && (
<section>
<div className="mb-4">
<h2 className="text-xl font-semibold">Invited Volunteers</h2>
<p className="text-sm text-muted-foreground">
{pendingDirectInvites.length} pending{" "}
{pendingDirectInvites.length === 1 ? "invite" : "invites"}
</p>
</div>
<InvitedVolunteersList invites={pendingDirectInvites} />
</section>
)}

{/* Volunteers Section */}
<section>
<div className="mb-4">
<h2 className="text-xl font-semibold">Volunteers</h2>
<p className="text-sm text-muted-foreground">
{invitations.length + volunteersWithAccess.length} total (
{invitations.length} application
{invitations.length === 1 ? "" : "s"},{" "}
{volunteersWithAccess.length} approved)
</p>
</div>

<VolunteersList
competitionId={competition.id}
competitionSlug={competition.slug}
competitionTeamId={competitionTeamId}
organizingTeamId={competition.organizingTeamId}
invitations={invitations}
volunteers={volunteersWithAccess}
volunteerAssignments={volunteerAssignments}
volunteerQuestions={volunteerQuestions}
answersByInvitation={answersByInvitation}
emailToInvitationId={emailToInvitationId}
volunteerWaiverStatus={volunteerWaiverStatus}
{...volunteersListCallbacks}
/>
</section>
</TabsContent>

{/* Shifts Tab */}
<TabsContent value="shifts" className="mt-6">
<ShiftList
competitionId={competition.id}
competitionTeamId={competitionTeamId}
shifts={shifts}
{...shiftListCallbacks}
/>
</TabsContent>

{/* Schedule Tab - Judge Scheduling & Rotations (in-person only) */}
{isInPerson && (
<TabsContent value="schedule">
<JudgeSchedulingContainer
competitionId={competition.id}
competitionSlug={competition.slug}
organizingTeamId={competition.organizingTeamId}
competitionType={competition.competitionType}
events={events}
heats={heats}
judges={judges}
judgeAssignments={judgeAssignments}
rotations={rotations}
eventDefaultsMap={eventDefaultsMap}
versionHistoryMap={versionHistoryMap}
activeVersionMap={activeVersionMap}
competitionDefaultHeats={competition.defaultHeatsPerRotation ?? 4}
competitionDefaultPattern={
(competition.defaultLaneShiftPattern as "stay" | "shift_right") ??
"shift_right"
}
selectedEventId={selectedEventId}
onEventChange={handleEventChange}
overrides={judgeSchedulingOverrides}
/>
</TabsContent>
)}

{/* Signup Questions Tab */}
<TabsContent value="registration-rules">
<RegistrationQuestionsEditor
entityType="competition"
entityId={competition.id}
teamId={questionsTeamId ?? competition.organizingTeamId}
questions={volunteerQuestions}
onQuestionsChange={handleQuestionsChange}
questionTarget="volunteer"
overrides={questionOverrides}
/>
</TabsContent>
</Tabs>
)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Add "use client" directive for client-side hooks.

This component uses useNavigate(), useRouter(), and useEffect(), all of which are client-side hooks. As per coding guidelines, components using client hooks must include the "use client" directive at the top of the file.

🔧 Add the directive
+"use client"
+
 /**
  * Competition Volunteers Page
🤖 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
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-pages/volunteers-page.tsx
around lines 1 - 315, The file is missing the "use client" directive required
for client-side hooks; add the string "use client" as the very first line of the
module (before any imports) so the VolunteersPage component (which uses
useNavigate, useRouter, and useEffect) is treated as a client component; ensure
the directive appears exactly as "use client" on its own line at the top of the
file.

Source: Coding guidelines

- Validate the submission review back-link: only internal paths render into
  the anchor; anything else falls back to the submissions list link
- Reset per-submission video tab and optimistic review state when the
  submissionId param changes while the component stays mounted
- Cohost verifyScore override fails fast if round-score adjustments are
  ever supplied instead of silently dropping them
- Compute the note-form modifier key label after mount so SSR and Mac
  clients render the same markup
- Type the submission-windows workout map param as the union element
- Use router Link for the overview Configure registration button; guard
  coupon copy-link when the clipboard API is unavailable; composite React
  key for breadcrumb segments
- Add @lat anchors tying all shared -pages components to the documented
  Shared Page Components section

https://claude.ai/code/session_01U1fRZH8SG5gmo9Nt49maQZ

Copy link
Copy Markdown
Contributor Author

Addressed the CodeRabbit review in e88cf60. Disposition of all 17 actionable comments:

Fixed (e88cf60):

  • backUrl is now validated as an internal path (/ prefix, not protocol-relative) before rendering into the back anchor; unsafe values fall back to the typed submissions-list Link
  • Per-submission state (activeVideoIndex, optimisticReviews) resets when the submissionId param changes while the component stays mounted
  • The cohost verifyScore override now throws if adjustedRoundScores are ever supplied instead of silently dropping them
  • The note-form modifier-key label (/Ctrl) is computed in an effect after mount, so SSR and Mac clients render identical markup
  • submission-windows-page map param typed as OrganizerWorkouts[number] | CohostCompetitionWorkout instead of any
  • Overview "Configure registration" uses a typed router Link; coupon copy-link guards navigator.clipboard availability; breadcrumb segments use a composite key
  • All 21 shared -pages/ files now carry // @lat: [[organizer-dashboard#Cohost Dashboard#Shared Component Callback Pattern#Shared Page Components]] anchors — note these must reference existing lat.md sections (lat check validates them), so the free-form ids suggested in some comments would fail CI of the lat tooling

Skipped — wrong framework assumption (the three "Critical" use client comments): this app is TanStack Start, not Next.js — there are no React Server Components and the directive is inert. None of the pre-refactor route files these bodies were extracted from carried it, and build/E2E/type-check are green without it.

Skipped — pre-existing behavior preserved deliberately (the PR's contract was byte-for-byte loader/body preservation):

  • The blanket .catch() fallbacks in the cohost overview, submissions list, and submission detail loaders all predate this PR (verifiable at the pre-refactor commit). Tightening to FORBIDDEN-only is worthwhile, but there are ~10 more identical catches across cohost loaders (schedule alone has 5) — doing it piecemeal here would leave the codebase inconsistent. Good candidate for a dedicated follow-up that converts all of them and adds a shared isForbiddenError helper.
  • Multi-round audit-log edit flattening, score-sort direction, sort-in-route-state, the Events stat tile count, and the hand-rolled review/coupon forms (RHF+Zod) are all organizer behaviors carried over verbatim — follow-up material, out of scope for a parity refactor.
  • The as any override wrappers in the cohost events/locations routes are the pre-existing pattern (main's feat: group existing events under a parent event on the events page #504 added groupEventsFn with the same shape); typing them properly against the manager interfaces is a good follow-up.

Verification on the new head: repo-wide type-check clean, all 2,820 tests pass, Biome clean, lat check passes.


Generated by Claude Code

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.

2 participants