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.
This commit is contained in:
Lucas Berger
2026-06-11 20:35:18 -04:00
parent 4bc0445173
commit 982438dc10
398 changed files with 19050 additions and 16382 deletions
+76 -81
View File
@@ -26,10 +26,10 @@
* Security: item text rendered as plain-text JSX children (T-04-06 XSS guard).
*/
import { useState } from 'react'
import { useParams, useNavigate } from 'react-router'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { ChevronLeft } from 'lucide-react'
import { useState } from 'react';
import { useParams, useNavigate } from 'react-router';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { ChevronLeft } from 'lucide-react';
import {
DndContext,
closestCenter,
@@ -39,25 +39,20 @@ import {
useSensors,
useSensor,
type DragEndEvent,
} from '@dnd-kit/core'
} from '@dnd-kit/core';
import {
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
} from '@dnd-kit/sortable'
import { generateKeyBetween } from 'fractional-indexing'
import {
fetchListItems,
addItem,
patchListItem,
deleteItem,
} from '../api/listsClient.js'
import { ItemRow } from '../components/ItemRow.js'
import { AddItemInput } from '../components/AddItemInput.js'
import { LiveSyncIndicator } from '../components/LiveSyncIndicator.js'
import { useListSSE } from '../hooks/useListSSE.js'
import type { SyncState } from '../hooks/useListSSE.js'
import type { ListItem, ListItemsResponse } from '../api/listsClient.js'
} from '@dnd-kit/sortable';
import { generateKeyBetween } from 'fractional-indexing';
import { fetchListItems, addItem, patchListItem, deleteItem } from '../api/listsClient.js';
import { ItemRow } from '../components/ItemRow.js';
import { AddItemInput } from '../components/AddItemInput.js';
import { LiveSyncIndicator } from '../components/LiveSyncIndicator.js';
import { useListSSE } from '../hooks/useListSSE.js';
import type { SyncState } from '../hooks/useListSSE.js';
import type { ListItem, ListItemsResponse } from '../api/listsClient.js';
/**
* ListEmptyState — shown inside ListDetail when list has no items.
@@ -97,23 +92,23 @@ function ListEmptyState() {
Add your first item below.
</div>
</div>
)
);
}
export function ListDetail() {
const { listId } = useParams<{ listId: string }>()
const navigate = useNavigate()
const queryClient = useQueryClient()
const parsedListId = Number(listId)
const { listId } = useParams<{ listId: string }>();
const navigate = useNavigate();
const queryClient = useQueryClient();
const parsedListId = Number(listId);
const [completedExpanded, setCompletedExpanded] = useState(true)
const [syncState, setSyncState] = useState<SyncState>('connected')
const [completedExpanded, setCompletedExpanded] = useState(true);
const [syncState, setSyncState] = useState<SyncState>('connected');
// ── Live sync (LIST-04) ──────────────────────────────────────────────────
// Bounded-backoff SSE hook (D-11). On open: invalidates ['list', listId] (D-10).
// On event: invalidates ['list', listId] → background refetch.
// Polling fallback: refetchInterval:30000 below stays always active (D-12).
useListSSE({ listId: parsedListId, onStateChange: setSyncState })
useListSSE({ listId: parsedListId, onStateChange: setSyncState });
// ── dnd-kit sensors ────────────────────────────────────────────────────────
// PointerSensor: desktop mouse — immediate activation
@@ -131,7 +126,7 @@ export function ListDetail() {
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
)
);
// ── Data fetch ─────────────────────────────────────────────────────────────
// refetchInterval: 30000 = D-12 polling fallback (always active; SSE layered in Plan 06)
@@ -140,25 +135,25 @@ export function ListDetail() {
queryFn: () => fetchListItems(parsedListId),
refetchInterval: 30_000,
enabled: !isNaN(parsedListId),
})
});
// Split into active (unchecked, sorted rank ASC) and completed (checked) per D-05
const allItems: ListItem[] = data?.items ?? []
const allItems: ListItem[] = data?.items ?? [];
const activeItems = allItems
.filter((i) => !i.checked)
.sort((a, b) => (a.rank < b.rank ? -1 : a.rank > b.rank ? 1 : 0))
const completedItems = allItems.filter((i) => i.checked)
.sort((a, b) => (a.rank < b.rank ? -1 : a.rank > b.rank ? 1 : 0));
const completedItems = allItems.filter((i) => i.checked);
// ── Add item mutation (D-07 optimistic: append at active bottom, opacity 0.6) ──
const addMutation = useMutation({
mutationFn: (text: string) => addItem(parsedListId, { text }),
onMutate: async (text: string) => {
await queryClient.cancelQueries({ queryKey: ['list', parsedListId] })
const previous = queryClient.getQueryData<ListItemsResponse>(['list', parsedListId])
await queryClient.cancelQueries({ queryKey: ['list', parsedListId] });
const previous = queryClient.getQueryData<ListItemsResponse>(['list', parsedListId]);
// Compute optimistic rank: after the last active item
const lastRank = activeItems.at(-1)?.rank ?? null
const optimisticRank = generateKeyBetween(lastRank, null)
const lastRank = activeItems.at(-1)?.rank ?? null;
const optimisticRank = generateKeyBetween(lastRank, null);
const optimisticItem: ListItem = {
id: -Date.now(), // temporary negative id
@@ -166,111 +161,109 @@ export function ListDetail() {
text,
checked: false,
rank: optimisticRank,
}
};
queryClient.setQueryData<ListItemsResponse>(['list', parsedListId], (old) => ({
items: [...(old?.items ?? []), optimisticItem],
}))
}));
return { previous, optimisticItem }
return { previous, optimisticItem };
},
onError: (_err, _text, context) => {
// Rollback on rejection
if (context?.previous) {
queryClient.setQueryData(['list', parsedListId], context.previous)
queryClient.setQueryData(['list', parsedListId], context.previous);
}
},
onSettled: () => {
// fire-and-forget: cache invalidation; React Query handles the refetch lifecycle
void queryClient.invalidateQueries({ queryKey: ['list', parsedListId] })
void queryClient.invalidateQueries({ queryKey: ['list', parsedListId] });
},
})
});
// ── Check/uncheck mutation (D-07 optimistic: move section immediately, rollback on error) ──
const checkMutation = useMutation({
mutationFn: ({ itemId, checked }: { itemId: number; checked: boolean }) =>
patchListItem(itemId, { checked }),
onMutate: async ({ itemId, checked }) => {
await queryClient.cancelQueries({ queryKey: ['list', parsedListId] })
const previous = queryClient.getQueryData<ListItemsResponse>(['list', parsedListId])
await queryClient.cancelQueries({ queryKey: ['list', parsedListId] });
const previous = queryClient.getQueryData<ListItemsResponse>(['list', parsedListId]);
queryClient.setQueryData<ListItemsResponse>(['list', parsedListId], (old) => ({
items: (old?.items ?? []).map((item) =>
item.id === itemId ? { ...item, checked } : item,
),
}))
items: (old?.items ?? []).map((item) => (item.id === itemId ? { ...item, checked } : item)),
}));
return { previous }
return { previous };
},
onError: (_err, _vars, context) => {
if (context?.previous) {
queryClient.setQueryData(['list', parsedListId], context.previous)
queryClient.setQueryData(['list', parsedListId], context.previous);
}
},
onSettled: () => {
// fire-and-forget: cache invalidation; React Query handles the refetch lifecycle
void queryClient.invalidateQueries({ queryKey: ['list', parsedListId] })
void queryClient.invalidateQueries({ queryKey: ['list', parsedListId] });
},
})
});
// ── Delete item mutation (D-09 delete-wins: NO rollback on error) ──
const deleteMutation = useMutation({
mutationFn: (itemId: number) => deleteItem(itemId),
onMutate: async (itemId: number) => {
await queryClient.cancelQueries({ queryKey: ['list', parsedListId] })
await queryClient.cancelQueries({ queryKey: ['list', parsedListId] });
// Remove item optimistically — no previous state saved (delete-wins D-09)
queryClient.setQueryData<ListItemsResponse>(['list', parsedListId], (old) => ({
items: (old?.items ?? []).filter((item) => item.id !== itemId),
}))
}));
},
// No onError rollback — delete-wins (D-09)
onSettled: () => {
// fire-and-forget: cache invalidation; React Query handles the refetch lifecycle
void queryClient.invalidateQueries({ queryKey: ['list', parsedListId] })
void queryClient.invalidateQueries({ queryKey: ['list', parsedListId] });
},
})
});
// ── Reorder mutation (D-13 single-row rank write, D-15 LWW convergence) ──
const reorderMutation = useMutation({
mutationFn: ({ itemId, position }: { itemId: number; position: string }) =>
patchListItem(itemId, { position }),
onMutate: async ({ itemId, position }) => {
await queryClient.cancelQueries({ queryKey: ['list', parsedListId] })
const previous = queryClient.getQueryData<ListItemsResponse>(['list', parsedListId])
await queryClient.cancelQueries({ queryKey: ['list', parsedListId] });
const previous = queryClient.getQueryData<ListItemsResponse>(['list', parsedListId]);
// Optimistic: update the moved item's rank in cache immediately
queryClient.setQueryData<ListItemsResponse>(['list', parsedListId], (old) => ({
items: (old?.items ?? []).map((item) =>
item.id === itemId ? { ...item, rank: position } : item,
),
}))
}));
return { previous }
return { previous };
},
onError: (_err, _vars, context) => {
// Rollback: animate back to previous order (D-14 — CSS transition in ItemRow handles animation)
if (context?.previous) {
queryClient.setQueryData(['list', parsedListId], context.previous)
queryClient.setQueryData(['list', parsedListId], context.previous);
}
},
onSettled: () => {
// fire-and-forget: cache invalidation; React Query handles the refetch lifecycle
void queryClient.invalidateQueries({ queryKey: ['list', parsedListId] })
void queryClient.invalidateQueries({ queryKey: ['list', parsedListId] });
},
})
});
// ── Handlers ───────────────────────────────────────────────────────────────
function handleAdd(text: string) {
addMutation.mutate(text)
addMutation.mutate(text);
}
function handleCheck(itemId: number, checked: boolean) {
checkMutation.mutate({ itemId, checked })
checkMutation.mutate({ itemId, checked });
}
function handleDelete(itemId: number) {
deleteMutation.mutate(itemId)
deleteMutation.mutate(itemId);
}
/**
@@ -289,29 +282,29 @@ export function ListDetail() {
* via SSE (Plan 06) or the 30s polling fallback (D-12, D-15).
*/
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event
const { active, over } = event;
// No-op: dropped outside or on itself
if (!over || active.id === over.id) return
if (!over || active.id === over.id) return;
// Find source and destination indices in the sorted active list
const activeIndex = activeItems.findIndex((i) => i.id === active.id)
const overIndex = activeItems.findIndex((i) => i.id === over.id)
const activeIndex = activeItems.findIndex((i) => i.id === active.id);
const overIndex = activeItems.findIndex((i) => i.id === over.id);
if (activeIndex === -1 || overIndex === -1) return
if (activeIndex === -1 || overIndex === -1) return;
// Build the new order by moving the item to the target position
const newOrder = [...activeItems]
const [moved] = newOrder.splice(activeIndex, 1)
newOrder.splice(overIndex, 0, moved)
const newOrder = [...activeItems];
const [moved] = newOrder.splice(activeIndex, 1);
newOrder.splice(overIndex, 0, moved);
// Derive prevRank/nextRank from the reordered list at the moved item's new position
const prevRank = newOrder[overIndex - 1]?.rank ?? null
const nextRank = newOrder[overIndex + 1]?.rank ?? null
const prevRank = newOrder[overIndex - 1]?.rank ?? null;
const nextRank = newOrder[overIndex + 1]?.rank ?? null;
const newRank = generateKeyBetween(prevRank, nextRank)
const newRank = generateKeyBetween(prevRank, nextRank);
reorderMutation.mutate({ itemId: Number(active.id), position: newRank })
reorderMutation.mutate({ itemId: Number(active.id), position: newRank });
}
// ── Loading / error states ─────────────────────────────────────────────────
@@ -327,7 +320,7 @@ export function ListDetail() {
>
Invalid list.
</div>
)
);
}
// ── Render ─────────────────────────────────────────────────────────────────
@@ -355,7 +348,9 @@ export function ListDetail() {
}}
>
<button
onClick={() => { void navigate('/lists') }}
onClick={() => {
void navigate('/lists');
}}
aria-label="Back to lists"
style={{
background: 'none',
@@ -501,5 +496,5 @@ export function ListDetail() {
{/* Sticky add-item input */}
<AddItemInput onAdd={handleAdd} isPending={addMutation.isPending} />
</div>
)
);
}