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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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. */}
|
||||
|
||||
Reference in New Issue
Block a user