--- phase: '03' plan: '05' subsystem: pwa-event-write-ui tags: [react, tanstack-query, zustand, eventform, caldav-write, pwa] dependency_graph: requires: ['03-03'] provides: [ 'EventForm component', 'createEvent/updateEvent/fetchWritableCalendars client calls', 'eventFormOpen/eventFormMode/eventFormUid Zustand keys', ] affects: ['CalendarShell', 'EventDetailPopover (future edit trigger)'] tech_stack: added: [] patterns: [ 'TanStack Query useMutation + useQuery', 'Zustand UI-only state', 'bottom-sheet/dialog responsive overlay', 'vi.hoisted() for mock TDZ', 'D-01 last-used calendar localStorage', 'D-02 conditional calendar picker', 'D-11 whole-series recurrence presets', 'T-03-15 plain-text JSX XSS guard', ] key_files: created: - apps/pwa/src/components/EventForm.tsx - apps/pwa/src/api/client.test.ts - apps/pwa/src/components/EventForm.test.tsx modified: - apps/pwa/src/api/client.ts - apps/pwa/src/store/calendarStore.ts - apps/pwa/src/components/CalendarShell.tsx decisions: - 'D-01 calendar default: last-used URL from localStorage (eventForm.lastCalendarUrl), first writable calendar as fallback' - 'D-02 calendar picker: hidden when writableCalendars.length === 1, shown when >1 — driven by GET /api/events/writable-calendars' - 'T-03-15 XSS: all field values as plain-text JSX children in EventForm; no dangerouslySetInnerHTML in code' - 'vi.hoisted() required for mock factory variables to avoid TDZ (D-03-04-hoisting pattern)' - 'eventFormOpen selector added to CalendarShell per-field selector pattern (Bug B guard preserved)' metrics: duration_minutes: 6 completed_date: '2026-06-05' tasks_completed: 3 files_created: 3 files_modified: 3 --- # Phase 03 Plan 05: Event Write UI (EventForm + Client Calls) Summary **One-liner:** EventForm modal with timed/all-day/recurring fields, conditional calendar picker (D-02), and typed write client (createEvent/updateEvent/fetchWritableCalendars) wired to the Plan 03 write API via TanStack Query mutations. ## What Was Built ### Task 1: Typed write client calls + Zustand form-state keys Extended `apps/pwa/src/api/client.ts` with: - `CreateEventPayload` interface (title, allDay, start, end, recurrence, optional location/description/calendarUrl) - `CreateEventResponse` interface ({ uid }) - `WritableCalendar` interface ({ url, displayName, color, isShared }) — D-03 server-authoritative shape - `RecurrencePreset` type ('none'|'daily'|'weekly'|'monthly'|'yearly') - `createEvent(payload)` — POST /api/events/create, credentials:include, returns {uid} - `updateEvent(uid, payload)` — PATCH /api/events/:uid/edit - `fetchWritableCalendars()` — GET /api/events/writable-calendars, parses `{ calendars }` envelope, returns `WritableCalendar[]` Extended `apps/pwa/src/store/calendarStore.ts` with: - `eventFormOpen: boolean` (default: false) - `eventFormMode: 'create' | 'edit'` (default: 'create') - `eventFormUid: string | null` (default: null) - `setEventForm(open, mode?, uid?)` setter — no server data in Zustand ### Task 2: EventForm modal New `apps/pwa/src/components/EventForm.tsx` (715 lines): - Bottom sheet on phone (≤767px), centered 480px dialog on tablet/desktop — reuses EventDetailPopover pattern - Fields per UI-SPEC §EventForm order: title, all-day toggle, start date/time, end date/time, calendar picker (conditional), recurrence, location, description - All-day toggle (`role="switch"`, aria-checked): hides time inputs when on, restores 09:00/10:00 defaults when off - Recurrence: `