A static short link — e.g. printed on a QR code — that resolves to a
Payload-editable internal target path, so the link itself never needs
reprinting when the underlying content moves. Tracks a click count.
- AGB: name/address/email now come from company-settings via new
VertragspartnerBlock (mid-document, see LegalPages.ts's contentPart2).
- VersandSections/VersandModal/CartContent/CheckoutContent/versand page:
shipping cost and free-shipping threshold now come from the real
ShippingMethods data (already fetched elsewhere for checkout), not the
hardcoded lib/shipping.ts constants — those had already drifted from
the real Payload values once. lib/shipping.ts deleted, nothing left to
export.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- ProductGallery: touch swipe (left/right) now navigates slides on mobile,
previously click-only.
- New shared ProductCard component used by ProductGrid/RelatedProducts/
MerklisteGrid — product title is now the card's link (replaces the
separate "Mehr erfahren" line), consistent aspect ratio across all
three grids, more compact cards.
- Datenschutz: name/address/email now come from company-settings via a
new VerantwortlicherBlock, same single-source pattern as Impressum's
AnbieterAngaben — no more hand-typed address to keep in sync.
- layout.tsx: render googleSearchConsoleVerification via Next's native
verification.google metadata field.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Uses Klaro's own built-in contextual-consent mechanism (not
loadTrackingCode.ts's script-injection path) — the iframe renders
with data-name="google-maps" and its real src; Klaro's DOM scan finds
it after mount and blanks/restores src based on that service's
consent state, showing its own placeholder (styled via
KlaroConsentManager.tsx's KlaroTheme) in the meantime.
No map embedded anywhere in the app yet — this is prep work only, per
explicit request ahead of an actual page needing one.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ProductGallery.tsx (main image + thumbnail strip) wired into
TodoKartenHero.tsx, only replacing the static hero photo once a
product actually has extra gallery photos — no visual change
otherwise. Backend field: Products.gallery (see payload repo).
Klaro's hard black border was still showing after the previous
color-only pass — the targeted border:none rule wasn't enough, so
this blanket-resets border/outline on every .klaro descendant and
re-adds only the shadow this theme actually wants. Also disables the
"Realisiert mit Klaro!" attribution link (BSD-3-Clause has no
on-page-attribution requirement, Klaro's own disablePoweredBy flag
covers this cleanly).
Updates the frontend README with the gallery + Klaro fixes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CookieBanner.tsx (equally-weighted Akzeptieren/Ablehnen, TTDSG) +
useConsent() cookie hook + TrackingScripts.tsx render active
tracking-codes rows only once their consentCategory is actually
accepted ('necessary' always renders). Wired into layout.tsx via the
new getTrackingCodes() fetcher. This is what makes the Phase 1
backend collection (tracking-codes) actually usable end to end.
Also reworks NotifyMeForm back to always-visible input+button (better
UX than a collapse-to-reveal step) — the resulting taller CTA is now
reserved on every card via NotifyMeFormReservedSpace, an invisible
twin rendered behind the real button, so an in-stock card's row
height matches an out-of-stock sibling's without the grid's
row-stretch pushing buttons out of alignment.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
'back-in-stock' was added to the Payload-side email-templates type
but never wired into this repo's own EmailTemplateType union or the
Live Preview page's VALID_TYPES/fallback-heading maps — every other
type is registered in three places, this one was only in one, so
opening its Live Preview 404'd.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Products now carry an optional categories relationship (backend:
new product-categories collection mirroring the blog's categories
pattern, migration applied and deployed). The shop page gains a left
sidebar (styled like AccountNav) with a dual-handle price slider,
category checkboxes, and an availability toggle — all instant-apply
via searchParams, same union-filter semantics as the blog's category
chips. Uncategorized products always match every category filter
rather than disappearing.
Also fixes CTA buttons sitting at different heights across sibling
cards when one product's title wraps to two lines — MerklisteGrid.tsx
and RelatedProducts.tsx get the same h-full/flex-1 spacer pattern
ProductGrid.tsx already used.
- CustomSelect.tsx: mousedown on an option blurred the trigger button
before the click landed, unmounting the list before anything could be
selected — add preventDefault on the list's mousedown to stop that.
- KontoShell no longer renders "Eingeloggt als ..." above each page's
content; each page renders its own title then AccountIdentity right
below it, and ProfileForm's now-duplicate email/Kundennummer line is
removed.
- Blog category filter: drop the separate "Zurücksetzen" link — clicking
an active category badge again already deactivates it.
- Werkzeuge cards: more vertical gap between stacked cards below sm:.
- Legal pages' "Stand: ..." line is now derived from the LegalPages doc's
own updatedAt instead of a hand-typed string.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018unaXmuzVA8ct1b6WoyP1U
Wires the new backend DHL endpoints into checkout: an address-autocomplete
dropdown on the street fields, live Postnummer validation for Packstation
delivery, and a return-label download link on the order-detail page.
Proxied through Next.js API routes since DHL credentials are tenant-
specific and CheckoutContent is a Client Component.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixed: blog category filter bar was rendering behind the featured-post
card — that card pulls itself up (-mt-8, z-10) to overlap the hero's
bottom edge (its original design), but inserting the filter bar
between the hero and that card meant the same pull now overlapped the
filter bar instead, with the card's stacking rendering on top. Given
relative z-20 to stay above that overlap regardless. Also bumped the
inactive chip's background to bg-bg-muted — border-border on bg-base
is only a ~2% lightness difference, nearly invisible as a pill outline.
Both filters (shop price, blog category) now gated behind new
CompanySettings toggles (shopFilterEnabled/blogFilterEnabled), same
off-by-default pattern as wishlistEnabled/searchEnabled.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Search icon now gated behind the new backend toggle, same pattern as
wishlistEnabled — off by default.
Fixed: blog category filter bar was invisible — it was wrapped in
<Reveal>, which sits right at/just past the hero's bottom edge, the
exact "already near the initial viewport" position Reveal.tsx's own
comment documents as a whileInView(margin:"-80px") trap (an element
already visible without scrolling can permanently never register as
"entered view", since `once: true` never gets a second chance). Now a
plain div, no scroll-reveal animation needed for a filter bar anyway.
Fixed: wishlist count showing one behind on the Navbar badge —
useWishlist.ts's toggle() broadcast the "refetch me" event immediately
after the optimistic local update, before the POST request was even
sent. Another instance's resulting refetch could race the actual
server-side write, get the pre-toggle list, and then never get told to
refetch again. Broadcast now only fires after the request settles.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New: useWishlist.ts (fetch+optimistic-toggle hook, server-backed since
a wishlist needs a logged-in customer, unlike the guest-friendly cart),
WishlistButton.tsx (heart toggle, login-redirects on 401), Navbar's
wishlist icon+badge (hidden below sm: — Account+Cart are the only
always-visible icons on true mobile, a 3rd icon there risks the same
computed nav-overflow class of bug documented in the figma-to-nextjs
skill), and /konto/merkliste (list page, 404s if the feature gets
disabled after a customer already has rows).
Product gained numericId (the raw Payload id) alongside its existing
slug id — WishlistItems.product is a real numeric relationship field,
unlike cart/checkout's slug-keyed "commerce id".
Backend counterpart (WishlistItems collection, CompanySettings toggle,
migration) already deployed separately.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New sendPaymentSwitchedEmail() — sent instead of sendOrderConfirmationEmail()
when confirmPaymentEmail.ts sees order.paymentSwitchedAt set. Resending the
full order-confirmation (with its invoice re-attached) after a payment-method
switch read like a brand-new purchase; this is a short, dedicated "Zahlung
erhalten" confirmation instead, matching the order-status-email shape.
Also bumps @einfach-produktiv/invoicing to 0.2.8, fixing a font-path bug
that broke every invoice PDF render inside the Payload backend specifically.
- Posts.categories is now hasMany — blog list/detail/live-preview render
a comma-joined list instead of a single category.
- Checkout's "Abweichende Lieferadresse" gains optional Firma + Kontakt-
E-Mail/Telefon fields (handed to the shipping carrier, not used for
customer communication).
- Customer profile can now store its own shipping address (mirroring
Customers.ts's new "Lieferadresse" tab), prefilling the checkout
override instead of always starting blank.
- Product-level optional SKU (previously only on variants) snapshots onto
each order item and shows up on invoices (visual + EN16931 XML),
the confirmation email, and the account order detail page.
getBlogPosts()/getPostBySlug() now only return status='published' posts
for normal requests — draftMode's live preview passes {draft:true} to
bypass it, same as before. Backend-side scheduling in the payload repo
(status/scheduledPublishAt + a per-minute autopublish job).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- api/checkout/route.ts: authoritative shipping charge is 0 whenever
every cart line opts out via noShippingCost, regardless of the
free-shipping threshold.
- Cart/checkout order summaries: the whole "Versand" line (cost, free-
shipping note, delivery time) is hidden entirely rather than showing
"Kostenlos" — that's a different state from hitting the threshold.
- ProductSpotlight/Pricing/TodoKartenHero: "zzgl. Versand" and delivery-
time hints drop for an exempted product's own page.
- /versand + its shared modal: one clarifying sentence that digital
products are exempt.
- Widerrufsformular link: opens inline in a new tab (no forced download),
arrow icon changed from a download glyph to a plain right arrow to
match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mirrors the backend's sellerInfo.ts buildFromHeader() change — SMTP
account moved to admin@einfach-produktiv.com, so From can finally point
at sellerEmail itself instead of the fixed admin@mk360.de placeholder.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- PaymentStep: bg-brand-primary was never a real Tailwind class, leaving
the "Jetzt bezahlen" button unstyled/invisible; switched to bg-brand
with dark text, matching every other CTA on the site.
- New app/icon.svg + app/apple-icon.png + app/favicon.ico: brand-yellow
rounded square with a bold italic "e", replacing the Next.js default.
- New public/og-image.png (logo on the site's cream background) wired in
as the SEO fallback default OG image/description/title in payload.ts.
- Navbar CTA: "Klarheitskompass" -> "Mein 3x3-System", now links to
/3x3-system (page not built yet).
- Blog overview hero image anchored to the top instead of center-cropped.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Skips the send entirely when the admin has deliberately deactivated
the order-confirmation template, matching the backend's new toggle for
the 7 order-status emails. Distinct from a missing row, which still
sends with the hardcoded default wording.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
order-tracking-added/order-tracking-corrected were missing from
VALID_TYPES (app/email-preview/[type]/page.tsx), EmailTemplateType
(app/lib/payload.ts), and ORDER_STATUS_EMAIL_ICON (emailTemplates.ts)
— Live Preview 404'd for both since they were added to the Payload
collection without a matching frontend update.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Organization (site-wide), Product (/todo-cards), BlogPosting (every
/blog/[slug]) JSON-LD via new app/lib/structuredData.ts — no new
Payload fields needed, derived from existing data. Verified locally
by curling each page and checking the rendered script tag.
- Order confirmation email gains the same "please transfer to this
account, processed after payment received" notice the invoice PDF
already had for Vorkasse orders — OrderConfirmationData's new
isManualPayment flag is set explicitly by each caller (never derived
from paymentMethodTitle, which already broke once this session after
a payment-methods rename). CompanySettings gains bankName (existed on
the backend, was missing from the frontend's type/usage).
- Newsletter signup now detects an already-subscribed email
(verified empirically: Brevo's doubleOptinConfirmation endpoint gives
identical 201 responses for new vs. already-confirmed contacts) via a
GET /v3/contacts/{email} pre-check, and shows a distinct message
instead of silently resending the confirmation mail. Success message
text centralized in useNewsletterSignup.ts instead of duplicated
across 4 forms.
- Bumped @einfach-produktiv/invoicing to the version with the
unpaid-notice layout fix (full width, more top spacing — was
squeezed into the narrow paid-badge column).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Hardcoded manual rows first, then the combined stripe option — so
"Online-Zahlung" (sortOrder 0) showed after "Überweisung (Vorkasse)"
(sortOrder 1) in checkout, contradicting the admin's own ordering.
Now preserves the fetch's sortOrder-sorted order, splicing the combined
entry in at the position of the first stripe row encountered.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both already route through the same Stripe PaymentIntent
(automatic_payment_methods: enabled — Stripe's own recommended Payment
Element pattern, letting Stripe itself decide which eligible method to
show). Pre-selecting one of two identical-behind-the-scenes rows before
the payment step was redundant friction, not a real choice. Collapses
them into one option with a hint text explaining the actual instrument
is picked on the next screen; Überweisung is unaffected.
Also refines paymentMethodTitle from a neutral "Online-Zahlung"
placeholder (snapshotted at order-creation time, before the customer has
picked an instrument) to the real one Stripe reports, once payment
confirms — carried through to both the stored order and the
sessionStorage snapshot shown on /bestellbestaetigung.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Checkout now branches on payment-methods.provider: Überweisung stays
immediate/unchanged, Kreditkarte/PayPal creates a pending_payment order,
mounts Stripe's Payment Element, and defers invoice/email to a webhook-
verified confirm-payment call once the backend actually confirms payment.
Includes a PAYMENT_TEST_MODE mock provider so the whole gated pipeline is
exercisable locally without a real Stripe account.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RichText.tsx switched to Payload's official React renderer + custom
JSXConverters (same call signature, LiveRichText/LivePostContent
untouched) — needed to render the new Lexical Blocks the Payload repo's
Posts.content just gained. Converters follow the existing CMS-image
convention (relative + aspect-[...] + fill + object-cover); the video
block resolves YouTube/Vimeo links to an iframe embed.
New getSeoSettings() fetcher (same pattern as getKleinunternehmer()),
app/layout.tsx now generateMetadata() reading it with the same fallback
values it used to hardcode. Per-post SEO overrides (seoTitle/
seoDescription/seoImage) wired into the blog detail page's metadata,
falling back to title/excerpt/thumbnail when empty.
Also fixed while auditing every page's metadata: missing descriptions on
3 konto pages, a static title on the dynamic order-detail route, and
missing OG images on /shop and /blog.
Checkout forces 0% VAT without de-grossing prices when the tenant is a
Kleinunternehmer (a business decision, not just an engineering default —
unlike the existing intra-community VAT exemption, which does de-gross).
Snapshotted onto the order at checkout time so a later toggle of the
company-settings checkbox never rewrites an already-issued invoice's tax
treatment — same reasoning as the existing vatExempt field.
Threaded through: checkout route, order creation/confirmation email,
on-demand invoice/Storno/Gutschrift downloads, the Bestellbestätigung
page, and the account order-detail page. The four storefront "inkl. X%
MwSt." price hints (shop grid, cart upsell, ToDo-Karten landing page,
homepage spotlight) drop that clause live when the setting is on. The
company-settings Live Preview reflects the checkbox in real time too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both the billing and shipping-override country selects, plus PLZ
maxLength/pattern validation, now read from the new shipping-countries
collection (getShippingCountries()) rather than a hardcoded
Deutschland/Österreich(/Schweiz) array. Lets an admin add or reorder
destination countries without a frontend deploy.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Stock was only checked at checkout; a shopper could add more of a
product to the cart than was actually in stock and only find out at
the last step. Product/variant now carry a real maxQty, and
AddToCartButton/AddToCartInlineButton/the cart's quantity stepper all
disable or cap once the cart already holds that many.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Corrected after user feedback: Stammkapital/Grundkapital is only required on business correspondence if voluntarily disclosed in the first place (§35a Abs. 1 S. 2 GmbHG) — not an unconditional Pflichtangabe. shareCapital's Impressum rendering stays (shown only if an admin voluntarily filled it in), but it's dropped from the email/invoice footer.
generalPartners is removed entirely — the legal basis was genuinely unclear on research (§125a HGB's Geschäftsbriefe-naming duty only applies to the narrow case where no partner is a natural person; whether §5 DDG's Impressum-specific "vertretungsberechtigte Person" requirement independently mandates it for the general OHG/KG case wasn't resolved with confidence) — reverted rather than shipped on an uncertain legal basis.
Also fixes stale "§5 TMG" citations to "§5 DDG" (TMG was replaced 14 May 2024).
Mirrors the backend's new CompanySettings.shareCapital/generalPartners fields: rendered in the Impressum (AnbieterAngaben.tsx — new "Gesellschafter" section, Stammkapital line under Handelsregister, and the "Verantwortlich für den Inhalt" fallback now considers a general partner before falling back to sellerName) and wired into buildLegalFooterLines() for the invoice/email footer, same as registerCourt/registerNumber/managingDirector already were.
No visible change today (current legalForm is sole-proprietorship, neither field is set) — this is prep so a future legalForm change in company-settings updates the Impressum automatically instead of needing a manual Impressum edit at that point.
CompanySettings type now mirrors the payload repo's split bankDetails ->
iban/bic (see that repo's own commit for the reasoning). No behavior
change here beyond the type/fallback update — the actual footer
rendering lives in @einfach-produktiv/invoicing.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Bug fixes:
- Navbar login/logout state now updates immediately (custom ep-auth-changed
event) instead of requiring a hard reload
- Status-change email links were broken by an un-encoded "#" in the order
number; fixed for all 4 status emails
- Cart discount code: manual input field restored (was removed entirely)
- Quote-label underline now scales with the label's actual text width
- Number Ranges admin list now shows the invoice prefix/counter columns
Pricing & VAT:
- Prices show the real per-product VAT rate ("inkl. X% MwSt.") instead of
a generic disclosure
- Cart/checkout/confirmation totals show the actual € amount of VAT
included, broken down per rate when a cart spans more than one
(new lib/taxBreakdown.ts, shared with the invoice PDF's own math)
- Account order pages gained product thumbnails and the same VAT breakdown
Low-stock warning: a "Nur noch wenige verfügbar" badge/hint across the
shop grid, spotlight, and add-to-cart variant pickers, driven by the
existing lowStockThreshold field (still never exposes raw stock counts).
Invoice PDFs: product thumbnails on every line item, a plain "Netto"
label (rate was redundant, already stated on the MwSt. line below), no
more duplicate USt-IdNr. in the header, and — for a Stornorechnung
specifically — an explicit "Versand" line that was previously only
folded silently into the tax totals.
Checkout:
- Optional deviating shipping address (separate from the billing address
used for the invoice), with its own toggle + address form
- Full checkout draft persistence (name/address/shipping/payment
selections) survives navigating away and back, via localStorage
- Invoice PDF shows a third "Lieferadresse" block when the shipping
address differs from billing
Mobile navigation: fullscreen panel with a circular reveal animation from
the hamburger's corner, replacing the old in-flow accordion drawer; no
login CTA inside it (redundant with the always-visible header icon).
Admin-facing (Payload backend, mirrored where the frontend has a ported
copy of the same renderer): dashboard rebuilt as individual cards, split
into 3 task queues (received/processing/returns) instead of 2, revenue
and order counts now exclude cancelled/returned orders immediately, and
the low-stock alert links to the specific affected product(s) instead of
the unfiltered list. A new immediate email notifies the shop owner the
moment an order comes in, instead of only via the daily digest.
Testimonials admin list now groups by page instead of interleaving all
three grids' entries. ~45 English admin field descriptions translated to
German for consistency.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- ProductGrid/AddToCartInlineButton/AddToCartButton now show "Ausverkauft"
and disable add-to-cart per variant (or product-level with no variants),
derived from trackInventory/stock/allowBackorder via isOutOfStock().
- AddToCartButton (todo-cards Hero+Pricing, homepage spotlight) gains the
same variant <select> AddToCartInlineButton already had — all three call
sites already fetch full product data server-side.
- /api/checkout re-validates stock server-side (depth-in-defense, not just
the disabled button), rejecting when trackInventory is on, allowBackorder
is off, and requested qty exceeds stock.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018PL4zfTY1sXc8x5QS6FatM
Completes the frontend half of the Payload backend's variant/inventory/
tracking work (see that repo's own commit):
- **Cart**: CartItem gained an optional `variant?: string` field — every
function that used to match a line by `id` alone (addToCart/
removeFromCart/setQuantity) now matches by `(id, variant)` together via
a shared sameLine() helper, so two lines for the same product with
different variants stay separate entries. `variant` undefined on both
sides (the no-variants case) still matches by simple equality, so every
pre-existing call site keeps working unchanged.
- **Selection UI**: AddToCartInlineButton renders a <select> above the
button when given a non-empty `variants` prop (ProductGrid/
RelatedProducts pass product.variants straight through); defaults to
the first variant.
- **Pricing**: cartTotals.ts's new effectivePrice(entry, product) — a
variant's priceOverride wins over the base product price. Every cart/
checkout/order-confirmation total and per-line price display now goes
through this instead of reading product.price directly (fixes both a
wrong-price bug and a duplicate-React-key bug the old `key={product.id}`
pattern would have had the moment two variants of one product were both
in the cart).
- **Checkout**: re-validates the requested variant server-side (same
"never trust the client" reasoning as price re-derivation) — a variant
name that doesn't exist on that product fails the whole checkout.
variantName snapshots onto orders.items, shown as a parenthetical next
to the product name on the confirmation email, both invoice PDF types,
and the order-detail page.
- **Cross-device cart**: Customers.cart[].variantName (synced via
/api/account/cart) carries the selection through a login/logout cycle,
not just the current session.
Also adds tracking-number display: /konto/bestellungen/[orderNumber]
shows a clickable link when orders.trackingNumber is set, built by a new
app/lib/tracking.ts that mirrors the Payload backend's own copy
byte-for-byte close (same carrier set/URL patterns) so what a customer
sees here matches exactly what the order-shipped email already links to.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mirrors the Payload backend's new company-settings.legalForm field: when
present, registerCourt/registerNumber/managingDirector now appear in the
email footer (buildLegalFooterLines) and both invoice PDF footers,
matching §37a HGB / §35a GmbHG requirements for registered legal forms.
A sole proprietorship (the default) renders identically to before —
these fields are only appended when actually set.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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 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.
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>
- 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.
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>