feat(gallery): multi-select feedback filters + sort direction controls (#889) (#929)

* feat(gallery): multi-select feedback filters + sort direction controls (#889)

* fix(gallery): keep mobile sidebar open while combining feedback filters (#889)

* fix(gallery): generic sort icon when direction is uncontrolled (#889)

---------

Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
Paul Nothaft
2026-07-31 08:57:08 +02:00
committed by GitHub
co-authored by Paul Nothaft
parent 08ff9f20e7
commit 3bcded78a4
5 changed files with 170 additions and 83 deletions
@@ -4,9 +4,13 @@ import { Button } from '../common';
import { useTranslation } from 'react-i18next';
export type FilterType = 'all' | 'liked' | 'favorited' | 'rated' | 'commented';
// Multi-select feedback filters (#889): the active set holds the concrete
// filters; an empty set means "All".
export type FeedbackFilterType = Exclude<FilterType, 'all'>;
interface GalleryFilterProps {
currentFilter: FilterType;
activeFilters: FeedbackFilterType[];
// Clicking a filter toggles it in the parent's set; 'all' clears the set.
onFilterChange: (filter: FilterType) => void;
feedbackEnabled: boolean;
likeCount?: number;
@@ -18,7 +22,7 @@ interface GalleryFilterProps {
}
export const GalleryFilter: React.FC<GalleryFilterProps> = ({
currentFilter,
activeFilters,
onFilterChange,
feedbackEnabled,
likeCount = 0,
@@ -30,6 +34,9 @@ export const GalleryFilter: React.FC<GalleryFilterProps> = ({
}) => {
const { t } = useTranslation();
const isActive = (filter: FilterType) =>
filter === 'all' ? activeFilters.length === 0 : activeFilters.includes(filter);
if (!feedbackEnabled) {
return null;
}
@@ -44,7 +51,7 @@ export const GalleryFilter: React.FC<GalleryFilterProps> = ({
</span>
<div className="flex items-center gap-1">
<Button
variant={currentFilter === 'all' ? 'primary' : 'outline'}
variant={isActive('all') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('all')}
className="p-1 w-8 h-8 flex items-center justify-center"
@@ -53,7 +60,7 @@ export const GalleryFilter: React.FC<GalleryFilterProps> = ({
<svg viewBox="0 0 24 24" className="w-3.5 h-3.5 text-current"><path d="M3 3h8v8H3V3zm10 0h8v8h-8V3zM3 13h8v8H3v-8zm10 8v-8h8v8h-8z"/></svg>
</Button>
<Button
variant={currentFilter === 'liked' ? 'primary' : 'outline'}
variant={isActive('liked') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('liked')}
className="p-1 w-8 h-8 flex items-center justify-center"
@@ -62,7 +69,7 @@ export const GalleryFilter: React.FC<GalleryFilterProps> = ({
<Heart className="w-3.5 h-3.5" />
</Button>
<Button
variant={currentFilter === 'favorited' ? 'primary' : 'outline'}
variant={isActive('favorited') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('favorited')}
className="p-1 w-8 h-8 flex items-center justify-center"
@@ -71,7 +78,7 @@ export const GalleryFilter: React.FC<GalleryFilterProps> = ({
<Bookmark className="w-3.5 h-3.5" />
</Button>
<Button
variant={currentFilter === 'rated' ? 'primary' : 'outline'}
variant={isActive('rated') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('rated')}
className="p-1 w-8 h-8 flex items-center justify-center"
@@ -80,7 +87,7 @@ export const GalleryFilter: React.FC<GalleryFilterProps> = ({
<Star className="w-3.5 h-3.5" />
</Button>
<Button
variant={currentFilter === 'commented' ? 'primary' : 'outline'}
variant={isActive('commented') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('commented')}
className="p-1 w-8 h-8 flex items-center justify-center"
@@ -104,7 +111,7 @@ export const GalleryFilter: React.FC<GalleryFilterProps> = ({
</div>
<div className="flex flex-wrap gap-2">
<Button
variant={currentFilter === 'all' ? 'primary' : 'outline'}
variant={isActive('all') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('all')}
className="text-xs flex-1 min-w-[80px]"
@@ -113,7 +120,7 @@ export const GalleryFilter: React.FC<GalleryFilterProps> = ({
</Button>
<Button
variant={currentFilter === 'liked' ? 'primary' : 'outline'}
variant={isActive('liked') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('liked')}
className="text-xs flex-1 min-w-[80px] flex items-center justify-center gap-1"
@@ -123,7 +130,7 @@ export const GalleryFilter: React.FC<GalleryFilterProps> = ({
</Button>
<Button
variant={currentFilter === 'favorited' ? 'primary' : 'outline'}
variant={isActive('favorited') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('favorited')}
className="text-xs flex-1 min-w-[80px] flex items-center justify-center gap-1"
@@ -133,7 +140,7 @@ export const GalleryFilter: React.FC<GalleryFilterProps> = ({
</Button>
<Button
variant={currentFilter === 'rated' ? 'primary' : 'outline'}
variant={isActive('rated') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('rated')}
className="text-xs flex-1 min-w-[80px] flex items-center justify-center gap-1"
@@ -151,7 +158,7 @@ export const GalleryFilter: React.FC<GalleryFilterProps> = ({
</span>
<div className="flex gap-2">
<Button
variant={currentFilter === 'all' ? 'primary' : 'outline'}
variant={isActive('all') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('all')}
className="text-xs sm:text-sm"
@@ -160,7 +167,7 @@ export const GalleryFilter: React.FC<GalleryFilterProps> = ({
</Button>
<Button
variant={currentFilter === 'liked' ? 'primary' : 'outline'}
variant={isActive('liked') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('liked')}
className="text-xs sm:text-sm flex items-center gap-1"
@@ -175,7 +182,7 @@ export const GalleryFilter: React.FC<GalleryFilterProps> = ({
</Button>
<Button
variant={currentFilter === 'favorited' ? 'primary' : 'outline'}
variant={isActive('favorited') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('favorited')}
className="text-xs sm:text-sm flex items-center gap-1"
@@ -190,7 +197,7 @@ export const GalleryFilter: React.FC<GalleryFilterProps> = ({
</Button>
<Button
variant={currentFilter === 'rated' ? 'primary' : 'outline'}
variant={isActive('rated') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('rated')}
className="text-xs sm:text-sm flex items-center gap-1"
@@ -205,7 +212,7 @@ export const GalleryFilter: React.FC<GalleryFilterProps> = ({
</Button>
<Button
variant={currentFilter === 'commented' ? 'primary' : 'outline'}
variant={isActive('commented') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('commented')}
className="text-xs sm:text-sm flex items-center gap-1"
@@ -1,9 +1,9 @@
import React, { useEffect, useRef } from 'react';
import { X, Download, Filter, SortAsc, Search, Calendar, Type, HardDrive, Check, Star, Upload, Camera } from 'lucide-react';
import { X, Download, Filter, SortAsc, SortDesc, Search, Calendar, Type, HardDrive, Check, Star, Upload, Camera } from 'lucide-react';
import { Button } from '../common';
import { PhotoCategory } from '../../types';
import { useTranslation } from 'react-i18next';
import { GalleryFilter, type FilterType } from './GalleryFilter';
import { GalleryFilter, type FilterType, type FeedbackFilterType } from './GalleryFilter';
interface GallerySidebarProps {
isOpen: boolean;
@@ -15,6 +15,9 @@ interface GallerySidebarProps {
onSearchChange: (term: string) => void;
sortBy: 'date' | 'name' | 'size' | 'rating' | 'capture_date';
onSortChange: (sort: 'date' | 'name' | 'size' | 'rating' | 'capture_date') => void;
// Sort direction (#889)
sortDesc?: boolean;
onSortDescChange?: (desc: boolean) => void;
isSelectionMode: boolean;
onToggleSelectionMode: () => void;
selectedCount: number;
@@ -29,7 +32,8 @@ interface GallerySidebarProps {
allowUploads?: boolean;
onUploadClick?: () => void;
feedbackEnabled?: boolean;
filterType?: FilterType;
// Multi-select feedback filters (#889): empty array = "All".
activeFilters?: FeedbackFilterType[];
onFilterChange?: (filter: FilterType) => void;
likeCount?: number;
favoriteCount?: number;
@@ -49,6 +53,8 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
onSearchChange,
sortBy,
onSortChange,
sortDesc = true,
onSortDescChange,
isSelectionMode,
onToggleSelectionMode,
selectedCount,
@@ -63,7 +69,7 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
allowUploads,
onUploadClick,
feedbackEnabled = false,
filterType = 'all',
activeFilters = [],
onFilterChange,
likeCount = 0,
favoriteCount = 0,
@@ -223,11 +229,11 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
{feedbackEnabled && onFilterChange && (
<div className="gallery-sidebar-section gallery-sidebar-feedback p-4 border-b border-surface">
<GalleryFilter
currentFilter={filterType}
onFilterChange={(filter) => {
onFilterChange(filter);
if (isMobile) onClose();
}}
activeFilters={activeFilters}
// Unlike the single-select category/sort buttons, feedback
// filters are multi-select toggles (#889) — keep the mobile
// sidebar open so several can be combined in one visit.
onFilterChange={onFilterChange}
feedbackEnabled={feedbackEnabled}
likeCount={likeCount}
favoriteCount={favoriteCount}
@@ -374,6 +380,30 @@ export const GallerySidebar: React.FC<GallerySidebarProps> = ({
);
})}
</div>
{/* Sort direction (#889) */}
{onSortDescChange && (
<div className="flex items-center gap-2 mt-3">
<Button
variant={!sortDesc ? 'primary' : 'outline'}
size="sm"
leftIcon={<SortAsc className="w-4 h-4" />}
onClick={() => onSortDescChange(false)}
className="gallery-btn flex-1"
>
{t('gallery.sortAscending', 'Sort ascending')}
</Button>
<Button
variant={sortDesc ? 'primary' : 'outline'}
size="sm"
leftIcon={<SortDesc className="w-4 h-4" />}
onClick={() => onSortDescChange(true)}
className="gallery-btn flex-1"
>
{t('gallery.sortDescending', 'Sort descending')}
</Button>
</div>
)}
</div>
)}
</div>
+49 -40
View File
@@ -17,7 +17,7 @@ import { UserPhotoUpload } from './UserPhotoUpload';
import { GuestNamePromptModal } from './GuestNamePromptModal';
import { GuestRecoveryModal } from './GuestRecoveryModal';
import { GuestIdentityProvider } from '../../contexts/GuestIdentityContext';
import type { FilterType } from './GalleryFilter';
import type { FilterType, FeedbackFilterType } from './GalleryFilter';
import { analyticsService } from '../../services/analytics.service';
import { useDevToolsProtection } from '../../hooks/useDevToolsProtection';
import { api } from '../../config/api';
@@ -90,7 +90,18 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
useGalleryCustomCss(slug);
const [protectionLevel, setProtectionLevel] = useState<'basic' | 'standard' | 'enhanced' | 'maximum'>('standard');
const [filterType, setFilterType] = useState<FilterType>('all');
// Multi-select feedback filters (#889): OR-combined; empty = "All".
// Clicking a filter toggles it, clicking "All" clears the set.
const [activeFilters, setActiveFilters] = useState<FeedbackFilterType[]>([]);
const handleFilterChange = (filter: FilterType) => {
if (filter === 'all') {
setActiveFilters([]);
} else {
setActiveFilters(prev => prev.includes(filter)
? prev.filter(f => f !== filter)
: [...prev, filter]);
}
};
const [mediaFilter, setMediaFilter] = useState<'all' | 'photo' | 'video'>('all');
const [guestId, setGuestId] = useState<string>('');
const [staticHeroPhoto, setStaticHeroPhoto] = useState<Photo | null>(null);
@@ -258,7 +269,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
//
// Always-on in guest mode (not gated on the active filter) because
// the chip counts render whether or not a feedback filter is selected
// — gating on filterType would leave "Liked (0)" stale until the user
// — gating on activeFilters would leave "Liked (0)" stale until the user
// clicks the chip, which is the same UX cliff bug 1 was reporting.
const isGuestIdentityMode = feedbackSettings?.identity_mode === 'guest';
const { data: myFeedbackRows } = useQuery<Array<{
@@ -348,7 +359,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
const [defaultHeroPhoto, setDefaultHeroPhoto] = useState<Photo | null>(null);
useEffect(() => {
if (!defaultHeroPhoto && data?.photos && filterType === 'all') {
if (!defaultHeroPhoto && data?.photos && activeFilters.length === 0) {
let hero: Photo | null = null;
const heroId = data?.event?.hero_photo_id || null;
if (heroId) {
@@ -363,7 +374,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
setStaticHeroPhoto(hero);
}
}
}, [data?.photos, data?.event?.hero_photo_id, filterType, defaultHeroPhoto]);
}, [data?.photos, data?.event?.hero_photo_id, activeFilters, defaultHeroPhoto]);
// Switch hero photo when a category with its own hero image is selected
useEffect(() => {
@@ -504,36 +515,30 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
);
}
// Apply feedback filter. In guest identity mode the filter has to
// scope to the *current guest's* interactions (#538 bug 1) — the
// aggregate counts on each photo row are global across all guests,
// which gave an empty grid when the guest had liked photos that
// nobody else had touched. Falls back to the aggregate-count check
// in simple/non-guest mode where there's no per-person identity to
// scope by.
switch (filterType) {
case 'liked':
photos = isGuestIdentityMode
? photos.filter(photo => myFeedbackPhotoIds.liked.has(photo.id))
: photos.filter(photo => (photo.like_count || 0) > 0);
break;
case 'favorited':
photos = isGuestIdentityMode
? photos.filter(photo => myFeedbackPhotoIds.favorited.has(photo.id))
: photos.filter(photo => (photo.favorite_count || 0) > 0);
break;
case 'rated':
photos = isGuestIdentityMode
? photos.filter(photo => myFeedbackPhotoIds.rated.has(photo.id))
: photos.filter(photo => (photo.average_rating || 0) > 0 || (photo.total_ratings || 0) > 0);
break;
case 'commented':
photos = isGuestIdentityMode
? photos.filter(photo => myFeedbackPhotoIds.commented.has(photo.id))
: photos.filter(photo => (photo.comment_count || 0) > 0);
break;
default:
break;
// Apply feedback filters. Multi-select (#889): a photo matching ANY
// active filter passes (OR-combined); an empty set means no feedback
// filtering. In guest identity mode each filter has to scope to the
// *current guest's* interactions (#538 bug 1) — the aggregate counts
// on each photo row are global across all guests, which gave an empty
// grid when the guest had liked photos that nobody else had touched.
// Falls back to the aggregate-count check in simple/non-guest mode
// where there's no per-person identity to scope by.
if (activeFilters.length > 0) {
const matchers: Record<FeedbackFilterType, (photo: Photo) => boolean> = {
liked: (photo) => isGuestIdentityMode
? myFeedbackPhotoIds.liked.has(photo.id)
: (photo.like_count || 0) > 0,
favorited: (photo) => isGuestIdentityMode
? myFeedbackPhotoIds.favorited.has(photo.id)
: (photo.favorite_count || 0) > 0,
rated: (photo) => isGuestIdentityMode
? myFeedbackPhotoIds.rated.has(photo.id)
: (photo.average_rating || 0) > 0 || (photo.total_ratings || 0) > 0,
commented: (photo) => isGuestIdentityMode
? myFeedbackPhotoIds.commented.has(photo.id)
: (photo.comment_count || 0) > 0,
};
photos = photos.filter(photo => activeFilters.some(filter => matchers[filter](photo)));
}
// Apply sorting
@@ -576,7 +581,7 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
}
return photos;
}, [data?.photos, selectedCategoryId, searchTerm, sortBy, sortDesc, watermarkEnabled, slug, filterType, mediaFilter, isGuestIdentityMode, myFeedbackPhotoIds]);
}, [data?.photos, selectedCategoryId, searchTerm, sortBy, sortDesc, watermarkEnabled, slug, activeFilters, mediaFilter, isGuestIdentityMode, myFeedbackPhotoIds]);
// Counts shown in the filter chips ("Liked (N)", etc.). In guest
// mode these need to mirror the per-guest filter behaviour above —
@@ -862,6 +867,8 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
onSearchChange={setSearchTerm}
sortBy={sortBy}
onSortChange={setSortBy}
sortDesc={sortDesc}
onSortDescChange={setSortDesc}
isSelectionMode={isSelectionMode}
onToggleSelectionMode={() => setIsSelectionMode(!isSelectionMode)}
selectedCount={selectedPhotos.size}
@@ -876,8 +883,8 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
allowUploads={data?.event?.allow_user_uploads || event?.allow_user_uploads || false}
onUploadClick={() => setShowUploadModal(true)}
feedbackEnabled={feedbackEnabled}
filterType={filterType}
onFilterChange={setFilterType}
activeFilters={activeFilters}
onFilterChange={handleFilterChange}
mediaFilter={mediaFilter}
onMediaFilterChange={setMediaFilter}
showMediaFilter={showMediaFilter}
@@ -1015,11 +1022,13 @@ export const GalleryView: React.FC<GalleryViewProps> = ({ slug, event }) => {
onSearchChange={setSearchTerm}
sortBy={sortBy}
onSortChange={setSortBy}
sortDesc={sortDesc}
onSortDescChange={setSortDesc}
photoCount={filteredPhotos.length}
// Feedback filter props
feedbackEnabled={feedbackEnabled}
currentFilter={filterType}
onFilterChange={setFilterType}
activeFilters={activeFilters}
onFilterChange={handleFilterChange}
mediaFilter={mediaFilter}
onMediaFilterChange={setMediaFilter}
showMediaFilter={showMediaFilter}
@@ -1,8 +1,8 @@
import React, { useState } from 'react';
import { Search, SortAsc, Grid, Heart, Star, MessageSquare, Bookmark } from 'lucide-react';
import { Search, SortAsc, SortDesc, Grid, Heart, Star, MessageSquare, Bookmark } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { Button, Input } from '../common';
import type { FilterType } from './GalleryFilter';
import type { FilterType, FeedbackFilterType } from './GalleryFilter';
interface PhotoCategory {
id: number | string;
@@ -27,10 +27,15 @@ interface PhotoFilterBarProps {
onSearchChange: (term: string) => void;
sortBy: 'date' | 'name' | 'size' | 'rating' | 'capture_date';
onSortChange: (sort: 'date' | 'name' | 'size' | 'rating' | 'capture_date') => void;
// Sort direction (#889). Direction controls only render when the
// callback is provided (PreviewPage doesn't pass it).
sortDesc?: boolean;
onSortDescChange?: (desc: boolean) => void;
photoCount: number;
// Feedback filter props
// Feedback filter props. Multi-select (#889): empty array = "All";
// clicking a filter toggles it in the parent's set.
feedbackEnabled?: boolean;
currentFilter?: FilterType;
activeFilters?: FeedbackFilterType[];
onFilterChange?: (filter: FilterType) => void;
mediaFilter?: 'all' | 'photo' | 'video';
onMediaFilterChange?: (filter: 'all' | 'photo' | 'video') => void;
@@ -46,9 +51,11 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
onSearchChange,
sortBy,
onSortChange,
sortDesc = true,
onSortDescChange,
photoCount,
feedbackEnabled = false,
currentFilter = 'all',
activeFilters = [],
onFilterChange,
mediaFilter = 'all',
onMediaFilterChange,
@@ -56,6 +63,8 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
}) => {
const { t } = useTranslation();
const [showSortMenu, setShowSortMenu] = useState(false);
const isActive = (filter: FilterType) =>
filter === 'all' ? activeFilters.length === 0 : activeFilters.includes(filter as FeedbackFilterType);
return (
<div className="space-y-4">
{/* Search and Sort */}
@@ -77,7 +86,9 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
<Button
variant="outline"
size="md"
leftIcon={<SortAsc className="w-4 h-4" />}
// Direction-aware icon only when the parent controls direction —
// PreviewPage renders this bar without it and sorts its own way.
leftIcon={onSortDescChange && sortDesc ? <SortDesc className="w-4 h-4" /> : <SortAsc className="w-4 h-4" />}
onClick={() => setShowSortMenu(!showSortMenu)}
className="w-full md:w-auto text-sm md:text-base"
>
@@ -147,6 +158,36 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
>
{t('gallery.sortByRating', 'Sort by Rating')}
</button>
{/* Sort direction (#889) */}
{onSortDescChange && (
<div className="border-t border-surface mt-1 pt-1">
<button
onClick={() => {
onSortDescChange(false);
setShowSortMenu(false);
}}
className={`w-full text-left px-4 py-2 text-sm hover:bg-black/10 flex items-center gap-2 ${
!sortDesc ? 'bg-accent-dark text-white' : 'text-muted-theme'
}`}
>
<SortAsc className="w-4 h-4" />
{t('gallery.sortAscending', 'Sort ascending')}
</button>
<button
onClick={() => {
onSortDescChange(true);
setShowSortMenu(false);
}}
className={`w-full text-left px-4 py-2 text-sm hover:bg-black/10 flex items-center gap-2 ${
sortDesc ? 'bg-accent-dark text-white' : 'text-muted-theme'
}`}
>
<SortDesc className="w-4 h-4" />
{t('gallery.sortDescending', 'Sort descending')}
</button>
</div>
)}
</div>
)}
</div>
@@ -202,7 +243,7 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
</span>
<div className="flex items-center gap-1">
<Button
variant={currentFilter === 'all' ? 'primary' : 'outline'}
variant={isActive('all') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('all')}
className="p-1 w-8 h-8 flex items-center justify-center"
@@ -211,7 +252,7 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
<Grid className="w-3.5 h-3.5" />
</Button>
<Button
variant={currentFilter === 'liked' ? 'primary' : 'outline'}
variant={isActive('liked') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('liked')}
className="p-1 w-8 h-8 flex items-center justify-center"
@@ -220,7 +261,7 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
<Heart className="w-3.5 h-3.5" />
</Button>
<Button
variant={currentFilter === 'favorited' ? 'primary' : 'outline'}
variant={isActive('favorited') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('favorited')}
className="p-1 w-8 h-8 flex items-center justify-center"
@@ -229,7 +270,7 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
<Bookmark className="w-3.5 h-3.5" />
</Button>
<Button
variant={currentFilter === 'rated' ? 'primary' : 'outline'}
variant={isActive('rated') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('rated')}
className="p-1 w-8 h-8 flex items-center justify-center"
@@ -238,7 +279,7 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
<Star className="w-3.5 h-3.5" />
</Button>
<Button
variant={currentFilter === 'commented' ? 'primary' : 'outline'}
variant={isActive('commented') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('commented')}
className="p-1 w-8 h-8 flex items-center justify-center"
@@ -301,7 +342,7 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
</span>
<div className="flex items-center gap-1">
<Button
variant={currentFilter === 'all' ? 'primary' : 'outline'}
variant={isActive('all') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('all')}
className="p-1 w-8 h-8 flex items-center justify-center"
@@ -310,7 +351,7 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
<Grid className="w-3.5 h-3.5" />
</Button>
<Button
variant={currentFilter === 'liked' ? 'primary' : 'outline'}
variant={isActive('liked') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('liked')}
className="p-1 w-8 h-8 flex items-center justify-center"
@@ -319,7 +360,7 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
<Heart className="w-3.5 h-3.5" />
</Button>
<Button
variant={currentFilter === 'favorited' ? 'primary' : 'outline'}
variant={isActive('favorited') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('favorited')}
className="p-1 w-8 h-8 flex items-center justify-center"
@@ -328,7 +369,7 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
<Bookmark className="w-3.5 h-3.5" />
</Button>
<Button
variant={currentFilter === 'rated' ? 'primary' : 'outline'}
variant={isActive('rated') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('rated')}
className="p-1 w-8 h-8 flex items-center justify-center"
@@ -337,7 +378,7 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
<Star className="w-3.5 h-3.5" />
</Button>
<Button
variant={currentFilter === 'commented' ? 'primary' : 'outline'}
variant={isActive('commented') ? 'primary' : 'outline'}
size="sm"
onClick={() => onFilterChange('commented')}
className="p-1 w-8 h-8 flex items-center justify-center"
@@ -41,7 +41,7 @@ describe('PhotoFilterBar feedback chips (#802)', () => {
<PhotoFilterBar
{...baseProps}
feedbackEnabled
currentFilter="all"
activeFilters={[]}
onFilterChange={vi.fn()}
/>
);
@@ -58,7 +58,7 @@ describe('PhotoFilterBar feedback chips (#802)', () => {
categories={[{ id: 1, name: 'Ceremony', slug: 'ceremony' }]}
photos={[{ id: 1, category_id: 1 } as never]}
feedbackEnabled
currentFilter="all"
activeFilters={[]}
onFilterChange={vi.fn()}
/>
);