diff --git a/app/cart/components/CartContent.tsx b/app/cart/components/CartContent.tsx index 56df4bd..58cbb6e 100644 --- a/app/cart/components/CartContent.tsx +++ b/app/cart/components/CartContent.tsx @@ -22,6 +22,7 @@ export function CartContent({ freeShippingThreshold, shippingSettings, defaultTaxRate, + showDiscountField, }: { trustBadges: TrustBadge[]; /** Price of the default (first active, i.e. Standard) ShippingMethod — an @@ -41,6 +42,13 @@ export function CartContent({ * override taxRatePercent themselves — see lib/cartTotals.ts's * effectiveTaxRate(). */ defaultTaxRate: number; + /** Whether Payload currently has at least one active discount code at + * all (lib/discountServer.ts's hasActiveDiscountCode()) — no point + * showing an open "enter a code" field when nothing could ever validate + * against it. Only gates the manual-entry form; a code already applied + * (e.g. from an earlier session, or one deactivated after being shared) + * still shows its own result row regardless. */ + showDiscountField: boolean; }) { const [versandOpen, setVersandOpen] = useState(false); const cart = useCart(); @@ -275,10 +283,14 @@ export function CartContent({ )} - {/* Rabattcode — manual input when nothing's applied yet; - once active, just the result + "Entfernen" (also reached - via a direct link with a prefilled code, see the useEffect - above). /checkout mirrors this exact block, sharing state + {/* Rabattcode — manual input when nothing's applied yet AND + Payload actually has at least one active code right now + (showDiscountField — no point offering an open field + that could never validate against anything); once + active, always shows the result + "Entfernen" regardless + of showDiscountField (also reached via a direct link + with a prefilled code, see the useEffect above). + /checkout mirrors this exact block, sharing state through lib/discount.ts's localStorage store. */} {discount ? (
@@ -295,7 +307,7 @@ export function CartContent({ Entfernen
- ) : ( + ) : showDiscountField ? (
{ e.preventDefault(); @@ -324,6 +336,16 @@ export function CartContent({ {discountError &&

{discountError}

} {discountLoading &&

Rabattcode wird geprüft…

}
+ ) : ( + // No manual field to attach an error to (no active codes + // exist at all right now) — but a ?code= URL param can + // still trigger the auto-apply attempt above regardless + // of showDiscountField, so its failure needs somewhere to + // show. + <> + {discountError &&

{discountError}

} + {discountLoading &&

Rabattcode wird geprüft…

} + )}
diff --git a/app/cart/components/RelatedProducts.tsx b/app/cart/components/RelatedProducts.tsx index 4cc9caf..ea01525 100644 --- a/app/cart/components/RelatedProducts.tsx +++ b/app/cart/components/RelatedProducts.tsx @@ -3,7 +3,8 @@ import { useEffect, useMemo, useRef, useState } from "react"; import Image from "next/image"; import { useProducts } from "../../lib/products"; -import { formatPrice } from "../../lib/format"; +import { formatPrice, discountPercent } from "../../lib/format"; +import { effectiveTaxRate } from "../../lib/cartTotals"; import { Reveal } from "../../components/Reveal"; import { AddToCartInlineButton, FEEDBACK_MS } from "../../components/AddToCartInlineButton"; import { useCart } from "../../lib/cart"; @@ -29,7 +30,7 @@ function pickAvailable(allIds: string[], excludeIds: string[], keep: string[], c return [...keep, ...pickRandom(allIds, [...excludeIds, ...keep], missing)]; } -export function RelatedProducts() { +export function RelatedProducts({ defaultTaxRate }: { defaultTaxRate: number }) { const cart = useCart(); const products = useProducts(); // Cart/checkout resolve any product regardless of `active` (see @@ -112,12 +113,7 @@ export function RelatedProducts() {

- {/* No separate price-disclosure footnote here — the single - "* inkl. MwSt., zzgl. Versandkosten" note lives directly under - the cart's own product table instead (CartContent.tsx), close - enough on the same page view to cover these cards too. - - Plain divs, not RevealGroup/RevealItem — this is the one grid on + {/* Plain divs, not RevealGroup/RevealItem — this is the one grid on the site whose items get swapped after the initial mount (see the swap-in-place effect above). RevealItem has no viewport trigger of its own; it only ever renders visible because it @@ -128,7 +124,13 @@ export function RelatedProducts() { scroll-reveal nicety on a list that mutates; a static grid renders correctly with no animation risk. */}
- {displayProducts.map((product, i) => ( + {displayProducts.map((product, i) => { + const discount = discountPercent(product.price, product.compareAtPrice); + const taxRate = effectiveTaxRate(product, defaultTaxRate); + // Same "any vs. every" split as ProductGrid.tsx. + const fullyOutOfStock = product.variants.length > 0 ? product.variants.every((v) => v.outOfStock) : product.outOfStock; + const anyLowStock = product.variants.length > 0 ? product.variants.some((v) => v.lowStock) : product.lowStock; + return (
+ {/* Same top-left pill pattern as ProductGrid.tsx/ + ProductSpotlight.tsx — position: absolute, so it never + affects this card's height the way the old inline + low-stock text hint under the button used to (that + variability was exactly what broke equal card heights + in this grid). */} + {fullyOutOfStock ? ( + + Ausverkauft + + ) : discount !== null ? ( + + -{discount}% + + ) : ( + anyLowStock && ( + + Nur noch wenige verfügbar + + ) + )}

{product.name}

-

{formatPrice(product.price)}

- +

+ {discount !== null && ( + {formatPrice(product.compareAtPrice!)} + )} + {formatPrice(product.price)} + inkl. {taxRate}% MwSt. +

+
- ))} + ); + })}
); diff --git a/app/cart/page.tsx b/app/cart/page.tsx index 25b54c5..4378af6 100644 --- a/app/cart/page.tsx +++ b/app/cart/page.tsx @@ -5,6 +5,7 @@ import { RelatedProducts } from "./components/RelatedProducts"; import { TrustRow } from "../components/TrustRow"; import { Footer } from "../components/Footer"; import { getCartTrustBadges, getShippingMethods, getShippingSettings, getDefaultTaxRatePercent } from "../lib/payload"; +import { hasActiveDiscountCode } from "../lib/discountServer"; // robots: noindex — transactional page (mirrors a specific shopper's cart // contents), per the figma-to-nextjs skill's Step 5 guidance: indexing @@ -19,11 +20,12 @@ export const metadata: Metadata = { }; export default async function CartPage() { - const [trustBadges, shippingMethods, shipping, defaultTaxRate] = await Promise.all([ + const [trustBadges, shippingMethods, shipping, defaultTaxRate, showDiscountField] = await Promise.all([ getCartTrustBadges(), getShippingMethods(), getShippingSettings(), getDefaultTaxRatePercent(), + hasActiveDiscountCode(), ]); // The cart doesn't ask which shipping method the shopper wants yet @@ -53,9 +55,10 @@ export default async function CartPage() { freeShippingThreshold={freeShippingThreshold} shippingSettings={shipping} defaultTaxRate={defaultTaxRate} + showDiscountField={showDiscountField} /> - +