feat: implement homepage blocks, animations and sticky whatsapp CTA
This commit is contained in:
+158
-21
@@ -15,6 +15,154 @@ const PAGES = [
|
|||||||
{ pl: 'Impressum', de: 'Impressum', isHome: false },
|
{ pl: 'Impressum', de: 'Impressum', isHome: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const homeLayoutPl = [
|
||||||
|
{
|
||||||
|
blockType: 'hero',
|
||||||
|
title: 'Odbierz zwrot podatku z Niemiec — rozliczymy Cię po polsku',
|
||||||
|
subtitle: 'Ponad 20 lat doświadczenia w rozliczeniach dla Polaków pracujących w Niemczech. Szybko, rzetelnie i w pełni po polsku.',
|
||||||
|
primaryCta: 'Napisz na WhatsApp',
|
||||||
|
secondaryCta: { text: 'Zobacz jak to działa', url: '/jak-to-dziala' },
|
||||||
|
yearsOfExperience: 'Ponad 20 lat doświadczenia',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'trustBar',
|
||||||
|
items: [
|
||||||
|
{ text: 'Działam w ramach b.b.h.', icon: 'shield' },
|
||||||
|
{ text: 'Doradztwo 100% po polsku', icon: 'message' },
|
||||||
|
{ text: 'Wsparcie z Finanzamt', icon: 'check' },
|
||||||
|
{ text: 'Wieloletnie doświadczenie', icon: 'star' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'uniqueValue',
|
||||||
|
title: 'Rozliczenia bez bariery językowej',
|
||||||
|
description: 'Niemieckie prawo podatkowe bywa skomplikowane. Przeprowadzę Cię przez cały proces w Twoim ojczystym języku, dbając o maksymalny zwrot i zero stresu.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'servicesList',
|
||||||
|
title: 'Czym się zajmuję',
|
||||||
|
services: [
|
||||||
|
{ title: 'Deklaracje podatkowe', description: 'Kompleksowe sporządzenie i złożenie zeznania rocznego (Einkommensteuererklärung).', linkText: 'Zobacz więcej' },
|
||||||
|
{ title: 'Kontrola decyzji', description: 'Szczegółowa weryfikacja decyzji podatkowej (Steuerbescheid) z Finanzamt.', linkText: 'Zobacz więcej' },
|
||||||
|
{ title: 'Ulgi i dopłaty', description: 'Pomoc w uzyskaniu Kindergeld, Riester-Rente oraz odliczaniu kosztów dojazdów i podwójnego gospodarstwa.', linkText: 'Zobacz więcej' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'process',
|
||||||
|
title: 'Jak to działa',
|
||||||
|
steps: [
|
||||||
|
{ title: 'Napisz na WhatsApp', description: 'Wyślij krótką wiadomość lub zadzwoń, by rozpocząć proces.' },
|
||||||
|
{ title: 'Przekaż dokumenty', description: 'Prześlij nam wymagane dokumenty bezpiecznie online lub dostarcz osobiście.' },
|
||||||
|
{ title: 'Odbierz zwrot', description: 'Zajmiemy się resztą i poinformujemy Cię o spodziewanym zwrocie z Finanzamt.' },
|
||||||
|
],
|
||||||
|
ctaText: 'Zobacz cały proces',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'affiliation',
|
||||||
|
title: 'Zaufanie i bezpieczeństwo',
|
||||||
|
description: 'Działam w ramach b.b.h. Lohnsteuerhilfeverein e.V. (Beratungsstelle). Oferujemy profesjonalną pomoc w sprawach podatku dochodowego w ramach uprawnień wynikających z §4 Nr. 11 StBerG.',
|
||||||
|
linkText: 'Poznaj stowarzyszenie',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'testimonials',
|
||||||
|
title: 'Opinie klientów',
|
||||||
|
testimonials: [
|
||||||
|
{ quote: 'Pan Janusz pomógł mi odzyskać podatek za 3 ostatnie lata. Wszystko jasno wytłumaczone po polsku.', author: 'Piotr, Monachium' },
|
||||||
|
{ quote: 'Szybki kontakt przez WhatsApp i pełne wsparcie przy odwołaniu od decyzji Finanzamt. Polecam!', author: 'Anna, Berlin' },
|
||||||
|
{ quote: 'Ogromna wiedza o ulgach dla pracowników budowlanych. Zwrot był znacznie wyższy niż się spodziewałem.', author: 'Krzysztof, Dortmund' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'faqTeaser',
|
||||||
|
title: 'Częste pytania',
|
||||||
|
questions: [
|
||||||
|
{ question: 'Czy muszę przyjeżdżać osobiście?', answer: 'Nie, wszystkie formalności możemy załatwić zdalnie, przesyłając dokumenty online.' },
|
||||||
|
{ question: 'Do kiedy muszę złożyć deklarację?', answer: 'Standardowy termin to 31 sierpnia, ale korzystając z naszych usług, termin ten wydłuża się do końca lutego następnego roku.' },
|
||||||
|
],
|
||||||
|
linkText: 'Zobacz wszystkie pytania',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'finalCta',
|
||||||
|
title: 'Gotowy na wyższy zwrot podatku?',
|
||||||
|
subtitle: 'Napisz do mnie już teraz. Odpowiem najszybciej jak to możliwe.',
|
||||||
|
buttonText: 'Rozpocznij na WhatsApp',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const homeLayoutDe = [
|
||||||
|
{
|
||||||
|
blockType: 'hero',
|
||||||
|
title: 'Sichern Sie sich Ihre Steuererstattung aus Deutschland — Beratung auf Polnisch',
|
||||||
|
subtitle: 'Über 20 Jahre Erfahrung in der Steuererklärung für in Deutschland arbeitende Polen. Schnell, zuverlässig und komplett auf Polnisch.',
|
||||||
|
primaryCta: 'Auf WhatsApp schreiben',
|
||||||
|
secondaryCta: { text: 'So funktioniert es', url: '/jak-to-dziala' },
|
||||||
|
yearsOfExperience: 'Über 20 Jahre Erfahrung',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'trustBar',
|
||||||
|
items: [
|
||||||
|
{ text: 'Teil der b.b.h.', icon: 'shield' },
|
||||||
|
{ text: '100% Beratung auf Polnisch', icon: 'message' },
|
||||||
|
{ text: 'Finanzamt Unterstützung', icon: 'check' },
|
||||||
|
{ text: 'Langjährige Erfahrung', icon: 'star' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'uniqueValue',
|
||||||
|
title: 'Steuererklärungen ohne Sprachbarriere',
|
||||||
|
description: 'Das deutsche Steuerrecht kann kompliziert sein. Ich begleite Sie durch den gesamten Prozess in Ihrer Muttersprache, sorge für die maximale Erstattung und garantiere null Stress.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'servicesList',
|
||||||
|
title: 'Was ich anbiete',
|
||||||
|
services: [
|
||||||
|
{ title: 'Steuererklärungen', description: 'Umfassende Erstellung und Einreichung der jährlichen Einkommensteuererklärung.', linkText: 'Mehr anzeigen' },
|
||||||
|
{ title: 'Steuerbescheidprüfung', description: 'Detaillierte Prüfung des Steuerbescheids vom Finanzamt.', linkText: 'Mehr anzeigen' },
|
||||||
|
{ title: 'Vergünstigungen und Zuschüsse', description: 'Hilfe bei der Beantragung von Kindergeld, Riester-Rente und der Absetzung von Fahrtkosten sowie doppelter Haushaltsführung.', linkText: 'Mehr anzeigen' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'process',
|
||||||
|
title: 'Wie es funktioniert',
|
||||||
|
steps: [
|
||||||
|
{ title: 'Auf WhatsApp schreiben', description: 'Senden Sie eine kurze Nachricht oder rufen Sie an, um den Prozess zu starten.' },
|
||||||
|
{ title: 'Dokumente einreichen', description: 'Senden Sie uns die erforderlichen Dokumente sicher online oder bringen Sie sie persönlich vorbei.' },
|
||||||
|
{ title: 'Erstattung erhalten', description: 'Wir kümmern uns um den Rest und informieren Sie über die erwartete Rückerstattung vom Finanzamt.' },
|
||||||
|
],
|
||||||
|
ctaText: 'Den gesamten Prozess ansehen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'affiliation',
|
||||||
|
title: 'Vertrauen und Sicherheit',
|
||||||
|
description: 'Ich bin im Rahmen des b.b.h. Lohnsteuerhilfevereins e.V. (Beratungsstelle) tätig. Wir bieten professionelle Hilfe in Lohnsteuersachen im Rahmen der Befugnisse nach §4 Nr. 11 StBerG.',
|
||||||
|
linkText: 'Den Verein kennenlernen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'testimonials',
|
||||||
|
title: 'Kundenrezensionen',
|
||||||
|
testimonials: [
|
||||||
|
{ quote: 'Herr Janusz half mir, meine Steuern für die letzten 3 Jahre zurückzubekommen. Alles wurde klar auf Polnisch erklärt.', author: 'Piotr, München' },
|
||||||
|
{ quote: 'Schneller Kontakt über WhatsApp und volle Unterstützung bei Einsprüchen beim Finanzamt. Sehr zu empfehlen!', author: 'Anna, Berlin' },
|
||||||
|
{ quote: 'Enormes Wissen über Vergünstigungen für Bauarbeiter. Die Erstattung war viel höher als erwartet.', author: 'Krzysztof, Dortmund' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'faqTeaser',
|
||||||
|
title: 'Häufige Fragen',
|
||||||
|
questions: [
|
||||||
|
{ question: 'Muss ich persönlich kommen?', answer: 'Nein, alle Formalitäten können online erledigt werden, indem Sie uns die Dokumente zusenden.' },
|
||||||
|
{ question: 'Bis wann muss ich die Steuererklärung einreichen?', answer: 'Die Standardfrist ist der 31. August, aber durch unsere Dienste verlängert sich diese bis Ende Februar des Folgejahres.' },
|
||||||
|
],
|
||||||
|
linkText: 'Alle Fragen ansehen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
blockType: 'finalCta',
|
||||||
|
title: 'Bereit für eine höhere Steuererstattung?',
|
||||||
|
subtitle: 'Schreiben Sie mir jetzt. Ich werde so schnell wie möglich antworten.',
|
||||||
|
buttonText: 'Auf WhatsApp starten',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
const payload = await getPayload({ config: configPromise })
|
const payload = await getPayload({ config: configPromise })
|
||||||
|
|
||||||
@@ -35,21 +183,16 @@ async function run() {
|
|||||||
data: {
|
data: {
|
||||||
title: page.pl,
|
title: page.pl,
|
||||||
slug: page.isHome ? 'home' : toSlug(page.pl),
|
slug: page.isHome ? 'home' : toSlug(page.pl),
|
||||||
layout: [
|
layout: (page.isHome ? homeLayoutPl : [
|
||||||
{
|
{
|
||||||
blockType: 'TextSection',
|
blockType: 'TextSection',
|
||||||
text: page.isHome
|
text: 'Treść w przygotowaniu',
|
||||||
? 'Witaj na nowej stronie głównej opartej o mechanizm klocków (blocks) i Payload CMS.'
|
|
||||||
: 'Treść w przygotowaniu',
|
|
||||||
}
|
}
|
||||||
]
|
]) as any
|
||||||
},
|
},
|
||||||
locale: 'pl',
|
locale: 'pl',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Rzutowanie na number: Payload domyślnie może traktować ID jako 'string | number',
|
|
||||||
// jednak przy adapterze Postgres (domyślnie serial) ID to zawsze 'number',
|
|
||||||
// czego z kolei ściśle wymaga pole relacyjne w updateGlobal.
|
|
||||||
pageIds.push(createdPage.id as unknown as number);
|
pageIds.push(createdPage.id as unknown as number);
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -59,14 +202,12 @@ async function run() {
|
|||||||
data: {
|
data: {
|
||||||
title: page.de,
|
title: page.de,
|
||||||
slug: page.isHome ? 'home' : toSlug(page.de),
|
slug: page.isHome ? 'home' : toSlug(page.de),
|
||||||
layout: [
|
layout: (page.isHome ? homeLayoutDe : [
|
||||||
{
|
{
|
||||||
blockType: 'TextSection',
|
blockType: 'TextSection',
|
||||||
text: page.isHome
|
text: 'Inhalt in Vorbereitung',
|
||||||
? 'Willkommen auf der neuen Startseite basierend auf dem Blöcke-Mechanismus und Payload CMS.'
|
|
||||||
: 'Inhalt in Vorbereitung',
|
|
||||||
}
|
}
|
||||||
]
|
]) as any
|
||||||
},
|
},
|
||||||
locale: 'de',
|
locale: 'de',
|
||||||
});
|
});
|
||||||
@@ -74,10 +215,7 @@ async function run() {
|
|||||||
console.log(`- Created ${page.pl} / ${page.de}`);
|
console.log(`- Created ${page.pl} / ${page.de}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dodanie ich do globalnego nagłówka (pominąć 'Stowarzyszenie' i strony prawne jako główne menu wg życzenia)
|
|
||||||
// "Domyślna zawartość na start: Strona główna / Usługi / Jak się umówić / O mnie / FAQ / Kontakt"
|
|
||||||
console.log('Seeding global header...')
|
console.log('Seeding global header...')
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await payload.updateGlobal({
|
await payload.updateGlobal({
|
||||||
slug: 'header',
|
slug: 'header',
|
||||||
@@ -91,7 +229,7 @@ async function run() {
|
|||||||
await payload.updateGlobal({
|
await payload.updateGlobal({
|
||||||
slug: 'company',
|
slug: 'company',
|
||||||
data: {
|
data: {
|
||||||
address: 'Beratungsstelle b.b.h.\nLohnsteuerhilfeverein e.V.\nJanusz Kedzierski\nPrzykładowa ulica 123\n12345 Przykładowe Miasto',
|
address: 'DO POTWIERDZENIA PRZEZ KLIENTA',
|
||||||
phone: '+49 123 456 789',
|
phone: '+49 123 456 789',
|
||||||
email: '[email protected]',
|
email: '[email protected]',
|
||||||
hours: 'Pon-Pt: 9:00 - 17:00\nPo wcześniejszym umówieniu',
|
hours: 'Pon-Pt: 9:00 - 17:00\nPo wcześniejszym umówieniu',
|
||||||
@@ -100,7 +238,6 @@ async function run() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log('Seeding global footer...')
|
console.log('Seeding global footer...')
|
||||||
// Wszystkie pozycje z headera + 3 strony prawne (Indeksy: od 0 do 5 oraz od 7 do 9)
|
|
||||||
const footerNavIndices = [0, 1, 2, 3, 4, 5, 7, 8, 9];
|
const footerNavIndices = [0, 1, 2, 3, 4, 5, 7, 8, 9];
|
||||||
const footerNavPages = footerNavIndices.map(index => ({ page: pageIds[index] }));
|
const footerNavPages = footerNavIndices.map(index => ({ page: pageIds[index] }));
|
||||||
|
|
||||||
@@ -110,7 +247,7 @@ async function run() {
|
|||||||
data: {
|
data: {
|
||||||
navigation: footerNavPages,
|
navigation: footerNavPages,
|
||||||
association: {
|
association: {
|
||||||
title: 'Członek b.b.h. Lohnsteuerhilfeverein e.V.',
|
title: 'Działam w ramach b.b.h. Lohnsteuerhilfeverein e.V.',
|
||||||
link: 'https://bbh-lohnsteuerhilfe.de',
|
link: 'https://bbh-lohnsteuerhilfe.de',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,10 +258,10 @@ async function run() {
|
|||||||
await payload.updateGlobal({
|
await payload.updateGlobal({
|
||||||
slug: 'site-settings',
|
slug: 'site-settings',
|
||||||
data: {
|
data: {
|
||||||
siteName: 'Janusz Kędzierski',
|
siteName: 'Finanz- & Lohnbuchhaltung Dipl.-Ing. Janusz Kędzierski',
|
||||||
titleOrder: 'page-first',
|
titleOrder: 'page-first',
|
||||||
titleSeparator: '|',
|
titleSeparator: '|',
|
||||||
homepage: pageIds[0], // Pierwsza strona to Strona główna (isHome: true)
|
homepage: pageIds[0],
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { resolveRoute } from '@intecion/ipal-kit'
|
|||||||
import { getLocalizedSlugs } from '@intecion/ipal-kit'
|
import { getLocalizedSlugs } from '@intecion/ipal-kit'
|
||||||
import { i18n } from '../../../../i18n.config'
|
import { i18n } from '../../../../i18n.config'
|
||||||
import { SetLanguageSlugs } from '../../../../components/LanguageSlugsProvider'
|
import { SetLanguageSlugs } from '../../../../components/LanguageSlugsProvider'
|
||||||
|
import { RenderBlocks } from '../../../../blocks/RenderBlocks'
|
||||||
|
|
||||||
export default async function Page(props: {
|
export default async function Page(props: {
|
||||||
params: Promise<{ locale: string; slug?: string[] }>
|
params: Promise<{ locale: string; slug?: string[] }>
|
||||||
@@ -57,16 +58,9 @@ export default async function Page(props: {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{layout.map((block: any, index: number) => {
|
<main>
|
||||||
if (block.blockType === 'TextSection') {
|
<RenderBlocks blocks={layout} />
|
||||||
return (
|
</main>
|
||||||
<div key={index} className="container-premium py-12">
|
|
||||||
<p className="text-lg">{block.text}</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return <div key={index}>Nieznany blok: {block.blockType}</div>
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const inter = Inter({ subsets: ['latin'], variable: '--font-sans' })
|
|||||||
const outfit = Outfit({ subsets: ['latin'], variable: '--font-heading' })
|
const outfit = Outfit({ subsets: ['latin'], variable: '--font-heading' })
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'Janusz Kędzierski - Beratungsstelle b.b.h. Lohnsteuerhilfeverein e.V.',
|
title: 'Finanz- & Lohnbuchhaltung Dipl.-Ing. Janusz Kędzierski',
|
||||||
description: 'Profesjonalne doradztwo podatkowe.',
|
description: 'Profesjonalne doradztwo podatkowe.',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,6 +57,19 @@ export default async function RootLayout({
|
|||||||
<Header navItems={headerNavItems} />
|
<Header navItems={headerNavItems} />
|
||||||
<main className="min-h-screen pt-24">{children}</main>
|
<main className="min-h-screen pt-24">{children}</main>
|
||||||
<Footer navItems={footerNavItems} companyInfo={companyData} association={footerData.association} />
|
<Footer navItems={footerNavItems} companyInfo={companyData} association={footerData.association} />
|
||||||
|
|
||||||
|
{/* Floating WhatsApp Button */}
|
||||||
|
<a
|
||||||
|
href={process.env.NEXT_PUBLIC_WHATSAPP_LINK || '#'}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="fixed bottom-6 right-6 z-50 flex items-center justify-center w-14 h-14 bg-green-500 hover:bg-green-600 text-white rounded-full shadow-lg shadow-green-500/30 transition-transform hover:scale-110 animate-in fade-in slide-in-from-bottom-10 duration-500"
|
||||||
|
aria-label="Napisz na WhatsApp"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
</LanguageSlugsProvider>
|
</LanguageSlugsProvider>
|
||||||
</Providers>
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import type { Page } from '../../payload-types'
|
||||||
|
|
||||||
|
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'affiliation' }>
|
||||||
|
|
||||||
|
export const AffiliationBlock: React.FC<Props> = ({ title, description, logo, linkText }) => {
|
||||||
|
return (
|
||||||
|
<section className="py-24 bg-slate-50 border-t border-slate-200">
|
||||||
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="bg-white p-8 md:p-12 rounded-2xl shadow-sm border border-slate-100 flex flex-col md:flex-row items-center gap-10"
|
||||||
|
>
|
||||||
|
<div className="flex-1 text-center md:text-left">
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold text-slate-900 mb-4">{title}</h2>
|
||||||
|
<p className="text-slate-600 mb-6 leading-relaxed text-sm md:text-base">{description}</p>
|
||||||
|
{linkText && (
|
||||||
|
<Link href="/stowarzyszenie" className="text-blue-600 font-semibold hover:text-blue-700 underline underline-offset-4">
|
||||||
|
{linkText}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{logo && typeof logo === 'object' && logo.url && (
|
||||||
|
<div className="w-48 shrink-0 flex items-center justify-center">
|
||||||
|
<img src={logo.url} alt={logo.alt || "Logo stowarzyszenia"} className="max-w-full h-auto grayscale hover:grayscale-0 transition-all duration-300" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const Affiliation: Block = {
|
||||||
|
slug: 'affiliation',
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', type: 'text', required: true, localized: true },
|
||||||
|
{ name: 'description', type: 'textarea', required: true, localized: true },
|
||||||
|
{ name: 'logo', type: 'upload', relationTo: 'media' },
|
||||||
|
{ name: 'linkText', type: 'text', localized: true },
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import type { Page } from '../../payload-types'
|
||||||
|
|
||||||
|
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'faqTeaser' }>
|
||||||
|
|
||||||
|
export const FAQTeaserBlock: React.FC<Props> = ({ title, questions, linkText }) => {
|
||||||
|
return (
|
||||||
|
<section className="py-24 bg-slate-50">
|
||||||
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
|
<motion.h2
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-3xl md:text-5xl font-bold text-slate-900 mb-12 text-center"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<div className="space-y-6 mb-12">
|
||||||
|
{questions?.map((q: any, index: number) => (
|
||||||
|
<motion.div
|
||||||
|
key={q.id || index}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.4, delay: index * 0.1 }}
|
||||||
|
className="bg-white p-6 md:p-8 rounded-2xl shadow-sm border border-slate-100"
|
||||||
|
>
|
||||||
|
<h3 className="text-xl font-bold text-slate-900 mb-3">{q.question}</h3>
|
||||||
|
<p className="text-slate-600">{q.answer}</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{linkText && (
|
||||||
|
<div className="text-center">
|
||||||
|
<Link href="/faq" className="inline-block text-blue-600 font-semibold hover:text-blue-700 underline underline-offset-4">
|
||||||
|
{linkText}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const FAQTeaser: Block = {
|
||||||
|
slug: 'faqTeaser',
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', type: 'text', required: true, localized: true },
|
||||||
|
{
|
||||||
|
name: 'questions',
|
||||||
|
type: 'array',
|
||||||
|
minRows: 1,
|
||||||
|
localized: true,
|
||||||
|
fields: [
|
||||||
|
{ name: 'question', type: 'text', required: true },
|
||||||
|
{ name: 'answer', type: 'textarea', required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ name: 'linkText', type: 'text', localized: true },
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import type { Page } from '../../payload-types'
|
||||||
|
|
||||||
|
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'finalCta' }>
|
||||||
|
|
||||||
|
export const FinalCTABlock: React.FC<Props> = ({ title, subtitle, buttonText }) => {
|
||||||
|
return (
|
||||||
|
<section className="py-32 bg-blue-600 text-white text-center">
|
||||||
|
<div className="container mx-auto px-4 max-w-3xl">
|
||||||
|
<motion.h2
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
whileInView={{ opacity: 1, scale: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
className="text-4xl md:text-5xl font-bold mb-6"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</motion.h2>
|
||||||
|
{subtitle && (
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.1 }}
|
||||||
|
className="text-xl md:text-2xl text-blue-100 mb-12"
|
||||||
|
>
|
||||||
|
{subtitle}
|
||||||
|
</motion.p>
|
||||||
|
)}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={process.env.NEXT_PUBLIC_WHATSAPP_LINK || '#'}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-block px-10 py-5 bg-green-500 hover:bg-green-400 text-white text-lg font-bold rounded-xl transition-transform hover:scale-105 shadow-lg shadow-green-500/30"
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
</a>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const FinalCTA: Block = {
|
||||||
|
slug: 'finalCta',
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', type: 'text', required: true, localized: true },
|
||||||
|
{ name: 'subtitle', type: 'text', localized: true },
|
||||||
|
{ name: 'buttonText', type: 'text', required: true, localized: true },
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import type { Page } from '../../payload-types'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'hero' }>
|
||||||
|
|
||||||
|
export const HeroBlock: React.FC<Props> = ({ title, subtitle, primaryCta, secondaryCta, yearsOfExperience }) => {
|
||||||
|
return (
|
||||||
|
<section className="relative overflow-hidden bg-slate-900 text-white py-24 lg:py-32">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-blue-900/40 to-slate-900 z-0" />
|
||||||
|
<div className="container mx-auto px-4 relative z-10 flex flex-col items-center text-center">
|
||||||
|
<motion.h1
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="text-4xl md:text-6xl font-bold mb-6 max-w-4xl"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.1 }}
|
||||||
|
className="text-xl md:text-2xl text-slate-300 mb-10 max-w-2xl"
|
||||||
|
>
|
||||||
|
{subtitle}
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
|
className="flex flex-col sm:flex-row gap-4 mb-12"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={process.env.NEXT_PUBLIC_WHATSAPP_LINK || '#'}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="px-8 py-4 bg-green-500 hover:bg-green-600 text-white font-semibold rounded-lg transition-colors flex items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
{primaryCta}
|
||||||
|
</a>
|
||||||
|
{secondaryCta?.text && secondaryCta?.url && (
|
||||||
|
<Link
|
||||||
|
href={secondaryCta.url}
|
||||||
|
className="px-8 py-4 bg-transparent border-2 border-slate-600 hover:border-slate-400 text-white font-semibold rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
{secondaryCta.text}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{yearsOfExperience && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.4 }}
|
||||||
|
className="text-sm text-slate-400 uppercase tracking-widest"
|
||||||
|
>
|
||||||
|
{yearsOfExperience}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const Hero: Block = {
|
||||||
|
slug: 'hero',
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', type: 'text', required: true, localized: true },
|
||||||
|
{ name: 'subtitle', type: 'text', required: true, localized: true },
|
||||||
|
{ name: 'primaryCta', type: 'text', required: true, localized: true },
|
||||||
|
{
|
||||||
|
name: 'secondaryCta',
|
||||||
|
type: 'group',
|
||||||
|
fields: [
|
||||||
|
{ name: 'text', type: 'text', localized: true },
|
||||||
|
{ name: 'url', type: 'text' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ name: 'yearsOfExperience', type: 'text', localized: true },
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import type { Page } from '../../payload-types'
|
||||||
|
|
||||||
|
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'process' }>
|
||||||
|
|
||||||
|
export const ProcessBlock: React.FC<Props> = ({ title, steps, ctaText }) => {
|
||||||
|
return (
|
||||||
|
<section className="py-24 bg-slate-900 text-white">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<motion.h2
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-3xl md:text-5xl font-bold mb-16 text-center"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<div className="flex flex-col md:flex-row gap-8 relative">
|
||||||
|
{/* Linia łącząca na desktopie */}
|
||||||
|
<div className="hidden md:block absolute top-6 left-[10%] right-[10%] h-0.5 bg-slate-800 z-0" />
|
||||||
|
|
||||||
|
{steps?.map((step: any, index: number) => (
|
||||||
|
<motion.div
|
||||||
|
key={step.id || index}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: index * 0.15 }}
|
||||||
|
className="flex-1 relative z-10 text-center md:text-left flex flex-col items-center md:items-start"
|
||||||
|
>
|
||||||
|
<div className="w-12 h-12 rounded-full bg-blue-600 border-4 border-slate-900 flex items-center justify-center text-xl font-bold mb-6">
|
||||||
|
{index + 1}
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold mb-3">{step.title}</h3>
|
||||||
|
<p className="text-slate-400 text-center md:text-left">{step.description}</p>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ctaText && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.6 }}
|
||||||
|
className="mt-16 text-center"
|
||||||
|
>
|
||||||
|
<Link href="/jak-to-dziala" className="inline-block px-8 py-4 border-2 border-slate-700 hover:border-slate-500 rounded-lg text-white font-semibold transition-colors">
|
||||||
|
{ctaText}
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const Process: Block = {
|
||||||
|
slug: 'process',
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', type: 'text', required: true, localized: true },
|
||||||
|
{
|
||||||
|
name: 'steps',
|
||||||
|
type: 'array',
|
||||||
|
minRows: 1,
|
||||||
|
maxRows: 3,
|
||||||
|
localized: true,
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', type: 'text', required: true },
|
||||||
|
{ name: 'description', type: 'textarea', required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ name: 'ctaText', type: 'text', localized: true },
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import React, { Fragment } from 'react'
|
||||||
|
import type { Page } from '../payload-types'
|
||||||
|
|
||||||
|
import { HeroBlock } from './Hero/Component'
|
||||||
|
import { TrustBarBlock } from './TrustBar/Component'
|
||||||
|
import { UniqueValueBlock } from './UniqueValue/Component'
|
||||||
|
import { ServicesListBlock } from './ServicesList/Component'
|
||||||
|
import { ProcessBlock } from './Process/Component'
|
||||||
|
import { AffiliationBlock } from './Affiliation/Component'
|
||||||
|
import { TestimonialsBlock } from './Testimonials/Component'
|
||||||
|
import { FAQTeaserBlock } from './FAQTeaser/Component'
|
||||||
|
import { FinalCTABlock } from './FinalCTA/Component'
|
||||||
|
|
||||||
|
const blockComponents = {
|
||||||
|
hero: HeroBlock,
|
||||||
|
trustBar: TrustBarBlock,
|
||||||
|
uniqueValue: UniqueValueBlock,
|
||||||
|
servicesList: ServicesListBlock,
|
||||||
|
process: ProcessBlock,
|
||||||
|
affiliation: AffiliationBlock,
|
||||||
|
testimonials: TestimonialsBlock,
|
||||||
|
faqTeaser: FAQTeaserBlock,
|
||||||
|
finalCta: FinalCTABlock,
|
||||||
|
TextSection: ({ text }: { text: string }) => <section className="py-20"><div className="container mx-auto px-4"><p>{text}</p></div></section>,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RenderBlocks: React.FC<{
|
||||||
|
blocks: Page['layout']
|
||||||
|
}> = (props) => {
|
||||||
|
const { blocks } = props
|
||||||
|
|
||||||
|
const hasBlocks = blocks && Array.isArray(blocks) && blocks.length > 0
|
||||||
|
|
||||||
|
if (hasBlocks) {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{blocks.map((block, index) => {
|
||||||
|
const { blockType } = block
|
||||||
|
|
||||||
|
if (blockType && blockType in blockComponents) {
|
||||||
|
const Block = blockComponents[blockType as keyof typeof blockComponents]
|
||||||
|
if (Block) {
|
||||||
|
return (
|
||||||
|
<Block key={index} id={`block-${index}`} {...(block as any)} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})}
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { ArrowRight } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import type { Page } from '../../payload-types'
|
||||||
|
|
||||||
|
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'servicesList' }>
|
||||||
|
|
||||||
|
export const ServicesListBlock: React.FC<Props> = ({ title, services }) => {
|
||||||
|
return (
|
||||||
|
<section className="py-24 bg-white">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<motion.h2
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-3xl md:text-5xl font-bold text-slate-900 mb-16 text-center"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{services?.map((service: any, index: number) => (
|
||||||
|
<motion.div
|
||||||
|
key={service.id || index}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||||
|
className="group"
|
||||||
|
>
|
||||||
|
<Link href="/uslugi" className="block h-full p-8 rounded-2xl bg-slate-50 border border-slate-100 transition-all duration-300 hover:-translate-y-2 hover:shadow-xl hover:bg-white">
|
||||||
|
<h3 className="text-xl font-bold text-slate-900 mb-4">{service.title}</h3>
|
||||||
|
<p className="text-slate-600 mb-8">{service.description}</p>
|
||||||
|
<div className="flex items-center text-blue-600 font-semibold group-hover:text-blue-700 mt-auto">
|
||||||
|
{service.linkText}
|
||||||
|
<ArrowRight size={20} className="ml-2 transition-transform group-hover:translate-x-1" />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const ServicesList: Block = {
|
||||||
|
slug: 'servicesList',
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', type: 'text', required: true, localized: true },
|
||||||
|
{
|
||||||
|
name: 'services',
|
||||||
|
type: 'array',
|
||||||
|
minRows: 1,
|
||||||
|
localized: true,
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', type: 'text', required: true },
|
||||||
|
{ name: 'description', type: 'textarea', required: true },
|
||||||
|
{ name: 'linkText', type: 'text', required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Quote } from 'lucide-react'
|
||||||
|
import type { Page } from '../../payload-types'
|
||||||
|
|
||||||
|
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'testimonials' }>
|
||||||
|
|
||||||
|
export const TestimonialsBlock: React.FC<Props> = ({ title, testimonials }) => {
|
||||||
|
return (
|
||||||
|
<section className="py-24 bg-white">
|
||||||
|
<div className="container mx-auto px-4 max-w-6xl">
|
||||||
|
<motion.h2
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
className="text-3xl md:text-5xl font-bold text-slate-900 mb-16 text-center"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</motion.h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{testimonials?.map((t: any, index: number) => (
|
||||||
|
<motion.div
|
||||||
|
key={t.id || index}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||||
|
className="bg-slate-50 p-8 rounded-2xl relative"
|
||||||
|
>
|
||||||
|
<Quote className="absolute top-6 right-6 text-slate-200" size={40} />
|
||||||
|
<p className="text-slate-700 italic mb-6 relative z-10">"{t.quote}"</p>
|
||||||
|
<div className="font-semibold text-slate-900">— {t.author}</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const Testimonials: Block = {
|
||||||
|
slug: 'testimonials',
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', type: 'text', required: true, localized: true },
|
||||||
|
{
|
||||||
|
name: 'testimonials',
|
||||||
|
type: 'array',
|
||||||
|
localized: true,
|
||||||
|
fields: [
|
||||||
|
{ name: 'quote', type: 'textarea', required: true },
|
||||||
|
{ name: 'author', type: 'text', required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Shield, CheckCircle, MessageCircle, Star } from 'lucide-react'
|
||||||
|
import type { Page } from '../../payload-types'
|
||||||
|
|
||||||
|
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'trustBar' }>
|
||||||
|
|
||||||
|
const iconMap = {
|
||||||
|
shield: Shield,
|
||||||
|
check: CheckCircle,
|
||||||
|
message: MessageCircle,
|
||||||
|
star: Star,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TrustBarBlock: React.FC<Props> = ({ items }) => {
|
||||||
|
if (!items || items.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="bg-slate-100 py-8 border-b border-slate-200">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 text-center">
|
||||||
|
{items.map((item: any, index: number) => {
|
||||||
|
const Icon = item.icon ? iconMap[item.icon as keyof typeof iconMap] : CheckCircle
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={item.id || index}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: index * 0.1 }}
|
||||||
|
className="flex flex-col items-center justify-center gap-3"
|
||||||
|
>
|
||||||
|
<div className="text-blue-600">
|
||||||
|
<Icon size={28} />
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-slate-700">{item.text}</span>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const TrustBar: Block = {
|
||||||
|
slug: 'trustBar',
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'items',
|
||||||
|
type: 'array',
|
||||||
|
minRows: 1,
|
||||||
|
maxRows: 4,
|
||||||
|
localized: true,
|
||||||
|
fields: [
|
||||||
|
{ name: 'text', type: 'text', required: true },
|
||||||
|
{ name: 'icon', type: 'select', options: ['shield', 'check', 'message', 'star'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
'use client'
|
||||||
|
import React from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import type { Page } from '../../payload-types'
|
||||||
|
|
||||||
|
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'uniqueValue' }>
|
||||||
|
|
||||||
|
export const UniqueValueBlock: React.FC<Props> = ({ title, description }) => {
|
||||||
|
return (
|
||||||
|
<section className="py-24 bg-blue-50 relative overflow-hidden">
|
||||||
|
<div className="absolute top-0 right-0 -mr-20 -mt-20 w-64 h-64 rounded-full bg-blue-100 opacity-50 blur-3xl" />
|
||||||
|
<div className="container mx-auto px-4 relative z-10 text-center max-w-3xl">
|
||||||
|
<motion.h2
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="text-3xl md:text-5xl font-bold text-slate-900 mb-8"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</motion.h2>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
|
className="text-lg md:text-xl text-slate-700 leading-relaxed"
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const UniqueValue: Block = {
|
||||||
|
slug: 'uniqueValue',
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', type: 'text', required: true, localized: true },
|
||||||
|
{ name: 'description', type: 'textarea', required: true, localized: true },
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -1,5 +1,14 @@
|
|||||||
import type { CollectionConfig } from 'payload'
|
import type { CollectionConfig } from 'payload'
|
||||||
import { buildSlugField } from '@intecion/ipal-kit'
|
import { buildSlugField } from '@intecion/ipal-kit'
|
||||||
|
import { Hero } from '../blocks/Hero/config'
|
||||||
|
import { TrustBar } from '../blocks/TrustBar/config'
|
||||||
|
import { UniqueValue } from '../blocks/UniqueValue/config'
|
||||||
|
import { ServicesList } from '../blocks/ServicesList/config'
|
||||||
|
import { Process } from '../blocks/Process/config'
|
||||||
|
import { Affiliation } from '../blocks/Affiliation/config'
|
||||||
|
import { Testimonials } from '../blocks/Testimonials/config'
|
||||||
|
import { FAQTeaser } from '../blocks/FAQTeaser/config'
|
||||||
|
import { FinalCTA } from '../blocks/FinalCTA/config'
|
||||||
|
|
||||||
export const Pages: CollectionConfig = {
|
export const Pages: CollectionConfig = {
|
||||||
slug: 'pages',
|
slug: 'pages',
|
||||||
@@ -22,6 +31,15 @@ export const Pages: CollectionConfig = {
|
|||||||
type: 'blocks',
|
type: 'blocks',
|
||||||
minRows: 1,
|
minRows: 1,
|
||||||
blocks: [
|
blocks: [
|
||||||
|
Hero,
|
||||||
|
TrustBar,
|
||||||
|
UniqueValue,
|
||||||
|
ServicesList,
|
||||||
|
Process,
|
||||||
|
Affiliation,
|
||||||
|
Testimonials,
|
||||||
|
FAQTeaser,
|
||||||
|
FinalCTA,
|
||||||
{
|
{
|
||||||
slug: 'TextSection',
|
slug: 'TextSection',
|
||||||
fields: [
|
fields: [
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ export function Footer({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-heading font-bold text-lg leading-tight tracking-tight text-foreground">
|
<span className="font-heading font-bold text-lg leading-tight tracking-tight text-foreground">
|
||||||
Janusz Kędzierski
|
Finanz- & Lohnbuchhaltung
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground uppercase tracking-widest font-medium">
|
<span className="text-xs text-muted-foreground uppercase tracking-widest font-medium">
|
||||||
Doradztwo Podatkowe
|
Dipl.-Ing. Janusz Kędzierski
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -131,7 +131,7 @@ export function Footer({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="container-premium mt-12 pt-8 border-t border-border/50 flex flex-col sm:flex-row justify-between items-center gap-4 text-xs text-muted-foreground">
|
<div className="container-premium mt-12 pt-8 border-t border-border/50 flex flex-col sm:flex-row justify-between items-center gap-4 text-xs text-muted-foreground">
|
||||||
<p>© {new Date().getFullYear()} Janusz Kędzierski. {currentLocaleCode === 'de' ? 'Alle Rechte vorbehalten.' : 'Wszelkie prawa zastrzeżone.'}</p>
|
<p>© {new Date().getFullYear()} Finanz- & Lohnbuchhaltung Dipl.-Ing. Janusz Kędzierski. {currentLocaleCode === 'de' ? 'Alle Rechte vorbehalten.' : 'Wszelkie prawa zastrzeżone.'}</p>
|
||||||
<p className="flex items-center gap-1">
|
<p className="flex items-center gap-1">
|
||||||
{currentLocaleCode === 'de' ? 'Erstellt von' : 'Realizacja:'}
|
{currentLocaleCode === 'de' ? 'Erstellt von' : 'Realizacja:'}
|
||||||
<a href="#" className="font-medium hover:text-foreground transition-colors">Intecion</a>
|
<a href="#" className="font-medium hover:text-foreground transition-colors">Intecion</a>
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ export function Header({ navItems = [] }: { navItems?: any[] }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-heading font-bold text-lg leading-tight tracking-tight text-foreground">
|
<span className="font-heading font-bold text-lg leading-tight tracking-tight text-foreground">
|
||||||
Janusz Kędzierski
|
Finanz- & Lohnbuchhaltung
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-muted-foreground uppercase tracking-widest font-medium">
|
<span className="text-xs text-muted-foreground uppercase tracking-widest font-medium">
|
||||||
Doradztwo Podatkowe
|
Dipl.-Ing. Janusz Kędzierski
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
+222
-1
@@ -212,12 +212,118 @@ export interface Page {
|
|||||||
*/
|
*/
|
||||||
slug: string;
|
slug: string;
|
||||||
layout?:
|
layout?:
|
||||||
|
| (
|
||||||
|
| {
|
||||||
|
title: string;
|
||||||
|
subtitle: string;
|
||||||
|
primaryCta: string;
|
||||||
|
secondaryCta?: {
|
||||||
|
text?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
};
|
||||||
|
yearsOfExperience?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'hero';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
items?:
|
||||||
|
| {
|
||||||
|
text: string;
|
||||||
|
icon?: ('shield' | 'check' | 'message' | 'star') | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'trustBar';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'uniqueValue';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
title: string;
|
||||||
|
services?:
|
||||||
|
| {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
linkText: string;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'servicesList';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
title: string;
|
||||||
|
steps?:
|
||||||
|
| {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
ctaText?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'process';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
logo?: (number | null) | Media;
|
||||||
|
linkText?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'affiliation';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
title: string;
|
||||||
|
testimonials?:
|
||||||
|
| {
|
||||||
|
quote: string;
|
||||||
|
author: string;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'testimonials';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
title: string;
|
||||||
|
questions?:
|
||||||
|
| {
|
||||||
|
question: string;
|
||||||
|
answer: string;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
linkText?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'faqTeaser';
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string | null;
|
||||||
|
buttonText: string;
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'finalCta';
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
text?: string | null;
|
text?: string | null;
|
||||||
id?: string | null;
|
id?: string | null;
|
||||||
blockName?: string | null;
|
blockName?: string | null;
|
||||||
blockType: 'TextSection';
|
blockType: 'TextSection';
|
||||||
}[]
|
}
|
||||||
|
)[]
|
||||||
| null;
|
| null;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
@@ -391,6 +497,121 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
layout?:
|
layout?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
|
hero?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
subtitle?: T;
|
||||||
|
primaryCta?: T;
|
||||||
|
secondaryCta?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
text?: T;
|
||||||
|
url?: T;
|
||||||
|
};
|
||||||
|
yearsOfExperience?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
trustBar?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
items?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
text?: T;
|
||||||
|
icon?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
uniqueValue?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
description?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
servicesList?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
services?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
description?: T;
|
||||||
|
linkText?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
process?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
steps?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
description?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
ctaText?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
affiliation?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
description?: T;
|
||||||
|
logo?: T;
|
||||||
|
linkText?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
testimonials?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
testimonials?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
quote?: T;
|
||||||
|
author?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
faqTeaser?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
questions?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
question?: T;
|
||||||
|
answer?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
linkText?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
|
finalCta?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
subtitle?: T;
|
||||||
|
buttonText?: T;
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
TextSection?:
|
TextSection?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
|
|||||||
Reference in New Issue
Block a user