Repository navigation
feat: share an outfit as a picture - #281
TheBlackSide243 wants to merge 2 commits into
Conversation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reviewer's GuideIntroduces a frontend-only share workflow that renders an outfit’s ordered item photos and localized metadata into a 1080×1350 canvas PNG, shares it through the Web Share API when available, and otherwise downloads it, with the action integrated into the preview dialog and translated across all supported locales. Sequence diagram for sharing an outfit collagesequenceDiagram
actor User
participant Preview as OutfitPreviewDialog
participant Button as ShareOutfitButton
participant Collage as renderOutfitCollage
participant Browser as WebBrowser
User->>Preview: Click Share
Preview->>Button: share()
Button->>Collage: renderOutfitCollage(imageUrls, text)
Collage->>Browser: Load item images and draw 1080x1350 canvas
Browser-->>Collage: PNG Blob
Button->>Browser: shareOrDownload(blob, filename, title)
alt File sharing supported
Browser-->>User: Open system share sheet
else File sharing unavailable
Browser-->>User: Download PNG
end
Flow diagram for outfit collage generationflowchart LR
A[Ordered outfit item photos] --> B[Load up to 9 images]
B --> C[Arrange images in 1 to 3 columns]
C --> D[Draw title, date, and footer]
D --> E[Export 1080x1350 PNG Blob]
E --> F[Share or download]
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 4 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="frontend/lib/outfit-collage.ts" line_range="22" />
<code_context>
+ const image = new Image();
+ image.crossOrigin = 'anonymous';
+ image.onload = () => resolve(image);
+ image.onerror = () => resolve(null);
+ image.src = src;
+ });
</code_context>
<issue_to_address>
**Incomplete collages report success**
When one or more image URLs fail to load, `loadImage` resolves failed requests as `null`, and `renderOutfitCollage` filters those results before exporting. The share flow therefore reports success with missing outfit photos, or a blank image.
Reject the render or report failed images instead of filtering them out and exporting the collage.
Also at `frontend/lib/outfit-collage.ts:62-64`.
</issue_to_address>
### Comment 2
<location path="frontend/lib/outfit-collage.ts" line_range="64" />
<code_context>
+ {busy ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Share2 className="mr-2 h-4 w-4" />}
+ {t('button')}
+ </Button>
+ );
+}
</code_context>
<issue_to_address>
**Shares a blank collage**
When all outfit items lack image URLs or all image loads fail, the image filter can leave `images` empty, but `renderOutfitCollage` still exports the canvas; the share button then delivers a collage with no outfit photos.
Reject or disable sharing when no photos load, or render a clear placeholder instead of exporting a blank collage.
</issue_to_address>
### Comment 3
<location path="frontend/components/share-outfit-button.tsx" line_range="25-30" />
<code_context>
+ setBusy(true);
+ try {
+ const urls = [...outfit.items]
+ .sort((a, b) => a.position - b.position)
+ .map((item) => item.image_url || item.thumbnail_url)
+ .filter((url): url is string => Boolean(url));
+ const title = outfit.name || t('title', { occasion: occasionLabel(outfit.occasion) });
+ const blob = await renderOutfitCollage(urls, {
+ title,
+ subtitle: outfit.scheduled_for ? formatShortDate(outfit.scheduled_for, locale) : undefined,
+ footer: t('footer'),
</code_context>
<issue_to_address>
**Expired image links create blank collage**
When the outfit data is still in use after its signed image URLs have expired, `ShareOutfitButton` reuses the outfit’s signed image URLs without refreshing them. Once they expire, `loadImage` drops the failed requests and `renderOutfitCollage` still exports a collage, so the user shares or downloads an image with no outfit photos.
Refresh the outfit or its image URLs before rendering, and do not export a collage when its images failed to load.
</issue_to_address>
### Comment 4
<location path="frontend/lib/outfit-collage.ts" line_range="62" />
<code_context>
+ ctx.fillText(text.subtitle, MARGIN, MARGIN + 100, WIDTH - 2 * MARGIN);
+ }
+
+ const images = (await Promise.all(imageUrls.slice(0, 9).map(loadImage))).filter(
+ (image): image is HTMLImageElement => image !== null
+ );
</code_context>
<issue_to_address>
**Collages omit later items**
When an outfit or pairing has more than nine image URLs, `renderOutfitCollage` loads only `imageUrls.slice(0, 9)`, so items after the ninth are absent from the shared PNG.
Render all image URLs, or explicitly indicate that the collage is truncated.
</issue_to_address>Sourcery assessment
Approval pending. 4 findings to address first.
Blocking findings: frontend/lib/outfit-collage.ts:22, frontend/lib/outfit-collage.ts:64, frontend/components/share-outfit-button.tsx:30, frontend/lib/outfit-collage.ts:62
| const image = new Image(); | ||
| image.crossOrigin = 'anonymous'; | ||
| image.onload = () => resolve(image); | ||
| image.onerror = () => resolve(null); |
There was a problem hiding this comment.
🟡 Medium · Incomplete collages report success
When one or more image URLs fail to load, loadImage resolves failed requests as null, and renderOutfitCollage filters those results before exporting. The share flow therefore reports success with missing outfit photos, or a blank image.
Reject the render or report failed images instead of filtering them out and exporting the collage.
Also at frontend/lib/outfit-collage.ts:62-64.
Prompt for AI agents
In `frontend/lib/outfit-collage.ts` at line 22:
**Incomplete collages report success**
When one or more image URLs fail to load, `loadImage` resolves failed requests as `null`, and `renderOutfitCollage` filters those results before exporting. The share flow therefore reports success with missing outfit photos, or a blank image.
Reject the render or report failed images instead of filtering them out and exporting the collage.
Also at `frontend/lib/outfit-collage.ts:62-64`.|
|
||
| const images = (await Promise.all(imageUrls.slice(0, 9).map(loadImage))).filter( | ||
| (image): image is HTMLImageElement => image !== null | ||
| ); |
There was a problem hiding this comment.
🟡 Medium · Shares a blank collage
When all outfit items lack image URLs or all image loads fail, the image filter can leave images empty, but renderOutfitCollage still exports the canvas; the share button then delivers a collage with no outfit photos.
Reject or disable sharing when no photos load, or render a clear placeholder instead of exporting a blank collage.
Prompt for AI agents
In `frontend/lib/outfit-collage.ts` at line 64:
**Shares a blank collage**
When all outfit items lack image URLs or all image loads fail, the image filter can leave `images` empty, but `renderOutfitCollage` still exports the canvas; the share button then delivers a collage with no outfit photos.
Reject or disable sharing when no photos load, or render a clear placeholder instead of exporting a blank collage.| .sort((a, b) => a.position - b.position) | ||
| .map((item) => item.image_url || item.thumbnail_url) | ||
| .filter((url): url is string => Boolean(url)); | ||
| const title = outfit.name || t('title', { occasion: occasionLabel(outfit.occasion) }); | ||
| const blob = await renderOutfitCollage(urls, { | ||
| title, |
There was a problem hiding this comment.
🟡 Medium · Expired image links create blank collage
When the outfit data is still in use after its signed image URLs have expired, ShareOutfitButton reuses the outfit’s signed image URLs without refreshing them. Once they expire, loadImage drops the failed requests and renderOutfitCollage still exports a collage, so the user shares or downloads an image with no outfit photos.
Refresh the outfit or its image URLs before rendering, and do not export a collage when its images failed to load.
Prompt for AI agents
In `frontend/components/share-outfit-button.tsx` at lines 25-30:
**Expired image links create blank collage**
When the outfit data is still in use after its signed image URLs have expired, `ShareOutfitButton` reuses the outfit’s signed image URLs without refreshing them. Once they expire, `loadImage` drops the failed requests and `renderOutfitCollage` still exports a collage, so the user shares or downloads an image with no outfit photos.
Refresh the outfit or its image URLs before rendering, and do not export a collage when its images failed to load.| ctx.fillText(text.subtitle, MARGIN, MARGIN + 100, WIDTH - 2 * MARGIN); | ||
| } | ||
|
|
||
| const images = (await Promise.all(imageUrls.slice(0, 9).map(loadImage))).filter( |
There was a problem hiding this comment.
🟡 Medium · Collages omit later items
When an outfit or pairing has more than nine image URLs, renderOutfitCollage loads only imageUrls.slice(0, 9), so items after the ninth are absent from the shared PNG.
Render all image URLs, or explicitly indicate that the collage is truncated.
Prompt for AI agents
In `frontend/lib/outfit-collage.ts` at line 62:
**Collages omit later items**
When an outfit or pairing has more than nine image URLs, `renderOutfitCollage` loads only `imageUrls.slice(0, 9)`, so items after the ninth are absent from the shared PNG.
Render all image URLs, or explicitly indicate that the collage is truncated.- A photo that fails to load stops the export instead of leaving a gap or a blank picture; the button first fetches the outfit again for fresh photo links and retries once, then explains what went wrong. - Outfits with more than nine pieces show eight photos and a +N tile. - The button is off when no piece has a photo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Description
Adds a Share button to the outfit preview: it draws the outfit's items into a 1080x1350 collage
(canvas, client-side) with the outfit name or occasion and date, then opens the system share
sheet (Web Share API with files) or downloads the PNG where sharing files is not supported.
lib/outfit-collage.ts,components/share-outfit-button.tsx.This is one of a set of small, independent PRs (each branches from
mainand can be reviewed ordropped on its own); they only overlap in the nav/router registration lines.
Type of Change
Checklist
Testing
npm run lint,i18n:check,vocab:check,tsc --noEmitand vitest (693 passed).Test Environment
🤖 Generated with Claude Code
Summary by Sourcery
Enable users to share outfit previews as localized collage images.
New Features:
Enhancements: