Files
calendars/SCHEDULER_RECURRENCE_INTEGRATION.md
Nathan Panchout 8a253950cc 📝(docs) update project documentation
Add CLAUDE.md for AI assistant guidance. Add documentation
for PR split plan, implementation checklist, and recurrence
feature specifications.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 16:56:21 +01:00

9.5 KiB

Scheduler Recurrence Integration Guide

How to Add Recurrence Support to EventModal in Scheduler.tsx

This guide shows exactly how to integrate the RecurrenceEditor component into the existing Scheduler event modal.

Step 1: Import RecurrenceEditor

Add to imports at the top of Scheduler.tsx:

import { RecurrenceEditor } from "../RecurrenceEditor";
import type { IcsRecurrenceRule } from "ts-ics";

Step 2: Add Recurrence State

In the EventModal component, add recurrence state after the existing useState declarations:

// Around line 110, after:
const [attendees, setAttendees] = useState<IcsAttendee[]>([]);
const [showAttendees, setShowAttendees] = useState(false);

// Add:
const [recurrence, setRecurrence] = useState<IcsRecurrenceRule | undefined>(
  event?.recurrenceRule
);
const [showRecurrence, setShowRecurrence] = useState(() => {
  return !!event?.recurrenceRule;
});

Step 3: Reset Recurrence When Event Changes

In the useEffect that resets form state, add recurrence reset:

// Around line 121-161, in the useEffect(() => { ... }, [event, calendarUrl])
useEffect(() => {
  setTitle(event?.summary || "");
  setDescription(event?.description || "");
  setLocation(event?.location || "");
  setSelectedCalendarUrl(calendarUrl);

  // Initialize attendees from event
  if (event?.attendees && event.attendees.length > 0) {
    setAttendees(event.attendees);
    setShowAttendees(true);
  } else {
    setAttendees([]);
    setShowAttendees(false);
  }

  // ADD THIS: Initialize recurrence from event
  if (event?.recurrenceRule) {
    setRecurrence(event.recurrenceRule);
    setShowRecurrence(true);
  } else {
    setRecurrence(undefined);
    setShowRecurrence(false);
  }

  // ... rest of the useEffect
}, [event, calendarUrl]);

Step 4: Include Recurrence in Save

In the handleSave function, add recurrence to the IcsEvent object:

// Around line 200-227, when creating the icsEvent
const icsEvent: IcsEvent = {
  ...eventWithoutDuration,
  uid: event?.uid || crypto.randomUUID(),
  summary: title,
  description: description || undefined,
  location: location || undefined,
  start: {
    date: fakeUtcStart,
    local: {
      timezone: BROWSER_TIMEZONE,
      tzoffset: adapter.getTimezoneOffset(startDate, BROWSER_TIMEZONE),
    },
  },
  end: {
    date: fakeUtcEnd,
    local: {
      timezone: BROWSER_TIMEZONE,
      tzoffset: adapter.getTimezoneOffset(endDate, BROWSER_TIMEZONE),
    },
  },
  organizer: organizer,
  attendees: attendees.length > 0 ? attendees : undefined,
  recurrenceRule: recurrence,  // ADD THIS LINE
};

Step 5: Add RecurrenceEditor to UI

In the modal JSX, add a button to show/hide recurrence and the RecurrenceEditor component.

Add Feature Button (like the attendees button)

Around line 350-360, after the attendees button:

{/* Existing code */}
<button
  type="button"
  className={`event-modal__feature-tag ${showAttendees ? 'event-modal__feature-tag--active' : ''}`}
  onClick={() => setShowAttendees(!showAttendees)}
>
  <span className="material-icons">group</span>
  {t('calendar.event.attendees')}
</button>

{/* ADD THIS: Recurrence button */}
<button
  type="button"
  className={`event-modal__feature-tag ${showRecurrence ? 'event-modal__feature-tag--active' : ''}`}
  onClick={() => setShowRecurrence(!showRecurrence)}
>
  <span className="material-icons">repeat</span>
  {t('calendar.recurrence.label')}
</button>

Add RecurrenceEditor Component

Around line 370, after the AttendeesInput:

{/* Existing attendees input */}
{showAttendees && (
  <div className="event-modal__attendees-input">
    <AttendeesInput
      attendees={attendees}
      onChange={setAttendees}
      organizerEmail={user?.email}
      organizer={organizer}
    />
  </div>
)}

{/* ADD THIS: Recurrence editor */}
{showRecurrence && (
  <div className="event-modal__recurrence-editor">
    <RecurrenceEditor
      value={recurrence}
      onChange={setRecurrence}
    />
  </div>
)}

Step 6: Add CSS for Recurrence Section

In Scheduler.scss, add styling for the recurrence section:

.event-modal {
  // ... existing styles

  &__recurrence-editor {
    padding: 1rem;
    background-color: #f8f9fa;
    border-radius: 4px;
    margin-top: 1rem;
  }

  // Ensure feature tags wrap properly
  &__features {
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;  // Add this if not present
  }
}

Complete EventModal Component Structure

Here's the complete structure with recurrence integrated:

