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 (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1102,6 +1102,8 @@ export interface Dictionary {
|
||||
eyebrow: string;
|
||||
emptyCart: string;
|
||||
yourDetails: string;
|
||||
stepPayment: string;
|
||||
stepsAriaLabel: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
|
||||
Reference in new issue
Block a user