Add price-range toggle filters to the shop overview
Scoped down from the original "category checkboxes + price slider" sidebar proposal — Products have no category taxonomy today (only Posts do), so a category filter isn't buildable against real data yet. Price buckets are computed from the active catalog's actual min/max price (not fixed round-number thresholds), so they stay sensible regardless of what's actually being sold. Same URL-search- param, plain-Link toggle-chip pattern as the blog/order-list filters. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,29 @@ import { WishlistButton } from "../../components/WishlistButton";
|
||||
// there's no reason to pay for a client fetch when a server one already
|
||||
// gives faster first paint and no loading flash.
|
||||
|
||||
export async function ProductGrid() {
|
||||
// Products have no category taxonomy today (only Posts do) — a "N
|
||||
// checkboxes" category sidebar isn't buildable against real data yet, so
|
||||
// this only covers price, the one dimension that already exists on every
|
||||
// product. 3 buckets split evenly across the active catalog's actual
|
||||
// min/max price (not fixed round-number thresholds like "unter 20 €") so
|
||||
// the ranges stay sensible regardless of what's actually being sold —
|
||||
// re-computed on every request from whatever's active, never stale.
|
||||
function buildPriceBuckets(products: { price: number }[]): { label: string; min: number; max: number }[] {
|
||||
if (products.length === 0) return [];
|
||||
const prices = products.map((p) => p.price);
|
||||
const min = Math.min(...prices);
|
||||
const max = Math.max(...prices);
|
||||
if (min === max) return [];
|
||||
const step = (max - min) / 3;
|
||||
const bounds = [min, min + step, min + step * 2, max];
|
||||
return [0, 1, 2].map((i) => ({
|
||||
label: i === 0 ? `bis ${formatPrice(bounds[i + 1])}` : i === 2 ? `ab ${formatPrice(bounds[i])}` : `${formatPrice(bounds[i])}–${formatPrice(bounds[i + 1])}`,
|
||||
min: bounds[i],
|
||||
max: bounds[i + 1],
|
||||
}));
|
||||
}
|
||||
|
||||
export async function ProductGrid({ searchParams }: { searchParams?: { priceBucket?: string } }) {
|
||||
const [allProducts, shipping, defaultTaxRate, kleinunternehmer, wishlistEnabled] = await Promise.all([
|
||||
getProducts(),
|
||||
getShippingSettings(),
|
||||
@@ -21,9 +43,9 @@ export async function ProductGrid() {
|
||||
getKleinunternehmer(),
|
||||
getWishlistEnabled(),
|
||||
]);
|
||||
const products = allProducts.filter((p) => p.active);
|
||||
const allActiveProducts = allProducts.filter((p) => p.active);
|
||||
|
||||
if (products.length === 0) {
|
||||
if (allActiveProducts.length === 0) {
|
||||
return (
|
||||
<section className="w-full bg-bg-base flex flex-col items-center pb-16 md:pb-20 px-[var(--layout-padding-x)]">
|
||||
<p className="text-body text-text-muted">Aktuell keine Produkte verfügbar.</p>
|
||||
@@ -31,8 +53,45 @@ export async function ProductGrid() {
|
||||
);
|
||||
}
|
||||
|
||||
const priceBuckets = buildPriceBuckets(allActiveProducts);
|
||||
const activeBucketIndex = searchParams?.priceBucket ? Number(searchParams.priceBucket) : null;
|
||||
const activeBucket = activeBucketIndex !== null ? priceBuckets[activeBucketIndex] : undefined;
|
||||
const products = activeBucket
|
||||
? allActiveProducts.filter((p) => p.price >= activeBucket.min && p.price <= activeBucket.max)
|
||||
: allActiveProducts;
|
||||
|
||||
return (
|
||||
<section className="w-full bg-bg-base flex flex-col pb-16 md:pb-20 px-[var(--layout-padding-x)]">
|
||||
{priceBuckets.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 w-full pb-6">
|
||||
<Link
|
||||
href="/shop"
|
||||
className={`inline-flex items-center px-3 py-1.5 rounded-full text-body-sm font-semibold whitespace-nowrap border transition-colors ${
|
||||
activeBucketIndex === null
|
||||
? "bg-brand border-brand text-text-primary"
|
||||
: "bg-bg-base border-border text-text-muted hover:border-brand hover:text-brand"
|
||||
}`}
|
||||
>
|
||||
Alle Preise
|
||||
</Link>
|
||||
{priceBuckets.map((bucket, i) => (
|
||||
<Link
|
||||
key={i}
|
||||
href={`/shop?priceBucket=${i}`}
|
||||
className={`inline-flex items-center px-3 py-1.5 rounded-full text-body-sm font-semibold whitespace-nowrap border transition-colors ${
|
||||
activeBucketIndex === i
|
||||
? "bg-brand border-brand text-text-primary"
|
||||
: "bg-bg-base border-border text-text-muted hover:border-brand hover:text-brand"
|
||||
}`}
|
||||
>
|
||||
{bucket.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{products.length === 0 && <p className="text-body text-text-muted pb-6">Keine Produkte in dieser Preisspanne gefunden.</p>}
|
||||
|
||||
{/* 2-up from the mobile breakpoint (sm, 640px) through 1023px — was
|
||||
sm:grid-cols-12 with each card sm:col-span-3 (4-up), too narrow a
|
||||
card through that tablet range. 4-up now only kicks in at lg
|
||||
|
||||
+7
-2
@@ -19,12 +19,17 @@ export const metadata: Metadata = {
|
||||
},
|
||||
};
|
||||
|
||||
export default function ShopPage() {
|
||||
export default async function ShopPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ priceBucket?: string }>;
|
||||
}) {
|
||||
const resolvedSearchParams = await searchParams;
|
||||
return (
|
||||
<>
|
||||
<main className="flex flex-col flex-1 bg-bg-base">
|
||||
<ShopHeader />
|
||||
<ProductGrid />
|
||||
<ProductGrid searchParams={resolvedSearchParams} />
|
||||
<TrustRow />
|
||||
</main>
|
||||
<Footer />
|
||||
|
||||
Reference in New Issue
Block a user