feat: convert 5 static pages to CMS-driven blocks

Replace hardcoded motivation, so-funktionierts, ueber-uns, impressum,
and datenschutz pages with CMS-driven content via BlockRenderer.
Add block components: HeroBlock, TextBlock, CardGridBlock, CTABlock,
ProcessStepsBlock, QuoteBlock, HtmlEmbedBlock.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
CCS Admin 2026-02-28 15:44:23 +00:00
parent 6371a2eee6
commit 728de20157
14 changed files with 410 additions and 504 deletions

View file

@ -1,27 +1,16 @@
import type { Metadata } from "next" import type { Metadata } from "next"
import { Container } from "@/components/ui/Container" import { getPage } from "@/lib/api"
import { BlockRenderer } from "@/components/blocks/BlockRenderer"
import { notFound } from "next/navigation"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Datenschutzerklärung", title: "Datenschutzerklärung",
description: "DSGVO-konforme Datenschutzerklärung von zweitmeinu.ng.", description: "DSGVO-konforme Datenschutzerklärung von zweitmeinu.ng.",
} }
export default function DatenschutzPage() { export default async function DatenschutzPage() {
return ( const page = await getPage("datenschutz")
<section className="py-20 bg-white"> if (!page) notFound()
<Container size="md">
<h1 className="text-3xl font-bold text-navy mb-8"> return <BlockRenderer blocks={(page.layout as any[]) || []} />
Datenschutzerklärung
</h1>
<iframe
src="https://app.alfright.eu/ext/dps/alfright_schutzteam/9f315103c43245bcb0806dd56c2be757?lang=de-de&headercolor=%23131F64&headerfont=Arial&headersize=21px&subheadersize=18px&fontcolor=%23333333&textfont=Arial&textsize=14px&background=%23ffffff&linkcolor=%23337ab7"
title="Datenschutzerklärung"
width="100%"
height={5000}
style={{ border: 0 }}
loading="lazy"
/>
</Container>
</section>
)
} }

View file

@ -1,96 +1,16 @@
import type { Metadata } from "next" import type { Metadata } from "next"
import { Container } from "@/components/ui/Container" import { getPage } from "@/lib/api"
import { BlockRenderer } from "@/components/blocks/BlockRenderer"
import { notFound } from "next/navigation"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Impressum", title: "Impressum",
description: "Impressum der complex care solutions GmbH.", description: "Impressum der complex care solutions GmbH.",
} }
export default function ImpressumPage() { export default async function ImpressumPage() {
return ( const page = await getPage("impressum")
<section className="py-20 bg-white"> if (!page) notFound()
<Container size="sm">
<article className="prose prose-gray max-w-none">
<h1 className="text-3xl font-bold text-navy mb-8">Impressum</h1>
<p> return <BlockRenderer blocks={(page.layout as any[]) || []} />
<strong>complex care solutions GmbH</strong>
<br />
Hans-Böckler-Str. 19
<br />
46236 Bottrop
</p>
<p>
Handelsregister: HRB 15753
<br />
Registergericht: Gelsenkirchen
</p>
<p>
<strong>Vertreten durch:</strong>
<br />
Martin Porwoll
</p>
<h2 className="text-xl font-bold text-navy mt-8 mb-4">Kontakt</h2>
<p>
Telefon: 0800 80 44 100
<br />
Telefax: 0800 80 44 190
<br />
E-Mail: kontakt@complexcaresolutions.de
</p>
<h2 className="text-xl font-bold text-navy mt-8 mb-4">
Umsatzsteuer-ID
</h2>
<p>
Umsatzsteuer-Identifikationsnummer gemäß § 27 a
Umsatzsteuergesetz:
<br />
DE334815479
</p>
<p>
<strong>Redaktionell verantwortlich:</strong>
<br />
Martin Porwoll
<br />
Hans-Böckler-Str. 19
<br />
46236 Bottrop
</p>
<h2 className="text-xl font-bold text-navy mt-8 mb-4">
EU-Streitschlichtung
</h2>
<p>
Die Europäische Kommission stellt eine Plattform zur
Online-Streitbeilegung (OS) bereit:{" "}
<a
href="https://ec.europa.eu/consumers/odr/"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
https://ec.europa.eu/consumers/odr/
</a>
.
<br />
Unsere E-Mail-Adresse finden Sie oben im Impressum.
</p>
<h2 className="text-xl font-bold text-navy mt-8 mb-4">
Verbraucherstreitbeilegung
</h2>
<p>
Wir sind nicht bereit oder verpflichtet, an
Streitbeilegungsverfahren vor einer
Verbraucherschlichtungsstelle teilzunehmen.
</p>
</article>
</Container>
</section>
)
} }

View file

@ -1,165 +1,16 @@
import type { Metadata } from "next" import type { Metadata } from "next"
import { Container } from "@/components/ui/Container" import { getPage } from "@/lib/api"
import { Heart, Shield, Eye, Users, Lightbulb, Scale } from "lucide-react" import { BlockRenderer } from "@/components/blocks/BlockRenderer"
import { notFound } from "next/navigation"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Motivation & Geschichte", title: "Motivation & Geschichte",
description: "Erfahren Sie, warum zweitmeinu.ng gegründet wurde und welche Werte uns antreiben.", description: "Erfahren Sie, warum zweitmeinu.ng gegründet wurde und welche Werte uns antreiben.",
} }
const values = [ export default async function MotivationPage() {
{ title: "Patientenwohl", description: "Der Mensch steht im Mittelpunkt jeder Entscheidung.", icon: Heart }, const page = await getPage("motivation")
{ title: "Unabhängigkeit", description: "Frei von wirtschaftlichen Interessen und institutionellen Bindungen.", icon: Shield }, if (!page) notFound()
{ title: "Transparenz", description: "Offene Kommunikation und nachvollziehbare Prozesse.", icon: Eye },
{ title: "Qualität", description: "Höchste Standards in medizinischer Expertise und Beratung.", icon: Lightbulb },
{ title: "Empathie", description: "Verständnis und Mitgefühl in jeder Situation.", icon: Users },
{ title: "Gerechtigkeit", description: "Gleicher Zugang zu medizinischer Expertise für alle.", icon: Scale },
]
export default function MotivationPage() { return <BlockRenderer blocks={(page.layout as any[]) || []} />
return (
<>
{/* Hero */}
<section className="bg-gradient-to-br from-navy via-navy-dark to-[#001a2e] py-20">
<Container size="md">
<div className="text-center">
<h1 className="text-4xl sm:text-5xl font-bold text-white mb-4">
Patientenwohl im Mittelpunkt
</h1>
<p className="text-white/60 text-lg">
Wir sind Streiter für das Patientenwohl
</p>
</div>
</Container>
</section>
{/* Story sections */}
<section className="py-20 bg-white">
<Container size="md">
<div className="space-y-16">
{/* Section 1 */}
<div>
<h2 className="text-2xl font-bold text-navy mb-4">Unser Fokus</h2>
<p className="text-text-muted leading-relaxed mb-4">
Seit der Gründung konzentrieren wir uns darauf,
Versorgungsangebote zu optimieren und dabei die Bedürfnisse der
Patienten in den Mittelpunkt zu stellen.
</p>
<ul className="space-y-2 text-text-muted">
<li className="flex gap-2">
<span className="text-primary font-bold">&#8250;</span>
Wir verfolgen einen patientenzentrierten Ansatz.
</li>
<li className="flex gap-2">
<span className="text-primary font-bold">&#8250;</span>
Wir legen besonderen Wert auf Transparenz, Unabhängigkeit und Qualitätssicherung.
</li>
<li className="flex gap-2">
<span className="text-primary font-bold">&#8250;</span>
Mit unserem nationalen und internationalen Expertennetzwerk entwickeln wir innovative Lösungen.
</li>
</ul>
</div>
{/* Section 2 */}
<div>
<h2 className="text-2xl font-bold text-navy mb-4">
Motivation und Geschichte
</h2>
<p className="text-text-muted leading-relaxed mb-6">
Complex care solutions wurde von Martin Porwoll, dem
Whistleblower des Bottroper Zytoskandals, gegründet.
</p>
<blockquote className="border-l-4 border-gold pl-6 py-2 bg-gold/5 rounded-r-lg">
<p className="text-navy italic">
&ldquo;Aus seinen Erfahrungen und der Erkenntnis um die Bedeutung
von Transparenz und Patientenwohl entstand die Idee, ein
unabhängiges Unternehmen zu etablieren.&rdquo;
</p>
</blockquote>
</div>
{/* Section 3 */}
<div>
<h2 className="text-2xl font-bold text-navy mb-4">
Der Zytoskandal Bottrop und Martin Porwoll
</h2>
<p className="text-text-muted leading-relaxed mb-4">
Im Jahr 2016 deckte Martin Porwoll als Whistleblower den
sogenannten Zytoskandal in Bottrop auf, bei dem ein Apotheker
über Jahre hinweg Krebsmedikamente für tausende Patienten
gestreckt hatte.
</p>
<blockquote className="border-l-4 border-primary pl-6 py-2">
<p className="text-navy italic">
&ldquo;Der Bottroper Zytoskandal, den ich im Jahr 2016 als
Whistleblower aufgedeckt habe, hat mich zutiefst
erschüttert.&rdquo;
</p>
<cite className="text-sm text-text-muted mt-2 block not-italic">
Martin Porwoll, Gründer &amp; Geschäftsführer
</cite>
</blockquote>
</div>
{/* Section 4 */}
<div>
<p className="text-text-muted leading-relaxed">
Die Erfahrungen mit dem Zytoskandal haben Martin Porwoll zu
einem engagierten Kämpfer für das Patientenwohl und gegen
Missstände im Gesundheitswesen gemacht. So gründete er Complex
care solutions mit dem Ziel, Patienten in komplexen
Versorgungssituationen bestmöglich zu unterstützen.
</p>
</div>
</div>
</Container>
</section>
{/* Core Values */}
<section className="py-20 bg-bg">
<Container>
<div className="text-center mb-12">
<h2 className="text-2xl sm:text-3xl font-bold text-navy mb-3">
Unsere Grundwerte
</h2>
<p className="text-text-muted">
Diese Prinzipien leiten uns bei allem, was wir für eine bessere
Patientenversorgung tun.
</p>
</div>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
{values.map((v) => (
<div key={v.title} className="bg-white rounded-xl p-6 border border-border">
<v.icon className="h-8 w-8 text-primary mb-4" />
<h3 className="font-bold text-navy mb-2">{v.title}</h3>
<p className="text-text-muted text-sm">{v.description}</p>
</div>
))}
</div>
</Container>
</section>
{/* Mission Statement */}
<section className="py-20 bg-navy">
<Container size="sm">
<div className="text-center">
<Heart className="h-10 w-10 text-gold mx-auto mb-6" />
<h2 className="text-2xl font-bold text-white mb-6">
Unsere Mission
</h2>
<blockquote className="text-white/80 text-lg leading-relaxed italic">
&ldquo;Wir setzen uns dafür ein, dass jeder Patient die bestmögliche
Versorgung erhält. Durch innovative Technologie, Transparenz und
unabhängige Expertise schaffen wir Vertrauen und verbessern die
Gesundheitsversorgung.&rdquo;
</blockquote>
<cite className="text-white/50 text-sm mt-4 block not-italic">
Das Team von zweitmeinu.ng
</cite>
</div>
</Container>
</section>
</>
)
} }

