feat: add comprehensive filter UI with chip-based dropdowns
Add advanced filtering with category, tags (multi-select), icon, season, and favorites filters. All filters use consistent chip-based dropdown UI with type-to-search functionality. New Components: - TagChip.svelte: Reusable chip component with selected/removable states - CategoryFilter.svelte: Single-select category with chip dropdown - TagFilter.svelte: Multi-select tags with AND logic and chip dropdown - IconFilter.svelte: Single-select emoji icon with chip dropdown - SeasonFilter.svelte: Multi-select months with chip dropdown - FavoritesFilter.svelte: Toggle for favorites-only filtering - FilterPanel.svelte: Container with responsive layout and mobile toggle Search Component: - Integrated FilterPanel with all filter types - Added applyNonTextFilters() for category/tags/icon/season/favorites - Implemented favorites filter logic (recipe.isFavorite check) - Made tags/icons reload reactively when language changes with $effect - Updated buildSearchUrl() for comma-separated array parameters - Passed categories and isLoggedIn props to enable all filters Server API: - Both /api/rezepte/search and /api/recipes/search support: - Multi-tag AND logic using MongoDB $all operator - Multi-season filtering using MongoDB $in operator - Backwards compatible with single tag/season parameters - Updated search page server load to parse tag/season arrays UI/UX: - Filters display inline on wide screens with 2rem gap - Mobile: collapsible with subtle toggle button and slide-down animation - Chip-based dropdowns appear on focus with filtering as you type - Selected items display as removable chips below inputs (no background) - Centered labels on desktop, left-aligned on mobile - Reduced vertical spacing on mobile (0.3rem gap) - Max-width constraints: 500px for filters, 600px for panel on mobile - Consistent naming: "Tags" and "Icon" instead of German translations
This commit is contained in:
@@ -8,9 +8,23 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
||||
|
||||
const query = url.searchParams.get('q')?.toLowerCase().trim() || '';
|
||||
const category = url.searchParams.get('category');
|
||||
const tag = url.searchParams.get('tag');
|
||||
|
||||
// Support both single tag (backwards compat) and multiple tags
|
||||
const singleTag = url.searchParams.get('tag');
|
||||
const multipleTags = url.searchParams.get('tags');
|
||||
const tags = multipleTags
|
||||
? multipleTags.split(',').map(t => t.trim()).filter(Boolean)
|
||||
: (singleTag ? [singleTag] : []);
|
||||
|
||||
const icon = url.searchParams.get('icon');
|
||||
const season = url.searchParams.get('season');
|
||||
|
||||
// Support both single season (backwards compat) and multiple seasons
|
||||
const singleSeason = url.searchParams.get('season');
|
||||
const multipleSeasons = url.searchParams.get('seasons');
|
||||
const seasons = multipleSeasons
|
||||
? multipleSeasons.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n))
|
||||
: (singleSeason ? [parseInt(singleSeason)].filter(n => !isNaN(n)) : []);
|
||||
|
||||
const favoritesOnly = url.searchParams.get('favorites') === 'true';
|
||||
|
||||
try {
|
||||
@@ -24,19 +38,18 @@ export const GET: RequestHandler = async ({ url, locals }) => {
|
||||
dbQuery['translations.en.category'] = category;
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
dbQuery['translations.en.tags'] = { $in: [tag] };
|
||||
// Multi-tag AND logic: recipe must have ALL selected tags
|
||||
if (tags.length > 0) {
|
||||
dbQuery['translations.en.tags'] = { $all: tags };
|
||||
}
|
||||
|
||||
if (icon) {
|
||||
dbQuery.icon = icon; // Icon is the same for both languages
|
||||
}
|
||||
|
||||
if (season) {
|
||||
const seasonNum = parseInt(season);
|
||||
if (!isNaN(seasonNum)) {
|
||||
dbQuery.season = { $in: [seasonNum] }; // Season is the same for both languages
|
||||
}
|
||||
// Multi-season OR logic: recipe in any selected season
|
||||
if (seasons.length > 0) {
|
||||
dbQuery.season = { $in: seasons }; // Season is the same for both languages
|
||||
}
|
||||
|
||||
// Get all recipes matching base filters
|
||||
|
||||
@@ -5,36 +5,49 @@ import { dbConnect } from '../../../../utils/db';
|
||||
|
||||
export const GET: RequestHandler = async ({ url, locals }) => {
|
||||
await dbConnect();
|
||||
|
||||
|
||||
const query = url.searchParams.get('q')?.toLowerCase().trim() || '';
|
||||
const category = url.searchParams.get('category');
|
||||
const tag = url.searchParams.get('tag');
|
||||
|
||||
// Support both single tag (backwards compat) and multiple tags
|
||||
const singleTag = url.searchParams.get('tag');
|
||||
const multipleTags = url.searchParams.get('tags');
|
||||
const tags = multipleTags
|
||||
? multipleTags.split(',').map(t => t.trim()).filter(Boolean)
|
||||
: (singleTag ? [singleTag] : []);
|
||||
|
||||
const icon = url.searchParams.get('icon');
|
||||
const season = url.searchParams.get('season');
|
||||
|
||||
// Support both single season (backwards compat) and multiple seasons
|
||||
const singleSeason = url.searchParams.get('season');
|
||||
const multipleSeasons = url.searchParams.get('seasons');
|
||||
const seasons = multipleSeasons
|
||||
? multipleSeasons.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n))
|
||||
: (singleSeason ? [parseInt(singleSeason)].filter(n => !isNaN(n)) : []);
|
||||
|
||||
const favoritesOnly = url.searchParams.get('favorites') === 'true';
|
||||
|
||||
|
||||
try {
|
||||
// Build base query
|
||||
let dbQuery: any = {};
|
||||
|
||||
|
||||
// Apply filters based on context
|
||||
if (category) {
|
||||
dbQuery.category = category;
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
dbQuery.tags = { $in: [tag] };
|
||||
|
||||
// Multi-tag AND logic: recipe must have ALL selected tags
|
||||
if (tags.length > 0) {
|
||||
dbQuery.tags = { $all: tags };
|
||||
}
|
||||
|
||||
|
||||
if (icon) {
|
||||
dbQuery.icon = icon;
|
||||
}
|
||||
|
||||
if (season) {
|
||||
const seasonNum = parseInt(season);
|
||||
if (!isNaN(seasonNum)) {
|
||||
dbQuery.season = { $in: [seasonNum] };
|
||||
}
|
||||
|
||||
// Multi-season OR logic: recipe in any selected season
|
||||
if (seasons.length > 0) {
|
||||
dbQuery.season = { $in: seasons };
|
||||
}
|
||||
|
||||
// Get all recipes matching base filters
|
||||
|
||||
Reference in New Issue
Block a user