feat(schedule): add configurable day-start hour - #463
Conversation
Add festivals.day_start_hour (0-23, default 0) so a festival can fold post-midnight sets into the previous festival day instead of always splitting at exact midnight. Widens the existing day-key seam (getFestivalDayKey/formatDayOnly) and threads the cutoff through schedule grouping, the day filter, the days reveal level, and the horizontal timeline's day boundaries/jump targets. Admin can set the cutoff in the festival edit dialog. Closes #86
…lter Self-review follow-up on the day-start-hour change: extract the repeated "shift by cutoff" and "day-key -> boundary instant" logic into shared timeUtils helpers instead of duplicating it across three timeline modules, and make DayFilterSelect's day options honor the cutoff too (a pre-cutoff set on the edition's first calendar day can fold onto the previous day's key, which the filter dropdown needs to offer).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Deploy →
|
There was a problem hiding this comment.
🟡 Changes recommended
The current dayStartHour implementation in src/lib/timeUtils.ts shifts instants in UTC before timezone formatting, which can misclassify festival days around DST transitions.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds a per-festival day_start_hour cutoff (defaulting to midnight) so post-midnight sets can be grouped/labelled under the previous “festival day”, and threads that cutoff through schedule grouping, day filtering, reveal-level day labels, and the horizontal timeline’s day boundaries/jump behavior.
Changes:
- Adds
festivals.day_start_hour(0–23, default0) to the database schema and Supabase generated types, and wires it into festival create/update flows and the admin festival edit dialog. - Extends the “festival day key” seam (
getFestivalDayKey/formatDayOnly) and propagatesdayStartHourthrough schedule list/timeline grouping, timeline geometry/boundaries/jump targets, and active-day detection. - Reworks day-filter option generation to include a leading shifted day when a cutoff is configured, and adds unit tests across the updated time/day utilities.
File summaries
| File | Description |
|---|---|
| supabase/migrations/20260829160000_add_festival_day_start_hour.sql | Adds day_start_hour column + range check constraint on festivals. |
| src/routes/festivals/$festivalSlug/editions/$editionSlug/schedule/timeline.tsx | Passes festival.day_start_hour into schedule/timeline data and container. |
| src/routes/festivals/$festivalSlug/editions/$editionSlug/schedule/list.tsx | Threads dayStartHour through list schedule grouping and memo deps. |
| src/pages/SetDetails/SetInfoCard.tsx | Uses formatDayOnly(..., day_start_hour) for reveal-level day display. |
| src/pages/SetDetails/NonMusicSetDetail/NonMusicSetBanner.tsx | Uses formatDayOnly(..., day_start_hour) for reveal-level day display. |
| src/pages/SetDetails/MultiArtistSetInfoCard.tsx | Uses formatDayOnly(..., day_start_hour) for reveal-level day display. |
| src/pages/EditionView/tabs/VoteTab/SetCard/SetMetadata.tsx | Uses formatDayOnly(..., day_start_hour) for reveal-level day display. |
| src/pages/EditionView/tabs/ScheduleTab/horizontal/timeScaleGeometry.ts | Makes date-change detection use festival-day keys with dayStartHour. |
| src/pages/EditionView/tabs/ScheduleTab/horizontal/timeScaleGeometry.test.ts | Adds unit test for cutoff folding behavior in date-change detection. |
| src/pages/EditionView/tabs/ScheduleTab/horizontal/TimeScaleContainer.tsx | Adds dayStartHour prop and passes it to TimeScale. |
| src/pages/EditionView/tabs/ScheduleTab/horizontal/TimeScale.tsx | Passes dayStartHour to computeDateChanges. |
| src/pages/EditionView/tabs/ScheduleTab/horizontal/TimelineToolbar.tsx | Threads dayStartHour into day jump controls. |
| src/pages/EditionView/tabs/ScheduleTab/horizontal/TimelineOverview.tsx | Threads dayStartHour into day-boundary calculation. |
| src/pages/EditionView/tabs/ScheduleTab/horizontal/TimelineContainer.tsx | Adds dayStartHour prop and threads into scroll sync / overview / scale. |
| src/pages/EditionView/tabs/ScheduleTab/horizontal/DayJumpButtons.tsx | Passes dayStartHour into getDayJumpMoment. |
| src/pages/EditionView/tabs/ScheduleTab/DayFilterSelect.tsx | Switches to buildDayFilterOptions(...) and includes festival cutoff. |
| src/pages/admin/festivals/FestivalDialog.tsx | Adds admin UI input for day_start_hour with clamping (0–23). |
| src/lib/timeUtils.ts | Extends festival-day key/label utilities and introduces festivalDayStart. |
| src/lib/timeUtils.test.ts | Adds tests for cutoff behavior in getFestivalDayKey and formatDayOnly. |
| src/lib/timelineOverviewGeometry.ts | Uses festivalDayStart for boundary placement with cutoff support. |
| src/lib/timelineOverviewGeometry.test.ts | Adds/updates tests for cutoff-aware boundary placement. |
| src/lib/timelineMountMoment.ts | Makes timeline mount moment resolve day-start using cutoff-aware boundary. |
| src/lib/timelineMountMoment.test.ts | Adds tests for cutoff-aware mount moment behavior. |
| src/lib/timelineDayJump.ts | Makes day jump fallback use cutoff-aware day start instead of midnight. |
| src/lib/timelineDayJump.test.ts | Adds test for cutoff-aware day jump fallback. |
| src/lib/dayFilterOptions.ts | New helper to build day-filter options, adding leading shifted day if needed. |
| src/lib/dayFilterOptions.test.ts | Unit tests for new day-filter option builder behavior. |
| src/integrations/supabase/types.ts | Updates generated DB types to include festivals.day_start_hour. |
| src/hooks/useTimelineScrollSync.ts | Threads dayStartHour into timeline mount-moment resolution. |
| src/hooks/useScheduleData.ts | Uses cutoff-aware festival day keys when grouping sets into schedule days. |
| src/hooks/useActiveTimelineDay.ts | Uses cutoff-aware day jump moments when computing active day offsets. |
| src/api/festivals/useUpdateFestival.ts | Allows updating day_start_hour via mutation typing. |
| src/api/festivals/useCreateFestival.ts | Allows setting day_start_hour on create via mutation typing. |
| docs/adr/0002-festival-timezone-display.md | Updates ADR to reflect the now-implemented configurable cutoff. |
Review details
Suppressed comments (1)
src/lib/timeUtils.ts:208
dayStartHouris defined in the festival's timezone, but the current implementation shifts the UTC instant withsubHoursbefore formatting in the target timezone. Around DST transitions this can produce the wrong festival-day key (e.g., after a spring-forward, subtracting N hours in UTC can subtract N±1 hours in local time), misgrouping sets and timeline day boundaries.
// festival's day rather than the viewer's. `dayStartHour` (0-23, the
// festival's configured day-start cutoff) shifts the instant back by that
// many hours first, so sets before the cutoff fold into the previous
// festival day instead of splitting at exact midnight.
- Files reviewed: 34/34 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if (!isValid(date)) return null; | ||
| const shifted = shiftForDayStart(date, dayStartHour); | ||
| const dayFormat = "EEE, MMM d"; | ||
| if (timezone) return formatInTimeZone(date, timezone, dayFormat); | ||
| return format(date, dayFormat); | ||
| if (timezone) return formatInTimeZone(shifted, timezone, dayFormat); | ||
| return format(shifted, dayFormat); |
Playwright test resultsDetails
|
Adds
festivals.day_start_hour(0-23, default 0) so a festival can fold post-midnight sets into the previous festival day instead of always splitting at exact midnight. Widens the existing day-key seam (getFestivalDayKey/formatDayOnly) and threads the cutoff through schedule grouping, the day filter, thedaysreveal level, and the horizontal timeline's day boundaries/jump targets.Closes #86
Verification
0(default): schedule list, timeline, day filter, and reveal-level day labels render identically to before.6in the festival edit dialog: a ~02:00 set moves under the previous festival day in both the list and the horizontal timeline; the day filter and thedays-reveal display agree; the day header reads the previous date.pnpm run typecheck,pnpm run lint,pnpm exec vitest run, andpnpm run buildall pass.Note: the issue's spec mentioned following "the existing react-hook-form pattern" in the festival edit dialog, but that dialog (
FestivalDialog.tsx) actually uses plainuseState, not react-hook-form — the new field follows the file's actual existing pattern instead.Generated by Claude Code