e5d218820b
Replace string-literal and template-literal hrefs across the codebase with the modern SvelteKit 2.26+ resolve() and asset() APIs. Migration makes route IDs explicit, type-checked against generated $app/types, and base-path-aware. Two codemod scripts handle the bulk; remaining ambiguous, query-bearing, and precomputed-href cases are converted manually at the assignment sites.
124 lines
4.6 KiB
Svelte
124 lines
4.6 KiB
Svelte
<script lang="ts">
|
|
import { resolve } from '$app/paths';
|
|
import type { PageData } from './$types';
|
|
import Search from '$lib/components/recipes/Search.svelte';
|
|
import CompactCard from '$lib/components/recipes/CompactCard.svelte';
|
|
let { data } = $props<{ data: PageData }>();
|
|
let current_month = new Date().getMonth() + 1;
|
|
|
|
const isEnglish = $derived(data.lang === 'en');
|
|
const labels = $derived({
|
|
title: isEnglish ? 'Search Results' : 'Suchergebnisse',
|
|
pageTitle: isEnglish
|
|
? `Search Results${data.query ? ` for "${data.query}"` : ''} - Bocken Recipes`
|
|
: `Suchergebnisse${data.query ? ` für "${data.query}"` : ''} - Bocken Rezepte`,
|
|
metaDescription: isEnglish
|
|
? 'Search results in Bocken\'s recipes.'
|
|
: 'Suchergebnisse in den Bockenschen Rezepten.',
|
|
filteredBy: isEnglish ? 'Filtered by:' : 'Gefiltert nach:',
|
|
category: isEnglish ? 'Category' : 'Kategorie',
|
|
keywords: isEnglish ? 'Keywords' : 'Stichwörter',
|
|
icon: 'Icon',
|
|
seasons: isEnglish ? 'Seasons' : 'Monate',
|
|
favoritesOnly: isEnglish ? 'Favorites only' : 'Nur Favoriten',
|
|
searchError: isEnglish ? 'Search error:' : 'Fehler bei der Suche:',
|
|
resultsFor: isEnglish ? 'results for' : 'Ergebnisse für',
|
|
noResults: isEnglish ? 'No recipes found.' : 'Keine Rezepte gefunden.',
|
|
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: Set<string>, categories: Set<string>) {
|
|
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: any) => matchedRecipeIds.has(r._id));
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
h1 {
|
|
text-align: center;
|
|
font-size: 3em;
|
|
}
|
|
.search-info {
|
|
text-align: center;
|
|
margin-bottom: 2rem;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
.filter-info {
|
|
text-align: center;
|
|
margin-bottom: 1rem;
|
|
font-size: 0.9em;
|
|
color: var(--color-text-tertiary);
|
|
}
|
|
</style>
|
|
|
|
<svelte:head>
|
|
<title>{labels.pageTitle}</title>
|
|
<meta name="description" content={labels.metaDescription} />
|
|
</svelte:head>
|
|
|
|
<h1>{labels.title}</h1>
|
|
|
|
{#if data.filters.category || data.filters.tags?.length > 0 || data.filters.icon || data.filters.seasons?.length > 0 || data.filters.favoritesOnly}
|
|
<div class="filter-info">
|
|
{labels.filteredBy}
|
|
{#if data.filters.category}{labels.category}: "{data.filters.category}"{/if}
|
|
{#if data.filters.tags?.length > 0}{labels.keywords}: {data.filters.tags.join(', ')}{/if}
|
|
{#if data.filters.icon}{labels.icon}: "{data.filters.icon}"{/if}
|
|
{#if data.filters.seasons?.length > 0}{labels.seasons}: {data.filters.seasons.join(', ')}{/if}
|
|
{#if data.filters.favoritesOnly}{labels.favoritesOnly}{/if}
|
|
</div>
|
|
{/if}
|
|
|
|
<Search
|
|
category={data.filters.category}
|
|
tag={data.filters.tags?.[0] || null}
|
|
icon={data.filters.icon}
|
|
season={data.filters.seasons?.[0] || null}
|
|
favoritesOnly={data.filters.favoritesOnly}
|
|
lang={data.lang}
|
|
recipes={data.allRecipes}
|
|
isLoggedIn={!!data.session?.user}
|
|
onSearchResults={handleSearchResults}
|
|
/>
|
|
|
|
{#if data.error}
|
|
<div class="search-info">
|
|
<p>{labels.searchError} {data.error}</p>
|
|
</div>
|
|
{:else if hasActiveSearch}
|
|
<div class="search-info">
|
|
<p>{displayedRecipes.length} {labels.resultsFor} "{data.query}"</p>
|
|
</div>
|
|
{:else if data.query}
|
|
<div class="search-info">
|
|
<p>{data.results.length} {labels.resultsFor} "{data.query}"</p>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if displayedRecipes.length > 0}
|
|
<div class="recipe-grid">
|
|
{#each displayedRecipes as recipe (recipe._id)}
|
|
<CompactCard {recipe} {current_month} isFavorite={recipe.isFavorite} showFavoriteIndicator={true} routePrefix={resolve('/[recipeLang=recipeLang]', { recipeLang: data.recipeLang })} />
|
|
{/each}
|
|
</div>
|
|
{:else if (data.query || hasActiveSearch) && !data.error}
|
|
<div class="search-info">
|
|
<p>{labels.noResults}</p>
|
|
<p>{labels.tryOther}</p>
|
|
</div>
|
|
{/if} |