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:
@@ -0,0 +1,243 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import type { CartItem } from "../../lib/cart";
|
||||
import { useProducts } from "../../lib/products";
|
||||
import { formatPrice, formatDate } from "../../lib/format";
|
||||
import { Reveal } from "../../components/Reveal";
|
||||
import { CheckoutSteps } from "../../components/CheckoutSteps";
|
||||
import { ORDER_KEY, type OrderSnapshot } from "../../lib/order";
|
||||
|
||||
export function BestellbestaetigungContent() {
|
||||
const products = useProducts();
|
||||
const [order, setOrder] = useState<OrderSnapshot | null>(null);
|
||||
const [checked, setChecked] = useState(false);
|
||||
|
||||
// The snapshot was already written (and the cart already cleared) by
|
||||
// /checkout's "Jetzt kaufen" click, before it ever navigated here — see
|
||||
// CheckoutContent.tsx's handlePurchase(). This is a pure read of that
|
||||
// browser-only value, deferred to an effect (not a lazy useState
|
||||
// initializer) purely to avoid an SSR/hydration mismatch: the server
|
||||
// has no sessionStorage, so it must render the same "nothing yet" state
|
||||
// the client shows before this effect runs.
|
||||
useEffect(() => {
|
||||
try {
|
||||
const raw = window.sessionStorage.getItem(ORDER_KEY);
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- reading a browser-only store on mount to avoid an SSR/hydration mismatch, see comment above
|
||||
if (raw) setOrder(JSON.parse(raw));
|
||||
} catch {
|
||||
// ignore — falls through to the "no order" state below
|
||||
}
|
||||
setChecked(true);
|
||||
}, []);
|
||||
|
||||
if (!checked) return null;
|
||||
|
||||
if (!order) {
|
||||
return (
|
||||
<Reveal className="flex flex-col gap-6 items-center text-center pt-20 pb-16 px-[var(--layout-padding-x)] w-full">
|
||||
<p
|
||||
className="font-semibold text-h-feature text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
Keine Bestellung gefunden
|
||||
</p>
|
||||
<p className="text-body text-text-muted max-w-[28rem]">
|
||||
Hier gibt es gerade nichts zu bestätigen — vielleicht ist die Sitzung abgelaufen.
|
||||
</p>
|
||||
<Link
|
||||
href="/shop"
|
||||
className="flex items-center gap-2 px-7 py-4 rounded-sm bg-brand text-h4 font-bold text-text-primary hover:bg-brand-hover active:scale-[0.97] transition-all"
|
||||
>
|
||||
Zum Shop
|
||||
</Link>
|
||||
</Reveal>
|
||||
);
|
||||
}
|
||||
|
||||
const productsLoading = products.length === 0;
|
||||
const items = order.items
|
||||
.map((entry) => ({ entry, product: products.find((p) => p.id === entry.id) }))
|
||||
.filter((row): row is { entry: CartItem; product: NonNullable<(typeof row)["product"]> } => Boolean(row.product));
|
||||
|
||||
const subtotal = items.reduce((sum, { entry, product }) => sum + entry.qty * product.price, 0);
|
||||
const total = subtotal + order.shippingCost;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* items-start + same pt-8/px as /checkout's own header block — not
|
||||
centered, so the bar sits at the exact same left edge/position a
|
||||
shopper just saw on the previous step. */}
|
||||
<Reveal className="flex flex-col items-start pt-8 pb-2 px-[var(--layout-padding-x)] w-full">
|
||||
<CheckoutSteps current={5} />
|
||||
</Reveal>
|
||||
|
||||
{/* Hero — success badge, headline, short recap */}
|
||||
<Reveal className="flex flex-col gap-4 items-center text-center pt-12 pb-10 px-[var(--layout-padding-x)] w-full">
|
||||
<div className="relative flex items-center justify-center size-24 mb-2">
|
||||
<div className="absolute inset-0 rounded-full bg-brand/10" />
|
||||
<div className="relative flex items-center justify-center size-14 rounded-full border-2 border-text-primary bg-bg-base">
|
||||
<svg viewBox="0 0 20 20" className="size-6 text-brand" fill="none" aria-hidden="true">
|
||||
<path d="m4 10 4 4 8-8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
className="font-semibold text-display text-text-primary"
|
||||
style={{ fontFamily: "var(--font-playfair)" }}
|
||||
>
|
||||
Vielen Dank!
|
||||
</p>
|
||||
<p
|
||||
className="font-semibold text-h3 text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
Deine Bestellung ist bei uns eingegangen.
|
||||
</p>
|
||||
<div className="h-[0.125rem] w-8 bg-brand" />
|
||||
|
||||
{!productsLoading && (
|
||||
<div className="flex flex-col gap-1 max-w-[26rem]">
|
||||
<p className="text-body text-text-muted">Deine Bestellung macht sich jetzt auf den Weg zu dir.</p>
|
||||
<p className="text-body text-text-muted">
|
||||
Du erhältst in Kürze eine Bestellbestätigung per E-Mail mit allen Details.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Reveal>
|
||||
|
||||
{!productsLoading && (
|
||||
<Reveal
|
||||
delay={0.05}
|
||||
className="w-full max-w-[56rem] mx-auto bg-bg-base border border-border rounded-md overflow-hidden flex flex-col lg:flex-row mb-16"
|
||||
>
|
||||
<div className="flex-1 p-6 md:p-8 flex flex-col sm:flex-row gap-8">
|
||||
{/* Order meta */}
|
||||
<div className="flex flex-row flex-wrap sm:flex-col gap-6 sm:w-36 sm:shrink-0">
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-label text-text-muted">Bestellnummer</p>
|
||||
<p className="font-bold text-body-sm text-text-primary whitespace-nowrap">{order.orderNumber}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-label text-text-muted">Bestelldatum</p>
|
||||
<p className="font-bold text-body-sm text-text-primary whitespace-nowrap">
|
||||
{formatDate(order.orderDateIso)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-label text-text-muted">Zahlungsmethode</p>
|
||||
<p className="font-bold text-body-sm text-text-primary whitespace-nowrap">{order.paymentMethodTitle}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Items + totals */}
|
||||
<div className="flex-1 flex flex-col gap-5 min-w-0">
|
||||
<p
|
||||
className="font-semibold text-h-small text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
Bestellübersicht
|
||||
</p>
|
||||
|
||||
{items.map(({ entry, product }) => (
|
||||
<div key={product.id} className="flex gap-4 items-center w-full">
|
||||
<div className="relative size-16 shrink-0 rounded-sm overflow-hidden">
|
||||
<Image src={product.image} alt={product.name} fill sizes="64px" className="object-cover" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 flex flex-col gap-0.5">
|
||||
<p className="text-body-sm text-text-primary">{product.name}</p>
|
||||
<p className="text-label text-text-muted">
|
||||
{entry.qty} × {formatPrice(product.price)} <span>inkl. MwSt.</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-body-sm text-text-primary whitespace-nowrap">
|
||||
{formatPrice(entry.qty * product.price)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="h-px bg-border w-full" />
|
||||
|
||||
<div className="flex items-center w-full">
|
||||
<span className="text-body-sm text-text-primary">Zwischensumme</span>
|
||||
<span className="flex-1" />
|
||||
<span className="text-body-sm text-text-primary">{formatPrice(subtotal)}</span>
|
||||
</div>
|
||||
<div className="flex items-center w-full">
|
||||
<span className="text-body-sm text-text-primary">Versand</span>
|
||||
<span className="flex-1" />
|
||||
<span className="text-body-sm text-text-primary">
|
||||
{order.shippingCost === 0 ? "Kostenlos" : formatPrice(order.shippingCost)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="h-px bg-border w-full" />
|
||||
|
||||
<div className="flex items-center w-full">
|
||||
<span
|
||||
className="font-semibold text-h4 text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
Gesamtbetrag
|
||||
</span>
|
||||
<span className="flex-1" />
|
||||
<span className="font-bold text-h-small text-text-primary">{formatPrice(total)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Delivery status panel */}
|
||||
<div className="w-full lg:w-[18rem] lg:shrink-0 bg-bg-muted p-7 flex flex-col items-center text-center gap-3 justify-center">
|
||||
<div className="flex items-center justify-center size-14 rounded-full bg-brand/10 text-text-primary shrink-0">
|
||||
<svg viewBox="0 0 24 24" className="size-6" fill="none" aria-hidden="true">
|
||||
<path d="M2 6.5h11v10H2z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" />
|
||||
<path d="M13 10.5h4l3.5 3v3H13z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" />
|
||||
<circle cx="6" cy="17.5" r="1.6" stroke="currentColor" strokeWidth="1.6" />
|
||||
<circle cx="17" cy="17.5" r="1.6" stroke="currentColor" strokeWidth="1.6" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="font-semibold text-body text-text-primary">Deine Bestellung wird bearbeitet.</p>
|
||||
<p className="text-body-sm text-text-muted">
|
||||
Wir versenden in der Regel innerhalb von 1–2 Werktagen.
|
||||
</p>
|
||||
<p className="text-body-sm text-text-muted">
|
||||
Du erhältst eine E-Mail, sobald dein Paket unterwegs ist.
|
||||
</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
)}
|
||||
|
||||
{/* Testimonial band — same photo+quote pattern as /not-found (see
|
||||
that page's own comment); bestellbestaetigung-testimonial-photo.jpg
|
||||
is cropped from the actual mockup pixels the same way. Explicit
|
||||
md:h- (not just min-h-), so the image and quote columns are
|
||||
pinned to an identical height regardless of how many lines the
|
||||
quote wraps to — a min-height alone lets the taller of the two
|
||||
content-driven columns stretch the row past what the other side
|
||||
visually fills up to. */}
|
||||
<Reveal className="relative w-full flex items-stretch h-[20rem] md:h-[22rem] bg-bg-muted overflow-hidden">
|
||||
<div className="relative w-full md:w-[45%] shrink-0">
|
||||
<img
|
||||
alt=""
|
||||
src="/bestellbestaetigung-testimonial-photo.jpg"
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
<div className="hidden md:block absolute inset-y-0 right-0 w-40 bg-gradient-to-l from-bg-muted to-transparent" />
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col justify-center gap-3 px-8 md:px-16 py-12">
|
||||
<p
|
||||
className="font-semibold text-h-section text-text-primary leading-[1.3] max-w-[28rem]"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
>
|
||||
„Produktivität beginnt nicht mit mehr – sondern mit dem, was wirklich zählt.“
|
||||
</p>
|
||||
<div className="h-[0.125rem] w-8 bg-brand" />
|
||||
<p className="text-body text-text-muted">Björn</p>
|
||||
</div>
|
||||
</Reveal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Metadata } from "next";
|
||||
import { BestellbestaetigungContent } from "./components/BestellbestaetigungContent";
|
||||
import { TrustRow } from "../components/TrustRow";
|
||||
import { Footer } from "../components/Footer";
|
||||
|
||||
// robots: noindex — transactional page, same reasoning as /cart and
|
||||
// /checkout (this one doubles as a receipt, not something to surface in
|
||||
// search results).
|
||||
export const metadata: Metadata = {
|
||||
title: "Bestellbestätigung",
|
||||
description: "Deine Bestellung bei einfach produktiv wurde erfolgreich aufgegeben.",
|
||||
robots: {
|
||||
index: false,
|
||||
follow: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default function BestellbestaetigungPage() {
|
||||
return (
|
||||
<>
|
||||
<main className="flex flex-col flex-1 bg-bg-base">
|
||||
<BestellbestaetigungContent />
|
||||
<TrustRow />
|
||||
</main>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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 & Tipps per E-Mail.
|
||||
Wenn du zustimmst, bekommst du nach deiner Bestellung regelmäßig Impulse & Tipps per E-Mail.
|
||||
</p>
|
||||
<p className="text-body-sm leading-[1.45]">
|
||||
Für mehr Klarheit, Fokus und Struktur – jede Woche.
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
const STEP_LABELS = ["Warenkorb", "Adresse", "Zahlung", "Abschluss"];
|
||||
|
||||
type StepState = "done" | "active" | "upcoming";
|
||||
|
||||
function StepCircle({ state, number }: { state: StepState; 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>
|
||||
);
|
||||
}
|
||||
|
||||
// Shared by /checkout (mid-flow, one step active) and /bestellbestaetigung
|
||||
// (every step done — `current` past the last step number makes every
|
||||
// stepNum < current true, so passing STEP_LABELS.length + 1 there marks
|
||||
// the whole bar complete without a separate "all done" branch).
|
||||
export function CheckoutSteps({ current }: { current: number }) {
|
||||
return (
|
||||
<div className="flex items-center w-full max-w-[50rem]">
|
||||
{STEP_LABELS.map((label, i) => {
|
||||
const stepNum = i + 1;
|
||||
const state: StepState = stepNum < current ? "done" : stepNum === current ? "active" : "upcoming";
|
||||
return (
|
||||
<div key={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={state} number={stepNum} />
|
||||
<span
|
||||
className={`hidden sm:block text-body-sm whitespace-nowrap ${
|
||||
state === "upcoming" ? "text-text-muted" : "font-semibold text-text-primary"
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{i < STEP_LABELS.length - 1 && <div className="h-px bg-border flex-1 mx-4 min-w-4" />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -39,6 +39,14 @@ export function removeFromCart(id: string) {
|
||||
writeCart(readCart().filter((i) => i.id !== id));
|
||||
}
|
||||
|
||||
// Called by /bestellbestaetigung once it has captured a snapshot of the
|
||||
// cart to display — there's no real order backend here, so "placing an
|
||||
// order" just means the local cart empties out the same way it would
|
||||
// after a real purchase completes.
|
||||
export function clearCart() {
|
||||
writeCart([]);
|
||||
}
|
||||
|
||||
// qty <= 0 removes the item outright — the cart page's quantity stepper
|
||||
// never lets the visible count go below 1, but this keeps the function
|
||||
// itself safe to call with any integer without a separate remove path.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { CartItem } from "./cart";
|
||||
|
||||
// sessionStorage, not localStorage — this is a one-time receipt for the
|
||||
// tab that just placed the order, not something that should persist
|
||||
// forever. Written by /checkout's "Jetzt kaufen" click (capturing
|
||||
// whichever shipping/payment method was actually selected there — the
|
||||
// site has no real order backend, so this snapshot IS the order record),
|
||||
// read once by /bestellbestaetigung.
|
||||
export const ORDER_KEY = "ep_last_order";
|
||||
|
||||
export type OrderSnapshot = {
|
||||
items: CartItem[];
|
||||
orderNumber: string;
|
||||
orderDateIso: string;
|
||||
shippingCost: number;
|
||||
paymentMethodTitle: string;
|
||||
};
|
||||
|
||||
export function generateOrderNumber(): string {
|
||||
const year = new Date().getFullYear();
|
||||
const rand = Math.floor(1000 + Math.random() * 9000);
|
||||
return `#EP-${year}-${rand}`;
|
||||
}
|
||||
Reference in New Issue
Block a user