View file

@ -1,126 +1,16 @@
import type { Metadata } from "next" import type { Metadata } from "next"
import { Container } from "@/components/ui/Container" import { getPage } from "@/lib/api"
import { Button } from "@/components/ui/Button" import { BlockRenderer } from "@/components/blocks/BlockRenderer"
import { getSiteSettings } from "@/lib/api" import { notFound } from "next/navigation"
import { phoneToHref } from "@/lib/payload-helpers"
import {
Phone, FileText, UserCheck, ClipboardCheck,
MessageSquare, HeartHandshake, Shield, Clock,
BadgeCheck, Users, ArrowRight,
} from "lucide-react"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "So funktioniert's", title: "So funktioniert\u0027s",
description: "Ihr Weg zur medizinischen Zweitmeinung in 6 einfachen Schritten. Transparent, sicher und patientenorientiert.", description: "Ihr Weg zur medizinischen Zweitmeinung in 6 einfachen Schritten. Transparent, sicher und patientenorientiert.",
} }
const steps = [ export default async function SoFunktioniertsPage() {
{ num: "01", title: "Kontaktaufnahme", description: "Sie rufen uns an oder schreiben uns. Unser Team nimmt Ihre Anfrage auf und klärt erste Fragen.", icon: Phone }, const page = await getPage("so-funktionierts")
{ num: "02", title: "Unterlagen sammeln", description: "Unsere Case Manager:innen unterstützen Sie bei der Zusammenstellung aller relevanten medizinischen Unterlagen.", icon: FileText }, if (!page) notFound()
{ num: "03", title: "Experten-Zuordnung", description: "Wir ordnen Ihren Fall einem passenden Facharzt bzw. einer Fachärztin aus unserem Netzwerk zu.", icon: UserCheck },
{ num: "04", title: "Medizinische Prüfung", description: "Der/die Fachärzt:in prüft Ihre Unterlagen und erstellt eine unabhängige medizinische Einschätzung.", icon: ClipboardCheck },
{ num: "05", title: "Schriftliches Gutachten", description: "Sie erhalten ein verständliches Zweitmeinungsgutachten mit klarer Empfehlung.", icon: MessageSquare },
{ num: "06", title: "Nachbetreuung", description: "Bei Bedarf besprechen wir das Ergebnis persönlich und unterstützen bei der weiteren Umsetzung.", icon: HeartHandshake },
]
const whyCards = [ return <BlockRenderer blocks={(page.layout as any[]) || []} />
{ title: "Unabhängig", description: "Unsere Expert:innen haben keine wirtschaftlichen Eigeninteressen.", icon: Shield },
{ title: "Schnell", description: "In der Regel erhalten Sie Ihr Gutachten innerhalb weniger Tage.", icon: Clock },
{ title: "Qualifiziert", description: "Nur erfahrene Fachärzt:innen mit nachgewiesener Expertise.", icon: BadgeCheck },
{ title: "Persönlich", description: "Case Management und persönliche Betreuung von Anfang an.", icon: Users },
]
export default async function SoFunktionierts() {
const settings = await getSiteSettings()
const phone = settings?.contact?.phone || "0800 80 44 100"
return (
<>
{/* Hero */}
<section className="bg-gradient-to-br from-navy via-navy-dark to-[#001a2e] py-20">
<Container size="md">
<div className="text-center">
<h1 className="text-4xl sm:text-5xl font-bold text-white mb-4">
So funktioniert&apos;s
</h1>
<p className="text-white/60 text-lg max-w-2xl mx-auto">
Ihr Weg zur medizinischen Zweitmeinung in 6 einfachen Schritten.
Transparent, sicher und patientenorientiert.
</p>
</div>
</Container>
</section>
{/* Process Steps */}
<section className="py-20 bg-bg">
<Container size="md">
<div className="space-y-6">
{steps.map((step) => (
<div key={step.num} className="flex gap-6 bg-white rounded-xl p-6 border border-border">
<div className="shrink-0">
<div className="w-14 h-14 rounded-2xl bg-primary/10 flex items-center justify-center">
<step.icon className="h-6 w-6 text-primary" />
</div>
</div>
<div>
<div className="flex items-center gap-3 mb-1">
<span className="text-xs font-bold text-primary bg-primary/10 px-2 py-0.5 rounded-full">
Schritt {step.num}
</span>
</div>
<h3 className="text-lg font-bold text-navy mb-1">{step.title}</h3>
<p className="text-text-muted text-sm">{step.description}</p>
</div>
</div>
))}
</div>
</Container>
</section>
{/* Why choose us cards */}
<section className="py-20 bg-white">
<Container>
<h2 className="text-2xl sm:text-3xl font-bold text-center text-navy mb-12">
Warum complex care solutions?
</h2>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
{whyCards.map((card) => (
<div key={card.title} className="bg-bg rounded-xl p-6 text-center border border-border">
<div className="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary mb-4">
<card.icon className="h-6 w-6" />
</div>
<h3 className="font-bold text-navy mb-2">{card.title}</h3>
<p className="text-text-muted text-sm">{card.description}</p>
</div>
))}
</div>
</Container>
</section>
{/* CTA */}
<section className="py-20 bg-bg">
<Container size="sm">
<div className="bg-white rounded-2xl p-8 sm:p-12 text-center border border-border">
<h2 className="text-2xl sm:text-3xl font-bold text-navy mb-4">
Bereit für Ihre Zweitmeinung?
</h2>
<p className="text-text-muted mb-8">
Starten Sie jetzt und erhalten Sie in kürzester Zeit eine
fundierte, unabhängige Einschätzung.
</p>
<div className="flex flex-wrap justify-center gap-4">
<Button href={phoneToHref(phone)} variant="gold" size="lg">
<Phone className="h-4 w-4" />
{phone}
</Button>
<Button href="/kontakt" variant="secondary" size="lg">
Kontaktformular
<ArrowRight className="h-4 w-4" />
</Button>
</div>
</div>
</Container>
</section>
</>
)
} }

