Add spotlight wishlist toggle and shop filter/grid polish
Per-product spotlightShowWishlist opt-in (independent of the global wishlistEnabled toggle), a reveal-on-hover WishlistButton variant for multi-card grids (avoids a heart on every card reading as visual noise), and related PriceRangeFilter/ProductGrid/CustomSelect adjustments. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,16 @@ export function CustomSelect({
|
||||
listRef.current?.querySelector<HTMLElement>(`[data-index="${highlighted}"]`)?.scrollIntoView({ block: "nearest" });
|
||||
}, [open, highlighted]);
|
||||
|
||||
// Keyboard/focus-driven close: Tabbing (or programmatically moving
|
||||
// focus) away from the trigger+list entirely used to leave the panel
|
||||
// open forever — the mousedown-outside listener above only ever reacts
|
||||
// to a mouse click, not focus leaving via Tab. `relatedTarget` is where
|
||||
// focus is headed; null on some browsers when it lands outside the
|
||||
// document/on a non-focusable element, which should also close.
|
||||
function onBlur(e: React.FocusEvent) {
|
||||
if (!rootRef.current?.contains(e.relatedTarget as Node)) setOpen(false);
|
||||
}
|
||||
|
||||
function select(index: number) {
|
||||
onChange(allOptions[index].value);
|
||||
setOpen(false);
|
||||
@@ -96,7 +106,7 @@ export function CustomSelect({
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className={`relative w-full ${fullWidth ? "" : "sm:w-auto"}`}>
|
||||
<div ref={rootRef} onBlur={onBlur} className={`relative w-full ${fullWidth ? "" : "sm:w-auto"}`}>
|
||||
<button
|
||||
type="button"
|
||||
aria-haspopup="listbox"
|
||||
|
||||
@@ -2,7 +2,8 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { AddToCartButton } from "./AddToCartButton";
|
||||
import { Reveal } from "./Reveal";
|
||||
import { getSpotlightProduct, getShippingSettings, getDefaultTaxRatePercent, getKleinunternehmer } from "../lib/payload";
|
||||
import { WishlistButton } from "./WishlistButton";
|
||||
import { getSpotlightProduct, getShippingSettings, getDefaultTaxRatePercent, getKleinunternehmer, getWishlistEnabled } from "../lib/payload";
|
||||
import { formatPrice, discountPercent } from "../lib/format";
|
||||
import { effectiveTaxRate } from "../lib/cartTotals";
|
||||
|
||||
@@ -23,11 +24,12 @@ import { effectiveTaxRate } from "../lib/cartTotals";
|
||||
* see Products.ts), not duplicated here as hardcoded literals.
|
||||
*/
|
||||
export async function ProductSpotlight() {
|
||||
const [product, shipping, defaultTaxRate, kleinunternehmer] = await Promise.all([
|
||||
const [product, shipping, defaultTaxRate, kleinunternehmer, wishlistEnabled] = await Promise.all([
|
||||
getSpotlightProduct(),
|
||||
getShippingSettings(),
|
||||
getDefaultTaxRatePercent(),
|
||||
getKleinunternehmer(),
|
||||
getWishlistEnabled(),
|
||||
]);
|
||||
if (!product) return null;
|
||||
|
||||
@@ -71,6 +73,9 @@ export async function ProductSpotlight() {
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
{wishlistEnabled && product.spotlightShowWishlist && (
|
||||
<WishlistButton productId={product.numericId} className="absolute top-3 right-3" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 items-start flex-1 min-w-0 w-full">
|
||||
|
||||
@@ -14,10 +14,24 @@ export function WishlistButton({
|
||||
productId,
|
||||
variant = "",
|
||||
className = "",
|
||||
revealOnHover = false,
|
||||
}: {
|
||||
productId: number;
|
||||
variant?: string;
|
||||
className?: string;
|
||||
/** false (default): always visible — right for single-product contexts
|
||||
* (ProductSpotlight, /konto/merkliste, the product detail page) where
|
||||
* there's no "wall of hearts" to thin out. true: invisible until the
|
||||
* card is hovered/focused, unless the product is already wishlisted (a
|
||||
* filled heart stays as a permanent status indicator) — right for a
|
||||
* multi-card grid (ProductGrid.tsx), where a heart on every single card
|
||||
* reads as visual noise (Marco: "sieht man überall Herzen", 2026-07-31).
|
||||
* Relies on the parent card already carrying `group`/`focus-within`
|
||||
* (see ProductGrid.tsx) — Tailwind's plain `:hover`, so tapping a card
|
||||
* on touch devices reveals it the same way the existing
|
||||
* `group-hover:-translate-y-1` card-lift already does, no separate
|
||||
* touch handling needed. */
|
||||
revealOnHover?: boolean;
|
||||
}) {
|
||||
const { isWishlisted, toggle } = useWishlist();
|
||||
const [pending, setPending] = useState(false);
|
||||
@@ -47,7 +61,11 @@ export function WishlistButton({
|
||||
aria-label={wishlisted ? "Von der Merkliste entfernen" : "Zur Merkliste hinzufügen"}
|
||||
aria-pressed={wishlisted}
|
||||
disabled={pending}
|
||||
className={`flex h-9 w-9 items-center justify-center rounded-full bg-bg-base/90 backdrop-blur-sm transition-transform active:scale-90 disabled:opacity-60 ${className}`}
|
||||
className={`flex h-9 w-9 items-center justify-center rounded-full bg-bg-base/90 backdrop-blur-sm transition-all active:scale-90 disabled:opacity-60 ${
|
||||
revealOnHover && !wishlisted
|
||||
? "opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100"
|
||||
: ""
|
||||
} ${className}`}
|
||||
>
|
||||
<svg width="20" height="18" viewBox="0 0 20 18" fill={wishlisted ? "currentColor" : "none"} className={wishlisted ? "text-brand" : "text-text-primary"}>
|
||||
<path
|
||||
|
||||
Reference in New Issue
Block a user