- 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
332 lines
17 KiB
Markdown
332 lines
17 KiB
Markdown
---
|
|
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>
|