Skip to content

feat: redesign public competitions page#360

Merged
theianjones merged 4 commits into
mainfrom
feat/compete-page-redesign
Mar 25, 2026
Merged

feat: redesign public competitions page#360
theianjones merged 4 commits into
mainfrom
feat/compete-page-redesign

Conversation

@theianjones

@theianjones theianjones commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replaces the list-based competitions page with a card grid layout featuring profile images (or deterministic gradient fallbacks with initials)
  • Adds status filter tabs (All, Open, Live, Upcoming, Past) synced to URL params
  • Adds collapsible advanced filters panel: location, organizer, competition type
  • Search input uses useTransition + useDeferredValue for lag-free typing
  • Staggered card entrance animation with prefers-reduced-motion support
  • Accessibility improvements: aria-labels, semantic input types, sr-only text

Test plan

  • Verify competitions page loads with card grid layout
  • Test status filter tabs filter correctly and update URL
  • Test advanced filters (location, organizer, type) filter correctly
  • Test search input is responsive (no lag on keystroke)
  • Verify cards show profile images when available, gradient fallback otherwise
  • Test on mobile: cards stack single column, reduced padding
  • Test dark mode rendering
  • Verify prefers-reduced-motion disables animations

🤖 Generated with Claude Code


Summary by cubic

Redesigned the public competitions page into a responsive card grid with status tabs, fast search, and advanced filters; state syncs to URL params for shareable views. Also fixes minor accessibility and lint issues without changing behavior.

  • New Features

    • Card grid with profile images and deterministic gradient fallbacks with initials
    • Status tabs (All, Open, Live, Upcoming, Past) synced to filter in the URL, with counts
    • Collapsible advanced filters (location, organizer, type) synced to URL
    • Fast search using useTransition + useDeferredValue for lag-free typing
    • Staggered card entrance animation with prefers-reduced-motion support
  • Accessibility & UX

    • Cards use @tanstack/react-router Link for client-side navigation and preloading
    • Semantic search input with type="search", name, autoComplete="off", spellCheck={false}, clear button, and ARIA label
    • Correct tablist roles with buttons, improved focus styles, and ARIA hints
    • Empty state with a single “Clear all filters” action
    • Reduced motion setting now disables card animations even when inline animation styles are present

Written for commit 75672a1. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Competition cards with status, dates, location, organizer and avatar handling.
    • Replaced list view with a responsive grid, status tabs, validated filters (status, location, organizer, type), advanced filters, and an empty-state with “Clear all filters”.
  • UI/UX

    • Improved search input accessibility, placeholder, autocomplete/spellcheck behavior.
    • Smooth card entrance animation with reduced-motion support and refined sorting (including mobile sort behavior tweaks).
  • Documentation

    • Clarified series-event template behavior and routing; updated registration flow references.

… filters

Replaces the utilitarian list view with a card-based grid layout featuring
profile images, status-aware filter tabs, text search with deferred updates,
and advanced filters (location, organizer, type). Cards show competition
logo or deterministic gradient fallback with initials.

- New CompetitionCard component with profile image / gradient fallback
- Status filter tabs (All, Open, Live, Upcoming, Past) synced to URL
- Advanced filters panel (location, organizer, competition type)
- Search input with useTransition + useDeferredValue for responsiveness
- Staggered card entrance animation with prefers-reduced-motion support
- Accessibility fixes: aria-labels, semantic search input, sr-only text
- Card enter keyframe animation in styles.css

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 22, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds a client-side CompetitionCard component, tweaks competition search accessibility, replaces the /compete listing with a status-filtered, advanced-filterable grid (new validated search params: filter, location, organizer, type), introduces a card entrance animation, and updates series-event-templates documentation.

Changes

