Move low-stock warning from image badge to a text hint, add it to the cart
The image-overlaid pill made the low-stock message read as clutter on product photos and had no equivalent in the cart at all. It's now a text line next to the price (ProductGrid/ProductSpotlight/RelatedProducts/ Pricing) and under the product name in cart line items (variant-specific, not "any variant low"). The line's height is always reserved, not conditionally rendered, so cards in the same row stay equal-height regardless of low-stock state — the exact regression an earlier text-based version of this hint caused before it was replaced by the image badge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -184,6 +184,13 @@ export function CartContent({
|
||||
// when the quantity or remove control is used, same "full
|
||||
// key" reasoning as cart.ts's own sameLine().
|
||||
const lineKey = entry.variant ? `${product.id}::${entry.variant}` : product.id;
|
||||
// The exact variant this line is for, not "any variant low"
|
||||
// like the product-grid cards use — a cart line already has
|
||||
// its variant chosen, so it should only warn when that
|
||||
// specific variant (not some other one) is running low.
|
||||
const lowStock = entry.variant
|
||||
? (product.variants.find((v) => v.name === entry.variant)?.lowStock ?? false)
|
||||
: product.lowStock;
|
||||
return (
|
||||
<div key={lineKey} className="w-full">
|
||||
{i > 0 && <div className="h-px bg-border w-full mb-6" />}
|
||||
@@ -204,6 +211,10 @@ export function CartContent({
|
||||
{product.name}
|
||||
{entry.variant ? ` (${entry.variant})` : ""}
|
||||
</p>
|
||||
{/* Independent stacked rows, not grid siblings — no
|
||||
equal-height pressure from neighboring lines, so a
|
||||
plain conditional line is enough here. */}
|
||||
{lowStock && <p className="text-label font-bold text-warning">Nur noch wenige verfügbar</p>}
|
||||
<p className="font-bold text-body-sm text-text-muted">{product.description}</p>
|
||||
<div className="flex flex-col gap-0.5 items-start">
|
||||
<p className="text-label text-text-muted">Einzelpreis</p>
|
||||
|
||||
@@ -159,28 +159,20 @@ export function RelatedProducts({ defaultTaxRate }: { defaultTaxRate: number })
|
||||
/>
|
||||
{/* Same top-left pill pattern as ProductGrid.tsx/
|
||||
ProductSpotlight.tsx — position: absolute, so it never
|
||||
affects this card's height the way the old inline
|
||||
low-stock text hint under the button used to (that
|
||||
variability was exactly what broke equal card heights
|
||||
in this grid). */}
|
||||
affects this card's height. Only the discount/Ausverkauft
|
||||
pill lives here now; the low-stock hint moved to a
|
||||
reserved-height text line below (see the min-h paragraph
|
||||
under the price) — plain conditional text here is what
|
||||
broke equal card heights in this grid before. */}
|
||||
{fullyOutOfStock ? (
|
||||
<span className="absolute top-3 left-3 rounded-full bg-text-muted px-2.5 py-1 text-label font-bold text-bg-base">
|
||||
Ausverkauft
|
||||
</span>
|
||||
) : (
|
||||
(discount !== null || anyLowStock) && (
|
||||
<div className="absolute top-3 left-3 flex flex-col items-start gap-1">
|
||||
{discount !== null && (
|
||||
<span className="rounded-full bg-brand px-2.5 py-1 text-label font-bold text-text-primary">
|
||||
-{discount}%
|
||||
</span>
|
||||
)}
|
||||
{anyLowStock && (
|
||||
<span className="rounded-full bg-warning px-2.5 py-1 text-label font-bold text-text-on-dark">
|
||||
Nur noch wenige verfügbar
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
discount !== null && (
|
||||
<span className="absolute top-3 left-3 rounded-full bg-brand px-2.5 py-1 text-label font-bold text-text-primary">
|
||||
-{discount}%
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
@@ -198,6 +190,14 @@ export function RelatedProducts({ defaultTaxRate }: { defaultTaxRate: number })
|
||||
<span className="font-bold text-h4 text-text-primary">{formatPrice(product.price)}</span>
|
||||
<span className="text-label text-text-muted">inkl. {taxRate}% MwSt.</span>
|
||||
</p>
|
||||
{/* Always rendered, text conditional — min-h reserves this
|
||||
line's height in both states so cards in the same row
|
||||
stay equal height regardless of low-stock status; this
|
||||
component has no h-full/flex-1 spacer trick like
|
||||
ProductGrid.tsx to absorb a variable-height line instead. */}
|
||||
<p className="min-h-[1.05rem] text-label font-bold text-warning">
|
||||
{anyLowStock ? "Nur noch wenige verfügbar" : null}
|
||||
</p>
|
||||
<AddToCartInlineButton id={product.id} outOfStock={product.outOfStock} variants={product.variants} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,19 +56,10 @@ export async function ProductSpotlight() {
|
||||
Ausverkauft
|
||||
</span>
|
||||
) : (
|
||||
(discount !== null || anyLowStock) && (
|
||||
<div className="absolute top-3 left-3 flex flex-col items-start gap-1">
|
||||
{discount !== null && (
|
||||
<span className="rounded-full bg-brand px-2.5 py-1 text-label font-bold text-text-primary">
|
||||
-{discount}%
|
||||
</span>
|
||||
)}
|
||||
{anyLowStock && (
|
||||
<span className="rounded-full bg-warning px-2.5 py-1 text-label font-bold text-text-on-dark">
|
||||
Nur noch wenige verfügbar
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
discount !== null && (
|
||||
<span className="absolute top-3 left-3 rounded-full bg-brand px-2.5 py-1 text-label font-bold text-text-primary">
|
||||
-{discount}%
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
@@ -99,6 +90,10 @@ export async function ProductSpotlight() {
|
||||
Lieferzeit: {shipping.totalDays.min}–{shipping.totalDays.max} Werktage innerhalb Deutschlands
|
||||
</p>
|
||||
</div>
|
||||
{/* Single product, no grid siblings to stay equal-height with
|
||||
(unlike ProductGrid.tsx/RelatedProducts.tsx), so this can be
|
||||
a plain conditional line instead of a reserved-height slot. */}
|
||||
{anyLowStock && <p className="text-label font-bold text-warning">Nur noch wenige verfügbar</p>}
|
||||
{/* items-start at sm: — without it, the default cross-axis
|
||||
stretch makes "Mehr erfahren" grow to match
|
||||
AddToCartButton's own height whenever that one gets taller
|
||||
|
||||
@@ -58,23 +58,10 @@ export async function ProductGrid() {
|
||||
Ausverkauft
|
||||
</span>
|
||||
) : (
|
||||
(discount !== null || anyLowStock) && (
|
||||
// Stacked, not either/or — a discounted product running
|
||||
// low on stock at the same time (the actual todo-karten
|
||||
// case) needs both badges, not just whichever came first
|
||||
// in a ternary.
|
||||
<div className="absolute top-3 left-3 flex flex-col items-start gap-1">
|
||||
{discount !== null && (
|
||||
<span className="rounded-full bg-brand px-2.5 py-1 text-label font-bold text-text-primary">
|
||||
-{discount}%
|
||||
</span>
|
||||
)}
|
||||
{anyLowStock && (
|
||||
<span className="rounded-full bg-warning px-2.5 py-1 text-label font-bold text-text-on-dark">
|
||||
Nur noch wenige verfügbar
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
discount !== null && (
|
||||
<span className="absolute top-3 left-3 rounded-full bg-brand px-2.5 py-1 text-label font-bold text-text-primary">
|
||||
-{discount}%
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
@@ -97,6 +84,16 @@ export async function ProductGrid() {
|
||||
Lieferzeit: {shipping.totalDays.min}–{shipping.totalDays.max} Werktage innerhalb Deutschlands
|
||||
</p>
|
||||
</div>
|
||||
{/* Always rendered, text conditional — not a conditional
|
||||
block — so this line's height (min-h as a cross-browser
|
||||
safety net for the empty case) is identical whether or
|
||||
not the product is low-stock. See AddToCartButton.tsx's
|
||||
own comment: an earlier text-based low-stock hint here
|
||||
broke equal card heights across the grid, which is why
|
||||
it moved to the image-overlay pill in the first place. */}
|
||||
<p className="min-h-[1.05rem] text-label font-bold text-warning">
|
||||
{anyLowStock ? "Nur noch wenige verfügbar" : null}
|
||||
</p>
|
||||
{product.href && (
|
||||
<Link
|
||||
href={product.href}
|
||||
|
||||
@@ -49,19 +49,10 @@ export async function Pricing() {
|
||||
Ausverkauft
|
||||
</span>
|
||||
) : (
|
||||
(discount !== null || anyLowStock) && (
|
||||
<div className="absolute top-3 left-3 flex flex-col items-start gap-1">
|
||||
{discount !== null && (
|
||||
<span className="rounded-full bg-brand px-2.5 py-1 text-label font-bold text-text-primary">
|
||||
-{discount}%
|
||||
</span>
|
||||
)}
|
||||
{anyLowStock && (
|
||||
<span className="rounded-full bg-warning px-2.5 py-1 text-label font-bold text-text-on-dark">
|
||||
Nur noch wenige verfügbar
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
discount !== null && (
|
||||
<span className="absolute top-3 left-3 rounded-full bg-brand px-2.5 py-1 text-label font-bold text-text-primary">
|
||||
-{discount}%
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
@@ -101,6 +92,9 @@ export async function Pricing() {
|
||||
Lieferzeit: {shipping.totalDays.min}–{shipping.totalDays.max} Werktage innerhalb Deutschlands
|
||||
</p>
|
||||
</div>
|
||||
{/* Single product, no grid siblings to stay equal-height with —
|
||||
plain conditional line, same reasoning as ProductSpotlight.tsx. */}
|
||||
{anyLowStock && <p className="text-label font-bold text-warning">Nur noch wenige verfügbar</p>}
|
||||
{/* No "Sichere Zahlung" trust note here (unlike Cart/Checkout) —
|
||||
this is an add-to-cart step, not the actual payment step, so
|
||||
a payment-security reassurance is premature here and just
|
||||
|
||||
Reference in New Issue
Block a user