Fix FAQFull component: Migrate correct page ID (6), fix JSON-LD SSR and TS errors, remove justified-text
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import { getPayload } from 'payload'
|
||||
import configPromise from '../src/payload.config'
|
||||
import type { Page } from '../src/payload-types'
|
||||
type FAQFullBlock = Extract<NonNullable<Page['layout']>[0], { blockType: 'faqFull' }>;
|
||||
|
||||
const PLACEHOLDER_ANSWER_PL = "Odpowiedź w przygotowaniu — treść do potwierdzenia przez klienta/doradcę podatkowego";
|
||||
const PLACEHOLDER_ANSWER_DE = "Antwort in Vorbereitung — Inhalt muss vom Kunden/Steuerberater bestätigt werden";
|
||||
|
||||
const questions_pl: FAQFullBlock['questions'] = [
|
||||
{
|
||||
category: 'Proces i dokumenty',
|
||||
question: 'Czy muszę przyjeżdżać osobiście do biura?',
|
||||
answer: 'Nie, wszystkie formalności możemy załatwić w 100% online lub pocztą tradycyjną. Dokumenty możesz wysłać bezpiecznym kurierem lub przez e-mail, a my zajmiemy się resztą.',
|
||||
},
|
||||
{
|
||||
category: 'Proces i dokumenty',
|
||||
question: 'Jakie dokumenty są potrzebne do rozliczenia?',
|
||||
answer: 'Podstawą jest roczne zestawienie zarobków (Lohnsteuerbescheinigung), dowód tożsamości oraz zaświadczenie o dochodach w Polsce (EU/EWR). Otrzymasz od nas szczegółową listę dopasowaną do Twojej sytuacji.',
|
||||
},
|
||||
{
|
||||
category: 'Proces i dokumenty',
|
||||
question: 'Do kiedy muszę złożyć deklarację podatkową w Niemczech?',
|
||||
answer: 'Standardowy termin to 31 lipca roku następującego po roku podatkowym. Jeśli korzystasz z usług doradcy podatkowego, termin jest automatycznie wydłużany, co daje Ci więcej czasu na zgromadzenie dokumentów.',
|
||||
},
|
||||
{
|
||||
category: 'Zwrot podatku i koszty',
|
||||
question: 'Jak długo czeka się na zwrot podatku z urzędu skarbowego?',
|
||||
answer: 'Średni czas oczekiwania na decyzję Finanzamtu to od 2 do 6 miesięcy, zależnie od obciążenia urzędu. Nasze staranne przygotowanie dokumentów minimalizuje ryzyko dodatkowych zapytań i przyspiesza proces.',
|
||||
},
|
||||
{
|
||||
category: 'Zwrot podatku i koszty',
|
||||
question: 'Ile kosztuje rozliczenie podatku z Niemiec?',
|
||||
answer: 'Opłaty doradców podatkowych w Niemczech są regulowane ustawowo (StBVV) i zależą od skomplikowania sprawy oraz wysokości dochodów. Zawsze przedstawiamy jasną wycenę przed podjęciem działań, bez ukrytych kosztów.',
|
||||
},
|
||||
{
|
||||
category: 'Świadczenia dodatkowe',
|
||||
question: 'Czy załatwiacie również zasiłek Kindergeld?',
|
||||
answer: 'Tak, pomagamy w pełnym procesie wnioskowania o zasiłek na dzieci (Kindergeld) z Familienkasse. Zajmujemy się kompletowaniem załączników, weryfikacją stanu rodziny oraz korespondencją z urzędem.',
|
||||
},
|
||||
{
|
||||
category: 'Dodatkowe (DO UZUPEŁNIENIA)',
|
||||
question: 'Czy mogę się rozliczyć, jeśli pracowałem w Niemczech tylko kilka miesięcy?',
|
||||
answer: PLACEHOLDER_ANSWER_PL,
|
||||
},
|
||||
{
|
||||
category: 'Dodatkowe (DO UZUPEŁNIENIA)',
|
||||
question: 'Co się stanie, jeśli spóźnię się ze złożeniem deklaracji?',
|
||||
answer: PLACEHOLDER_ANSWER_PL,
|
||||
},
|
||||
{
|
||||
category: 'Dodatkowe (DO UZUPEŁNIENIA)',
|
||||
question: 'Czy mogę rozliczyć się wspólnie z małżonkiem, jeśli on/ona nie pracuje w Niemczech?',
|
||||
answer: PLACEHOLDER_ANSWER_PL,
|
||||
},
|
||||
{
|
||||
category: 'Dodatkowe (DO UZUPEŁNIENIA)',
|
||||
question: 'Czy pomagacie też w rozliczeniu za lata wcześniejsze (wsteczne)?',
|
||||
answer: PLACEHOLDER_ANSWER_PL,
|
||||
}
|
||||
];
|
||||
|
||||
const questions_de: FAQFullBlock['questions'] = [
|
||||
{
|
||||
category: 'Prozess und Dokumente',
|
||||
question: 'Muss ich persönlich ins Büro kommen?',
|
||||
answer: 'Nein, alle Formalitäten können wir zu 100% online oder per Post erledigen. Sie können die Dokumente sicher per Kurier oder E-Mail senden, und wir kümmern uns um den Rest.',
|
||||
},
|
||||
{
|
||||
category: 'Prozess und Dokumente',
|
||||
question: 'Welche Dokumente werden für die Steuererklärung benötigt?',
|
||||
answer: 'Grundlegend sind die Lohnsteuerbescheinigung, ein Ausweisdokument und die Bescheinigung EU/EWR über Einkünfte im Heimatland. Sie erhalten von uns eine detaillierte, auf Ihre Situation zugeschnittene Liste.',
|
||||
},
|
||||
{
|
||||
category: 'Prozess und Dokumente',
|
||||
question: 'Bis wann muss ich die Steuererklärung in Deutschland abgeben?',
|
||||
answer: 'Die Standardfrist ist der 31. Juli des auf das Steuerjahr folgenden Jahres. Wenn Sie die Dienste eines Steuerberaters in Anspruch nehmen, verlängert sich die Frist automatisch.',
|
||||
},
|
||||
{
|
||||
category: 'Steuererstattung und Kosten',
|
||||
question: 'Wie lange dauert es, bis die Steuererstattung vom Finanzamt kommt?',
|
||||
answer: 'Die durchschnittliche Wartezeit auf den Bescheid des Finanzamts beträgt 2 bis 6 Monate. Unsere sorgfältige Vorbereitung der Dokumente minimiert Rückfragen und beschleunigt den Prozess.',
|
||||
},
|
||||
{
|
||||
category: 'Steuererstattung und Kosten',
|
||||
question: 'Was kostet eine Steuererklärung in Deutschland?',
|
||||
answer: 'Die Gebühren von Steuerberatern sind in Deutschland gesetzlich geregelt (StBVV) und hängen von der Komplexität des Falles und der Höhe der Einkünfte ab. Wir machen immer einen klaren Kostenvoranschlag.',
|
||||
},
|
||||
{
|
||||
category: 'Zusätzliche Leistungen',
|
||||
question: 'Beantragen Sie auch Kindergeld?',
|
||||
answer: 'Ja, wir unterstützen Sie beim gesamten Antragsprozess für Kindergeld bei der Familienkasse. Wir kümmern uns um die Anlagen, die Überprüfung des Familienstands und den Schriftverkehr.',
|
||||
},
|
||||
{
|
||||
category: 'Zusätzlich (ZUM AUSFÜLLEN)',
|
||||
question: 'Kann ich eine Steuererklärung abgeben, wenn ich nur wenige Monate in Deutschland gearbeitet habe?',
|
||||
answer: PLACEHOLDER_ANSWER_DE,
|
||||
},
|
||||
{
|
||||
category: 'Zusätzlich (ZUM AUSFÜLLEN)',
|
||||
question: 'Was passiert, wenn ich die Steuererklärung zu spät abgebe?',
|
||||
answer: PLACEHOLDER_ANSWER_DE,
|
||||
},
|
||||
{
|
||||
category: 'Zusätzlich (ZUM AUSFÜLLEN)',
|
||||
question: 'Kann ich mit meinem Ehepartner zusammenveranlagt werden, auch wenn er/sie nicht in Deutschland arbeitet?',
|
||||
answer: PLACEHOLDER_ANSWER_DE,
|
||||
},
|
||||
{
|
||||
category: 'Zusätzlich (ZUM AUSFÜLLEN)',
|
||||
question: 'Helfen Sie auch bei der Steuererklärung für Vorjahre (rückwirkend)?',
|
||||
answer: PLACEHOLDER_ANSWER_DE,
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
async function run() {
|
||||
console.log('Rozpoczynam migrację strony FAQ...')
|
||||
const payload = await getPayload({ config: configPromise })
|
||||
|
||||
const pageId = 6; // ID of FAQ page based on actual db
|
||||
|
||||
for (const locale of ['pl', 'de'] as const) {
|
||||
console.log(`[${locale}] Sprawdzam stan strony ${pageId}...`)
|
||||
try {
|
||||
const page = await payload.findByID({
|
||||
collection: 'pages',
|
||||
id: pageId,
|
||||
locale: locale
|
||||
})
|
||||
|
||||
const hasFaqFull = page.layout?.some(block => block.blockType === 'faqFull');
|
||||
|
||||
if (hasFaqFull) {
|
||||
console.log(`[${locale}] Migracja już wykonana - znaleziono blok faqFull. Pomijam.`)
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`[${locale}] Migruję... Dodaję faqFull...`)
|
||||
|
||||
const title = locale === 'de' ? 'Häufig gestellte Fragen' : 'Często zadawane pytania'
|
||||
const subtitle = locale === 'de' ? 'Hier finden Sie Antworten auf die wichtigsten Fragen zur Steuererklärung.' : 'Tutaj znajdziesz odpowiedzi na najważniejsze pytania dotyczące rozliczeń podatkowych.'
|
||||
|
||||
const ctaText = locale === 'de' ? 'Haben Sie noch Fragen? Schreiben Sie uns auf WhatsApp' : 'Masz więcej pytań? Napisz na WhatsApp'
|
||||
const ctaUrl = '#'; // CMS link, dynamically handled later if needed
|
||||
|
||||
const newLayout = [
|
||||
{
|
||||
blockType: 'faqFull',
|
||||
title: title,
|
||||
subtitle: subtitle,
|
||||
questions: locale === 'pl' ? questions_pl : questions_de,
|
||||
ctaText: ctaText,
|
||||
ctaUrl: ctaUrl
|
||||
} as FAQFullBlock
|
||||
]
|
||||
|
||||
await payload.update({
|
||||
collection: 'pages',
|
||||
id: pageId,
|
||||
locale: locale,
|
||||
data: {
|
||||
layout: newLayout
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`[${locale}] Zaktualizowano pomyślnie.`)
|
||||
} catch (error) {
|
||||
console.error(`[${locale}] Błąd podczas aktualizacji:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Zakończono.')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
run()
|
||||
@@ -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