refactor: render cohost dashboard routes through shared organizer page components - #506
refactor: render cohost dashboard routes through shared organizer page components#506zacjones93 wants to merge 6 commits into
Conversation
… 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
WalkthroughThis PR consolidates organizer and cohost competition dashboard functionality by extracting 16 shared page components and updating both route trees to use them. A new ChangesCompetition Dashboard Shell and Navigation Foundation
Shared Page Components for Organizer and Cohost Dashboards
Cohost Route Refactoring to Delegate to Shared Pages
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
💡 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".
| verifyScore: async ({ adjustedRoundScores: _unsupported, ...input }) => | ||
| verifyScore({ data: { ...input, competitionTeamId } }), |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 }) => |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Thanks for the feedback! I've saved this as a new learning to improve future reviews.
| @@ -0,0 +1,2329 @@ | |||
| /** | |||
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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({ |
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Got it — I’ll treat this as intentional parity and avoid flagging it here.
… 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
There was a problem hiding this comment.
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 winUse a unique key for breadcrumb segments.
Line 36 uses
segment.labelas 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 winOnly 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 valueConsider consolidating date formatting with existing utilities.
The
formatDateTimehelper duplicates YYYY-MM-DD parsing logic already present informatUTCDateFullandformatDateStringFullfrom@/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 valueType assertion on
athletesLinkTocould be avoided with stricter typing.The
as stringcast at line 345 is safe due to the default value, but consider typingathletesLinkToprop 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 winPrefer 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 tradeoffConsider adding Zod validation for the coupon creation form.
The form uses manual validation (checking
NaNand<= 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 winImprove clipboard error handling for permission denial.
The clipboard
writeTextoperation 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 winValidate
defaultLaneShiftPatternbefore 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 winConsider 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 winDon't cast away the shared override contract.
This adapter is now the compile-time sync point between the cohost wrapper and
EventsPage, butRecord<string, unknown>plusas anyremoves the checks that would catch signature drift here before runtime. Please typeoverridesagainst 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 winAdd the required
@latanchors 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 winAdd 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 winAdd
@latanchors 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 winAdd
@latcomments 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 winAdd
@latcomments around the sharedEventDetailPagehandoff.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 winAdd the required
@latanchors 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
📒 Files selected for processing (71)
apps/wodsmith-start/src/components/cohost-sidebar.tsxapps/wodsmith-start/src/components/competition-dashboard-shell.tsxapps/wodsmith-start/src/components/competition-header.tsxapps/wodsmith-start/src/components/competition-sidebar.tsxapps/wodsmith-start/src/components/organizer-breadcrumb.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/athletes.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/coupons.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/divisions.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/$eventId.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/$eventId/index.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/$eventId/submissions/$submissionId.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/$eventId/submissions/index.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/events/index.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/index.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/leaderboard-preview.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/locations.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/pricing.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/results.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/revenue.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/schedule.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/scoring.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/sponsors.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/submission-windows.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/volunteers.tsxapps/wodsmith-start/src/routes/compete/cohost/$competitionId/waivers.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/athletes-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/coupons-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/divisions-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/events/event-detail-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/events/events-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/events/submission-review-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/events/submission-review/review-notes.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/events/submission-review/verification-controls.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/events/submissions-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/leaderboard-preview-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/locations-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/overview-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/pricing-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/results-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/revenue-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/schedule-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/scoring-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/sponsors-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/submission-windows-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/volunteers-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/-pages/waivers-page.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/athletes/index.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/coupons.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/divisions.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/events/$eventId.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/events/$eventId/index.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/events/$eventId/submissions/$submissionId.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/events/$eventId/submissions/index.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/events/index.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/index.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/leaderboard-preview.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/locations.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/pricing.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/results.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/revenue.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/schedule.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/scoring.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/sponsors.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/submission-windows.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/volunteers.tsxapps/wodsmith-start/src/routes/compete/organizer/$competitionId/waivers.tsxapps/wodsmith-start/src/server-fns/cohost/cohost-submission-fns.tslat.md/architecture.mdlat.md/organizer-dashboard.md
💤 Files with no reviewable changes (1)
- apps/wodsmith-start/src/components/cohost-sidebar.tsx
| cohostGetOrganizerSubmissionsFn({ | ||
| data: { | ||
| trackWorkoutId: params.eventId, | ||
| competitionId: params.competitionId, | ||
| competitionTeamId, | ||
| divisionFilter: deps?.division, | ||
| }, | ||
| }).catch(() => ({ | ||
| submissions: [], | ||
| totals: { total: 0, reviewed: 0, pending: 0 }, | ||
| })), |
There was a problem hiding this comment.
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.
| cohostGetRevenueStatsFn({ | ||
| data: { competitionId: params.competitionId, competitionTeamId }, | ||
| }).catch(() => ({ stats: { totalGrossCents: 0, totalOrganizerNetCents: 0, purchaseCount: 0 } })), | ||
| }).catch(() => ({ | ||
| stats: { | ||
| totalGrossCents: 0, | ||
| totalOrganizerNetCents: 0, | ||
| purchaseCount: 0, | ||
| }, | ||
| })), |
There was a problem hiding this comment.
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.
| /** | ||
| * 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" |
There was a problem hiding this comment.
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(usesuseState,useEffect,useServerFn)cohost/divisions.tsx(usesuseServerFn,Route.useLoaderData)cohost/pricing.tsx(usesuseServerFn,Route.useLoaderData)cohost/results.tsx(usesuseRouter,useServerFn,useCallback,Route.useLoaderData)cohost/revenue.tsx(usesRoute.useLoaderData)cohost/scoring.tsx(usesuseServerFn,Route.useLoaderData)cohost/coupons.tsx(usesuseServerFn,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
| /** | ||
| * 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" |
There was a problem hiding this comment.
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 PageAs 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
| /** | ||
| * 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> | ||
| ) | ||
| } |
There was a problem hiding this comment.
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.
| /** | |
| * 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
| {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} | ||
| /> |
There was a problem hiding this comment.
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.
| /** | ||
| * 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> | ||
| ) | ||
| } |
There was a problem hiding this comment.
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
|
Addressed the CodeRabbit review in e88cf60. Disposition of all 17 actionable comments: Fixed (e88cf60):
Skipped — wrong framework assumption (the three "Critical" Skipped — pre-existing behavior preserved deliberately (the PR's contract was byte-for-byte loader/body preservation):
Verification on the new head: repo-wide type-check clean, all 2,820 tests pass, Biome clean, Generated by Claude Code |
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
CohostSidebardeleted;CompetitionSidebarnow serves both dashboards. Each nav item declares an optionalcohostPermissionkey (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.CompetitionDashboardShellrendered by both layout routes — cohosts now get the organizer breadcrumb andCompetitionHeader(with Edit / Go to Series hidden, since those target organizer-only routes).Shared page bodies
organizer/$competitionId/-pages/.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.Permission gates (preserved, now prop-driven)
editRegistrationsgates the Add Registration button, row action menus/column, Form Questions tab, and thetab=registration-rules→athletescoercioneditEvents/revenuegate overview quick-action and revenue cards; pricing/revenue/coupons loader redirects unchangedrequireCohostPermissionDrift fixes cohosts gain
Impact
-pages/)Verification
pnpm type-checkclean repo-wideloaderData!assertion pattern)lat checkpasses;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
organizer/$competitionId/-pages/, while keeping cohost loaders and permissioned callbacks.CompetitionDashboardShellused by both layouts; mergedCohostSidebarintoCompetitionSidebarwith permission-based filtering and organizer-only actions hidden viashowOrganizerActions.revenueis granted (targets cohost revenue). Cohost pricing loader now runs in parallel to match organizer.-pages/.Bug Fixes
videoIndexand divisionteamSizefor stable grouped primary-row selection; partial-partner-videos badge now correct.registrationAllReviewedis accurate under filters.submissionIdchanges; cohostverifyScorefails fast if round adjustments are supplied.Written for commit e88cf60. Summary will update on new commits.
Summary by CodeRabbit