From a50524832eac785aaecbf8a4fc3c43cdc0d264ff Mon Sep 17 00:00:00 2001 From: Marco Date: Wed, 22 Jul 2026 23:25:18 +0000 Subject: [PATCH] Move low-stock hint to badge, right-align VAT breakdown, gate cart discount field, split billing/shipping delivery method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Removed the inline "Nur noch wenige verfügbar" text hint from AddToCartButton/AddToCartInlineButton (was making card heights vary in every grid that renders them — RelatedProducts, ProductSpotlight's CTA row) — now only shown via the same image-overlaid pill badge Ausverkauft/discount already use (position: absolute, doesn't affect layout). Added that badge to RelatedProducts.tsx and todo-cards' Pricing.tsx, which didn't have it before. - RelatedProducts cards now also show "inkl. X% MwSt." (was missing entirely) - VatBreakdown rows are now flex rows with a spacer instead of plain text, so every € amount right-aligns to the same edge regardless of how many digits the rate itself has (was visibly staggered with mixed 7%/19% rates) - Cart's manual discount-code field only renders when Payload actually has at least one active code right now (lib/discountServer.ts's new hasActiveDiscountCode()) — no point showing an open field that could never validate. An already-applied code (e.g. from an older session) still always shows its own result row regardless. - Checkout's "1. Rechnungsadresse" no longer offers a Packstation option — a Packstation isn't a valid billing address for an invoice. Only a plain street address now; Packstation is only offered on the separate, optional "Abweichende Lieferadresse" section, which already had its own address/Packstation toggle. Co-Authored-By: Claude Sonnet 5 --- app/cart/components/CartContent.tsx | 32 +++++- app/cart/components/RelatedProducts.tsx | 54 +++++++-- app/cart/page.tsx | 7 +- app/checkout/components/CheckoutContent.tsx | 109 ++++--------------- app/components/AddToCartButton.tsx | 20 ++-- app/components/AddToCartInlineButton.tsx | 15 +-- app/components/ProductSpotlight.tsx | 2 +- app/components/VatBreakdown.tsx | 26 +++-- app/lib/checkoutDraft.ts | 6 +- app/lib/discountServer.ts | 25 +++++ app/shop/components/ProductGrid.tsx | 2 +- app/todo-cards/components/Pricing.tsx | 16 ++- app/todo-cards/components/TodoKartenHero.tsx | 2 +- 13 files changed, 175 insertions(+), 141 deletions(-) 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} /> - +