Fix out-of-stock CTA: replace button instead of stacking, collapse notify form
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>
This commit is contained in:
@@ -88,7 +88,9 @@ export function AddToCartButton({
|
||||
: added
|
||||
? "border border-success! bg-success-subtle! hover:bg-success-subtle! text-success!"
|
||||
: "";
|
||||
const displayLabel = currentlyOutOfStock ? "Ausverkauft" : limitReached ? "Maximale Menge im Warenkorb" : label;
|
||||
// currentlyOutOfStock has no branch here — that state renders
|
||||
// NotifyMeForm instead of this button entirely (see below).
|
||||
const displayLabel = limitReached ? "Maximale Menge im Warenkorb" : label;
|
||||
|
||||
return (
|
||||
// Low stock is deliberately NOT surfaced here as its own text line
|
||||
@@ -117,47 +119,48 @@ export function AddToCartButton({
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<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
|
||||
button visibly shrink while showing the success state. Stacking
|
||||
both possible texts in the same grid cell (both invisible ones
|
||||
still contribute to sizing) reserves width for whichever is
|
||||
wider, so the button's box never changes size either way. Now
|
||||
also reserves space for "Ausverkauft"/"Maximale Menge im
|
||||
Warenkorb" — the widest of the four wins regardless of which is
|
||||
showing. */}
|
||||
{/* whitespace-nowrap — inherited by every stacked span below. On a
|
||||
w-full button (e.g. this page's mobile layout), "Maximale Menge
|
||||
im Warenkorb" is long enough to wrap to two lines without this,
|
||||
and since every stacked span shares the same grid cell, that
|
||||
inflated the row height for whichever text is actually showing
|
||||
too — "Ausverkauft" rendered with a tall empty gap underneath it
|
||||
(fixed 2026-07-24). */}
|
||||
<span className="relative grid whitespace-nowrap">
|
||||
<span className="invisible [grid-area:1/1]" aria-hidden="true">
|
||||
{label}
|
||||
{currentlyOutOfStock ? (
|
||||
// Replaces the button slot entirely rather than stacking below a
|
||||
// disabled "Ausverkauft" button — same reasoning as
|
||||
// AddToCartInlineButton's identical swap.
|
||||
<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
|
||||
button visibly shrink while showing the success state. Stacking
|
||||
both possible texts in the same grid cell (both invisible ones
|
||||
still contribute to sizing) reserves width for whichever is
|
||||
wider, so the button's box never changes size either way. Now
|
||||
also reserves space for "Maximale Menge im Warenkorb" — the
|
||||
widest of the three wins regardless of which is showing. */}
|
||||
{/* whitespace-nowrap — inherited by every stacked span below. On a
|
||||
w-full button (e.g. this page's mobile layout), "Maximale Menge
|
||||
im Warenkorb" is long enough to wrap to two lines without this,
|
||||
and since every stacked span shares the same grid cell, that
|
||||
inflated the row height for whichever text is actually showing
|
||||
too (fixed 2026-07-24). */}
|
||||
<span className="relative grid whitespace-nowrap">
|
||||
<span className="invisible [grid-area:1/1]" aria-hidden="true">
|
||||
{label}
|
||||
</span>
|
||||
<span className="invisible [grid-area:1/1]" aria-hidden="true">
|
||||
Hinzugefügt ✓
|
||||
</span>
|
||||
<span className="invisible [grid-area:1/1]" aria-hidden="true">
|
||||
Maximale Menge im Warenkorb
|
||||
</span>
|
||||
<span className="[grid-area:1/1]">{added ? "Hinzugefügt ✓" : displayLabel}</span>
|
||||
</span>
|
||||
<span className="invisible [grid-area:1/1]" aria-hidden="true">
|
||||
Hinzugefügt ✓
|
||||
</span>
|
||||
<span className="invisible [grid-area:1/1]" aria-hidden="true">
|
||||
Ausverkauft
|
||||
</span>
|
||||
<span className="invisible [grid-area:1/1]" aria-hidden="true">
|
||||
Maximale Menge im Warenkorb
|
||||
</span>
|
||||
<span className="[grid-area:1/1]">{added ? "Hinzugefügt ✓" : displayLabel}</span>
|
||||
</span>
|
||||
</button>
|
||||
{currentlyOutOfStock && <NotifyMeForm productId={numericId} variantName={variants.length > 0 ? (selectedVariant ?? "") : ""} />}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -113,24 +113,35 @@ export function AddToCartInlineButton({
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<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")
|
||||
}
|
||||
{currentlyOutOfStock ? (
|
||||
// Replaces the button slot entirely rather than stacking below a
|
||||
// disabled "Ausverkauft" button — this component's collapsed idle
|
||||
// state is a single button, same height as the "In den Warenkorb"
|
||||
// button it replaces, so an out-of-stock card doesn't end up taller
|
||||
// than its in-stock siblings and stretch their own buttons down
|
||||
// (plain CSS Grid rows stretch to the tallest card — see
|
||||
// ProductGrid.tsx's own flex-1-spacer comment on why equal card
|
||||
// height matters here).
|
||||
<NotifyMeForm productId={numericId} variantName={variants.length > 0 ? (selectedVariant ?? "") : ""} />
|
||||
) : (
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
className={`${base} ${stateClasses}`}
|
||||
>
|
||||
{currentlyOutOfStock ? "Ausverkauft" : 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>
|
||||
{currentlyOutOfStock && <NotifyMeForm productId={numericId} variantName={variants.length > 0 ? (selectedVariant ?? "") : ""} />}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,15 @@ import { isValidEmail } from "../lib/email";
|
||||
* same "numericId, not id" split WishlistButton already uses.
|
||||
*/
|
||||
export function NotifyMeForm({ productId, variantName = "" }: { productId: number; variantName?: string }) {
|
||||
// Collapsed by default (a single button, same height/style as
|
||||
// AddToCartInlineButton's own "In den Warenkorb" button) — grid card
|
||||
// layouts here rely on every card in a row being stretched to equal
|
||||
// height (plain CSS Grid, no explicit height), so an always-expanded
|
||||
// input+button would make an out-of-stock card taller than its row
|
||||
// siblings and push THEIR buttons down as the row stretches to match.
|
||||
// Expanding only after a click keeps the idle-state card height
|
||||
// identical to every in-stock card next to it.
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [email, setEmail] = useState("");
|
||||
const [status, setStatus] = useState<"idle" | "submitting" | "success" | "error">("idle");
|
||||
const [error, setError] = useState("");
|
||||
@@ -48,12 +57,25 @@ export function NotifyMeForm({ productId, variantName = "" }: { productId: numbe
|
||||
return <p className="text-body-sm text-success">Danke! Wir melden uns, sobald es wieder verfügbar ist.</p>;
|
||||
}
|
||||
|
||||
if (!expanded) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded(true)}
|
||||
className="flex items-center justify-between px-5 py-3 rounded-sm border border-border w-full hover:border-brand transition-colors"
|
||||
>
|
||||
<span className="text-body-sm text-text-primary">Bei Verfügbarkeit benachrichtigen</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
// Stacked, not side-by-side — narrow product cards (shop grid, related
|
||||
// products) don't leave enough width for input + button in one row
|
||||
// without either truncating the placeholder or squeezing the button.
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-2 w-full">
|
||||
<input
|
||||
autoFocus
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
@@ -66,7 +88,7 @@ export function NotifyMeForm({ productId, variantName = "" }: { productId: numbe
|
||||
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" ? "…" : "Absenden"}
|
||||
</button>
|
||||
{error && <p className="text-label text-red-600">{error}</p>}
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user