Skip to content

feat: Adds calendar comparison mode and related UI/logic#85

Merged
panteLx merged 4 commits intomainfrom
feat/calendar-comparison
Dec 20, 2025
Merged

feat: Adds calendar comparison mode and related UI/logic#85
panteLx merged 4 commits intomainfrom
feat/calendar-comparison

Conversation

@panteLx
Copy link
Owner

@panteLx panteLx commented Dec 20, 2025

Adds a comparison mode to view and interact with multiple calendars side-by-side to simplify cross-calendar review and editing.

Implements a compare selector and comparison view that load shifts, notes, presets and external syncs for selected calendars; enables creating/deleting shifts via presets with password verification and reloads compare-specific data after edits. Integrates compare controls into header and selector, adds localized strings, and adjusts several UI helpers (manage button hiding, sheet save label). Also includes small backend/UI fixes to ensure counts and pending actions include calendar context.

Summary by CodeRabbit

  • New Features

    • Calendar comparison mode: compare 2–3 calendars side-by-side with per-calendar cards, presets, unlock flows and shareable compare links
    • In-app selector sheet to pick calendars to compare
  • UI

    • Desktop & mobile Compare buttons, compare skeletons, mobile warnings, and responsive compare layout
    • Preset list can optionally hide the Manage button
    • Customizable sheet save label
  • Localization

    • Added translations for compare-related UI strings
  • Bug Fixes

    • Fixed calendar API count field in GET responses
  • Documentation

    • README updated to mention calendar comparison mode

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

Adds a comparison mode to view and interact with multiple calendars side-by-side to simplify cross-calendar review and editing.

Implements a compare selector and comparison view that load shifts, notes, presets and external syncs for selected calendars; enables creating/deleting shifts via presets with password verification and reloads compare-specific data after edits. Integrates compare controls into header and selector, adds localized strings, and adjusts several UI helpers (manage button hiding, sheet save label). Also includes small backend/UI fixes to ensure counts and pending actions include calendar context.
Copilot AI review requested due to automatic review settings December 20, 2025 16:13
@coderabbitai
Copy link

coderabbitai bot commented Dec 20, 2025

Caution

Review failed

The pull request is closed.

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

Adds a calendar compare mode (select 2–3 calendars to view side‑by‑side) with new compare UI components, per‑calendar parallel data loading and password‑aware actions, a small API response alias change (_count), new props/extensions, and added translations.

Changes

Cohort / File(s) Summary
API
app/api/calendars/route.ts
Renamed the SQL alias for the counts subquery from shift_count to _count in the GET response object (property _count).
Page / Compare Mode
app/page.tsx
Added compare-mode state (isCompareMode, showCompareSelector, selectedCompareIds, compareCalendarData), URL compare query handling, parallel per-calendar data loading (shifts, notes, externalSyncs, presets), compare-specific handlers (day interactions, notes, shift create/delete with password flow), and render branches for CalendarCompareSheet and CalendarCompareView.
New Compare UI Components
components/calendar-compare-sheet.tsx, components/calendar-compare-view.tsx, components/calendar-compare-skeleton.tsx
Added CalendarCompareSheet (select 2–3 calendars), CalendarCompareView (responsive per‑calendar cards, share link, lock handling, many per‑calendar callbacks/props), and CalendarCompareSkeleton (loading skeleton for 1–3 columns).
Header / Selector Prop Wiring
components/app-header.tsx, components/calendar-selector.tsx
Added onCompare?: () => void to AppHeaderProps and CalendarSelectorProps; CalendarSelector shows a Compare button when 2+ calendars; mobile selector closes dialog before invoking onCompare.
Preset UI Props
components/preset-list.tsx, components/preset-selector.tsx
Added hideManageButton?: boolean to PresetListProps and PresetSelectorProps; PresetSelector forwards hideManageButton to PresetList.
Base Sheet
components/ui/base-sheet.tsx
Added optional saveLabel?: string prop to BaseSheetProps to override the Save button label when not saving.
Password Hook
hooks/usePasswordManagement.ts
Added optional calendarId?: string to PendingAction interface.
Translations
messages/en.json, messages/de.json, messages/it.json
Added multiple calendar.* keys (compare, compareMode, exitCompare, selectToCompare, selectToCompareDescription, compareSelected, startComparing, mobileNotOptimized, shareLink, shareLinkDescription, linkCopied) and preset.cannotAddPresetHint across locales.
Docs
README.md
Mentioned Calendar Comparison feature in Key Features and overview.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant AppHeader
    participant CalendarSelector
    participant CalendarCompareSheet
    participant AppPage
    participant API
    participant CalendarCompareView

    User->>AppHeader: open header
    AppHeader->>CalendarSelector: render (onCompare wired)
    User->>CalendarSelector: click Compare
    CalendarSelector->>CalendarCompareSheet: open modal
    User->>CalendarCompareSheet: select 2–3 calendars + Start
    CalendarCompareSheet->>AppPage: onStartCompare(selectedIds)
    AppPage->>AppPage: set isCompareMode, selectedCompareIds
    AppPage->>API: fetch per-calendar data (shifts, notes, externalSyncs, presets) [parallel]
    API-->>AppPage: return per-calendar payloads (includes `_count`)
    AppPage->>CalendarCompareView: render compare UI with data & handlers
    User->>CalendarCompareView: interact (day click / note / shift)
    CalendarCompareView->>AppPage: invoke handler (may require password)
    AppPage->>API: create/update/delete shift or note
    API-->>AppPage: confirm
    AppPage->>AppPage: refresh affected calendar data
    User->>CalendarCompareView: click Exit Compare
    CalendarCompareView->>AppPage: onExitCompare
    AppPage->>AppPage: unset isCompareMode
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Focus areas:
    • app/page.tsx (many new hooks, parallel fetch logic, password flows, URL state).
    • components/calendar-compare-view.tsx (dense props, lock/unlock handling, per-calendar disabling and handler wiring).
    • app/api/calendars/route.ts (alias _count change — verify consumers and typings).
    • Prop threading: AppHeader → CalendarSelector onCompare; PresetSelector → PresetList hideManageButton.

Possibly related PRs

Suggested labels

enhancement

"🐰
I hopped in with a tiny cheer,
Three calendars now side-by-side appear,
Shifts and presets tucked in neat,
Languages joined — what a treat! 🥕"

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Adds calendar comparison mode and related UI/logic' directly and comprehensively summarizes the main change: introducing a calendar comparison feature with supporting UI components and logic.

📜 Recent review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3fe131e and e53affe.

📒 Files selected for processing (1)
  • app/page.tsx (10 hunks)

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 pull request introduces a calendar comparison mode that allows users to view and interact with 2-3 calendars side-by-side. The implementation includes a comparison selector sheet, a dedicated comparison view, localization strings for German/English/Italian, and infrastructure to manage data loading and interactions across multiple calendars simultaneously.

Key changes:

  • New comparison UI components (CalendarCompareView, CalendarCompareSheet) with responsive grid layouts
  • Calendar-specific data management using Map structures for shifts, notes, presets, and external syncs
  • Password verification support for multi-calendar operations in compare mode
  • Integration of compare mode toggle into calendar selector (desktop and mobile)

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
messages/de.json Adds German translations for comparison mode UI strings
messages/en.json Adds English translations for comparison mode UI strings
messages/it.json Adds Italian translations for comparison mode UI strings
hooks/usePasswordManagement.ts Extends PendingAction interface with calendarId field for multi-calendar support
components/ui/base-sheet.tsx Adds saveLabel prop to customize save button text
components/preset-selector.tsx Adds hideManageButton prop to hide preset management in compare mode
components/preset-list.tsx Implements conditional rendering of manage button based on hideManageButton prop
components/calendar-selector.tsx Adds compare button with "Copy" icon to both desktop and mobile layouts
components/calendar-compare-view.tsx New component rendering side-by-side calendar views with shared preset selection
components/calendar-compare-sheet.tsx New component for selecting 2-3 calendars to compare with visual feedback
components/app-header.tsx Propagates onCompare handler to calendar selectors
app/page.tsx Implements compare mode state management, multi-calendar data loading, and interaction handlers
app/api/calendars/route.ts Renames SQL alias from "shift_count" to "_count" for consistency
Comments suppressed due to low confidence (1)

app/page.tsx:350

  • In compare mode, when a password is required for shift toggling, the handlePasswordSuccess function only reloads notes data, not shifts or presets. This means after entering a password and the pending action executes (creating/deleting a shift), the local state won't be updated because handlePasswordSuccess doesn't reload shifts for the specific calendar. The pendingAction should include the calendarId so that shifts can be reloaded for that calendar after password entry.
  const handlePasswordSuccess = async () => {
    if (isCompareMode && compareNoteCalendarId) {
      // In compare mode, reload notes for the specific calendar
      await refetchNotes();
      const password = getCachedPassword(compareNoteCalendarId);
      const passwordParam = password ? `&password=${password}` : "";
      const notesRes = await fetch(
        `/api/notes?calendarId=${compareNoteCalendarId}${passwordParam}`
      );
      const notesData = notesRes.ok ? await notesRes.json() : [];
      setCompareCalendarData((prev) => {
        const updated = new Map(prev);
        const data = updated.get(compareNoteCalendarId);
        if (data) {
          updated.set(compareNoteCalendarId, { ...data, notes: notesData });
        }
        return updated;
      });
    } else {
      await Promise.all([
        refetchShifts(),
        refetchPresets(),
        refetchNotes(),
        fetchExternalSyncs(),
        fetchSyncErrorStatus(),
      ]);
      setStatsRefreshTrigger((prev) => prev + 1);
    }
    baseHandlePasswordSuccess();

    // Execute pending action if exists
    if (pendingAction?.action) {
      await pendingAction.action();
      setPendingAction(null);
    }

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (2)
app/api/calendars/route.ts (2)

10-27: CRITICAL: Remove passwordHash from API response.

The GET endpoint exposes passwordHash for all calendars, creating a serious security vulnerability. Even though passwords are hashed, exposing hashes allows attackers to perform offline brute-force attacks or rainbow table lookups.

🔎 Recommended fix

Remove passwordHash from the select statement and only return metadata needed by the client:

    const allCalendars = await db
      .select({
        id: calendars.id,
        name: calendars.name,
        color: calendars.color,
-       passwordHash: calendars.passwordHash,
        isLocked: calendars.isLocked,
        createdAt: calendars.createdAt,
        updatedAt: calendars.updatedAt,
        _count:
          sql<number>`(SELECT COUNT(*) FROM ${shifts} WHERE ${shifts.calendarId} = ${calendars.id})`.as(
            "_count"
          ),
      })
      .from(calendars)
      .orderBy(calendars.createdAt);

The isLocked field is sufficient for the client to know whether a calendar requires password verification.


58-68: CRITICAL: Remove passwordHash from creation response.

The POST endpoint returns the created calendar including passwordHash, exposing the hash to the client. This creates the same security vulnerability as the GET endpoint.

🔎 Recommended fix

Explicitly select only the fields that should be returned to the client:

    const [calendar] = await db
      .insert(calendars)
      .values({
        name,
        color: color || "#3b82f6",
        passwordHash: password ? hashPassword(password) : null,
        isLocked: isLocked || false,
      })
-     .returning();
+     .returning({
+       id: calendars.id,
+       name: calendars.name,
+       color: calendars.color,
+       isLocked: calendars.isLocked,
+       createdAt: calendars.createdAt,
+       updatedAt: calendars.updatedAt,
+     });
🧹 Nitpick comments (6)
components/calendar-compare-sheet.tsx (1)

72-79: Consider removing redundant onCheckedChange handler.

The Checkbox has pointer-events-none (line 78), which prevents mouse events from reaching it. This means the onCheckedChange handler (lines 75-77) will never fire. All interaction is handled by the parent motion.div onClick (line 67).

While this doesn't cause bugs, the unused handler adds cognitive overhead.

🔎 Proposed refactor to remove unused handler
 <Checkbox
   checked={isSelected}
   disabled={isDisabled}
-  onCheckedChange={() =>
-    !isDisabled && onToggleCalendar(calendar.id)
-  }
   className="pointer-events-none"
 />
components/calendar-compare-view.tsx (1)

81-90: Consider simplifying the IIFE pattern for finding preset owner.

The IIFE is functional but could be more readable using Array.from().find().

🔎 Proposed refactor
-  const selectedPresetCalendarId = props.selectedPresetId
-    ? (() => {
-        for (const [calendarId, presets] of props.presetsMap.entries()) {
-          if (presets.some((p) => p.id === props.selectedPresetId)) {
-            return calendarId;
-          }
-        }
-        return null;
-      })()
-    : null;
+  const selectedPresetCalendarId = props.selectedPresetId
+    ? Array.from(props.presetsMap.entries()).find(([, presets]) =>
+        presets.some((p) => p.id === props.selectedPresetId)
+      )?.[0] ?? null
+    : null;
app/page.tsx (4)

141-166: Extract duplicate fetch-and-update logic for compare notes.

handleNoteSubmit and handleNoteDelete share nearly identical logic for reloading notes in compare mode. Consider extracting a helper function.

🔎 Proposed refactor
// Add helper function before the handlers
const reloadCompareNotes = async (calendarId: string) => {
  const password = getCachedPassword(calendarId);
  const passwordParam = password ? `&password=${password}` : "";
  try {
    const notesRes = await fetch(
      `/api/notes?calendarId=${calendarId}${passwordParam}`
    );
    const notesData = notesRes.ok ? await notesRes.json() : [];
    setCompareCalendarData((prev) => {
      const updated = new Map(prev);
      const data = updated.get(calendarId);
      if (data) {
        updated.set(calendarId, { ...data, notes: notesData });
      }
      return updated;
    });
  } catch (error) {
    console.error("Failed to reload notes:", error);
  }
};

// Then simplify handlers:
const handleNoteSubmit = async (noteText: string) => {
  await noteActions.handleNoteSubmit(noteText);
  if (isCompareMode && compareNoteCalendarId) {
    await reloadCompareNotes(compareNoteCalendarId);
  }
};

244-306: Consider parallelizing compare data fetches for better performance.

Currently, data for each calendar is fetched sequentially in a for...of loop. Using Promise.all would load all calendars in parallel.

🔎 Proposed refactor
 const loadCompareData = async () => {
-  const dataMap = new Map();
-
-  for (const calendarId of selectedCompareIds) {
-    try {
-      const password = getCachedPassword(calendarId);
-      const passwordParam = password ? `&password=${password}` : "";
-
-      // Fetch shifts
-      const shiftsRes = await fetch(
-        `/api/shifts?calendarId=${calendarId}${passwordParam}`
-      );
-      // ... more sequential fetches
-    } catch (error) {
-      // ...
-    }
-  }
-
-  setCompareCalendarData(dataMap);
+  const entries = await Promise.all(
+    selectedCompareIds.map(async (calendarId) => {
+      try {
+        const password = getCachedPassword(calendarId);
+        const passwordParam = password ? `&password=${password}` : "";
+
+        const [shiftsRes, notesRes, syncsRes, presetsRes] = await Promise.all([
+          fetch(`/api/shifts?calendarId=${calendarId}${passwordParam}`),
+          fetch(`/api/notes?calendarId=${calendarId}${passwordParam}`),
+          fetch(`/api/external-syncs?calendarId=${calendarId}${passwordParam}`),
+          fetch(`/api/presets?calendarId=${calendarId}${passwordParam}`),
+        ]);
+
+        const [shiftsData, notesData, syncsData, presetsData] = await Promise.all([
+          shiftsRes.ok ? shiftsRes.json() : [],
+          notesRes.ok ? notesRes.json() : [],
+          syncsRes.ok ? syncsRes.json() : [],
+          presetsRes.ok ? presetsRes.json() : [],
+        ]);
+
+        return [calendarId, {
+          shifts: shiftsData,
+          notes: notesData,
+          externalSyncs: syncsData,
+          presets: presetsData,
+          togglingDates: new Set<string>(),
+        }] as const;
+      } catch (error) {
+        console.error(`Error loading data for calendar ${calendarId}:`, error);
+        return [calendarId, {
+          shifts: [],
+          notes: [],
+          externalSyncs: [],
+          presets: [],
+          togglingDates: new Set<string>(),
+        }] as const;
+      }
+    })
+  );
+
+  setCompareCalendarData(new Map(entries));
 };

474-632: Large handler function - consider extracting shift toggle logic.

handleCompareDayClick at ~158 lines handles multiple responsibilities: preset validation, toggling state, password verification, shift existence check, delete/create operations, and state updates. Consider extracting the core shift toggle logic into a separate function or custom hook for better maintainability.


710-750: Maps created inline in JSX props cause unnecessary re-renders.

Creating new Map objects inside the JSX props (e.g., shiftsMap, notesMap, etc.) will create new object references on every render, potentially causing unnecessary re-renders of CalendarCompareView.

🔎 Proposed refactor using useMemo
// Add these before the return statement in the compare mode branch
const shiftsMap = useMemo(
  () => new Map(
    Array.from(compareCalendarData.entries()).map(([id, data]) => [id, data.shifts])
  ),
  [compareCalendarData]
);

const notesMap = useMemo(
  () => new Map(
    Array.from(compareCalendarData.entries()).map(([id, data]) => [id, data.notes])
  ),
  [compareCalendarData]
);

// ... similar for externalSyncsMap, presetsMap, togglingDatesMap

// Then in JSX:
<CalendarCompareView
  shiftsMap={shiftsMap}
  notesMap={notesMap}
  // ...
/>
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 83efd21 and 93e150c.

📒 Files selected for processing (13)
  • app/api/calendars/route.ts (1 hunks)
  • app/page.tsx (11 hunks)
  • components/app-header.tsx (4 hunks)
  • components/calendar-compare-sheet.tsx (1 hunks)
  • components/calendar-compare-view.tsx (1 hunks)
  • components/calendar-selector.tsx (6 hunks)
  • components/preset-list.tsx (4 hunks)
  • components/preset-selector.tsx (3 hunks)
  • components/ui/base-sheet.tsx (3 hunks)
  • hooks/usePasswordManagement.ts (1 hunks)
  • messages/de.json (2 hunks)
  • messages/en.json (2 hunks)
  • messages/it.json (2 hunks)
🧰 Additional context used
📓 Path-based instructions (7)
components/**/*.tsx

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

components/**/*.tsx: Use BaseSheet component from components/ui/base-sheet.tsx for simple sheets (create, edit, settings) with props: open, onOpenChange, title, description, showSaveButton, onSave, isSaving, saveDisabled, hasUnsavedChanges, maxWidth
Dirty state tracking in custom sheets must use useDirtyState hook with isDirty, handleClose, showConfirmDialog, setShowConfirmDialog, handleConfirmClose for unsaved changes confirmation
Custom sheet components must follow the structure: SheetContent with flex layout, SheetHeader with gradient background and border, scrollable content area with flex-1, and SheetFooter with border and action buttons
Calendar left-click toggles shift with selected preset (delete if exists, create if not), right-click opens note dialog preventing default context menu, and day indicators show <StickyNote> icon for days with notes
All components must support real-time updates via Server-Sent Events (SSE) by listening to relevant events and using silent refresh patterns with counter-based refresh triggers
New components with sheets must use BaseSheet for simple forms or follow the custom sheet pattern for complex layouts, with props: open, onOpenChange, onSubmit, optional onDelete, reset state on open change

Files:

  • components/ui/base-sheet.tsx
  • components/calendar-compare-sheet.tsx
  • components/preset-selector.tsx
  • components/preset-list.tsx
  • components/calendar-compare-view.tsx
  • components/calendar-selector.tsx
  • components/app-header.tsx
**/*.{ts,tsx}

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

**/*.{ts,tsx}: Never use native confirm() - always use ConfirmationDialog component with state management for confirmation dialogs
Use PRESET_COLORS array with hex format (#3b82f6) for colors, and use 20% opacity for backgrounds (e.g., ${color}20)
Use formatDateToLocal() for date formatting to YYYY-MM-DD format in all date display components
Use useTranslations() for all user-facing text and ensure all new translation keys are added to messages/de.json, messages/en.json, and messages/it.json
Use centralized translation keys for common messages: common.created, common.updated, common.deleted, common.createError, common.updateError, common.deleteError, common.deleteConfirm, common.deleteConfirmWithWarning with {item} parameter instead of duplicating messages
Use centralized validation translation keys: validation.passwordMatch, validation.passwordIncorrect, validation.passwordRequired, validation.fileRequired, validation.fileTooLarge, validation.urlRequired, validation.urlInvalid, validation.urlAlreadyExists
Use centralized form field translation keys: form.nameLabel, form.namePlaceholder, form.colorLabel, form.passwordLabel, form.passwordPlaceholder, form.notesLabel, form.notesPlaceholder, form.urlLabel, form.urlPlaceholder
Date locale formatting must use: locale === "de" ? de : (locale === "it" ? it : enUS) based on the next-intl locale setting

Files:

  • components/ui/base-sheet.tsx
  • components/calendar-compare-sheet.tsx
  • components/preset-selector.tsx
  • components/preset-list.tsx
  • hooks/usePasswordManagement.ts
  • app/api/calendars/route.ts
  • components/calendar-compare-view.tsx
  • components/calendar-selector.tsx
  • app/page.tsx
  • components/app-header.tsx
**/*.tsx

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

**/*.tsx: React components must use React 19 hooks for state management with useState for shifts/presets/notes/calendars and useEffect for data fetching on calendar/date changes, and useRouter().replace() for URL state sync
Use statsRefreshTrigger counter pattern to track mutations and trigger data refreshes in components without page reloads
Mobile UI must separate calendar selector with showMobileCalendarDialog to prevent cramped interfaces on small screens

Files:

  • components/ui/base-sheet.tsx
  • components/calendar-compare-sheet.tsx
  • components/preset-selector.tsx
  • components/preset-list.tsx
  • components/calendar-compare-view.tsx
  • components/calendar-selector.tsx
  • app/page.tsx
  • components/app-header.tsx
hooks/**.ts

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

All data-fetching hooks (useShifts, usePresets, useNotes) must automatically call getCachedPassword() before each fetch and append password as query parameter if present, returning empty arrays on 401 responses

Files:

  • hooks/usePasswordManagement.ts
app/api/**/route.ts

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

app/api/**/route.ts: Passwords must be SHA-256 hashed using utilities from lib/password-utils.ts
API route GET endpoints must check both passwordHash AND isLocked fields for full protection, while POST/PUT/PATCH/DELETE endpoints must check only passwordHash for write-only protection

Files:

  • app/api/calendars/route.ts
messages/de.json

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

German translations must always use informal "du" form (never formal "Sie" form) in all user-facing messages, descriptions, hints, and instructions

Files:

  • messages/de.json
app/**/*.tsx

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

Client-side password caching must use utilities from lib/password-cache.ts: getCachedPassword(), verifyAndCachePassword(), setCachedPassword(), removeCachedPassword()

Files:

  • app/page.tsx
🧠 Learnings (14)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to components/**/*.tsx : Calendar left-click toggles shift with selected preset (delete if exists, create if not), right-click opens note dialog preventing default context menu, and day indicators show `<StickyNote>` icon for days with notes
📚 Learning: 2025-12-20T13:30:34.343Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.343Z
Learning: Applies to components/**/*.tsx : Use `BaseSheet` component from `components/ui/base-sheet.tsx` for simple sheets (create, edit, settings) with props: `open`, `onOpenChange`, `title`, `description`, `showSaveButton`, `onSave`, `isSaving`, `saveDisabled`, `hasUnsavedChanges`, `maxWidth`

Applied to files:

  • components/ui/base-sheet.tsx
  • components/calendar-compare-sheet.tsx
  • app/page.tsx
📚 Learning: 2025-12-20T13:30:34.344Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to components/**/*.tsx : New components with sheets must use `BaseSheet` for simple forms or follow the custom sheet pattern for complex layouts, with props: `open`, `onOpenChange`, `onSubmit`, optional `onDelete`, reset state on open change

Applied to files:

  • components/ui/base-sheet.tsx
  • components/calendar-compare-sheet.tsx
📚 Learning: 2025-12-20T13:30:34.343Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.343Z
Learning: Applies to components/**/*.tsx : Dirty state tracking in custom sheets must use `useDirtyState` hook with `isDirty`, `handleClose`, `showConfirmDialog`, `setShowConfirmDialog`, `handleConfirmClose` for unsaved changes confirmation

Applied to files:

  • components/ui/base-sheet.tsx
  • components/calendar-compare-sheet.tsx
📚 Learning: 2025-12-20T13:30:34.344Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to **/*.{ts,tsx} : Use centralized form field translation keys: `form.nameLabel`, `form.namePlaceholder`, `form.colorLabel`, `form.passwordLabel`, `form.passwordPlaceholder`, `form.notesLabel`, `form.notesPlaceholder`, `form.urlLabel`, `form.urlPlaceholder`

Applied to files:

  • components/ui/base-sheet.tsx
📚 Learning: 2025-12-20T13:30:34.344Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to components/**/*.tsx : Calendar left-click toggles shift with selected preset (delete if exists, create if not), right-click opens note dialog preventing default context menu, and day indicators show `<StickyNote>` icon for days with notes

Applied to files:

  • components/calendar-compare-sheet.tsx
  • components/preset-selector.tsx
  • components/preset-list.tsx
  • components/calendar-compare-view.tsx
  • components/calendar-selector.tsx
  • app/page.tsx
  • components/app-header.tsx
📚 Learning: 2025-12-20T13:30:34.344Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to **/*.tsx : Mobile UI must separate calendar selector with `showMobileCalendarDialog` to prevent cramped interfaces on small screens

Applied to files:

  • components/calendar-compare-sheet.tsx
  • components/calendar-compare-view.tsx
  • components/calendar-selector.tsx
  • app/page.tsx
  • components/app-header.tsx
📚 Learning: 2025-12-20T13:30:34.344Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to **/*.tsx : React components must use React 19 hooks for state management with `useState` for shifts/presets/notes/calendars and `useEffect` for data fetching on calendar/date changes, and `useRouter().replace()` for URL state sync

Applied to files:

  • components/calendar-compare-sheet.tsx
  • components/calendar-compare-view.tsx
  • components/calendar-selector.tsx
  • app/page.tsx
📚 Learning: 2025-12-20T13:30:34.343Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.343Z
Learning: Applies to components/**/*.tsx : Custom sheet components must follow the structure: SheetContent with flex layout, SheetHeader with gradient background and border, scrollable content area with flex-1, and SheetFooter with border and action buttons

Applied to files:

  • components/calendar-compare-sheet.tsx
  • app/page.tsx
📚 Learning: 2025-12-20T13:30:34.343Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.343Z
Learning: Applies to hooks/**.ts : All data-fetching hooks (`useShifts`, `usePresets`, `useNotes`) must automatically call `getCachedPassword()` before each fetch and append password as query parameter if present, returning empty arrays on 401 responses

Applied to files:

  • hooks/usePasswordManagement.ts
  • app/page.tsx
📚 Learning: 2025-12-20T13:30:34.344Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to lib/db/schema.ts : Database cascading relationships: `calendars` cascade delete to `shifts`, `shiftPresets`, `calendarNotes`; `shiftPresets` set null on delete to `shifts`

Applied to files:

  • hooks/usePasswordManagement.ts
  • app/api/calendars/route.ts
📚 Learning: 2025-12-20T13:30:34.344Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to **/*.{ts,tsx} : Use `useTranslations()` for all user-facing text and ensure all new translation keys are added to `messages/de.json`, `messages/en.json`, and `messages/it.json`

Applied to files:

  • messages/de.json
  • messages/it.json
📚 Learning: 2025-12-20T13:30:34.344Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to **/*.{ts,tsx} : Use centralized translation keys for common messages: `common.created`, `common.updated`, `common.deleted`, `common.createError`, `common.updateError`, `common.deleteError`, `common.deleteConfirm`, `common.deleteConfirmWithWarning` with `{item}` parameter instead of duplicating messages

Applied to files:

  • messages/de.json
  • messages/it.json
  • messages/en.json
📚 Learning: 2025-12-20T13:30:34.343Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.343Z
Learning: Applies to app/**/*.tsx : Client-side password caching must use utilities from `lib/password-cache.ts`: `getCachedPassword()`, `verifyAndCachePassword()`, `setCachedPassword()`, `removeCachedPassword()`

Applied to files:

  • app/page.tsx
🧬 Code graph analysis (2)
components/calendar-compare-sheet.tsx (4)
lib/types.ts (1)
  • CalendarWithCount (4-13)
components/ui/base-sheet.tsx (1)
  • BaseSheet (41-142)
lib/db/schema.ts (1)
  • calendars (4-18)
components/ui/checkbox.tsx (1)
  • Checkbox (32-32)
components/preset-list.tsx (1)
components/ui/button.tsx (1)
  • Button (60-60)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Agent
  • GitHub Check: build-dev
🔇 Additional comments (21)
app/api/calendars/route.ts (1)

19-22: LGTM: Alias consistency improvement.

Updating the SQL alias from "shift_count" to "_count" aligns it with the field name in the select object, improving code consistency.

components/ui/base-sheet.tsx (1)

29-29: LGTM! Clean implementation of customizable save button label.

The new saveLabel prop allows consumers to override the default "Save" button text while maintaining backward compatibility through proper fallback logic.

Also applies to: 53-53, 123-125

messages/de.json (1)

85-93: LGTM! Translations correctly use informal "du" form.

The new translation keys for compare mode and preset hints are properly localized and consistently follow the informal "du" form requirement for German translations.

Based on coding guidelines, German translations must use informal "du" form.

Also applies to: 164-165

components/calendar-selector.tsx (2)

13-13: LGTM! Proper capability gating for compare functionality.

The canCompare check ensures the compare feature is only enabled when at least 2 calendars are available, preventing runtime errors and providing clear UX expectations.

Also applies to: 24-24, 37-37, 48-48


118-128: LGTM! Consistent compare button implementation across desktop and mobile.

The compare action is properly integrated into both layouts:

  • Desktop: Icon-only button with tooltip for compact header
  • Mobile: Full button with icon + text in restructured grid layout with conditional spanning

The grid layout correctly handles the Create button spanning both columns when compare is unavailable.

Also applies to: 216-238

components/app-header.tsx (1)

40-40: LGTM! Proper threading of compare action with mobile dialog handling.

The onCompare prop is correctly wired to the CalendarSelector in both desktop and mobile contexts. The mobile implementation properly closes the calendar dialog before invoking the compare action, maintaining clean UI state transitions.

Also applies to: 67-67, 154-154, 339-346

messages/it.json (1)

85-93: LGTM! Italian translations complete the multilingual support for compare mode.

The new translation keys are consistent with the English and German versions, ensuring full localization support for the calendar comparison feature.

Also applies to: 164-165

components/calendar-compare-sheet.tsx (1)

1-107: LGTM! Well-structured calendar comparison selection component.

The component follows the custom sheet pattern correctly using BaseSheet with the new saveLabel prop. The capability gating (2-3 calendars), mobile warning, and disabled states are all properly implemented.

messages/en.json (1)

85-93: LGTM! English translations complete the base language support.

The new translation keys provide clear, user-facing text for the calendar comparison feature and are consistent across all three supported languages (English, German, Italian).

Also applies to: 164-165

hooks/usePasswordManagement.ts (1)

11-11: Verify that the new calendarId field is utilized in the compare mode flow.

The optional calendarId field extends PendingAction to support calendar-scoped actions. This field is set during compare mode password verification scenarios (in handleCompareDayClick) and is consumed when rendering the ShiftDialog to maintain the correct calendar context for the pending action.

components/preset-selector.tsx (1)

24-24: LGTM!

The new hideManageButton prop is cleanly added with proper typing, a sensible default value of false for backward compatibility, and correctly forwarded to PresetList.

Also applies to: 41-41, 77-77

components/preset-list.tsx (3)

32-32: LGTM!

The hideManageButton prop is properly typed and destructured with a default value.

Also applies to: 47-47


134-146: LGTM!

The conditional rendering in the empty state correctly hides the "Create Your First" preset button when hideManageButton is true.


211-222: LGTM!

The manage button in the control row is correctly hidden when either hidePresetHeader or hideManageButton is true.

components/calendar-compare-view.tsx (4)

125-132: Good UX: Mobile warning for non-optimized experience.

This appropriately sets user expectations for the compare view on smaller screens.


191-216: LGTM!

The PresetSelector is correctly configured per-calendar with hideManageButton={true} to prevent managing presets in compare mode, which aligns with the compare view's read-focused design.


263-294: LGTM!

Optional callback handling with conditional wrappers correctly adapts callbacks to include calendarId context for compare mode interactions.


153-156: Pattern is correct and intentional.

The logic (passwordHash && isLocked) && !hasPassword in calendar-compare-view aligns with API route protection, which consistently checks both passwordHash && isLocked before verifying passwords. This dual-field check is the correct pattern for read operations per the coding guidelines.

Note: PresetList only checks passwordHash without verifying isLocked, creating an inconsistency in client-side protection logic.

app/page.tsx (3)

991-1005: LGTM!

The AnimatePresence wrapper correctly handles the compare selector overlay animation, and the cancel handler properly resets both the selector visibility and selected IDs.


1023-1023: LGTM!

The onCompare prop is correctly wired to trigger the compare selector.


523-531: No recursive call pattern exists; password dialog dismissal doesn't trigger action retry.

When the password dialog is dismissed without entering a password, the pendingAction remains set but is never executed. The action only runs via handlePasswordSuccess, which is only called on successful password verification. There's no automatic retry mechanism or loop—the user would need to click the day again to trigger a new action.

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

♻️ Duplicate comments (5)
components/calendar-compare-view.tsx (1)

268-327: Previous concern has been addressed.

The selectedPresetId is now correctly set to undefined for disabled calendars (line 274-276), and onDayClick is set to a no-op function when disabled (lines 290-294). This prevents confusion and unnecessary clicks on calendars where the selected preset doesn't belong.

messages/de.json (1)

85-96: German translations correctly use informal "du" form.

The translations appropriately use informal German ("nutze bitte" instead of formal "nutzen Sie bitte"), which aligns with the coding guideline requiring informal "du" form in all German user-facing text.

app/page.tsx (3)

260-313: Parallel data loading is well-implemented.

The compare mode data loading correctly uses Promise.all for both calendar-level parallelism and per-calendar fetch parallelism. This addresses previous performance concerns about sequential API calls.


561-590: Toggling state cleanup before password dialog is now properly handled.

The code now correctly clears the toggling state (lines 568-580) before showing the password dialog, preventing the cell from remaining in a "toggling" state indefinitely if the user cancels or enters an incorrect password.


885-928: Stale closure issue in onShiftsChange has been addressed.

The implementation now correctly:

  1. Builds updates in parallel with Promise.all (lines 890-911)
  2. Collects results into a local updates Map
  3. Uses the functional state updater pattern (lines 913-925) to apply updates to the latest state

This avoids the stale closure risk from the previous implementation.

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

93-105: Consider adding error handling for clipboard API.

The share link handler has basic error handling, but the .catch() shows a generic error. Consider providing more specific feedback if clipboard access is denied.

🔎 Optional enhancement
   const handleShareLink = () => {
     const url = new URL(window.location.href);
     url.searchParams.set("compare", props.selectedIds.join(","));
 
     navigator.clipboard
       .writeText(url.toString())
       .then(() => {
         toast.success(t("calendar.linkCopied"));
       })
       .catch(() => {
-        toast.error(t("common.error"));
+        // Fallback: show URL in a prompt if clipboard fails
+        window.prompt("Copy link:", url.toString());
       });
   };
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 93e150c and fc08082.

📒 Files selected for processing (7)
  • README.md (2 hunks)
  • app/page.tsx (10 hunks)
  • components/calendar-compare-skeleton.tsx (1 hunks)
  • components/calendar-compare-view.tsx (1 hunks)
  • messages/de.json (2 hunks)
  • messages/en.json (2 hunks)
  • messages/it.json (2 hunks)
✅ Files skipped from review due to trivial changes (1)
  • README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • messages/it.json
🧰 Additional context used
📓 Path-based instructions (5)
components/**/*.tsx

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

components/**/*.tsx: Use BaseSheet component from components/ui/base-sheet.tsx for simple sheets (create, edit, settings) with props: open, onOpenChange, title, description, showSaveButton, onSave, isSaving, saveDisabled, hasUnsavedChanges, maxWidth
Dirty state tracking in custom sheets must use useDirtyState hook with isDirty, handleClose, showConfirmDialog, setShowConfirmDialog, handleConfirmClose for unsaved changes confirmation
Custom sheet components must follow the structure: SheetContent with flex layout, SheetHeader with gradient background and border, scrollable content area with flex-1, and SheetFooter with border and action buttons
Calendar left-click toggles shift with selected preset (delete if exists, create if not), right-click opens note dialog preventing default context menu, and day indicators show <StickyNote> icon for days with notes
All components must support real-time updates via Server-Sent Events (SSE) by listening to relevant events and using silent refresh patterns with counter-based refresh triggers
New components with sheets must use BaseSheet for simple forms or follow the custom sheet pattern for complex layouts, with props: open, onOpenChange, onSubmit, optional onDelete, reset state on open change

Files:

  • components/calendar-compare-skeleton.tsx
  • components/calendar-compare-view.tsx
**/*.{ts,tsx}

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

**/*.{ts,tsx}: Never use native confirm() - always use ConfirmationDialog component with state management for confirmation dialogs
Use PRESET_COLORS array with hex format (#3b82f6) for colors, and use 20% opacity for backgrounds (e.g., ${color}20)
Use formatDateToLocal() for date formatting to YYYY-MM-DD format in all date display components
Use useTranslations() for all user-facing text and ensure all new translation keys are added to messages/de.json, messages/en.json, and messages/it.json
Use centralized translation keys for common messages: common.created, common.updated, common.deleted, common.createError, common.updateError, common.deleteError, common.deleteConfirm, common.deleteConfirmWithWarning with {item} parameter instead of duplicating messages
Use centralized validation translation keys: validation.passwordMatch, validation.passwordIncorrect, validation.passwordRequired, validation.fileRequired, validation.fileTooLarge, validation.urlRequired, validation.urlInvalid, validation.urlAlreadyExists
Use centralized form field translation keys: form.nameLabel, form.namePlaceholder, form.colorLabel, form.passwordLabel, form.passwordPlaceholder, form.notesLabel, form.notesPlaceholder, form.urlLabel, form.urlPlaceholder
Date locale formatting must use: locale === "de" ? de : (locale === "it" ? it : enUS) based on the next-intl locale setting

Files:

  • components/calendar-compare-skeleton.tsx
  • components/calendar-compare-view.tsx
  • app/page.tsx
**/*.tsx

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

**/*.tsx: React components must use React 19 hooks for state management with useState for shifts/presets/notes/calendars and useEffect for data fetching on calendar/date changes, and useRouter().replace() for URL state sync
Use statsRefreshTrigger counter pattern to track mutations and trigger data refreshes in components without page reloads
Mobile UI must separate calendar selector with showMobileCalendarDialog to prevent cramped interfaces on small screens

Files:

  • components/calendar-compare-skeleton.tsx
  • components/calendar-compare-view.tsx
  • app/page.tsx
app/**/*.tsx

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

Client-side password caching must use utilities from lib/password-cache.ts: getCachedPassword(), verifyAndCachePassword(), setCachedPassword(), removeCachedPassword()

Files:

  • app/page.tsx
messages/de.json

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

German translations must always use informal "du" form (never formal "Sie" form) in all user-facing messages, descriptions, hints, and instructions

Files:

  • messages/de.json
🧠 Learnings (9)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to components/**/*.tsx : Calendar left-click toggles shift with selected preset (delete if exists, create if not), right-click opens note dialog preventing default context menu, and day indicators show `<StickyNote>` icon for days with notes
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to **/*.tsx : Mobile UI must separate calendar selector with `showMobileCalendarDialog` to prevent cramped interfaces on small screens
📚 Learning: 2025-12-20T13:30:34.344Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to components/**/*.tsx : Calendar left-click toggles shift with selected preset (delete if exists, create if not), right-click opens note dialog preventing default context menu, and day indicators show `<StickyNote>` icon for days with notes

Applied to files:

  • components/calendar-compare-skeleton.tsx
  • components/calendar-compare-view.tsx
  • app/page.tsx
📚 Learning: 2025-12-20T13:30:34.344Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to **/*.tsx : Mobile UI must separate calendar selector with `showMobileCalendarDialog` to prevent cramped interfaces on small screens

Applied to files:

  • components/calendar-compare-skeleton.tsx
  • components/calendar-compare-view.tsx
  • app/page.tsx
📚 Learning: 2025-12-20T13:30:34.344Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to **/*.tsx : React components must use React 19 hooks for state management with `useState` for shifts/presets/notes/calendars and `useEffect` for data fetching on calendar/date changes, and `useRouter().replace()` for URL state sync

Applied to files:

  • components/calendar-compare-skeleton.tsx
  • components/calendar-compare-view.tsx
  • app/page.tsx
📚 Learning: 2025-12-20T13:30:34.343Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.343Z
Learning: Applies to hooks/**.ts : All data-fetching hooks (`useShifts`, `usePresets`, `useNotes`) must automatically call `getCachedPassword()` before each fetch and append password as query parameter if present, returning empty arrays on 401 responses

Applied to files:

  • app/page.tsx
📚 Learning: 2025-12-20T13:30:34.343Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.343Z
Learning: Applies to components/**/*.tsx : Use `BaseSheet` component from `components/ui/base-sheet.tsx` for simple sheets (create, edit, settings) with props: `open`, `onOpenChange`, `title`, `description`, `showSaveButton`, `onSave`, `isSaving`, `saveDisabled`, `hasUnsavedChanges`, `maxWidth`

Applied to files:

  • app/page.tsx
📚 Learning: 2025-12-20T13:30:34.344Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to **/*.{ts,tsx} : Use centralized translation keys for common messages: `common.created`, `common.updated`, `common.deleted`, `common.createError`, `common.updateError`, `common.deleteError`, `common.deleteConfirm`, `common.deleteConfirmWithWarning` with `{item}` parameter instead of duplicating messages

Applied to files:

  • messages/en.json
  • messages/de.json
📚 Learning: 2025-12-20T13:30:34.344Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to **/*.{ts,tsx} : Use `useTranslations()` for all user-facing text and ensure all new translation keys are added to `messages/de.json`, `messages/en.json`, and `messages/it.json`

Applied to files:

  • messages/de.json
📚 Learning: 2025-12-20T13:30:34.344Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to messages/de.json : German translations must always use informal "du" form (never formal "Sie" form) in all user-facing messages, descriptions, hints, and instructions

Applied to files:

  • messages/de.json
🧬 Code graph analysis (1)
app/page.tsx (6)
lib/locales.ts (1)
  • getDateLocale (13-15)
lib/password-cache.ts (2)
  • getCachedPassword (9-14)
  • verifyAndCachePassword (36-89)
lib/types.ts (3)
  • ShiftWithCalendar (15-35)
  • ExternalSync (2-2)
  • Shift (2-2)
lib/db/schema.ts (5)
  • CalendarNote (160-160)
  • ExternalSync (154-154)
  • ShiftPreset (158-158)
  • calendars (4-18)
  • Shift (156-156)
hooks/useVersionInfo.ts (1)
  • useVersionInfo (9-45)
lib/date-utils.ts (1)
  • formatDateToLocal (5-10)
🪛 GitHub Check: Tests & Checks
components/calendar-compare-skeleton.tsx

[warning] 2-2:
'CalendarContentSkeleton' is defined but never used

app/page.tsx

[warning] 336-336:
React Hook useEffect has a missing dependency: 'isCompareMode'. Either include it or remove the dependency array

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Agent
  • GitHub Check: build-dev
🔇 Additional comments (7)
components/calendar-compare-skeleton.tsx (1)

10-92: Well-structured skeleton component.

The component correctly implements responsive grid layouts based on the count prop and properly forwards the hidePresetHeader prop to the PresetListSkeleton. The skeleton structure mirrors the actual CalendarCompareView layout nicely.

messages/en.json (2)

85-96: Translation keys are well-structured.

The new compare mode translations follow the existing conventions and appropriately use parameterized strings ({count}) for dynamic content. The keys align with the new compare UI components.


167-168: LGTM for preset translations.

The cannotAddPresetHint key provides clear guidance for users when a preset from a different calendar is selected.

components/calendar-compare-view.tsx (1)

74-338: Well-structured compare view component.

The component correctly implements:

  • Lock state handling with getCachedPassword
  • Responsive grid layout (1-3 columns)
  • Per-calendar preset selection with proper disabling logic
  • Proper forwarding of callbacks with calendar context
  • Mobile warning banner for non-optimized view
messages/de.json (1)

167-168: LGTM for German preset translations.

The cannotAddPresetHint translation is clear and follows German language conventions.

app/page.tsx (2)

148-200: Note action wrappers for compare mode are well-structured.

The handleNoteSubmit and handleNoteDelete wrappers properly handle both regular and compare mode scenarios, reloading notes for the specific calendar in compare mode using the functional state updater pattern.


517-690: Comprehensive compare day click handler.

The handleCompareDayClick function properly handles:

  • No preset selected (shows existing shifts)
  • Toggling state management with proper cleanup
  • Password verification with state cleanup before dialog
  • Shift creation/deletion with appropriate toasts
  • Data reload after mutations

The implementation follows the coding guidelines for using formatDateToLocal, getCachedPassword, verifyAndCachePassword, and centralized translation keys.

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

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

app/page.tsx:389

  • The password success handler has incomplete logic for compare mode. When isCompareMode && compareNoteCalendarId is true, it only reloads notes data. However, if the pending action is a shift toggle action (set in handleCompareDayClick at line 581), executing that action after password success won't properly refresh the UI because the shift data reload happens inside the pending action itself, not here. This creates inconsistent behavior compared to normal mode where all data is refreshed.

Consider checking the pendingAction?.calendarId to determine which calendar's data needs reloading, or ensure the pending action itself handles all necessary data reloads.

  const handlePasswordSuccess = async () => {
    if (isCompareMode && compareNoteCalendarId) {
      // In compare mode, reload notes for the specific calendar
      await refetchNotes();
      const password = getCachedPassword(compareNoteCalendarId);
      const passwordParam = password ? `&password=${password}` : "";
      const notesRes = await fetch(
        `/api/notes?calendarId=${compareNoteCalendarId}${passwordParam}`
      );
      const notesData = notesRes.ok ? await notesRes.json() : [];
      setCompareCalendarData((prev) => {
        const updated = new Map(prev);
        const data = updated.get(compareNoteCalendarId);
        if (data) {
          updated.set(compareNoteCalendarId, { ...data, notes: notesData });
        }
        return updated;
      });
    } else {
      await Promise.all([
        refetchShifts(),
        refetchPresets(),
        refetchNotes(),
        fetchExternalSyncs(),
        fetchSyncErrorStatus(),
      ]);
      setStatsRefreshTrigger((prev) => prev + 1);
    }
    baseHandlePasswordSuccess();

    // Execute pending action if exists
    if (pendingAction?.action) {
      await pendingAction.action();
      setPendingAction(null);
    }
  };

@panteLx panteLx merged commit 5461a9d into main Dec 20, 2025
2 checks passed
@panteLx panteLx deleted the feat/calendar-comparison branch December 20, 2025 17:01
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

♻️ Duplicate comments (1)
app/page.tsx (1)

315-335: Add missing isCompareMode dependency to useEffect.

The effect reads isCompareMode (lines 317, 329) but doesn't include it in the dependency array. This can cause stale closure issues where the effect doesn't properly respond to compare mode state changes.

🔎 Proposed fix
   useEffect(() => {
     const compareParam = searchParams.get("compare");
     if (compareParam && !isCompareMode) {
       const calendarIds = compareParam.split(",").filter((id) => id.trim());
       if (calendarIds.length >= 2 && calendarIds.length <= 3) {
         // Verify that all calendars exist
         const validIds = calendarIds.filter((id) =>
           calendars.some((cal) => cal.id === id)
         );
         if (validIds.length >= 2) {
           setSelectedCompareIds(validIds);
           setIsCompareMode(true);
         }
       }
     } else if (!compareParam && isCompareMode) {
       // If compare param is removed from URL, exit compare mode
       setIsCompareMode(false);
       setSelectedCompareIds([]);
       setCompareCalendarData(new Map());
     }
-  }, [searchParams, calendars]);
+  }, [searchParams, calendars, isCompareMode]);
🧹 Nitpick comments (4)
app/page.tsx (4)

148-200: Extract duplicated note reload logic.

The logic for reloading notes in compare mode (lines 152-172 and 179-199) is duplicated between handleNoteSubmit and handleNoteDelete. Extract this into a helper function.

🔎 Proposed refactor
+ // Helper to reload notes for a calendar in compare mode
+ const reloadCompareNotes = async (calendarId: string) => {
+   const password = getCachedPassword(calendarId);
+   const passwordParam = password ? `&password=${password}` : "";
+   try {
+     const notesRes = await fetch(
+       `/api/notes?calendarId=${calendarId}${passwordParam}`
+     );
+     const notesData = notesRes.ok ? await notesRes.json() : [];
+     setCompareCalendarData((prev) => {
+       const updated = new Map(prev);
+       const data = updated.get(calendarId);
+       if (data) {
+         updated.set(calendarId, { ...data, notes: notesData });
+       }
+       return updated;
+     });
+   } catch (error) {
+     console.error("Failed to reload notes:", error);
+   }
+ };
+
  // Wrapper for note submit that reloads compare data
  const handleNoteSubmit = async (noteText: string) => {
    await noteActions.handleNoteSubmit(noteText);

-   // Reload notes for the specific calendar in compare mode
    if (isCompareMode && compareNoteCalendarId) {
-     const password = getCachedPassword(compareNoteCalendarId);
-     const passwordParam = password ? `&password=${password}` : "";
-     try {
-       const notesRes = await fetch(
-         `/api/notes?calendarId=${compareNoteCalendarId}${passwordParam}`
-       );
-       const notesData = notesRes.ok ? await notesRes.json() : [];
-       setCompareCalendarData((prev) => {
-         const updated = new Map(prev);
-         const data = updated.get(compareNoteCalendarId);
-         if (data) {
-           updated.set(compareNoteCalendarId, { ...data, notes: notesData });
-         }
-         return updated;
-       });
-     } catch (error) {
-       console.error("Failed to reload notes:", error);
-     }
+     await reloadCompareNotes(compareNoteCalendarId);
    }
  };

  // Wrapper for note delete that reloads compare data
  const handleNoteDelete = async () => {
    await noteActions.handleNoteDelete();

-   // Reload notes for the specific calendar in compare mode
    if (isCompareMode && compareNoteCalendarId) {
-     const password = getCachedPassword(compareNoteCalendarId);
-     const passwordParam = password ? `&password=${password}` : "";
-     try {
-       const notesRes = await fetch(
-         `/api/notes?calendarId=${compareNoteCalendarId}${passwordParam}`
-       );
-       const notesData = notesRes.ok ? await notesRes.json() : [];
-       setCompareCalendarData((prev) => {
-         const updated = new Map(prev);
-         const data = updated.get(compareNoteCalendarId);
-         if (data) {
-           updated.set(compareNoteCalendarId, { ...data, notes: notesData });
-         }
-         return updated;
-       });
-     } catch (error) {
-       console.error("Failed to reload notes:", error);
-     }
+     await reloadCompareNotes(compareNoteCalendarId);
    }
  };

517-689: Consider extracting logic from handleCompareDayClick.

This function is 172 lines and handles multiple responsibilities: password verification, shift creation/deletion, data reloading, and UI feedback. For better maintainability and testability, consider extracting:

  • Password verification flow into a helper
  • Shift creation logic
  • Shift deletion logic
  • Compare data reload logic

848-883: Consider extracting inline onPresetsChange handler.

The onPresetsChange handler (36 lines) is defined inline within the CalendarCompareView props. For better readability, consider extracting it as a named function similar to other handlers.


933-984: Consider extracting inline onUnlockCalendar handler.

The onUnlockCalendar handler (52 lines) is defined inline within the CalendarCompareView props. For better readability, consider extracting it as a named function similar to other handlers.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fc08082 and 3fe131e.

📒 Files selected for processing (2)
  • app/page.tsx (10 hunks)
  • components/calendar-compare-skeleton.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/calendar-compare-skeleton.tsx
🧰 Additional context used
📓 Path-based instructions (3)
app/**/*.tsx

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

Client-side password caching must use utilities from lib/password-cache.ts: getCachedPassword(), verifyAndCachePassword(), setCachedPassword(), removeCachedPassword()

Files:

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

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

**/*.{ts,tsx}: Never use native confirm() - always use ConfirmationDialog component with state management for confirmation dialogs
Use PRESET_COLORS array with hex format (#3b82f6) for colors, and use 20% opacity for backgrounds (e.g., ${color}20)
Use formatDateToLocal() for date formatting to YYYY-MM-DD format in all date display components
Use useTranslations() for all user-facing text and ensure all new translation keys are added to messages/de.json, messages/en.json, and messages/it.json
Use centralized translation keys for common messages: common.created, common.updated, common.deleted, common.createError, common.updateError, common.deleteError, common.deleteConfirm, common.deleteConfirmWithWarning with {item} parameter instead of duplicating messages
Use centralized validation translation keys: validation.passwordMatch, validation.passwordIncorrect, validation.passwordRequired, validation.fileRequired, validation.fileTooLarge, validation.urlRequired, validation.urlInvalid, validation.urlAlreadyExists
Use centralized form field translation keys: form.nameLabel, form.namePlaceholder, form.colorLabel, form.passwordLabel, form.passwordPlaceholder, form.notesLabel, form.notesPlaceholder, form.urlLabel, form.urlPlaceholder
Date locale formatting must use: locale === "de" ? de : (locale === "it" ? it : enUS) based on the next-intl locale setting

Files:

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

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

**/*.tsx: React components must use React 19 hooks for state management with useState for shifts/presets/notes/calendars and useEffect for data fetching on calendar/date changes, and useRouter().replace() for URL state sync
Use statsRefreshTrigger counter pattern to track mutations and trigger data refreshes in components without page reloads
Mobile UI must separate calendar selector with showMobileCalendarDialog to prevent cramped interfaces on small screens

Files:

  • app/page.tsx
🧠 Learnings (6)
📓 Common learnings
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to components/**/*.tsx : Calendar left-click toggles shift with selected preset (delete if exists, create if not), right-click opens note dialog preventing default context menu, and day indicators show `<StickyNote>` icon for days with notes
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to **/*.tsx : Mobile UI must separate calendar selector with `showMobileCalendarDialog` to prevent cramped interfaces on small screens
📚 Learning: 2025-12-20T13:30:34.344Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to components/**/*.tsx : Calendar left-click toggles shift with selected preset (delete if exists, create if not), right-click opens note dialog preventing default context menu, and day indicators show `<StickyNote>` icon for days with notes

Applied to files:

  • app/page.tsx
📚 Learning: 2025-12-20T13:30:34.344Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to **/*.tsx : Mobile UI must separate calendar selector with `showMobileCalendarDialog` to prevent cramped interfaces on small screens

Applied to files:

  • app/page.tsx
📚 Learning: 2025-12-20T13:30:34.344Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.344Z
Learning: Applies to **/*.tsx : React components must use React 19 hooks for state management with `useState` for shifts/presets/notes/calendars and `useEffect` for data fetching on calendar/date changes, and `useRouter().replace()` for URL state sync

Applied to files:

  • app/page.tsx
📚 Learning: 2025-12-20T13:30:34.343Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.343Z
Learning: Applies to hooks/**.ts : All data-fetching hooks (`useShifts`, `usePresets`, `useNotes`) must automatically call `getCachedPassword()` before each fetch and append password as query parameter if present, returning empty arrays on 401 responses

Applied to files:

  • app/page.tsx
📚 Learning: 2025-12-20T13:30:34.343Z
Learnt from: CR
Repo: panteLx/BetterShift PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-20T13:30:34.343Z
Learning: Applies to components/**/*.tsx : Use `BaseSheet` component from `components/ui/base-sheet.tsx` for simple sheets (create, edit, settings) with props: `open`, `onOpenChange`, `title`, `description`, `showSaveButton`, `onSave`, `isSaving`, `saveDisabled`, `hasUnsavedChanges`, `maxWidth`

Applied to files:

  • app/page.tsx
🧬 Code graph analysis (1)
app/page.tsx (9)
lib/locales.ts (1)
  • getDateLocale (13-15)
hooks/useNotes.ts (1)
  • useNotes (8-189)
lib/password-cache.ts (2)
  • getCachedPassword (9-14)
  • verifyAndCachePassword (36-89)
lib/types.ts (3)
  • ShiftWithCalendar (15-35)
  • ExternalSync (2-2)
  • Shift (2-2)
lib/db/schema.ts (5)
  • CalendarNote (160-160)
  • ExternalSync (154-154)
  • ShiftPreset (158-158)
  • calendars (4-18)
  • Shift (156-156)
lib/date-utils.ts (1)
  • formatDateToLocal (5-10)
components/calendar-compare-skeleton.tsx (1)
  • CalendarCompareSkeleton (9-91)
components/calendar-compare-view.tsx (1)
  • CalendarCompareView (74-338)
components/calendar-compare-sheet.tsx (1)
  • CalendarCompareSheet (19-107)
🪛 GitHub Check: Tests & Checks
app/page.tsx

[warning] 335-335:
React Hook useEffect has a missing dependency: 'isCompareMode'. Either include it or remove the dependency array

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build-dev

} = useNotes(selectedCalendar);

