docs(21): create phase plan — 3 plans across 2 waves

This commit is contained in:
2026-04-06 14:53:08 +02:00
parent e0ce45a57c
commit b10d81798f
4 changed files with 817 additions and 2 deletions

View File

@@ -217,7 +217,11 @@ Plans:
3. Thread candidates navigate to detail pages instead of opening slide-out panels
4. Item slide-out panel and candidate slide-out panel are removed from the root layout
5. No visual distinction between reference items and standalone items — same layout, some fields may be empty
**Plans**: TBD
**Plans:** 3 plans
Plans:
- [ ] 21-01-PLAN.md — Item detail page with edit mode + catalog detail page enhancement
- [ ] 21-02-PLAN.md — Candidate detail page with edit mode + add-candidate modal
- [ ] 21-03-PLAN.md — Rewire card navigation + remove slide-out panels + UIStore cleanup
**UI hint**: yes
### Phase 22: Add-from-Catalog & Thread Integration
@@ -266,6 +270,6 @@ Plans:
| 18. Global Items & Public Profiles | v2.0 | 4/5 | Complete | 2026-04-05 |
| 19. Reference Item Model & Tags Schema | v2.0 | 3/3 | Complete | 2026-04-05 |
| 20. FAB & Full-Screen Catalog Search | v2.0 | 2/2 | Complete | 2026-04-06 |
| 21. Item & Catalog Detail Pages | v2.0 | 0/? | Not started | - |
| 21. Item & Catalog Detail Pages | v2.0 | 0/3 | Planning | - |
| 22. Add-from-Catalog & Thread Integration | v2.0 | 0/? | Not started | - |
| 23. Manual Entry Fallback | v2.0 | 0/? | Not started | - |

View File

@@ -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"
---
<objective>
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`)
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<interfaces>
<!-- Key hooks the executor needs -->
From src/client/hooks/useItems.ts:
```typescript
export function useItem(id: number | null): UseQueryResult<ItemWithCategory>;
export function useUpdateItem(): UseMutationResult<ItemWithCategory, Error, { id: number } & Partial<CreateItem>>;
export function useDeleteItem(): UseMutationResult<{ success: boolean }, Error, number>;
export function useDuplicateItem(): UseMutationResult<Item, Error, number>;
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>;
// 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;
}
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create private item detail page with edit mode toggle</name>
<files>src/client/routes/items/$itemId.tsx</files>
<read_first>
- 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)
</read_first>
<action>
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 — `<Link to="/collection">← Back to collection</Link>`.
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<boolean>(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.
</action>
<verify>
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>
- `/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
</done>
</task>
<task type="auto">
<name>Task 2: Enhance catalog detail page with Add to Collection button and improved layout</name>
<files>src/client/routes/global-items/$globalItemId.tsx</files>
<read_first>
- 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)
</read_first>
<action>
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)
</action>
<verify>
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>
- 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)
</done>
</task>
</tasks>
<verification>
- `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
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/21-item-catalog-detail-pages/21-01-SUMMARY.md`
</output>

View File

@@ -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"
---
<objective>
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
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<interfaces>
<!-- Key hooks and types the executor needs -->
From src/client/hooks/useCandidates.ts:
```typescript
export function useCreateCandidate(threadId: number): UseMutationResult<CandidateResponse, Error, CreateCandidate & { imageFilename?: string }>;
export function useUpdateCandidate(threadId: number): UseMutationResult<CandidateResponse, Error, UpdateCandidate & { candidateId: number; imageFilename?: string }>;
export function useDeleteCandidate(threadId: number): UseMutationResult<{ success: boolean }, Error, number>;
```
From src/client/hooks/useThreads.ts:
```typescript
export function useThread(id: number): UseQueryResult<ThreadWithCandidates>;
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
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Restructure thread route and create candidate detail page</name>
<files>
src/client/routes/threads/$threadId.tsx
src/client/routes/threads/$threadId/index.tsx
src/client/routes/threads/$threadId/candidates/$candidateId.tsx
</files>
<read_first>
- 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)
</read_first>
<action>
**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<boolean>(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 — `<Link to="/threads/$threadId" params={{ threadId: String(threadId) }}>← Back to thread</Link>`.
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).
</action>
<verify>
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>
- 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
</done>
</task>
<task type="auto">
<name>Task 2: Add candidate modal dialog on thread page</name>
<files>
src/client/routes/threads/$threadId/index.tsx
</files>
<read_first>
- 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)
</read_first>
<action>
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 && <AddCandidateModal threadId={threadId} onClose={() => 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).
</action>
<verify>
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>
- 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
</done>
</task>
</tasks>
<verification>
- `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
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/21-item-catalog-detail-pages/21-02-SUMMARY.md`
</output>

View File

@@ -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"
---
<objective>
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
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.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
<!-- Read Plan 01 and 02 SUMMARYs to confirm navigation targets exist -->
@.planning/phases/21-item-catalog-detail-pages/21-01-SUMMARY.md
@.planning/phases/21-item-catalog-detail-pages/21-02-SUMMARY.md
<interfaces>
<!-- Navigation patterns the executor needs -->
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
<SlideOutPanel isOpen={isItemPanelOpen} onClose={closePanel} title={...}>
<ItemForm mode="add" | mode="edit" itemId={editingItemId} />
</SlideOutPanel>
<SlideOutPanel isOpen={isCandidatePanelOpen} onClose={closeCandidatePanel} title={...}>
<CandidateForm mode="add" | mode="edit" threadId={...} candidateId={...} />
</SlideOutPanel>
```
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
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Rewire card click handlers to navigate to detail pages</name>
<files>
src/client/components/ItemCard.tsx
src/client/components/CandidateCard.tsx
src/client/components/CandidateListItem.tsx
src/client/components/CatalogSearchOverlay.tsx
</files>
<read_first>
- 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)
</read_first>
<action>
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
</action>
<verify>
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>
- 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
</done>
</task>
<task type="auto">
<name>Task 2: Remove slide-out panels from root layout and clean UIStore</name>
<files>
src/client/routes/__root.tsx
src/client/stores/uiStore.ts
</files>
<read_first>
- 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)
</read_first>
<action>
Per D-21, D-22: **__root.tsx** — Remove slide-out panel instances.
1. Remove the Item SlideOutPanel block (lines ~189-199 in current file):
```
<SlideOutPanel isOpen={isItemPanelOpen} onClose={closePanel} title={...}>
{panelMode === "add" && <ItemForm mode="add" />}
{panelMode === "edit" && <ItemForm mode="edit" itemId={editingItemId} />}
</SlideOutPanel>
```
2. Remove the Candidate SlideOutPanel block (lines ~202-221 in current file):
```
{currentThreadId != null && (
<SlideOutPanel isOpen={isCandidatePanelOpen} ...>
<CandidateForm ... />
</SlideOutPanel>
)}
```
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).
</action>
<verify>
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>
- 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
</done>
</task>
</tasks>
<verification>
- `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
</verification>
<success_criteria>
- 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
</success_criteria>
<output>
After completion, create `.planning/phases/21-item-catalog-detail-pages/21-03-SUMMARY.md`
</output>