9 Commits

13 changed files with 384 additions and 144 deletions

View File

@@ -257,7 +257,7 @@ Plans:
| 26. Discovery Landing Page | v2.1 | 3/3 | Complete | 2026-04-10 | | 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 | | 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 | | 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 | | 30. Onboarding Redesign | v2.2 | 3/3 | Complete | 2026-04-12 |
| 31. Mobile Polish | v2.2 | 2/2 | Complete | 2026-04-12 | | 31. Mobile Polish | v2.2 | 2/2 | Complete | 2026-04-12 |
| 32. Setup Sharing System | v2.3 | TBD | Pending | — | | 32. Setup Sharing System | v2.3 | TBD | Pending | — |

View File

@@ -80,7 +80,7 @@ v2.1 decisions:
### Pending Todos ### Pending Todos
None active. - Fix Add Candidate button shows wrong modal on thread page (ui)
### Blockers/Concerns ### Blockers/Concerns

View File

@@ -9,6 +9,6 @@
"plan_check": true, "plan_check": true,
"verifier": true, "verifier": true,
"nyquist_validation": true, "nyquist_validation": true,
"_auto_chain_active": true "_auto_chain_active": false
} }
} }

View 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: []

View File

@@ -1,9 +1,9 @@
--- ---
status: partial status: complete
phase: 28-profile-and-logto-integration phase: 28-profile-and-logto-integration
source: [28-01-SUMMARY.md, 28-02-SUMMARY.md, 28-03-SUMMARY.md] source: [28-01-SUMMARY.md, 28-02-SUMMARY.md, 28-03-SUMMARY.md]
started: 2026-04-12T18:30:00Z started: 2026-04-12T18:30:00Z
updated: 2026-04-12T19:00:00Z updated: 2026-04-12T21:00:00Z
--- ---
## Current Test ## 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. expected: /settings page shows only app preferences: weight unit, currency, import/export, API keys. No profile section.
result: pass result: pass
### 4. Edit display name and bio ### 4. Edit display name, bio, and avatar
expected: On /profile, change display name and bio, click Save. Success message appears. Refreshing the page shows the updated values. expected: On /profile, upload an avatar, change display name and bio, click Save. Avatar image renders. Refreshing shows updated values.
result: issue result: pass
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." reported: "Fixed: avatar now uses presigned S3 URLs instead of /uploads/ paths. Avatar also shows in top nav."
severity: major
### 5. Email display ### 5. Email display
expected: Account Info section shows your email address (from Logto) and a "Change" button next to it. expected: Account Info section shows your email address (from Logto) and a "Change" button next to it.
result: blocked result: pass
blocked_by: third-party reported: "Fixed: M2M credentials configured, email change now reflects in UI immediately via optimistic cache update."
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"
### 6. Password change form ### 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. expected: Security section shows a password change form. Current password, new password, confirm new password fields.
result: blocked result: pass
blocked_by: third-party
reason: "Logto Management API returns 500 — M2M env vars not configured on test env"
### 7. Delete account UI ### 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. 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 result: pass
blocked_by: third-party
reason: "Logto Management API returns 500 — M2M env vars not configured on test env"
### 8. Member-since date ### 8. Member-since date
expected: Account Info section shows a "Member since" date formatted nicely (e.g., "April 2026"). expected: Account Info section shows a "Member since" date formatted nicely (e.g., "April 2026").
@@ -55,22 +49,12 @@ result: pass
## Summary ## Summary
total: 8 total: 8
passed: 4 passed: 8
issues: 1 issues: 0
pending: 0 pending: 0
skipped: 0 skipped: 0
blocked: 3 blocked: 0
## Gaps ## 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." [none]
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)

View 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>

View 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.

View File

