feat(search): auto web search when no local hits, offer link otherwise
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 55s
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 55s
Homepage (/):
- Keine lokalen Treffer → automatisch die Internet-Suche auslösen und
die Ergebnisse als Karten unterhalb der Suche anzeigen.
- Mindestens ein lokaler Treffer → Karten zeigen + darunter ein
dezenter Link "🌐 Im Internet weitersuchen" (geht zur /search/web
Vollseite), keine automatische Internet-Suche.
Header-Dropdown (auf Rezept- und Vorschau-Seiten):
- Gleiche Logik: lokale Treffer oben + Fuß-Link; keine lokalen
Treffer → Internet-Ergebnisse werden direkt im Dropdown angezeigt.
- Abschnittsüberschrift "Keine lokalen Rezepte – aus dem Internet:"
trennt den Fallback visuell ab.
Race-Safety bleibt bestehen: Query-Vergleich vor jedem State-Write,
sodass spät ankommende Antworten keinen neueren Suchstand überschreiben.
This commit is contained in:
@@ -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<SearchHit[]>([]);
|
||||
let navWebHits = $state<WebHit[]>([]);
|
||||
let navSearching = $state(false);
|
||||
let navWebSearching = $state(false);
|
||||
let navWebError = $state<string | null>(null);
|
||||
let navOpen = $state(false);
|
||||
let navContainer: HTMLElement | undefined = $state();
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | 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 @@
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else if navQuery.trim().length > 3 && !navSearching}
|
||||
<p class="dd-status">Keine lokalen Treffer.</p>
|
||||
{/if}
|
||||
{#if navQuery.trim().length > 3 && !navSearching}
|
||||
<a
|
||||
class="dd-web"
|
||||
href={`/search/web?q=${encodeURIComponent(navQuery.trim())}`}
|
||||
@@ -144,6 +169,39 @@
|
||||
>
|
||||
🌐 Im Internet weitersuchen
|
||||
</a>
|
||||
{:else}
|
||||
<p class="dd-section">Keine lokalen Rezepte – aus dem Internet:</p>
|
||||
{#if navWebSearching}
|
||||
<p class="dd-status">Suche im Internet läuft …</p>
|
||||
{:else if navWebError}
|
||||
<p class="dd-status dd-error">Internet-Suche zurzeit nicht möglich.</p>
|
||||
{:else if navWebHits.length > 0}
|
||||
<ul class="dd-list">
|
||||
{#each navWebHits as w (w.url)}
|
||||
<li>
|
||||
<a
|
||||
href={`/preview?url=${encodeURIComponent(w.url)}`}
|
||||
class="dd-item"
|
||||
onclick={pickHit}
|
||||
role="option"
|
||||
aria-selected="false"
|
||||
>
|
||||
{#if w.thumbnail}
|
||||
<img src={w.thumbnail} alt="" loading="lazy" />
|
||||
{:else}
|
||||
<div class="dd-placeholder">🍽️</div>
|
||||
{/if}
|
||||
<div class="dd-body">
|
||||
<div class="dd-title">{w.title}</div>
|
||||
<div class="dd-domain">{w.domain}</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{:else}
|
||||
<p class="dd-status">Auch im Internet nichts gefunden.</p>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/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;
|
||||
|
||||
Reference in New Issue
Block a user