management: product image upload to R2 #44

Closed
opened 2026-08-27 06:35:52 +00:00 by yuris · 0 comments
Owner

Every mockup renders products at a 1:1 contained aspect ratio, and apps/store/next.config.ts sets images.unoptimized — so whatever URL lands in inventory.images is served verbatim. Today both inventory forms accept pasted URL strings only (add-inventory-modal.tsx, inventory-edit-form.tsx), and scripts/seed-demo.mts uses placehold.co. There is no upload path, so real product photos depend on the operator hosting files somewhere else. The design cannot ship with real imagery until this exists.

Change

lib/storage/r2.ts already has the whole mechanism in uploadReceipt(file, "order" | "purchase") — S3 client, MIME allow-list, size caps, ${R2_PUBLIC_URL}/${key} return. Generalise the key prefix and kind rather than writing a second uploader: accept a product kind writing to products/{YYYY}/{MM}/{uuid}{ext}, keep receipts on their existing prefix so nothing already stored moves. Images only for this kind — no PDF branch, and hold the 5 MB image cap.

  • Route: extend app/api/[[...route]]/uploads.ts, whose type is currently restricted to order|purchase.
  • Client: extend lib/uploads.ts, then replace the URL-row inputs in both inventory forms with a file picker that appends the returned public URL into images. Keep manual URL entry working — items already reference external URLs.
  • MCP: lib/mcp/tools/images.ts gains the product kind alongside upload_receipt.

Acceptance

An operator uploads a photo in the stok form and the item renders it on the storefront after the next deploy, with no external hosting. Receipt uploads are byte-for-byte unaffected: same prefix, same caps, same PDF handling. A non-image file and an oversized file are both rejected with a readable message. Cover the kind→prefix mapping and the rejection paths in vitest.

Every mockup renders products at a 1:1 contained aspect ratio, and `apps/store/next.config.ts` sets `images.unoptimized` — so whatever URL lands in `inventory.images` is served verbatim. Today both inventory forms accept **pasted URL strings only** (`add-inventory-modal.tsx`, `inventory-edit-form.tsx`), and `scripts/seed-demo.mts` uses `placehold.co`. There is no upload path, so real product photos depend on the operator hosting files somewhere else. The design cannot ship with real imagery until this exists. ### Change `lib/storage/r2.ts` already has the whole mechanism in `uploadReceipt(file, "order" | "purchase")` — S3 client, MIME allow-list, size caps, `${R2_PUBLIC_URL}/${key}` return. Generalise the key prefix and kind rather than writing a second uploader: accept a `product` kind writing to `products/{YYYY}/{MM}/{uuid}{ext}`, keep receipts on their existing prefix so nothing already stored moves. Images only for this kind — no PDF branch, and hold the 5 MB image cap. - Route: extend `app/api/[[...route]]/uploads.ts`, whose `type` is currently restricted to `order|purchase`. - Client: extend `lib/uploads.ts`, then replace the URL-row inputs in both inventory forms with a file picker that appends the returned public URL into `images`. Keep manual URL entry working — items already reference external URLs. - MCP: `lib/mcp/tools/images.ts` gains the product kind alongside `upload_receipt`. ### Acceptance An operator uploads a photo in the stok form and the item renders it on the storefront after the next deploy, with no external hosting. Receipt uploads are byte-for-byte unaffected: same prefix, same caps, same PDF handling. A non-image file and an oversized file are both rejected with a readable message. Cover the kind→prefix mapping and the rejection paths in `vitest`.
yuris closed this issue 2026-08-28 01:15:43 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
yuris/garage#44
No description provided.