Skip to content

chore: refactor app#3

Merged
panteLx merged 8 commits intomainfrom
chore/refactor-page-tsx
Dec 1, 2025
Merged

chore: refactor app#3
panteLx merged 8 commits intomainfrom
chore/refactor-page-tsx

Conversation

@panteLx
Copy link
Owner

@panteLx panteLx commented Dec 1, 2025

Summary by CodeRabbit

  • New Features

    • New responsive header, calendar grid, and collapsible monthly shifts list; realtime updates for shifts, presets, and notes.
    • Preset creation/editing/management UI and improved shift form with preset integration and long-press day interactions; per-day notes with tooltips.
  • Refactor

    • UI reorganized into modular components and hook-driven data flows for cleaner loading, syncing, and interactions.
  • Bug Fixes / Security

    • Password checks enforced for protected calendars/notes; password now accepted via request body for protected actions.

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

- Split PresetSelector into PresetList, PresetEditDialog, and PresetManageDialog for better modularity.
- Introduced usePasswordProtection hook to handle password verification and management.
- Updated PresetSelector to utilize new components and hook for creating, editing, and deleting presets with password protection.
- Removed redundant state management and form handling from PresetSelector, delegating to PresetEditDialog.
- Enhanced user experience by managing dialogs and preset actions more efficiently.
Copilot AI review requested due to automatic review settings December 1, 2025 18:39
@coderabbitai
Copy link

coderabbitai bot commented Dec 1, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

Replaces inline fetching/state with hook-driven data management (useCalendars, useShifts, usePresets, useNotes, useSSEConnection); extracts header/calendar/shift UIs into AppHeader, CalendarGrid, ShiftsList and preset dialogs/components; centralizes password protection via usePasswordProtection; adds password checks to notes APIs and moves DELETE password payloads to JSON bodies.

Changes

Cohort / File(s) Summary
App entry
app/page.tsx
Replaced local fetch/state with hooks (useCalendars, useShifts, usePresets, useNotes, useSSEConnection); delegates UI to AppHeader, CalendarGrid, ShiftsList; remapped dialog callbacks to hook-provided actions.
Header & preset orchestration
src/components/app-header.tsx, src/components/preset-selector.tsx, src/components/preset-list.tsx, src/components/preset-select.tsx, src/components/preset-manage-dialog.tsx, src/components/preset-edit-dialog.tsx
New AppHeader and refactored preset flow: PresetList/PresetSelect/PresetManageDialog/PresetEditDialog added; preset actions now routed through usePasswordProtection and dialog-driven handlers.
Calendar display
src/components/calendar-grid.tsx
New CalendarGrid rendering weekday headers and day cells with click/right-click/long-press, per-day shifts, overflow indicator, and per-day notes UI.
Shifts UI & form components
src/components/shifts-list.tsx, src/components/shift-dialog.tsx, src/components/shift-form-fields.tsx
New ShiftsList (month grouping/collapse); ShiftDialog refactored to use useShiftForm and ShiftFormFields; save-as-preset flow moved to hook-driven handling.
Hooks — data, form & SSE
src/hooks/useCalendars.ts, src/hooks/useShifts.ts, src/hooks/usePresets.ts, src/hooks/useNotes.ts, src/hooks/useShiftForm.ts, src/hooks/useSSEConnection.ts, src/hooks/usePasswordProtection.ts
New hooks providing calendars/shifts/presets/notes fetching and CRUD, refetch utilities, optimistic updates, shift form lifecycle, SSE real-time sync (visibility/online handling) and password protection wrappers (verify/withPasswordCheck/getPassword).
API — notes (auth checks)
app/api/notes/route.ts, app/api/notes/[id]/route.ts
POST/PUT/DELETE now validate calendar existence and verify passwords for protected calendars; POST/DELETE accept password in request body; return 401/404 where appropriate.
API — DELETE password source change
app/api/calendars/[id]/route.ts, app/api/presets/[id]/route.ts, app/api/shifts/[id]/route.ts
DELETE handlers now read optional password from JSON request body (when Content-Type is application/json) instead of URL query params; authorization/verification flow otherwise preserved.

Sequence Diagram

sequenceDiagram
    participant User
    participant Client as App Client
    participant Hooks as Data Hooks
    participant API as API Server
    participant SSE as SSE Stream
    participant DB as Database

    User->>Client: Open app / select calendar
    Client->>Hooks: useCalendars(), useShifts(), usePresets(), useNotes()
    Hooks->>API: GET /api/calendars, /api/shifts, /api/presets, /api/notes
    API->>DB: Query data
    DB-->>API: Data
    API-->>Hooks: Responses
    Hooks-->>Client: Provide state & actions to components (AppHeader, CalendarGrid, ShiftsList)

    Client->>SSE: Open /api/events/stream?calendarId=...
    SSE-->>Client: events (shift/preset/note)
    Client->>Hooks: useSSEConnection triggers onShiftUpdate/onPresetUpdate/onNoteUpdate
    Hooks->>Client: refetch or patch local state

    User->>Client: Create/modify/delete shift/preset/note (may require password)
    Client->>Hooks: invoke create/update/delete actions
    Hooks->>API: POST/PATCH/PUT/DELETE /api/...
    API->>DB: Persist change (verify password if protected)
    DB-->>API: Confirm
    API->>SSE: Broadcast update
    SSE-->>Client: event delivered -> Hooks update state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45–90 minutes

  • Review useSSEConnection lifecycle: event handling, visibility/online logic, reconnection/resync behavior.
  • Inspect cross-hook interactions and potential race conditions between useCalendars, useShifts, usePresets, useNotes, and useShiftForm.
  • Verify password flow: usePasswordProtection (localStorage usage), API verifyPassword usage, and JSON-body password handling in POST/DELETE endpoints.
  • Check optimistic update/rollback in useShifts and toast/error handling.
  • Confirm component prop mappings in AppHeader, PresetSelector, ShiftDialog align with new hook APIs.

Poem

🐇
I hopped through hooks and polished the night,
SSE whispers keep presets bright,
Dialogs snug, forms all in line,
Days and shifts now dance in time,
— A rabbit’s refactor, crisp and spry!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.63% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'chore: refactor app' is overly vague and generic. It does not clearly convey the specific nature or scope of changes, such as the component extraction, hook creation, or architectural reorganization that actually occurred. Consider a more descriptive title such as 'chore: refactor page.tsx with modular hooks and UI components' or 'chore: extract app logic into custom hooks and components' to better communicate the primary refactoring work.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/refactor-page-tsx

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 refactors the main application page by extracting logic into custom hooks and breaking down large components into smaller, reusable pieces. The refactoring improves code organization, maintainability, and separation of concerns.

Key changes:

  • Extracted data fetching and state management into custom hooks (useShifts, usePresets, useNotes, useCalendars, useSSEConnection, usePasswordProtection, useShiftForm)
  • Split large components into smaller, focused components (AppHeader, CalendarGrid, ShiftsList, PresetList, PresetEditDialog, etc.)
  • Reduced the main page from ~1500 lines to ~700 lines by delegating functionality to hooks and components

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
hooks/useShifts.ts Custom hook for shift CRUD operations with optimistic updates
hooks/useShiftForm.ts Form state management hook for shift dialog
hooks/useSSEConnection.ts Server-Sent Events connection management for real-time updates
hooks/usePresets.ts Preset data fetching hook
hooks/usePasswordProtection.ts Password verification utilities
hooks/useNotes.ts Calendar notes CRUD operations
hooks/useCalendars.ts Calendar management and selection logic
components/shifts-list.tsx Collapsible shifts list component with grouping by date
components/shift-form-fields.tsx Extracted shift form fields from dialog
components/shift-dialog.tsx Refactored to use custom hooks
components/preset-selector.tsx Simplified by extracting sub-components
components/preset-select.tsx Preset dropdown selector component
components/preset-manage-dialog.tsx Preset management dialog
components/preset-list.tsx Preset display and selection component
components/preset-edit-dialog.tsx Preset creation/editing dialog
components/calendar-grid.tsx Calendar grid display component
components/app-header.tsx Application header with navigation
app/page.tsx Main page refactored to use custom hooks and extracted components

You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.

return () => {
eventSource.close();
};
}, [calendarId]);
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Missing dependencies in useEffect. The callback functions onShiftUpdate, onPresetUpdate, onNoteUpdate, and onStatsRefresh should be included in the dependency array. This will cause the SSE connection to reconnect unnecessarily on every render if these callbacks change.

Suggested change
}, [calendarId]);
}, [calendarId, onShiftUpdate, onPresetUpdate, onNoteUpdate, onStatsRefresh]);

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +129
useEffect(() => {
if (calendarId) {
fetchShifts();
} else {
setShifts([]);
}
}, [calendarId]);
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Missing dependency in useEffect. The fetchShifts function should be included in the dependency array, or alternatively, wrap it in a useCallback to stabilize its reference.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +25
useEffect(() => {
if (calendarId) {
fetchPresets();
} else {
setPresets([]);
}
}, [calendarId]);
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Missing dependency in useEffect. The fetchPresets function should be included in the dependency array, or alternatively, wrap it in a useCallback to stabilize its reference.

Copilot uses AI. Check for mistakes.
onManageClick,
}: PresetListProps) {
const t = useTranslations();
const [showSecondary, setShowSecondary] = React.useState(false);
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Missing React import. The React.useState is used on line 23 but React is not imported. Add import React from "react"; at the top of the file.

Copilot uses AI. Check for mistakes.
if (!calendarId) return;

try {
const response = await fetch(`/api/shifts?calendarId=${calendarId}`);
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Missing error handling for API response status. The code should check response.ok before parsing JSON to handle HTTP errors properly. Currently, failed requests will throw errors when trying to parse non-JSON error responses.

Suggested change
const response = await fetch(`/api/shifts?calendarId=${calendarId}`);
const response = await fetch(`/api/shifts?calendarId=${calendarId}`);
if (!response.ok) {
console.error(`Failed to fetch shifts: ${response.status} ${response.statusText}`);
toast.error(t("shift.fetchError"));
return;
}

Copilot uses AI. Check for mistakes.
);
}

