From d292b9fa10bffe5d751629b900c606763fa8e73a Mon Sep 17 00:00:00 2001 From: Paul Nothaft Date: Sun, 31 May 2026 22:35:15 +0200 Subject: [PATCH] fix(gallery): toggle (not add) the local liked set on click (#590) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /feedback like endpoint is a server-side toggle — the same one the lightbox uses. Every grid layout's optimistic-UI setter only ever did next.add(photoId), so click 2 on a liked tile fired a server unlike but kept the heart filled in the UI. Switch each setter to toggle (delete if present, else add). Covers Masonry (default), Grid, Justified, Timeline, Carousel, Mosaic, and Premium layouts — including their identity-modal callback paths for shape consistency. Lightbox toggle is unchanged (already correct). --- .../gallery/layouts/CarouselGalleryLayout.tsx | 16 ++++++++++++++-- .../gallery/layouts/GalleryPremiumLayout.tsx | 16 ++++++++++++---- .../gallery/layouts/GridGalleryLayout.tsx | 10 +++++++--- .../gallery/layouts/JustifiedGalleryLayout.tsx | 9 +++++++-- .../gallery/layouts/MasonryGalleryLayout.tsx | 6 +++++- .../gallery/layouts/MosaicGalleryLayout.tsx | 6 ++++-- .../gallery/layouts/TimelineGalleryLayout.tsx | 16 ++++++++++++++-- 7 files changed, 63 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/gallery/layouts/CarouselGalleryLayout.tsx b/frontend/src/components/gallery/layouts/CarouselGalleryLayout.tsx index f20d040e..14a3360b 100644 --- a/frontend/src/components/gallery/layouts/CarouselGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/CarouselGalleryLayout.tsx @@ -158,7 +158,13 @@ export const CarouselGalleryLayout: React.FC = ({ } catch { return; } - setLikedIds(prev => new Set(prev).add(currentPhoto.id)); + // Toggle — server /feedback like is a toggle (#590). + setLikedIds(prev => { + const next = new Set(prev); + if (next.has(currentPhoto.id)) next.delete(currentPhoto.id); + else next.add(currentPhoto.id); + return next; + }); try { await feedbackService.submitFeedback(slug!, String(currentPhoto.id), { feedback_type: 'like', @@ -171,7 +177,13 @@ export const CarouselGalleryLayout: React.FC = ({ setShowIdentityModal(true); return; } - setLikedIds(prev => new Set(prev).add(currentPhoto.id)); + // Toggle — server /feedback like is a toggle (#590). + setLikedIds(prev => { + const next = new Set(prev); + if (next.has(currentPhoto.id)) next.delete(currentPhoto.id); + else next.add(currentPhoto.id); + return next; + }); try { await feedbackService.submitFeedback(slug!, String(currentPhoto.id), { feedback_type: 'like', diff --git a/frontend/src/components/gallery/layouts/GalleryPremiumLayout.tsx b/frontend/src/components/gallery/layouts/GalleryPremiumLayout.tsx index 9bc02f12..1cd4ef23 100644 --- a/frontend/src/components/gallery/layouts/GalleryPremiumLayout.tsx +++ b/frontend/src/components/gallery/layouts/GalleryPremiumLayout.tsx @@ -260,9 +260,11 @@ export const GalleryPremiumLayout: React.FC = ({ } catch { return; } + // Toggle — server /feedback like is a toggle (#590). setLikedPhotoIds(prev => { const next = new Set(prev); - next.add(photo.id); + if (next.has(photo.id)) next.delete(photo.id); + else next.add(photo.id); return next; }); try { @@ -282,10 +284,11 @@ export const GalleryPremiumLayout: React.FC = ({ return; } - // Optimistic update + // Optimistic update — toggle, not add (#590). setLikedPhotoIds(prev => { const next = new Set(prev); - next.add(photo.id); + if (next.has(photo.id)) next.delete(photo.id); + else next.add(photo.id); return next; }); @@ -306,9 +309,14 @@ export const GalleryPremiumLayout: React.FC = ({ setShowIdentityModal(false); if (pendingLikePhotoId) { + // Toggle — server /feedback like is a toggle (#590). The identity + // modal only fires the first time per session, so the user is + // intentionally liking a not-yet-liked photo here, but keep the + // setter shape consistent with the other paths. setLikedPhotoIds(prev => { const next = new Set(prev); - next.add(pendingLikePhotoId); + if (next.has(pendingLikePhotoId)) next.delete(pendingLikePhotoId); + else next.add(pendingLikePhotoId); return next; }); diff --git a/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx b/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx index 4a89c1f2..2bbff5a8 100644 --- a/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/GridGalleryLayout.tsx @@ -443,9 +443,12 @@ export const GridGalleryLayout: React.FC = ({ onFeedbackChange={onFeedbackChange} liked={likedPhotoIds.has(photo.id)} onLikeSuccess={() => { + // Toggle, not add — like endpoint toggles server-side, + // so the optimistic UI has to follow suit on click 2 (#590). setLikedPhotoIds((prev) => { const next = new Set(prev); - next.add(photo.id); + if (next.has(photo.id)) next.delete(photo.id); + else next.add(photo.id); return next; }); }} @@ -482,11 +485,12 @@ export const GridGalleryLayout: React.FC = ({ guest_name: name, guest_email: email, }); - // Immediately reflect like UI + // Immediately reflect like UI — toggle for consistency (#590). if (pendingAction.type === 'like') { setLikedPhotoIds((prev) => { const next = new Set(prev); - next.add(pendingAction.photoId); + if (next.has(pendingAction.photoId)) next.delete(pendingAction.photoId); + else next.add(pendingAction.photoId); return next; }); } diff --git a/frontend/src/components/gallery/layouts/JustifiedGalleryLayout.tsx b/frontend/src/components/gallery/layouts/JustifiedGalleryLayout.tsx index f34d4f36..00b26ccb 100644 --- a/frontend/src/components/gallery/layouts/JustifiedGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/JustifiedGalleryLayout.tsx @@ -763,9 +763,12 @@ export const JustifiedGalleryLayout: React.FC = ({ onFeedbackChange={onFeedbackChange} liked={likedPhotoIds.has(photo.id)} onLikeSuccess={() => { + // Toggle, not add — like endpoint toggles server-side, + // so the optimistic UI has to follow suit on click 2 (#590). setLikedPhotoIds((prev) => { const next = new Set(prev); - next.add(photo.id); + if (next.has(photo.id)) next.delete(photo.id); + else next.add(photo.id); return next; }); }} @@ -788,10 +791,12 @@ export const JustifiedGalleryLayout: React.FC = ({ guest_name: name, guest_email: email, }); + // Toggle for consistency (#590). if (pendingAction.type === 'like') { setLikedPhotoIds((prev) => { const next = new Set(prev); - next.add(pendingAction.photoId); + if (next.has(pendingAction.photoId)) next.delete(pendingAction.photoId); + else next.add(pendingAction.photoId); return next; }); } diff --git a/frontend/src/components/gallery/layouts/MasonryGalleryLayout.tsx b/frontend/src/components/gallery/layouts/MasonryGalleryLayout.tsx index 9cd5b0db..bad05802 100644 --- a/frontend/src/components/gallery/layouts/MasonryGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/MasonryGalleryLayout.tsx @@ -832,9 +832,13 @@ export const MasonryGalleryLayout: React.FC = ({ columnWidth={columnWidth} liked={likedPhotoIds.has(photo.id)} onLikeSuccess={() => { + // Toggle, not add — the /feedback like endpoint toggles + // server-side, so click 2 on a liked photo unlikes it; + // the optimistic UI must follow suit (#590). setLikedPhotoIds((prev) => { const next = new Set(prev); - next.add(photo.id); + if (next.has(photo.id)) next.delete(photo.id); + else next.add(photo.id); return next; }); }} diff --git a/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx b/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx index 2335afe2..c47643b1 100644 --- a/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/MosaicGalleryLayout.tsx @@ -116,7 +116,8 @@ const MosaicPhoto: React.FC = ({ } catch { return; } - setLikedLocal(true); + // Toggle — server /feedback like is a toggle (#590). + setLikedLocal(prev => !prev); try { await feedbackService.submitFeedback(slug!, String(photo.id), { feedback_type: 'like', @@ -129,7 +130,8 @@ const MosaicPhoto: React.FC = ({ setShowIdentityModal(true); return; } - setLikedLocal(true); + // Toggle — server /feedback like is a toggle (#590). + setLikedLocal(prev => !prev); try { await feedbackService.submitFeedback(slug!, String(photo.id), { feedback_type: 'like', diff --git a/frontend/src/components/gallery/layouts/TimelineGalleryLayout.tsx b/frontend/src/components/gallery/layouts/TimelineGalleryLayout.tsx index 279fdbb7..de3a7ddd 100644 --- a/frontend/src/components/gallery/layouts/TimelineGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/TimelineGalleryLayout.tsx @@ -155,7 +155,13 @@ export const TimelineGalleryLayout: React.FC = ({ } catch { return; } - setLikedIds(prev => new Set(prev).add(photo.id)); + // Toggle — server /feedback like is a toggle (#590). + setLikedIds(prev => { + const next = new Set(prev); + if (next.has(photo.id)) next.delete(photo.id); + else next.add(photo.id); + return next; + }); try { await feedbackService.submitFeedback(slug!, String(photo.id), { feedback_type: 'like', @@ -168,7 +174,13 @@ export const TimelineGalleryLayout: React.FC = ({ setShowIdentityModal(true); return; } - setLikedIds(prev => new Set(prev).add(photo.id)); + // Toggle — server /feedback like is a toggle (#590). + setLikedIds(prev => { + const next = new Set(prev); + if (next.has(photo.id)) next.delete(photo.id); + else next.add(photo.id); + return next; + }); try { await feedbackService.submitFeedback(slug!, String(photo.id), { feedback_type: 'like',