/** * Typed API client for the FamilySync lists API. * * credentials: 'include' is required so the OIDC session cookie is sent with * every request (same pattern as client.ts). * * Exports: fetchLists, createList, patchList, deleteList, * fetchListItems, addItem, patchListItem, deleteItem * Types: List, ListItem, ListsResponse, ListItemsResponse */ const BASE = '/api'; async function apiFetch(path: string, init?: RequestInit): Promise { const res = await fetch(`${BASE}${path}`, { credentials: 'include', ...init, }); if (!res.ok) throw new Error(`${init?.method ?? 'GET'} ${path} failed: ${res.status}`); return res; } // ── Types ────────────────────────────────────────────────────────────────── export interface List { id: number; name: string; isShared: boolean; ownerId: number; activeCount: number; doneCount: number; createdAt?: string; updatedAt?: string; } export interface ListItem { id: number; listId: number; text: string; checked: boolean; rank: string; createdAt?: string; updatedAt?: string; } export interface ListsResponse { lists: List[]; } export interface ListItemsResponse { items: ListItem[]; } // ── Functions ────────────────────────────────────────────────────────────── export async function fetchLists(): Promise { return apiFetch('/lists').then((r) => r.json() as Promise); } export async function createList(payload: { name: string; isShared: boolean }): Promise { return apiFetch('/lists', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }).then((r) => r.json() as Promise); } export async function patchList( id: number, patch: { name?: string; isShared?: boolean }, ): Promise { return apiFetch(`/lists/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch), }).then((r) => r.json() as Promise); } export async function deleteList(id: number): Promise<{ id: number }> { return apiFetch(`/lists/${id}`, { method: 'DELETE', }).then((r) => r.json() as Promise<{ id: number }>); } // ── Item functions (LIST-02) ──────────────────────────────────────────────── /** * Fetch all items for a list, ordered by rank ASC. * Returns active and completed items; the UI splits them into sections. */ export async function fetchListItems(listId: number): Promise { return apiFetch(`/lists/${listId}/items`).then((r) => r.json() as Promise); } /** * Add an item to a list. Server assigns the fractional rank (D-13). */ export async function addItem(listId: number, payload: { text: string }): Promise { return apiFetch(`/lists/${listId}/items`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }).then((r) => r.json() as Promise); } /** * Per-field PATCH for a list item (D-08). * Exactly one of: checked, text, or position. * Enforced server-side by zod refine; callers must pass exactly one field. */ export async function patchListItem( itemId: number, patch: { checked: boolean } | { text: string } | { position: string }, ): Promise { return apiFetch(`/list-items/${itemId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch), }).then((r) => r.json() as Promise); } /** * Delete an item instantly — no confirmation (D-06). * Delete-wins semantics (D-09): no rollback in the client after success. */ export async function deleteItem(itemId: number): Promise<{ id: number }> { return apiFetch(`/list-items/${itemId}`, { method: 'DELETE', }).then((r) => r.json() as Promise<{ id: number }>); }