Files

17 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
22-add-from-catalog-thread-integration 01 execute 1
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
true
CATFLOW-03
truths artifacts key_links
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
path provides min_lines
src/client/components/AddToCollectionModal.tsx Add-to-collection confirmation modal 80
path provides contains
src/client/stores/uiStore.ts Modal state slices for addToCollectionModal, addToThreadModal, and catalogSessionThreadId addToCollectionModal
path provides contains
src/client/routes/__root.tsx Toaster and AddToCollectionModal rendered at root Toaster
from to via pattern
src/client/components/CatalogSearchOverlay.tsx src/client/stores/uiStore.ts openAddToCollection call replacing handleAddStub openAddToCollection
from to via pattern
src/client/components/AddToCollectionModal.tsx /api/items useCreateItem mutation with globalItemId useCreateItem
from to via pattern
src/client/routes/global-items/$globalItemId.tsx src/client/stores/uiStore.ts openAddToCollection on button click openAddToCollection
from to via pattern
src/client/stores/uiStore.ts src/client/components/AddToThreadModal.tsx (Plan 02) addToThreadModal state slice and catalogSessionThreadId consumed by Plan 02 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.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:

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:

export function useCreateItem() // mutationFn: (data: CreateItem) => apiPost<ItemWithCategory>("/api/items", data)
// onSuccess invalidates ["items"] and ["totals"]

From src/client/hooks/useCategories.ts:

export function useCategories() // queryKey: ["categories"], returns Category[]

From src/client/stores/uiStore.ts (existing pattern):

catalogSearchOpen: boolean;
catalogSearchMode: "collection" | "thread" | null;
openCatalogSearch: (mode: "collection" | "thread") => void;
closeCatalogSearch: () => void;

From src/client/components/CreateThreadModal.tsx (modal pattern):

// 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:

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:

// 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:

// 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):

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: <p className="text-sm text-gray-500 mb-4">{globalItemName}</p>
  • Category dropdown (per D-02): <select> with categories, same pattern as CreateThreadModal
  • Notes textarea (per D-02): <textarea> optional, placeholder "Personal notes (optional)"
  • Purchase price field (per D-02): <input type="number"> for price in dollars (display), convert to cents on submit by multiplying by 100 and rounding. Placeholder "Purchase price (optional)". Label "Purchase Price ($)".
  • Submit button: "Add to Collection", disabled during isPending
  • Cancel button: calls closeAddToCollection()
  • On submit (per D-03): call createItem.mutate({ name: globalItemName ?? "Unknown Item", categoryId, globalItemId, notes: notes || undefined, purchasePriceCents: purchasePriceCents || undefined })
  • On success (per D-04): call toast.success("Added to Collection") then closeAddToCollection()
  • On error: show error message inline
  • Reset form state when modal closes via useEffect watching open

Import toast from sonner.

Step 4: Add Toaster and AddToCollectionModal to root layout (src/client/routes/__root.tsx)

Add imports:

import { Toaster } from "sonner";
import { AddToCollectionModal } from "../components/AddToCollectionModal";

In the RootLayout JSX, after the <CatalogSearchOverlay /> line (around line 205), add:

