Refine gallery feedback actions
Mirror to GitHub / mirror (push) Successful in 2m10s
Test and Lint / backend-test (push) Successful in 1m48s
Test and Lint / frontend-test (push) Successful in 2m14s
Version and Release / version-bump (push) Failing after 50s
Version and Release / trigger-drone (push) Has been skipped
Mirror to GitHub / mirror (push) Successful in 2m10s
Test and Lint / backend-test (push) Successful in 1m48s
Test and Lint / frontend-test (push) Successful in 2m14s
Version and Release / version-bump (push) Failing after 50s
Version and Release / trigger-drone (push) Has been skipped
This commit is contained in:
@@ -5,6 +5,10 @@ export interface BaseGalleryLayoutProps {
|
||||
photos: Photo[];
|
||||
slug: string;
|
||||
onPhotoClick: (index: number) => void;
|
||||
// Optional: open the lightbox with feedback panel visible
|
||||
onOpenPhotoWithFeedback?: (index: number) => void;
|
||||
// Notify parent that feedback (like/favorite/rating/comment) changed
|
||||
onFeedbackChange?: () => void;
|
||||
onDownload: (photo: Photo, e: React.MouseEvent) => void;
|
||||
selectedPhotos?: Set<number>;
|
||||
isSelectionMode?: boolean;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { ChevronLeft, ChevronRight, Download, Maximize2, Play, Pause, Heart, Bookmark } from 'lucide-react';
|
||||
import { ChevronLeft, ChevronRight, Download, Maximize2, Play, Pause, Heart, MessageSquare } from 'lucide-react';
|
||||
import { useTheme } from '../../../contexts/ThemeContext';
|
||||
import { AuthenticatedImage, Button } from '../../common';
|
||||
import type { BaseGalleryLayoutProps } from './BaseGalleryLayout';
|
||||
@@ -10,6 +10,7 @@ export const CarouselGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
photos,
|
||||
slug,
|
||||
onPhotoClick,
|
||||
onOpenPhotoWithFeedback,
|
||||
onDownload,
|
||||
allowDownloads = true,
|
||||
feedbackEnabled = false,
|
||||
@@ -63,8 +64,10 @@ export const CarouselGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
|
||||
const currentPhoto = photos[currentIndex];
|
||||
const [showIdentityModal, setShowIdentityModal] = useState(false);
|
||||
const [pendingAction, setPendingAction] = useState<null | { type: 'like' | 'favorite'; photoId: number }>(null);
|
||||
const [pendingAction, setPendingAction] = useState<null | { type: 'like'; photoId: number }>(null);
|
||||
const [savedIdentity, setSavedIdentity] = useState<{ name: string; email: string } | null>(null);
|
||||
const [likedIds, setLikedIds] = useState<Set<number>>(new Set());
|
||||
const canQuickComment = Boolean(feedbackEnabled && feedbackOptions?.allowComments && onOpenPhotoWithFeedback);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
@@ -142,7 +145,7 @@ export const CarouselGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
<Download className="w-5 h-5" />
|
||||
</Button>
|
||||
)}
|
||||
{feedbackEnabled && feedbackOptions?.allowLikes && (
|
||||
{feedbackOptions?.allowLikes && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -152,38 +155,32 @@ export const CarouselGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
setShowIdentityModal(true);
|
||||
return;
|
||||
}
|
||||
await feedbackService.submitFeedback(slug!, String(currentPhoto.id), {
|
||||
feedback_type: 'like',
|
||||
guest_name: savedIdentity?.name,
|
||||
guest_email: savedIdentity?.email,
|
||||
});
|
||||
setLikedIds(prev => new Set(prev).add(currentPhoto.id));
|
||||
try {
|
||||
await feedbackService.submitFeedback(slug!, String(currentPhoto.id), {
|
||||
feedback_type: 'like',
|
||||
guest_name: savedIdentity?.name,
|
||||
guest_email: savedIdentity?.email,
|
||||
});
|
||||
} catch (_) {}
|
||||
}}
|
||||
className="text-white hover:bg-white/20"
|
||||
className={`hover:bg-white/20 ${likedIds.has(currentPhoto.id) ? 'text-red-400' : 'text-white'}`}
|
||||
title="Like photo"
|
||||
aria-pressed={likedIds.has(currentPhoto.id)}
|
||||
>
|
||||
<Heart className="w-5 h-5" />
|
||||
</Button>
|
||||
)}
|
||||
{feedbackEnabled && feedbackOptions?.allowFavorites && (
|
||||
{canQuickComment && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
if (feedbackOptions?.requireNameEmail && !savedIdentity) {
|
||||
setPendingAction({ type: 'favorite', photoId: currentPhoto.id });
|
||||
setShowIdentityModal(true);
|
||||
return;
|
||||
}
|
||||
await feedbackService.submitFeedback(slug!, String(currentPhoto.id), {
|
||||
feedback_type: 'favorite',
|
||||
guest_name: savedIdentity?.name,
|
||||
guest_email: savedIdentity?.email,
|
||||
});
|
||||
}}
|
||||
onClick={() => { onOpenPhotoWithFeedback?.(currentIndex); }}
|
||||
className="text-white hover:bg-white/20"
|
||||
title="Favorite photo"
|
||||
title="Comment"
|
||||
aria-label="Comment on photo"
|
||||
>
|
||||
<Bookmark className="w-5 h-5" />
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -246,7 +243,7 @@ export const CarouselGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
setPendingAction(null);
|
||||
}
|
||||
}}
|
||||
feedbackType={pendingAction?.type === 'favorite' ? 'favorite' : 'like'}
|
||||
feedbackType="like"
|
||||
/>
|
||||
|
||||
<style>{`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Download, Maximize2, Check, MessageSquare, Star, Heart, Bookmark } from 'lucide-react';
|
||||
import { Download, Maximize2, Check, MessageSquare, Star, Heart } from 'lucide-react';
|
||||
import { useInView } from 'react-intersection-observer';
|
||||
import { useTheme } from '../../../contexts/ThemeContext';
|
||||
import { AuthenticatedImage } from '../../common';
|
||||
@@ -23,13 +23,17 @@ interface GridPhotoProps {
|
||||
feedbackEnabled?: boolean;
|
||||
feedbackOptions?: {
|
||||
allowLikes?: boolean;
|
||||
allowFavorites?: boolean;
|
||||
allowRatings?: boolean;
|
||||
allowComments?: boolean;
|
||||
requireNameEmail?: boolean;
|
||||
};
|
||||
savedIdentity?: { name: string; email: string } | null;
|
||||
onRequireIdentity?: (action: 'like' | 'favorite', photoId: number) => void;
|
||||
onRequireIdentity?: (action: 'like', photoId: number) => void;
|
||||
onQuickComment?: () => void;
|
||||
onFeedbackChange?: () => void;
|
||||
// Immediate UI like state and callback
|
||||
liked?: boolean;
|
||||
onLikeSuccess?: () => void;
|
||||
}
|
||||
|
||||
const GridPhoto: React.FC<GridPhotoProps> = ({
|
||||
@@ -45,7 +49,13 @@ const GridPhoto: React.FC<GridPhotoProps> = ({
|
||||
protectionLevel = 'standard',
|
||||
useEnhancedProtection = false,
|
||||
feedbackEnabled = false,
|
||||
feedbackOptions
|
||||
feedbackOptions,
|
||||
savedIdentity,
|
||||
onRequireIdentity,
|
||||
onQuickComment,
|
||||
onFeedbackChange,
|
||||
liked = false,
|
||||
onLikeSuccess
|
||||
}) => {
|
||||
// handled by parent layout; kept here for type completeness but not used
|
||||
const { ref, inView } = useInView({
|
||||
@@ -94,7 +104,7 @@ const GridPhoto: React.FC<GridPhotoProps> = ({
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity duration-200 rounded-lg flex items-center justify-center gap-2">
|
||||
<div className="absolute inset-0 bg-black/40 opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity duration-200 rounded-lg flex items-center justify-center gap-2">
|
||||
{!isSelectionMode && (
|
||||
<>
|
||||
<button
|
||||
@@ -116,47 +126,45 @@ const GridPhoto: React.FC<GridPhotoProps> = ({
|
||||
<Download className="w-5 h-5 text-neutral-800" />
|
||||
</button>
|
||||
)}
|
||||
{onQuickComment && (
|
||||
<button
|
||||
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
||||
onClick={(e) => { e.stopPropagation(); onQuickComment(); }}
|
||||
aria-label="Comment on photo"
|
||||
title="Comment"
|
||||
>
|
||||
<MessageSquare className="w-5 h-5 text-neutral-800" />
|
||||
</button>
|
||||
)}
|
||||
{/* Quick feedback actions */}
|
||||
{feedbackOptions?.allowLikes && (
|
||||
<button
|
||||
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
||||
className={`p-2 rounded-full transition-colors ${liked ? 'bg-red-500/90 hover:bg-red-500' : 'bg-white/90 hover:bg-white'}`}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (feedbackOptions?.requireNameEmail && !savedIdentity && onRequireIdentity) {
|
||||
onRequireIdentity('like', photo.id);
|
||||
return;
|
||||
}
|
||||
await feedbackService.submitFeedback(slug!, String(photo.id), {
|
||||
feedback_type: 'like',
|
||||
guest_name: savedIdentity?.name,
|
||||
guest_email: savedIdentity?.email,
|
||||
});
|
||||
// Optimistic UI: mark as liked immediately
|
||||
if (onLikeSuccess) onLikeSuccess();
|
||||
try {
|
||||
await feedbackService.submitFeedback(slug!, String(photo.id), {
|
||||
feedback_type: 'like',
|
||||
guest_name: savedIdentity?.name,
|
||||
guest_email: savedIdentity?.email,
|
||||
});
|
||||
} catch (err) {
|
||||
// Keep optimistic state; a refresh will reconcile
|
||||
console.warn('Like submit failed, keeping optimistic UI', err);
|
||||
}
|
||||
if (onFeedbackChange) onFeedbackChange();
|
||||
}}
|
||||
aria-label="Like photo"
|
||||
aria-pressed={liked}
|
||||
title="Like"
|
||||
>
|
||||
<Heart className="w-5 h-5 text-neutral-800" />
|
||||
</button>
|
||||
)}
|
||||
{feedbackOptions?.allowFavorites && (
|
||||
<button
|
||||
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (feedbackOptions?.requireNameEmail && !savedIdentity && onRequireIdentity) {
|
||||
onRequireIdentity('favorite', photo.id);
|
||||
return;
|
||||
}
|
||||
await feedbackService.submitFeedback(slug!, String(photo.id), {
|
||||
feedback_type: 'favorite',
|
||||
guest_name: savedIdentity?.name,
|
||||
guest_email: savedIdentity?.email,
|
||||
});
|
||||
}}
|
||||
aria-label="Favorite photo"
|
||||
title="Favorite"
|
||||
>
|
||||
<Bookmark className="w-5 h-5 text-neutral-800" />
|
||||
<Heart className={`w-5 h-5 ${liked ? 'text-white fill-white' : 'text-neutral-800'}`} />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
@@ -180,10 +188,10 @@ const GridPhoto: React.FC<GridPhotoProps> = ({
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Feedback Indicators (always visible, bottom-left) */}
|
||||
{feedbackEnabled && (photo.comment_count > 0 || photo.average_rating > 0 || photo.like_count > 0) && (
|
||||
{/* Feedback Indicators (always visible, bottom-left). Show like immediately when user liked */}
|
||||
{(photo.comment_count > 0 || photo.average_rating > 0 || photo.like_count > 0 || liked) && (
|
||||
<div className={`absolute ${photo.type === 'collage' ? 'bottom-8' : 'bottom-2'} left-2 flex items-center gap-1 z-10`}>
|
||||
{photo.like_count > 0 && (
|
||||
{(photo.like_count > 0 || liked) && (
|
||||
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-white/90 backdrop-blur-sm" title="Liked">
|
||||
<Heart className="w-3.5 h-3.5 text-red-500" fill="currentColor" />
|
||||
</span>
|
||||
@@ -220,6 +228,8 @@ export const GridGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
photos,
|
||||
slug,
|
||||
onPhotoClick,
|
||||
onOpenPhotoWithFeedback,
|
||||
onFeedbackChange,
|
||||
onDownload,
|
||||
selectedPhotos = new Set(),
|
||||
isSelectionMode = false,
|
||||
@@ -237,7 +247,8 @@ export const GridGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
const animation = gallerySettings.photoAnimation || 'fade';
|
||||
|
||||
const [showIdentityModal, setShowIdentityModal] = React.useState(false);
|
||||
const [pendingAction, setPendingAction] = React.useState<null | { type: 'like' | 'favorite'; photoId: number }>(null);
|
||||
const [pendingAction, setPendingAction] = React.useState<null | { type: 'like'; photoId: number }>(null);
|
||||
const [likedPhotoIds, setLikedPhotoIds] = React.useState<Set<number>>(new Set());
|
||||
const [savedIdentity, setSavedIdentity] = React.useState<{ name: string; email: string } | null>(null);
|
||||
|
||||
const spacingClass = spacing === 'tight' ? 'gap-2' : spacing === 'relaxed' ? 'gap-6' : 'gap-4';
|
||||
@@ -271,6 +282,16 @@ export const GridGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
setPendingAction({ type: action, photoId });
|
||||
setShowIdentityModal(true);
|
||||
}}
|
||||
onQuickComment={() => onOpenPhotoWithFeedback && onOpenPhotoWithFeedback(index)}
|
||||
onFeedbackChange={onFeedbackChange}
|
||||
liked={likedPhotoIds.has(photo.id)}
|
||||
onLikeSuccess={() => {
|
||||
setLikedPhotoIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(photo.id);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<FeedbackIdentityModal
|
||||
@@ -285,10 +306,18 @@ export const GridGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
guest_name: name,
|
||||
guest_email: email,
|
||||
});
|
||||
// Immediately reflect like UI
|
||||
if (pendingAction.type === 'like') {
|
||||
setLikedPhotoIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(pendingAction.photoId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
setPendingAction(null);
|
||||
}
|
||||
}}
|
||||
feedbackType={pendingAction?.type === 'favorite' ? 'favorite' : 'like'}
|
||||
feedbackType="like"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Download, Maximize2, Check, ChevronDown, Calendar, Clock, Heart, Bookmark } from 'lucide-react';
|
||||
import { Download, Maximize2, Check, ChevronDown, Calendar, Clock, Heart, MessageSquare } from 'lucide-react';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocalizedDate } from '../../../hooks/useLocalizedDate';
|
||||
@@ -16,12 +16,15 @@ interface HeroGalleryLayoutProps extends BaseGalleryLayoutProps {
|
||||
eventLogo?: string | null;
|
||||
eventDate?: string;
|
||||
expiresAt?: string;
|
||||
// Use a static hero photo independent of current filter
|
||||
heroPhotoOverride?: Photo | null;
|
||||
}
|
||||
|
||||
export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
||||
photos,
|
||||
slug,
|
||||
onPhotoClick,
|
||||
onOpenPhotoWithFeedback,
|
||||
onDownload,
|
||||
selectedPhotos = new Set(),
|
||||
isSelectionMode = false,
|
||||
@@ -30,6 +33,7 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
||||
eventLogo,
|
||||
eventDate,
|
||||
expiresAt,
|
||||
heroPhotoOverride,
|
||||
allowDownloads = true,
|
||||
feedbackEnabled = false,
|
||||
feedbackOptions
|
||||
@@ -40,10 +44,20 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
||||
const [heroPhoto, setHeroPhoto] = useState<Photo | null>(null);
|
||||
const [hasInitialized, setHasInitialized] = useState(false);
|
||||
const [showIdentityModal, setShowIdentityModal] = useState(false);
|
||||
const [pendingAction, setPendingAction] = useState<null | { type: 'like' | 'favorite'; photoId: number }>(null);
|
||||
const [pendingAction, setPendingAction] = useState<null | { type: 'like'; photoId: number }>(null);
|
||||
const [savedIdentity, setSavedIdentity] = useState<{ name: string; email: string } | null>(null);
|
||||
const gallerySettings = theme.gallerySettings || {};
|
||||
const overlayOpacity = gallerySettings.heroOverlayOpacity || 0.3;
|
||||
const [likedIds, setLikedIds] = useState<Set<number>>(new Set());
|
||||
const canQuickComment = Boolean(feedbackEnabled && feedbackOptions?.allowComments && onOpenPhotoWithFeedback);
|
||||
|
||||
// If an override is provided, always use it and skip initialization logic
|
||||
useEffect(() => {
|
||||
if (heroPhotoOverride) {
|
||||
setHeroPhoto(heroPhotoOverride);
|
||||
setHasInitialized(true);
|
||||
}
|
||||
}, [heroPhotoOverride]);
|
||||
|
||||
// Reset initialization when heroImageId changes
|
||||
useEffect(() => {
|
||||
@@ -54,29 +68,28 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
||||
|
||||
// Select hero photo (admin-selected or first photo only if gallery was empty)
|
||||
useEffect(() => {
|
||||
// When an override is provided, the effect above has already set the hero.
|
||||
if (heroPhotoOverride) return;
|
||||
|
||||
if (photos.length > 0) {
|
||||
const heroId = gallerySettings.heroImageId;
|
||||
// Process hero layout with provided photos
|
||||
|
||||
// If admin has selected a specific hero image, always use it
|
||||
// If admin has selected a specific hero image, always use it when available
|
||||
if (heroId) {
|
||||
const adminSelectedHero = photos.find(p => p.id === heroId);
|
||||
// Hero photo selected by admin
|
||||
if (adminSelectedHero) {
|
||||
setHeroPhoto(adminSelectedHero);
|
||||
setHasInitialized(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Only auto-select first photo on initial load when gallery was empty
|
||||
// This prevents changing the hero when new photos are uploaded
|
||||
|
||||
// Only auto-select first photo on initial load
|
||||
if (!hasInitialized) {
|
||||
setHeroPhoto(photos[0]);
|
||||
setHasInitialized(true);
|
||||
}
|
||||
}
|
||||
}, [photos, gallerySettings.heroImageId, hasInitialized]);
|
||||
}, [photos, gallerySettings.heroImageId, hasInitialized, heroPhotoOverride]);
|
||||
|
||||
if (!heroPhoto) return null;
|
||||
|
||||
@@ -198,9 +211,9 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
||||
<Download className="w-5 h-5 text-neutral-800" />
|
||||
</button>
|
||||
)}
|
||||
{feedbackEnabled && feedbackOptions?.allowLikes && (
|
||||
{feedbackOptions?.allowLikes && (
|
||||
<button
|
||||
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
||||
className={`p-2 rounded-full transition-colors ${likedIds.has(photo.id) ? 'bg-red-500/90 hover:bg-red-500' : 'bg-white/90 hover:bg-white'}`}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (feedbackOptions?.requireNameEmail && !savedIdentity) {
|
||||
@@ -208,38 +221,30 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
||||
setShowIdentityModal(true);
|
||||
return;
|
||||
}
|
||||
await feedbackService.submitFeedback(slug!, String(photo.id), {
|
||||
feedback_type: 'like',
|
||||
guest_name: savedIdentity?.name,
|
||||
guest_email: savedIdentity?.email,
|
||||
});
|
||||
setLikedIds(prev => new Set(prev).add(photo.id));
|
||||
try {
|
||||
await feedbackService.submitFeedback(slug!, String(photo.id), {
|
||||
feedback_type: 'like',
|
||||
guest_name: savedIdentity?.name,
|
||||
guest_email: savedIdentity?.email,
|
||||
});
|
||||
} catch (_) {}
|
||||
}}
|
||||
aria-label="Like photo"
|
||||
aria-pressed={likedIds.has(photo.id)}
|
||||
title="Like"
|
||||
>
|
||||
<Heart className="w-5 h-5 text-neutral-800" />
|
||||
<Heart className={`w-5 h-5 ${likedIds.has(photo.id) ? 'text-white fill-white' : 'text-neutral-800'}`} />
|
||||
</button>
|
||||
)}
|
||||
{feedbackEnabled && feedbackOptions?.allowFavorites && (
|
||||
{canQuickComment && (
|
||||
<button
|
||||
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (feedbackOptions?.requireNameEmail && !savedIdentity) {
|
||||
setPendingAction({ type: 'favorite', photoId: photo.id });
|
||||
setShowIdentityModal(true);
|
||||
return;
|
||||
}
|
||||
await feedbackService.submitFeedback(slug!, String(photo.id), {
|
||||
feedback_type: 'favorite',
|
||||
guest_name: savedIdentity?.name,
|
||||
guest_email: savedIdentity?.email,
|
||||
});
|
||||
}}
|
||||
aria-label="Favorite photo"
|
||||
title="Favorite"
|
||||
onClick={(e) => { e.stopPropagation(); onOpenPhotoWithFeedback?.(actualIndex); }}
|
||||
aria-label="Comment on photo"
|
||||
title="Comment"
|
||||
>
|
||||
<Bookmark className="w-5 h-5 text-neutral-800" />
|
||||
<MessageSquare className="w-5 h-5 text-neutral-800" />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
@@ -263,18 +268,16 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* Feedback indicators (always visible, bottom-left) */}
|
||||
{(feedbackEnabled && (photo.like_count > 0 || (photo.average_rating || 0) > 0 || (photo.comment_count || 0) > 0)) && (
|
||||
{/* Feedback indicators (always visible, bottom-left). Show like immediately when liked */}
|
||||
{(photo.like_count > 0 || likedIds.has(photo.id) || (photo.average_rating || 0) > 0 || (photo.comment_count || 0) > 0) && (
|
||||
<div className={`absolute ${photo.type === 'collage' ? 'bottom-8' : 'bottom-2'} left-2 flex items-center gap-1 z-20`}>
|
||||
{photo.like_count > 0 && (
|
||||
{(photo.like_count > 0 || likedIds.has(photo.id)) && (
|
||||
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-white/90 backdrop-blur-sm" title="Liked">
|
||||
<Heart className="w-3.5 h-3.5 text-red-500" fill="currentColor" />
|
||||
</span>
|
||||
)}
|
||||
{(photo.average_rating || 0) > 0 && (
|
||||
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-white/90 backdrop-blur-sm" title="Rated">
|
||||
<Bookmark className="hidden" />
|
||||
{/* Using star icon to indicate rating */}
|
||||
<svg viewBox="0 0 24 24" className="w-3.5 h-3.5 text-yellow-500 fill-current"><path d="M12 .587l3.668 7.431 8.2 1.193-5.934 5.787 1.402 8.168L12 18.897l-7.336 3.869 1.402-8.168L.132 9.211l8.2-1.193z"/></svg>
|
||||
</span>
|
||||
)}
|
||||
@@ -305,7 +308,7 @@ export const HeroGalleryLayout: React.FC<HeroGalleryLayoutProps> = ({
|
||||
setPendingAction(null);
|
||||
}
|
||||
}}
|
||||
feedbackType={pendingAction?.type === 'favorite' ? 'favorite' : 'like'}
|
||||
feedbackType="like"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Download, Maximize2, Check, MessageSquare, Star, Heart, Bookmark } from 'lucide-react';
|
||||
import { Download, Maximize2, Check, MessageSquare, Star, Heart } from 'lucide-react';
|
||||
import { useTheme } from '../../../contexts/ThemeContext';
|
||||
import { AuthenticatedImage } from '../../common';
|
||||
import { FeedbackIdentityModal } from '../../gallery/FeedbackIdentityModal';
|
||||
@@ -20,9 +20,10 @@ interface MasonryPhotoProps {
|
||||
slug?: string;
|
||||
feedbackOptions?: {
|
||||
allowLikes?: boolean;
|
||||
allowFavorites?: boolean;
|
||||
allowComments?: boolean;
|
||||
requireNameEmail?: boolean;
|
||||
};
|
||||
onQuickComment?: () => void;
|
||||
}
|
||||
|
||||
const MasonryPhoto: React.FC<MasonryPhotoProps> = ({
|
||||
@@ -36,11 +37,12 @@ const MasonryPhoto: React.FC<MasonryPhotoProps> = ({
|
||||
allowDownloads = true,
|
||||
feedbackEnabled = false,
|
||||
slug,
|
||||
feedbackOptions
|
||||
feedbackOptions,
|
||||
onQuickComment
|
||||
}) => {
|
||||
const [imageHeight, setImageHeight] = useState<number>(200);
|
||||
const [showIdentityModal, setShowIdentityModal] = useState(false);
|
||||
const [pendingAction, setPendingAction] = useState<null | { type: 'like' | 'favorite'; photoId: number }>(null);
|
||||
const [pendingAction, setPendingAction] = useState<null | { type: 'like'; photoId: number }>(null);
|
||||
const [savedIdentity, setSavedIdentity] = useState<{ name: string; email: string } | null>(null);
|
||||
|
||||
// Generate random heights for masonry effect
|
||||
@@ -115,6 +117,16 @@ const MasonryPhoto: React.FC<MasonryPhotoProps> = ({
|
||||
<Download className="w-5 h-5 text-neutral-800" />
|
||||
</button>
|
||||
)}
|
||||
{onQuickComment && (
|
||||
<button
|
||||
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
||||
onClick={(e) => { e.stopPropagation(); onQuickComment(); }}
|
||||
aria-label="Comment on photo"
|
||||
title="Comment"
|
||||
>
|
||||
<MessageSquare className="w-5 h-5 text-neutral-800" />
|
||||
</button>
|
||||
)}
|
||||
{feedbackOptions?.allowLikes && (
|
||||
<button
|
||||
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
||||
@@ -137,28 +149,6 @@ const MasonryPhoto: React.FC<MasonryPhotoProps> = ({
|
||||
<Heart className="w-5 h-5 text-neutral-800" />
|
||||
</button>
|
||||
)}
|
||||
{feedbackOptions?.allowFavorites && (
|
||||
<button
|
||||
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (feedbackOptions?.requireNameEmail && !savedIdentity) {
|
||||
setPendingAction({ type: 'favorite', photoId: photo.id });
|
||||
setShowIdentityModal(true);
|
||||
return;
|
||||
}
|
||||
await feedbackService.submitFeedback(slug!, String(photo.id), {
|
||||
feedback_type: 'favorite',
|
||||
guest_name: savedIdentity?.name,
|
||||
guest_email: savedIdentity?.email,
|
||||
});
|
||||
}}
|
||||
aria-label="Favorite photo"
|
||||
title="Favorite"
|
||||
>
|
||||
<Bookmark className="w-5 h-5 text-neutral-800" />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -179,7 +169,7 @@ const MasonryPhoto: React.FC<MasonryPhotoProps> = ({
|
||||
setPendingAction(null);
|
||||
}
|
||||
}}
|
||||
feedbackType={pendingAction?.type === 'favorite' ? 'favorite' : 'like'}
|
||||
feedbackType="like"
|
||||
/>
|
||||
|
||||
{/* Selection Checkbox (visible on hover or when selected) */}
|
||||
@@ -214,6 +204,7 @@ export const MasonryGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
photos,
|
||||
slug,
|
||||
onPhotoClick,
|
||||
onOpenPhotoWithFeedback,
|
||||
onDownload,
|
||||
selectedPhotos = new Set(),
|
||||
isSelectionMode = false,
|
||||
@@ -278,6 +269,7 @@ export const MasonryGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
feedbackEnabled={feedbackEnabled}
|
||||
slug={slug}
|
||||
feedbackOptions={feedbackOptions}
|
||||
onQuickComment={() => onOpenPhotoWithFeedback && onOpenPhotoWithFeedback(originalIndex)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Download, Maximize2, Check, Heart, Bookmark } from 'lucide-react';
|
||||
import { Download, Maximize2, Check, Heart, MessageSquare } from 'lucide-react';
|
||||
// import { useTheme } from '../../../contexts/ThemeContext';
|
||||
import { AuthenticatedImage } from '../../common';
|
||||
import { FeedbackIdentityModal } from '../../gallery/FeedbackIdentityModal';
|
||||
@@ -20,9 +20,10 @@ interface MosaicPhotoProps {
|
||||
feedbackEnabled?: boolean;
|
||||
feedbackOptions?: {
|
||||
allowLikes?: boolean;
|
||||
allowFavorites?: boolean;
|
||||
allowComments?: boolean;
|
||||
requireNameEmail?: boolean;
|
||||
};
|
||||
onQuickComment?: () => void;
|
||||
}
|
||||
|
||||
const MosaicPhoto: React.FC<MosaicPhotoProps> = ({
|
||||
@@ -35,12 +36,16 @@ const MosaicPhoto: React.FC<MosaicPhotoProps> = ({
|
||||
className = '',
|
||||
allowDownloads = true,
|
||||
slug,
|
||||
feedbackEnabled,
|
||||
feedbackOptions
|
||||
feedbackEnabled = false,
|
||||
feedbackOptions,
|
||||
onQuickComment
|
||||
}) => {
|
||||
const [showIdentityModal, setShowIdentityModal] = React.useState(false);
|
||||
const [pendingAction, setPendingAction] = React.useState<null | { type: 'like' | 'favorite'; photoId: number }>(null);
|
||||
const [pendingAction, setPendingAction] = React.useState<null | { type: 'like'; photoId: number }>(null);
|
||||
const [savedIdentity, setSavedIdentity] = React.useState<{ name: string; email: string } | null>(null);
|
||||
const [likedLocal, setLikedLocal] = React.useState(false);
|
||||
const canComment = Boolean(feedbackEnabled && feedbackOptions?.allowComments && onQuickComment);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@@ -83,9 +88,9 @@ const MosaicPhoto: React.FC<MosaicPhotoProps> = ({
|
||||
<Download className="w-5 h-5 text-neutral-800" />
|
||||
</button>
|
||||
)}
|
||||
{feedbackEnabled && feedbackOptions?.allowLikes && (
|
||||
{feedbackOptions?.allowLikes && (
|
||||
<button
|
||||
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
||||
className={`p-2 rounded-full transition-colors ${likedLocal ? 'bg-red-500/90 hover:bg-red-500' : 'bg-white/90 hover:bg-white'}`}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (feedbackOptions?.requireNameEmail && !savedIdentity) {
|
||||
@@ -93,44 +98,45 @@ const MosaicPhoto: React.FC<MosaicPhotoProps> = ({
|
||||
setShowIdentityModal(true);
|
||||
return;
|
||||
}
|
||||
await feedbackService.submitFeedback(slug!, String(photo.id), {
|
||||
feedback_type: 'like',
|
||||
guest_name: savedIdentity?.name,
|
||||
guest_email: savedIdentity?.email,
|
||||
});
|
||||
setLikedLocal(true);
|
||||
try {
|
||||
await feedbackService.submitFeedback(slug!, String(photo.id), {
|
||||
feedback_type: 'like',
|
||||
guest_name: savedIdentity?.name,
|
||||
guest_email: savedIdentity?.email,
|
||||
});
|
||||
} catch (_) {}
|
||||
}}
|
||||
aria-label="Like photo"
|
||||
aria-pressed={likedLocal}
|
||||
title="Like"
|
||||
>
|
||||
<Heart className="w-5 h-5 text-neutral-800" />
|
||||
<Heart className={`w-5 h-5 ${likedLocal ? 'text-white fill-white' : 'text-neutral-800'}`} />
|
||||
</button>
|
||||
)}
|
||||
{feedbackEnabled && feedbackOptions?.allowFavorites && (
|
||||
{canComment && (
|
||||
<button
|
||||
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (feedbackOptions?.requireNameEmail && !savedIdentity) {
|
||||
setPendingAction({ type: 'favorite', photoId: photo.id });
|
||||
setShowIdentityModal(true);
|
||||
return;
|
||||
}
|
||||
await feedbackService.submitFeedback(slug!, String(photo.id), {
|
||||
feedback_type: 'favorite',
|
||||
guest_name: savedIdentity?.name,
|
||||
guest_email: savedIdentity?.email,
|
||||
});
|
||||
}}
|
||||
aria-label="Favorite photo"
|
||||
title="Favorite"
|
||||
onClick={(e) => { e.stopPropagation(); onQuickComment?.(); }}
|
||||
aria-label="Comment on photo"
|
||||
title="Comment"
|
||||
>
|
||||
<Bookmark className="w-5 h-5 text-neutral-800" />
|
||||
<MessageSquare className="w-5 h-5 text-neutral-800" />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Feedback Indicators (bottom-left) */}
|
||||
{(photo.like_count > 0 || likedLocal) && (
|
||||
<div className={`absolute ${photo.type === 'collage' ? 'bottom-8' : 'bottom-2'} left-2 z-10`}>
|
||||
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-white/90 backdrop-blur-sm" title="Liked">
|
||||
<Heart className="w-3.5 h-3.5 text-red-500" fill="currentColor" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selection Checkbox (visible on hover or when selected) */}
|
||||
<button
|
||||
type="button"
|
||||
@@ -171,7 +177,7 @@ const MosaicPhoto: React.FC<MosaicPhotoProps> = ({
|
||||
setPendingAction(null);
|
||||
}
|
||||
}}
|
||||
feedbackType={pendingAction?.type === 'favorite' ? 'favorite' : 'like'}
|
||||
feedbackType="like"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
@@ -181,6 +187,7 @@ export const MosaicGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
photos,
|
||||
slug,
|
||||
onPhotoClick,
|
||||
onOpenPhotoWithFeedback,
|
||||
onDownload,
|
||||
selectedPhotos = new Set(),
|
||||
isSelectionMode = false,
|
||||
@@ -236,6 +243,7 @@ export const MosaicGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
slug={slug}
|
||||
feedbackEnabled={feedbackEnabled}
|
||||
feedbackOptions={feedbackOptions}
|
||||
onQuickComment={() => { if (typeof onOpenPhotoWithFeedback !== 'undefined' && onOpenPhotoWithFeedback) onOpenPhotoWithFeedback(idx0); }}
|
||||
/>
|
||||
)}
|
||||
<div className="grid grid-rows-2 gap-2">
|
||||
@@ -244,14 +252,15 @@ export const MosaicGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
photo={photo1}
|
||||
isSelected={selectedPhotos.has(photo1.id)}
|
||||
isSelectionMode={isSelectionMode}
|
||||
onClick={() => onPhotoClick(idx1)}
|
||||
onDownload={(e) => onDownload(photo1, e)}
|
||||
onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo1.id)}
|
||||
className=""
|
||||
allowDownloads={allowDownloads}
|
||||
slug={slug}
|
||||
feedbackEnabled={feedbackEnabled}
|
||||
feedbackOptions={feedbackOptions}
|
||||
onClick={() => onPhotoClick(idx1)}
|
||||
onDownload={(e) => onDownload(photo1, e)}
|
||||
onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo1.id)}
|
||||
className=""
|
||||
allowDownloads={allowDownloads}
|
||||
slug={slug}
|
||||
feedbackEnabled={feedbackEnabled}
|
||||
feedbackOptions={feedbackOptions}
|
||||
onQuickComment={() => { if (typeof onOpenPhotoWithFeedback !== 'undefined' && onOpenPhotoWithFeedback) onOpenPhotoWithFeedback(idx1); }}
|
||||
/>
|
||||
)}
|
||||
{photo2 && (
|
||||
@@ -259,14 +268,15 @@ export const MosaicGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
photo={photo2}
|
||||
isSelected={selectedPhotos.has(photo2.id)}
|
||||
isSelectionMode={isSelectionMode}
|
||||
onClick={() => onPhotoClick(idx2)}
|
||||
onDownload={(e) => onDownload(photo2, e)}
|
||||
onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo2.id)}
|
||||
className=""
|
||||
allowDownloads={allowDownloads}
|
||||
slug={slug}
|
||||
feedbackEnabled={feedbackEnabled}
|
||||
feedbackOptions={feedbackOptions}
|
||||
onClick={() => onPhotoClick(idx2)}
|
||||
onDownload={(e) => onDownload(photo2, e)}
|
||||
onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo2.id)}
|
||||
className=""
|
||||
allowDownloads={allowDownloads}
|
||||
slug={slug}
|
||||
feedbackEnabled={feedbackEnabled}
|
||||
feedbackOptions={feedbackOptions}
|
||||
onQuickComment={() => { if (typeof onOpenPhotoWithFeedback !== 'undefined' && onOpenPhotoWithFeedback) onOpenPhotoWithFeedback(idx2); }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -290,6 +300,10 @@ export const MosaicGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
onDownload={(e) => onDownload(photo, e)}
|
||||
className=""
|
||||
allowDownloads={allowDownloads}
|
||||
slug={slug}
|
||||
feedbackEnabled={feedbackEnabled}
|
||||
feedbackOptions={feedbackOptions}
|
||||
onQuickComment={() => { if (typeof onOpenPhotoWithFeedback !== 'undefined' && onOpenPhotoWithFeedback) onOpenPhotoWithFeedback(currentIndex); }}
|
||||
/>
|
||||
) : null;
|
||||
})}
|
||||
@@ -321,6 +335,7 @@ export const MosaicGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
slug={slug}
|
||||
feedbackEnabled={feedbackEnabled}
|
||||
feedbackOptions={feedbackOptions}
|
||||
onQuickComment={() => { if (typeof onOpenPhotoWithFeedback !== 'undefined' && onOpenPhotoWithFeedback) onOpenPhotoWithFeedback(idx0); }}
|
||||
/>
|
||||
)}
|
||||
<div className="grid grid-rows-2 gap-2">
|
||||
@@ -329,14 +344,15 @@ export const MosaicGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
photo={photo1}
|
||||
isSelected={selectedPhotos.has(photo1.id)}
|
||||
isSelectionMode={isSelectionMode}
|
||||
onClick={() => onPhotoClick(idx1)}
|
||||
onDownload={(e) => onDownload(photo1, e)}
|
||||
onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo1.id)}
|
||||
className=""
|
||||
allowDownloads={allowDownloads}
|
||||
slug={slug}
|
||||
feedbackEnabled={feedbackEnabled}
|
||||
feedbackOptions={feedbackOptions}
|
||||
onClick={() => onPhotoClick(idx1)}
|
||||
onDownload={(e) => onDownload(photo1, e)}
|
||||
onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo1.id)}
|
||||
className=""
|
||||
allowDownloads={allowDownloads}
|
||||
slug={slug}
|
||||
feedbackEnabled={feedbackEnabled}
|
||||
feedbackOptions={feedbackOptions}
|
||||
onQuickComment={() => { if (typeof onOpenPhotoWithFeedback !== 'undefined' && onOpenPhotoWithFeedback) onOpenPhotoWithFeedback(idx1); }}
|
||||
/>
|
||||
)}
|
||||
{photo2 && (
|
||||
@@ -344,14 +360,15 @@ export const MosaicGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
photo={photo2}
|
||||
isSelected={selectedPhotos.has(photo2.id)}
|
||||
isSelectionMode={isSelectionMode}
|
||||
onClick={() => onPhotoClick(idx2)}
|
||||
onDownload={(e) => onDownload(photo2, e)}
|
||||
onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo2.id)}
|
||||
className=""
|
||||
allowDownloads={allowDownloads}
|
||||
slug={slug}
|
||||
feedbackEnabled={feedbackEnabled}
|
||||
feedbackOptions={feedbackOptions}
|
||||
onClick={() => onPhotoClick(idx2)}
|
||||
onDownload={(e) => onDownload(photo2, e)}
|
||||
onToggleSelect={() => onPhotoSelect && onPhotoSelect(photo2.id)}
|
||||
className=""
|
||||
allowDownloads={allowDownloads}
|
||||
slug={slug}
|
||||
feedbackEnabled={feedbackEnabled}
|
||||
feedbackOptions={feedbackOptions}
|
||||
onQuickComment={() => { if (typeof onOpenPhotoWithFeedback !== 'undefined' && onOpenPhotoWithFeedback) onOpenPhotoWithFeedback(idx2); }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -387,6 +404,7 @@ export const MosaicGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
slug={slug}
|
||||
feedbackEnabled={feedbackEnabled}
|
||||
feedbackOptions={feedbackOptions}
|
||||
onQuickComment={() => { if (typeof onOpenPhotoWithFeedback !== 'undefined' && onOpenPhotoWithFeedback) onOpenPhotoWithFeedback(index); }}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Download, Maximize2, Check, Calendar, Heart, Bookmark } from 'lucide-react';
|
||||
import { Download, Maximize2, Check, Calendar, Heart, MessageSquare } from 'lucide-react';
|
||||
import { format, parseISO, startOfDay, startOfWeek, startOfMonth } from 'date-fns';
|
||||
import { useTheme } from '../../../contexts/ThemeContext';
|
||||
import { AuthenticatedImage } from '../../common';
|
||||
@@ -12,6 +12,7 @@ export const TimelineGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
photos,
|
||||
slug,
|
||||
onPhotoClick,
|
||||
onOpenPhotoWithFeedback,
|
||||
onDownload,
|
||||
selectedPhotos = new Set(),
|
||||
isSelectionMode = false,
|
||||
@@ -21,12 +22,14 @@ export const TimelineGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
feedbackOptions
|
||||
}) => {
|
||||
const { theme } = useTheme();
|
||||
const [likedIds, setLikedIds] = useState<Set<number>>(new Set());
|
||||
const [showIdentityModal, setShowIdentityModal] = useState(false);
|
||||
const [pendingAction, setPendingAction] = useState<null | { type: 'like' | 'favorite'; photoId: number }>(null);
|
||||
const [pendingAction, setPendingAction] = useState<null | { type: 'like'; photoId: number }>(null);
|
||||
const [savedIdentity, setSavedIdentity] = useState<{ name: string; email: string } | null>(null);
|
||||
const gallerySettings = theme.gallerySettings || {};
|
||||
const grouping = gallerySettings.timelineGrouping || 'day';
|
||||
const showDates = gallerySettings.timelineShowDates !== false;
|
||||
const canQuickComment = Boolean(feedbackEnabled && feedbackOptions?.allowComments && onOpenPhotoWithFeedback);
|
||||
|
||||
// Group photos by date
|
||||
const groupedPhotos = useMemo(() => {
|
||||
@@ -139,9 +142,9 @@ export const TimelineGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
<Download className="w-5 h-5 text-neutral-800" />
|
||||
</button>
|
||||
)}
|
||||
{feedbackEnabled && feedbackOptions?.allowLikes && (
|
||||
{feedbackOptions?.allowLikes && (
|
||||
<button
|
||||
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
||||
className={`p-2 rounded-full transition-colors ${likedIds.has(photo.id) ? 'bg-red-500/90 hover:bg-red-500' : 'bg-white/90 hover:bg-white'}`}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (feedbackOptions?.requireNameEmail && !savedIdentity) {
|
||||
@@ -149,44 +152,44 @@ export const TimelineGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
setShowIdentityModal(true);
|
||||
return;
|
||||
}
|
||||
await feedbackService.submitFeedback(slug!, String(photo.id), {
|
||||
feedback_type: 'like',
|
||||
guest_name: savedIdentity?.name,
|
||||
guest_email: savedIdentity?.email,
|
||||
});
|
||||
setLikedIds(prev => new Set(prev).add(photo.id));
|
||||
try {
|
||||
await feedbackService.submitFeedback(slug!, String(photo.id), {
|
||||
feedback_type: 'like',
|
||||
guest_name: savedIdentity?.name,
|
||||
guest_email: savedIdentity?.email,
|
||||
});
|
||||
} catch (_) {}
|
||||
}}
|
||||
aria-label="Like photo"
|
||||
aria-pressed={likedIds.has(photo.id)}
|
||||
title="Like"
|
||||
>
|
||||
<Heart className="w-5 h-5 text-neutral-800" />
|
||||
<Heart className={`w-5 h-5 ${likedIds.has(photo.id) ? 'text-white fill-white' : 'text-neutral-800'}`} />
|
||||
</button>
|
||||
)}
|
||||
{feedbackEnabled && feedbackOptions?.allowFavorites && (
|
||||
{canQuickComment && (
|
||||
<button
|
||||
className="p-2 bg-white/90 rounded-full hover:bg-white transition-colors"
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
if (feedbackOptions?.requireNameEmail && !savedIdentity) {
|
||||
setPendingAction({ type: 'favorite', photoId: photo.id });
|
||||
setShowIdentityModal(true);
|
||||
return;
|
||||
}
|
||||
await feedbackService.submitFeedback(slug!, String(photo.id), {
|
||||
feedback_type: 'favorite',
|
||||
guest_name: savedIdentity?.name,
|
||||
guest_email: savedIdentity?.email,
|
||||
});
|
||||
}}
|
||||
aria-label="Favorite photo"
|
||||
title="Favorite"
|
||||
onClick={(e) => { e.stopPropagation(); onOpenPhotoWithFeedback?.(actualIndex); }}
|
||||
aria-label="Comment on photo"
|
||||
title="Comment"
|
||||
>
|
||||
<Bookmark className="w-5 h-5 text-neutral-800" />
|
||||
<MessageSquare className="w-5 h-5 text-neutral-800" />
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(photo.like_count > 0 || likedIds.has(photo.id)) && (
|
||||
<div className={`absolute ${photo.type === 'collage' ? 'bottom-8' : 'bottom-2'} left-2 z-10`}>
|
||||
<span className="inline-flex items-center justify-center w-6 h-6 rounded-full bg-white/90 backdrop-blur-sm" title="Liked">
|
||||
<Heart className="w-3.5 h-3.5 text-red-500" fill="currentColor" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selection Checkbox (visible on hover or when selected) */}
|
||||
<button
|
||||
type="button"
|
||||
@@ -225,7 +228,7 @@ export const TimelineGalleryLayout: React.FC<BaseGalleryLayoutProps> = ({
|
||||
setPendingAction(null);
|
||||
}
|
||||
}}
|
||||
feedbackType={pendingAction?.type === 'favorite' ? 'favorite' : 'like'}
|
||||
feedbackType="like"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user