Customers can now select which items and how many units to return
instead of only the whole order. The Gutschrift reflects only the
returned quantities, excludes shipping (already delivered), and leaves
the original discount untouched — confirmed policy, not an engineering
default. Stornorechnung (pre-shipping cancellation) is unaffected and
stays a full reversal including shipping.
Company data now has its own Payload admin group and a live in-browser
PDF preview (react-pdf's PDFViewer) instead of just a plain settings
form. Invoice header is a brand-colored rule instead of a filled band,
and the footer is now pinned to the page bottom instead of following
content flow.
Invoice + Stornorechnung/Gutschrift PDFs get a modern header-band layout,
a "bereits beglichen" badge for immediately-paid orders, labelled bank
details, and a per-tax-rate summary breakdown. Correction invoices can
now be re-downloaded from the account (regenerated deterministically,
not stored as files, same approach as the original invoice). Return
requests capture a reason. Products can define bundles (bundleItems) and
a per-product VAT rate override, both snapshotted onto order items.
Invoice PDFs (§14 UStG line items, tenant-configurable VAT rate) are now
generated at checkout and attached to the confirmation email, plus
available on demand from the order-detail page. Payload-side, orders now
also email the customer on shipped/cancelled/return_requested/returned,
with Stornorechnung/Gutschrift correction PDFs attached for the latter two
so the original invoice's immutable number stays honest.
The always-visible "Schon Kundin?" toggle was gendered and shown to every
logged-out visitor regardless of relevance. Card 1's email field now
checks on blur (/api/account/check-email) whether that address already
has an account, and only then swaps in a gender-neutral login form,
pre-filled — the collision check in handleSubmit stays as a fallback.
The order-confirmation and password-reset emails also got a real visual
pass: same warm background/brand color/circular success-icon treatment as
the on-screen /bestellbestaetigung page, serif heading, thin brand
divider, table-based layout for email-client compatibility. Copy is
on-brand and a little playful now instead of generic transactional
boilerplate.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Registering with an email that already has an account previously just
failed with a generic error and no clear next step. registerCustomer()
now flags emailExists specifically, and checkout switches straight to the
login toggle (email pre-filled, scrolled into view) instead. The account
icon also gets a small underline while logged in, matching the nav links'
active-state styling — it was otherwise the only nav element that gave no
visual signal of session state.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Password reset uses Payload's built-in forgot/reset-password flow,
customized to link to this app instead of the Payload admin. Order
confirmation email and the password-reset email's wording both come from
a new Payload email-templates collection, editable without a deploy and
previewable via Live Preview at /email-preview/[type] (same mechanism as
Posts/LegalPages/Testimonials, sample data instead of a real document).
Also: order numbers get a random suffix (prevents guessing, motivated by
a considered-and-deferred guest order-lookup feature); the discount code
field only shows in the cart when a code is actually active (codes now
apply via a ?code= link instead of manual entry); and three navigation
gaps found while testing — no reachable login link with an empty cart, no
logout link anywhere, no way back from profile to order history.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.
- Products gain `active`/`spotlight*`/`updatedAt` on the base Product type
(folded in from the now-removed separate SpotlightProduct type) so shop
grid, spotlight, and related-products can each filter `.active` from the
same already-fetched list — cart/checkout/order-confirmation/product-
detail pages keep resolving any product regardless of active status.
- getSpotlightProduct() now derives from getProducts() instead of its own
Payload query: with exactly 1 active product, that one IS the spotlight
(overriding any `spotlight` flag elsewhere); otherwise same
most-recently-updated tie-break as before, just computed client-side.
- ProductGrid drops the already-dead SHOP_GRID_EXCLUDE_IDS list in favor of
the same `active` filter, with an empty-state message if 0 active.
- RelatedProducts gates on >=2 active products regardless of cart contents
or how many display slots would otherwise resolve.
- Navbar's "Shop" link becomes an anchor to the homepage spotlight section
(id="spotlight") instead of a real /shop navigation whenever exactly 1
product is active — passed down from the now-async root layout, which
fetches the catalog once for this decision.
payload.ts's mapping functions/types are also imported by "use client"
components (LiveTestimonialsGrid, LivePostContent) — importing
next/headers anywhere in that module made it unbundlable for the client,
breaking the production build. draftMode() is now only ever called in the
Server Component pages themselves; they pass the resulting boolean into
getPostBySlug/getLegalPage/getTestimonials as a plain `draft` option.
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.
Both were fully hardcoded before: every blockquote showed a static
"Merke dir:" label, and every post's "Passend dazu" card always linked
the same flagship product. Now driven by two new Posts fields —
quoteLabel (empty hides the label/icon/underline, blockquote still
renders) and relatedProduct (empty hides the card entirely) — mirroring
Products.spotlight but per-post instead of a single site-wide flag.
README's collection table updated to match today's Payload changes
(shipping-settings, the new Posts fields, trust-badges' placeholder
tokens, admin sidebar grouping) — also fixed a stale claim that `media`
isn't tenant-scoped; it already was.
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.
Testimonial band: rebuilt as a proper flexbox two-column layout
(photo column with an explicit width, quote in a flex-1 sibling)
instead of an absolutely-positioned text block offset with
percentage margin/padding — that measured against the row's full
width and, combined with a max-w- on the text box, left almost no
room for the actual text on wide viewports (wrapped to one word per
line). Also dropped the muted-bg box behind the quote and widened
the photo fade to match the actual mockup, which has no separate
colored panel there at all.
Hero: removed the separate big checkmark badge — the 4-step bar
right above it already renders every step as a checkmark, so it was
just repeating that. Added more top spacing to compensate. Dropped
"Deine Bestellung macht sich jetzt auf den Weg zu dir." and added
"inkl. MwSt." under Gesamtbetrag for consistency with /cart and
/checkout.
Wired the new Products.spotlightEyebrow field (CMS-editable "Neu im
Shop" label) through lib/payload.ts into ProductSpotlight.tsx.
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").
Products with compareAtPrice set now show a "-XX%" badge over the
image plus a struck-through original price, wherever price is
displayed (shop grid, homepage spotlight, cart line items). Also
switched the "inkl. MwSt. zzgl. Versand" rows on Pricing.tsx and
ProductSpotlight from items-center to items-baseline — with a large
price next to small disclosure text, center alignment left the
small text visibly floating above the price's bottom edge.
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>
- Posts collection: new "featured" checkbox (Payload) — /blog now prefers
a manually-featured post over just the most recent one. Query sorts by
"-featured,-publishedAt" so multiple accidentally-featured posts
resolve deterministically (most recent among them wins) instead of
erroring or being ambiguous.
- challenge/page.tsx: step-connector arrow now uses the same
/icon-arrow-connector.svg asset as todo-cards/newsletter's HowItWorks,
replacing its own hand-drawn inline SVG.
- todo-cards & newsletter HowItWorks: connector arrows now vertically
centered on their icon (md:mt-[1.625rem]), matching Challenge's
margin-based centering technique instead of sitting flush at the row's
top edge.
- Blog overview: featured card overlaps the header slightly (-mt-8) per
Figma-inspired refinement, and now also shows the publish date in its
meta row (previously only the list items did).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- app/blog/page.tsx: header (title/subheading + bleeding photo with a
left-edge fade, per Figma node 4577:330), featured-post card (most
recent), remaining posts as a divided list with category/readTime/date,
and the reusable Newsletter signup panel. Uses all real posts from
Payload — the Figma mockup padded its list out to 4 items with two
posts that were never actually seeded, so this only renders what
exists (currently 1 featured + 2 listed).
- lib/payload.ts: BlogPost now carries publishedAt (needed for the list's
per-post date), removed the now-redundant duplicate field on PostDetail.
- Navbar: "Blog" now links to /blog instead of scrolling to the
homepage's #blog anchor — same real-route pattern as "Shop".
- blog/[slug]/page.tsx: earlier title-size tuning (fluid clamp between
text-h-feature and text-display) from this session's follow-up
feedback, not yet pushed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- app/blog/[slug]/page.tsx: article header (category/readTime, Playfair
display title, excerpt, byline), full-bleed hero image, RichText body,
"Passend dazu: ToDo-Karten" cross-promo, author bio card, "Weiterlesen"
related-post card. Widths, fonts, and the Passend-dazu card's
border/padding/spacing were corrected against the actual built Figma
frame (page-blog-detail, node 4667:344, jCCZyh1DGwdjpv1wGge9To) via
get_design_context after a few visually-wrong guesses.
- RichText.tsx: new "quote" case renders Lexical's blockquote feature as
the "Merke dir:" pull-quote — label+underline+divider+Caveat lines,
using the real exported sparkle/underline assets, matching Figma's
actual (background-less) structure instead of a guessed bordered card.
- lib/payload.ts: getPostBySlug() for single-post fetches.
- lib/format.ts: formatDate() — "03. Juli 2025"-style German dates.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- New /widerruf page: CMS-driven content via getLegalPage("widerruf"),
sidebar TOC + "Nachhaltig handeln" callout, TrustRow, and a download
card for the Muster-Widerrufsformular PDF attachment.
- New /agb page, same legal-page shell pattern as Impressum/Datenschutz.
- lib/payload.ts: getLegalPage now also resolves the optional `attachment`
media relation (depth=1) for legal pages that have a downloadable file.
- NewsletterModal: replaced the body overflow/position scroll-lock hacks
(which fought with the sticky Navbar and the global scroll-smooth CSS)
with a wheel/touch event interceptor that never touches scroll position
at all, plus `preventScroll: true` on all modal/drawer focus() calls to
stop the browser's implicit scrollIntoView from nudging the page for
elements near the sticky navbar's reserved scroll-padding-top zone.
Same preventScroll fix applied to VersandModal and the Navbar mobile
drawer for consistency.
- globals.css: scrollbar-gutter: stable, so a modal's overflow:hidden
lock never causes a scrollbar-width layout shift.
- Divider: stagger its reveal animation slightly (delay 0.3s) behind the
section above it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- lib/payload.ts: getLegalPage(type) alongside the existing
getBlogPosts/getProducts fetchers
- New RichText component: small dependency-free Lexical JSON → JSX
renderer for Payload's richText fields (headings get stable
"section-N" ids for anchor/TOC linking)
- New SectionTOC: generalized from VersandTOC into a reusable
scroll-spy sidebar driven by any {id, title}[] — Datenschutz needs one
built from CMS-authored headings, not a hardcoded array. VersandTOC is
now a thin wrapper around it. Sticky positioning moved from the nav
itself to each page's sidebar wrapper, so a TOC and an extra card
(Impressum/Datenschutz both have one) scroll together as one unit
instead of the card drifting away independently
- TOC clicks set the active item immediately and hold it for ~1s over
the scroll-spy observer, covering both "target already fully visible,
nothing to scroll" and "an unrelated section flickers through the
intersection band mid-scroll"
- Both pages follow the same breadcrumb+H1+TOC-sidebar+content layout
established for /versand
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/.
- New /shop overview, /cart (real cart state via useSyncExternalStore),
/versand (shipping policy page with TOC) and /newsletter detail page
- Cart: quantity/removal, order summary, related-products cross-sell
with randomized picks, VersandModal quick-reference instead of
navigating away, MwSt. disclosure next to unit prices
- Add-to-cart UX: inline success feedback (green state) plus a
fly-to-navbar-cart-icon animation (CartFlyProvider) with a delayed
badge count-up; AddToCartButton no longer navigates straight to /cart
- Shared lib/products.ts catalog and lib/shipping.ts constants (cost,
free-shipping threshold, handling/transit days) so cart, trust badges
and the versand page can never drift apart
- Fix Navbar smooth-scroll easing (ease-out instead of ease-in-out, no
more perceived start delay); compress newsletter modal photo
2.4MB -> 85KB to fix first-open jank
New /todo-cards page (Hero, How-it-works, Focus, Testimonials, Pricing)
built with the fluid clamp() token system (app/lib/fluid.ts) and
scroll-reveal animations (app/components/Reveal.tsx), matching styling
consistency with /challenge's testimonial section.
Navbar: page-aware active state and anchor-link navigation from any
route (not just "/"), fixed logo click to actually navigate instead of
silently rewriting the URL, fluid nav text size, custom hash-scroll
handling for cross-page anchor links.
Hero: responsive breakpoint fix for the text/image split at Tablet
widths, entrance animation for the brand's orange dot, removed a red
dot artifact from hero.png.
Also includes prior uncommitted work on About/Blog/Newsletter/Footer
and the cart lib (app/lib/cart.ts).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Blog.tsx now fetches the einfach-produktiv tenant's posts from the
shared Payload instance at build/render time (ISR, 60s revalidate),
via a small typed fetch helper in app/lib/payload.ts.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>