feat: enable live search on all recipe pages
All checks were successful
CI / update (push) Successful in 1m9s
All checks were successful
CI / update (push) Successful in 1m9s
Previously, live client-side search only worked on the main /rezepte and /recipes pages. All other pages (category, tag, favorites, search results, icon, and season pages) fell back to server-side search with form submission. Now all recipe pages support live filtering as users type, providing consistent UX across the site.
This commit is contained in:
@@ -6,6 +6,8 @@
|
|||||||
export let active_icon
|
export let active_icon
|
||||||
export let routePrefix = '/rezepte'
|
export let routePrefix = '/rezepte'
|
||||||
export let lang = 'de'
|
export let lang = 'de'
|
||||||
|
export let recipes = []
|
||||||
|
export let onSearchResults = (ids, categories) => {}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -74,7 +76,7 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<section>
|
<section>
|
||||||
<Search icon={active_icon} {lang}></Search>
|
<Search icon={active_icon} {lang} {recipes} {onSearchResults}></Search>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<slot name=recipes></slot>
|
<slot name=recipes></slot>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
export let active_index;
|
export let active_index;
|
||||||
export let routePrefix = '/rezepte';
|
export let routePrefix = '/rezepte';
|
||||||
export let lang = 'de';
|
export let lang = 'de';
|
||||||
|
export let recipes = []
|
||||||
|
export let onSearchResults = (ids, categories) => {}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@@ -43,7 +45,7 @@ a.month:hover,
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<section>
|
<section>
|
||||||
<Search season={active_index + 1} {lang}></Search>
|
<Search season={active_index + 1} {lang} {recipes} {onSearchResults}></Search>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<slot name=recipes></slot>
|
<slot name=recipes></slot>
|
||||||
|
|||||||
@@ -9,6 +9,24 @@
|
|||||||
|
|
||||||
const isEnglish = $derived(data.lang === 'en');
|
const isEnglish = $derived(data.lang === 'en');
|
||||||
const label = $derived(isEnglish ? 'Recipes in Category' : 'Rezepte in Kategorie');
|
const label = $derived(isEnglish ? 'Recipes in Category' : 'Rezepte in Kategorie');
|
||||||
|
|
||||||
|
// Search state
|
||||||
|
let matchedRecipeIds = $state(new Set());
|
||||||
|
let hasActiveSearch = $state(false);
|
||||||
|
|
||||||
|
// Handle search results from Search component
|
||||||
|
function handleSearchResults(ids, categories) {
|
||||||
|
matchedRecipeIds = ids;
|
||||||
|
hasActiveSearch = ids.size < data.recipes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter recipes based on search
|
||||||
|
const filteredRecipes = $derived.by(() => {
|
||||||
|
if (!hasActiveSearch) {
|
||||||
|
return data.recipes;
|
||||||
|
}
|
||||||
|
return data.recipes.filter(r => matchedRecipeIds.has(r._id));
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
h1 {
|
h1 {
|
||||||
@@ -17,10 +35,10 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<h1>{label} <q>{data.category}</q>:</h1>
|
<h1>{label} <q>{data.category}</q>:</h1>
|
||||||
<Search category={data.category} lang={data.lang}></Search>
|
<Search category={data.category} lang={data.lang} recipes={data.recipes} onSearchResults={handleSearchResults}></Search>
|
||||||
<section>
|
<section>
|
||||||
<Recipes>
|
<Recipes>
|
||||||
{#each rand_array(data.recipes) as recipe}
|
{#each rand_array(filteredRecipes) as recipe}
|
||||||
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
|
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
|
||||||
{/each}
|
{/each}
|
||||||
</Recipes>
|
</Recipes>
|
||||||
|
|||||||
@@ -27,6 +27,24 @@
|
|||||||
: 'Besuche ein Rezept und klicke auf das Herz-Symbol, um es zu deinen Favoriten hinzuzufügen.',
|
: 'Besuche ein Rezept und klicke auf das Herz-Symbol, um es zu deinen Favoriten hinzuzufügen.',
|
||||||
recipesLink: isEnglish ? 'recipe' : 'Rezept'
|
recipesLink: isEnglish ? 'recipe' : 'Rezept'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Search state
|
||||||
|
let matchedRecipeIds = $state(new Set());
|
||||||
|
let hasActiveSearch = $state(false);
|
||||||
|
|
||||||
|
// Handle search results from Search component
|
||||||
|
function handleSearchResults(ids, categories) {
|
||||||
|
matchedRecipeIds = ids;
|
||||||
|
hasActiveSearch = ids.size < data.favorites.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter recipes based on search
|
||||||
|
const filteredFavorites = $derived.by(() => {
|
||||||
|
if (!hasActiveSearch) {
|
||||||
|
return data.favorites;
|
||||||
|
}
|
||||||
|
return data.favorites.filter(r => matchedRecipeIds.has(r._id));
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -61,16 +79,20 @@ h1{
|
|||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Search favoritesOnly={true} lang={data.lang}></Search>
|
<Search favoritesOnly={true} lang={data.lang} recipes={data.favorites} onSearchResults={handleSearchResults}></Search>
|
||||||
|
|
||||||
{#if data.error}
|
{#if data.error}
|
||||||
<p class="empty-state">{labels.errorLoading} {data.error}</p>
|
<p class="empty-state">{labels.errorLoading} {data.error}</p>
|
||||||
{:else if data.favorites.length > 0}
|
{:else if filteredFavorites.length > 0}
|
||||||
<Recipes>
|
<Recipes>
|
||||||
{#each data.favorites as recipe}
|
{#each filteredFavorites as recipe}
|
||||||
<Card {recipe} {current_month} isFavorite={true} showFavoriteIndicator={true} routePrefix="/{data.recipeLang}"></Card>
|
<Card {recipe} {current_month} isFavorite={true} showFavoriteIndicator={true} routePrefix="/{data.recipeLang}"></Card>
|
||||||
{/each}
|
{/each}
|
||||||
</Recipes>
|
</Recipes>
|
||||||
|
{:else if data.favorites.length > 0}
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>{isEnglish ? 'No matching favorites found.' : 'Keine passenden Favoriten gefunden.'}</p>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<p>{labels.emptyState1}</p>
|
<p>{labels.emptyState1}</p>
|
||||||
|
|||||||
@@ -7,10 +7,28 @@
|
|||||||
import Search from '$lib/components/Search.svelte';
|
import Search from '$lib/components/Search.svelte';
|
||||||
let { data }: { data: PageData } = $props();
|
let { data }: { data: PageData } = $props();
|
||||||
import { rand_array } from '$lib/js/randomize';
|
import { rand_array } from '$lib/js/randomize';
|
||||||
|
|
||||||
|
// Search state
|
||||||
|
let matchedRecipeIds = $state(new Set());
|
||||||
|
let hasActiveSearch = $state(false);
|
||||||
|
|
||||||
|
// Handle search results from Search component
|
||||||
|
function handleSearchResults(ids, categories) {
|
||||||
|
matchedRecipeIds = ids;
|
||||||
|
hasActiveSearch = ids.size < data.season.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter recipes based on search
|
||||||
|
const filteredRecipes = $derived.by(() => {
|
||||||
|
if (!hasActiveSearch) {
|
||||||
|
return data.season;
|
||||||
|
}
|
||||||
|
return data.season.filter(r => matchedRecipeIds.has(r._id));
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<IconLayout icons={data.icons} active_icon={data.icon} routePrefix="/{data.recipeLang}" lang={data.lang}>
|
<IconLayout icons={data.icons} active_icon={data.icon} routePrefix="/{data.recipeLang}" lang={data.lang} recipes={data.season} onSearchResults={handleSearchResults}>
|
||||||
<Recipes slot=recipes>
|
<Recipes slot=recipes>
|
||||||
{#each rand_array(data.season) as recipe}
|
{#each rand_array(filteredRecipes) as recipe}
|
||||||
<Card {recipe} icon_override=true isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
|
<Card {recipe} icon_override=true isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
|
||||||
{/each}
|
{/each}
|
||||||
</Recipes>
|
</Recipes>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favorites";
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ url, fetch, params }) => {
|
export const load: PageServerLoad = async ({ url, fetch, params, locals }) => {
|
||||||
const isEnglish = params.recipeLang === 'recipes';
|
const isEnglish = params.recipeLang === 'recipes';
|
||||||
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
|
const apiBase = isEnglish ? '/api/recipes' : '/api/rezepte';
|
||||||
|
|
||||||
@@ -21,13 +22,21 @@ export const load: PageServerLoad = async ({ url, fetch, params }) => {
|
|||||||
if (favoritesOnly) apiUrl.searchParams.set('favorites', 'true');
|
if (favoritesOnly) apiUrl.searchParams.set('favorites', 'true');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(apiUrl.toString());
|
// Fetch both search results and all recipes for live search
|
||||||
const results = await response.json();
|
const [searchResponse, allRecipesResponse, userFavorites] = await Promise.all([
|
||||||
|
fetch(apiUrl.toString()),
|
||||||
|
fetch(`${apiBase}/items/all_brief`),
|
||||||
|
getUserFavorites(fetch, locals)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const results = await searchResponse.json();
|
||||||
|
const allRecipes = await allRecipesResponse.json();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
query,
|
query,
|
||||||
results: response.ok ? results : [],
|
results: searchResponse.ok ? addFavoriteStatusToRecipes(results, userFavorites) : [],
|
||||||
error: response.ok ? null : results.error || 'Search failed',
|
allRecipes: addFavoriteStatusToRecipes(allRecipes, userFavorites),
|
||||||
|
error: searchResponse.ok ? null : results.error || 'Search failed',
|
||||||
filters: {
|
filters: {
|
||||||
category,
|
category,
|
||||||
tag,
|
tag,
|
||||||
@@ -40,6 +49,7 @@ export const load: PageServerLoad = async ({ url, fetch, params }) => {
|
|||||||
return {
|
return {
|
||||||
query,
|
query,
|
||||||
results: [],
|
results: [],
|
||||||
|
allRecipes: [],
|
||||||
error: 'Search failed',
|
error: 'Search failed',
|
||||||
filters: {
|
filters: {
|
||||||
category,
|
category,
|
||||||
|
|||||||
@@ -26,6 +26,26 @@
|
|||||||
noResults: isEnglish ? 'No recipes found.' : 'Keine Rezepte gefunden.',
|
noResults: isEnglish ? 'No recipes found.' : 'Keine Rezepte gefunden.',
|
||||||
tryOther: isEnglish ? 'Try different search terms.' : 'Versuche es mit anderen Suchbegriffen.'
|
tryOther: isEnglish ? 'Try different search terms.' : 'Versuche es mit anderen Suchbegriffen.'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Search state for live filtering
|
||||||
|
let matchedRecipeIds = $state(new Set());
|
||||||
|
let hasActiveSearch = $state(false);
|
||||||
|
|
||||||
|
// Handle search results from Search component
|
||||||
|
function handleSearchResults(ids, categories) {
|
||||||
|
matchedRecipeIds = ids;
|
||||||
|
hasActiveSearch = ids.size < data.allRecipes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter recipes based on live search
|
||||||
|
const displayedRecipes = $derived.by(() => {
|
||||||
|
if (!hasActiveSearch) {
|
||||||
|
// No active search - show server-side results
|
||||||
|
return data.results;
|
||||||
|
}
|
||||||
|
// Active search - show client-side filtered results
|
||||||
|
return data.allRecipes.filter(r => matchedRecipeIds.has(r._id));
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -71,25 +91,31 @@
|
|||||||
season={data.filters.season}
|
season={data.filters.season}
|
||||||
favoritesOnly={data.filters.favoritesOnly}
|
favoritesOnly={data.filters.favoritesOnly}
|
||||||
lang={data.lang}
|
lang={data.lang}
|
||||||
|
recipes={data.allRecipes}
|
||||||
|
onSearchResults={handleSearchResults}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if data.error}
|
{#if data.error}
|
||||||
<div class="search-info">
|
<div class="search-info">
|
||||||
<p>{labels.searchError} {data.error}</p>
|
<p>{labels.searchError} {data.error}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if hasActiveSearch}
|
||||||
|
<div class="search-info">
|
||||||
|
<p>{displayedRecipes.length} {labels.resultsFor} "{data.query}"</p>
|
||||||
|
</div>
|
||||||
{:else if data.query}
|
{:else if data.query}
|
||||||
<div class="search-info">
|
<div class="search-info">
|
||||||
<p>{data.results.length} {labels.resultsFor} "{data.query}"</p>
|
<p>{data.results.length} {labels.resultsFor} "{data.query}"</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if data.results.length > 0}
|
{#if displayedRecipes.length > 0}
|
||||||
<Recipes>
|
<Recipes>
|
||||||
{#each data.results as recipe}
|
{#each displayedRecipes as recipe}
|
||||||
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={true} routePrefix="/{data.recipeLang}"></Card>
|
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={true} routePrefix="/{data.recipeLang}"></Card>
|
||||||
{/each}
|
{/each}
|
||||||
</Recipes>
|
</Recipes>
|
||||||
{:else if data.query && !data.error}
|
{:else if (data.query || hasActiveSearch) && !data.error}
|
||||||
<div class="search-info">
|
<div class="search-info">
|
||||||
<p>{labels.noResults}</p>
|
<p>{labels.noResults}</p>
|
||||||
<p>{labels.tryOther}</p>
|
<p>{labels.tryOther}</p>
|
||||||
|
|||||||
@@ -14,11 +14,29 @@
|
|||||||
const months = $derived(isEnglish
|
const months = $derived(isEnglish
|
||||||
? ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
|
? ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
|
||||||
: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]);
|
: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]);
|
||||||
|
|
||||||
|
// Search state
|
||||||
|
let matchedRecipeIds = $state(new Set());
|
||||||
|
let hasActiveSearch = $state(false);
|
||||||
|
|
||||||
|
// Handle search results from Search component
|
||||||
|
function handleSearchResults(ids, categories) {
|
||||||
|
matchedRecipeIds = ids;
|
||||||
|
hasActiveSearch = ids.size < data.season.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter recipes based on search
|
||||||
|
const filteredRecipes = $derived.by(() => {
|
||||||
|
if (!hasActiveSearch) {
|
||||||
|
return data.season;
|
||||||
|
}
|
||||||
|
return data.season.filter(r => matchedRecipeIds.has(r._id));
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SeasonLayout active_index={current_month-1} {months} routePrefix="/{data.recipeLang}" lang={data.lang}>
|
<SeasonLayout active_index={current_month-1} {months} routePrefix="/{data.recipeLang}" lang={data.lang} recipes={data.season} onSearchResults={handleSearchResults}>
|
||||||
<Recipes slot=recipes>
|
<Recipes slot=recipes>
|
||||||
{#each rand_array(data.season) as recipe}
|
{#each rand_array(filteredRecipes) as recipe}
|
||||||
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
|
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
|
||||||
{/each}
|
{/each}
|
||||||
</Recipes>
|
</Recipes>
|
||||||
|
|||||||
@@ -13,10 +13,28 @@
|
|||||||
: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]);
|
: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]);
|
||||||
|
|
||||||
import { rand_array } from '$lib/js/randomize';
|
import { rand_array } from '$lib/js/randomize';
|
||||||
|
|
||||||
|
// Search state
|
||||||
|
let matchedRecipeIds = $state(new Set());
|
||||||
|
let hasActiveSearch = $state(false);
|
||||||
|
|
||||||
|
// Handle search results from Search component
|
||||||
|
function handleSearchResults(ids, categories) {
|
||||||
|
matchedRecipeIds = ids;
|
||||||
|
hasActiveSearch = ids.size < data.season.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter recipes based on search
|
||||||
|
const filteredRecipes = $derived.by(() => {
|
||||||
|
if (!hasActiveSearch) {
|
||||||
|
return data.season;
|
||||||
|
}
|
||||||
|
return data.season.filter(r => matchedRecipeIds.has(r._id));
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<SeasonLayout active_index={data.month -1} {months} routePrefix="/{data.recipeLang}" lang={data.lang}>
|
<SeasonLayout active_index={data.month -1} {months} routePrefix="/{data.recipeLang}" lang={data.lang} recipes={data.season} onSearchResults={handleSearchResults}>
|
||||||
<Recipes slot=recipes>
|
<Recipes slot=recipes>
|
||||||
{#each rand_array(data.season) as recipe}
|
{#each rand_array(filteredRecipes) as recipe}
|
||||||
<Card {recipe} icon_override=true isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
|
<Card {recipe} icon_override=true isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
|
||||||
{/each}
|
{/each}
|
||||||
</Recipes>
|
</Recipes>
|
||||||
|
|||||||
@@ -9,6 +9,24 @@
|
|||||||
|
|
||||||
const isEnglish = $derived(data.lang === 'en');
|
const isEnglish = $derived(data.lang === 'en');
|
||||||
const label = $derived(isEnglish ? 'Recipes with Keyword' : 'Rezepte mit Stichwort');
|
const label = $derived(isEnglish ? 'Recipes with Keyword' : 'Rezepte mit Stichwort');
|
||||||
|
|
||||||
|
// Search state
|
||||||
|
let matchedRecipeIds = $state(new Set());
|
||||||
|
let hasActiveSearch = $state(false);
|
||||||
|
|
||||||
|
// Handle search results from Search component
|
||||||
|
function handleSearchResults(ids, categories) {
|
||||||
|
matchedRecipeIds = ids;
|
||||||
|
hasActiveSearch = ids.size < data.recipes.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter recipes based on search
|
||||||
|
const filteredRecipes = $derived.by(() => {
|
||||||
|
if (!hasActiveSearch) {
|
||||||
|
return data.recipes;
|
||||||
|
}
|
||||||
|
return data.recipes.filter(r => matchedRecipeIds.has(r._id));
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
h1 {
|
h1 {
|
||||||
@@ -17,10 +35,10 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<h1>{label} <q>{data.tag}</q>:</h1>
|
<h1>{label} <q>{data.tag}</q>:</h1>
|
||||||
<Search tag={data.tag} lang={data.lang}></Search>
|
<Search tag={data.tag} lang={data.lang} recipes={data.recipes} onSearchResults={handleSearchResults}></Search>
|
||||||
<section>
|
<section>
|
||||||
<Recipes>
|
<Recipes>
|
||||||
{#each rand_array(data.recipes) as recipe}
|
{#each rand_array(filteredRecipes) as recipe}
|
||||||
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
|
<Card {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={!!data.session?.user} routePrefix="/{data.recipeLang}"></Card>
|
||||||
{/each}
|
{/each}
|
||||||
</Recipes>
|
</Recipes>
|
||||||
|
|||||||
Reference in New Issue
Block a user