Files
calendars/RECURRENCE_IMPLEMENTATION.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

407 lines
12 KiB
Markdown

# Recurring Events Implementation Guide
## Overview
This document describes the complete implementation of recurring events in the CalDAV calendar application. The implementation follows the iCalendar RFC 5545 standard for RRULE (Recurrence Rule).
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Frontend (Next.js) │
├─────────────────────────────────────────────────────────────┤
│ RecurrenceEditor Component │
│ ├─ UI for frequency selection (DAILY/WEEKLY/MONTHLY/YEARLY) │
│ ├─ Interval input │
│ ├─ Day/Month/Date selection │
│ └─ End conditions (never/until/count) │
│ │
│ EventCalendarAdapter │
│ ├─ Converts IcsRecurrenceRule to RRULE string │
│ └─ Parses RRULE to IcsRecurrenceRule │
├─────────────────────────────────────────────────────────────┤
│ ts-ics Library │
│ IcsRecurrenceRule interface │
│ ├─ frequency: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY' │
│ ├─ interval?: number │
│ ├─ byDay?: IcsWeekDay[] │
│ ├─ byMonthDay?: number[] │
│ ├─ byMonth?: number[] │
│ ├─ count?: number │
│ └─ until?: IcsDateObject │
├─────────────────────────────────────────────────────────────┤
│ CalDAV Protocol │
│ RRULE property in VEVENT │
│ Example: RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,FR │
├─────────────────────────────────────────────────────────────┤
│ Sabre/dav Server (PHP) │
│ Stores and serves iCalendar (.ics) files │
│ Handles recurring event expansion │
└─────────────────────────────────────────────────────────────┘
```
## Component Structure
### RecurrenceEditor Component
Location: `src/features/calendar/components/RecurrenceEditor.tsx`
**Props:**
```typescript
interface RecurrenceEditorProps {
value?: IcsRecurrenceRule;
onChange: (rule: IcsRecurrenceRule | undefined) => void;
}
```
**Features:**
- ✅ Simple mode: Quick selection (None, Daily, Weekly, Monthly, Yearly)
- ✅ Custom mode: Full control over all recurrence parameters
- ✅ DAILY: Interval support (every X days)
- ✅ WEEKLY: Interval + day selection (MO, TU, WE, TH, FR, SA, SU)
- ✅ MONTHLY: Day of month (1-31) with validation
- ✅ YEARLY: Month + day selection with leap year support
- ✅ End conditions: Never / Until date / After N occurrences
- ✅ Date validation warnings (Feb 30th, Feb 29th leap year, etc.)
### Example Usage
```tsx
import { RecurrenceEditor } from '@/features/calendar/components/RecurrenceEditor';
import { useState } from 'react';
import type { IcsRecurrenceRule } from 'ts-ics';
function EventForm() {
const [recurrence, setRecurrence] = useState<IcsRecurrenceRule | undefined>();
return (
<form>
{/* Other event fields */}
<RecurrenceEditor
value={recurrence}
onChange={setRecurrence}
/>
{/* Save button */}
</form>
);
}
```
## Integration with Scheduler
To integrate the RecurrenceEditor into the existing Scheduler modal, add the following:
### 1. Add recurrence state
```typescript
// In EventModal component
const [recurrence, setRecurrence] = useState<IcsRecurrenceRule | undefined>(
event?.recurrenceRule
);
```
### 2. Add RecurrenceEditor to the form
```tsx
import { RecurrenceEditor } from '../RecurrenceEditor';
// In the modal JSX, after location/description fields
<RecurrenceEditor
value={recurrence}
onChange={setRecurrence}
/>
```
### 3. Include recurrence in event save
```typescript
const icsEvent: IcsEvent = {
// ... existing fields
recurrenceRule: recurrence,
};
```
### 4. Reset recurrence when modal opens
```typescript
useEffect(() => {
// ... existing resets
setRecurrence(event?.recurrenceRule);
}, [event]);
```
## RRULE Examples
### Daily Recurrence
**Every day:**
```
RRULE:FREQ=DAILY;INTERVAL=1
```
**Every 3 days:**
```
RRULE:FREQ=DAILY;INTERVAL=3
```
**Daily for 10 occurrences:**
```
RRULE:FREQ=DAILY;COUNT=10
```
**Daily until Dec 31, 2025:**
```
RRULE:FREQ=DAILY;UNTIL=20251231T235959Z
```
### Weekly Recurrence
**Every week on Monday:**
```
RRULE:FREQ=WEEKLY;BYDAY=MO
```
**Every 2 weeks on Monday and Friday:**
```
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,FR
```
**Weekly on weekdays (Mon-Fri):**
```
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR
```
### Monthly Recurrence
**Every month on the 15th:**
```
RRULE:FREQ=MONTHLY;BYMONTHDAY=15
```
**Every 3 months on the 1st:**
```
RRULE:FREQ=MONTHLY;INTERVAL=3;BYMONTHDAY=1
```
**Monthly on the last day (31st with fallback):**
```
RRULE:FREQ=MONTHLY;BYMONTHDAY=31
```
Note: For months with fewer than 31 days, most implementations skip that occurrence.
### Yearly Recurrence
**Every year on March 15th:**
```
RRULE:FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=15
```
**Every year on February 29th (leap years only):**
```
RRULE:FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=29
```
**Every 2 years on December 25th:**
```
RRULE:FREQ=YEARLY;INTERVAL=2;BYMONTH=12;BYMONTHDAY=25
```
## Date Validation
The RecurrenceEditor includes smart validation for invalid dates:
### February 30th/31st
**Warning:** "February has at most 29 days"
### February 29th
**Warning:** "This date (Feb 29) only exists in leap years"
### April 31st, June 31st, etc.
**Warning:** "This month has at most 30 days"
### Day > 31
**Warning:** "Day must be between 1 and 31"
## IcsRecurrenceRule Interface (ts-ics)
```typescript
interface IcsRecurrenceRule {
frequency: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';
interval?: number; // Default: 1
count?: number; // Number of occurrences
until?: IcsDateObject; // End date
byDay?: IcsWeekDay[]; // Days of week (WEEKLY)
byMonthDay?: number[]; // Days of month (MONTHLY, YEARLY)
byMonth?: number[]; // Months (YEARLY)
bySetPos?: number[]; // Position (e.g., 1st Monday)
weekStart?: IcsWeekDay; // Week start day
}
type IcsWeekDay = 'MO' | 'TU' | 'WE' | 'TH' | 'FR' | 'SA' | 'SU';
```
## Backend Considerations
The Django backend **does not need modifications** for recurring events. CalDAV handles recurrence natively:
1. **Storage:** RRULE is stored as a property in the VEVENT within the .ics file
2. **Expansion:** Sabre/dav handles recurring event expansion when clients query date ranges
3. **Modifications:** Individual instances can be modified by creating exception events with RECURRENCE-ID
### Example .ics file with recurrence
```ics
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//CalDavService//NONSGML v1.0//EN
METHOD:PUBLISH
BEGIN:VEVENT
UID:abc-123-def-456
DTSTART:20260125T140000Z
DTEND:20260125T150000Z
SUMMARY:Weekly Team Meeting
RRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20261231T235959Z
ORGANIZER;CN=Alice:mailto:alice@example.com
ATTENDEE;CN=Bob;PARTSTAT=NEEDS-ACTION:mailto:bob@example.com
END:VEVENT
END:VCALENDAR
```
## Testing
### Manual Testing Checklist
- [ ] Daily recurrence with interval 1, 3, 7
- [ ] Weekly recurrence with single day (Monday)
- [ ] Weekly recurrence with multiple days (Mon, Wed, Fri)
- [ ] Weekly recurrence with interval 2
- [ ] Monthly recurrence on day 1, 15, 31
- [ ] Monthly recurrence with February validation
- [ ] Yearly recurrence on Jan 1, Dec 25
- [ ] Yearly recurrence on Feb 29 with leap year warning
- [ ] Never-ending recurrence
- [ ] Until date recurrence
- [ ] Count-based recurrence (10 occurrences)
- [ ] Edit recurring event
- [ ] Delete recurring event
### Test Cases
```typescript
// Test: Weekly on Monday and Friday
const rule: IcsRecurrenceRule = {
frequency: 'WEEKLY',
interval: 1,
byDay: [{ day: 'MO' }, { day: 'FR' }],
};
// Expected RRULE: FREQ=WEEKLY;BYDAY=MO,FR
// Test: Monthly on 31st (handles months with fewer days)
const rule: IcsRecurrenceRule = {
frequency: 'MONTHLY',
interval: 1,
byMonthDay: [31],
};
// Expected RRULE: FREQ=MONTHLY;BYMONTHDAY=31
// Test: Yearly on Feb 29
const rule: IcsRecurrenceRule = {
frequency: 'YEARLY',
interval: 1,
byMonth: [2],
byMonthDay: [29],
count: 10,
};
// Expected RRULE: FREQ=YEARLY;BYMONTH=2;BYMONTHDAY=29;COUNT=10
```
## Translations
All UI strings are internationalized (i18n) with support for:
- 🇬🇧 English
- 🇫🇷 French
- 🇳🇱 Dutch
Translation keys are defined in `src/features/i18n/translations.json`:
```json
{
"calendar": {
"recurrence": {
"label": "Repeat",
"daily": "Daily",
"weekly": "Weekly",
"monthly": "Monthly",
"yearly": "Yearly",
"repeatOnDay": "Repeat on day",
"repeatOnDate": "Repeat on date",
"dayOfMonth": "Day",
"months": {
"january": "January",
"february": "February",
...
},
"warnings": {
"februaryMax": "February has at most 29 days",
"leapYear": "This date (Feb 29) only exists in leap years",
...
}
}
}
}
```
## Styling
Styles are in `RecurrenceEditor.scss` using BEM methodology:
```scss
.recurrence-editor {
&__label { ... }
&__weekday-button { ... }
&__weekday-button--selected { ... }
&__warning { ... }
}
.recurrence-editor-layout {
&--row { ... }
&--gap-1rem { ... }
&--flex-wrap { ... }
}
```
## Known Limitations
1. **No BYDAY with position** (e.g., "2nd Tuesday of month")
- Future enhancement
- Requires UI for "1st/2nd/3rd/4th/last" + weekday selection
2. **No BYSETPOS** (complex patterns)
- e.g., "Last Friday of every month"
- Requires advanced UI
3. **Time zone handling**
- UNTIL dates are converted to UTC
- Local time events use floating time
4. **Recurring event modifications**
- Editing single instance creates exception (RECURRENCE-ID)
- Not yet implemented in UI (future work)
## Future Enhancements
- [ ] Visual calendar preview of recurrence pattern
- [ ] Natural language summary ("Every 2 weeks on Monday and Friday")
- [ ] Support for BYSETPOS (nth occurrence patterns)
- [ ] Exception handling UI for editing single instances
- [ ] Recurring event series deletion options (this only / this and future / all)
## References
- [RFC 5545 - iCalendar](https://datatracker.ietf.org/doc/html/rfc5545)
- [RRULE Specification](https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html)
- [ts-ics Documentation](https://github.com/Neuvernetzung/ts-ics)
- [Sabre/dav Documentation](https://sabre.io/dav/)