---
phase: 29-image-presentation
plan: 05
type: execute
wave: 1
depends_on: []
files_modified:
  - src/client/components/ImageUpload.tsx
autonomous: true
gap_closure: true
requirements: []

must_haves:
  truths:
    - "After cropping in the upload crop editor, the GearImage preview immediately reflects the crop values without needing to save the form"
  artifacts:
    - path: "src/client/components/ImageUpload.tsx"
      provides: "Local crop state that feeds GearImage preview"
      contains: "cropZoom"
  key_links:
    - from: "ImageCropEditor onSave"
      to: "GearImage cropZoom/cropX/cropY props"
      via: "local state in ImageUpload"
      pattern: "localCrop"
---

<objective>
Fix cropped image preview not updating immediately after cropping in edit mode.

Purpose: When a user crops an image via the ImageCropEditor inside ImageUpload, the preview should reflect the crop immediately — not only after form save and query refetch.

Output: ImageUpload component with local crop state that feeds into GearImage preview props.
</objective>

<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.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/GearImage.tsx
@src/client/components/ImageCropEditor.tsx

<interfaces>
<!-- GearImage accepts optional crop props -->
From src/client/components/GearImage.tsx:
```typescript
interface GearImageProps {
  src: string;
  alt: string;
  dominantColor?: string | null;
  cropZoom?: number | null;
  cropX?: number | null;
  cropY?: number | null;
  className?: string;
  cover?: boolean;
}
```

<!-- ImageCropEditor returns CropResult on save -->
From src/client/components/ImageCropEditor.tsx:
```typescript
interface CropResult {
  zoom: number;
  x: number;
  y: number;
}
// onSave: (result: CropResult) => void;
```

<!-- ImageUpload current props -->
From src/client/components/ImageUpload.tsx:
```typescript
interface ImageUploadProps {
  value: string | null;
  imageUrl?: string | null;
  dominantColor?: string | null;
  onChange: (filename: string | null, dominantColor?: string | null) => void;
  onCropChange?: (crop: { zoom: number; x: number; y: number }) => void;
}
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add local crop state to ImageUpload and wire to GearImage preview</name>
  <files>src/client/components/ImageUpload.tsx</files>
  <action>
In ImageUpload.tsx, make these changes:

1. Add a local crop state to track the most recent crop values:
   ```typescript
   const [localCrop, setLocalCrop] = useState<{ zoom: number; x: number; y: number } | null>(null);
   ```

2. In the ImageCropEditor onSave handler (around line 88-91), update localCrop before calling the parent onCropChange:
   ```typescript
   onSave={(result) => {
     setLocalCrop(result);
     onCropChange(result);
     setShowCropEditor(false);
   }}
   ```

3. In the GearImage render (around line 110-114), pass localCrop values as props:
   ```typescript
   <GearImage
     src={displayUrl}
     alt="Item"
     dominantColor={dominantColor}
     cropZoom={localCrop?.zoom}
     cropX={localCrop?.x}
     cropY={localCrop?.y}
   />
   ```

4. When the image is removed (handleRemove), also clear localCrop:
   ```typescript
   function handleRemove(e: React.MouseEvent) {
     e.stopPropagation();
     setLocalPreview(null);
     setLocalCrop(null);
     onChange(null);
   }
   ```

This ensures the GearImage preview immediately reflects crop adjustments without waiting for a server round-trip and query refetch.
  </action>
  <verify>
    <automated>cd /home/jlmak/Projects/jlmak/GearBox && bunx tsc --noEmit --pretty 2>&1 | head -30</automated>
  </verify>
  <done>After using the crop editor on an uploaded image, the GearImage preview in ImageUpload immediately shows the cropped framing. Removing the image clears both the preview and crop state.</done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

No new trust boundaries — this is a client-side-only state management fix within existing components.

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-29-05-01 | T (Tampering) | localCrop state | accept | Client-side display only; actual crop values are persisted via existing server mutation in parent component |
</threat_model>

<verification>
1. TypeScript compiles without errors
2. Manual: Open item in edit mode, upload image, crop it, verify preview shows crop immediately (without clicking Save)
3. Manual: Open existing item in edit mode, click crop button, adjust, save framing — preview updates immediately
</verification>

<success_criteria>
- Cropped image preview updates in edit state immediately after cropping, without needing to save the form
- No TypeScript errors
- Image removal clears crop state
</success_criteria>

<output>
After completion, create `.planning/phases/29-image-presentation/29-05-SUMMARY.md`
</output>
