"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).
Same fixed-height + -inset-1 treatment as page-bestellbestaetigung's
own testimonial band, and the same class of top-edge boundary
artifact turned up in 404-testimonial-photo.jpg too (invisible in a
downscaled preview, visible once object-cover scales it up in the
browser) — re-cropped 15px further down to clear it.
Also shrunk both pages' testimonial band from h-[20rem]/24rem to
h-[14rem]/16rem — the taller version left too much empty vertical
space around the 3-line quote.
feat(cart): show the -XX% sale badge on cart line-item photos too
Same badge already used in the shop grid/spotlight/todo-cards
pricing panel, now also on /cart's product thumbnails when that
item has a compareAtPrice set.
The 58%-wide/no-muted-bg rewrite left a large visible gap between
the photo and the quote on wide viewports, and its very wide 4.9:1
photo crop needed heavy object-cover zooming to fill a narrower
column. Reverted to the exact structure already shipped and working
on /not-found (w-[45%] column, 40px edge gradient into bg-muted) —
the only real change kept is a fixed h- instead of min-h-, which is
what actually fixed the original height-mismatch complaint.
Also re-cropped the photo twice: first to match 404-testimonial-
photo.jpg's 2.65:1 aspect ratio (avoids the aggressive cropping/zoom
a too-wide source needs to fill a taller column), then pushed the
top edge down further after finding the crop still included a few
residual pixels of a cream/shadow boundary line from the source
mockup — invisible in a downscaled preview but a visible seam once
the browser scales the 126px-tall source up 2-3x via object-cover.
Also overflows the image 4px past its container (-inset-1 instead of
inset-0) as a defensive measure against sub-pixel gaps in general.
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").
The hero's "ToDo-Karten bestellen" button had no price anywhere near
it — a shopper had to scroll past the whole page to the Pricing
panel to find out what it costs. Adds a compact price line above
the button (same strikethrough+price style as ProductSpotlight),
fetched from the same "todo-karten" product Pricing.tsx already
reads further down.
Pricing.tsx got the items-baseline alignment fix in the last commit
but the actual discount display (badge + strikethrough) was never
wired in, unlike the shop grid and spotlight that got both.
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.
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.
The catalog only has 4 products; with 2 already in the cart, the
initial pick's pool shrinks to 2 and pickRandom's slice can't pad
past what's available, so the grid rendered 2 cards instead of 3.
The swap-after-add path already had fallback logic for this exact
case — extracted it into pickWithFallback() and reused it for the
initial pick too.
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>
QuoteNode's children are flat text/linebreak nodes directly (confirmed
by reading @lexical/rich-text's source — insertNewAfter exits the quote
into a new paragraph on Enter, it doesn't nest one inside). The "quote"
case assumed nested paragraph children, which only happened to work for
this session's own hand-authored seed JSON; any blockquote actually
typed in the Payload admin editor rendered blank, since child.children
was undefined on a plain text/linebreak node.
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>
- Replace both placeholder images with the actual exports pulled from
the built Figma page-404 (node 4692:341, file jCCZyh1DGwdjpv1wGge9To):
testimonial-photo (4699:372) at full 810x306 resolution, and the
sticky-note-404 (4693:382) — a real photographed/rendered note, not the
hand-coded CSS+Caveat approximation this had before.
- Help-link icon hover: subtle scale-110 zoom (matching Testimonials.tsx's
avatar hover) instead of a color inversion.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Swap the testimonial band's placeholder photo for one cropped directly
from the actual mockup pixels (mockups/einfach-produktiv_mockup_404.png)
instead of reusing an unrelated existing site asset.
- Remove the search bar — no search backend exists to wire it to, so it
was a dead decorative element.
- Add micro-interactions: main CTA arrow slides right on hover, sticky
note un-rotates on hover, help-link cards lift + icon circle inverts to
brand fill + arrow slides right on hover.
- Help-link CTAs ("Zur Startseite" etc.) now text-text-primary (black)
instead of text-brand.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
app/not-found.tsx — root not-found handles all unmatched routes
automatically (Next 16, no global-not-found flag needed for a single
root layout). Hero (Ups. / subheading / CTA) with a natively-drawn
sticky-note illustration (Lora "404" + Caveat handwritten line, no
image asset needed), 3 help links (Startseite/Blog/Werkzeuge), a
decorative search bar (no search backend exists yet to wire it to),
photo+quote testimonial band, TrustRow, Footer. Sets robots: noindex.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
backdrop-filter (backdrop-blur-md, applied to the sticky header once
scrolled) creates a new containing block for position:fixed descendants,
same as transform does. NewsletterModal was rendered inside that header,
so its `fixed inset-0` backdrop silently stopped being fixed to the
viewport once scrolled and became fixed to the 100px header instead,
clipping the modal to the top of the page. Moved the modal to be a
sibling of <header> instead of a child.
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/.
Shows "Noch X € bis zum kostenlosen Versand" with a progress bar while
below the threshold; briefly confirms "freigeschaltet ✓" on crossing it,
then auto-hides. Re-appears if the subtotal drops back below the
threshold (e.g. after removing an item).
- 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>
Strategy: one layout breakpoint at lg (1024px), clamp() for fluid type.
Navbar: hamburger menu (animated ✕) with max-h dropdown, resize-close,
h-20 mobile / h-[6.25rem] desktop, blur triggers on scroll or menu-open.
Hero: flex-col→lg:flex-row, clamp(2rem,5vw,4rem) heading, background-image
scales responsively, gradient mask via .hero-mask only at lg+ in CSS.
Divider: flex-wrap + gap-y for smaller screens.
Tools: grid-cols-1→md:2→lg:3, removed fixed height and per-card paddingRight.
Blog: featured flex-col→lg:flex-row, secondary flex-col→md:flex-row,
excerpt hidden on mobile secondary cards to save space.
About: flex-col→lg:flex-row, clamp on quote size, min-h-[18rem] for photo.
Newsletter: flex-col→lg:flex-row, input+button stack on xs / row on sm+.
Footer: flex-col→lg:flex-row, flex-wrap on legal links.
globals.css: scroll-padding-top 5rem mobile / 6.25rem desktop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Featured had 8px (only content px-2, no card gap).
Secondary had 16px (gap-2 + content p-2 left).
Both now use gap-3 (12px) at card level with no left padding on content.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces browser scroll-behavior:smooth (no duration control) with a
requestAnimationFrame loop using cubic ease-in-out over 800ms. Offset
accounts for navbar height (100px) via getBoundingClientRect + scrollY.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Next.js Link appends hash instead of replacing it (#a#b#b...). Anchor
links now use native <a> with e.preventDefault(), manual scrollIntoView,
and history.replaceState so only the current hash appears in the URL.
Page routes (/shop etc.) still use Link as before.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
flex-1 (shrink:1) let the text column grow past 50% by shrinking the
image. flex-[1_0_0] (shrink:0) matches Figma's flex:1 0 0 so neither
column can shrink — both are exactly half the section width.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Navbar is sticky top-0 z-50. On scroll >8px: bg opacity drops to 80%
and backdrop-blur-md activates with a subtle shadow. Transitions on
background-color and backdrop-filter (300ms). Smooth scrolling via
html { scroll-behavior: smooth } in globals.css.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>