@@ -1,9 +1,9 @@
--- ---
status: complete status: diagnosed
phase: 29-image-presentation phase: 29-image-presentation
source: [29-01-SUMMARY.md, 29-02-SUMMARY.md, 29-03-SUMMARY.md, 29-04-SUMMARY.md] source: [29-01-SUMMARY.md, 29-02-SUMMARY.md, 29-03-SUMMARY.md, 29-04-SUMMARY.md]
started: 2026-04-12T19:10:00Z started: 2026-04-12T19:10:00Z
updated: 2026-04-12T19:20:00Z updated: 2026-04-13T12:15:00Z
--- ---
## Current Test ## Current Test
@@ -14,102 +14,53 @@ updated: 2026-04-12T19:20:00Z
### 1. Images use fit-within instead of crop ### 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. 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 result: pass
reported: "Fit-within works on detail pages but collection view cards don't render images at all."
severity: major
### 2. Dominant color background fill ### 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). 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 result: pass
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
### 3. Crop editor on item detail ### 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. 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: issue result: pass
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." reported: "Initially reported as issue but confirmed working on re-test — false claim"
severity: minor
### 4. Crop editor on image upload ### 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 result: issue
reported: "Crop editor doesn't open when adding an image to an existing item. Can only be edited afterward." 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: major severity: minor
### 5. Crop settings persist ### 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. expected: Adjust the crop on an item image, save it. Navigate away and come back — image displays with saved crop settings.
result: issue result: pass
reported: "Framing adjustment doesn't save. No error message, no console log — silently fails."
severity: blocker
### 6. Consistency across surfaces ### 6. Consistency across surfaces
expected: All image surfaces use the same fit-within + dominant color treatment. expected: All image surfaces use the same fit-within + dominant color treatment.
result: pass result: pass
reported: "Consistent across all surfaces where images render (detail pages). Collection cards don't render images (see test 1)."
## Summary ## Summary
total: 6 total: 6
passed: 2 passed: 5
issues: 4 issues: 1
pending: 0 pending: 0
skipped: 0 skipped: 0
blocked: 0
## Gaps ## 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 status: failed
reason: "User reported: images not rendering on collection view cards, only on detail pages" reason: "User reported: cropped image not shown in edit state after cropping, but renders correctly after save"
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."
severity: minor 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 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: 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: missing:
- Trigger crop editor after upload in ImageUpload component - 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"
- 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)

View File

@@ -3,12 +3,12 @@ status: partial
phase: 30-onboarding-redesign phase: 30-onboarding-redesign
source: [30-01-SUMMARY.md, 30-02-SUMMARY.md, 30-03-SUMMARY.md] source: [30-01-SUMMARY.md, 30-02-SUMMARY.md, 30-03-SUMMARY.md]
started: 2026-04-12T19:30:00Z started: 2026-04-12T19:30:00Z
updated: 2026-04-12T19:40:00Z updated: 2026-04-13T12:30:00Z
--- ---
## Current Test ## Current Test
[testing complete] [testing paused — 3 items blocked by catalog seed data]
## Tests ## Tests
@@ -30,19 +30,19 @@ severity: cosmetic
expected: After picking a hobby, you see a grid of popular catalog items filtered by that hobby. expected: After picking a hobby, you see a grid of popular catalog items filtered by that hobby.
result: blocked result: blocked
blocked_by: server 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 ### 5. Review screen
expected: After selecting items, a review/summary screen shows all selections grouped by category. expected: After selecting items, a review/summary screen shows all selections grouped by category.
result: blocked result: blocked
blocked_by: server blocked_by: prior-phase
reason: "Depends on test 4 — no catalog items to select." reason: "Depends on test 4 — catalog seed data needed."
### 6. Completion and collection ### 6. Completion and collection
expected: After confirming, items are batch-added to collection with auto-created categories. expected: After confirming, items are batch-added to collection with auto-created categories.
result: blocked result: blocked
blocked_by: server blocked_by: prior-phase
reason: "Depends on test 4 — no catalog items to select." reason: "Depends on test 4 — catalog seed data needed."
### 7. Onboarding doesn't show again ### 7. Onboarding doesn't show again
expected: Refresh the page or sign out and back in. Onboarding does NOT appear again. expected: Refresh the page or sign out and back in. Onboarding does NOT appear again.
@@ -65,6 +65,7 @@ blocked: 3
severity: cosmetic severity: cosmetic
test: 3 test: 3
artifacts: artifacts:
- src/client/components/onboarding/OnboardingHobbyPicker.tsx - path: "src/client/components/onboarding/OnboardingHobbyPicker.tsx"
issue: "Weak selected state styling"
missing: missing:
- Stronger selected state styling (dark bg, inverted colors) - Stronger selected state styling (dark bg, inverted colors)

