--- phase: 22-add-from-catalog-thread-integration plan: 01 type: execute wave: 1 depends_on: [] files_modified: - src/client/stores/uiStore.ts - src/client/components/AddToCollectionModal.tsx - src/client/components/CatalogSearchOverlay.tsx - src/client/routes/global-items/$globalItemId.tsx - src/client/routes/__root.tsx autonomous: true requirements: [CATFLOW-03] must_haves: truths: - "Clicking Add on a catalog search card in collection mode opens the AddToCollectionModal" - "AddToCollectionModal shows category dropdown, optional notes, optional purchase price, and submit/cancel buttons" - "Submitting the modal creates a reference item with globalItemId and personal fields" - "Success toast appears after adding item to collection" - "Clicking Add to Collection on the global item detail page opens the same modal" artifacts: - path: "src/client/components/AddToCollectionModal.tsx" provides: "Add-to-collection confirmation modal" min_lines: 80 - path: "src/client/stores/uiStore.ts" provides: "Modal state slices for addToCollectionModal, addToThreadModal, and catalogSessionThreadId" contains: "addToCollectionModal" - path: "src/client/routes/__root.tsx" provides: "Toaster and AddToCollectionModal rendered at root" contains: "Toaster" key_links: - from: "src/client/components/CatalogSearchOverlay.tsx" to: "src/client/stores/uiStore.ts" via: "openAddToCollection call replacing handleAddStub" pattern: "openAddToCollection" - from: "src/client/components/AddToCollectionModal.tsx" to: "/api/items" via: "useCreateItem mutation with globalItemId" pattern: "useCreateItem" - from: "src/client/routes/global-items/$globalItemId.tsx" to: "src/client/stores/uiStore.ts" via: "openAddToCollection on button click" pattern: "openAddToCollection" - from: "src/client/stores/uiStore.ts" to: "src/client/components/AddToThreadModal.tsx (Plan 02)" via: "addToThreadModal state slice and catalogSessionThreadId consumed by Plan 02" pattern: "addToThreadModal|catalogSessionThreadId" --- Wire the add-to-collection flow: install sonner for toasts, extend UIStore with modal states, build the AddToCollectionModal component, and replace the stub handler in CatalogSearchOverlay and global item detail page for collection mode. Purpose: CATFLOW-03 -- users can add catalog items to their collection as reference items with personal fields (category, notes, purchase price). Output: Working add-to-collection flow from both catalog search and global item detail page. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/22-add-from-catalog-thread-integration/22-CONTEXT.md @.planning/phases/22-add-from-catalog-thread-integration/22-RESEARCH.md From src/shared/schemas.ts: ```typescript export const createItemSchema = z.object({ name: z.string().min(1, "Name is required"), weightGrams: z.number().nonnegative().optional(), priceCents: z.number().int().nonnegative().optional(), categoryId: z.number().int().positive(), notes: z.string().optional(), productUrl: z.string().url().optional().or(z.literal("")), imageFilename: z.string().optional(), imageSourceUrl: z.string().url().optional().or(z.literal("")), quantity: z.number().int().positive().optional(), globalItemId: z.number().int().positive().optional(), purchasePriceCents: z.number().int().nonnegative().optional(), }); ``` From src/client/hooks/useItems.ts: ```typescript export function useCreateItem() // mutationFn: (data: CreateItem) => apiPost("/api/items", data) // onSuccess invalidates ["items"] and ["totals"] ``` From src/client/hooks/useCategories.ts: ```typescript export function useCategories() // queryKey: ["categories"], returns Category[] ``` From src/client/stores/uiStore.ts (existing pattern): ```typescript catalogSearchOpen: boolean; catalogSearchMode: "collection" | "thread" | null; openCatalogSearch: (mode: "collection" | "thread") => void; closeCatalogSearch: () => void; ``` From src/client/components/CreateThreadModal.tsx (modal pattern): ```typescript // Modal pattern: fixed inset-0 z-50, bg-black/50 backdrop, onClick={handleClose} // Inner div: w-full max-w-md bg-white rounded-xl shadow-xl p-6 // Form with local state, UIStore for open/close, mutation hook for submit ``` From CatalogSearchOverlay.tsx CardProps: ```typescript interface CardProps { item: { id: number; brand: string; model: string; category: string | null; weightGrams: number | null; priceCents: number | null; imageUrl: string | null; }; onAdd: (e: React.MouseEvent) => void; onCardClick: () => void; weight: (g: number) => string; price: (cents: number) => string; } ``` Task 1: UIStore extension + sonner setup + AddToCollectionModal src/client/stores/uiStore.ts src/client/components/AddToCollectionModal.tsx src/client/routes/__root.tsx src/client/stores/uiStore.ts src/client/components/CreateThreadModal.tsx src/client/hooks/useItems.ts src/client/hooks/useCategories.ts src/shared/schemas.ts src/client/routes/__root.tsx **Step 1: Install sonner** ```bash bun add sonner ``` **Step 2: Extend UIStore** (`src/client/stores/uiStore.ts`) Add to the `UIState` interface: ```typescript // Add-to-collection modal (per D-20) addToCollectionModal: { open: boolean; globalItemId: number | null; globalItemName: string | null }; openAddToCollection: (globalItemId: number, globalItemName: string) => void; closeAddToCollection: () => void; // Add-to-thread modal (per D-21) addToThreadModal: { open: boolean; globalItemId: number | null; globalItemName: string | null }; openAddToThread: (globalItemId: number, globalItemName: string) => void; closeAddToThread: () => void; // Session thread tracking (per D-22) catalogSessionThreadId: number | null; setCatalogSessionThreadId: (id: number | null) => void; ``` Add to the `create` implementation: ```typescript // Add-to-collection modal addToCollectionModal: { open: false, globalItemId: null, globalItemName: null }, openAddToCollection: (globalItemId, globalItemName) => set({ addToCollectionModal: { open: true, globalItemId, globalItemName } }), closeAddToCollection: () => set({ addToCollectionModal: { open: false, globalItemId: null, globalItemName: null } }), // Add-to-thread modal addToThreadModal: { open: false, globalItemId: null, globalItemName: null }, openAddToThread: (globalItemId, globalItemName) => set({ addToThreadModal: { open: true, globalItemId, globalItemName } }), closeAddToThread: () => set({ addToThreadModal: { open: false, globalItemId: null, globalItemName: null } }), // Session thread tracking catalogSessionThreadId: null, setCatalogSessionThreadId: (id) => set({ catalogSessionThreadId: id }), ``` Also update `closeCatalogSearch` to reset session thread (per D-22): ```typescript closeCatalogSearch: () => set({ catalogSearchOpen: false, catalogSearchMode: null, catalogSessionThreadId: null }), ``` **Step 3: Create AddToCollectionModal** (`src/client/components/AddToCollectionModal.tsx`) Follow CreateThreadModal pattern exactly (per D-05). Component structure: - Read `addToCollectionModal` from UIStore (`open`, `globalItemId`, `globalItemName`) - Read `closeAddToCollection` from UIStore - Use `useCategories()` for category dropdown - Use `useCreateItem()` for the mutation - Local state: `categoryId` (number | null), `notes` (string), `purchasePriceCents` (number | undefined) - Auto-match category: when categories load, find category where `c.name.toLowerCase() === globalItemCategory?.toLowerCase()`, fall back to `categories?.[0]?.id`. Since we only have `globalItemName` in UIStore (not category), skip auto-match for now -- default to first category. - If `!open || !globalItemId` return null - Render modal: `fixed inset-0 z-50 flex items-center justify-center bg-black/50` - Inner form: `w-full max-w-md bg-white rounded-xl shadow-xl p-6` - Header: "Add to Collection" h2 - Show item name as a subheading: `

{globalItemName}

` - Category dropdown (per D-02): `