Move low-stock hint to badge, right-align VAT breakdown, gate cart discount field, split billing/shipping delivery method

- Removed the inline "Nur noch wenige verfügbar" text hint from
  AddToCartButton/AddToCartInlineButton (was making card heights vary in
  every grid that renders them — RelatedProducts, ProductSpotlight's CTA
  row) — now only shown via the same image-overlaid pill badge
  Ausverkauft/discount already use (position: absolute, doesn't affect
  layout). Added that badge to RelatedProducts.tsx and todo-cards'
  Pricing.tsx, which didn't have it before.
- RelatedProducts cards now also show "inkl. X% MwSt." (was missing
  entirely)
- VatBreakdown rows are now flex rows with a spacer instead of plain
  text, so every € amount right-aligns to the same edge regardless of
  how many digits the rate itself has (was visibly staggered with mixed
  7%/19% rates)
- Cart's manual discount-code field only renders when Payload actually
  has at least one active code right now (lib/discountServer.ts's new
  hasActiveDiscountCode()) — no point showing an open field that could
  never validate. An already-applied code (e.g. from an older session)
  still always shows its own result row regardless.
- Checkout's "1. Rechnungsadresse" no longer offers a Packstation option
  — a Packstation isn't a valid billing address for an invoice. Only a
  plain street address now; Packstation is only offered on the separate,
  optional "Abweichende Lieferadresse" section, which already had its own
  address/Packstation toggle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-22 23:25:18 +00:00
