diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index e788712..ec6114c 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -45,11 +45,12 @@ Plans: 2. User can add candidate products to a thread with weight, price, notes, and product link 3. User can edit and remove candidates from an active thread 4. User can resolve a thread by selecting a winning candidate, which automatically creates a new item in their collection and archives the thread -**Plans**: TBD +**Plans:** 3 plans Plans: -- [ ] 02-01: TBD -- [ ] 02-02: TBD +- [ ] 02-01-PLAN.md — Backend API: thread/candidate CRUD, resolution transaction, with TDD +- [ ] 02-02-PLAN.md — Frontend: tab navigation, thread list, candidate UI, resolution flow +- [ ] 02-03-PLAN.md — Visual verification checkpoint ### Phase 3: Setups and Dashboard **Goal**: Users can compose named loadouts from their collection items with live totals, and navigate the app through a dashboard home page @@ -74,5 +75,5 @@ Phases execute in numeric order: 1 -> 2 -> 3 | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| | 1. Foundation and Collection | 4/4 | Complete | 2026-03-14 | -| 2. Planning Threads | 0/0 | Not started | - | +| 2. Planning Threads | 0/3 | In progress | - | | 3. Setups and Dashboard | 0/0 | Not started | - | diff --git a/.planning/phases/02-planning-threads/02-01-PLAN.md b/.planning/phases/02-planning-threads/02-01-PLAN.md new file mode 100644 index 0000000..3c4aaf1 --- /dev/null +++ b/.planning/phases/02-planning-threads/02-01-PLAN.md @@ -0,0 +1,263 @@ +--- +phase: 02-planning-threads +plan: 01 +type: tdd +wave: 1 +depends_on: [] +files_modified: + - src/db/schema.ts + - src/shared/schemas.ts + - src/shared/types.ts + - src/server/services/thread.service.ts + - src/server/routes/threads.ts + - src/server/index.ts + - tests/helpers/db.ts + - tests/services/thread.service.test.ts + - tests/routes/threads.test.ts +autonomous: true +requirements: [THRD-01, THRD-02, THRD-03, THRD-04] + +must_haves: + truths: + - "POST /api/threads creates a thread and returns it with 201" + - "GET /api/threads returns active threads with candidate count and price range" + - "POST /api/threads/:id/candidates adds a candidate to a thread" + - "PUT/DELETE /api/threads/:threadId/candidates/:id updates/removes candidates" + - "POST /api/threads/:id/resolve atomically creates a collection item from candidate data and archives the thread" + - "GET /api/threads?includeResolved=true includes archived threads" + - "Resolved thread no longer appears in default active thread list" + artifacts: + - path: "src/db/schema.ts" + provides: "threads and threadCandidates table definitions" + contains: "threads" + - path: "src/shared/schemas.ts" + provides: "Zod schemas for thread and candidate validation" + contains: "createThreadSchema" + - path: "src/shared/types.ts" + provides: "TypeScript types for threads and candidates" + contains: "Thread" + - path: "src/server/services/thread.service.ts" + provides: "Thread and candidate business logic with resolution transaction" + exports: ["getAllThreads", "getThreadWithCandidates", "createThread", "resolveThread"] + - path: "src/server/routes/threads.ts" + provides: "Hono API routes for threads and candidates" + exports: ["threadRoutes"] + - path: "tests/services/thread.service.test.ts" + provides: "Unit tests for thread service" + min_lines: 80 + - path: "tests/routes/threads.test.ts" + provides: "Integration tests for thread API" + min_lines: 60 + key_links: + - from: "src/server/routes/threads.ts" + to: "src/server/services/thread.service.ts" + via: "service function calls" + pattern: "import.*thread\\.service" + - from: "src/server/services/thread.service.ts" + to: "src/db/schema.ts" + via: "Drizzle queries on threads/threadCandidates tables" + pattern: "from.*schema" + - from: "src/server/services/thread.service.ts" + to: "src/server/services/item.service.ts" + via: "resolveThread uses items table to create collection item" + pattern: "items" + - from: "src/server/index.ts" + to: "src/server/routes/threads.ts" + via: "app.route mount" + pattern: "threadRoutes" +--- + + +Build the complete backend API for planning threads: database schema, shared validation schemas, service layer with thread resolution transaction, and Hono API routes. All via TDD. + +Purpose: Establish the data model and API that the frontend (Plan 02) will consume. Thread resolution -- the atomic operation that creates a collection item from a candidate and archives the thread -- is the core business logic of this phase. + +Output: Working API endpoints for thread CRUD, candidate CRUD, and thread resolution, with comprehensive tests. + + + +@/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md +@/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/02-planning-threads/02-RESEARCH.md + + + + +From src/db/schema.ts (existing tables to extend): +```typescript +export const categories = sqliteTable("categories", { + id: integer("id").primaryKey({ autoIncrement: true }), + name: text("name").notNull().unique(), + emoji: text("emoji").notNull().default("\u{1F4E6}"), + createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => new Date()), +}); + +export const items = sqliteTable("items", { + id: integer("id").primaryKey({ autoIncrement: true }), + name: text("name").notNull(), + weightGrams: real("weight_grams"), + priceCents: integer("price_cents"), + categoryId: integer("category_id").notNull().references(() => categories.id), + notes: text("notes"), + productUrl: text("product_url"), + imageFilename: text("image_filename"), + createdAt: integer("created_at", { mode: "timestamp" }).notNull().$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().$defaultFn(() => new Date()), +}); +``` + +From src/shared/schemas.ts (existing pattern to follow): +```typescript +export const createItemSchema = z.object({ + name: z.string().min(1, "Name is required"), + weightGrams: z.number().nonnegative().optional(), + priceCents: z.number().int().nonnegative().optional(), + categoryId: z.number().int().positive(), + notes: z.string().optional(), + productUrl: z.string().url().optional().or(z.literal("")), +}); +``` + +From src/server/services/item.service.ts (DI pattern): +```typescript +type Db = typeof prodDb; +export function createItem(db: Db = prodDb, data: ...) { ... } +``` + +From src/server/index.ts (route mounting): +```typescript +app.route("/api/items", itemRoutes); +``` + +From tests/helpers/db.ts (test DB pattern): +```typescript +export function createTestDb() { + const sqlite = new Database(":memory:"); + sqlite.run("PRAGMA foreign_keys = ON"); + // CREATE TABLE statements... + const db = drizzle(sqlite, { schema }); + db.insert(schema.categories).values({ name: "Uncategorized", emoji: "\u{1F4E6}" }).run(); + return db; +} +``` + + + + + + + Task 1: Schema, shared schemas, test helper, and service layer with TDD + src/db/schema.ts, src/shared/schemas.ts, src/shared/types.ts, tests/helpers/db.ts, src/server/services/thread.service.ts, tests/services/thread.service.test.ts + + - createThread: creates thread with name, returns thread with id/status/timestamps + - getAllThreads: returns active threads with candidateCount, minPriceCents, maxPriceCents; excludes resolved by default; includes resolved when includeResolved=true + - getThreadWithCandidates: returns thread with nested candidates array including category info; returns null for non-existent thread + - createCandidate: adds candidate to thread with all item-compatible fields (name, weightGrams, priceCents, categoryId, notes, productUrl, imageFilename) + - updateCandidate: updates candidate fields, returns updated candidate; returns null for non-existent + - deleteCandidate: removes candidate, returns deleted candidate; returns null for non-existent + - updateThread: updates thread name + - deleteThread: removes thread and cascading candidates + - resolveThread: atomically creates collection item from candidate data and sets thread status to "resolved" with resolvedCandidateId; fails if thread not active; fails if candidate not in thread; fails if candidate not found + + + **RED phase first:** + + 1. Add `threads` and `threadCandidates` tables to `src/db/schema.ts` following the existing pattern. Schema per RESEARCH.md Pattern 1: threads has id, name, status (default "active"), resolvedCandidateId, createdAt, updatedAt. threadCandidates has id, threadId (FK to threads with cascade delete), and the same fields as items (name, weightGrams, priceCents, categoryId FK to categories, notes, productUrl, imageFilename, createdAt, updatedAt). + + 2. Add Zod schemas to `src/shared/schemas.ts`: createThreadSchema (name required), updateThreadSchema (name optional), createCandidateSchema (same shape as createItemSchema), updateCandidateSchema (partial of create), resolveThreadSchema (candidateId required). + + 3. Add types to `src/shared/types.ts`: Thread (inferred from Drizzle threads table), ThreadCandidate (inferred from Drizzle threadCandidates table), CreateThread, UpdateThread, CreateCandidate, UpdateCandidate, ResolveThread (from Zod schemas). + + 4. Update `tests/helpers/db.ts`: Add CREATE TABLE statements for `threads` and `thread_candidates` matching the Drizzle schema (use same pattern as existing items/categories tables). + + 5. Write `tests/services/thread.service.test.ts` with failing tests covering all behaviors listed above. Follow the pattern from `tests/services/item.service.test.ts`. Each test uses `createTestDb()` for isolation. + + **GREEN phase:** + + 6. Implement `src/server/services/thread.service.ts` following the DI pattern from item.service.ts (db as first param with prodDb default). Functions: getAllThreads (with subquery aggregates for candidateCount and price range), getThreadWithCandidates (with candidate+category join), createThread, updateThread, deleteThread (with image cleanup collection), createCandidate, updateCandidate, deleteCandidate, resolveThread (transactional: validate thread is active + candidate belongs to thread, insert into items from candidate data, update thread status to "resolved" and set resolvedCandidateId). On resolution, if candidate's categoryId no longer exists, fall back to categoryId=1 (Uncategorized). On resolution, if candidate has imageFilename, copy the file to a new filename so the item has an independent image copy. + + All tests must pass after implementation. + + + cd /home/jean-luc-makiola/Development/projects/GearBox && bun test tests/services/thread.service.test.ts --bail + + All thread service unit tests pass. Schema, shared schemas, types, and test helper updated. Service layer implements full thread + candidate CRUD and transactional resolution. + + + + Task 2: Thread API routes with integration tests + src/server/routes/threads.ts, src/server/index.ts, tests/routes/threads.test.ts + + - POST /api/threads with valid body returns 201 + thread object + - POST /api/threads with empty name returns 400 + - GET /api/threads returns array of active threads with metadata + - GET /api/threads?includeResolved=true includes archived threads + - GET /api/threads/:id returns thread with candidates + - GET /api/threads/:id for non-existent returns 404 + - PUT /api/threads/:id updates thread name + - DELETE /api/threads/:id removes thread + - POST /api/threads/:id/candidates adds candidate, returns 201 + - PUT /api/threads/:threadId/candidates/:candidateId updates candidate + - DELETE /api/threads/:threadId/candidates/:candidateId removes candidate + - POST /api/threads/:id/resolve with valid candidateId returns 200 + created item + - POST /api/threads/:id/resolve on already-resolved thread returns 400 + - POST /api/threads/:id/resolve with wrong candidateId returns 400 + + + **RED phase first:** + + 1. Write `tests/routes/threads.test.ts` following the pattern from `tests/routes/items.test.ts`. Use `createTestDb()`, inject test DB via Hono context middleware (`c.set("db", testDb)`), and use `app.request()` for integration tests. Cover all behaviors above. + + **GREEN phase:** + + 2. Create `src/server/routes/threads.ts` as a Hono app. Follow the exact pattern from `src/server/routes/items.ts`: + - Use `zValidator("json", schema)` for request body validation + - Get DB from `c.get("db") ?? prodDb` for testability + - Thread CRUD: GET / (with optional ?includeResolved query param), POST /, GET /:id, PUT /:id, DELETE /:id + - Candidate CRUD nested under thread: POST /:id/candidates (with image upload support via formData, same pattern as items), PUT /:threadId/candidates/:candidateId, DELETE /:threadId/candidates/:candidateId (with image file cleanup) + - Resolution: POST /:id/resolve with resolveThreadSchema validation + - Return appropriate status codes (201 for creation, 200 for success, 400 for validation/business errors, 404 for not found) + + 3. Mount routes in `src/server/index.ts`: `app.route("/api/threads", threadRoutes)` alongside existing routes. + + For candidate image upload: follow the same pattern as the items image upload route. Candidates need a POST endpoint that accepts multipart form data with an optional image file. Use the same file validation (type/size) and storage pattern. + + All integration tests must pass. + + + cd /home/jean-luc-makiola/Development/projects/GearBox && bun test tests/routes/threads.test.ts --bail + + All thread API integration tests pass. Routes mounted in server index. Full thread and candidate CRUD available via REST API. Resolution endpoint creates collection item and archives thread. + + + + + +```bash +# All tests pass (Phase 1 + Phase 2) +cd /home/jean-luc-makiola/Development/projects/GearBox && bun test --bail + +# Thread API responds +curl -s http://localhost:3000/api/threads | head -1 +``` + + + +- All thread service unit tests pass +- All thread API integration tests pass +- All existing Phase 1 tests still pass (no regressions) +- POST /api/threads creates a thread +- POST /api/threads/:id/candidates adds a candidate +- POST /api/threads/:id/resolve creates a collection item and archives the thread +- Thread resolution is transactional (atomic create item + archive thread) + + + +After completion, create `.planning/phases/02-planning-threads/02-01-SUMMARY.md` + diff --git a/.planning/phases/02-planning-threads/02-02-PLAN.md b/.planning/phases/02-planning-threads/02-02-PLAN.md new file mode 100644 index 0000000..4ca3a3c --- /dev/null +++ b/.planning/phases/02-planning-threads/02-02-PLAN.md @@ -0,0 +1,279 @@ +--- +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" +--- + + +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. + + + +@/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md +@/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + +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 } + + + + + + + Task 1: Hooks, store, tab navigation, and thread list + 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 + + 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) + + + cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build + + 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). + + + + Task 2: Thread detail page with candidate CRUD and resolution flow + src/client/routes/threads/$threadId.tsx, src/client/components/CandidateCard.tsx, src/client/components/CandidateForm.tsx, src/client/routes/__root.tsx + + 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. + + + cd /home/jean-luc-makiola/Development/projects/GearBox && bun run build + + 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. + + + + + +```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 +``` + + + +- 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 + + + +After completion, create `.planning/phases/02-planning-threads/02-02-SUMMARY.md` + diff --git a/.planning/phases/02-planning-threads/02-03-PLAN.md b/.planning/phases/02-planning-threads/02-03-PLAN.md new file mode 100644 index 0000000..a751246 --- /dev/null +++ b/.planning/phases/02-planning-threads/02-03-PLAN.md @@ -0,0 +1,110 @@ +--- +phase: 02-planning-threads +plan: 03 +type: execute +wave: 3 +depends_on: [02-02] +files_modified: [] +autonomous: false +requirements: [THRD-01, THRD-02, THRD-03, THRD-04] + +must_haves: + truths: + - "User can create a planning thread and see it in the list" + - "User can add candidates with weight, price, category, notes, and product link" + - "User can edit and remove candidates" + - "User can resolve a thread by picking a winner that appears in their collection" + artifacts: [] + key_links: [] +--- + + +Visual verification of the complete planning threads feature. Confirm all user-facing behaviors work end-to-end in the browser. + +Purpose: Catch visual, interaction, and integration issues that automated tests cannot detect. + +Output: Confirmation that Phase 2 requirements are met from the user's perspective. + + + +@/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md +@/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md + + + +@.planning/ROADMAP.md +@.planning/phases/02-planning-threads/02-CONTEXT.md +@.planning/phases/02-planning-threads/02-01-SUMMARY.md +@.planning/phases/02-planning-threads/02-02-SUMMARY.md + + + + + + Task 1: Visual verification of complete planning threads feature + + + Verify the complete planning threads feature in the browser. + + What was built: Tab navigation between My Gear and Planning views, thread CRUD with card-based list, candidate CRUD with slide-out panel, and thread resolution flow with confirmation dialog. + + Start the dev server if not running: `bun run dev` + Open http://localhost:5173 + + **1. Tab Navigation (THRD-01)** + - Verify "My Gear" and "Planning" tabs are visible + - Click "Planning" tab -- URL should update to /?tab=planning + - Click "My Gear" tab -- should show your gear collection + - Verify top navigation bar is always visible + + **2. Thread Creation (THRD-01)** + - On the Planning tab, create a new thread (e.g. "Helmet") + - Verify it appears as a card in the thread list + - Card should show: name, "0 candidates", creation date + - Create a second thread to verify list ordering (most recent first) + + **3. Candidate Management (THRD-02, THRD-03)** + - Click a thread card to open thread detail page + - Verify back navigation to Planning tab works + - Add a candidate via slide-out panel with: name, weight, price, category, notes, product URL + - Verify candidate appears as a card in the grid + - Add 2-3 more candidates with different prices + - Verify the thread card on the list page shows updated candidate count and price range + - Edit a candidate (change price or name) -- verify changes saved + - Delete a candidate -- verify confirmation dialog and removal + + **4. Thread Resolution (THRD-04)** + - On a thread with multiple candidates, click "Pick Winner" on one + - Verify confirmation dialog: "Pick [X] as winner? This will add it to your collection." + - Confirm the resolution + - Verify thread disappears from active thread list + - Toggle "Show archived" -- verify resolved thread appears (visually distinct) + - Switch to "My Gear" tab -- verify the winning candidate appears as a new collection item with correct data + + **5. Visual Consistency** + - Thread cards match the visual style of item cards (same shadows, rounded corners) + - Candidate cards match item card style + - Pill/chip tags are consistent with existing tag pattern + - Slide-out panel for candidates looks like the item panel + - Empty states are present and helpful + + User confirms all checks pass by typing "approved" + All four THRD requirements verified by user in browser. Visual consistency confirmed. Resolution flow works end-to-end. + + + + + +User confirms all four THRD requirements work visually and interactively. + + + +- All four THRD requirements verified by user in browser +- Visual consistency with Phase 1 collection UI +- Resolution flow creates item and archives thread correctly +- No regressions to existing gear collection functionality + + + +After completion, create `.planning/phases/02-planning-threads/02-03-SUMMARY.md` +