docs(03): research phase domain
This commit is contained in:
540
.planning/phases/03-setups-and-dashboard/03-RESEARCH.md
Normal file
540
.planning/phases/03-setups-and-dashboard/03-RESEARCH.md
Normal file
@@ -0,0 +1,540 @@
|
|||||||
|
# Phase 3: Setups and Dashboard - Research
|
||||||
|
|
||||||
|
**Researched:** 2026-03-15
|
||||||
|
**Domain:** Full-stack CRUD (Drizzle + Hono + React) with navigation restructure
|
||||||
|
**Confidence:** HIGH
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Phase 3 adds two features: (1) named setups (loadouts) that compose collection items with live weight/cost totals, and (2) a dashboard home page with summary cards. The codebase has strong established patterns from Phases 1 and 2 -- database schema in Drizzle, service layer with DB injection, Hono routes with Zod validation, TanStack Query hooks, and Zustand UI state. This phase follows identical patterns with one significant difference: the many-to-many relationship between items and setups via a junction table (`setup_items`).
|
||||||
|
|
||||||
|
The navigation restructure moves the current `/` (gear + planning tabs) to `/collection` and replaces `/` with a dashboard. This requires moving the existing `index.tsx` route content to a new `collection/index.tsx` route, creating new `/setups` routes, and making TotalsBar route-aware for contextual stats.
|
||||||
|
|
||||||
|
**Primary recommendation:** Follow the exact thread CRUD pattern for setups (schema, service, routes, hooks, components), add a `setup_items` junction table for the many-to-many relationship, compute setup totals server-side via SQL aggregation, and restructure routes with TanStack Router file-based routing.
|
||||||
|
|
||||||
|
<user_constraints>
|
||||||
|
## User Constraints (from CONTEXT.md)
|
||||||
|
|
||||||
|
### Locked Decisions
|
||||||
|
- Setup item selection: Checklist picker in SlideOutPanel, items grouped by category with emoji headers, toggle via checkboxes, "Done" button to confirm, items shared across setups
|
||||||
|
- Setup creation: Inline name input + create button (same pattern as thread creation)
|
||||||
|
- Setup display: Card grid grouped by category reusing ItemCard with small x remove icon, per-category subtotals in CategoryHeader
|
||||||
|
- Setup totals: Sticky bar at top showing setup name, item count, total weight, total cost (reuses TotalsBar pattern)
|
||||||
|
- Dashboard cards: Three equal-width cards (Collection, Planning, Setups) side by side on desktop, stacking on mobile, with summary stats on each card
|
||||||
|
- Dashboard header: "GearBox" title only, no welcome message
|
||||||
|
- Navigation: `/` = Dashboard, `/collection` = Gear|Planning tabs, `/setups` = Setups list, `/setups/:id` = Setup detail
|
||||||
|
- "GearBox" title in TotalsBar is always a clickable link back to dashboard
|
||||||
|
- No breadcrumbs or back arrows -- GearBox title link is the only back navigation
|
||||||
|
|
||||||
|
### Claude's Discretion
|
||||||
|
- Setup list card design (stats/info per setup card beyond name and totals)
|
||||||
|
- Exact Tailwind styling, spacing, and transitions for dashboard cards
|
||||||
|
- Setup detail page layout specifics beyond card grid + sticky totals
|
||||||
|
- How checklist picker handles large number of items (scroll behavior)
|
||||||
|
- Error states and loading skeletons
|
||||||
|
|
||||||
|
### Deferred Ideas (OUT OF SCOPE)
|
||||||
|
None -- discussion stayed within phase scope
|
||||||
|
</user_constraints>
|
||||||
|
|
||||||
|
<phase_requirements>
|
||||||
|
## Phase Requirements
|
||||||
|
|
||||||
|
| ID | Description | Research Support |
|
||||||
|
|----|-------------|-----------------|
|
||||||
|
| SETP-01 | User can create named setups (e.g. "Summer Bikepacking") | Setup CRUD: schema, service, routes, hooks -- follows thread creation pattern exactly |
|
||||||
|
| SETP-02 | User can add/remove collection items to a setup | Junction table `setup_items`, checklist picker in SlideOutPanel, batch sync endpoint |
|
||||||
|
| SETP-03 | User can see total weight and cost for a setup | Server-side SQL aggregation via `setup_items` JOIN `items`, setup totals endpoint |
|
||||||
|
| DASH-01 | User sees dashboard home page with cards linking to collection, threads, and setups | New `/` route with three summary cards, existing content moves to `/collection` |
|
||||||
|
</phase_requirements>
|
||||||
|
|
||||||
|
## Standard Stack
|
||||||
|
|
||||||
|
### Core (already installed, no new dependencies)
|
||||||
|
| Library | Version | Purpose | Why Standard |
|
||||||
|
|---------|---------|---------|--------------|
|
||||||
|
| drizzle-orm | 0.45.1 | Database schema + queries | Already used for items, threads |
|
||||||
|
| hono | 4.12.8 | API routes | Already used for all server routes |
|
||||||
|
| @hono/zod-validator | 0.7.6 | Request validation | Already used on all routes |
|
||||||
|
| zod | 4.3.6 | Schema validation | Already used in shared schemas |
|
||||||
|
| @tanstack/react-query | 5.90.21 | Data fetching + cache | Already used for items, threads, totals |
|
||||||
|
| @tanstack/react-router | 1.167.0 | File-based routing | Already used, auto-generates route tree |
|
||||||
|
| zustand | 5.0.11 | UI state | Already used for panel/dialog state |
|
||||||
|
| tailwindcss | 4.2.1 | Styling | Already used throughout |
|
||||||
|
|
||||||
|
### Supporting
|
||||||
|
No new libraries needed. Phase 3 uses only existing dependencies.
|
||||||
|
|
||||||
|
### Alternatives Considered
|
||||||
|
None -- all decisions are locked to existing stack patterns.
|
||||||
|
|
||||||
|
**Installation:**
|
||||||
|
```bash
|
||||||
|
# No new packages needed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Patterns
|
||||||
|
|
||||||
|
### New Files Structure
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
db/
|
||||||
|
schema.ts # ADD: setups + setup_items tables
|
||||||
|
shared/
|
||||||
|
schemas.ts # ADD: setup Zod schemas
|
||||||
|
types.ts # ADD: Setup + SetupItem types
|
||||||
|
server/
|
||||||
|
routes/
|
||||||
|
setups.ts # NEW: setup CRUD routes
|
||||||
|
services/
|
||||||
|
setup.service.ts # NEW: setup business logic
|
||||||
|
index.ts # UPDATE: mount setup routes
|
||||||
|
client/
|
||||||
|
routes/
|
||||||
|
index.tsx # REWRITE: dashboard page
|
||||||
|
collection/
|
||||||
|
index.tsx # NEW: moved from current index.tsx (gear + planning tabs)
|
||||||
|
setups/
|
||||||
|
index.tsx # NEW: setups list page
|
||||||
|
$setupId.tsx # NEW: setup detail page
|
||||||
|
hooks/
|
||||||
|
useSetups.ts # NEW: setup query/mutation hooks
|
||||||
|
components/
|
||||||
|
SetupCard.tsx # NEW: setup list card
|
||||||
|
ItemPicker.tsx # NEW: checklist picker for SlideOutPanel
|
||||||
|
DashboardCard.tsx # NEW: dashboard summary card
|
||||||
|
stores/
|
||||||
|
uiStore.ts # UPDATE: add setup-related UI state
|
||||||
|
tests/
|
||||||
|
helpers/
|
||||||
|
db.ts # UPDATE: add setups + setup_items tables
|
||||||
|
services/
|
||||||
|
setup.service.test.ts # NEW: setup service tests
|
||||||
|
routes/
|
||||||
|
setups.test.ts # NEW: setup route tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 1: Many-to-Many Junction Table
|
||||||
|
**What:** `setup_items` links setups to items (items can belong to multiple setups)
|
||||||
|
**When to use:** This is the only new DB pattern in this phase
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In src/db/schema.ts
|
||||||
|
export const setups = sqliteTable("setups", {
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
|
updatedAt: integer("updated_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const setupItems = sqliteTable("setup_items", {
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
setupId: integer("setup_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => setups.id, { onDelete: "cascade" }),
|
||||||
|
itemId: integer("item_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => items.id, { onDelete: "cascade" }),
|
||||||
|
addedAt: integer("added_at", { mode: "timestamp" })
|
||||||
|
.notNull()
|
||||||
|
.$defaultFn(() => new Date()),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key design decisions:**
|
||||||
|
- `onDelete: "cascade"` on both FKs: deleting a setup removes its setup_items; deleting a collection item removes it from all setups
|
||||||
|
- No unique constraint on (setupId, itemId) at DB level -- enforce in service layer for better error messages
|
||||||
|
- `addedAt` for potential future ordering, but not critical for v1
|
||||||
|
|
||||||
|
### Pattern 2: Batch Sync for Setup Items
|
||||||
|
**What:** Instead of individual add/remove endpoints, use a single "sync" endpoint that receives the full list of selected item IDs
|
||||||
|
**When to use:** When the checklist picker submits all selections at once via "Done" button
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In setup.service.ts
|
||||||
|
export function syncSetupItems(db: Db = prodDb, setupId: number, itemIds: number[]) {
|
||||||
|
return db.transaction((tx) => {
|
||||||
|
// Delete all existing setup_items for this setup
|
||||||
|
tx.delete(setupItems).where(eq(setupItems.setupId, setupId)).run();
|
||||||
|
// Insert new ones
|
||||||
|
if (itemIds.length > 0) {
|
||||||
|
tx.insert(setupItems)
|
||||||
|
.values(itemIds.map(itemId => ({ setupId, itemId })))
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why batch sync over individual add/remove:**
|
||||||
|
- The checklist picker has a "Done" button that submits all at once
|
||||||
|
- Simpler than tracking individual toggles
|
||||||
|
- Single transaction = atomic operation
|
||||||
|
- Still need a single-item remove for the x button on cards (separate endpoint)
|
||||||
|
|
||||||
|
### Pattern 3: Setup Totals via SQL Aggregation
|
||||||
|
**What:** Compute setup weight/cost totals server-side by joining `setup_items` with `items`
|
||||||
|
**When to use:** For the setup detail page totals bar and setup list cards
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// In setup.service.ts
|
||||||
|
export function getSetupWithItems(db: Db = prodDb, setupId: number) {
|
||||||
|
const setup = db.select().from(setups).where(eq(setups.id, setupId)).get();
|
||||||
|
if (!setup) return null;
|
||||||
|
|
||||||
|
const itemList = db
|
||||||
|
.select({
|
||||||
|
id: items.id,
|
||||||
|
name: items.name,
|
||||||
|
weightGrams: items.weightGrams,
|
||||||
|
priceCents: items.priceCents,
|
||||||
|
categoryId: items.categoryId,
|
||||||
|
notes: items.notes,
|
||||||
|
productUrl: items.productUrl,
|
||||||
|
imageFilename: items.imageFilename,
|
||||||
|
createdAt: items.createdAt,
|
||||||
|
updatedAt: items.updatedAt,
|
||||||
|
categoryName: categories.name,
|
||||||
|
categoryEmoji: categories.emoji,
|
||||||
|
})
|
||||||
|
.from(setupItems)
|
||||||
|
.innerJoin(items, eq(setupItems.itemId, items.id))
|
||||||
|
.innerJoin(categories, eq(items.categoryId, categories.id))
|
||||||
|
.where(eq(setupItems.setupId, setupId))
|
||||||
|
.all();
|
||||||
|
|
||||||
|
return { ...setup, items: itemList };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Totals are computed client-side from the items array** (not a separate endpoint) since the setup detail page already fetches all items. This avoids an extra API call and keeps totals always in sync with displayed data.
|
||||||
|
|
||||||
|
For the setup list cards (showing totals per setup), use a SQL subquery:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export function getAllSetups(db: Db = prodDb) {
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
id: setups.id,
|
||||||
|
name: setups.name,
|
||||||
|
createdAt: setups.createdAt,
|
||||||
|
updatedAt: setups.updatedAt,
|
||||||
|
itemCount: sql<number>`(
|
||||||
|
SELECT COUNT(*) FROM setup_items
|
||||||
|
WHERE setup_items.setup_id = setups.id
|
||||||
|
)`.as("item_count"),
|
||||||
|
totalWeight: sql<number>`COALESCE((
|
||||||
|
SELECT SUM(items.weight_grams) FROM setup_items
|
||||||
|
INNER JOIN items ON setup_items.item_id = items.id
|
||||||
|
WHERE setup_items.setup_id = setups.id
|
||||||
|
), 0)`.as("total_weight"),
|
||||||
|
totalCost: sql<number>`COALESCE((
|
||||||
|
SELECT SUM(items.price_cents) FROM setup_items
|
||||||
|
INNER JOIN items ON setup_items.item_id = items.id
|
||||||
|
WHERE setup_items.setup_id = setups.id
|
||||||
|
), 0)`.as("total_cost"),
|
||||||
|
})
|
||||||
|
.from(setups)
|
||||||
|
.orderBy(desc(setups.updatedAt))
|
||||||
|
.all();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: Route-Aware TotalsBar
|
||||||
|
**What:** Make TotalsBar show different content based on the current route
|
||||||
|
**When to use:** Dashboard shows "GearBox" title only; collection shows global totals; setup detail shows setup-specific totals
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// TotalsBar accepts optional props to override default behavior
|
||||||
|
interface TotalsBarProps {
|
||||||
|
title?: string; // Override the title text
|
||||||
|
stats?: TotalsStat[]; // Override stats display (empty = title only)
|
||||||
|
linkTo?: string; // Make title a link (defaults to "/")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Approach:** Rather than making TotalsBar read route state internally, have each page pass the appropriate stats. This keeps TotalsBar a pure presentational component.
|
||||||
|
|
||||||
|
- Dashboard page: `<TotalsBar />` (title only, no stats, no link since already on dashboard)
|
||||||
|
- Collection page: `<TotalsBar stats={globalStats} />` (current behavior)
|
||||||
|
- Setup detail: `<TotalsBar title={setupName} stats={setupStats} />`
|
||||||
|
- Thread detail: keep current behavior
|
||||||
|
|
||||||
|
The "GearBox" title becomes a `<Link to="/">` on all pages except the dashboard itself.
|
||||||
|
|
||||||
|
### Pattern 5: TanStack Router File-Based Routing
|
||||||
|
**What:** New route files auto-register via TanStack Router plugin
|
||||||
|
**When to use:** Creating `/collection`, `/setups`, `/setups/:id` routes
|
||||||
|
|
||||||
|
```
|
||||||
|
src/client/routes/
|
||||||
|
__root.tsx # Existing root layout
|
||||||
|
index.tsx # REWRITE: Dashboard
|
||||||
|
collection/
|
||||||
|
index.tsx # NEW: current index.tsx content moves here
|
||||||
|
setups/
|
||||||
|
index.tsx # NEW: setups list
|
||||||
|
$setupId.tsx # NEW: setup detail
|
||||||
|
threads/
|
||||||
|
$threadId.tsx # Existing, unchanged
|
||||||
|
```
|
||||||
|
|
||||||
|
The TanStack Router plugin will auto-generate `routeTree.gen.ts` with the new routes. Route files use `createFileRoute("/path")` -- the path must match the file location.
|
||||||
|
|
||||||
|
### Pattern 6: Dashboard Summary Stats
|
||||||
|
**What:** Dashboard cards need aggregate data from multiple domains
|
||||||
|
**When to use:** The dashboard page
|
||||||
|
|
||||||
|
The dashboard needs: collection item count + total weight + total cost, active thread count, setup count. Two approaches:
|
||||||
|
|
||||||
|
**Recommended: Aggregate on client from existing hooks**
|
||||||
|
- `useTotals()` already provides collection stats
|
||||||
|
- `useThreads()` provides thread list (count from `.length`)
|
||||||
|
- New `useSetups()` provides setup list (count from `.length`)
|
||||||
|
|
||||||
|
This avoids a new dashboard-specific API endpoint. Three parallel queries that TanStack Query handles efficiently with its deduplication.
|
||||||
|
|
||||||
|
### Anti-Patterns to Avoid
|
||||||
|
- **Don't add setup state to Zustand beyond UI concerns:** Setup data belongs in TanStack Query cache, not Zustand. Zustand is only for panel open/close state.
|
||||||
|
- **Don't compute totals in the component loop:** Use SQL aggregation for list views, and derive from the fetched items array for detail views.
|
||||||
|
- **Don't create a separate "dashboard totals" API:** Reuse existing totals endpoint + new setup/thread counts from their list endpoints.
|
||||||
|
|
||||||
|
## Don't Hand-Roll
|
||||||
|
|
||||||
|
| Problem | Don't Build | Use Instead | Why |
|
||||||
|
|---------|-------------|-------------|-----|
|
||||||
|
| Many-to-many sync | Custom diff logic | Delete-all + re-insert in transaction | Simpler, atomic, handles edge cases |
|
||||||
|
| Route generation | Manual route registration | TanStack Router file-based plugin | Already configured, auto-generates types |
|
||||||
|
| Data fetching cache | Custom cache | TanStack Query | Already used, handles invalidation |
|
||||||
|
| SQL totals aggregation | Client-side loops over raw data | SQL COALESCE + SUM subqueries | Consistent with existing totals.service.ts pattern |
|
||||||
|
|
||||||
|
**Key insight:** Every pattern in this phase has a direct precedent in Phases 1-2. The only new concept is the junction table.
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
### Pitfall 1: Stale Setup Totals After Item Edit
|
||||||
|
**What goes wrong:** User edits a collection item's weight/price, but setup detail page shows old totals
|
||||||
|
**Why it happens:** Setup query cache not invalidated when items change
|
||||||
|
**How to avoid:** In `useUpdateItem` and `useDeleteItem` mutation `onSuccess`, also invalidate `["setups"]` query key
|
||||||
|
**Warning signs:** Totals don't update until page refresh
|
||||||
|
|
||||||
|
### Pitfall 2: Orphaned Setup Items After Collection Item Deletion
|
||||||
|
**What goes wrong:** Deleting a collection item leaves dangling references in `setup_items`
|
||||||
|
**Why it happens:** Missing cascade or no FK constraint
|
||||||
|
**How to avoid:** `onDelete: "cascade"` on `setupItems.itemId` FK -- already specified in schema pattern above
|
||||||
|
**Warning signs:** Setup shows items that no longer exist in collection
|
||||||
|
|
||||||
|
### Pitfall 3: Route Migration Breaking Existing Links
|
||||||
|
**What goes wrong:** Moving `/` content to `/collection` breaks hardcoded links like the "Back to planning" link in thread detail
|
||||||
|
**Why it happens:** Thread detail page currently links to `{ to: "/", search: { tab: "planning" } }`
|
||||||
|
**How to avoid:** Update ALL internal links: thread detail back link, resolution dialog redirect, floating add button visibility check
|
||||||
|
**Warning signs:** Clicking links after restructure navigates to wrong page
|
||||||
|
|
||||||
|
### Pitfall 4: TanStack Router Route Tree Not Regenerating
|
||||||
|
**What goes wrong:** New route files exist but routes 404
|
||||||
|
**Why it happens:** Vite dev server needs restart, or route file doesn't export `Route` correctly
|
||||||
|
**How to avoid:** Use `createFileRoute("/correct/path")` matching the file location. Restart dev server after adding new route directories.
|
||||||
|
**Warning signs:** `routeTree.gen.ts` doesn't include new routes
|
||||||
|
|
||||||
|
### Pitfall 5: Floating Add Button Showing on Wrong Pages
|
||||||
|
**What goes wrong:** The floating "+" button (for adding items) appears on dashboard or setups pages
|
||||||
|
**Why it happens:** Current logic only hides it on thread pages (`!threadMatch`)
|
||||||
|
**How to avoid:** Update __root.tsx to only show the floating add button on `/collection` route (gear tab)
|
||||||
|
**Warning signs:** "+" button visible on dashboard or setup pages
|
||||||
|
|
||||||
|
### Pitfall 6: TotalsBar in Root Layout vs Per-Page
|
||||||
|
**What goes wrong:** TotalsBar in `__root.tsx` shows global stats on every page including dashboard
|
||||||
|
**Why it happens:** TotalsBar is currently rendered unconditionally in root layout
|
||||||
|
**How to avoid:** Either (a) make TotalsBar route-aware via props from root, or (b) move TotalsBar out of root layout and render per-page. Option (a) is simpler -- pass a mode/props based on route matching.
|
||||||
|
**Warning signs:** Dashboard shows stats in TotalsBar instead of just the title
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
|
||||||
|
### Setup Zod Schemas
|
||||||
|
```typescript
|
||||||
|
// In src/shared/schemas.ts
|
||||||
|
export const createSetupSchema = z.object({
|
||||||
|
name: z.string().min(1, "Setup name is required"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const updateSetupSchema = z.object({
|
||||||
|
name: z.string().min(1).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const syncSetupItemsSchema = z.object({
|
||||||
|
itemIds: z.array(z.number().int().positive()),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Setup Hooks Pattern
|
||||||
|
```typescript
|
||||||
|
// In src/client/hooks/useSetups.ts -- follows useThreads.ts pattern exactly
|
||||||
|
export function useSetups() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["setups"],
|
||||||
|
queryFn: () => apiGet<SetupListItem[]>("/api/setups"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSetup(setupId: number | null) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["setups", setupId],
|
||||||
|
queryFn: () => apiGet<SetupWithItems>(`/api/setups/${setupId}`),
|
||||||
|
enabled: setupId != null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSyncSetupItems(setupId: number) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (itemIds: number[]) =>
|
||||||
|
apiPut<{ success: boolean }>(`/api/setups/${setupId}/items`, { itemIds }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["setups"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Remove Single Item from Setup
|
||||||
|
```typescript
|
||||||
|
// Separate from batch sync -- used by the x button on item cards in setup detail
|
||||||
|
export function useRemoveSetupItem(setupId: number) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (itemId: number) =>
|
||||||
|
apiDelete<{ success: boolean }>(`/api/setups/${setupId}/items/${itemId}`),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["setups"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dashboard Route
|
||||||
|
```typescript
|
||||||
|
// src/client/routes/index.tsx -- new dashboard
|
||||||
|
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/")({
|
||||||
|
component: DashboardPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function DashboardPage() {
|
||||||
|
// Three hooks in parallel -- TanStack Query deduplicates
|
||||||
|
const { data: totals } = useTotals();
|
||||||
|
const { data: threads } = useThreads();
|
||||||
|
const { data: setups } = useSetups();
|
||||||
|
|
||||||
|
const activeThreadCount = threads?.filter(t => t.status === "active").length ?? 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<DashboardCard to="/collection" ... />
|
||||||
|
<DashboardCard to="/collection?tab=planning" ... />
|
||||||
|
<DashboardCard to="/setups" ... />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Collection Route (moved from current index.tsx)
|
||||||
|
```typescript
|
||||||
|
// src/client/routes/collection/index.tsx
|
||||||
|
import { createFileRoute } from "@tanstack/react-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const searchSchema = z.object({
|
||||||
|
tab: z.enum(["gear", "planning"]).catch("gear"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/collection/")({
|
||||||
|
validateSearch: searchSchema,
|
||||||
|
component: CollectionPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
function CollectionPage() {
|
||||||
|
// Exact same content as current HomePage in src/client/routes/index.tsx
|
||||||
|
// Just update navigation targets (e.g., handleTabChange navigates to "/collection")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## State of the Art
|
||||||
|
|
||||||
|
| Old Approach | Current Approach | When Changed | Impact |
|
||||||
|
|--------------|------------------|--------------|--------|
|
||||||
|
| Current `/` has gear+planning | `/` becomes dashboard, content moves to `/collection` | Phase 3 | All internal links must update |
|
||||||
|
| TotalsBar always shows global stats | TotalsBar becomes route-aware with contextual stats | Phase 3 | Root layout needs route matching logic |
|
||||||
|
| No many-to-many relationships | `setup_items` junction table | Phase 3 | New Drizzle pattern for this project |
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. **Should setup deletion require confirmation?**
|
||||||
|
- What we know: CONTEXT.md mentions using ConfirmDialog for setup deletion
|
||||||
|
- What's unclear: Whether to also confirm when removing all items from a setup
|
||||||
|
- Recommendation: Use ConfirmDialog for setup deletion (destructive). No confirmation for removing individual items from setup (non-destructive, per CONTEXT.md decision).
|
||||||
|
|
||||||
|
2. **Should `useThreads` on dashboard include resolved threads for the count?**
|
||||||
|
- What we know: Dashboard "Planning" card shows active thread count
|
||||||
|
- What's unclear: Whether to show "3 active" or "3 active / 5 total"
|
||||||
|
- Recommendation: Show only active count for simplicity. `useThreads(false)` already filters to active.
|
||||||
|
|
||||||
|
## Validation Architecture
|
||||||
|
|
||||||
|
### Test Framework
|
||||||
|
| Property | Value |
|
||||||
|
|----------|-------|
|
||||||
|
| Framework | bun:test (built into Bun) |
|
||||||
|
| Config file | None (Bun built-in, runs from `package.json` `"test": "bun test"`) |
|
||||||
|
| Quick run command | `bun test tests/services/setup.service.test.ts` |
|
||||||
|
| Full suite command | `bun test` |
|
||||||
|
|
||||||
|
### Phase Requirements to Test Map
|
||||||
|
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|
||||||
|
|--------|----------|-----------|-------------------|-------------|
|
||||||
|
| SETP-01 | Create/list/delete named setups | unit + integration | `bun test tests/services/setup.service.test.ts` | No - Wave 0 |
|
||||||
|
| SETP-01 | Setup CRUD API routes | integration | `bun test tests/routes/setups.test.ts` | No - Wave 0 |
|
||||||
|
| SETP-02 | Add/remove items to setup (junction table) | unit | `bun test tests/services/setup.service.test.ts` | No - Wave 0 |
|
||||||
|
| SETP-02 | Setup items sync API route | integration | `bun test tests/routes/setups.test.ts` | No - Wave 0 |
|
||||||
|
| SETP-03 | Setup totals (weight/cost aggregation) | unit | `bun test tests/services/setup.service.test.ts` | No - Wave 0 |
|
||||||
|
| DASH-01 | Dashboard summary data | manual-only | Manual browser verification | N/A (UI-only, data from existing endpoints) |
|
||||||
|
|
||||||
|
### Sampling Rate
|
||||||
|
- **Per task commit:** `bun test tests/services/setup.service.test.ts && bun test tests/routes/setups.test.ts`
|
||||||
|
- **Per wave merge:** `bun test`
|
||||||
|
- **Phase gate:** Full suite green before `/gsd:verify-work`
|
||||||
|
|
||||||
|
### Wave 0 Gaps
|
||||||
|
- [ ] `tests/services/setup.service.test.ts` -- covers SETP-01, SETP-02, SETP-03
|
||||||
|
- [ ] `tests/routes/setups.test.ts` -- covers SETP-01, SETP-02 API layer
|
||||||
|
- [ ] `tests/helpers/db.ts` -- needs `setups` and `setup_items` CREATE TABLE statements added
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
### Primary (HIGH confidence)
|
||||||
|
- Existing codebase: `src/db/schema.ts`, `src/server/services/thread.service.ts`, `src/server/routes/threads.ts` -- direct pattern references
|
||||||
|
- Existing codebase: `src/client/hooks/useThreads.ts`, `src/client/stores/uiStore.ts` -- client-side patterns
|
||||||
|
- Existing codebase: `tests/services/thread.service.test.ts`, `tests/helpers/db.ts` -- test infrastructure patterns
|
||||||
|
- Existing codebase: `src/client/routes/__root.tsx`, `src/client/routes/index.tsx` -- routing patterns
|
||||||
|
|
||||||
|
### Secondary (MEDIUM confidence)
|
||||||
|
- TanStack Router file-based routing conventions -- verified against existing `routeTree.gen.ts` auto-generation
|
||||||
|
|
||||||
|
### Tertiary (LOW confidence)
|
||||||
|
- None
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
**Confidence breakdown:**
|
||||||
|
- Standard stack: HIGH -- no new dependencies, all patterns established in Phases 1-2
|
||||||
|
- Architecture: HIGH -- direct 1:1 mapping from thread patterns to setup patterns, only new concept is junction table
|
||||||
|
- Pitfalls: HIGH -- identified from direct codebase analysis (hardcoded links, TotalsBar in root, cascade behavior)
|
||||||
|
|
||||||
|
**Research date:** 2026-03-15
|
||||||
|
**Valid until:** 2026-04-15 (stable -- no external dependencies changing)
|
||||||
Reference in New Issue
Block a user