From 19f8facc49f2250a8279d6cc73f1c9a28389df41 Mon Sep 17 00:00:00 2001 From: paul Date: Wed, 17 Sep 2025 22:27:13 +0200 Subject: [PATCH] Refine gallery feedback actions --- backend/src/routes/gallery.js | 81 +++++++++ .../src/components/gallery/GalleryFilter.tsx | 49 +++--- .../src/components/gallery/GallerySidebar.tsx | 6 +- .../src/components/gallery/GalleryView.tsx | 25 ++- .../src/components/gallery/PhotoFeedback.tsx | 28 +--- .../src/components/gallery/PhotoFilterBar.tsx | 24 +-- frontend/src/components/gallery/PhotoGrid.tsx | 38 ++--- .../gallery/PhotoGridWithLayouts.tsx | 51 +++--- .../src/components/gallery/PhotoLightbox.tsx | 154 +++++++++++++++++- frontend/src/components/gallery/index.ts | 1 - .../gallery/layouts/BaseGalleryLayout.tsx | 4 + .../gallery/layouts/CarouselGalleryLayout.tsx | 47 +++--- .../gallery/layouts/GridGalleryLayout.tsx | 105 +++++++----- .../gallery/layouts/HeroGalleryLayout.tsx | 85 +++++----- .../gallery/layouts/MasonryGalleryLayout.tsx | 46 +++--- .../gallery/layouts/MosaicGalleryLayout.tsx | 144 +++++++++------- .../gallery/layouts/TimelineGalleryLayout.tsx | 59 +++---- frontend/src/hooks/useGallery.ts | 11 +- frontend/src/services/gallery.service.ts | 67 ++++++-- package-lock.json | 64 ++++++++ package.json | 6 +- 21 files changed, 719 insertions(+), 376 deletions(-) diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index e64105d..4bd0947 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -393,6 +393,87 @@ router.get('/:slug/download-all', verifyGalleryAccess, async (req, res) => { } }); +// Download selected photos as ZIP +router.post('/:slug/download-selected', verifyGalleryAccess, async (req, res) => { + try { + // Check if downloads are allowed for this event + if (req.event.allow_downloads === false) { + return res.status(403).json({ error: 'Downloads are disabled for this gallery' }); + } + + const ids = Array.isArray(req.body?.photo_ids) ? req.body.photo_ids : []; + if (!ids.length) { + return res.status(400).json({ error: 'photo_ids is required (non-empty array)' }); + } + + // Clean IDs + const photoIds = ids + .map((v) => parseInt(v, 10)) + .filter((v) => Number.isInteger(v)) + .slice(0, 500); + + if (photoIds.length === 0) { + return res.status(400).json({ error: 'No valid photo IDs provided' }); + } + + // Fetch photos + const photos = await db('photos') + .where('photos.event_id', req.event.id) + .whereIn('photos.id', photoIds) + .select('photos.*') + .orderBy('photos.uploaded_at', 'desc'); + + if (photos.length === 0) { + return res.status(404).json({ error: 'No photos found for selected IDs' }); + } + + const archiveName = `${req.event.slug}-selected.zip`; + res.setHeader('Content-Type', 'application/zip'); + res.setHeader('Content-Disposition', `attachment; filename="${archiveName}"`); + + const archive = archiver('zip', { zlib: { level: 5 } }); + archive.on('error', (err) => { + console.error('Zip error:', err); + try { res.status(500).end(); } catch (e) {} + }); + archive.pipe(res); + + const { resolvePhotoFilePath } = require('../services/photoResolver'); + const fs = require('fs'); + // Check watermark settings similar to download-all + const watermarkSettings = await watermarkService.getWatermarkSettings(); + for (const photo of photos) { + try { + const filePath = resolvePhotoFilePath(req.event, photo); + if (filePath && fs.existsSync(filePath)) { + const name = photo.filename || `photo-${photo.id}.jpg`; + if (watermarkSettings && watermarkSettings.enabled) { + // Apply watermark like download-all + const watermarkedBuffer = await watermarkService.applyWatermark(filePath, watermarkSettings); + archive.append(watermarkedBuffer, { name }); + } else { + archive.file(filePath, { name }); + } + } + } catch (e) { + // skip missing/inaccessible files + } + } + + await archive.finalize(); + + await db('access_logs').insert({ + event_id: req.event.id, + ip_address: req.ip, + user_agent: req.headers['user-agent'], + action: 'download_selected' + }); + } catch (error) { + console.error('Error in download-selected:', error); + res.status(500).json({ error: 'Failed to download selected photos' }); + } +}); + // View single photo (with watermark if enabled) router.get('/:slug/photo/:photoId', diff --git a/frontend/src/components/gallery/GalleryFilter.tsx b/frontend/src/components/gallery/GalleryFilter.tsx index 68db24c..c539563 100644 --- a/frontend/src/components/gallery/GalleryFilter.tsx +++ b/frontend/src/components/gallery/GalleryFilter.tsx @@ -1,16 +1,16 @@ import React from 'react'; -import { Heart, Star, Bookmark, MessageSquare } from 'lucide-react'; +import { Heart, Star, MessageSquare } from 'lucide-react'; import { Button } from '../common'; import { useTranslation } from 'react-i18next'; -export type FilterType = 'all' | 'liked' | 'favorited' | 'rated' | 'commented'; +export type FilterType = 'all' | 'liked' | 'rated' | 'commented'; interface GalleryFilterProps { currentFilter: FilterType; onFilterChange: (filter: FilterType) => void; feedbackEnabled: boolean; likeCount?: number; - favoriteCount?: number; + ratedCount?: number; className?: string; isMobile?: boolean; variant?: 'default' | 'compact'; @@ -21,7 +21,7 @@ export const GalleryFilter: React.FC = ({ onFilterChange, feedbackEnabled, likeCount = 0, - favoriteCount = 0, + ratedCount = 0, className = '', isMobile = false, variant = 'default' @@ -60,14 +60,23 @@ export const GalleryFilter: React.FC = ({ + @@ -103,13 +112,13 @@ export const GalleryFilter: React.FC = ({ @@ -144,21 +153,6 @@ export const GalleryFilter: React.FC = ({ )} - - - - )} + + {/* Inline Like */} + {feedbackEnabled && feedbackSettings?.allow_likes && ( +
+ + {likeCount} +
+ )} + + {/* Inline Rating */} + {feedbackEnabled && feedbackSettings?.allow_ratings && ( +
+ {[1,2,3,4,5].map((i) => ( + + ))} + {avgRating.toFixed(1)} ({totalRatings}) +
+ )} {/* Feedback button with indicator */} {feedbackEnabled && ( @@ -403,6 +527,34 @@ export const PhotoLightbox: React.FC = ({ )} + + {/* Identity Modal for required name/email */} + { setShowIdentityModal(false); setPendingAction(null); }} + onSubmit={async (name, email) => { + setSavedIdentity({ name, email }); + setShowIdentityModal(false); + if (pendingAction?.type === 'like') { + await feedbackService.submitFeedback(slug, String(currentPhoto.id), { + feedback_type: 'like', + guest_name: name, + guest_email: email, + }); + setMyLiked(true); + } else if (pendingAction?.type === 'rating' && pendingAction.rating) { + await feedbackService.submitFeedback(slug, String(currentPhoto.id), { + feedback_type: 'rating', + rating: pendingAction.rating, + guest_name: name, + guest_email: email, + }); + setMyRating(pendingAction.rating); + } + setPendingAction(null); + }} + feedbackType={pendingAction?.type === 'rating' ? 'rating' : 'like'} + /> ); }; diff --git a/frontend/src/components/gallery/index.ts b/frontend/src/components/gallery/index.ts index a89ae0a..7229843 100644 --- a/frontend/src/components/gallery/index.ts +++ b/frontend/src/components/gallery/index.ts @@ -10,4 +10,3 @@ export { PhotoFeedback } from './PhotoFeedback'; export { PhotoRating } from './PhotoRating'; export { PhotoLikes } from './PhotoLikes'; export { PhotoComments } from './PhotoComments'; -export { PhotoFavorites } from './PhotoFavorites'; \ No newline at end of file diff --git a/frontend/src/components/gallery/layouts/BaseGalleryLayout.tsx b/frontend/src/components/gallery/layouts/BaseGalleryLayout.tsx index 89baba8..90b7d41 100644 --- a/frontend/src/components/gallery/layouts/BaseGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/BaseGalleryLayout.tsx @@ -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; isSelectionMode?: boolean; diff --git a/frontend/src/components/gallery/layouts/CarouselGalleryLayout.tsx b/frontend/src/components/gallery/layouts/CarouselGalleryLayout.tsx index a44c6cc..61da92b 100644 --- a/frontend/src/components/gallery/layouts/CarouselGalleryLayout.tsx +++ b/frontend/src/components/gallery/layouts/CarouselGalleryLayout.tsx @@ -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 = ({ photos, slug, onPhotoClick, + onOpenPhotoWithFeedback, onDownload, allowDownloads = true, feedbackEnabled = false, @@ -63,8 +64,10 @@ export const CarouselGalleryLayout: React.FC = ({ const currentPhoto = photos[currentIndex]; const [showIdentityModal, setShowIdentityModal] = useState(false); - const [pendingAction, setPendingAction] = useState(null); + const [pendingAction, setPendingAction] = useState(null); const [savedIdentity, setSavedIdentity] = useState<{ name: string; email: string } | null>(null); + const [likedIds, setLikedIds] = useState>(new Set()); + const canQuickComment = Boolean(feedbackEnabled && feedbackOptions?.allowComments && onOpenPhotoWithFeedback); return (
@@ -142,7 +145,7 @@ export const CarouselGalleryLayout: React.FC = ({ )} - {feedbackEnabled && feedbackOptions?.allowLikes && ( + {feedbackOptions?.allowLikes && ( )} - {feedbackEnabled && feedbackOptions?.allowFavorites && ( + {canQuickComment && ( )}
@@ -246,7 +243,7 @@ export const CarouselGalleryLayout: React.FC = ({ setPendingAction(null); } }} - feedbackType={pendingAction?.type === 'favorite' ? 'favorite' : 'like'} + feedbackType="like" />