diff --git a/backend/src/routes/gallery.js b/backend/src/routes/gallery.js index 4f3d06f..9bb8824 100644 --- a/backend/src/routes/gallery.js +++ b/backend/src/routes/gallery.js @@ -97,12 +97,42 @@ router.get('/:slug/info', async (req, res) => { // Get all photos router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => { try { + // Get filter parameters from query + const { filter, guest_id } = req.query; + const feedbackService = require('../services/feedbackService'); + // First get all photos - const photos = await db('photos') + let photos = await db('photos') .where('photos.event_id', req.event.id) .select('photos.*') .orderBy('photos.uploaded_at', 'desc'); + // Apply filtering if requested + if (filter && guest_id) { + let filters = {}; + + // Parse filter parameter + if (filter === 'liked') { + filters.liked = true; + } else if (filter === 'favorited') { + filters.favorited = true; + } else if (filter === 'liked,favorited' || filter === 'favorited,liked') { + filters.liked = true; + filters.favorited = true; + filters.operator = 'OR'; + } + + // Get filtered photo IDs + const filteredPhotoIds = await feedbackService.getFilteredPhotos( + req.event.id, + guest_id, + filters + ); + + // Filter photos to only include those with feedback + photos = photos.filter(photo => filteredPhotoIds.includes(photo.id)); + } + // Then get comment counts separately const commentCounts = await db('photo_feedback') .whereIn('photo_id', photos.map(p => p.id)) @@ -150,13 +180,6 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => { overlay_protection: req.event.overlay_protection !== false }; - console.log('[Gallery Photos] Event data:', { - id: req.event.id, - slug: req.params.slug, - protection_level: req.event.protection_level, - calculated_protection: protectionSettings.protection_level, - is_basic_or_standard: (protectionSettings.protection_level === 'basic' || protectionSettings.protection_level === 'standard') - }); res.json({ event: { @@ -181,8 +204,6 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => { `/api/gallery/${req.params.slug}/photo/${photo.id}` : `/api/secure-images/${req.params.slug}/secure/${photo.id}/{{token}}`; - console.log(`[Photo ${photo.id}] Protection: ${protectionSettings.protection_level}, Use JWT: ${useJwtUrl}, URL: ${photoUrl}`); - return { id: photo.id, filename: photo.filename, @@ -363,14 +384,6 @@ router.get('/:slug/download-all', verifyGalleryAccess, async (req, res) => { } }); -// Test route -router.get('/:slug/photo-test/:photoId', - verifyGalleryAccess, - (req, res) => { - console.log('TEST ROUTE EXECUTED!'); - res.json({ message: 'Test route works!', photoId: req.params.photoId }); - } -); // View single photo (with watermark if enabled) router.get('/:slug/photo/:photoId', diff --git a/backend/src/services/feedbackService.js b/backend/src/services/feedbackService.js index 27e7c52..e254b43 100644 --- a/backend/src/services/feedbackService.js +++ b/backend/src/services/feedbackService.js @@ -390,6 +390,76 @@ class FeedbackService { throw error; } } + + /** + * Get filtered photos based on feedback criteria + * @param {number} eventId - Event ID + * @param {string} guestIdentifier - Guest identifier + * @param {object} filters - Filter criteria + * @param {boolean} filters.liked - Include liked photos + * @param {boolean} filters.favorited - Include favorited photos + * @param {string} filters.operator - 'AND' or 'OR' for multiple filters + * @returns {Promise} Array of photo IDs that match criteria + */ + async getFilteredPhotos(eventId, guestIdentifier, filters = {}) { + try { + const { liked, favorited, operator = 'OR' } = filters; + + // If no filters specified, return all photos + if (!liked && !favorited) { + const allPhotos = await db('photos') + .where('event_id', eventId) + .select('id'); + return allPhotos.map(p => p.id); + } + + // Build query based on filters + let query = db('photo_feedback') + .where('event_id', eventId) + .where('guest_identifier', guestIdentifier) + .where('is_hidden', false); + + // Apply filter logic + if (operator === 'AND' && liked && favorited) { + // For AND operation, we need photos that have both types of feedback + const likedPhotos = await db('photo_feedback') + .where('event_id', eventId) + .where('guest_identifier', guestIdentifier) + .where('feedback_type', 'like') + .where('is_hidden', false) + .select('photo_id'); + + const favoritedPhotos = await db('photo_feedback') + .where('event_id', eventId) + .where('guest_identifier', guestIdentifier) + .where('feedback_type', 'favorite') + .where('is_hidden', false) + .select('photo_id'); + + const likedIds = new Set(likedPhotos.map(p => p.photo_id)); + const favoritedIds = new Set(favoritedPhotos.map(p => p.photo_id)); + + // Return intersection of both sets + return Array.from(likedIds).filter(id => favoritedIds.has(id)); + } else { + // OR operation or single filter + const feedbackTypes = []; + if (liked) feedbackTypes.push('like'); + if (favorited) feedbackTypes.push('favorite'); + + query.whereIn('feedback_type', feedbackTypes); + } + + const filteredPhotos = await query + .distinct('photo_id') + .select('photo_id'); + + return filteredPhotos.map(p => p.photo_id); + } catch (error) { + logger.error('Error getting filtered photos:', error); + throw error; + } + } } module.exports = new FeedbackService(); \ No newline at end of file diff --git a/frontend/src/components/gallery/GalleryFilter.tsx b/frontend/src/components/gallery/GalleryFilter.tsx new file mode 100644 index 0000000..b19b92c --- /dev/null +++ b/frontend/src/components/gallery/GalleryFilter.tsx @@ -0,0 +1,122 @@ +import React from 'react'; +import { Heart, Star } from 'lucide-react'; +import { Button } from '../common'; +import { useTranslation } from 'react-i18next'; + +export type FilterType = 'all' | 'liked' | 'favorited'; + +interface GalleryFilterProps { + currentFilter: FilterType; + onFilterChange: (filter: FilterType) => void; + feedbackEnabled: boolean; + likeCount?: number; + favoriteCount?: number; + className?: string; + isMobile?: boolean; +} + +export const GalleryFilter: React.FC = ({ + currentFilter, + onFilterChange, + feedbackEnabled, + likeCount = 0, + favoriteCount = 0, + className = '', + isMobile = false +}) => { + const { t } = useTranslation(); + + if (!feedbackEnabled) { + return null; + } + + return ( +
+ {/* Mobile-optimized vertical layout */} + {isMobile ? ( +
+
+ {t('gallery.feedbackFilter', 'Feedback Filter')} +
+
+ + + + + +
+
+ ) : ( + /* Desktop layout - inline with categories */ +
+ + {t('gallery.feedbackFilter', 'Feedback Filter')}: + +
+ + + + + +
+
+ )} +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/gallery/GalleryView.tsx b/frontend/src/components/gallery/GalleryView.tsx index 46f702e..31e6105 100644 --- a/frontend/src/components/gallery/GalleryView.tsx +++ b/frontend/src/components/gallery/GalleryView.tsx @@ -13,6 +13,7 @@ import { GalleryLayout } from './GalleryLayout'; import { GallerySidebar } from './GallerySidebar'; import { PhotoFilterBar } from './PhotoFilterBar'; import { UserPhotoUpload } from './UserPhotoUpload'; +import type { FilterType } from './GalleryFilter'; import { analyticsService } from '../../services/analytics.service'; import { useDevToolsProtection } from '../../hooks/useDevToolsProtection'; import { GALLERY_THEME_PRESETS } from '../../types/theme.types'; @@ -55,9 +56,22 @@ export const GalleryView: React.FC = ({ slug, event }) => { const [isMobile, setIsMobile] = useState(window.innerWidth < 768); const { watermarkEnabled } = useWatermarkSettings(); const [protectionLevel, setProtectionLevel] = useState<'basic' | 'standard' | 'enhanced' | 'maximum'>('standard'); + const [filterType, setFilterType] = useState('all'); + const [guestId, setGuestId] = useState(''); - // Fetch photos - const { data, isLoading, error, refetch } = useGalleryPhotos(slug); + // Generate a unique guest ID for this session + useEffect(() => { + // Use existing guest ID from localStorage or generate new one + let storedGuestId = localStorage.getItem('gallery_guest_id'); + if (!storedGuestId) { + storedGuestId = `guest_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + localStorage.setItem('gallery_guest_id', storedGuestId); + } + setGuestId(storedGuestId); + }, []); + + // Fetch photos with filter support + const { data, isLoading, error, refetch } = useGalleryPhotos(slug, filterType, guestId); // Set protection level when data is available useEffect(() => { @@ -122,7 +136,6 @@ export const GalleryView: React.FC = ({ slug, event }) => { try { // Use public endpoint to get feedback settings const response = await api.get(`/gallery/${slug}/feedback-settings`); - console.log('Feedback settings response:', response.data); return response.data; } catch (error) { console.error('Error fetching feedback settings:', error); @@ -136,7 +149,6 @@ export const GalleryView: React.FC = ({ slug, event }) => { // Update feedbackEnabled when settings change useEffect(() => { if (feedbackSettings) { - console.log('Setting feedbackEnabled to:', feedbackSettings.feedback_enabled); setFeedbackEnabled(feedbackSettings.feedback_enabled || false); } }, [feedbackSettings]); @@ -424,6 +436,11 @@ export const GalleryView: React.FC = ({ slug, event }) => { galleryLayout={theme.galleryLayout} allowUploads={data?.event?.allow_user_uploads || event?.allow_user_uploads || false} onUploadClick={() => setShowUploadModal(true)} + feedbackEnabled={feedbackEnabled} + filterType={filterType} + onFilterChange={setFilterType} + likeCount={data?.photos?.filter(p => p.like_count > 0).length || 0} + favoriteCount={data?.photos?.filter(p => p.favorite_count > 0).length || 0} /> ) : null} @@ -510,6 +527,12 @@ export const GalleryView: React.FC = ({ slug, event }) => { sortBy={sortBy} onSortChange={setSortBy} photoCount={filteredPhotos.length} + // Feedback filter props + feedbackEnabled={feedbackEnabled} + currentFilter={filterType} + onFilterChange={setFilterType} + likeCount={data?.photos?.filter(p => p.like_count > 0).length || 0} + favoriteCount={data?.photos?.filter(p => p.favorite_count > 0).length || 0} /> ) : null} diff --git a/frontend/src/components/gallery/PhotoFeedback.tsx b/frontend/src/components/gallery/PhotoFeedback.tsx index e4c3f86..0a3353b 100644 --- a/frontend/src/components/gallery/PhotoFeedback.tsx +++ b/frontend/src/components/gallery/PhotoFeedback.tsx @@ -28,7 +28,6 @@ export const PhotoFeedback: React.FC = ({ queryKey: ['gallery-feedback-settings', gallerySlug], queryFn: async () => { const data = await feedbackService.getGalleryFeedbackSettings(gallerySlug); - console.log('PhotoFeedback received settings:', data); return data; }, staleTime: 5 * 60 * 1000, // Cache for 5 minutes diff --git a/frontend/src/components/gallery/PhotoFilterBar.tsx b/frontend/src/components/gallery/PhotoFilterBar.tsx index 0cf711b..c84299a 100644 --- a/frontend/src/components/gallery/PhotoFilterBar.tsx +++ b/frontend/src/components/gallery/PhotoFilterBar.tsx @@ -1,7 +1,8 @@ import React, { useState } from 'react'; -import { Search, SortAsc, Grid } from 'lucide-react'; +import { Search, SortAsc, Grid, Heart, Star } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Button, Input } from '../common'; +import type { FilterType } from './GalleryFilter'; interface PhotoCategory { id: number; @@ -13,6 +14,8 @@ interface PhotoCategory { interface Photo { id: number; category_id?: number; + like_count?: number; + favorite_count?: number; } interface PhotoFilterBarProps { @@ -25,6 +28,12 @@ interface PhotoFilterBarProps { sortBy: 'date' | 'name' | 'size' | 'rating'; onSortChange: (sort: 'date' | 'name' | 'size' | 'rating') => void; photoCount: number; + // Feedback filter props + feedbackEnabled?: boolean; + currentFilter?: FilterType; + onFilterChange?: (filter: FilterType) => void; + likeCount?: number; + favoriteCount?: number; } export const PhotoFilterBar: React.FC = ({ @@ -37,6 +46,11 @@ export const PhotoFilterBar: React.FC = ({ sortBy, onSortChange, photoCount, + feedbackEnabled = false, + currentFilter = 'all', + onFilterChange, + likeCount = 0, + favoriteCount = 0, }) => { const { t } = useTranslation(); const [showSortMenu, setShowSortMenu] = useState(false); @@ -44,7 +58,7 @@ export const PhotoFilterBar: React.FC = ({ return (
{/* Search and Sort */} -
+
{/* Search Bar */}
= ({ leftIcon={} value={searchTerm} onChange={(e) => onSearchChange(e.target.value)} - className="text-sm sm:text-base" + className="text-sm md:text-base" />
@@ -64,9 +78,9 @@ export const PhotoFilterBar: React.FC = ({ size="md" leftIcon={} onClick={() => setShowSortMenu(!showSortMenu)} - className="w-full sm:w-auto text-sm sm:text-base" + className="w-full md:w-auto text-sm md:text-base" > - {t('common.sortBy')} + {t('common.sortBy')} {sortBy === 'date' ? t('gallery.sortByDate').replace('Sort by ', '') : sortBy === 'name' ? t('gallery.sortByName').replace('Sort by ', '') : sortBy === 'size' ? t('gallery.sortBySize').replace('Sort by ', '') : @@ -74,7 +88,7 @@ export const PhotoFilterBar: React.FC = ({ {showSortMenu && ( -
+
- {/* Category Filter */} - {categories && categories.length > 0 && ( -
-
-
+ {/* Category and Feedback Filters */} +
+ {/* Categories Row */} + {categories && categories.length > 0 && ( +
+
@@ -149,21 +164,92 @@ export const PhotoFilterBar: React.FC = ({ variant={selectedCategoryId === category.id ? 'primary' : 'outline'} size="sm" onClick={() => onCategoryChange(category.id)} - className="text-xs sm:text-sm whitespace-nowrap" + className="text-xs md:text-sm whitespace-nowrap flex-shrink-0" > {category.name} ({categoryPhotoCount}) ); })} + + {/* Feedback Filter - Inline on desktop, below on mobile/tablet */} + {feedbackEnabled && onFilterChange && ( + <> + {/* Desktop: Divider and inline filter - only on larger screens */} +
+ {t('gallery.feedbackFilter')}: + + + +
+ + )}
-

+

{photoCount} {photoCount === 1 ? t('common.photo') : t('common.photos')}

-
- )} + )} + + {/* Mobile/Tablet: Feedback Filter below categories */} + {feedbackEnabled && onFilterChange && ( +
+ {t('gallery.feedbackFilter')}: +
+ + + +
+
+ )} +
); }; diff --git a/frontend/src/components/gallery/PhotoLightbox.tsx b/frontend/src/components/gallery/PhotoLightbox.tsx index 78219c3..1eb4490 100644 --- a/frontend/src/components/gallery/PhotoLightbox.tsx +++ b/frontend/src/components/gallery/PhotoLightbox.tsx @@ -35,9 +35,6 @@ export const PhotoLightbox: React.FC = ({ const [touchDistance, setTouchDistance] = useState(null); const [showFeedback, setShowFeedback] = useState(false); - // Debug logging - console.log('PhotoLightbox feedbackEnabled:', feedbackEnabled); - const downloadPhotoMutation = useDownloadPhoto(); const currentPhoto = photos[currentIndex]; @@ -288,7 +285,6 @@ export const PhotoLightbox: React.FC = ({ {feedbackEnabled && (