From 77f07e9329e47f6ac5040f2e85d2710ebbea3ced Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Sat, 11 Apr 2026 14:42:28 +0200 Subject: [PATCH] fix: guest feedback flow bugs in Masonry grid and PhotoLightbox (#292) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes two bugs reported on #292 after 3.27.0-beta.0 shipped: 1. Masonry grid showed no visual feedback after liking a photo. MasonryGalleryLayout's Like button had no liked-state plumbing — the Heart icon was a static regardless of whether the user had liked the photo. 2. PhotoLightbox (fullscreen view) silently failed to like photos in guest identity mode. submitLike() and submitRating() never called ensureIdentity() before firing the API request, so the first interaction from a fresh session hit a 401 from the server instead of opening the name prompt. Root causes: 1. MasonryGalleryLayout was missing the 'liked' state pattern that GridGalleryLayout already uses (likedPhotoIds Set in the parent, passed down as a `liked` prop, updated via onLikeSuccess callback). The bug was invisible in simple mode (no personal state) but surfaced immediately in guest mode where each guest expects to see confirmation of their own action. 2. PhotoLightbox's submit handlers were written before the guest identity context existed and only checked the legacy require_name_email flag. They were never updated when guest mode landed. Also fixed: z-index conflict where the GuestNamePromptModal (z-50) was sitting at the same level as PhotoLightbox (z-50), so when the prompt opened over the lightbox, the fullscreen image intercepted pointer events and the modal's Continue button was unclickable. Bumped both guest modals to z-[60]. Changes: - MasonryGalleryLayout.tsx - MasonryPhotoProps gains `liked?: boolean` + `onLikeSuccess?: () => void`. - Like button: red bg + filled white Heart icon when liked; aria-label toggles between "Like photo"/"Unlike photo"; aria-pressed mirrors state. - onClick wires onLikeSuccess() for optimistic UI in both guest-mode and simple-mode branches plus the FeedbackIdentityModal onSubmit path. - Parent layout holds `likedPhotoIds: Set` and passes it to each MasonryPhoto (matches the GridGalleryLayout pattern). - PhotoLightbox.tsx - Consumes useGuestIdentityOptional(); new `isGuestMode` flag. - submitLike() and submitRating() get a guest-mode branch that calls ensureIdentity() first and submits without body guest_name/email (server reads from the verified token). - Optimistic UI updates happen after successful submit in guest mode. - GuestNamePromptModal.tsx, GuestRecoveryModal.tsx - z-50 → z-[60] so they render above PhotoLightbox. Verified end-to-end against local Docker with Playwright MCP on event 168 (Masonry Columns Test layout): - Fresh session, click Like in Masonry grid → name prompt opens, register, feedback persists with guest_id, Heart button turns red with aria-pressed and "Unlike photo" label. Subsequent likes on other photos also show red state. DB confirms feedback rows. - Fresh session, open photo in lightbox BEFORE registering → click Like, the name prompt correctly opens on top of the lightbox, register, feedback persists. Rate 4 stars → works, average 4.0 (1) displayed in lightbox, ★ badge appears on toggle-feedback button, grid cell shows "1 likes" + "Rating: 4.0" indicators after closing lightbox. - Backend DB: gallery_guests row created, photo_feedback rows have correct guest_id, server reads name from verified token (body values ignored). Out of scope (documented in audit, not reported by the user, no regression from guest mode): Mosaic/Carousel/Timeline have partial optimistic-UI issues unrelated to this report; they pre-date guest mode and behave the same in simple mode. Leaving alone per scope discipline. --- .../gallery/GuestNamePromptModal.tsx | 2 +- .../components/gallery/GuestRecoveryModal.tsx | 2 +- .../src/components/gallery/PhotoLightbox.tsx | 57 ++++++++++++++++ .../gallery/layouts/MasonryGalleryLayout.tsx | 66 +++++++++++++++---- 4 files changed, 112 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/gallery/GuestNamePromptModal.tsx b/frontend/src/components/gallery/GuestNamePromptModal.tsx index 95c3efd6..9e520bba 100644 --- a/frontend/src/components/gallery/GuestNamePromptModal.tsx +++ b/frontend/src/components/gallery/GuestNamePromptModal.tsx @@ -72,7 +72,7 @@ export const GuestNamePromptModal: React.FC = ({ }; return ( -
+
{allowCancel && ( diff --git a/frontend/src/components/gallery/GuestRecoveryModal.tsx b/frontend/src/components/gallery/GuestRecoveryModal.tsx index 44300d2d..1f5768eb 100644 --- a/frontend/src/components/gallery/GuestRecoveryModal.tsx +++ b/frontend/src/components/gallery/GuestRecoveryModal.tsx @@ -86,7 +86,7 @@ export const GuestRecoveryModal: React.FC = () => { }; return ( -
+
)} @@ -193,6 +219,9 @@ const MasonryPhoto: React.FC = ({ setSavedIdentity({ name, email }); setShowIdentityModal(false); if (pendingAction) { + if (pendingAction.type === 'like' && onLikeSuccess) { + onLikeSuccess(); + } await feedbackService.submitFeedback(slug!, String(pendingAction.photoId), { feedback_type: pendingAction.type, guest_name: name, @@ -249,6 +278,9 @@ export const MasonryGalleryLayout: React.FC = ({ const containerRef = useRef(null); const [columns, setColumns] = useState(3); const [containerWidth, setContainerWidth] = useState(0); + // Optimistic "I liked this" state — lifted here so it survives re-renders + // of individual MasonryPhoto components during layout reflow/resize. + const [likedPhotoIds, setLikedPhotoIds] = useState>(new Set()); const gallerySettings = theme.gallerySettings || {}; const gutter = gallerySettings.masonryGutter || 16; const mode = gallerySettings.masonryMode || 'columns'; @@ -798,6 +830,14 @@ export const MasonryGalleryLayout: React.FC = ({ feedbackOptions={feedbackOptions} onQuickComment={() => onOpenPhotoWithFeedback && onOpenPhotoWithFeedback(originalIndex)} columnWidth={columnWidth} + liked={likedPhotoIds.has(photo.id)} + onLikeSuccess={() => { + setLikedPhotoIds((prev) => { + const next = new Set(prev); + next.add(photo.id); + return next; + }); + }} /> ); })}