---
phase: 01-foundation-and-collection
plan: 03
type: execute
wave: 3
depends_on: ["01-02"]
files_modified:
  - src/client/lib/api.ts
  - src/client/lib/formatters.ts
  - src/client/hooks/useItems.ts
  - src/client/hooks/useCategories.ts
  - src/client/hooks/useTotals.ts
  - src/client/stores/uiStore.ts
  - src/client/components/TotalsBar.tsx
  - src/client/components/CategoryHeader.tsx
  - src/client/components/ItemCard.tsx
  - src/client/components/SlideOutPanel.tsx
  - src/client/components/ItemForm.tsx
  - src/client/components/CategoryPicker.tsx
  - src/client/components/ConfirmDialog.tsx
  - src/client/components/ImageUpload.tsx
  - src/client/routes/__root.tsx
  - src/client/routes/index.tsx
autonomous: true
requirements:
  - COLL-01
  - COLL-02
  - COLL-03
  - COLL-04

must_haves:
  truths:
    - "User can see their gear items displayed as cards grouped by category"
    - "User can add a new item via the slide-out panel with all fields"
    - "User can edit an existing item by clicking its card and modifying fields in the panel"
    - "User can delete an item with a confirmation dialog"
    - "User can create new categories inline via the category picker combobox"
    - "User can rename or delete categories from category headers"
    - "User can see per-category weight and cost subtotals in category headers"
    - "User can see global totals in a sticky bar at the top"
    - "User can upload an image for an item and see it on the card"
  artifacts:
    - path: "src/client/components/ItemCard.tsx"
      provides: "Gear item card with name, weight/price/category chips, and image"
      min_lines: 30
    - path: "src/client/components/SlideOutPanel.tsx"
      provides: "Right slide-out panel container for add/edit forms"
      min_lines: 20
    - path: "src/client/components/ItemForm.tsx"
      provides: "Form with all item fields, used inside SlideOutPanel"
      min_lines: 50
    - path: "src/client/components/CategoryPicker.tsx"
      provides: "Combobox: search existing categories or create new inline"
      min_lines: 40
    - path: "src/client/components/TotalsBar.tsx"
      provides: "Sticky bar showing total items, weight, and cost"
    - path: "src/client/components/CategoryHeader.tsx"
      provides: "Category group header with emoji, name, subtotals, and edit/delete actions"
    - path: "src/client/routes/index.tsx"
      provides: "Collection page assembling all components"
      min_lines: 40
  key_links:
    - from: "src/client/hooks/useItems.ts"
      to: "/api/items"
      via: "TanStack Query fetch calls"
      pattern: "fetch.*/api/items"
    - from: "src/client/components/ItemForm.tsx"
      to: "src/client/hooks/useItems.ts"
      via: "Mutation hooks for create/update"
      pattern: "useCreateItem|useUpdateItem"
    - from: "src/client/components/CategoryPicker.tsx"
      to: "src/client/hooks/useCategories.ts"
      via: "Categories query and create mutation"
      pattern: "useCategories|useCreateCategory"
    - from: "src/client/routes/index.tsx"
      to: "src/client/stores/uiStore.ts"
      via: "Panel open/close state"
      pattern: "useUIStore"
---

<objective>
Build the complete frontend collection UI: card grid layout grouped by category, slide-out panel for add/edit with all item fields, category picker combobox, confirmation dialog for delete, image upload, and sticky totals bar.

Purpose: This is the primary user-facing feature of Phase 1 -- the gear collection view where users catalog, organize, and browse their gear.
Output: A fully functional collection page with CRUD operations, category management, and computed totals.
</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/01-foundation-and-collection/01-CONTEXT.md
@.planning/phases/01-foundation-and-collection/01-RESEARCH.md
@.planning/phases/01-foundation-and-collection/01-01-SUMMARY.md
@.planning/phases/01-foundation-and-collection/01-02-SUMMARY.md

<interfaces>
<!-- API endpoints from Plan 02 -->
GET    /api/items         -> Item[] (with category name/emoji joined)
GET    /api/items/:id     -> Item | 404
POST   /api/items         -> Item (201) | validation error (400)
PUT    /api/items/:id     -> Item (200) | 404
DELETE /api/items/:id     -> { success: true } (200) | 404

GET    /api/categories    -> Category[]
POST   /api/categories    -> Category (201) | validation error (400)
PUT    /api/categories/:id -> Category (200) | 404
DELETE /api/categories/:id -> { success: true } (200) | 400 (Uncategorized) | 404

GET    /api/totals        -> { categories: CategoryTotals[], global: GlobalTotals }

POST   /api/images        -> { filename: string } (201) | 400

<!-- Shared types from Plan 01 -->
From src/shared/types.ts:
  Item, Category, CreateItem, UpdateItem, CreateCategory, UpdateCategory

<!-- UI Store pattern from RESEARCH.md -->
From src/client/stores/uiStore.ts:
  panelMode: "closed" | "add" | "edit"
  editingItemId: number | null
  openAddPanel(), openEditPanel(id), closePanel()
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Data hooks, utilities, UI store, and foundational components</name>
  <files>src/client/lib/api.ts, src/client/lib/formatters.ts, src/client/hooks/useItems.ts, src/client/hooks/useCategories.ts, src/client/hooks/useTotals.ts, src/client/stores/uiStore.ts, src/client/components/TotalsBar.tsx, src/client/components/CategoryHeader.tsx, src/client/components/ItemCard.tsx, src/client/components/ConfirmDialog.tsx, src/client/components/ImageUpload.tsx</files>
  <action>
    1. Create `src/client/lib/api.ts`: A thin fetch wrapper that throws on non-ok responses with error message from response body. Functions: apiGet(url), apiPost(url, body), apiPut(url, body), apiDelete(url), apiUpload(url, file) for multipart form data.

    2. Create `src/client/lib/formatters.ts`: formatWeight(grams) returns "123g" or "--" if null. formatPrice(cents) returns "$12.34" or "--" if null. These are display-only, no unit conversion in v1.

    3. Create `src/client/hooks/useItems.ts` per RESEARCH.md TanStack Query Hook example: useItems() query, useCreateItem() mutation (invalidates items+totals), useUpdateItem() mutation (invalidates items+totals), useDeleteItem() mutation (invalidates items+totals). All mutations invalidate both "items" and "totals" query keys.

    4. Create `src/client/hooks/useCategories.ts`: useCategories() query, useCreateCategory() mutation (invalidates categories), useUpdateCategory() mutation (invalidates categories), useDeleteCategory() mutation (invalidates categories+items+totals since items may be reassigned).

    5. Create `src/client/hooks/useTotals.ts`: useTotals() query returning { categories: CategoryTotals[], global: GlobalTotals }.

    6. Create `src/client/stores/uiStore.ts` per RESEARCH.md Pattern 3: Zustand store with panelMode, editingItemId, openAddPanel, openEditPanel, closePanel. Also add confirmDeleteItemId: number | null with openConfirmDelete(id) and closeConfirmDelete().

    7. Create `src/client/components/TotalsBar.tsx`: Sticky bar at top of page (position: sticky, top: 0, z-10). Shows total item count, total weight (formatted), total cost (formatted). Uses useTotals() hook. Clean minimal style per user decision: white background, subtle bottom border, light text.

    8. Create `src/client/components/CategoryHeader.tsx`: Receives category name, emoji, weight subtotal, cost subtotal, item count. Displays: emoji + name prominently, then subtotals in lighter text. Include edit (rename/emoji) and delete buttons that appear on hover. Delete triggers confirmation. Per user decision: empty categories are NOT shown (filtering happens in parent).

    9. Create `src/client/components/ItemCard.tsx`: Card displaying item name (prominent), image (if imageFilename exists, use /uploads/{filename} as src with object-fit cover), and tag-style chips for weight, price, and category. Per user decisions: clean, minimal, light and airy aesthetic with white backgrounds and whitespace. Clicking the card calls openEditPanel(item.id).

    10. Create `src/client/components/ConfirmDialog.tsx`: Modal dialog with "Are you sure you want to delete {itemName}?" message, Cancel and Delete buttons. Delete button is red/destructive. Uses confirmDeleteItemId from uiStore. Calls useDeleteItem mutation on confirm, then closes.

    11. Create `src/client/components/ImageUpload.tsx`: File input that accepts image/jpeg, image/png, image/webp. On file select, uploads via POST /api/images, returns filename to parent via onChange callback. Shows preview of selected/existing image. Max 5MB validation client-side before upload.
  </action>
  <verify>
    <automated>bun run build 2>&1 | tail -5</automated>
  </verify>
  <done>All hooks fetch from API and handle mutations with cache invalidation. UI store manages panel and confirm dialog state. TotalsBar, CategoryHeader, ItemCard, ConfirmDialog, and ImageUpload components exist and compile. Build succeeds.</done>
