280 lines
15 KiB
Markdown
280 lines
15 KiB
Markdown
---
|
|
phase: 02-planning-threads
|
|
plan: 02
|
|
type: execute
|
|
wave: 2
|
|
depends_on: [02-01]
|
|
files_modified:
|
|
- src/client/routes/index.tsx
|
|
- src/client/routes/__root.tsx
|
|
- src/client/routes/threads/$threadId.tsx
|
|
- src/client/components/ThreadCard.tsx
|
|
- src/client/components/CandidateCard.tsx
|
|
- src/client/components/CandidateForm.tsx
|
|
- src/client/components/ThreadTabs.tsx
|
|
- src/client/hooks/useThreads.ts
|
|
- src/client/hooks/useCandidates.ts
|
|
- src/client/stores/uiStore.ts
|
|
autonomous: true
|
|
requirements: [THRD-01, THRD-02, THRD-03, THRD-04]
|
|
|
|
must_haves:
|
|
truths:
|
|
- "User can switch between My Gear and Planning tabs on the home page"
|
|
- "User can see a list of planning threads as cards with name, candidate count, date, and price range"
|
|
- "User can create a new thread from the Planning tab"
|
|
- "User can click a thread card to see its candidates as a card grid"
|
|
- "User can add a candidate to a thread via slide-out panel with all item fields"
|
|
- "User can edit and delete candidates from a thread"
|
|
- "User can pick a winning candidate which creates a collection item and archives the thread"
|
|
- "Resolved threads are hidden by default with a toggle to show them"
|
|
- "After resolution, switching to My Gear tab shows the new item without page refresh"
|
|
artifacts:
|
|
- path: "src/client/routes/index.tsx"
|
|
provides: "Home page with tab navigation between gear and planning"
|
|
contains: "tab"
|
|
- path: "src/client/routes/threads/$threadId.tsx"
|
|
provides: "Thread detail page showing candidates"
|
|
contains: "threadId"
|
|
- path: "src/client/components/ThreadCard.tsx"
|
|
provides: "Thread card with name, candidate count, price range tags"
|
|
min_lines: 30
|
|
- path: "src/client/components/CandidateCard.tsx"
|
|
provides: "Candidate card matching ItemCard visual pattern"
|
|
min_lines: 30
|
|
- path: "src/client/components/CandidateForm.tsx"
|
|
provides: "Candidate add/edit form with same fields as ItemForm"
|
|
min_lines: 40
|
|
- path: "src/client/hooks/useThreads.ts"
|
|
provides: "TanStack Query hooks for thread CRUD and resolution"
|
|
exports: ["useThreads", "useThread", "useCreateThread", "useResolveThread"]
|
|
- path: "src/client/hooks/useCandidates.ts"
|
|
provides: "TanStack Query hooks for candidate CRUD"
|
|
exports: ["useCreateCandidate", "useUpdateCandidate", "useDeleteCandidate"]
|
|
- path: "src/client/stores/uiStore.ts"
|
|
provides: "Extended UI state for thread panels and resolve dialog"
|
|
contains: "candidatePanelMode"
|
|
key_links:
|
|
- from: "src/client/hooks/useThreads.ts"
|
|
to: "/api/threads"
|
|
via: "apiGet/apiPost/apiDelete"
|
|
pattern: "api/threads"
|
|
- from: "src/client/hooks/useCandidates.ts"
|
|
to: "/api/threads/:id/candidates"
|
|
via: "apiPost/apiPut/apiDelete"
|
|
pattern: "api/threads.*candidates"
|
|
- from: "src/client/hooks/useThreads.ts"
|
|
to: "queryClient.invalidateQueries"
|
|
via: "onSuccess invalidates threads + items + totals after resolution"
|
|
pattern: "invalidateQueries.*items"
|
|
- from: "src/client/routes/index.tsx"
|
|
to: "src/client/components/ThreadCard.tsx"
|
|
via: "renders thread cards in Planning tab"
|
|
pattern: "ThreadCard"
|
|
- from: "src/client/routes/threads/$threadId.tsx"
|
|
to: "src/client/components/CandidateCard.tsx"
|
|
via: "renders candidate cards in thread detail"
|
|
pattern: "CandidateCard"
|
|
---
|
|
|
|
<objective>
|
|
Build the complete frontend for planning threads: tab navigation, thread list with cards, thread detail page with candidate grid, candidate add/edit via slide-out panel, and thread resolution flow with confirmation dialog.
|
|
|
|
Purpose: Give users the full planning thread workflow in the UI -- create threads, add candidates, compare them visually, and resolve by picking a winner.
|
|
|
|
Output: Fully interactive thread planning UI that consumes the API from Plan 01.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/phases/02-planning-threads/02-CONTEXT.md
|
|
@.planning/phases/02-planning-threads/02-RESEARCH.md
|
|
@.planning/phases/02-planning-threads/02-01-SUMMARY.md
|
|
|
|
<interfaces>
|
|
<!-- Existing components to reuse/reference -->
|
|
|
|
From src/client/stores/uiStore.ts (extend this):
|
|
```typescript
|
|
interface UIState {
|
|
panelMode: "closed" | "add" | "edit";
|
|
editingItemId: number | null;
|
|
confirmDeleteItemId: number | null;
|
|
openAddPanel: () => void;
|
|
openEditPanel: (itemId: number) => void;
|
|
closePanel: () => void;
|
|
openConfirmDelete: (itemId: number) => void;
|
|
closeConfirmDelete: () => void;
|
|
}
|
|
```
|
|
|
|
From src/client/routes/__root.tsx (modify for tab-aware layout):
|
|
```typescript
|
|
// Currently renders TotalsBar, Outlet, SlideOutPanel (item-specific), ConfirmDialog, FAB
|
|
// Need to: make SlideOutPanel and FAB context-aware (items vs candidates)
|
|
// Need to: add candidate panel handling alongside item panel
|
|
```
|
|
|
|
From src/client/routes/index.tsx (refactor to add tabs):
|
|
```typescript
|
|
// Currently: CollectionPage renders items grouped by category
|
|
// Becomes: HomePage with tab switcher, CollectionView (existing content) and PlanningView (new)
|
|
```
|
|
|
|
From src/client/hooks/useItems.ts (pattern to follow for hooks):
|
|
```typescript
|
|
// Uses apiGet, apiPost, apiPut, apiDelete from "../lib/api"
|
|
// Uses useQuery with queryKey: ["items"]
|
|
// Uses useMutation with onSuccess: invalidateQueries(["items"])
|
|
```
|
|
|
|
API endpoints from Plan 01:
|
|
- GET /api/threads (optional ?includeResolved=true)
|
|
- POST /api/threads { name }
|
|
- GET /api/threads/:id (returns thread with candidates)
|
|
- PUT /api/threads/:id { name }
|
|
- DELETE /api/threads/:id
|
|
- POST /api/threads/:id/candidates (form data with optional image)
|
|
- PUT /api/threads/:threadId/candidates/:candidateId
|
|
- DELETE /api/threads/:threadId/candidates/:candidateId
|
|
- POST /api/threads/:id/resolve { candidateId }
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Hooks, store, tab navigation, and thread list</name>
|
|
<files>src/client/hooks/useThreads.ts, src/client/hooks/useCandidates.ts, src/client/stores/uiStore.ts, src/client/components/ThreadTabs.tsx, src/client/components/ThreadCard.tsx, src/client/routes/index.tsx</files>
|
|
<action>
|
|
1. **Create `src/client/hooks/useThreads.ts`:** TanStack Query hooks following the useItems pattern.
|
|
- `useThreads(includeResolved = false)`: GET /api/threads, queryKey: ["threads", { includeResolved }]
|
|
- `useThread(threadId: number | null)`: GET /api/threads/:id, queryKey: ["threads", threadId], enabled when threadId != null
|
|
- `useCreateThread()`: POST /api/threads, onSuccess invalidates ["threads"]
|
|
- `useUpdateThread()`: PUT /api/threads/:id, onSuccess invalidates ["threads"]
|
|
- `useDeleteThread()`: DELETE /api/threads/:id, onSuccess invalidates ["threads"]
|
|
- `useResolveThread()`: POST /api/threads/:id/resolve, onSuccess invalidates ["threads"], ["items"], AND ["totals"] (critical for cross-tab freshness)
|
|
|
|
2. **Create `src/client/hooks/useCandidates.ts`:** TanStack Query mutation hooks.
|
|
- `useCreateCandidate(threadId: number)`: POST /api/threads/:id/candidates (use apiUpload for form data with optional image), onSuccess invalidates ["threads", threadId] and ["threads"] (list needs updated candidate count)
|
|
- `useUpdateCandidate(threadId: number)`: PUT endpoint, onSuccess invalidates ["threads", threadId]
|
|
- `useDeleteCandidate(threadId: number)`: DELETE endpoint, onSuccess invalidates ["threads", threadId] and ["threads"]
|
|
|
|
3. **Extend `src/client/stores/uiStore.ts`:** Add thread-specific UI state alongside existing item state. Add:
|
|
- `candidatePanelMode: "closed" | "add" | "edit"` (separate from item panelMode)
|
|
- `editingCandidateId: number | null`
|
|
- `confirmDeleteCandidateId: number | null`
|
|
- `resolveThreadId: number | null` and `resolveCandidateId: number | null` (for resolution confirm dialog)
|
|
- Actions: `openCandidateAddPanel()`, `openCandidateEditPanel(id)`, `closeCandidatePanel()`, `openConfirmDeleteCandidate(id)`, `closeConfirmDeleteCandidate()`, `openResolveDialog(threadId, candidateId)`, `closeResolveDialog()`
|
|
- Keep all existing item state unchanged.
|
|
|
|
4. **Create `src/client/components/ThreadTabs.tsx`:** Tab switcher component.
|
|
- Two tabs: "My Gear" and "Planning"
|
|
- Accept `active: "gear" | "planning"` and `onChange: (tab) => void` props
|
|
- Clean, minimal styling consistent with the app. Underline/highlight active tab.
|
|
|
|
5. **Create `src/client/components/ThreadCard.tsx`:** Card for thread list.
|
|
- Props: id, name, candidateCount, minPriceCents, maxPriceCents, createdAt, status
|
|
- Card layout matching ItemCard visual pattern (same rounded corners, shadows, padding)
|
|
- Name displayed prominently
|
|
- Pill/chip tags for: candidate count (e.g. "3 candidates"), creation date (formatted), price range (e.g. "$50-$120" or "No prices" if null)
|
|
- Click navigates to thread detail: `navigate({ to: "/threads/$threadId", params: { threadId: String(id) } })`
|
|
- Visual distinction for resolved threads (muted/grayed)
|
|
|
|
6. **Refactor `src/client/routes/index.tsx`:** Transform from CollectionPage into tabbed HomePage.
|
|
- Add `validateSearch` with `z.object({ tab: z.enum(["gear", "planning"]).catch("gear") })`
|
|
- Render ThreadTabs at the top
|
|
- When tab="gear": render existing collection content (extract into a CollectionView section or keep inline)
|
|
- When tab="planning": render PlanningView with thread list
|
|
- PlanningView shows: thread cards in a grid, "Create Thread" button (inline input or small form -- use a simple text input + button above the grid), empty state if no threads ("No planning threads yet. Start one to research your next purchase.")
|
|
- Toggle for "Show archived threads" that passes includeResolved to useThreads
|
|
- The FAB (floating add button) in __root.tsx should be context-aware: on gear tab it opens add item panel, on planning tab it could create a thread (or just hide -- use discretion)
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build</automated>
|
|
</verify>
|
|
<done>Home page has working tab navigation. Planning tab shows thread list with cards. Threads can be created. Clicking a thread card navigates to detail route (detail page built in Task 2).</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Thread detail page with candidate CRUD and resolution flow</name>
|
|
<files>src/client/routes/threads/$threadId.tsx, src/client/components/CandidateCard.tsx, src/client/components/CandidateForm.tsx, src/client/routes/__root.tsx</files>
|
|
<action>
|
|
1. **Create `src/client/components/CandidateCard.tsx`:** Card for candidates within a thread.
|
|
- Same visual style as ItemCard (same card shape, shadows, tag chips)
|
|
- Props: id, name, weightGrams, priceCents, categoryName, categoryEmoji, imageFilename, threadId
|
|
- Display: name, weight (formatted in g/kg), price (formatted in dollars from cents), category chip with emoji
|
|
- Image display if imageFilename present (use /uploads/ path)
|
|
- Edit button (opens candidate edit panel via uiStore)
|
|
- Delete button (opens confirm delete dialog via uiStore)
|
|
- "Pick as Winner" button -- a distinct action button (e.g. a crown/trophy icon or "Pick Winner" text button). Clicking opens the resolve confirmation dialog via `openResolveDialog(threadId, candidateId)`.
|
|
- Only show "Pick as Winner" when the thread is active (not resolved)
|
|
|
|
2. **Create `src/client/components/CandidateForm.tsx`:** Form for adding/editing candidates.
|
|
- Structurally similar to ItemForm but uses candidate hooks (useCreateCandidate, useUpdateCandidate)
|
|
- Same fields: name (required), weight (in grams, displayed as user-friendly input), price (in dollars, converted to cents for API), category (reuse CategoryPicker), notes, product URL, image upload (reuse ImageUpload component)
|
|
- mode="add": creates candidate via useCreateCandidate
|
|
- mode="edit": loads candidate data, updates via useUpdateCandidate
|
|
- On success: closes panel via closeCandidatePanel()
|
|
- Dollar-to-cents conversion on submit (same as ItemForm pattern)
|
|
|
|
3. **Create `src/client/routes/threads/$threadId.tsx`:** Thread detail page.
|
|
- File-based route using `createFileRoute("/threads/$threadId")`
|
|
- Parse threadId from route params
|
|
- Use `useThread(threadId)` to fetch thread with candidates
|
|
- Header: thread name, back link to `/?tab=planning`, thread status badge
|
|
- If thread is active: "Add Candidate" button that opens candidate add panel
|
|
- Candidate grid: same responsive grid as collection (1 col mobile, 2 md, 3 lg) using CandidateCard
|
|
- Empty state: "No candidates yet. Add your first candidate to start comparing."
|
|
- If thread is resolved: show which candidate won (highlight the winning candidate or show a banner)
|
|
- Loading and error states
|
|
|
|
4. **Update `src/client/routes/__root.tsx`:** Make the root layout handle both item and candidate panels/dialogs.
|
|
- Add a second SlideOutPanel instance for candidates (controlled by candidatePanelMode from uiStore). Title: "Add Candidate" or "Edit Candidate".
|
|
- Render CandidateForm inside the candidate panel.
|
|
- Add a resolution ConfirmDialog: when resolveThreadId is set in uiStore, show "Pick [candidate name] as winner? This will add it to your collection." On confirm, call useResolveThread mutation, on success close dialog and navigate back to `/?tab=planning`. On cancel, close dialog.
|
|
- Add a candidate delete ConfirmDialog: when confirmDeleteCandidateId is set, show delete confirmation. On confirm, call useDeleteCandidate.
|
|
- Keep existing item panel and delete dialog unchanged.
|
|
- The existing FAB should still work on the gear tab. On the threads detail page, the "Add Candidate" button handles adding, so the FAB can remain item-focused or be hidden on non-index routes.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build</automated>
|
|
</verify>
|
|
<done>Thread detail page renders candidates as cards. Candidates can be added/edited via slide-out panel and deleted with confirmation. Resolution flow works: pick winner -> confirmation dialog -> item created in collection -> thread archived. All existing Phase 1 functionality unchanged.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
```bash
|
|
# Build succeeds with no TypeScript errors
|
|
cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build
|
|
|
|
# All tests still pass (no regressions)
|
|
bun test --bail
|
|
```
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- Tab navigation switches between My Gear and Planning views
|
|
- Thread list shows cards with name, candidate count, date, price range
|
|
- New threads can be created from the Planning tab
|
|
- Thread detail page shows candidate cards in a grid
|
|
- Candidates can be added, edited, and deleted via slide-out panel
|
|
- Resolution confirmation dialog appears when picking a winner
|
|
- After resolution, thread is archived and item appears in collection
|
|
- Resolved threads hidden by default, visible with toggle
|
|
- All existing Phase 1 UI functionality unaffected
|
|
- Build succeeds with no errors
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/02-planning-threads/02-02-SUMMARY.md`
|
|
</output>
|