mirror of
https://github.com/complexcaresolutions/frontend.blogwoman.de.git
synced 2026-03-17 15:04:01 +00:00
- Add server.js to ESLint globalIgnores (CJS file for Passenger) - Prefix unused destructured vars with underscore - Comment out unused PAYLOAD_URL constant - Configure underscore-prefix pattern for unused vars Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
198 lines
5.4 KiB
TypeScript
198 lines
5.4 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import Script from 'next/script'
|
|
import { cn } from '@/lib/utils'
|
|
import { RichTextRenderer } from './RichTextRenderer'
|
|
import type { FAQBlock as FAQBlockType, FAQ } from '@/lib/types'
|
|
|
|
type FAQBlockProps = Omit<FAQBlockType, 'blockType'> & {
|
|
faqs?: FAQ[]
|
|
}
|
|
|
|
export function FAQBlock({
|
|
title,
|
|
subtitle,
|
|
displayMode,
|
|
selectedFaqs,
|
|
_filterCategory,
|
|
layout = 'accordion',
|
|
expandFirst = false,
|
|
showSchema = true,
|
|
faqs: externalFaqs,
|
|
}: FAQBlockProps) {
|
|
// Use selectedFaqs if displayMode is 'selected', otherwise use externalFaqs
|
|
const items = displayMode === 'selected' ? selectedFaqs : externalFaqs
|
|
|
|
if (!items || items.length === 0) return null
|
|
|
|
// Generate JSON-LD schema data
|
|
const schemaData = showSchema
|
|
? {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'FAQPage',
|
|
mainEntity: items.map((faq) => ({
|
|
'@type': 'Question',
|
|
name: faq.question,
|
|
acceptedAnswer: {
|
|
'@type': 'Answer',
|
|
text: extractTextFromRichText(faq.answer),
|
|
},
|
|
})),
|
|
}
|
|
: null
|
|
|
|
return (
|
|
<section className="py-16 md:py-20">
|
|
<div className="container">
|
|
{/* Section Header */}
|
|
{(title || subtitle) && (
|
|
<div className="text-center max-w-2xl mx-auto mb-12">
|
|
{title && <h2 className="mb-4">{title}</h2>}
|
|
{subtitle && (
|
|
<p className="text-lg text-espresso/80">{subtitle}</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* FAQ Items */}
|
|
<div className="max-w-3xl mx-auto">
|
|
{layout === 'accordion' ? (
|
|
<AccordionFAQ items={items} expandFirst={expandFirst} />
|
|
) : layout === 'grid' ? (
|
|
<GridFAQ items={items} />
|
|
) : (
|
|
<ListFAQ items={items} />
|
|
)}
|
|
</div>
|
|
|
|
{/* JSON-LD Schema using Next.js Script component for safety */}
|
|
{schemaData && (
|
|
<Script
|
|
id="faq-schema"
|
|
type="application/ld+json"
|
|
strategy="afterInteractive"
|
|
>
|
|
{JSON.stringify(schemaData)}
|
|
</Script>
|
|
)}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
function AccordionFAQ({
|
|
items,
|
|
expandFirst,
|
|
}: {
|
|
items: FAQ[]
|
|
expandFirst: boolean
|
|
}) {
|
|
const [openIndex, setOpenIndex] = useState<number | null>(
|
|
expandFirst ? 0 : null
|
|
)
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{items.map((faq, index) => (
|
|
<div
|
|
key={faq.id}
|
|
className="border border-warm-gray rounded-xl overflow-hidden"
|
|
>
|
|
<button
|
|
type="button"
|
|
className="w-full px-6 py-4 flex items-center justify-between text-left bg-soft-white hover:bg-ivory transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brass focus-visible:ring-inset"
|
|
onClick={() => setOpenIndex(openIndex === index ? null : index)}
|
|
aria-expanded={openIndex === index}
|
|
>
|
|
<span className="font-headline text-lg font-medium text-espresso pr-4">
|
|
{faq.question}
|
|
</span>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
strokeWidth={2}
|
|
stroke="currentColor"
|
|
className={cn(
|
|
'w-5 h-5 text-brass transition-transform duration-200 flex-shrink-0',
|
|
openIndex === index && 'rotate-180'
|
|
)}
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
|
|
<div
|
|
className={cn(
|
|
'grid transition-all duration-300 ease-out',
|
|
openIndex === index
|
|
? 'grid-rows-[1fr] opacity-100'
|
|
: 'grid-rows-[0fr] opacity-0'
|
|
)}
|
|
>
|
|
<div className="overflow-hidden">
|
|
<div className="px-6 pb-6 pt-2">
|
|
<RichTextRenderer content={faq.answer} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ListFAQ({ items }: { items: FAQ[] }) {
|
|
return (
|
|
<div className="space-y-8">
|
|
{items.map((faq) => (
|
|
<div key={faq.id}>
|
|
<h3 className="text-xl font-semibold mb-3">{faq.question}</h3>
|
|
<RichTextRenderer content={faq.answer} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function GridFAQ({ items }: { items: FAQ[] }) {
|
|
return (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{items.map((faq) => (
|
|
<div
|
|
key={faq.id}
|
|
className="bg-soft-white border border-warm-gray rounded-xl p-6"
|
|
>
|
|
<h3 className="text-lg font-semibold mb-3">{faq.question}</h3>
|
|
<RichTextRenderer content={faq.answer} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Helper to extract plain text from RichText for schema
|
|
function extractTextFromRichText(richText: FAQ['answer']): string {
|
|
if (!richText?.root?.children) return ''
|
|
|
|
function extractFromNode(node: Record<string, unknown>): string {
|
|
if (node.text) return node.text as string
|
|
|
|
const children = node.children as Record<string, unknown>[] | undefined
|
|
if (children) {
|
|
return children.map(extractFromNode).join('')
|
|
}
|
|
|
|
return ''
|
|
}
|
|
|
|
return richText.root.children
|
|
.map((node) => extractFromNode(node as Record<string, unknown>))
|
|
.join(' ')
|
|
.trim()
|
|
}
|