View file

@ -1,134 +1,16 @@
import type { Metadata } from "next" import type { Metadata } from "next"
import { Container } from "@/components/ui/Container" import { getPage } from "@/lib/api"
import { Button } from "@/components/ui/Button" import { BlockRenderer } from "@/components/blocks/BlockRenderer"
import { getSiteSettings } from "@/lib/api" import { notFound } from "next/navigation"
import { phoneToHref } from "@/lib/payload-helpers"
import {
Shield, Award, Users, Heart, Phone,
CheckCircle, Building, Globe,
} from "lucide-react"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Über uns", title: "Über uns",
description: "complex care solutions GmbH Ihr Partner für unabhängige medizinische Zweitmeinungen.", description: "complex care solutions GmbH Ihr Partner für unabhängige medizinische Zweitmeinungen.",
} }
const qualityCards = [
{ title: "Unabhängigkeit", description: "Unsere Gutachter haben keine wirtschaftlichen Verbindungen zu behandelnden Einrichtungen.", icon: Shield },
{ title: "Expertise", description: "Nur Fachärzt:innen mit nachgewiesener Expertise und langjähriger Erfahrung.", icon: Award },
{ title: "Patientenorientierung", description: "Der Mensch steht im Mittelpunkt nicht die Diagnose.", icon: Heart },
{ title: "Netzwerk", description: "Über 1000 Expert:innen aus über 50 Fachbereichen deutschlandweit.", icon: Globe },
]
export default async function UeberUnsPage() { export default async function UeberUnsPage() {
const settings = await getSiteSettings() const page = await getPage("ueber-uns")
const phone = settings?.contact?.phone || "0800 80 44 100" if (!page) notFound()
return ( return <BlockRenderer blocks={(page.layout as any[]) || []} />
<>
{/* Hero */}
<section className="bg-gradient-to-br from-navy via-navy-dark to-[#001a2e] py-20">
<Container size="md">
<div className="text-center">
<h1 className="text-4xl sm:text-5xl font-bold text-white mb-4">
Über uns
</h1>
<p className="text-white/60 text-lg max-w-2xl mx-auto">
complex care solutions GmbH Ihr Partner für unabhängige
medizinische Zweitmeinungen seit über 15 Jahren.
</p>
</div>
</Container>
</section>
{/* Quality */}
<section className="py-20 bg-white">
<Container>
<h2 className="text-2xl sm:text-3xl font-bold text-center text-navy mb-12">
Was uns auszeichnet
</h2>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
{qualityCards.map((c) => (
<div key={c.title} className="bg-bg rounded-xl p-6 text-center border border-border">
<div className="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary mb-4">
<c.icon className="h-6 w-6" />
</div>
<h3 className="font-bold text-navy mb-2">{c.title}</h3>
<p className="text-text-muted text-sm">{c.description}</p>
</div>
))}
</div>
</Container>
</section>
{/* Company info */}
<section className="py-20 bg-bg">
<Container size="md">
<div className="bg-white rounded-2xl p-8 sm:p-12 border border-border">
<div className="flex items-center gap-3 mb-6">
<Building className="h-6 w-6 text-primary" />
<h2 className="text-2xl font-bold text-navy">
complex care solutions GmbH
</h2>
</div>
<div className="grid sm:grid-cols-2 gap-8">
<div className="space-y-4 text-sm text-text-muted">
<p>
Die complex care solutions GmbH mit Sitz in Bottrop ist ein
unabhängiges Gesundheitsunternehmen, das sich auf medizinische
Zweitmeinungen spezialisiert hat.
</p>
<p>
Gegründet von Martin Porwoll, dem Whistleblower des Bottroper
Zytoskandals, verfolgen wir das Ziel, Patient:innen in
komplexen medizinischen Situationen bestmöglich zu
unterstützen.
</p>
</div>
<div className="space-y-3">
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="h-4 w-4 text-gold shrink-0" />
<span className="text-text-muted">Über 15 Jahre Erfahrung im Gesundheitswesen</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="h-4 w-4 text-gold shrink-0" />
<span className="text-text-muted">Netzwerk aus über 1000 Fachärzt:innen</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="h-4 w-4 text-gold shrink-0" />
<span className="text-text-muted">DSGVO-konform und datenschutzgeprüft</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="h-4 w-4 text-gold shrink-0" />
<span className="text-text-muted">Sitz in Bottrop, Hans-Böckler-Str. 19</span>
</div>
<div className="flex items-center gap-2 text-sm">
<CheckCircle className="h-4 w-4 text-gold shrink-0" />
<span className="text-text-muted">Kostenlose Servicehotline: {phone}</span>
</div>
</div>
</div>
</div>
</Container>
</section>
{/* CTA */}
<section className="py-16 bg-white">
<Container size="sm">
<div className="text-center">
<h2 className="text-2xl font-bold text-navy mb-4">
Haben Sie Fragen?
</h2>
<p className="text-text-muted mb-8">
Wir beraten Sie gerne unverbindlich und kostenfrei.
</p>
<Button href={phoneToHref(phone)} variant="gold" size="lg">
<Phone className="h-4 w-4" />
{phone} (kostenlos)
</Button>
</div>
</Container>
</section>
</>
)
} }

