- ColorLegend: per-member color swatches (12px circle, label) + always-visible Family row - AppNav: phone 48px top bar (avatar with aria-label/title) + tablet/desktop 240px sidebar with ColorLegend - ViewToolbar: Today/prev/next + Day/Week/Month/Agenda view switcher; 44px min-height; active state uses surface tint not accent - SkeletonCalendar: shimmer month (6x7 grid) and agenda (4 date-group blocks) variants; aria-busy=true - EmptyState: CalendarDays icon + 'Nothing here' heading + body copy per UI-SPEC - CalendarShell: full phone/desktop layout with AppNav + ViewToolbar + ColorLegend chrome - CalendarShell: state branches — loading→SkeletonCalendar, empty→EmptyState, error→'Couldn't load events' + Retry button (refetchQueries) - EventProof.tsx deleted; legacy types removed from client.ts - CalendarShell.test.tsx: updated to waitFor ScheduleXCalendar after data loads - All 36 tests pass, tsc clean, vite build clean (490kB)
100 lines
3.4 KiB
TypeScript
100 lines
3.4 KiB
TypeScript
/**
|
|
* Typed API client for the FamilySync backend.
|
|
*
|
|
* credentials: 'include' is required so the OIDC session cookie is sent with
|
|
* every cross-origin request (Vite dev proxy routes to :3000; production is
|
|
* same-origin via Pangolin).
|
|
*
|
|
* 401 responses mean the session has expired — the browser will follow the
|
|
* 302 redirect to Authelia on the next API call automatically (full-page nav).
|
|
*/
|
|
|
|
// ── /api/me ────────────────────────────────────────────────────────────────
|
|
|
|
export interface MeUser {
|
|
id: number
|
|
displayName: string | null
|
|
color: string
|
|
}
|
|
|
|
export interface MeResponse {
|
|
user: MeUser
|
|
}
|
|
|
|
export async function fetchMe(): Promise<MeResponse> {
|
|
const res = await fetch('/api/me', {
|
|
credentials: 'include',
|
|
})
|
|
|
|
if (!res.ok) {
|
|
// 302 → browser follows redirect to Authelia automatically.
|
|
// For 4xx/5xx, throw so React Query can surface the error.
|
|
throw new Error(`GET /api/me failed: ${res.status}`)
|
|
}
|
|
|
|
return res.json() as Promise<MeResponse>
|
|
}
|
|
|
|
// ── /api/events (windowed — Phase 2) ─────────────────────────────────────
|
|
|
|
/**
|
|
* A single concrete occurrence of a calendar event.
|
|
*
|
|
* Mirrors the CalendarOccurrence shape produced by the backend's
|
|
* expandOccurrences() helper (apps/api/src/broker/expand.ts).
|
|
*
|
|
* calendarId and ownerUserId are both present:
|
|
* calendarId — DB calendar-row id (do NOT use for Schedule-X routing)
|
|
* ownerUserId — DB user id (use for Schedule-X calendarId routing)
|
|
* isShared — true when this event belongs to the shared-family calendar
|
|
*
|
|
* hydrateEvents() uses isShared/ownerUserId — never String(calendarId) —
|
|
* to build the Schedule-X calendarId that keys into buildCalendarConfig().
|
|
*/
|
|
export interface CalendarOccurrence {
|
|
id: string // `${uid}::${dtstart_iso}` — stable identity
|
|
uid: string
|
|
calendarId: number // DB calendar-row id — do NOT use for SX calendarId routing
|
|
calendarName: string
|
|
ownerUserId: number // DB user id — the correct Schedule-X routing key
|
|
color: string // hex from users.color or shared-family constant
|
|
isShared: boolean // true → 'shared' slot; false → String(ownerUserId) slot
|
|
title: string
|
|
start: string // 'YYYY-MM-DD' for allDay:true; ISO 8601 with IANA tz for timed
|
|
end: string
|
|
allDay: boolean
|
|
location: string | null
|
|
description: string | null
|
|
}
|
|
|
|
export interface OccurrencesResponse {
|
|
occurrences: CalendarOccurrence[]
|
|
}
|
|
|
|
/**
|
|
* Fetch windowed calendar occurrences.
|
|
*
|
|
* The ?start=&end= window is mandatory — an unwindowed call would expand 500+
|
|
* cached events with all their recurring occurrences (RESEARCH.md Pitfall 5).
|
|
*
|
|
* @param start ISO date string 'YYYY-MM-DD' — window start (inclusive)
|
|
* @param end ISO date string 'YYYY-MM-DD' — window end (exclusive)
|
|
*/
|
|
export async function fetchEvents(
|
|
start: string,
|
|
end: string,
|
|
): Promise<OccurrencesResponse> {
|
|
const res = await fetch(`/api/events?start=${start}&end=${end}`, {
|
|
credentials: 'include',
|
|
})
|
|
|
|
if (!res.ok) {
|
|
throw new Error(`GET /api/events failed: ${res.status}`)
|
|
}
|
|
|
|
return res.json() as Promise<OccurrencesResponse>
|
|
}
|
|
|
|
// Phase 1 legacy types (CalendarEvent, EventsResponse, fetchEventsLegacy) removed in Plan 05
|
|
// when the Phase 1 broker-proof component was retired.
|