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',