Skip to content

fix: timezone parsing for local dates#119

Merged
panteLx merged 3 commits intomainfrom
fix/timezone-parsing
Jan 10, 2026
Merged

fix: timezone parsing for local dates#119
panteLx merged 3 commits intomainfrom
fix/timezone-parsing

Conversation

@panteLx
Copy link
Owner

@panteLx panteLx commented Jan 10, 2026

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

    • More reliable local-date parsing with strict validation; endpoints now return clear errors for invalid date input and preserve data on non-auth fetch failures.
    • Exports (ICS/PDF) and UI filtering now handle dates consistently, fixing off-by-one/timezone issues.
  • Refactor

    • Unified date normalization across UI, hooks, and export flows; added helpers to normalize shifts/notes and consistently parse/display dates.
  • Chores

    • Removed several deprecated legacy utility helpers.

✏️ Tip: You can customize this high-level summary in your review settings.

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.
Copilot AI review requested due to automatic review settings January 10, 2026 11:45
@coderabbitai
Copy link

coderabbitai bot commented Jan 10, 2026

📝 Walkthrough

Walkthrough

Standardizes local-date parsing and normalization: adds parseLocalDate() and helpers (normalizeShift, normalizeNote), replaces many new Date(...) usages with local-aware parsing or Date assertions, updates API routes and UI to validate/normalize dates, and removes several deprecated exported helpers.

Changes