import React from "react";
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

React import is at the wrong location. The import React from "react"; statement should be moved to the top of the file with other imports, not at the bottom (line 217).

Copilot uses AI. Check for mistakes.
Comment on lines 14 to 17
try {
const response = await fetch(`/api/notes?calendarId=${calendarId}`);
const data = await response.json();
setNotes(data);
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Missing error handling for API response status. The code should check response.ok before parsing JSON to handle HTTP errors properly. Currently, failed requests will throw errors when trying to parse non-JSON error responses.

Copilot uses AI. Check for mistakes.
} from "date-fns";
import { de, enUS } from "date-fns/locale";
import { ShiftPreset, CalendarNote } from "@/lib/db/schema";
import { CalendarNote, ShiftPreset } from "@/lib/db/schema";
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Unused import ShiftPreset.

Suggested change
import { CalendarNote, ShiftPreset } from "@/lib/db/schema";
import { CalendarNote } from "@/lib/db/schema";

Copilot uses AI. Check for mistakes.
Comment on lines +267 to +276
const handleUpdateShift = async (id: string, formData: ShiftFormData) => {
const success = await updateShiftHook(id, formData, () => {
setPendingAction({ type: "edit", shiftId: id, formData });
setShowPasswordDialog(true);
});
if (success) {
setStatsRefreshTrigger((prev) => prev + 1);
}
};

Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Unused variable handleUpdateShift.

Suggested change
const handleUpdateShift = async (id: string, formData: ShiftFormData) => {
const success = await updateShiftHook(id, formData, () => {
setPendingAction({ type: "edit", shiftId: id, formData });
setShowPasswordDialog(true);
});
if (success) {
setStatsRefreshTrigger((prev) => prev + 1);
}
};

Copilot uses AI. Check for mistakes.
applyPreset,
saveAsPresetHandler,
resetForm,
refetchPresets,
Copy link

Copilot AI Dec 1, 2025

Choose a reason for hiding this comment

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

Unused variable refetchPresets.

Suggested change
refetchPresets,

Copilot uses AI. Check for mistakes.
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: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
components/preset-selector.tsx (2)

58-92: Missing response validation before success toast.