<AddToCollectionModal />
<Toaster position="bottom-right" richColors />
cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build 2>&1 | tail -5 && bun test tests/services/item.service.test.ts tests/services/thread.service.test.ts 2>&1 | tail -10 - `src/client/stores/uiStore.ts` contains `addToCollectionModal: { open: boolean; globalItemId: number | null; globalItemName: string | null }` - `src/client/stores/uiStore.ts` contains `openAddToCollection:` - `src/client/stores/uiStore.ts` contains `closeAddToCollection:` - `src/client/stores/uiStore.ts` contains `addToThreadModal:` - `src/client/stores/uiStore.ts` contains `catalogSessionThreadId: number | null` - `src/client/stores/uiStore.ts` closeCatalogSearch resets `catalogSessionThreadId: null` - `src/client/components/AddToCollectionModal.tsx` exists with `export function AddToCollectionModal` - `AddToCollectionModal.tsx` contains `useCreateItem` import - `AddToCollectionModal.tsx` contains `useCategories` import - `AddToCollectionModal.tsx` contains `toast.success("Added to Collection")` - `AddToCollectionModal.tsx` contains `globalItemId` in the mutate call - `AddToCollectionModal.tsx` contains `purchasePriceCents` - `src/client/routes/__root.tsx` contains `import { Toaster } from "sonner"` - `src/client/routes/__root.tsx` contains `` - `src/client/routes/__root.tsx` contains ` UIStore has all Phase 22 modal states (addToCollectionModal, addToThreadModal, catalogSessionThreadId). AddToCollectionModal renders with category dropdown, notes, purchase price. Sonner Toaster is in root layout. Build passes. Service tests pass. Task 2: Wire CatalogSearchOverlay and global item detail page for collection mode src/client/components/CatalogSearchOverlay.tsx src/client/routes/global-items/$globalItemId.tsx src/client/components/CatalogSearchOverlay.tsx src/client/routes/global-items/$globalItemId.tsx src/client/stores/uiStore.ts src/client/components/AddToCollectionModal.tsx **Step 1: Update CatalogSearchOverlay** (`src/client/components/CatalogSearchOverlay.tsx`)

Replace the handleAddStub function (lines 111-114) with a proper handler (per D-16, D-17, D-18):

const openAddToCollection = useUIStore((s) => s.openAddToCollection);
const openAddToThread = useUIStore((s) => s.openAddToThread);

function handleAdd(e: React.MouseEvent, item: { id: number; brand: string; model: string }) {
  e.stopPropagation();
  const itemName = `${item.brand} ${item.model}`;
  if (catalogSearchMode === "collection") {
    openAddToCollection(item.id, itemName);
  } else if (catalogSearchMode === "thread") {
    openAddToThread(item.id, itemName);
  }
}

Update onAdd prop usage in both GridCard and ListRow renders. Currently:

onAdd={handleAddStub}

Change to:

onAdd={(e) => handleAdd(e, item)}

The onAdd prop type on CardProps stays (e: React.MouseEvent) => void -- the item data is captured in the closure.

Step 2: Update global item detail page (src/client/routes/global-items/$globalItemId.tsx)

Add imports:

import { useUIStore } from "../../stores/uiStore";

Inside GlobalItemDetail function, add:

const openAddToCollection = useUIStore((s) => s.openAddToCollection);
const openAddToThread = useUIStore((s) => s.openAddToThread);

Replace the existing "Add to Collection" button (line 131-135) that has console.log:

<div className="flex gap-3 mb-6">
  <button
    type="button"
    onClick={() => openAddToCollection(item.id, `${item.brand} ${item.model}`)}
    className="bg-gray-700 text-white rounded-lg px-5 py-2.5 text-sm font-medium hover:bg-gray-800 transition-colors"
  >
    Add to Collection
  </button>
  <button
    type="button"
    onClick={() => openAddToThread(item.id, `${item.brand} ${item.model}`)}
    className="bg-white text-gray-700 border border-gray-200 rounded-lg px-5 py-2.5 text-sm font-medium hover:bg-gray-50 transition-colors"
  >
    Add to Thread
  </button>
</div>

Per D-14 and D-15: both buttons on the detail page. "Add to Collection" is primary (filled), "Add to Thread" is secondary (outlined). cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build 2>&1 | tail -5 && bun test tests/services/item.service.test.ts tests/services/thread.service.test.ts 2>&1 | tail -10 <acceptance_criteria> - src/client/components/CatalogSearchOverlay.tsx does NOT contain handleAddStub - CatalogSearchOverlay.tsx contains openAddToCollection - CatalogSearchOverlay.tsx contains openAddToThread - CatalogSearchOverlay.tsx contains catalogSearchMode === "collection" - CatalogSearchOverlay.tsx contains catalogSearchMode === "thread" - src/client/routes/global-items/$globalItemId.tsx does NOT contain console.log - $globalItemId.tsx contains openAddToCollection(item.id - $globalItemId.tsx contains openAddToThread(item.id - $globalItemId.tsx contains Add to Thread - $globalItemId.tsx imports useUIStore - bun run build exits with code 0 - bun test tests/services/item.service.test.ts tests/services/thread.service.test.ts passes </acceptance_criteria> CatalogSearchOverlay dispatches to correct modal based on catalogSearchMode. Global item detail page has both "Add to Collection" and "Add to Thread" buttons wired to UIStore. handleAddStub is fully replaced. No console.log stubs remain. Service tests pass.

1. `bun run build` passes with no type errors 2. `bun test tests/services/item.service.test.ts tests/services/thread.service.test.ts` passes 3. In collection mode, clicking Add on catalog card opens AddToCollectionModal 4. Submitting modal with category creates a reference item (POST /api/items with globalItemId) 5. Toast "Added to Collection" appears after successful add 6. Global item detail page shows both "Add to Collection" and "Add to Thread" buttons

<success_criteria>

  • CATFLOW-03 is functional: user can add catalog item to collection with category picker + notes + purchase price
  • AddToCollectionModal creates reference items via existing useCreateItem hook with globalItemId
  • Sonner toast system operational for success feedback
  • Both entry points (catalog search overlay + global item detail page) wire to the modal
  • "Add to Thread" button exists on detail page (wired to UIStore, modal built in Plan 02) </success_criteria>
After completion, create `.planning/phases/22-add-from-catalog-thread-integration/22-01-SUMMARY.md`