diff --git a/app/cart/components/RelatedProducts.tsx b/app/cart/components/RelatedProducts.tsx
index 95fbefb..d03cb82 100644
--- a/app/cart/components/RelatedProducts.tsx
+++ b/app/cart/components/RelatedProducts.tsx
@@ -132,7 +132,7 @@ export function RelatedProducts({
(opacity: 0) — invisible. Not worth chasing a fix for a
scroll-reveal nicety on a list that mutates; a static grid
renders correctly with no animation risk. */}
-
+
{displayProducts.map((product, i) => {
const discount = discountPercent(product.price, product.compareAtPrice);
const taxRate = effectiveTaxRate(product, defaultTaxRate);
diff --git a/app/components/AddToCartButton.tsx b/app/components/AddToCartButton.tsx
index 5a1d698..c87ae21 100644
--- a/app/components/AddToCartButton.tsx
+++ b/app/components/AddToCartButton.tsx
@@ -3,7 +3,7 @@
import { useEffect, useRef, useState } from "react";
import { addToCart, useCart } from "../lib/cart";
import { useCartFly } from "./CartFly";
-import { NotifyMeForm, NotifyMeFormReservedSpace } from "./NotifyMeForm";
+import { NotifyMeForm } from "./NotifyMeForm";
const FEEDBACK_MS = 2000;
@@ -119,23 +119,20 @@ export function AddToCartButton({
))}
)}
- {/* Same reserved-space stack as AddToCartInlineButton.tsx's identical
- block — see that file's own comment. */}
-
);
}
diff --git a/app/components/AddToCartInlineButton.tsx b/app/components/AddToCartInlineButton.tsx
index cbac8e7..5be25ab 100644
--- a/app/components/AddToCartInlineButton.tsx
+++ b/app/components/AddToCartInlineButton.tsx
@@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from "react";
import Image from "next/image";
import { addToCart, useCart } from "../lib/cart";
import { useCartFly } from "./CartFly";
-import { NotifyMeForm, NotifyMeFormReservedSpace } from "./NotifyMeForm";
+import { NotifyMeForm } from "./NotifyMeForm";
// Exported so consumers like RelatedProducts.tsx can delay their own
// follow-up UI changes (e.g. swapping out this exact card) until after
@@ -113,46 +113,36 @@ export function AddToCartInlineButton({
))}
)}
- {/* grid + [grid-area:1/1] stack — NotifyMeFormReservedSpace (an
- invisible, non-interactive twin of NotifyMeForm's markup) always
- contributes its height here, even for an in-stock card that
- never shows the real form. Without it, only out-of-stock cards
- would be tall enough to need the input+button, and plain CSS
- Grid's row-stretch (ProductGrid.tsx has no explicit row height)
- would push every sibling card's button down to match whichever
- card in the row happens to be out of stock. */}
-
-
-
-
- {/* self-start, not the stretch default — the button's TOP edge is
- what must align across cards (ProductGrid.tsx's flex-1 spacer
- pins this whole block to a card's bottom already); the extra
- reserved height below a single button just stays blank. */}
-
+ {currentlyOutOfStock ? (
+ // Replaces the button slot entirely rather than stacking below a
+ // disabled "Ausverkauft" button. An out-of-stock card is taller
+ // than its in-stock siblings now — ProductGrid.tsx/MerklisteGrid.tsx/
+ // RelatedProducts.tsx all use `items-start` on their grid (not the
+ // CSS Grid default `stretch`) specifically so that doesn't cascade
+ // into pushing every other card's button down to match; an earlier
+ // attempt reserved the extra height invisibly on every card
+ // instead, which looked worse in practice (visible dead space
+ // under in-stock cards' buttons).
+ 0 ? (selectedVariant ?? "") : ""} />
+ ) : (
+
+
+ {limitReached ? "Maximale Menge im Warenkorb" : added ? "Hinzugefügt ✓" : label}
+
+
+
+ )}
);
}
diff --git a/app/components/NotifyMeForm.tsx b/app/components/NotifyMeForm.tsx
index 2409918..9e7525d 100644
--- a/app/components/NotifyMeForm.tsx
+++ b/app/components/NotifyMeForm.tsx
@@ -8,12 +8,12 @@ import { isValidEmail } from "../lib/email";
* is out of stock — lets a visitor leave their email to be notified once
* lib/jobs/sendBackInStockEmails.ts (Payload backend) sends the "it's
* back" mail. Always shows the email input + submit button directly (no
- * extra click to reveal them) — the resulting taller CTA area is
- * reserved on every card via NotifyMeFormReservedSpace below, not just
- * the out-of-stock one, so the grid's row-stretch never pushes sibling
- * cards' buttons down (see AddToCartInlineButton.tsx/AddToCartButton.tsx's
- * own comment on why that reservation lives there). `productId` is the
- * numeric Payload id (`product.numericId`), NOT AddToCartInlineButton/
+ * extra click to reveal them). The resulting out-of-stock card is taller
+ * than its in-stock siblings — ProductGrid.tsx/MerklisteGrid.tsx/
+ * RelatedProducts.tsx all use `items-start` on their grid so that doesn't
+ * cascade into pushing every other card's button down to match (see
+ * AddToCartInlineButton.tsx's own comment). `productId` is the numeric
+ * Payload id (`product.numericId`), NOT AddToCartInlineButton/
* AddToCartButton's own `id`/`productId` props — those are the commerce
* slug (see lib/payload.ts's Product.id comment) — same "numericId, not
* id" split WishlistButton already uses.
@@ -67,34 +67,19 @@ export function NotifyMeForm({ productId, variantName = "" }: { productId: numbe
aria-label="E-Mail-Adresse für Verfügbarkeits-Info"
className="w-full rounded-sm border border-border px-3 py-2 text-body-sm text-text-primary bg-bg-base focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
/>
+ {/* Short on purpose — "Bei Verfügbarkeit benachrichtigen" wrapped to
+ two lines on narrow single-column cards (e.g. /konto/merkliste).
+ The "Ausverkauft" badge + email field right above already say
+ what this is for, so "Benachrichtigen" alone reads fine here. */}
- {status === "submitting" ? "…" : "Bei Verfügbarkeit benachrichtigen"}
+ {status === "submitting" ? "…" : "Benachrichtigen"}
{error &&
{error}
}
);
}
-// A non-interactive, visually identical (markup/classes) twin of
-// NotifyMeForm's idle state — rendered `invisible` behind every card's
-// actual CTA (see the two AddToCartButton components) so an in-stock
-// card's own single-button slot reserves the SAME height an out-of-stock
-// sibling's input+button would need. Without this, only out-of-stock
-// cards would be taller, and plain CSS Grid's row-stretch would then push
-// every other card's button down to match — same class of bug as
-// ProductGrid.tsx's existing min-h-reserved low-stock line, just for a
-// taller block instead of one text line.
-export function NotifyMeFormReservedSpace() {
- return (
-
diff --git a/app/email-preview/[type]/page.tsx b/app/email-preview/[type]/page.tsx
index 3fd3784..be429b8 100644
--- a/app/email-preview/[type]/page.tsx
+++ b/app/email-preview/[type]/page.tsx
@@ -42,13 +42,10 @@ const STATUS_TYPE_FALLBACK_HEADING: Record = {
// the actual sent email (orderEmail.ts) always reads the published version
// instead.
//
-// "back-in-stock" shares the generic order-status renderer below like
-// every other status type — an approximation, not pixel-identical (its
-// real send, lib/jobs/sendBackInStockEmails.ts on the Payload side, has no
-// order number/link at all, just a product name + "Jetzt ansehen" button),
-// same established gap as password-reset's own. Good enough to edit
-// subject/heading/bodyText/footerText live, which is what this page is
-// actually for.
+// "back-in-stock" has its own renderer (renderBackInStockHtml) rather than
+// sharing the generic order-status one below — it has no order at all, so
+// no order-number line, and its CTA points at the product page ("Zum
+// Produkt"), not /konto/bestellungen.
export default async function EmailPreviewPage({ params }: { params: Promise<{ type: string }> }) {
const { type } = await params;
if (!VALID_TYPES.includes(type as EmailTemplateType)) notFound();
diff --git a/app/konto/merkliste/components/MerklisteGrid.tsx b/app/konto/merkliste/components/MerklisteGrid.tsx
index 32ae4d9..ccb0b5f 100644
--- a/app/konto/merkliste/components/MerklisteGrid.tsx
+++ b/app/konto/merkliste/components/MerklisteGrid.tsx
@@ -42,7 +42,7 @@ export function MerklisteGrid({
}
return (
-
+
{visibleEntries.map(({ item, product }) => {
const discount = discountPercent(product.price, product.compareAtPrice);
const taxRate = effectiveTaxRate(product, defaultTaxRate);
diff --git a/app/lib/emailTemplates.ts b/app/lib/emailTemplates.ts
index d616570..0a0e5ec 100644
--- a/app/lib/emailTemplates.ts
+++ b/app/lib/emailTemplates.ts
@@ -361,6 +361,29 @@ export function renderOrderStatusHtml(
return emailShell(icon, escapeHtml(template.heading), body, template.footerText, buildLegalFooterLines(seller));
}
+// Separate from renderOrderStatusHtml — a back-in-stock mail has no order
+// at all (it fires from a "notify me" signup, not a purchase), so no
+// "Bestellnummer" line, and the CTA points at the product page, not
+// /konto/bestellungen. Mirrors lib/jobs/sendBackInStockEmails.ts on the
+// Payload backend, which is where the real send actually happens (see
+// this file's own top-of-file comment on why the two aren't literally
+// shared code).
+export function renderBackInStockHtml(template: EmailTemplateContent, productLabel: string, productUrl: string, seller: CompanySettings | null): string {
+ const body = `
+ ${paragraphs(template.bodyText, "center")}
+