Add mobile swipe gallery, shared ProductCard, dynamic Datenschutz address, GSC verification
- ProductGallery: touch swipe (left/right) now navigates slides on mobile, previously click-only. - New shared ProductCard component used by ProductGrid/RelatedProducts/ MerklisteGrid — product title is now the card's link (replaces the separate "Mehr erfahren" line), consistent aspect ratio across all three grids, more compact cards. - Datenschutz: name/address/email now come from company-settings via a new VerantwortlicherBlock, same single-source pattern as Impressum's AnbieterAngaben — no more hand-typed address to keep in sync. - layout.tsx: render googleSearchConsoleVerification via Next's native verification.google metadata field. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { RevealGroup, RevealItem } from "../../../components/Reveal";
|
||||
import { formatPrice, discountPercent, formatDate } from "../../../lib/format";
|
||||
import { AddToCartInlineButton } from "../../../components/AddToCartInlineButton";
|
||||
import { WishlistButton } from "../../../components/WishlistButton";
|
||||
import { formatDate } from "../../../lib/format";
|
||||
import { ProductCard } from "../../../components/ProductCard";
|
||||
import { useWishlist } from "../../../lib/useWishlist";
|
||||
import { effectiveTaxRate } from "../../../lib/cartTotals";
|
||||
import type { Product } from "../../../lib/payload";
|
||||
|
||||
// Client Component so removing an item (WishlistButton toggling it off)
|
||||
@@ -43,73 +40,32 @@ export function MerklisteGrid({
|
||||
|
||||
return (
|
||||
<RevealGroup className="grid items-start grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 w-full">
|
||||
{visibleEntries.map(({ item, product }) => {
|
||||
const discount = discountPercent(product.price, product.compareAtPrice);
|
||||
const taxRate = effectiveTaxRate(product, defaultTaxRate);
|
||||
const fullyOutOfStock = product.variants.length > 0 ? product.variants.every((v) => v.outOfStock) : product.outOfStock;
|
||||
// Already-purchased takes precedence over "Ausverkauft" — a
|
||||
// customer who already bought this doesn't need a restock notice,
|
||||
// they need to know they already own it (and can still remove it
|
||||
// manually via WishlistButton — this is a status note, not an
|
||||
// auto-removal, see feedback discussion this implements).
|
||||
return (
|
||||
<RevealItem key={`${product.id}-${item.variant}`} className="bg-bg-base border border-border rounded-md overflow-hidden flex flex-col h-full">
|
||||
<div className="relative w-full aspect-[276/210] overflow-hidden">
|
||||
<Image
|
||||
src={product.image}
|
||||
alt={product.name}
|
||||
fill
|
||||
sizes="(min-width: 1024px) 33vw, (min-width: 640px) 50vw, 100vw"
|
||||
className={`object-cover ${item.purchasedAt || fullyOutOfStock ? "opacity-60" : ""}`}
|
||||
/>
|
||||
{item.purchasedAt ? (
|
||||
{visibleEntries.map(({ item, product }) => (
|
||||
<RevealItem key={`${product.id}-${item.variant}`}>
|
||||
<ProductCard
|
||||
product={product}
|
||||
defaultTaxRate={defaultTaxRate}
|
||||
kleinunternehmer={kleinunternehmer}
|
||||
wishlistEnabled
|
||||
className="h-full"
|
||||
// Already-purchased takes precedence over the default
|
||||
// Ausverkauft/discount badge — a customer who already bought
|
||||
// this doesn't need a restock notice, they need to know they
|
||||
// already own it (and can still remove it manually via
|
||||
// WishlistButton — this is a status note, not an
|
||||
// auto-removal, see feedback discussion this implements).
|
||||
// undefined (not purchased) falls through to ProductCard's
|
||||
// own default Ausverkauft/discount badge.
|
||||
topLeftBadge={
|
||||
item.purchasedAt ? (
|
||||
<span className="absolute top-3 left-3 rounded-full bg-success-subtle px-2.5 py-1 text-label font-bold text-success">
|
||||
Gekauft am {formatDate(item.purchasedAt)}
|
||||
</span>
|
||||
) : (
|
||||
fullyOutOfStock && (
|
||||
<span className="absolute top-3 left-3 rounded-full bg-text-muted px-2.5 py-1 text-label font-bold text-bg-base">
|
||||
Ausverkauft
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
<WishlistButton productId={product.numericId} className="absolute top-3 right-3" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 items-start px-5 pb-5 pt-4 w-full flex-1">
|
||||
<div className="flex flex-col gap-1 items-start w-full">
|
||||
{/* Same eyebrow treatment as ProductGrid.tsx/Blog.tsx's
|
||||
category line — omitted for an uncategorized product. */}
|
||||
{product.categories.length > 0 && (
|
||||
<p className="text-label font-semibold text-text-muted uppercase tracking-wide">{product.categories.join(", ")}</p>
|
||||
)}
|
||||
<p className="font-semibold text-h4 text-text-primary w-full" style={{ fontFamily: "var(--font-lora)" }}>
|
||||
{product.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 items-start">
|
||||
<p className="flex items-baseline gap-1.5">
|
||||
{discount !== null && (
|
||||
<span className="text-label text-text-muted line-through">{formatPrice(product.compareAtPrice!)}</span>
|
||||
)}
|
||||
<span className="font-bold text-h4 text-text-primary">{formatPrice(product.price)}</span>
|
||||
{!kleinunternehmer && <span className="text-label text-text-muted">inkl. {taxRate}% MwSt.</span>}
|
||||
</p>
|
||||
</div>
|
||||
{/* flex-1 spacer — pins every card's button to the same Y
|
||||
regardless of whether `product.name` wraps to one or two
|
||||
lines (see ProductGrid.tsx's identical spacer). */}
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* No className override — AddToCartInlineButton's `className`
|
||||
prop REPLACES its whole default styling (`?? defaultClass`,
|
||||
not a merge), so passing just "w-full" here previously threw
|
||||
away all the button's actual styling. Its default is
|
||||
already `w-full`. */}
|
||||
<AddToCartInlineButton id={product.id} numericId={product.numericId} outOfStock={product.outOfStock} maxQty={product.maxQty} variants={product.variants} />
|
||||
</div>
|
||||
</RevealItem>
|
||||
);
|
||||
})}
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
</RevealItem>
|
||||
))}
|
||||
</RevealGroup>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user