Skip to content

feat(photos): show photo preview in "Add photo details" modal (#1722)#1724

Merged
steilerDev merged 1 commit into
betafrom
feat/1722-photo-preview-metadata-modal
Jun 16, 2026
Merged

feat(photos): show photo preview in "Add photo details" modal (#1722)#1724
steilerDev merged 1 commit into
betafrom
feat/1722-photo-preview-metadata-modal

Conversation

@steilerDev

Copy link
Copy Markdown
Owner

Summary

  • Renders a preview image of the selected photo inside the PhotoMetadataModal, displayed above the description field
  • Object URL is created from the selected File on file change and revoked on unmount/file change to prevent memory leaks
  • PhotoMetadataSidepanel is intentionally out of scope for this story

Fixes #1722

Test plan

  • Unit tests pass — PhotoMetadataModal.test.tsx covers preview rendering, object URL lifecycle (create/revoke), and absence when no file is selected
  • E2E tests pass — photo-capture-flow.spec.ts verifies the preview image appears in the modal after file selection
  • Pre-commit hook quality gates pass (lint, typecheck, build)

🤖 Generated with Claude Code

Renders a preview of the selected photo inside the PhotoMetadataModal
above the description field. The preview image is created from a local
object URL of the selected File object, created when the file changes
and revoked on unmount (and on file change) to avoid memory leaks.

Out of scope: PhotoMetadataSidepanel (unchanged by this story).

Fixes #1722

Co-Authored-By: Claude dev-team-lead (Sonnet 4.6) <noreply@anthropic.com>
Co-Authored-By: Claude frontend-developer (Haiku 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude qa-integration-tester (Sonnet 4.5) <noreply@anthropic.com>
Co-Authored-By: Claude e2e-test-engineer (Sonnet 4.5) <noreply@anthropic.com>
@steilerDev

Copy link
Copy Markdown
Owner Author

[product-owner] Product Owner review of #1722 acceptance criteria.

Verdict: APPROVED — all 6 acceptance criteria met.

AC Status Evidence
1. Preview at top of modal body, above Description PASS {objectUrl && (<div className={styles.photoPreview}>...)} is rendered as the first child of formBody, before the Description textarea. Unit test "preview img is the first element child of formBody" + E2E asserts the img is visible in the modal.
2. Source generated locally from File (no server URL) PASS useEffect calls URL.createObjectURL(file); <img src={objectUrl}>. Unit test asserts createObjectURL is called with the file instance; E2E asserts src starts with blob:.
3. Object URL revoked on unmount / file change PASS useEffect cleanup returns URL.revokeObjectURL(url), keyed on [file]. Unit tests cover both revoke-on-unmount and revoke-old/create-new-on-file-change.
4. Accessible name on the image PASS alt={file.name} provides a non-empty, descriptive accessible name. AC example ("Selected photo preview") is non-binding ("e.g."); filename is an appropriate alternative. Unit + E2E assert alt equals the filename.
5. Responsive, aspect-ratio preserved, dark mode, tokens PASS object-fit: contain preserves aspect ratio; max-width:100% + max-height:14rem (10rem on max-width:767px) constrain to modal width. Colors use --color-bg-tertiary/--color-border; radius via --radius-md — all tokens, no hardcoded values (works in dark mode). E2E @responsive test confirms visibility at 375px.
6. Save/upload behavior unchanged PASS No change to onSave payload construction. Unit test "Save behavior unchanged" asserts onSave called with { caption, areaId, orientationId } exactly as before.

Scope discipline: Frontend-only; PhotoMetadataSidepanel correctly left untouched per the out-of-scope note. Diff is 4 files (component, CSS, unit test, E2E), no backend/schema/API changes.

Notes (non-blocking):

  • Test authorship: confirm the unit test (PhotoMetadataModal.test.tsx) carries the qa-integration-tester trailer and the E2E test (photo-capture-flow.spec.ts) carries the e2e-test-engineer trailer, per the test-ownership rule.
  • Minor: alt={file.name} exposes the raw filename to assistive tech. Acceptable per AC, but a localized generic string could be considered if filenames are ever non-descriptive. Not a blocker.

All acceptance criteria are satisfied. Recommend approval pending the standard architecture/security/QA reviews and green CI.

@steilerDev steilerDev left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[product-architect]

Architecture Review — APPROVE (posted as comment; cannot self-approve)

Small, well-scoped frontend enhancement. Adds a pre-upload photo preview to PhotoMetadataModal. Reviewed for object-URL lifecycle, React patterns, scope, and test coverage.

Object-URL lifecycle — correct

  • useEffect keyed [file] creates the URL; the cleanup closure revokes it. Critically, cleanup captures the local url const, not the objectUrl state — this avoids the classic stale-state revocation bug and guarantees the exact URL created by that effect run is the one revoked.
  • Lifecycle fully covered: create on mount, revoke on unmount, revoke-old + create-new on file change. No memory leak.

React patterns — clean

  • Standard create/revoke effect; conditional render {objectUrl && ...} is the right guard for the brief pre-effect frame. Existing focus-trap effect untouched; handleSave payload unchanged. alt={file.name} gives an accessible label.

Scope — tight

  • Only the preview is added; PhotoMetadataSidepanel correctly left out of scope per the PR description. No contract/schema/shared-type changes — appropriately frontend-only.

CSS — compliant

  • Uses design tokens (--color-bg-tertiary, --color-border, --radius-md). object-fit: contain + max-height with a mobile breakpoint is sound. No hardcoded values.

Tests — adequate

  • 7 unit cases cover create-once, render (src/alt), DOM position, falsy-URL no-render, revoke-on-unmount, revoke+recreate on file change, Save-payload-unchanged. jsdom URL API stubbing is properly saved/restored per test.
  • E2E Scenario 12 (smoke) + 12b (mobile) assert visibility, alt, and blob: src across viewports.

Non-blocking notes (Low)

  • The "first element child of formBody" unit test couples to DOM nesting via parentElement walks — correct but brittle to future layout wrapping. Fine to leave.

Verdict: approve. No blocking findings.

@steilerDev steilerDev left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

[ux-designer]

Design review for PR #1724 — PhotoMetadataModal photo preview (Story #1722).

Verdict: APPROVE

All checks pass. The implementation matches the visual spec exactly.

Token Adherence

  • .photoPreview: background: var(--color-bg-tertiary), border: 1px solid var(--color-border), border-radius: var(--radius-md) — all semantic tokens with confirmed dark mode overrides in tokens.css. No hardcoded color or radius values.
  • .photoPreviewImage: no color properties; layout-only values (max-width: 100%, max-height: 14rem, object-fit: contain, object-position: center). The 14rem/10rem max-height values are plain layout constraints — acceptable as documented in the style guide notes.
  • border-radius: var(--radius-md) on the image element is correct (prevents image corners bleeding through the container's overflow: hidden).

Dark Mode

Automatic via semantic tokens. --color-bg-tertiary resolves to var(--color-slate-600) in dark mode and --color-border resolves to var(--color-slate-500). No special dark mode treatment needed.

Responsive Behavior

  • Desktop: max-height: 14rem — matches spec.
  • Mobile breakpoint: @media (max-width: 767px) reduces to max-height: 10rem on both container and image — matches spec. (Note: the spec used 767px as the upper bound for the tablet/mobile breakpoint, consistent with the 1023px convention for desktop upper bounds.)
  • Both container and image max-height are kept in sync across the media query, which is correct — the image's max-height must match the container's or the object-fit: contain calculation can overflow.

Accessibility

  • alt={file.name} is informative (not decorative alt=""), appropriate for a pre-upload preview that represents a specific file the user selected.
  • Conditional render {objectUrl && ...} means the image is absent from DOM until the object URL is created — no empty src or broken-image state exposed to screen readers.
  • No interactive element added; no focus management changes required.

Visual Consistency

  • display: flex; align-items: center; justify-content: center on the container correctly centers landscape and portrait photos within the fixed-height box.
  • overflow: hidden on the container clips any edge cases where the image might exceed bounds.
  • display: block on the image eliminates the inline baseline gap.
  • Placement above the description textarea matches the spec's "above the description field" requirement.

Lifecycle Correctness

  • useEffect with [file] dependency creates a new object URL on mount and when the file changes, and revokes the previous URL on cleanup — no memory leaks.
  • The unit tests cover the full lifecycle (create, revoke on unmount, revoke-and-recreate on file change), the falsy-URL conditional branch, and the save behavior regression. This is thorough.

No findings. Implementation is spec-compliant.

@steilerDev steilerDev merged commit d514645 into beta Jun 16, 2026
29 of 32 checks passed
@steilerDev steilerDev deleted the feat/1722-photo-preview-metadata-modal branch June 16, 2026 22:09
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.9.0-beta.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.9.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

steilerDev added a commit that referenced this pull request Jun 17, 2026
Document the two photo enhancements shipped in v2.9.0:
- Photo preview in the "Add photo details" modal (#1722, #1724)
- Hierarchy-aware area picker (indentation + ancestor path) and
  description-aware orientation search (#1723, #1725)

Updates guides/diary/photo-capture.md and guides/work-items/areas-and-trades.md,
and rewrites RELEASE_SUMMARY.md for the v2.9.0 release.

Co-authored-by: Frank Steiler <frank@steiler.de>
Co-authored-by: Claude docs-writer (Opus 4.6) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant