diff --git a/app/bestellbestaetigung/components/BestellbestaetigungContent.tsx b/app/bestellbestaetigung/components/BestellbestaetigungContent.tsx
new file mode 100644
index 0000000..a3e35d5
--- /dev/null
+++ b/app/bestellbestaetigung/components/BestellbestaetigungContent.tsx
@@ -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(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 (
+
+
+ Keine Bestellung gefunden
+
+
+ Hier gibt es gerade nichts zu bestätigen — vielleicht ist die Sitzung abgelaufen.
+
+
+ Zum Shop
+
+
+ );
+ }
+
+ 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. */}
+
+
+
+
+ {/* Hero — success badge, headline, short recap */}
+
+
+
+
+
+
+
+
+
+ Vielen Dank!
+
+
+ Deine Bestellung ist bei uns eingegangen.
+
+
+
+ {!productsLoading && (
+
+
Deine Bestellung macht sich jetzt auf den Weg zu dir.
+
+ Du erhältst in Kürze eine Bestellbestätigung per E-Mail mit allen Details.
+
+ Wir versenden in der Regel innerhalb von 1–2 Werktagen.
+
+
+ Du erhältst eine E-Mail, sobald dein Paket unterwegs ist.
+
+
+
+ )}
+
+ {/* 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. */}
+
+
+
+
+
+
+
+ „Produktivität beginnt nicht mit mehr – sondern mit dem, was wirklich zählt.“
+
+
+
Björn
+
+
+ >
+ );
+}
diff --git a/app/bestellbestaetigung/page.tsx b/app/bestellbestaetigung/page.tsx
new file mode 100644
index 0000000..56a4478
--- /dev/null
+++ b/app/bestellbestaetigung/page.tsx
@@ -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 (
+ <>
+
+
+
+
+
+ >
+ );
+}
diff --git a/app/checkout/components/CheckoutContent.tsx b/app/checkout/components/CheckoutContent.tsx
index 935be16..be665ec 100644
--- a/app/checkout/components/CheckoutContent.tsx
+++ b/app/checkout/components/CheckoutContent.tsx
@@ -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 (
-
-
-
- );
- }
- if (state === "active") {
- return (
-
- {number}
-
- );
- }
- return (
-
- {number}
-
- );
-}
-
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({
Checkout
-
- {steps.map((step, i) => (
-
-
-
-
-
- {step.label}
-
-
-
- {i < steps.length - 1 && }
-
- ))}
-
+
Jetzt kaufen (zahlungspflichtig)
@@ -433,7 +409,7 @@ export function CheckoutContent({
shouldn't promise emails as if opting in were already
decided either. */}
- 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.
Für mehr Klarheit, Fokus und Struktur – jede Woche.
diff --git a/app/components/CheckoutSteps.tsx b/app/components/CheckoutSteps.tsx
new file mode 100644
index 0000000..03ede26
--- /dev/null
+++ b/app/components/CheckoutSteps.tsx
@@ -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 (
+
+
+
+ );
+ }
+ if (state === "active") {
+ return (
+
+ {number}
+
+ );
+ }
+ return (
+
+ {number}
+
+ );
+}
+
+// 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 (
+
+ );
+}
diff --git a/app/lib/cart.ts b/app/lib/cart.ts
index ab09f70..aaa0a74 100644
--- a/app/lib/cart.ts
+++ b/app/lib/cart.ts
@@ -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.
diff --git a/app/lib/order.ts b/app/lib/order.ts
new file mode 100644
index 0000000..a66e85b
--- /dev/null
+++ b/app/lib/order.ts
@@ -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}`;
+}
diff --git a/public/bestellbestaetigung-testimonial-photo.jpg b/public/bestellbestaetigung-testimonial-photo.jpg
new file mode 100644
index 0000000..9343e4e
Binary files /dev/null and b/public/bestellbestaetigung-testimonial-photo.jpg differ