View file

@ -0,0 +1,43 @@
import { HeroBlock } from "./HeroBlock"
import { TextBlock } from "./TextBlock"
import { CardGridBlock } from "./CardGridBlock"
import { CTABlock } from "./CTABlock"
import { ProcessStepsBlock } from "./ProcessStepsBlock"
import { QuoteBlock } from "./QuoteBlock"
import { HtmlEmbedBlock } from "./HtmlEmbedBlock"
/* eslint-disable @typescript-eslint/no-explicit-any */
interface BlockRendererProps {
blocks: any[]
}
export function BlockRenderer({ blocks }: BlockRendererProps) {
if (!blocks || blocks.length === 0) return null
return (
<>
{blocks.map((block: any, index: number) => {
const key = block.id || `block-${index}`
switch (block.blockType) {
case "hero-block":
return <HeroBlock key={key} block={block} isFirst={index === 0} />
case "text-block":
return <TextBlock key={key} block={block} />
case "card-grid-block":
return <CardGridBlock key={key} block={block} />
case "cta-block":
return <CTABlock key={key} block={block} />
case "process-steps-block":
return <ProcessStepsBlock key={key} block={block} />
case "quote-block":
return <QuoteBlock key={key} block={block} />
case "html-embed-block":
return <HtmlEmbedBlock key={key} block={block} />
default:
return null
}
})}
</>
)
}

