TrustRow.tsx: reserve 2 lines of height for the title below lg: so the
description starts at the same y-position regardless of whether that
badge's own title happened to wrap to 1 or 2 lines in its narrow
stacked column.
BestellbestaetigungContent.tsx: same fix category as NewsletterModal's
dialog width cap — max-w-[56rem] doesn't constrain anything below
896px, so the order-details card stretched full width through the
640-1023px tablet range. Added max-w-[36rem] there, widening to the
real cap only once lg:'s sidebar split needs the room.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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).
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>
A validated EU business buyer (Österreich, the one cross-border option
this checkout offers) gets the sale zero-rated per §4 Nr. 1b UStG —
but only after a live VIES lookup confirms the VAT ID is actually
registered right now, never from format-validity alone (real
compliance risk otherwise). VIES unreachable fails closed: normal VAT
applies, no guessed exemption.
- lib/vies.ts: calls the EU's public VIES REST API.
- lib/vatExemption.ts: de-grosses item/shipping prices and computes
the exempt totals; also picks the actual destination country
(shipping override when set, billing otherwise).
- api/checkout/validate-vat: on-blur live check for instant feedback;
api/checkout/route.ts re-runs the same check server-side at submit
as the actual source of truth, and re-prices every line net-of-VAT
when exempt.
- CheckoutContent.tsx: VIES status + a live exempt-totals preview;
BestellbestaetigungContent.tsx mirrors it from the persisted
snapshot. Both blur-validate every other checkout field now too
(immediate inline errors, not just on submit).
- vatExempt/vatIdValidatedAt threaded through orderServer.ts,
customerAuth.ts, orderEmail.ts, and both invoice-download routes so
the invoice PDF and its e-invoice XML (companion payload-repo
commit) reflect the exemption correctly wherever it's rendered.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Phase 0 of the e-invoicing migration plan (see the E-Rechnung planning
session) — moves invoicePdf.tsx, correctionInvoicePdf.tsx, and
taxBreakdown.ts into a new shared package, consumed as a git dependency
by both this repo and the payload backend, instead of hand-duplicating
the correction-invoice logic between them (see that package's own README
for the three real drifts the duplication had already caused).
Consumed as raw TS/TSX source via next.config.ts's transpilePackages, not
a pre-built package. Needs `git` in the Docker deps stage and a
project .npmrc (allow-git=root) to let npm ci fetch a git-URL dependency
at all — npm 12+ disables that by default.
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>
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>
Checkout now persists orders server-side (Payload orders collection,
re-priced from live product data, discount codes redeemed exactly once)
instead of writing a client-only sessionStorage snapshot. Buying requires
an account (registration inline in checkout, no separate step) — accounts
get order history with delivery status, profile/address editing, password
change, and a cart that syncs across devices while logged in.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Discount codes:
- New shared lib/cartTotals.ts (computeSubtotal/computeCartTotals) factored
out of the previously-triplicated subtotal/totalSavings/total math in
CartContent/CheckoutContent/BestellbestaetigungContent, extended to also
fold in a discount amount (percent or fixed, clamped so total can't go
negative).
- lib/discount.ts mirrors lib/cart.ts's exact localStorage pattern so an
applied code survives the /cart -> /checkout transition without a second
input field — Checkout only displays it.
- New /api/discount/validate (read-only check) and /api/discount/redeem
(re-validates + increments the redemption counter, called once from
checkout's handlePurchase right before the OrderSnapshot is written).
Both talk to Payload's new discount-codes collection through
lib/discountServer.ts, a server-only module kept separate from
lib/payload.ts on purpose (that file is also imported by "use client"
components; the RSC-boundary break hit earlier this session was exactly
this mistake with next/headers).
- OrderSnapshot gains discountCode/discountAmount so /bestellbestaetigung
displays what was actually applied instead of losing it on recompute.
RelatedProducts: no longer falls back to re-suggesting a product already
in the cart just to pad the grid out to 3 cards — shows only the
genuinely available remainder (down to 1 card), centered in the 12-column
grid instead of left-aligned.
Clears every remaining @next/next/no-img-element warning — automatic
responsive srcset, lazy-loading, and format optimization instead of
always loading the original file at full size. Fixed-size icons got
explicit width/height; dynamic-aspect photos got fill inside a
relative wrapper.
"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").