Merge pull request #804 from PicPeak/fix/gallery-feedback-filter-chips
fix(gallery): show feedback filter chips on desktop for galleries without categories
This commit is contained in:
@@ -154,39 +154,45 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
|
|||||||
|
|
||||||
{/* Category and Feedback Filters */}
|
{/* Category and Feedback Filters */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* Categories Row */}
|
{/* Categories + desktop feedback row. Rendered whenever EITHER part
|
||||||
{categories && categories.length > 0 && (
|
has content: the desktop feedback chips must not depend on the
|
||||||
|
(optional) categories existing, or category-less galleries show
|
||||||
|
no feedback filter at all on desktop (#802 — the lg:hidden
|
||||||
|
fallback block below only covers mobile/tablet). */}
|
||||||
|
{((categories && categories.length > 0) || (feedbackEnabled && !!onFilterChange)) && (
|
||||||
<div className="flex items-start lg:items-center justify-between flex-col lg:flex-row gap-3">
|
<div className="flex items-start lg:items-center justify-between flex-col lg:flex-row gap-3">
|
||||||
{/* Categories: keep in a horizontal scroll container */}
|
{/* Categories: keep in a horizontal scroll container */}
|
||||||
<div className="w-full overflow-x-auto pb-2 lg:pb-0">
|
{categories && categories.length > 0 && (
|
||||||
<div className="flex items-center gap-2 min-w-max">
|
<div className="w-full overflow-x-auto pb-2 lg:pb-0">
|
||||||
<Button
|
<div className="flex items-center gap-2 min-w-max">
|
||||||
variant={selectedCategoryId === null ? 'primary' : 'outline'}
|
<Button
|
||||||
size="sm"
|
variant={selectedCategoryId === null ? 'primary' : 'outline'}
|
||||||
onClick={() => onCategoryChange(null)}
|
size="sm"
|
||||||
leftIcon={<Grid className="w-3 h-3 md:w-4 md:h-4" />}
|
onClick={() => onCategoryChange(null)}
|
||||||
className="text-xs md:text-sm whitespace-nowrap flex-shrink-0"
|
leftIcon={<Grid className="w-3 h-3 md:w-4 md:h-4" />}
|
||||||
>
|
className="text-xs md:text-sm whitespace-nowrap flex-shrink-0"
|
||||||
{showMediaFilter ? t('gallery.allMedia', 'All media') : t('gallery.allPhotos')} ({photos.length})
|
>
|
||||||
</Button>
|
{showMediaFilter ? t('gallery.allMedia', 'All media') : t('gallery.allPhotos')} ({photos.length})
|
||||||
{categories.map((category) => {
|
</Button>
|
||||||
const categoryPhotoCount = photos.filter(p => p.category_id === category.id).length;
|
{categories.map((category) => {
|
||||||
if (categoryPhotoCount === 0) return null;
|
const categoryPhotoCount = photos.filter(p => p.category_id === category.id).length;
|
||||||
|
if (categoryPhotoCount === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={category.id}
|
key={category.id}
|
||||||
variant={selectedCategoryId === category.id ? 'primary' : 'outline'}
|
variant={selectedCategoryId === category.id ? 'primary' : 'outline'}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => onCategoryChange(category.id)}
|
onClick={() => onCategoryChange(category.id)}
|
||||||
className="text-xs md:text-sm whitespace-nowrap flex-shrink-0"
|
className="text-xs md:text-sm whitespace-nowrap flex-shrink-0"
|
||||||
>
|
>
|
||||||
{category.name} ({categoryPhotoCount})
|
{category.name} ({categoryPhotoCount})
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Desktop: compact horizontal feedback filter with headline (icons only) */}
|
{/* Desktop: compact horizontal feedback filter with headline (icons only) */}
|
||||||
{feedbackEnabled && onFilterChange && (
|
{feedbackEnabled && onFilterChange && (
|
||||||
@@ -244,7 +250,10 @@ export const PhotoFilterBar: React.FC<PhotoFilterBarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-xs md:text-sm text-muted-theme flex-shrink-0 ml-auto">
|
{/* Without categories this row only carries desktop content (the
|
||||||
|
chips are lg-only; mobile has its own block below), so hide
|
||||||
|
the count below lg to keep the mobile layout unchanged. */}
|
||||||
|
<p className={`text-xs md:text-sm text-muted-theme flex-shrink-0 ml-auto ${categories && categories.length > 0 ? '' : 'hidden lg:block'}`}>
|
||||||
{photoCount} {t('common.media', 'media')}
|
{photoCount} {t('common.media', 'media')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* Regression coverage for #802: the desktop feedback-filter chips
|
||||||
|
* (All / Likes / Saved / Rated / Commented) were nested inside the
|
||||||
|
* categories row, so a gallery WITHOUT photo categories (the default)
|
||||||
|
* rendered no feedback filter at all on desktop — the standalone
|
||||||
|
* fallback block is lg:hidden (mobile/tablet only). These tests pin
|
||||||
|
* that both chip groups exist in the DOM regardless of categories.
|
||||||
|
*/
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { PhotoFilterBar } from '../PhotoFilterBar';
|
||||||
|
|
||||||
|
vi.mock('react-i18next', async () => {
|
||||||
|
const actual = await vi.importActual<typeof import('react-i18next')>('react-i18next');
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (_key: string, fallback?: unknown) =>
|
||||||
|
typeof fallback === 'string' ? fallback : _key,
|
||||||
|
i18n: { language: 'en' }
|
||||||
|
})
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const baseProps = {
|
||||||
|
categories: [] as Array<{ id: number; name: string; slug: string }>,
|
||||||
|
photos: [] as never[],
|
||||||
|
selectedCategoryId: null,
|
||||||
|
onCategoryChange: vi.fn(),
|
||||||
|
searchTerm: '',
|
||||||
|
onSearchChange: vi.fn(),
|
||||||
|
sortBy: 'date' as const,
|
||||||
|
onSortChange: vi.fn(),
|
||||||
|
photoCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('PhotoFilterBar feedback chips (#802)', () => {
|
||||||
|
it('renders both chip groups (desktop lg:flex + mobile lg:hidden) with NO categories', () => {
|
||||||
|
render(
|
||||||
|
<PhotoFilterBar
|
||||||
|
{...baseProps}
|
||||||
|
feedbackEnabled
|
||||||
|
currentFilter="all"
|
||||||
|
onFilterChange={vi.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
// Two groups: the desktop row variant and the mobile fallback. Before
|
||||||
|
// the fix, only the mobile one rendered when categories were empty,
|
||||||
|
// leaving desktop with no feedback filter at all.
|
||||||
|
expect(screen.getAllByText('Feedback Filter')).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('still renders both chip groups when categories exist', () => {
|
||||||
|
render(
|
||||||
|
<PhotoFilterBar
|
||||||
|
{...baseProps}
|
||||||
|
categories={[{ id: 1, name: 'Ceremony', slug: 'ceremony' }]}
|
||||||
|
photos={[{ id: 1, category_id: 1 } as never]}
|
||||||
|
feedbackEnabled
|
||||||
|
currentFilter="all"
|
||||||
|
onFilterChange={vi.fn()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getAllByText('Feedback Filter')).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders no chips when feedback is disabled', () => {
|
||||||
|
render(<PhotoFilterBar {...baseProps} />);
|
||||||
|
expect(screen.queryByText('Feedback Filter')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user