Cohort / File(s) Summary
Date utilities
lib/date-utils.ts
Add export function parseLocalDate(dateString: string): Date with strict YYYY-MM-DD parsing, range and leap-year validation, and local-time construction; adds isLeapYear helper.
API routes — date parsing / exports
app/api/shifts/route.ts, app/api/shifts/[id]/route.ts, app/api/notes/route.ts, app/api/calendars/[id]/export/ics/route.ts, app/api/calendars/[id]/export/pdf/route.ts
Replace new Date(...) with parseLocalDate(...) for input parsing/validation (notes, shifts); export routes now treat shift.date as Date via type assertions and use formatDateToLocal() for all-day ICS date strings.
Hooks — normalization & input handling
hooks/useShifts.ts, hooks/useNotes.ts, hooks/useShiftActions.ts
Add normalizeShift() and normalizeNote(); map API responses through normalizers; widen handlers to accept `Date
UI components — date handling & normalization
app/page.tsx, components/calendar-grid.tsx, components/shifts-list.tsx
Apply normalization to fetched shifts/notes, parse string date inputs to Date via parseLocalDate, and replace runtime new Date(...) usage with shift.date as Date assertions in comparisons/formatting.
Event & external calendar utils
lib/event-utils.ts, lib/external-calendar-utils.ts
findEventsForDate/findNotesForDate now accept Date, ISO-like strings (parsed with parseLocalDate), or other strings; removed legacy helpers findEventForDate/findNoteForDate; needsUpdate uses formatDateToLocal() for comparisons; removed isValidICloudUrl.
Public API cleanup
lib/rate-limiter.ts, lib/version.ts
Removed exported helpers resetRateLimit(), getRateLimiterStats(), and getCurrentVersionSync() (deprecated/removed public APIs).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐇
I hopped through strings and midnight air,
Parsed each date with careful care.
Normalized shifts, notes neat and bright,
No more wobbling through timezone night.
A carrot for code — everything's right!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% 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 clearly summarizes the main change: adding timezone parsing for local dates and fixing related date handling issues across the application.

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

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between afe20e3 and d77d85b.

📒 Files selected for processing (3)
  • app/api/notes/route.ts
  • app/api/shifts/route.ts
  • lib/event-utils.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • app/api/shifts/route.ts
  • app/api/notes/route.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{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.
Use Drizzle-inferred types from schema directly (no any types). TypeScript must remain in strict mode with proper type safety throughout the application.
All React files must be in strict mode with TypeScript 5. No any types allowed. Import inferred types from Drizzle schema.
Use formatDateToLocal(date) utility from lib/date-utils.ts to convert dates to YYYY-MM-DD format before saving to database.

Files:

  • lib/event-utils.ts
🧠 Learnings (5)
📓 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 **/*.{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.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} : 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.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.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
🧬 Code graph analysis (1)
lib/event-utils.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 (2)
lib/event-utils.ts (2)

2-2: LGTM!

The import of parseLocalDate correctly supports the new local-date parsing strategy to prevent UTC shifts.


80-86: Error handling for parseLocalDate is already present in the hooks via try-catch at the boundary level. Both useNotes and useShifts wrap the normalization operations in try-catch blocks (lines 30-51 and 40-53 respectively), catching errors from parseLocalDate and logging them intentionally while preserving existing data. The fallback case (new Date(dateValue)) is an extra defensive measure that rarely triggers since normalizeNote and normalizeShift normalize incoming data to Date objects before it reaches findNotesForDate and findEventsForDate. Type safety is correct—CalendarNote uses Drizzle-inferred types without any. No changes needed.


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.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 parseLocalDate function to parse YYYY-MM-DD strings as local dates (avoiding UTC shift bugs)
  • Introduces normalizeShift and normalizeNote helpers 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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 for YYYY-MM-DD strings—use parseLocalDate for that case.

This is the exact scenario the PR is fixing. If notes can arrive as "YYYY-MM-DD", parsing with new 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 newNote from the API response is added to state without normalization, while fetchNotes normalizes 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 - the updatedNote response 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 updatedShift response is set to state without normalization, causing inconsistent date types until the next refetch. This mirrors the issue in createNote/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, the notesData is 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 handleNoteDelete 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;
         });

237-248: Missing normalization for reloaded notes in handleDeleteNoteFromList.

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 repeated as Date assertions with a safe coercion (and prefer formatDateToLocal for the yyyy-MM-dd key).

Right now, any non-Date shift.date will become a runtime exception during filtering/sorting/formatting. Also, using formatDateToLocal() for dateKey keeps grouping consistent with the rest of the app’s “local date string” convention. As per coding guidelines/learnings, prefer formatDateToLocal() 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 via formatDateToLocal (but remove the redundant cast).

formatDateToLocal comparison matches the PR intent. Minor: existing.date is already typed as Date, so the as Date is 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 date is meant to be a local “date-only” value, prefer formatDateToLocal(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 targetDate when calling setHours(). While technically correct (since setHours returns 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 db

This makes it explicit that we're creating copies before mutating them.

hooks/useShifts.ts (1)

8-22: Consider exporting normalizeShift to avoid duplication.

This helper is duplicated in app/page.tsx (lines 46-59). Unlike normalizeNote which is exported from useNotes.ts, normalizeShift remains private, forcing duplication.

♻️ Suggested approach

Export normalizeShift from this file and import it in app/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 normalizeShift function from app/page.tsx.

app/page.tsx (1)

45-59: Duplicate normalizeShift implementation.

This function duplicates the one in hooks/useShifts.ts. If normalizeShift is 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 useShiftActions are always normalized upstream in app/page.tsx via normalizeShift, which ensures date is a Date object. The shift.date && check on line 92 provides sufficient type narrowing; the as Date cast on line 93 is redundant and should be removed.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 56d6ba1 and f01d8fc.

📒 Files selected for processing (16)
  • app/api/calendars/[id]/export/ics/route.ts
  • app/api/calendars/[id]/export/pdf/route.ts
  • app/api/notes/route.ts
  • app/api/shifts/[id]/route.ts
  • app/api/shifts/route.ts
  • app/page.tsx
  • components/calendar-grid.tsx
  • components/shifts-list.tsx
  • hooks/useNotes.ts
  • hooks/useShiftActions.ts
  • hooks/useShifts.ts
  • lib/date-utils.ts
  • lib/event-utils.ts
  • lib/external-calendar-utils.ts
  • lib/rate-limiter.ts
  • lib/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.ts
  • app/api/calendars/[id]/export/pdf/route.ts
  • app/api/notes/route.ts
  • app/api/calendars/[id]/export/ics/route.ts
  • app/api/shifts/route.ts
  • app/page.tsx
app/api/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/api/**/*.ts: All API routes must include GET/POST/PUT/PATCH/DELETE exports in app/api/** directory and follow the pattern: check permissions → perform database operation → emit SSE event.
Use getSessionUser(request.headers) from lib/auth/sessions.ts to get current user in API routes. Use Better Auth's authClient methods on the client side for session management.
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.
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.
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.

Files:

  • app/api/shifts/[id]/route.ts
  • app/api/calendars/[id]/export/pdf/route.ts
  • app/api/notes/route.ts
  • app/api/calendars/[id]/export/ics/route.ts
  • app/api/shifts/route.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{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.
Use Drizzle-inferred types from schema directly (no any types). TypeScript must remain in strict mode with proper type safety throughout the application.
All React files must be in strict mode with TypeScript 5. No any types allowed. Import inferred types from Drizzle schema.
Use formatDateToLocal(date) utility from lib/date-utils.ts to convert dates to YYYY-MM-DD format before saving to database.

Files:

  • app/api/shifts/[id]/route.ts
  • lib/date-utils.ts
  • hooks/useNotes.ts
  • components/shifts-list.tsx
  • app/api/calendars/[id]/export/pdf/route.ts
  • app/api/notes/route.ts
  • app/api/calendars/[id]/export/ics/route.ts
  • components/calendar-grid.tsx
  • hooks/useShiftActions.ts
  • app/api/shifts/route.ts
  • lib/event-utils.ts
  • hooks/useShifts.ts
  • lib/external-calendar-utils.ts
  • app/page.tsx
hooks/use*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Implement data fetching hooks following the pattern: useShifts, usePresets, useNotes, useCalendars with accompanying action hooks like useShiftActions, useNoteActions for CRUD operations with optimistic updates.

Files:

  • hooks/useNotes.ts
  • hooks/useShiftActions.ts
  • hooks/useShifts.ts
components/**/*.{tsx,ts}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Components in /components directory 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.tsx
  • components/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.ts
  • lib/date-utils.ts
  • hooks/useNotes.ts
  • components/shifts-list.tsx
  • app/api/calendars/[id]/export/pdf/route.ts
  • app/api/notes/route.ts
  • app/api/calendars/[id]/export/ics/route.ts
  • components/calendar-grid.tsx
  • hooks/useShiftActions.ts
  • app/api/shifts/route.ts
  • lib/event-utils.ts
  • hooks/useShifts.ts
  • lib/external-calendar-utils.ts
  • 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 **/*.{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.ts
  • lib/date-utils.ts
  • hooks/useNotes.ts
  • components/shifts-list.tsx
  • app/api/calendars/[id]/export/pdf/route.ts
  • app/api/notes/route.ts
  • app/api/calendars/[id]/export/ics/route.ts
  • components/calendar-grid.tsx
  • hooks/useShiftActions.ts
  • app/api/shifts/route.ts
  • lib/event-utils.ts
  • hooks/useShifts.ts
  • lib/external-calendar-utils.ts
  • 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 **/*.{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.ts
  • lib/date-utils.ts
  • hooks/useNotes.ts
  • components/shifts-list.tsx
  • app/api/calendars/[id]/export/pdf/route.ts
  • app/api/notes/route.ts
  • app/api/calendars/[id]/export/ics/route.ts
  • components/calendar-grid.tsx
  • hooks/useShiftActions.ts
  • app/api/shifts/route.ts
  • lib/event-utils.ts
  • hooks/useShifts.ts
  • lib/external-calendar-utils.ts
  • 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 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.ts
  • app/api/notes/route.ts
  • app/api/calendars/[id]/export/ics/route.ts
  • app/api/shifts/route.ts
  • 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: 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.ts
  • app/api/calendars/[id]/export/ics/route.ts
  • components/calendar-grid.tsx
  • app/api/shifts/route.ts
  • hooks/useShifts.ts
  • 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 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.ts
  • app/api/notes/route.ts
  • app/api/calendars/[id]/export/ics/route.ts
  • app/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.ts
  • hooks/useNotes.ts
  • hooks/useShiftActions.ts
  • hooks/useShifts.ts
  • 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 `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.ts
  • hooks/useNotes.ts
  • app/api/notes/route.ts
  • app/api/calendars/[id]/export/ics/route.ts
  • hooks/useShiftActions.ts
  • app/api/shifts/route.ts
  • hooks/useShifts.ts
  • lib/external-calendar-utils.ts
  • 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: 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.ts
  • app/api/calendars/[id]/export/ics/route.ts
  • app/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.ts
  • 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 (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 removed isValidICloudUrl found in codebase.

The refactoring to replace isValidICloudUrl with the detectCalendarSyncType() function is complete and clean.

app/api/calendars/[id]/export/pdf/route.ts (1)

54-70: Remove unnecessary type guards; shift.date is already a Date object per the schema definition.

The shifts table defines date: integer("date", { mode: "timestamp" }), which configures Drizzle to return JavaScript Date objects. The as Date casts are correct and safe—no runtime guards are needed. The proposed coerceShiftDate() function is unnecessary.

However, note that this pattern conflicts with the coding guidelines that specify storing dates as YYYY-MM-DD strings. If the architecture should follow that pattern, consider updating the schema and using formatDateToLocal() / parseLocalDate() instead.

Likely an incorrect or invalid review comment.

components/calendar-grid.tsx (1)

74-78: Remove the as Date type assertion—the type system already guarantees shift.date is Date | null.

Since ShiftWithCalendar declares date: Date | null and all shifts are normalized via parseLocalDate() before state, the cast is redundant. The existing shift.date && guard already handles the null case safely for the custom isSameDay implementation.

app/api/shifts/[id]/route.ts (1)

8-8: LGTM! Correct use of parseLocalDate for date normalization.

The change from new Date(date) to parseLocalDate(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) with parseLocalDate(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 casting shift.date as Date is appropriate since Drizzle returns Date objects for timestamp columns. The use of formatDateToLocal() 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 | string with the regex guard provides type-safe date handling. Using parseLocalDate for 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 normalizeNote function correctly handles both YYYY-MM-DD strings (via parseLocalDate) and other date formats. Exporting it allows reuse in app/page.tsx for 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 parseLocalDate is consistently applied across handleDayClick, handleDayRightClick, handleNoteIconClick, and handleCompareDayClick. 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 normalizeShift and normalizeNote respectively when loading compare mode data.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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: Type dataMap (avoids implicit Map<any, any>), and ensure compare-mode note refresh paths also normalize.

const dataMap = new Map(); will infer Map<any, any> and weakens strict typing. Also, since compare-mode now relies on normalization (notesData.map(normalizeNote)), any later compare-mode refresh that sets raw notes back into state will reintroduce inconsistent note.date types 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), so findNotesForDate always receives normalized notes. As per coding guidelines, avoid any and 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 repeated Date | stringDate parsing 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 the as Date casts are a smell.

Using normalizeShift on reload paths is consistent and should keep compare-mode stable. The remaining shift.date as Date casts suggest the types don’t reflect the post-normalization reality—consider tightening ShiftWithCalendar["date"] to Date (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

📥 Commits

Reviewing files that changed from the base of the PR and between f01d8fc and afe20e3.

📒 Files selected for processing (6)
  • app/api/notes/route.ts
  • app/api/shifts/route.ts
  • app/page.tsx
  • hooks/useShifts.ts
  • lib/date-utils.ts
  • lib/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 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.
Use Drizzle-inferred types from schema directly (no any types). TypeScript must remain in strict mode with proper type safety throughout the application.
All React files must be in strict mode with TypeScript 5. No any types allowed. Import inferred types from Drizzle schema.
Use formatDateToLocal(date) utility from lib/date-utils.ts to convert dates to YYYY-MM-DD format before saving to database.

Files:

  • lib/event-utils.ts
  • app/api/shifts/route.ts
  • app/page.tsx
  • app/api/notes/route.ts
  • lib/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.ts
  • app/page.tsx
  • app/api/notes/route.ts
app/api/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

app/api/**/*.ts: All API routes must include GET/POST/PUT/PATCH/DELETE exports in app/api/** directory and follow the pattern: check permissions → perform database operation → emit SSE event.
Use getSessionUser(request.headers) from lib/auth/sessions.ts to get current user in API routes. Use Better Auth's authClient methods on the client side for session management.
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.
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.
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.

Files:

  • app/api/shifts/route.ts
  • app/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.ts
  • app/api/shifts/route.ts
  • app/page.tsx
  • app/api/notes/route.ts
  • lib/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.ts
  • app/api/shifts/route.ts
  • app/page.tsx
  • app/api/notes/route.ts
  • lib/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.ts
  • app/api/shifts/route.ts
  • app/page.tsx
  • app/api/notes/route.ts
  • lib/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.ts
  • app/page.tsx
  • app/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.ts
  • app/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.ts
  • app/page.tsx
  • app/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.ts
  • app/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.ts
  • 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 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.

@panteLx panteLx merged commit d916d59 into main Jan 10, 2026
3 checks passed
@panteLx panteLx deleted the fix/timezone-parsing branch January 10, 2026 12:27
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.

bug: Shifts are created on the previous day (timezone issue)

1 participant