feat(ui): Checkout mit Schrittanzeige und Lieferart-Karten

- Schrittanzeige "Deine Daten -> Bezahlen" mit Waben (rein anzeigend,
  gesteuert weiter ueber den bestehenden Zahlungszustand)
- Lieferart als Karten mit Symbol, Auswahl mit Waben-Haekchen, aria-pressed
- Bestelluebersicht abgehoben, Namen und Gesamtsumme in Serifenschrift
- Keine Aenderung an Bezahllogik oder Formularfeldern

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-09-25 13:27:35 +02:00
1 parent d4bb6ec1cf
commit 578ec767b2
4 files changed
+119 -23

No files matched your search

+113 -23
View File
@@ -303,6 +303,15 @@ export function CheckoutForm({
return (
<div className="py-16">
{/* Zwei Schritte, sichtbar gemacht: erst Daten, dann die eingebettete
Zahlung. Rein anzeigend — gesteuert wird weiter ueber "payment". */}
<Container className="mb-10">
<CheckoutSteps
current={payment ? 1 : 0}
labels={[t.checkoutPage.yourDetails, t.checkoutPage.stepPayment]}
ariaLabel={t.checkoutPage.stepsAriaLabel}
/>
</Container>
<Container className="grid lg:grid-cols-3 gap-14">
{payment && stripePublishableKey ? (
<div className="lg:col-span-3">
@@ -358,21 +367,52 @@ export function CheckoutForm({
<div>
<p className="text-sm text-ink/70 mb-3">{t.checkoutPage.fulfillmentLabel}</p>
<div className="flex gap-4">
{(["VERSAND", "ABHOLUNG"] as const).map((option) => (
<button
type="button"
key={option}
onClick={() => setFulfillment(option)}
className={`flex-1 border px-4 py-3 text-sm text-left transition-colors ${
fulfillment === option
? "border-forest bg-forest text-cream"
: "border-ink/20 text-ink/70"
}`}
>
{option === "VERSAND" ? t.checkoutPage.shippingOption : t.checkoutPage.pickupOption}
</button>
))}
<div className="grid gap-4 sm:grid-cols-2">
{(["VERSAND", "ABHOLUNG"] as const).map((option) => {
const selected = fulfillment === option;
return (
<button
type="button"
key={option}
onClick={() => setFulfillment(option)}
aria-pressed={selected}
className={`group relative flex items-center gap-4 border px-5 py-4 text-left transition-[border-color,background-color,box-shadow] duration-300 ${
selected
? "border-forest bg-forest/[0.04] text-forest shadow-[0_14px_30px_-22px_rgba(36,53,43,0.6)]"
: "border-ink/15 text-ink/70 hover:border-forest/50"
}`}
>
<span
aria-hidden="true"
className={`flex h-10 w-10 flex-shrink-0 items-center justify-center border transition-colors ${
selected ? "border-forest bg-forest text-cream" : "border-ink/15 text-forest"
}`}
>
{option === "VERSAND" ? (
<svg viewBox="0 0 24 24" className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M3 7h11v9H3zM14 10h4l3 3v3h-7z" strokeLinejoin="round" />
<circle cx="7" cy="17.5" r="1.5" />
<circle cx="17.5" cy="17.5" r="1.5" />
</svg>
) : (
<svg viewBox="0 0 24 24" className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M4 11l8-6 8 6v9H4z" strokeLinejoin="round" />
<path d="M10 20v-5h4v5" />
</svg>
)}
</span>
<span className="font-serif text-xl">
{option === "VERSAND" ? t.checkoutPage.shippingOption : t.checkoutPage.pickupOption}
</span>
{selected ? (
<svg viewBox="0 0 24 26" aria-hidden="true" className="absolute right-4 top-1/2 h-4 w-4 -translate-y-1/2 text-honey">
<polygon points="12,1 22.4,7 22.4,19 12,25 1.6,19 1.6,7" fill="currentColor" />
<path d="M8 13.2l2.8 2.8 5.2-5.6" stroke="var(--color-cream)" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" />
</svg>
) : null}
</button>
);
})}
</div>
{fulfillment === "VERSAND" ? (
<p className="mt-2 text-xs text-ink/70">
@@ -603,15 +643,16 @@ export function CheckoutForm({
</p>
</form>
<aside hidden={payment !== null} className="border border-ink/10 p-8 h-fit lg:sticky lg:top-24">
<h2 className="text-lg text-forest mb-6">{t.checkoutPage.yourOrderHeading}</h2>
<aside hidden={payment !== null} className="border border-ink/10 bg-background p-8 h-fit lg:sticky lg:top-28 shadow-[0_24px_50px_-34px_rgba(36,53,43,0.45)]">
<h2 className="text-2xl text-forest mb-6">{t.checkoutPage.yourOrderHeading}</h2>
<ul className="space-y-3 text-sm">
{items.map((item) => (
<li key={item.productId} className="flex justify-between">
<li key={item.productId} className="flex items-baseline justify-between gap-4">
<span>
{item.quantity} × {item.name}
<span className="text-ink/55 tabular-nums">{item.quantity} ×</span>{" "}
<span className="font-serif text-lg text-forest">{item.name}</span>
</span>
<span>{formatPrice(item.priceCents * item.quantity, "EUR", locale)}</span>
<span className="tabular-nums">{formatPrice(item.priceCents * item.quantity, "EUR", locale)}</span>
</li>
))}
</ul>
@@ -637,9 +678,9 @@ export function CheckoutForm({
</div>
) : null}
</div>
<div className="mt-3 pt-3 border-t border-ink/10 flex justify-between text-base text-forest">
<span>{t.cartPage.total}</span>
<span>{formatPrice(total, "EUR", locale)}</span>
<div className="mt-4 pt-4 border-t border-ink/10 flex items-baseline justify-between text-forest">
<span className="text-base">{t.cartPage.total}</span>
<span className="font-serif text-3xl tabular-nums">{formatPrice(total, "EUR", locale)}</span>
</div>
<TrustBadges className="mt-6 pt-6 border-t border-ink/10" />
</aside>
@@ -685,3 +726,52 @@ function Field({
</label>
);
}
/** Schrittanzeige mit Waben: erledigt = gefuellt, aktuell = Honig mit Puls. */
function CheckoutSteps({ current, labels, ariaLabel }: { current: number; labels: string[]; ariaLabel: string }) {
return (
<ol aria-label={ariaLabel} className="flex items-center gap-3 text-sm">
{labels.map((label, i) => {
const done = i < current;
const active = i === current;
return (
<li key={label} className="flex items-center gap-3" aria-current={active ? "step" : undefined}>
<span className="flex items-center gap-2.5">
<svg
viewBox="0 0 24 26"
aria-hidden="true"
className={`h-6 w-6 rounded-full ${active ? "motion-pulse animate-[wh-pulse_2.2s_ease-out_infinite]" : ""}`}
>
<polygon
points="12,1 22.4,7 22.4,19 12,25 1.6,19 1.6,7"
fill={done ? "var(--color-forest)" : active ? "var(--color-honey)" : "var(--color-background)"}
stroke={done ? "var(--color-forest)" : active ? "var(--color-honey-dark)" : "color-mix(in oklab, var(--color-ink) 25%, transparent)"}
strokeWidth="1.5"
/>
{done ? (
<path d="M8 13.2l2.8 2.8 5.2-5.6" stroke="var(--color-cream)" strokeWidth="2" fill="none" strokeLinecap="round" strokeLinejoin="round" />
) : (
<text x="12" y="16.5" textAnchor="middle" fontSize="10" fill={active ? "var(--color-cream)" : "var(--color-ink)"}>
{i + 1}
</text>
)}
</svg>
<span className={`tracking-wide-caps text-xs ${active ? "text-forest" : done ? "text-forest/70" : "text-ink/50"}`}>
{label}
</span>
</span>
{i < labels.length - 1 ? (
<span aria-hidden="true" className="relative h-[2px] w-12 overflow-hidden rounded-full bg-ink/10 sm:w-20">
<span
className={`absolute inset-0 origin-left bg-honey transition-transform duration-700 ease-[var(--ease-arrive)] ${
done ? "scale-x-100" : "scale-x-0"
}`}
/>
</span>
) : null}
</li>
);
})}
</ol>
);
}
+2
View File
@@ -1453,6 +1453,8 @@ export const de: Dictionary = {
eyebrow: "Checkout",
emptyCart: "Dein Warenkorb ist leer",
yourDetails: "Deine Daten",
stepPayment: "Bezahlen",
stepsAriaLabel: "Schritte der Bestellung",
firstName: "Vorname",
lastName: "Nachname",
email: "E-Mail",
+2
View File
@@ -1439,6 +1439,8 @@ export const en: Dictionary = {
eyebrow: "Checkout",
emptyCart: "Your cart is empty",
yourDetails: "Your Details",
stepPayment: "Payment",
stepsAriaLabel: "Checkout steps",
firstName: "First name",
lastName: "Last name",
email: "Email",
+2
View File
@@ -1102,6 +1102,8 @@ export interface Dictionary {
eyebrow: string;
emptyCart: string;
yourDetails: string;
stepPayment: string;
stepsAriaLabel: string;
firstName: string;
lastName: string;
email: string;