</task>

<task type="auto">
  <name>Task 2: Slide-out panel, item form with category picker, and collection page assembly</name>
  <files>src/client/components/SlideOutPanel.tsx, src/client/components/ItemForm.tsx, src/client/components/CategoryPicker.tsx, src/client/routes/__root.tsx, src/client/routes/index.tsx</files>
  <action>
    1. Create `src/client/components/CategoryPicker.tsx`: Combobox component per user decision. Type to search existing categories, select from filtered dropdown, or create new inline. Uses useCategories() for the list and useCreateCategory() to create new. Props: value (categoryId), onChange(categoryId). Implementation: text input with dropdown list filtered by input text. If no match and input non-empty, show "Create [input]" option. On selecting create, call mutation, wait for result, then call onChange with new category id. Proper ARIA attributes: role combobox, listbox, option. Keyboard navigation: arrow keys to navigate, Enter to select, Escape to close.

    2. Create `src/client/components/SlideOutPanel.tsx`: Container component that slides in from the right side of the screen. Per user decisions: collection remains visible behind (use fixed positioning with right: 0, width ~400px on desktop, full width on mobile). Tailwind transition-transform + translate-x for animation. Props: isOpen, onClose, title (string). Renders children inside. Backdrop overlay (semi-transparent) that closes panel on click. Close button (X) in header.

    3. Create `src/client/components/ItemForm.tsx`: Form rendered inside SlideOutPanel. Props: mode ("add" | "edit"), itemId? (for edit mode). When edit mode: fetch item by id (useItems data or separate query), pre-fill all fields. Fields: name (text, required), weight in grams (number input, labeled "Weight (g)"), price in dollars (number input that converts to/from cents for display -- show $, store cents), category (CategoryPicker component), notes (textarea), product link (url input), image (ImageUpload component). On submit: call useCreateItem or useUpdateItem depending on mode, close panel on success. Validation: use Zod createItemSchema for client-side validation, show inline error messages. Per Claude's discretion: all fields visible in a single scrollable form (not tabbed/grouped).

    4. Update `src/client/routes/__root.tsx`: Import and render TotalsBar at top. Render Outlet below. Render SlideOutPanel (controlled by uiStore panelMode). When panelMode is "add", render ItemForm with mode="add" inside panel. When "edit", render ItemForm with mode="edit" and itemId from uiStore. Render ConfirmDialog. Add a floating "+" button (fixed, bottom-right) to trigger openAddPanel().

    5. Update `src/client/routes/index.tsx` as the collection page: Use useItems() to get all items. Use useTotals() to get category totals (for subtotals in headers). Group items by categoryId. For each category that has items (skip empty per user decision): render CategoryHeader with subtotals, then render a responsive card grid of ItemCards (CSS grid: 1 col mobile, 2 col md, 3 col lg). If no items exist at all, show an empty state message encouraging the user to add their first item. Per user decision: card grid layout grouped by category headers.
  </action>
  <verify>
    <automated>bun run build 2>&1 | tail -5</automated>
  </verify>
  <done>Collection page renders card grid grouped by category. Slide-out panel opens for add/edit with all item fields. Category picker supports search and inline creation. Confirm dialog works for delete. All CRUD operations work end-to-end through the UI. Build succeeds.</done>
</task>

</tasks>

<verification>
- `bun run build` succeeds
- Dev server renders collection page at http://localhost:5173
- Adding an item via the slide-out panel persists to database and appears in the card grid
- Editing an item pre-fills the form and saves changes
- Deleting an item shows confirmation dialog and removes the card
- Creating a new category via the picker adds it to the list
- Category headers show correct subtotals
- Sticky totals bar shows correct global totals
- Image upload displays on the item card
</verification>

<success_criteria>
- Card grid layout displays items grouped by category with per-category subtotals
- Slide-out panel works for both add and edit with all item fields
- Category picker supports search, select, and inline creation
- Delete confirmation dialog prevents accidental deletion
- Sticky totals bar shows global item count, weight, and cost
- Empty categories are hidden from the view
- Image upload and display works on cards
- All CRUD operations work end-to-end (UI -> API -> DB -> UI)
</success_criteria>

<output>
After completion, create `.planning/phases/01-foundation-and-collection/01-03-SUMMARY.md`
</output>
