Skip to content

feat: share an outfit as a picture - #281

Open
TheBlackSide243 wants to merge 2 commits into
Anyesh:mainfrom
TheBlackSide243:pr/share-outfit
Open

TheBlackSide243 wants to merge 2 commits into
Anyesh:mainfrom
TheBlackSide243:pr/share-outfit

Conversation

@TheBlackSide243

@TheBlackSide243 TheBlackSide243 commented Oct 18, 2026 •

Copy link
Copy Markdown

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.

  • Frontend only: lib/outfit-collage.ts, components/share-outfit-button.tsx.
  • Translations for all 8 locales.

This is one of a set of small, independent PRs (each branches from main and can be reviewed or
dropped on its own); they only overlap in the nav/router registration lines.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Refactoring (no functional changes)
  • CI/CD or build changes

Checklist

  • I have read the CONTRIBUTING guide
  • My code follows the project's coding style
  • I have added tests that prove my fix/feature works (not covered: it is canvas drawing plus the browser share sheet)
  • New and existing tests pass locally
  • I have updated documentation as needed
  • My changes don't introduce new warnings or errors

Testing

  • Backend: untouched.
  • Frontend: npm run lint, i18n:check, vocab:check, tsc --noEmit and vitest (693 passed).
  • Running on my own self-hosted instance (Docker Compose on TrueNAS, Ollama).

Test Environment

  • Docker Compose
  • Kubernetes
  • Local development

🤖 Generated with Claude Code

Summary by Sourcery

Enable users to share outfit previews as localized collage images.

New Features:

  • Add outfit sharing from the preview dialog as a generated portrait collage image.
  • Support sharing the collage through the Web Share API or downloading it as a PNG when file sharing is unavailable.

Enhancements:

  • Handle expired item photo links by refreshing outfit data and retrying collage generation.
  • Add localized sharing labels and messages for all supported locales.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Introduces 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 collage

sequenceDiagram
    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
Loading

Flow diagram for outfit collage generation

flowchart 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]
Loading

File-Level Changes

Change Details Files
Adds a client-side outfit collage renderer that produces a shareable 1080×1350 PNG from item images.
  • Loads up to nine item images in position order with anonymous CORS requests.
  • Lays out images in responsive one-, two-, or three-column grids with rounded clipping, spacing, and branded text.
  • Exports the canvas as a PNG blob. If images fail to load, they are omitted from the collage.
frontend/lib/outfit-collage.ts
Adds outfit sharing with Web Share API file support and download fallback.
  • Builds the collage title, localized occasion/date metadata, and localized footer.
  • Opens the native file share sheet when supported, otherwise downloads a deterministic PNG filename.
  • Disables the action during generation or when the outfit has no items, and reports fallback/error states with localized toasts.
frontend/components/share-outfit-button.tsx
Integrates the share action into the outfit preview dialog.
  • Places the share button beside the existing close button with equal-width layout.
frontend/components/outfit-preview-dialog.tsx
Registers localized sharing strings across all supported locales.
  • Adds the share translation namespace.
  • Provides button, title, footer, download, and error strings for eight locales.
frontend/lib/i18n/locales.ts
frontend/messages/de/share.json
frontend/messages/en/share.json
frontend/messages/fr/share.json
frontend/messages/it/share.json
frontend/messages/ja/share.json
frontend/messages/ko/share.json
frontend/messages/zh-CN/share.json
frontend/messages/zh-TW/share.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions github-actions Bot added the javascript Pull requests that update javascript code label Oct 18, 2026

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

const image = new Image();
image.crossOrigin = 'anonymous';
image.onload = () => resolve(image);
image.onerror = () => resolve(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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`.

Comment thread frontend/lib/outfit-collage.ts Outdated

const images = (await Promise.all(imageUrls.slice(0, 9).map(loadImage))).filter(
(image): image is HTMLImageElement => image !== null
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +25 to +30
.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,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread frontend/lib/outfit-collage.ts Outdated
ctx.fillText(text.subtitle, MARGIN, MARGIN + 100, WIDTH - 2 * MARGIN);
}

const images = (await Promise.all(imageUrls.slice(0, 9).map(loadImage))).filter(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants