docs(03): create phase plan (8 plans, 5 waves)
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
---
|
||||
phase: 03-event-write-back-pwa-install
|
||||
plan: 05
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: ["03-03"]
|
||||
files_modified:
|
||||
- apps/pwa/src/api/client.ts
|
||||
- apps/pwa/src/store/calendarStore.ts
|
||||
- apps/pwa/src/components/EventForm.tsx
|
||||
- apps/pwa/src/components/CalendarShell.tsx
|
||||
autonomous: true
|
||||
requirements: [CAL-04, CAL-05, CAL-07]
|
||||
user_setup: []
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "A member can tap 'New Event', fill the form, and save — POST /api/events/create fires and the form closes"
|
||||
- "The form supports timed and all-day events, a recurrence preset (None/Daily/Weekly/Monthly/Yearly), title/location/description"
|
||||
- "The calendar picker is hidden when the member has exactly one writable calendar (D-02)"
|
||||
- "Edit mode pre-populates the form and calls PATCH /api/events/:uid/edit"
|
||||
artifacts:
|
||||
- path: "apps/pwa/src/components/EventForm.tsx"
|
||||
provides: "create/edit modal form (bottom sheet on phone, dialog on desktop)"
|
||||
min_lines: 80
|
||||
- path: "apps/pwa/src/api/client.ts"
|
||||
provides: "createEvent, updateEvent, fetchWritableCalendars typed calls"
|
||||
exports: ["createEvent", "updateEvent", "fetchWritableCalendars"]
|
||||
key_links:
|
||||
- from: "apps/pwa/src/components/EventForm.tsx"
|
||||
to: "/api/events/create"
|
||||
via: "createEvent mutation"
|
||||
pattern: "createEvent"
|
||||
- from: "apps/pwa/src/components/CalendarShell.tsx"
|
||||
to: "EventForm"
|
||||
via: "New Event FAB toggles eventFormOpen"
|
||||
pattern: "eventFormOpen"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Build the create/edit event UI: the typed write client calls, the Zustand form-state
|
||||
keys, the `EventForm` modal (timed/all-day/recurring fields, conditional calendar
|
||||
picker), and the "New Event" FAB/toolbar entry on the calendar shell. This is the
|
||||
front half of the create and edit vertical slices — after this plan a member can
|
||||
open the form and submit a write (delete + sync feedback land in Plan 06).
|
||||
|
||||
Purpose: CAL-04 (create timed/all-day) and CAL-07 (create recurring) become user-reachable.
|
||||
Built against the UI Design Contract (03-UI-SPEC.md) for fields, copy, tokens, and
|
||||
interaction; reuses the Phase 2 EventDetailPopover overlay/focus-trap/responsive pattern (D-10).
|
||||
|
||||
Output: EventForm + client write calls + store keys + FAB, all wired to the Plan 03 API.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/phases/03-event-write-back-pwa-install/03-UI-SPEC.md
|
||||
@.planning/phases/03-event-write-back-pwa-install/03-PATTERNS.md
|
||||
@apps/pwa/src/api/client.ts
|
||||
@apps/pwa/src/store/calendarStore.ts
|
||||
@apps/pwa/src/components/EventDetailPopover.tsx
|
||||
@apps/pwa/src/components/CalendarShell.tsx
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Typed write client calls + Zustand form-state keys</name>
|
||||
<files>apps/pwa/src/api/client.ts, apps/pwa/src/store/calendarStore.ts</files>
|
||||
<read_first>
|
||||
- apps/pwa/src/api/client.ts (existing — fetch function + interface-first pattern; CalendarOccurrence shape)
|
||||
- apps/pwa/src/store/calendarStore.ts (existing — CalendarStore interface + create() pattern)
|
||||
- .planning/phases/03-event-write-back-pwa-install/03-UI-SPEC.md (§State Management Contract — Zustand keys; §EventForm fields → request shape)
|
||||
- .planning/phases/03-event-write-back-pwa-install/03-PATTERNS.md (§client.ts — POST/PATCH fetch shape; §Zustand UI state)
|
||||
</read_first>
|
||||
<behavior>
|
||||
Tests (extend pwa test suite where one exists, else add a small client unit test):
|
||||
- createEvent posts to /api/events/create with credentials:'include' and JSON body; returns { uid } on 202.
|
||||
- updateEvent PATCHes /api/events/:uid/edit.
|
||||
- fetchWritableCalendars GETs the writable-calendar set.
|
||||
- The Zustand store exposes the new keys with correct defaults.
|
||||
</behavior>
|
||||
<action>
|
||||
In client.ts add exported interfaces `CreateEventPayload` (title, allDay, start, end, optional location, description, recurrence: 'none'|'daily'|'weekly'|'monthly'|'yearly', calendarUrl?), `CreateEventResponse` ({ uid }), `WritableCalendar` ({ url, displayName, color, isShared }). Add `createEvent(payload): Promise<CreateEventResponse>` (POST), `updateEvent(uid, payload): Promise<CreateEventResponse>` (PATCH `/api/events/${uid}/edit`), and `fetchWritableCalendars(): Promise<WritableCalendar[]>` (GET `/api/events/writable-calendars` — if Plan 03 did not add this endpoint, derive the writable set on the client from the existing calendars data; document which). All follow the existing fetch shape with credentials:'include' and `if (!res.ok) throw`.
|
||||
|
||||
In calendarStore.ts extend `CalendarStore` with `eventFormOpen: boolean`, `eventFormMode: 'create'|'edit'`, `eventFormUid: string|null`, plus setters `setEventForm(open, mode?, uid?)`. Defaults: closed, mode 'create', uid null. Keep all server data out of Zustand (D — server state stays in TanStack Query).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/pwa exec tsc --noEmit && grep -q "createEvent" apps/pwa/src/api/client.ts && grep -q "eventFormOpen" apps/pwa/src/store/calendarStore.ts && pnpm --filter @familysync/pwa test</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -Eq "createEvent|updateEvent" apps/pwa/src/api/client.ts`.
|
||||
- `grep -q "eventFormOpen" apps/pwa/src/store/calendarStore.ts`.
|
||||
- PWA tsc --noEmit passes; existing PWA tests stay green.
|
||||
</acceptance_criteria>
|
||||
<done>Write client calls and form-state Zustand keys exist and type-check.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 2: EventForm modal (create + edit) per UI Design Contract</name>
|
||||
<files>apps/pwa/src/components/EventForm.tsx</files>
|
||||
<read_first>
|
||||
- .planning/phases/03-event-write-back-pwa-install/03-UI-SPEC.md (§EventForm — field order/types/required; §CalendarPicker D-02; §Recurrence picker; §Copywriting Contract; §Interaction Contract all-day toggle + recurrence + keyboard; §Spacing/Typography/Color tokens)
|
||||
- apps/pwa/src/components/EventDetailPopover.tsx (analog — backdrop+dialog structure ~202-221, Escape+focus-trap useEffect ~143-159, responsive isPhone/dialogStyle ~165-199, design tokens, XSS plain-text rule)
|
||||
- .planning/phases/03-event-write-back-pwa-install/03-PATTERNS.md (§EventForm.tsx — modal/overlay, focus trap, TanStack mutation, Zustand)
|
||||
- apps/pwa/src/api/client.ts (createEvent/updateEvent/fetchWritableCalendars from Task 1)
|
||||
</read_first>
|
||||
<behavior>
|
||||
Tests (EventForm.test.tsx): renders title/all-day/start/end/recurrence/location/description fields; toggling "All day" hides time inputs; calendar picker is absent when one writable calendar and present when two (D-02); empty title shows "Title is required"; end-before-start shows "End time must be after start"; submitting calls the createEvent mutation in create mode and updateEvent in edit mode; Escape and backdrop close the form.
|
||||
</behavior>
|
||||
<action>
|
||||
Implement `EventForm.tsx` as a modal overlay reusing the EventDetailPopover backdrop+dialog+focus-trap+responsive pattern (bottom sheet on phone, centered 480px dialog on desktop). Fields and order exactly per UI-SPEC §EventForm. All-day toggle (`role="switch"`) hides start/end time inputs and applies the auto-advance rule; defaults start 09:00/end 10:00 when toggled off. Recurrence as a segmented select (`role="radiogroup"` or `<select>`) of None/Daily/Weekly/Monthly/Yearly (D-11 whole-series; map to the recurrence enum). Calendar picker rendered only when `fetchWritableCalendars()` returns >1 (D-02); default selection = last-used (read from a localStorage key) else personal (D-01). Use `useMutation` (TanStack Query) calling `createEvent`/`updateEvent` by `eventFormMode`; on success close the form (`setEventForm(false)`) and set `lastSyncedUid` (added in Plan 06; if absent, store the returned uid in a placeholder for now). Validation: empty title and end-before-start show the exact UI-SPEC error copy in `--color-destructive`. All spacing/color via tokens; all field values rendered as plain-text JSX children (XSS guard); 44px min touch targets; `role="dialog"` `aria-modal="true"` `aria-label` "New Event"/"Edit Event"; focus the Title input on open; Escape/backdrop close. Edit mode pre-populates fields from the occurrence identified by `eventFormUid` (read from the TanStack `['events']` cache like EventDetailPopover does).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/luc/Projects/familysync && pnpm --filter @familysync/pwa test -- EventForm && pnpm --filter @familysync/pwa exec tsc --noEmit</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- EventForm.test.tsx GREEN (fields, all-day toggle, D-02 picker visibility, validation copy, create vs edit mutation, Escape/backdrop close).
|
||||
- `grep -q 'aria-modal="true"' apps/pwa/src/components/EventForm.tsx`.
|
||||
- No `dangerouslySetInnerHTML` in EventForm.tsx.
|
||||
</acceptance_criteria>
|
||||
<done>EventForm renders all contract fields, enforces D-02/D-11/validation, and submits create/edit; tests GREEN.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Mount EventForm + add "New Event" FAB/toolbar trigger on CalendarShell</name>
|
||||
<files>apps/pwa/src/components/CalendarShell.tsx</files>
|
||||
<read_first>
|
||||
- apps/pwa/src/components/CalendarShell.tsx (existing — where EventDetailPopover is mounted; toolbar/nav structure)
|
||||
- .planning/phases/03-event-write-back-pwa-install/03-UI-SPEC.md (§Interaction Contract — Create opens from FAB (phone) or toolbar button (desktop); Copywriting "New Event" + Plus icon)
|
||||
- apps/pwa/src/store/calendarStore.ts (eventFormOpen / setEventForm from Task 1)
|
||||
</read_first>
|
||||
<action>
|
||||
Mount `<EventForm />` in CalendarShell (conditionally rendered while `eventFormOpen`). Add a "New Event" entry point: a floating action button (Plus icon, lucide-react) bottom-right on phone and a toolbar button on tablet/desktop, both calling `setEventForm(true, 'create')`. Use the dark neutral primary fill (`--color-text-primary` bg, white label) per UI-SPEC — never an accent color. 44px min touch target. Do not alter existing read-only calendar rendering.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/luc/Projects/familysync && grep -q "EventForm" apps/pwa/src/components/CalendarShell.tsx && grep -q "setEventForm" apps/pwa/src/components/CalendarShell.tsx && pnpm --filter @familysync/pwa test && pnpm --filter @familysync/pwa exec tsc --noEmit</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- CalendarShell mounts EventForm and a "New Event" trigger that opens it in create mode.
|
||||
- Full PWA suite green; tsc --noEmit passes.
|
||||
</acceptance_criteria>
|
||||
<done>A member can open the create form from the calendar; EventForm is mounted and wired to Zustand.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| form input → API | Member-typed event fields cross to the write API |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-03-15 | Tampering | XSS via event title/location/description in the form | mitigate | All values rendered as plain-text JSX children; never dangerouslySetInnerHTML (Phase 2 T-02e-01 pattern); server re-validates with zod (Plan 03) |
|
||||
| T-03-16 | Elevation of Privilege | client offering a non-writable calendar in the picker | mitigate | Picker is populated only from the member's writable set; server enforces D-03 ownership regardless (Plan 03 is authoritative) |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `pnpm --filter @familysync/pwa test` green (EventForm + existing).
|
||||
- `pnpm --filter @familysync/pwa exec tsc --noEmit` passes.
|
||||
- EventForm reachable from CalendarShell; D-02 picker conditional; D-11 recurrence presets present.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- CAL-04 and CAL-07 create paths are user-reachable through EventForm → POST /api/events/create.
|
||||
- Edit mode pre-populates and PATCHes; calendar picker honors D-01/D-02.
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/03-event-write-back-pwa-install/03-05-SUMMARY.md` when done.
|
||||
</output>
|
||||
Reference in New Issue
Block a user