import Image from "next/image"; import Link from "next/link"; import type { PageBlock } from "../lib/payload"; import { getTestimonials } from "../lib/payload"; import { RichText } from "./RichText"; import { Quote } from "./Quote"; import { StepArrow } from "./StepArrow"; import { STEP_ICONS, SYMBOLIC_ICONS } from "./icons/StepIcons"; import { TestimonialsGrid } from "./TestimonialsGrid"; // Renders a Payload Pages document's `layout` blocks field — structurally // parallel to RichText.tsx's own `blocks: {...}` converter map, but one // level up (full page sections, not inline Lexical nodes). Each block // component reuses the exact same visual treatment already hand-built on // /lebensuhr, /3x3-system, and /7-tage-klarheits-check, rather than // inventing new styling — this is what those pages' repeated patterns get // consolidated into for CMS-driven pages going forward. export async function PageBlocks({ blocks }: { blocks: PageBlock[] }) { const rendered = await Promise.all( blocks.map(async (block) => { if (block.blockType === "testimonialsRef") { const testimonials = await getTestimonials(block.page); if (testimonials.length === 0) return null; return ; } return renderPageBlockSync(block); }) ); return <>{rendered}; } // Every block EXCEPT testimonialsRef (needs its own async data fetch, // which a client component can't await — see LivePageContent.tsx, which // uses this directly and simply skips that one block type in preview, // same "live-previewable subset" scope-cut LivePostContent.tsx already // makes for its own author-bio/"Weiterlesen" chrome). export function renderPageBlockSync(block: PageBlock): React.ReactNode { switch (block.blockType) { // Same "font-playfair clamp headline" treatment every hand-built page // (/lebensuhr, /3x3-system, /ueber-mich) used for its own H1 before // this block existed — see HeroBlock.ts's own comment on why the // headline moved out of Pages.title and into here. case "hero": { const headline = (

{block.headline}

); return (
{/* Optional round portrait beside the headline — e.g. /ueber-mich's "Hallo, ich bin Björn." size-20 (80px) read as barely-there/ easy to miss next to a full headline — bumped to size-32 (128px) so it actually registers as a real photo, not a small decorative blob. */} {block.image ? (
{headline}
) : ( headline )} {/* text-primary + text-h-emphasis, not text-muted body — same weight class as the homepage Hero's own subheading ("Ich helfe dir..."), which a plain muted text-body read too quiet next to. */} {block.subline && (

{block.subline}

)} {block.ctaLabel && block.ctaHref && ( {block.ctaLabel} )}
); } case "richTextSection": return ; case "quote": return {block.text}; case "icon": { const SymbolIcon = SYMBOLIC_ICONS[block.icon]; return SymbolIcon ?
{SymbolIcon()}
: null; } case "image": if (!block.image) return null; return (
{block.caption
{block.caption &&

{block.caption}

}
); case "pillList": return (
{block.items.map((item) => ( {item.label} ))}
); case "checklistImage": return (
{block.image && (
)}
    {block.items.map((item) => (
  • {item.text}

  • ))}
); case "table": return (
{block.rows.map((row, i) => ( ))}
{block.labelHeader} {block.valueHeader}
{row.label} {row.value}
); case "stepRow": { const icons = block.items.map((item) => STEP_ICONS[item.icon]); return (
{block.items.flatMap((item, i) => [
{icons[i]?.()}

{item.title}

{item.subtitle &&

{item.subtitle}

}

{item.description}

, i < block.items.length - 1 ? (
) : null, ])}
); } case "testimonialsRef": // Needs an async fetch — handled by PageBlocks (server) above. // Skipped in the client-side live-preview renderer. return null; case "ctaCard": return (

{block.eyebrow}

{block.title}

{block.description &&

{block.description}

}
); default: // Unknown/malformed block — same "skip rather than crash" convention // RichText.tsx's own blocks use. return null; } } // Same checkmark used by /lebensuhr's and /7-tage-klarheits-check's own // checklists. function CheckIcon() { return ( ); }