Complements Payload's per-account login lockout with per-IP rate limiting
on auth routes; proxy.ts silently refreshes an active customer's session
via Payload's built-in refresh-token endpoint instead of a long-lived
token. Registration now sends a non-blocking email-verification link
(doesn't gate login, since checkout registers and immediately logs in
mid-purchase). /konto/profil gets GDPR export/delete; order detail pages
get self-service cancel/return-request, backed by a Payload hook that
closes a real gap (a customer's JWT could previously PATCH any field of
their own order, not just status). Checkout failures now email an alert
independent of Payload's own health, since Kuma's uptime checks can't see
an order silently failing to persist.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.
Testimonials on /todo-cards, /newsletter, /challenge now come from the new
Payload testimonials collection via a shared TestimonialsGrid component,
instead of 3 separately hardcoded arrays.
Adds Next.js Draft Mode (/api/preview) plus Live-Preview-aware client
wrappers (LiveRichText, LiveTestimonialsGrid, LivePostContent) for posts,
legal pages, and testimonials — mounted only while Draft Mode is enabled,
so ordinary visitors keep getting the plain static components.
Products now come from Payload's new "products" collection instead of a
hardcoded catalog, same pattern already used for blog posts:
- lib/payload.ts: getProducts()/getProductBySlug() (server-side fetch,
60s ISR)
- New /api/products route so client components (CartContent,
RelatedProducts) can reach the same data without a server-only import
- lib/products.ts: useProducts() hook replacing the old PRODUCTS record
- ProductGrid (/shop) fetches server-side directly; now shows all
catalog products except notizbuch-klarheit (matches Figma's 4-card
page-shop-overview — still cross-sold via RelatedProducts)
- ProductSpotlight and /todo-cards' Pricing now pull price/photo from
the same CMS product instead of a separately hardcoded "12,90 €", so
the two can't silently drift apart
- formatPrice moved to a new lib/format.ts (plain, no "use client") —
Server Components can't call functions exported from a "use client"
module directly, which lib/products.ts now is because of the hook
Also fixes two unrelated bugs surfaced along the way: the add-to-cart
button visibly resizing when its "Hinzugefügt ✓" success state showed
(fixed with a CSS-grid text stack sized to the wider of the two
strings), and removes the now-unused local product images from public/.