Centering it site-wide below lg: overcorrected — true Mobile (<640px)
should stay centered (matches the CTA there), only the 640-1023px
Tablet band needed the left-align fix from the previous commit.
Centered avatars+text below lg: read as floating/disconnected from the
left-aligned heading/subheading/CTA above it. Left-aligned now to match
the rest of the column; still switches to a single centered row at lg:.
The wider text share at sm: didn't actually help the inner quote/bio
row go inline (it stays stacked until lg: regardless) — it just took
space away from the photo, which read as too narrow/cramped on Tablet.
1:1.4 text:image ratio now applies from sm: up; only the -ml-48 overlap
itself stays gated to lg:, since that trick needs more absolute room.
The sm:/lg: ratio collapse in About.tsx from the breakpoint migration
was wrong: the Tablet-specific wider text share wasn't just compensating
for the grid arriving too early, it's what the inner quote/bio row and
the photo's -ml-48 overlap actually need room for. Restores the
two-tier ratio (wider text, no overlap at sm:; real ratio + overlap
only at lg:), just anchored to sm: instead of the old md:.
Also centers the Hero CTA button below sm: (Mobile) per feedback.
Tablet layout (768-1023px) hit the md: grid switch exactly where the
fluid clamp() tokens were already at their floor, leaving no room to
shrink. Moves the fluid floor and structural breakpoint down together
to sm: (640px) so real tablets always get the fluid, desktop-like
structure; consolidates the ad-hoc md:+lg: patchwork in Hero/About/
Newsletter/Footer/Tools back onto one line, leaving documented lg:
exceptions where content genuinely doesn't fit yet. Also shortens the
Hero heading to a single sentence with no trailing period (the brand's
orange dot already renders one, animated).
NewsletterModal.tsx advertised a different offer (the 7-Tage-Challenge)
than Newsletter.tsx's own description (7 Impulse) for the same signup
flow. Also rewrote both descriptions to drop the vague "und gewinne
Klarheit" buzzword formula and the word "Klarheit" repeated from the
heading directly above it.
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>
The modal's left photo stretches (items-stretch, md:aspect-auto) to
match the right column's height. The "already subscribed"/invalid-email
messages were conditionally mounted, so their appearance grew the right
column and dragged the photo's height along with it. Both are now
always rendered with a reserved min-height instead, so toggling them no
longer changes the modal's size.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Matches standard form-validation behavior: any further edit to the
email or consent checkbox after the already-subscribed message
appears now dismisses it, instead of leaving it stuck until submit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Per explicit feedback: swapping the whole form out for a bare success
message felt wrong for "you're already signed up, nothing to do" — the
form stays visible with a small red note below it instead, same as
every other inline validation error. No per-form UI changes needed,
they already render the error state.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The customer-facing message stays generic on purpose (never leak
Brevo's internal error text), but the real reason was discarded
entirely before this — every failure looked identical from the
outside. Cost real debugging time today tracking down a misconfigured
BREVO_LIST_ID in Coolify (was "2", should have been "5") that made
every single signup attempt fail with the same unhelpful message.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Was one run-on sentence; split into three paragraphs: the instruction,
the bank details on their own bold line, then the processing-time note
with a line break after the bank line.
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>
"Bitte ein Passwort für dein neues Konto vergeben" / "Passwort (für
dein neues Konto)" read oddly — just "dein Konto".
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>
A cancelled order with no invoiceNumber is a Stripe payment that never
succeeded (failed or timed out before ever reaching received/invoiced),
not a real cancellation of something that actually happened — from the
customer's point of view it was never really an order. Filtered out of
getCustomerOrders/getCustomerOrderDetail by default; the row stays in
Payload for admin/audit purposes (shown there as "Zahlung
fehlgeschlagen", see backend).
getCustomerOrderDetail's filter is opt-in via a new optional parameter,
not the default — /api/checkout/status/route.ts's post-payment polling
needs to keep seeing exactly this order to show the "Zahlung
fehlgeschlagen, bitte erneut versuchen" retry state. The GDPR export
route also opts out for the same reason a legal completeness export
can't silently drop rows.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The width="56" height="56" HTML attributes on the icon badge <td> weren't
mirrored in its inline style — renderers that ignore/strip HTML
width/height (Brevo's own editor among them, per a real report) shrink
the cell to fit the glyph instead of staying a 56px circle. Add explicit
width:56px;height:56px to the style too.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Brevo's redirectionUrl now points here instead of the homepage — a
static page matching /bestellbestaetigung's visual language (brand-
tinted checkmark circle, serif display heading, thin brand divider).
No query params to read; Brevo's confirmation redirect carries nothing
this page needs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Was a plain POST /v3/contacts upsert (single opt-in — straight onto the
list, no confirmation required). Now calls doubleOptinConfirmation
instead, so a signup only requests subscription; Brevo sends its own
confirmation email and adds the contact to the real list only once they
click through. Needs BREVO_DOUBLE_OPTIN_TEMPLATE_ID set in Coolify
before this works — not yet configured, signups will fail closed with a
logged reason until it is.
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>
The webhook route previously assumed the backend's confirm-payment
endpoint sent the customer confirmation email; the backend assumed the
opposite. Net effect: a successful Stripe payment never triggered any
confirmation email. Consume the order snapshot confirm-payment now
returns and send it from here, matching what the checkout route already
does for a manual/Überweisung order.
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>
- Checkout: the 4 step cards' border-border and CheckoutSteps' connector
lines/upcoming-circle borders were nearly the same luminance as the
page's own bg-bg-base background, barely visible. Darker (#c4b8a0),
scoped to just these spots rather than the shared border-border token.
- Hero.tsx: fixes a mistake from the last Tablet pass — moving the CTA/
subtitle/icon/social-proof size overrides from lg: to md: (to match the
grid breakpoint move) actually removed their smaller sizing from the
whole Tablet range, recreating the exact 3-line-wrap problem the lg:
exception used to prevent. Reverted those specific overrides back to
lg: (grid structure stays at md:), and added a smaller heading size
(text-h1) below lg: too — text-display's 44px floor doesn't fit the
~283-320px Tablet column any better than the CTA did.
- About.tsx: the quote/divider/bio row and its divider orientation also
pushed from md: to lg: — still too tight for the Tablet column even
after the text/photo ratio swap from the previous pass.
- Hero.tsx: structural breakpoint reverted from lg: back to md: — the
smaller CTA/subtitle/icon sizes added during the Mobile pass fit
comfortably in the ~283px Tablet column, so the original 3-line-wrap
problem that justified lg: doesn't recur. Tablet gets the real 5/7
grid (image beside text) again instead of a stacked mobile layout.
- Tools.tsx: full-size (56px) card icon pushed from md: to lg: — at
Tablet it dwarfed the still-close-to-floor title/description text.
- About.tsx: text/photo flex ratio swapped at Tablet (text gets the
bigger share, no overlap) vs. the original ratio + overlap trick from
lg: up, where it was designed for — Tablet's text column was too
narrow for its fixed-width statement + quote/bio row otherwise.
- Newsletter.tsx/Footer.tsx: the input+button row and the logo/handle/
legal-links row both went side-by-side at md:, but their surrounding
columns didn't leave enough width at 768px — pushed to lg:flex-row.
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.
Input and button were a fixed row at every width, squeezing together on a
narrow phone — now flex-col sm:flex-row, full width when stacked, same
pattern as Challenge's EmailCapture and the other mail CTAs already fixed.
- Checklist checkmarks (Challenge, todo-cards, newsletter): mt-0.5 -> mt-1,
centering against the first line's glyphs instead of sitting flush at
the very top edge.
- Navbar login icon: -translate-y-0.5 — its round head vs. wide shoulders
read as optically bottom-heavy next to the cart icon despite both
having a mathematically centered bounding box.
- /todo-cards's detail page (the only page resolving a product regardless
of active status) now renders both CTAs as "Ausverkauft" and disabled
when the product is deactivated, instead of fully buyable — variants are
dropped too, since AddToCartButton's outOfStock prop is otherwise ignored
whenever variants is non-empty.
- StepArrow: brand orange didn't fit here after all, switched to
near-black (#222221) per feedback.
- Newsletter/"Impulse & Tipps" checklist checkmarks (WeeklyImpulsesHero.tsx,
WeeklyBenefits.tsx) were still the old un-recolorable icon-check.svg —
same inline-SVG orange-checkmark fix as todo-cards/Challenge, plus
items-start instead of items-center on WeeklyImpulsesHero's list for
top-alignment consistency.
- PopIn (Home Hero's brand dot) switched from whileInView to animate — its
translate-based entrance could push the element off-screen on a narrow
phone before the IntersectionObserver ever saw it as visible, leaving it
stuck invisible permanently.
- Hero image: no longer wrapped in Reveal below lg: — whileInView's margin
meant it stayed at opacity:0 (a white gap above the fold) on short mobile
viewports until scrolled. Reveal's fade-in kept from lg: up.
- "→ Label" CTA links (Tools.tsx, Blog.tsx) now use a flex row with the
arrow as its own span instead of a literal inline "→" character, which
doesn't reliably align to the surrounding text's cap-height.
- Replaced icon-arrow-connector.svg with a new shared StepArrow component
(inline SVG) across all three step sections — the old asset's color
couldn't be overridden from outside the SVG file, so it could never
actually become brand-orange. Bigger and better-shaped below the
structural breakpoint per feedback.
- Added MobileSectionTOC (SectionTOC.tsx) — a <details> accordion shown
below lg: on Impressum/Datenschutz/AGB/Widerruf/Versand, which previously
had no on-page navigation aid at all below lg: (the sidebar TOC is
`hidden` entirely there).
- Updated the figma-to-nextjs skill with 8 new dated Gotchas from this
mobile-responsive pass, and expanded Step 6's verification checklist.
- README: new "Mobile responsive pass" section summarizing the above.
- Newsletter card icon: fixed an aspect-ratio distortion bug (w-16 maps
to this project's fluid --spacing-16, which floors to 40px below
768px, paired with a fixed 55px height — squished the icon on
mobile). Same root cause existed in NewsletterModal's envelope icon;
that one's now just hidden below md: instead per feedback.
- Hero: subtitle sized down and CTA checkmark icon scaled to match
below lg:, social proof text always wraps below the avatars there
instead of only when it doesn't fit.
- Werkzeuge connector arrows (todo-cards/newsletter/challenge
HowItWorks): object-contain added — the SVG has
preserveAspectRatio="none" and was stretching to fill the square
mobile box instead of keeping its thin-arrow shape.
- Challenge bottom CTA: icon now stacks above the copy, centered, below
lg: to match the Home Newsletter card's pattern.
- Homepage Werkzeuge icons: smaller below md: to match the (fluid-floor)
text size next to them.
- Blog detail "Passend dazu" card: the fixed w-[19rem] title column plus
"Entdecken" sharing its row overflowed on mobile; stacked below sm:
instead, with a little top margin on "Entdecken".
- Todo-Karten checklist checkmarks: recolored brand-orange (icon-check
.svg's fill lives in an internal CSS var that can't be overridden from
outside an <img>-loaded SVG, so switched to an inline SVG) and
top-aligned instead of vertically centered.
- AddToCartButton: added whitespace-nowrap to the stacked-label grid
(used to reserve button width across all possible label texts) — one
of those labels wrapping to two lines on a narrow w-full button was
inflating the row height for whichever label is actually showing,
leaving a tall empty gap under "Ausverkauft".
- Hero CTA: back to a single line below lg: (wrapping put the icon
beside two lines and looked broken), sized down instead so the full
phrase fits.
- Hero heading: dropped the forced <br> after "darf" at true mobile
widths, kept it only for the sm-lg tablet range it was added for.
- Hero social proof: avatars+text now wrap and center instead of
cramming onto one row (flex-wrap + justify-center, no fixed
breakpoint needed).
- Newsletter card icon: stacked above the copy and centered below md:
instead of squeezed into a row beside it.
- TrustRow: left-aligned below md: instead of centered (shop, cart, and
every other page using this component).
- Cart line item image: full width below sm: instead of a fixed 150px
square.
- Challenge EmailCapture (top + bottom CTA, same shared component):
input and button stack full-width below sm: instead of squeezing
into one row.
- Challenge steps: icon above text and centered at every breakpoint
(previously only from lg: up), connector arrows centered to match.
- Hero CTA: whitespace-nowrap forced the button as wide as the full
32-char phrase, overflowing the stacked mobile column. Now wraps to
two lines below lg:, single line from lg: up where there's room.
- Navbar: the account/cart icons are 44px touch targets with the glyph
centered inside, so the outer gap-2 read as too much space on top of
that padding. Grouped them with no gap between just the two.
- Divider: "Klarheit -> Fokus -> Entlastung" plus its connector icons
needs ~550px to fit one row even at the site's 768px fluid floor, far
more than a phone's ~310px content width. Added a component-scoped
--divider-word-size (independent from --text-h2) plus a <640px
override that shrinks the words/icons/gaps together.
FormField is now forwardRef so the email input can be targeted
imperatively — clicking the button clears the field (previous commit)
and now also focuses it, so the shopper can start typing immediately
instead of having to click in first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
"Andere E-Mail-Adresse verwenden" only hid the inline login prompt
before, leaving the already-registered address still sitting in the
field — the shopper had to manually select/delete it before typing a
new one. Now clears email + loginPassword + the email field's error
alongside dismissing the prompt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AnbieterAngaben.tsx's address/VAT/register blocks each got the same
gap-4 as the section headings, so related lines (e.g. name/street/zip+
city) read as visually disconnected paragraphs instead of one block —
wrapped each in its own gap-1 container, keeping gap-4 only between
sections.
Reveal.tsx's shared fadeUp variant animated opacity and a 28px y-
translate together, which read as the whole section hopping into place
on top of the fade. Renamed to fadeIn, opacity only — applies site-wide
via Reveal/RevealGroup/RevealItem.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Login prompt (email already has an account) now renders inline under
Card 1's own email field instead of a separate block above the whole
form — no scrolling needed in the common case, and no more re-typing
the email into a second field. The submit-time fallback still scrolls
it into view via a useEffect, now that the target is conditionally
rendered.
/konto/profil's "Land" select was still hardcoded to Deutschland/
Österreich/Schweiz independently of /checkout's own Payload-configurable
shipping-countries list.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The new-account password field's onBlur re-focused itself whenever the
value was still invalid (<8 chars), the same generic pattern every other
blur-validated checkout field uses. For most fields that's a helpful
"fix it now" nudge, but it's a genuine trap on a password field near the
end of a card — a shopper could never Tab or click the submit button
until the password was already valid. Dropped just the refocus here
(inline error text still appears immediately); every other field keeps
the existing behavior.
TodoKartenHero.tsx's price line never showed "zzgl. Versand" at all
(pre-existing gap, unrelated to Kleinunternehmer), unlike Pricing.tsx/
ProductSpotlight.tsx — now consistent across all three.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
VIES answers HTTP 200 even when it couldn't actually perform the check
(actionSucceed: false, e.g. MS_UNAVAILABLE — Germany's own national
gateway does this fairly regularly). checkVatIdViaVies() only ever read
data.valid, which is absent on that response shape, so it silently
read as valid: false — a real, currently-registered German VAT ID
(reported: DE351362947) looked rejected. Worse, /api/checkout/validate-
vat then wrapped even a correctly-returned ok:false as { ok: true,
valid: false }, which the client reads as "invalid" rather than
"unavailable" — the actual bug the user hit, compounding the vies.ts
gap. Both are fixed now: an unconfirmable check surfaces to the client
as ok:false, which CheckoutContent.tsx's handleVatIdBlur already
correctly renders as "USt-IdNr.-Prüfung derzeit nicht möglich"
instead of a rejection. Same fix applied to the payload backend's own
copy of vies.ts (company-settings' VAT check).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Same short "check your inbox" message everywhere instead of each form
having its own success wording (they'd drifted since each is a
separately-coded component, not a shared one).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>