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. +

+
+ )} + + + {!productsLoading && ( + +
+ {/* Order meta */} +
+
+

Bestellnummer

+

{order.orderNumber}

+
+
+

Bestelldatum

+

+ {formatDate(order.orderDateIso)} +

+
+
+

Zahlungsmethode

+

{order.paymentMethodTitle}

+
+
+ + {/* Items + totals */} +
+

+ Bestellübersicht +

+ + {items.map(({ entry, product }) => ( +
+
+ {product.name} +
+
+

{product.name}

+

+ {entry.qty} × {formatPrice(product.price)} inkl. MwSt. +

+
+

+ {formatPrice(entry.qty * product.price)} +

+
+ ))} + +
+ +
+ Zwischensumme + + {formatPrice(subtotal)} +
+
+ Versand + + + {order.shippingCost === 0 ? "Kostenlos" : formatPrice(order.shippingCost)} + +
+ +
+ +
+ + Gesamtbetrag + + + {formatPrice(total)} +
+
+
+ + {/* Delivery status panel */} +
+
+ +
+

Deine Bestellung wird bearbeitet.

+

+ 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 ( + <> +
+ + +
+