Gate shop/blog filters behind CompanySettings toggles; fix blog filter z-index bug

Fixed: blog category filter bar was rendering behind the featured-post
card — that card pulls itself up (-mt-8, z-10) to overlap the hero's
bottom edge (its original design), but inserting the filter bar
between the hero and that card meant the same pull now overlapped the
filter bar instead, with the card's stacking rendering on top. Given
relative z-20 to stay above that overlap regardless. Also bumped the
inactive chip's background to bg-bg-muted — border-border on bg-base
is only a ~2% lightness difference, nearly invisible as a pill outline.

Both filters (shop price, blog category) now gated behind new
CompanySettings toggles (shopFilterEnabled/blogFilterEnabled), same
off-by-default pattern as wishlistEnabled/searchEnabled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-30 23:07:49 +00:00
parent fc8a1d4201
commit f5b898a69a
3 changed files with 56 additions and 11 deletions
+19 -6
View File
@@ -4,7 +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 { getBlogPosts } from "../lib/payload";
import { getBlogPosts, getBlogFilterEnabled } from "../lib/payload";
import { formatDate } from "../lib/format";
export const metadata: Metadata = {
@@ -41,10 +41,10 @@ export default async function BlogOverviewPage({
}: {
searchParams: Promise<{ categories?: string }>;
}) {
const allPosts = await getBlogPosts(100);
const [allPosts, blogFilterEnabled] = await Promise.all([getBlogPosts(100), getBlogFilterEnabled()]);
const { categories: categoriesParam } = await searchParams;
const activeCategories = categoriesParam ? categoriesParam.split(",").filter(Boolean) : [];
const allCategories = distinctCategories(allPosts);
const activeCategories = blogFilterEnabled && categoriesParam ? categoriesParam.split(",").filter(Boolean) : [];
const allCategories = blogFilterEnabled ? distinctCategories(allPosts) : [];
const posts =
activeCategories.length === 0 ? allPosts : allPosts.filter((post) => post.categories.some((c) => activeCategories.includes(c)));
const [featured, ...rest] = posts;
@@ -84,18 +84,31 @@ export default async function BlogOverviewPage({
hit the identical bug and switched to a plain animate — this
filter bar doesn't need a scroll-reveal animation at all, so
it's simplest to just not wrap it in Reveal in the first place. */}
{/* relative z-20 — the featured-post card right below pulls itself
up by -mt-8 with its own z-10 to overlap the *hero's* bottom
edge (its original, intended design). Inserting this filter
bar between the hero and that card meant the same -mt-8 pull
now overlapped THIS bar instead, and the card's higher/equal
stacking rendered on top of it, visually hiding the chips
behind the card. z-20 keeps this bar above that overlap
regardless. */}
{allCategories.length > 1 && (
<div className="flex flex-wrap gap-2 w-full max-w-[80rem] mx-auto px-[var(--layout-padding-x)] pt-6">
<div className="relative z-20 flex flex-wrap gap-2 w-full max-w-[80rem] mx-auto px-[var(--layout-padding-x)] pt-6">
{allCategories.map((category) => {
const active = activeCategories.includes(category);
return (
<Link
key={category}
href={buildCategoryHref(activeCategories, category)}
// bg-bg-muted, not bg-bg-base — border-border (#e5e0d8)
// on bg-base (#f8f5f1) is a ~2% lightness difference,
// nearly invisible as a pill outline; a filled muted
// background makes the chip read as a discrete control
// regardless of the border's own low contrast.
className={`inline-flex items-center px-3 py-1.5 rounded-full text-body-sm font-semibold whitespace-nowrap border transition-colors ${
active
? "bg-brand border-brand text-text-primary"
: "bg-bg-base border-border text-text-muted hover:border-brand hover:text-brand"
: "bg-bg-muted border-border text-text-muted hover:border-brand hover:text-brand"
}`}
>
{category}
+31
View File
@@ -1039,6 +1039,37 @@ export async function getSearchEnabled(): Promise<boolean> {
return data.docs?.[0]?.searchEnabled ?? false;
}
// Same pattern as getWishlistEnabled()/getSearchEnabled() — gates the
// shop overview's price-range filter (ProductGrid.tsx/PriceRangeFilter.tsx).
export async function getShopFilterEnabled(): Promise<boolean> {
const params = new URLSearchParams({ "where[tenant.slug][equals]": TENANT_SLUG, limit: "1" });
const res = await fetch(`${PAYLOAD_URL}/api/company-settings?${params}`, {
headers: { "x-order-service-secret": process.env.ORDER_SERVICE_SECRET || "" },
next: { revalidate: 60 },
});
if (!res.ok) {
console.error(`getShopFilterEnabled: Payload returned ${res.status} ${res.statusText}`);
return false;
}
const data: { docs?: { shopFilterEnabled?: boolean }[] } = await res.json();
return data.docs?.[0]?.shopFilterEnabled ?? false;
}
// Same pattern — gates the blog overview's category filter (blog/page.tsx).
export async function getBlogFilterEnabled(): Promise<boolean> {
const params = new URLSearchParams({ "where[tenant.slug][equals]": TENANT_SLUG, limit: "1" });
const res = await fetch(`${PAYLOAD_URL}/api/company-settings?${params}`, {
headers: { "x-order-service-secret": process.env.ORDER_SERVICE_SECRET || "" },
next: { revalidate: 60 },
});
if (!res.ok) {
console.error(`getBlogFilterEnabled: Payload returned ${res.status} ${res.statusText}`);
return false;
}
const data: { docs?: { blogFilterEnabled?: boolean }[] } = await res.json();
return data.docs?.[0]?.blogFilterEnabled ?? false;
}
export type SeoSettings = {
defaultTitle: string | null;
titleTemplate: string | null;
+6 -5
View File
@@ -1,6 +1,6 @@
import Link from "next/link";
import Image from "next/image";
import { getProducts, getShippingSettings, getDefaultTaxRatePercent, getKleinunternehmer, getWishlistEnabled } from "../../lib/payload";
import { getProducts, getShippingSettings, getDefaultTaxRatePercent, getKleinunternehmer, getWishlistEnabled, getShopFilterEnabled } from "../../lib/payload";
import { effectiveTaxRate } from "../../lib/cartTotals";
import { formatPrice, discountPercent } from "../../lib/format";
import { RevealGroup, RevealItem } from "../../components/Reveal";
@@ -21,12 +21,13 @@ import { PriceRangeFilter } from "./PriceRangeFilter";
// buckets — tried buckets-as-toggle-chips first, reverted 2026-07-30
// ("keine toggle badges").
export async function ProductGrid({ searchParams }: { searchParams?: { minPrice?: string; maxPrice?: string } }) {
const [allProducts, shipping, defaultTaxRate, kleinunternehmer, wishlistEnabled] = await Promise.all([
const [allProducts, shipping, defaultTaxRate, kleinunternehmer, wishlistEnabled, shopFilterEnabled] = await Promise.all([
getProducts(),
getShippingSettings(),
getDefaultTaxRatePercent(),
getKleinunternehmer(),
getWishlistEnabled(),
getShopFilterEnabled(),
]);
const allActiveProducts = allProducts.filter((p) => p.active);
@@ -41,13 +42,13 @@ export async function ProductGrid({ searchParams }: { searchParams?: { minPrice?
const catalogPrices = allActiveProducts.map((p) => p.price);
const catalogMin = Math.min(...catalogPrices);
const catalogMax = Math.max(...catalogPrices);
const minPrice = searchParams?.minPrice ? Number(searchParams.minPrice) : null;
const maxPrice = searchParams?.maxPrice ? Number(searchParams.maxPrice) : null;
const minPrice = shopFilterEnabled && searchParams?.minPrice ? Number(searchParams.minPrice) : null;
const maxPrice = shopFilterEnabled && searchParams?.maxPrice ? Number(searchParams.maxPrice) : null;
const products = allActiveProducts.filter((p) => (minPrice === null || p.price >= minPrice) && (maxPrice === null || p.price <= maxPrice));
return (
<section className="w-full bg-bg-base flex flex-col pb-16 md:pb-20 px-[var(--layout-padding-x)]">
{catalogMin !== catalogMax && <PriceRangeFilter catalogMin={catalogMin} catalogMax={catalogMax} />}
{shopFilterEnabled && catalogMin !== catalogMax && <PriceRangeFilter catalogMin={catalogMin} catalogMax={catalogMax} />}
{products.length === 0 && <p className="text-body text-text-muted pb-6">Keine Produkte in dieser Preisspanne gefunden.</p>}