👁️
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix undo bug

+45 -24
+2 -4
src/lib/deck-types.ts
··· 112 112 oracleId: OracleId, 113 113 section: Section, 114 114 quantity = 1, 115 + tags: string[] = [], 115 116 ): Deck { 116 117 const existingCard = findCardInSection(deck, scryfallId, section); 117 118 ··· 129 130 130 131 return { 131 132 ...deck, 132 - cards: [ 133 - ...deck.cards, 134 - { scryfallId, oracleId, quantity, section, tags: [] }, 135 - ], 133 + cards: [...deck.cards, { scryfallId, oracleId, quantity, section, tags }], 136 134 updatedAt: new Date().toISOString(), 137 135 }; 138 136 }
+36 -15
src/routes/profile/$did/deck/$rkey/index.tsx
··· 2 2 import { type DragEndEvent, useDndMonitor } from "@dnd-kit/core"; 3 3 import { useSuspenseQuery } from "@tanstack/react-query"; 4 4 import { createFileRoute, Link } from "@tanstack/react-router"; 5 - import { useCallback, useMemo, useState } from "react"; 5 + import { useCallback, useMemo, useRef, useState } from "react"; 6 6 import { ErrorBoundary } from "react-error-boundary"; 7 7 import { toast } from "sonner"; 8 8 import { CardDragOverlay } from "@/components/deck/CardDragOverlay"; ··· 147 147 ); 148 148 const deck = deckRecord.deck; 149 149 150 + // Ref to always have fresh deck state (avoids stale closures in toast callbacks) 151 + const deckRef = useRef(deck); 152 + deckRef.current = deck; 153 + 150 154 const [groupBy, setGroupBy] = usePersistedState<GroupBy>( 151 155 "deckbelcher:viewConfig:groupBy", 152 156 "typeAndTags", ··· 205 209 ); 206 210 207 211 // Helper to update deck via mutation 208 - const updateDeck = async (updater: (prev: Deck) => Deck) => { 209 - if (!isOwner) return; 210 - const updated = updater(deck); 211 - await mutation.mutateAsync(updated); 212 - }; 212 + // Uses deckRef to avoid stale closures in async callbacks (e.g., toast undo) 213 + const updateDeck = useCallback( 214 + async (updater: (prev: Deck) => Deck) => { 215 + if (!isOwner) return; 216 + const updated = updater(deckRef.current); 217 + await mutation.mutateAsync(updated); 218 + }, 219 + [isOwner, mutation], 220 + ); 213 221 214 222 // Highlight cards that were changed - clear after paint so it can trigger again 215 223 const handleCardsChanged = (changedIds: Set<ScryfallId>) => { ··· 288 296 // Update to success with undo action 289 297 toast.success("Card removed from deck", { 290 298 id: toastId, 299 + duration: 10000, 291 300 action: { 292 301 label: "Undo", 293 302 onClick: () => { 294 303 toast.promise( 295 - updateDeck((prev) => ({ 296 - ...prev, 297 - cards: [...prev.cards, cardToDelete], 298 - })), 304 + updateDeck((prev) => 305 + addCardToDeck( 306 + prev, 307 + cardToDelete.scryfallId, 308 + cardToDelete.oracleId, 309 + cardToDelete.section as Section, 310 + cardToDelete.quantity, 311 + cardToDelete.tags ?? [], 312 + ), 313 + ), 299 314 { 300 315 loading: "Undoing...", 301 316 success: "Card restored", ··· 381 396 if (cardToDelete) { 382 397 toast.success("Card removed from deck", { 383 398 id: toastId, 399 + duration: 10000, 384 400 action: { 385 401 label: "Undo", 386 402 onClick: () => { 387 - // Re-insert the exact card that was deleted 388 403 toast.promise( 389 - updateDeck((prev) => ({ 390 - ...prev, 391 - cards: [...prev.cards, cardToDelete], 392 - })), 404 + updateDeck((prev) => 405 + addCardToDeck( 406 + prev, 407 + cardToDelete.scryfallId, 408 + cardToDelete.oracleId, 409 + cardToDelete.section as Section, 410 + cardToDelete.quantity, 411 + cardToDelete.tags ?? [], 412 + ), 413 + ), 393 414 { 394 415 loading: "Undoing...", 395 416 success: "Card restored",
+7 -5
todos.md
··· 8 8 9 9 ## Bugs 10 10 11 - ### Delete undo adds N+1 copies 12 - - **Location**: Deck editor undo logic 13 - - **Issue**: Undoing a card deletion adds N+1 of the card as independent copies instead of restoring the original single entry 14 - - **Repro**: Delete a card with qty 4, undo, observe 5 separate entries 15 - 16 11 ### Bare regex for name search doesn't work 17 12 - **Location**: `src/lib/search/parser.ts`, `parseNameExpr()` 18 13 - **Issue**: `/goblin.*king/i` syntax is parsed but not matched correctly for bare name searches (works in field values like `o:/regex/`) ··· 73 68 ## Refactoring (Technical Debt) 74 69 75 70 ### High Priority 71 + 72 + #### Deck editor: Replace ref with reducer pattern 73 + - **Location**: `src/routes/profile/$did/deck/$rkey/index.tsx` 74 + - **Issue**: Currently uses `deckRef` to avoid stale closures in toast undo callbacks. This is a band-aid fix. 75 + - **Better approach**: Use `useReducer` for local deck state with explicit actions (`ADD_CARD`, `REMOVE_CARD`, etc.) 76 + - **Benefits**: Natural fit for undo/redo (action history), integrates well with Immer, clearer mental model 77 + - **Effort**: Medium 76 78 77 79 #### Reduce computeManaSymbolsVsSources complexity 78 80 - **Location**: `src/lib/deck-stats.ts:327-502` (176 lines)