diff --git a/.planning/phases/21-item-catalog-detail-pages/21-01-PLAN.md b/.planning/phases/21-item-catalog-detail-pages/21-01-PLAN.md new file mode 100644 index 0000000..4e08c31 --- /dev/null +++ b/.planning/phases/21-item-catalog-detail-pages/21-01-PLAN.md @@ -0,0 +1,241 @@ +--- +phase: 21-item-catalog-detail-pages +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/client/routes/items/$itemId.tsx + - src/client/routes/global-items/$globalItemId.tsx +autonomous: true +requirements: [DETAIL-01, DETAIL-02, DETAIL-03] +must_haves: + truths: + - "Navigating to /items/:id renders a full detail page with all item data" + - "Item detail page shows hero image, name, weight/price/category badges, notes, quantity, purchase price, product link" + - "Clicking Edit toggles fields to editable inputs; Save persists via updateItem mutation" + - "Navigating to /global-items/:id shows enhanced catalog page with Add to Collection button" + - "Catalog detail page shows hero image, brand, model, specs, description, owner count" + artifacts: + - path: "src/client/routes/items/$itemId.tsx" + provides: "Private item detail page with edit mode" + exports: ["Route"] + - path: "src/client/routes/global-items/$globalItemId.tsx" + provides: "Enhanced catalog detail page with Add to Collection button" + exports: ["Route"] + key_links: + - from: "src/client/routes/items/$itemId.tsx" + to: "useItem hook" + via: "useItem(Number(itemId))" + pattern: "useItem\\(Number" + - from: "src/client/routes/items/$itemId.tsx" + to: "useUpdateItem mutation" + via: "updateItem.mutate" + pattern: "useUpdateItem" + - from: "src/client/routes/global-items/$globalItemId.tsx" + to: "Add to Collection button" + via: "button element" + pattern: "Add to Collection" +--- + + +Create the private item detail page at `/items/:id` with edit mode toggle, and enhance the existing catalog detail page at `/global-items/:id` with an "Add to Collection" button and improved layout. + +Purpose: These are the navigation targets that card click handlers will point to in Plan 03. They must exist before rewiring. +Output: Two route files — one new (`items/$itemId.tsx`), one enhanced (`global-items/$globalItemId.tsx`) + + + +@$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/21-item-catalog-detail-pages/21-CONTEXT.md +@.planning/phases/21-item-catalog-detail-pages/21-RESEARCH.md + +@src/client/routes/global-items/$globalItemId.tsx +@src/client/hooks/useItems.ts +@src/client/hooks/useGlobalItems.ts +@src/client/hooks/useFormatters.ts +@src/client/components/ItemForm.tsx +@src/client/components/CategoryPicker.tsx +@src/client/components/ImageUpload.tsx + + + + +From src/client/hooks/useItems.ts: +```typescript +export function useItem(id: number | null): UseQueryResult; +export function useUpdateItem(): UseMutationResult>; +export function useDeleteItem(): UseMutationResult<{ success: boolean }, Error, number>; +export function useDuplicateItem(): UseMutationResult; + +interface ItemWithCategory { + id: number; name: string; weightGrams: number | null; priceCents: number | null; + quantity: number; categoryId: number; notes: string | null; productUrl: string | null; + imageFilename: string | null; createdAt: string; updatedAt: string; + categoryName: string; categoryIcon: string; +} +``` + +From src/client/hooks/useGlobalItems.ts: +```typescript +export function useGlobalItem(id: number): UseQueryResult; +// GlobalItemWithDetails includes: id, brand, model, description, category, weightGrams, priceCents, imageUrl, ownerCount +``` + +From src/client/hooks/useFormatters.ts: +```typescript +export function useFormatters(): { weight: (g: number) => string; price: (cents: number) => string; }; +``` + +From src/client/components/ItemForm.tsx: +```typescript +interface FormData { + name: string; weightGrams: string; priceDollars: string; quantity: number; + categoryId: number; notes: string; productUrl: string; imageFilename: string | null; +} +``` + + + + + + + Task 1: Create private item detail page with edit mode toggle + src/client/routes/items/$itemId.tsx + + - src/client/routes/global-items/$globalItemId.tsx (existing detail page pattern to follow) + - src/client/components/ItemForm.tsx (form fields, validation logic, FormData interface to reuse) + - src/client/hooks/useItems.ts (useItem, useUpdateItem, useDeleteItem, useDuplicateItem hooks) + - src/client/hooks/useFormatters.ts (weight/price formatting) + - src/client/components/CategoryPicker.tsx (category selection for edit mode) + - src/client/components/ImageUpload.tsx (image upload for edit mode) + - src/client/stores/uiStore.ts (openConfirmDelete for delete action, openExternalLink for product links) + + + Create `src/client/routes/items/$itemId.tsx` with `createFileRoute("/items/$itemId")`. + + Per D-01: Route at `/items/:id`, full page showing all item data. + Per D-02: Hero section — large product image (or placeholder icon using LucideIcon with categoryIcon), item name as h1 title, key specs as badges (weight via `useFormatters().weight`, price via `useFormatters().price`, category name). + Per D-03: Personal section below hero — notes (rendered as paragraph), quantity (if > 1), purchase price (priceCents formatted), product link (as external link button), image. For reference items, COALESCE merge is transparent — useItem already returns merged data, no special handling needed. + Per D-05: Page is read-only by default. Clean aesthetic, spacious gallery-like layout. + Per D-06: Back navigation at top — `← Back to collection`. + Per D-07: Actions section — "Edit" button (top-right), "Duplicate" button (uses `useDuplicateItem`), "Delete" button (uses `useUIStore.openConfirmDelete` which triggers the existing ConfirmDialog). + + Per D-04: Edit mode toggle. Local `useState(false)` for `isEditing`. When "Edit" clicked, `setIsEditing(true)`. In edit mode: + - Name becomes text input + - Weight becomes number input (display in grams, store as string like ItemForm does) + - Price becomes dollar input (convert cents to dollars for display, back to cents on save) + - Quantity becomes number input + - Category becomes CategoryPicker component + - Notes becomes textarea + - Product URL becomes text input + - Image becomes ImageUpload component + - Show Save and Cancel buttons. Save calls `useUpdateItem().mutate({ id, name, weightGrams: parsed, priceCents: parsed, quantity, categoryId, notes, productUrl, imageFilename })` with `onSuccess: () => setIsEditing(false)`. Cancel calls `setIsEditing(false)`. + - Initialize form state from item data when entering edit mode (in the toggle handler or via useEffect when isEditing becomes true AND item data is available). + + Layout: `max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-6` (matches existing global item detail page). + Loading state: shimmer placeholders (same pattern as `$globalItemId.tsx`). + Error state: "Item not found" with back link. + Image placeholder when no image: centered LucideIcon with categoryIcon, bg-gray-50 container. + + Do NOT import from UIStore for panel state (openEditPanel, closePanel, etc.) — this page replaces the panel. + + + cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint 2>&1 | tail -5 + + + - grep -q "createFileRoute.*items.*itemId" src/client/routes/items/\$itemId.tsx + - grep -q "useItem" src/client/routes/items/\$itemId.tsx + - grep -q "useUpdateItem" src/client/routes/items/\$itemId.tsx + - grep -q "isEditing" src/client/routes/items/\$itemId.tsx + - grep -q "Back to collection" src/client/routes/items/\$itemId.tsx + - grep -q "useDuplicateItem\|useDeleteItem" src/client/routes/items/\$itemId.tsx + - grep -q "CategoryPicker" src/client/routes/items/\$itemId.tsx + - grep -q "ImageUpload" src/client/routes/items/\$itemId.tsx + + + - `/items/:id` route renders item detail with hero image, name, badges, personal fields + - Edit button toggles between read-only and editable states + - Save persists changes via useUpdateItem and exits edit mode + - Cancel exits edit mode without saving + - Duplicate and Delete actions work via existing hooks/dialogs + - Back link navigates to /collection + - Loading and error states handled + + + + + Task 2: Enhance catalog detail page with Add to Collection button and improved layout + src/client/routes/global-items/$globalItemId.tsx + + - src/client/routes/global-items/$globalItemId.tsx (current implementation to enhance) + - src/client/hooks/useGlobalItems.ts (useGlobalItem hook) + - src/client/routes/items/$itemId.tsx (just created — follow same layout proportions) + + + Enhance the existing `src/client/routes/global-items/$globalItemId.tsx` file. + + Per D-08: Enhance existing route — add "Add to Collection" button and improve layout. + Per D-09: Layout improvements: + - Hero image section: if `item.imageUrl` exists, show in `aspect-[16/9] bg-gray-50 rounded-xl overflow-hidden` (already exists). If no image, show a placeholder div with a package icon. + - Brand as small uppercase label above model title (already exists). + - Model as h1 title (already exists). + - Manufacturer specs as badges: weight, price, category (already exists). + - Description section (already exists). + - Owner count badge (already exists). + + Per D-10: "Add to Collection" button — prominent placement after the header/badges section. Styled as `bg-gray-700 text-white rounded-lg px-5 py-2.5 text-sm font-medium hover:bg-gray-800 transition-colors`. This is a STUB — onClick shows a console.log("Add to collection — wired in Phase 22") or a brief toast/alert. The actual flow is Phase 22. Match the same stub pattern as the Add button in CatalogSearchOverlay. + + Per D-11: No edit functionality on this page. Read-only display only. + Per D-12: This page is accessible from catalog search overlay — no changes needed here (overlay card click wiring happens in Plan 03). + + Specific enhancements to the existing page: + 1. Add image placeholder when no imageUrl (currently the image section is skipped entirely — add a placeholder div) + 2. Add "Add to Collection" button between badges and description + 3. Ensure layout is consistent with the new item detail page from Task 1 (same max-width, padding, spacing) + 4. Add tags display if the item has tags (optional, Claude's discretion — subtle tag chips below badges) + + + cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint 2>&1 | tail -5 + + + - grep -q "Add to Collection" src/client/routes/global-items/\$globalItemId.tsx + - grep -q "button" src/client/routes/global-items/\$globalItemId.tsx + - grep -q "createFileRoute.*global-items.*globalItemId" src/client/routes/global-items/\$globalItemId.tsx + + + - Catalog detail page shows "Add to Collection" button (stub, not wired to actual add flow) + - Image placeholder shown when no image exists + - Layout is clean and consistent with item detail page styling + - All existing functionality preserved (back link, specs, owner count) + + + + + + +- `bun run lint` passes with no errors +- `bun run dev` starts and both routes render correctly: + - Navigate to `/items/1` — shows item detail with edit toggle + - Navigate to `/global-items/1` — shows catalog detail with Add to Collection button +- Edit mode on item detail: clicking Edit shows form fields, Save persists, Cancel reverts + + + +- Item detail page at `/items/:id` renders all item data in gallery-like layout +- Edit mode toggle works: read-only by default, editable when toggled +- Catalog detail page shows "Add to Collection" button (stub) +- Both pages follow consistent layout patterns (max-w-3xl, same spacing) +- Lint passes cleanly + + + +After completion, create `.planning/phases/21-item-catalog-detail-pages/21-01-SUMMARY.md` + diff --git a/.planning/phases/21-item-catalog-detail-pages/21-02-PLAN.md b/.planning/phases/21-item-catalog-detail-pages/21-02-PLAN.md new file mode 100644 index 0000000..8a353f4 --- /dev/null +++ b/.planning/phases/21-item-catalog-detail-pages/21-02-PLAN.md @@ -0,0 +1,239 @@ +--- +phase: 21-item-catalog-detail-pages +plan: 02 +type: execute +wave: 1 +depends_on: [] +files_modified: + - src/client/routes/threads/$threadId/index.tsx + - src/client/routes/threads/$threadId/candidates/$candidateId.tsx +autonomous: true +requirements: [DETAIL-04] +must_haves: + truths: + - "Navigating to /threads/:threadId/candidates/:candidateId renders a full candidate detail page" + - "Candidate detail page shows name, weight, price, notes, pros, cons, status, product link, image" + - "Edit mode toggle works: read-only by default, editable inputs when toggled" + - "Back navigation returns to parent thread" + - "Pick as winner and delete candidate actions are available" + - "Thread detail page at /threads/:threadId still works after route restructuring" + - "Add candidate button on thread page uses modal dialog instead of slide-out panel" + artifacts: + - path: "src/client/routes/threads/$threadId/index.tsx" + provides: "Restructured thread detail page (moved from $threadId.tsx)" + - path: "src/client/routes/threads/$threadId/candidates/$candidateId.tsx" + provides: "Candidate detail page with edit mode" + exports: ["Route"] + key_links: + - from: "src/client/routes/threads/$threadId/candidates/$candidateId.tsx" + to: "useThread hook" + via: "useThread(threadId) to get candidate data" + pattern: "useThread" + - from: "src/client/routes/threads/$threadId/candidates/$candidateId.tsx" + to: "useUpdateCandidate mutation" + via: "updateCandidate.mutate" + pattern: "useUpdateCandidate" +--- + + +Create the candidate detail page at `/threads/:threadId/candidates/:candidateId` with edit mode toggle, and restructure the thread route to support nested candidate routes. Replace the "Add Candidate" slide-out trigger on the thread page with a modal dialog. + +Purpose: Creates the navigation target for candidate card clicks (rewired in Plan 03). The thread route restructuring is a prerequisite for the nested candidate route. +Output: Restructured thread route directory + new candidate detail page + add-candidate modal on thread 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/21-item-catalog-detail-pages/21-CONTEXT.md +@.planning/phases/21-item-catalog-detail-pages/21-RESEARCH.md + +@src/client/routes/threads/$threadId.tsx +@src/client/hooks/useCandidates.ts +@src/client/hooks/useThreads.ts +@src/client/components/CandidateForm.tsx +@src/client/components/CandidateCard.tsx +@src/client/components/CandidateListItem.tsx +@src/client/stores/uiStore.ts + + + + +From src/client/hooks/useCandidates.ts: +```typescript +export function useCreateCandidate(threadId: number): UseMutationResult; +export function useUpdateCandidate(threadId: number): UseMutationResult; +export function useDeleteCandidate(threadId: number): UseMutationResult<{ success: boolean }, Error, number>; +``` + +From src/client/hooks/useThreads.ts: +```typescript +export function useThread(id: number): UseQueryResult; +export function useResolveThread(): UseMutationResult; +// ThreadWithCandidates.candidates[]: { id, threadId, name, weightGrams, priceCents, categoryId, categoryName, categoryIcon, notes, productUrl, imageFilename, imageUrl, status, pros, cons, sortOrder, createdAt, updatedAt } +``` + +From src/client/components/CandidateForm.tsx: +```typescript +interface CandidateFormProps { mode: "add" | "edit"; threadId: number; candidateId?: number | null; } +interface FormData { + name: string; weightGrams: string; priceDollars: string; categoryId: number; + notes: string; productUrl: string; imageFilename: string | null; pros: string; cons: string; +} +``` + +From src/client/stores/uiStore.ts (properties used by thread page): +```typescript +openCandidateAddPanel: () => void; // WILL BE REMOVED in Plan 03 +openResolveDialog: (threadId: number, candidateId: number) => void; // KEEP +openConfirmDeleteCandidate: (id: number) => void; // KEEP +``` + + + + + + + Task 1: Restructure thread route and create candidate detail page + + src/client/routes/threads/$threadId.tsx + src/client/routes/threads/$threadId/index.tsx + src/client/routes/threads/$threadId/candidates/$candidateId.tsx + + + - src/client/routes/threads/$threadId.tsx (full file — must move to index.tsx intact) + - src/client/hooks/useCandidates.ts (useUpdateCandidate, useDeleteCandidate hooks) + - src/client/hooks/useThreads.ts (useThread — returns ThreadWithCandidates with candidates array) + - src/client/components/CandidateForm.tsx (form fields and FormData interface to reuse for edit mode) + - src/client/components/CategoryPicker.tsx (for edit mode category picker) + - src/client/components/ImageUpload.tsx (for edit mode image upload) + - src/client/routes/global-items/$globalItemId.tsx (layout pattern reference) + - src/client/stores/uiStore.ts (openResolveDialog, openConfirmDeleteCandidate — keep using these) + + + **Step 1: Restructure thread route directory.** + - Move `src/client/routes/threads/$threadId.tsx` to `src/client/routes/threads/$threadId/index.tsx` + - Delete the original `$threadId.tsx` file + - The route path `/threads/$threadId` continues to work via the index.tsx convention + - IMPORTANT: Do NOT manually edit `routeTree.gen.ts` — it auto-regenerates + + **Step 2: Create candidate detail page.** + Create `src/client/routes/threads/$threadId/candidates/$candidateId.tsx` with `createFileRoute("/threads/$threadId/candidates/$candidateId")`. + + Per D-13: Shows candidate data — name, weight, price, notes, pros, cons, status, product link, image. + Per D-14: Edit mode toggle — same pattern as item detail page (local `useState(false)`). In edit mode: + - Name, weight (grams string), price (dollars string), category (CategoryPicker), notes (textarea), product URL (text input), image (ImageUpload), pros (textarea), cons (textarea) + - Save button calls `useUpdateCandidate(threadId).mutate({ candidateId, name, weightGrams: parsed, priceCents: parsed, categoryId, notes, productUrl, imageFilename, pros, cons })` with `onSuccess: () => setIsEditing(false)` + - Cancel resets and exits edit mode + - Initialize form from candidate data when entering edit mode + + Per D-15: Back navigation — `← Back to thread`. + Per D-16: Thread-specific actions: + - "Pick as winner" button (only if thread is active/not resolved) — calls `useUIStore.openResolveDialog(threadId, candidateId)` which triggers the existing ResolveDialog in __root.tsx + - "Delete candidate" button — calls `useUIStore.openConfirmDeleteCandidate(candidateId)` which triggers the existing CandidateDeleteDialog in __root.tsx + + Data fetching: Use `useThread(Number(threadIdParam))` to get the thread, then find the candidate via `thread.candidates.find(c => c.id === Number(candidateIdParam))`. This avoids needing a new API endpoint. + + Layout: Same gallery-like pattern as item detail — `max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-6`. Hero image section, name as h1, badges (weight, price, category, status), pros/cons sections, notes section. + + Loading state: shimmer placeholders. + Error state: "Candidate not found" with back link to thread. + + Status display: Show current status using the StatusBadge component (but read-only — status changes happen via the existing status cycle on the card, not on the detail page). + + + cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint 2>&1 | tail -5 + + + - test -f src/client/routes/threads/\$threadId/index.tsx + - test ! -f src/client/routes/threads/\$threadId.tsx + - grep -q "createFileRoute.*threads.*threadId.*candidates.*candidateId" src/client/routes/threads/\$threadId/candidates/\$candidateId.tsx + - grep -q "useThread" src/client/routes/threads/\$threadId/candidates/\$candidateId.tsx + - grep -q "useUpdateCandidate" src/client/routes/threads/\$threadId/candidates/\$candidateId.tsx + - grep -q "isEditing" src/client/routes/threads/\$threadId/candidates/\$candidateId.tsx + - grep -q "Back to thread" src/client/routes/threads/\$threadId/candidates/\$candidateId.tsx + - grep -q "openResolveDialog\|Pick as winner" src/client/routes/threads/\$threadId/candidates/\$candidateId.tsx + + + - Thread route restructured: `$threadId.tsx` moved to `$threadId/index.tsx`, `/threads/:threadId` still works + - Candidate detail page renders at `/threads/:threadId/candidates/:candidateId` + - Shows all candidate data: name, weight, price, notes, pros, cons, status, image + - Edit mode toggle works with form fields for all editable properties + - Back link returns to parent thread + - Pick as winner and delete actions available via existing dialogs + + + + + Task 2: Add candidate modal dialog on thread page + + src/client/routes/threads/$threadId/index.tsx + + + - src/client/routes/threads/$threadId/index.tsx (just restructured — find the "Add Candidate" button) + - src/client/components/CandidateForm.tsx (form fields to replicate in modal) + - src/client/hooks/useCandidates.ts (useCreateCandidate hook) + - src/client/components/CategoryPicker.tsx (for category field) + - src/client/components/ImageUpload.tsx (for image field) + + + Per D-28: Replace the `openCandidateAddPanel()` call on the thread page with a local modal dialog for adding candidates. + + In `src/client/routes/threads/$threadId/index.tsx`: + 1. Add local state: `const [addCandidateOpen, setAddCandidateOpen] = useState(false)` + 2. Replace the existing "Add Candidate" button's `onClick={() => openCandidateAddPanel()}` with `onClick={() => setAddCandidateOpen(true)}` + 3. Remove the `openCandidateAddPanel` import from useUIStore + 4. Add an `AddCandidateModal` component (can be defined in the same file or as a separate component — Claude's discretion) that: + - Renders as a fixed overlay with backdrop (`fixed inset-0 z-50 flex items-center justify-center`) + - Contains a form with fields: name (required), weight (grams), price (dollars), category (CategoryPicker), notes, product URL, image (ImageUpload), pros, cons + - Uses `useCreateCandidate(threadId)` for submission + - On success: closes modal, form resets + - On cancel/backdrop click: closes modal + - Style consistent with the existing CandidateDeleteDialog and ResolveDialog patterns in `__root.tsx` + 5. Render the modal conditionally: `{addCandidateOpen && setAddCandidateOpen(false)} />}` + + The modal should be a reasonable size (`max-w-lg`) with scrollable content if needed. Form fields should match CandidateForm's field set. Validation: name is required (show inline error if empty on submit). + + + cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint 2>&1 | tail -5 + + + - grep -q "addCandidateOpen\|AddCandidateModal" src/client/routes/threads/\$threadId/index.tsx + - grep -q "useCreateCandidate" src/client/routes/threads/\$threadId/index.tsx + - grep -qv "openCandidateAddPanel" src/client/routes/threads/\$threadId/index.tsx + + + - Thread page "Add Candidate" button opens a modal dialog instead of a slide-out panel + - Modal contains all candidate form fields (name, weight, price, category, notes, URL, image, pros, cons) + - Creating a candidate via the modal works and refreshes the thread data + - The `openCandidateAddPanel` UIStore call is no longer used on the thread page + + + + + + +- `bun run lint` passes with no errors +- `bun run dev` starts and routes work: + - `/threads/:threadId` renders thread detail (unchanged behavior after restructuring) + - `/threads/:threadId/candidates/:candidateId` renders candidate detail with edit toggle + - "Add Candidate" button on thread page opens modal, candidate creation works + + + +- Thread route restructured without breaking existing `/threads/:threadId` functionality +- Candidate detail page at `/threads/:threadId/candidates/:candidateId` with edit mode +- Add candidate modal replaces slide-out panel trigger on thread page +- All existing thread page functionality preserved (reorder, view modes, impact deltas) +- Lint passes cleanly + + + +After completion, create `.planning/phases/21-item-catalog-detail-pages/21-02-SUMMARY.md` + diff --git a/.planning/phases/21-item-catalog-detail-pages/21-03-PLAN.md b/.planning/phases/21-item-catalog-detail-pages/21-03-PLAN.md new file mode 100644 index 0000000..803047b --- /dev/null +++ b/.planning/phases/21-item-catalog-detail-pages/21-03-PLAN.md @@ -0,0 +1,331 @@ +--- +phase: 21-item-catalog-detail-pages +plan: 03 +type: execute +wave: 2 +depends_on: [21-01, 21-02] +files_modified: + - src/client/components/ItemCard.tsx + - src/client/components/CandidateCard.tsx + - src/client/components/CandidateListItem.tsx + - src/client/components/CatalogSearchOverlay.tsx + - src/client/routes/__root.tsx + - src/client/stores/uiStore.ts +autonomous: true +requirements: [DETAIL-04, DETAIL-05] +must_haves: + truths: + - "Clicking an ItemCard navigates to /items/:id instead of opening a slide-out panel" + - "Clicking a CandidateCard navigates to /threads/:threadId/candidates/:candidateId" + - "Clicking a CandidateListItem navigates to the candidate detail page" + - "Clicking a catalog search result card navigates to /global-items/:id" + - "Item slide-out panel is removed from __root.tsx" + - "Candidate slide-out panel is removed from __root.tsx" + - "Panel-related state is removed from UIStore" + - "SlideOutPanel.tsx, ItemForm.tsx, and CandidateForm.tsx files still exist" + artifacts: + - path: "src/client/components/ItemCard.tsx" + provides: "ItemCard with navigation instead of panel open" + - path: "src/client/components/CandidateCard.tsx" + provides: "CandidateCard with navigation instead of panel open" + - path: "src/client/components/CandidateListItem.tsx" + provides: "CandidateListItem with navigation instead of panel open" + - path: "src/client/routes/__root.tsx" + provides: "Root layout without slide-out panels" + - path: "src/client/stores/uiStore.ts" + provides: "UIStore without panel state properties" + key_links: + - from: "src/client/components/ItemCard.tsx" + to: "/items/$itemId route" + via: "useNavigate → /items/$itemId" + pattern: "navigate.*items.*itemId" + - from: "src/client/components/CandidateCard.tsx" + to: "/threads/$threadId/candidates/$candidateId route" + via: "useNavigate → /threads/$threadId/candidates/$candidateId" + pattern: "navigate.*threads.*candidates" + - from: "src/client/components/CatalogSearchOverlay.tsx" + to: "/global-items/$globalItemId route" + via: "useNavigate → /global-items/$globalItemId" + pattern: "navigate.*global-items" +--- + + +Rewire all card click handlers to navigate to detail pages instead of opening slide-out panels, then remove the slide-out panels from the root layout and clean up the UIStore. + +Purpose: Completes the transition from panel-based editing to full detail pages. This is the final step — navigation targets from Plans 01 and 02 must exist first. +Output: All cards navigate to detail pages, panels removed, UIStore cleaned + + + +@$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/21-item-catalog-detail-pages/21-CONTEXT.md +@.planning/phases/21-item-catalog-detail-pages/21-RESEARCH.md + +@src/client/components/ItemCard.tsx +@src/client/components/CandidateCard.tsx +@src/client/components/CandidateListItem.tsx +@src/client/components/CatalogSearchOverlay.tsx +@src/client/routes/__root.tsx +@src/client/stores/uiStore.ts + + +@.planning/phases/21-item-catalog-detail-pages/21-01-SUMMARY.md +@.planning/phases/21-item-catalog-detail-pages/21-02-SUMMARY.md + + + + +ItemCard currently uses: +```typescript +const openEditPanel = useUIStore((s) => s.openEditPanel); +onClick={() => openEditPanel(id)} +// Also: duplicateItem onSuccess calls openEditPanel(newItem.id) +``` + +CandidateCard currently uses: +```typescript +const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel); +onClick={() => openCandidateEditPanel(id)} +// Props include: threadId (available for navigation) +``` + +CandidateListItem currently uses: +```typescript +const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel); +onClick={() => openCandidateEditPanel(candidate.id)} +// candidate.threadId is available +``` + +__root.tsx panel instances (lines 189-221): +```typescript + + + + + + +``` + +UIStore properties to REMOVE: +```typescript +panelMode, editingItemId, openAddPanel, openEditPanel, closePanel +candidatePanelMode, editingCandidateId, openCandidateAddPanel, openCandidateEditPanel, closeCandidatePanel +``` + +UIStore properties to KEEP: +```typescript +confirmDeleteItemId, openConfirmDelete, closeConfirmDelete +confirmDeleteCandidateId, openConfirmDeleteCandidate, closeConfirmDeleteCandidate +resolveThreadId, resolveCandidateId, openResolveDialog, closeResolveDialog +// all other non-panel state +``` + + + + + + + Task 1: Rewire card click handlers to navigate to detail pages + + src/client/components/ItemCard.tsx + src/client/components/CandidateCard.tsx + src/client/components/CandidateListItem.tsx + src/client/components/CatalogSearchOverlay.tsx + + + - src/client/components/ItemCard.tsx (full file — find openEditPanel usage) + - src/client/components/CandidateCard.tsx (full file — find openCandidateEditPanel usage) + - src/client/components/CandidateListItem.tsx (full file — find openCandidateEditPanel usage) + - src/client/components/CatalogSearchOverlay.tsx (full file — find card rendering, add click navigation) + - src/client/stores/uiStore.ts (verify which store properties each component uses) + + + Per D-17: **ItemCard.tsx** — Replace panel open with navigation. + 1. Remove: `const openEditPanel = useUIStore((s) => s.openEditPanel);` + 2. Add: `import { useNavigate } from "@tanstack/react-router";` and `const navigate = useNavigate();` + 3. Change main button `onClick`: `() => navigate({ to: "/items/$itemId", params: { itemId: String(id) } })` + 4. Change duplicate onSuccess: `(newItem) => navigate({ to: "/items/$itemId", params: { itemId: String(newItem.id) } })` (navigate to the new item's detail page instead of opening edit panel) + 5. Keep all other UIStore usage (openExternalLink, etc.) + 6. If the component no longer imports anything from useUIStore, remove the import entirely. If it still uses openExternalLink, keep the import. + + Per D-18: **CandidateCard.tsx** — Replace panel open with navigation. + 1. Remove: `const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel);` + 2. Add: `import { useNavigate } from "@tanstack/react-router";` and `const navigate = useNavigate();` + 3. Change main button `onClick`: `() => navigate({ to: "/threads/$threadId/candidates/$candidateId", params: { threadId: String(threadId), candidateId: String(id) } })` + 4. Keep: openConfirmDeleteCandidate, openResolveDialog, openExternalLink from UIStore. + + Per D-20: **CandidateListItem.tsx** — Replace panel open with navigation. + 1. Remove: `const openCandidateEditPanel = useUIStore((s) => s.openCandidateEditPanel);` + 2. Add: `import { useNavigate } from "@tanstack/react-router";` and `const navigate = useNavigate();` + 3. Change the click handler: `() => navigate({ to: "/threads/$threadId/candidates/$candidateId", params: { threadId: String(candidate.threadId), candidateId: String(candidate.id) } })` + 4. Keep all other UIStore usage. + + Per D-19: **CatalogSearchOverlay.tsx** — Add card click navigation to `/global-items/:id`. + 1. Add: `import { useNavigate } from "@tanstack/react-router";` and `const navigate = useNavigate();` + 2. Find the card/grid item rendering for search results. Wrap the card body (not the "Add" button) in a clickable element that: + - Calls `closeCatalogSearch()` then `navigate({ to: "/global-items/$globalItemId", params: { globalItemId: String(item.id) } })` + - The existing "Add" button stays separate with `e.stopPropagation()` to prevent navigation when clicking Add + 3. The card body should have `cursor-pointer` and hover styling to indicate clickability + + + cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint 2>&1 | tail -5 + + + - grep -q "useNavigate" src/client/components/ItemCard.tsx + - grep -q "items.*itemId" src/client/components/ItemCard.tsx + - grep -qv "openEditPanel" src/client/components/ItemCard.tsx + - grep -q "useNavigate" src/client/components/CandidateCard.tsx + - grep -q "threads.*candidates" src/client/components/CandidateCard.tsx + - grep -qv "openCandidateEditPanel" src/client/components/CandidateCard.tsx + - grep -q "useNavigate" src/client/components/CandidateListItem.tsx + - grep -qv "openCandidateEditPanel" src/client/components/CandidateListItem.tsx + - grep -q "useNavigate" src/client/components/CatalogSearchOverlay.tsx + - grep -q "global-items" src/client/components/CatalogSearchOverlay.tsx + + + - ItemCard click navigates to /items/:id + - CandidateCard click navigates to /threads/:threadId/candidates/:candidateId + - CandidateListItem click navigates to candidate detail page + - Catalog search result card click navigates to /global-items/:id (closing overlay first) + - "Add" button on catalog cards still works independently + - No component references openEditPanel or openCandidateEditPanel + + + + + Task 2: Remove slide-out panels from root layout and clean UIStore + + src/client/routes/__root.tsx + src/client/stores/uiStore.ts + + + - src/client/routes/__root.tsx (full file — identify panel JSX to remove, imports to clean) + - src/client/stores/uiStore.ts (full file — identify panel state to remove) + - src/client/components/ItemCard.tsx (verify openEditPanel no longer imported — done in Task 1) + - src/client/components/CandidateCard.tsx (verify openCandidateEditPanel no longer imported) + - src/client/routes/threads/$threadId/index.tsx (verify openCandidateAddPanel no longer imported — done in Plan 02) + + + Per D-21, D-22: **__root.tsx** — Remove slide-out panel instances. + 1. Remove the Item SlideOutPanel block (lines ~189-199 in current file): + ``` + + {panelMode === "add" && } + {panelMode === "edit" && } + + ``` + 2. Remove the Candidate SlideOutPanel block (lines ~202-221 in current file): + ``` + {currentThreadId != null && ( + + + + )} + ``` + 3. Remove all panel-related state reads from the component: + - `const panelMode = useUIStore((s) => s.panelMode);` + - `const editingItemId = useUIStore((s) => s.editingItemId);` + - `const closePanel = useUIStore((s) => s.closePanel);` + - `const candidatePanelMode = useUIStore((s) => s.candidatePanelMode);` + - `const editingCandidateId = useUIStore((s) => s.editingCandidateId);` + - `const closeCandidatePanel = useUIStore((s) => s.closeCandidatePanel);` + - `const isItemPanelOpen = panelMode !== "closed";` + - `const isCandidatePanelOpen = candidatePanelMode !== "closed";` + 4. Remove unused imports: `SlideOutPanel`, `ItemForm`, `CandidateForm` (from the import block) + 5. Per D-25: Keep the `SlideOutPanel.tsx` component FILE — just remove its usage from __root.tsx + 6. Per D-26: Keep `ItemForm.tsx` and `CandidateForm.tsx` files — just remove their import from __root.tsx + 7. The `currentThreadId` variable may still be needed by CandidateDeleteDialog and ResolveDialog — check if it's still referenced. If those dialogs use it, keep the `threadMatch` and `currentThreadId` logic. If not, remove. + + Per D-23, D-24: **uiStore.ts** — Remove panel state. + Remove from UIState interface AND implementation: + - `panelMode: "closed" | "add" | "edit"` + - `editingItemId: number | null` + - `openAddPanel: () => void` + - `openEditPanel: (itemId: number) => void` + - `closePanel: () => void` + - `candidatePanelMode: "closed" | "add" | "edit"` + - `editingCandidateId: number | null` + - `openCandidateAddPanel: () => void` + - `openCandidateEditPanel: (id: number) => void` + - `closeCandidatePanel: () => void` + + KEEP all other state: + - confirmDeleteItemId, openConfirmDelete, closeConfirmDelete + - confirmDeleteCandidateId, openConfirmDeleteCandidate, closeConfirmDeleteCandidate + - resolveThreadId, resolveCandidateId, openResolveDialog, closeResolveDialog + - itemPickerOpen, openItemPicker, closeItemPicker + - confirmDeleteSetupId, openConfirmDeleteSetup, closeConfirmDeleteSetup + - createThreadModalOpen, openCreateThreadModal, closeCreateThreadModal + - externalLinkUrl, openExternalLink, closeExternalLink + - candidateViewMode, setCandidateViewMode + - selectedSetupId, setSelectedSetupId + - fabMenuOpen, openFabMenu, closeFabMenu + - catalogSearchOpen, catalogSearchMode, openCatalogSearch, closeCatalogSearch + + After cleanup, do a project-wide search for any remaining references to the removed properties: + `grep -r "openEditPanel\|openAddPanel\|closePanel\|openCandidateEditPanel\|openCandidateAddPanel\|closeCandidatePanel\|panelMode\|editingItemId\|editingCandidateId\|candidatePanelMode" src/client/ --include="*.tsx" --include="*.ts"` + If any references remain, update those files to remove the dead references (likely just removing unused imports or store selectors). + + + cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint 2>&1 | tail -5 + + + - grep -qv "SlideOutPanel" src/client/routes/__root.tsx + - grep -qv "ItemForm" src/client/routes/__root.tsx + - grep -qv "CandidateForm" src/client/routes/__root.tsx + - grep -qv "panelMode" src/client/routes/__root.tsx + - grep -qv "openEditPanel" src/client/stores/uiStore.ts + - grep -qv "openAddPanel" src/client/stores/uiStore.ts + - grep -qv "openCandidateEditPanel" src/client/stores/uiStore.ts + - grep -qv "openCandidateAddPanel" src/client/stores/uiStore.ts + - grep -qv "candidatePanelMode" src/client/stores/uiStore.ts + - grep -q "openConfirmDelete" src/client/stores/uiStore.ts + - grep -q "openResolveDialog" src/client/stores/uiStore.ts + - test -f src/client/components/SlideOutPanel.tsx + - test -f src/client/components/ItemForm.tsx + - test -f src/client/components/CandidateForm.tsx + + + - Item and candidate SlideOutPanel instances removed from __root.tsx + - SlideOutPanel, ItemForm, CandidateForm imports removed from __root.tsx + - Panel-related state (panelMode, editingItemId, candidatePanelMode, editingCandidateId) removed from UIStore + - Panel-related actions (openEditPanel, openAddPanel, closePanel, openCandidateEditPanel, openCandidateAddPanel, closeCandidatePanel) removed from UIStore + - Non-panel state preserved (confirm delete, resolve dialog, FAB, catalog search, etc.) + - SlideOutPanel.tsx, ItemForm.tsx, CandidateForm.tsx files still exist on disk + - No remaining references to removed properties anywhere in src/client/ + - Lint passes cleanly + + + + + + +- `bun run lint` passes with no errors +- `bun run dev` starts and the full flow works: + - Click an item card → navigates to `/items/:id` detail page + - Click a candidate card → navigates to `/threads/:threadId/candidates/:candidateId` + - Click a catalog search result → closes overlay, navigates to `/global-items/:id` + - No slide-out panels appear anywhere in the app + - Confirm delete, resolve dialog, FAB menu, catalog search overlay all still work +- `grep -r "openEditPanel\|openCandidateEditPanel\|panelMode\|candidatePanelMode" src/client/ --include="*.tsx" --include="*.ts"` returns no results + + + +- All card components navigate to detail pages instead of opening panels +- Slide-out panels completely removed from the application +- UIStore cleaned of all panel-related state +- All non-panel functionality preserved (dialogs, FAB, catalog search) +- No broken references or dead imports +- Lint passes cleanly + + + +After completion, create `.planning/phases/21-item-catalog-detail-pages/21-03-SUMMARY.md` + diff --git a/.planning/phases/21-item-catalog-detail-pages/21-CONTEXT.md b/.planning/phases/21-item-catalog-detail-pages/21-CONTEXT.md new file mode 100644 index 0000000..6f72970 --- /dev/null +++ b/.planning/phases/21-item-catalog-detail-pages/21-CONTEXT.md @@ -0,0 +1,151 @@ +# Phase 21: Item & Catalog Detail Pages - Context + +**Gathered:** 2026-04-06 +**Status:** Ready for planning + + +## Phase Boundary + +Create full detail pages for collection items (`/items/:id`) and enhance the existing catalog detail page (`/global-items/:id`). Remove slide-out panels for items and candidates from the root layout. Add edit mode toggle on private detail pages. Thread candidates get their own detail route. No visual distinction between reference items and standalone items — same layout, some fields may be empty. + + + + +## Implementation Decisions + +### Private Item Detail Page (`/items/:id`) +- **D-01:** New route at `/items/:id` — full page showing all item data (name, weight, price, category, notes, product link, image, quantity, purchase price). +- **D-02:** Hero section at top — large product image (or placeholder), item name as title, key specs as badges (weight, price, category). +- **D-03:** Personal section below — notes, quantity, purchase price, product link. For reference items, this data comes from the COALESCE merge (transparent, no visual distinction). +- **D-04:** Edit mode toggle — "Edit" button in top-right corner. When toggled, fields become editable inputs (same styling as current ItemForm). Save/Cancel buttons appear. Uses existing `updateItem` mutation. +- **D-05:** No inline editing by default — page is read-only until edit button is pressed. Clean aesthetic. +- **D-06:** Back navigation — link at top to return to collection (`/collection`). +- **D-07:** Actions — duplicate item, delete item (with confirmation), available in a menu or as buttons. + +### Public Catalog Detail Page (`/global-items/:id`) +- **D-08:** Enhance existing route at `/global-items/:id` — currently has basic info. Add "Add to Collection" button. +- **D-09:** Layout: hero image, brand + model title, manufacturer specs (weight, price, category), description, owner count badge. +- **D-10:** "Add to Collection" button — prominent, top-right or below hero. Uses same flow as Phase 22's add-from-catalog (stub for now, like the search overlay's Add button). +- **D-11:** No edit functionality on public page — catalog items are admin-curated (future). +- **D-12:** Accessible from catalog search overlay — clicking a result card navigates here (currently cards only have an Add button). + +### Candidate Detail Page (`/threads/:threadId/candidates/:candidateId`) +- **D-13:** New route for candidate details within thread context. Shows candidate data (name, weight, price, notes, pros, cons, status, product link, image). +- **D-14:** Edit mode toggle — same pattern as item detail. Uses existing `updateCandidate` mutation. +- **D-15:** Back navigation — returns to parent thread (`/threads/:threadId`). +- **D-16:** Thread-specific actions: "Pick as winner" (resolve), delete candidate. + +### Navigation Changes +- **D-17:** ItemCard click → navigates to `/items/:id` instead of opening edit panel. +- **D-18:** CandidateCard click → navigates to `/threads/:threadId/candidates/:candidateId` instead of opening candidate panel. +- **D-19:** Catalog search result card click → navigates to `/global-items/:id`. The "Add" button stays as quick-add action (separate from card click). +- **D-20:** CandidateListItem click → same as CandidateCard, navigates to detail page. + +### Panel Removal +- **D-21:** Remove item SlideOutPanel instances from `__root.tsx` (both add and edit modes). +- **D-22:** Remove candidate SlideOutPanel instances from `__root.tsx`. +- **D-23:** Remove `openAddPanel`, `openEditPanel`, `closePanelx, `panelMode`, `editingItemId` from UIStore (item panel state). +- **D-24:** Remove `openCandidateEditPanel`, `closeCandidatePanel`, `candidatePanelMode`, `editingCandidateId` from UIStore. +- **D-25:** Keep `SlideOutPanel.tsx` component file — may be used for other panels later. +- **D-26:** Keep `ItemForm.tsx` and `CandidateForm.tsx` — their form logic/validation can be reused in detail page edit mode. + +### Adding New Items +- **D-27:** The "add item" flow now goes through catalog search (FAB → CatalogSearchOverlay). No more direct "add item" panel. The empty state in CollectionView already opens catalog search (fixed earlier). +- **D-28:** Adding candidates to threads — currently uses candidate add panel. For now, keep a simple "Add Candidate" button on the thread page that opens a minimal form/modal (not a slide-out). Detail: Claude's discretion on the exact pattern. + +### Claude's Discretion +- Exact layout proportions and spacing for detail pages +- Whether to use tabs or sections for organizing detail page content +- How the edit mode transition animates (if at all) +- "Add Candidate" button pattern on thread page (inline form, modal, or navigate to add route) +- Whether to show a "Linked to catalog" indicator on private items (subtle, not prominent) +- Mobile layout adaptations for detail pages + + + + +## Canonical References + +**Downstream agents MUST read these before planning or implementing.** + +### Existing Routes to Modify/Enhance +- `src/client/routes/global-items/$globalItemId.tsx` — Existing catalog detail page (enhance with Add button, better layout) + +### Components to Modify +- `src/client/components/ItemCard.tsx` — Change click handler from openEditPanel to navigate +- `src/client/components/CandidateCard.tsx` — Change click handler to navigate +- `src/client/components/CandidateListItem.tsx` — Change click handler to navigate +- `src/client/routes/__root.tsx` — Remove SlideOutPanel instances +- `src/client/stores/uiStore.ts` — Remove panel state +- `src/client/components/CatalogSearchOverlay.tsx` — Add card click navigation to `/global-items/:id` + +### Components to Reuse +- `src/client/components/ItemForm.tsx` — Form fields/validation for edit mode +- `src/client/components/CandidateForm.tsx` — Candidate form fields for edit mode +- `src/client/components/SlideOutPanel.tsx` — Keep file, remove usage + +### Services (no changes needed) +- `src/server/services/item.service.ts` — getItemById already returns merged data via COALESCE +- `src/server/services/thread.service.ts` — getThreadWithCandidates returns candidate data + +### Design Spec +- `docs/superpowers/specs/2026-04-05-catalog-driven-gear-flow-design.md` — Overall vision + +### Requirements +- `.planning/REQUIREMENTS.md` — DETAIL-01 through DETAIL-05 + + + + +## Existing Code Insights + +### Reusable Assets +- `global-items/$globalItemId.tsx` — Existing catalog detail page pattern to extend +- `ItemForm.tsx` — Form fields (name, weight, price, quantity, category, notes, productUrl, image) reusable for edit mode +- `CandidateForm.tsx` — Candidate form fields reusable for edit mode +- Badge pattern from `GlobalItemCard`/`ItemCard` — weight/price/category chips +- `useItem(id)` hook — fetches single item with merged data +- `useUpdateItem` mutation — for edit mode save +- `useDeleteItem` mutation — for delete action +- `useDuplicateItem` mutation — for duplicate action + +### Established Patterns +- TanStack Router file-based routes with `createFileRoute` +- Route params via `$paramName` convention +- Framer Motion for page transitions (AnimatePresence) +- Light/airy minimalist design (Tailwind CSS v4, white backgrounds, lots of whitespace) +- `max-w-7xl mx-auto px-4 sm:px-6 lg:px-8` for page content containment + +### Integration Points +- `src/client/routes/items/` — New directory for item detail route +- `src/client/routes/threads/$threadId/candidates/` — New directory for candidate detail route +- `src/client/routes/__root.tsx` — Remove panel instances, keep FabMenu and CatalogSearchOverlay +- `src/client/stores/uiStore.ts` — Clean up panel state + + + + +## Specific Ideas + +- Detail pages should feel spacious and gallery-like — big image, clean typography, generous whitespace +- Edit mode should feel seamless — fields transform from display text to inputs, no jarring layout shift +- The "Add to Collection" button on the catalog detail page should be prominent but not overwhelming +- Back navigation should be consistent: "← Back to collection" or "← Back to thread" + + + + +## Deferred Ideas + +- Reviews/ratings section on detail pages — future phase +- Community stats (average weight, price history) — future phase +- Setup appearances ("This item is in 3 setups") — future phase +- "Similar items" recommendation section — future phase +- Image gallery with multiple photos — future phase + + + +--- + +*Phase: 21-item-catalog-detail-pages* +*Context gathered: 2026-04-06* diff --git a/.planning/phases/21-item-catalog-detail-pages/21-DISCUSSION-LOG.md b/.planning/phases/21-item-catalog-detail-pages/21-DISCUSSION-LOG.md new file mode 100644 index 0000000..d88e2a2 --- /dev/null +++ b/.planning/phases/21-item-catalog-detail-pages/21-DISCUSSION-LOG.md @@ -0,0 +1,33 @@ +# Phase 21: Item & Catalog Detail Pages - Discussion Log + +> **Audit trail only.** Do not use as input to planning, research, or execution agents. + +**Date:** 2026-04-06 +**Phase:** 21-item-catalog-detail-pages +**Areas discussed:** Detail page layout, Edit mode UX, Candidate navigation, Panel removal, Route structure +**Mode:** Auto (--auto) — decisions from earlier conversation applied + +--- + +## Key Decisions from Conversation + +| Decision | Source | Notes | +|----------|--------|-------| +| Private detail at `/items/:id` | User conversation | Full page, not side panel | +| Public detail at `/global-items/:id` | User conversation | With "Add to Collection" button | +| No visual distinction reference vs standalone | User conversation | Same layout, fields may be empty | +| Edit via toggle button, not inline by default | User conversation | Clean aesthetic priority | +| Candidates get detail pages too | User conversation | Same pattern as items | +| Remove all slide-out panels | User conversation | Both item and candidate panels | + +## Claude's Discretion + +- Detail page layout proportions +- Add Candidate pattern on thread page +- Edit mode animation +- Mobile adaptations +- "Linked to catalog" indicator style + +## Deferred Ideas + +- Reviews/ratings, community stats, setup appearances, similar items, image gallery diff --git a/.planning/phases/21-item-catalog-detail-pages/21-RESEARCH.md b/.planning/phases/21-item-catalog-detail-pages/21-RESEARCH.md new file mode 100644 index 0000000..8ccfba4 --- /dev/null +++ b/.planning/phases/21-item-catalog-detail-pages/21-RESEARCH.md @@ -0,0 +1,391 @@ +# Phase 21: Item & Catalog Detail Pages - Research + +**Researched:** 2026-04-06 +**Domain:** Frontend routing, detail page patterns, edit mode toggle, panel removal +**Confidence:** HIGH + +## Summary + +This phase creates full detail pages for collection items, enhances the existing catalog detail page, adds candidate detail routes, and removes slide-out panels from the application. The entire scope is frontend-only -- no new API endpoints or service changes are required. All necessary hooks (`useItem`, `useUpdateItem`, `useDeleteItem`, `useDuplicateItem`, `useGlobalItem`, `useUpdateCandidate`, `useDeleteCandidate`) and API routes already exist. + +The primary complexity is the edit mode toggle pattern (read-only by default, fields transform to inputs when "Edit" is pressed) and coordinating the panel removal without breaking existing flows. TanStack Router's file-based routing makes adding new routes mechanical -- create the file, export a `Route` constant, and the route tree auto-generates. + +**Primary recommendation:** Build detail pages first (item, candidate, catalog enhancement), then rewire card click handlers, then remove panels and clean up UIStore in a final wave. This ordering ensures the navigation targets exist before redirecting clicks to them. + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions +- D-01 through D-07: Private item detail page at `/items/:id` with hero section, personal section, edit mode toggle, back navigation, and actions (duplicate, delete). +- D-08 through D-12: Public catalog detail page at `/global-items/:id` enhanced with "Add to Collection" button (stub), hero layout, no edit functionality. +- D-13 through D-16: Candidate detail page at `/threads/:threadId/candidates/:candidateId` with edit mode toggle and thread-specific actions. +- D-17 through D-20: Navigation changes -- ItemCard, CandidateCard, CandidateListItem, and catalog search result clicks navigate to detail pages. +- D-21 through D-26: Remove SlideOutPanel instances from `__root.tsx`, remove panel state from UIStore, keep `SlideOutPanel.tsx`, `ItemForm.tsx`, `CandidateForm.tsx` files. +- D-27: "Add item" flow goes through catalog search (FAB). No more direct "add item" panel. +- D-28: Adding candidates to threads -- keep a simple button/form pattern (not a slide-out). + +### Claude's Discretion +- Exact layout proportions and spacing for detail pages +- Whether to use tabs or sections for organizing detail page content +- How the edit mode transition animates (if at all) +- "Add Candidate" button pattern on thread page (inline form, modal, or navigate to add route) +- Whether to show a "Linked to catalog" indicator on private items (subtle, not prominent) +- Mobile layout adaptations for detail pages + +### Deferred Ideas (OUT OF SCOPE) +- Reviews/ratings section on detail pages +- Community stats (average weight, price history) +- Setup appearances ("This item is in 3 setups") +- "Similar items" recommendation section +- Image gallery with multiple photos + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| DETAIL-01 | Clicking a collection item navigates to a full detail page (`/items/:id`) showing all item data | New route file at `src/client/routes/items/$itemId.tsx`; existing `useItem(id)` hook returns all merged data via COALESCE | +| DETAIL-02 | Clicking a catalog search result navigates to a public detail page (`/global-items/:id`) with "Add to Collection" button | Existing route at `src/client/routes/global-items/$globalItemId.tsx` needs enhancement; `useGlobalItem(id)` returns data with ownerCount | +| DETAIL-03 | Item detail page has edit mode toggle for modifying personal fields | `useState` for edit mode boolean; reuse validation logic from `ItemForm.tsx`; `useUpdateItem` mutation for save | +| DETAIL-04 | Thread candidates navigate to detail pages instead of opening slide-out panels | New route at `src/client/routes/threads/$threadId/candidates/$candidateId.tsx`; `useThread(threadId)` returns candidates array | +| DETAIL-05 | Slide-out panels for items and candidates are removed from the application | Remove from `__root.tsx` lines 189-221; clean UIStore panel state (lines 3-28 of store) | + + +## Project Constraints (from CLAUDE.md) + +- **Routing**: TanStack Router with file-based routes in `src/client/routes/`. Route tree auto-generated to `routeTree.gen.ts` -- never edit manually. +- **Data fetching**: TanStack React Query via custom hooks. Mutations invalidate related query keys. +- **UI state**: Zustand store for panel/dialog state only -- server data lives in React Query. +- **Styling**: Tailwind CSS v4. +- **Testing**: Bun test runner for unit/integration. Playwright for E2E. +- **Path alias**: `@/*` maps to `./src/*`. +- **Dev command**: `bun run dev` starts both client and server. +- **Lint**: `bun run lint` -- Biome check (tabs, double quotes, organized imports). + +## Standard Stack + +### Core (already installed, no new dependencies) +| Library | Purpose | Why Standard | +|---------|---------|--------------| +| TanStack Router | File-based routing, route params | Already used throughout app | +| TanStack React Query | Data fetching, caching, mutations | All data fetching goes through this | +| Zustand | UI state management | Panel/dialog state management | +| Tailwind CSS v4 | Styling | Project standard | +| Framer Motion | Animations (AnimatePresence) | Already used for page transitions | + +### No New Dependencies Required +This phase adds no new libraries. All required functionality is covered by the existing stack. + +## Architecture Patterns + +### New Route Files to Create +``` +src/client/routes/ +├── items/ +│ └── $itemId.tsx # /items/:id - private item detail +├── threads/ +│ └── $threadId/ +│ └── candidates/ +│ └── $candidateId.tsx # /threads/:threadId/candidates/:candidateId +├── global-items/ +│ └── $globalItemId.tsx # EXISTING - enhance with Add button +``` + +### Pattern 1: TanStack Router File-Based Route +**What:** Create route file with `createFileRoute`, export `Route` constant. +**When to use:** Every new page. +**Example (from existing codebase):** +```typescript +// src/client/routes/global-items/$globalItemId.tsx +import { createFileRoute, Link } from "@tanstack/react-router"; + +export const Route = createFileRoute("/global-items/$globalItemId")({ + component: GlobalItemDetail, +}); + +function GlobalItemDetail() { + const { globalItemId } = Route.useParams(); + // ... +} +``` + +For nested routes like `/threads/$threadId/candidates/$candidateId`, TanStack Router requires the directory structure to match. The file goes at `src/client/routes/threads/$threadId/candidates/$candidateId.tsx` with route path `"/threads/$threadId/candidates/$candidateId"`. + +**IMPORTANT:** The existing `src/client/routes/threads/$threadId.tsx` must be renamed to `src/client/routes/threads/$threadId/index.tsx` (or `route.tsx`) to allow the nested candidates directory to exist alongside it. TanStack Router file-based routing requires this restructuring when adding child routes under a parameterized parent. + +### Pattern 2: Edit Mode Toggle +**What:** Local `useState` boolean controls read-only vs. editable view. No Zustand needed. +**When to use:** Item detail and candidate detail pages. +**Example:** +```typescript +function ItemDetail() { + const [isEditing, setIsEditing] = useState(false); + const [form, setForm] = useState(/* initial */); + const updateItem = useUpdateItem(); + const { data: item } = useItem(Number(itemId)); + + // Sync form state when entering edit mode or when item data loads + useEffect(() => { + if (item && isEditing) { + setForm(/* map item to form fields */); + } + }, [item, isEditing]); + + function handleSave() { + updateItem.mutate({ id: item.id, ...payload }, { + onSuccess: () => setIsEditing(false), + }); + } + + function handleCancel() { + setIsEditing(false); + // Form resets on next edit mode entry via useEffect + } + + return ( +
+ {isEditing ? ( + + ) : ( +

{item.name}

+ )} +
+ ); +} +``` + +### Pattern 3: Detail Page Layout (gallery-like) +**What:** Consistent layout: back nav, hero image, title/badges, content sections, action buttons. +**When to use:** All three detail pages. +**Example structure (from existing `$globalItemId.tsx`):** +```typescript +
+ {/* Back navigation */} + + ← Back to collection + + + {/* Hero image */} +
+ +
+ + {/* Title + badges */} +

{item.name}

+
+ {/* weight, price, category badges */} +
+ + {/* Content sections */} + {/* ... notes, product link, etc. */} +
+``` + +### Pattern 4: Navigation from Card Components +**What:** Replace `openEditPanel(id)` calls with TanStack Router `useNavigate()` or `Link` component. +**When to use:** ItemCard, CandidateCard, CandidateListItem, CatalogSearchOverlay grid/list cards. +**Example:** +```typescript +// Before (ItemCard.tsx) +const openEditPanel = useUIStore((s) => s.openEditPanel); +onClick={() => openEditPanel(id)} + +// After +import { useNavigate } from "@tanstack/react-router"; +const navigate = useNavigate(); +onClick={() => navigate({ to: "/items/$itemId", params: { itemId: String(id) } })} +``` + +### Anti-Patterns to Avoid +- **Do NOT edit `routeTree.gen.ts` manually** -- it auto-generates when route files change. +- **Do NOT put edit form state in Zustand** -- local `useState` per detail page is correct. Zustand is for cross-component UI state only. +- **Do NOT remove `ItemForm.tsx` or `CandidateForm.tsx`** -- reuse their validation logic and field definitions in the edit mode sections of detail pages. +- **Do NOT remove SlideOutPanel.tsx component file** -- D-25 explicitly keeps it for potential future use. + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Form validation | Custom validation from scratch | Copy validation logic from `ItemForm.tsx` / `CandidateForm.tsx` | Proven patterns, consistent behavior | +| Image URL resolution | Manual URL building | `withImageUrl` / `withImageUrls` from storage service (already called in routes) | Server already returns `imageUrl` field in API response | +| Loading/error states | Custom skeleton per page | Follow existing pattern from `$globalItemId.tsx` (shimmer placeholders, error state with back link) | Consistency | +| Confirmation dialogs | New dialog components | Existing `ConfirmDialog` pattern in `__root.tsx` and `useUIStore.openConfirmDelete` | Already works for items; same pattern for candidates on detail pages | + +## Common Pitfalls + +### Pitfall 1: Nested Route File Structure for Candidates +**What goes wrong:** Creating `src/client/routes/threads/$threadId/candidates/$candidateId.tsx` while `$threadId.tsx` exists as a file (not a directory) causes TanStack Router confusion. +**Why it happens:** TanStack Router file-based routing treats `$threadId.tsx` and `$threadId/` directory as conflicting. You need to restructure. +**How to avoid:** Rename `src/client/routes/threads/$threadId.tsx` to `src/client/routes/threads/$threadId/index.tsx` (or `route.tsx`). Then create `$threadId/candidates/$candidateId.tsx` as a sibling directory. +**Warning signs:** Route tree generation errors, blank page at `/threads/:threadId`. + +### Pitfall 2: Removing Panel State Before Wiring Navigation +**What goes wrong:** Removing UIStore panel state and `__root.tsx` panel JSX before the card click handlers are updated causes runtime errors (calling undefined functions). +**Why it happens:** Components like `ItemCard` still import `useUIStore((s) => s.openEditPanel)`. +**How to avoid:** Order of operations: (1) Create detail pages, (2) Update card click handlers to navigate, (3) Then remove panel state and JSX. +**Warning signs:** TypeScript errors on removed store properties, blank areas where panels used to be. + +### Pitfall 3: Edit Mode Form Not Syncing with Server Data +**What goes wrong:** User enters edit mode, but form shows stale or default data. +**Why it happens:** `useItem(id)` fetch may not have completed when edit mode is toggled, or form state is initialized once and never updated. +**How to avoid:** Use `useEffect` to sync form state when both `isEditing === true` AND item data changes. Alternatively, initialize form data from item data only when entering edit mode (in the toggle handler). +**Warning signs:** Form fields show empty or previous values. + +### Pitfall 4: CandidateCard/CandidateListItem Need threadId for Navigation +**What goes wrong:** Navigation to `/threads/:threadId/candidates/:candidateId` requires both IDs, but the component may only have `candidateId`. +**Why it happens:** CandidateCard already receives `threadId` as a prop. CandidateListItem receives it via `candidate.threadId`. Both are safe. +**How to avoid:** Verify both components have access to `threadId` before changing click handlers. Both do. +**Warning signs:** Navigation produces undefined params. + +### Pitfall 5: Catalog Search Overlay Card Click vs. Add Button +**What goes wrong:** Making the entire card clickable navigates away from the search overlay, losing context. +**Why it happens:** D-19 says card click navigates to `/global-items/:id`, but the overlay is open. +**How to avoid:** Close the catalog search overlay before navigating. Or use `Link` component and let the navigation naturally close the overlay (overlay checks `catalogSearchOpen` state). The card body navigates; the "Add" button stays as a separate action with `e.stopPropagation()`. +**Warning signs:** Overlay stays visible on top of detail page, or user loses search context unexpectedly. + +### Pitfall 6: "Add Candidate" on Thread Page After Panel Removal +**What goes wrong:** The thread page currently opens the candidate add panel via `openCandidateAddPanel()`. After panels are removed, there's no way to add candidates. +**Why it happens:** D-28 says to keep a simple button/form pattern, but the existing flow is entirely panel-based. +**How to avoid:** Replace the "Add Candidate" button action on the thread page. Options: (1) inline form that expands on the thread page, (2) small modal dialog, (3) navigate to a dedicated add route. Recommendation: use a modal dialog -- it's the lightest change and consistent with existing dialog patterns in the app. +**Warning signs:** No way to add candidates after panel removal. + +## Code Examples + +### Creating the Item Detail Route +```typescript +// src/client/routes/items/$itemId.tsx +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; +import { useState } from "react"; +import { useFormatters } from "../../hooks/useFormatters"; +import { useDeleteItem, useDuplicateItem, useItem, useUpdateItem } from "../../hooks/useItems"; + +export const Route = createFileRoute("/items/$itemId")({ + component: ItemDetailPage, +}); +``` + +### Restructuring Thread Route for Nesting +``` +# Before: +src/client/routes/threads/$threadId.tsx + +# After: +src/client/routes/threads/$threadId/index.tsx # same content, path unchanged +src/client/routes/threads/$threadId/candidates/ + $candidateId.tsx # new candidate detail +``` + +The `index.tsx` file maps to `/threads/$threadId` exactly as before. The nested `candidates/$candidateId.tsx` maps to `/threads/$threadId/candidates/$candidateId`. + +### UIStore Cleanup (properties to remove) +```typescript +// Remove these from UIState interface and implementation: +panelMode: "closed" | "add" | "edit"; +editingItemId: number | null; +openAddPanel: () => void; +openEditPanel: (itemId: number) => void; +closePanel: () => void; + +candidatePanelMode: "closed" | "add" | "edit"; +editingCandidateId: number | null; +openCandidateAddPanel: () => void; +openCandidateEditPanel: (id: number) => void; +closeCandidatePanel: () => void; + +// Keep these (still used): +confirmDeleteItemId / openConfirmDelete / closeConfirmDelete // used by ConfirmDialog +confirmDeleteCandidateId / openConfirmDeleteCandidate / closeConfirmDeleteCandidate // candidate delete +resolveThreadId / resolveCandidateId / openResolveDialog / closeResolveDialog // resolve dialog +// ... all other non-panel state +``` + +### Updating ItemCard Click Handler +```typescript +// src/client/components/ItemCard.tsx +// Replace: +import { useUIStore } from "../stores/uiStore"; +const openEditPanel = useUIStore((s) => s.openEditPanel); +// With: +import { useNavigate } from "@tanstack/react-router"; +const navigate = useNavigate(); + +// onClick changes from: +onClick={() => openEditPanel(id)} +// To: +onClick={() => navigate({ to: "/items/$itemId", params: { itemId: String(id) } })} +``` + +### Adding "Add to Collection" Button on Catalog Detail Page +```typescript +// In global-items/$globalItemId.tsx, add after hero/header: + +``` +This is a stub per D-10 -- actual add flow wired in Phase 22. + +## Validation Architecture + +### Test Framework +| Property | Value | +|----------|-------| +| Framework | Playwright + Bun test | +| Config file | `playwright.config.ts` (E2E), `bunfig.toml` (unit) | +| Quick run command | `bun test tests/routes/` | +| Full suite command | `bun run test:e2e` | + +### Phase Requirements to Test Map +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| DETAIL-01 | Item detail page renders at `/items/:id` | E2E | `bun run test:e2e -- --grep "item detail"` | No -- Wave 0 | +| DETAIL-02 | Catalog detail page has "Add to Collection" button | E2E | `bun run test:e2e -- --grep "catalog detail"` | No -- Wave 0 | +| DETAIL-03 | Edit mode toggle shows/hides form inputs | E2E | `bun run test:e2e -- --grep "edit mode"` | No -- Wave 0 | +| DETAIL-04 | Candidate card click navigates to detail page | E2E | `bun run test:e2e -- --grep "candidate detail"` | No -- Wave 0 | +| DETAIL-05 | No SlideOutPanel instances in rendered DOM | E2E | `bun run test:e2e -- --grep "panel removed"` | No -- Wave 0 | + +### Sampling Rate +- **Per task commit:** `bun run lint && bun run dev` (manual visual check -- frontend-heavy) +- **Per wave merge:** `bun run test:e2e` +- **Phase gate:** Full E2E suite green before `/gsd:verify-work` + +### Wave 0 Gaps +- [ ] `e2e/item-detail.spec.ts` -- covers DETAIL-01, DETAIL-03 +- [ ] `e2e/catalog-detail.spec.ts` -- covers DETAIL-02 +- [ ] `e2e/candidate-detail.spec.ts` -- covers DETAIL-04 +- [ ] `e2e/panel-removal.spec.ts` -- covers DETAIL-05 +- [ ] E2E seed data: verify `e2e/seed.ts` creates items and threads with candidates for detail page testing + +## Sources + +### Primary (HIGH confidence) +- Codebase inspection: `src/client/routes/global-items/$globalItemId.tsx` -- existing detail page pattern +- Codebase inspection: `src/client/routes/__root.tsx` -- panel instances to remove (lines 189-221) +- Codebase inspection: `src/client/stores/uiStore.ts` -- panel state to clean (full file reviewed) +- Codebase inspection: `src/client/components/ItemCard.tsx` -- current click handler using `openEditPanel` +- Codebase inspection: `src/client/components/CandidateCard.tsx` -- current click handler using `openCandidateEditPanel` +- Codebase inspection: `src/client/components/CandidateListItem.tsx` -- current click handler +- Codebase inspection: `src/client/components/ItemForm.tsx` -- form fields and validation to reuse +- Codebase inspection: `src/client/components/CandidateForm.tsx` -- candidate form to reuse +- Codebase inspection: `src/client/hooks/useItems.ts` -- `useItem`, `useUpdateItem`, `useDeleteItem`, `useDuplicateItem` hooks +- Codebase inspection: `src/client/hooks/useCandidates.ts` -- `useUpdateCandidate`, `useDeleteCandidate` hooks +- Codebase inspection: `src/client/hooks/useGlobalItems.ts` -- `useGlobalItem` hook with ownerCount +- Codebase inspection: `src/server/services/item.service.ts` -- `getItemById` COALESCE merge pattern +- Codebase inspection: `src/server/routes/items.ts` -- `withImageUrl` applied to single item responses + +### Secondary (MEDIUM confidence) +- TanStack Router file-based routing: nested parameterized routes require directory restructuring (verified against established pattern in `src/client/routes/setups/$setupId.tsx`) + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH -- no new dependencies, all existing +- Architecture: HIGH -- all patterns verified against existing codebase +- Pitfalls: HIGH -- identified from direct code inspection of affected files + +**Research date:** 2026-04-06 +**Valid until:** 2026-05-06 (stable -- frontend patterns, no external dependencies) diff --git a/.planning/phases/21-item-catalog-detail-pages/21-VALIDATION.md b/.planning/phases/21-item-catalog-detail-pages/21-VALIDATION.md new file mode 100644 index 0000000..6fbc7a5 --- /dev/null +++ b/.planning/phases/21-item-catalog-detail-pages/21-VALIDATION.md @@ -0,0 +1,69 @@ +--- +phase: 21 +slug: item-catalog-detail-pages +status: draft +nyquist_compliant: false +wave_0_complete: false +created: 2026-04-06 +--- + +# Phase 21 — Validation Strategy + +> Per-phase validation contract for feedback sampling during execution. + +--- + +## Test Infrastructure + +| Property | Value | +|----------|-------| +| **Framework** | bun test + Playwright (E2E) | +| **Config file** | playwright.config.ts | +| **Quick run command** | `bun run lint` | +| **Full suite command** | `bun run lint && bun run build` | +| **Estimated runtime** | ~10 seconds | + +--- + +## Sampling Rate + +- **After every task commit:** Run `bun run lint` +- **After every plan wave:** Run `bun run lint && bun run build` +- **Before `/gsd:verify-work`:** Full suite + manual visual check +- **Max feedback latency:** 10 seconds + +--- + +## Per-Task Verification Map + +| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | Status | +|---------|------|------|-------------|-----------|-------------------|--------| +| 01-T1 | 21-01 | 1 | DETAIL-01, DETAIL-03 | lint+visual | `bun run lint` | ⬜ pending | +| 01-T2 | 21-01 | 1 | DETAIL-02 | lint+visual | `bun run lint` | ⬜ pending | +| 02-T1 | 21-02 | 1 | DETAIL-04 | lint+visual | `bun run lint` | ⬜ pending | +| 02-T2 | 21-02 | 1 | DETAIL-04 | lint+visual | `bun run lint` | ⬜ pending | +| 03-T1 | 21-03 | 2 | DETAIL-04 | lint+grep | `bun run lint` | ⬜ pending | +| 03-T2 | 21-03 | 2 | DETAIL-05 | lint+grep | `bun run lint` | ⬜ pending | + +--- + +## Manual-Only Verifications + +| Behavior | Requirement | Why Manual | Test Instructions | +|----------|-------------|------------|-------------------| +| Item detail page renders with correct data | DETAIL-01 | Visual layout | Navigate to /items/:id, verify hero image, specs, notes | +| Edit mode toggle works | DETAIL-03 | Visual interaction | Click Edit, verify fields become editable, save changes | +| Catalog detail page with Add button | DETAIL-02 | Visual layout | Navigate to /global-items/:id, verify Add button present | +| Candidate detail page in thread context | DETAIL-04 | Visual navigation | Click candidate in thread, verify detail page | +| Panels removed from UI | DETAIL-05 | Visual absence | Verify no slide-out panels appear anywhere | + +--- + +## Validation Sign-Off + +- [x] All tasks have automated verify (lint) +- [x] Sampling continuity maintained +- [ ] Manual visual verification pending +- [ ] `nyquist_compliant: true` set in frontmatter + +**Approval:** pending diff --git a/src/client/routes/items/$itemId.tsx b/src/client/routes/items/$itemId.tsx new file mode 100644 index 0000000..f86e0a2 --- /dev/null +++ b/src/client/routes/items/$itemId.tsx @@ -0,0 +1,454 @@ +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; +import { useState } from "react"; +import { CategoryPicker } from "../../components/CategoryPicker"; +import { ImageUpload } from "../../components/ImageUpload"; +import { useFormatters } from "../../hooks/useFormatters"; +import { useDuplicateItem, useItem, useUpdateItem } from "../../hooks/useItems"; +import { LucideIcon } from "../../lib/iconData"; +import { useUIStore } from "../../stores/uiStore"; + +export const Route = createFileRoute("/items/$itemId")({ + component: ItemDetail, +}); + +interface EditFormState { + name: string; + weightGrams: string; + priceDollars: string; + quantity: number; + categoryId: number; + notes: string; + productUrl: string; + imageFilename: string | null; +} + +function ItemDetail() { + const { itemId } = Route.useParams(); + const navigate = useNavigate(); + const { + data: item, + isLoading, + error, + } = useItem(Number(itemId)) as ReturnType & { + data: + | (NonNullable["data"]> & { + imageUrl?: string | null; + }) + | undefined; + }; + const { weight, price } = useFormatters(); + const updateItem = useUpdateItem(); + const duplicateItem = useDuplicateItem(); + const openConfirmDelete = useUIStore((s) => s.openConfirmDelete); + const openExternalLink = useUIStore((s) => s.openExternalLink); + + const [isEditing, setIsEditing] = useState(false); + const [form, setForm] = useState({ + name: "", + weightGrams: "", + priceDollars: "", + quantity: 1, + categoryId: 0, + notes: "", + productUrl: "", + imageFilename: null, + }); + + function enterEditMode() { + if (!item) return; + setForm({ + name: item.name, + weightGrams: item.weightGrams != null ? String(item.weightGrams) : "", + priceDollars: + item.priceCents != null ? (item.priceCents / 100).toFixed(2) : "", + quantity: item.quantity, + categoryId: item.categoryId, + notes: item.notes || "", + productUrl: item.productUrl || "", + imageFilename: item.imageFilename, + }); + setIsEditing(true); + } + + function cancelEdit() { + setIsEditing(false); + } + + function handleSave() { + if (!item) return; + const weightGrams = form.weightGrams.trim() + ? Math.round(Number(form.weightGrams)) + : null; + const priceCents = form.priceDollars.trim() + ? Math.round(Number(form.priceDollars) * 100) + : null; + + updateItem.mutate( + { + id: item.id, + name: form.name.trim(), + weightGrams, + priceCents, + quantity: form.quantity, + categoryId: form.categoryId, + notes: form.notes.trim() || null, + productUrl: form.productUrl.trim() || null, + imageFilename: form.imageFilename, + }, + { + onSuccess: () => setIsEditing(false), + }, + ); + } + + function handleDuplicate() { + if (!item) return; + duplicateItem.mutate(item.id, { + onSuccess: (newItem) => { + navigate({ + to: "/items/$itemId", + params: { itemId: String(newItem.id) }, + }); + }, + }); + } + + function handleDelete() { + if (!item) return; + openConfirmDelete(item.id); + } + + if (isLoading) { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); + } + + if (error || !item) { + return ( +
+ + ← Back to collection + +
+

Item not found

+
+
+ ); + } + + const imageUrl = item.imageUrl || null; + + return ( +
+ {/* Top bar */} +
+ + ← Back to collection + + {!isEditing && ( +
+ + + +
+ )} + {isEditing && ( +
+ + +
+ )} +
+ + {/* Hero image */} + {isEditing ? ( +
+ + setForm((f) => ({ ...f, imageFilename: filename })) + } + /> +
+ ) : ( +
+ {imageUrl ? ( + {item.name} + ) : ( +
+ +
+ )} +
+ )} + + {/* Header / Name */} +
+ {isEditing ? ( + setForm((f) => ({ ...f, name: e.target.value }))} + className="w-full text-2xl font-bold text-gray-900 border border-gray-200 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" + placeholder="Item name" + /> + ) : ( +

{item.name}

+ )} +
+ + {/* Badges / Specs */} + {isEditing ? ( +
+
+ + + setForm((f) => ({ + ...f, + weightGrams: e.target.value, + })) + } + className="w-full py-2 px-3 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" + placeholder="0" + /> +
+
+ + + setForm((f) => ({ + ...f, + priceDollars: e.target.value, + })) + } + className="w-full py-2 px-3 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" + placeholder="0.00" + /> +
+
+ + + setForm((f) => ({ + ...f, + quantity: Math.max(1, Number(e.target.value)), + })) + } + className="w-full py-2 px-3 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-gray-400 focus:border-transparent" + /> +
+
+ + setForm((f) => ({ ...f, categoryId: id }))} + /> +
+
+ ) : ( +
+ {item.weightGrams != null && ( + + {weight(item.weightGrams)} + + )} + {item.priceCents != null && ( + + {price(item.priceCents)} + + )} + + + {item.categoryName} + + {item.quantity > 1 && ( + + Qty: {item.quantity} + + )} +
+ )} + + {/* Notes */} + {isEditing ? ( +
+ +