// External sync management
} = useNotes(isCompareMode ? compareNoteCalendarId : selectedCalendar);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and examine useNotes implementation
if [ -f "hooks/useNotes.ts" ]; then
  wc -l hooks/useNotes.ts
  echo "---"
  cat -n hooks/useNotes.ts
elif [ -f "src/hooks/useNotes.ts" ]; then
  wc -l src/hooks/useNotes.ts
  echo "---"
  cat -n src/hooks/useNotes.ts
else
  # Search for useNotes definition
  find . -name "*.ts" -o -name "*.tsx" | xargs grep -l "export.*useNotes" 2>/dev/null | head -5
fi

Repository: panteLx/BetterShift

Length of output: 6896


useNotes handles undefined gracefully for the main flow, but updateNote and deleteNote lack defensive checks.

The hook correctly accepts calendarId: string | undefined and guards against undefined in fetchNotes() (line 13: if (!calendarId) return) and the useEffect dependency (lines 173-178). When undefined, notes remain empty and no API calls are made—which safely covers the scenario at line 109.

However, updateNote (line 96) and deleteNote (line 135) call getCachedPassword(calendarId) without first checking if calendarId is defined. Add guards like if (!calendarId) return false; at the start of both methods to prevent potential runtime errors if these functions are called when the calendar ID is undefined.

🤖 Prompt for AI Agents
In app/page.tsx around lines 96, 109 and 135, updateNote and deleteNote call
getCachedPassword(calendarId) without ensuring calendarId is defined; add a
defensive guard at the start of both functions (e.g., if (!calendarId) return
false;) so they return early when calendarId is undefined and avoid runtime
errors, leaving the existing main flow handling for undefined calendarId
unchanged.

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.

feature request: Compare calendars with each other

1 participant