/** * ListsIndex — Lists overview route (/lists). * * UI-SPEC §ListsIndex: * - Full-height scrollable column with 16px horizontal padding * - "Lists" heading + FAB ("+ New List") in top-right corner * - State branches: isLoading → skeleton, isError → error+retry, data → list cards * - Empty state: ListsEmptyState when no lists * - ListCard for each list — navigates to /lists/:id on tap * - ListDeleteDialog for delete confirmation (D-06) * - CreateListSheet for new-list creation (D-01) * * Data flow: * TanStack Query ['lists'] → fetchLists → render list cards or empty state * useMutation(deleteList) → optimistic remove → onError rollback → onSettled invalidate * * T-04-06 XSS guard: all user-supplied strings rendered via ListCard/ListDeleteDialog * as plain-text JSX children — no dangerouslySetInnerHTML anywhere. */ import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router'; import { Plus } from 'lucide-react'; import { fetchLists, deleteList } from '../api/listsClient.js'; import type { List, ListsResponse } from '../api/listsClient.js'; import { useListsStore } from '../store/listsStore.js'; import { ListCard } from '../components/ListCard.js'; import { ListDeleteDialog } from '../components/ListDeleteDialog.js'; import { ListsEmptyState } from '../components/ListsEmptyState.js'; import { CreateListSheet } from '../components/CreateListSheet.js'; // ── Main Component ───────────────────────────────────────────────────────── export function ListsIndex() { const navigate = useNavigate(); const queryClient = useQueryClient(); const setCreateListSheetOpen = useListsStore((s) => s.setCreateListSheetOpen); const [deleteTarget, setDeleteTarget] = useState(null); const { data, isLoading, isError, refetch } = useQuery({ queryKey: ['lists'], queryFn: fetchLists, retry: 2, staleTime: 30 * 1000, }); const lists = data?.lists ?? []; // Delete mutation with optimistic removal const deleteMutation = useMutation({ mutationFn: (listId: number) => deleteList(listId), onMutate: async (listId) => { await queryClient.cancelQueries({ queryKey: ['lists'] }); const previous = queryClient.getQueryData(['lists']); queryClient.setQueryData(['lists'], (old) => ({ lists: (old?.lists ?? []).filter((l) => l.id !== listId), })); return { previous }; }, onError: (_err, _vars, context) => { if (context?.previous) { queryClient.setQueryData(['lists'], context.previous); } // TODO: surface "Couldn't delete. Try again." toast (Plan 06 / notification layer) }, onSettled: () => { // fire-and-forget: cache invalidation; React Query handles the refetch lifecycle void queryClient.invalidateQueries({ queryKey: ['lists'] }); }, onSuccess: () => { void navigate('/lists'); setDeleteTarget(null); }, }); const handleDeleteRequest = (list: List) => { setDeleteTarget(list); }; const handleDeleteConfirm = () => { if (!deleteTarget) return; deleteMutation.mutate(deleteTarget.id); }; const handleDeleteClose = () => { setDeleteTarget(null); }; return ( <>
{/* Header */}

{/* Plain text — XSS guard */} Lists

{/* FAB / inline button — opens CreateListSheet */}
{/* Content area */}
{/* Loading state */} {isLoading && (
{/* Plain text — XSS guard */} Loading lists…
)} {/* Error state */} {isError && !isLoading && (
{/* Plain text — XSS guard */} Could not load lists
)} {/* Empty state */} {!isLoading && !isError && lists.length === 0 && } {/* List cards */} {!isLoading && !isError && lists.length > 0 && (
{lists.map((list) => ( ))}
)}
{/* Delete confirmation dialog (D-06) */} {/* Create list sheet (D-01) */} ); }