feat(04-04): add ListDetail with active/completed split + ItemRow + AddItemInput (LIST-02)
- listsClient.ts: add fetchListItems, addItem, patchListItem, deleteItem + ListItemsResponse type - ListDetail.tsx: replace placeholder with real implementation — useQuery(['list', listId]) with 30s polling fallback (D-12); active/completed split (D-05); optimistic mutations (D-07); delete-wins no-rollback (D-09); per-field check PATCH (D-08) - ItemRow.tsx: 44px touch target, checkbox (20px visual/44px touch, accent fill when checked), plain-text item text (T-04-06 XSS guard), GripVertical handle slot for Plan 05, hover Trash2 delete + swipe-left zone, transform 150ms ease-out animation slot (D-14) - AddItemInput.tsx: sticky bottom input + Add button, disabled when empty, Enter key support - ListDetail.test.tsx: 7 real tests replacing todo stubs — optimistic add/check/uncheck/delete, rollback on error, D-05 completed-sink split, D-09 delete-wins no-rollback - Playwright browser check: add milk → sinks to Completed on check → vanishes on delete PASS
This commit is contained in:
@@ -4,8 +4,9 @@
|
||||
* credentials: 'include' is required so the OIDC session cookie is sent with
|
||||
* every request (same pattern as client.ts).
|
||||
*
|
||||
* Exports: fetchLists, createList, patchList, deleteList
|
||||
* Types: List, ListItem, ListsResponse
|
||||
* Exports: fetchLists, createList, patchList, deleteList,
|
||||
* fetchListItems, addItem, patchListItem, deleteItem
|
||||
* Types: List, ListItem, ListsResponse, ListItemsResponse
|
||||
*/
|
||||
|
||||
const BASE = '/api'
|
||||
@@ -38,12 +39,18 @@ export interface ListItem {
|
||||
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> {
|
||||
@@ -77,3 +84,55 @@ export async function deleteList(id: number): Promise<{ id: number }> {
|
||||
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 }>)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user