Checkout now persists orders server-side (Payload orders collection,
re-priced from live product data, discount codes redeemed exactly once)
instead of writing a client-only sessionStorage snapshot. Buying requires
an account (registration inline in checkout, no separate step) — accounts
get order history with delivery status, profile/address editing, password
change, and a cart that syncs across devices while logged in.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Discount codes:
- New shared lib/cartTotals.ts (computeSubtotal/computeCartTotals) factored
out of the previously-triplicated subtotal/totalSavings/total math in
CartContent/CheckoutContent/BestellbestaetigungContent, extended to also
fold in a discount amount (percent or fixed, clamped so total can't go
negative).
- lib/discount.ts mirrors lib/cart.ts's exact localStorage pattern so an
applied code survives the /cart -> /checkout transition without a second
input field — Checkout only displays it.
- New /api/discount/validate (read-only check) and /api/discount/redeem
(re-validates + increments the redemption counter, called once from
checkout's handlePurchase right before the OrderSnapshot is written).
Both talk to Payload's new discount-codes collection through
lib/discountServer.ts, a server-only module kept separate from
lib/payload.ts on purpose (that file is also imported by "use client"
components; the RSC-boundary break hit earlier this session was exactly
this mistake with next/headers).
- OrderSnapshot gains discountCode/discountAmount so /bestellbestaetigung
displays what was actually applied instead of losing it on recompute.
RelatedProducts: no longer falls back to re-suggesting a product already
in the cart just to pad the grid out to 3 cards — shows only the
genuinely available remainder (down to 1 card), centered in the 12-column
grid instead of left-aligned.
The delivery-time range (handling + transit days) was a hardcoded
HANDLING_DAYS/TRANSIT_DAYS_DE pair in lib/shipping.ts — changing it
needed a code deploy. Now sourced from Payload's new Shipping Settings
collection via getShippingSettings(), threaded down as a prop to the
few Client Components (Cart/Checkout/VersandModal) that can't fetch it
themselves, with the old code constants removed.
Also: the delivery-time note is now shown on every purchase CTA (shop
grid, home spotlight, ToDo-Karten hero + pricing panel), not just one
of them — required next to each buy button per Art. 246a §1 Abs.1
Nr.8 EGBGB, not just somewhere reachable via a link. Checkout's
sidebar was missing the "ab 39€ kostenlos" note Cart already had;
that's fixed too, and both now show the delivery-time range on its own
line instead of crammed onto the shipping-cost line.
Related smaller fixes bundled in since they touch the same files:
price/delivery-time spacing tightened into its own group, the
redundant "Sichere Zahlung" note under Cart's checkout button (already
shown via the trustBadges list right below) replaced with "Sichere
SSL-Verschlüsselung" to match Checkout, and ToDo-Karten's pricing panel
no longer shows a premature payment-security note at the add-to-cart
step.
Every email-signup form (Home/newsletter Newsletter section, /newsletter
hero, /challenge, the Newsletter overlay) now links "Datenschutzerklärung"
to /datenschutz next to its consent checkbox, opened in a new tab so a
partially filled form isn't lost. Checkout gets an equivalent AGB +
Datenschutzerklärung note under the order button, and the Versand
modal's Widerrufsbelehrung link also opens in a new tab.
Unified the "Keine Werbung. Jederzeit abbestellbar." trust note (icon +
#888 copy) across all newsletter forms, matching /challenge's existing
style instead of each form having its own wording/color.
Also converted the remaining raw <img> tags in these files to next/image
per the no-img-element lint rule (bandwidth/LCP).
"Du sparst -X,XX €" line under Zwischensumme, summing (compareAtPrice
- price) × qty across items with a discount — only shown when > 0,
same conditional-render convention as the rest of the sale-pricing
UI (badge/strikethrough already gated on discountPercent() !== null).
Matches the Figma mockup (checkmark hero, order summary card,
delivery-status panel, testimonial band) with the checkout's 4-step
bar inserted (all steps done) and the "Bis dahin: Lass dich
inspirieren" block omitted, per request.
Extracted the step bar into a shared CheckoutSteps component so
/checkout and /bestellbestaetigung don't duplicate it. The actually-
selected shipping cost and payment method are captured into a
sessionStorage snapshot by /checkout's "Jetzt kaufen" click (there's
no real order backend, so this click is what "placing the order"
means here) and read back on the confirmation page — not just
defaulted to the first active method of each, so the receipt matches
what the shopper actually picked. Also tightened the checkout
newsletter-consent copy ("Wenn du zustimmst" instead of "Wenn du
oben zustimmst").
Toggle between "Lieferadresse" and "Packstation" in the checkout
address form. Packstation swaps Straße und Hausnummer for
Packstationnummer + Postnummer — there's no house number for a
Packstation, so the field doesn't apply.
New Payload collections, all editable without a code deploy:
- TrustBadges: the horizontal Schneller-Versand/Versandkostenfrei/Mit-
Liebe-verpackt row (TrustRow.tsx, now an async server component).
- CartTrustBadges: the Sichere-Zahlung/14-Tage-Rückgaberecht/Nachhaltig-
verpackt sidebar bullets — shared by /cart (title only) and /checkout
(title + description), which previously had two different hardcoded
bullet lists for what's conceptually the same content.
- ShippingMethods: /checkout's Versandart radios. Each method has its own
optional freeShippingThreshold — omitted means "never free" (Express),
not "always free". /cart's FreeShippingBanner now targets the lowest
threshold among active methods instead of a single global constant, and
hides entirely if no active method has one.
- PaymentMethods: /checkout's Zahlungsart radios, icons as an array
(Kreditkarte shows 3 logos, PayPal/Überweisung show 1).
- Products: new spotlight/spotlightHeadline/spotlightText/spotlightImage/
compareAtPrice fields. ProductSpotlight.tsx (homepage) now shows
whichever product has `spotlight` checked instead of being hardcoded to
ToDo-Karten, with its own marketing copy separate from the plain
catalog name/description. AddToCartButton takes an explicit productId
prop now instead of a hardcoded "todo-karten" constant.
- WerkzeugeCards: the homepage's "Meine Werkzeuge" 3-card grid (Tools.tsx,
now async). Icons use a uniform box instead of the previous per-card
hand-tuned width/height/rotation, which only worked for 3 known,
upside-down-authored SVGs — those were re-exported as pre-flipped PNGs.
lib/payload.ts gained getTrustBadges/getCartTrustBadges/getShippingMethods/
getPaymentMethods/getWerkzeugeCards/getSpotlightProduct, all with the same
graceful-empty-array-on-fetch-failure pattern as the existing functions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both /cart's and /checkout's newsletter hint boxes phrased regular
marketing emails as an already-decided outcome ("bekommst du...").
The actual consent checkbox (unchecked by default) only exists on
/checkout's address form, and even there the copy didn't make the
outcome conditional on it. Pre-supposing opt-in like this is unlawful
under UWG/DSGVO (see the ECJ's Planet49 ruling on pre-checked consent) —
reworded both to frame it as conditional/an option, not a promise.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes the dead "Zur Kasse gehen" link on /cart. Built against the actual
Figma frame (page-checkout, node 4761:465, jCCZyh1DGwdjpv1wGge9To):
breadcrumb, 4-step progress stepper (Warenkorb done / Adresse active /
Zahlung+Abschluss upcoming), Rechnungsadresse form, Versandart (real
radio toggle — switching to Express actually updates the shown total),
Zahlungsart (Kreditkarte/PayPal/Überweisung with the real exported
payment-network logos), sidebar order summary reusing the same
cart/products data as /cart, trust bullets, and a newsletter hint box
(divider under the text, not the icon — deliberately different from
/cart's own version per this page's own Figma spec).
No real payment processing — "Jetzt kaufen" links to
/bestellbestaetigung (not yet built) same as the rest of this prototype
site's forms (cart, newsletter signup) don't hit a real backend either.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>