Match sold-out card height to in-stock siblings

NotifyMeForm was two stacked rows (email input + full-width button),
making an out-of-stock ProductCard taller than its in-stock siblings.
Redesigned as a single input with the submit control embedded inside it
(same py-3 as AddToCartInlineButton's own button, so the row height
matches exactly), and ProductCard now skips its reserved-height
low-stock line entirely for a fully-out-of-stock product, since that
line can never apply there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-08-02 07:14:49 +00:00
parent c8f231baa6
commit 82c1b1fd43
2 changed files with 48 additions and 37 deletions
+36 -34
View File
@@ -2,21 +2,27 @@
import { useState } from "react";
import { isValidEmail } from "../lib/email";
import { ArrowRightIcon } from "./ArrowRightIcon";
/**
* Replaces the (disabled) Add-to-cart button's spot once a product/variant
* 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 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.
* back" mail. Always shows the email input + submit control directly (no
* extra click to reveal them).
*
* One input with the submit control embedded inside it (absolutely
* positioned, not a layout sibling) — not a separate stacked button below
* the input. A first version stacked input + full-width "Benachrichtigen"
* button, which made an out-of-stock ProductCard.tsx visibly taller than
* its in-stock siblings (two form rows vs. one button); shrinking that
* stacked version's own padding was tried and reverted — the actual ask
* was to keep every control's height untouched and make the CARD match
* instead. Embedding the submit icon inside the input keeps this whole
* control to exactly one row, sized with the same `py-3` as
* AddToCartInlineButton's own "In den Warenkorb" button, so a sold-out
* card ends up exactly as tall as an in-stock one — see that component's
* own button classes.
*/
export function NotifyMeForm({ productId, variantName = "" }: { productId: number; variantName?: string }) {
const [email, setEmail] = useState("");
@@ -55,31 +61,27 @@ export function NotifyMeForm({ productId, variantName = "" }: { productId: numbe
}
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
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="E-Mail-Adresse"
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-left text-body-sm font-semibold text-text-primary hover:border-brand transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
>
{status === "submitting" ? "…" : "Benachrichtigen"}
</button>
<form onSubmit={handleSubmit} className="flex flex-col gap-1.5 w-full">
<div className="relative w-full">
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="E-Mail-Adresse"
aria-label="E-Mail-Adresse für Verfügbarkeits-Info"
className="w-full rounded-sm border border-border pl-3 pr-12 py-3 text-body-sm text-text-primary bg-bg-base focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand"
/>
<button
type="submit"
disabled={status === "submitting"}
aria-label="Benachrichtigen"
title="Benachrichtigen"
className="absolute right-1.5 top-1/2 -translate-y-1/2 flex size-8 items-center justify-center rounded-sm text-text-primary hover:text-brand transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
>
{status === "submitting" ? "…" : <ArrowRightIcon />}
</button>
</div>
{error && <p className="text-label text-red-600">{error}</p>}
</form>
);
}
+12 -3
View File
@@ -108,9 +108,18 @@ export function ProductCard({
{!kleinunternehmer && <span className="text-label text-text-muted">inkl. {taxRate}% MwSt.</span>}
</p>
{belowPrice}
{/* Always rendered, text conditional — reserved height keeps every
card in a row equal height regardless of low-stock status. */}
<p className="min-h-[1.05rem] text-label font-bold text-warning">{anyLowStock ? "Nur noch wenige verfügbar" : null}</p>
{/* Reserved height keeps every IN-STOCK card in a row equal height
regardless of low-stock status — but a fully-out-of-stock
product can never show this line (anyLowStock is meaningless
once nothing's left to sell), so it's omitted entirely rather
than reserving dead space here. That dead space was exactly
what made a sold-out card's NotifyMeForm sit lower than
necessary, growing the whole card taller than its in-stock
siblings — see NotifyMeForm.tsx's own comment on that height
mismatch. */}
{!fullyOutOfStock && (
<p className="min-h-[1.05rem] text-label font-bold text-warning">{anyLowStock ? "Nur noch wenige verfügbar" : null}</p>
)}
{/* flex-1 spacer — pins every card's button to the same Y
regardless of whether the title/category line wraps. */}