fix(bestellbestaetigung): rebuild testimonial band, tighten hero, add spotlightEyebrow
Testimonial band: rebuilt as a proper flexbox two-column layout (photo column with an explicit width, quote in a flex-1 sibling) instead of an absolutely-positioned text block offset with percentage margin/padding — that measured against the row's full width and, combined with a max-w- on the text box, left almost no room for the actual text on wide viewports (wrapped to one word per line). Also dropped the muted-bg box behind the quote and widened the photo fade to match the actual mockup, which has no separate colored panel there at all. Hero: removed the separate big checkmark badge — the 4-step bar right above it already renders every step as a checkmark, so it was just repeating that. Added more top spacing to compensate. Dropped "Deine Bestellung macht sich jetzt auf den Weg zu dir." and added "inkl. MwSt." under Gesamtbetrag for consistency with /cart and /checkout. Wired the new Products.spotlightEyebrow field (CMS-editable "Neu im Shop" label) through lib/payload.ts into ProductSpotlight.tsx.
This commit is contained in:
@@ -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<OrderSnapshot | null>(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() {
|
||||
<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>
|
||||
|
||||
{/* 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. */}
|
||||
<Reveal className="flex flex-col gap-4 items-center text-center pt-24 pb-10 px-[var(--layout-padding-x)] w-full">
|
||||
<p
|
||||
className="font-semibold text-display text-text-primary"
|
||||
style={{ fontFamily: "var(--font-playfair)" }}
|
||||
@@ -101,7 +130,6 @@ export function BestellbestaetigungContent() {
|
||||
|
||||
{!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>
|
||||
@@ -176,15 +204,18 @@ export function BestellbestaetigungContent() {
|
||||
|
||||
<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 className="flex flex-col gap-0.5 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>
|
||||
<p className="text-label text-text-muted">inkl. MwSt.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,24 +241,30 @@ export function BestellbestaetigungContent() {
|
||||
</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">
|
||||
{/* 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. */}
|
||||
<Reveal className="relative w-full flex items-stretch h-[20rem] md:h-[24rem] bg-bg-base overflow-hidden">
|
||||
<div className="relative w-full md:w-[58%] 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 className="hidden md:block absolute inset-y-0 right-0 w-32 bg-gradient-to-l from-transparent to-bg-base" />
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col justify-center gap-3 px-8 md:px-16 py-12">
|
||||
<div className="flex-1 min-w-0 flex flex-col justify-center gap-3 px-8 md:px-12">
|
||||
<p
|
||||
className="font-semibold text-h-section text-text-primary leading-[1.3] max-w-[28rem]"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function ProductSpotlight() {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 items-start flex-1 min-w-0 w-full">
|
||||
<p className="font-bold text-h-small text-brand">Neu im Shop</p>
|
||||
<p className="font-bold text-h-small text-brand">{product.spotlightEyebrow || "Neu im Shop"}</p>
|
||||
<p
|
||||
className="font-semibold text-h-section text-text-primary"
|
||||
style={{ fontFamily: "var(--font-lora)" }}
|
||||
|
||||
@@ -169,12 +169,14 @@ export async function getProductBySlug(slug: string): Promise<Product | null> {
|
||||
}
|
||||
|
||||
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<SpotlightProduct | null> {
|
||||
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:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 18 KiB |
Reference in New Issue
Block a user