diff --git a/app/bestellbestaetigung/components/BestellbestaetigungContent.tsx b/app/bestellbestaetigung/components/BestellbestaetigungContent.tsx index a3e35d5..4fdb67d 100644 --- a/app/bestellbestaetigung/components/BestellbestaetigungContent.tsx +++ b/app/bestellbestaetigung/components/BestellbestaetigungContent.tsx @@ -10,6 +10,32 @@ import { Reveal } from "../../components/Reveal"; import { CheckoutSteps } from "../../components/CheckoutSteps"; import { ORDER_KEY, type OrderSnapshot } from "../../lib/order"; +// Rejects (rather than silently patching with fallback values) anything +// that doesn't match the current OrderSnapshot shape — e.g. a snapshot +// left over from before shippingCost/paymentMethodTitle were added to it. +// Fabricating "Kostenlos"/"—" for missing fields would render a receipt +// that looks real but states things about an order that were never true; +// treating it as no-order-found and clearing it is the honest fallback. +function parseOrderSnapshot(raw: string): OrderSnapshot | null { + try { + const data = JSON.parse(raw); + if ( + !data || + !Array.isArray(data.items) || + data.items.length === 0 || + typeof data.orderNumber !== "string" || + typeof data.orderDateIso !== "string" || + typeof data.shippingCost !== "number" || + typeof data.paymentMethodTitle !== "string" + ) { + return null; + } + return data as OrderSnapshot; + } catch { + return null; + } +} + export function BestellbestaetigungContent() { const products = useProducts(); const [order, setOrder] = useState(null); @@ -25,8 +51,15 @@ export function BestellbestaetigungContent() { 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)); + const parsed = raw ? parseOrderSnapshot(raw) : null; + if (parsed) { + // 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 + setOrder(parsed); + } else if (raw) { + // Present but invalid/outdated — drop it so it doesn't keep + // failing validation on every future visit either. + window.sessionStorage.removeItem(ORDER_KEY); + } } catch { // ignore — falls through to the "no order" state below } @@ -74,17 +107,13 @@ export function BestellbestaetigungContent() { - {/* Hero — success badge, headline, short recap */} - -
-
-
- -
-
- + {/* Hero — headline, short recap. No separate big checkmark badge — + all four steps in the bar right above already render as + checkmarks (current={5}), a second one here would just repeat + it; pt-24 (up from the badge-era pt-12) keeps real breathing + room between the bar and the headline now that the badge isn't + filling that space itself. */} +

-

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.

@@ -176,15 +204,18 @@ export function BestellbestaetigungContent() {
-
- - Gesamtbetrag - - - {formatPrice(total)} +
+
+ + Gesamtbetrag + + + {formatPrice(total)} +
+

inkl. MwSt.

@@ -210,24 +241,30 @@ export function BestellbestaetigungContent() {
)} - {/* 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. */} - -
+ {/* Testimonial band — different from /not-found's version: this + page's own mockup has no separate muted-bg box on the text + side at all, just a wide photo column that fades into the + plain page background at its own trailing edge, with the + quote sitting in a normal flex-1 column beside it (not + absolute-positioned with %-based offsets — that measures + against the row's full width and, combined with a max-w- on a + narrow text box, can leave almost nothing for the text itself; + flexbox distributes the two columns' widths correctly on its + own regardless of viewport size). items-stretch on a row with + a FIXED h- (not min-h-) keeps both columns pinned to an + identical height regardless of how many lines the quote wraps + to — a min-height alone lets whichever column has more content + stretch the row past what the other side visually fills. */} + +
-
+
-
+

-

Neu im Shop

+

{product.spotlightEyebrow || "Neu im Shop"}

{ } export type SpotlightProduct = Product & { + spotlightEyebrow: string | null; spotlightHeadline: string | null; spotlightText: string | null; spotlightImage: string | null; }; type PayloadSpotlightProduct = PayloadProduct & { + spotlightEyebrow: string | null; spotlightHeadline: string | null; spotlightText: string | null; spotlightImage: { url: string } | number | null; @@ -211,6 +213,7 @@ export async function getSpotlightProduct(): Promise { compareAtPrice: doc.compareAtPrice ?? null, image: typeof doc.image === "object" && doc.image ? doc.image.url : "", href: doc.detailHref || null, + spotlightEyebrow: doc.spotlightEyebrow || null, spotlightHeadline: doc.spotlightHeadline || null, spotlightText: doc.spotlightText || null, spotlightImage: diff --git a/public/bestellbestaetigung-testimonial-photo.jpg b/public/bestellbestaetigung-testimonial-photo.jpg index 9343e4e..509e903 100644 Binary files a/public/bestellbestaetigung-testimonial-photo.jpg and b/public/bestellbestaetigung-testimonial-photo.jpg differ