Add wishlist feature (frontend), gated by CompanySettings.wishlistEnabled
New: useWishlist.ts (fetch+optimistic-toggle hook, server-backed since a wishlist needs a logged-in customer, unlike the guest-friendly cart), WishlistButton.tsx (heart toggle, login-redirects on 401), Navbar's wishlist icon+badge (hidden below sm: — Account+Cart are the only always-visible icons on true mobile, a 3rd icon there risks the same computed nav-overflow class of bug documented in the figma-to-nextjs skill), and /konto/merkliste (list page, 404s if the feature gets disabled after a customer already has rows). Product gained numericId (the raw Payload id) alongside its existing slug id — WishlistItems.product is a real numeric relationship field, unlike cart/checkout's slug-keyed "commerce id". Backend counterpart (WishlistItems collection, CompanySettings toggle, migration) already deployed separately. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import Image from "next/image";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useCartCount } from "../lib/cart";
|
||||
import { useWishlist } from "../lib/useWishlist";
|
||||
import { AUTH_CHANGED_EVENT } from "../lib/auth";
|
||||
import { NewsletterModal } from "./NewsletterModal";
|
||||
import { useCartFly } from "./CartFly";
|
||||
@@ -131,6 +132,40 @@ function AccountLink() {
|
||||
);
|
||||
}
|
||||
|
||||
// Wishlist icon + count badge — only rendered by the caller when
|
||||
// `wishlistEnabled` (CompanySettings), and even then hidden below `sm:`.
|
||||
// Account+Cart are the only always-visible icons on true mobile (see the
|
||||
// trailing-controls group's own comment: gap-2 there was already tuned
|
||||
// specifically for exactly 2 icons) — a 3rd icon squeezed in at the
|
||||
// smallest phone widths risks the exact nav-overflow class of bug
|
||||
// documented in the figma-to-nextjs skill (computed hamburger/icon-row
|
||||
// thresholds, not assumed ones). sm+ has real room to spare.
|
||||
function WishlistLink() {
|
||||
const { count } = useWishlist();
|
||||
|
||||
return (
|
||||
<Link
|
||||
href="/konto/merkliste"
|
||||
aria-label={count > 0 ? `Merkliste, ${count} Artikel` : "Merkliste"}
|
||||
className="relative hidden sm:flex h-11 w-11 items-center justify-center shrink-0 active:scale-[0.9] transition-transform"
|
||||
>
|
||||
<svg viewBox="0 0 20 18" className="h-6 w-6 text-text-primary" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M10 17S1 11.5 1 5.8C1 2.6 3.4 1 5.8 1c1.6 0 3.2.9 4.2 2.4C11 1.9 12.6 1 14.2 1 16.6 1 19 2.6 19 5.8 19 11.5 10 17 10 17Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.6"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
{count > 0 && (
|
||||
<span className="absolute top-0 right-0 flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-brand px-1 text-[0.6875rem] font-bold leading-none text-white">
|
||||
{count > 99 ? "99+" : count}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
// Cart icon + count badge — traced from the Figma Navbar/Default component's
|
||||
// btn-cart (icon-cart 32x30 + cart-count-badge, node 4849:24). Visible at
|
||||
// every breakpoint tier (unlike the nav links / CTA buttons, which move into
|
||||
@@ -210,7 +245,7 @@ function CartLink() {
|
||||
);
|
||||
}
|
||||
|
||||
export function Navbar({ singleActiveProduct }: { singleActiveProduct: boolean }) {
|
||||
export function Navbar({ singleActiveProduct, wishlistEnabled }: { singleActiveProduct: boolean; wishlistEnabled: boolean }) {
|
||||
const pathname = usePathname();
|
||||
const [scrolled, setScrolled] = useState(false);
|
||||
const [activeSection, setActiveSection] = useState("");
|
||||
@@ -510,6 +545,7 @@ export function Navbar({ singleActiveProduct }: { singleActiveProduct: boolean }
|
||||
these two icons are the only always-visible controls. */}
|
||||
<div className="flex items-center">
|
||||
<AccountLink />
|
||||
{wishlistEnabled && <WishlistLink />}
|
||||
<CartLink />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useWishlist } from "../lib/useWishlist";
|
||||
|
||||
// Heart-toggle for a product card/detail page. Login-gated (unlike the
|
||||
// cart, which works for guests) — a logged-out click redirects to
|
||||
// /konto/login?redirect=<back-here> instead of silently failing, since
|
||||
// there's no local-storage fallback that would make sense for a wishlist
|
||||
// (see useWishlist.ts's own comment on why this can't reuse cart.ts's
|
||||
// guest-friendly pattern).
|
||||
export function WishlistButton({
|
||||
productId,
|
||||
variant = "",
|
||||
className = "",
|
||||
}: {
|
||||
productId: number;
|
||||
variant?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const { isWishlisted, toggle } = useWishlist();
|
||||
const [pending, setPending] = useState(false);
|
||||
const router = useRouter();
|
||||
const wishlisted = isWishlisted(productId, variant);
|
||||
|
||||
async function handleClick(e: React.MouseEvent) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (pending) return;
|
||||
|
||||
const res = await fetch("/api/account/wishlist", { method: "GET", cache: "no-store" });
|
||||
if (res.status === 401) {
|
||||
router.push(`/konto/login?redirect=${encodeURIComponent(window.location.pathname)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
setPending(true);
|
||||
await toggle(productId, variant);
|
||||
setPending(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
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}`}
|
||||
>
|
||||
<svg width="20" height="18" viewBox="0 0 20 18" fill={wishlisted ? "currentColor" : "none"} className={wishlisted ? "text-brand" : "text-text-primary"}>
|
||||
<path
|
||||
d="M10 17S1 11.5 1 5.8C1 2.6 3.4 1 5.8 1c1.6 0 3.2.9 4.2 2.4C11 1.9 12.6 1 14.2 1 16.6 1 19 2.6 19 5.8 19 11.5 10 17 10 17Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1.5"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user