Mark already-purchased items on the wishlist instead of auto-removing

A customer often wishlists something specifically to buy it again (gifts, repurchases) — silently removing it after purchase would defeat that. /konto/merkliste now shows a dimmed image + "Gekauft am [date]" badge instead, derived read-only from the customer's own orders (cancelled/returned orders excluded). Removal stays manual.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-31 13:51:10 +00:00
parent 8a4170a1e6
commit 230b8ebaad
4 changed files with 104 additions and 20 deletions
@@ -2,7 +2,7 @@
import Image from "next/image";
import { RevealGroup, RevealItem } from "../../../components/Reveal";
import { formatPrice, discountPercent } from "../../../lib/format";
import { formatPrice, discountPercent, formatDate } from "../../../lib/format";
import { AddToCartInlineButton } from "../../../components/AddToCartInlineButton";
import { WishlistButton } from "../../../components/WishlistButton";
import { useWishlist } from "../../../lib/useWishlist";
@@ -27,36 +27,51 @@ export function MerklisteGrid({
kleinunternehmer: boolean;
}) {
const { items } = useWishlist();
const wishlistedIds = new Set(items.map((i) => i.productId));
// Preserve the wishlist's own order (most-recently-added-first, via
// `items`) rather than initialProducts' own order.
// `items`) rather than initialProducts' own order. Zipped with the
// originating wishlist item (not just the product) so purchasedAt stays
// attached — the earlier `.map().filter()` chain that only kept the
// product lost that association.
const productsByNumericId = new Map(initialProducts.map((p) => [p.numericId, p]));
const visibleProducts = items.map((i) => productsByNumericId.get(i.productId)).filter((p): p is Product => Boolean(p));
const visibleEntries = items
.map((item) => ({ item, product: productsByNumericId.get(item.productId) }))
.filter((entry): entry is { item: (typeof items)[number]; product: Product } => Boolean(entry.product));
if (visibleProducts.length === 0) {
if (visibleEntries.length === 0) {
return <p className="text-body text-text-muted">Du hast noch keine Produkte gemerkt.</p>;
}
return (
<RevealGroup className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 w-full">
{visibleProducts.map((product) => {
{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} className="bg-bg-base border border-border rounded-md overflow-hidden flex flex-col h-full">
<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 ${fullyOutOfStock ? "opacity-60" : ""}`}
className={`object-cover ${item.purchasedAt || fullyOutOfStock ? "opacity-60" : ""}`}
/>
{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
{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>