diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 954f03d..0f94187 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -15,6 +15,8 @@ let { children } = $props(); + const NAV_PAGE_SIZE = 30; + let navQuery = $state(''); let navHits = $state([]); let navWebHits = $state([]); @@ -22,6 +24,10 @@ 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); @@ -41,6 +47,9 @@ navWebSearching = false; navWebError = null; navOpen = false; + navLocalExhausted = false; + navWebPageno = 0; + navWebExhausted = false; return; } navSearching = true; @@ -48,23 +57,34 @@ 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)}`); + const res = await fetch( + `/api/recipes/search?q=${encodeURIComponent(q)}&limit=${NAV_PAGE_SIZE}` + ); const body = await res.json(); if (navQuery.trim() !== q) return; navHits = body.hits; - if (body.hits.length === 0) { + 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)}`); + const wres = await fetch( + `/api/recipes/search/web?q=${encodeURIComponent(q)}&pageno=1` + ); 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; @@ -76,6 +96,56 @@ }, 300); }); + 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}` + ); + 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}` + ); + 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 submitNav(e: SubmitEvent) { e.preventDefault(); const q = navQuery.trim(); @@ -156,48 +226,43 @@ {#if navOpen} {/if} @@ -429,15 +508,23 @@ justify-content: center; gap: 0.4rem; padding: 0.75rem 0.85rem; + border: 0; border-top: 1px solid #e4eae7; text-decoration: none; color: #2b6a3d; font-size: 0.95rem; background: #fafdfb; + width: 100%; + cursor: pointer; + font-family: inherit; } - .dd-web:hover { + .dd-web:hover:not(:disabled) { background: #eaf4ed; } + .dd-web:disabled { + opacity: 0.6; + cursor: progress; + } .bar-right { display: flex; align-items: center;