Fix FAQFull: Use isDraft boolean for draft filtering, fix Kontakt ID override
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Block } from 'payload'
|
||||
|
||||
export const TextSection: Block = {
|
||||
slug: 'TextSection',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'richText',
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user