diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md
index 84cf29a..859b7d8 100644
--- a/.planning/REQUIREMENTS.md
+++ b/.planning/REQUIREMENTS.md
@@ -63,7 +63,7 @@ Requirements for this milestone. Each maps to roadmap phases.
- [ ] **DETAIL-01**: Clicking a collection item navigates to a full detail page (`/items/:id`) showing all item data
- [ ] **DETAIL-02**: Clicking a catalog search result navigates to a public detail page (`/global-items/:id`) with "Add to Collection" button
- [ ] **DETAIL-03**: Item detail page has edit mode toggle for modifying personal fields (notes, category, quantity, purchase price)
-- [ ] **DETAIL-04**: Thread candidates navigate to detail pages instead of opening slide-out panels
+- [x] **DETAIL-04**: Thread candidates navigate to detail pages instead of opening slide-out panels
- [ ] **DETAIL-05**: Slide-out panels for items and candidates are removed from the application
### Tags
@@ -184,7 +184,7 @@ Which phases cover which requirements. Updated during roadmap creation.
| DETAIL-01 | Phase 21 | Pending |
| DETAIL-02 | Phase 21 | Pending |
| DETAIL-03 | Phase 21 | Pending |
-| DETAIL-04 | Phase 21 | Pending |
+| DETAIL-04 | Phase 21 | Complete |
| DETAIL-05 | Phase 21 | Pending |
**Coverage:**
diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 62d4f9a..a0da163 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -57,7 +57,7 @@
- [x] **Phase 18: Global Items & Public Profiles** — Global item catalog, user profiles, and public setup sharing (completed 2026-04-05)
- [x] **Phase 19: Reference Item Model & Tags Schema** — Collection items as references to global catalog, tag system for discovery (completed 2026-04-05)
- [x] **Phase 20: FAB & Full-Screen Catalog Search** — Global FAB with mini menu, full-screen catalog search with tag filtering (completed 2026-04-06)
-- [ ] **Phase 21: Item & Catalog Detail Pages** — Full detail pages for collection items and catalog entries, replacing slide-out panels
+- [x] **Phase 21: Item & Catalog Detail Pages** — Full detail pages for collection items and catalog entries, replacing slide-out panels (completed 2026-04-06)
- [ ] **Phase 22: Add-from-Catalog & Thread Integration** — Add catalog items to collection and threads, resolution creates reference items
- [ ] **Phase 23: Manual Entry Fallback** — Manual add for items not in catalog, non-functional submission prompt
@@ -266,6 +266,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 | 1/1 | Complete | 2026-04-06 |
| 22. Add-from-Catalog & Thread Integration | v2.0 | 0/? | Not started | - |
| 23. Manual Entry Fallback | v2.0 | 0/? | Not started | - |
diff --git a/.planning/STATE.md b/.planning/STATE.md
index 3a7f1de..3f0e44b 100644
--- a/.planning/STATE.md
+++ b/.planning/STATE.md
@@ -3,14 +3,14 @@ gsd_state_version: 1.0
milestone: v1.3
milestone_name: Research & Decision Tools
status: planning
-stopped_at: Completed 20-02-PLAN.md
-last_updated: "2026-04-06T06:17:39.050Z"
+stopped_at: Completed 21-02-PLAN.md
+last_updated: "2026-04-06T13:03:13.009Z"
last_activity: 2026-04-06
progress:
- total_phases: 14
- completed_phases: 13
- total_plans: 38
- completed_plans: 36
+ total_phases: 15
+ completed_phases: 14
+ total_plans: 39
+ completed_plans: 37
percent: 0
---
@@ -58,6 +58,8 @@ Key decisions made during v2.0 planning:
- [Phase 20]: Created tags table in schema (was missing, needed for GET /api/tags endpoint)
- [Phase 20]: FAB visible on all authenticated routes, not just collection gear tab
- [Phase 20]: Add button on catalog search cards is a stub (Phase 21 wires actual flow)
+- [Phase 21]: Candidate data fetched from useThread hook (find in array) not new API endpoint
+- [Phase 21]: AddCandidateModal inline in thread page, local modal pattern replacing UIStore panel
### Pending Todos
@@ -68,6 +70,7 @@ None active.
| # | Description | Date | Commit | Directory |
|---|-------------|------|--------|-----------|
| 260406-j44 | Comprehensive dev seed script for bikepacking gear data | 2026-04-06 | — | [260406-j44-comprehensive-dev-seed-script-for-bikepa](./quick/260406-j44-comprehensive-dev-seed-script-for-bikepa/) |
+| Phase 21 P02 | 4min | 2 tasks | 2 files |
### Blockers/Concerns
@@ -76,6 +79,6 @@ None active.
## Session Continuity
-Last session: 2026-04-06T06:12:00.000Z
-Stopped at: Completed 20-02-PLAN.md
+Last session: 2026-04-06T13:03:13.007Z
+Stopped at: Completed 21-02-PLAN.md
Resume file: None
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
+
+
+
diff --git a/.planning/phases/21-item-catalog-detail-pages/21-02-SUMMARY.md b/.planning/phases/21-item-catalog-detail-pages/21-02-SUMMARY.md
new file mode 100644
index 0000000..ebd293e
--- /dev/null
+++ b/.planning/phases/21-item-catalog-detail-pages/21-02-SUMMARY.md
@@ -0,0 +1,90 @@
+---
+phase: 21-item-catalog-detail-pages
+plan: 02
+subsystem: ui
+tags: [react, tanstack-router, candidate-detail, modal-dialog, edit-mode]
+
+requires:
+ - phase: 20-fab-full-screen-catalog-search
+ provides: FAB and catalog search overlay foundation
+provides:
+ - Candidate detail page at /threads/:threadId/candidates/:candidateId with edit mode
+ - Restructured thread route directory for nested candidate routes
+ - Add-candidate modal dialog on thread page replacing slide-out panel
+affects: [21-03-candidate-card-navigation-rewire]
+
+tech-stack:
+ added: []
+ patterns: [nested-route-directory-structure, local-modal-pattern]
+
+key-files:
+ created:
+ - src/client/routes/threads/$threadId/candidates/$candidateId.tsx
+ modified:
+ - src/client/routes/threads/$threadId/index.tsx
+
+key-decisions:
+ - "StatusBadge on detail page is read-only (no onStatusChange) since status cycling happens on cards"
+ - "AddCandidateModal defined inline in thread index.tsx rather than separate component file"
+ - "Candidate data fetched from useThread hook (find in candidates array) rather than new endpoint"
+
+patterns-established:
+ - "Nested route directory: $threadId/index.tsx + $threadId/candidates/$candidateId.tsx"
+ - "Local modal pattern: useState in parent page controls modal visibility, no UIStore needed"
+
+requirements-completed: [DETAIL-04]
+
+duration: 4min
+completed: 2026-04-06
+---
+
+# Phase 21 Plan 02: Candidate Detail Page & Thread Route Restructuring Summary
+
+**Candidate detail page with edit mode toggle at /threads/:threadId/candidates/:candidateId, thread route directory restructured for nested routes, add-candidate modal replacing slide-out panel**
+
+## Performance
+
+- **Duration:** 4 min
+- **Started:** 2026-04-06T12:57:42Z
+- **Completed:** 2026-04-06T13:02:26Z
+- **Tasks:** 2
+- **Files modified:** 2
+
+## Accomplishments
+- Restructured thread route from flat file to directory structure supporting nested candidate routes
+- Created full candidate detail page with read/edit modes, image display, pros/cons, notes, and thread actions
+- Replaced UIStore openCandidateAddPanel call with local modal dialog containing all form fields
+
+## Task Commits
+
+Each task was committed atomically:
+
+1. **Task 1: Restructure thread route and create candidate detail page** - `cecaf78` (feat)
+2. **Task 2: Add candidate modal dialog on thread page** - `47b416e` (feat)
+
+## Files Created/Modified
+- `src/client/routes/threads/$threadId/index.tsx` - Moved from $threadId.tsx, updated imports, added AddCandidateModal
+- `src/client/routes/threads/$threadId/candidates/$candidateId.tsx` - New candidate detail page with edit mode toggle
+
+## Decisions Made
+- StatusBadge rendered as read-only on detail page (status changes happen via card interactions)
+- AddCandidateModal defined inline in the thread page file for simplicity
+- Candidate data sourced from useThread hook (find in array) to avoid new API endpoint
+
+## Deviations from Plan
+
+None - plan executed exactly as written.
+
+## Issues Encountered
+None
+
+## User Setup Required
+None - no external service configuration required.
+
+## Next Phase Readiness
+- Candidate detail page is ready for Plan 03 to rewire CandidateCard clicks as navigation links
+- Thread route directory structure supports the nested /candidates/:candidateId path
+
+---
+*Phase: 21-item-catalog-detail-pages*
+*Completed: 2026-04-06*