Compare commits
9 Commits
159ff824b2
...
ba13fa8ded
| Author | SHA1 | Date | |
|---|---|---|---|
| ba13fa8ded | |||
| 13883ea14d | |||
| bedef04581 | |||
| c1177764ef | |||
| ded6bf521e | |||
| d91d32deaf | |||
| c98ac6e46f | |||
| e536f68bd1 | |||
| 80cb313b08 |
@@ -257,7 +257,7 @@ Plans:
|
||||
| 26. Discovery Landing Page | v2.1 | 3/3 | Complete | 2026-04-10 |
|
||||
| 27. Top Nav Restructure & Search Bar Rethink | v2.1 | 4/4 | Complete | 2026-04-12 |
|
||||
| 28. Profile & Logto Integration | v2.2 | 3/3 | Complete | 2026-04-12 |
|
||||
| 29. Image Presentation | v2.2 | 4/4 | Complete | 2026-04-12 |
|
||||
| 29. Image Presentation | v2.2 | 5/5 | Complete | 2026-04-13 |
|
||||
| 30. Onboarding Redesign | v2.2 | 3/3 | Complete | 2026-04-12 |
|
||||
| 31. Mobile Polish | v2.2 | 2/2 | Complete | 2026-04-12 |
|
||||
| 32. Setup Sharing System | v2.3 | TBD | Pending | — |
|
||||
|
||||
@@ -80,7 +80,7 @@ v2.1 decisions:
|
||||
|
||||
### Pending Todos
|
||||
|
||||
None active.
|
||||
- Fix Add Candidate button shows wrong modal on thread page (ui)
|
||||
|
||||
### Blockers/Concerns
|
||||
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
"plan_check": true,
|
||||
"verifier": true,
|
||||
"nyquist_validation": true,
|
||||
"_auto_chain_active": true
|
||||
"_auto_chain_active": false
|
||||
}
|
||||
}
|
||||
55
.planning/debug/crop-preview-edit-state.md
Normal file
55
.planning/debug/crop-preview-edit-state.md
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
status: diagnosed
|
||||
trigger: "crop editor opens on upload correctly, but after cropping the cropped image isn't shown in the edit state always — after clicking save it is shown correctly"
|
||||
created: 2026-04-13T12:30:00Z
|
||||
updated: 2026-04-13T12:35:00Z
|
||||
---
|
||||
|
||||
## Current Focus
|
||||
|
||||
hypothesis: GearImage in ImageUpload receives no crop props after cropping — crop values are sent to server via onCropChange but never stored locally or passed to the preview GearImage
|
||||
test: trace data flow from ImageCropEditor.onSave through ImageUpload to GearImage rendering
|
||||
expecting: GearImage in ImageUpload has no cropZoom/cropX/cropY props
|
||||
next_action: return diagnosis
|
||||
|
||||
## Symptoms
|
||||
|
||||
expected: After cropping in the crop editor, the image preview in edit mode should immediately reflect the crop
|
||||
actual: Cropped image not shown in edit state after cropping; shows correctly only after Save
|
||||
errors: None
|
||||
reproduction: Upload image to item -> crop editor opens -> adjust crop -> close editor -> preview shows uncropped image -> Save item -> page re-renders with crop applied
|
||||
started: Since Phase 29 implementation
|
||||
|
||||
## Eliminated
|
||||
|
||||
(none needed — root cause found on first hypothesis)
|
||||
|
||||
## Evidence
|
||||
|
||||
- timestamp: 2026-04-13T12:32:00Z
|
||||
checked: ImageUpload.tsx lines 83-95 — ImageCropEditor onSave handler
|
||||
found: onSave calls onCropChange(result) then setShowCropEditor(false). The crop values are passed up to the parent but NOT stored in any local state within ImageUpload.
|
||||
implication: After crop editor closes, ImageUpload has no memory of what crop was applied.
|
||||
|
||||
- timestamp: 2026-04-13T12:33:00Z
|
||||
checked: ImageUpload.tsx lines 109-114 — GearImage rendering after crop editor closes
|
||||
found: GearImage is rendered with only src, alt, and dominantColor props. NO cropZoom, cropX, or cropY props are passed. The component never receives crop values.
|
||||
implication: GearImage renders uncropped because it literally has no crop data to apply.
|
||||
|
||||
- timestamp: 2026-04-13T12:34:00Z
|
||||
checked: $itemId.tsx lines 277-294 — onCropChange callback in item detail page
|
||||
found: onCropChange triggers updateItem.mutate() which sends crop values to the server immediately. This is a fire-and-forget mutation — it does NOT update local state or the React Query cache synchronously.
|
||||
implication: Crop values reach the server, but the local component tree has no access to them until the query is invalidated/refetched.
|
||||
|
||||
- timestamp: 2026-04-13T12:34:30Z
|
||||
checked: $itemId.tsx lines 326-335 — GearImage in non-edit view mode
|
||||
found: Non-edit view reads cropZoom, cropX, cropY from item (React Query cache data). After Save, the mutation invalidates the query, item refetches with crop values, and GearImage renders correctly.
|
||||
implication: Confirms the "works after save" behavior — the query refetch provides the crop data.
|
||||
|
||||
## Resolution
|
||||
|
||||
root_cause: ImageUpload component does not track crop values locally after the crop editor closes. When the crop editor's onSave fires, the crop values are forwarded to the parent ($itemId.tsx) which sends them to the server via updateItem.mutate(), but no local state is updated. The GearImage rendered inside ImageUpload receives zero crop-related props (cropZoom, cropX, cropY are never passed). So the preview always shows the uncropped/default image. After the user clicks Save on the item form, the React Query cache is invalidated, the item refetches with server-side crop values, and the page re-renders in view mode with the correct crop applied.
|
||||
|
||||
fix: (not applied — diagnosis only)
|
||||
verification: (not applied — diagnosis only)
|
||||
files_changed: []
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
status: partial
|
||||
status: complete
|
||||
phase: 28-profile-and-logto-integration
|
||||
source: [28-01-SUMMARY.md, 28-02-SUMMARY.md, 28-03-SUMMARY.md]
|
||||
started: 2026-04-12T18:30:00Z
|
||||
updated: 2026-04-12T19:00:00Z
|
||||
updated: 2026-04-12T21:00:00Z
|
||||
---
|
||||
|
||||
## Current Test
|
||||
@@ -24,29 +24,23 @@ result: pass
|
||||
expected: /settings page shows only app preferences: weight unit, currency, import/export, API keys. No profile section.
|
||||
result: pass
|
||||
|
||||
### 4. Edit display name and bio
|
||||
expected: On /profile, change display name and bio, click Save. Success message appears. Refreshing the page shows the updated values.
|
||||
result: issue
|
||||
reported: "Save works for name/bio after Zod null fix, but avatar upload doesn't persist after save. Also needs: crop editor for avatar, larger profile pic with name/bio side-by-side layout, click-to-open modal with full-size image + Remove/Update buttons."
|
||||
severity: major
|
||||
### 4. Edit display name, bio, and avatar
|
||||
expected: On /profile, upload an avatar, change display name and bio, click Save. Avatar image renders. Refreshing shows updated values.
|
||||
result: pass
|
||||
reported: "Fixed: avatar now uses presigned S3 URLs instead of /uploads/ paths. Avatar also shows in top nav."
|
||||
|
||||
### 5. Email display
|
||||
expected: Account Info section shows your email address (from Logto) and a "Change" button next to it.
|
||||
result: blocked
|
||||
blocked_by: third-party
|
||||
reason: "Logto Management API returns 500 — M2M env vars (LOGTO_MANAGEMENT_API_ENDPOINT, LOGTO_M2M_APP_ID, LOGTO_M2M_APP_SECRET) not configured on test env"
|
||||
result: pass
|
||||
reported: "Fixed: M2M credentials configured, email change now reflects in UI immediately via optimistic cache update."
|
||||
|
||||
### 6. Password change form
|
||||
expected: Security section shows a password change form. If you signed in with email+password: current password, new password, confirm new password fields. If social login only: just new password + confirm fields.
|
||||
result: blocked
|
||||
blocked_by: third-party
|
||||
reason: "Logto Management API returns 500 — M2M env vars not configured on test env"
|
||||
expected: Security section shows a password change form. Current password, new password, confirm new password fields.
|
||||
result: pass
|
||||
|
||||
### 7. Delete account UI
|
||||
expected: Danger Zone shows a red-bordered card with "Delete Account" button. Clicking it shows a confirmation dialog requiring you to type "DELETE" before proceeding.
|
||||
result: blocked
|
||||
blocked_by: third-party
|
||||
reason: "Logto Management API returns 500 — M2M env vars not configured on test env"
|
||||
result: pass
|
||||
|
||||
### 8. Member-since date
|
||||
expected: Account Info section shows a "Member since" date formatted nicely (e.g., "April 2026").
|
||||
@@ -55,22 +49,12 @@ result: pass
|
||||
## Summary
|
||||
|
||||
total: 8
|
||||
passed: 4
|
||||
issues: 1
|
||||
passed: 8
|
||||
issues: 0
|
||||
pending: 0
|
||||
skipped: 0
|
||||
blocked: 3
|
||||
blocked: 0
|
||||
|
||||
## Gaps
|
||||
|
||||
- truth: "Avatar upload should persist after save. Profile pic should have crop editor, larger display with side-by-side layout, and click-to-open modal."
|
||||
status: failed
|
||||
reason: "User reported: avatar upload doesn't persist. Also needs crop editor, layout redesign (larger pic, name/bio beside it), click-to-open modal with Remove/Update."
|
||||
severity: major
|
||||
test: 4
|
||||
artifacts:
|
||||
- src/client/components/ProfileSection.tsx
|
||||
missing:
|
||||
- Avatar save persistence fix
|
||||
- Crop editor integration for avatar
|
||||
- Profile layout redesign (larger pic, side-by-side, modal)
|
||||
[none]
|
||||
|
||||
169
.planning/phases/29-image-presentation/29-05-PLAN.md
Normal file
169
.planning/phases/29-image-presentation/29-05-PLAN.md
Normal file
@@ -0,0 +1,169 @@
|
||||
---
|
||||
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>
|
||||
34
.planning/phases/29-image-presentation/29-05-SUMMARY.md
Normal file
34
.planning/phases/29-image-presentation/29-05-SUMMARY.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
phase: 29-image-presentation
|
||||
plan: 05
|
||||
status: complete
|
||||
gap_closure: true
|
||||
started: 2026-04-13T12:00:00Z
|
||||
completed: 2026-04-13T12:10:00Z
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Fixed cropped image preview not updating immediately in edit mode. Added `localCrop` state to `ImageUpload` that captures crop values from `ImageCropEditor` and passes them to `GearImage` as props. Previously, the preview only reflected crop settings after saving the form and refetching from the server.
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Added `localCrop` useState to ImageUpload for immediate crop feedback
|
||||
- Wired ImageCropEditor onSave to set localCrop before forwarding to parent
|
||||
- Passed localCrop values (cropZoom, cropX, cropY) to GearImage preview
|
||||
- Clear localCrop on image removal to prevent stale state
|
||||
|
||||
## Key Files
|
||||
|
||||
### Modified
|
||||
- `src/client/components/ImageUpload.tsx` — local crop state + GearImage prop wiring
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- TypeScript compiles without errors (no new errors in ImageUpload.tsx)
|
||||
- Local crop state correctly flows: ImageCropEditor → localCrop → GearImage props
|
||||
- Image removal clears both preview and crop state
|
||||
|
||||
## Deviations
|
||||
|
||||
None — implemented exactly as planned.
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
status: complete
|
||||
status: diagnosed
|
||||
phase: 29-image-presentation
|
||||
source: [29-01-SUMMARY.md, 29-02-SUMMARY.md, 29-03-SUMMARY.md, 29-04-SUMMARY.md]
|
||||
started: 2026-04-12T19:10:00Z
|
||||
updated: 2026-04-12T19:20:00Z
|
||||
updated: 2026-04-13T12:15:00Z
|
||||
---
|
||||
|
||||
## Current Test
|
||||
@@ -14,102 +14,53 @@ updated: 2026-04-12T19:20:00Z
|
||||
|
||||
### 1. Images use fit-within instead of crop
|
||||
expected: Browse any page with item/catalog cards. Images should fit inside the frame without cropping — full image visible, no parts cut off.
|
||||
result: issue
|
||||
reported: "Fit-within works on detail pages but collection view cards don't render images at all."
|
||||
severity: major
|
||||
result: pass
|
||||
|
||||
### 2. Dominant color background fill
|
||||
expected: Where an image doesn't fill the entire frame, the empty space is filled with a color extracted from the image (not white or gray).
|
||||
result: issue
|
||||
reported: "Background is just plain gray. Even newly uploaded images don't get a dominant color — extraction not working or color not passed to frontend."
|
||||
severity: major
|
||||
result: pass
|
||||
|
||||
### 3. Crop editor on item detail
|
||||
expected: Open an item that has an image. You should see an "Adjust framing" button. Clicking it opens a crop editor with zoom slider.
|
||||
result: issue
|
||||
reported: "'Adjust framing' text doesn't feel like an action. Should be an icon button, only visible in edit mode, positioned below the X icon. X icon should be a trash icon to symbolize removal."
|
||||
severity: minor
|
||||
expected: Open an item that has an image. In edit mode, you should see a crop icon button next to the trash icon, positioned as an overlay on the image. Clicking it opens a crop editor with zoom slider.
|
||||
result: pass
|
||||
reported: "Initially reported as issue but confirmed working on re-test — false claim"
|
||||
|
||||
### 4. Crop editor on image upload
|
||||
expected: Upload a new image to an item. After the upload completes, a crop editor should appear.
|
||||
expected: Upload a new image to an item. After the upload completes, a crop editor should appear automatically. After cropping, the preview should reflect the crop immediately.
|
||||
result: issue
|
||||
reported: "Crop editor doesn't open when adding an image to an existing item. Can only be edited afterward."
|
||||
severity: major
|
||||
reported: "crop editor opens on upload correctly, but after cropping the cropped image isn't shown in the edit state always — after clicking save it is shown correctly"
|
||||
severity: minor
|
||||
|
||||
### 5. Crop settings persist
|
||||
expected: Adjust the crop on an item image, save it. Navigate away and come back — image displays with saved crop settings.
|
||||
result: issue
|
||||
reported: "Framing adjustment doesn't save. No error message, no console log — silently fails."
|
||||
severity: blocker
|
||||
result: pass
|
||||
|
||||
### 6. Consistency across surfaces
|
||||
expected: All image surfaces use the same fit-within + dominant color treatment.
|
||||
result: pass
|
||||
reported: "Consistent across all surfaces where images render (detail pages). Collection cards don't render images (see test 1)."
|
||||
|
||||
## Summary
|
||||
|
||||
total: 6
|
||||
passed: 2
|
||||
issues: 4
|
||||
passed: 5
|
||||
issues: 1
|
||||
pending: 0
|
||||
skipped: 0
|
||||
blocked: 0
|
||||
|
||||
## Gaps
|
||||
|
||||
- truth: "Collection view cards should render images using GearImage component"
|
||||
- truth: "Cropped image preview should update in edit state immediately after cropping"
|
||||
status: failed
|
||||
reason: "User reported: images not rendering on collection view cards, only on detail pages"
|
||||
severity: major
|
||||
test: 1
|
||||
artifacts:
|
||||
- src/client/components/ItemCard.tsx
|
||||
- src/client/components/GearImage.tsx
|
||||
missing:
|
||||
- GearImage integration in collection card view
|
||||
|
||||
- truth: "Dominant color should be extracted on upload and used as background fill"
|
||||
status: failed
|
||||
reason: "User reported: background is plain gray even for newly uploaded images — extraction not working or color not reaching frontend"
|
||||
severity: major
|
||||
test: 2
|
||||
artifacts:
|
||||
- src/server/services/image.service.ts
|
||||
- src/client/components/GearImage.tsx
|
||||
missing:
|
||||
- Debug extractDominantColor pipeline
|
||||
- Verify color is returned from upload API and stored in DB
|
||||
- Verify frontend reads and applies dominantColor
|
||||
|
||||
- truth: "Crop editor should be an icon button visible only in edit mode, with trash icon for image removal"
|
||||
status: failed
|
||||
reason: "User reported: 'Adjust framing' text doesn't feel like an action. Should be icon, edit-mode only. X should be trash icon."
|
||||
reason: "User reported: cropped image not shown in edit state after cropping, but renders correctly after save"
|
||||
severity: minor
|
||||
test: 3
|
||||
artifacts:
|
||||
- src/client/routes/items/$itemId.tsx
|
||||
- src/client/components/ImageCropEditor.tsx
|
||||
missing:
|
||||
- Redesign crop trigger as icon button in edit mode
|
||||
- Replace X with trash icon for image removal
|
||||
|
||||
- truth: "Crop editor should open automatically when uploading a new image"
|
||||
status: failed
|
||||
reason: "User reported: crop editor doesn't open when adding image to existing item"
|
||||
severity: major
|
||||
test: 4
|
||||
root_cause: "ImageUpload component does not store or forward crop values to its GearImage preview after crop editor closes. onCropChange sends to server but no local state is updated. GearImage in ImageUpload receives zero crop props. Only after form save + query refetch do crop values appear."
|
||||
artifacts:
|
||||
- src/client/components/ImageUpload.tsx
|
||||
- path: "src/client/components/ImageUpload.tsx"
|
||||
issue: "GearImage preview (line 110-114) rendered without cropZoom/cropX/cropY props; no local crop state exists"
|
||||
- path: "src/client/routes/items/$itemId.tsx"
|
||||
issue: "onCropChange (line 288-293) fires server mutation but updates no local/form state"
|
||||
missing:
|
||||
- Trigger crop editor after upload in ImageUpload component
|
||||
|
||||
- truth: "Crop settings (zoom, x, y) should persist to DB and render on subsequent views"
|
||||
status: failed
|
||||
reason: "User reported: framing adjustment doesn't save, no error, no log — silent failure"
|
||||
severity: blocker
|
||||
test: 5
|
||||
artifacts:
|
||||
- src/client/routes/items/$itemId.tsx
|
||||
- src/server/routes/items.ts
|
||||
missing:
|
||||
- Debug crop save pipeline (client mutation → API → DB)
|
||||
- Add local crop state in ImageUpload that gets set from crop editor result and passed as props to GearImage
|
||||
debug_session: ".planning/debug/crop-preview-edit-state.md"
|
||||
|
||||
@@ -3,12 +3,12 @@ status: partial
|
||||
phase: 30-onboarding-redesign
|
||||
source: [30-01-SUMMARY.md, 30-02-SUMMARY.md, 30-03-SUMMARY.md]
|
||||
started: 2026-04-12T19:30:00Z
|
||||
updated: 2026-04-12T19:40:00Z
|
||||
updated: 2026-04-13T12:30:00Z
|
||||
---
|
||||
|
||||
## Current Test
|
||||
|
||||
[testing complete]
|
||||
[testing paused — 3 items blocked by catalog seed data]
|
||||
|
||||
## Tests
|
||||
|
||||
@@ -30,19 +30,19 @@ severity: cosmetic
|
||||
expected: After picking a hobby, you see a grid of popular catalog items filtered by that hobby.
|
||||
result: blocked
|
||||
blocked_by: server
|
||||
reason: "Catalog is empty on test server — no items to browse. Needs seed data migration for test env."
|
||||
reason: "Catalog is empty on test server — need some kind of seeding for the test env."
|
||||
|
||||
### 5. Review screen
|
||||
expected: After selecting items, a review/summary screen shows all selections grouped by category.
|
||||
result: blocked
|
||||
blocked_by: server
|
||||
reason: "Depends on test 4 — no catalog items to select."
|
||||
blocked_by: prior-phase
|
||||
reason: "Depends on test 4 — catalog seed data needed."
|
||||
|
||||
### 6. Completion and collection
|
||||
expected: After confirming, items are batch-added to collection with auto-created categories.
|
||||
result: blocked
|
||||
blocked_by: server
|
||||
reason: "Depends on test 4 — no catalog items to select."
|
||||
blocked_by: prior-phase
|
||||
reason: "Depends on test 4 — catalog seed data needed."
|
||||
|
||||
### 7. Onboarding doesn't show again
|
||||
expected: Refresh the page or sign out and back in. Onboarding does NOT appear again.
|
||||
@@ -65,6 +65,7 @@ blocked: 3
|
||||
severity: cosmetic
|
||||
test: 3
|
||||
artifacts:
|
||||
- src/client/components/onboarding/OnboardingHobbyPicker.tsx
|
||||
- path: "src/client/components/onboarding/OnboardingHobbyPicker.tsx"
|
||||
issue: "Weak selected state styling"
|
||||
missing:
|
||||
- Stronger selected state styling (dark bg, inverted colors)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
created: 2026-04-13T11:39:30.356Z
|
||||
title: Fix Add Candidate button shows wrong modal on thread page
|
||||
area: ui
|
||||
files:
|
||||
- src/client/routes/threads/$threadId.tsx
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
The "Add Candidate" button at the top of the thread detail page opens a manual-add modal (plain form fields) instead of the catalog search dialogue. The FAB (floating action button) in the bottom right of the same page correctly opens the catalog search dialog where you can browse and pick from global items. Both buttons should behave the same way — showing the catalog search dialog as the primary add flow.
|
||||
|
||||
## Solution
|
||||
|
||||
Wire the top "Add Candidate" button to open the same catalog search dialog/overlay that the FAB triggers. The manual-add form should still be reachable as a fallback (e.g., "Can't find it? Add manually") but not be the default.
|
||||
@@ -25,6 +25,11 @@ export function ImageUpload({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [localPreview, setLocalPreview] = useState<string | null>(null);
|
||||
const [showCropEditor, setShowCropEditor] = useState(false);
|
||||
const [localCrop, setLocalCrop] = useState<{
|
||||
zoom: number;
|
||||
x: number;
|
||||
y: number;
|
||||
} | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
@@ -70,6 +75,7 @@ export function ImageUpload({
|
||||
function handleRemove(e: React.MouseEvent) {
|
||||
e.stopPropagation();
|
||||
setLocalPreview(null);
|
||||
setLocalCrop(null);
|
||||
onChange(null);
|
||||
}
|
||||
|
||||
@@ -86,6 +92,7 @@ export function ImageUpload({
|
||||
imageUrl={displayUrl}
|
||||
dominantColor={dominantColor}
|
||||
onSave={(result) => {
|
||||
setLocalCrop(result);
|
||||
onCropChange(result);
|
||||
setShowCropEditor(false);
|
||||
}}
|
||||
@@ -111,6 +118,9 @@ export function ImageUpload({
|
||||
src={displayUrl}
|
||||
alt="Item"
|
||||
dominantColor={dominantColor}
|
||||
cropZoom={localCrop?.zoom}
|
||||
cropX={localCrop?.x}
|
||||
cropY={localCrop?.y}
|
||||
/>
|
||||
{/* Crop button */}
|
||||
{onCropChange && (
|
||||
|
||||
@@ -369,36 +369,45 @@ export const DEV_GLOBAL_ITEMS = [
|
||||
// Maps global item index -> tag names. Tags are seeded by seedGlobalItems().
|
||||
|
||||
export const DEV_TAG_ASSIGNMENTS = [
|
||||
{ globalItemIndex: 0, tagNames: ["saddlebag", "bike-bag"] },
|
||||
{ globalItemIndex: 1, tagNames: ["handlebar-bag", "bike-bag"] },
|
||||
{ globalItemIndex: 2, tagNames: ["framebag", "bike-bag"] },
|
||||
{ globalItemIndex: 3, tagNames: ["handlebar-bag", "bike-bag"] },
|
||||
{ globalItemIndex: 4, tagNames: ["framebag", "bike-bag"] },
|
||||
{ globalItemIndex: 5, tagNames: ["top-tube-bag", "bike-bag"] },
|
||||
{ globalItemIndex: 6, tagNames: ["tent"] },
|
||||
{ globalItemIndex: 7, tagNames: ["tent"] },
|
||||
{ globalItemIndex: 8, tagNames: ["tent"] },
|
||||
{ globalItemIndex: 9, tagNames: ["tent"] },
|
||||
{ globalItemIndex: 10, tagNames: ["quilt"] },
|
||||
{ globalItemIndex: 11, tagNames: ["sleeping-pad"] },
|
||||
{ globalItemIndex: 12, tagNames: ["sleeping-pad"] },
|
||||
{ globalItemIndex: 13, tagNames: ["pillow"] },
|
||||
{ globalItemIndex: 14, tagNames: ["sleeping-bag"] },
|
||||
{ globalItemIndex: 15, tagNames: ["stove"] },
|
||||
{ globalItemIndex: 16, tagNames: ["stove"] },
|
||||
{ globalItemIndex: 17, tagNames: ["cookware", "mug"] },
|
||||
{ globalItemIndex: 18, tagNames: ["cookware"] },
|
||||
{ globalItemIndex: 19, tagNames: ["stove"] },
|
||||
{ globalItemIndex: 20, tagNames: ["headlamp"] },
|
||||
{ globalItemIndex: 21, tagNames: ["bike-light"] },
|
||||
{ globalItemIndex: 22, tagNames: ["headlamp"] },
|
||||
{ globalItemIndex: 29, tagNames: ["water-filter"] },
|
||||
{ globalItemIndex: 30, tagNames: ["water-filter"] },
|
||||
{ globalItemIndex: 31, tagNames: ["water-bottle"] },
|
||||
{ globalItemIndex: 32, tagNames: ["multi-tool", "repair-kit"] },
|
||||
{ globalItemIndex: 33, tagNames: ["rain-jacket"] },
|
||||
{ globalItemIndex: 34, tagNames: ["bike-computer", "gps"] },
|
||||
{ globalItemIndex: 35, tagNames: ["handlebar-bag", "bike-bag", "dry-bag"] },
|
||||
// Bags — bikepacking/cycling gear
|
||||
{ globalItemIndex: 0, tagNames: ["saddlebag", "bike-bag", "bikepacking", "cycling"] },
|
||||
{ globalItemIndex: 1, tagNames: ["handlebar-bag", "bike-bag", "bikepacking", "cycling"] },
|
||||
{ globalItemIndex: 2, tagNames: ["framebag", "bike-bag", "bikepacking", "cycling"] },
|
||||
{ globalItemIndex: 3, tagNames: ["handlebar-bag", "bike-bag", "bikepacking", "cycling"] },
|
||||
{ globalItemIndex: 4, tagNames: ["framebag", "bike-bag", "bikepacking", "cycling"] },
|
||||
{ globalItemIndex: 5, tagNames: ["top-tube-bag", "bike-bag", "bikepacking", "cycling"] },
|
||||
// Shelter — camping/hiking/bikepacking
|
||||
{ globalItemIndex: 6, tagNames: ["tent", "camping", "hiking", "bikepacking", "backpacking"] },
|
||||
{ globalItemIndex: 7, tagNames: ["tent", "camping", "hiking", "bikepacking", "backpacking"] },
|
||||
{ globalItemIndex: 8, tagNames: ["tent", "camping", "hiking", "backpacking"] },
|
||||
{ globalItemIndex: 9, tagNames: ["tent", "camping", "hiking", "backpacking", "climbing", "mountaineering"] },
|
||||
// Sleep — camping/hiking/bikepacking
|
||||
{ globalItemIndex: 10, tagNames: ["quilt", "camping", "hiking", "bikepacking", "backpacking"] },
|
||||
{ globalItemIndex: 11, tagNames: ["sleeping-pad", "camping", "hiking", "bikepacking", "backpacking"] },
|
||||
{ globalItemIndex: 12, tagNames: ["sleeping-pad", "camping", "hiking", "backpacking"] },
|
||||
{ globalItemIndex: 13, tagNames: ["pillow", "camping", "hiking", "bikepacking", "backpacking"] },
|
||||
{ globalItemIndex: 14, tagNames: ["sleeping-bag", "camping", "hiking", "backpacking", "climbing"] },
|
||||
// Cooking — camping/hiking/bikepacking
|
||||
{ globalItemIndex: 15, tagNames: ["stove", "camping", "hiking", "bikepacking", "backpacking"] },
|
||||
{ globalItemIndex: 16, tagNames: ["stove", "camping", "hiking", "backpacking"] },
|
||||
{ globalItemIndex: 17, tagNames: ["cookware", "mug", "camping", "hiking", "bikepacking"] },
|
||||
{ globalItemIndex: 18, tagNames: ["cookware", "camping", "hiking", "backpacking"] },
|
||||
{ globalItemIndex: 19, tagNames: ["stove", "camping", "hiking", "backpacking", "climbing"] },
|
||||
// Lighting — general outdoor
|
||||
{ globalItemIndex: 20, tagNames: ["headlamp", "camping", "hiking", "climbing", "backpacking", "running", "trail-running"] },
|
||||
{ globalItemIndex: 21, tagNames: ["bike-light", "bikepacking", "cycling", "road-cycling", "gravel"] },
|
||||
{ globalItemIndex: 22, tagNames: ["headlamp", "camping", "hiking", "climbing", "backpacking"] },
|
||||
// Water — hiking/camping/bikepacking
|
||||
{ globalItemIndex: 29, tagNames: ["water-filter", "hiking", "camping", "bikepacking", "backpacking"] },
|
||||
{ globalItemIndex: 30, tagNames: ["water-filter", "hiking", "camping", "backpacking"] },
|
||||
{ globalItemIndex: 31, tagNames: ["water-bottle", "hiking", "camping", "cycling", "running"] },
|
||||
// Tools — bikepacking/cycling
|
||||
{ globalItemIndex: 32, tagNames: ["multi-tool", "repair-kit", "bikepacking", "cycling"] },
|
||||
// Clothing — general outdoor
|
||||
{ globalItemIndex: 33, tagNames: ["rain-jacket", "hiking", "camping", "bikepacking", "climbing", "running"] },
|
||||
// Electronics — bikepacking/cycling
|
||||
{ globalItemIndex: 34, tagNames: ["bike-computer", "gps", "bikepacking", "cycling", "gravel"] },
|
||||
{ globalItemIndex: 35, tagNames: ["handlebar-bag", "bike-bag", "dry-bag", "bikepacking", "cycling"] },
|
||||
] as const;
|
||||
|
||||
// ── Category name mapping (for FK lookups by category name) ────────
|
||||
|
||||
@@ -5,6 +5,18 @@ import { globalItems, tags } from "./schema.ts";
|
||||
type Db = typeof prodDb;
|
||||
|
||||
const SEED_TAGS = [
|
||||
// Hobby / activity tags (used by onboarding hobby picker)
|
||||
"bikepacking",
|
||||
"cycling",
|
||||
"hiking",
|
||||
"backpacking",
|
||||
"camping",
|
||||
"climbing",
|
||||
"mountaineering",
|
||||
"road-cycling",
|
||||
"gravel",
|
||||
"running",
|
||||
"trail-running",
|
||||
// Bag types
|
||||
"handlebar-bag",
|
||||
"framebag",
|
||||
|
||||
Reference in New Issue
Block a user