View file

@ -0,0 +1,57 @@
import { Container } from "@/components/ui/Container"
import { Button } from "@/components/ui/Button"
/* eslint-disable @typescript-eslint/no-explicit-any */
interface CTABlockProps {
block: any
}
const bgClasses: Record<string, string> = {
dark: "bg-navy text-white",
light: "bg-bg text-navy",
accent: "bg-primary text-white",
}
export function CTABlock({ block }: CTABlockProps) {
const bg = block.backgroundColor || "dark"
const buttons = block.buttons || []
const isLight = bg === "light"
return (
<section className={`py-16 ${bgClasses[bg] || bgClasses.dark}`}>
<Container size="sm">
<div className="text-center">
<h2 className="text-2xl sm:text-3xl font-bold mb-4">
{block.headline}
</h2>
{block.description && (
<p className={`mb-8 ${isLight ? "text-text-muted" : "text-white/80"}`}>
{block.description}
</p>
)}
{buttons.length > 0 && (
<div className="flex flex-wrap justify-center gap-4">
{buttons.map((btn: any, i: number) => {
const variant = btn.style === "outline"
? "outline"
: btn.style === "secondary"
? "secondary"
: isLight ? "primary" : "gold"
return (
<Button
key={btn.id || i}
href={btn.link}
variant={variant}
size="lg"
>
{btn.text}
</Button>
)
})}
</div>
)}
</div>
</Container>
</section>
)
}

