feat(03-05): add write client calls and eventForm Zustand keys
- createEvent(payload): POST /api/events/create, credentials:include, returns {uid}
- updateEvent(uid, payload): PATCH /api/events/:uid/edit
- fetchWritableCalendars(): GET /api/events/writable-calendars, returns calendars array (D-03 server-authoritative)
- Exported interfaces: CreateEventPayload, CreateEventResponse, WritableCalendar, RecurrencePreset
- calendarStore: eventFormOpen (bool), eventFormMode ('create'|'edit'), eventFormUid (string|null)
- setEventForm(open, mode?, uid?) setter with correct defaults
This commit is contained in:
@@ -103,3 +103,108 @@ export async function fetchEvents(
|
|||||||
|
|
||||||
// Phase 1 legacy types (CalendarEvent, EventsResponse, fetchEventsLegacy) removed in Plan 05
|
// Phase 1 legacy types (CalendarEvent, EventsResponse, fetchEventsLegacy) removed in Plan 05
|
||||||
// when the Phase 1 broker-proof component was retired.
|
// when the Phase 1 broker-proof component was retired.
|
||||||
|
|
||||||
|
// ── /api/events (write — Plan 03-05) ─────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recurrence presets supported by the EventForm.
|
||||||
|
* Maps 1:1 to the RRULE frequency values the API accepts.
|
||||||
|
*/
|
||||||
|
export type RecurrencePreset = 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload for creating or updating a calendar event.
|
||||||
|
* Mirrors the Zod schema on POST /api/events/create and PATCH /api/events/:uid/edit.
|
||||||
|
*/
|
||||||
|
export interface CreateEventPayload {
|
||||||
|
title: string
|
||||||
|
allDay: boolean
|
||||||
|
start: string // 'YYYY-MM-DD' for allDay; ISO 8601 for timed
|
||||||
|
end: string // same format as start
|
||||||
|
recurrence: RecurrencePreset
|
||||||
|
location?: string
|
||||||
|
description?: string
|
||||||
|
calendarUrl?: string // omit to use the member's default writable calendar (D-01)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Response from POST /api/events/create and PATCH /api/events/:uid/edit */
|
||||||
|
export interface CreateEventResponse {
|
||||||
|
uid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A writable calendar returned by GET /api/events/writable-calendars.
|
||||||
|
* The server is the authoritative source of the writable set (D-03).
|
||||||
|
* The client never derives writability — it reads this endpoint verbatim.
|
||||||
|
*/
|
||||||
|
export interface WritableCalendar {
|
||||||
|
url: string
|
||||||
|
displayName: string
|
||||||
|
color: string
|
||||||
|
isShared: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new calendar event.
|
||||||
|
*
|
||||||
|
* POSTs to /api/events/create and returns immediately with 202 + uid.
|
||||||
|
* The server enqueues the write to CalDAV asynchronously (D-05/D-12).
|
||||||
|
*/
|
||||||
|
export async function createEvent(payload: CreateEventPayload): Promise<CreateEventResponse> {
|
||||||
|
const res = await fetch('/api/events/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`POST /api/events/create failed: ${res.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json() as Promise<CreateEventResponse>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing calendar event.
|
||||||
|
*
|
||||||
|
* PATCHes /api/events/:uid/edit with the updated payload.
|
||||||
|
* Returns 202 + uid; the write is enqueued asynchronously (D-05).
|
||||||
|
*/
|
||||||
|
export async function updateEvent(
|
||||||
|
uid: string,
|
||||||
|
payload: CreateEventPayload,
|
||||||
|
): Promise<CreateEventResponse> {
|
||||||
|
const res = await fetch(`/api/events/${uid}/edit`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`PATCH /api/events/${uid}/edit failed: ${res.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json() as Promise<CreateEventResponse>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the authoritative list of writable calendars for the current member.
|
||||||
|
*
|
||||||
|
* The server owns the D-03 writable set (WHERE userId=currentUser.id OR isShared=1).
|
||||||
|
* The client NEVER derives writability — it reads this endpoint verbatim.
|
||||||
|
* Drives the calendar picker visibility (D-02: hidden when only 1 writable calendar).
|
||||||
|
*/
|
||||||
|
export async function fetchWritableCalendars(): Promise<WritableCalendar[]> {
|
||||||
|
const res = await fetch('/api/events/writable-calendars', {
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`GET /api/events/writable-calendars failed: ${res.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = (await res.json()) as { calendars: WritableCalendar[] }
|
||||||
|
return body.calendars
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,10 +35,27 @@ export interface CalendarStore {
|
|||||||
openEventId: string | null
|
openEventId: string | null
|
||||||
calendarRange: { start: string; end: string }
|
calendarRange: { start: string; end: string }
|
||||||
|
|
||||||
|
// ── EventForm UI state (Plan 03-05) ──────────────────────────────────────
|
||||||
|
// Server state (events, calendars) lives in TanStack Query. These are pure
|
||||||
|
// UI-shape keys: whether the form is open, what mode it is in, and which
|
||||||
|
// event UID to pre-populate in edit mode.
|
||||||
|
eventFormOpen: boolean
|
||||||
|
eventFormMode: 'create' | 'edit'
|
||||||
|
eventFormUid: string | null
|
||||||
|
|
||||||
setSelectedView: (view: string) => void
|
setSelectedView: (view: string) => void
|
||||||
setSelectedDate: (date: string) => void
|
setSelectedDate: (date: string) => void
|
||||||
setOpenEventId: (id: string | null) => void
|
setOpenEventId: (id: string | null) => void
|
||||||
setCalendarRange: (range: { start: string; end: string }) => void
|
setCalendarRange: (range: { start: string; end: string }) => void
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open or close the EventForm.
|
||||||
|
*
|
||||||
|
* @param open true to open, false to close
|
||||||
|
* @param mode 'create' (default) or 'edit'
|
||||||
|
* @param uid UID of the event to pre-populate in edit mode (null otherwise)
|
||||||
|
*/
|
||||||
|
setEventForm: (open: boolean, mode?: 'create' | 'edit', uid?: string | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||||
@@ -93,6 +110,11 @@ export const useCalendarStore = create<CalendarStore>((set) => ({
|
|||||||
openEventId: null,
|
openEventId: null,
|
||||||
calendarRange: initialCalendarRange(),
|
calendarRange: initialCalendarRange(),
|
||||||
|
|
||||||
|
// EventForm UI state — closed by default, create mode, no pre-fill UID
|
||||||
|
eventFormOpen: false,
|
||||||
|
eventFormMode: 'create',
|
||||||
|
eventFormUid: null,
|
||||||
|
|
||||||
setSelectedView: (view: string) => {
|
setSelectedView: (view: string) => {
|
||||||
set({ selectedView: view })
|
set({ selectedView: view })
|
||||||
// Persist to localStorage keyed by breakpoint group
|
// Persist to localStorage keyed by breakpoint group
|
||||||
@@ -110,4 +132,7 @@ export const useCalendarStore = create<CalendarStore>((set) => ({
|
|||||||
|
|
||||||
setCalendarRange: (range: { start: string; end: string }) =>
|
setCalendarRange: (range: { start: string; end: string }) =>
|
||||||
set({ calendarRange: range }),
|
set({ calendarRange: range }),
|
||||||
|
|
||||||
|
setEventForm: (open: boolean, mode: 'create' | 'edit' = 'create', uid: string | null = null) =>
|
||||||
|
set({ eventFormOpen: open, eventFormMode: mode, eventFormUid: uid }),
|
||||||
}))
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user