Picks up the Liberation Sans font-embedding fix for PDF/A-3 compliance
— fixes invoice PDF downloads/emails generated by this app.
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>
Error-clears-on-interaction behavior and the modal photo no longer
resizing when the already-subscribed/format-error message appears.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Moves the §19/§4-Nr.-1b-UStG notice out of the invoice summary card
into its own full-width row, matching the Vorkasse-notice fix.
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>
Everything code-side is already live — this is purely the remaining
provisioning steps (test keys -> Coolify -> backend .env -> redeploy ->
end-to-end test -> live keys), so the next person doesn't have to
reconstruct the sequence from scattered comments.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes isPaidImmediately() incorrectly showing "Bereits beglichen" on
unpaid Überweisung (Vorkasse) invoices after the payment-methods rename
earlier today, plus adds the missing "please transfer to this account,
processed after payment received" instruction text.
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>
- Real payment capture for Kreditkarte/PayPal via Stripe's Payment
Element, webhook-gated order confirmation, PAYMENT_TEST_MODE for
local testing without a real Stripe account.
- Newsletter signup switched from single to double opt-in
(POST /contacts/doubleOptinConfirmation), plus /newsletter-confirmed
as the post-confirmation landing page.
Backend counterpart already deployed and verified (confirm-payment
endpoint live, payment-methods provider field set on Kreditkarte/PayPal).
STRIPE_SECRET_KEY/STRIPE_WEBHOOK_SECRET/NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
still unset in Coolify — PAYMENT_TEST_MODE auto-engages until then, so
checkout is safe to test without a real Stripe account.
BREVO_DOUBLE_OPTIN_TEMPLATE_ID is set.
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>
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>