feat(04-03): wire ListsIndex + ListCard + CreateListSheet + ListDeleteDialog (LIST-01)

- listsClient.ts: add createList/patchList/deleteList + List/ListItem types with activeCount/doneCount
- ListsEmptyState.tsx: extracted standalone component (ClipboardList icon, UI-SPEC copy)
- ListCard.tsx: name/count badge/Shared pill/ChevronRight; hover-reveal delete button; navigates /lists/:id
- CreateListSheet.tsx: bottom-sheet/modal; Shared default (D-01); optimistic useMutation; auto-focus; Escape to close
- ListDeleteDialog.tsx: mirrors DeleteConfirmationDialog pattern; props-driven (no calendarStore); XSS guard on name
- ListsIndex.tsx: replaced placeholder with real data via useQuery+useMutation; mounts CreateListSheet+ListDeleteDialog
- DeleteConfirmationDialog.tsx NOT modified (stable, D-06 pattern preserved)
- PWA typecheck passes; DeleteConfirmationDialog.test.tsx 10 passed
- Playwright E2E: create Groceries+Gift Ideas (Shared pills); delete dialog → confirm → card disappears
This commit is contained in:
Lucas Berger
2026-06-09 12:45:19 -04:00
parent 9546b747d2
commit 95dbc663c1
6 changed files with 998 additions and 209 deletions
+34 -2
View File
@@ -4,8 +4,8 @@
* credentials: 'include' is required so the OIDC session cookie is sent with
* every request (same pattern as client.ts).
*
* This file provides only the functions needed in Plan 04-01 (ListsIndex empty
* state). Plans 04-02 and 04-03 expand it with createList, deleteList, item CRUD.
* Exports: fetchLists, createList, patchList, deleteList
* Types: List, ListItem, ListsResponse
*/
const BASE = '/api'
@@ -26,6 +26,10 @@ export interface List {
name: string
isShared: boolean
ownerId: number
activeCount: number
doneCount: number
createdAt?: string
updatedAt?: string
}
export interface ListItem {
@@ -45,3 +49,31 @@ export interface ListsResponse {
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 }>)
}