feat: implement feedback filter for liked/favorited photos (Issue #17)
Implemented Feature Request 1 from github.com/the-luap/picpeak/issues/17: - Added filter functionality to display only liked or favorited photos - Integrated feedback filter directly into PhotoFilterBar component - Implemented responsive design with proper mobile/tablet/desktop layouts - Filter only shows when feedback is enabled for the gallery - Added proper count display for liked and favorited photos Improvements: - Fixed responsive breakpoints (mobile <768px, tablet 768-1023px, desktop ≥1024px) - Feedback filter shows inline with categories on desktop with vertical divider - On mobile/tablet, filter appears below categories to prevent layout issues - Added horizontal scrolling for category buttons to prevent cut-off Code cleanup: - Removed all debug console.log statements from production code - Removed test route from backend gallery.js - Cleaned up unnecessary logging in frontend components 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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<number[]>} 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();
|
||||
@@ -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<GalleryFilterProps> = ({
|
||||
currentFilter,
|
||||
onFilterChange,
|
||||
feedbackEnabled,
|
||||
likeCount = 0,
|
||||
favoriteCount = 0,
|
||||
className = '',
|
||||
isMobile = false
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!feedbackEnabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
{/* Mobile-optimized vertical layout */}
|
||||
{isMobile ? (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-neutral-600 font-medium">
|
||||
{t('gallery.feedbackFilter', 'Feedback Filter')}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Button
|
||||
variant={currentFilter === 'all' ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onFilterChange('all')}
|
||||
className="text-xs flex-1 min-w-[80px]"
|
||||
>
|
||||
{t('gallery.all', 'All')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={currentFilter === 'liked' ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onFilterChange('liked')}
|
||||
className="text-xs flex-1 min-w-[80px] flex items-center justify-center gap-1"
|
||||
>
|
||||
<Heart className="w-3 h-3" />
|
||||
<span>{likeCount > 0 ? likeCount : t('gallery.liked', 'Liked')}</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={currentFilter === 'favorited' ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onFilterChange('favorited')}
|
||||
className="text-xs flex-1 min-w-[80px] flex items-center justify-center gap-1"
|
||||
>
|
||||
<Star className="w-3 h-3" />
|
||||
<span>{favoriteCount > 0 ? favoriteCount : t('gallery.favorites', 'Favorites')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Desktop layout - inline with categories */
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-neutral-600 font-medium whitespace-nowrap">
|
||||
{t('gallery.feedbackFilter', 'Feedback Filter')}:
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={currentFilter === 'all' ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onFilterChange('all')}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
{t('gallery.all', 'All')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={currentFilter === 'liked' ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onFilterChange('liked')}
|
||||
className="text-xs sm:text-sm flex items-center gap-1"
|
||||
>
|
||||
<Heart className="w-3 h-3 sm:w-4 sm:h-4" />
|
||||
<span className="hidden sm:inline">{t('gallery.liked', 'Liked')}</span>
|
||||
{likeCount > 0 && (
|
||||
<span className="bg-primary-100 text-primary-700 px-1.5 rounded">
|
||||
{likeCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={currentFilter === 'favorited' ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onFilterChange('favorited')}
|
||||
className="text-xs sm:text-sm flex items-center gap-1"
|
||||
>
|
||||
<Star className="w-3 h-3 sm:w-4 sm:h-4" />
|
||||
<span className="hidden sm:inline">{t('gallery.favorited', 'Favorites')}</span>
|
||||
{favoriteCount > 0 && (
|
||||
<span className="bg-primary-100 text-primary-700 px-1.5 rounded">
|
||||
{favoriteCount}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<GalleryViewProps> = ({ 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<FilterType>('all');
|
||||
const [guestId, setGuestId] = useState<string>('');
|
||||
|
||||
// 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<GalleryViewProps> = ({ 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<GalleryViewProps> = ({ 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<GalleryViewProps> = ({ 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<GalleryViewProps> = ({ 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}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -28,7 +28,6 @@ export const PhotoFeedback: React.FC<PhotoFeedbackProps> = ({
|
||||
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
|
||||
|
||||
@@ -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<PhotoFilterBarProps> = ({
|
||||
@@ -37,6 +46,11 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
|
||||
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<PhotoFilterBarProps> = ({
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Search and Sort */}
|
||||
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4">
|
||||
<div className="flex flex-col md:flex-row gap-3 md:gap-4">
|
||||
{/* Search Bar */}
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
@@ -53,7 +67,7 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
|
||||
leftIcon={<Search className="w-5 h-5 text-neutral-400" />}
|
||||
value={searchTerm}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="text-sm sm:text-base"
|
||||
className="text-sm md:text-base"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -64,9 +78,9 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
|
||||
size="md"
|
||||
leftIcon={<SortAsc className="w-4 h-4" />}
|
||||
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"
|
||||
>
|
||||
<span className="hidden sm:inline">{t('common.sortBy')} </span>
|
||||
<span className="hidden md:inline">{t('common.sortBy')} </span>
|
||||
{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<PhotoFilterBarProps> = ({
|
||||
</Button>
|
||||
|
||||
{showSortMenu && (
|
||||
<div className="absolute right-0 sm:right-auto sm:left-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-neutral-200 py-1 z-10">
|
||||
<div className="absolute right-0 md:right-auto md:left-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-neutral-200 py-1 z-10">
|
||||
<button
|
||||
onClick={() => {
|
||||
onSortChange('date');
|
||||
@@ -124,18 +138,19 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Filter */}
|
||||
{categories && categories.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start sm:items-center justify-between flex-col sm:flex-row gap-3">
|
||||
<div className="w-full sm:w-auto overflow-x-auto pb-2 sm:pb-0">
|
||||
{/* Category and Feedback Filters */}
|
||||
<div className="space-y-3">
|
||||
{/* Categories Row */}
|
||||
{categories && categories.length > 0 && (
|
||||
<div className="flex items-start lg:items-center justify-between flex-col lg:flex-row gap-3">
|
||||
<div className="w-full overflow-x-auto pb-2 lg:pb-0">
|
||||
<div className="flex items-center gap-2 min-w-max">
|
||||
<Button
|
||||
variant={selectedCategoryId === null ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onCategoryChange(null)}
|
||||
leftIcon={<Grid className="w-3 h-3 sm:w-4 sm:h-4" />}
|
||||
className="text-xs sm:text-sm whitespace-nowrap"
|
||||
leftIcon={<Grid className="w-3 h-3 md:w-4 md:h-4" />}
|
||||
className="text-xs md:text-sm whitespace-nowrap flex-shrink-0"
|
||||
>
|
||||
{t('gallery.allPhotos')} ({photos.length})
|
||||
</Button>
|
||||
@@ -149,21 +164,92 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
|
||||
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})
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Feedback Filter - Inline on desktop, below on mobile/tablet */}
|
||||
{feedbackEnabled && onFilterChange && (
|
||||
<>
|
||||
{/* Desktop: Divider and inline filter - only on larger screens */}
|
||||
<div className="hidden lg:flex items-center gap-2 ml-2 pl-2 border-l border-neutral-300">
|
||||
<span className="text-sm text-neutral-600 whitespace-nowrap">{t('gallery.feedbackFilter')}:</span>
|
||||
<Button
|
||||
variant={currentFilter === 'all' ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onFilterChange('all')}
|
||||
className="text-xs sm:text-sm"
|
||||
>
|
||||
{t('gallery.all')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={currentFilter === 'liked' ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onFilterChange('liked')}
|
||||
className="text-xs sm:text-sm flex items-center gap-1"
|
||||
>
|
||||
<Heart className="w-3 h-3" />
|
||||
{likeCount > 0 && <span>{likeCount}</span>}
|
||||
</Button>
|
||||
<Button
|
||||
variant={currentFilter === 'favorited' ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onFilterChange('favorited')}
|
||||
className="text-xs sm:text-sm flex items-center gap-1"
|
||||
>
|
||||
<Star className="w-3 h-3" />
|
||||
{favoriteCount > 0 && <span>{favoriteCount}</span>}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs sm:text-sm text-neutral-600 flex-shrink-0">
|
||||
<p className="text-xs md:text-sm text-neutral-600 flex-shrink-0 ml-auto">
|
||||
{photoCount} {photoCount === 1 ? t('common.photo') : t('common.photos')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Mobile/Tablet: Feedback Filter below categories */}
|
||||
{feedbackEnabled && onFilterChange && (
|
||||
<div className="flex lg:hidden items-center gap-2">
|
||||
<span className="text-xs text-neutral-600">{t('gallery.feedbackFilter')}:</span>
|
||||
<div className="flex gap-1 flex-1">
|
||||
<Button
|
||||
variant={currentFilter === 'all' ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onFilterChange('all')}
|
||||
className="text-xs flex-1"
|
||||
>
|
||||
{t('gallery.all')}
|
||||
</Button>
|
||||
<Button
|
||||
variant={currentFilter === 'liked' ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onFilterChange('liked')}
|
||||
className="text-xs flex-1 flex items-center justify-center gap-1"
|
||||
>
|
||||
<Heart className="w-3 h-3" />
|
||||
{likeCount > 0 && <span>{likeCount}</span>}
|
||||
</Button>
|
||||
<Button
|
||||
variant={currentFilter === 'favorited' ? 'primary' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onFilterChange('favorited')}
|
||||
className="text-xs flex-1 flex items-center justify-center gap-1"
|
||||
>
|
||||
<Star className="w-3 h-3" />
|
||||
{favoriteCount > 0 && <span>{favoriteCount}</span>}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,9 +35,6 @@ export const PhotoLightbox: React.FC<PhotoLightboxProps> = ({
|
||||
const [touchDistance, setTouchDistance] = useState<number | null>(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<PhotoLightboxProps> = ({
|
||||
{feedbackEnabled && (
|
||||
<button
|
||||
onClick={() => {
|
||||
console.log('Feedback button clicked, current feedbackEnabled:', feedbackEnabled);
|
||||
setShowFeedback(!showFeedback);
|
||||
}}
|
||||
className="relative p-2 bg-white/10 hover:bg-white/20 rounded-full transition-colors"
|
||||
|
||||
@@ -11,10 +11,10 @@ export const useGalleryInfo = (slug: string, token?: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useGalleryPhotos = (slug: string, enabled: boolean = true) => {
|
||||
export const useGalleryPhotos = (slug: string, filter?: 'liked' | 'favorited' | 'all', guestId?: string, enabled: boolean = true) => {
|
||||
return useQuery({
|
||||
queryKey: ['gallery-photos', slug],
|
||||
queryFn: () => galleryService.getGalleryPhotos(slug),
|
||||
queryKey: ['gallery-photos', slug, filter, guestId],
|
||||
queryFn: () => galleryService.getGalleryPhotos(slug, filter, guestId),
|
||||
enabled,
|
||||
retry: 1,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
|
||||
@@ -134,6 +134,12 @@
|
||||
"sortByName": "Sort by Name",
|
||||
"sortBySize": "Sort by Size",
|
||||
"allPhotos": "All Photos",
|
||||
"filter": "Filter",
|
||||
"feedbackFilter": "Feedback Filter",
|
||||
"all": "All",
|
||||
"liked": "Liked",
|
||||
"favorited": "Favorited",
|
||||
"favorites": "Favorites",
|
||||
"downloadSelected": "Download Selected",
|
||||
"shareGallery": "Share Gallery",
|
||||
"needHelp": "Need help? Contact us at",
|
||||
|
||||
@@ -16,8 +16,13 @@ export const galleryService = {
|
||||
},
|
||||
|
||||
// Get gallery photos (requires auth)
|
||||
async getGalleryPhotos(slug: string): Promise<GalleryData> {
|
||||
const response = await api.get<GalleryData>(`/gallery/${slug}/photos`);
|
||||
async getGalleryPhotos(slug: string, filter?: 'liked' | 'favorited' | 'all', guestId?: string): Promise<GalleryData> {
|
||||
const params: any = {};
|
||||
if (filter && filter !== 'all' && guestId) {
|
||||
params.filter = filter;
|
||||
params.guest_id = guestId;
|
||||
}
|
||||
const response = await api.get<GalleryData>(`/gallery/${slug}/photos`, { params });
|
||||
return response.data;
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user