diff --git a/app/blog/page.tsx b/app/blog/page.tsx
index 0b4fd3f..d2f2854 100644
--- a/app/blog/page.tsx
+++ b/app/blog/page.tsx
@@ -4,6 +4,7 @@ import Image from "next/image";
import { Reveal, RevealGroup, RevealItem } from "../components/Reveal";
import { Newsletter } from "../components/Newsletter";
import { Footer } from "../components/Footer";
+import { BlogCategoryFilter } from "../components/BlogCategoryFilter";
import { getBlogPosts, getBlogFilterEnabled } from "../lib/payload";
import { formatDate } from "../lib/format";
@@ -31,11 +32,6 @@ function distinctCategories(posts: { categories: string[] }[]): string[] {
return Array.from(seen);
}
-function buildCategoryHref(active: string[], category: string): string {
- const next = active.includes(category) ? active.filter((c) => c !== category) : [...active, category];
- return next.length > 0 ? `/blog?categories=${next.map(encodeURIComponent).join(",")}` : "/blog";
-}
-
export default async function BlogOverviewPage({
searchParams,
}: {
@@ -93,29 +89,7 @@ export default async function BlogOverviewPage({
under the description keeps them in view immediately, on
every width, no scrolling past the photo needed. */}
{allCategories.length > 1 && (
-
- {allCategories.map((category) => {
- const active = activeCategories.includes(category);
- return (
-
- {category}
-
- );
- })}
-
+
)}
diff --git a/app/components/BlogCategoryFilter.tsx b/app/components/BlogCategoryFilter.tsx
new file mode 100644
index 0000000..bfcb975
--- /dev/null
+++ b/app/components/BlogCategoryFilter.tsx
@@ -0,0 +1,44 @@
+"use client";
+
+import { useTransition } from "react";
+import { useRouter } from "next/navigation";
+
+function buildCategoryHref(active: string[], category: string): string {
+ const next = active.includes(category) ? active.filter((c) => c !== category) : [...active, category];
+ return next.length > 0 ? `/blog?categories=${next.map(encodeURIComponent).join(",")}` : "/blog";
+}
+
+// Client-side navigation (not a plain
) specifically so `isPending`
+// can gate the chips: rapid clicks used to fire multiple concurrent RSC
+// navigations with no guarantee they'd commit in the order they were
+// requested, so a slower, stale navigation could land after a newer one and
+// briefly show the wrong (sometimes empty) result. Disabling the chips
+// while a navigation is in flight makes that ordering race impossible —
+// only one navigation can ever be outstanding at a time.
+export function BlogCategoryFilter({ allCategories, activeCategories }: { allCategories: string[]; activeCategories: string[] }) {
+ const router = useRouter();
+ const [isPending, startTransition] = useTransition();
+
+ return (
+
+ {allCategories.map((category) => {
+ const active = activeCategories.includes(category);
+ return (
+ startTransition(() => router.push(buildCategoryHref(activeCategories, category)))}
+ className={`inline-flex items-center px-3 py-1.5 rounded-full text-body-sm font-semibold whitespace-nowrap border transition-colors disabled:opacity-60 disabled:pointer-events-none ${
+ active
+ ? "bg-brand border-brand text-text-primary"
+ : "bg-bg-muted border-border text-text-muted hover:border-brand hover:text-brand"
+ }`}
+ >
+ {category}
+
+ );
+ })}
+
+ );
+}
diff --git a/app/components/Navbar.tsx b/app/components/Navbar.tsx
index d3f706a..b35da16 100644
--- a/app/components/Navbar.tsx
+++ b/app/components/Navbar.tsx
@@ -7,7 +7,7 @@ import { usePathname } from "next/navigation";
import { AnimatePresence, motion } from "motion/react";
import { useCartCount } from "../lib/cart";
import { useWishlist } from "../lib/useWishlist";
-import { SearchButton } from "./SearchOverlay";
+import { SearchButton, SearchOverlay } from "./SearchOverlay";
import { AUTH_CHANGED_EVENT } from "../lib/auth";
import { NewsletterModal } from "./NewsletterModal";
import { useCartFly } from "./CartFly";
@@ -302,6 +302,7 @@ export function Navbar({
const [activeSection, setActiveSection] = useState("");
const [mobileOpen, setMobileOpen] = useState(false);
const [newsletterOpen, setNewsletterOpen] = useState(false);
+ const [searchOpen, setSearchOpen] = useState(false);
const panelRef = useRef
(null);
const hamburgerRef = useRef(null);
@@ -619,7 +620,7 @@ export function Navbar({
separates this group from the CTA-buttons/hamburger group
that follows. */}
- {searchEnabled &&
}
+ {searchEnabled &&
setSearchOpen(true)} />}
{wishlistEnabled && }
@@ -813,6 +814,7 @@ export function Navbar({
setNewsletterOpen(false)} />
+ {searchEnabled && setSearchOpen(false)} />}
>
);
}
diff --git a/app/components/SearchOverlay.tsx b/app/components/SearchOverlay.tsx
index 070f013..d206e12 100644
--- a/app/components/SearchOverlay.tsx
+++ b/app/components/SearchOverlay.tsx
@@ -7,41 +7,33 @@ import type { SearchResult } from "../api/search/route";
const DEBOUNCE_MS = 250;
-export function SearchButton() {
- const [open, setOpen] = useState(false);
-
- useEffect(() => {
- if (!open) return;
- function onKeyDown(e: KeyboardEvent) {
- if (e.key === "Escape") setOpen(false);
- }
- document.addEventListener("keydown", onKeyDown);
- document.body.style.overflow = "hidden";
- return () => {
- document.removeEventListener("keydown", onKeyDown);
- document.body.style.overflow = "";
- };
- }, [open]);
-
+// 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 (
- <>
- setOpen(true)}
- aria-label="Suche öffnen"
- className="flex h-11 w-11 items-center justify-center shrink-0 active:scale-[0.9] transition-transform"
- >
-
-
-
-
-
- {open && setOpen(false)} />}
- >
+
+
+
+
+
+
);
}
-function SearchOverlay({ onClose }: { onClose: () => void }) {
+export function SearchOverlay({ open, onClose }: { open: boolean; onClose: () => void }) {
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
@@ -49,8 +41,21 @@ function SearchOverlay({ onClose }: { onClose: () => void }) {
const debounceRef = useRef | null>(null);
useEffect(() => {
- inputRef.current?.focus();
- }, []);
+ 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);
@@ -74,6 +79,8 @@ function SearchOverlay({ onClose }: { onClose: () => void }) {
const products = results.filter((r) => r.type === "product");
const posts = results.filter((r) => r.type === "post");
+ if (!open) return null;
+
return (