Files
familysync/apps/pwa/src/api/listsClient.ts
T
Lucas Berger 982438dc10 style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
2026-06-11 20:35:18 -04:00

131 lines
4.1 KiB
TypeScript

/**
* 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<Response> {
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<ListsResponse> {
return apiFetch('/lists').then((r) => r.json() as Promise<ListsResponse>);
}
export async function createList(payload: { name: string; isShared: boolean }): Promise<List> {
return apiFetch('/lists', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}).then((r) => r.json() as Promise<List>);
}
export async function patchList(
id: number,
patch: { name?: string; isShared?: boolean },
): Promise<List> {
return apiFetch(`/lists/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch),
}).then((r) => r.json() as Promise<List>);
}
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<ListItemsResponse> {
return apiFetch(`/lists/${listId}/items`).then((r) => r.json() as Promise<ListItemsResponse>);
}
/**
* Add an item to a list. Server assigns the fractional rank (D-13).
*/
export async function addItem(listId: number, payload: { text: string }): Promise<ListItem> {
return apiFetch(`/lists/${listId}/items`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}).then((r) => r.json() as Promise<ListItem>);
}
/**
* 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<ListItem> {
return apiFetch(`/list-items/${itemId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch),
}).then((r) => r.json() as Promise<ListItem>);
}
/**
* 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 }>);
}