parent dc6b61324f
commit a50524832e
13 changed files with 175 additions and 141 deletions
+21 -88
View File
@@ -70,7 +70,6 @@ export function CheckoutContent({
const [shippingMethodId, setShippingMethodId] = useState<number | null>(shippingMethods[0]?.id ?? null);
const [paymentMethodId, setPaymentMethodId] = useState<number | null>(paymentMethods[0]?.id ?? null);
const [versandOpen, setVersandOpen] = useState(false);
const [deliveryMethod, setDeliveryMethod] = useState<"address" | "packstation">(savedProfile?.deliveryMethod ?? "address");
const [purchaseError, setPurchaseError] = useState<string | null>(null);
const [purchasing, setPurchasing] = useState(false);
const [showLogin, setShowLogin] = useState(false);
@@ -90,9 +89,10 @@ export function CheckoutContent({
const [firstName, setFirstName] = useState(savedProfile?.firstName ?? "");
const [lastName, setLastName] = useState(savedProfile?.lastName ?? "");
const [email, setEmail] = useState(savedProfile?.email ?? customerEmail ?? "");
// Always a plain street address — a Packstation isn't a valid Rechnungs-
// adresse (an invoice needs a real postal address). Packstation is only
// ever offered on the separate, optional shipping-address override below.
const [street, setStreet] = useState(savedProfile?.street ?? "");
const [packstationNumber, setPackstationNumber] = useState(savedProfile?.packstationNumber ?? "");
const [postNumber, setPostNumber] = useState(savedProfile?.postNumber ?? "");
const [zip, setZip] = useState(savedProfile?.zip ?? "");
const [city, setCity] = useState(savedProfile?.city ?? "");
const [country, setCountry] = useState(savedProfile?.country ?? "Deutschland");
@@ -130,10 +130,7 @@ export function CheckoutContent({
if (draft.firstName) setFirstName(draft.firstName);
if (draft.lastName) setLastName(draft.lastName);
if (draft.email) setEmail(draft.email);
if (draft.deliveryMethod) setDeliveryMethod(draft.deliveryMethod);
if (draft.street) setStreet(draft.street);
if (draft.packstationNumber) setPackstationNumber(draft.packstationNumber);
if (draft.postNumber) setPostNumber(draft.postNumber);
if (draft.zip) setZip(draft.zip);
if (draft.city) setCity(draft.city);
if (draft.country) setCountry(draft.country);
@@ -161,10 +158,7 @@ export function CheckoutContent({
firstName,
lastName,
email,
deliveryMethod,
street,
packstationNumber,
postNumber,
zip,
city,
country,
@@ -187,10 +181,7 @@ export function CheckoutContent({
firstName,
lastName,
email,
deliveryMethod,
street,
packstationNumber,
postNumber,
zip,
city,
country,
@@ -317,10 +308,10 @@ export function CheckoutContent({
// one address-card field that stays uncontrolled/unpersisted (see
// lib/checkoutDraft.ts's own comment on why).
password: customerEmail ? undefined : String(form.get("password") ?? ""),
deliveryMethod,
// Rechnungsadresse is always a plain street address — see the
// street state's own comment on why Packstation isn't offered here.
deliveryMethod: "address" as const,
street: street || undefined,
packstationNumber: packstationNumber || undefined,
postNumber: postNumber || undefined,
zip,
city,
country,
@@ -547,79 +538,21 @@ export function CheckoutContent({
</p>
</div>
)}
{/* Segmented control, same sm:w-[calc(50%-0.5rem)] half-row
width as the field(s) below it — Lieferadresse keeps
Straße und Hausnummer, Packstation swaps it out for
Packstationnummer + Postnummer (DHL's two Packstation
identifiers; there's no house number to give). */}
<div className="w-full sm:w-[calc(50%-0.5rem)] flex flex-col gap-2 items-start">
<span className="text-label text-text-muted">Lieferart</span>
<div className="flex w-full rounded-sm border border-border overflow-hidden">
<button
type="button"
onClick={() => setDeliveryMethod("address")}
aria-pressed={deliveryMethod === "address"}
className={`flex-1 py-3 text-body-sm font-bold transition-colors ${
deliveryMethod === "address"
? "bg-brand text-text-primary"
: "text-text-muted hover:text-text-primary"
}`}
>
Lieferadresse
</button>
<button
type="button"
onClick={() => setDeliveryMethod("packstation")}
aria-pressed={deliveryMethod === "packstation"}
className={`flex-1 py-3 text-body-sm font-bold border-l border-border transition-colors ${
deliveryMethod === "packstation"
? "bg-brand text-text-primary"
: "text-text-muted hover:text-text-primary"
}`}
>
Packstation
</button>
</div>
</div>
{deliveryMethod === "address" ? (
<FormField
label="Straße und Hausnummer"
name="street"
type="text"
value={street}
onChange={(e) => setStreet(e.target.value)}
placeholder="Musterstraße 1"
autoComplete="street-address"
required
wrapperClassName="w-full sm:w-[calc(50%-0.5rem)] sm:flex-none min-w-0"
/>
) : (
<div className="flex flex-col sm:flex-row gap-4 w-full">
<FormField
label="Packstationnummer"
name="packstationNumber"
type="text"
value={packstationNumber}
onChange={(e) => setPackstationNumber(e.target.value)}
inputMode="numeric"
placeholder="123"
autoComplete="off"
required
/>
<FormField
label="Postnummer"
name="postNumber"
type="text"
value={postNumber}
onChange={(e) => setPostNumber(e.target.value)}
inputMode="numeric"
placeholder="1234567"
autoComplete="off"
required
/>
</div>
)}
{/* Always a plain street address — Packstation isn't a valid
Rechnungsadresse (an invoice needs a real postal address).
Packstation is only ever offered below, in the optional
"Abweichende Lieferadresse" section's own Lieferart toggle. */}
<FormField
label="Straße und Hausnummer"
name="street"
type="text"
value={street}
onChange={(e) => setStreet(e.target.value)}
placeholder="Musterstraße 1"
autoComplete="street-address"
required
wrapperClassName="w-full sm:w-[calc(50%-0.5rem)] sm:flex-none min-w-0"
/>
<div className="flex flex-col sm:flex-row gap-4 w-full">
<FormField label="PLZ" name="zip" type="text" value={zip} onChange={(e) => setZip(e.target.value)} placeholder="10115" autoComplete="postal-code" required />
<FormField label="Ort" name="city" type="text" value={city} onChange={(e) => setCity(e.target.value)} placeholder="Berlin" autoComplete="address-level2" required />