feat: implement UX patterns inspired by Greenback (Hero slider, Stats block, numbered Process cards)
This commit is contained in:
@@ -39,6 +39,26 @@ const homeLayoutPl = [
|
|||||||
title: 'Rozliczenia bez bariery językowej',
|
title: 'Rozliczenia bez bariery językowej',
|
||||||
description: 'Niemieckie prawo podatkowe bywa skomplikowane. Przeprowadzę Cię przez cały proces w Twoim ojczystym języku, dbając o maksymalny zwrot i zero stresu.',
|
description: 'Niemieckie prawo podatkowe bywa skomplikowane. Przeprowadzę Cię przez cały proces w Twoim ojczystym języku, dbając o maksymalny zwrot i zero stresu.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
blockType: 'stats',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
stat: '20+',
|
||||||
|
label: 'Lat doświadczenia',
|
||||||
|
description: 'w prawie podatkowym i rozliczeniach dla Polaków w Niemczech',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stat: '100%',
|
||||||
|
label: 'Obsługa po polsku',
|
||||||
|
description: 'bezpośredni kontakt i wyjaśnienie każdej kwestii podatkowej',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stat: '§4 Nr. 11',
|
||||||
|
label: 'Stowarzyszenie b.b.h.',
|
||||||
|
description: 'oficjalna podstawa prawna doradztwa zgodnie ze StBerG',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
blockType: 'servicesList',
|
blockType: 'servicesList',
|
||||||
title: 'Czym się zajmuję',
|
title: 'Czym się zajmuję',
|
||||||
@@ -113,6 +133,26 @@ const homeLayoutDe = [
|
|||||||
title: 'Steuererklärungen ohne Sprachbarriere',
|
title: 'Steuererklärungen ohne Sprachbarriere',
|
||||||
description: 'Das deutsche Steuerrecht kann kompliziert sein. Ich begleite Sie durch den gesamten Prozess in Ihrer Muttersprache, sorge für die maximale Erstattung und garantiere null Stress.',
|
description: 'Das deutsche Steuerrecht kann kompliziert sein. Ich begleite Sie durch den gesamten Prozess in Ihrer Muttersprache, sorge für die maximale Erstattung und garantiere null Stress.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
blockType: 'stats',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
stat: '20+',
|
||||||
|
label: 'Jahre Erfahrung',
|
||||||
|
description: 'in der Steuerberatung für in Deutschland arbeitende Polen',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stat: '100%',
|
||||||
|
label: 'Auf Polnisch',
|
||||||
|
description: 'direkte Kommunikation in Ihrer vertrauten Muttersprache',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stat: '§4 Nr. 11',
|
||||||
|
label: 'b.b.h. e.V.',
|
||||||
|
description: 'gesetzliche Beratungsbefugnis nach dem Steuerberatungsgesetz',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
blockType: 'servicesList',
|
blockType: 'servicesList',
|
||||||
title: 'Was ich anbiete',
|
title: 'Was ich anbiete',
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import React from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { motion } from 'framer-motion'
|
|
||||||
import type { Page } from '../../payload-types'
|
import type { Page } from '../../payload-types'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
@@ -8,7 +7,32 @@ import { Shield, MessageCircle } from 'lucide-react'
|
|||||||
|
|
||||||
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'hero' }>
|
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'hero' }>
|
||||||
|
|
||||||
|
/* PLACEHOLDER - do podmiany na realne zdjęcia klienta */
|
||||||
|
const heroImages = [
|
||||||
|
{
|
||||||
|
url: 'https://images.unsplash.com/photo-1554224155-8d04cb21cd6c?auto=format&fit=crop&w=1200&q=80',
|
||||||
|
alt: 'Dokumenty podatkowe i praca biurowa',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&w=1200&q=80',
|
||||||
|
alt: 'Profesjonalne biuro doradztwa podatkowego',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?auto=format&fit=crop&w=1200&q=80',
|
||||||
|
alt: 'Praca z rozliczeniami podatkowymi',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
export const HeroBlock: React.FC<Props> = ({ title, subtitle, primaryCta, secondaryCta, yearsOfExperience }) => {
|
export const HeroBlock: React.FC<Props> = ({ title, subtitle, primaryCta, secondaryCta, yearsOfExperience }) => {
|
||||||
|
const [currentSlide, setCurrentSlide] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setCurrentSlide((prev) => (prev + 1) % heroImages.length)
|
||||||
|
}, 5000)
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative bg-background text-foreground py-16 md:py-24 border-b border-border/60">
|
<section className="relative bg-background text-foreground py-16 md:py-24 border-b border-border/60">
|
||||||
<div className="container-premium">
|
<div className="container-premium">
|
||||||
@@ -51,23 +75,46 @@ export const HeroBlock: React.FC<Props> = ({ title, subtitle, primaryCta, second
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Kolumna ze zdjęciem */}
|
{/* Kolumna ze sliderem zdjęć */}
|
||||||
<div className="lg:col-span-5 relative">
|
<div className="lg:col-span-5 relative">
|
||||||
<div className="relative mx-auto max-w-md lg:max-w-none">
|
<div className="relative mx-auto max-w-md lg:max-w-none">
|
||||||
<div className="relative aspect-[4/3] sm:aspect-[5/4] rounded-2xl overflow-hidden border border-border shadow-md bg-card">
|
<div className="relative aspect-[4/3] sm:aspect-[5/4] rounded-2xl overflow-hidden border border-border shadow-md bg-card">
|
||||||
{/* PLACEHOLDER - do podmiany na realne zdjęcia klienta */}
|
{heroImages.map((img, idx) => (
|
||||||
<Image
|
<div
|
||||||
src="https://images.unsplash.com/photo-1497366216548-37526070297c?auto=format&fit=crop&w=1200&q=80"
|
key={idx}
|
||||||
alt="Biuro doradztwa podatkowego"
|
className={`absolute inset-0 transition-opacity duration-700 ease-in-out ${
|
||||||
fill
|
idx === currentSlide ? 'opacity-100 z-10' : 'opacity-0 z-0 pointer-events-none'
|
||||||
priority
|
}`}
|
||||||
sizes="(max-width: 1024px) 100vw, 50vw"
|
>
|
||||||
className="object-cover"
|
{/* PLACEHOLDER - do podmiany na realne zdjęcia klienta */}
|
||||||
/>
|
<Image
|
||||||
|
src={img.url}
|
||||||
|
alt={img.alt}
|
||||||
|
fill
|
||||||
|
priority={idx === 0}
|
||||||
|
sizes="(max-width: 1024px) 100vw, 50vw"
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Kropki nawigacji slidera */}
|
||||||
|
<div className="absolute bottom-3 right-3 z-20 flex items-center gap-1.5 bg-background/80 backdrop-blur-md px-2.5 py-1 rounded-full border border-border/80">
|
||||||
|
{heroImages.map((_, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
onClick={() => setCurrentSlide(idx)}
|
||||||
|
className={`w-2 h-2 rounded-full transition-all ${
|
||||||
|
idx === currentSlide ? 'bg-accent w-4' : 'bg-foreground/30 hover:bg-foreground/60'
|
||||||
|
}`}
|
||||||
|
aria-label={`Przejdź do zdjęcia ${idx + 1}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Subtelna plakietka informacyjna */}
|
{/* Subtelna plakietka informacyjna */}
|
||||||
<div className="absolute -bottom-5 -left-4 sm:left-6 bg-card/95 backdrop-blur-sm border border-border rounded-xl p-4 shadow-lg flex items-center gap-3.5 max-w-xs">
|
<div className="absolute -bottom-5 -left-4 sm:left-6 bg-card/95 backdrop-blur-sm border border-border rounded-xl p-4 shadow-lg flex items-center gap-3.5 max-w-xs z-30">
|
||||||
<div className="w-10 h-10 rounded-lg bg-accent text-white font-bold font-heading flex items-center justify-center shrink-0">
|
<div className="w-10 h-10 rounded-lg bg-accent text-white font-bold font-heading flex items-center justify-center shrink-0">
|
||||||
JK
|
JK
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,61 +1,60 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { motion } from 'framer-motion'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import type { Page } from '../../payload-types'
|
import type { Page } from '../../payload-types'
|
||||||
|
import { ArrowRight } from 'lucide-react'
|
||||||
|
|
||||||
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'process' }>
|
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'process' }>
|
||||||
|
|
||||||
export const ProcessBlock: React.FC<Props> = ({ title, steps, ctaText }) => {
|
export const ProcessBlock: React.FC<Props> = ({ title, steps, ctaText }) => {
|
||||||
return (
|
return (
|
||||||
<section className="py-24 bg-background text-foreground">
|
<section className="py-20 md:py-28 bg-background text-foreground border-t border-border">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container-premium">
|
||||||
<motion.h2
|
<div className="max-w-2xl mx-auto text-center mb-16">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
<h2 className="font-heading text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight text-foreground mb-4">
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
{title}
|
||||||
viewport={{ once: true }}
|
</h2>
|
||||||
className="text-3xl md:text-5xl font-bold mb-16 text-center"
|
<p className="text-foreground/70 text-base sm:text-lg">
|
||||||
>
|
Prosty i przejrzysty proces rozliczenia w 3 krokach — bez zbędnych formalności.
|
||||||
{title}
|
</p>
|
||||||
</motion.h2>
|
|
||||||
|
|
||||||
<div className="max-w-4xl mx-auto">
|
|
||||||
<div className="flex flex-col md:flex-row gap-8 relative">
|
|
||||||
{/* Linia łącząca na desktopie */}
|
|
||||||
<div className="hidden md:block absolute top-6 left-[10%] right-[10%] h-0.5 bg-border z-0" />
|
|
||||||
|
|
||||||
{steps?.map((step: NonNullable<Props['steps']>[0], index: number) => (
|
|
||||||
<motion.div
|
|
||||||
key={step.id || index}
|
|
||||||
initial={{ opacity: 0, y: 20 }}
|
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: index * 0.15 }}
|
|
||||||
className="flex-1 relative z-10 text-center md:text-left flex flex-col items-center md:items-start"
|
|
||||||
>
|
|
||||||
<div className="w-12 h-12 rounded-full bg-accent text-white border-4 border-background flex items-center justify-center text-xl font-bold mb-6">
|
|
||||||
{index + 1}
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl font-bold mb-3">{step.title}</h3>
|
|
||||||
<p className="text-foreground/80 text-center md:text-left">{step.description}</p>
|
|
||||||
</motion.div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{ctaText && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
whileInView={{ opacity: 1 }}
|
|
||||||
viewport={{ once: true }}
|
|
||||||
transition={{ duration: 0.5, delay: 0.6 }}
|
|
||||||
className="mt-16 text-center"
|
|
||||||
>
|
|
||||||
<Link href="/jak-to-dziala" className="inline-block px-8 py-4 border-2 border-border hover:border-foreground rounded-lg text-foreground font-semibold transition-colors">
|
|
||||||
{ctaText}
|
|
||||||
</Link>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 md:gap-8 mb-12">
|
||||||
|
{steps?.map((step: NonNullable<Props['steps']>[0], index: number) => {
|
||||||
|
const stepNumber = String(index + 1).padStart(2, '0')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={step.id || index}
|
||||||
|
className="bg-card border border-border rounded-2xl p-6 sm:p-8 shadow-xs flex flex-col justify-between hover:border-accent/40 transition-colors"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="font-heading text-3xl font-extrabold text-accent/80 mb-4">
|
||||||
|
{stepNumber}
|
||||||
|
</div>
|
||||||
|
<h3 className="font-heading text-xl font-bold text-foreground mb-3 leading-snug">
|
||||||
|
{step.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-foreground/75 text-sm sm:text-base leading-relaxed">
|
||||||
|
{step.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ctaText && (
|
||||||
|
<div className="text-center">
|
||||||
|
<Link
|
||||||
|
href="/jak-sie-umowic"
|
||||||
|
className="inline-flex items-center gap-2 px-8 py-4 bg-card hover:bg-accent/5 border border-border hover:border-foreground/40 rounded-xl text-foreground font-semibold text-base transition-colors"
|
||||||
|
>
|
||||||
|
<span>{ctaText}</span>
|
||||||
|
<ArrowRight size={18} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ import { FinalCTABlock } from './FinalCTA/Component'
|
|||||||
import { PageHeaderBlock } from './PageHeader/Component'
|
import { PageHeaderBlock } from './PageHeader/Component'
|
||||||
import { ServicesDetailedBlock } from './ServicesDetailed/Component'
|
import { ServicesDetailedBlock } from './ServicesDetailed/Component'
|
||||||
import { EligibilityBlock } from './Eligibility/Component'
|
import { EligibilityBlock } from './Eligibility/Component'
|
||||||
|
import { StatsBlock } from './Stats/Component'
|
||||||
|
|
||||||
const blockComponents = {
|
const blockComponents = {
|
||||||
pageHeader: PageHeaderBlock,
|
pageHeader: PageHeaderBlock,
|
||||||
servicesDetailed: ServicesDetailedBlock,
|
servicesDetailed: ServicesDetailedBlock,
|
||||||
eligibility: EligibilityBlock,
|
eligibility: EligibilityBlock,
|
||||||
hero: HeroBlock,
|
hero: HeroBlock,
|
||||||
|
stats: StatsBlock,
|
||||||
trustBar: TrustBarBlock,
|
trustBar: TrustBarBlock,
|
||||||
uniqueValue: UniqueValueBlock,
|
uniqueValue: UniqueValueBlock,
|
||||||
servicesList: ServicesListBlock,
|
servicesList: ServicesListBlock,
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export interface StatsProps {
|
||||||
|
title?: string | null
|
||||||
|
subtitle?: string | null
|
||||||
|
items?: Array<{
|
||||||
|
id?: string | null
|
||||||
|
stat: string
|
||||||
|
label: string
|
||||||
|
description?: string | null
|
||||||
|
}> | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StatsBlock: React.FC<StatsProps> = ({ title, subtitle, items }) => {
|
||||||
|
if (!items || items.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-16 md:py-20 bg-card border-y border-border">
|
||||||
|
<div className="container-premium">
|
||||||
|
{(title || subtitle) && (
|
||||||
|
<div className="max-w-2xl mx-auto text-center mb-12">
|
||||||
|
{title && (
|
||||||
|
<h2 className="font-heading text-2xl sm:text-3xl font-bold text-foreground mb-3">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
|
{subtitle && (
|
||||||
|
<p className="text-foreground/70 text-sm sm:text-base leading-relaxed">
|
||||||
|
{subtitle}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 md:gap-12">
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={item.id || index}
|
||||||
|
className="flex flex-col items-center text-center p-6 rounded-2xl bg-background/50 border border-border/80 shadow-xs"
|
||||||
|
>
|
||||||
|
<div className="font-heading text-4xl sm:text-5xl lg:text-6xl font-extrabold text-accent mb-2 tracking-tight">
|
||||||
|
{item.stat}
|
||||||
|
</div>
|
||||||
|
<div className="font-heading text-lg sm:text-xl font-bold text-foreground mb-1.5">
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
|
{item.description && (
|
||||||
|
<p className="text-sm text-foreground/70 leading-relaxed max-w-xs">
|
||||||
|
{item.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import type { Block } from 'payload'
|
||||||
|
|
||||||
|
export const Stats: Block = {
|
||||||
|
slug: 'stats',
|
||||||
|
fields: [
|
||||||
|
{ name: 'title', type: 'text' },
|
||||||
|
{ name: 'subtitle', type: 'textarea' },
|
||||||
|
{
|
||||||
|
name: 'items',
|
||||||
|
type: 'array',
|
||||||
|
minRows: 1,
|
||||||
|
maxRows: 4,
|
||||||
|
required: true,
|
||||||
|
fields: [
|
||||||
|
{ name: 'stat', type: 'text', required: true },
|
||||||
|
{ name: 'label', type: 'text', required: true },
|
||||||
|
{ name: 'description', type: 'text' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import { FinalCTA } from '../blocks/FinalCTA/config'
|
|||||||
import { PageHeader } from '../blocks/PageHeader/config'
|
import { PageHeader } from '../blocks/PageHeader/config'
|
||||||
import { ServicesDetailed } from '../blocks/ServicesDetailed/config'
|
import { ServicesDetailed } from '../blocks/ServicesDetailed/config'
|
||||||
import { Eligibility } from '../blocks/Eligibility/config'
|
import { Eligibility } from '../blocks/Eligibility/config'
|
||||||
|
import { Stats } from '../blocks/Stats/config'
|
||||||
|
|
||||||
export const Pages: CollectionConfig = {
|
export const Pages: CollectionConfig = {
|
||||||
slug: 'pages',
|
slug: 'pages',
|
||||||
@@ -40,6 +41,7 @@ export const Pages: CollectionConfig = {
|
|||||||
ServicesDetailed,
|
ServicesDetailed,
|
||||||
Eligibility,
|
Eligibility,
|
||||||
Hero,
|
Hero,
|
||||||
|
Stats,
|
||||||
TrustBar,
|
TrustBar,
|
||||||
UniqueValue,
|
UniqueValue,
|
||||||
ServicesList,
|
ServicesList,
|
||||||
|
|||||||
@@ -266,6 +266,19 @@ export interface Page {
|
|||||||
blockName?: string | null;
|
blockName?: string | null;
|
||||||
blockType: 'hero';
|
blockType: 'hero';
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
title?: string | null;
|
||||||
|
subtitle?: string | null;
|
||||||
|
items: {
|
||||||
|
stat: string;
|
||||||
|
label: string;
|
||||||
|
description?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[];
|
||||||
|
id?: string | null;
|
||||||
|
blockName?: string | null;
|
||||||
|
blockType: 'stats';
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
items?:
|
items?:
|
||||||
| {
|
| {
|
||||||
@@ -602,6 +615,22 @@ export interface PagesSelect<T extends boolean = true> {
|
|||||||
id?: T;
|
id?: T;
|
||||||
blockName?: T;
|
blockName?: T;
|
||||||
};
|
};
|
||||||
|
stats?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
title?: T;
|
||||||
|
subtitle?: T;
|
||||||
|
items?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
stat?: T;
|
||||||
|
label?: T;
|
||||||
|
description?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
id?: T;
|
||||||
|
blockName?: T;
|
||||||
|
};
|
||||||
trustBar?:
|
trustBar?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
|
|||||||
Reference in New Issue
Block a user