"use client"; import { useEffect, useRef, useState } from "react"; import Link from "next/link"; import Image from "next/image"; import type { SearchResult } from "../api/search/route"; const DEBOUNCE_MS = 250; // Plain trigger button — no state of its own. `open`/`onOpen` are lifted to // Navbar (mirrors NewsletterModal's pattern) so SearchOverlay itself can be // rendered as a
*sibling* instead of a descendant. Rendering it // inside
put it under the header's conditional `backdrop-blur-md` // (applied once `scrolled` or `mobileOpen` is true), and per spec a // `backdrop-filter` makes its element a new containing block for // `position: fixed` descendants — the overlay's `fixed inset-0` then // resolved against the ~100px header instead of the viewport, clipping its // opaque background to that band while the input/results overflowed past // it, letting the page content underneath show through. export function SearchButton({ onOpen }: { onOpen: () => void }) { return ( ); } export function SearchOverlay({ open, onClose }: { open: boolean; onClose: () => void }) { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const inputRef = useRef(null); const debounceRef = useRef | null>(null); useEffect(() => { if (!open) return; function onKeyDown(e: KeyboardEvent) { if (e.key === "Escape") onClose(); } document.addEventListener("keydown", onKeyDown); document.body.style.overflow = "hidden"; return () => { document.removeEventListener("keydown", onKeyDown); document.body.style.overflow = ""; }; }, [open, onClose]); useEffect(() => { if (open) inputRef.current?.focus(); }, [open]); useEffect(() => { if (debounceRef.current) clearTimeout(debounceRef.current); if (query.trim().length < 2) { setResults([]); setLoading(false); return; } setLoading(true); debounceRef.current = setTimeout(async () => { const res = await fetch(`/api/search?q=${encodeURIComponent(query.trim())}`, { cache: "no-store" }); const data: { results?: SearchResult[] } = await res.json().catch(() => ({ results: [] })); setResults(data.results ?? []); setLoading(false); }, DEBOUNCE_MS); return () => { if (debounceRef.current) clearTimeout(debounceRef.current); }; }, [query]); const products = results.filter((r) => r.type === "product"); const posts = results.filter((r) => r.type === "post"); if (!open) return null; return (
e.stopPropagation()}>
setQuery(e.target.value)} placeholder="Produkte, Blogbeiträge…" className="flex-1 min-w-0 bg-transparent outline-none text-h4 text-text-primary placeholder:text-text-muted" /> {/* X icon, not the old "Esc" text — the keyboard shortcut still works (see the Escape keydown handler above), this button is just the mouse/touch affordance, and a close icon reads faster than a text label at a glance. */}
{loading &&

Suche…

} {!loading && query.trim().length >= 2 && results.length === 0 && (

Keine Treffer für „{query}“.

)} {products.length > 0 && (

Produkte

{products.map((r) => ( ))}
)} {posts.length > 0 && (

Blog

{posts.map((r) => ( ))}
)}
); } function SearchResultRow({ result, onClose }: { result: SearchResult; onClose: () => void }) { return (
{result.thumbnail && }
{result.title} ); }