Conversation
Adds a local-date parser and normalizes date handling across the app to avoid UTC shifts when parsing YYYY-MM-DD strings. Normalizes API responses to real Date objects in hooks and UI, updates server/export code to treat stored timestamps as Dates, and unifies comparisons/formatting so calendar, notes, PDF and ICS exports behave consistently. Prevents incorrect day drift and ensures reliable date-based filtering, grouping, and display.
📝 WalkthroughWalkthroughStandardizes local-date parsing and normalization: adds Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
📜 Recent review detailsConfiguration used: defaults Review profile: CHILL Plan: Pro 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
🧰 Additional context used📓 Path-based instructions (1)**/*.{ts,tsx}📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Files:
🧠 Learnings (5)📓 Common learnings📚 Learning: 2026-01-03T02:03:48.622ZApplied to files:
📚 Learning: 2026-01-03T02:03:48.622ZApplied to files:
📚 Learning: 2026-01-03T02:03:48.622ZApplied to files:
📚 Learning: 2026-01-03T02:03:48.622ZApplied to files:
🧬 Code graph analysis (1)lib/event-utils.ts (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
🔇 Additional comments (2)
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. Comment |
There was a problem hiding this comment.
Pull request overview
This PR fixes timezone parsing issues for local dates by introducing a parseLocalDate utility function that interprets YYYY-MM-DD strings as local dates rather than UTC. The changes normalize date handling across the application, update API responses to use real Date objects, and remove deprecated backward-compatibility functions.
Changes:
- Adds
parseLocalDatefunction to parse YYYY-MM-DD strings as local dates (avoiding UTC shift bugs) - Introduces
normalizeShiftandnormalizeNotehelpers to convert API responses to Date objects - Updates all date comparisons and filtering to use Date objects consistently
- Removes deprecated functions:
getCurrentVersionSync,resetRateLimit,getRateLimiterStats,isValidICloudUrl,findEventForDate,findNoteForDate
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/date-utils.ts | Adds parseLocalDate function to parse YYYY-MM-DD strings as local dates |
| lib/version.ts | Removes deprecated getCurrentVersionSync function |
| lib/rate-limiter.ts | Removes deprecated resetRateLimit and getRateLimiterStats functions |
| lib/external-calendar-utils.ts | Updates date comparison to use formatDateToLocal and removes isValidICloudUrl |
| lib/event-utils.ts | Updates to handle Date objects directly and removes legacy functions |
| hooks/useShifts.ts | Adds normalizeShift helper and updates to use parseLocalDate |
| hooks/useNotes.ts | Adds and exports normalizeNote helper using parseLocalDate |
| hooks/useShiftActions.ts | Updates to handle both Date and string inputs with parseLocalDate |
| components/shifts-list.tsx | Updates to treat dates as Date objects (removes new Date() wrapping) |
| components/calendar-grid.tsx | Updates date filtering to use Date objects directly |
| app/page.tsx | Comprehensive updates for date handling with normalization and parsing |
| app/api/shifts/route.ts | Updates to use parseLocalDate for query date filtering |
| app/api/shifts/[id]/route.ts | Updates shift updates to use parseLocalDate |
| app/api/notes/route.ts | Updates to use parseLocalDate for query date filtering |
| app/api/calendars/[id]/export/pdf/route.ts | Updates to treat dates as Date objects in PDF export |
| app/api/calendars/[id]/export/ics/route.ts | Updates ICS export to use formatDateToLocal and treat dates as Date objects |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
lib/event-utils.ts (1)
72-98:new Date(note.date)reintroduces the UTC parsing bug forYYYY-MM-DDstrings—useparseLocalDatefor that case.This is the exact scenario the PR is fixing. If notes can arrive as
"YYYY-MM-DD", parsing withnew Date(...)can shift the day depending on timezone.Proposed fix
import { CalendarNote } from "./db/schema"; +import { parseLocalDate } from "./date-utils"; +function coerceNoteDate(value: unknown): Date | null { + if (value instanceof Date) return value; + if (typeof value === "string") { + if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return parseLocalDate(value); + const d = new Date(value); + return Number.isNaN(d.getTime()) ? null : d; + } + return null; +} export function findEventsForDate( notes: CalendarNote[], date: Date ): CalendarNote[] { return notes.filter((note) => { if (note.type !== "event" || !note.date) return false; - const noteDate = note.date as Date; + const noteDate = coerceNoteDate(note.date); + if (!noteDate) return false; ... export function findNotesForDate( notes: CalendarNote[], date: Date ): CalendarNote[] { return notes.filter((note) => { if (!note.date) return false; - // Ensure noteDate is a Date object (handle both Date objects and string dates) - const noteDate = - note.date instanceof Date ? note.date : new Date(note.date); + const noteDate = coerceNoteDate(note.date); + if (!noteDate) return false;Also applies to: 100-110
hooks/useNotes.ts (2)
89-90: Missing normalization for newly created note.The
newNotefrom the API response is added to state without normalization, whilefetchNotesnormalizes all fetched data. This inconsistency means newly created notes will have string dates until the next refetch.🐛 Proposed fix
const newNote = await response.json(); - setNotes((prev) => [...prev, newNote]); + setNotes((prev) => [...prev, normalizeNote(newNote)]);
133-134: Missing normalization for updated note.Same issue as
createNote- theupdatedNoteresponse should be normalized to maintain consistent Date types in state.🐛 Proposed fix
const updatedNote = await response.json(); - setNotes((prev) => prev.map((n) => (n.id === noteId ? updatedNote : n))); + setNotes((prev) => prev.map((n) => (n.id === noteId ? normalizeNote(updatedNote) : n)));hooks/useShifts.ts (1)
133-134: Missing normalization for updated shift.The
updatedShiftresponse is set to state without normalization, causing inconsistent date types until the next refetch. This mirrors the issue increateNote/updateNote.🐛 Proposed fix
const updatedShift = await response.json(); - setShifts((prev) => prev.map((s) => (s.id === id ? updatedShift : s))); + setShifts((prev) => prev.map((s) => (s.id === id ? normalizeShift(updatedShift) : s)));app/page.tsx (3)
170-178: Missing normalization for reloaded notes in compare mode.When notes are reloaded after
handleNoteSubmit, thenotesDatais set directly without normalization. This is inconsistent with line 336 where notes are normalized during initial compare data load.🐛 Proposed fix
const notesData = notesRes.ok ? await notesRes.json() : []; setCompareCalendarData((prev) => { const updated = new Map(prev); const data = updated.get(compareNoteCalendarId); if (data) { - updated.set(compareNoteCalendarId, { ...data, notes: notesData }); + updated.set(compareNoteCalendarId, { ...data, notes: notesData.map(normalizeNote) }); } return updated; });
192-203: Missing normalization for reloaded notes after delete.Same issue - notes reloaded after
handleNoteDeleteare not normalized.🐛 Proposed fix
const notesData = notesRes.ok ? await notesRes.json() : []; setCompareCalendarData((prev) => { const updated = new Map(prev); const data = updated.get(compareNoteCalendarId); if (data) { - updated.set(compareNoteCalendarId, { ...data, notes: notesData }); + updated.set(compareNoteCalendarId, { ...data, notes: notesData.map(normalizeNote) }); } return updated; });
237-248: Missing normalization for reloaded notes inhandleDeleteNoteFromList.Same pattern - notes reloaded in compare mode are not normalized.
🐛 Proposed fix
const notesData = notesRes.ok ? await notesRes.json() : []; setCompareCalendarData((prev) => { const updated = new Map(prev); const data = updated.get(compareNoteCalendarId); if (data) { - updated.set(compareNoteCalendarId, { ...data, notes: notesData }); + updated.set(compareNoteCalendarId, { ...data, notes: notesData.map(normalizeNote) }); } return updated; });
🤖 Fix all issues with AI agents
In @lib/date-utils.ts:
- Around line 21-25: The parseLocalDate function currently assumes a valid
YYYY-MM-DD input; add input validation at the top of parseLocalDate to (1)
ensure dateString is a non-empty string, (2) match a strict
/^\d{4}-\d{2}-\d{2}$/ pattern, (3) parse year/month/day as numbers, validate
month is 1–12 and day is within the valid range for that month (account for leap
years for February), then construct the Date and verify it's not NaN; if any
check fails, throw a descriptive TypeError (e.g., "Invalid date format: expected
YYYY-MM-DD") so callers get a clear failure instead of producing invalid Date
objects.
🧹 Nitpick comments (7)
components/shifts-list.tsx (1)
33-40: Replace repeatedas Dateassertions with a safe coercion (and preferformatDateToLocalfor the yyyy-MM-dd key).Right now, any non-
Dateshift.datewill become a runtime exception during filtering/sorting/formatting. Also, usingformatDateToLocal()fordateKeykeeps grouping consistent with the rest of the app’s “local date string” convention. As per coding guidelines/learnings, preferformatDateToLocal()for local-date keys.Proposed fix
import { format } from "date-fns"; import { useTranslations, useLocale } from "next-intl"; import { getDateLocale } from "@/lib/locales"; import { ShiftWithCalendar } from "@/lib/types"; +import { formatDateToLocal } from "@/lib/date-utils"; +function coerceDate(value: unknown): Date | null { + return value instanceof Date ? value : null; // or normalize upstream to always be Date +} const shiftsInMonth = shifts.filter((shift) => { if (!shift.date) return false; - const shiftDate = shift.date as Date; + const shiftDate = coerceDate(shift.date); + if (!shiftDate) return false; return ( shiftDate.getMonth() === currentDate.getMonth() && shiftDate.getFullYear() === currentDate.getFullYear() ); }); const groupedShifts = shiftsInMonth .sort( (a, b) => - (a.date ? (a.date as Date).getTime() : 0) - - (b.date ? (b.date as Date).getTime() : 0) + ((coerceDate(a.date)?.getTime() ?? 0) - (coerceDate(b.date)?.getTime() ?? 0)) ) .reduce((acc, shift) => { - const dateKey = shift.date - ? format(shift.date as Date, "yyyy-MM-dd") - : "unknown"; + const shiftDate = coerceDate(shift.date); + const dateKey = shiftDate ? formatDateToLocal(shiftDate) : "unknown"; if (!acc[dateKey]) acc[dateKey] = []; acc[dateKey].push(shift); return acc; }, {} as Record<string, ShiftWithCalendar[]>); ... {dayShifts[0].date && format( - dayShifts[0].date as Date, + coerceDate(dayShifts[0].date) ?? new Date(0), "EEEE, MMMM d, yyyy", { locale: dateLocale } )}Also applies to: 42-55, 145-152
lib/external-calendar-utils.ts (2)
6-8: Good: date-only compare viaformatDateToLocal(but remove the redundant cast).
formatDateToLocalcomparison matches the PR intent. Minor:existing.dateis already typed asDate, so theas Dateis unnecessary.Proposed tweak
- const existingDate = formatDateToLocal(existing.date as Date); + const existingDate = formatDateToLocal(existing.date);Also applies to: 360-387
335-352:createEventFingerprint()still uses UTC (toISOString()), which can reintroduce day drift.If
dateis meant to be a local “date-only” value, preferformatDateToLocal(date)here as well; otherwise fingerprints can differ by timezone.app/api/notes/route.ts (1)
53-55: Consider refactoring date range calculation for clarity.The current pattern mutates
targetDatewhen callingsetHours(). While technically correct (sincesetHoursreturns a timestamp that gets wrapped in a new Date), this code is confusing and relies on understanding the mutation side effect.♻️ Proposed refactor for improved clarity
if (date) { const targetDate = parseLocalDate(date); - const startOfDay = new Date(targetDate.setHours(0, 0, 0, 0)); - const endOfDay = new Date(targetDate.setHours(23, 59, 59, 999)); + const startOfDay = new Date(targetDate); + startOfDay.setHours(0, 0, 0, 0); + const endOfDay = new Date(targetDate); + endOfDay.setHours(23, 59, 59, 999); const result = await dbThis makes it explicit that we're creating copies before mutating them.
hooks/useShifts.ts (1)
8-22: Consider exportingnormalizeShiftto avoid duplication.This helper is duplicated in
app/page.tsx(lines 46-59). UnlikenormalizeNotewhich is exported fromuseNotes.ts,normalizeShiftremains private, forcing duplication.♻️ Suggested approach
Export
normalizeShiftfrom this file and import it inapp/page.tsx:-function normalizeShift(shift: Record<string, unknown>): ShiftWithCalendar { +export function normalizeShift(shift: Record<string, unknown>): ShiftWithCalendar {Then in
app/page.tsx:-import { useShifts } from "@/hooks/useShifts"; +import { useShifts, normalizeShift } from "@/hooks/useShifts";And remove the duplicate
normalizeShiftfunction fromapp/page.tsx.app/page.tsx (1)
45-59: DuplicatenormalizeShiftimplementation.This function duplicates the one in
hooks/useShifts.ts. IfnormalizeShiftis exported from the hook (as suggested in the useShifts review), this duplicate can be removed.hooks/useShiftActions.ts (1)
90-97: Remove unnecessary type cast - TypeScript narrows type correctly.Shifts passed to
useShiftActionsare always normalized upstream inapp/page.tsxvianormalizeShift, which ensuresdateis aDateobject. Theshift.date &&check on line 92 provides sufficient type narrowing; theas Datecast on line 93 is redundant and should be removed.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (16)
app/api/calendars/[id]/export/ics/route.tsapp/api/calendars/[id]/export/pdf/route.tsapp/api/notes/route.tsapp/api/shifts/[id]/route.tsapp/api/shifts/route.tsapp/page.tsxcomponents/calendar-grid.tsxcomponents/shifts-list.tsxhooks/useNotes.tshooks/useShiftActions.tshooks/useShifts.tslib/date-utils.tslib/event-utils.tslib/external-calendar-utils.tslib/rate-limiter.tslib/version.ts
💤 Files with no reviewable changes (2)
- lib/version.ts
- lib/rate-limiter.ts
🧰 Additional context used
📓 Path-based instructions (6)
app/**/*.{tsx,ts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
All components under
app/are Server Components by default unless marked with"use client". Use"use client"directive only for interactive UI components.
Files:
app/api/shifts/[id]/route.tsapp/api/calendars/[id]/export/pdf/route.tsapp/api/notes/route.tsapp/api/calendars/[id]/export/ics/route.tsapp/api/shifts/route.tsapp/page.tsx
app/api/**/*.ts
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
app/api/**/*.ts: All API routes must includeGET/POST/PUT/PATCH/DELETEexports inapp/api/**directory and follow the pattern: check permissions → perform database operation → emit SSE event.
UsegetSessionUser(request.headers)fromlib/auth/sessions.tsto get current user in API routes. Use Better Auth'sauthClientmethods on the client side for session management.
After any database mutation (create/update/delete), emit an SSE event usingeventEmitter.emit('calendar-change', {...})with appropriate type and action to keep clients in sync.
For calendar permission hierarchy in SQL queries and permission checks, follow:owner(highest) >admin>write>read(lowest). UsegetUserAccessibleCalendars(userId)to get all accessible calendars.
UsegetUserCalendarPermission(userId, calendarId)fromlib/auth/permissions.tsto retrieve the exact permission level, which returns permission string ornullif user has no access.
Files:
app/api/shifts/[id]/route.tsapp/api/calendars/[id]/export/pdf/route.tsapp/api/notes/route.tsapp/api/calendars/[id]/export/ics/route.tsapp/api/shifts/route.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}: Store and display dates asYYYY-MM-DDstrings (local dates without timezone conversion). UseformatDateToLocal()fromlib/date-utils.tsbefore saving to database anddate-fnswith locale fromgetDateLocale()for display.
Use Drizzle-inferred types from schema directly (noanytypes). TypeScript must remain in strict mode with proper type safety throughout the application.
All React files must be in strict mode with TypeScript 5. Noanytypes allowed. Import inferred types from Drizzle schema.
UseformatDateToLocal(date)utility fromlib/date-utils.tsto convert dates toYYYY-MM-DDformat before saving to database.
Files:
app/api/shifts/[id]/route.tslib/date-utils.tshooks/useNotes.tscomponents/shifts-list.tsxapp/api/calendars/[id]/export/pdf/route.tsapp/api/notes/route.tsapp/api/calendars/[id]/export/ics/route.tscomponents/calendar-grid.tsxhooks/useShiftActions.tsapp/api/shifts/route.tslib/event-utils.tshooks/useShifts.tslib/external-calendar-utils.tsapp/page.tsx
hooks/use*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Implement data fetching hooks following the pattern:
useShifts,usePresets,useNotes,useCalendarswith accompanying action hooks likeuseShiftActions,useNoteActionsfor CRUD operations with optimistic updates.
Files:
hooks/useNotes.tshooks/useShiftActions.tshooks/useShifts.ts
components/**/*.{tsx,ts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Components in
/componentsdirectory should be"use client"by default for interactive UI, and should use shadcn/ui patterns with Radix UI primitives and Lucide icons.
Files:
components/shifts-list.tsxcomponents/calendar-grid.tsx
hooks/use*Actions.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Implement optimistic updates in action hooks by: generating temp ID (
temp-${Date.now()}), adding item to state immediately, making API call, removing temp item on error with toast, replacing temp item on success.
Files:
hooks/useShiftActions.ts
🧠 Learnings (14)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{ts,tsx} : Store and display dates as `YYYY-MM-DD` strings (local dates without timezone conversion). Use `formatDateToLocal()` from `lib/date-utils.ts` before saving to database and `date-fns` with locale from `getDateLocale()` for display.
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{ts,tsx} : Use `formatDateToLocal(date)` utility from `lib/date-utils.ts` to convert dates to `YYYY-MM-DD` format before saving to database.
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `getDateLocale(locale)` from `lib/date-utils.ts` when formatting dates with `date-fns` to ensure proper locale-aware date display.
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `getDateLocale(locale)` from `lib/date-utils.ts` when formatting dates with `date-fns` to ensure proper locale-aware date display.
Applied to files:
app/api/shifts/[id]/route.tslib/date-utils.tshooks/useNotes.tscomponents/shifts-list.tsxapp/api/calendars/[id]/export/pdf/route.tsapp/api/notes/route.tsapp/api/calendars/[id]/export/ics/route.tscomponents/calendar-grid.tsxhooks/useShiftActions.tsapp/api/shifts/route.tslib/event-utils.tshooks/useShifts.tslib/external-calendar-utils.tsapp/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{ts,tsx} : Use `formatDateToLocal(date)` utility from `lib/date-utils.ts` to convert dates to `YYYY-MM-DD` format before saving to database.
Applied to files:
app/api/shifts/[id]/route.tslib/date-utils.tshooks/useNotes.tscomponents/shifts-list.tsxapp/api/calendars/[id]/export/pdf/route.tsapp/api/notes/route.tsapp/api/calendars/[id]/export/ics/route.tscomponents/calendar-grid.tsxhooks/useShiftActions.tsapp/api/shifts/route.tslib/event-utils.tshooks/useShifts.tslib/external-calendar-utils.tsapp/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{ts,tsx} : Store and display dates as `YYYY-MM-DD` strings (local dates without timezone conversion). Use `formatDateToLocal()` from `lib/date-utils.ts` before saving to database and `date-fns` with locale from `getDateLocale()` for display.
Applied to files:
app/api/shifts/[id]/route.tslib/date-utils.tshooks/useNotes.tscomponents/shifts-list.tsxapp/api/calendars/[id]/export/pdf/route.tsapp/api/notes/route.tsapp/api/calendars/[id]/export/ics/route.tscomponents/calendar-grid.tsxhooks/useShiftActions.tsapp/api/shifts/route.tslib/event-utils.tshooks/useShifts.tslib/external-calendar-utils.tsapp/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : Use `getUserCalendarPermission(userId, calendarId)` from `lib/auth/permissions.ts` to retrieve the exact permission level, which returns permission string or `null` if user has no access.
Applied to files:
app/api/shifts/[id]/route.tsapp/api/notes/route.tsapp/api/calendars/[id]/export/ics/route.tsapp/api/shifts/route.tsapp/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Respect `shift.syncedFromExternal` flag - synced shifts from external calendars cannot be edited or deleted by users. Check this flag before allowing shift mutations.
Applied to files:
app/api/shifts/[id]/route.tsapp/api/calendars/[id]/export/ics/route.tscomponents/calendar-grid.tsxapp/api/shifts/route.tshooks/useShifts.tsapp/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : For calendar permission hierarchy in SQL queries and permission checks, follow: `owner` (highest) > `admin` > `write` > `read` (lowest). Use `getUserAccessibleCalendars(userId)` to get all accessible calendars.
Applied to files:
app/api/shifts/[id]/route.tsapp/api/notes/route.tsapp/api/calendars/[id]/export/ics/route.tsapp/api/shifts/route.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to hooks/use*.{ts,tsx} : Implement data fetching hooks following the pattern: `useShifts`, `usePresets`, `useNotes`, `useCalendars` with accompanying action hooks like `useShiftActions`, `useNoteActions` for CRUD operations with optimistic updates.
Applied to files:
app/api/shifts/[id]/route.tshooks/useNotes.tshooks/useShiftActions.tshooks/useShifts.tsapp/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useCalendarPermission(calendarId)` hook to check UI permissions, which returns `canEdit`, `canDelete`, `canShare`, and `permission` level for displaying conditional UI elements.
Applied to files:
app/api/shifts/[id]/route.tshooks/useNotes.tsapp/api/notes/route.tsapp/api/calendars/[id]/export/ics/route.tshooks/useShiftActions.tsapp/api/shifts/route.tshooks/useShifts.tslib/external-calendar-utils.tsapp/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Always check permissions using `checkPermission(userId, calendarId, requiredLevel)` function from `lib/auth/permissions.ts` in both API routes and UI components before allowing mutations.
Applied to files:
app/api/notes/route.tsapp/api/calendars/[id]/export/ics/route.tsapp/api/shifts/route.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to components/**/*sheet*.{tsx,ts} : Use the Sheet pattern (`components/ui/base-sheet.tsx`) for all forms with side panels, including unsaved changes confirmation via `useDirtyState` hook and consistent header/footer styling with gradient backgrounds.
Applied to files:
hooks/useShiftActions.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : After any database mutation (create/update/delete), emit an SSE event using `eventEmitter.emit('calendar-change', {...})` with appropriate type and action to keep clients in sync.
Applied to files:
lib/event-utils.tsapp/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useSSEConnection` hook from `hooks/useSSEConnection.ts` for real-time sync connection management. The hook automatically handles reconnection, visibility changes, and triggers data refreshes.
Applied to files:
app/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to components/**/*dialog*.{tsx,ts} : Use the Dialog pattern (`components/ui/dialog.tsx` from shadcn/ui) for list views, confirmations, and read-only displays.
Applied to files:
app/page.tsx
🧬 Code graph analysis (6)
hooks/useNotes.ts (2)
lib/db/schema.ts (1)
CalendarNote(407-407)lib/date-utils.ts (1)
parseLocalDate(21-25)
hooks/useShiftActions.ts (1)
lib/date-utils.ts (1)
parseLocalDate(21-25)
app/api/shifts/route.ts (1)
lib/date-utils.ts (1)
parseLocalDate(21-25)
hooks/useShifts.ts (3)
lib/types.ts (1)
ShiftWithCalendar(20-40)lib/date-utils.ts (1)
parseLocalDate(21-25)lib/db/schema.ts (1)
shifts(216-249)
lib/external-calendar-utils.ts (1)
lib/date-utils.ts (1)
formatDateToLocal(5-10)
app/page.tsx (4)
lib/types.ts (1)
ShiftWithCalendar(20-40)lib/date-utils.ts (1)
parseLocalDate(21-25)hooks/useNotes.ts (1)
normalizeNote(8-21)lib/event-utils.ts (1)
findNotesForDate(101-132)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Agent
- GitHub Check: build-dev
🔇 Additional comments (12)
lib/external-calendar-utils.ts (1)
16-41: No remaining call sites for removedisValidICloudUrlfound in codebase.The refactoring to replace
isValidICloudUrlwith thedetectCalendarSyncType()function is complete and clean.app/api/calendars/[id]/export/pdf/route.ts (1)
54-70: Remove unnecessary type guards;shift.dateis already aDateobject per the schema definition.The shifts table defines
date: integer("date", { mode: "timestamp" }), which configures Drizzle to return JavaScript Date objects. Theas Datecasts are correct and safe—no runtime guards are needed. The proposedcoerceShiftDate()function is unnecessary.However, note that this pattern conflicts with the coding guidelines that specify storing dates as
YYYY-MM-DDstrings. If the architecture should follow that pattern, consider updating the schema and usingformatDateToLocal()/parseLocalDate()instead.Likely an incorrect or invalid review comment.
components/calendar-grid.tsx (1)
74-78: Remove theas Datetype assertion—the type system already guaranteesshift.dateisDate | null.Since
ShiftWithCalendardeclaresdate: Date | nulland all shifts are normalized viaparseLocalDate()before state, the cast is redundant. The existingshift.date &&guard already handles the null case safely for the customisSameDayimplementation.app/api/shifts/[id]/route.ts (1)
8-8: LGTM! Correct use of parseLocalDate for date normalization.The change from
new Date(date)toparseLocalDate(date)correctly ensures that date strings are interpreted in local time rather than UTC, preventing the day-drift bug mentioned in the PR objectives.Based on learnings and coding guidelines about storing dates as YYYY-MM-DD strings with local timezone interpretation.
Also applies to: 120-120
app/api/notes/route.ts (1)
8-8: LGTM! Consistent use of parseLocalDate across GET and POST handlers.The replacement of
new Date(date)withparseLocalDate(date)in both the GET filtering logic and POST creation logic correctly normalizes date handling to avoid UTC interpretation issues.Based on learnings and coding guidelines about storing dates as YYYY-MM-DD strings with local timezone interpretation.
Also applies to: 53-53, 137-137
app/api/shifts/route.ts (1)
8-8: LGTM! Consistent date normalization in GET and POST handlers.The use of
parseLocalDate(date)in both the GET filtering (line 75) and POST creation (line 161) correctly ensures local timezone interpretation, preventing the UTC midnight bug.Based on learnings and coding guidelines about storing dates as YYYY-MM-DD strings with local timezone interpretation.
Also applies to: 75-75, 161-161
app/api/calendars/[id]/export/ics/route.ts (1)
77-83: LGTM! Correct handling of Date objects from Drizzle.The change from constructing
new Date(shift.date)to castingshift.date as Dateis appropriate since Drizzle returns Date objects for timestamp columns. The use offormatDateToLocal()for all-day event formatting correctly produces YYYY-MM-DD strings in local time.Note: The type assertion relies on the Drizzle schema having
mode: "timestamp"for the date column. This is valid but creates a coupling between the export logic and the schema configuration.Based on learnings and coding guidelines about date formatting.
hooks/useShiftActions.ts (1)
72-82: LGTM - Date parsing logic is well-implemented.The union type
Date | stringwith the regex guard provides type-safe date handling. UsingparseLocalDatefor YYYY-MM-DD strings correctly avoids UTC timezone shifts. Based on learnings, this aligns with the project's date handling guidelines.hooks/useNotes.ts (1)
7-21: Well-structured normalization helper.The
normalizeNotefunction correctly handles both YYYY-MM-DD strings (viaparseLocalDate) and other date formats. Exporting it allows reuse inapp/page.tsxfor compare mode data.hooks/useShifts.ts (1)
64-76: LGTM - Optimistic shift creation is properly normalized.The optimistic shift uses
parseLocalDate(formData.date)ensuring the date is a proper Date object from the start. The actual response is also normalized before replacing the temp shift.app/page.tsx (2)
430-436: LGTM - Consistent date parsing in event handlers.The date parsing pattern using the regex guard and
parseLocalDateis consistently applied acrosshandleDayClick,handleDayRightClick,handleNoteIconClick, andhandleCompareDayClick. This ensures proper local date handling regardless of input type.
334-340: LGTM - Compare mode initial data load is properly normalized.Both shifts and notes are correctly normalized using
normalizeShiftandnormalizeNoterespectively when loading compare mode data.
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/page.tsx (1)
289-325: TypedataMap(avoids implicitMap<any, any>), and ensure compare-mode note refresh paths also normalize.
const dataMap = new Map();will inferMap<any, any>and weakens strict typing. Also, since compare-mode now relies on normalization (notesData.map(normalizeNote)), any later compare-mode refresh that sets rawnotesback into state will reintroduce inconsistentnote.datetypes and potentially the timezone drift behavior.Proposed fix (typing)
- const dataMap = new Map(); + const dataMap = new Map< + string, + { + shifts: ShiftWithCalendar[]; + notes: CalendarNote[]; + externalSyncs: ExternalSync[]; + presets: ShiftPreset[]; + togglingDates: Set<string>; + } + >();Please also update any compare-mode “reload notes” code paths to do
notes: notesData.map(normalizeNote)(parallel to what you already do here), sofindNotesForDatealways receives normalized notes. As per coding guidelines, avoidanyand keep strict typing.
🤖 Fix all issues with AI agents
In @app/api/notes/route.ts:
- Around line 52-57: The GET and POST handlers call parseLocalDate(date) without
handling its TypeError for invalid input; wrap the parseLocalDate(...) call in
both the GET handler and the POST handler (where calendarNotes.date is set) in
try-catch blocks, and on catch return a 400 Bad Request response (e.g.,
NextResponse.json or new Response with status 400) indicating invalid date input
instead of letting the error bubble to a 500; reference parseLocalDate and the
POST assignment to calendarNotes.date to locate the two places to change.
In @lib/event-utils.ts:
- Around line 78-81: The code currently constructs noteDate with new
Date(note.date), which can shift date-only strings; update the logic in the
blocks that set noteDate (the one using note.type/date and the later block at
lines ~107-111) to detect date-only strings (e.g. /^\d{4}-\d{2}-\d{2}$/) and
call parseLocalDate(note.date) for those cases, otherwise keep the existing
behavior (if note.date is instance of Date use it, else new Date(note.date));
mirror the same approach used in normalizeNote/normalizeShift so date-only
inputs are parsed as local dates to avoid UTC day drift.
🧹 Nitpick comments (2)
app/page.tsx (2)
414-462: Optional: extract the repeatedDate | string→Dateparsing into a helper.The same
typeof date === "string" && /^\d{4}-\d{2}-\d{2}$/ ...block appears in multiple handlers; a small helper (e.g.coerceToDateInput(date)) would reduce duplication and keep semantics consistent.
532-541: Compare-mode shift reload normalization is consistent (good), but theas Datecasts are a smell.Using
normalizeShifton reload paths is consistent and should keep compare-mode stable. The remainingshift.date as Datecasts suggest the types don’t reflect the post-normalization reality—consider tighteningShiftWithCalendar["date"]toDate(or ensuring normalization guarantees it) so the casts can go away.Also applies to: 548-549, 588-590, 612-616, 652-656, 892-893, 936-937
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
app/api/notes/route.tsapp/api/shifts/route.tsapp/page.tsxhooks/useShifts.tslib/date-utils.tslib/event-utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- hooks/useShifts.ts
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{ts,tsx}: Store and display dates asYYYY-MM-DDstrings (local dates without timezone conversion). UseformatDateToLocal()fromlib/date-utils.tsbefore saving to database anddate-fnswith locale fromgetDateLocale()for display.
Use Drizzle-inferred types from schema directly (noanytypes). TypeScript must remain in strict mode with proper type safety throughout the application.
All React files must be in strict mode with TypeScript 5. Noanytypes allowed. Import inferred types from Drizzle schema.
UseformatDateToLocal(date)utility fromlib/date-utils.tsto convert dates toYYYY-MM-DDformat before saving to database.
Files:
lib/event-utils.tsapp/api/shifts/route.tsapp/page.tsxapp/api/notes/route.tslib/date-utils.ts
app/**/*.{tsx,ts}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
All components under
app/are Server Components by default unless marked with"use client". Use"use client"directive only for interactive UI components.
Files:
app/api/shifts/route.tsapp/page.tsxapp/api/notes/route.ts
app/api/**/*.ts
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
app/api/**/*.ts: All API routes must includeGET/POST/PUT/PATCH/DELETEexports inapp/api/**directory and follow the pattern: check permissions → perform database operation → emit SSE event.
UsegetSessionUser(request.headers)fromlib/auth/sessions.tsto get current user in API routes. Use Better Auth'sauthClientmethods on the client side for session management.
After any database mutation (create/update/delete), emit an SSE event usingeventEmitter.emit('calendar-change', {...})with appropriate type and action to keep clients in sync.
For calendar permission hierarchy in SQL queries and permission checks, follow:owner(highest) >admin>write>read(lowest). UsegetUserAccessibleCalendars(userId)to get all accessible calendars.
UsegetUserCalendarPermission(userId, calendarId)fromlib/auth/permissions.tsto retrieve the exact permission level, which returns permission string ornullif user has no access.
Files:
app/api/shifts/route.tsapp/api/notes/route.ts
🧠 Learnings (13)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{ts,tsx} : Store and display dates as `YYYY-MM-DD` strings (local dates without timezone conversion). Use `formatDateToLocal()` from `lib/date-utils.ts` before saving to database and `date-fns` with locale from `getDateLocale()` for display.
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `getDateLocale(locale)` from `lib/date-utils.ts` when formatting dates with `date-fns` to ensure proper locale-aware date display.
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{ts,tsx} : Use `formatDateToLocal(date)` utility from `lib/date-utils.ts` to convert dates to `YYYY-MM-DD` format before saving to database.
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{ts,tsx} : Use `formatDateToLocal(date)` utility from `lib/date-utils.ts` to convert dates to `YYYY-MM-DD` format before saving to database.
Applied to files:
lib/event-utils.tsapp/api/shifts/route.tsapp/page.tsxapp/api/notes/route.tslib/date-utils.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `getDateLocale(locale)` from `lib/date-utils.ts` when formatting dates with `date-fns` to ensure proper locale-aware date display.
Applied to files:
lib/event-utils.tsapp/api/shifts/route.tsapp/page.tsxapp/api/notes/route.tslib/date-utils.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{ts,tsx} : Store and display dates as `YYYY-MM-DD` strings (local dates without timezone conversion). Use `formatDateToLocal()` from `lib/date-utils.ts` before saving to database and `date-fns` with locale from `getDateLocale()` for display.
Applied to files:
lib/event-utils.tsapp/api/shifts/route.tsapp/page.tsxapp/api/notes/route.tslib/date-utils.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : After any database mutation (create/update/delete), emit an SSE event using `eventEmitter.emit('calendar-change', {...})` with appropriate type and action to keep clients in sync.
Applied to files:
lib/event-utils.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : Use `getUserCalendarPermission(userId, calendarId)` from `lib/auth/permissions.ts` to retrieve the exact permission level, which returns permission string or `null` if user has no access.
Applied to files:
app/api/shifts/route.tsapp/page.tsxapp/api/notes/route.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to app/api/**/*.ts : For calendar permission hierarchy in SQL queries and permission checks, follow: `owner` (highest) > `admin` > `write` > `read` (lowest). Use `getUserAccessibleCalendars(userId)` to get all accessible calendars.
Applied to files:
app/api/shifts/route.tsapp/api/notes/route.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useCalendarPermission(calendarId)` hook to check UI permissions, which returns `canEdit`, `canDelete`, `canShare`, and `permission` level for displaying conditional UI elements.
Applied to files:
app/api/shifts/route.tsapp/page.tsxapp/api/notes/route.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Always check permissions using `checkPermission(userId, calendarId, requiredLevel)` function from `lib/auth/permissions.ts` in both API routes and UI components before allowing mutations.
Applied to files:
app/api/shifts/route.tsapp/api/notes/route.ts
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Respect `shift.syncedFromExternal` flag - synced shifts from external calendars cannot be edited or deleted by users. Check this flag before allowing shift mutations.
Applied to files:
app/api/shifts/route.tsapp/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to hooks/use*.{ts,tsx} : Implement data fetching hooks following the pattern: `useShifts`, `usePresets`, `useNotes`, `useCalendars` with accompanying action hooks like `useShiftActions`, `useNoteActions` for CRUD operations with optimistic updates.
Applied to files:
app/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to **/*.{tsx} : Use `useSSEConnection` hook from `hooks/useSSEConnection.ts` for real-time sync connection management. The hook automatically handles reconnection, visibility changes, and triggers data refreshes.
Applied to files:
app/page.tsx
📚 Learning: 2026-01-03T02:03:48.622Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-03T02:03:48.622Z
Learning: Applies to components/**/*dialog*.{tsx,ts} : Use the Dialog pattern (`components/ui/dialog.tsx` from shadcn/ui) for list views, confirmations, and read-only displays.
Applied to files:
app/page.tsx
🧬 Code graph analysis (3)
app/api/shifts/route.ts (1)
lib/date-utils.ts (1)
parseLocalDate(22-77)
app/page.tsx (4)
hooks/useShifts.ts (1)
normalizeShift(9-24)hooks/useNotes.ts (1)
normalizeNote(8-21)lib/date-utils.ts (1)
parseLocalDate(22-77)lib/event-utils.ts (1)
findNotesForDate(102-133)
app/api/notes/route.ts (1)
lib/date-utils.ts (1)
parseLocalDate(22-77)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build-dev
🔇 Additional comments (1)
lib/date-utils.ts (1)
12-89:parseLocalDate()looks solid and matches the intended “local date, not UTC” semantics.Strict format validation + leap-year/day-range checks are a nice touch and should prevent a lot of silent bad inputs. Consider adding a small unit test matrix (invalid formats, leap day, DST-adjacent dates) to lock this in.
Adds a local-date parser and normalizes date handling across the app to avoid UTC shifts when parsing YYYY-MM-DD strings.
Normalizes API responses to real Date objects in hooks and UI, updates server/export code to treat stored timestamps as Dates, and unifies comparisons/formatting so calendar, notes, PDF and ICS exports behave consistently.
Prevents incorrect day drift and ensures reliable date-based filtering, grouping, and display.
Summary by CodeRabbit
Bug Fixes
Refactor
Chores
✏️ Tip: You can customize this high-level summary in your review settings.