Cohort / File(s) Summary
Competition Card
apps/wodsmith-start/src/components/competition-card.tsx
New exported CompetitionCard component: navigates to /compete/$slug, displays profile image or deterministic gradient initials, computes locationBadge, maps status to styled status pill, formats UTC-safe date ranges, and applies index-based card-enter animation delay.
Compete route & Search UI
apps/wodsmith-start/src/routes/compete/index.tsx, apps/wodsmith-start/src/components/competition-search.tsx
Replaced legacy past param with validated filter enum (all,registration-open,active,upcoming,past); added optional location, organizer, type params. Overhauled UI to header + responsive grid of CompetitionCards, added deferred q-state, advanced filters (Selects), status-priority sorting, tab counts, empty-state with “Clear all filters”. Search input: added name="competition-search", changed placeholder punctuation, autoComplete="off", spellCheck={false}, and SR-only clear label.
Styling / Animation
apps/wodsmith-start/src/styles.css
Added --animate-card-enter and @keyframes card-enter (opacity + translateY) and prefers-reduced-motion override to disable motion.
Leaderboard minor edits
apps/wodsmith-start/src/components/competition-leaderboard-table.tsx, apps/wodsmith-start/src/components/online-competition-leaderboard-table.tsx, apps/wodsmith-start/src/components/series-leaderboard-table.tsx
Small local changes: removed explicit = undefined initializers for currentParentId in two leaderboard tables; adjusted mobile sort dropdown desc expression in series leaderboard. No API changes.
Server annotations
apps/wodsmith-start/src/server-fns/registration-fns.ts
Updated @lat documentation annotation paths for two exported server functions; removed accumulation of totalPlatformFeeCents local variable (no public API change).
Documentation
lat.md/series-event-templates.md
Clarified previewSyncEventsToCompetitionsFn diff output, enumerated series_event_mappings keys, documented saveSeriesEventMappingsFn full-replace behavior, and noted organizer-dashboard route nesting.

Sequence Diagram(s)