View File

@@ -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.

View File

@@ -25,6 +25,11 @@ export function ImageUpload({
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [localPreview, setLocalPreview] = useState<string | null>(null); const [localPreview, setLocalPreview] = useState<string | null>(null);
const [showCropEditor, setShowCropEditor] = useState(false); const [showCropEditor, setShowCropEditor] = useState(false);
const [localCrop, setLocalCrop] = useState<{
zoom: number;
x: number;
y: number;
} | null>(null);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) { async function handleFileChange(e: React.ChangeEvent<HTMLInputElement>) {
@@ -70,6 +75,7 @@ export function ImageUpload({
function handleRemove(e: React.MouseEvent) { function handleRemove(e: React.MouseEvent) {
e.stopPropagation(); e.stopPropagation();
setLocalPreview(null); setLocalPreview(null);
setLocalCrop(null);
onChange(null); onChange(null);
} }
@@ -86,6 +92,7 @@ export function ImageUpload({
imageUrl={displayUrl} imageUrl={displayUrl}
dominantColor={dominantColor} dominantColor={dominantColor}
onSave={(result) => { onSave={(result) => {
setLocalCrop(result);
onCropChange(result); onCropChange(result);
setShowCropEditor(false); setShowCropEditor(false);
}} }}
@@ -111,6 +118,9 @@ export function ImageUpload({
src={displayUrl} src={displayUrl}
alt="Item" alt="Item"
dominantColor={dominantColor} dominantColor={dominantColor}
cropZoom={localCrop?.zoom}
cropX={localCrop?.x}
cropY={localCrop?.y}
/> />
{/* Crop button */} {/* Crop button */}
{onCropChange && ( {onCropChange && (

View File

@@ -369,36 +369,45 @@ export const DEV_GLOBAL_ITEMS = [
// Maps global item index -> tag names. Tags are seeded by seedGlobalItems(). // Maps global item index -> tag names. Tags are seeded by seedGlobalItems().
export const DEV_TAG_ASSIGNMENTS = [ export const DEV_TAG_ASSIGNMENTS = [
{ globalItemIndex: 0, tagNames: ["saddlebag", "bike-bag"] }, // Bags — bikepacking/cycling gear
{ globalItemIndex: 1, tagNames: ["handlebar-bag", "bike-bag"] }, { globalItemIndex: 0, tagNames: ["saddlebag", "bike-bag", "bikepacking", "cycling"] },
{ globalItemIndex: 2, tagNames: ["framebag", "bike-bag"] }, { globalItemIndex: 1, tagNames: ["handlebar-bag", "bike-bag", "bikepacking", "cycling"] },
{ globalItemIndex: 3, tagNames: ["handlebar-bag", "bike-bag"] }, { globalItemIndex: 2, tagNames: ["framebag", "bike-bag", "bikepacking", "cycling"] },
{ globalItemIndex: 4, tagNames: ["framebag", "bike-bag"] }, { globalItemIndex: 3, tagNames: ["handlebar-bag", "bike-bag", "bikepacking", "cycling"] },
{ globalItemIndex: 5, tagNames: ["top-tube-bag", "bike-bag"] }, { globalItemIndex: 4, tagNames: ["framebag", "bike-bag", "bikepacking", "cycling"] },
{ globalItemIndex: 6, tagNames: ["tent"] }, { globalItemIndex: 5, tagNames: ["top-tube-bag", "bike-bag", "bikepacking", "cycling"] },
{ globalItemIndex: 7, tagNames: ["tent"] }, // Shelter — camping/hiking/bikepacking
{ globalItemIndex: 8, tagNames: ["tent"] }, { globalItemIndex: 6, tagNames: ["tent", "camping", "hiking", "bikepacking", "backpacking"] },
{ globalItemIndex: 9, tagNames: ["tent"] }, { globalItemIndex: 7, tagNames: ["tent", "camping", "hiking", "bikepacking", "backpacking"] },
{ globalItemIndex: 10, tagNames: ["quilt"] }, { globalItemIndex: 8, tagNames: ["tent", "camping", "hiking", "backpacking"] },
{ globalItemIndex: 11, tagNames: ["sleeping-pad"] }, { globalItemIndex: 9, tagNames: ["tent", "camping", "hiking", "backpacking", "climbing", "mountaineering"] },
{ globalItemIndex: 12, tagNames: ["sleeping-pad"] }, // Sleep — camping/hiking/bikepacking
{ globalItemIndex: 13, tagNames: ["pillow"] }, { globalItemIndex: 10, tagNames: ["quilt", "camping", "hiking", "bikepacking", "backpacking"] },
{ globalItemIndex: 14, tagNames: ["sleeping-bag"] }, { globalItemIndex: 11, tagNames: ["sleeping-pad", "camping", "hiking", "bikepacking", "backpacking"] },
{ globalItemIndex: 15, tagNames: ["stove"] }, { globalItemIndex: 12, tagNames: ["sleeping-pad", "camping", "hiking", "backpacking"] },
{ globalItemIndex: 16, tagNames: ["stove"] }, { globalItemIndex: 13, tagNames: ["pillow", "camping", "hiking", "bikepacking", "backpacking"] },
{ globalItemIndex: 17, tagNames: ["cookware", "mug"] }, { globalItemIndex: 14, tagNames: ["sleeping-bag", "camping", "hiking", "backpacking", "climbing"] },
{ globalItemIndex: 18, tagNames: ["cookware"] }, // Cooking — camping/hiking/bikepacking
{ globalItemIndex: 19, tagNames: ["stove"] }, { globalItemIndex: 15, tagNames: ["stove", "camping", "hiking", "bikepacking", "backpacking"] },
{ globalItemIndex: 20, tagNames: ["headlamp"] }, { globalItemIndex: 16, tagNames: ["stove", "camping", "hiking", "backpacking"] },
{ globalItemIndex: 21, tagNames: ["bike-light"] }, { globalItemIndex: 17, tagNames: ["cookware", "mug", "camping", "hiking", "bikepacking"] },
{ globalItemIndex: 22, tagNames: ["headlamp"] }, { globalItemIndex: 18, tagNames: ["cookware", "camping", "hiking", "backpacking"] },
{ globalItemIndex: 29, tagNames: ["water-filter"] }, { globalItemIndex: 19, tagNames: ["stove", "camping", "hiking", "backpacking", "climbing"] },
{ globalItemIndex: 30, tagNames: ["water-filter"] }, // Lighting — general outdoor
{ globalItemIndex: 31, tagNames: ["water-bottle"] }, { globalItemIndex: 20, tagNames: ["headlamp", "camping", "hiking", "climbing", "backpacking", "running", "trail-running"] },
{ globalItemIndex: 32, tagNames: ["multi-tool", "repair-kit"] }, { globalItemIndex: 21, tagNames: ["bike-light", "bikepacking", "cycling", "road-cycling", "gravel"] },
{ globalItemIndex: 33, tagNames: ["rain-jacket"] }, { globalItemIndex: 22, tagNames: ["headlamp", "camping", "hiking", "climbing", "backpacking"] },
{ globalItemIndex: 34, tagNames: ["bike-computer", "gps"] }, // Water — hiking/camping/bikepacking
{ globalItemIndex: 35, tagNames: ["handlebar-bag", "bike-bag", "dry-bag"] }, { 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; ] as const;
// ── Category name mapping (for FK lookups by category name) ──────── // ── Category name mapping (for FK lookups by category name) ────────

View File

@@ -5,6 +5,18 @@ import { globalItems, tags } from "./schema.ts";
type Db = typeof prodDb; type Db = typeof prodDb;
const SEED_TAGS = [ 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 // Bag types
"handlebar-bag", "handlebar-bag",
"framebag", "framebag",