Implement progressive enhancement for universal search with context-aware filtering
Some checks failed
CI / update (push) Failing after 5s
Some checks failed
CI / update (push) Failing after 5s
Add comprehensive search solution that works across all recipe pages with proper fallbacks. Features include universal API endpoint, context-aware filtering (category/tag/icon/season/favorites), and progressive enhancement with form submission fallback for no-JS users. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
74
src/routes/api/rezepte/search/+server.ts
Normal file
74
src/routes/api/rezepte/search/+server.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import type { BriefRecipeType } from '../../../../types/types';
|
||||
import { Recipe } from '../../../../models/Recipe';
|
||||
import { dbConnect, dbDisconnect } 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');
|
||||
const icon = url.searchParams.get('icon');
|
||||
const season = url.searchParams.get('season');
|
||||
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] };
|
||||
}
|
||||
|
||||
if (icon) {
|
||||
dbQuery.icon = icon;
|
||||
}
|
||||
|
||||
if (season) {
|
||||
const seasonNum = parseInt(season);
|
||||
if (!isNaN(seasonNum)) {
|
||||
dbQuery.season = { $in: [seasonNum] };
|
||||
}
|
||||
}
|
||||
|
||||
// Get all recipes matching base filters
|
||||
let recipes = await Recipe.find(dbQuery, 'name short_name tags category icon description season dateModified').lean() as BriefRecipeType[];
|
||||
|
||||
// Handle favorites filter
|
||||
if (favoritesOnly && locals.session?.user) {
|
||||
const User = (await import('../../../../models/User')).User;
|
||||
const user = await User.findById(locals.session.user.id);
|
||||
if (user && user.favoriteRecipes) {
|
||||
const favoriteShortNames = user.favoriteRecipes;
|
||||
recipes = recipes.filter(recipe => favoriteShortNames.includes(recipe.short_name));
|
||||
} else {
|
||||
recipes = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Apply text search if query provided
|
||||
if (query) {
|
||||
const searchTerms = query.normalize('NFD').replace(/\p{Diacritic}/gu, "").split(" ");
|
||||
|
||||
recipes = recipes.filter(recipe => {
|
||||
const searchString = `${recipe.name} ${recipe.description || ''} ${recipe.tags?.join(' ') || ''}`.toLowerCase()
|
||||
.normalize('NFD').replace(/\p{Diacritic}/gu, "").replace(/­|/g, '');
|
||||
|
||||
return searchTerms.every(term => searchString.includes(term));
|
||||
});
|
||||
}
|
||||
|
||||
await dbDisconnect();
|
||||
return json(JSON.parse(JSON.stringify(recipes)));
|
||||
|
||||
} catch (error) {
|
||||
await dbDisconnect();
|
||||
return json({ error: 'Search failed' }, { status: 500 });
|
||||
}
|
||||
};
|
@@ -14,7 +14,7 @@
|
||||
}
|
||||
</style>
|
||||
<h1>Rezepte in Kategorie <q>{data.category}</q>:</h1>
|
||||
<Search></Search>
|
||||
<Search category={data.category}></Search>
|
||||
<section>
|
||||
<Recipes>
|
||||
{#each rand_array(data.recipes) as recipe}
|
||||
|
@@ -40,7 +40,7 @@ h1{
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
<Search></Search>
|
||||
<Search favoritesOnly={true}></Search>
|
||||
|
||||
{#if data.error}
|
||||
<p class="empty-state">Fehler beim Laden der Favoriten: {data.error}</p>
|
||||
|
50
src/routes/rezepte/search/+page.server.ts
Normal file
50
src/routes/rezepte/search/+page.server.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ url, fetch }) => {
|
||||
const query = url.searchParams.get('q') || '';
|
||||
const category = url.searchParams.get('category');
|
||||
const tag = url.searchParams.get('tag');
|
||||
const icon = url.searchParams.get('icon');
|
||||
const season = url.searchParams.get('season');
|
||||
const favoritesOnly = url.searchParams.get('favorites') === 'true';
|
||||
|
||||
// Build API URL with filters
|
||||
const apiUrl = new URL('/api/rezepte/search', url.origin);
|
||||
if (query) apiUrl.searchParams.set('q', query);
|
||||
if (category) apiUrl.searchParams.set('category', category);
|
||||
if (tag) apiUrl.searchParams.set('tag', tag);
|
||||
if (icon) apiUrl.searchParams.set('icon', icon);
|
||||
if (season) apiUrl.searchParams.set('season', season);
|
||||
if (favoritesOnly) apiUrl.searchParams.set('favorites', 'true');
|
||||
|
||||
try {
|
||||
const response = await fetch(apiUrl.toString());
|
||||
const results = await response.json();
|
||||
|
||||
return {
|
||||
query,
|
||||
results: response.ok ? results : [],
|
||||
error: response.ok ? null : results.error || 'Search failed',
|
||||
filters: {
|
||||
category,
|
||||
tag,
|
||||
icon,
|
||||
season,
|
||||
favoritesOnly
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
query,
|
||||
results: [],
|
||||
error: 'Search failed',
|
||||
filters: {
|
||||
category,
|
||||
tag,
|
||||
icon,
|
||||
season,
|
||||
favoritesOnly
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
75
src/routes/rezepte/search/+page.svelte
Normal file
75
src/routes/rezepte/search/+page.svelte
Normal file
@@ -0,0 +1,75 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
import Recipes from '$lib/components/Recipes.svelte';
|
||||
import Search from '$lib/components/Search.svelte';
|
||||
import Card from '$lib/components/Card.svelte';
|
||||
export let data: PageData;
|
||||
export let current_month = new Date().getMonth() + 1;
|
||||
</script>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 3em;
|
||||
}
|
||||
.search-info {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
color: var(--nord3);
|
||||
}
|
||||
.filter-info {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9em;
|
||||
color: var(--nord2);
|
||||
}
|
||||
</style>
|
||||
|
||||
<svelte:head>
|
||||
<title>Suchergebnisse{data.query ? ` für "${data.query}"` : ''} - Bocken Rezepte</title>
|
||||
<meta name="description" content="Suchergebnisse in den Bockenschen Rezepten." />
|
||||
</svelte:head>
|
||||
|
||||
<h1>Suchergebnisse</h1>
|
||||
|
||||
{#if data.filters.category || data.filters.tag || data.filters.icon || data.filters.season || data.filters.favoritesOnly}
|
||||
<div class="filter-info">
|
||||
Gefiltert nach:
|
||||
{#if data.filters.category}Kategorie "{data.filters.category}"{/if}
|
||||
{#if data.filters.tag}Stichwort "{data.filters.tag}"{/if}
|
||||
{#if data.filters.icon}Icon "{data.filters.icon}"{/if}
|
||||
{#if data.filters.season}Saison "{data.filters.season}"{/if}
|
||||
{#if data.filters.favoritesOnly}Nur Favoriten{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Search
|
||||
category={data.filters.category}
|
||||
tag={data.filters.tag}
|
||||
icon={data.filters.icon}
|
||||
season={data.filters.season}
|
||||
favoritesOnly={data.filters.favoritesOnly}
|
||||
/>
|
||||
|
||||
{#if data.error}
|
||||
<div class="search-info">
|
||||
<p>Fehler bei der Suche: {data.error}</p>
|
||||
</div>
|
||||
{:else if data.query}
|
||||
<div class="search-info">
|
||||
<p>{data.results.length} Ergebnisse für "{data.query}"</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if data.results.length > 0}
|
||||
<Recipes>
|
||||
{#each data.results as recipe}
|
||||
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={true}></Card>
|
||||
{/each}
|
||||
</Recipes>
|
||||
{:else if data.query && !data.error}
|
||||
<div class="search-info">
|
||||
<p>Keine Rezepte gefunden.</p>
|
||||
<p>Versuche es mit anderen Suchbegriffen.</p>
|
||||
</div>
|
||||
{/if}
|
@@ -14,7 +14,7 @@
|
||||
}
|
||||
</style>
|
||||
<h1>Rezepte mit Stichwort <q>{data.tag}</q>:</h1>
|
||||
<Search></Search>
|
||||
<Search tag={data.tag}></Search>
|
||||
<section>
|
||||
<Recipes>
|
||||
{#each rand_array(data.recipes) as recipe}
|
||||
|
Reference in New Issue
Block a user