- Add 21-02-SUMMARY.md with execution results - Update STATE.md, ROADMAP.md, REQUIREMENTS.md
240 lines
13 KiB
Markdown
240 lines
13 KiB
Markdown
---
|
|
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>
|