mirror of
https://github.com/complexcaresolutions/frontend.zweitmeinu.ng.git
synced 2026-03-17 15:03:48 +00:00
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:
parent
62769379ad
commit
1aad09cc0f
14 changed files with 492 additions and 473 deletions
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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<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)
|
||||
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<Metadata> {
|
||||
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({
|
|||
<Icon className="h-8 w-8" />
|
||||
</div>
|
||||
<p className="text-primary-light text-sm font-semibold mb-2">
|
||||
Fachbereiche / {service.title}
|
||||
Fachbereiche / {shortTitle}
|
||||
</p>
|
||||
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-6">
|
||||
{service.fullTitle} – klare Empfehlungen bei{" "}
|
||||
{service.title === "Intensivmedizin"
|
||||
? "kritischen Entscheidungen"
|
||||
: `${service.title}-Entscheidungen`}
|
||||
{service.title}
|
||||
</h1>
|
||||
<p className="text-white/60 text-lg max-w-2xl mx-auto mb-10">
|
||||
{service.shortDescription}
|
||||
|
|
@ -229,59 +73,77 @@ export default async function ServiceDetailPage({
|
|||
</Container>
|
||||
</section>
|
||||
|
||||
{/* When is a second opinion useful */}
|
||||
<section className="py-20 bg-white">
|
||||
<Container size="md">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-center text-navy mb-4">
|
||||
Wann ist eine Zweitmeinung sinnvoll?
|
||||
</h2>
|
||||
<p className="text-text-muted text-center max-w-2xl mx-auto mb-12">
|
||||
{service.description[0]}
|
||||
</p>
|
||||
<div className="grid sm:grid-cols-2 gap-6">
|
||||
{service.benefits.map((b) => (
|
||||
<div
|
||||
key={b.title}
|
||||
className="bg-bg rounded-xl p-6 border border-border"
|
||||
>
|
||||
<b.icon className="h-6 w-6 text-primary mb-3" />
|
||||
<h3 className="font-bold text-navy mb-2">{b.title}</h3>
|
||||
<p className="text-sm text-text-muted">{b.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
{/* Benefits grid from features[] */}
|
||||
{service.features && service.features.length > 0 && (
|
||||
<section className="py-20 bg-white">
|
||||
<Container size="md">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-center text-navy mb-4">
|
||||
Wann ist eine Zweitmeinung sinnvoll?
|
||||
</h2>
|
||||
<p className="text-text-muted text-center max-w-2xl mx-auto mb-12">
|
||||
{service.shortDescription}
|
||||
</p>
|
||||
<div className="grid sm:grid-cols-2 gap-6">
|
||||
{service.features.map((f) => {
|
||||
const FeatureIcon = getLucideIcon(f.icon)
|
||||
return (
|
||||
<div
|
||||
key={f.id || f.title}
|
||||
className="bg-bg rounded-xl p-6 border border-border"
|
||||
>
|
||||
<FeatureIcon className="h-6 w-6 text-primary mb-3" />
|
||||
<h3 className="font-bold text-navy mb-2">{f.title}</h3>
|
||||
<p className="text-sm text-text-muted">{f.description}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* What we do for you */}
|
||||
<section id="was-wir-tun" className="py-20 bg-bg">
|
||||
<Container size="md">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-center text-navy mb-3">
|
||||
Was wir für Sie tun
|
||||
</h2>
|
||||
<p className="text-text-muted text-center mb-12">
|
||||
{service.description[1]}
|
||||
</p>
|
||||
<div className="space-y-4 max-w-2xl mx-auto">
|
||||
{service.checklist.map((item) => (
|
||||
<div
|
||||
key={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" />
|
||||
{/* Checklist from detailSections[] */}
|
||||
{service.detailSections && service.detailSections.length > 0 && (
|
||||
<section id="was-wir-tun" className="py-20 bg-bg">
|
||||
<Container size="md">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold text-center text-navy mb-3">
|
||||
Was wir für Sie tun
|
||||
</h2>
|
||||
<p className="text-text-muted text-center mb-12">
|
||||
Unser Leistungsumfang im Bereich {shortTitle}
|
||||
</p>
|
||||
<div className="space-y-4 max-w-2xl mx-auto">
|
||||
{service.detailSections.map((item) => (
|
||||
<div
|
||||
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>
|
||||
<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>
|
||||
<h3 className="font-bold text-navy">{item.title}</h3>
|
||||
<p className="text-sm text-text-muted mt-1">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Full description (richText) */}
|
||||
{service.description && (
|
||||
<section className="py-20 bg-white">
|
||||
<Container size="md">
|
||||
<div className="prose prose-lg max-w-none text-text-muted">
|
||||
<RichTextRenderer content={service.description as any} />
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
<section className="py-16 bg-white border-y border-border">
|
||||
|
|
@ -299,9 +161,7 @@ export default async function ServiceDetailPage({
|
|||
500+
|
||||
</div>
|
||||
<p className="text-sm text-text-muted mt-1">
|
||||
{service.title === "Kardiologie"
|
||||
? "Kardiologische Zweitmeinungen"
|
||||
: "Medizinische Zweitmeinungen"}
|
||||
Medizinische Zweitmeinungen
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -330,11 +190,11 @@ export default async function ServiceDetailPage({
|
|||
<Icon className="h-7 w-7" />
|
||||
</div>
|
||||
<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>
|
||||
<p className="text-text-muted mb-8">
|
||||
Kontaktieren Sie uns für eine unabhängige, professionelle
|
||||
Einschätzung.
|
||||
Einschätzung im Bereich {shortTitle}.
|
||||
</p>
|
||||
<div className="flex flex-wrap justify-center gap-4">
|
||||
<Button href="tel:+4980080441000" variant="gold" size="lg">
|
||||
|
|
|
|||
|
|
@ -1,67 +1,28 @@
|
|||
import type { Metadata } from "next"
|
||||
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 { 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 = {
|
||||
title: "Fachbereiche",
|
||||
description: "Medizinische Zweitmeinung in 6 Fachbereichen: Intensivmedizin, Kardiologie, Onkologie, Nephrologie, Gallenblase und Schilddrüse.",
|
||||
}
|
||||
|
||||
const services = [
|
||||
{
|
||||
title: "Intensivmedizin",
|
||||
slug: "zweitmeinung-intensivmedizin",
|
||||
description: "Unabhängige ärztliche Zweitmeinung bei laufender oder geplanter Intensivbehandlung. Wir prüfen medizinische Indikation, Patientenwille und Behandlungsalternativen.",
|
||||
icon: Activity,
|
||||
category: "Notfall",
|
||||
color: "bg-red-50 text-red-600",
|
||||
},
|
||||
{
|
||||
title: "Kardiologie",
|
||||
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",
|
||||
},
|
||||
]
|
||||
const colorMap: Record<string, string> = {
|
||||
"zweitmeinung-intensivmedizin": "bg-red-50 text-red-600",
|
||||
"zweitmeinung-kardiologie": "bg-primary/10 text-primary",
|
||||
"zweitmeinung-onkologie": "bg-purple-50 text-purple-600",
|
||||
"zweitmeinung-nephrologie": "bg-emerald-50 text-emerald-600",
|
||||
"zweitmeinung-gallenblase": "bg-amber-50 text-amber-600",
|
||||
"zweitmeinung-schilddruese": "bg-cyan-50 text-cyan-600",
|
||||
}
|
||||
|
||||
export default async function FachbereichePage() {
|
||||
const services = await getServices()
|
||||
|
||||
export default function FachbereichePage() {
|
||||
return (
|
||||
<>
|
||||
{/* Hero */}
|
||||
|
|
@ -75,7 +36,7 @@ export default function FachbereichePage() {
|
|||
Medizinische Zweitmeinung
|
||||
</h1>
|
||||
<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,
|
||||
neutral und sicher.
|
||||
</p>
|
||||
|
|
@ -87,34 +48,42 @@ export default function FachbereichePage() {
|
|||
<section className="py-20 bg-bg">
|
||||
<Container>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{services.map((s) => (
|
||||
<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 ${s.color} flex items-center justify-center`}>
|
||||
<s.icon className="h-7 w-7" />
|
||||
{services.map((s) => {
|
||||
const Icon = getLucideIcon(s.icon)
|
||||
const color = colorMap[s.slug] || "bg-primary/10 text-primary"
|
||||
const categoryName = typeof s.category === "object" && s.category
|
||||
? s.category.name
|
||||
: null
|
||||
|
||||
return (
|
||||
<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>
|
||||
{s.category === "Notfall" && (
|
||||
<span className="text-xs font-bold bg-red-100 text-red-700 px-3 py-1 rounded-full">
|
||||
24/7 Notfall
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="text-xl font-bold mb-3 text-navy group-hover:text-primary transition-colors">
|
||||
Zweitmeinung {s.title}
|
||||
</h2>
|
||||
<p className="text-text-muted text-sm leading-relaxed mb-6">
|
||||
{s.description}
|
||||
</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>
|
||||
))}
|
||||
<h2 className="text-xl font-bold mb-3 text-navy group-hover:text-primary transition-colors">
|
||||
{s.title}
|
||||
</h2>
|
||||
<p className="text-text-muted text-sm leading-relaxed mb-6">
|
||||
{s.shortDescription}
|
||||
</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>
|
||||
</Container>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { Container } from "@/components/ui/Container"
|
|||
import { Button } from "@/components/ui/Button"
|
||||
import { FAQClient } from "@/components/faq/FAQClient"
|
||||
import { Phone, Mail, HelpCircle } from "lucide-react"
|
||||
import { getFaqs } from "@/lib/api"
|
||||
import { richTextToPlainText } from "@/components/ui/RichTextRenderer"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
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.",
|
||||
}
|
||||
|
||||
const categories = [
|
||||
{ name: "Allgemeine Fragen", slug: "allgemein" },
|
||||
{ name: "Intensivmedizin", slug: "intensivmedizin" },
|
||||
{ name: "Kardiologie", slug: "kardiologie" },
|
||||
{ name: "Onkologie", slug: "onkologie" },
|
||||
{ name: "Nephrologie", slug: "nephrologie" },
|
||||
{ name: "Gallenblase", slug: "gallenblase" },
|
||||
{ name: "Schilddrüse", slug: "schilddruese" },
|
||||
]
|
||||
const categoryNames: Record<string, string> = {
|
||||
allgemein: "Allgemeine Fragen",
|
||||
intensivmedizin: "Intensivmedizin",
|
||||
kardiologie: "Kardiologie",
|
||||
onkologie: "Onkologie",
|
||||
nephrologie: "Nephrologie",
|
||||
gallenblase: "Gallenblase",
|
||||
schilddruese: "Schilddrüse",
|
||||
}
|
||||
|
||||
const faqData = [
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ 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" },
|
||||
{ question: "Wann ist eine Zweitmeinung zur Schilddrüsen-OP sinnvoll?", answer: "Bei empfohlener OP wegen Knoten, Struma, Autonomie oder unklarer Laborwerte.", category: "schilddruese" },
|
||||
{ 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: "Wer erstellt die Schilddrüsen-Zweitmeinung?", answer: "Fachärzt:innen für Endokrinologie oder Schilddrüsenchirurgie mit langjähriger Erfahrung.", category: "schilddruese" },
|
||||
{ question: "Ist die Zweitmeinung verbindlich?", answer: "Nein, die Zweitmeinung ist eine unabhängige Empfehlung. Sie sind nicht verpflichtet, ihr zu folgen.", category: "schilddruese" },
|
||||
{ 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" },
|
||||
{ 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 async function FAQPage() {
|
||||
const faqs = await getFaqs()
|
||||
|
||||
// Derive categories from data
|
||||
const categorySlugs = [...new Set(faqs.map((f) => f.category).filter((c): c is string => !!c))]
|
||||
const categories = categorySlugs.map((slug: string) => ({
|
||||
name: categoryNames[slug] || slug,
|
||||
slug,
|
||||
}))
|
||||
|
||||
// Transform for client component
|
||||
const faqItems = faqs.map((f) => ({
|
||||
question: f.question,
|
||||
answer: f.answer,
|
||||
answerText: richTextToPlainText(f.answer as any),
|
||||
category: f.category || "allgemein",
|
||||
}))
|
||||
|
||||
export default function FAQPage() {
|
||||
return (
|
||||
<>
|
||||
{/* Hero */}
|
||||
|
|
@ -65,7 +57,7 @@ export default function FAQPage() {
|
|||
</section>
|
||||
|
||||
{/* FAQ Content (Client Component for search/filter) */}
|
||||
<FAQClient categories={categories} faqs={faqData} />
|
||||
<FAQClient categories={categories} faqs={faqItems} />
|
||||
|
||||
{/* CTA */}
|
||||
<section className="py-20 bg-navy">
|
||||
|
|
@ -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 },
|
||||
})),
|
||||
}),
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
</h2>
|
||||
<div className="space-y-5">
|
||||
<a
|
||||
href="tel:+4980080441000"
|
||||
className="flex items-start gap-4 group"
|
||||
>
|
||||
<a href={phoneHref} 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">
|
||||
<Phone className="h-5 w-5 text-primary group-hover:text-white" />
|
||||
</div>
|
||||
<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>
|
||||
</a>
|
||||
<div className="flex items-start gap-4">
|
||||
|
|
@ -54,19 +62,16 @@ export default function KontaktPage() {
|
|||
</div>
|
||||
<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>
|
||||
<a
|
||||
href="mailto:kontakt@zweitmeinu.ng"
|
||||
className="flex items-start gap-4 group"
|
||||
>
|
||||
<a href={`mailto:${email}`} 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">
|
||||
<Mail className="h-5 w-5 text-primary group-hover:text-white" />
|
||||
</div>
|
||||
<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>
|
||||
</a>
|
||||
<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="text-text-muted text-sm">
|
||||
complex care solutions GmbH<br />
|
||||
Hans-Böckler-Str. 19<br />
|
||||
46236 Bottrop
|
||||
{street}<br />
|
||||
{zip} {city}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<html lang="de">
|
||||
<body className="font-body antialiased">
|
||||
|
|
@ -41,13 +48,13 @@ export default async function RootLayout({
|
|||
Zum Hauptinhalt springen
|
||||
</a>
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<TopBar />
|
||||
<Header mainMenu={navigation?.mainMenu ?? null} />
|
||||
<TopBar settings={settings} />
|
||||
<Header mainMenu={navigation?.mainMenu ?? null} services={headerServices} />
|
||||
<main id="main-content" className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
<EmergencyBanner />
|
||||
<Footer socialLinks={socialLinks} />
|
||||
<EmergencyBanner settings={settings} />
|
||||
<Footer socialLinks={socialLinks} settings={settings} />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
</button>
|
||||
{openItems.has(i) && (
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<section className="py-20 bg-bg">
|
||||
<Container>
|
||||
|
|
@ -56,38 +15,45 @@ export function ServiceOverview() {
|
|||
Unsere Fachbereiche
|
||||
</p>
|
||||
<h2 className="text-3xl sm:text-4xl font-bold text-navy">
|
||||
Zweitmeinung in 6 Fachbereichen
|
||||
Zweitmeinung in {services.length} Fachbereichen
|
||||
</h2>
|
||||
</div>
|
||||
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{services.map((s) => (
|
||||
<Link
|
||||
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">
|
||||
<s.icon className="h-6 w-6" />
|
||||
{services.map((s) => {
|
||||
const Icon = getLucideIcon(s.icon)
|
||||
const categoryName = typeof s.category === "object" && s.category
|
||||
? s.category.name
|
||||
: null
|
||||
|
||||
return (
|
||||
<Link
|
||||
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>
|
||||
{s.category === "Notfall" && (
|
||||
<span className="text-xs font-bold bg-gold/20 text-gold-hover px-2 py-1 rounded-full">
|
||||
24/7
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="font-bold text-lg mb-2 text-navy group-hover:text-primary transition-colors">
|
||||
Zweitmeinung {s.title}
|
||||
</h3>
|
||||
<p className="text-text-muted text-sm leading-relaxed mb-4">
|
||||
{s.description}
|
||||
</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>
|
||||
))}
|
||||
<h3 className="font-bold text-lg mb-2 text-navy group-hover:text-primary transition-colors">
|
||||
{s.title}
|
||||
</h3>
|
||||
<p className="text-text-muted text-sm leading-relaxed mb-4">
|
||||
{s.shortDescription}
|
||||
</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>
|
||||
</Container>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="bg-gold">
|
||||
<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?
|
||||
</span>
|
||||
<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"
|
||||
>
|
||||
<Phone className="h-4 w-4" />
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<footer className="bg-navy text-white">
|
||||
|
|
@ -142,9 +145,7 @@ export function Footer({ socialLinks }: FooterProps) {
|
|||
</a>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-white/40">
|
||||
© {new Date().getFullYear()} complex care solutions GmbH. Alle Rechte vorbehalten.
|
||||
</p>
|
||||
<p className="text-xs text-white/40">{copyright}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
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 (
|
||||
<>
|
||||
<header
|
||||
|
|
@ -81,7 +88,7 @@ export function Header({ mainMenu }: HeaderProps) {
|
|||
<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="p-2">
|
||||
{services.map((s) => (
|
||||
{megaServices.map((s) => (
|
||||
<Link
|
||||
key={s.slug}
|
||||
href={`/fachbereiche/${s.slug}`}
|
||||
|
|
@ -142,7 +149,7 @@ export function Header({ mainMenu }: HeaderProps) {
|
|||
<MobileMenu
|
||||
open={mobileOpen}
|
||||
onClose={() => setMobileOpen(false)}
|
||||
services={services}
|
||||
services={megaServices}
|
||||
navLinks={navLinks}
|
||||
/>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<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="flex items-center justify-between h-10">
|
||||
<div className="flex items-center gap-4 sm:gap-6">
|
||||
<a
|
||||
href="tel:+4980080441000"
|
||||
href={phoneHref}
|
||||
className="flex items-center gap-1.5 hover:text-gold transition-colors"
|
||||
>
|
||||
<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>
|
||||
<span className="hidden sm:inline text-white/30">|</span>
|
||||
<a
|
||||
href="mailto:kontakt@zweitmeinu.ng"
|
||||
href={`mailto:${email}`}
|
||||
className="hidden sm:flex items-center gap-1.5 hover:text-gold transition-colors"
|
||||
>
|
||||
<Mail className="h-3.5 w-3.5" />
|
||||
<span>kontakt@zweitmeinu.ng</span>
|
||||
<span>{email}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-xs">
|
||||
|
|
|
|||
168
src/components/ui/RichTextRenderer.tsx
Normal file
168
src/components/ui/RichTextRenderer.tsx
Normal 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
25
src/lib/icon-map.ts
Normal 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
|
||||
}
|
||||
Loading…
Reference in a new issue