const EventModal = ({
  isOpen,
  mode,
  event,
  calendarUrl,
  calendars,
  adapter,
  onSave,
  onDelete,
  onClose,
}: EventModalProps) => {
  const { t } = useTranslation();
  const { user } = useAuth();

  // Form state
  const [title, setTitle] = useState(event?.summary || "");
  const [description, setDescription] = useState(event?.description || "");
  const [location, setLocation] = useState(event?.location || "");
  const [startDateTime, setStartDateTime] = useState("");
  const [endDateTime, setEndDateTime] = useState("");
  const [selectedCalendarUrl, setSelectedCalendarUrl] = useState(calendarUrl);
  const [isLoading, setIsLoading] = useState(false);

  // Features state
  const [attendees, setAttendees] = useState<IcsAttendee[]>([]);
  const [showAttendees, setShowAttendees] = useState(false);
  const [recurrence, setRecurrence] = useState<IcsRecurrenceRule | undefined>();
  const [showRecurrence, setShowRecurrence] = useState(false);

  // Calculate organizer
  const organizer: IcsOrganizer | undefined = event?.organizer || ...;

  // Reset form when event changes
  useEffect(() => {
    // Reset basic fields
    setTitle(event?.summary || "");
    setDescription(event?.description || "");
    setLocation(event?.location || "");
    setSelectedCalendarUrl(calendarUrl);

    // Reset attendees
    if (event?.attendees && event.attendees.length > 0) {
      setAttendees(event.attendees);
      setShowAttendees(true);
    } else {
      setAttendees([]);
      setShowAttendees(false);
    }

    // Reset recurrence
    if (event?.recurrenceRule) {
      setRecurrence(event.recurrenceRule);
      setShowRecurrence(true);
    } else {
      setRecurrence(undefined);
      setShowRecurrence(false);
    }

    // Reset dates
    // ... existing date reset logic
  }, [event, calendarUrl]);

  const handleSave = async () => {
    // ... create icsEvent with recurrence
    const icsEvent: IcsEvent = {
      // ... all fields
      recurrenceRule: recurrence,
    };

    await onSave(icsEvent, selectedCalendarUrl);
  };

  return (
    <Modal isOpen={isOpen} onClose={onClose}>
      {/* Title, Calendar selector, Dates */}

      {/* Feature tags */}
      <div className="event-modal__features">
        <button onClick={() => setShowAttendees(!showAttendees)}>
          <span className="material-icons">group</span>
          {t('calendar.event.attendees')}
        </button>

        <button onClick={() => setShowRecurrence(!showRecurrence)}>
          <span className="material-icons">repeat</span>
          {t('calendar.recurrence.label')}
        </button>
      </div>

      {/* Location, Description */}

      {/* Attendees section */}
      {showAttendees && (
        <AttendeesInput
          attendees={attendees}
          onChange={setAttendees}
          organizerEmail={user?.email}
          organizer={organizer}
        />
      )}

      {/* Recurrence section */}
      {showRecurrence && (
        <RecurrenceEditor
          value={recurrence}
          onChange={setRecurrence}
        />
      )}

      {/* Save/Cancel buttons */}
    </Modal>
  );
};

Material Icons

The recurrence button uses the repeat Material icon. Make sure Material Icons are loaded:

<!-- In _app.tsx or layout -->
<link
  href="https://fonts.googleapis.com/icon?family=Material+Icons"
  rel="stylesheet"
/>

Testing the Integration

  1. Create new recurring event:

    • Click "Create" in calendar
    • Click "Repeat" button (🔁 icon)
    • Select "Weekly", check "Monday" and "Wednesday"
    • Save
  2. Edit recurring event:

    • Click on a recurring event
    • Modal should show recurrence with "Repeat" button active
    • Modify recurrence pattern
    • Save
  3. Remove recurrence:

    • Open recurring event
    • Click "Repeat" button to expand
    • Select "None" from dropdown
    • Save

Expected Behavior

  • Recurrence button toggles RecurrenceEditor visibility
  • Active button shows blue background (like attendees)
  • RecurrenceEditor state persists when toggling visibility
  • Saving event includes recurrence in IcsEvent
  • Opening existing recurring event loads recurrence correctly
  • Calendar displays recurring event instances

Troubleshooting

Recurrence not saving

  • Check that recurrenceRule: recurrence is in the icsEvent object
  • Verify ts-ics is correctly serializing the RRULE

Recurrence not loading when editing

  • Check the useEffect includes recurrence reset
  • Verify event?.recurrenceRule is being passed from EventCalendarAdapter

UI not showing properly

  • Ensure RecurrenceEditor.scss is imported in globals.scss
  • Check that Material Icons font is loaded

Events not appearing as recurring

  • Verify CalDAV server supports RRULE
  • Check browser console for errors
  • Inspect .ics file content in network tab

Next Steps

After integration, consider:

  1. Adding recurrence summary text (e.g., "Repeats weekly on Monday")
  2. Handle editing single instance vs series
  3. Add "Delete series" vs "Delete this occurrence" options
  4. Show recurrence icon in calendar event display