feat: migrate services, FAQs, and contact info to CMS-driven data

Replace hardcoded content with Payload CMS data for:
- Services overview, listing, and detail pages (features, icons, sections)
- FAQ page with rich text rendering and Schema.org structured data
- Contact info in TopBar, EmergencyBanner, Footer, and Kontakt page
- Header mega-menu with dynamic service list

New utilities: icon-map.ts (Lucide icon mapping), RichTextRenderer.tsx
Fix: ecosystem.config.js PM2 script path for Next.js

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
CCS Admin 2026-02-21 01:34:20 +00:00
parent 62769379ad
commit 1aad09cc0f
14 changed files with 492 additions and 473 deletions

View file

@ -1,7 +1,7 @@
module.exports = { module.exports = {
apps: [{ apps: [{
name: 'zweitmeinu.ng', name: 'zweitmeinu.ng',
script: 'node_modules/.bin/next', script: 'node_modules/next/dist/bin/next',
args: 'start', args: 'start',
cwd: '/home/frontend/frontend.zweitmeinu.ng', cwd: '/home/frontend/frontend.zweitmeinu.ng',
env: { env: {

View file

@ -2,174 +2,14 @@ import type { Metadata } from "next"
import { notFound } from "next/navigation" import { notFound } from "next/navigation"
import { Container } from "@/components/ui/Container" import { Container } from "@/components/ui/Container"
import { Button } from "@/components/ui/Button" import { Button } from "@/components/ui/Button"
import { import { RichTextRenderer } from "@/components/ui/RichTextRenderer"
Phone, Mail, ArrowRight, Check, Heart, Brain, import { getLucideIcon } from "@/lib/icon-map"
Stethoscope, Pill, FlaskConical, Activity, Shield, import { getServices, getServiceBySlug } from "@/lib/api"
FileCheck, Users, Clock, HeartHandshake, import { Phone, Mail, ArrowRight, Check } from "lucide-react"
} from "lucide-react"
// Static service data — will be enhanced with CMS data when populated
const servicesData: Record<string, {
title: string
fullTitle: string
category: string
icon: React.ElementType
shortDescription: string
description: string[]
benefits: Array<{ title: string; description: string; icon: React.ElementType }>
checklist: Array<{ title: string; description: string }>
seo: { title: string; description: string }
}> = {
"zweitmeinung-intensivmedizin": {
title: "Intensivmedizin",
fullTitle: "Zweitmeinung Intensivmedizin",
category: "Notfall",
icon: Activity,
shortDescription: "Unabhängige ärztliche Zweitmeinung bei laufender oder geplanter Intensivbehandlung. Wir prüfen medizinische Indikation, Patientenwille und Behandlungsalternativen.",
description: [
"Wenn intensivmedizinische Entscheidungen anstehen, brauchen Patient:innen und ihre Angehörigen mehr als nur medizinische Information sie brauchen Orientierung, Sicherheit und eine unabhängige fachliche Einschätzung.",
"Unsere Dienstleistung richtet sich an Menschen in sehr schwerer gesundheitlicher Lage, etwa bei Langzeitbeatmung, Wachkoma oder im palliativen Kontext.",
],
benefits: [
{ title: "Unabhängige Beurteilung durch erfahrene Intensivmediziner", description: "Neutrale Einschätzung ohne wirtschaftliche Eigeninteressen ausschließlich in Ihrem Interesse.", icon: Shield },
{ title: "Vermeidung unnötiger Eingriffe bei fehlender Indikation", description: "Schutz vor überflüssigen invasiven Behandlungen durch fundierte medizinische Bewertung.", icon: FileCheck },
{ title: "Besseres Verständnis der Risiken und Therapieziele", description: "Umfassende Aufklärung über alle Behandlungsoptionen und deren Auswirkungen.", icon: Users },
{ title: "Stärkung Ihrer Entscheidungssicherheit und Eigenverantwortung", description: "Fundierte Basis für selbstbestimmte Entscheidungen über Ihre Versorgung.", icon: HeartHandshake },
],
checklist: [
{ title: "Strukturierte Beratung durch erfahrene Case Manager:innen", description: "Individuelle Betreuung für Ihre weitere Behandlungsplanung." },
{ title: "Unabhängige ärztliche Zweitmeinung inkl. schriftlichem Gutachten", description: "Fundierte Einschätzung durch Fachärzt:innen für Intensivmedizin." },
{ title: "Bewertung von Therapiezielen, Indikationen und Prognose", description: "Umfassende Analyse aller relevanten medizinischen Aspekte." },
{ title: "Unterstützung bei palliativer Umsteuerung bis Pflegeüberleitung", description: "Begleitung bei der Umsetzung der empfohlenen Maßnahmen." },
],
seo: { title: "Zweitmeinung Intensivmedizin", description: "Unabhängige ärztliche Zweitmeinung bei intensivmedizinischer Behandlung. Jetzt fundierte Empfehlung einholen." },
},
"zweitmeinung-kardiologie": {
title: "Kardiologie",
fullTitle: "Zweitmeinung Kardiologie",
category: "Beratung",
icon: Heart,
shortDescription: "Unabhängige ärztliche Zweitmeinung vor Herzkatheter, Stent oder OP. Fundierte Empfehlung durch erfahrene Kardiolog:innen verständlich, sicher, neutral.",
description: [
"Herzbeschwerden verunsichern und geplante Eingriffe wie eine Stent-Implantation werfen viele Fragen auf.",
"Unsere kardiologische Zweitmeinung gibt Ihnen Klarheit und Sicherheit bei wichtigen Herzentscheidungen.",
],
benefits: [
{ title: "Unabhängige Beurteilung durch erfahrene Kardiolog:innen", description: "Neutrale Einschätzung Ihrer kardiologischen Befunde ohne wirtschaftliche Interessen.", icon: Shield },
{ title: "Vermeidung unnötiger Eingriffe bei fehlender Indikation", description: "Schutz vor überflüssigen invasiven Behandlungen durch fundierte Bewertung.", icon: FileCheck },
{ title: "Besseres Verständnis der Risiken und Therapieziele", description: "Umfassende Aufklärung über alle Behandlungsoptionen.", icon: Users },
{ title: "Stärkung Ihrer Entscheidungssicherheit", description: "Fundierte Basis für selbstbestimmte Entscheidungen über Ihre Herzgesundheit.", icon: HeartHandshake },
],
checklist: [
{ title: "Bewertung Ihrer Diagnosen und EKG-/Katheterbefunde", description: "Sach- und leitliniengerechte Beurteilung durch Fachärzt:innen." },
{ title: "Zweitmeinung bei geplanter PCI, Bypass-OP oder Umstellung", description: "Unabhängige Einschätzung aller kardiologischen Behandlungsoptionen." },
{ title: "Schriftliches ärztliches Gutachten mit klarer Empfehlung", description: "Nachvollziehbare, fundierte Empfehlung für Ihre Entscheidung." },
{ title: "Persönliche Erläuterung telefonisch oder per Video", description: "Direkter Austausch mit unseren Kardiologie-Expert:innen." },
],
seo: { title: "Zweitmeinung Kardiologie", description: "Unabhängige Zweitmeinung bei geplanter PCI oder Herzoperation." },
},
"zweitmeinung-onkologie": {
title: "Onkologie",
fullTitle: "Zweitmeinung Onkologie",
category: "Beratung",
icon: FlaskConical,
shortDescription: "Unabhängige ärztliche Zweitmeinung bei Krebs. Fundierte Einschätzung von Therapieoptionen durch erfahrene Onkolog:innen.",
description: [
"Eine Krebsdiagnose ist ein Einschnitt. Neben der seelischen Belastung stellt sich oft die Frage: Ist die empfohlene Behandlung wirklich die beste Wahl?",
"Unsere onkologische Zweitmeinung hilft Ihnen, die richtige Therapieentscheidung zu treffen.",
],
benefits: [
{ title: "Unabhängige Beurteilung durch erfahrene Onkolog:innen", description: "Neutrale Einschätzung Ihrer Krebsdiagnose und Therapieoptionen.", icon: Shield },
{ title: "Bewertung von Wirksamkeit und Nebenwirkungen", description: "Transparente Analyse aller Behandlungswege und deren Auswirkungen auf Ihre Lebensqualität.", icon: FileCheck },
{ title: "Einbindung von Case Management und Palliativberatung", description: "Ganzheitliche Betreuung über die reine Diagnose hinaus.", icon: Users },
{ title: "Stärkung Ihrer Entscheidungssicherheit", description: "Fundierte Grundlage für informierte Therapieentscheidungen.", icon: HeartHandshake },
],
checklist: [
{ title: "Auswertung Ihrer Diagnose und Befunde", description: "Umfassende Prüfung durch erfahrene Fachärzt:innen." },
{ title: "Bewertung der geplanten Therapie", description: "Analyse von Wirksamkeit, Nebenwirkungen und Lebensqualität." },
{ title: "Schriftliches Zweitmeinungsgutachten", description: "Nachvollziehbare, medizinisch fundierte Empfehlung." },
{ title: "Optionales Gespräch per Telefon oder Video", description: "Persönliche Erläuterung und Beantwortung Ihrer Fragen." },
],
seo: { title: "Zweitmeinung Onkologie", description: "Unabhängige ärztliche Zweitmeinung bei Krebs. Behandlungsalternativen prüfen." },
},
"zweitmeinung-nephrologie": {
title: "Nephrologie",
fullTitle: "Zweitmeinung Nephrologie",
category: "Beratung",
icon: Stethoscope,
shortDescription: "Unabhängige ärztliche Einschätzung bei Nierenerkrankungen, Dialyseempfehlung oder Transplantationsvorbereitung.",
description: [
"Die Diagnose einer chronischen Nierenerkrankung oder die Empfehlung zur Dialyse ist ein gravierender Einschnitt.",
"Unsere nephrologische Zweitmeinung gibt Ihnen Klarheit bei wichtigen Nierenentscheidungen.",
],
benefits: [
{ title: "Unabhängige Beurteilung durch erfahrene Nephrolog:innen", description: "Neutrale Einschätzung Ihrer nephrologischen Diagnostik.", icon: Shield },
{ title: "Prüfung der Dialyse-Notwendigkeit", description: "Bewertung ob und wann eine Dialyse tatsächlich erforderlich ist.", icon: FileCheck },
{ title: "Bewertung konservativer Behandlungsoptionen", description: "Prüfung alternativer Therapiewege vor invasiven Maßnahmen.", icon: Users },
{ title: "Stärkung Ihrer Entscheidungssicherheit", description: "Fundierte Basis für selbstbestimmte Entscheidungen.", icon: HeartHandshake },
],
checklist: [
{ title: "Prüfung der nephrologischen Diagnostik und Laborwerte", description: "Umfassende Analyse Ihrer Nierenfunktionswerte." },
{ title: "Zweitmeinung durch Fachärzt:innen für Nephrologie", description: "Unabhängige Einschätzung erfahrener Spezialist:innen." },
{ title: "Gutachten zur Notwendigkeit einer Dialyse", description: "Klare Empfehlung zum Zeitpunkt und zur Art der Behandlung." },
{ title: "Bewertung konservativer Behandlungsoptionen", description: "Prüfung aller verfügbaren Therapiealternativen." },
],
seo: { title: "Zweitmeinung Nephrologie", description: "Unabhängige ärztliche Zweitmeinung bei chronischer Niereninsuffizienz oder Dialyseempfehlung." },
},
"zweitmeinung-gallenblase": {
title: "Gallenblase",
fullTitle: "Zweitmeinung Gallenblase",
category: "Beratung",
icon: Pill,
shortDescription: "Unabhängige ärztliche Zweitmeinung vor einer geplanten Gallenblasen-OP. Wir prüfen, ob der Eingriff medizinisch notwendig ist.",
description: [
"Viele Menschen erhalten bei Gallensteinen die Empfehlung, die Gallenblase entfernen zu lassen. Doch nicht in jedem Fall ist eine Operation notwendig.",
"Unsere Zweitmeinung hilft Ihnen, unnötige Operationen zu vermeiden.",
],
benefits: [
{ title: "Unabhängige Beurteilung durch erfahrene Chirurg:innen", description: "Neutrale Einschätzung der OP-Indikation.", icon: Shield },
{ title: "Vermeidung unnötiger Operationen", description: "Schutz vor überflüssigen Eingriffen durch fundierte Bewertung.", icon: FileCheck },
{ title: "Aufklärung über konservative Alternativen", description: "Information über nicht-operative Behandlungsmöglichkeiten.", icon: Users },
{ title: "Verständliche Erklärung der Befunde", description: "Klare Darstellung von Risiken und Nutzen.", icon: HeartHandshake },
],
checklist: [
{ title: "Bewertung Ihrer Beschwerden und Untersuchungsergebnisse", description: "Sorgfältige Analyse aller vorliegenden Befunde." },
{ title: "Prüfung der OP-Indikation nach medizinischen Leitlinien", description: "Leitlinienbasierte Bewertung der Operationsnotwendigkeit." },
{ title: "Zweitmeinung durch Viszeralchirurg:innen oder Gastroenterolog:innen", description: "Unabhängige Einschätzung spezialisierter Fachärzt:innen." },
{ title: "Verständliches Gutachten mit klarer Empfehlung", description: "Nachvollziehbare Entscheidungsgrundlage." },
],
seo: { title: "Zweitmeinung Gallenblase", description: "Gallenblasen-OP empfohlen? Holen Sie sich eine unabhängige Zweitmeinung." },
},
"zweitmeinung-schilddruese": {
title: "Schilddrüse",
fullTitle: "Zweitmeinung Schilddrüse",
category: "Beratung",
icon: Brain,
shortDescription: "Unabhängige ärztliche Einschätzung vor einer geplanten Schilddrüsen-OP durch erfahrene Endokrinolog:innen.",
description: [
"Die Empfehlung zur Entfernung der Schilddrüse ist für viele Menschen mit Sorgen verbunden. Doch ist eine Operation wirklich notwendig?",
"Unsere Zweitmeinung gibt Ihnen Sicherheit bei der Entscheidung.",
],
benefits: [
{ title: "Unabhängige Beurteilung durch erfahrene Endokrinolog:innen", description: "Neutrale Einschätzung Ihrer Schilddrüsenbefunde.", icon: Shield },
{ title: "Prüfung der OP-Notwendigkeit", description: "Bewertung ob eine Operation tatsächlich indiziert ist.", icon: FileCheck },
{ title: "Bewertung konservativer Alternativen", description: "Prüfung medikamentöser oder abwartender Therapieoptionen.", icon: Users },
{ title: "Verständliche Erklärung der Befunde", description: "Nachvollziehbare Darstellung aller Optionen.", icon: HeartHandshake },
],
checklist: [
{ title: "Prüfung von Ultraschallbefunden, Szintigrammen, Laborwerten", description: "Umfassende Analyse aller diagnostischen Ergebnisse." },
{ title: "Zweitmeinung durch Endokrinolog:innen oder Schilddrüsenchirurg:innen", description: "Spezialisierte Fachärzt:innen bewerten Ihren Fall." },
{ title: "Schriftliches Gutachten mit nachvollziehbarer Empfehlung", description: "Fundierte Entscheidungsgrundlage für Sie und Ihre Ärzt:innen." },
{ title: "Bei Bedarf: telefonische Erläuterung", description: "Persönliches Gespräch zu allen offenen Fragen." },
],
seo: { title: "Zweitmeinung Schilddrüse", description: "Schilddrüsen-OP empfohlen? Lassen Sie die Notwendigkeit prüfen." },
},
}
const slugs = Object.keys(servicesData)
export async function generateStaticParams() { export async function generateStaticParams() {
return slugs.map((slug) => ({ slug })) const services = await getServices()
return services.map((s) => ({ slug: s.slug }))
} }
export async function generateMetadata({ export async function generateMetadata({
@ -178,9 +18,12 @@ export async function generateMetadata({
params: Promise<{ slug: string }> params: Promise<{ slug: string }>
}): Promise<Metadata> { }): Promise<Metadata> {
const { slug } = await params const { slug } = await params
const service = servicesData[slug] const service = await getServiceBySlug(slug)
if (!service) return {} if (!service) return {}
return { title: service.seo.title, description: service.seo.description } return {
title: service.metaTitle || service.title,
description: service.metaDescription || service.shortDescription,
}
} }
export default async function ServiceDetailPage({ export default async function ServiceDetailPage({
@ -189,10 +32,14 @@ export default async function ServiceDetailPage({
params: Promise<{ slug: string }> params: Promise<{ slug: string }>
}) { }) {
const { slug } = await params const { slug } = await params
const service = servicesData[slug] const service = await getServiceBySlug(slug)
if (!service) notFound() if (!service) notFound()
const Icon = service.icon const Icon = getLucideIcon(service.icon)
const shortTitle = service.title.replace(/^Zweitmeinung\s+/, "")
const categoryName = typeof service.category === "object" && service.category
? service.category.name
: null
return ( return (
<> <>
@ -204,13 +51,10 @@ export default async function ServiceDetailPage({
<Icon className="h-8 w-8" /> <Icon className="h-8 w-8" />
</div> </div>
<p className="text-primary-light text-sm font-semibold mb-2"> <p className="text-primary-light text-sm font-semibold mb-2">
Fachbereiche / {service.title} Fachbereiche / {shortTitle}
</p> </p>
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-6"> <h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-6">
{service.fullTitle} klare Empfehlungen bei{" "} {service.title}
{service.title === "Intensivmedizin"
? "kritischen Entscheidungen"
: `${service.title}-Entscheidungen`}
</h1> </h1>
<p className="text-white/60 text-lg max-w-2xl mx-auto mb-10"> <p className="text-white/60 text-lg max-w-2xl mx-auto mb-10">
{service.shortDescription} {service.shortDescription}
@ -229,59 +73,77 @@ export default async function ServiceDetailPage({
</Container> </Container>
</section> </section>
{/* When is a second opinion useful */} {/* Benefits grid from features[] */}
<section className="py-20 bg-white"> {service.features && service.features.length > 0 && (
<Container size="md"> <section className="py-20 bg-white">
<h2 className="text-2xl sm:text-3xl font-bold text-center text-navy mb-4"> <Container size="md">
Wann ist eine Zweitmeinung sinnvoll? <h2 className="text-2xl sm:text-3xl font-bold text-center text-navy mb-4">
</h2> Wann ist eine Zweitmeinung sinnvoll?
<p className="text-text-muted text-center max-w-2xl mx-auto mb-12"> </h2>
{service.description[0]} <p className="text-text-muted text-center max-w-2xl mx-auto mb-12">
</p> {service.shortDescription}
<div className="grid sm:grid-cols-2 gap-6"> </p>
{service.benefits.map((b) => ( <div className="grid sm:grid-cols-2 gap-6">
<div {service.features.map((f) => {
key={b.title} const FeatureIcon = getLucideIcon(f.icon)
className="bg-bg rounded-xl p-6 border border-border" return (
> <div
<b.icon className="h-6 w-6 text-primary mb-3" /> key={f.id || f.title}
<h3 className="font-bold text-navy mb-2">{b.title}</h3> className="bg-bg rounded-xl p-6 border border-border"
<p className="text-sm text-text-muted">{b.description}</p> >
</div> <FeatureIcon className="h-6 w-6 text-primary mb-3" />
))} <h3 className="font-bold text-navy mb-2">{f.title}</h3>
</div> <p className="text-sm text-text-muted">{f.description}</p>
</Container> </div>
</section> )
})}
</div>
</Container>
</section>
)}
{/* What we do for you */} {/* Checklist from detailSections[] */}
<section id="was-wir-tun" className="py-20 bg-bg"> {service.detailSections && service.detailSections.length > 0 && (
<Container size="md"> <section id="was-wir-tun" className="py-20 bg-bg">
<h2 className="text-2xl sm:text-3xl font-bold text-center text-navy mb-3"> <Container size="md">
Was wir für Sie tun <h2 className="text-2xl sm:text-3xl font-bold text-center text-navy mb-3">
</h2> Was wir für Sie tun
<p className="text-text-muted text-center mb-12"> </h2>
{service.description[1]} <p className="text-text-muted text-center mb-12">
</p> Unser Leistungsumfang im Bereich {shortTitle}
<div className="space-y-4 max-w-2xl mx-auto"> </p>
{service.checklist.map((item) => ( <div className="space-y-4 max-w-2xl mx-auto">
<div {service.detailSections.map((item) => (
key={item.title} <div
className="flex gap-4 bg-white rounded-xl p-5 border border-border" key={item.id || item.title}
> className="flex gap-4 bg-white rounded-xl p-5 border border-border"
<div className="shrink-0 w-8 h-8 rounded-full bg-gold/20 flex items-center justify-center"> >
<Check className="h-4 w-4 text-gold-hover" /> <div className="shrink-0 w-8 h-8 rounded-full bg-gold/20 flex items-center justify-center">
<Check className="h-4 w-4 text-gold-hover" />
</div>
<div>
<h3 className="font-bold text-navy">{item.title}</h3>
<div className="text-sm text-text-muted mt-1">
<RichTextRenderer content={item.content as any} />
</div>
</div>
</div> </div>
<div> ))}
<h3 className="font-bold text-navy">{item.title}</h3> </div>
<p className="text-sm text-text-muted mt-1"> </Container>
{item.description} </section>
</p> )}
</div>
</div> {/* Full description (richText) */}
))} {service.description && (
</div> <section className="py-20 bg-white">
</Container> <Container size="md">
</section> <div className="prose prose-lg max-w-none text-text-muted">
<RichTextRenderer content={service.description as any} />
</div>
</Container>
</section>
)}
{/* Stats */} {/* Stats */}
<section className="py-16 bg-white border-y border-border"> <section className="py-16 bg-white border-y border-border">
@ -299,9 +161,7 @@ export default async function ServiceDetailPage({
500+ 500+
</div> </div>
<p className="text-sm text-text-muted mt-1"> <p className="text-sm text-text-muted mt-1">
{service.title === "Kardiologie" Medizinische Zweitmeinungen
? "Kardiologische Zweitmeinungen"
: "Medizinische Zweitmeinungen"}
</p> </p>
</div> </div>
<div> <div>
@ -330,11 +190,11 @@ export default async function ServiceDetailPage({
<Icon className="h-7 w-7" /> <Icon className="h-7 w-7" />
</div> </div>
<h2 className="text-2xl sm:text-3xl font-bold text-navy mb-4"> <h2 className="text-2xl sm:text-3xl font-bold text-navy mb-4">
Bereit für Ihre {service.title === "Intensivmedizin" ? "intensivmedizinische" : service.title.toLowerCase() === "gallenblase" ? "Gallenblasen-" : service.title.toLowerCase() === "schilddrüse" ? "Schilddrüsen-" : `${service.title.toLowerCase()}ische`} Zweitmeinung? Bereit für Ihre Zweitmeinung?
</h2> </h2>
<p className="text-text-muted mb-8"> <p className="text-text-muted mb-8">
Kontaktieren Sie uns für eine unabhängige, professionelle Kontaktieren Sie uns für eine unabhängige, professionelle
Einschätzung. Einschätzung im Bereich {shortTitle}.
</p> </p>
<div className="flex flex-wrap justify-center gap-4"> <div className="flex flex-wrap justify-center gap-4">
<Button href="tel:+4980080441000" variant="gold" size="lg"> <Button href="tel:+4980080441000" variant="gold" size="lg">

View file

@ -1,67 +1,28 @@
import type { Metadata } from "next" import type { Metadata } from "next"
import Link from "next/link" import Link from "next/link"
import { ArrowRight, Heart, Brain, Stethoscope, Pill, FlaskConical, Activity } from "lucide-react" import { ArrowRight, Phone } from "lucide-react"
import { Container } from "@/components/ui/Container" import { Container } from "@/components/ui/Container"
import { Button } from "@/components/ui/Button" import { Button } from "@/components/ui/Button"
import { Phone } from "lucide-react" import { getServices } from "@/lib/api"
import { getLucideIcon } from "@/lib/icon-map"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Fachbereiche", title: "Fachbereiche",
description: "Medizinische Zweitmeinung in 6 Fachbereichen: Intensivmedizin, Kardiologie, Onkologie, Nephrologie, Gallenblase und Schilddrüse.", description: "Medizinische Zweitmeinung in 6 Fachbereichen: Intensivmedizin, Kardiologie, Onkologie, Nephrologie, Gallenblase und Schilddrüse.",
} }
const services = [ const colorMap: Record<string, string> = {
{ "zweitmeinung-intensivmedizin": "bg-red-50 text-red-600",
title: "Intensivmedizin", "zweitmeinung-kardiologie": "bg-primary/10 text-primary",
slug: "zweitmeinung-intensivmedizin", "zweitmeinung-onkologie": "bg-purple-50 text-purple-600",
description: "Unabhängige ärztliche Zweitmeinung bei laufender oder geplanter Intensivbehandlung. Wir prüfen medizinische Indikation, Patientenwille und Behandlungsalternativen.", "zweitmeinung-nephrologie": "bg-emerald-50 text-emerald-600",
icon: Activity, "zweitmeinung-gallenblase": "bg-amber-50 text-amber-600",
category: "Notfall", "zweitmeinung-schilddruese": "bg-cyan-50 text-cyan-600",
color: "bg-red-50 text-red-600", }
},
{ export default async function FachbereichePage() {
title: "Kardiologie", const services = await getServices()
slug: "zweitmeinung-kardiologie",
description: "Unabhängige ärztliche Zweitmeinung vor Herzkatheter, Stent oder OP. Fundierte Empfehlung durch erfahrene Kardiolog:innen.",
icon: Heart,
category: "Beratung",
color: "bg-primary/10 text-primary",
},
{
title: "Onkologie",
slug: "zweitmeinung-onkologie",
description: "Unabhängige ärztliche Zweitmeinung bei Krebs. Fundierte Einschätzung von Therapieoptionen durch erfahrene Onkolog:innen.",
icon: FlaskConical,
category: "Beratung",
color: "bg-purple-50 text-purple-600",
},
{
title: "Nephrologie",
slug: "zweitmeinung-nephrologie",
description: "Unabhängige ärztliche Einschätzung bei Nierenerkrankungen, Dialyseempfehlung oder Transplantationsvorbereitung.",
icon: Stethoscope,
category: "Beratung",
color: "bg-emerald-50 text-emerald-600",
},
{
title: "Gallenblase",
slug: "zweitmeinung-gallenblase",
description: "Unabhängige ärztliche Zweitmeinung vor einer geplanten Gallenblasen-OP. Ist der Eingriff wirklich notwendig?",
icon: Pill,
category: "Beratung",
color: "bg-amber-50 text-amber-600",
},
{
title: "Schilddrüse",
slug: "zweitmeinung-schilddruese",
description: "Unabhängige ärztliche Einschätzung vor einer geplanten Schilddrüsen-OP durch erfahrene Endokrinolog:innen.",
icon: Brain,
category: "Beratung",
color: "bg-cyan-50 text-cyan-600",
},
]
export default function FachbereichePage() {
return ( return (
<> <>
{/* Hero */} {/* Hero */}
@ -75,7 +36,7 @@ export default function FachbereichePage() {
Medizinische Zweitmeinung Medizinische Zweitmeinung
</h1> </h1>
<p className="text-white/60 text-lg max-w-2xl mx-auto"> <p className="text-white/60 text-lg max-w-2xl mx-auto">
Unabhängige, fundierte Zweitmeinungen in 6 medizinischen Unabhängige, fundierte Zweitmeinungen in {services.length} medizinischen
Fachbereichen. Von erfahrenen Fachärzt:innen verständlich, Fachbereichen. Von erfahrenen Fachärzt:innen verständlich,
neutral und sicher. neutral und sicher.
</p> </p>
@ -87,34 +48,42 @@ export default function FachbereichePage() {
<section className="py-20 bg-bg"> <section className="py-20 bg-bg">
<Container> <Container>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"> <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
{services.map((s) => ( {services.map((s) => {
<Link const Icon = getLucideIcon(s.icon)
key={s.slug} const color = colorMap[s.slug] || "bg-primary/10 text-primary"
href={`/fachbereiche/${s.slug}`} const categoryName = typeof s.category === "object" && s.category
className="group bg-white rounded-2xl p-8 border border-border hover:border-primary/30 hover:shadow-xl transition-all" ? s.category.name
> : null
<div className="flex items-start justify-between mb-5">
<div className={`w-14 h-14 rounded-2xl ${s.color} flex items-center justify-center`}> return (
<s.icon className="h-7 w-7" /> <Link
key={s.slug}
href={`/fachbereiche/${s.slug}`}
className="group bg-white rounded-2xl p-8 border border-border hover:border-primary/30 hover:shadow-xl transition-all"
>
<div className="flex items-start justify-between mb-5">
<div className={`w-14 h-14 rounded-2xl ${color} flex items-center justify-center`}>
<Icon className="h-7 w-7" />
</div>
{categoryName === "Notfall" && (
<span className="text-xs font-bold bg-red-100 text-red-700 px-3 py-1 rounded-full">
24/7 Notfall
</span>
)}
</div> </div>
{s.category === "Notfall" && ( <h2 className="text-xl font-bold mb-3 text-navy group-hover:text-primary transition-colors">
<span className="text-xs font-bold bg-red-100 text-red-700 px-3 py-1 rounded-full"> {s.title}
24/7 Notfall </h2>
</span> <p className="text-text-muted text-sm leading-relaxed mb-6">
)} {s.shortDescription}
</div> </p>
<h2 className="text-xl font-bold mb-3 text-navy group-hover:text-primary transition-colors"> <span className="inline-flex items-center gap-1 text-sm font-semibold text-primary group-hover:gap-2 transition-all">
Zweitmeinung {s.title} Mehr erfahren
</h2> <ArrowRight className="h-4 w-4" />
<p className="text-text-muted text-sm leading-relaxed mb-6"> </span>
{s.description} </Link>
</p> )
<span className="inline-flex items-center gap-1 text-sm font-semibold text-primary group-hover:gap-2 transition-all"> })}
Mehr erfahren
<ArrowRight className="h-4 w-4" />
</span>
</Link>
))}
</div> </div>
</Container> </Container>
</section> </section>

View file

@ -3,6 +3,8 @@ import { Container } from "@/components/ui/Container"
import { Button } from "@/components/ui/Button" import { Button } from "@/components/ui/Button"
import { FAQClient } from "@/components/faq/FAQClient" import { FAQClient } from "@/components/faq/FAQClient"
import { Phone, Mail, HelpCircle } from "lucide-react" import { Phone, Mail, HelpCircle } from "lucide-react"
import { getFaqs } from "@/lib/api"
import { richTextToPlainText } from "@/components/ui/RichTextRenderer"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "FAQ - Häufige Fragen", title: "FAQ - Häufige Fragen",
@ -10,44 +12,34 @@ export const metadata: Metadata = {
"Antworten auf die wichtigsten Fragen zur medizinischen Zweitmeinung. Von Onkologie über Kardiologie bis Intensivmedizin.", "Antworten auf die wichtigsten Fragen zur medizinischen Zweitmeinung. Von Onkologie über Kardiologie bis Intensivmedizin.",
} }
const categories = [ const categoryNames: Record<string, string> = {
{ name: "Allgemeine Fragen", slug: "allgemein" }, allgemein: "Allgemeine Fragen",
{ name: "Intensivmedizin", slug: "intensivmedizin" }, intensivmedizin: "Intensivmedizin",
{ name: "Kardiologie", slug: "kardiologie" }, kardiologie: "Kardiologie",
{ name: "Onkologie", slug: "onkologie" }, onkologie: "Onkologie",
{ name: "Nephrologie", slug: "nephrologie" }, nephrologie: "Nephrologie",
{ name: "Gallenblase", slug: "gallenblase" }, gallenblase: "Gallenblase",
{ name: "Schilddrüse", slug: "schilddruese" }, schilddruese: "Schilddrüse",
] }
const faqData = [ export default async function FAQPage() {
{ question: "Was bringt mir eine Zweitmeinung bei Krebs?", answer: "Eine Zweitmeinung kann Ihnen Sicherheit geben - vor allem bei schweren Diagnosen oder belastenden Therapien. Sie hilft, Behandlungsoptionen besser zu verstehen, Alternativen zu erkennen und eine informierte Entscheidung zu treffen.", category: "allgemein" }, const faqs = await getFaqs()
{ question: "Wie läuft das Zweitmeinungsverfahren ab?", answer: "Nach einem telefonischen Vorgespräch prüfen unsere Fachärzt:innen Ihre Unterlagen. Anschließend erhalten Sie ein schriftliches Gutachten mit einer klaren, medizinisch fundierten Empfehlung. Wenn gewünscht, besprechen wir das Ergebnis zusätzlich persönlich mit Ihnen.", category: "allgemein" },
{ question: "Muss ich alle meine Unterlagen selbst zusammensuchen?", answer: "Nein. Unsere Case Manager:innen unterstützen Sie bei der Zusammenstellung aller relevanten medizinischen Unterlagen. Sie müssen lediglich eine Schweigepflichtentbindung unterschreiben.", category: "allgemein" }, // Derive categories from data
{ question: "Kann ich die Zweitmeinung auch einholen, wenn die Therapie schon begonnen hat?", answer: "Ja, auch während einer laufenden Behandlung kann eine Zweitmeinung sinnvoll sein - z.B. um die Therapie zu überprüfen oder Alternativen zu bewerten.", category: "allgemein" }, const categorySlugs = [...new Set(faqs.map((f) => f.category).filter((c): c is string => !!c))]
{ question: "Beeinflusst die Zweitmeinung meine Behandlung?", answer: "Die Zweitmeinung ist eine unabhängige Empfehlung. Sie ersetzt nicht die Behandlung durch Ihre Ärzt:innen, sondern ergänzt sie. Die Entscheidung liegt immer bei Ihnen.", category: "allgemein" }, const categories = categorySlugs.map((slug: string) => ({
{ question: "Wann ist eine Zweitmeinung vor einem Herzkatheter sinnvoll?", answer: "Immer dann, wenn ein planbarer Eingriff wie eine PCI (Stent) oder eine OP empfohlen wurde. Auch bei Unsicherheit über Nutzen und Risiken ist eine Zweitmeinung sinnvoll.", category: "kardiologie" }, name: categoryNames[slug] || slug,
{ question: "Wer erstellt die kardiologische Zweitmeinung?", answer: "Ausschließlich erfahrene, unabhängige Fachärzt:innen für Kardiologie mit Leitlinienkompetenz und klinischer Erfahrung.", category: "kardiologie" }, slug,
{ question: "Welche Unterlagen brauche ich für die kardiologische Zweitmeinung?", answer: "Ideal sind aktuelle Befunde, EKGs, Herzkatheterberichte, Medikamente und ggf. OP-Empfehlungen. Wir helfen Ihnen gern bei der Beschaffung und Sichtung der Unterlagen.", category: "kardiologie" }, }))
{ question: "Muss ich den Eingriff absagen, wenn ich eine Zweitmeinung einhole?", answer: "Nein. Die Zweitmeinung führt Ihren bestehenden Entscheidungsprozess nicht in Verzug. Sie entscheiden selbst, wie Sie mit dem Ergebnis umgehen.", category: "kardiologie" },
{ question: "Was passiert, wenn die Einschätzung abweicht?", answer: "Dann besprechen wir mit Ihnen nachvollziehbar, warum das so ist - z.B. weil die Leitlinien einen anderen Weg vorsehen oder weil Ihre individuelle Situation neu bewertet wurde.", category: "kardiologie" }, // Transform for client component
{ question: "Wann ist eine Zweitmeinung zur Schilddrüsen-OP sinnvoll?", answer: "Bei empfohlener OP wegen Knoten, Struma, Autonomie oder unklarer Laborwerte.", category: "schilddruese" }, const faqItems = faqs.map((f) => ({
{ question: "Welche Unterlagen werden für die Schilddrüsen-Zweitmeinung benötigt?", answer: "Ultraschallbefunde, Szintigramme, Laborwerte (TSH, fT3, fT4) und ggf. Feinnadelpunktionsergebnisse.", category: "schilddruese" }, question: f.question,
{ question: "Wer erstellt die Schilddrüsen-Zweitmeinung?", answer: "Fachärzt:innen für Endokrinologie oder Schilddrüsenchirurgie mit langjähriger Erfahrung.", category: "schilddruese" }, answer: f.answer,
{ question: "Ist die Zweitmeinung verbindlich?", answer: "Nein, die Zweitmeinung ist eine unabhängige Empfehlung. Sie sind nicht verpflichtet, ihr zu folgen.", category: "schilddruese" }, answerText: richTextToPlainText(f.answer as any),
{ question: "Kostet die Schilddrüsen-Zweitmeinung etwas?", answer: "Für gesetzlich Versicherte ist die Zweitmeinung in vielen Fällen kostenfrei. Sprechen Sie uns an.", category: "schilddruese" }, category: f.category || "allgemein",
{ question: "Wann ist eine Zweitmeinung zur Gallenblasenentfernung sinnvoll?", answer: "Wenn eine Cholezystektomie empfohlen wurde und Sie unsicher sind, ob die OP tatsächlich notwendig ist.", category: "gallenblase" }, }))
{ question: "Wer erstellt die Gallenblase-Zweitmeinung?", answer: "Fachärzt:innen für Viszeralchirurgie oder Gastroenterologie.", category: "gallenblase" },
{ question: "Welche Unterlagen brauche ich für die Gallenblase-Zweitmeinung?", answer: "Ultraschallbefunde, Laborbefunde und die OP-Empfehlung Ihres behandelnden Arztes.", category: "gallenblase" },
{ question: "Gibt es Alternativen zur Gallenblasen-OP?", answer: "In manchen Fällen ja - etwa medikamentöse Therapie oder abwartendes Beobachten. Das hängt von Ihren individuellen Befunden ab.", category: "gallenblase" },
{ question: "Was kostet die Gallenblase-Zweitmeinung?", answer: "Sprechen Sie uns an - in vielen Fällen übernimmt Ihre Krankenkasse die Kosten.", category: "gallenblase" },
{ question: "Wann ist eine Zweitmeinung bei Nierenerkrankungen sinnvoll?", answer: "Bei Diagnose einer chronischen Niereninsuffizienz, Empfehlung zur Dialyse oder vor einer Transplantation.", category: "nephrologie" },
{ question: "Welche Unterlagen sind für die nephrologische Zweitmeinung wichtig?", answer: "Laborwerte (Kreatinin, GFR, Harnstoff), Urinbefunde, Ultraschall und bisherige Behandlungsdokumentation.", category: "nephrologie" },
{ question: "Kann ich die Zweitmeinung auch einholen, wenn die Dialyse bereits begonnen hat?", answer: "Ja, auch bei laufender Dialyse kann eine Zweitmeinung sinnvoll sein - etwa zur Prüfung von Therapiealternativen.", category: "nephrologie" },
{ question: "Kostet die nephrologische Zweitmeinung etwas?", answer: "In vielen Fällen wird die Zweitmeinung von Ihrer Krankenkasse übernommen. Wir beraten Sie gerne.", category: "nephrologie" },
]
export default function FAQPage() {
return ( return (
<> <>
{/* Hero */} {/* Hero */}
@ -65,7 +57,7 @@ export default function FAQPage() {
</section> </section>
{/* FAQ Content (Client Component for search/filter) */} {/* FAQ Content (Client Component for search/filter) */}
<FAQClient categories={categories} faqs={faqData} /> <FAQClient categories={categories} faqs={faqItems} />
{/* CTA */} {/* CTA */}
<section className="py-20 bg-navy"> <section className="py-20 bg-navy">
@ -106,10 +98,10 @@ export default function FAQPage() {
__html: JSON.stringify({ __html: JSON.stringify({
"@context": "https://schema.org", "@context": "https://schema.org",
"@type": "FAQPage", "@type": "FAQPage",
mainEntity: faqData.map((faq) => ({ mainEntity: faqItems.map((faq) => ({
"@type": "Question", "@type": "Question",
name: faq.question, name: faq.question,
acceptedAnswer: { "@type": "Answer", text: faq.answer }, acceptedAnswer: { "@type": "Answer", text: faq.answerText },
})), })),
}), }),
}} }}

View file

@ -2,13 +2,24 @@ import type { Metadata } from "next"
import { Container } from "@/components/ui/Container" import { Container } from "@/components/ui/Container"
import { ContactForm } from "@/components/contact/ContactForm" import { ContactForm } from "@/components/contact/ContactForm"
import { Phone, Mail, MapPin, Clock, Printer } from "lucide-react" import { Phone, Mail, MapPin, Clock, Printer } from "lucide-react"
import { getSiteSettings } from "@/lib/api"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Kontakt", title: "Kontakt",
description: "Kontaktieren Sie uns für eine kostenlose Erstberatung. Telefon: 0800 80 44 100.", description: "Kontaktieren Sie uns für eine kostenlose Erstberatung. Telefon: 0800 80 44 100.",
} }
export default function KontaktPage() { export default async function KontaktPage() {
const settings = await getSiteSettings()
const phone = settings?.contact?.phone || "0800 80 44 100"
const email = settings?.contact?.email || "kontakt@zweitmeinu.ng"
const fax = settings?.contact?.fax || "0800 80 44 190"
const street = settings?.address?.street || "Hans-Böckler-Str. 19"
const zip = settings?.address?.zip || "46236"
const city = settings?.address?.city || "Bottrop"
const phoneHref = `tel:${phone.replace(/[\s/]/g, "")}`
return ( return (
<> <>
{/* Hero */} {/* Hero */}
@ -36,16 +47,13 @@ export default function KontaktPage() {
So erreichen Sie uns So erreichen Sie uns
</h2> </h2>
<div className="space-y-5"> <div className="space-y-5">
<a <a href={phoneHref} className="flex items-start gap-4 group">
href="tel:+4980080441000"
className="flex items-start gap-4 group"
>
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0 group-hover:bg-primary group-hover:text-white transition-colors"> <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0 group-hover:bg-primary group-hover:text-white transition-colors">
<Phone className="h-5 w-5 text-primary group-hover:text-white" /> <Phone className="h-5 w-5 text-primary group-hover:text-white" />
</div> </div>
<div> <div>
<div className="font-semibold text-navy">Telefon (kostenlos)</div> <div className="font-semibold text-navy">Telefon (kostenlos)</div>
<div className="text-primary font-bold">0800 80 44 100</div> <div className="text-primary font-bold">{phone}</div>
</div> </div>
</a> </a>
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
@ -54,19 +62,16 @@ export default function KontaktPage() {
</div> </div>
<div> <div>
<div className="font-semibold text-navy">Telefax</div> <div className="font-semibold text-navy">Telefax</div>
<div className="text-text-muted">0800 80 44 190</div> <div className="text-text-muted">{fax}</div>
</div> </div>
</div> </div>
<a <a href={`mailto:${email}`} className="flex items-start gap-4 group">
href="mailto:kontakt@zweitmeinu.ng"
className="flex items-start gap-4 group"
>
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0 group-hover:bg-primary group-hover:text-white transition-colors"> <div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center shrink-0 group-hover:bg-primary group-hover:text-white transition-colors">
<Mail className="h-5 w-5 text-primary group-hover:text-white" /> <Mail className="h-5 w-5 text-primary group-hover:text-white" />
</div> </div>
<div> <div>
<div className="font-semibold text-navy">E-Mail</div> <div className="font-semibold text-navy">E-Mail</div>
<div className="text-primary">kontakt@zweitmeinu.ng</div> <div className="text-primary">{email}</div>
</div> </div>
</a> </a>
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
@ -77,8 +82,8 @@ export default function KontaktPage() {
<div className="font-semibold text-navy">Adresse</div> <div className="font-semibold text-navy">Adresse</div>
<div className="text-text-muted text-sm"> <div className="text-text-muted text-sm">
complex care solutions GmbH<br /> complex care solutions GmbH<br />
Hans-Böckler-Str. 19<br /> {street}<br />
46236 Bottrop {zip} {city}
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,6 +1,6 @@
import type { Metadata } from "next" import type { Metadata } from "next"
import { TopBar, Header, Footer, EmergencyBanner } from "@/components/layout" import { TopBar, Header, Footer, EmergencyBanner } from "@/components/layout"
import { getNavigation, getSiteSettings, getSocialLinks } from "@/lib/api" import { getNavigation, getSiteSettings, getSocialLinks, getServices } from "@/lib/api"
import "./globals.css" import "./globals.css"
export const metadata: Metadata = { export const metadata: Metadata = {
@ -25,12 +25,19 @@ export default async function RootLayout({
}: { }: {
children: React.ReactNode children: React.ReactNode
}) { }) {
const [navigation, settings, socialLinks] = await Promise.all([ const [navigation, settings, socialLinks, services] = await Promise.all([
getNavigation(), getNavigation(),
getSiteSettings(), getSiteSettings(),
getSocialLinks(), getSocialLinks(),
getServices(),
]) ])
const headerServices = services.map((s) => ({
title: s.title,
slug: s.slug,
icon: s.icon ?? null,
}))
return ( return (
<html lang="de"> <html lang="de">
<body className="font-body antialiased"> <body className="font-body antialiased">
@ -41,13 +48,13 @@ export default async function RootLayout({
Zum Hauptinhalt springen Zum Hauptinhalt springen
</a> </a>
<div className="flex min-h-screen flex-col"> <div className="flex min-h-screen flex-col">
<TopBar /> <TopBar settings={settings} />
<Header mainMenu={navigation?.mainMenu ?? null} /> <Header mainMenu={navigation?.mainMenu ?? null} services={headerServices} />
<main id="main-content" className="flex-1"> <main id="main-content" className="flex-1">
{children} {children}
</main> </main>
<EmergencyBanner /> <EmergencyBanner settings={settings} />
<Footer socialLinks={socialLinks} /> <Footer socialLinks={socialLinks} settings={settings} />
</div> </div>
</body> </body>
</html> </html>

View file

@ -3,10 +3,12 @@
import { useState, useMemo } from "react" import { useState, useMemo } from "react"
import { Search, ChevronDown } from "lucide-react" import { Search, ChevronDown } from "lucide-react"
import { Container } from "@/components/ui/Container" import { Container } from "@/components/ui/Container"
import { RichTextRenderer } from "@/components/ui/RichTextRenderer"
interface FAQ { interface FAQ {
question: string question: string
answer: string answer: unknown
answerText: string
category: string category: string
} }
@ -35,7 +37,7 @@ export function FAQClient({ categories, faqs }: FAQClientProps) {
items = items.filter( items = items.filter(
(f) => (f) =>
f.question.toLowerCase().includes(q) || f.question.toLowerCase().includes(q) ||
f.answer.toLowerCase().includes(q), f.answerText.toLowerCase().includes(q),
) )
} }
return items return items
@ -142,7 +144,7 @@ export function FAQClient({ categories, faqs }: FAQClientProps) {
</button> </button>
{openItems.has(i) && ( {openItems.has(i) && (
<div className="px-6 pb-5 text-text-muted text-sm leading-relaxed border-t border-border pt-4"> <div className="px-6 pb-5 text-text-muted text-sm leading-relaxed border-t border-border pt-4">
{faq.answer} <RichTextRenderer content={faq.answer as any} />
</div> </div>
)} )}
</div> </div>

View file

@ -1,53 +1,12 @@
import Link from "next/link" import Link from "next/link"
import { ArrowRight, Heart, Brain, Stethoscope, Pill, FlaskConical, Activity } from "lucide-react" import { ArrowRight } from "lucide-react"
import { Container } from "@/components/ui/Container" import { Container } from "@/components/ui/Container"
import { getServices } from "@/lib/api"
import { getLucideIcon } from "@/lib/icon-map"
const services = [ export async function ServiceOverview() {
{ const services = await getServices()
title: "Intensivmedizin",
slug: "zweitmeinung-intensivmedizin",
description: "Unabhängige Zweitmeinung bei laufender oder geplanter Intensivbehandlung.",
icon: Activity,
category: "Notfall",
},
{
title: "Kardiologie",
slug: "zweitmeinung-kardiologie",
description: "Fundierte Empfehlung vor Herzkatheter, Stent oder kardiologischer OP.",
icon: Heart,
category: "Beratung",
},
{
title: "Onkologie",
slug: "zweitmeinung-onkologie",
description: "Unabhängige Einschätzung bei Krebsdiagnosen und Therapieoptionen.",
icon: FlaskConical,
category: "Beratung",
},
{
title: "Nephrologie",
slug: "zweitmeinung-nephrologie",
description: "Ärztliche Einschätzung bei Nierenerkrankungen und Dialyseempfehlung.",
icon: Stethoscope,
category: "Beratung",
},
{
title: "Gallenblase",
slug: "zweitmeinung-gallenblase",
description: "Zweitmeinung vor geplanter Gallenblasen-OP ist sie wirklich nötig?",
icon: Pill,
category: "Beratung",
},
{
title: "Schilddrüse",
slug: "zweitmeinung-schilddruese",
description: "Fundierte Einschätzung vor einer geplanten Schilddrüsen-Operation.",
icon: Brain,
category: "Beratung",
},
]
export function ServiceOverview() {
return ( return (
<section className="py-20 bg-bg"> <section className="py-20 bg-bg">
<Container> <Container>
@ -56,38 +15,45 @@ export function ServiceOverview() {
Unsere Fachbereiche Unsere Fachbereiche
</p> </p>
<h2 className="text-3xl sm:text-4xl font-bold text-navy"> <h2 className="text-3xl sm:text-4xl font-bold text-navy">
Zweitmeinung in 6 Fachbereichen Zweitmeinung in {services.length} Fachbereichen
</h2> </h2>
</div> </div>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
{services.map((s) => ( {services.map((s) => {
<Link const Icon = getLucideIcon(s.icon)
key={s.slug} const categoryName = typeof s.category === "object" && s.category
href={`/fachbereiche/${s.slug}`} ? s.category.name
className="group bg-white rounded-xl p-6 border border-border hover:border-primary/30 hover:shadow-lg transition-all" : null
>
<div className="flex items-start justify-between mb-4"> return (
<div className="w-12 h-12 rounded-xl bg-primary/10 text-primary flex items-center justify-center group-hover:bg-primary group-hover:text-white transition-colors"> <Link
<s.icon className="h-6 w-6" /> key={s.slug}
href={`/fachbereiche/${s.slug}`}
className="group bg-white rounded-xl p-6 border border-border hover:border-primary/30 hover:shadow-lg transition-all"
>
<div className="flex items-start justify-between mb-4">
<div className="w-12 h-12 rounded-xl bg-primary/10 text-primary flex items-center justify-center group-hover:bg-primary group-hover:text-white transition-colors">
<Icon className="h-6 w-6" />
</div>
{categoryName === "Notfall" && (
<span className="text-xs font-bold bg-gold/20 text-gold-hover px-2 py-1 rounded-full">
24/7
</span>
)}
</div> </div>
{s.category === "Notfall" && ( <h3 className="font-bold text-lg mb-2 text-navy group-hover:text-primary transition-colors">
<span className="text-xs font-bold bg-gold/20 text-gold-hover px-2 py-1 rounded-full"> {s.title}
24/7 </h3>
</span> <p className="text-text-muted text-sm leading-relaxed mb-4">
)} {s.shortDescription}
</div> </p>
<h3 className="font-bold text-lg mb-2 text-navy group-hover:text-primary transition-colors"> <span className="inline-flex items-center gap-1 text-sm font-semibold text-primary">
Zweitmeinung {s.title} Mehr erfahren
</h3> <ArrowRight className="h-4 w-4 group-hover:translate-x-1 transition-transform" />
<p className="text-text-muted text-sm leading-relaxed mb-4"> </span>
{s.description} </Link>
</p> )
<span className="inline-flex items-center gap-1 text-sm font-semibold text-primary"> })}
Mehr erfahren
<ArrowRight className="h-4 w-4 group-hover:translate-x-1 transition-transform" />
</span>
</Link>
))}
</div> </div>
</Container> </Container>
</section> </section>

View file

@ -1,6 +1,14 @@
import { Phone } from "lucide-react" import { Phone } from "lucide-react"
import type { SiteSetting } from "@/lib/api"
interface EmergencyBannerProps {
settings?: SiteSetting | null
}
export function EmergencyBanner({ settings }: EmergencyBannerProps) {
const phone = settings?.contact?.phone || "0800 80 44 100"
const phoneHref = `tel:${phone.replace(/[\s/]/g, "")}`
export function EmergencyBanner() {
return ( return (
<div className="bg-gold"> <div className="bg-gold">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4"> <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-4">
@ -9,7 +17,7 @@ export function EmergencyBanner() {
Medizinischer Notfall? Medizinischer Notfall?
</span> </span>
<a <a
href="tel:+4980080441000" href={phoneHref}
className="inline-flex items-center gap-2 bg-navy-dark hover:bg-navy text-white font-bold px-6 py-2.5 rounded-lg transition-colors" className="inline-flex items-center gap-2 bg-navy-dark hover:bg-navy text-white font-bold px-6 py-2.5 rounded-lg transition-colors"
> >
<Phone className="h-4 w-4" /> <Phone className="h-4 w-4" />

View file

@ -1,10 +1,11 @@
import Link from "next/link" import Link from "next/link"
import { Phone, Mail, MapPin, Clock, Shield, Lock } from "lucide-react" import { Shield, Lock } from "lucide-react"
import type { SocialLink } from "@c2s/payload-contracts/types" import type { SocialLink, SiteSetting } from "@/lib/api"
import { socialLinksToMap } from "@/lib/payload-helpers" import { socialLinksToMap } from "@/lib/payload-helpers"
interface FooterProps { interface FooterProps {
socialLinks?: SocialLink[] socialLinks?: SocialLink[]
settings?: SiteSetting | null
} }
const footerColumns = [ const footerColumns = [
@ -45,8 +46,10 @@ const footerColumns = [
}, },
] ]
export function Footer({ socialLinks }: FooterProps) { export function Footer({ socialLinks, settings }: FooterProps) {
const socialMap = socialLinksToMap(socialLinks) const socialMap = socialLinksToMap(socialLinks)
const copyright = settings?.footer?.copyrightText ||
`\u00A9 ${new Date().getFullYear()} complex care solutions GmbH. Alle Rechte vorbehalten.`
return ( return (
<footer className="bg-navy text-white"> <footer className="bg-navy text-white">
@ -142,9 +145,7 @@ export function Footer({ socialLinks }: FooterProps) {
</a> </a>
)} )}
</div> </div>
<p className="text-xs text-white/40"> <p className="text-xs text-white/40">{copyright}</p>
&copy; {new Date().getFullYear()} complex care solutions GmbH. Alle Rechte vorbehalten.
</p>
</div> </div>
</div> </div>
</div> </div>

View file

@ -8,16 +8,17 @@ import type { Navigation } from "@c2s/payload-contracts/types"
interface HeaderProps { interface HeaderProps {
mainMenu: Navigation["mainMenu"] | null mainMenu: Navigation["mainMenu"] | null
services?: Array<{ title: string; slug: string; icon: string | null }>
} }
const services = [ const emojiMap: Record<string, string> = {
{ name: "Intensivmedizin", slug: "zweitmeinung-intensivmedizin", icon: "🏥" }, activity: "🏥",
{ name: "Kardiologie", slug: "zweitmeinung-kardiologie", icon: "❤️" }, heart: "❤️",
{ name: "Onkologie", slug: "zweitmeinung-onkologie", icon: "🔬" }, "flask-conical": "🔬",
{ name: "Nephrologie", slug: "zweitmeinung-nephrologie", icon: "🫘" }, stethoscope: "🫘",
{ name: "Gallenblase", slug: "zweitmeinung-gallenblase", icon: "💊" }, pill: "💊",
{ name: "Schilddrüse", slug: "zweitmeinung-schilddruese", icon: "🦋" }, brain: "🦋",
] }
const navLinks = [ const navLinks = [
{ label: "So funktioniert's", href: "/so-funktionierts" }, { label: "So funktioniert's", href: "/so-funktionierts" },
@ -27,7 +28,7 @@ const navLinks = [
{ label: "Kontakt", href: "/kontakt" }, { label: "Kontakt", href: "/kontakt" },
] ]
export function Header({ mainMenu }: HeaderProps) { export function Header({ mainMenu, services = [] }: HeaderProps) {
const [mobileOpen, setMobileOpen] = useState(false) const [mobileOpen, setMobileOpen] = useState(false)
const [megaOpen, setMegaOpen] = useState(false) const [megaOpen, setMegaOpen] = useState(false)
const [scrolled, setScrolled] = useState(false) const [scrolled, setScrolled] = useState(false)
@ -38,6 +39,12 @@ export function Header({ mainMenu }: HeaderProps) {
return () => window.removeEventListener("scroll", onScroll) return () => window.removeEventListener("scroll", onScroll)
}, []) }, [])
const megaServices = services.map((s) => ({
name: s.title.replace(/^Zweitmeinung\s+/, ""),
slug: s.slug,
icon: emojiMap[s.icon || ""] || "📋",
}))
return ( return (
<> <>
<header <header
@ -81,7 +88,7 @@ export function Header({ mainMenu }: HeaderProps) {
<div className="absolute top-full left-0 pt-2 w-72"> <div className="absolute top-full left-0 pt-2 w-72">
<div className="bg-white rounded-xl shadow-2xl border border-gray-100 overflow-hidden"> <div className="bg-white rounded-xl shadow-2xl border border-gray-100 overflow-hidden">
<div className="p-2"> <div className="p-2">
{services.map((s) => ( {megaServices.map((s) => (
<Link <Link
key={s.slug} key={s.slug}
href={`/fachbereiche/${s.slug}`} href={`/fachbereiche/${s.slug}`}
@ -142,7 +149,7 @@ export function Header({ mainMenu }: HeaderProps) {
<MobileMenu <MobileMenu
open={mobileOpen} open={mobileOpen}
onClose={() => setMobileOpen(false)} onClose={() => setMobileOpen(false)}
services={services} services={megaServices}
navLinks={navLinks} navLinks={navLinks}
/> />
</> </>

View file

@ -1,25 +1,34 @@
import { Phone, Mail, Clock, AlertTriangle } from "lucide-react" import { Phone, Mail, Clock, AlertTriangle } from "lucide-react"
import type { SiteSetting } from "@/lib/api"
interface TopBarProps {
settings?: SiteSetting | null
}
export function TopBar({ settings }: TopBarProps) {
const phone = settings?.contact?.phone || "0800 80 44 100"
const email = settings?.contact?.email || "kontakt@zweitmeinu.ng"
const phoneHref = `tel:${phone.replace(/[\s/]/g, "")}`
export function TopBar() {
return ( return (
<div className="bg-navy text-white text-sm"> <div className="bg-navy text-white text-sm">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8"> <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-10"> <div className="flex items-center justify-between h-10">
<div className="flex items-center gap-4 sm:gap-6"> <div className="flex items-center gap-4 sm:gap-6">
<a <a
href="tel:+4980080441000" href={phoneHref}
className="flex items-center gap-1.5 hover:text-gold transition-colors" className="flex items-center gap-1.5 hover:text-gold transition-colors"
> >
<Phone className="h-3.5 w-3.5" /> <Phone className="h-3.5 w-3.5" />
<span className="font-semibold">+49 800 80 44 100</span> <span className="font-semibold">{phone}</span>
</a> </a>
<span className="hidden sm:inline text-white/30">|</span> <span className="hidden sm:inline text-white/30">|</span>
<a <a
href="mailto:kontakt@zweitmeinu.ng" href={`mailto:${email}`}
className="hidden sm:flex items-center gap-1.5 hover:text-gold transition-colors" className="hidden sm:flex items-center gap-1.5 hover:text-gold transition-colors"
> >
<Mail className="h-3.5 w-3.5" /> <Mail className="h-3.5 w-3.5" />
<span>kontakt@zweitmeinu.ng</span> <span>{email}</span>
</a> </a>
</div> </div>
<div className="flex items-center gap-4 text-xs"> <div className="flex items-center gap-4 text-xs">

View file

@ -0,0 +1,168 @@
import Link from "next/link"
import Image from "next/image"
interface RichTextRendererProps {
content?: { root?: { children?: Record<string, unknown>[] } } | null
className?: string
}
export function RichTextRenderer({ content, className }: RichTextRendererProps) {
if (!content?.root?.children) return null
return (
<div className={className}>
{content.root.children.map((node, index) => (
<RenderNode key={index} node={node} />
))}
</div>
)
}
function RenderNode({ node }: { node: Record<string, unknown> }) {
const type = node.type as string
const children = node.children as Record<string, unknown>[] | undefined
switch (type) {
case "paragraph":
return (
<p className="mb-4 last:mb-0">
{children?.map((child, i) => <RenderNode key={i} node={child} />)}
</p>
)
case "heading": {
const tag = (node.tag as string) || "h2"
const Tag = tag as "h1" | "h2" | "h3" | "h4" | "h5" | "h6"
const classes: Record<string, string> = {
h1: "text-3xl sm:text-4xl font-bold text-navy mb-6",
h2: "text-2xl sm:text-3xl font-bold text-navy mb-4",
h3: "text-xl sm:text-2xl font-bold text-navy mb-3",
h4: "text-lg sm:text-xl font-bold text-navy mb-3",
h5: "text-base sm:text-lg font-semibold text-navy mb-2",
h6: "text-sm sm:text-base font-semibold text-navy mb-2",
}
return (
<Tag className={classes[tag] || classes.h2}>
{children?.map((child, i) => <RenderNode key={i} node={child} />)}
</Tag>
)
}
case "list": {
const Tag = node.listType === "number" ? "ol" : "ul"
return (
<Tag
className={
Tag === "ol"
? "list-decimal list-inside mb-4 space-y-1.5"
: "list-disc list-inside mb-4 space-y-1.5"
}
>
{children?.map((child, i) => <RenderNode key={i} node={child} />)}
</Tag>
)
}
case "listitem":
return (
<li>
{children?.map((child, i) => <RenderNode key={i} node={child} />)}
</li>
)
case "link": {
const url = node.url as string
const newTab = node.newTab as boolean
if (node.linkType === "internal") {
return (
<Link
href={url || "/"}
className="text-primary hover:text-primary-dark underline underline-offset-2 transition-colors"
>
{children?.map((child, i) => <RenderNode key={i} node={child} />)}
</Link>
)
}
return (
<a
href={url}
target={newTab ? "_blank" : undefined}
rel={newTab ? "noopener noreferrer" : undefined}
className="text-primary hover:text-primary-dark underline underline-offset-2 transition-colors"
>
{children?.map((child, i) => <RenderNode key={i} node={child} />)}
</a>
)
}
case "upload": {
const value = node.value as { url?: string; alt?: string; width?: number; height?: number } | undefined
if (!value?.url) return null
return (
<figure className="my-6">
<Image
src={value.url}
alt={value.alt || ""}
width={value.width || 800}
height={value.height || 600}
className="rounded-lg"
/>
</figure>
)
}
case "quote":
return (
<blockquote className="border-l-4 border-primary pl-6 my-6 italic text-lg text-text-muted">
{children?.map((child, i) => <RenderNode key={i} node={child} />)}
</blockquote>
)
case "text": {
const text = node.text as string
if (!text) return null
const format = node.format as number | undefined
let element: React.ReactNode = text
if (format) {
if (format & 1) element = <strong>{element}</strong>
if (format & 2) element = <em>{element}</em>
if (format & 8) element = <u>{element}</u>
if (format & 4) element = <s>{element}</s>
if (format & 16) element = <code className="bg-gray-100 px-1 py-0.5 rounded text-sm">{element}</code>
}
return <>{element}</>
}
case "linebreak":
return <br />
default:
if (children) {
return <>{children.map((child, i) => <RenderNode key={i} node={child} />)}</>
}
return null
}
}
/** Extract plain text from Lexical JSON (for search, Schema.org, etc.) */
export function richTextToPlainText(content: { root?: { children?: Record<string, unknown>[] } } | null | undefined): string {
if (!content?.root?.children) return ""
return extractText(content.root.children).trim()
}
function extractText(nodes: Record<string, unknown>[]): string {
let text = ""
for (const node of nodes) {
if (node.type === "text") {
text += (node.text as string) || ""
} else if (node.type === "linebreak") {
text += "\n"
} else if (node.children) {
text += extractText(node.children as Record<string, unknown>[])
if (node.type === "paragraph" || node.type === "heading" || node.type === "listitem") {
text += " "
}
}
}
return text
}

25
src/lib/icon-map.ts Normal file
View file

@ -0,0 +1,25 @@
import {
Activity, Heart, FlaskConical, Stethoscope, Pill, Brain,
Shield, FileCheck, Users, HeartHandshake, Clock, CheckCircle,
type LucideIcon,
} from "lucide-react"
const iconMap: Record<string, LucideIcon> = {
activity: Activity,
heart: Heart,
"flask-conical": FlaskConical,
stethoscope: Stethoscope,
pill: Pill,
brain: Brain,
shield: Shield,
"file-check": FileCheck,
users: Users,
"heart-handshake": HeartHandshake,
clock: Clock,
"check-circle": CheckCircle,
}
export function getLucideIcon(name: string | null | undefined): LucideIcon {
if (!name) return Activity
return iconMap[name] ?? Activity
}