Image architecture
How event images flow from admin upload → S3 → storefront rendering. Cards use the square profile; the detail hero uses the wide banner.
Card image source
The adapter at eventGridAdapter.ts reads:
e.profile_image_url ?? e.banner_image_url ?? ""| Source | Where | Fallback role |
|---|---|---|
| Profile (1:1, 400x400) | Event cards, search results, category pages. | Primary. |
| Banner (~2.5:1, 1600x643) | Event detail page hero only. Letterboxed on cards. | Fallback when no profile uploaded. |
| No image | Gradient placeholder (teal→blue). Preserves grid alignment. | Last resort. |
Why profile, not banner
- Aspect ratio. Profile is 400x400 (1:1). Banner is 1600x643 (~2.5:1). A card slot is square; the banner letterboxes or crops the subject.
- Subject framing. Producers crop identity (logo, face) into the square. The wide canvas is atmosphere/venue for the detail hero at full width.
- Legacy parity. Legacy storefront cards rendered the profile image. v2 mirrors this behaviour.
Event detail hero
The detail page (/[locale]/events/[slug]/page.tsx) renders the wide banner_image_url as the full-width hero image. When a profile_image_url exists, it overlays at the bottom-left as a small square mark (96x96px on desktop, 64x64px on mobile) — mirroring legacy's layout where the event's identity mark sits over the banner.
Upload flow (admin)
The admin EventForm Visual tab has two uploaders and a default banner picker:
- Profile image —
S3ImageUploaderwithaspect="square", folderevents/profiles. DSImageUploadermolecule withaspect-square h-44. - Banner image —
S3ImageUploaderwithaspect="wide", folderevents/banners. DSImageUploaderwithaspect-[1600/643] h-44. - Default banners —
DefaultBannerPicker(consumer-layer composition of DSRadio+ token utilities). Four legacy stock banners atfiles/default-banners/banner{1..4}.jpg. - YouTube video —
video_urlstored raw, iframe src rebuilt from parsed 11-char id viayoutubeEmbedUrl().
The upload pipe: file picker → POST /producer/uploads/presign (Medusa signs a 15-min PUT URL) → XHR direct-to-S3 PUT with progress → publicUrl stored on the row.
DS ImageUploader molecule changes
The DS ImageUploader molecule was extended with new props:
currentSrc— URL of a persisted image. When set and no new file picked, the saved image becomes the control (click to replace).aspect—"square" | "wide" | "auto". Controls both the empty dropzone and the filled preview shape. Height-driven (h-44) so square and wide previews line up on the same row.onRemove/removeLabel— exposed remove control over the preview, positioned top-right via DSIconButton.
The empty dropzone holds the same aspect as the filled preview — the field does not jump size when an image lands. Previously the empty state was a fixed h-24 w-full bar.
DS EventCard organism change
The image slot changed from aspect-[16/9] to aspect-square. The card now renders a 1:1 crop matching the profile image dimensions.
Side-by-side layout call
The EventForm Visual tab places the two uploaders side-by-side (flex-row on sm:, stacks below). This deliberately avoids ResponsiveFieldGroup — that molecule splits into equal flex-1 slots, which would strand the 176px square far to the right of the much wider banner. Both previews share one height (h-44) so they line up regardless of their different aspect ratios.
Backend: profile_image_url not selected
The /web/events listing endpoint previously did SELECT … banner_image_url but omitted profile_image_url. The adapter preferred profile (null every time) and silently fell back to the 2.5:1 banner. This is now fixed — profile_image_url is selected and returned alongside the banner.
Cross-references
eventGridAdapter.ts— fallback chain: profile → banner → emptyevents/[slug]/page.tsx— detail hero (banner + profile overlay) + YouTube embedEventForm.tsx— admin editor Visual tab (side-by-side uploaders + DefaultBannerPicker + video_url)EventDetail.tsx— read-only Visual step mirroring the editorS3ImageUploader.tsx— reusable widget (DS ImageUploader + presign/PUT + ProgressUpload)DefaultBannerPicker.tsx— consumer-layer composition (DS Radio + thumbnails)ImageUploader.tsx(DS molecule) — extended with currentSrc / aspect / onRemoveEventCard.tsx(DS organism) — aspect-square image slotyoutube.ts— URL → embed id parser (security-critical: iframe src rebuilt from parsed 11-char id)hetzner-storage.ts— presignUpload() method/web/events route.ts— now SELECTs profile_image_urlnext.config.js— nbg1.your-objectstorage.com added to images.remotePatterns
Captured 2026-08-14 as part of ClickUp task EVENT-UPLOAD-01. Updated 2026-08-14 after visual review with full component audit. Do not re-open without an ADR.