View file

@ -0,0 +1,51 @@
import { Container } from "@/components/ui/Container"
import { getLucideIcon } from "@/lib/icon-map"
/* eslint-disable @typescript-eslint/no-explicit-any */
interface CardGridBlockProps {
block: any
}
const columnClasses: Record<string, string> = {
"2": "grid-cols-1 sm:grid-cols-2",
"3": "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
"4": "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4",
}
export function CardGridBlock({ block }: CardGridBlockProps) {
const cols = block.columns || "3"
const cards = block.cards || []
return (
<section className="py-16 bg-bg">
<Container>
{block.headline && (
<h2 className="text-2xl sm:text-3xl font-bold text-center text-navy mb-12">
{block.headline}
</h2>
)}
<div className={`grid ${columnClasses[cols] || columnClasses["3"]} gap-6`}>
{cards.map((card: any, i: number) => {
const Icon = card.mediaType === "icon" && card.icon
? getLucideIcon(card.icon)
: null
return (
<div key={card.id || i} className="bg-white rounded-xl p-6 text-center border border-border">
{Icon && (
<div className="inline-flex items-center justify-center w-12 h-12 rounded-xl bg-primary/10 text-primary mb-4">
<Icon className="h-6 w-6" />
</div>
)}
<h3 className="font-bold text-navy mb-2">{card.title}</h3>
{card.description && (
<p className="text-text-muted text-sm">{card.description}</p>
)}
</div>
)
})}
</div>
</Container>
</section>
)
}

View file

@ -0,0 +1,42 @@
import { Container } from "@/components/ui/Container"
/* eslint-disable @typescript-eslint/no-explicit-any */
interface HeroBlockProps {
block: any
isFirst?: boolean
}
export function HeroBlock({ block, isFirst }: HeroBlockProps) {
const alignment = block.alignment || "center"
const alignClasses: Record<string, string> = {
left: "text-left",
center: "text-center",
right: "text-right",
}
return (
<section className="bg-gradient-to-br from-navy via-navy-dark to-[#001a2e] py-20">
<Container size={isFirst ? "md" : "md"}>
<div className={alignClasses[alignment] || "text-center"}>
<h1 className="text-4xl sm:text-5xl font-bold text-white mb-4">
{block.headline}
</h1>
{block.subline && (
<p className="text-white/60 text-lg max-w-2xl mx-auto">
{block.subline}
</p>
)}
{block.cta?.text && block.cta?.link && (
<a
href={block.cta.link}
className="mt-6 inline-flex items-center gap-2 bg-primary hover:bg-primary-dark text-white px-8 py-3.5 rounded-lg font-semibold transition-all"
>
{block.cta.text}
</a>
)}
</div>
</Container>
</section>
)
}

View file

@ -0,0 +1,28 @@
import { Container } from "@/components/ui/Container"
/* eslint-disable @typescript-eslint/no-explicit-any */
interface HtmlEmbedBlockProps {
block: any
}
const widthClasses: Record<string, string> = {
narrow: "max-w-[620px]",
medium: "max-w-[900px]",
full: "max-w-7xl",
}
export function HtmlEmbedBlock({ block }: HtmlEmbedBlockProps) {
if (!block.code) return null
const maxWidth = block.maxWidth || "full"
return (
<section className="py-16 bg-white">
<Container>
<div className={`mx-auto ${widthClasses[maxWidth] || widthClasses.full}`}>
<div dangerouslySetInnerHTML={{ __html: block.code }} />
</div>
</Container>
</section>
)
}

View file

