Fix out-of-stock CTA regressions: grid alignment, preview accuracy, label length
- 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>
This commit is contained in:
@@ -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({
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{/* Same reserved-space stack as AddToCartInlineButton.tsx's identical
|
||||
block — see that file's own comment. */}
|
||||
<div className="relative grid w-full">
|
||||
<div className="invisible pointer-events-none [grid-area:1/1]">
|
||||
<NotifyMeFormReservedSpace />
|
||||
</div>
|
||||
<div className="[grid-area:1/1] self-start">
|
||||
{currentlyOutOfStock ? (
|
||||
<NotifyMeForm productId={numericId} variantName={variants.length > 0 ? (selectedVariant ?? "") : ""} />
|
||||
) : (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
className={`${base} ${stateClasses}`}
|
||||
>
|
||||
{currentlyOutOfStock ? (
|
||||
// Replaces the button slot entirely rather than stacking below a
|
||||
// disabled "Ausverkauft" button — same reasoning as
|
||||
// AddToCartInlineButton's identical swap (see that file's own
|
||||
// comment on the `items-start` grid fix this relies on).
|
||||
<NotifyMeForm productId={numericId} variantName={variants.length > 0 ? (selectedVariant ?? "") : ""} />
|
||||
) : (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
className={`${base} ${stateClasses}`}
|
||||
>
|
||||
{/* CSS-grid text-stack, not just swapping the button's text node
|
||||
directly — this button is inline-flex/content-sized (no w-full),
|
||||
so "Hinzugefügt ✓" being shorter than most labels made the whole
|
||||
@@ -163,10 +160,8 @@ export function AddToCartButton({
|
||||
</span>
|
||||
<span className="[grid-area:1/1]">{added ? "Hinzugefügt ✓" : displayLabel}</span>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
{/* 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. */}
|
||||
<div className="relative grid w-full">
|
||||
<div className="invisible pointer-events-none [grid-area:1/1]">
|
||||
<NotifyMeFormReservedSpace />
|
||||
</div>
|
||||
{/* 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. */}
|
||||
<div className="[grid-area:1/1] self-start">
|
||||
{currentlyOutOfStock ? (
|
||||
<NotifyMeForm productId={numericId} variantName={variants.length > 0 ? (selectedVariant ?? "") : ""} />
|
||||
) : (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
className={`${base} ${stateClasses}`}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
"text-body-sm transition-colors " +
|
||||
(disabled ? "text-text-muted" : added ? "font-semibold text-success" : "text-text-primary")
|
||||
}
|
||||
>
|
||||
{limitReached ? "Maximale Menge im Warenkorb" : added ? "Hinzugefügt ✓" : label}
|
||||
</span>
|
||||
<Image alt="" src="/icon-cart-outline.png" width={32} height={30} className="h-[1.875rem] w-8 object-contain" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{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).
|
||||
<NotifyMeForm productId={numericId} variantName={variants.length > 0 ? (selectedVariant ?? "") : ""} />
|
||||
) : (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
className={`${base} ${stateClasses}`}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
"text-body-sm transition-colors " +
|
||||
(disabled ? "text-text-muted" : added ? "font-semibold text-success" : "text-text-primary")
|
||||
}
|
||||
>
|
||||
{limitReached ? "Maximale Menge im Warenkorb" : added ? "Hinzugefügt ✓" : label}
|
||||
</span>
|
||||
<Image alt="" src="/icon-cart-outline.png" width={32} height={30} className="h-[1.875rem] w-8 object-contain" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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. */}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={status === "submitting"}
|
||||
className="w-full rounded-sm border border-border px-4 py-2 text-body-sm font-semibold text-text-primary hover:border-brand transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
>
|
||||
{status === "submitting" ? "…" : "Bei Verfügbarkeit benachrichtigen"}
|
||||
{status === "submitting" ? "…" : "Benachrichtigen"}
|
||||
</button>
|
||||
{error && <p className="text-label text-red-600">{error}</p>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="flex flex-col gap-2 w-full" aria-hidden="true">
|
||||
<input type="email" tabIndex={-1} disabled placeholder="E-Mail-Adresse" className="w-full rounded-sm border border-border px-3 py-2 text-body-sm" />
|
||||
<button type="button" tabIndex={-1} disabled className="w-full rounded-sm border border-border px-4 py-2 text-body-sm font-semibold">
|
||||
Bei Verfügbarkeit benachrichtigen
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user