From 0ca42f33295462fdf738a0ba36f130bf7d8e71d9 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sun, 19 Apr 2026 12:51:11 +0200 Subject: [PATCH] refactor(layout): Header-Dropdown nutzt SearchStore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ersetzt die 10 lokalen $state-Felder, den Debounce-$effect und die lokalen Search-Funktionen durch eine SearchStore-Instanz. Nav-Open- Toggle, Click-outside und Menu-State bleiben lokal — UI-Concerns. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/routes/+layout.svelte | 176 +++++++------------------------------- 1 file changed, 32 insertions(+), 144 deletions(-) diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index bf32315..646eb86 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -17,26 +17,20 @@ import { network } from '$lib/client/network.svelte'; import { installPrompt } from '$lib/client/install-prompt.svelte'; import { registerServiceWorker } from '$lib/client/sw-register'; - import type { SearchHit } from '$lib/server/recipes/search-local'; - import type { WebHit } from '$lib/server/search/searxng'; + import { SearchStore } from '$lib/client/search.svelte'; let { children } = $props(); - const NAV_PAGE_SIZE = 30; + const navStore = new SearchStore({ + pageSize: 30, + filterParam: () => { + const p = searchFilterStore.queryParam; + return p ? `&domains=${encodeURIComponent(p)}` : ''; + } + }); - let navQuery = $state(''); - let navHits = $state([]); - let navWebHits = $state([]); - let navSearching = $state(false); - let navWebSearching = $state(false); - let navWebError = $state(null); let navOpen = $state(false); - let navLocalExhausted = $state(false); - let navWebPageno = $state(0); - let navWebExhausted = $state(false); - let navLoadingMore = $state(false); let navContainer: HTMLElement | undefined = $state(); - let debounceTimer: ReturnType | null = null; let menuOpen = $state(false); let menuContainer: HTMLElement | undefined = $state(); @@ -44,123 +38,21 @@ $page.url.pathname.startsWith('/recipes/') || $page.url.pathname === '/preview' ); - function filterParam(): string { - const p = searchFilterStore.queryParam; - return p ? `&domains=${encodeURIComponent(p)}` : ''; - } - $effect(() => { - const q = navQuery.trim(); - if (debounceTimer) clearTimeout(debounceTimer); - if (q.length <= 3) { - navHits = []; - navWebHits = []; - navSearching = false; - navWebSearching = false; - navWebError = null; - navOpen = false; - navLocalExhausted = false; - navWebPageno = 0; - navWebExhausted = false; - return; - } - navSearching = true; - navWebHits = []; - navWebSearching = false; - navWebError = null; - navOpen = true; - navLocalExhausted = false; - navWebPageno = 0; - navWebExhausted = false; - debounceTimer = setTimeout(async () => { - try { - const res = await fetch( - `/api/recipes/search?q=${encodeURIComponent(q)}&limit=${NAV_PAGE_SIZE}${filterParam()}` - ); - const body = await res.json(); - if (navQuery.trim() !== q) return; - navHits = body.hits; - if (navHits.length < NAV_PAGE_SIZE) navLocalExhausted = true; - if (navHits.length === 0) { - navWebSearching = true; - try { - const wres = await fetch( - `/api/recipes/search/web?q=${encodeURIComponent(q)}&pageno=1${filterParam()}` - ); - if (navQuery.trim() !== q) return; - if (!wres.ok) { - const err = await wres.json().catch(() => ({})); - navWebError = err.message ?? `HTTP ${wres.status}`; - navWebExhausted = true; - } else { - const wbody = await wres.json(); - navWebHits = wbody.hits; - navWebPageno = 1; - if (navWebHits.length === 0) navWebExhausted = true; - } - } finally { - if (navQuery.trim() === q) navWebSearching = false; - } - } - } finally { - if (navQuery.trim() === q) navSearching = false; - } - }, 300); + // Bare reads register the reactive deps; then kick the store. + const q = navStore.query; + navStore.runDebounced(); + // navOpen follows query length: open while typing, close when cleared. + navOpen = q.trim().length > 3; }); - async function loadMoreNav() { - if (navLoadingMore) return; - const q = navQuery.trim(); - if (!q) return; - navLoadingMore = true; - try { - if (!navLocalExhausted) { - const res = await fetch( - `/api/recipes/search?q=${encodeURIComponent(q)}&limit=${NAV_PAGE_SIZE}&offset=${navHits.length}${filterParam()}` - ); - const body = await res.json(); - if (navQuery.trim() !== q) return; - const more = body.hits as SearchHit[]; - const seen = new Set(navHits.map((h) => h.id)); - const deduped = more.filter((h) => !seen.has(h.id)); - navHits = [...navHits, ...deduped]; - if (more.length < NAV_PAGE_SIZE) navLocalExhausted = true; - } else if (!navWebExhausted) { - const nextPage = navWebPageno + 1; - navWebSearching = navWebHits.length === 0; - try { - const wres = await fetch( - `/api/recipes/search/web?q=${encodeURIComponent(q)}&pageno=${nextPage}${filterParam()}` - ); - if (navQuery.trim() !== q) return; - if (!wres.ok) { - const err = await wres.json().catch(() => ({})); - navWebError = err.message ?? `HTTP ${wres.status}`; - navWebExhausted = true; - return; - } - const wbody = await wres.json(); - const more = wbody.hits as WebHit[]; - const seen = new Set(navWebHits.map((h) => h.url)); - const deduped = more.filter((h) => !seen.has(h.url)); - if (deduped.length === 0) { - navWebExhausted = true; - } else { - navWebHits = [...navWebHits, ...deduped]; - navWebPageno = nextPage; - } - } finally { - if (navQuery.trim() === q) navWebSearching = false; - } - } - } finally { - navLoadingMore = false; - } + function loadMoreNav() { + return navStore.loadMore(); } function submitNav(e: SubmitEvent) { e.preventDefault(); - const q = navQuery.trim(); + const q = navStore.query.trim(); if (!q) return; navOpen = false; void goto(`/?q=${encodeURIComponent(q)}`); @@ -184,15 +76,11 @@ function pickHit() { navOpen = false; - navQuery = ''; - navHits = []; - navWebHits = []; + navStore.reset(); } afterNavigate(() => { - navQuery = ''; - navHits = []; - navWebHits = []; + navStore.reset(); navOpen = false; menuOpen = false; // Badge nach jeder Client-Navigation frisch halten — sonst kann er @@ -239,9 +127,9 @@ { - if (navHits.length > 0 || navQuery.trim().length > 3) navOpen = true; + if (navStore.hits.length > 0 || navStore.query.trim().length > 3) navOpen = true; }} placeholder="Rezept suchen…" autocomplete="off" @@ -251,12 +139,12 @@ {#if navOpen}