feat(21-01): create private item detail page with edit mode toggle

- Full detail page at /items/:id with hero image, name, spec badges, notes, product link
- Edit mode toggle: read-only by default, editable inputs when Edit clicked
- Save persists via useUpdateItem, Cancel reverts to read-only
- Duplicate and Delete actions via existing hooks/dialogs
- Back link to /collection, loading shimmer, error state
- CategoryPicker and ImageUpload in edit mode
This commit is contained in:
2026-04-06 15:01:10 +02:00
parent 2d71ce15af
commit 3228bcadbe
8 changed files with 1909 additions and 0 deletions

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>