diff --git a/src/components/board/board.tsx b/src/components/board/board.tsx index 964522c..0241af4 100644 --- a/src/components/board/board.tsx +++ b/src/components/board/board.tsx @@ -92,7 +92,18 @@ export function Board({ const [cards, setCards] = useState(initialCards); const [activeId, setActiveId] = useState(null); - // A client manager gets no sensors, so @dnd-kit never starts a drag. + /** + * True from the moment a card is dropped until the server confirms the move. + * + * Separate from `activeId` on purpose. `activeId` is cleared the instant the + * pointer is released, so gating on it alone leaves a window where a focus + * event could refresh the page and overwrite the optimistic position with + * stale server state — the card would appear to snap back. + */ + const [saving, setSaving] = useState(false); + + // A client manager never renders DndContext at all (see the early return + // below), so these sensors are only ever reached by the owner. const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 4 } }) ); @@ -107,11 +118,13 @@ export function Board({ const activeCard = activeId ? cards.find((c) => c.id === activeId) : null; - useRefreshOnFocus(activeId !== null); + const busy = activeId !== null || saving; + + useRefreshOnFocus(busy); useEffect(() => { - if (activeId === null) setCards(initialCards); - }, [initialCards, activeId]); + if (!busy) setCards(initialCards); + }, [initialCards, busy]); function onDragStart(event: DragStartEvent) { setActiveId(String(event.active.id)); @@ -160,19 +173,27 @@ export function Board({ c.id === card.id ? { ...c, column: destinationColumnId, position } : c ); setCards(optimistic); + setSaving(true); - // Through the server route, not the browser SDK: the auth token lives in - // an httpOnly cookie that JavaScript cannot read, so the browser-side - // PocketBase instance has no credentials. See Task 10. - const response = await fetch(`/api/owner/cards/${card.id}/move`, { - method: "PATCH", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ column: destinationColumnId, position }), - }); + try { + // Through the server route, not the browser SDK: the auth token lives in + // an httpOnly cookie that JavaScript cannot read, so the browser-side + // PocketBase instance has no credentials. See Task 10. + const response = await fetch(`/api/owner/cards/${card.id}/move`, { + method: "PATCH", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ column: destinationColumnId, position }), + }); - if (!response.ok) { - console.error("[board] move failed", response.status); + if (!response.ok) { + console.error("[board] move failed", response.status); + setCards(previous); + } + } catch (error) { + console.error("[board] move failed", error); setCards(previous); + } finally { + setSaving(false); } }