The fetch calls don't check response.ok before showing success toasts. If the API returns an error status (e.g., 400, 401, 500), the code still shows a success message.

       if (isCreatingNew) {
-        await fetch("/api/presets", {
+        const response = await fetch("/api/presets", {
           method: "POST",
           headers: { "Content-Type": "application/json" },
           body: JSON.stringify({
             calendarId,
             ...formData,
             password,
           }),
         });
+        if (!response.ok) {
+          throw new Error("Failed to create preset");
+        }
         toast.success(t("preset.created"));
       } else if (editingPreset) {
-        await fetch(`/api/presets/${editingPreset.id}`, {
+        const response = await fetch(`/api/presets/${editingPreset.id}`, {
           method: "PATCH",
           headers: { "Content-Type": "application/json" },
           body: JSON.stringify({ ...formData, password }),
         });
+        if (!response.ok) {
+          throw new Error("Failed to update preset");
+        }
         toast.success(t("preset.updated"));

94-118: Delete operation missing response validation.

Similar issue - the delete fetch doesn't check response.ok.

-        await fetch(url, { method: "DELETE" });
+        const response = await fetch(url, { method: "DELETE" });
+        if (!response.ok) {
+          throw new Error("Failed to delete preset");
+        }
🟡 Minor comments (7)
components/preset-edit-dialog.tsx-65-87 (1)

65-87: Reset form state when dialog closes.

The dialog resets state when opening but doesn't clear state when closing (when open becomes false). This violates the coding guideline that dialog components must "reset internal state when open changes to false."

As per coding guidelines, dialog components should reset their internal state when closed.

Apply this diff:

   useEffect(() => {
-    if (open && preset && !isCreating) {
+    if (open) {
+      if (preset && !isCreating) {
-      setFormData({
-        title: preset.title,
-        startTime: preset.startTime,
-        endTime: preset.endTime,
-        color: preset.color,
-        notes: preset.notes || "",
-        isSecondary: preset.isSecondary || false,
-        isAllDay: preset.isAllDay || false,
-      });
+        setFormData({
+          title: preset.title,
+          startTime: preset.startTime,
+          endTime: preset.endTime,
+          color: preset.color,
+          notes: preset.notes || "",
+          isSecondary: preset.isSecondary || false,
+          isAllDay: preset.isAllDay || false,
+        });
+      } else if (isCreating) {
-    } else if (open && isCreating) {
-      setFormData({
-        title: "",
-        startTime: "09:00",
-        endTime: "17:00",
-        color: PRESET_COLORS[0].value,
-        notes: "",
-        isSecondary: false,
-        isAllDay: false,
-      });
+        setFormData({
+          title: "",
+          startTime: "09:00",
+          endTime: "17:00",
+          color: PRESET_COLORS[0].value,
+          notes: "",
+          isSecondary: false,
+          isAllDay: false,
+        });
+      }
+    } else {
+      // Reset to defaults when dialog closes
+      setFormData({
+        title: "",
+        startTime: "09:00",
+        endTime: "17:00",
+        color: PRESET_COLORS[0].value,
+        notes: "",
+        isSecondary: false,
+        isAllDay: false,
+      });
     }
   }, [open, preset, isCreating]);
hooks/useCalendars.ts-20-27 (1)

20-27: Stale closure on selectedCalendar check.

The condition !selectedCalendar at line 25 captures the initial value of selectedCalendar (which is always undefined on mount). This works for the initial fetch but would be incorrect if fetchCalendars is called again via refetchCalendars. Consider using a ref or functional state check.

-      if (
-        initialCalendarId &&
-        data.some((cal: CalendarWithCount) => cal.id === initialCalendarId)
-      ) {
-        setSelectedCalendar(initialCalendarId);
-      } else if (data.length > 0 && !selectedCalendar) {
-        setSelectedCalendar(data[0].id);
-      }
+      setSelectedCalendar((current) => {
+        if (
+          initialCalendarId &&
+          data.some((cal: CalendarWithCount) => cal.id === initialCalendarId)
+        ) {
+          return initialCalendarId;
+        }
+        if (data.length > 0 && !current) {
+          return data[0].id;
+        }
+        return current;
+      });
hooks/usePasswordProtection.ts-15-26 (1)

15-26: Missing response.ok check before parsing JSON.

If the API returns a non-2xx status, response.json() may fail or return unexpected data. Add a guard to handle error responses gracefully.

       const response = await fetch(
         `/api/calendars/${calendarId}/verify-password`,
         {
           method: "POST",
           headers: { "Content-Type": "application/json" },
           body: JSON.stringify({ password }),
         }
       );

+      if (!response.ok) {
+        localStorage.removeItem(`calendar_password_${calendarId}`);
+        return false;
+      }
+
       const data = await response.json();
hooks/useShifts.ts-123-129 (1)

123-129: Missing dependency in useEffect.

fetchShifts is referenced but not listed in the dependency array. Either wrap fetchShifts in useCallback with [calendarId] as dependency, or inline the fetch logic within the effect.

+  const fetchShifts = useCallback(async () => {
+    if (!calendarId) return;
+    try {
+      const response = await fetch(`/api/shifts?calendarId=${calendarId}`);
+      const data = await response.json();
+      setShifts(data);
+    } catch (error) {
+      console.error("Failed to fetch shifts:", error);
+    }
+  }, [calendarId]);

   useEffect(() => {
     if (calendarId) {
       fetchShifts();
     } else {
       setShifts([]);
     }
-  }, [calendarId]);
+  }, [calendarId, fetchShifts]);

Committable suggestion skipped: line range outside the PR's diff.

app/page.tsx-370-394 (1)

370-394: Optimistic UI rollback uses stale reference.

When the delete fails (lines 382-394), the rollback uses shifts from closure, but shifts may have changed since the optimistic update on line 371. This could restore incorrect state.

Consider capturing the previous state before the optimistic update:

     if (existingShift) {
+      const previousShifts = shifts;
       setShifts(shifts.filter((s) => s.id !== existingShift.id));
       setStatsRefreshTrigger((prev) => prev + 1);

       try {
         ...
         if (!response.ok) {
-          setShifts(shifts);
+          setShifts(previousShifts);
           ...
         }
       } catch (error) {
-        setShifts(shifts);
+        setShifts(previousShifts);
         ...
       }
     }
hooks/useSSEConnection.ts-143-164 (1)

143-164: Potential timer leak on rapid calendarId changes.

The setTimeout calls in onerror aren't cleaned up if calendarId changes rapidly. The effect cleanup only closes the EventSource but doesn't clear the pending timeouts.

+    let errorTimeout: NodeJS.Timeout | null = null;
+    let reconnectTimeout: NodeJS.Timeout | null = null;

     eventSource.onerror = (error) => {
       console.error("SSE connection error:", error);
       setIsConnected(false);
       disconnectTimeRef.current = Date.now();
       eventSource.close();

-      const errorTimeout = setTimeout(() => {
+      errorTimeout = setTimeout(() => {
         if (!navigator.onLine) {
           toast.error(t("sync.disconnected"), { duration: Infinity });
         }
       }, 5000);

-      setTimeout(() => {
+      reconnectTimeout = setTimeout(() => {
         clearTimeout(errorTimeout);
         if (calendarId && navigator.onLine) {
           console.log("Attempting to reconnect and resync...");
           onShiftUpdate();
           onPresetUpdate();
           onNoteUpdate();
         }
       }, 3000);
     };

     ...

     return () => {
       eventSource.close();
+      if (errorTimeout) clearTimeout(errorTimeout);
+      if (reconnectTimeout) clearTimeout(reconnectTimeout);
     };
hooks/useSSEConnection.ts-29-81 (1)

29-81: Both useEffect hooks are missing callback dependencies.

The effect at line 29 uses onShiftUpdate, onPresetUpdate, onNoteUpdate, onStatsRefresh, and setIsConnected but excludes them from the dependency array [calendarId, t]. The same issue exists in the SSE connection effect at line 83 with dependency array [calendarId].

If these callbacks are not memoized at the call site (using useCallback), they should be added to both dependency arrays. If they are guaranteed to be stable references from the parent component, this should be documented with an ESLint disable comment explaining why.

🧹 Nitpick comments (7)
components/preset-list.tsx (1)

217-217: Move React import to the top of the file.

The React import appears at the bottom of the file (line 217) but is used at line 23 with React.useState. While this works after bundling, it violates conventional import ordering and reduces code readability.

+import React from "react";
 import { motion } from "motion/react";
 import { useTranslations } from "next-intl";
 import { Button } from "@/components/ui/button";
 import { Check, Plus, Settings, ChevronDown, ChevronUp } from "lucide-react";
 import { ShiftPreset } from "@/lib/db/schema";
 
 // ... rest of file ...
 
-import React from "react";
components/app-header.tsx (1)

103-116: Consider extracting duplicated gradient styling logic.

The calendar color gradient logic (lines 103-116 and 146-159) is duplicated. Consider extracting this into a helper function or a reusable component to improve maintainability.

// Helper at top of component or in a utils file
const getCalendarGradient = (
  calendars: CalendarWithCount[],
  selectedCalendar: string | undefined
) => {
  const color = calendars.find((c) => c.id === selectedCalendar)?.color;
  return color
    ? `linear-gradient(to bottom, ${color}, ${color}80)`
    : "linear-gradient(to bottom, hsl(var(--primary)), hsl(var(--primary) / 0.5))";
};
components/shift-dialog.tsx (2)

90-92: Remove unnecessary async keyword.

handlePresetSelect is declared async but doesn't use await - the function body is synchronous.

-  const handlePresetSelect = async (preset: Parameters<typeof applyPreset>[0]) => {
+  const handlePresetSelect = (preset: Parameters<typeof applyPreset>[0]) => {
     applyPreset(preset);
   };

63-88: Verify error handling for saveAsPresetHandler.

The saveAsPresetHandler call's failure case only logs via the hook, but the dialog proceeds to reset and close regardless. If preset saving fails, the user sees no feedback except a console error.

Consider surfacing the failure to the user:

     // Save as preset if enabled and it's a new shift
     if (!shift && saveAsPreset && presetName.trim()) {
       const success = await saveAsPresetHandler(submitData);
-      if (success && onPresetsChange) {
+      if (success) {
+        onPresetsChange?.();
+      } else {
+        // saveAsPresetHandler already logs the error; optionally notify user
+      }
-        onPresetsChange();
-      }
     }
components/shifts-list.tsx (2)

26-48: Consider memoizing filtered and grouped shifts.

The shiftsInMonth filter and groupedShifts computation run on every render. For large shift lists, this could cause performance issues.

+import { useState, useMemo } from "react";
...
-  const shiftsInMonth = shifts.filter((shift) => {
+  const shiftsInMonth = useMemo(() => shifts.filter((shift) => {
     if (!shift.date) return false;
     const shiftDate = new Date(shift.date);
     return (
       shiftDate.getMonth() === currentDate.getMonth() &&
       shiftDate.getFullYear() === currentDate.getFullYear()
     );
-  });
+  }), [shifts, currentDate]);

-  const groupedShifts = shiftsInMonth
+  const groupedShifts = useMemo(() => shiftsInMonth
     .sort(...)
-    .reduce(...);
+    .reduce(...), [shiftsInMonth]);

50-78: Collapsible button accessibility improvement.

The toggle button lacks aria-expanded for screen readers.

       <button
         onClick={() => setShowShiftsSection(!showShiftsSection)}
         className="w-full px-3 sm:px-4 py-3 sm:py-3.5 flex items-center justify-between hover:bg-primary/5 transition-all"
+        aria-expanded={showShiftsSection}
       >
app/page.tsx (1)

324-326: Early return could provide user feedback.

When !selectedPresetId, the function silently returns. Consider showing a toast to guide the user.

-    if (isTogglingShift || !selectedPresetId) return;
+    if (isTogglingShift) return;
+    if (!selectedPresetId) {
+      toast.info(t("shift.selectPresetFirst"));
+      return;
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d9f48a3 and 76fa322.

📒 Files selected for processing (18)
  • app/page.tsx (15 hunks)
  • components/app-header.tsx (1 hunks)
  • components/calendar-grid.tsx (1 hunks)
  • components/preset-edit-dialog.tsx (1 hunks)
  • components/preset-list.tsx (1 hunks)
  • components/preset-manage-dialog.tsx (1 hunks)
  • components/preset-select.tsx (1 hunks)
  • components/preset-selector.tsx (4 hunks)
  • components/shift-dialog.tsx (3 hunks)
  • components/shift-form-fields.tsx (1 hunks)
  • components/shifts-list.tsx (1 hunks)
  • hooks/useCalendars.ts (1 hunks)
  • hooks/useNotes.ts (1 hunks)
  • hooks/usePasswordProtection.ts (1 hunks)
  • hooks/usePresets.ts (1 hunks)
  • hooks/useSSEConnection.ts (1 hunks)
  • hooks/useShiftForm.ts (1 hunks)
  • hooks/useShifts.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
components/**/*.tsx

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

Use PRESET_COLORS constant array for all color picker options

Files:

  • components/preset-select.tsx
  • components/preset-edit-dialog.tsx
  • components/shifts-list.tsx
  • components/shift-form-fields.tsx
  • components/calendar-grid.tsx
  • components/preset-manage-dialog.tsx
  • components/preset-list.tsx
  • components/shift-dialog.tsx
  • components/app-header.tsx
  • components/preset-selector.tsx
**/*.{ts,tsx}

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

Store colors as hex values (e.g., #3b82f6) and apply 20% opacity for backgrounds using format ${color}20

Files:

  • components/preset-select.tsx
  • components/preset-edit-dialog.tsx
  • hooks/usePresets.ts
  • components/shifts-list.tsx
  • components/shift-form-fields.tsx
  • components/calendar-grid.tsx
  • hooks/useSSEConnection.ts
  • components/preset-manage-dialog.tsx
  • hooks/useShiftForm.ts
  • hooks/useNotes.ts
  • hooks/useCalendars.ts
  • components/preset-list.tsx
  • hooks/useShifts.ts
  • components/shift-dialog.tsx
  • components/app-header.tsx
  • app/page.tsx
  • hooks/usePasswordProtection.ts
  • components/preset-selector.tsx
components/**/*-dialog.tsx

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

components/**/*-dialog.tsx: Dialog components must control open state via props, reset internal state when open changes to false, and call parent callback on form submission
Form submission in dialogs: prevent default, validate, call parent callback, close dialog

Files:

  • components/preset-edit-dialog.tsx
  • components/preset-manage-dialog.tsx
  • components/shift-dialog.tsx
components/**/calendar*.tsx

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

Calendar interaction: left-click toggles shift (requires preset selection), right-click opens note dialog with e.preventDefault()

Files:

  • components/calendar-grid.tsx
app/page.tsx

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

app/page.tsx: Password verification for calendar access must use pendingAction state pattern: set pending action, show PasswordDialog, retry after password verification via /api/calendars/[id]/verify-password
Main page (app/page.tsx) uses URL sync via useRouter().replace() for selected calendar
Refresh data after mutations by incrementing statsRefreshTrigger counter

Files:

  • app/page.tsx
app/**/*.{ts,tsx}

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

Use useTranslations() hook for all user-facing text and t("key") to access translation keys from messages/{de,en}.json

Files:

  • app/page.tsx
app/**/*.tsx

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

app/**/*.tsx: Use formatDateToLocal() helper for consistent YYYY-MM-DD date formatting in UI
Shift toggle logic must check if matching shift exists: delete if present, create if not
Use locale-specific date formatters in components: locale === "de" ? de : enUS
Frontend uses Next.js 16 App Router (app/ directory), React 19, and client-side state management via useState/useEffect

Files:

  • app/page.tsx
🧠 Learnings (13)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Shift toggle logic must check if matching shift exists: delete if present, create if not
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/page.tsx : Main page (app/page.tsx) uses URL sync via useRouter().replace() for selected calendar
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to components/**/calendar*.tsx : Calendar interaction: left-click toggles shift (requires preset selection), right-click opens note dialog with e.preventDefault()
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to components/**/*.tsx : Use PRESET_COLORS constant array for all color picker options

Applied to files:

  • components/preset-select.tsx
  • components/preset-edit-dialog.tsx
  • hooks/usePresets.ts
  • components/shift-form-fields.tsx
  • components/preset-manage-dialog.tsx
  • components/preset-list.tsx
  • components/shift-dialog.tsx
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to components/**/calendar*.tsx : Calendar interaction: left-click toggles shift (requires preset selection), right-click opens note dialog with e.preventDefault()

Applied to files:

  • components/preset-select.tsx
  • components/preset-edit-dialog.tsx
  • hooks/usePresets.ts
  • components/shifts-list.tsx
  • components/shift-form-fields.tsx
  • components/calendar-grid.tsx
  • components/preset-manage-dialog.tsx
  • hooks/useShiftForm.ts
  • components/preset-list.tsx
  • hooks/useShifts.ts
  • components/shift-dialog.tsx
  • components/app-header.tsx
  • app/page.tsx
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Shift toggle logic must check if matching shift exists: delete if present, create if not

Applied to files:

  • components/preset-select.tsx
  • components/shifts-list.tsx
  • components/shift-form-fields.tsx
  • hooks/useShiftForm.ts
  • hooks/useShifts.ts
  • components/shift-dialog.tsx
  • app/page.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to components/**/*-dialog.tsx : Dialog components must control open state via props, reset internal state when open changes to false, and call parent callback on form submission

Applied to files:

  • components/preset-edit-dialog.tsx
  • components/shift-form-fields.tsx
  • components/preset-manage-dialog.tsx
  • hooks/useShiftForm.ts
  • components/shift-dialog.tsx
  • app/page.tsx
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to components/**/*-dialog.tsx : Form submission in dialogs: prevent default, validate, call parent callback, close dialog

Applied to files:

  • components/preset-edit-dialog.tsx
  • components/preset-manage-dialog.tsx
  • hooks/useShiftForm.ts
  • components/shift-dialog.tsx
  • app/page.tsx
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Use locale-specific date formatters in components: locale === "de" ? de : enUS

Applied to files:

  • components/shift-form-fields.tsx
  • app/page.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Use formatDateToLocal() helper for consistent YYYY-MM-DD date formatting in UI

Applied to files:

  • components/calendar-grid.tsx
  • app/page.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/page.tsx : Password verification for calendar access must use pendingAction state pattern: set pending action, show PasswordDialog, retry after password verification via /api/calendars/[id]/verify-password

Applied to files:

  • hooks/useCalendars.ts
  • hooks/useShifts.ts
  • components/app-header.tsx
  • app/page.tsx
  • hooks/usePasswordProtection.ts
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/page.tsx : Main page (app/page.tsx) uses URL sync via useRouter().replace() for selected calendar

Applied to files:

  • hooks/useCalendars.ts
  • components/app-header.tsx
  • app/page.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Frontend uses Next.js 16 App Router (app/ directory), React 19, and client-side state management via useState/useEffect

Applied to files:

  • components/shift-dialog.tsx
  • components/app-header.tsx
  • app/page.tsx
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.{ts,tsx} : Use useTranslations() hook for all user-facing text and t("key") to access translation keys from messages/{de,en}.json

Applied to files:

  • components/shift-dialog.tsx
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/page.tsx : Refresh data after mutations by incrementing statsRefreshTrigger counter

Applied to files:

  • app/page.tsx
🧬 Code graph analysis (14)
components/preset-select.tsx (3)
lib/db/schema.ts (1)
  • ShiftPreset (92-92)
components/ui/label.tsx (1)
  • Label (24-24)
components/ui/select.tsx (5)
  • Select (177-177)
  • SelectTrigger (185-185)
  • SelectValue (186-186)
  • SelectContent (178-178)
  • SelectItem (180-180)
hooks/usePresets.ts (1)
lib/db/schema.ts (1)
  • ShiftPreset (92-92)
components/shifts-list.tsx (3)
lib/types.ts (1)
  • ShiftWithCalendar (14-31)
lib/db/schema.ts (1)
  • shifts (19-45)
components/shift-card.tsx (1)
  • ShiftCard (15-68)
components/calendar-grid.tsx (2)
lib/types.ts (1)
  • ShiftWithCalendar (14-31)
lib/db/schema.ts (1)
  • CalendarNote (94-94)
components/preset-manage-dialog.tsx (3)
lib/db/schema.ts (1)
  • ShiftPreset (92-92)
components/ui/dialog.tsx (5)
  • Dialog (139-139)
  • DialogContent (141-141)
  • DialogHeader (144-144)
  • DialogTitle (147-147)
  • DialogDescription (142-142)
components/ui/button.tsx (1)
  • Button (60-60)
hooks/useShiftForm.ts (4)
lib/types.ts (1)
  • ShiftWithCalendar (14-31)
components/shift-dialog.tsx (1)
  • ShiftFormData (27-36)
lib/date-utils.ts (1)
  • formatDateToLocal (5-10)
lib/db/schema.ts (1)
  • ShiftPreset (92-92)
hooks/useNotes.ts (2)
lib/db/schema.ts (1)
  • CalendarNote (94-94)
lib/date-utils.ts (1)
  • formatDateToLocal (5-10)
hooks/useCalendars.ts (2)
lib/db/schema.ts (1)
  • calendars (4-17)
lib/types.ts (1)
  • CalendarWithCount (4-12)
components/preset-list.tsx (2)
lib/db/schema.ts (1)
  • ShiftPreset (92-92)
components/ui/button.tsx (1)
  • Button (60-60)
hooks/useShifts.ts (3)
lib/db/schema.ts (1)
  • shifts (19-45)
lib/types.ts (1)
  • ShiftWithCalendar (14-31)
components/shift-dialog.tsx (1)
  • ShiftFormData (27-36)
components/shift-dialog.tsx (3)
hooks/useShiftForm.ts (1)
  • useShiftForm (14-149)
components/preset-select.tsx (1)
  • PresetSelect (17-52)
components/shift-form-fields.tsx (1)
  • ShiftFormFields (31-205)
app/page.tsx (10)
hooks/useCalendars.ts (1)
  • useCalendars (6-105)
hooks/useShifts.ts (1)
  • useShifts (7-139)
hooks/usePresets.ts (1)
  • usePresets (4-31)
hooks/useNotes.ts (1)
  • useNotes (7-87)
hooks/useSSEConnection.ts (1)
  • useSSEConnection (15-177)
lib/db/schema.ts (2)
  • shifts (19-45)
  • calendars (4-17)
lib/date-utils.ts (1)
  • formatDateToLocal (5-10)
components/app-header.tsx (1)
  • AppHeader (36-251)
components/calendar-grid.tsx (1)
  • CalendarGrid (21-178)
components/shifts-list.tsx (1)
  • ShiftsList (16-173)
hooks/usePasswordProtection.ts (1)
lib/password-utils.ts (1)
  • verifyPassword (16-23)
components/preset-selector.tsx (5)
lib/db/schema.ts (1)
  • ShiftPreset (92-92)
hooks/usePasswordProtection.ts (1)
  • usePasswordProtection (8-64)
components/preset-edit-dialog.tsx (2)
  • PresetFormData (252-252)
  • PresetEditDialog (47-250)
components/preset-list.tsx (1)
  • PresetList (15-126)
components/preset-manage-dialog.tsx (1)
  • PresetManageDialog (22-102)
🔇 Additional comments (13)
components/preset-select.tsx (1)

17-52: LGTM!

The PresetSelect component is well-structured with proper early returns, clean separation of concerns, and good use of translations. The color and time range display logic is clear and handles both all-day and timed shifts appropriately.

components/calendar-grid.tsx (1)

146-148: LGTM!

The color application correctly uses the ${color}20 format for 20% opacity backgrounds, following the coding guidelines. The color bar with left border provides good visual hierarchy.

As per coding guidelines, colors are stored as hex values with 20% opacity applied using the ${color}20 format.

components/shift-form-fields.tsx (1)

31-205: LGTM!

The ShiftFormFields component is well-structured with clean separation of concerns. The use of animated transitions for conditional fields (all-day toggle, preset name input) provides a polished UX. Form state management through props maintains proper unidirectional data flow.

hooks/usePasswordProtection.ts (1)

39-53: Consider memoization stability for onPasswordRequired.

onPasswordRequired is included in the dependency array but is likely an inline function from the parent. This could cause withPasswordCheck to be recreated on every render. Ensure the caller wraps onPasswordRequired in useCallback, or consider using a ref to hold the callback to avoid stale closure issues.

components/preset-manage-dialog.tsx (1)

22-101: LGTM!

The dialog correctly controls open state via props and follows the coding guidelines. The component is stateless and delegates all actions to parent callbacks, which is the correct pattern for dialog components. The preset list rendering with edit/delete actions is well-structured.

hooks/useCalendars.ts (1)

92-94: Consider memoizing fetchCalendars or note the dependency caveat.

fetchCalendars references initialCalendarId and selectedCalendar but the effect only runs once (empty deps). This is intentional for initial load, but if refetchCalendars is exposed and called later, the stale closure issues noted above would manifest. If refetching should respect the current selection, consider wrapping in useCallback with appropriate deps.

components/shift-dialog.tsx (1)

49-61: Good refactor to hook-based form management.

The extraction of form state to useShiftForm hook improves separation of concerns. The hook properly handles state initialization when the dialog opens (as shown in the relevant code snippet for useShiftForm lines 105-120).

components/shifts-list.tsx (1)

1-8: LGTM - Proper imports and dependencies.

Good use of locale-specific date formatters (de, enUS) from date-fns, and proper integration with next-intl for translations. As per coding guidelines, this follows the pattern locale === "de" ? de : enUS.

app/page.tsx (3)

53-82: Good hook-based data management refactor.

Clean extraction of data management to custom hooks. The hooks follow consistent patterns for fetching, mutations, and refetching. URL sync via useRouter().replace() is maintained as per guidelines.


115-124: SSE connection properly wired to hooks.

Good integration of useSSEConnection with the refetch callbacks. The onStatsRefresh correctly increments the trigger counter as per guidelines.


361-408: Shift toggle logic correctly implements check-then-act pattern.

The handleAddShift function properly checks if a matching shift exists (by title, startTime, endTime on the same day) and deletes if present or creates if not. This aligns with the learning: "Shift toggle logic must check if matching shift exists: delete if present, create if not."

components/preset-selector.tsx (2)

7-10: Good componentization of preset management.

Clean extraction of UI into PresetList, PresetEditDialog, and PresetManageDialog components. The usePasswordProtection hook centralizes password verification logic.


130-143: Dialog cascade handled correctly.

Good pattern: closing PresetManageDialog before opening PresetEditDialog prevents dialog stacking issues.

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/api/notes/route.ts (1)

9-54: Critical security gap: GET route bypasses password protection.

The GET endpoint allows anyone to read notes from password-protected calendars without authentication. While the POST handler (lines 82-90) correctly verifies passwords before creating notes, the GET handler has no such protection. This defeats the purpose of calendar password protection.

Apply this diff to add password verification to the GET route:

 export async function GET(request: Request) {
   try {
     const { searchParams } = new URL(request.url);
     const calendarId = searchParams.get("calendarId");
     const date = searchParams.get("date");
+    const password = searchParams.get("password");
 
     if (!calendarId) {
       return NextResponse.json(
         { error: "Calendar ID is required" },
         { status: 400 }
       );
     }
 
+    // Fetch calendar to check password
+    const [calendar] = await db
+      .select()
+      .from(calendars)
+      .where(eq(calendars.id, calendarId));
+
+    if (!calendar) {
+      return NextResponse.json(
+        { error: "Calendar not found" },
+        { status: 404 }
+      );
+    }
+
+    // Verify password if calendar is protected
+    if (calendar.passwordHash) {
+      if (!password || !verifyPassword(password, calendar.passwordHash)) {
+        return NextResponse.json(
+          { error: "Invalid password" },
+          { status: 401 }
+        );
+      }
+    }
+
     let query = db
       .select()
🧹 Nitpick comments (3)
hooks/usePresets.ts (1)

7-31: Optional: guard against stale preset responses when calendar changes quickly

If the user switches calendars fast, an earlier /api/presets response for a previous calendarId can arrive after a later one and overwrite presets with stale data. Not a blocker, but you could tighten this by tracking the active request, e.g. with an AbortController or a currentCalendarIdRef check before calling setPresets.

components/calendar-grid.tsx (1)

35-45: Minor: make long‑press timer typing/browser semantics a bit more robust

pressTimerRef is typed as Record<string, NodeJS.Timeout>, which is Node‑centric; in the browser setTimeout returns a number. Not a bug today, but you can make this component more platform‑agnostic by switching to:

const pressTimerRef = useRef<Record<string, ReturnType<typeof setTimeout>>>({});

The rest of the logic (per‑day keys, cleanup in the unmount effect, and clearing on touch end/move) looks solid and matches the required calendar interactions.

Also applies to: 87-100

app/page.tsx (1)

382-470: Shift toggle behavior is correct; consider functional state updates for robustness

The handleAddShift implementation correctly follows the requirement: with a selected preset, left‑clicking a day checks for a matching shift (same day/title/start/end) and deletes it if present, otherwise creates a new one using formatDateToLocal. This aligns with the documented shift toggle behavior. As per coding guidelines, this is the right logic.

One minor robustness issue is that both the optimistic delete and the rollback use the captured shifts array:

setShifts(shifts.filter((s) => s.id !== existingShift.id));
// ...
setShifts(shifts);

If shifts changes between the click and the async DELETE completing (e.g., because SSE or some other action updated shifts), these calls can clobber newer state.

You can avoid that by switching to functional updates, for example:

setShifts((prev) => prev.filter((s) => s.id !== existingShift.id));
// ...
setShifts((prev) => prev); // or refetchShifts() instead of trying to roll back

The same applies to the rollback paths. Not critical, but it makes the toggle logic safer under concurrent updates.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 76fa322 and decb04d.

📒 Files selected for processing (12)
  • app/api/notes/[id]/route.ts (3 hunks)
  • app/api/notes/route.ts (3 hunks)
  • app/page.tsx (16 hunks)
  • components/app-header.tsx (1 hunks)
  • components/calendar-grid.tsx (1 hunks)
  • components/preset-selector.tsx (3 hunks)
  • hooks/useCalendars.ts (1 hunks)
  • hooks/useNotes.ts (1 hunks)
  • hooks/usePresets.ts (1 hunks)
  • hooks/useSSEConnection.ts (1 hunks)
  • hooks/useShiftForm.ts (1 hunks)
  • hooks/useShifts.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • hooks/useShifts.ts
  • hooks/useShiftForm.ts
  • hooks/useCalendars.ts
🧰 Additional context used
📓 Path-based instructions (8)
app/api/**/*.ts

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

app/api/**/*.ts: Always await dynamic route params in API routes and server components (Next.js 16 breaking change)
Create API route handlers for new database tables at app/api/tablename/route.ts and app/api/tablename/[id]/route.ts
Log all API errors with console.error() for debugging

Files:

  • app/api/notes/[id]/route.ts
  • app/api/notes/route.ts
app/api/**/route.ts

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

API GET routes must validate required query parameters and return 400 status for missing params

Files:

  • app/api/notes/[id]/route.ts
  • app/api/notes/route.ts
app/**/*.{ts,tsx}

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

Use useTranslations() hook for all user-facing text and t("key") to access translation keys from messages/{de,en}.json

Files:

  • app/api/notes/[id]/route.ts
  • app/api/notes/route.ts
  • app/page.tsx
**/*.{ts,tsx}

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

Store colors as hex values (e.g., #3b82f6) and apply 20% opacity for backgrounds using format ${color}20

Files:

  • app/api/notes/[id]/route.ts
  • app/api/notes/route.ts
  • hooks/useNotes.ts
  • components/calendar-grid.tsx
  • hooks/useSSEConnection.ts
  • hooks/usePresets.ts
  • components/app-header.tsx
  • app/page.tsx
  • components/preset-selector.tsx
components/**/*.tsx

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

Use PRESET_COLORS constant array for all color picker options

Files:

  • components/calendar-grid.tsx
  • components/app-header.tsx
  • components/preset-selector.tsx
components/**/calendar*.tsx

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

Calendar interaction: left-click toggles shift (requires preset selection), right-click opens note dialog with e.preventDefault()

Files:

  • components/calendar-grid.tsx
app/page.tsx

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

app/page.tsx: Password verification for calendar access must use pendingAction state pattern: set pending action, show PasswordDialog, retry after password verification via /api/calendars/[id]/verify-password
Main page (app/page.tsx) uses URL sync via useRouter().replace() for selected calendar
Refresh data after mutations by incrementing statsRefreshTrigger counter

Files:

  • app/page.tsx
app/**/*.tsx

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

app/**/*.tsx: Use formatDateToLocal() helper for consistent YYYY-MM-DD date formatting in UI
Shift toggle logic must check if matching shift exists: delete if present, create if not
Use locale-specific date formatters in components: locale === "de" ? de : enUS
Frontend uses Next.js 16 App Router (app/ directory), React 19, and client-side state management via useState/useEffect

Files:

  • app/page.tsx
🧠 Learnings (13)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Shift toggle logic must check if matching shift exists: delete if present, create if not
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/page.tsx : Password verification for calendar access must use pendingAction state pattern: set pending action, show PasswordDialog, retry after password verification via /api/calendars/[id]/verify-password
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/page.tsx : Main page (app/page.tsx) uses URL sync via useRouter().replace() for selected calendar
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to components/**/calendar*.tsx : Calendar interaction: left-click toggles shift (requires preset selection), right-click opens note dialog with e.preventDefault()
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/page.tsx : Password verification for calendar access must use pendingAction state pattern: set pending action, show PasswordDialog, retry after password verification via /api/calendars/[id]/verify-password

Applied to files:

  • app/api/notes/[id]/route.ts
  • app/api/notes/route.ts
  • components/app-header.tsx
  • app/page.tsx
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to components/**/calendar*.tsx : Calendar interaction: left-click toggles shift (requires preset selection), right-click opens note dialog with e.preventDefault()

Applied to files:

  • components/calendar-grid.tsx
  • hooks/useSSEConnection.ts
  • hooks/usePresets.ts
  • components/app-header.tsx
  • app/page.tsx
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Use formatDateToLocal() helper for consistent YYYY-MM-DD date formatting in UI

Applied to files:

  • components/calendar-grid.tsx
  • app/page.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to components/**/*.tsx : Use PRESET_COLORS constant array for all color picker options

Applied to files:

  • hooks/usePresets.ts
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/page.tsx : Main page (app/page.tsx) uses URL sync via useRouter().replace() for selected calendar

Applied to files:

  • components/app-header.tsx
  • app/page.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Frontend uses Next.js 16 App Router (app/ directory), React 19, and client-side state management via useState/useEffect

Applied to files:

  • components/app-header.tsx
  • app/page.tsx
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Shift toggle logic must check if matching shift exists: delete if present, create if not

Applied to files:

  • app/page.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Use locale-specific date formatters in components: locale === "de" ? de : enUS

Applied to files:

  • app/page.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/page.tsx : Refresh data after mutations by incrementing statsRefreshTrigger counter

Applied to files:

  • app/page.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to components/**/*-dialog.tsx : Form submission in dialogs: prevent default, validate, call parent callback, close dialog

Applied to files:

  • app/page.tsx
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to components/**/*-dialog.tsx : Dialog components must control open state via props, reset internal state when open changes to false, and call parent callback on form submission

Applied to files:

  • app/page.tsx
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.{ts,tsx} : Use useTranslations() hook for all user-facing text and t("key") to access translation keys from messages/{de,en}.json

Applied to files:

  • components/preset-selector.tsx
🧬 Code graph analysis (7)
app/api/notes/[id]/route.ts (3)
lib/db/index.ts (1)
  • db (19-19)
lib/db/schema.ts (2)
  • calendarNotes (71-86)
  • calendars (4-17)
lib/password-utils.ts (1)
  • verifyPassword (16-23)
app/api/notes/route.ts (3)
lib/db/index.ts (1)
  • db (19-19)
lib/db/schema.ts (1)
  • calendars (4-17)
lib/password-utils.ts (1)
  • verifyPassword (16-23)
hooks/useNotes.ts (2)
lib/db/schema.ts (1)
  • CalendarNote (94-94)
lib/date-utils.ts (1)
  • formatDateToLocal (5-10)
components/calendar-grid.tsx (2)
lib/types.ts (1)
  • ShiftWithCalendar (14-31)
lib/db/schema.ts (2)
  • CalendarNote (94-94)
  • shifts (19-45)
hooks/usePresets.ts (1)
lib/db/schema.ts (1)
  • ShiftPreset (92-92)
components/app-header.tsx (4)
lib/types.ts (1)
  • CalendarWithCount (4-12)
lib/db/schema.ts (2)
  • ShiftPreset (92-92)
  • calendars (4-17)
components/calendar-selector.tsx (1)
  • CalendarSelector (24-86)
components/preset-selector.tsx (1)
  • PresetSelector (25-189)
components/preset-selector.tsx (5)
lib/db/schema.ts (1)
  • ShiftPreset (92-92)
hooks/usePasswordProtection.ts (1)
  • usePasswordProtection (8-64)
components/preset-edit-dialog.tsx (2)
  • PresetFormData (252-252)
  • PresetEditDialog (47-250)
components/preset-list.tsx (1)
  • PresetList (15-126)
components/preset-manage-dialog.tsx (1)
  • PresetManageDialog (22-102)
⏰ 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-and-push
🔇 Additional comments (7)
app/api/notes/route.ts (3)

3-3: LGTM! Imports support password verification.

The new imports for calendars schema and verifyPassword utility are correctly used in the POST handler's password verification logic.

Also applies to: 6-6


82-90: Password verification logic is correct.

The password verification properly checks for the presence of passwordHash and validates the provided password using the verifyPassword utility. The 401 response for invalid passwords is appropriate.


69-80: Foreign key constraint is properly configured.

The database schema already enforces the foreign key constraint on calendarNotes.calendarId with cascade delete (see lib/db/schema.ts:77). This provides database-level protection against orphaned notes and race conditions, making the application-level calendar existence check redundant but still good practice for early error feedback.

app/api/notes/[id]/route.ts (2)

3-3: LGTM!

The additional imports for calendars schema and verifyPassword utility are necessary and correctly used for the password verification flow.

Also applies to: 6-6


45-45: LGTM! Password verification flow is correctly implemented.

The PUT handler properly:

  • Extracts password from the request body (secure for PUT operations)
  • Fetches the existing note to obtain the calendarId
  • Loads the related calendar to check password protection
  • Verifies the password before allowing updates
  • Returns appropriate HTTP status codes (404 for not found, 401 for unauthorized)

This aligns with the password-protected calendar pattern used throughout the application.

Also applies to: 51-85

components/preset-selector.tsx (1)

61-153: Preset CRUD flow and password integration look solid

The refactored handleSavePreset / handleDeletePreset logic is cohesive:

  • All mutations go through usePasswordProtection.withPasswordCheck, so protected calendars consistently trigger the shared onPasswordRequired flow.
  • Responses are validated with response.ok, error bodies are logged, and user feedback is surfaced via toasts for both save and delete paths.
  • On success you call onPresetsChange (and onShiftsChange when relevant) and close/reset the edit dialog state, which keeps the UI in sync.

This is a nice improvement over inline preset handling and matches the centralized password‑protection approach introduced elsewhere in the PR.

hooks/useNotes.ts (1)

11-27: No changes needed—GET /api/notes does not require password verification

The review's concern assumes that all note API endpoints require password protection for protected calendars. However, examining the actual API implementation shows that only write operations (POST, PUT, DELETE) verify passwords. The GET endpoints for fetching notes have no password checks, so fetchNotes will never encounter a 401 error. This is an intentional design: read operations are public, while write operations are protected. The code is correctly aligned with this model.

Likely an incorrect or invalid review comment.

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: 0

♻️ Duplicate comments (3)
app/api/notes/[id]/route.ts (1)

129-140: Past security concern addressed - password now read from request body.

This change correctly addresses the previous review comment about password exposure in query strings. The password is now securely read from the request body.

app/page.tsx (2)

37-37: Remove unused ShiftPreset import.

ShiftPreset is imported but not used in this file. Only CalendarNote is referenced.

-import { CalendarNote, ShiftPreset } from "@/lib/db/schema";
+import { CalendarNote } from "@/lib/db/schema";

326-335: Remove unused handleUpdateShift function.

This function is defined but never called or passed to any component. ShiftsList only receives onDeleteShift (line 668). If shift editing functionality is planned, consider adding it; otherwise, remove to reduce dead code.

🧹 Nitpick comments (5)
app/api/presets/[id]/route.ts (1)

114-126: Password extraction from request body looks good, but consider logging parse failures.

The change from URL params to request body for password handling is a security improvement. However, the silent catch block on line 123-125 swallows parsing errors without any logging, which could make debugging difficult if clients send malformed JSON.

       try {
         const body = await request.json();
         password = body.password || null;
-      } catch (e) {
+      } catch {
         // If body parsing fails, continue with null password
+        console.warn("Failed to parse request body in DELETE /api/presets/[id]");
       }
app/api/shifts/[id]/route.ts (1)

147-159: Consistent password extraction pattern - same silent catch concern.

The password extraction from request body matches the pattern in other DELETE handlers. Same suggestion as for presets: consider adding a warning log in the catch block for debugging purposes.

       try {
         const body = await request.json();
         password = body.password || null;
-      } catch (e) {
+      } catch {
         // If body parsing fails, continue with null password
+        console.warn("Failed to parse request body in DELETE /api/shifts/[id]");
       }
app/api/calendars/[id]/route.ts (1)

108-120: Consistent password extraction pattern.

Same pattern as other DELETE handlers. Consider adding warning log in catch block for debugging.

       try {
         const body = await request.json();
         password = body.password || null;
-      } catch (e) {
+      } catch {
         // If body parsing fails, continue with null password
+        console.warn("Failed to parse request body in DELETE /api/calendars/[id]");
       }
hooks/useSSEConnection.ts (1)

5-13: Consider removing unused isConnected parameter.

The isConnected parameter is destructured but never used within the hook. Only setIsConnected is used. If it's not needed, removing it would simplify the interface.

 interface SSEConnectionOptions {
   calendarId: string | undefined;
   onShiftUpdate: () => void;
   onPresetUpdate: () => void;
   onNoteUpdate: () => void;
   onStatsRefresh: () => void;
-  isConnected: boolean;
   setIsConnected: (connected: boolean) => void;
 }

 export function useSSEConnection({
   calendarId,
   onShiftUpdate,
   onPresetUpdate,
   onNoteUpdate,
   onStatsRefresh,
-  isConnected,
   setIsConnected,
 }: SSEConnectionOptions) {

Also applies to: 21-21

components/app-header.tsx (1)

103-116: Consider caching the calendar lookup.

The calendars.find((c) => c.id === selectedCalendar) is called twice in the gradient style computation. While not a performance blocker, extracting to a variable would improve readability.

+            const selectedCalendarData = calendars.find((c) => c.id === selectedCalendar);
             <div
               className="w-1 h-8 bg-gradient-to-b rounded-full transition-colors duration-300"
               style={{
                 backgroundImage: selectedCalendar
                   ? `linear-gradient(to bottom, ${
-                      calendars.find((c) => c.id === selectedCalendar)
-                        ?.color || "hsl(var(--primary))"
+                      selectedCalendarData?.color || "hsl(var(--primary))"
                     }, ${
-                      calendars.find((c) => c.id === selectedCalendar)
-                        ?.color || "hsl(var(--primary))"
+                      selectedCalendarData?.color || "hsl(var(--primary))"
                     }80)`
                   : "linear-gradient(to bottom, hsl(var(--primary)), hsl(var(--primary) / 0.5))",
               }}
             ></div>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between decb04d and a7d7c66.

📒 Files selected for processing (11)
  • app/api/calendars/[id]/route.ts (1 hunks)
  • app/api/notes/[id]/route.ts (3 hunks)
  • app/api/presets/[id]/route.ts (1 hunks)
  • app/api/shifts/[id]/route.ts (1 hunks)
  • app/page.tsx (16 hunks)
  • components/app-header.tsx (1 hunks)
  • components/preset-selector.tsx (3 hunks)
  • hooks/useCalendars.ts (1 hunks)
  • hooks/useNotes.ts (1 hunks)
  • hooks/useSSEConnection.ts (1 hunks)
  • hooks/useShifts.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • hooks/useNotes.ts
  • hooks/useCalendars.ts
  • hooks/useShifts.ts
🧰 Additional context used
📓 Path-based instructions (7)
app/api/**/*.ts

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

app/api/**/*.ts: Always await dynamic route params in API routes and server components (Next.js 16 breaking change)
Create API route handlers for new database tables at app/api/tablename/route.ts and app/api/tablename/[id]/route.ts
Log all API errors with console.error() for debugging

Files:

  • app/api/notes/[id]/route.ts
  • app/api/calendars/[id]/route.ts
  • app/api/shifts/[id]/route.ts
  • app/api/presets/[id]/route.ts
app/api/**/route.ts

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

API GET routes must validate required query parameters and return 400 status for missing params

Files:

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

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

Use useTranslations() hook for all user-facing text and t("key") to access translation keys from messages/{de,en}.json

Files:

  • app/api/notes/[id]/route.ts
  • app/api/calendars/[id]/route.ts
  • app/api/shifts/[id]/route.ts
  • app/api/presets/[id]/route.ts
  • app/page.tsx
**/*.{ts,tsx}

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

Store colors as hex values (e.g., #3b82f6) and apply 20% opacity for backgrounds using format ${color}20

Files:

  • app/api/notes/[id]/route.ts
  • app/api/calendars/[id]/route.ts
  • app/api/shifts/[id]/route.ts
  • app/api/presets/[id]/route.ts
  • components/app-header.tsx
  • hooks/useSSEConnection.ts
  • app/page.tsx
  • components/preset-selector.tsx
components/**/*.tsx

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

Use PRESET_COLORS constant array for all color picker options

Files:

  • components/app-header.tsx
  • components/preset-selector.tsx
app/page.tsx

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

app/page.tsx: Password verification for calendar access must use pendingAction state pattern: set pending action, show PasswordDialog, retry after password verification via /api/calendars/[id]/verify-password
Main page (app/page.tsx) uses URL sync via useRouter().replace() for selected calendar
Refresh data after mutations by incrementing statsRefreshTrigger counter

Files:

  • app/page.tsx
app/**/*.tsx

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

app/**/*.tsx: Use formatDateToLocal() helper for consistent YYYY-MM-DD date formatting in UI
Shift toggle logic must check if matching shift exists: delete if present, create if not
Use locale-specific date formatters in components: locale === "de" ? de : enUS
Frontend uses Next.js 16 App Router (app/ directory), React 19, and client-side state management via useState/useEffect

Files:

  • app/page.tsx
🧠 Learnings (12)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Shift toggle logic must check if matching shift exists: delete if present, create if not
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/page.tsx : Password verification for calendar access must use pendingAction state pattern: set pending action, show PasswordDialog, retry after password verification via /api/calendars/[id]/verify-password
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/page.tsx : Main page (app/page.tsx) uses URL sync via useRouter().replace() for selected calendar
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Frontend uses Next.js 16 App Router (app/ directory), React 19, and client-side state management via useState/useEffect
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/page.tsx : Password verification for calendar access must use pendingAction state pattern: set pending action, show PasswordDialog, retry after password verification via /api/calendars/[id]/verify-password

Applied to files:

  • app/api/notes/[id]/route.ts
  • app/api/calendars/[id]/route.ts
  • app/api/shifts/[id]/route.ts
  • app/api/presets/[id]/route.ts
  • components/app-header.tsx
  • app/page.tsx
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to components/**/calendar*.tsx : Calendar interaction: left-click toggles shift (requires preset selection), right-click opens note dialog with e.preventDefault()

Applied to files:

  • components/app-header.tsx
  • hooks/useSSEConnection.ts
  • app/page.tsx
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/page.tsx : Main page (app/page.tsx) uses URL sync via useRouter().replace() for selected calendar

Applied to files:

  • components/app-header.tsx
  • app/page.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Frontend uses Next.js 16 App Router (app/ directory), React 19, and client-side state management via useState/useEffect

Applied to files:

  • components/app-header.tsx
  • app/page.tsx
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Use formatDateToLocal() helper for consistent YYYY-MM-DD date formatting in UI

Applied to files:

  • app/page.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Shift toggle logic must check if matching shift exists: delete if present, create if not

Applied to files:

  • app/page.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Use locale-specific date formatters in components: locale === "de" ? de : enUS

Applied to files:

  • app/page.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to components/**/*-dialog.tsx : Form submission in dialogs: prevent default, validate, call parent callback, close dialog

Applied to files:

  • app/page.tsx
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to components/**/*-dialog.tsx : Dialog components must control open state via props, reset internal state when open changes to false, and call parent callback on form submission

Applied to files:

  • app/page.tsx
  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to components/**/*.tsx : Use PRESET_COLORS constant array for all color picker options

Applied to files:

  • components/preset-selector.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.{ts,tsx} : Use useTranslations() hook for all user-facing text and t("key") to access translation keys from messages/{de,en}.json

Applied to files:

  • components/preset-selector.tsx
🧬 Code graph analysis (1)
components/preset-selector.tsx (5)
lib/db/schema.ts (1)
  • ShiftPreset (92-92)
hooks/usePasswordProtection.ts (1)
  • usePasswordProtection (8-64)
components/preset-edit-dialog.tsx (2)
  • PresetFormData (252-252)
  • PresetEditDialog (47-250)
components/preset-list.tsx (1)
  • PresetList (15-126)
components/preset-manage-dialog.tsx (1)
  • PresetManageDialog (22-102)
⏰ 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-and-push
🔇 Additional comments (25)
app/api/presets/[id]/route.ts (1)

161-173: LGTM!

The cascading delete logic correctly removes related shifts before deleting the preset, and the SSE event emission ensures real-time UI updates. Password verification flow is properly implemented.

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

191-199: LGTM!

The delete operation and SSE event emission are correctly implemented. The password verification flow properly protects the operation when the calendar is protected.

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

51-85: LGTM! Password protection properly added to PUT handler.

The implementation correctly:

  1. Fetches the existing note to get the calendar ID
  2. Verifies the calendar exists
  3. Validates password if the calendar is protected

This aligns with the broader PR's password-protection flow.

hooks/useSSEConnection.ts (3)

31-54: LGTM! Ref pattern properly addresses stale closure concerns.

The implementation correctly uses refs to hold the latest callback references and updates them in a separate effect. This ensures SSE handlers always use current callbacks without causing unnecessary reconnections when callbacks change.


170-209: SSE error handling correctly relies on auto-reconnect.

The onerror handler properly avoids calling eventSource.close(), allowing the browser's built-in EventSource auto-reconnect mechanism to work. The resync timeout provides additional data refresh while waiting for reconnection. This addresses the past review concern about the stream being closed and never re-established.


228-231: LGTM! Clean hook return interface.

Exposing lastSyncTimeRef and disconnectTimeRef allows consumers to inspect sync state when needed. The overall hook implementation is well-structured with proper cleanup.

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

145-147: No action needed. The database schema has ON DELETE CASCADE properly configured for all foreign key relationships to the calendars table:

  • shifts.calendarIdcalendars.id with onDelete: "cascade"
  • shiftPresets.calendarIdcalendars.id with onDelete: "cascade"
  • calendarNotes.calendarIdcalendars.id with onDelete: "cascade"

The simple deletion approach in the API route correctly relies on database-level cascade deletion to clean up related entities automatically, preventing orphaned records.

Likely an incorrect or invalid review comment.

components/preset-selector.tsx (5)

1-23: LGTM! Clean imports and well-defined props interface.

The component structure follows good separation of concerns by delegating UI to child components (PresetList, PresetEditDialog, PresetManageDialog) while handling business logic (API calls, password protection) here. The useTranslations() hook usage aligns with coding guidelines.


45-59: LGTM! Password-protected create/edit flows.

The withPasswordCheck wrapper correctly gates actions behind password verification before showing the edit dialog. The state setup differentiates between create (isCreatingNew=true) and edit modes properly.


61-117: LGTM! Comprehensive save logic with proper error handling.

The function correctly handles both create (POST) and update (PATCH) paths with appropriate error logging and user feedback via toasts. The conditional onShiftsChange() call on update (but not create) is logical since creating a preset doesn't affect existing shifts.


119-156: LGTM! Delete with proper confirmation and cleanup.

The deletion flow correctly sequences password verification before confirmation dialog, handles errors consistently, and properly deselects the preset if it was the active selection. The onShiftsChange() call ensures any cascading effects are reflected.


158-191: LGTM! Well-orchestrated dialog composition.

The render correctly composes the three UI components with proper callback wiring. The pattern of closing PresetManageDialog before opening PresetEditDialog (lines 172-179) prevents dialog stacking and provides smooth UX.

components/app-header.tsx (4)

17-34: LGTM! Well-structured props interface.

The AppHeaderProps interface clearly defines all required props with appropriate types. The separation between callbacks (on* prefixed) and state props is clean and follows React conventions.


137-145: Verified: Mobile connection indicator fix applied.

The mobile connection status indicator now correctly uses isConnected to toggle between green and red, matching the desktop behavior. This addresses the previous review comment.


203-216: Verified: PresetSelector conditional rendering fix applied.

PresetSelector is now only rendered when selectedCalendar is truthy, and calendarId receives the actual value without an empty string fallback. This prevents invalid API calls and ensures proper password protection flow.


221-258: LGTM! Mobile dialog with proper state management.

The mobile calendar dialog correctly closes before triggering parent actions (lines 243-253), preventing UI state conflicts. The CalendarSelector integration maintains consistent behavior between desktop and mobile views.

app/page.tsx (9)

53-83: LGTM! Clean hook-based data management setup.

The custom hooks are properly initialized with appropriate dependencies. The aliasing pattern (e.g., createCalendar: createCalendarHook) prevents naming conflicts with local handlers and maintains clarity.


116-125: LGTM! SSE integration for real-time updates.

The useSSEConnection hook correctly wires refetch callbacks to handle real-time updates for shifts, presets, and notes. The statsRefreshTrigger increment ensures stats recalculate on relevant changes.


142-147: LGTM! URL sync follows coding guidelines.

The URL synchronization correctly uses router.replace() with scroll: false to update the URL query parameter when the selected calendar changes, as per the coding guidelines.


104-110: LGTM! Extended pendingAction pattern for notes.

The pendingAction state correctly includes noteAction to support password-protected note operations, following the established pattern for shifts and presets per coding guidelines.


383-472: LGTM! Shift toggle logic follows guidelines.

The function correctly implements the shift toggle pattern: checks for an existing shift by matching date, title, and times, deletes if present, creates if not. The optimistic update pattern (lines 430-431) with rollback on failure (lines 443-445, 451-453) provides good UX. Uses formatDateToLocal() per coding guidelines.


559-576: LGTM! AppHeader integration is complete.

All required props are correctly wired to local state, handlers, and hook functions. The callback mappings properly connect UI actions to data management functions.


614-625: LGTM! CalendarGrid properly configured.

The component receives all necessary data and handlers for the calendar interaction model: left-click for shift toggle (onDayClick), right-click/long-press for notes (onDayRightClick, onLongPress), per coding guidelines.


665-669: Verify: Is shift editing intentionally removed from ShiftsList?

ShiftsList receives onDeleteShift but not onUpdateShift. The handleUpdateShift function exists but is unused. If editing functionality is expected, ensure it's wired; if intentionally removed, delete the unused handler.


692-750: LGTM! Dialog configurations properly updated.

All dialogs are correctly configured with hook-based callbacks (createCalendarHook, refetchPresets, refetchCalendars) replacing the previous inline implementations. Conditional rendering for password-related dialogs prevents issues when no calendar is selected.

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

🧹 Nitpick comments (2)
components/calendar-grid.tsx (2)

28-28: Unused prop isTogglingShift.

The isTogglingShift prop is destructured but never used in the component. Either remove it from the interface and component signature, or implement the intended behavior (e.g., disabling interaction during shift toggle operations).

 interface CalendarGridProps {
   calendarDays: Date[];
   currentDate: Date;
   shifts: ShiftWithCalendar[];
   notes: CalendarNote[];
   selectedPresetId: string | undefined;
-  isTogglingShift: boolean;
   onDayClick: (date: Date) => void;
   onDayRightClick: (e: React.MouseEvent, date: Date) => void;
   onNoteIconClick: (e: React.MouseEvent, date: Date) => void;
   onLongPress: (date: Date) => void;
 }

 export function CalendarGrid({
   calendarDays,
   currentDate,
   shifts,
   notes,
   selectedPresetId,
-  isTogglingShift,
   onDayClick,
   onDayRightClick,
   onNoteIconClick,
   onLongPress,
 }: CalendarGridProps) {

110-110: Remove redundant disabled={false}.

The disabled attribute defaults to false, so this explicit assignment is unnecessary.

             onTouchMove={handleTouchEnd}
-            disabled={false}
             whileTap={{ scale: 0.95 }}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a7d7c66 and 492f3a1.

📒 Files selected for processing (1)
  • components/calendar-grid.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
components/**/*.tsx

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

Use PRESET_COLORS constant array for all color picker options

Files:

  • components/calendar-grid.tsx
**/*.{ts,tsx}

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

Store colors as hex values (e.g., #3b82f6) and apply 20% opacity for backgrounds using format ${color}20

Files:

  • components/calendar-grid.tsx
components/**/calendar*.tsx

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

Calendar interaction: left-click toggles shift (requires preset selection), right-click opens note dialog with e.preventDefault()

Files:

  • components/calendar-grid.tsx
🧠 Learnings (4)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Shift toggle logic must check if matching shift exists: delete if present, create if not
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/page.tsx : Password verification for calendar access must use pendingAction state pattern: set pending action, show PasswordDialog, retry after password verification via /api/calendars/[id]/verify-password
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to components/**/calendar*.tsx : Calendar interaction: left-click toggles shift (requires preset selection), right-click opens note dialog with e.preventDefault()

Applied to files:

  • components/calendar-grid.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Use formatDateToLocal() helper for consistent YYYY-MM-DD date formatting in UI

Applied to files:

  • components/calendar-grid.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Use locale-specific date formatters in components: locale === "de" ? de : enUS

Applied to files:

  • components/calendar-grid.tsx
🧬 Code graph analysis (1)
components/calendar-grid.tsx (2)
lib/types.ts (1)
  • ShiftWithCalendar (14-31)
lib/db/schema.ts (2)
  • CalendarNote (94-94)
  • shifts (19-45)
🔇 Additional comments (2)
components/calendar-grid.tsx (2)

37-45: Timer cleanup implementation looks good.

The use of useRef with a Record<string, NodeJS.Timeout> pattern and proper cleanup in useEffect correctly addresses the memory leak concern from the previous review. Each day cell has its own timer keyed by dayKey, and all timers are cleared on unmount.

Also applies to: 89-100


159-200: Shift rendering follows color conventions.

The background color implementation using ${shift.color}20 correctly applies 20% opacity as per coding guidelines, with appropriate fallback to the default color #3b82f620.

@panteLx panteLx merged commit 1feae50 into main Dec 1, 2025
1 of 2 checks passed
@panteLx panteLx deleted the chore/refactor-page-tsx branch December 1, 2025 19:59
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: 2

🧹 Nitpick comments (1)
components/calendar-grid.tsx (1)

35-45: Calendar interactions, long‑press, and color handling look correct

  • Long‑press timers are now scoped per day via pressTimerRef and fully cleaned up in useEffect, addressing prior leak/scoping concerns.
  • Right‑click calls e.preventDefault() before delegating to onDayRightClick, matching the calendar interaction guideline.
  • Shift colors use hex + ${color}20 opacity and sensible fallbacks ("#3b82f620" / "#3b82f6"), aligning with the color storage/usage rules.

Optionally, you could also wire onTouchCancel={handleTouchEnd} on the button to cancel timers on interrupted touches, but the current implementation is already sound. As per coding guidelines, the interaction and color behavior here looks good.

Also applies to: 87-113, 163-172

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 492f3a1 and 54986de.

📒 Files selected for processing (1)
  • components/calendar-grid.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
components/**/*.tsx

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

Use PRESET_COLORS constant array for all color picker options

Files:

  • components/calendar-grid.tsx
**/*.{ts,tsx}

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

Store colors as hex values (e.g., #3b82f6) and apply 20% opacity for backgrounds using format ${color}20

Files:

  • components/calendar-grid.tsx
components/**/calendar*.tsx

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

Calendar interaction: left-click toggles shift (requires preset selection), right-click opens note dialog with e.preventDefault()

Files:

  • components/calendar-grid.tsx
🧠 Learnings (3)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Shift toggle logic must check if matching shift exists: delete if present, create if not
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/page.tsx : Password verification for calendar access must use pendingAction state pattern: set pending action, show PasswordDialog, retry after password verification via /api/calendars/[id]/verify-password
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/page.tsx : Main page (app/page.tsx) uses URL sync via useRouter().replace() for selected calendar
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to components/**/calendar*.tsx : Calendar interaction: left-click toggles shift (requires preset selection), right-click opens note dialog with e.preventDefault()

Applied to files:

  • components/calendar-grid.tsx
📚 Learning: 2025-11-29T20:11:49.992Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-29T20:11:49.992Z
Learning: Applies to app/**/*.tsx : Use formatDateToLocal() helper for consistent YYYY-MM-DD date formatting in UI

Applied to files:

  • components/calendar-grid.tsx
🧬 Code graph analysis (1)
components/calendar-grid.tsx (2)
lib/types.ts (1)
  • ShiftWithCalendar (14-31)
lib/db/schema.ts (2)
  • CalendarNote (94-94)
  • shifts (19-45)
⏰ 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-and-push

Comment on lines +9 to +20
interface CalendarGridProps {
calendarDays: Date[];
currentDate: Date;
shifts: ShiftWithCalendar[];
notes: CalendarNote[];
selectedPresetId: string | undefined;
isTogglingShift: boolean;
onDayClick: (date: Date) => void;
onDayRightClick: (e: React.MouseEvent, date: Date) => void;
onNoteIconClick: (e: React.MouseEvent, date: Date) => void;
onLongPress: (date: Date) => void;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the components/calendar-grid.tsx file
fd "calendar-grid.tsx" --type f

Repository: panteLx/BetterShift

Length of output: 91


🏁 Script executed:

# Get the full contents of the file to check imports and type usage
cat -n components/calendar-grid.tsx | head -150

Repository: panteLx/BetterShift

Length of output: 6035


Add React type import for event type annotations

The file uses React.MouseEvent (lines 17–18) and React.TouchEvent (line 89) in the CalendarGridProps interface and event handlers, but React isn't imported as a type, causing TypeScript compilation errors. Add a type-only import:

+import type React from "react";
import { useRef, useEffect } from "react";

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In components/calendar-grid.tsx around lines 9 to 20, the props use
React.MouseEvent and React.TouchEvent but React isn't imported as a type; add a
type-only import at the top of the file (for example import type React from
'react' or import type { MouseEvent, TouchEvent } from 'react') and update the
props to use those imported type names if you choose specific imports, ensuring
TypeScript recognizes the event types without bundling React at runtime.

Comment on lines +103 to +151
<motion.button
key={idx}
onClick={() => onDayClick(day)}
onContextMenu={(e) => {
e.preventDefault();
onDayRightClick(e, day);
}}
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
onTouchMove={handleTouchEnd}
disabled={false}
whileTap={{ scale: 0.95 }}
style={{
WebkitUserSelect: "none",
userSelect: "none",
WebkitTouchCallout: "none",
}}
className={`
min-h-25 sm:min-h-28 px-1 py-1.5 sm:p-2.5 rounded-md sm:rounded-lg text-sm transition-all relative flex flex-col border sm:border-2
${isCurrentMonth ? "text-foreground" : "text-muted-foreground/50"}
${
isTodayDate
? "border-primary shadow-lg shadow-primary/20 bg-primary/5 ring-2 ring-primary/20"
: "border-border/30 sm:border-border/50"
}
${
isCurrentMonth
? "hover:bg-accent cursor-pointer active:bg-accent/80 hover:border-border"
: selectedPresetId
? "cursor-not-allowed"
: "cursor-pointer"
}
${!isCurrentMonth ? "opacity-40" : ""}
`}
>
<div
className={`text-sm sm:text-sm font-semibold mb-1 flex items-center justify-between ${
isTodayDate ? "text-primary" : ""
}`}
>
<span>{day.getDate()}</span>
{dayNote && (
<motion.div
className="group/note relative"
onClick={(e) => onNoteIconClick(e, day)}
title={dayNote.note}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Prevent note icon clicks from also toggling shifts (stop click bubbling)

Because the note icon is inside the motion.button day cell, clicking it will fire both onNoteIconClick and the parent onDayClick, unintentionally toggling shifts when the user just wants to open/edit a note.

Stop propagation in the note icon handler:

-                <motion.div
-                  className="group/note relative"
-                  onClick={(e) => onNoteIconClick(e, day)}
+                <motion.div
+                  className="group/note relative"
+                  onClick={(e) => {
+                    e.stopPropagation();
+                    onNoteIconClick(e, day);
+                  }}

This keeps left‑click toggling behavior on the day cell while making the note icon strictly note-focused. Based on learnings, this better matches the intended calendar interactions.

🤖 Prompt for AI Agents
In components/calendar-grid.tsx around lines 103 to 151, the note icon's onClick
resides inside the parent motion.button so clicks bubble and trigger the parent
onDayClick; update the note icon handler to stop event propagation before
delegating to onNoteIconClick (e.g., call e.stopPropagation() at start of the
click handler) so clicking the note icon only opens/edits notes and does not
toggle shifts on the parent cell. Ensure the handler accepts the event parameter
and invokes stopPropagation for mouse/touch events as appropriate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant