Fix FAQFull component: Migrate correct page ID (6), fix JSON-LD SSR and TS errors, remove justified-text
This commit is contained in:
@@ -7,6 +7,17 @@ import { getLocalizedSlugs } from '@intecion/ipal-kit'
|
||||
import { i18n } from '../../../../i18n.config'
|
||||
import { SetLanguageSlugs } from '../../../../components/LanguageSlugsProvider'
|
||||
import { RenderBlocks } from '../../../../blocks/RenderBlocks'
|
||||
import type { Page } from '../../../../payload-types'
|
||||
|
||||
type FAQFullBlock = Extract<NonNullable<Page['layout']>[0], { blockType: 'faqFull' }>
|
||||
|
||||
const PLACEHOLDER_PL = "Odpowiedź w przygotowaniu"
|
||||
const PLACEHOLDER_DE = "Antwort in Vorbereitung"
|
||||
|
||||
function isPlaceholder(answer: string | null | undefined): boolean {
|
||||
if (!answer) return true;
|
||||
return answer.includes(PLACEHOLDER_PL) || answer.includes(PLACEHOLDER_DE) || answer.includes('DO POTWIERDZENIA');
|
||||
}
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ locale: string; slug?: string[] }>
|
||||
@@ -47,8 +58,35 @@ export default async function Page(props: {
|
||||
|
||||
const layout = docForRender.layout || []
|
||||
|
||||
// Generowanie schematu FAQPage dla bloku faqFull (na poziomie Server Component gwarantuje to SSR)
|
||||
let faqSchema: Record<string, any> | null = null;
|
||||
const faqBlock = layout.find((b: any) => b.blockType === 'faqFull') as FAQFullBlock | undefined;
|
||||
if (faqBlock && faqBlock.questions) {
|
||||
const validQuestions = faqBlock.questions.filter((q: any) => !isPlaceholder(q.answer));
|
||||
if (validQuestions.length > 0) {
|
||||
faqSchema = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "FAQPage",
|
||||
"mainEntity": validQuestions.map((q: any) => ({
|
||||
"@type": "Question",
|
||||
"name": q.question,
|
||||
"acceptedAnswer": {
|
||||
"@type": "Answer",
|
||||
"text": q.answer
|
||||
}
|
||||
}))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pt-8 pb-24">
|
||||
{faqSchema && (
|
||||
<script
|
||||
type="application/ld+json"
|
||||
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }}
|
||||
/>
|
||||
)}
|
||||
{/* Rejestracja zlokalizowanych slugów w globalnym kontekście dla LanguageSwitcher */}
|
||||
<SetLanguageSlugs slugs={slugs} />
|
||||
|
||||
@@ -59,7 +97,7 @@ export default async function Page(props: {
|
||||
)}
|
||||
|
||||
<main>
|
||||
<RenderBlocks blocks={layout} />
|
||||
<RenderBlocks blocks={layout} locale={locale} />
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
'use client'
|
||||
import React, { useState } from 'react'
|
||||
import { motion, AnimatePresence, useScroll, useSpring } from 'framer-motion'
|
||||
import { ChevronDown, MessageCircle } from 'lucide-react'
|
||||
import type { Page } from '../../payload-types'
|
||||
|
||||
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'faqFull' }>
|
||||
|
||||
const PLACEHOLDER_PL = "Odpowiedź w przygotowaniu"
|
||||
const PLACEHOLDER_DE = "Antwort in Vorbereitung"
|
||||
|
||||
function isPlaceholder(answer: string | null | undefined): boolean {
|
||||
if (!answer) return true;
|
||||
return answer.includes(PLACEHOLDER_PL) || answer.includes(PLACEHOLDER_DE) || answer.includes('DO POTWIERDZENIA');
|
||||
}
|
||||
|
||||
export const FAQFullBlockComponent: React.FC<Props> = ({ title, subtitle, questions, ctaText, ctaUrl }) => {
|
||||
const { scrollYProgress } = useScroll();
|
||||
const scaleX = useSpring(scrollYProgress, {
|
||||
stiffness: 100,
|
||||
damping: 30,
|
||||
restDelta: 0.001
|
||||
});
|
||||
|
||||
// State to track open question per category
|
||||
const [openStates, setOpenStates] = useState<Record<string, number | null>>({});
|
||||
|
||||
const toggleQuestion = (category: string, index: number) => {
|
||||
setOpenStates(prev => ({
|
||||
...prev,
|
||||
[category]: prev[category] === index ? null : index
|
||||
}))
|
||||
}
|
||||
|
||||
// Filter out placeholder questions
|
||||
const validQuestions = (questions || []).filter(q => !isPlaceholder(q.answer));
|
||||
|
||||
// Group by category
|
||||
const groupedQuestions = validQuestions.reduce((acc, q) => {
|
||||
const cat = q.category || 'Inne';
|
||||
if (!acc[cat]) {
|
||||
acc[cat] = [];
|
||||
}
|
||||
acc[cat].push(q);
|
||||
return acc;
|
||||
}, {} as Record<string, typeof validQuestions>);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Sticky Progress Bar */}
|
||||
<motion.div
|
||||
className="fixed top-0 left-0 right-0 h-1 bg-accent z-50 origin-left"
|
||||
style={{ scaleX }}
|
||||
/>
|
||||
|
||||
{/* Page Header Area */}
|
||||
<section className="border-b border-border py-16 md:py-20 bg-background">
|
||||
<div className="container-premium">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="max-w-4xl"
|
||||
>
|
||||
<h1 className="font-heading text-4xl sm:text-5xl lg:text-5xl font-extrabold text-foreground tracking-tight leading-tight text-balance max-w-4xl">
|
||||
{title}
|
||||
</h1>
|
||||
{subtitle && (
|
||||
<p className="mt-6 text-lg md:text-xl text-foreground/70 leading-relaxed max-w-[65ch]">
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{ctaText && ctaUrl && (
|
||||
<div className="mt-8">
|
||||
<a
|
||||
href={ctaUrl}
|
||||
className="inline-flex items-center justify-center gap-2 px-6 py-3 text-base font-bold text-accent bg-accent/10 rounded-md hover:bg-accent/20 transition-colors"
|
||||
>
|
||||
<MessageCircle size={20} />
|
||||
{ctaText}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ Categories */}
|
||||
<section className="py-16 md:py-24 bg-background">
|
||||
<div className="container-premium max-w-4xl">
|
||||
<div className="space-y-16">
|
||||
{Object.entries(groupedQuestions).map(([category, catQuestions], catIndex) => (
|
||||
<motion.div
|
||||
key={category}
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-100px" }}
|
||||
transition={{ duration: 0.5, delay: catIndex * 0.1 }}
|
||||
className="space-y-6"
|
||||
>
|
||||
<h2 className="font-heading text-2xl sm:text-3xl font-extrabold text-foreground">
|
||||
{category}
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{catQuestions.map((q, index) => {
|
||||
const isOpen = openStates[category] === index;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={q.id || index}
|
||||
className="bg-card border border-border overflow-hidden shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<button
|
||||
onClick={() => toggleQuestion(category, index)}
|
||||
className="w-full flex items-center justify-between p-6 text-left hover:bg-background transition-colors focus:outline-none"
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<h3 className="font-heading text-lg sm:text-xl font-bold text-foreground pr-8">
|
||||
{q.question}
|
||||
</h3>
|
||||
<div className="w-8 h-8 rounded-full bg-accent/10 flex items-center justify-center shrink-0">
|
||||
<ChevronDown
|
||||
className={`w-5 h-5 text-accent transition-transform duration-300 ${isOpen ? 'rotate-180' : ''}`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
<AnimatePresence initial={false}>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: 'auto', opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
||||
>
|
||||
<div className="p-6 pt-0 text-foreground/80 leading-relaxed border-t border-border/10 max-w-[70ch]">
|
||||
{q.answer}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user