docs(05): create phase plan for image handling
This commit is contained in:
198
.planning/phases/05-image-handling/05-01-PLAN.md
Normal file
198
.planning/phases/05-image-handling/05-01-PLAN.md
Normal file
@@ -0,0 +1,198 @@
|
||||
---
|
||||
phase: 05-image-handling
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/client/components/ImageUpload.tsx
|
||||
- src/client/components/ItemForm.tsx
|
||||
- src/client/components/CandidateForm.tsx
|
||||
autonomous: true
|
||||
requirements: [IMG-01, IMG-03, IMG-04]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Uploaded images display correctly in the ImageUpload preview area (not broken/missing)"
|
||||
- "Item form shows a full-width 4:3 hero image area at the top of the form"
|
||||
- "When no image is set, hero area shows gray background with centered icon and 'Click to add photo' text"
|
||||
- "Clicking the placeholder opens file picker and uploaded image replaces placeholder immediately"
|
||||
- "When image exists, a small circular X button in top-right removes the image"
|
||||
- "Clicking an existing image opens file picker to replace it"
|
||||
- "CandidateForm has the same hero area redesign as ItemForm"
|
||||
artifacts:
|
||||
- path: "src/client/components/ImageUpload.tsx"
|
||||
provides: "Hero image area with placeholder, upload, preview, remove"
|
||||
min_lines: 60
|
||||
- path: "src/client/components/ItemForm.tsx"
|
||||
provides: "ImageUpload moved to top of form as first element"
|
||||
- path: "src/client/components/CandidateForm.tsx"
|
||||
provides: "ImageUpload moved to top of form as first element"
|
||||
key_links:
|
||||
- from: "src/client/components/ImageUpload.tsx"
|
||||
to: "/api/images"
|
||||
via: "apiUpload call in handleFileChange"
|
||||
pattern: "apiUpload.*api/images"
|
||||
- from: "src/client/components/ItemForm.tsx"
|
||||
to: "src/client/components/ImageUpload.tsx"
|
||||
via: "ImageUpload component at top of form"
|
||||
pattern: "<ImageUpload"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Fix the image display bug so uploaded images render correctly, then redesign the ImageUpload component into a hero image preview area and move it to the top of both ItemForm and CandidateForm.
|
||||
|
||||
Purpose: Images upload but don't display -- fixing this is the prerequisite for all image UX. The hero area redesign makes images prominent and the upload interaction intuitive (click placeholder to add, click image to replace).
|
||||
|
||||
Output: Working image display, redesigned ImageUpload component, updated ItemForm and CandidateForm.
|
||||
</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/STATE.md
|
||||
|
||||
@src/client/components/ImageUpload.tsx
|
||||
@src/client/components/ItemForm.tsx
|
||||
@src/client/components/CandidateForm.tsx
|
||||
@src/client/lib/api.ts
|
||||
@src/server/routes/images.ts
|
||||
@src/server/index.ts
|
||||
@vite.config.ts
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs -->
|
||||
|
||||
From src/client/components/ImageUpload.tsx:
|
||||
```typescript
|
||||
interface ImageUploadProps {
|
||||
value: string | null;
|
||||
onChange: (filename: string | null) => void;
|
||||
}
|
||||
```
|
||||
|
||||
From src/client/lib/api.ts:
|
||||
```typescript
|
||||
export async function apiUpload<T>(url: string, file: File): Promise<T>
|
||||
// Uses FormData with field name "image"
|
||||
```
|
||||
|
||||
From src/server/routes/images.ts:
|
||||
```typescript
|
||||
// POST /api/images -> { filename: string } (201)
|
||||
// Saves to ./uploads/{timestamp}-{uuid}.{ext}
|
||||
```
|
||||
|
||||
From src/server/index.ts:
|
||||
```typescript
|
||||
// Static serving: app.use("/uploads/*", serveStatic({ root: "./" }));
|
||||
```
|
||||
|
||||
From vite.config.ts:
|
||||
```typescript
|
||||
// Dev proxy: "/uploads": "http://localhost:3000"
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Fix image display bug and investigate root cause</name>
|
||||
<files>src/client/components/ImageUpload.tsx, src/server/routes/images.ts, src/server/index.ts, vite.config.ts</files>
|
||||
<action>
|
||||
Investigate why uploaded images don't render in the UI. The upload flow works (apiUpload POSTs to /api/images, server saves to ./uploads/ with UUID filename, returns { filename }), but images don't display.
|
||||
|
||||
Debugging checklist (work through systematically):
|
||||
1. Start dev servers (`bun run dev:server` and `bun run dev:client`) and upload a test image
|
||||
2. Check the uploads/ directory -- does the file exist on disk?
|
||||
3. Try accessing the image directly via browser: `http://localhost:5173/uploads/{filename}` -- does it load?
|
||||
4. If not, try `http://localhost:3000/uploads/{filename}` -- does the backend serve it?
|
||||
5. Check Vite proxy config in vite.config.ts -- `/uploads` proxy to `http://localhost:3000` is configured
|
||||
6. Check Hono static serving in src/server/index.ts -- `serveStatic({ root: "./" })` should serve `./uploads/*`
|
||||
7. Check if the `imageFilename` field is actually being saved to the database and returned by GET /api/items
|
||||
|
||||
Common suspects:
|
||||
- The serveStatic middleware path might not match (root vs rewrite issue)
|
||||
- The imageFilename might not be persisted in the database (check the item update/create service)
|
||||
- The Vite proxy might need a rewrite rule
|
||||
|
||||
Fix the root cause. If the issue is in static file serving, fix the serveStatic config. If it's a database persistence issue, fix the service layer. If it's a proxy issue, fix vite.config.ts.
|
||||
|
||||
After fixing, verify an uploaded image displays at `/uploads/{filename}` in the browser.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/uploads/ 2>/dev/null; echo "Server static route configured"</automated>
|
||||
</verify>
|
||||
<done>Uploaded images display correctly when referenced via /uploads/{filename} path. The root cause is identified, documented in the summary, and fixed.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Redesign ImageUpload as hero area and move to top of forms</name>
|
||||
<files>src/client/components/ImageUpload.tsx, src/client/components/ItemForm.tsx, src/client/components/CandidateForm.tsx</files>
|
||||
<action>
|
||||
Redesign ImageUpload.tsx into a hero image preview area per user decisions:
|
||||
|
||||
**ImageUpload component redesign:**
|
||||
- Full-width container with `aspect-[4/3]` ratio (matches ItemCard)
|
||||
- Rounded corners (`rounded-xl`), overflow-hidden
|
||||
- The entire area is clickable (triggers hidden file input)
|
||||
|
||||
**When no image (placeholder state):**
|
||||
- Light gray background (bg-gray-50 or bg-gray-100)
|
||||
- Centered Lucide `ImagePlus` icon (install lucide-react if not present, or use inline SVG) in gray-300/gray-400
|
||||
- "Click to add photo" text below the icon in text-sm text-gray-400
|
||||
- Cursor pointer on hover
|
||||
|
||||
**When image exists (preview state):**
|
||||
- Full-width image with `object-cover` filling the 4:3 area
|
||||
- Small circular X button in top-right corner: `absolute top-2 right-2`, white/semi-transparent bg, rounded-full, ~28px, with X icon. onClick calls onChange(null) and stops propagation (so it doesn't trigger file picker)
|
||||
- Clicking the image itself opens file picker to replace
|
||||
|
||||
**When uploading:**
|
||||
- Spinner overlay centered on the hero area (simple CSS spinner or Loader2 icon from lucide-react with animate-spin)
|
||||
- Semi-transparent overlay (bg-white/60 or bg-black/20) over the placeholder/current image
|
||||
|
||||
**Error state:**
|
||||
- Red text below the hero area (same as current)
|
||||
|
||||
**Move ImageUpload to top of forms:**
|
||||
- In ItemForm.tsx: Move the `<ImageUpload>` from the bottom of the form (currently after Product Link) to the very first element, BEFORE the Name field. Remove the wrapping `<div>` with the "Image" label -- the hero area is self-explanatory.
|
||||
- In CandidateForm.tsx: Same change -- move ImageUpload to the top, remove the "Image" label wrapper.
|
||||
|
||||
Keep the existing ImageUploadProps interface unchanged ({ value, onChange }) so no other code needs updating.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<done>ImageUpload renders as a 4:3 hero area with placeholder icon when empty, full image preview when set, spinner during upload, and X button to remove. Both ItemForm and CandidateForm show ImageUpload as the first form element.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. Upload an image via ItemForm -- it should appear in the hero preview area immediately
|
||||
2. The hero area shows a placeholder icon when no image is set
|
||||
3. Clicking the placeholder opens the file picker
|
||||
4. Clicking an existing image opens the file picker to replace
|
||||
5. The X button removes the image
|
||||
6. CandidateForm has identical hero area behavior
|
||||
7. `bun run lint` passes
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Uploaded images display correctly (bug fixed)
|
||||
- Hero image area renders at top of ItemForm and CandidateForm
|
||||
- Placeholder with icon shown when no image set
|
||||
- Upload via click works, preview updates immediately
|
||||
- Remove button clears the image
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/05-image-handling/05-01-SUMMARY.md`
|
||||
</output>
|
||||
168
.planning/phases/05-image-handling/05-02-PLAN.md
Normal file
168
.planning/phases/05-image-handling/05-02-PLAN.md
Normal file
@@ -0,0 +1,168 @@
|
||||
---
|
||||
phase: 05-image-handling
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: [05-01]
|
||||
files_modified:
|
||||
- src/client/components/ItemCard.tsx
|
||||
- src/client/components/CandidateCard.tsx
|
||||
- src/client/routes/setups/$setupId.tsx
|
||||
autonomous: true
|
||||
requirements: [IMG-02]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Item cards always show a 4:3 image area, even when no image exists"
|
||||
- "Cards without images show a gray placeholder with the item's category emoji centered"
|
||||
- "Cards with images display the image in the 4:3 area"
|
||||
- "Candidate cards have the same placeholder treatment as item cards"
|
||||
- "Setup item lists show small square thumbnails (~40px) next to item names"
|
||||
- "Setup thumbnails show category emoji placeholder when item has no image"
|
||||
artifacts:
|
||||
- path: "src/client/components/ItemCard.tsx"
|
||||
provides: "Always-visible 4:3 image area with placeholder fallback"
|
||||
- path: "src/client/components/CandidateCard.tsx"
|
||||
provides: "Always-visible 4:3 image area with placeholder fallback"
|
||||
- path: "src/client/routes/setups/$setupId.tsx"
|
||||
provides: "Small square thumbnails in setup item list"
|
||||
key_links:
|
||||
- from: "src/client/components/ItemCard.tsx"
|
||||
to: "/uploads/{imageFilename}"
|
||||
via: "img src attribute"
|
||||
pattern: "src=.*uploads"
|
||||
- from: "src/client/routes/setups/$setupId.tsx"
|
||||
to: "/uploads/{imageFilename}"
|
||||
via: "img src for thumbnails"
|
||||
pattern: "src=.*uploads"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add image placeholders to all gear cards (items and candidates) so every card has a consistent 4:3 image area, and add small thumbnails to setup item lists.
|
||||
|
||||
Purpose: Consistent card heights in the grid (no layout shift between cards with/without images) and visual context in setup lists via thumbnails.
|
||||
|
||||
Output: Updated ItemCard, CandidateCard, and setup detail route with image placeholders and thumbnails.
|
||||
</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/STATE.md
|
||||
@.planning/phases/05-image-handling/05-01-SUMMARY.md
|
||||
|
||||
@src/client/components/ItemCard.tsx
|
||||
@src/client/components/CandidateCard.tsx
|
||||
@src/client/routes/setups/$setupId.tsx
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts the executor needs -->
|
||||
|
||||
From src/client/components/ItemCard.tsx:
|
||||
```typescript
|
||||
interface ItemCardProps {
|
||||
id: number;
|
||||
name: string;
|
||||
weightGrams: number | null;
|
||||
priceCents: number | null;
|
||||
categoryName: string;
|
||||
categoryEmoji: string;
|
||||
imageFilename: string | null;
|
||||
onRemove?: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
From src/client/components/CandidateCard.tsx:
|
||||
```typescript
|
||||
interface CandidateCardProps {
|
||||
id: number;
|
||||
name: string;
|
||||
weightGrams: number | null;
|
||||
priceCents: number | null;
|
||||
categoryName: string;
|
||||
categoryEmoji: string;
|
||||
imageFilename: string | null;
|
||||
threadId: number;
|
||||
isActive: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
Setup route renders items via ItemCard with all props including categoryEmoji and imageFilename.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Add always-visible 4:3 image area with placeholders to ItemCard and CandidateCard</name>
|
||||
<files>src/client/components/ItemCard.tsx, src/client/components/CandidateCard.tsx</files>
|
||||
<action>
|
||||
Update both ItemCard and CandidateCard to ALWAYS render the 4:3 image area (currently they conditionally render it only when imageFilename exists).
|
||||
|
||||
**ItemCard.tsx changes:**
|
||||
- Replace the conditional `{imageFilename && (...)}` block with an always-rendered `<div className="aspect-[4/3] bg-gray-50">` container
|
||||
- When imageFilename exists: render `<img src={/uploads/${imageFilename}} alt={name} className="w-full h-full object-cover" />` (same as current)
|
||||
- When imageFilename is null: render a centered placeholder with the category emoji. Use `<div className="w-full h-full flex flex-col items-center justify-center">` containing a `<span className="text-3xl">{categoryEmoji}</span>`. The gray-50 background provides the subtle placeholder look.
|
||||
|
||||
**CandidateCard.tsx changes:**
|
||||
- Identical treatment: always render the 4:3 area, show image or category emoji placeholder
|
||||
- Same structure as ItemCard
|
||||
|
||||
Both cards already receive categoryEmoji as a prop, so no prop changes needed.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<done>Every ItemCard and CandidateCard renders a 4:3 image area. Cards with images show the image; cards without show a gray placeholder with the category emoji centered.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add small thumbnails to setup item lists</name>
|
||||
<files>src/client/routes/setups/$setupId.tsx</files>
|
||||
<action>
|
||||
The setup detail page currently renders items using ItemCard in a grid. The setup also has a concept of item lists. Add small square thumbnails next to item names in the setup's item display.
|
||||
|
||||
Since the setup page uses ItemCard components in a grid (which now have the 4:3 area from Task 1), the card-level display is already handled. The additional work here is for any list-style display of setup items.
|
||||
|
||||
Check the setup detail route for list-view rendering of items. If items are only shown via ItemCard grid, then this task focuses on ensuring the ItemCard placeholder works in the setup context. If there's a separate list view, add thumbnails:
|
||||
|
||||
**Thumbnail spec (for list views):**
|
||||
- Small square image: `w-10 h-10 rounded-lg object-cover flex-shrink-0` (~40px)
|
||||
- Placed to the left of the item name in a flex row
|
||||
- When imageFilename exists: `<img src={/uploads/${imageFilename}} />`
|
||||
- When null: `<div className="w-10 h-10 rounded-lg bg-gray-50 flex items-center justify-center flex-shrink-0"><span className="text-sm">{categoryEmoji}</span></div>`
|
||||
|
||||
If the setup page only uses ItemCard (no list view), verify the ItemCard changes from Task 1 render correctly in the setup context and note this in the summary.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/jean-luc-makiola/Development/projects/GearBox && bun run lint 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<done>Setup item lists show small square thumbnails (or category emoji placeholders) next to item names. If setup only uses ItemCard grid, the placeholder from Task 1 renders correctly in setup context.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. Item cards in the gear collection always show a 4:3 area (no layout jump between cards with/without images)
|
||||
2. Cards without images show gray background with category emoji centered
|
||||
3. Cards with images show the image with object-cover
|
||||
4. Candidate cards have identical placeholder behavior
|
||||
5. Setup item display includes image context (thumbnails or card placeholders)
|
||||
6. `bun run lint` passes
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- All gear cards have consistent heights due to always-present 4:3 image area
|
||||
- Placeholder shows category emoji when no image exists
|
||||
- Setup items show image context (thumbnail or card placeholder)
|
||||
- No layout shift between cards with and without images
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/05-image-handling/05-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user