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 |
|
true |
|
|
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.mdFrom 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;
}
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
addToCollectionModalfrom UIStore (open,globalItemId,globalItemName) - Read
closeAddToCollectionfrom 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 tocategories?.[0]?.id. Since we only haveglobalItemNamein UIStore (not category), skip auto-match for now -- default to first category. - If
!open || !globalItemIdreturn 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")thencloseAddToCollection() - On error: show error message inline
- Reset form state when modal closes via
useEffectwatchingopen
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 />
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.
<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>