diff --git a/ecosystem.config.js b/ecosystem.config.js index 093a2c2..5150f55 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -1,7 +1,7 @@ module.exports = { apps: [{ name: 'zweitmeinu.ng', - script: 'node_modules/.bin/next', + script: 'node_modules/next/dist/bin/next', args: 'start', cwd: '/home/frontend/frontend.zweitmeinu.ng', env: { diff --git a/src/app/fachbereiche/[slug]/page.tsx b/src/app/fachbereiche/[slug]/page.tsx index 59f1a7d..10f00e0 100644 --- a/src/app/fachbereiche/[slug]/page.tsx +++ b/src/app/fachbereiche/[slug]/page.tsx @@ -2,174 +2,14 @@ import type { Metadata } from "next" import { notFound } from "next/navigation" import { Container } from "@/components/ui/Container" import { Button } from "@/components/ui/Button" -import { - Phone, Mail, ArrowRight, Check, Heart, Brain, - Stethoscope, Pill, FlaskConical, Activity, Shield, - FileCheck, Users, Clock, HeartHandshake, -} from "lucide-react" - -// Static service data — will be enhanced with CMS data when populated -const servicesData: Record - 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) +import { RichTextRenderer } from "@/components/ui/RichTextRenderer" +import { getLucideIcon } from "@/lib/icon-map" +import { getServices, getServiceBySlug } from "@/lib/api" +import { Phone, Mail, ArrowRight, Check } from "lucide-react" export async function generateStaticParams() { - return slugs.map((slug) => ({ slug })) + const services = await getServices() + return services.map((s) => ({ slug: s.slug })) } export async function generateMetadata({ @@ -178,9 +18,12 @@ export async function generateMetadata({ params: Promise<{ slug: string }> }): Promise { const { slug } = await params - const service = servicesData[slug] + const service = await getServiceBySlug(slug) 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({ @@ -189,10 +32,14 @@ export default async function ServiceDetailPage({ params: Promise<{ slug: string }> }) { const { slug } = await params - const service = servicesData[slug] + const service = await getServiceBySlug(slug) 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 ( <> @@ -204,13 +51,10 @@ export default async function ServiceDetailPage({

- Fachbereiche / {service.title} + Fachbereiche / {shortTitle}

- {service.fullTitle} – klare Empfehlungen bei{" "} - {service.title === "Intensivmedizin" - ? "kritischen Entscheidungen" - : `${service.title}-Entscheidungen`} + {service.title}

{service.shortDescription} @@ -229,59 +73,77 @@ export default async function ServiceDetailPage({ - {/* When is a second opinion useful */} -

- -

- Wann ist eine Zweitmeinung sinnvoll? -

-

- {service.description[0]} -

-
- {service.benefits.map((b) => ( -
- -

{b.title}

-

{b.description}

-
- ))} -
-
-
+ {/* Benefits grid from features[] */} + {service.features && service.features.length > 0 && ( +
+ +

+ Wann ist eine Zweitmeinung sinnvoll? +

+

+ {service.shortDescription} +

+
+ {service.features.map((f) => { + const FeatureIcon = getLucideIcon(f.icon) + return ( +
+ +

{f.title}

+

{f.description}

+
+ ) + })} +
+
+
+ )} - {/* What we do for you */} -
- -

- Was wir für Sie tun -

-

- {service.description[1]} -

-
- {service.checklist.map((item) => ( -
-
- + {/* Checklist from detailSections[] */} + {service.detailSections && service.detailSections.length > 0 && ( +
+ +

+ Was wir für Sie tun +

+

+ Unser Leistungsumfang im Bereich {shortTitle} +

+
+ {service.detailSections.map((item) => ( +
+
+ +
+
+

{item.title}

+
+ +
+
-
-

{item.title}

-

- {item.description} -

-
-
- ))} -
- -
+ ))} + + + + )} + + {/* Full description (richText) */} + {service.description && ( +
+ +
+ +
+
+
+ )} {/* Stats */}
@@ -299,9 +161,7 @@ export default async function ServiceDetailPage({ 500+

- {service.title === "Kardiologie" - ? "Kardiologische Zweitmeinungen" - : "Medizinische Zweitmeinungen"} + Medizinische Zweitmeinungen

@@ -330,11 +190,11 @@ export default async function ServiceDetailPage({

- 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?

Kontaktieren Sie uns für eine unabhängige, professionelle - Einschätzung. + Einschätzung im Bereich {shortTitle}.

{/* FAQ Content (Client Component for search/filter) */} - + {/* CTA */}
@@ -106,10 +98,10 @@ export default function FAQPage() { __html: JSON.stringify({ "@context": "https://schema.org", "@type": "FAQPage", - mainEntity: faqData.map((faq) => ({ + mainEntity: faqItems.map((faq) => ({ "@type": "Question", name: faq.question, - acceptedAnswer: { "@type": "Answer", text: faq.answer }, + acceptedAnswer: { "@type": "Answer", text: faq.answerText }, })), }), }} diff --git a/src/app/kontakt/page.tsx b/src/app/kontakt/page.tsx index 1e4560a..76c0f07 100644 --- a/src/app/kontakt/page.tsx +++ b/src/app/kontakt/page.tsx @@ -2,13 +2,24 @@ import type { Metadata } from "next" import { Container } from "@/components/ui/Container" import { ContactForm } from "@/components/contact/ContactForm" import { Phone, Mail, MapPin, Clock, Printer } from "lucide-react" +import { getSiteSettings } from "@/lib/api" export const metadata: Metadata = { title: "Kontakt", 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 ( <> {/* Hero */} @@ -36,16 +47,13 @@ export default function KontaktPage() { So erreichen Sie uns
- +
Telefon (kostenlos)
-
0800 80 44 100
+
{phone}
@@ -54,19 +62,16 @@ export default function KontaktPage() {
Telefax
-
0800 80 44 190
+
{fax}
- +
E-Mail
-
kontakt@zweitmeinu.ng
+
{email}
@@ -77,8 +82,8 @@ export default function KontaktPage() {
Adresse
complex care solutions GmbH
- Hans-Böckler-Str. 19
- 46236 Bottrop + {street}
+ {zip} {city}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4ba2aaf..97f23ee 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,6 @@ import type { Metadata } from "next" 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" export const metadata: Metadata = { @@ -25,12 +25,19 @@ export default async function RootLayout({ }: { children: React.ReactNode }) { - const [navigation, settings, socialLinks] = await Promise.all([ + const [navigation, settings, socialLinks, services] = await Promise.all([ getNavigation(), getSiteSettings(), getSocialLinks(), + getServices(), ]) + const headerServices = services.map((s) => ({ + title: s.title, + slug: s.slug, + icon: s.icon ?? null, + })) + return ( @@ -41,13 +48,13 @@ export default async function RootLayout({ Zum Hauptinhalt springen
- -
+ +
{children}
- -
+ +
diff --git a/src/components/faq/FAQClient.tsx b/src/components/faq/FAQClient.tsx index 714a980..a09faa8 100644 --- a/src/components/faq/FAQClient.tsx +++ b/src/components/faq/FAQClient.tsx @@ -3,10 +3,12 @@ import { useState, useMemo } from "react" import { Search, ChevronDown } from "lucide-react" import { Container } from "@/components/ui/Container" +import { RichTextRenderer } from "@/components/ui/RichTextRenderer" interface FAQ { question: string - answer: string + answer: unknown + answerText: string category: string } @@ -35,7 +37,7 @@ export function FAQClient({ categories, faqs }: FAQClientProps) { items = items.filter( (f) => f.question.toLowerCase().includes(q) || - f.answer.toLowerCase().includes(q), + f.answerText.toLowerCase().includes(q), ) } return items @@ -142,7 +144,7 @@ export function FAQClient({ categories, faqs }: FAQClientProps) { {openItems.has(i) && (
- {faq.answer} +
)} diff --git a/src/components/home/ServiceOverview.tsx b/src/components/home/ServiceOverview.tsx index cedcc1b..6b635e9 100644 --- a/src/components/home/ServiceOverview.tsx +++ b/src/components/home/ServiceOverview.tsx @@ -1,53 +1,12 @@ 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 { getServices } from "@/lib/api" +import { getLucideIcon } from "@/lib/icon-map" -const services = [ - { - 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 async function ServiceOverview() { + const services = await getServices() -export function ServiceOverview() { return (
@@ -56,38 +15,45 @@ export function ServiceOverview() { Unsere Fachbereiche

- Zweitmeinung in 6 Fachbereichen + Zweitmeinung in {services.length} Fachbereichen

- {services.map((s) => ( - -
-
- + {services.map((s) => { + const Icon = getLucideIcon(s.icon) + const categoryName = typeof s.category === "object" && s.category + ? s.category.name + : null + + return ( + +
+
+ +
+ {categoryName === "Notfall" && ( + + 24/7 + + )}
- {s.category === "Notfall" && ( - - 24/7 - - )} -
-

- Zweitmeinung {s.title} -

-

- {s.description} -

- - Mehr erfahren - - - - ))} +

+ {s.title} +

+

+ {s.shortDescription} +

+ + Mehr erfahren + + + + ) + })}
diff --git a/src/components/layout/EmergencyBanner.tsx b/src/components/layout/EmergencyBanner.tsx index 24d3a77..0479d28 100644 --- a/src/components/layout/EmergencyBanner.tsx +++ b/src/components/layout/EmergencyBanner.tsx @@ -1,6 +1,14 @@ 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 (
@@ -9,7 +17,7 @@ export function EmergencyBanner() { Medizinischer Notfall? diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx index 9e548e0..358e3bc 100644 --- a/src/components/layout/Footer.tsx +++ b/src/components/layout/Footer.tsx @@ -1,10 +1,11 @@ import Link from "next/link" -import { Phone, Mail, MapPin, Clock, Shield, Lock } from "lucide-react" -import type { SocialLink } from "@c2s/payload-contracts/types" +import { Shield, Lock } from "lucide-react" +import type { SocialLink, SiteSetting } from "@/lib/api" import { socialLinksToMap } from "@/lib/payload-helpers" interface FooterProps { socialLinks?: SocialLink[] + settings?: SiteSetting | null } const footerColumns = [ @@ -45,8 +46,10 @@ const footerColumns = [ }, ] -export function Footer({ socialLinks }: FooterProps) { +export function Footer({ socialLinks, settings }: FooterProps) { const socialMap = socialLinksToMap(socialLinks) + const copyright = settings?.footer?.copyrightText || + `\u00A9 ${new Date().getFullYear()} complex care solutions GmbH. Alle Rechte vorbehalten.` return (
-

- © {new Date().getFullYear()} complex care solutions GmbH. Alle Rechte vorbehalten. -

+

{copyright}

diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index d44f1c3..a73a7ed 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -8,16 +8,17 @@ import type { Navigation } from "@c2s/payload-contracts/types" interface HeaderProps { mainMenu: Navigation["mainMenu"] | null + services?: Array<{ title: string; slug: string; icon: string | null }> } -const services = [ - { name: "Intensivmedizin", slug: "zweitmeinung-intensivmedizin", icon: "🏥" }, - { name: "Kardiologie", slug: "zweitmeinung-kardiologie", icon: "❤️" }, - { name: "Onkologie", slug: "zweitmeinung-onkologie", icon: "🔬" }, - { name: "Nephrologie", slug: "zweitmeinung-nephrologie", icon: "🫘" }, - { name: "Gallenblase", slug: "zweitmeinung-gallenblase", icon: "💊" }, - { name: "Schilddrüse", slug: "zweitmeinung-schilddruese", icon: "🦋" }, -] +const emojiMap: Record = { + activity: "🏥", + heart: "❤️", + "flask-conical": "🔬", + stethoscope: "🫘", + pill: "💊", + brain: "🦋", +} const navLinks = [ { label: "So funktioniert's", href: "/so-funktionierts" }, @@ -27,7 +28,7 @@ const navLinks = [ { label: "Kontakt", href: "/kontakt" }, ] -export function Header({ mainMenu }: HeaderProps) { +export function Header({ mainMenu, services = [] }: HeaderProps) { const [mobileOpen, setMobileOpen] = useState(false) const [megaOpen, setMegaOpen] = useState(false) const [scrolled, setScrolled] = useState(false) @@ -38,6 +39,12 @@ export function Header({ mainMenu }: HeaderProps) { return () => window.removeEventListener("scroll", onScroll) }, []) + const megaServices = services.map((s) => ({ + name: s.title.replace(/^Zweitmeinung\s+/, ""), + slug: s.slug, + icon: emojiMap[s.icon || ""] || "📋", + })) + return ( <>
- {services.map((s) => ( + {megaServices.map((s) => ( setMobileOpen(false)} - services={services} + services={megaServices} navLinks={navLinks} /> diff --git a/src/components/layout/TopBar.tsx b/src/components/layout/TopBar.tsx index 2e3e953..f071bab 100644 --- a/src/components/layout/TopBar.tsx +++ b/src/components/layout/TopBar.tsx @@ -1,25 +1,34 @@ 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 (
diff --git a/src/components/ui/RichTextRenderer.tsx b/src/components/ui/RichTextRenderer.tsx new file mode 100644 index 0000000..0e9c065 --- /dev/null +++ b/src/components/ui/RichTextRenderer.tsx @@ -0,0 +1,168 @@ +import Link from "next/link" +import Image from "next/image" + +interface RichTextRendererProps { + content?: { root?: { children?: Record[] } } | null + className?: string +} + +export function RichTextRenderer({ content, className }: RichTextRendererProps) { + if (!content?.root?.children) return null + + return ( +
+ {content.root.children.map((node, index) => ( + + ))} +
+ ) +} + +function RenderNode({ node }: { node: Record }) { + const type = node.type as string + const children = node.children as Record[] | undefined + + switch (type) { + case "paragraph": + return ( +

+ {children?.map((child, i) => )} +

+ ) + + case "heading": { + const tag = (node.tag as string) || "h2" + const Tag = tag as "h1" | "h2" | "h3" | "h4" | "h5" | "h6" + const classes: Record = { + 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 ( + + {children?.map((child, i) => )} + + ) + } + + case "list": { + const Tag = node.listType === "number" ? "ol" : "ul" + return ( + + {children?.map((child, i) => )} + + ) + } + + case "listitem": + return ( +
  • + {children?.map((child, i) => )} +
  • + ) + + case "link": { + const url = node.url as string + const newTab = node.newTab as boolean + if (node.linkType === "internal") { + return ( + + {children?.map((child, i) => )} + + ) + } + return ( + + {children?.map((child, i) => )} + + ) + } + + case "upload": { + const value = node.value as { url?: string; alt?: string; width?: number; height?: number } | undefined + if (!value?.url) return null + return ( +
    + {value.alt +
    + ) + } + + case "quote": + return ( +
    + {children?.map((child, i) => )} +
    + ) + + 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 = {element} + if (format & 2) element = {element} + if (format & 8) element = {element} + if (format & 4) element = {element} + if (format & 16) element = {element} + } + return <>{element} + } + + case "linebreak": + return
    + + default: + if (children) { + return <>{children.map((child, i) => )} + } + return null + } +} + +/** Extract plain text from Lexical JSON (for search, Schema.org, etc.) */ +export function richTextToPlainText(content: { root?: { children?: Record[] } } | null | undefined): string { + if (!content?.root?.children) return "" + return extractText(content.root.children).trim() +} + +function extractText(nodes: Record[]): 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[]) + if (node.type === "paragraph" || node.type === "heading" || node.type === "listitem") { + text += " " + } + } + } + return text +} diff --git a/src/lib/icon-map.ts b/src/lib/icon-map.ts new file mode 100644 index 0000000..ccace96 --- /dev/null +++ b/src/lib/icon-map.ts @@ -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 = { + 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 +}