6 GitHub Issues to Create
Issue 1: Use z.coerce for query string schema fields
Title: Use z.coerce for query string schema fields
Labels: bug, type-safety
Body:
Priority: High (data integrity issue)
Related: Copilot review - src/lib/searchSchemas.ts lines 17-22
Problem
filterSortSearchSchema uses z.number() and z.boolean() for query-string values (e.g. minRating, use24Hour, sortLocked). TanStack Router's default search parsing yields strings, so these fields will fail validation on page load/deep links and fall back to the .catch(...) defaults. Values don't round-trip correctly via the URL.
Current Code
minRating: z.number().catch(0),
use24Hour: z.boolean().catch(true),
sortLocked: z.boolean().catch(false),
Solution
Use z.coerce.number() and z.coerce.boolean() so values are properly parsed from query strings:
minRating: z.coerce.number().catch(0),
use24Hour: z.coerce.boolean().catch(true),
sortLocked: z.coerce.boolean().catch(false),
Testing
Navigate to sets page with query params: ?minRating=3&use24Hour=false
Verify params are preserved in URL
Verify filter state reflects the query params
Navigate away and back - params should persist
Files
src/lib/searchSchemas.ts
---
## Issue 2: Fix schedule route schema mismatch causing param loss
**Title:** Fix schedule route schema mismatch causing param loss
**Labels:** `bug`, `routing`
**Body:**
```markdown
**Priority:** High (data loss)
**Related:** Copilot review - `src/routes/festivals/$festivalSlug/editions/$editionSlug/schedule.tsx` lines 8-16
## Problem
The `/schedule` route validates search params with `filterSortSearchSchema`, but its children (`/schedule/timeline` and `/schedule/list`) validate with `timelineSearchSchema`. When redirecting from `/schedule` to `/schedule/timeline`, `location.search` will only include keys accepted by the parent route's `validateSearch`, so timeline-specific params like `day`, `time`, `view` can be dropped if someone lands on `/schedule?day=2024-06-15`.
### Current Code
```typescript
export const Route = createFileRoute(
"/festivals/$festivalSlug/editions/$editionSlug/schedule",
)({
component: ScheduleTab,
validateSearch: filterSortSearchSchema, // ❌ Wrong schema
beforeLoad: ({ params, location }) => {
if (location.pathname.endsWith("/schedule")) {
throw redirect({
to: "/festivals/$festivalSlug/editions/$editionSlug/schedule/timeline",
params,
search: location.search as Record<string, unknown>,
});
}
},
});
Solution
Switch to timelineSearchSchema so the redirect preserves all timeline search params:
export const Route = createFileRoute(
"/festivals/$festivalSlug/editions/$editionSlug/schedule",
)({
component: ScheduleTab,
validateSearch: timelineSearchSchema, // ✅ Use child schema
beforeLoad: ({ params, location }) => {
if (location.pathname.endsWith("/schedule")) {
throw redirect({
to: "/festivals/$festivalSlug/editions/$editionSlug/schedule/timeline",
params,
search: location.search as Record<string, unknown>,
});
}
},
});
Testing
Navigate directly to /schedule?day=2024-06-15&view=timeline
Verify redirect preserves all query params
Verify timeline displays the correct day/view
Files
src/routes/festivals/$festivalSlug/editions/$editionSlug/schedule.tsx
---
## Issue 3: Fix missing MouseEvent type import in FestivalSelection
**Title:** Fix missing MouseEvent type import in FestivalSelection
**Labels:** `bug`, `type-safety`
**Body:**
```markdown
**Priority:** Medium (type checking failure)
**Related:** Copilot review - `src/pages/FestivalSelection.tsx` lines 104-111
## Problem
`handleClick` uses the `React.MouseEvent` type, but this file doesn't import the `React` namespace (only `useEffect`). With the modern JSX transform, `React` isn't in scope automatically, so this will fail typechecking.
### Current Code
```typescript
import { useEffect } from "react"; // No React namespace import
function handleClick(e: React.MouseEvent) { // ❌ React not in scope
// ...
}
Solution
Import the needed type:
import { useEffect, type MouseEvent } from "react";
function handleClick(e: MouseEvent) {
// ...
}
Files
src/pages/FestivalSelection.tsx
---
## Issue 4: Simplify FestivalEditionProvider error handling
**Title:** Simplify FestivalEditionProvider error handling
**Labels:** `enhancement`, `ux`, `cleanup`
**Body:**
```markdown
**Related:** Copilot review comment #7
## Problem
`FestivalEditionProvider` has error handling code that:
1. Renders error UI
2. Navigates away via useEffect
3. Causes flash before redirect
4. Is likely unreachable due to route-level preloading in `beforeLoad`
The edition is preloaded in the route's `beforeLoad` via `ensureQueryData`. If preload fails, the route never renders (loader throws). The provider's error handling is unreachable in practice.
## Solution
- Remove `useEffect` that navigates on error (lines 53-62 in `FestivalEditionContext.tsx`)
- Remove error UI rendering (lines 64-77)
- Keep simple loading check: `if (editionQuery.isLoading) return <LoadingFallback />`
- Let route-level error boundaries handle actual errors
## Files
- `src/contexts/FestivalEditionContext.tsx`
Issue 5: Move EditionSelection auto-redirect to beforeLoad
Title: Move EditionSelection auto-redirect to beforeLoad
Labels: enhancement, ux
Body:
**Related:** Copilot review comment #8
## Problem
`EditionSelection` component renders "Redirecting..." state then navigates via `useEffect`, causing a flash before redirect when there's only one edition.
## Solution
Move redirect logic to route's `beforeLoad`:
1. Fetch editions via `queryClient.ensureQueryData`
2. If exactly 1 edition exists, `throw redirect({ ... })`
3. Component never renders when redirecting - no flash
### Implementation
```typescript
// In route file: src/routes/festivals/$festivalSlug/index.tsx
beforeLoad: async ({ params, context }) => {
const editions = await context.queryClient.ensureQueryData({
queryKey: editionsKeys.forFestival(params.festivalSlug),
queryFn: () => fetchEditionsForFestival(params.festivalSlug),
});
if (editions.length === 1) {
throw redirect({
to: "/festivals/$festivalSlug/editions/$editionSlug/sets",
params: { festivalSlug: params.festivalSlug, editionSlug: editions[0].slug },
});
}
}
Remove useEffect redirect from EditionSelection.tsx component.
Files
Create/update: src/routes/festivals/$festivalSlug/index.tsx
Update: src/pages/EditionSelection.tsx
---
## Issue 6: Move FestivalSelection auto-redirect to beforeLoad
**Title:** Move FestivalSelection auto-redirect to beforeLoad
**Labels:** `enhancement`, `ux`
**Body:**
```markdown
**Related:** Copilot review comment #9
## Problem
`FestivalSelection` has two issues:
1. `useEffect` calls `handleFestivalClick` but doesn't include it in deps (ESLint warning)
2. Duplicate subdomain logic in both effect and `FestivalCard.handleClick`
3. Renders content before redirecting when there's only one festival
## Solution
Move redirect logic to route `beforeLoad`:
1. Fetch festivals via `queryClient.ensureQueryData`
2. If exactly 1 festival exists, handle redirect there
3. Component never renders when redirecting - no flash
### Implementation
```typescript
// In route file: src/routes/index.tsx
beforeLoad: async ({ context }) => {
const festivals = await context.queryClient.ensureQueryData({
queryKey: festivalsKeys.all(),
queryFn: () => fetchFestivals(),
});
if (festivals.length === 1) {
// Fetch editions for the single festival
const editions = await context.queryClient.ensureQueryData({
queryKey: editionsKeys.forFestival(festivals[0].slug),
queryFn: () => fetchEditionsForFestival(festivals[0].slug),
});
if (editions.length === 1) {
throw redirect({
to: "/festivals/$festivalSlug/editions/$editionSlug/sets",
params: {
festivalSlug: festivals[0].slug,
editionSlug: editions[0].slug
},
});
} else {
throw redirect({
to: "/festivals/$festivalSlug",
params: { festivalSlug: festivals[0].slug },
});
}
}
}
Remove useEffect from FestivalSelection.tsx, keep click handler for user-initiated clicks.
Files
Update: src/routes/index.tsx
Update: src/pages/FestivalSelection.tsx
---
**Recommendation:** Create issues #1-3 first (high priority bugs), then #4-6 (UX improvements).
6 GitHub Issues to Create
Issue 1: Use z.coerce for query string schema fields
Title: Use z.coerce for query string schema fields
Labels: bug, type-safety
Body:
Priority: High (data integrity issue)
Related: Copilot review -
src/lib/searchSchemas.tslines 17-22Problem
filterSortSearchSchemausesz.number()andz.boolean()for query-string values (e.g.minRating,use24Hour,sortLocked). TanStack Router's default search parsing yields strings, so these fields will fail validation on page load/deep links and fall back to the.catch(...)defaults. Values don't round-trip correctly via the URL.Current Code