diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index a9172fc..2e9bf10 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -6,12 +6,16 @@ import ProfileSwitcher from '$lib/components/ProfileSwitcher.svelte'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import type { SearchHit } from '$lib/server/recipes/search-local'; + import type { WebHit } from '$lib/server/search/searxng'; let { children } = $props(); 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 navContainer: HTMLElement | undefined = $state(); let debounceTimer: ReturnType | null = null; @@ -25,17 +29,40 @@ if (debounceTimer) clearTimeout(debounceTimer); if (q.length <= 3) { navHits = []; + navWebHits = []; navSearching = false; + navWebSearching = false; + navWebError = null; navOpen = false; return; } navSearching = true; + navWebHits = []; + navWebSearching = false; + navWebError = null; navOpen = true; debounceTimer = setTimeout(async () => { try { const res = await fetch(`/api/recipes/search?q=${encodeURIComponent(q)}`); const body = await res.json(); - if (navQuery.trim() === q) navHits = body.hits; + if (navQuery.trim() !== q) return; + navHits = body.hits; + if (body.hits.length === 0) { + navWebSearching = true; + try { + const wres = await fetch(`/api/recipes/search/web?q=${encodeURIComponent(q)}`); + if (navQuery.trim() !== q) return; + if (!wres.ok) { + const err = await wres.json().catch(() => ({})); + navWebError = err.message ?? `HTTP ${wres.status}`; + } else { + const wbody = await wres.json(); + navWebHits = wbody.hits; + } + } finally { + if (navQuery.trim() === q) navWebSearching = false; + } + } } finally { if (navQuery.trim() === q) navSearching = false; } @@ -64,11 +91,13 @@ navOpen = false; navQuery = ''; navHits = []; + navWebHits = []; } afterNavigate(() => { navQuery = ''; navHits = []; + navWebHits = []; navOpen = false; }); @@ -133,10 +162,6 @@ {/each} - {:else if navQuery.trim().length > 3 && !navSearching} -

Keine lokalen Treffer.

- {/if} - {#if navQuery.trim().length > 3 && !navSearching} 🌐 Im Internet weitersuchen + {:else} +

Keine lokalen Rezepte – aus dem Internet:

+ {#if navWebSearching} +

Suche im Internet läuft …

+ {:else if navWebError} +

Internet-Suche zurzeit nicht möglich.

+ {:else if navWebHits.length > 0} + + {:else} +

Auch im Internet nichts gefunden.

+ {/if} {/if} {/if} @@ -290,6 +348,18 @@ margin: 0; font-size: 0.9rem; } + .dd-error { + color: #c53030; + } + .dd-section { + margin: 0; + padding: 0.6rem 0.85rem 0.3rem; + font-size: 0.8rem; + color: #888; + border-bottom: 1px solid #f0f3f1; + text-transform: uppercase; + letter-spacing: 0.03em; + } .dd-web { display: block; padding: 0.75rem 0.85rem; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index ace0500..9d2b580 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -2,11 +2,15 @@ import { onMount } from 'svelte'; import { goto } from '$app/navigation'; import type { SearchHit } from '$lib/server/recipes/search-local'; + import type { WebHit } from '$lib/server/search/searxng'; let query = $state(''); let recent = $state([]); let hits = $state([]); + let webHits = $state([]); let searching = $state(false); + let webSearching = $state(false); + let webError = $state(null); let searchedFor = $state(null); onMount(async () => { @@ -22,18 +26,39 @@ if (debounceTimer) clearTimeout(debounceTimer); if (q.length <= 3) { hits = []; + webHits = []; searchedFor = null; searching = false; + webSearching = false; + webError = null; return; } searching = true; + webHits = []; + webSearching = false; + webError = null; debounceTimer = setTimeout(async () => { try { const res = await fetch(`/api/recipes/search?q=${encodeURIComponent(q)}`); const body = await res.json(); - if (query.trim() === q) { - hits = body.hits; - searchedFor = q; + if (query.trim() !== q) return; + hits = body.hits; + searchedFor = q; + if (body.hits.length === 0) { + webSearching = true; + try { + const wres = await fetch(`/api/recipes/search/web?q=${encodeURIComponent(q)}`); + if (query.trim() !== q) return; + if (!wres.ok) { + const err = await wres.json().catch(() => ({})); + webError = err.message ?? `HTTP ${wres.status}`; + } else { + const wbody = await wres.json(); + webHits = wbody.hits; + } + } finally { + if (query.trim() === q) webSearching = false; + } } } finally { if (query.trim() === q) searching = false; @@ -90,13 +115,36 @@ {/each} + + 🌐 Im Internet weitersuchen + {:else if searchedFor === query.trim()} -
-

Keine lokalen Rezepte für „{searchedFor}".

- - 🌐 Im Internet weitersuchen - -
+

Keine lokalen Rezepte für „{searchedFor}" — Ergebnisse aus dem Internet:

+ {#if webSearching} +

Suche im Internet läuft …

+ {:else if webError} +

Internet-Suche zurzeit nicht möglich: {webError}

+ {:else if webHits.length > 0} + + {:else} +

Auch im Internet nichts gefunden.

+ {/if} {/if} {:else if recent.length > 0} @@ -174,7 +222,16 @@ .muted { color: #888; text-align: center; - padding: 2rem 0; + padding: 1rem 0; + } + .no-local-msg { + font-size: 0.95rem; + padding: 0.25rem 0 1rem; + } + .error { + color: #c53030; + text-align: center; + padding: 1rem 0; } .cards { list-style: none; @@ -220,22 +277,18 @@ color: #888; margin-top: 0.25rem; } - .no-local { - text-align: center; - padding: 1.5rem 0; - } - .no-local p { - color: #666; - margin: 0 0 1rem; - } - .web-btn { + .web-more { display: inline-block; - padding: 0.8rem 1.25rem; - background: #2b6a3d; - color: white; - text-decoration: none; + margin-top: 1rem; + padding: 0.7rem 1.1rem; + border: 1px solid #b7d6c2; border-radius: 10px; - font-size: 1rem; - min-height: 48px; + text-decoration: none; + color: #2b6a3d; + background: #eaf4ed; + font-size: 0.95rem; + } + .web-more:hover { + background: #d8e8df; }