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:
1 parent
d4bb6ec1cf
commit
578ec767b2
4 files changed
+119
-23
No files matched your search
@@ -303,6 +303,15 @@ export function CheckoutForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-16">
|
<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">
|
<Container className="grid lg:grid-cols-3 gap-14">
|
||||||
{payment && stripePublishableKey ? (
|
{payment && stripePublishableKey ? (
|
||||||
<div className="lg:col-span-3">
|
<div className="lg:col-span-3">
|
||||||
@@ -358,21 +367,52 @@ export function CheckoutForm({
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-ink/70 mb-3">{t.checkoutPage.fulfillmentLabel}</p>
|
<p className="text-sm text-ink/70 mb-3">{t.checkoutPage.fulfillmentLabel}</p>
|
||||||
<div className="flex gap-4">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
{(["VERSAND", "ABHOLUNG"] as const).map((option) => (
|
{(["VERSAND", "ABHOLUNG"] as const).map((option) => {
|
||||||
<button
|
const selected = fulfillment === option;
|
||||||
type="button"
|
return (
|
||||||
key={option}
|
<button
|
||||||
onClick={() => setFulfillment(option)}
|
type="button"
|
||||||
className={`flex-1 border px-4 py-3 text-sm text-left transition-colors ${
|
key={option}
|
||||||
fulfillment === option
|
onClick={() => setFulfillment(option)}
|
||||||
? "border-forest bg-forest text-cream"
|
aria-pressed={selected}
|
||||||
: "border-ink/20 text-ink/70"
|
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)]"
|
||||||
{option === "VERSAND" ? t.checkoutPage.shippingOption : t.checkoutPage.pickupOption}
|
: "border-ink/15 text-ink/70 hover:border-forest/50"
|
||||||
</button>
|
}`}
|
||||||
))}
|
>
|
||||||
|
<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>
|
</div>
|
||||||
{fulfillment === "VERSAND" ? (
|
{fulfillment === "VERSAND" ? (
|
||||||
<p className="mt-2 text-xs text-ink/70">
|
<p className="mt-2 text-xs text-ink/70">
|
||||||
@@ -603,15 +643,16 @@ export function CheckoutForm({
|
|||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<aside hidden={payment !== null} className="border border-ink/10 p-8 h-fit lg:sticky lg:top-24">
|
<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-lg text-forest mb-6">{t.checkoutPage.yourOrderHeading}</h2>
|
<h2 className="text-2xl text-forest mb-6">{t.checkoutPage.yourOrderHeading}</h2>
|
||||||
<ul className="space-y-3 text-sm">
|
<ul className="space-y-3 text-sm">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<li key={item.productId} className="flex justify-between">
|
<li key={item.productId} className="flex items-baseline justify-between gap-4">
|
||||||
<span>
|
<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>
|
||||||
<span>{formatPrice(item.priceCents * item.quantity, "EUR", locale)}</span>
|
<span className="tabular-nums">{formatPrice(item.priceCents * item.quantity, "EUR", locale)}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -637,9 +678,9 @@ export function CheckoutForm({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 pt-3 border-t border-ink/10 flex justify-between text-base text-forest">
|
<div className="mt-4 pt-4 border-t border-ink/10 flex items-baseline justify-between text-forest">
|
||||||
<span>{t.cartPage.total}</span>
|
<span className="text-base">{t.cartPage.total}</span>
|
||||||
<span>{formatPrice(total, "EUR", locale)}</span>
|
<span className="font-serif text-3xl tabular-nums">{formatPrice(total, "EUR", locale)}</span>
|
||||||
</div>
|
</div>
|
||||||
<TrustBadges className="mt-6 pt-6 border-t border-ink/10" />
|
<TrustBadges className="mt-6 pt-6 border-t border-ink/10" />
|
||||||
</aside>
|
</aside>
|
||||||
@@ -685,3 +726,52 @@ function Field({
|
|||||||
</label>
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1453,6 +1453,8 @@ export const de: Dictionary = {
|
|||||||
eyebrow: "Checkout",
|
eyebrow: "Checkout",
|
||||||
emptyCart: "Dein Warenkorb ist leer",
|
emptyCart: "Dein Warenkorb ist leer",
|
||||||
yourDetails: "Deine Daten",
|
yourDetails: "Deine Daten",
|
||||||
|
stepPayment: "Bezahlen",
|
||||||
|
stepsAriaLabel: "Schritte der Bestellung",
|
||||||
firstName: "Vorname",
|
firstName: "Vorname",
|
||||||
lastName: "Nachname",
|
lastName: "Nachname",
|
||||||
email: "E-Mail",
|
email: "E-Mail",
|
||||||
|
|||||||
@@ -1439,6 +1439,8 @@ export const en: Dictionary = {
|
|||||||
eyebrow: "Checkout",
|
eyebrow: "Checkout",
|
||||||
emptyCart: "Your cart is empty",
|
emptyCart: "Your cart is empty",
|
||||||
yourDetails: "Your Details",
|
yourDetails: "Your Details",
|
||||||
|
stepPayment: "Payment",
|
||||||
|
stepsAriaLabel: "Checkout steps",
|
||||||
firstName: "First name",
|
firstName: "First name",
|
||||||
lastName: "Last name",
|
lastName: "Last name",
|
||||||
email: "Email",
|
email: "Email",
|
||||||
|
|||||||
@@ -1102,6 +1102,8 @@ export interface Dictionary {
|
|||||||
eyebrow: string;
|
eyebrow: string;
|
||||||
emptyCart: string;
|
emptyCart: string;
|
||||||
yourDetails: string;
|
yourDetails: string;
|
||||||
|
stepPayment: string;
|
||||||
|
stepsAriaLabel: string;
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
Reference in new issue
Block a user