renderBackInStockHtml() (and sendBackInStockEmails.ts on the backend)
now show a small product image above the product name, when the
product has one. Also left-aligns NotifyMeForm's button text to match
the email input's own left-aligned placeholder — was centered, read
as inconsistent next to it.
Also adds the klaro dependency, ahead of swapping the custom cookie
banner for it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Replace the invisible reserved-space trick with items-start on every
product grid (ProductGrid/MerklisteGrid/RelatedProducts) — the
reservation looked worse in practice (visible dead space under
in-stock cards' buttons) than letting an out-of-stock card simply be
taller than its siblings.
- New renderBackInStockHtml() in lib/emailTemplates.ts, used by the
Live Preview instead of the generic order-status renderer — that one
showed a fake order number and "Bestellung ansehen", neither of
which apply to a back-in-stock mail (no order exists). CTA is now
"Zum Produkt", matching the real backend send.
- Shortened NotifyMeForm's button label ("Benachrichtigen") — the
longer version wrapped to two lines on narrow single-column cards.
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>
Two problems in the previous back-in-stock commit: the disabled
"Ausverkauft" button and NotifyMeForm stacked, making out-of-stock
cards visibly taller than in-stock siblings — and since ProductGrid's
cards rely on plain CSS Grid row-stretch for equal card height, that
extra height stretched sibling cards and pushed their own buttons
down (screenshot: "In den Warenkorb" CTAs misaligned across a row).
NotifyMeForm now replaces the button slot entirely when out of stock
(matching the height of a normal button when collapsed), and only
expands to the email input after a click.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Narrow product cards (shop grid, related products) truncated the
email placeholder when input and button shared a row.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
NotifyMeForm.tsx replaces the disabled Add-to-cart button's spot once
a product/variant is out of stock — POSTs to the new
/api/stock-notifications route, which forwards to the backend's new
stock-notifications collection. Threaded product.numericId (not the
commerce slug id) into AddToCartButton/AddToCartInlineButton for this,
same split WishlistButton already uses.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CategoryFilter used gap-4 between the category group and the
availability checkbox but gap-2 within the category list itself,
reading as uneven spacing. ProductGrid's <lg filter bar only had
pb-6 before the product grid.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The create POST omitted the customer relationship field entirely, so
added items never matched getWishlist's customer-scoped query. Also
scope the toggle-off fallback lookup to the current customer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RelatedProducts.tsx was the only product-card grid missing the
WishlistButton (ProductGrid.tsx/ProductSpotlight.tsx already had it).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mergeServerCartIntoLocal() relied on CartSync's own change-triggered
push to upload the merged cart — but if the server cart was empty
(first login on this account) and the local guest cart wasn't, the
merge loop never actually ran (nothing in the server response to
add), so no write/change-event ever fired and CartSync's effect never
pushed anything. The guest cart looked fine on the device that just
logged in, but never reached any other device. Now pushes explicitly
at the end regardless of whether anything changed.
pullServerCart() only added/updated matching lines and never dropped
a local line the server no longer had — a removal on one device never
reached another (adding synced, removing didn't). Now replaces the
local cart outright with the server's instead of merging into it,
including down to empty.
Cross-device sync only ever worked one way (local→server on every
change via CartSync) plus a server→local merge at the exact moment of
login/register — a customer already signed in on a second device (no
fresh login action happening) never picked up changes made elsewhere.
Adds pullServerCart(): idempotent (server qty overwrites a matching
local line rather than adding to it, safe to call repeatedly, unlike
mergeServerCartIntoLocal()'s additive merge which only stays correct
right after a login clears the local cart's ambiguity). Triggered on
CartSync's mount and on window focus — covers "open the site while
already logged in" and "switch back to this tab after changing the
cart on another device," without a polling interval.
Was always visible in ProductSpotlight.tsx, unlike ProductGrid.tsx's
cards — inconsistent for what's still a single card in a hoverable
context. The `group` ancestor needed for revealOnHover already exists
there (used for the image's hover-zoom effect), so this is just the
prop.
The previous replace_all only matched the empty-cart instance's exact
indentation — this one, inside the has-items branch, was a level
deeper and kept the raw Unicode "←" (aria-hidden), still hitting the
mobile font-fallback vertical-metrics bug ArrowLeftIcon.tsx exists to
avoid.
Products table gains the categories field; Search & filters section
covers the price slider, category checkboxes, availability toggle,
the blog's client-side category filter, and the RevealGroup key fix
both grids needed to stop going blank after a filter change.
Same treatment as the blog cards' category line (Blog.tsx) — small
uppercase text-muted line above the product name, applied
consistently across all three card grids (shop, Merkliste, cart's
related products). Omitted entirely for an uncategorized product
rather than showing an empty line.
Same RevealGroup stuck-at-opacity-0 bug as the blog's category
filter (see f95feaf) — whileInView only fires once per component
instance, and a price/category/inStock filter change re-renders the
same /shop page in place, so RevealGroup never naturally remounts
between filter states. Keying it on the actual filtered product set
forces a remount, restarting the viewport tracking each time.
Same font-fallback bug ArrowRightIcon.tsx already documents and fixed
for "→" elsewhere (Tools.tsx, Blog.tsx, ProductGrid.tsx) — the site's
custom web fonts don't cover U+2190, so it fell back to a system font
whose vertical metrics sit visibly low on mobile. Missed in that
earlier pass since CartContent.tsx uses the left-pointing twin.
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.
RevealGroup's whileInView fires once per component instance. Since a
category-filter navigation re-renders the same /blog page in place
(only searchParams changes, no full remount), and rest.length stays
> 0 across most filter transitions, RevealGroup itself never
naturally unmounts — so once it already fired "show" for one
filter's list, newly swapped-in RevealItems (different post ids)
mounted into an already-settled parent with no reason to re-fire the
reveal trigger, staying stuck at opacity 0 forever. Keying
RevealGroup (and the featured Reveal) on the actual rendered post
set forces a remount whenever the filtered posts change, restarting
the viewport tracking each time.
SearchOverlay was rendered as a <header> descendant, so the header's
conditional backdrop-blur-md (once scrolled) made it the containing
block for the overlay's fixed positioning, clipping the opaque
background to the header's height and letting page content show
through underneath. Now rendered as a header sibling, same pattern
already used for NewsletterModal.
Blog category chips are now a client component gating navigation
behind useTransition, disabling the chips while a navigation is
pending so rapid clicks can't fire overlapping RSC navigations that
commit out of order and briefly show an empty result.
- IconHand had 2 finger segments + a thumb curve (3 digits total,
visibly wrong for an open-hand icon) — rebuilt as 4 finger capsules +
thumb using simple rounded-rect shapes instead of hand-tuned path
arcs, verified visually via a local render before applying.
- Worum-es-geht was too thin for a page opener — expanded with a
concrete, specific opening (not a generic "in a busy world" framing)
and more of what the monthly invitations actually look like in
practice, still in first person.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018unaXmuzVA8ct1b6WoyP1U
Free monthly ritual page, modeled on /todo-cards' conventions
(Reveal/RevealGroup, breadcrumb + text-display heading) but without a
product-style hero — this isn't a paid product, so no photo/price/buy
CTA. The WordPress original's monthly-changing "aktuelle Ausgabe",
printable template, and archive have no content model in this codebase
yet (nothing like a `Posts` collection for "editions") — this ships the
evergreen concept page only, with the existing Newsletter signup
standing in as the "stay in the loop" mechanism. Deferred to a memory
note rather than blocking this page on designing a new collection.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018unaXmuzVA8ct1b6WoyP1U
Same mount-guard pattern as CartLink's own pulse fix — useWishlist()'s
count only fills in after its client-side fetch resolves, so without the
guard this would have pulsed on every page load too, not just a real
add/remove during the session. Fires on either direction (add or
remove) since the wishlist has no separate "flying" animation like the
cart's ball-to-icon effect to lean on instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018unaXmuzVA8ct1b6WoyP1U
- RichText.tsx: Lexical's auto-detected "autolink" nodes (a typed-out
email/URL, as opposed to an editor-inserted link) fell through to
Payload's unstyled default converter — only "link" was overridden.
AGB's Vertragspartner email rendered as plain black text because of
this. Both node types now get the same text-brand/hover:underline
treatment.
- Breadcrumb color revert: the breadcrumb Startseite links across the
4 legal pages/shop should stay their original hover:text-brand
treatment — only the actual content/contact links needed the
always-brand-colored style, not page-level breadcrumbs.
- /blog was the one page missing both a breadcrumb and the
text-h-feature heading size every other section page (/shop, legal
pages) already uses — added to match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018unaXmuzVA8ct1b6WoyP1U
- Cart badge pulsed on every page load/reload, not just real cart
changes — useCartCount's SSR snapshot is always 0, so hydration alone
satisfied the "count increased" check. Skip the very first effect run.
- WishlistButton: touch-device smaller/subtler sizing now applies
regardless of revealOnHover (e.g. the homepage spotlight heart), not
just the shop grid's hover-reveal mode.
- /blog: the big featured-style hero card was applied to whatever post
sorted first post-filter, even when that post isn't actually the
`featured` one (a category filter can exclude the real featured post
entirely). Now gated on the post's own `featured` flag.
- Legal pages' "Startseite" breadcrumb link now matches the same
text-brand/hover:underline treatment already used elsewhere on these
pages (e.g. Impressum's Anbieter email link), instead of staying muted
until hover.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018unaXmuzVA8ct1b6WoyP1U
- 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
- Remove Merkliste from AccountNav — the Navbar's own wishlist icon already covers it, a second nav entry was redundant.
- Fix duplicate "Mein Profil" heading (ProfileForm already renders its own; KontoShell no longer adds a second one).
- "Eingeloggt als ..." now renders once in KontoShell, consistently on every /konto/* page, instead of only on the orders page.
- Blog category filter chips moved into the hero's own text column (was a separate bar below the entire hero including the photo) — visible immediately on every width instead of requiring a scroll past the hero image first.
- ArrowRightIcon's arrowhead wings shortened (looked like a generic oversized chevron at full length).
- Search overlay's "Esc" text button replaced with an X icon; the actual Escape keyboard shortcut is unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Notes what moved into the shared package (VIES, VAT-ID, PLZ, carrier-tracking) and why — this repo's README previously didn't call out the modularization/dedup pass at all, only individual file references scattered through other sections.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Konto-Aktionen links (profile, wishlist, logout) used to live at the bottom of the orders page's own content — unreachable once the order list got long enough. New KontoShell + AccountNav give every /konto/* page a persistent sidebar (sm+) / tab bar (mobile) instead, always reachable regardless of list length. The order filters also collapse behind a "Filter" toggle on mobile now, since the tab bar above them left little room for 3 full-width dropdowns.
Also: replaced the Unicode "→" arrow in CTA links (Tools.tsx, Blog.tsx, ProductGrid.tsx) with an SVG icon — the glyph isn't covered by the site's custom fonts, so browsers fall back to a system font per-platform; confirmed sitting visibly low relative to the label text on Android/Chrome (Galaxy S22), not reproducible in desktop Chromium. An SVG has no font-fallback path, so it renders identically everywhere.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Revert the 40px icon-shrink below 375px (hard to tap accurately) and reclaim that space from the logo instead (76-130px depending on width, vs the always-181px controls). Re-verified overflow-free at 320px via Playwright with all 4 icons at full 44px.
Also: wishlist heart's touch-friendly smaller/subtler size now applies to already-wishlisted (filled) hearts too, not just not-yet-wishlisted ones. The wishlist page's login redirect now returns to the wishlist instead of the generic account/orders overview. Desktop nav-link gap now scales fluidly between 1024-1300px instead of a fixed 48px that felt too wide right where nav labels have the least room.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both were deliberately hidden below 640px to avoid icon-row overflow. Verified via an actual Playwright viewport sweep (320-640px) that simply un-hiding them did overflow — the header's fixed 181px logo + 32px padding left no room. Fixed properly: logo shrinks to 130px and padding drops to px-4 below sm, plus a 40px icon-size step below 375px. Re-verified overflow-free down to 320px.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fully transparent made the heart invisible against some product photos — keeps it smaller than the default but with a semi-transparent (60%) background circle instead of none, and stops dimming the icon itself.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Full-size, always-visible hearts on touch devices recreated the exact "a heart on every card" visual noise revealOnHover exists to avoid. Touch devices now get a smaller, backgroundless, slightly transparent heart instead — present and tappable everywhere, but visually quiet. Desktop hover-reveal behavior is unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
revealOnHover relied purely on group-hover/group-focus-within, both mouse/keyboard-only pseudo-classes with no reliable touch equivalent — a tap on a card never reliably triggers group-hover the way a mouse hover does, leaving the heart permanently hidden on touch devices. Adds pointer-coarse:opacity-100 so it's always visible there; desktop hover-reveal behavior is unchanged.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Removes all dated section headings (2026-07-24 through 2026-07-31) and inline dated asides, folding still-true facts into the evergreen structure (Pages, Cart & checkout, Orders & customer accounts, etc.). Also fixes references to app/lib/vies.ts/vatId.ts/tracking.ts, which moved into @einfach-produktiv/invoicing. Git history is the actual changelog. 2375 -> 1798 lines.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deletes app/lib/vies.ts, vatId.ts, tracking.ts in favor of the newly unified @einfach-produktiv/invoicing modules — fixes the actual PLZ inconsistency (this repo already validated per-country digit counts; the backend hardcoded German-only) rather than just deduplicating code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A customer often wishlists something specifically to buy it again (gifts, repurchases) — silently removing it after purchase would defeat that. /konto/merkliste now shows a dimmed image + "Gekauft am [date]" badge instead, derived read-only from the customer's own orders (cancelled/returned orders excluded). Removal stays manual.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
Per-product spotlightShowWishlist opt-in (independent of the global
wishlistEnabled toggle), a reveal-on-hover WishlistButton variant for
multi-card grids (avoids a heart on every card reading as visual noise),
and related PriceRangeFilter/ProductGrid/CustomSelect adjustments.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Promoted CustomSelect from konto/bestellungen/components/ to a shared
app/components/ location. New includeAllOption (checkout doesn't want
a "clear" pseudo-option — a country is always genuinely selected) and
fullWidth (matches the other w-full form fields, no sm: shrink) props.
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>
New /konto/registrieren (+ RegisterForm.tsx) — until now an account
could only be created inline during checkout, which stopped making
sense the moment the Wishlist gave accounts a non-purchase use case.
LoginForm now also honors ?redirect= (previously ignored, always
landing on /konto/bestellungen regardless of where the login was
triggered from, e.g. WishlistButton's login-gate) and links to the
new registration page instead of implying "only via checkout".
Fixed: removing an item from the wishlist didn't disappear from
/konto/merkliste — that page was a Server Component with a fixed
render, not reactive to the client-side toggle. New MerklisteGrid.tsx
(Client Component) uses useWishlist()'s live state as the actual
source of truth for which products are still shown.
Fixed: AddToCartInlineButton looked broken on that same page —
`className="w-full"` was passed to it, but its `className` prop
*replaces* the whole default styling (`?? default`, not a merge),
throwing away all the button's actual styling. Its default is
already `w-full`; no override needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Preset price buckets as toggle chips were reverted per feedback
("keine toggle badges") — PriceRangeFilter.tsx is a plain min/max
number-input pair instead, still a URL search param underneath
(?minPrice=&maxPrice=), same shareable/bookmarkable approach.
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>