Fix FAQFull: Use isDraft boolean for draft filtering, fix Kontakt ID override

This commit is contained in:
MichalC
2026-09-16 12:31:19 +02:00
parent 437e1e4823
commit 9043301da6
140 changed files with 23337 additions and 724 deletions
@@ -11,14 +11,6 @@ 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[] }>
}) {
@@ -62,7 +54,7 @@ export default async function Page(props: {
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));
const validQuestions = faqBlock.questions.filter((q: any) => !q.isDraft);
if (validQuestions.length > 0) {
faqSchema = {
"@context": "https://schema.org",
+71 -57
View File
@@ -1,12 +1,18 @@
import React from 'react'
import { Public_Sans, Fraunces } from 'next/font/google'
import { Providers } from '../../components/Providers'
import { LanguageSlugsProvider } from '../../components/LanguageSlugsProvider'
import { Header } from '../../components/Header'
import { Footer } from '../../components/Footer'
import { Providers } from '../../../components/Providers'
import { LanguageSlugsProvider } from '../../../components/LanguageSlugsProvider'
import { Header } from '../../../components/Header'
import { Footer } from '../../../components/Footer'
import { getPayload } from 'payload'
import configPromise from '@payload-config'
import '../globals.css'
import type { Config, Page } from '../../../payload-types'
import { getGlobal } from '../../../lib/content'
import '../../globals.css'
import { ConsentProvider, CookieBanner, CookieButton } from '@intecion/ipal-kit/client'
import { FloatingWhatsApp } from '../../../components/FloatingWhatsApp'
import { getConsentTexts } from '@intecion/ipal-kit'
import { i18n } from '../../../i18n.config'
const publicSans = Public_Sans({ subsets: ['latin'], variable: '--font-sans' })
const fraunces = Fraunces({
@@ -19,66 +25,74 @@ export const metadata = {
description: 'Profesjonalne doradztwo podatkowe.',
}
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
export default async function RootLayout(props: {
children: React.ReactNode,
params: Promise<{ locale: string }>
}) {
const { children } = props;
const { locale } = await props.params;
const payload = await getPayload({ config: configPromise })
const typedLocale = locale as Config['locale']
// Pobieramy Globals z locale 'all', by od razu otrzymać zlokalizowane dane we wszystkich obsługiwanych językach
// Używamy overrideAccess: false, żeby Payload nie zwracał stron, które nie są opublikowane
const [headerData, footerData, companyData] = await Promise.all([
payload.findGlobal({
slug: 'header',
locale: 'all',
depth: 1,
overrideAccess: false,
}),
payload.findGlobal({
slug: 'footer',
locale: 'all',
depth: 1,
overrideAccess: false,
}),
payload.findGlobal({
slug: 'company',
locale: 'all',
overrideAccess: false,
}),
const [headerData, footerData, companyData, siteData] = await Promise.all([
getGlobal('header', 1, typedLocale),
getGlobal('footer', 1, typedLocale),
getGlobal('company', 0, typedLocale),
getGlobal('site-settings', 1, typedLocale),
]);
// Filtrujemy tylko poprawnie spopulowane strony (chroni przed brakiem dostępu lub usuniętymi relacjami) lub linki anchor
const headerNavItems = (headerData.navigation || []).filter((item: any) => (item.page && typeof item.page === 'object' && item.page !== null) || item.anchor);
const footerNavItems = (footerData.navigation || []).map((item: any) => item.page).filter((page: any) => typeof page === 'object' && page !== null);
const headerNavItems = (headerData?.navigation || []).filter((item) => (item.page && typeof item.page === 'object' && item.page !== null) || item.anchor);
const footerNavItems = (footerData?.navigation || []).map((item) => item.page).filter((page): page is Page => typeof page === 'object' && page !== null);
const consentTexts = await getConsentTexts({
payload,
config: i18n,
locale: locale,
privacyPolicy: { label: locale === 'de' ? 'Datenschutzerklärung / Cookies' : 'Polityka prywatności / Cookies', page: siteData?.cookiePolicy as unknown as { slug?: Record<string, unknown> } | null },
});
const cookieButtonAriaLabel = locale === 'de' ? 'Cookie-Einstellungen' : 'Ustawienia cookies';
const whatsappAriaLabel = locale === 'de' ? 'Auf WhatsApp schreiben' : 'Napisz na WhatsApp';
// CMS: fallback to generic link if no number is configured.
const whatsappLink = companyData?.whatsappNumber ? `https://wa.me/${companyData.whatsappNumber}` : '#';
return (
<html lang="pl" suppressHydrationWarning className={`${publicSans.variable} ${fraunces.variable}`}>
<body>
<Providers>
<LanguageSlugsProvider>
<Header navItems={headerNavItems} />
<main className="min-h-screen pt-24">{children}</main>
<Footer
navItems={footerNavItems}
companyInfo={companyData}
association={footerData.association}
copyright={footerData.copyright}
creator={footerData.creator}
/>
<html lang={locale} suppressHydrationWarning className={`${publicSans.variable} ${fraunces.variable}`}>
<body className="font-sans antialiased bg-background text-foreground min-h-screen flex flex-col">
<Providers>
<LanguageSlugsProvider>
<ConsentProvider texts={consentTexts}>
<Header
navItems={headerNavItems}
whatsappNumber={companyData?.whatsappNumber}
/>
<div className="flex-1">
{children}
</div>
<Footer
navItems={footerNavItems}
companyInfo={companyData}
association={footerData?.association}
copyright={footerData?.copyright}
creator={footerData?.creator}
/>
<div className="ipal-banner-wrapper">
<CookieBanner />
</div>
<div className="fixed bottom-6 left-6 z-40">
<CookieButton aria-label={cookieButtonAriaLabel} className="w-12 h-12 rounded-full bg-background border border-border shadow-md flex items-center justify-center text-foreground hover:bg-card transition-colors [&>svg]:w-6 [&>svg]:h-6 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2" />
</div>
</ConsentProvider>
{/* 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>
{companyData?.whatsappNumber && (
<FloatingWhatsApp
whatsappLink={whatsappLink}
ariaLabel={locale === 'de' ? 'Auf WhatsApp schreiben' : 'Napisz do nas na WhatsApp'}
/>
)}
</LanguageSlugsProvider>
</Providers>
</body>
+46
View File
@@ -1,9 +1,55 @@
import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc'
import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client'
import { MaskedField as MaskedField_630f031911beff97691b963f10a20a86 } from '@intecion/ipal-kit/client'
import { TestEmailButton as TestEmailButton_630f031911beff97691b963f10a20a86 } from '@intecion/ipal-kit/client'
import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc'
/** @type import('payload').ImportMap */
export const importMap = {
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e,
"@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864,
"@intecion/ipal-kit/client#MaskedField": MaskedField_630f031911beff97691b963f10a20a86,
"@intecion/ipal-kit/client#TestEmailButton": TestEmailButton_630f031911beff97691b963f10a20a86,
"@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1
+13 -1
View File
@@ -61,7 +61,8 @@ h1, h2, h3, h4, h5, h6 {
/* Stylizacja elementów interaktywnych dla "premium feel" */
.btn-primary {
@apply inline-flex items-center justify-center rounded-none px-6 py-3 font-medium text-white transition-all duration-300;
@apply inline-flex items-center justify-center px-6 py-3 font-medium text-white transition-all duration-300;
border-radius: var(--radius-button);
background-color: var(--accent-color);
}
.btn-primary:hover {
@@ -73,3 +74,14 @@ h1, h2, h3, h4, h5, h6 {
.container-premium {
@apply max-w-7xl mx-auto px-6 md:px-12 lg:px-24;
}
/* Tekstura papieru dla sekcji (bardzo subtelny szum) */
.bg-paper-noise {
background-color: var(--bg-color);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='0.04'/%3E%3C/svg%3E");
}
/* Fix ipal-kit justified text anti-pattern */
.ipal-banner-wrapper p.text-justify {
text-align: left !important;
}
+65 -33
View File
@@ -1,9 +1,10 @@
'use client'
import React from 'react'
import { Landmark, Scale, ShieldCheck } from 'lucide-react'
import { Landmark, Scale, ShieldCheck, CheckCircle } from 'lucide-react'
import Link from 'next/link'
import type { Page } from '../../payload-types'
import { motion } from 'framer-motion'
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'affiliation' }>
@@ -14,52 +15,83 @@ const iconMap = {
}
export const AffiliationBlock: React.FC<Props> = ({ eyebrow, title, description, logo, linkText, linkUrl, trustPoints }) => {
const logoUrl = (logo && typeof logo === 'object' && logo.url) ? logo.url : null
return (
<section className="py-20 md:py-32 bg-background border-t border-border overflow-hidden">
<section className="py-20 md:py-32 bg-accent/5 border-y border-accent/10 overflow-hidden">
<div className="container-premium">
<div className="flex flex-col gap-16 lg:gap-24">
<div className="max-w-3xl">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 lg:gap-24 items-center">
{/* Lewa kolumna: Logo (Stamp) */}
<div className="lg:col-span-5 flex justify-center lg:justify-end order-1">
<motion.div
initial={{ opacity: 0, scale: 1.5, rotate: -15 }}
whileInView={{ opacity: 1, scale: 1, rotate: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ type: "spring", stiffness: 100, damping: 15 }}
className="w-48 h-48 md:w-64 md:h-64 relative bg-card rounded-full p-8 shadow-xl border-4 border-accent/20 flex items-center justify-center shrink-0"
>
{logoUrl ? (
/* eslint-disable-next-line @next/next/no-img-element */
<img
src={logoUrl}
alt={(logo && typeof logo === 'object' && logo.alt) || "Logo b.b.h. Lohnsteuerhilfeverein e.V."}
className="max-w-full max-h-full object-contain mix-blend-multiply"
/>
) : (
<div className="w-full h-full flex flex-col items-center justify-center text-border bg-card rounded-full">
<Landmark size={48} strokeWidth={1.5} />
</div>
)}
</motion.div>
</div>
{/* Prawa kolumna: Tekst i lista */}
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1], delay: 0.2 }}
className="lg:col-span-7 flex flex-col items-start order-2"
>
<h2 className="font-heading text-3xl sm:text-4xl md:text-5xl font-extrabold tracking-tight text-foreground mb-6 text-balance">
{title}
</h2>
<p className="text-foreground/75 text-lg md:text-xl font-light leading-relaxed mb-12 max-w-xl">
<p className="text-foreground/80 text-lg md:text-xl font-light leading-relaxed mb-10 max-w-2xl">
{description}
</p>
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-8">
{logo && typeof logo === 'object' && logo.url && (
<div className="w-40 shrink-0">
<img src={logo.url} alt={logo.alt || "Logo stowarzyszenia"} className="max-w-full h-auto grayscale hover:grayscale-0 transition-all duration-300" />
</div>
)}
{linkText && linkUrl && (
linkUrl.startsWith('#') ? (
<a href={linkUrl} className="text-accent font-semibold text-lg hover:text-accent-hover underline underline-offset-4">
{trustPoints && trustPoints.length > 0 && (
<div className="flex flex-col gap-5 mb-10 w-full">
{trustPoints.map((point: NonNullable<Props['trustPoints']>[0], index: number) => {
return (
<div key={point.id || index} className="flex items-start gap-4">
<CheckCircle size={24} className="text-accent shrink-0 mt-0.5" strokeWidth={2} />
<div>
<h3 className="font-heading text-xl font-bold text-foreground mb-1">{point.title}</h3>
<p className="text-base text-foreground/75 leading-relaxed">{point.description}</p>
</div>
</div>
)
})}
</div>
)}
{linkText && linkUrl && (
<div className="mt-2 pt-8 border-t border-accent/20 w-full">
{linkUrl.startsWith('#') ? (
<a href={linkUrl} className="inline-flex items-center gap-2 text-accent font-bold text-lg hover:text-accent-hover transition-colors">
{linkText}
</a>
) : (
<Link href={linkUrl} className="text-accent font-semibold text-lg hover:text-accent-hover underline underline-offset-4">
<Link href={linkUrl} className="inline-flex items-center gap-2 text-accent font-bold text-lg hover:text-accent-hover transition-colors">
{linkText}
</Link>
)
)}
</div>
</div>
)}
</div>
)}
</motion.div>
{trustPoints && trustPoints.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-6">
{trustPoints.map((point: any, index: number) => {
const Icon = (point.icon && iconMap[point.icon as keyof typeof iconMap]) || ShieldCheck
return (
<div key={point.id || index} className="bg-card border border-border p-6 md:p-8 flex flex-col gap-4">
<Icon size={28} className="text-accent" />
<h3 className="font-heading text-xl font-bold text-foreground">{point.title}</h3>
<p className="text-sm text-foreground/70 leading-relaxed">{point.description}</p>
</div>
)
})}
</div>
)}
</div>
</div>
</section>
+44 -23
View File
@@ -1,8 +1,9 @@
'use client'
import React from 'react'
import { motion } from 'framer-motion'
import React, { useRef } from 'react'
import { motion, useScroll, useTransform } from 'framer-motion'
import type { Page, Media } from '../../payload-types'
import Image from 'next/image'
import { Check, User } from 'lucide-react'
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'bio' }>
@@ -22,41 +23,61 @@ const sectionVariants = {
export const BioBlock: React.FC<Props> = ({ title, paragraphs, credentials, image }) => {
const bioMedia = typeof image === 'object' && image !== null ? (image as Media) : null
// PLACEHOLDER - do podmiany na realne zdjęcie Janusza Kędzierskiego (bez twarzy na etapie placeholderu)
const imageUrl = bioMedia?.url || 'https://images.unsplash.com/photo-1554224155-8d04cb21cd6c?auto=format&fit=crop&w=1200&q=80'
const imageAlt = bioMedia?.alt || 'Janusz Kędzierski - Doradztwo podatkowe'
const imageUrl = bioMedia?.url
const imageAlt = bioMedia?.alt || ''
const containerRef = useRef<HTMLDivElement>(null)
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start end", "end start"]
})
// Parallax tilt effect
const y = useTransform(scrollYProgress, [0, 1], [40, -40])
const rotateX = useTransform(scrollYProgress, [0, 1], [4, -4])
return (
<motion.section
initial="hidden"
animate="visible"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
variants={sectionVariants}
className="py-20 md:py-32 bg-background border-y border-border overflow-hidden"
>
<div className="container-premium">
<div className="container-premium" ref={containerRef}>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-14 lg:gap-20 items-center">
{/* Kolumna ze zdjęciem */}
<div className="lg:col-span-5 order-2 lg:order-1">
<div className="relative aspect-[4/5] overflow-hidden border border-border rounded-none shadow-sm">
<Image
src={imageUrl}
alt={imageAlt}
fill
sizes="(max-width: 1024px) 100vw, 42vw"
className="object-cover grayscale hover:grayscale-0 transition-all duration-700"
/>
</div>
<div className="lg:col-span-5 order-2 lg:order-1" style={{ perspective: '1200px' }}>
<motion.div
style={{ y, rotateX, transformStyle: "preserve-3d" }}
className="relative aspect-[4/5] overflow-hidden"
>
{imageUrl ? (
<Image
src={imageUrl}
alt={imageAlt}
fill
sizes="(max-width: 1024px) 100vw, 42vw"
className="object-cover transition-all duration-700"
/>
) : (
<div className="w-full h-full flex flex-col items-center justify-center bg-transparent text-border gap-4 p-8 text-center">
<User size={64} strokeWidth={1} />
<span className="text-xs tracking-widest uppercase font-semibold text-foreground/40">ZDJĘCIE PORTRETOWE</span>
</div>
)}
</motion.div>
</div>
{/* Kolumna tekstowa */}
<div className="lg:col-span-7 order-1 lg:order-2 flex flex-col items-start max-w-3xl">
<div className="lg:col-span-7 order-1 lg:order-2 flex flex-col items-start">
<h2 className="font-heading text-3xl sm:text-4xl md:text-5xl font-extrabold text-foreground tracking-tight mb-8 text-balance">
{title}
</h2>
{paragraphs && paragraphs.length > 0 && (
<div className="space-y-5 text-foreground/80 text-lg md:text-xl font-light leading-relaxed mb-10">
<div className="space-y-6 text-foreground/80 text-lg md:text-xl leading-relaxed mb-10 max-w-[70ch]">
{paragraphs.map((p: any, index: number) => (
<p key={p.id || index}>
{p.text}
@@ -66,11 +87,11 @@ export const BioBlock: React.FC<Props> = ({ title, paragraphs, credentials, imag
)}
{credentials && credentials.length > 0 && (
<div className="flex flex-col gap-3.5 w-full pt-8 border-t border-border">
<div className="flex flex-col gap-4 w-full pt-8 border-t border-border/50">
{credentials.map((cred: any, index: number) => (
<div key={cred.id || index} className="flex items-start gap-3 text-foreground/85 text-base">
<span className="w-2 h-2 rounded-full bg-accent mt-2 shrink-0" />
<span className="font-medium">{cred.text}</span>
<div key={cred.id || index} className="flex items-start gap-3 text-foreground/90 text-base max-w-[70ch]">
<Check size={20} className="text-accent shrink-0 mt-0.5" strokeWidth={2.5} />
<span className="font-medium leading-relaxed">{cred.text}</span>
</div>
))}
</div>
+77 -223
View File
@@ -1,10 +1,12 @@
'use client'
import React, { useState } from 'react'
import React from 'react'
import { usePathname } from 'next/navigation'
import { MapPin, Phone, Mail, Clock, MessageCircle, Send } from 'lucide-react'
import { MapPin, Phone, Mail, Clock } from 'lucide-react'
import type { Page } from '../../payload-types'
import { motion } from 'framer-motion'
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'contactInfo' }>
type BaseProps = Extract<NonNullable<Page['layout']>[0], { blockType: 'contactInfo' }>
type Props = BaseProps & { whatsappNumber?: string | null }
export const ContactInfoBlock: React.FC<Props> = ({
address = 'DO POTWIERDZENIA PRZEZ KLIENTA',
@@ -13,240 +15,92 @@ export const ContactInfoBlock: React.FC<Props> = ({
hours = 'Pon-Pt: 9:00 - 17:00\nPo wcześniejszym umówieniu',
}) => {
const pathname = usePathname()
const isDe = pathname?.startsWith('/de')
const [formData, setFormData] = useState({
service: '',
name: '',
email: '',
message: '',
})
const [submitted, setSubmitted] = useState(false)
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// TODO: Podłączyć logikę wysyłki formularza przez backend API (np. Resend / SMTP / Payload custom endpoint).
// Pamiętać o przekazaniu w payloadzie: formData.service (wybrana usługa), formData.name, formData.email, formData.message.
setSubmitted(true)
}
const isDe = pathname?.split('/').filter(Boolean)[0] === 'de'
return (
<section className="py-12 md:py-16 bg-background">
<div className="container-premium">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 lg:gap-16 items-start">
{/* Kolumna 1: Dane kontaktowe + WhatsApp CTA (Główna opcja) */}
<div className="lg:col-span-5 flex flex-col gap-8">
<div className="bg-card border border-border p-8 rounded-none shadow-sm flex flex-col gap-6">
<h2 className="font-heading text-2xl font-bold text-foreground">
{isDe ? 'Direkter Kontakt' : 'Bezpośredni kontakt'}
</h2>
<ul className="flex flex-col gap-5 text-foreground/80 text-base">
{address && (
<li className="flex items-start gap-4">
<div className="w-10 h-10 rounded-none bg-accent/10 flex items-center justify-center text-accent shrink-0 mt-0.5">
<MapPin size={20} />
</div>
<div>
<span className="text-xs text-foreground/50 uppercase tracking-wider font-semibold block mb-0.5">
{isDe ? 'Adresse' : 'Adres'}
</span>
<span className="font-medium whitespace-pre-line text-foreground">{address}</span>
</div>
</li>
)}
<section className="py-16 md:py-24 bg-background">
<div className="container-premium flex justify-center">
<motion.div
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
className="w-full max-w-4xl bg-card border border-border p-8 md:p-16 relative overflow-hidden"
style={{
boxShadow: '0 10px 40px -10px rgba(0,0,0,0.08), 0 1px 3px rgba(0,0,0,0.03)'
}}
>
{/* Subtelny narożnik (zagięcie kartki) */}
<div className="absolute top-0 right-0 w-12 h-12 bg-background border-l border-b border-border shadow-sm transform translate-x-6 -translate-y-6 rotate-45 pointer-events-none" />
{phone && (
<li className="flex items-start gap-4">
<div className="w-10 h-10 rounded-none bg-accent/10 flex items-center justify-center text-accent shrink-0 mt-0.5">
<Phone size={20} />
</div>
<div>
<span className="text-xs text-foreground/50 uppercase tracking-wider font-semibold block mb-0.5">
{isDe ? 'Telefon' : 'Telefon'}
</span>
<a href={`tel:${phone.replace(/\s+/g, '')}`} className="font-medium hover:text-accent transition-colors text-foreground">
{phone}
</a>
</div>
</li>
)}
<h2 className="font-heading text-2xl md:text-3xl font-bold text-foreground mb-10 text-center md:text-left">
{isDe ? 'Kontaktinformationen' : 'Dane kontaktowe'}
</h2>
{email && (
<li className="flex items-start gap-4">
<div className="w-10 h-10 rounded-none bg-accent/10 flex items-center justify-center text-accent shrink-0 mt-0.5">
<Mail size={20} />
</div>
<div>
<span className="text-xs text-foreground/50 uppercase tracking-wider font-semibold block mb-0.5">
{isDe ? 'E-Mail' : 'E-mail'}
</span>
<a href={`mailto:${email}`} className="font-medium hover:text-accent transition-colors text-foreground">
{email}
</a>
</div>
</li>
)}
{hours && (
<li className="flex items-start gap-4">
<div className="w-10 h-10 rounded-none bg-accent/10 flex items-center justify-center text-accent shrink-0 mt-0.5">
<Clock size={20} />
</div>
<div>
<span className="text-xs text-foreground/50 uppercase tracking-wider font-semibold block mb-0.5">
{isDe ? 'Öffnungszeiten' : 'Godziny pracy'}
</span>
<span className="font-medium whitespace-pre-line text-foreground">{hours}</span>
</div>
</li>
)}
</ul>
</div>
{/* Główne CTA: WhatsApp */}
<div className="p-8 bg-card border-2 border-green-500/20 rounded-none flex flex-col gap-4">
<span className="inline-block text-xs font-bold uppercase tracking-widest text-green-600 dark:text-green-400">
{isDe ? 'Empfohlen & Am schnellsten' : 'Zalecany i najszybszy kontakt'}
</span>
<h3 className="font-heading text-xl font-bold text-foreground">
{isDe ? 'Schreiben Sie auf WhatsApp' : 'Napisz na WhatsApp'}
</h3>
<p className="text-sm text-foreground/70 leading-relaxed">
{isDe
? 'Erhalten Sie schnelle Antworten auf Ihre Fragen direkt von Dipl.-Ing. Janusz Kędzierski.'
: 'Najszybsza forma kontaktu. Prześlij zapytanie lub dokumenty i uzyskaj szybką odpowiedź.'}
</p>
<a
href={process.env.NEXT_PUBLIC_WHATSAPP_LINK || '#'}
target="_blank"
rel="noopener noreferrer"
className="mt-2 inline-flex items-center justify-center gap-3 px-6 py-4 bg-green-500 hover:bg-green-600 text-white font-bold text-base rounded-none transition-colors shadow-sm"
>
<MessageCircle size={22} />
<span>{isDe ? 'WhatsApp Chat starten' : 'Napisz na WhatsApp'}</span>
</a>
</div>
</div>
{/* Kolumna 2: Prosty formularz kontaktowy (Drugorzędna opcja) */}
<div className="lg:col-span-7 bg-card border border-border p-8 md:p-10 rounded-none shadow-sm">
<div className="mb-8">
<h2 className="font-heading text-2xl md:text-3xl font-bold text-foreground">
{isDe ? 'Kontaktformular' : 'Formularz kontaktowy'}
</h2>
<p className="text-sm md:text-base text-foreground/70 mt-2 leading-relaxed">
{isDe
? 'Bevorzugen Sie eine schriftliche Nachricht? Füllen Sie das folgende Formular aus — wir antworten in der Regel innerhalb von 24 Stunden.'
: 'Wolisz kontakt tradycyjny? Wypełnij formularz, a odpowiemy najszybciej jak to możliwe.'}
</p>
</div>
{submitted ? (
<div className="p-6 bg-accent/10 border border-accent/20 rounded-none text-center">
<h3 className="font-heading font-bold text-lg text-foreground mb-2">
{isDe ? 'Vielen Dank für Ihre Nachricht!' : 'Dziękujemy za wiadomość!'}
</h3>
<p className="text-sm text-foreground/70">
{isDe
? 'Ihre Anfrage wurde entgegengenommen. Wir melden uns in Kürze.'
: 'Twoje zapytanie zostało zarejestrowane. Skontaktujemy się wkrótce.'}
<div className="grid grid-cols-1 md:grid-cols-2 gap-10 md:gap-12">
{address && (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
<MapPin size={24} strokeWidth={2.5} />
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
{isDe ? 'Adresse' : 'Adres'}
</h3>
</div>
<p className="text-foreground/80 leading-relaxed font-medium whitespace-pre-line text-base max-w-[70ch]">
{address}
</p>
</div>
) : (
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
<div>
<label htmlFor="service" className="block text-sm font-semibold text-foreground mb-2">
{isDe ? 'Worum geht es?' : 'Czego dotyczy zapytanie?'} <span className="text-accent">*</span>
</label>
<select
id="service"
required
value={formData.service}
onChange={(e) => setFormData({ ...formData, service: e.target.value })}
className="w-full px-4 py-3 bg-background border border-border rounded-none text-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
>
<option value="" disabled>
{isDe ? '— Bitte auswählen —' : '— Wybierz temat zapytania —'}
</option>
{isDe ? (
<>
<option value="Steuererklärung">Steuererklärung</option>
<option value="Steuerbescheidprüfung">Steuerbescheidprüfung</option>
<option value="Kindergeld / Riester-Rente">Kindergeld / Riester-Rente</option>
<option value="Sonstiges">Sonstiges</option>
</>
) : (
<>
<option value="Rozliczenie roczne (Steuererklärung)">Rozliczenie roczne (Steuererklärung)</option>
<option value="Sprawdzenie decyzji podatkowej (Steuerbescheid)">Sprawdzenie decyzji podatkowej (Steuerbescheid)</option>
<option value="Zasiłki i dopłaty (Kindergeld, Riester-Rente)">Zasiłki i dopłaty (Kindergeld, Riester-Rente)</option>
<option value="Inna sprawa">Inna sprawa</option>
</>
)}
</select>
</div>
)}
<div>
<label htmlFor="name" className="block text-sm font-semibold text-foreground mb-2">
{isDe ? 'Vor- und Nachname' : 'Imię i nazwisko'} <span className="text-accent">*</span>
</label>
<input
type="text"
id="name"
required
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder={isDe ? 'z. B. Jan Kowalski' : 'np. Jan Kowalski'}
className="w-full px-4 py-3 bg-background border border-border rounded-none text-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
/>
{phone && (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
<Phone size={24} strokeWidth={2.5} />
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
{isDe ? 'Telefon' : 'Telefon'}
</h3>
</div>
<a
href={`tel:${phone.replace(/\s+/g, '')}`}
className="text-foreground/80 leading-relaxed font-medium text-base hover:text-accent transition-colors max-w-[70ch]"
>
{phone}
</a>
</div>
)}
<div>
<label htmlFor="email" className="block text-sm font-semibold text-foreground mb-2">
{isDe ? 'E-Mail-Adresse' : 'Adres e-mail'} <span className="text-accent">*</span>
</label>
<input
type="email"
id="email"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="[email protected]"
className="w-full px-4 py-3 bg-background border border-border rounded-none text-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all"
/>
{email && (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
<Mail size={24} strokeWidth={2.5} />
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
{isDe ? 'E-Mail' : 'E-mail'}
</h3>
</div>
<a
href={`mailto:${email}`}
className="text-foreground/80 leading-relaxed font-medium text-base hover:text-accent transition-colors max-w-[70ch]"
>
{email}
</a>
</div>
)}
<div>
<label htmlFor="message" className="block text-sm font-semibold text-foreground mb-2">
{isDe ? 'Ihre Nachricht' : 'Treść wiadomości'} <span className="text-accent">*</span>
</label>
<textarea
id="message"
required
rows={5}
value={formData.message}
onChange={(e) => setFormData({ ...formData, message: e.target.value })}
placeholder={isDe ? 'Wie können wir Ihnen helfen?' : 'W czym możemy Ci pomóc? Opisz krótko swoją sprawę...'}
className="w-full px-4 py-3 bg-background border border-border rounded-none text-foreground focus:outline-none focus:ring-2 focus:ring-accent focus:border-transparent transition-all resize-y"
/>
{hours && (
<div className="flex flex-col gap-3">
<div className="flex items-center gap-3 text-accent border-b border-border/50 pb-3">
<Clock size={24} strokeWidth={2.5} />
<h3 className="font-heading font-bold text-lg text-foreground tracking-wide">
{isDe ? 'Öffnungszeiten' : 'Godziny pracy'}
</h3>
</div>
<div className="pt-2">
<button
type="submit"
className="w-full sm:w-auto px-8 py-4 bg-accent hover:bg-accent-hover text-accent-foreground font-bold text-base rounded-none transition-colors shadow-sm flex items-center justify-center gap-2 cursor-pointer"
>
<Send size={18} />
<span>{isDe ? 'Nachricht senden' : 'Wyślij wiadomość'}</span>
</button>
</div>
</form>
<p className="text-foreground/80 leading-relaxed font-medium whitespace-pre-line text-base max-w-[70ch]">
{hours}
</p>
</div>
)}
</div>
</div>
</motion.div>
</div>
</section>
)
+93 -86
View File
@@ -1,5 +1,7 @@
'use client'
import React from 'react'
import { CheckCircle2, XCircle, AlertTriangle, ShieldAlert } from 'lucide-react'
import { CheckCircle2, XCircle, FileText, Scale } from 'lucide-react'
import { motion } from 'framer-motion'
export interface EligibilityProps {
eyebrow?: string | null
@@ -25,94 +27,99 @@ export const EligibilityBlock: React.FC<EligibilityProps> = ({ eyebrow, title,
legalNotice,
}) => {
return (
<section className="py-12 md:py-20 bg-card/60 border-y border-border">
<section className="py-16 md:py-24 bg-card border-y border-border">
<div className="container-premium">
<div className="max-w-3xl mb-12">
<motion.div
initial={{ opacity: 0, y: 8 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.5 }}
>
<div className="max-w-3xl mb-12">
<h2 className="font-heading text-3xl sm:text-4xl md:text-5xl font-extrabold text-foreground tracking-tight">
{title}
</h2>
{subtitle && (
<p className="mt-3 text-base sm:text-lg text-foreground/70 leading-relaxed max-w-[70ch]">
{subtitle}
</p>
)}
</div>
<h2 className="font-heading text-3xl sm:text-4xl md:text-5xl font-extrabold text-foreground tracking-tight">
{title}
</h2>
{subtitle && (
<p className="mt-3 text-base sm:text-lg text-foreground/70 leading-relaxed">
{subtitle}
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 mb-10">
{/* 1: Kto się kwalifikuje */}
<div className="flex flex-col">
<div className="flex items-center gap-3 mb-6 pb-4 border-b border-border/50">
<div className="text-emerald-600/80 dark:text-emerald-500/80 shrink-0">
<CheckCircle2 size={24} />
</div>
<h3 className="font-heading text-xl font-bold text-foreground">
{eligibleTitle}
</h3>
</div>
<ul className="space-y-4">
{(eligibleItems || []).map((item, idx) => (
<li key={item.id || idx} className="flex items-start gap-3">
<CheckCircle2
size={20}
className="text-emerald-600/60 dark:text-emerald-500/60 shrink-0 mt-0.5"
/>
<span className="text-foreground/80 text-sm sm:text-base leading-relaxed max-w-[70ch]">
{item.text}
</span>
</li>
))}
</ul>
</div>
{/* 3: Wykluczenia - kolor stonowany, jak dokument */}
<div className="flex flex-col">
<div className="flex items-center gap-3 mb-6 pb-4 border-b border-border/50">
<div className="text-foreground/60 shrink-0">
<XCircle size={24} />
</div>
<h3 className="font-heading text-xl font-bold text-foreground">
{excludedTitle}
</h3>
</div>
<ul className="space-y-4">
{(excludedItems || []).map((item, idx) => (
<li key={item.id || idx} className="flex items-start gap-3">
<XCircle
size={20}
className="text-foreground/40 shrink-0 mt-0.5"
/>
<span className="text-foreground/80 text-sm sm:text-base leading-relaxed max-w-[70ch]">
{item.text}
</span>
</li>
))}
</ul>
</div>
</div>
{/* 2: Limit przychodów - techniczny wtręt */}
<div className="border-y border-border/50 py-6 mb-8 flex flex-row items-start gap-4">
<div className="mt-1 text-foreground/40 shrink-0">
<FileText size={22} />
</div>
<div>
<h4 className="font-bold text-foreground mb-1 text-base sm:text-lg">{limitationsTitle}</h4>
<p className="text-foreground/75 leading-relaxed text-sm sm:text-base max-w-[70ch]">
{limitationsText}
</p>
</div>
</div>
{legalNotice && (
<div className="pt-6 border-t border-border flex items-start gap-3 text-foreground/50 text-xs sm:text-sm">
<Scale size={16} className="shrink-0 mt-0.5" />
<p className="leading-relaxed max-w-[70ch]">{legalNotice}</p>
</div>
)}
</div>
<div className="flex flex-col gap-6">
{/* 1: Kto się kwalifikuje */}
<div className="bg-card border-2 border-border rounded-none p-6 sm:p-8 shadow-sm">
<div className="flex items-center gap-3 mb-6 pb-4 border-b border-border">
<div className="w-10 h-10 rounded-full bg-accent/10 text-accent flex items-center justify-center shrink-0">
<CheckCircle2 size={24} />
</div>
<h3 className="font-heading text-xl sm:text-2xl font-bold text-foreground">
{eligibleTitle}
</h3>
</div>
<ul className="space-y-4">
{(eligibleItems || []).map((item, idx) => (
<li key={item.id || idx} className="flex items-start gap-3">
<CheckCircle2
size={20}
className="text-accent shrink-0 mt-0.5"
/>
<span className="text-foreground/90 font-medium leading-relaxed">
{item.text}
</span>
</li>
))}
</ul>
</div>
{/* 2: Limit przychodów */}
<div className="bg-card border border-border rounded-none p-6 sm:p-8 shadow-sm">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-full bg-amber-500/10 text-amber-500 flex items-center justify-center shrink-0">
<AlertTriangle size={22} />
</div>
<h3 className="font-heading text-xl sm:text-2xl font-bold text-foreground">
{limitationsTitle}
</h3>
</div>
<p className="text-foreground/80 leading-relaxed text-sm sm:text-base">
{limitationsText}
</p>
</div>
{/* 3: Wykluczenia - czerwony kolor ostrzegawczy dla bezpieczeństwa prawnego klienta */}
<div className="bg-red-500/5 border-2 border-red-500/30 rounded-none p-6 sm:p-8 shadow-sm">
<div className="flex items-center gap-3 mb-4 pb-3 border-b border-red-500/20">
<div className="w-10 h-10 rounded-full bg-red-500/15 text-red-600 flex items-center justify-center shrink-0">
<XCircle size={22} />
</div>
<h3 className="font-heading text-xl sm:text-2xl font-bold text-red-950 dark:text-red-200">
{excludedTitle}
</h3>
</div>
<ul className="space-y-3">
{(excludedItems || []).map((item, idx) => (
<li key={item.id || idx} className="flex items-start gap-3">
<XCircle
size={18}
className="text-red-500 shrink-0 mt-0.5"
/>
<span className="text-foreground/90 font-medium text-sm sm:text-base leading-relaxed">
{item.text}
</span>
</li>
))}
</ul>
</div>
</div>
{legalNotice && (
<div className="mt-10 p-5 rounded-none bg-background border border-border text-xs sm:text-sm text-foreground/60 leading-relaxed">
<p>{legalNotice}</p>
</div>
)}
</motion.div>
</div>
</section>
)
+8 -10
View File
@@ -6,15 +6,13 @@ 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 }) => {
export const FAQFullBlockComponent: React.FC<Props> = ({
title,
subtitle,
questions,
ctaText,
ctaUrl,
}) => {
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, {
stiffness: 100,
@@ -33,7 +31,7 @@ export const FAQFullBlockComponent: React.FC<Props> = ({ title, subtitle, questi
}
// Filter out placeholder questions
const validQuestions = (questions || []).filter(q => !isPlaceholder(q.answer));
const validQuestions = (questions || []).filter((q: any) => !q.isDraft);
// Group by category
const groupedQuestions = validQuestions.reduce((acc, q) => {
+35
View File
@@ -0,0 +1,35 @@
import type { Block } from 'payload'
export const FAQFull: Block = {
slug: 'faqFull',
fields: [
{ name: 'title', type: 'text', required: true, localized: true },
{
name: 'subtitle',
type: 'text',
required: false,
localized: true,
},
{
name: 'questions',
type: 'array',
minRows: 1,
localized: true,
fields: [
{ name: 'category', type: 'text', required: true },
{
name: 'isDraft',
type: 'checkbox',
defaultValue: false,
admin: {
description: 'Zaznacz, jeśli treść nie jest jeszcze gotowa do publikacji (pytanie nie pojawi się na stronie ani w SEO)',
},
},
{ name: 'question', type: 'text', required: true },
{ name: 'answer', type: 'textarea', required: true },
],
},
{ name: 'ctaText', type: 'text', required: false, localized: true },
{ name: 'ctaUrl', type: 'text', required: false },
],
}
+13 -2
View File
@@ -6,7 +6,7 @@ import type { Page } from '../../payload-types'
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'faqTeaser' }>
export const FAQTeaserBlock: React.FC<Props> = ({ title, questions }) => {
export const FAQTeaserBlock: React.FC<Props> = ({ title, questions, ctaText, ctaUrl }) => {
const [openIndex, setOpenIndex] = useState<number | null>(0)
const toggleQuestion = (index: number) => {
@@ -14,7 +14,7 @@ export const FAQTeaserBlock: React.FC<Props> = ({ title, questions }) => {
}
return (
<section className="py-20 md:py-32 bg-card/40 border-t border-border" id="faq">
<section className="py-20 md:py-32" id="faq">
<div className="container-premium max-w-4xl">
<div className="mb-12 md:mb-16 text-center">
<h2 className="font-heading text-3xl sm:text-4xl md:text-5xl font-extrabold text-foreground tracking-tight text-balance">
@@ -63,6 +63,17 @@ export const FAQTeaserBlock: React.FC<Props> = ({ title, questions }) => {
)
})}
</div>
{ctaText && ctaUrl && (
<div className="mt-12 text-center">
<a
href={ctaUrl}
className="inline-flex items-center justify-center h-14 px-8 text-base sm:text-lg font-bold text-background bg-accent rounded-[6px] hover:bg-accent-hover transition-colors"
>
{ctaText}
</a>
</div>
)}
</div>
</section>
)
+3
View File
@@ -10,9 +10,12 @@ export const FAQTeaser: Block = {
minRows: 1,
localized: true,
fields: [
{ name: 'category', type: 'text', required: false },
{ name: 'question', type: 'text', required: true },
{ name: 'answer', type: 'textarea', required: true },
],
},
{ name: 'ctaText', type: 'text', required: false, localized: true },
{ name: 'ctaUrl', type: 'text', required: false },
],
}
+23 -21
View File
@@ -1,40 +1,42 @@
'use client'
import React from 'react'
import type { Page } from '../../payload-types'
import { MessageCircle } from 'lucide-react'
import { MotionStamp } from '../../components/MotionStamp'
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'finalCta' }>
type BaseProps = Extract<NonNullable<Page['layout']>[0], { blockType: 'finalCta' }>
type Props = BaseProps & { whatsappNumber?: string | null }
export const FinalCTABlock: React.FC<Props> = ({ eyebrow, title, subtitle, buttonText }) => {
export const FinalCTABlock: React.FC<Props> = ({ eyebrow, title, subtitle, buttonText, whatsappNumber }) => {
const whatsappLink = whatsappNumber ? `https://wa.me/${whatsappNumber}` : '#'
return (
<section className="py-20 md:py-32 bg-card border-y border-border text-left">
<MotionStamp id="final-cta" className="py-20 md:py-32 text-center" style={{ backgroundColor: 'var(--accent-color)', color: 'var(--bg-color)' }}>
<div className="container-premium">
<div className="max-w-3xl">
{/* Removed eyebrow */}
<h2 className="font-heading text-3xl sm:text-4xl md:text-5xl font-extrabold text-foreground tracking-tight mb-6 text-balance">
<div className="max-w-3xl mx-auto flex flex-col items-center">
<h2 className="font-heading text-3xl sm:text-4xl md:text-5xl font-extrabold tracking-tight mb-6 text-balance text-[var(--bg-color)]">
{title}
</h2>
{subtitle && (
<p className="text-lg md:text-xl font-light text-foreground/75 mb-12 leading-relaxed">
<p className="text-lg md:text-xl font-light mb-12 leading-relaxed text-[var(--bg-color)]/90">
{subtitle}
</p>
)}
<div>
<a
href={process.env.NEXT_PUBLIC_WHATSAPP_LINK || '#'}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2.5 px-8 py-4 bg-green-500 hover:bg-green-600 text-white text-base sm:text-lg font-bold rounded-none transition-colors shadow-sm"
>
<MessageCircle size={22} />
<span>{buttonText}</span>
</a>
</div>
{whatsappNumber && (
<div>
<a
href={whatsappLink}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2.5 px-8 py-4 bg-background hover:bg-border text-accent text-base sm:text-lg font-bold rounded-md transition-colors shadow-sm"
>
<MessageCircle size={22} />
<span>{buttonText}</span>
</a>
</div>
)}
</div>
</div>
</section>
</MotionStamp>
)
}
+64 -35
View File
@@ -4,8 +4,10 @@ import { motion } from 'framer-motion'
import type { Page, Media } from '../../payload-types'
import Link from 'next/link'
import Image from 'next/image'
import { ImageIcon } from 'lucide-react'
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'hero' }>
type BaseProps = Extract<NonNullable<Page['layout']>[0], { blockType: 'hero' }>
type Props = BaseProps & { whatsappNumber?: string | null }
const customEase = [0.22, 1, 0.36, 1] as const
@@ -90,10 +92,14 @@ export const HeroBlock: React.FC<Props> = ({
image,
imageSecondary,
floatingStat,
whatsappNumber,
}) => {
const [imagesLoaded, setImagesLoaded] = useState(false)
const whatsappLink = whatsappNumber ? `https://wa.me/${whatsappNumber}` : '#'
const heroMedia = typeof image === 'object' && image !== null ? (image as Media) : null
const imageUrl = heroMedia?.url || 'https://images.unsplash.com/photo-1554224155-8d04cb21cd6c?auto=format&fit=crop&w=1200&q=80'
const imageAlt = heroMedia?.alt || 'Doradztwo podatkowe - biuro'
const imageUrl = heroMedia?.url
const imageAlt = heroMedia?.alt || ''
const secondaryMedia = typeof imageSecondary === 'object' && imageSecondary !== null ? (imageSecondary as Media) : null
const secondaryUrl = secondaryMedia?.url
@@ -109,7 +115,7 @@ export const HeroBlock: React.FC<Props> = ({
initial="hidden"
animate="visible"
variants={containerVariants}
className="w-full bg-background text-foreground min-h-[calc(100svh-6rem)] flex items-center py-12 lg:py-0 overflow-hidden"
className="w-full bg-paper-noise text-foreground min-h-[calc(100svh-6rem)] flex items-center py-12 lg:py-0 overflow-hidden"
>
<div className="container-premium w-full">
<div className="flex flex-col-reverse lg:flex-row items-center justify-between gap-14 lg:gap-20">
@@ -118,15 +124,21 @@ export const HeroBlock: React.FC<Props> = ({
<div className="w-full lg:w-1/2 max-w-xl flex flex-col">
<motion.h1
variants={itemVariants}
className="font-heading text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold leading-[1.05] mb-8 text-balance"
className="font-heading text-4xl sm:text-5xl md:text-6xl font-bold leading-tight mb-8 text-balance"
>
<span className="block text-foreground">{title}</span>
{titleAccent && <span className="block text-accent">{titleAccent}</span>}
{titleAccent && (
<span className="block text-accent">
{titleAccent.split(/(po polsku)/i).map((part, i) =>
part.toLowerCase() === 'po polsku' ? <i key={i} className="italic font-medium">{part}</i> : part
)}
</span>
)}
</motion.h1>
<motion.p
variants={itemVariants}
className="text-xl sm:text-2xl text-foreground/80 mb-12 leading-relaxed font-light"
className="text-xl sm:text-2xl text-foreground/80 mb-12 leading-relaxed font-light max-w-[65ch]"
>
{subtitle}
</motion.p>
@@ -135,14 +147,16 @@ export const HeroBlock: React.FC<Props> = ({
variants={itemVariants}
className="flex flex-col sm:flex-row items-start sm:items-center gap-8"
>
<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-bold text-base sm:text-lg transition-colors flex items-center justify-center rounded-none shadow-sm cursor-pointer"
>
{primaryCta}
</a>
{whatsappNumber && (
<a
href={whatsappLink}
target="_blank"
rel="noopener noreferrer"
className="px-8 py-4 bg-[#25D366] hover:bg-[#20bd5a] text-[#1C1B1A] font-bold text-base sm:text-lg transition-colors flex items-center justify-center rounded-none shadow-sm cursor-pointer"
>
{primaryCta}
</a>
)}
{secondaryCta?.text && secondaryCta?.url && (
secondaryCta.url.startsWith('#') ? (
<a
@@ -191,40 +205,55 @@ export const HeroBlock: React.FC<Props> = ({
/* Układ kolażu dwóch nachodzących na siebie zdjęć */
<div className="relative w-full aspect-[4/5] sm:aspect-[1/1] pt-6 pl-4">
{/* Zdjęcie 1 (mniejsze, przesunięte w lewo-górę) */}
<div className="absolute top-2 left-0 w-[62%] aspect-[4/5] border border-border shadow-sm overflow-hidden z-10">
<Image
src={imageUrl}
alt={imageAlt}
fill
priority
sizes="(max-width: 1024px) 55vw, 25vw"
className="object-cover object-center grayscale hover:grayscale-0 transition-all duration-700"
/>
<div className="absolute top-2 left-0 w-[62%] aspect-[4/5] border border-border shadow-sm overflow-hidden z-10 bg-card">
{imageUrl ? (
<Image
src={imageUrl}
alt={imageAlt}
fill
priority
sizes="(max-width: 1024px) 55vw, 25vw"
onLoad={() => setImagesLoaded(true)}
className={`object-cover object-center grayscale hover:grayscale-0 transition-all duration-700 ease-out ${imagesLoaded ? 'opacity-100 scale-100' : 'opacity-0 scale-105'}`}
/>
) : (
<div className="w-full h-full flex items-center justify-center text-border border border-border">
<ImageIcon size={32} strokeWidth={1} />
</div>
)}
</div>
{/* Zdjęcie 2 (większe, przesunięte w prawo-dół, nachodzące na pierwsze o ok. 18%) */}
<div className="absolute bottom-0 right-0 w-[74%] aspect-[4/5] border border-border shadow-md overflow-hidden z-20">
<div className="absolute bottom-0 right-0 w-[74%] aspect-[4/5] border border-border shadow-md overflow-hidden z-20 bg-border/20">
<Image
src={secondaryUrl}
alt={secondaryAlt}
fill
priority
sizes="(max-width: 1024px) 65vw, 32vw"
className="object-cover object-center grayscale hover:grayscale-0 transition-all duration-700"
className={`object-cover object-center grayscale hover:grayscale-0 transition-all duration-700 ease-out ${imagesLoaded ? 'opacity-100 scale-100' : 'opacity-0 scale-105'}`}
/>
</div>
</div>
) : (
/* Pojedyncze zdjęcie jeśli imageSecondary jest puste */
<div className="relative w-full aspect-[4/5] border border-border shadow-sm overflow-hidden">
<Image
src={imageUrl}
alt={imageAlt}
fill
priority
sizes="(max-width: 768px) 100vw, 50vw"
className="object-cover object-center grayscale hover:grayscale-0 transition-all duration-700"
/>
<div className="relative w-full aspect-[4/5] border border-border shadow-sm overflow-hidden bg-card">
{imageUrl ? (
<Image
src={imageUrl}
alt={imageAlt}
fill
priority
sizes="(max-width: 768px) 100vw, 50vw"
onLoad={() => setImagesLoaded(true)}
className={`object-cover object-center grayscale hover:grayscale-0 transition-all duration-700 ease-out ${imagesLoaded ? 'opacity-100 scale-100' : 'opacity-0 scale-105'}`}
/>
) : (
<div className="w-full h-full flex flex-col items-center justify-center text-border gap-4 p-8 text-center border border-border">
<ImageIcon size={64} strokeWidth={1} />
<span className="text-xs tracking-widest uppercase font-semibold text-foreground/40">ZDJĘCIE GŁÓWNE</span>
</div>
)}
</div>
)}
</div>
+12 -5
View File
@@ -1,4 +1,6 @@
'use client'
import React from 'react'
import { motion } from 'framer-motion'
export interface PageHeaderProps {
title: string
@@ -7,18 +9,23 @@ export interface PageHeaderProps {
export const PageHeaderBlock: React.FC<PageHeaderProps> = ({ title, subtitle }) => {
return (
<section className="bg-card border-b border-border py-16 md:py-20 mb-12">
<section className="border-b border-border py-16 md:py-20 mb-12 bg-background">
<div className="container-premium">
<div className="max-w-4xl">
<h1 className="font-heading text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-extrabold text-foreground tracking-tight leading-tight text-balance">
<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-4 text-lg md:text-xl text-foreground/70 leading-relaxed max-w-3xl">
<p className="mt-6 text-lg md:text-xl text-foreground/70 leading-relaxed max-w-[65ch]">
{subtitle}
</p>
)}
</div>
</motion.div>
</div>
</section>
)
+52 -28
View File
@@ -2,50 +2,67 @@
import React from 'react'
import type { Page, Media } from '../../payload-types'
import Image from 'next/image'
import { ImageIcon } from 'lucide-react'
import { ArrowLink } from '../../components/ArrowLink'
import { motion, useScroll } from 'framer-motion'
import { useRef } from 'react'
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'process' }>
export const ProcessBlock: React.FC<Props> = ({ subtitle, title, steps, ctaText, ctaUrl, image }) => {
const processMedia = typeof image === 'object' && image !== null ? (image as Media) : null;
const imageUrl = processMedia?.url || 'https://images.unsplash.com/photo-1611162617474-5b21e879e113?auto=format&fit=crop&w=1200&q=80';
const imageAlt = processMedia?.alt || 'Proces współpracy';
const imageUrl = processMedia?.url
const imageAlt = processMedia?.alt || ''
const containerRef = useRef<HTMLDivElement>(null)
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start center", "end center"]
})
return (
<section id="jak-to-dziala" className="py-20 md:py-32 bg-background text-foreground overflow-hidden">
<div className="container-premium">
<div className="container-premium" ref={containerRef}>
<div className="max-w-3xl mb-16 md:mb-24">
<h2 className="font-heading text-3xl sm:text-4xl md:text-5xl font-extrabold tracking-tight text-foreground mb-6 text-balance">
{title}
</h2>
{subtitle && (
<p className="text-foreground/75 text-lg md:text-xl font-light leading-relaxed">
<p className="text-foreground/75 text-lg md:text-xl font-light leading-relaxed max-w-[70ch]">
{subtitle}
</p>
)}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-24 items-center">
<div className="flex flex-col gap-16">
{steps?.map((step: NonNullable<Props['steps']>[0], index: number) => {
const stepNumber = String(index + 1).padStart(2, '0')
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 lg:gap-24 items-start">
<div className="flex flex-col relative pl-6 md:pl-10 pb-8">
{/* Ścieżka pionowa z scroll-linked line-draw */}
<div className="absolute top-2 bottom-0 left-0 w-px bg-border/40">
<motion.div
className="absolute top-0 w-full bg-accent origin-top"
style={{ height: '100%', scaleY: scrollYProgress }}
/>
</div>
{steps?.map((step: NonNullable<Props['steps']>[0], index: number) => {
const stepNumber = index + 1
return (
<div
key={step.id || index}
className="relative flex flex-col sm:flex-row gap-6 group"
className="relative flex flex-col gap-2 mb-16 last:mb-0 group"
>
<div className="font-heading text-6xl md:text-7xl font-black text-foreground/5 tracking-tighter leading-none sm:-mt-2 group-hover:text-accent/10 transition-colors shrink-0">
{stepNumber}
</div>
<div className="relative z-10 pt-2 border-t-2 border-transparent group-hover:border-accent transition-colors flex-1">
<h3 className="font-heading text-xl sm:text-2xl font-bold text-foreground mb-4">
{step.title}
</h3>
<p className="text-foreground/75 text-lg font-light leading-relaxed">
{step.description}
</p>
{/* Znacznik kroku (Kropka na ścieżce) */}
<div className="absolute -left-[28px] md:-left-[44px] top-1.5 w-2 h-2 rounded-full bg-border group-hover:bg-accent transition-colors z-10 ring-4 ring-background" />
<div className="font-heading text-sm md:text-base font-bold text-accent uppercase tracking-widest mb-1">
Krok {stepNumber}
</div>
<h3 className="font-heading text-2xl sm:text-3xl font-bold text-foreground mb-4">
{step.title}
</h3>
<p className="text-foreground/75 text-lg md:text-xl font-light leading-relaxed max-w-[70ch]">
{step.description}
</p>
</div>
)
})}
@@ -59,15 +76,22 @@ export const ProcessBlock: React.FC<Props> = ({ subtitle, title, steps, ctaText,
)}
</div>
<div className="relative aspect-[4/5] w-full bg-card rounded-none overflow-hidden">
<Image
src={imageUrl}
alt={imageAlt}
fill
sizes="(max-width: 1024px) 100vw, 50vw"
className="object-cover w-full h-full grayscale hover:grayscale-0 transition-all duration-700"
/>
<div className="absolute inset-0 bg-gradient-to-t from-background/80 to-transparent mix-blend-multiply pointer-events-none" />
<div className="relative aspect-[4/5] w-full bg-border/20 overflow-hidden lg:sticky lg:top-32">
{imageUrl ? (
<Image
src={imageUrl}
alt={imageAlt}
fill
sizes="(max-width: 1024px) 100vw, 50vw"
className="object-cover w-full h-full grayscale hover:grayscale-0 transition-all duration-700"
/>
) : (
<div className="w-full h-full flex flex-col items-center justify-center bg-card text-border gap-4 p-8 text-center border border-border">
<ImageIcon size={64} strokeWidth={1} />
<span className="text-xs tracking-widest uppercase font-semibold text-foreground/40">ZDJĘCIE ILUSTRACYJNE</span>
</div>
)}
<div className="absolute inset-0 bg-gradient-to-t from-background/40 to-transparent pointer-events-none" />
</div>
</div>
</div>
+16 -6
View File
@@ -10,6 +10,7 @@ import { AffiliationBlock } from './Affiliation/Component'
import { TestimonialsBlock } from './Testimonials/Component'
import { TestimonialsGridBlock } from './TestimonialsGrid/Component'
import { FAQTeaserBlock } from './FAQTeaser/Component'
import { FAQFullBlockComponent } from './FAQFull/Component'
import { FinalCTABlock } from './FinalCTA/Component'
import { PageHeaderBlock } from './PageHeader/Component'
@@ -17,6 +18,7 @@ import { ContactInfoBlock } from './ContactInfo/Component'
import { BioBlock } from './Bio/Component'
import { TeamBlock } from './Team/Component'
import { ServicesDetailedBlock } from './ServicesDetailed/Component'
import { TextSectionBlock } from './TextSection/Component'
import { EligibilityBlock } from './Eligibility/Component'
import { StatsBlock } from './Stats/Component'
@@ -37,14 +39,22 @@ const blockComponents = {
testimonials: TestimonialsBlock,
testimonialsGrid: TestimonialsGridBlock,
faqTeaser: FAQTeaserBlock,
faqFull: FAQFullBlockComponent,
finalCta: FinalCTABlock,
TextSection: ({ text }: { text: string }) => <section className="py-20"><div className="container mx-auto px-4"><p>{text}</p></div></section>,
TextSection: TextSectionBlock,
}
export const RenderBlocks: React.FC<{
blocks: Page['layout']
}> = (props) => {
const { blocks } = props
import { getGlobal } from '../lib/content'
import type { Config } from '../payload-types'
export const RenderBlocks = async (props: {
blocks: Page['layout'],
locale: string
}) => {
const { blocks, locale } = props
const companyData = await getGlobal('company', 0, locale as Config['locale'])
const whatsappNumber = companyData?.whatsappNumber
const hasBlocks = blocks && Array.isArray(blocks) && blocks.length > 0
@@ -58,7 +68,7 @@ export const RenderBlocks: React.FC<{
const Block = blockComponents[blockType as keyof typeof blockComponents]
if (Block) {
return (
<Block key={index} id={`block-${index}`} {...(block as any)} />
<Block key={index} id={`block-${index}`} {...(block as any)} whatsappNumber={whatsappNumber} />
)
}
}
+41 -38
View File
@@ -47,61 +47,64 @@ export const ServicesDetailedBlock: React.FC<Props> = ({
)}
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-12 gap-6 md:gap-8">
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-100px" }}
variants={{
hidden: {},
visible: { transition: { staggerChildren: 0.08 } }
}}
className="grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8"
>
{serviceList.map((service, index) => {
const Icon = (service.icon && iconMap[service.icon]) || FileText
const isFirst = hasBento && index === 0
const isSecondOrThird = hasBento && (index === 1 || index === 2)
// Asymmetric Bento grid layout:
// 1st: col-span-6 on desktop, dark card
// 2nd & 3rd: col-span-3 on desktop, light card
// Remaining (4+): col-span-4 on desktop, light card
// Fallback (< 3 items): col-span-4 on desktop, light card
let colSpanClasses = 'col-span-1 sm:col-span-1 lg:col-span-4'
if (isFirst) {
colSpanClasses = 'col-span-1 sm:col-span-2 lg:col-span-6'
} else if (isSecondOrThird) {
colSpanClasses = 'col-span-1 sm:col-span-1 lg:col-span-3'
}
const isFirst = index === 0
return (
<motion.div
key={service.id || index}
whileHover={{ y: -4 }}
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
className={`${colSpanClasses} ${
isFirst
? 'bg-foreground text-background border-0 shadow-md p-8 md:p-10'
: 'bg-card text-foreground border border-border shadow-xs p-6 md:p-8'
} rounded-none min-h-[240px] flex flex-col justify-between overflow-hidden`}
variants={{
hidden: { opacity: 0, y: isFirst ? 24 : 16 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.6, ease: [0.16, 1, 0.3, 1] }
}
}}
className={`${isFirst ? 'md:col-span-2' : 'md:col-span-1'}
bg-card border border-border p-6 md:p-8 flex flex-col relative overflow-hidden group hover:border-border/80 transition-colors
`}
>
<div className="flex-1 min-w-0">
<div className="text-accent mb-6">
<Icon size={isFirst ? 32 : 28} />
{/* Górny cienki akcent (line-draw przy wejściu) */}
<motion.div
variants={{
hidden: { scaleX: 0 },
visible: {
scaleX: 1,
transition: { delay: 0.2 + (index * 0.08), duration: 0.7, ease: "easeOut" }
}
}}
style={{ originX: 0 }}
className="absolute top-0 left-0 w-full h-[2px] bg-accent"
/>
<div className={`flex ${isFirst ? 'flex-row items-center gap-4 mb-4' : 'flex-row items-center gap-3 mb-3'}`}>
<div className="text-accent shrink-0">
<Icon size={isFirst ? 28 : 20} strokeWidth={2.5} />
</div>
<h3
className={`font-heading font-bold mb-3 leading-snug break-words hyphens-auto ${
isFirst
? 'text-2xl sm:text-3xl text-background'
: 'text-xl sm:text-2xl text-foreground'
}`}
>
<h3 className={`font-heading font-bold text-foreground leading-tight ${isFirst ? 'text-2xl sm:text-3xl' : 'text-xl sm:text-2xl'}`}>
{service.title}
</h3>
</div>
<p
className={`mt-4 text-sm sm:text-base leading-relaxed ${
isFirst ? 'text-background/80' : 'text-foreground/75'
}`}
>
<p className={`text-foreground/75 leading-relaxed ${isFirst ? 'text-base sm:text-lg max-w-[70ch]' : 'text-sm sm:text-base mt-2 max-w-[70ch]'}`}>
{service.description}
</p>
</motion.div>
)
})}
</div>
</motion.div>
</div>
</section>
)
+47 -13
View File
@@ -1,12 +1,32 @@
'use client'
import React from 'react'
import type { Page } from '../../payload-types'
import { motion } from 'framer-motion'
import { ArrowRight, CheckCircle } from 'lucide-react'
const containerVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.1,
}
}
}
const cardVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] }
}
}
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'servicesList' }>
export const ServicesListBlock: React.FC<Props> = ({ title, services }) => {
return (
<section className="py-20 md:py-32 bg-background text-foreground">
<section className="py-24 md:py-32 relative overflow-hidden" id="uslugi">
<div className="container-premium">
<div className="max-w-3xl mb-16 md:mb-24">
<h2 className="font-heading text-3xl sm:text-4xl md:text-5xl font-extrabold text-foreground tracking-tight max-w-2xl text-balance">
@@ -14,25 +34,39 @@ export const ServicesListBlock: React.FC<Props> = ({ title, services }) => {
</h2>
</div>
<div className="border-t border-border">
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-50px" }}
variants={containerVariants}
className="grid grid-cols-1 md:grid-cols-3 gap-8"
>
{services?.map((service: NonNullable<Props['services']>[0], index: number) => (
<div
<motion.div
key={service.id || index}
className="group border-b border-border py-8 md:py-12 flex flex-col md:flex-row gap-6 md:gap-12 lg:gap-24 items-start md:items-center hover:bg-foreground/[0.02] transition-colors"
variants={cardVariants}
className="group flex flex-col bg-card border border-border p-8 hover:shadow-sm hover:border-border/80 transition-all duration-300"
>
<div className="md:w-1/3 shrink-0">
<h3 className="font-heading text-xl sm:text-2xl font-bold text-foreground group-hover:text-accent transition-colors">
<div className="flex items-center gap-3 mb-6">
<CheckCircle className="text-accent w-5 h-5 shrink-0" />
<h3 className="font-heading text-xl sm:text-2xl font-bold text-foreground">
{service.title}
</h3>
</div>
<div className="md:w-7/12 flex-grow">
<p className="text-foreground/75 text-lg md:text-xl font-light leading-relaxed">
{service.description}
</p>
</div>
</div>
<p className="text-foreground/75 text-base md:text-lg font-light leading-relaxed flex-grow">
{service.description}
</p>
<a
href={service.linkUrl || '#'}
className="mt-8 inline-flex items-center gap-2 text-accent font-bold hover:text-accent-hover transition-colors"
>
{service.linkText || 'Zobacz więcej'}
<ArrowRight size={18} />
</a>
</motion.div>
))}
</div>
</motion.div>
</div>
</section>
)
+2
View File
@@ -13,6 +13,8 @@ export const ServicesList: Block = {
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'description', type: 'textarea', required: true },
{ name: 'linkText', type: 'text' },
{ name: 'linkUrl', type: 'text' },
],
},
],
+42 -4
View File
@@ -1,6 +1,35 @@
'use client'
import React from 'react'
import type { Page } from '../../payload-types'
import { motion, animate, useInView } from 'framer-motion'
import { useRef, useEffect } from 'react'
const CountUp = ({ text }: { text: string }) => {
const ref = useRef<HTMLSpanElement>(null)
const inView = useInView(ref, { once: true })
useEffect(() => {
if (inView && ref.current) {
const match = text.match(/^(\d+)([%+]?)$/)
if (match) {
const [_, numStr, suffix] = match
const num = parseInt(numStr, 10)
animate(0, num, {
duration: 1.5,
ease: 'easeOut',
onUpdate: (val) => {
if (ref.current) ref.current.textContent = `${Math.floor(val)}${suffix}`
}
})
}
}
}, [inView, text])
const isNumeric = /^\d+[%+]?$/.test(text)
if (!isNumeric) return <span>{text}</span>
return <span ref={ref}>{text.replace(/\d+/, '0')}</span>
}
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'stats' }>
@@ -8,7 +37,7 @@ export const StatsBlock: React.FC<Props> = ({ title, subtitle, items }) => {
if (!items || items.length === 0) return null
return (
<section className="py-20 md:py-32 bg-background border-y border-border">
<section className="py-20 md:py-32 relative">
<div className="container-premium">
{(title || subtitle) && (
<div className="max-w-3xl mb-16 md:mb-24">
@@ -25,14 +54,14 @@ export const StatsBlock: React.FC<Props> = ({ title, subtitle, items }) => {
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-3 border-t border-l border-border mt-12">
<div className="grid grid-cols-1 md:grid-cols-3 divide-y md:divide-y-0 md:divide-x divide-border mt-12 border-y border-border/30">
{items.map((item, index) => (
<div
key={item.id || index}
className="flex flex-col p-8 sm:p-12 border-b border-r border-border hover:bg-foreground/[0.02] transition-colors"
className="flex flex-col p-8 sm:p-12 hover:bg-foreground/[0.02] transition-colors"
>
<div className="font-heading text-5xl lg:text-7xl font-extrabold text-accent mb-4 tracking-tight">
{item.stat}
<CountUp text={item.stat} />
</div>
<div className="font-heading text-xl font-bold text-foreground mb-2">
{item.label}
@@ -46,6 +75,15 @@ export const StatsBlock: React.FC<Props> = ({ title, subtitle, items }) => {
))}
</div>
</div>
{/* Line Draw Animation na samym dole sekcji */}
<motion.div
initial={{ scaleX: 0 }}
whileInView={{ scaleX: 1 }}
viewport={{ once: true }}
transition={{ duration: 1.5, ease: "easeInOut" }}
className="absolute bottom-0 left-0 w-full h-[1px] bg-border origin-left"
/>
</section>
)
}
+1 -1
View File
@@ -8,7 +8,7 @@ import { motion } from 'framer-motion'
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'team' }>
export const TeamBlock: React.FC<Props> = ({ members }) => {
if (!members || members.length === 0) return null
if (!members || members.length <= 1) return null
return (
<section className="py-20 md:py-32 bg-background border-y border-border overflow-hidden">
+46 -52
View File
@@ -1,32 +1,33 @@
'use client'
import React, { useState, useEffect } from 'react'
import React from 'react'
import type { Page } from '../../payload-types'
import { motion } from 'framer-motion'
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'testimonials' }>
export const TestimonialsBlock: React.FC<Props> = ({ title, testimonials }) => {
const [currentIndex, setCurrentIndex] = useState(0)
const [lastActionTime, setLastActionTime] = useState(Date.now())
useEffect(() => {
if (!testimonials || testimonials.length <= 1) return
const interval = setInterval(() => {
setCurrentIndex((prev) => (prev + 1) % testimonials.length)
}, 7500)
return () => clearInterval(interval)
}, [testimonials, lastActionTime])
const handleDotClick = (index: number) => {
setCurrentIndex(index)
setLastActionTime(Date.now())
const containerVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.1,
}
}
}
const cardVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.5, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] }
}
}
export const TestimonialsBlock: React.FC<Props> = ({ title, testimonials }) => {
if (!testimonials || testimonials.length === 0) return null
return (
<section className="relative py-24 md:py-40 bg-background border-y border-border overflow-hidden">
<section className="relative py-24 md:py-40 bg-background overflow-hidden">
<div className="container-premium relative z-10">
<div className="max-w-3xl mb-16 md:mb-24">
{title && (
@@ -36,50 +37,43 @@ export const TestimonialsBlock: React.FC<Props> = ({ title, testimonials }) => {
)}
</div>
<div className="relative min-h-[300px] sm:min-h-[240px]">
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-50px" }}
variants={containerVariants}
className="grid grid-cols-1 md:grid-cols-2 gap-8"
>
{testimonials.map((t: NonNullable<Props['testimonials']>[0], index: number) => {
const initial = t.author ? t.author.charAt(0).toUpperCase() : ''
return (
<div
<motion.div
key={t.id || index}
className={`absolute inset-0 flex flex-col items-start justify-start transition-opacity duration-1000 ease-in-out ${
index === currentIndex ? 'opacity-100 z-10' : 'opacity-0 z-0 pointer-events-none'
}`}
variants={cardVariants}
className="bg-card border border-border p-8 md:p-12 flex flex-col items-start shadow-sm"
>
<p className="relative z-10 font-heading text-3xl sm:text-4xl md:text-5xl font-extrabold text-foreground leading-[1.2] mb-10 text-balance tracking-tight max-w-4xl">
{t.quote}
<div className="text-accent mb-6">
<svg width="40" height="40" viewBox="0 0 24 24" fill="currentColor">
<path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z" />
</svg>
</div>
<p className="font-sans italic text-xl sm:text-2xl text-foreground/90 leading-relaxed mb-10 flex-grow">
"{t.quote}"
</p>
<div className="flex items-center gap-6">
<div className="hidden sm:block w-12 h-px bg-accent/60" />
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-full bg-accent text-background font-heading flex items-center justify-center text-xl sm:text-2xl font-bold shrink-0">
{initial}
</div>
<p className="font-heading font-bold text-lg sm:text-xl text-foreground/80 uppercase tracking-wide">
{t.author}
</p>
<div className="flex items-center gap-4 mt-auto">
<div className="w-12 h-12 rounded-full bg-accent text-background font-heading flex items-center justify-center text-xl font-bold shrink-0">
{initial}
</div>
<p className="font-heading font-bold text-lg text-foreground uppercase tracking-wide">
{t.author}
</p>
</div>
</div>
</motion.div>
)
})}
</div>
{testimonials.length > 1 && (
<div className="flex items-center gap-3 mt-16 md:mt-24">
{testimonials.map((_, index) => (
<button
key={index}
onClick={() => handleDotClick(index)}
className={`w-2 h-2 transition-all ${
index === currentIndex ? 'bg-accent w-8' : 'bg-border hover:bg-foreground/30'
}`}
aria-label={`Pokaż opinię ${index + 1}`}
/>
))}
</div>
)}
</motion.div>
</div>
</section>
)
+20
View File
@@ -0,0 +1,20 @@
'use client'
import React from 'react'
import type { Page } from '../../payload-types'
import { RichText } from '@payloadcms/richtext-lexical/react'
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'TextSection' }>
export const TextSectionBlock: React.FC<Props> = ({ text }) => {
if (!text) return null;
return (
<section className="py-20 md:py-32 bg-background text-foreground">
<div className="container-premium max-w-4xl">
<div className="prose prose-lg dark:prose-invert prose-headings:font-heading prose-a:text-accent hover:prose-a:text-accent-hover">
{typeof text === 'string' ? <p>{text}</p> : <RichText data={text} />}
</div>
</div>
</section>
)
}
+11
View File
@@ -0,0 +1,11 @@
import type { Block } from 'payload'
export const TextSection: Block = {
slug: 'TextSection',
fields: [
{
name: 'text',
type: 'richText',
},
],
}
+36 -9
View File
@@ -1,4 +1,6 @@
'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'
@@ -11,28 +13,53 @@ const iconMap = {
star: Star,
}
const containerVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.08,
}
}
}
const itemVariants = {
hidden: { opacity: 0, y: 15 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4, ease: [0.22, 1, 0.36, 1] as [number, number, number, number] }
}
}
export const TrustBarBlock: React.FC<Props> = ({ items }) => {
if (!items || items.length === 0) return null
return (
<section className="bg-background py-8 border-b border-border">
<section className="bg-card py-10 border-b border-border">
<div className="container-premium">
<div className="flex flex-wrap gap-x-8 gap-y-6 md:gap-x-12 lg:gap-x-16 items-start justify-start">
<motion.div
initial="hidden"
whileInView="visible"
viewport={{ once: true, margin: "-50px" }}
variants={containerVariants}
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 divide-y sm:divide-y-0 sm:divide-x divide-border"
>
{items.map((item: NonNullable<Props['items']>[0], index: number) => {
const Icon = item.icon ? iconMap[item.icon as keyof typeof iconMap] : CheckCircle
return (
<div
<motion.div
key={item.id || index}
className="flex items-center gap-3"
variants={itemVariants}
className={`flex items-center gap-4 ${index !== 0 ? 'sm:pl-6' : ''} ${index > 1 ? 'lg:pl-6' : ''} pt-6 sm:pt-0`}
>
<div className="text-foreground flex-shrink-0">
<Icon size={24} />
<div className="text-accent flex-shrink-0">
<Icon size={24} strokeWidth={1.5} />
</div>
<span className="text-sm font-medium text-foreground">{item.text}</span>
</div>
<span className="text-sm md:text-base font-medium text-foreground">{item.text}</span>
</motion.div>
)
})}
</div>
</motion.div>
</div>
</section>
)
+41 -21
View File
@@ -2,52 +2,72 @@
import React from 'react'
import type { Page } from '../../payload-types'
import Image from 'next/image'
import { CheckCircle } from 'lucide-react'
import { CheckCircle, ImageIcon } from 'lucide-react'
import { motion } from 'framer-motion'
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'uniqueValue' }>
export const UniqueValueBlock: React.FC<Props> = ({ eyebrow, features, title, description, image }) => {
const imageUrl = image && typeof image === 'object' && image.url ? image.url : null
const imageAlt = (image && typeof image === 'object' && image.alt) || "Rozliczenia bez bariery językowej - komunikacja"
return (
<section className="py-20 md:py-32 bg-card/60 border-y border-border">
<div className="container-premium">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 lg:gap-24 items-center">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 lg:gap-24 items-center overflow-hidden">
{/* Kolumna ze zdjęciem */}
<div className="lg:col-span-5 order-2 lg:order-1">
<div className="relative aspect-[4/5] overflow-hidden border border-border">
{/* PLACEHOLDER - do podmiany na realne zdjęcia klienta */}
<Image
src={image && typeof image === 'object' && image.url ? image.url : "https://images.unsplash.com/photo-1454165804606-c3d57bc86b40?auto=format&fit=crop&w=1000&q=80"}
alt={(image && typeof image === 'object' && image.alt) || "Dokumenty i profesjonalne doradztwo podatkowe"}
fill
sizes="(max-width: 1024px) 100vw, 40vw"
className="object-cover grayscale hover:grayscale-0 transition-all duration-700"
/>
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1] }}
className="lg:col-span-5 order-2 lg:order-1"
>
<div className="relative aspect-[4/5] overflow-hidden border border-border bg-card">
{imageUrl ? (
<Image
src={imageUrl}
alt={imageAlt}
fill
sizes="(max-width: 1024px) 100vw, 40vw"
className="object-cover grayscale hover:grayscale-0 transition-all duration-700"
/>
) : (
<div className="w-full h-full flex flex-col items-center justify-center text-border gap-4 p-8 text-center border border-border">
<ImageIcon size={64} strokeWidth={1} />
<span className="text-xs tracking-widest uppercase font-semibold text-foreground/40">ZDJĘCIE ILUSTRACYJNE</span>
</div>
)}
</div>
</div>
</motion.div>
{/* Kolumna tekstowa */}
<div className="lg:col-span-7 order-1 lg:order-2 flex flex-col items-start max-w-3xl">
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.6, ease: [0.22, 1, 0.36, 1], delay: 0.1 }}
className="lg:col-span-7 order-1 lg:order-2 flex flex-col items-start max-w-3xl"
>
<h2 className="font-heading text-3xl sm:text-4xl md:text-5xl font-extrabold text-foreground tracking-tight mb-6 text-balance">
{title}
</h2>
<p className="text-foreground/75 text-lg md:text-xl font-light leading-relaxed mb-12">
<p className="text-foreground/75 text-lg md:text-xl font-light leading-relaxed mb-12 max-w-[65ch]">
{description}
</p>
{features && features.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 w-full pt-6 border-t border-border/60 text-base">
{features.map((feature: any, index: number) => (
<div key={feature.id || index} className="flex items-center gap-2.5 text-foreground/80 font-medium">
<span className="w-2 h-2 rounded-full bg-accent" />
{features.map((feature: NonNullable<Props['features']>[0], index: number) => (
<div key={feature.id || index} className="flex items-center gap-3 text-foreground/90 font-medium">
<CheckCircle className="text-accent flex-shrink-0" size={20} strokeWidth={2} />
<span>{feature.text}</span>
</div>
))}
</div>
)}
</div>
</motion.div>
</div>
</div>
</section>
+4 -9
View File
@@ -8,10 +8,12 @@ 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 { FAQFull } from '../blocks/FAQFull/config'
import { FinalCTA } from '../blocks/FinalCTA/config'
import { PageHeader } from '../blocks/PageHeader/config'
import { ContactInfo } from '../blocks/ContactInfo/config'
import { TextSection } from '../blocks/TextSection/config'
import { ServicesDetailed } from '../blocks/ServicesDetailed/config'
import { Eligibility } from '../blocks/Eligibility/config'
import { Stats } from '../blocks/Stats/config'
@@ -57,16 +59,9 @@ export const Pages: CollectionConfig = {
Testimonials,
TestimonialsGrid,
FAQTeaser,
FAQFull,
FinalCTA,
{
slug: 'TextSection',
fields: [
{
name: 'text',
type: 'text',
}
]
}
TextSection,
]
}
],
+50
View File
@@ -0,0 +1,50 @@
'use client'
import React, { useEffect, useState } from 'react'
import { MessageCircle } from 'lucide-react'
import { motion, AnimatePresence } from 'framer-motion'
export const FloatingWhatsApp = ({ whatsappLink, ariaLabel }: { whatsappLink: string, ariaLabel: string }) => {
const [isVisible, setIsVisible] = useState(true)
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
setIsVisible(!entry.isIntersecting)
})
}, { rootMargin: '0px 0px -100px 0px' })
const finalCta = document.getElementById('final-cta')
if (finalCta) {
observer.observe(finalCta)
}
return () => {
if (finalCta) observer.unobserve(finalCta)
observer.disconnect()
}
}, [])
return (
<AnimatePresence>
{isVisible && (
<motion.div
className="fixed bottom-6 right-6 z-40"
initial={{ scale: 1.5, rotate: 10, opacity: 0 }}
animate={{ scale: 1, rotate: 0, opacity: 1 }}
exit={{ scale: 0.8, opacity: 0 }}
transition={{ duration: 0.4, ease: [0.175, 0.885, 0.32, 1.275] }}
>
<a
href={whatsappLink}
target="_blank"
rel="noopener noreferrer"
className="flex h-14 w-14 items-center justify-center rounded-full bg-[#25D366] text-[#1C1B1A] shadow-lg transition-transform hover:scale-105 hover:bg-[#20bd5a] focus:outline-none focus:ring-4 focus:ring-[#25D366]/50"
aria-label={ariaLabel}
>
<MessageCircle size={28} />
</a>
</motion.div>
)}
</AnimatePresence>
)
}
+13 -10
View File
@@ -9,7 +9,7 @@ import { Menu, X, MessageCircle } from "lucide-react";
import { i18n } from '../../i18n.config';
import { LocalizedLink } from '../LocalizedLink';
export function Header({ navItems = [] }: { navItems?: any[] }) {
export function Header({ navItems = [], whatsappNumber }: { navItems?: any[], whatsappNumber?: string | null }) {
const pathname = usePathname();
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const [isScrolled, setIsScrolled] = useState(false);
@@ -24,6 +24,7 @@ export function Header({ navItems = [] }: { navItems?: any[] }) {
const currentLocaleCode = (pathname?.split('/').filter(Boolean)[0]) || i18n.defaultLocale;
const currentLocale = i18n.locales.find(l => l.code === currentLocaleCode) || i18n.locales[0];
const whatsappLink = whatsappNumber ? `https://wa.me/${whatsappNumber}` : '#';
return (
<>
@@ -139,15 +140,17 @@ export function Header({ navItems = [] }: { navItems?: any[] }) {
<LanguageSwitcher />
</div>
<Link
href={process.env.NEXT_PUBLIC_WHATSAPP_NUMBER ? `https://wa.me/${process.env.NEXT_PUBLIC_WHATSAPP_NUMBER}` : '#'}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 bg-green-500 hover:bg-green-600 text-white px-8 py-4 rounded-none transition-colors w-full max-w-xs shadow-lg"
>
<MessageCircle size={24} />
<span className="font-bold text-lg">WhatsApp</span>
</Link>
{whatsappNumber && (
<Link
href={whatsappLink}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 bg-[#25D366] hover:bg-[#20bd5a] text-[#1C1B1A] px-8 py-4 rounded-none transition-colors w-full max-w-xs shadow-lg"
>
<MessageCircle size={24} />
<span className="font-bold text-lg">WhatsApp</span>
</Link>
)}
</div>
</div>
</>
+19
View File
@@ -0,0 +1,19 @@
'use client'
import React from 'react'
import { motion } from 'framer-motion'
export const MotionStamp: React.FC<{ children: React.ReactNode, className?: string, id?: string, style?: React.CSSProperties }> = ({ children, className, id, style }) => {
return (
<motion.section
id={id}
style={style}
initial={{ scale: 1.1, rotate: 2, opacity: 0 }}
whileInView={{ scale: 1, rotate: 0, opacity: 1 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ type: "spring", stiffness: 100, damping: 15 }}
className={className}
>
{children}
</motion.section>
)
}
+3 -1
View File
@@ -30,7 +30,9 @@ export const Company: GlobalConfig = {
name: 'whatsappNumber',
type: 'text',
localized: true,
defaultValue: () => process.env.NEXT_PUBLIC_WHATSAPP_NUMBER || '',
admin: {
description: 'Numer WhatsApp do kontaktu. Wpisz z numerem kierunkowym bez + (np. 48123456789).',
},
},
],
}
+23
View File
@@ -0,0 +1,23 @@
import { getPayload } from 'payload';
import configPromise from '@payload-config';
import { cache } from 'react';
import type { Config } from '../payload-types';
export const getGlobal = cache(async <T extends keyof Config['globals']>(
slug: T,
depth = 0,
locale: Config['locale'] | 'all' = 'all'
) => {
const payload = await getPayload({ config: configPromise });
const global = await payload.findGlobal({
slug,
depth,
locale,
overrideAccess: false,
}).catch((err) => {
payload.logger.error(`[getGlobal] Failed to fetch '${slug}': ${err instanceof Error ? err.message : String(err)}`);
return null;
});
return global;
});
+68 -1
View File
@@ -360,6 +360,8 @@ export interface Page {
| {
title: string;
description: string;
linkText?: string | null;
linkUrl?: string | null;
id?: string | null;
}[]
| null;
@@ -432,15 +434,39 @@ export interface Page {
title: string;
questions?:
| {
category?: string | null;
question: string;
answer: string;
id?: string | null;
}[]
| null;
ctaText?: string | null;
ctaUrl?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'faqTeaser';
}
| {
title: string;
subtitle?: string | null;
questions?:
| {
category: string;
/**
* Zaznacz, jeśli treść nie jest jeszcze gotowa do publikacji (pytanie nie pojawi się na stronie ani w SEO)
*/
isDraft?: boolean | null;
question: string;
answer: string;
id?: string | null;
}[]
| null;
ctaText?: string | null;
ctaUrl?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'faqFull';
}
| {
eyebrow?: string | null;
title: string;
@@ -451,7 +477,21 @@ export interface Page {
blockType: 'finalCta';
}
| {
text?: string | null;
text?: {
root: {
type: string;
children: {
type: any;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
id?: string | null;
blockName?: string | null;
blockType: 'TextSection';
@@ -804,6 +844,8 @@ export interface PagesSelect<T extends boolean = true> {
| {
title?: T;
description?: T;
linkText?: T;
linkUrl?: T;
id?: T;
};
id?: T;
@@ -883,10 +925,32 @@ export interface PagesSelect<T extends boolean = true> {
questions?:
| T
| {
category?: T;
question?: T;
answer?: T;
id?: T;
};
ctaText?: T;
ctaUrl?: T;
id?: T;
blockName?: T;
};
faqFull?:
| T
| {
title?: T;
subtitle?: T;
questions?:
| T
| {
category?: T;
isDraft?: T;
question?: T;
answer?: T;
id?: T;
};
ctaText?: T;
ctaUrl?: T;
id?: T;
blockName?: T;
};
@@ -975,6 +1039,9 @@ export interface Company {
phone?: string | null;
email?: string | null;
hours?: string | null;
/**
* Numer WhatsApp do kontaktu. Wpisz z numerem kierunkowym bez + (np. 48123456789).
*/
whatsappNumber?: string | null;
updatedAt?: string | null;
createdAt?: string | null;
View File