From 2dc871c50fa50ec7685d6e72519138aafee93619 Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Thu, 4 Sep 2025 17:53:59 +0200 Subject: [PATCH] Implement progressive enhancement for universal search with context-aware filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/lib/components/IconLayout.svelte | 2 +- src/lib/components/Search.svelte | 223 +++++++++++------- src/lib/components/SeasonLayout.svelte | 2 +- src/routes/api/rezepte/search/+server.ts | 74 ++++++ .../rezepte/category/[category]/+page.svelte | 2 +- src/routes/rezepte/favorites/+page.svelte | 2 +- src/routes/rezepte/search/+page.server.ts | 50 ++++ src/routes/rezepte/search/+page.svelte | 75 ++++++ src/routes/rezepte/tag/[tag]/+page.svelte | 2 +- 9 files changed, 348 insertions(+), 84 deletions(-) create mode 100644 src/routes/api/rezepte/search/+server.ts create mode 100644 src/routes/rezepte/search/+page.server.ts create mode 100644 src/routes/rezepte/search/+page.svelte diff --git a/src/lib/components/IconLayout.svelte b/src/lib/components/IconLayout.svelte index bdadb69..48d1755 100644 --- a/src/lib/components/IconLayout.svelte +++ b/src/lib/components/IconLayout.svelte @@ -72,7 +72,7 @@ {/each}
- +
diff --git a/src/lib/components/Search.svelte b/src/lib/components/Search.svelte index abc11ec..02424a0 100644 --- a/src/lib/components/Search.svelte +++ b/src/lib/components/Search.svelte @@ -1,81 +1,128 @@ - + diff --git a/src/lib/components/SeasonLayout.svelte b/src/lib/components/SeasonLayout.svelte index fdb88b9..cb8b278 100644 --- a/src/lib/components/SeasonLayout.svelte +++ b/src/lib/components/SeasonLayout.svelte @@ -41,7 +41,7 @@ a.month:hover, {/each}
- +
diff --git a/src/routes/api/rezepte/search/+server.ts b/src/routes/api/rezepte/search/+server.ts new file mode 100644 index 0000000..e2a1391 --- /dev/null +++ b/src/routes/api/rezepte/search/+server.ts @@ -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 }); + } +}; \ No newline at end of file diff --git a/src/routes/rezepte/category/[category]/+page.svelte b/src/routes/rezepte/category/[category]/+page.svelte index 53dcb98..d982098 100644 --- a/src/routes/rezepte/category/[category]/+page.svelte +++ b/src/routes/rezepte/category/[category]/+page.svelte @@ -14,7 +14,7 @@ }

Rezepte in Kategorie {data.category}:

- +
{#each rand_array(data.recipes) as recipe} diff --git a/src/routes/rezepte/favorites/+page.svelte b/src/routes/rezepte/favorites/+page.svelte index 7873fbf..8906894 100644 --- a/src/routes/rezepte/favorites/+page.svelte +++ b/src/routes/rezepte/favorites/+page.svelte @@ -40,7 +40,7 @@ h1{ {/if}

- + {#if data.error}

Fehler beim Laden der Favoriten: {data.error}

diff --git a/src/routes/rezepte/search/+page.server.ts b/src/routes/rezepte/search/+page.server.ts new file mode 100644 index 0000000..a50a2a5 --- /dev/null +++ b/src/routes/rezepte/search/+page.server.ts @@ -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 + } + }; + } +}; \ No newline at end of file diff --git a/src/routes/rezepte/search/+page.svelte b/src/routes/rezepte/search/+page.svelte new file mode 100644 index 0000000..7e6bbab --- /dev/null +++ b/src/routes/rezepte/search/+page.svelte @@ -0,0 +1,75 @@ + + + + + + Suchergebnisse{data.query ? ` für "${data.query}"` : ''} - Bocken Rezepte + + + +

Suchergebnisse

+ +{#if data.filters.category || data.filters.tag || data.filters.icon || data.filters.season || data.filters.favoritesOnly} +
+ 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} +
+{/if} + + + +{#if data.error} +
+

Fehler bei der Suche: {data.error}

+
+{:else if data.query} +
+

{data.results.length} Ergebnisse für "{data.query}"

+
+{/if} + +{#if data.results.length > 0} + + {#each data.results as recipe} + + {/each} + +{:else if data.query && !data.error} +
+

Keine Rezepte gefunden.

+

Versuche es mit anderen Suchbegriffen.

+
+{/if} \ No newline at end of file diff --git a/src/routes/rezepte/tag/[tag]/+page.svelte b/src/routes/rezepte/tag/[tag]/+page.svelte index 6993f55..cc35626 100644 --- a/src/routes/rezepte/tag/[tag]/+page.svelte +++ b/src/routes/rezepte/tag/[tag]/+page.svelte @@ -14,7 +14,7 @@ }

Rezepte mit Stichwort {data.tag}:

- +
{#each rand_array(data.recipes) as recipe}