(Skipped.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped in with a card so neat,
Filters lined up and tabs in a seat,
Status pills shimmer, avatars glow,
Dates kept in UTC, the results on show,
A tiny rabbit cheers — let competitions grow!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: redesign public competitions page' directly and accurately summarizes the main change—a comprehensive redesign of the competitions page from a list-based layout to a responsive card-grid with new filtering and search capabilities.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/compete-page-redesign

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

4 issues found across 5 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="lat.md/series-event-templates.md">

<violation number="1" location="lat.md/series-event-templates.md:81">
P3: This sentence misstates the `series_event_mappings` schema. The table has an `id` primary key; the four listed fields are columns, not four keys.</violation>

<violation number="2" location="lat.md/series-event-templates.md:116">
P2: The route examples in this section still omit the `/compete/organizer` prefix, so readers will copy incorrect URLs from the docs.</violation>
</file>

<file name="apps/wodsmith-start/src/components/competition-card.tsx">

<violation number="1" location="apps/wodsmith-start/src/components/competition-card.tsx:122">
P2: Use TanStack Router's `Link` for this internal competition route. The raw `<a href>` forces a full page reload instead of client-side navigation.</violation>
</file>

<file name="apps/wodsmith-start/src/routes/compete/index.tsx">

<violation number="1" location="apps/wodsmith-start/src/routes/compete/index.tsx:146">
P2: Keep the controlled search input synchronized with `Route.useSearch()` changes; otherwise the field can display a stale query after URL-driven navigation.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


## Routes

All series event template routes are nested under the organizer dashboard.

@cubic-dev-ai cubic-dev-ai Bot Mar 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The route examples in this section still omit the /compete/organizer prefix, so readers will copy incorrect URLs from the docs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lat.md/series-event-templates.md, line 116:

<comment>The route examples in this section still omit the `/compete/organizer` prefix, so readers will copy incorrect URLs from the docs.</comment>

<file context>
@@ -109,6 +113,8 @@ All defined in `src/server-fns/series-event-template-fns.ts`:
 
 ## Routes
 
+All series event template routes are nested under the organizer dashboard.
+
 - `/series/{groupId}/events` — Layout route with `<Outlet />`
</file context>
Fix with Cubic

Comment thread apps/wodsmith-start/src/components/competition-card.tsx Outdated
const showPast = past === true
const activeStatus = search.filter ?? "all"
const [, startTransition] = useTransition()
const [localSearch, setLocalSearch] = useState(search.q ?? "")

@cubic-dev-ai cubic-dev-ai Bot Mar 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Keep the controlled search input synchronized with Route.useSearch() changes; otherwise the field can display a stale query after URL-driven navigation.

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/index.tsx, line 146:

<comment>Keep the controlled search input synchronized with `Route.useSearch()` changes; otherwise the field can display a stale query after URL-driven navigation.</comment>

<file context>
@@ -33,147 +59,502 @@ export const Route = createFileRoute("/compete/")({
-  const showPast = past === true
+  const activeStatus = search.filter ?? "all"
+  const [, startTransition] = useTransition()
+  const [localSearch, setLocalSearch] = useState(search.q ?? "")
+  const deferredSearchQuery = useDeferredValue(search.q)
+  const [showAdvanced, setShowAdvanced] = useState(
</file context>
Fix with Cubic

### Persistence

Mappings are stored in `series_event_mappings` (groupId, competitionId, competitionEventId, templateEventId). `saveSeriesEventMappingsFn` does a full replace — deletes all existing mappings for the group, then inserts the new set atomically in a transaction.
Mappings are stored in `series_event_mappings` with four keys: groupId, competitionId, competitionEventId, templateEventId.

@cubic-dev-ai cubic-dev-ai Bot Mar 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: This sentence misstates the series_event_mappings schema. The table has an id primary key; the four listed fields are columns, not four keys.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lat.md/series-event-templates.md, line 81:

<comment>This sentence misstates the `series_event_mappings` schema. The table has an `id` primary key; the four listed fields are columns, not four keys.</comment>

<file context>
@@ -76,7 +78,9 @@ Each template event can only be claimed once (no duplicate mappings).
 ### Persistence
 
-Mappings are stored in `series_event_mappings` (groupId, competitionId, competitionEventId, templateEventId). `saveSeriesEventMappingsFn` does a full replace — deletes all existing mappings for the group, then inserts the new set atomically in a transaction.
+Mappings are stored in `series_event_mappings` with four keys: groupId, competitionId, competitionEventId, templateEventId.
+
+`saveSeriesEventMappingsFn` does a full replace — deletes all existing mappings for the group, then inserts the new set atomically in a transaction.
</file context>
Suggested change
Mappings are stored in `series_event_mappings` with four keys: groupId, competitionId, competitionEventId, templateEventId.
Mappings are stored in `series_event_mappings` with an `id` primary key plus four columns: groupId, competitionId, competitionEventId, templateEventId.
Fix with Cubic

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/wodsmith-start/src/components/competition-card.tsx`:
- Around line 122-123: Replace the raw anchor used for navigation with TanStack
Router's Link: import Link from the router and swap the <a
href={`/compete/${competition.slug}`}> usage in the CompetitionCard component
with <Link to="/compete/$slug" params={{ slug: competition.slug }}> preserving
the same children and classes/props; ensure the import for Link is added at the
top of the file (competition-card.tsx) and remove or update the old href-based
anchor so client-side navigation and router preloading work.

In `@apps/wodsmith-start/src/routes/compete/index.tsx`:
- Around line 118-123: The "all" filter currently excludes past competitions (it
returns status !== "past"), causing the label/count to be misleading; update the
filter logic so when filter === "all" it returns true (include every status) or,
if you prefer the existing behavior, rename the tab/label to reflect "non-past";
modify the conditional block that checks filter and status (the expressions with
filter === "all", "past", "active", "registration-open", "upcoming") so the
"all" branch includes past (or change the UI label/counter logic accordingly),
and apply the same change to the duplicate block that mirrors this logic
elsewhere in the file (the other conditional set using filter and status).
- Around line 352-360: The search input currently uses placeholder text only and
type="text"; change it to a real accessible search control by setting
type="search" and adding an accessible name (either add aria-label="Search
competitions" on the Input or add a visible/visually-hidden <label> tied via
id/htmlFor). Update the Input JSX where value={localSearch} and onChange={(e) =>
handleSearchChange(e.target.value)} to include the new aria-label or id so
assistive tech can announce the field properly.
- Around line 145-169: The current dual state (localSearch + URL search.q) and
manual navigation in handleSearchChange cause stale input vs URL mismatches and
create history entries per keystroke; replace this pattern by using NUQS:
install nuqs, wrap the root route with <NuqsAdapter />
(nuqs/adapters/tanstack-router), then remove localSearch, setLocalSearch,
deferredSearchQuery, and updateSearch logic and instead use useQueryState to
bind the q param to the input; update handleSearchChange to write to the
useQueryState setter (which will handle replace/merge semantics) and update the
"Clear all filters" handler to clear the same useQueryState keys so URL and
input remain single source of truth (refer to localSearch, handleSearchChange,
updateSearch, and deferredSearchQuery to find the code to remove/replace).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2116c5da-9875-49c5-8ca0-7aeb33f44d2d

📥 Commits

Reviewing files that changed from the base of the PR and between a672923 and f7b68e6.

📒 Files selected for processing (5)
  • apps/wodsmith-start/src/components/competition-card.tsx
  • apps/wodsmith-start/src/components/competition-search.tsx
  • apps/wodsmith-start/src/routes/compete/index.tsx
  • apps/wodsmith-start/src/styles.css
  • lat.md/series-event-templates.md

Comment thread apps/wodsmith-start/src/components/competition-card.tsx Outdated
Comment on lines +118 to +123
if (filter === "all") return status !== "past"
if (filter === "past") return status === "past"
if (filter === "active") return status === "active"
if (filter === "registration-open") return status === "registration-open"
if (filter === "upcoming")
return status === "coming-soon" || status === "registration-closed"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

All doesn't actually mean all right now.

The all branch filters out past, and the counter does the same. That makes the label/count misleading and hides completed competitions from the default view. Either include past in all, or rename the tab to match the behavior.

🧭 Suggested fix if `All` should include every status
 function matchesStatusFilter(
   status: CompetitionStatus,
   filter: StatusFilter,
 ): boolean {
-  if (filter === "all") return status !== "past"
+  if (filter === "all") return true
   if (filter === "past") return status === "past"
   if (filter === "active") return status === "active"
   if (filter === "registration-open") return status === "registration-open"
   if (filter === "upcoming")
     return status === "coming-soon" || status === "registration-closed"
   return true
 }
@@
     }
     for (const comp of competitionsWithStatus) {
-      if (comp._status !== "past") c.all++
+      c.all++
       if (comp._status === "registration-open") c["registration-open"]++
       if (comp._status === "active") c.active++
       if (

Also applies to: 208-217

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/wodsmith-start/src/routes/compete/index.tsx` around lines 118 - 123, The
"all" filter currently excludes past competitions (it returns status !==
"past"), causing the label/count to be misleading; update the filter logic so
when filter === "all" it returns true (include every status) or, if you prefer
the existing behavior, rename the tab/label to reflect "non-past"; modify the
conditional block that checks filter and status (the expressions with filter ===
"all", "past", "active", "registration-open", "upcoming") so the "all" branch
includes past (or change the UI label/counter logic accordingly), and apply the
same change to the duplicate block that mirrors this logic elsewhere in the file
(the other conditional set using filter and status).

Comment on lines +145 to +169
const [, startTransition] = useTransition()
const [localSearch, setLocalSearch] = useState(search.q ?? "")
const deferredSearchQuery = useDeferredValue(search.q)
const [showAdvanced, setShowAdvanced] = useState(
Boolean(search.location || search.organizer || search.type),
)

// Handlers for search state updates
const handleSearchChange = (value: string) => {
navigate({
search: (prev) => ({ ...prev, q: value || undefined }),
const updateSearch = (updates: Partial<CompeteSearch>) => {
startTransition(() => {
navigate({
search: (prev) => {
const next = { ...prev, ...updates }
// Clean undefined values
for (const key of Object.keys(next) as (keyof CompeteSearch)[]) {
if (next[key] === undefined) delete next[key]
}
return next
},
})
})
}

const handleShowPastChange = (value: boolean) => {
navigate({
search: (prev) => ({ ...prev, past: value || undefined }),
})
const handleSearchChange = (value: string) => {
setLocalSearch(value)
updateSearch({ q: value || undefined })

@coderabbitai coderabbitai Bot Mar 22, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

git ls-files apps/wodsmith-start/src/routes/compete/index.tsx

Repository: wodsmith/thewodapp

Length of output: 110


🏁 Script executed:

find . -type f -name "package.json" | head -5

Repository: wodsmith/thewodapp

Length of output: 244


🏁 Script executed:

wc -l apps/wodsmith-start/src/routes/compete/index.tsx

Repository: wodsmith/thewodapp

Length of output: 114


🏁 Script executed:

cat -n apps/wodsmith-start/src/routes/compete/index.tsx | sed -n '140,175p'

Repository: wodsmith/thewodapp

Length of output: 1467


🏁 Script executed:

cat -n apps/wodsmith-start/src/routes/compete/index.tsx | sed -n '545,560p'

Repository: wodsmith/thewodapp

Length of output: 606


🏁 Script executed:

find . -name "package.json" -path "*/apps/wodsmith-start/*"

Repository: wodsmith/thewodapp

Length of output: 96


🏁 Script executed:

cat apps/wodsmith-start/package.json | grep -A 5 -B 5 "nuqs\|dependencies"

Repository: wodsmith/thewodapp

Length of output: 660


🏁 Script executed:

rg "nuqs" --type ts --type tsx

Repository: wodsmith/thewodapp

Length of output: 89


🏁 Script executed:

rg "CompeteSearch" --type ts --type tsx -A 3

Repository: wodsmith/thewodapp

Length of output: 89


🏁 Script executed:

rg "nuqs" --glob "*.ts" --glob "*.tsx"

Repository: wodsmith/thewodapp

Length of output: 44


🏁 Script executed:

rg "CompeteSearch" --glob "*.ts" --glob "*.tsx" -A 3

Repository: wodsmith/thewodapp

Length of output: 1527


🏁 Script executed:

cat apps/wodsmith-start/package.json | jq '.dependencies | keys[]' | grep -i nuqs

Repository: wodsmith/thewodapp

Length of output: 44


🏁 Script executed:

cat apps/wodsmith-start/package.json | jq '.dependencies'

Repository: wodsmith/thewodapp

Length of output: 3218


🏁 Script executed:

rg "useSearch|useNavigate" --glob "*.ts" --glob "*.tsx" apps/wodsmith-start/src/routes/ -l | head -10

Repository: wodsmith/thewodapp

Length of output: 863


🏁 Script executed:

find apps/wodsmith-start/src/routes -name "*.tsx" -o -name "*.ts" | head -10

Repository: wodsmith/thewodapp

Length of output: 593


🏁 Script executed:

cat -n apps/wodsmith-start/src/routes/compete/index.tsx | head -30

Repository: wodsmith/thewodapp

Length of output: 1309


🏁 Script executed:

cat -n apps/wodsmith-start/src/routes/compete/index.tsx | sed -n '25,60p'

Repository: wodsmith/thewodapp

Length of output: 1389


🌐 Web query:

NUQS TanStack Router compatibility v1

💡 Result:

Yes, NUQS (nuqs) is compatible with TanStack Router v1. Official documentation confirms support for @tanstack/react-router@^1 via the nuqs/adapters/tanstack-router adapter. Key details: - Wrap your root route with from 'nuqs/adapters/tanstack-router'. - Support added in nuqs v2.5 (experimental initially), matured through fixes like serialization (PR #1128) and trailing slash navigation (Issue #1215, fixed in v2.8.1). - Limited type-safe integration via Standard Schema for trivial types (strings, numbers, booleans, JSON); complex serialization differences limit full TSR type-safety. - Does not yet support TanStack Start. - Installation: npm install nuqs, requires React ^18.3 or ^19. Example usage: import { NuqsAdapter } from 'nuqs/adapters/tanstack-router' import { Outlet, createRootRoute } from '@tanstack/react-router' export const Route = createRootRoute({ component: => ( ), }) Use hooks like useQueryState as usual in components.

Citations:


Implement NUQS for URL search parameter state management.

The dual state setup (localSearch and search.q) allows URL-driven changes like back/forward navigation or deep-linked queries to clear the results while leaving stale text in the search field. Additionally, handleSearchChange navigates on every keystroke, which creates a history entry per character typed unless using replace mode. Using NUQS with the TanStack Router adapter will unify input state and URL params into one source of truth.

This project's coding guidelines require "Use NUQS for URL search parameter state management" for TypeScript/TSX files. After installing NUQS, wrap the root route with <NuqsAdapter /> from nuqs/adapters/tanstack-router, then replace the manual search state management with useQueryState hooks to synchronize the input field and URL automatically.

This applies to the search input at lines 145–169 and the "Clear all filters" button at line 554.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/wodsmith-start/src/routes/compete/index.tsx` around lines 145 - 169, The
current dual state (localSearch + URL search.q) and manual navigation in
handleSearchChange cause stale input vs URL mismatches and create history
entries per keystroke; replace this pattern by using NUQS: install nuqs, wrap
the root route with <NuqsAdapter /> (nuqs/adapters/tanstack-router), then remove
localSearch, setLocalSearch, deferredSearchQuery, and updateSearch logic and
instead use useQueryState to bind the q param to the input; update
handleSearchChange to write to the useQueryState setter (which will handle
replace/merge semantics) and update the "Clear all filters" handler to clear the
same useQueryState keys so URL and input remain single source of truth (refer to
localSearch, handleSearchChange, updateSearch, and deferredSearchQuery to find
the code to remove/replace).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We dont use NUQS anymore

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@theianjones, got it — my apologies for the noise. I'll remove the outdated NUQS guideline from the knowledge base now.

The current approach using TanStack Router's useSearch / useNavigate for URL search parameter state management looks fine then. Would you like me to add a learning to capture what you use instead (e.g. "Use TanStack Router's useSearch/useNavigate for URL search parameter state management"), so future reviews don't re-flag this pattern?


⛔ Learnings removed
Learnt from: CR
Repo: wodsmith/thewodapp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-22T22:10:54.989Z
Learning: Applies to **/*.{ts,tsx} : Use NUQS for URL search parameter state management

🧠 Learnings used
Learnt from: CR
Repo: wodsmith/thewodapp PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-22T22:10:54.989Z
Learning: Applies to **/*.{ts,tsx} : Use NUQS for URL search parameter state management

Comment thread apps/wodsmith-start/src/routes/compete/index.tsx
…bility

Replace raw <a href> with TanStack Router <Link> for client-side navigation
and preloading. Add aria-label and type="search" to competition search input.
Fix broken lat.md refs for registration sections.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

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/index.tsx">

<violation number="1" location="apps/wodsmith-start/src/routes/compete/index.tsx:353">
P2: `type="search"` adds a native clear button in Blink/WebKit, so this input now shows two competing clear controls.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +353 to +357
type="search"
aria-label="Search competitions"
name="competition-search"
placeholder="Search…"
value={localSearch}

@cubic-dev-ai cubic-dev-ai Bot Mar 25, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: type="search" adds a native clear button in Blink/WebKit, so this input now shows two competing clear controls.

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/index.tsx, line 353:

<comment>`type="search"` adds a native clear button in Blink/WebKit, so this input now shows two competing clear controls.</comment>

<file context>
@@ -350,7 +350,8 @@ function CompetePage() {
             />
             <Input
-              type="text"
+              type="search"
+              aria-label="Search competitions"
               name="competition-search"
</file context>
Fix with Cubic

Remove unused totalPlatformFeeCents variable, replace nav with div for
tablist role, remove useless undefined initializations, simplify ternary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
apps/wodsmith-start/src/routes/compete/index.tsx (2)

114-124: ⚠️ Potential issue | 🟠 Major

All still excludes completed competitions.

The filter and the counter both drop past, so the default tab label/count are misleading. This also makes a dataset with only past competitions render as empty.

Suggested fix
 function matchesStatusFilter(
   status: CompetitionStatus,
   filter: StatusFilter,
 ): boolean {
-  if (filter === "all") return status !== "past"
+  if (filter === "all") return true
   if (filter === "past") return status === "past"
   if (filter === "active") return status === "active"
   if (filter === "registration-open") return status === "registration-open"
   if (filter === "upcoming")
     return status === "coming-soon" || status === "registration-closed"
   return true
 }
@@
     }
     for (const comp of competitionsWithStatus) {
-      if (comp._status !== "past") c.all++
+      c.all++
       if (comp._status === "registration-open") c["registration-open"]++
       if (comp._status === "active") c.active++
       if (

Also applies to: 199-220

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/wodsmith-start/src/routes/compete/index.tsx` around lines 114 - 124, The
"all" branch in matchesStatusFilter currently excludes past competitions (if
(filter === "all") return status !== "past"), causing counts/tabs to be
misleading and datasets of only past comps to render empty; update
matchesStatusFilter so the "all" case returns true (include past) and
verify/align the duplicate implementation (the other matchesStatusFilter block)
to use the same behavior so both the filter and counter treat "all"
consistently.

146-169: ⚠️ Potential issue | 🟠 Major

Keep the search box synced with URL state, and don’t push history on every keystroke.

localSearch only hydrates from search.q once. After browser back/forward or Line 555’s navigate({ search: {} }), the list can clear while the input keeps stale text. This path also adds a history entry per character typed.

One way to tighten this up
   const [, startTransition] = useTransition()
   const [localSearch, setLocalSearch] = useState(search.q ?? "")
   const deferredSearchQuery = useDeferredValue(search.q)
+  useEffect(() => {
+    setLocalSearch(search.q ?? "")
+  }, [search.q])
@@
   const handleSearchChange = (value: string) => {
     setLocalSearch(value)
-    updateSearch({ q: value || undefined })
+    startTransition(() => {
+      navigate({
+        replace: true,
+        search: (prev) => {
+          const next = { ...prev, q: value || undefined }
+          if (next.q === undefined) delete next.q
+          return next
+        },
+      })
+    })
   }
// also update the React import at Line 1
import { useDeferredValue, useEffect, useMemo, useState, useTransition } from "react"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/wodsmith-start/src/routes/compete/index.tsx` around lines 146 - 169, The
input value isn't kept in sync with URL changes and you push a history entry on
every keystroke; fix by syncing localSearch from the URL and only pushing URL
updates from the deferred value (or using replace) so typing doesn't create
history entries: add a useEffect that sets setLocalSearch(search.q ?? "") when
search.q changes (to handle back/forward and external navigate calls), and
change the update path so the actual navigate call is triggered from an effect
that watches deferredSearchQuery (the useDeferredValue of search.q) or calls
navigate with { replace: true } when updating from the typing path; reference
localSearch/setLocalSearch, deferredSearchQuery, handleSearchChange,
updateSearch and navigate to locate where to apply these changes.
🧹 Nitpick comments (2)
apps/wodsmith-start/src/components/competition-card.tsx (1)

86-104: Prefer a named params object for formatDateRange.

startDate and endDate are the same type, so the current call site is easy to swap accidentally. A small params interface keeps the call explicit.

Possible cleanup
+interface FormatDateRangeParams {
+  startDate: string
+  endDate: string
+}
+
-function formatDateRange(startDate: string, endDate: string) {
+function formatDateRange({ startDate, endDate }: FormatDateRangeParams) {
   const start = new Date(startDate)
   const end = new Date(endDate)
const dateRange = formatDateRange({
  startDate: competition.startDate,
  endDate: competition.endDate,
})

As per coding guidelines, "Use named object parameters for functions with more than one parameter" and "Use TypeScript everywhere and prefer interfaces over types".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/wodsmith-start/src/components/competition-card.tsx` around lines 86 -
104, The function formatDateRange should accept a single named-params object to
avoid argument-order mistakes: change its signature to accept an object (e.g., {
startDate, endDate }) with a small interface (prefer interface
IFormatDateRangeParams { startDate: string; endDate: string }), update the
internal references (startDate/endDate -> params.startDate/params.endDate or
destructure in the parameter list) and adjust all callers (e.g., replace
formatDateRange(a,b) with formatDateRange({ startDate: a, endDate: b })); keep
the existing logic (including the isSameUTCDay call) intact.
apps/wodsmith-start/src/routes/compete/index.tsx (1)

28-34: Use an interface for CompeteSearch.

This is an object contract, so it can follow the repo’s TypeScript convention and stay consistent with the other prop/search shapes in this change.

Minimal change
-type CompeteSearch = {
+interface CompeteSearch {
   q?: string
   filter?: StatusFilter
   location?: string
   organizer?: string
   type?: "in-person" | "online"
-}
+}

As per coding guidelines, "Use TypeScript everywhere and prefer interfaces over types".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/wodsmith-start/src/routes/compete/index.tsx` around lines 28 - 34,
Replace the type alias CompeteSearch with an equivalent interface to follow the
repo convention: declare interface CompeteSearch { q?: string; filter?:
StatusFilter; location?: string; organizer?: string; type?: "in-person" |
"online"; } so the object contract remains identical; update any imports/usages
referencing CompeteSearch if needed but keep the same optional fields and the
union for the type property.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/wodsmith-start/src/components/competition-card.tsx`:
- Around line 126-135: The inline animation set via the style prop (style={{
animation: `card-enter 0.35s ease-out ${index * 50}ms backwards` }}) still runs
for users who prefer reduced motion; change the component to detect
prefers-reduced-motion (e.g., using window.matchMedia('(prefers-reduced-motion:
reduce)').matches or a useReducedMotion hook) and only set the style.animation
when reduced motion is false, otherwise omit the animation property (or set it
to 'none'); update the code that uses the inline style and the index-based
stagger so the animation is conditionally applied.

In `@apps/wodsmith-start/src/routes/compete/index.tsx`:
- Around line 299-341: The controls are simple list filters but currently use
tablist/tab semantics; change them to use standard toggle buttons by removing
role="tablist" on the <nav> and role="tab" and aria-selected on each <button>,
and instead add aria-pressed={isActive} to the <button> (keep the existing
aria-label on the nav and keyboard behavior untouched). Update the render that
references STATUS_TABS, activeStatus, counts, and updateSearch so each button
reflects isActive via aria-pressed and continues to call updateSearch when
clicked.

---

Duplicate comments:
In `@apps/wodsmith-start/src/routes/compete/index.tsx`:
- Around line 114-124: The "all" branch in matchesStatusFilter currently
excludes past competitions (if (filter === "all") return status !== "past"),
causing counts/tabs to be misleading and datasets of only past comps to render
empty; update matchesStatusFilter so the "all" case returns true (include past)
and verify/align the duplicate implementation (the other matchesStatusFilter
block) to use the same behavior so both the filter and counter treat "all"
consistently.
- Around line 146-169: The input value isn't kept in sync with URL changes and
you push a history entry on every keystroke; fix by syncing localSearch from the
URL and only pushing URL updates from the deferred value (or using replace) so
typing doesn't create history entries: add a useEffect that sets
setLocalSearch(search.q ?? "") when search.q changes (to handle back/forward and
external navigate calls), and change the update path so the actual navigate call
is triggered from an effect that watches deferredSearchQuery (the
useDeferredValue of search.q) or calls navigate with { replace: true } when
updating from the typing path; reference localSearch/setLocalSearch,
deferredSearchQuery, handleSearchChange, updateSearch and navigate to locate
where to apply these changes.

---

Nitpick comments:
In `@apps/wodsmith-start/src/components/competition-card.tsx`:
- Around line 86-104: The function formatDateRange should accept a single
named-params object to avoid argument-order mistakes: change its signature to
accept an object (e.g., { startDate, endDate }) with a small interface (prefer
interface IFormatDateRangeParams { startDate: string; endDate: string }), update
the internal references (startDate/endDate -> params.startDate/params.endDate or
destructure in the parameter list) and adjust all callers (e.g., replace
formatDateRange(a,b) with formatDateRange({ startDate: a, endDate: b })); keep
the existing logic (including the isSameUTCDay call) intact.

In `@apps/wodsmith-start/src/routes/compete/index.tsx`:
- Around line 28-34: Replace the type alias CompeteSearch with an equivalent
interface to follow the repo convention: declare interface CompeteSearch { q?:
string; filter?: StatusFilter; location?: string; organizer?: string; type?:
"in-person" | "online"; } so the object contract remains identical; update any
imports/usages referencing CompeteSearch if needed but keep the same optional
fields and the union for the type property.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: aac4f714-180e-4b4c-91ca-a3341cf71809

📥 Commits

Reviewing files that changed from the base of the PR and between f7b68e6 and a1ce4d1.

📒 Files selected for processing (3)
  • apps/wodsmith-start/src/components/competition-card.tsx
  • apps/wodsmith-start/src/routes/compete/index.tsx
  • apps/wodsmith-start/src/server-fns/registration-fns.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/wodsmith-start/src/server-fns/registration-fns.ts

Comment on lines +126 to +135
className={cn(
"group relative flex gap-4 rounded-lg border bg-card p-4 sm:p-5",
"transition-[border-color,box-shadow] duration-200",
"hover:border-primary/30 hover:shadow-md hover:shadow-primary/5",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"motion-reduce:transition-none",
)}
style={{
animation: `card-enter 0.35s ease-out ${index * 50}ms backwards`,
}}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The enter animation still runs for reduced-motion users.

motion-reduce:transition-none only disables transitions. The inline animation on Lines 133-135 still fires, so the staggered entrance effect is not actually disabled for prefers-reduced-motion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/wodsmith-start/src/components/competition-card.tsx` around lines 126 -
135, The inline animation set via the style prop (style={{ animation:
`card-enter 0.35s ease-out ${index * 50}ms backwards` }}) still runs for users
who prefer reduced motion; change the component to detect prefers-reduced-motion
(e.g., using window.matchMedia('(prefers-reduced-motion: reduce)').matches or a
useReducedMotion hook) and only set the style.animation when reduced motion is
false, otherwise omit the animation property (or set it to 'none'); update the
code that uses the inline style and the index-based stagger so the animation is
conditionally applied.

Comment on lines +299 to +341
<nav
className="flex items-center gap-0.5 -ml-2"
role="tablist"
aria-label="Filter competitions"
>
{STATUS_TABS.map((tab) => {
const isActive = activeStatus === tab.value
const count = counts[tab.value]
return (
<button
key={tab.value}
type="button"
role="tab"
aria-selected={isActive}
onClick={() =>
updateSearch({
filter: tab.value === "all" ? undefined : tab.value,
})
}
className={cn(
"relative px-3 py-1.5 text-sm rounded-md",
"transition-colors duration-100 motion-reduce:transition-none",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
isActive
? "font-semibold text-foreground bg-secondary"
: "font-medium text-muted-foreground hover:text-foreground hover:bg-secondary/50",
)}
>
{tab.label}
{count > 0 && (
<span
className={cn(
"ml-1.5 text-xs tabular-nums",
isActive
? "text-foreground/50"
: "text-muted-foreground/60",
)}
>
{count}
</span>
)}
</button>
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use pressed-state buttons here instead of ARIA tabs.

These controls filter one list; they do not implement roving focus, arrow-key navigation, or tabpanel relationships. The current tablist/tab roles can mislead assistive tech.

Safer semantics for this pattern
-        <nav
-          className="flex items-center gap-0.5 -ml-2"
-          role="tablist"
-          aria-label="Filter competitions"
-        >
+        <nav className="flex items-center gap-0.5 -ml-2" aria-label="Filter competitions">
@@
               <button
                 key={tab.value}
                 type="button"
-                role="tab"
-                aria-selected={isActive}
+                aria-pressed={isActive}
                 onClick={() =>
                   updateSearch({
                     filter: tab.value === "all" ? undefined : tab.value,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/wodsmith-start/src/routes/compete/index.tsx` around lines 299 - 341, The
controls are simple list filters but currently use tablist/tab semantics; change
them to use standard toggle buttons by removing role="tablist" on the <nav> and
role="tab" and aria-selected on each <button>, and instead add
aria-pressed={isActive} to the <button> (keep the existing aria-label on the nav
and keyboard behavior untouched). Update the render that references STATUS_TABS,
activeStatus, counts, and updateSearch so each button reflects isActive via
aria-pressed and continues to call updateSearch when clicked.

Add motion-reduce:!animate-none to override inline animation style
when user has reduced motion preference enabled.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@theianjones theianjones merged commit 108fb4f into main Mar 25, 2026
16 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant