# Phase 21: Item & Catalog Detail Pages - Research

**Researched:** 2026-04-06
**Domain:** Frontend routing, detail page patterns, edit mode toggle, panel removal
**Confidence:** HIGH

## Summary

This phase creates full detail pages for collection items, enhances the existing catalog detail page, adds candidate detail routes, and removes slide-out panels from the application. The entire scope is frontend-only -- no new API endpoints or service changes are required. All necessary hooks (`useItem`, `useUpdateItem`, `useDeleteItem`, `useDuplicateItem`, `useGlobalItem`, `useUpdateCandidate`, `useDeleteCandidate`) and API routes already exist.

The primary complexity is the edit mode toggle pattern (read-only by default, fields transform to inputs when "Edit" is pressed) and coordinating the panel removal without breaking existing flows. TanStack Router's file-based routing makes adding new routes mechanical -- create the file, export a `Route` constant, and the route tree auto-generates.

**Primary recommendation:** Build detail pages first (item, candidate, catalog enhancement), then rewire card click handlers, then remove panels and clean up UIStore in a final wave. This ordering ensures the navigation targets exist before redirecting clicks to them.

<user_constraints>
## User Constraints (from CONTEXT.md)

### Locked Decisions
- D-01 through D-07: Private item detail page at `/items/:id` with hero section, personal section, edit mode toggle, back navigation, and actions (duplicate, delete).
- D-08 through D-12: Public catalog detail page at `/global-items/:id` enhanced with "Add to Collection" button (stub), hero layout, no edit functionality.
- D-13 through D-16: Candidate detail page at `/threads/:threadId/candidates/:candidateId` with edit mode toggle and thread-specific actions.
- D-17 through D-20: Navigation changes -- ItemCard, CandidateCard, CandidateListItem, and catalog search result clicks navigate to detail pages.
- D-21 through D-26: Remove SlideOutPanel instances from `__root.tsx`, remove panel state from UIStore, keep `SlideOutPanel.tsx`, `ItemForm.tsx`, `CandidateForm.tsx` files.
- D-27: "Add item" flow goes through catalog search (FAB). No more direct "add item" panel.
- D-28: Adding candidates to threads -- keep a simple button/form pattern (not a slide-out).

### Claude's Discretion
- Exact layout proportions and spacing for detail pages
- Whether to use tabs or sections for organizing detail page content
- How the edit mode transition animates (if at all)
- "Add Candidate" button pattern on thread page (inline form, modal, or navigate to add route)
- Whether to show a "Linked to catalog" indicator on private items (subtle, not prominent)
- Mobile layout adaptations for detail pages

### Deferred Ideas (OUT OF SCOPE)
- Reviews/ratings section on detail pages
- Community stats (average weight, price history)
- Setup appearances ("This item is in 3 setups")
- "Similar items" recommendation section
- Image gallery with multiple photos
</user_constraints>

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|------------------|
| DETAIL-01 | Clicking a collection item navigates to a full detail page (`/items/:id`) showing all item data | New route file at `src/client/routes/items/$itemId.tsx`; existing `useItem(id)` hook returns all merged data via COALESCE |
| DETAIL-02 | Clicking a catalog search result navigates to a public detail page (`/global-items/:id`) with "Add to Collection" button | Existing route at `src/client/routes/global-items/$globalItemId.tsx` needs enhancement; `useGlobalItem(id)` returns data with ownerCount |
| DETAIL-03 | Item detail page has edit mode toggle for modifying personal fields | `useState` for edit mode boolean; reuse validation logic from `ItemForm.tsx`; `useUpdateItem` mutation for save |
| DETAIL-04 | Thread candidates navigate to detail pages instead of opening slide-out panels | New route at `src/client/routes/threads/$threadId/candidates/$candidateId.tsx`; `useThread(threadId)` returns candidates array |
| DETAIL-05 | Slide-out panels for items and candidates are removed from the application | Remove from `__root.tsx` lines 189-221; clean UIStore panel state (lines 3-28 of store) |
</phase_requirements>

## Project Constraints (from CLAUDE.md)

- **Routing**: TanStack Router with file-based routes in `src/client/routes/`. Route tree auto-generated to `routeTree.gen.ts` -- never edit manually.
- **Data fetching**: TanStack React Query via custom hooks. Mutations invalidate related query keys.
- **UI state**: Zustand store for panel/dialog state only -- server data lives in React Query.
- **Styling**: Tailwind CSS v4.
- **Testing**: Bun test runner for unit/integration. Playwright for E2E.
- **Path alias**: `@/*` maps to `./src/*`.
- **Dev command**: `bun run dev` starts both client and server.
- **Lint**: `bun run lint` -- Biome check (tabs, double quotes, organized imports).

## Standard Stack

### Core (already installed, no new dependencies)
| Library | Purpose | Why Standard |
|---------|---------|--------------|
| TanStack Router | File-based routing, route params | Already used throughout app |
| TanStack React Query | Data fetching, caching, mutations | All data fetching goes through this |
| Zustand | UI state management | Panel/dialog state management |
| Tailwind CSS v4 | Styling | Project standard |
| Framer Motion | Animations (AnimatePresence) | Already used for page transitions |

### No New Dependencies Required
This phase adds no new libraries. All required functionality is covered by the existing stack.

## Architecture Patterns

### New Route Files to Create
```
src/client/routes/
├── items/
│   └── $itemId.tsx              # /items/:id - private item detail
├── threads/
│   └── $threadId/
│       └── candidates/
│           └── $candidateId.tsx  # /threads/:threadId/candidates/:candidateId
├── global-items/
│   └── $globalItemId.tsx        # EXISTING - enhance with Add button
```

### Pattern 1: TanStack Router File-Based Route
**What:** Create route file with `createFileRoute`, export `Route` constant.
**When to use:** Every new page.
**Example (from existing codebase):**
```typescript
// src/client/routes/global-items/$globalItemId.tsx
import { createFileRoute, Link } from "@tanstack/react-router";

export const Route = createFileRoute("/global-items/$globalItemId")({
  component: GlobalItemDetail,
});

function GlobalItemDetail() {
  const { globalItemId } = Route.useParams();
  // ...
}
```

For nested routes like `/threads/$threadId/candidates/$candidateId`, TanStack Router requires the directory structure to match. The file goes at `src/client/routes/threads/$threadId/candidates/$candidateId.tsx` with route path `"/threads/$threadId/candidates/$candidateId"`.

**IMPORTANT:** The existing `src/client/routes/threads/$threadId.tsx` must be renamed to `src/client/routes/threads/$threadId/index.tsx` (or `route.tsx`) to allow the nested candidates directory to exist alongside it. TanStack Router file-based routing requires this restructuring when adding child routes under a parameterized parent.

### Pattern 2: Edit Mode Toggle
**What:** Local `useState` boolean controls read-only vs. editable view. No Zustand needed.
**When to use:** Item detail and candidate detail pages.
**Example:**
```typescript
function ItemDetail() {
  const [isEditing, setIsEditing] = useState(false);
  const [form, setForm] = useState<FormData>(/* initial */);
  const updateItem = useUpdateItem();
  const { data: item } = useItem(Number(itemId));

  // Sync form state when entering edit mode or when item data loads
  useEffect(() => {
    if (item && isEditing) {
      setForm(/* map item to form fields */);
    }
  }, [item, isEditing]);

  function handleSave() {
    updateItem.mutate({ id: item.id, ...payload }, {
      onSuccess: () => setIsEditing(false),
    });
  }

  function handleCancel() {
    setIsEditing(false);
    // Form resets on next edit mode entry via useEffect
  }

  return (
    <div>
      {isEditing ? (
        <input value={form.name} onChange={...} />
      ) : (
        <h1>{item.name}</h1>
      )}
    </div>
  );
}
```

### Pattern 3: Detail Page Layout (gallery-like)
**What:** Consistent layout: back nav, hero image, title/badges, content sections, action buttons.
**When to use:** All three detail pages.
**Example structure (from existing `$globalItemId.tsx`):**
```typescript
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
  {/* Back navigation */}
  <Link to="/collection" className="text-sm text-gray-400 hover:text-gray-600">
    &larr; Back to collection
  </Link>

  {/* Hero image */}
  <div className="aspect-[16/9] bg-gray-50 rounded-xl overflow-hidden mb-6">
    <img ... />
  </div>

  {/* Title + badges */}
  <h1 className="text-2xl font-bold text-gray-900 mb-3">{item.name}</h1>
  <div className="flex flex-wrap gap-2 mb-6">
    {/* weight, price, category badges */}
  </div>

  {/* Content sections */}
  {/* ... notes, product link, etc. */}
</div>
```

### Pattern 4: Navigation from Card Components
**What:** Replace `openEditPanel(id)` calls with TanStack Router `useNavigate()` or `Link` component.
**When to use:** ItemCard, CandidateCard, CandidateListItem, CatalogSearchOverlay grid/list cards.
**Example:**
```typescript
// Before (ItemCard.tsx)
const openEditPanel = useUIStore((s) => s.openEditPanel);
onClick={() => openEditPanel(id)}

// After
import { useNavigate } from "@tanstack/react-router";
const navigate = useNavigate();
onClick={() => navigate({ to: "/items/$itemId", params: { itemId: String(id) } })}
```

### Anti-Patterns to Avoid
- **Do NOT edit `routeTree.gen.ts` manually** -- it auto-generates when route files change.
- **Do NOT put edit form state in Zustand** -- local `useState` per detail page is correct. Zustand is for cross-component UI state only.
- **Do NOT remove `ItemForm.tsx` or `CandidateForm.tsx`** -- reuse their validation logic and field definitions in the edit mode sections of detail pages.
- **Do NOT remove SlideOutPanel.tsx component file** -- D-25 explicitly keeps it for potential future use.

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Form validation | Custom validation from scratch | Copy validation logic from `ItemForm.tsx` / `CandidateForm.tsx` | Proven patterns, consistent behavior |
| Image URL resolution | Manual URL building | `withImageUrl` / `withImageUrls` from storage service (already called in routes) | Server already returns `imageUrl` field in API response |
| Loading/error states | Custom skeleton per page | Follow existing pattern from `$globalItemId.tsx` (shimmer placeholders, error state with back link) | Consistency |
| Confirmation dialogs | New dialog components | Existing `ConfirmDialog` pattern in `__root.tsx` and `useUIStore.openConfirmDelete` | Already works for items; same pattern for candidates on detail pages |

## Common Pitfalls

### Pitfall 1: Nested Route File Structure for Candidates
**What goes wrong:** Creating `src/client/routes/threads/$threadId/candidates/$candidateId.tsx` while `$threadId.tsx` exists as a file (not a directory) causes TanStack Router confusion.
**Why it happens:** TanStack Router file-based routing treats `$threadId.tsx` and `$threadId/` directory as conflicting. You need to restructure.
**How to avoid:** Rename `src/client/routes/threads/$threadId.tsx` to `src/client/routes/threads/$threadId/index.tsx` (or `route.tsx`). Then create `$threadId/candidates/$candidateId.tsx` as a sibling directory.
**Warning signs:** Route tree generation errors, blank page at `/threads/:threadId`.

### Pitfall 2: Removing Panel State Before Wiring Navigation
**What goes wrong:** Removing UIStore panel state and `__root.tsx` panel JSX before the card click handlers are updated causes runtime errors (calling undefined functions).
**Why it happens:** Components like `ItemCard` still import `useUIStore((s) => s.openEditPanel)`.
**How to avoid:** Order of operations: (1) Create detail pages, (2) Update card click handlers to navigate, (3) Then remove panel state and JSX.
**Warning signs:** TypeScript errors on removed store properties, blank areas where panels used to be.

### Pitfall 3: Edit Mode Form Not Syncing with Server Data
**What goes wrong:** User enters edit mode, but form shows stale or default data.
**Why it happens:** `useItem(id)` fetch may not have completed when edit mode is toggled, or form state is initialized once and never updated.
**How to avoid:** Use `useEffect` to sync form state when both `isEditing === true` AND item data changes. Alternatively, initialize form data from item data only when entering edit mode (in the toggle handler).
**Warning signs:** Form fields show empty or previous values.

### Pitfall 4: CandidateCard/CandidateListItem Need threadId for Navigation
**What goes wrong:** Navigation to `/threads/:threadId/candidates/:candidateId` requires both IDs, but the component may only have `candidateId`.
**Why it happens:** CandidateCard already receives `threadId` as a prop. CandidateListItem receives it via `candidate.threadId`. Both are safe.
**How to avoid:** Verify both components have access to `threadId` before changing click handlers. Both do.
**Warning signs:** Navigation produces undefined params.

### Pitfall 5: Catalog Search Overlay Card Click vs. Add Button
**What goes wrong:** Making the entire card clickable navigates away from the search overlay, losing context.
**Why it happens:** D-19 says card click navigates to `/global-items/:id`, but the overlay is open.
**How to avoid:** Close the catalog search overlay before navigating. Or use `Link` component and let the navigation naturally close the overlay (overlay checks `catalogSearchOpen` state). The card body navigates; the "Add" button stays as a separate action with `e.stopPropagation()`.
**Warning signs:** Overlay stays visible on top of detail page, or user loses search context unexpectedly.

### Pitfall 6: "Add Candidate" on Thread Page After Panel Removal
**What goes wrong:** The thread page currently opens the candidate add panel via `openCandidateAddPanel()`. After panels are removed, there's no way to add candidates.
**Why it happens:** D-28 says to keep a simple button/form pattern, but the existing flow is entirely panel-based.
**How to avoid:** Replace the "Add Candidate" button action on the thread page. Options: (1) inline form that expands on the thread page, (2) small modal dialog, (3) navigate to a dedicated add route. Recommendation: use a modal dialog -- it's the lightest change and consistent with existing dialog patterns in the app.
**Warning signs:** No way to add candidates after panel removal.

## Code Examples

### Creating the Item Detail Route
```typescript
// src/client/routes/items/$itemId.tsx
import { createFileRoute, Link, useNavigate } from "@tanstack/react-router";
import { useState } from "react";
import { useFormatters } from "../../hooks/useFormatters";
import { useDeleteItem, useDuplicateItem, useItem, useUpdateItem } from "../../hooks/useItems";

export const Route = createFileRoute("/items/$itemId")({
  component: ItemDetailPage,
});
```

### Restructuring Thread Route for Nesting
```
# Before:
src/client/routes/threads/$threadId.tsx

# After:
src/client/routes/threads/$threadId/index.tsx        # same content, path unchanged
src/client/routes/threads/$threadId/candidates/
  $candidateId.tsx                                    # new candidate detail
```

The `index.tsx` file maps to `/threads/$threadId` exactly as before. The nested `candidates/$candidateId.tsx` maps to `/threads/$threadId/candidates/$candidateId`.

### UIStore Cleanup (properties to remove)
```typescript
// Remove these from UIState interface and implementation:
panelMode: "closed" | "add" | "edit";
editingItemId: number | null;
openAddPanel: () => void;
openEditPanel: (itemId: number) => void;
closePanel: () => void;

candidatePanelMode: "closed" | "add" | "edit";
editingCandidateId: number | null;
openCandidateAddPanel: () => void;
openCandidateEditPanel: (id: number) => void;
closeCandidatePanel: () => void;

// Keep these (still used):
confirmDeleteItemId / openConfirmDelete / closeConfirmDelete  // used by ConfirmDialog
confirmDeleteCandidateId / openConfirmDeleteCandidate / closeConfirmDeleteCandidate  // candidate delete
resolveThreadId / resolveCandidateId / openResolveDialog / closeResolveDialog  // resolve dialog
// ... all other non-panel state
```

### Updating ItemCard Click Handler
```typescript
// src/client/components/ItemCard.tsx
// Replace:
import { useUIStore } from "../stores/uiStore";
const openEditPanel = useUIStore((s) => s.openEditPanel);
// With:
import { useNavigate } from "@tanstack/react-router";
const navigate = useNavigate();

// onClick changes from:
onClick={() => openEditPanel(id)}
// To:
onClick={() => navigate({ to: "/items/$itemId", params: { itemId: String(id) } })}
```

### Adding "Add to Collection" Button on Catalog Detail Page
```typescript
// In global-items/$globalItemId.tsx, add after hero/header:
<button
  type="button"
  onClick={handleAddToCollection}
  className="bg-gray-700 text-white rounded-lg px-4 py-2 text-sm font-medium hover:bg-gray-800 transition-colors"
>
  Add to Collection
</button>
```
This is a stub per D-10 -- actual add flow wired in Phase 22.

## Validation Architecture

### Test Framework
| Property | Value |
|----------|-------|
| Framework | Playwright + Bun test |
| Config file | `playwright.config.ts` (E2E), `bunfig.toml` (unit) |
| Quick run command | `bun test tests/routes/` |
| Full suite command | `bun run test:e2e` |

### Phase Requirements to Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| DETAIL-01 | Item detail page renders at `/items/:id` | E2E | `bun run test:e2e -- --grep "item detail"` | No -- Wave 0 |
| DETAIL-02 | Catalog detail page has "Add to Collection" button | E2E | `bun run test:e2e -- --grep "catalog detail"` | No -- Wave 0 |
| DETAIL-03 | Edit mode toggle shows/hides form inputs | E2E | `bun run test:e2e -- --grep "edit mode"` | No -- Wave 0 |
| DETAIL-04 | Candidate card click navigates to detail page | E2E | `bun run test:e2e -- --grep "candidate detail"` | No -- Wave 0 |
| DETAIL-05 | No SlideOutPanel instances in rendered DOM | E2E | `bun run test:e2e -- --grep "panel removed"` | No -- Wave 0 |

### Sampling Rate
- **Per task commit:** `bun run lint && bun run dev` (manual visual check -- frontend-heavy)
- **Per wave merge:** `bun run test:e2e`
- **Phase gate:** Full E2E suite green before `/gsd:verify-work`

### Wave 0 Gaps
- [ ] `e2e/item-detail.spec.ts` -- covers DETAIL-01, DETAIL-03
- [ ] `e2e/catalog-detail.spec.ts` -- covers DETAIL-02
- [ ] `e2e/candidate-detail.spec.ts` -- covers DETAIL-04
- [ ] `e2e/panel-removal.spec.ts` -- covers DETAIL-05
- [ ] E2E seed data: verify `e2e/seed.ts` creates items and threads with candidates for detail page testing

## Sources

### Primary (HIGH confidence)
- Codebase inspection: `src/client/routes/global-items/$globalItemId.tsx` -- existing detail page pattern
- Codebase inspection: `src/client/routes/__root.tsx` -- panel instances to remove (lines 189-221)
- Codebase inspection: `src/client/stores/uiStore.ts` -- panel state to clean (full file reviewed)
- Codebase inspection: `src/client/components/ItemCard.tsx` -- current click handler using `openEditPanel`
- Codebase inspection: `src/client/components/CandidateCard.tsx` -- current click handler using `openCandidateEditPanel`
- Codebase inspection: `src/client/components/CandidateListItem.tsx` -- current click handler
- Codebase inspection: `src/client/components/ItemForm.tsx` -- form fields and validation to reuse
- Codebase inspection: `src/client/components/CandidateForm.tsx` -- candidate form to reuse
- Codebase inspection: `src/client/hooks/useItems.ts` -- `useItem`, `useUpdateItem`, `useDeleteItem`, `useDuplicateItem` hooks
- Codebase inspection: `src/client/hooks/useCandidates.ts` -- `useUpdateCandidate`, `useDeleteCandidate` hooks
- Codebase inspection: `src/client/hooks/useGlobalItems.ts` -- `useGlobalItem` hook with ownerCount
- Codebase inspection: `src/server/services/item.service.ts` -- `getItemById` COALESCE merge pattern
- Codebase inspection: `src/server/routes/items.ts` -- `withImageUrl` applied to single item responses

### Secondary (MEDIUM confidence)
- TanStack Router file-based routing: nested parameterized routes require directory restructuring (verified against established pattern in `src/client/routes/setups/$setupId.tsx`)

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH -- no new dependencies, all existing
- Architecture: HIGH -- all patterns verified against existing codebase
- Pitfalls: HIGH -- identified from direct code inspection of affected files

**Research date:** 2026-04-06
**Valid until:** 2026-05-06 (stable -- frontend patterns, no external dependencies)
