feat: add /bestellbestaetigung order confirmation page

Matches the Figma mockup (checkmark hero, order summary card,
delivery-status panel, testimonial band) with the checkout's 4-step
bar inserted (all steps done) and the "Bis dahin: Lass dich
inspirieren" block omitted, per request.

Extracted the step bar into a shared CheckoutSteps component so
/checkout and /bestellbestaetigung don't duplicate it. The actually-
selected shipping cost and payment method are captured into a
sessionStorage snapshot by /checkout's "Jetzt kaufen" click (there's
no real order backend, so this click is what "placing the order"
means here) and read back on the confirmation page — not just
defaulted to the first active method of each, so the receipt matches
what the shopper actually picked. Also tightened the checkout
newsletter-consent copy ("Wenn du zustimmst" instead of "Wenn du
oben zustimmst").
This commit is contained in:
Marco
2026-07-19 23:57:03 +00:00
parent f973eccca6
commit 90be4696af
7 changed files with 389 additions and 52 deletions
+28 -52
View File
@@ -3,44 +3,15 @@
import { useState } from "react";
import Link from "next/link";
import Image from "next/image";
import { useCart } from "../../lib/cart";
import { useCart, clearCart } from "../../lib/cart";
import { useProducts } from "../../lib/products";
import { formatPrice } from "../../lib/format";
import { Reveal } from "../../components/Reveal";
import { VersandModal } from "../../components/VersandModal";
import { CheckoutSteps } from "../../components/CheckoutSteps";
import { ORDER_KEY, generateOrderNumber, type OrderSnapshot } from "../../lib/order";
import type { ShippingMethod, PaymentMethod, TrustBadge } from "../../lib/payload";
const steps = [
{ label: "Warenkorb", state: "done" as const },
{ label: "Adresse", state: "active" as const },
{ label: "Zahlung", state: "upcoming" as const },
{ label: "Abschluss", state: "upcoming" as const },
];
function StepCircle({ state, number }: { state: "done" | "active" | "upcoming"; number: number }) {
if (state === "done") {
return (
<div className="flex size-9 items-center justify-center rounded-full bg-brand text-text-primary">
<svg viewBox="0 0 20 20" className="size-4" fill="none" aria-hidden="true">
<path d="m4 10 4 4 8-8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
);
}
if (state === "active") {
return (
<div className="flex size-9 items-center justify-center rounded-full bg-brand font-bold text-body-sm text-bg-white">
{number}
</div>
);
}
return (
<div className="flex size-9 items-center justify-center rounded-full border border-border font-bold text-body-sm text-text-muted">
{number}
</div>
);
}
function FormField({
label,
wrapperClassName = "flex-1 min-w-0",
@@ -86,6 +57,28 @@ export function CheckoutContent({
subtotal >= selectedShipping.freeShippingThreshold;
const shipping = items.length === 0 || freeShipping ? 0 : selectedShipping?.price ?? 0;
const total = subtotal + shipping;
const selectedPayment = paymentMethods.find((m) => m.id === paymentMethodId) ?? null;
// Captures the actually-selected shipping/payment method as the order
// snapshot /bestellbestaetigung reads — see lib/order.ts's own comment,
// there's no real order backend so this click IS what "placing the
// order" means here.
function handlePurchase() {
const snapshot: OrderSnapshot = {
items: cart,
orderNumber: generateOrderNumber(),
orderDateIso: new Date().toISOString(),
shippingCost: shipping,
paymentMethodTitle: selectedPayment?.title ?? "—",
};
try {
window.sessionStorage.setItem(ORDER_KEY, JSON.stringify(snapshot));
} catch {
// sessionStorage unavailable (private browsing etc.) — the
// confirmation page falls back to its own empty state.
}
clearCart();
}
if (!productsLoading && items.length === 0) {
return (
@@ -121,25 +114,7 @@ export function CheckoutContent({
<span className="text-text-primary">Checkout</span>
</p>
<div className="flex items-center w-full max-w-[50rem]">
{steps.map((step, i) => (
<div key={step.label} className="flex items-center flex-1 last:flex-initial">
<div className="flex flex-col items-center gap-0 shrink-0">
<div className="flex items-center gap-3">
<StepCircle state={step.state} number={i + 1} />
<span
className={`hidden sm:block text-body-sm whitespace-nowrap ${
step.state === "upcoming" ? "text-text-muted" : "font-semibold text-text-primary"
}`}
>
{step.label}
</span>
</div>
</div>
{i < steps.length - 1 && <div className="h-px bg-border flex-1 mx-4 min-w-4" />}
</div>
))}
</div>
<CheckoutSteps current={2} />
<div className="flex flex-col gap-2">
<p
@@ -322,6 +297,7 @@ export function CheckoutContent({
<Link
href="/bestellbestaetigung"
onClick={handlePurchase}
className="w-full flex items-center justify-center py-4 rounded-sm bg-brand hover:bg-brand-hover active:scale-[0.97] transition-all font-bold text-body text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 focus-visible:ring-offset-bg-base"
>
Jetzt kaufen (zahlungspflichtig)
@@ -433,7 +409,7 @@ export function CheckoutContent({
shouldn't promise emails as if opting in were already
decided either. */}
<p className="text-body-sm leading-[1.45]">
Wenn du oben zustimmst, bekommst du nach deiner Bestellung regelmäßig Impulse &amp; Tipps per E-Mail.
Wenn du zustimmst, bekommst du nach deiner Bestellung regelmäßig Impulse &amp; Tipps per E-Mail.
</p>
<p className="text-body-sm leading-[1.45]">
Für mehr Klarheit, Fokus und Struktur jede Woche.