@ -0,0 +1,74 @@
import { Container } from "@/components/ui/Container"
import { Button } from "@/components/ui/Button"
/* eslint-disable @typescript-eslint/no-explicit-any */
interface ProcessStepsBlockProps {
block: any
}
const bgClasses: Record<string, string> = {
white: "bg-white",
light: "bg-bg",
dark: "bg-navy text-white",
}
export function ProcessStepsBlock({ block }: ProcessStepsBlockProps) {
const steps = block.steps || []
const bg = block.backgroundColor || "white"
return (
<section className={`py-16 ${bgClasses[bg] || "bg-white"}`}>
<Container size="md">
{(block.title || block.subtitle) && (
<div className="text-center mb-12">
{block.title && (
<h2 className="text-2xl sm:text-3xl font-bold text-navy mb-3">
{block.title}
</h2>
)}
{block.subtitle && (
<p className="text-text-muted">{block.subtitle}</p>
)}
</div>
)}
<div className="space-y-6">
{steps.map((step: any, i: number) => (
<div key={step.id || i} className="flex gap-6 bg-white rounded-xl p-6 border border-border">
<div className="shrink-0">
<div className="w-14 h-14 rounded-2xl bg-primary/10 flex items-center justify-center">
{step.icon ? (
<span className="text-xl">{step.icon}</span>
) : block.showNumbers ? (
<span className="text-sm font-bold text-primary">
{String(i + 1).padStart(2, "0")}
</span>
) : null}
</div>
</div>
<div>
{block.showNumbers && (
<span className="text-xs font-bold text-primary bg-primary/10 px-2 py-0.5 rounded-full">
Schritt {String(i + 1).padStart(2, "0")}
</span>
)}
<h3 className="text-lg font-bold text-navy mb-1 mt-1">{step.title}</h3>
{step.description && (
<p className="text-text-muted text-sm">{step.description}</p>
)}
</div>
</div>
))}
</div>
{block.cta?.show && block.cta?.href && (
<div className="text-center mt-10">
<Button href={block.cta.href} variant="primary" size="lg">
{block.cta.label || "Jetzt starten"}
</Button>
</div>
)}
</Container>
</section>
)
}

View file

@ -0,0 +1,46 @@
import { Container } from "@/components/ui/Container"
/* eslint-disable @typescript-eslint/no-explicit-any */
interface QuoteBlockProps {
block: any
}
export function QuoteBlock({ block }: QuoteBlockProps) {
const isHighlighted = block.style === "highlighted"
if (isHighlighted) {
return (
<section className="py-12 bg-navy">
<Container size="sm">
<div className="text-center">
<blockquote className="text-white/80 text-lg leading-relaxed italic">
&ldquo;{block.quote}&rdquo;
</blockquote>
{(block.author || block.role) && (
<cite className="text-white/50 text-sm mt-4 block not-italic">
{block.author && `${block.author}`}
{block.role && `, ${block.role}`}
</cite>
)}
</div>
</Container>
</section>
)
}
return (
<section className="py-12 bg-white">
<Container size="md">
<blockquote className="border-l-4 border-gold pl-6 py-2 bg-gold/5 rounded-r-lg">
<p className="text-navy italic">&ldquo;{block.quote}&rdquo;</p>
{(block.author || block.role) && (
<cite className="text-sm text-text-muted mt-2 block not-italic">
{block.author && `${block.author}`}
{block.role && `, ${block.role}`}
</cite>
)}
</blockquote>
</Container>
</section>
)
}

View file

@ -0,0 +1,25 @@
import { Container } from "@/components/ui/Container"
import { RichTextRenderer } from "@/components/ui/RichTextRenderer"
/* eslint-disable @typescript-eslint/no-explicit-any */
interface TextBlockProps {
block: any
}
const widthMap: Record<string, "sm" | "md" | "lg"> = {
narrow: "sm",
medium: "md",
full: "lg",
}
export function TextBlock({ block }: TextBlockProps) {
const size = widthMap[block.width] || "md"
return (
<section className="py-16 bg-white">
<Container size={size}>
<RichTextRenderer content={block.content} />
</Container>
</section>
)
}

View file

@ -0,0 +1,8 @@
export { BlockRenderer } from "./BlockRenderer"
export { HeroBlock } from "./HeroBlock"
export { TextBlock } from "./TextBlock"
export { CardGridBlock } from "./CardGridBlock"
export { CTABlock } from "./CTABlock"
export { ProcessStepsBlock } from "./ProcessStepsBlock"
export { QuoteBlock } from "./QuoteBlock"
export { HtmlEmbedBlock } from "./HtmlEmbedBlock"