feat: make Hero full viewport, add TestimonialsGrid block mechanism, and remove footer availability pill

This commit is contained in:
MichalC
2026-09-03 09:44:43 +02:00
parent e19bb1b0c1
commit e32f907625
9 changed files with 133 additions and 47 deletions
-1
View File
@@ -65,7 +65,6 @@ export default async function RootLayout({
association={footerData.association} association={footerData.association}
copyright={footerData.copyright} copyright={footerData.copyright}
creator={footerData.creator} creator={footerData.creator}
availability={footerData.availabilityStatus}
/> />
{/* Floating WhatsApp Button */} {/* Floating WhatsApp Button */}
+2 -2
View File
@@ -109,9 +109,9 @@ export const HeroBlock: React.FC<Props> = ({
initial="hidden" initial="hidden"
animate="visible" animate="visible"
variants={containerVariants} variants={containerVariants}
className="w-full bg-background text-foreground pt-32 pb-16 md:pt-44 md:pb-24 overflow-hidden" className="w-full bg-background text-foreground min-h-[calc(100svh-6rem)] flex items-center py-12 lg:py-0 overflow-hidden"
> >
<div className="container-premium"> <div className="container-premium w-full">
<div className="flex flex-col-reverse lg:flex-row items-center justify-between gap-14 lg:gap-20"> <div className="flex flex-col-reverse lg:flex-row items-center justify-between gap-14 lg:gap-20">
{/* Kolumna tekstowa z animacjami wejścia */} {/* Kolumna tekstowa z animacjami wejścia */}
+2
View File
@@ -8,6 +8,7 @@ import { ServicesListBlock } from './ServicesList/Component'
import { ProcessBlock } from './Process/Component' import { ProcessBlock } from './Process/Component'
import { AffiliationBlock } from './Affiliation/Component' import { AffiliationBlock } from './Affiliation/Component'
import { TestimonialsBlock } from './Testimonials/Component' import { TestimonialsBlock } from './Testimonials/Component'
import { TestimonialsGridBlock } from './TestimonialsGrid/Component'
import { FAQTeaserBlock } from './FAQTeaser/Component' import { FAQTeaserBlock } from './FAQTeaser/Component'
import { FinalCTABlock } from './FinalCTA/Component' import { FinalCTABlock } from './FinalCTA/Component'
@@ -30,6 +31,7 @@ const blockComponents = {
process: ProcessBlock, process: ProcessBlock,
affiliation: AffiliationBlock, affiliation: AffiliationBlock,
testimonials: TestimonialsBlock, testimonials: TestimonialsBlock,
testimonialsGrid: TestimonialsGridBlock,
faqTeaser: FAQTeaserBlock, faqTeaser: FAQTeaserBlock,
finalCta: FinalCTABlock, finalCta: FinalCTABlock,
TextSection: ({ text }: { text: string }) => <section className="py-20"><div className="container mx-auto px-4"><p>{text}</p></div></section>, TextSection: ({ text }: { text: string }) => <section className="py-20"><div className="container mx-auto px-4"><p>{text}</p></div></section>,
+83
View File
@@ -0,0 +1,83 @@
'use client'
import React from 'react'
import { motion } from 'framer-motion'
import type { Page } from '../../payload-types'
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'testimonialsGrid' }>
interface ColumnProps {
items: NonNullable<Props['items']>
duration: number
className?: string
}
const ScrollingColumn: React.FC<ColumnProps> = ({ items, duration, className = '' }) => {
// Dublujemy x2 dla bezszwowej pętli
const doubledItems = [...items, ...items]
return (
<div className={`overflow-hidden relative ${className}`}>
<motion.div
animate={{ y: ['0%', '-50%'] }}
transition={{
duration,
repeat: Infinity,
ease: 'linear',
}}
className="flex flex-col gap-6"
>
{doubledItems.map((item, index) => (
<div
key={item.id ? `${item.id}-${index}` : index}
className="bg-card border border-border rounded-none p-6 md:p-8 flex flex-col justify-between shadow-xs select-none"
>
<p className="text-foreground/90 text-base sm:text-lg leading-relaxed italic mb-6">
&ldquo;{item.quote}&rdquo;
</p>
<div>
<div className="w-10 h-px bg-accent/60 mb-3" />
<p className="font-heading font-bold text-foreground text-base tracking-tight">
{item.authorName}
</p>
{item.authorRole && (
<p className="text-foreground/60 text-xs sm:text-sm font-medium mt-0.5">
{item.authorRole}
</p>
)}
</div>
</div>
))}
</motion.div>
</div>
)
}
export const TestimonialsGridBlock: React.FC<Props> = ({ items }) => {
// WARUNEK KRYTYCZNY: komponent renderuje się TYLKO jeśli items.length >= 6
if (!items || items.length < 6) return null
// Podział na 3 kolumny
const col1 = items.filter((_, i) => i % 3 === 0)
const col2 = items.filter((_, i) => i % 3 === 1)
const col3 = items.filter((_, i) => i % 3 === 2)
return (
<section className="py-20 md:py-32 bg-background border-y border-border overflow-hidden">
<div className="container-premium">
<div
className="relative h-[640px] overflow-hidden"
style={{
maskImage: 'linear-gradient(to bottom, transparent 0%, black 12%, black 88%, transparent 100%)',
WebkitMaskImage: 'linear-gradient(to bottom, transparent 0%, black 12%, black 88%, transparent 100%)',
}}
>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 h-full items-start">
<ScrollingColumn items={col1} duration={15} />
<ScrollingColumn items={col2} duration={19} className="hidden md:block" />
<ScrollingColumn items={col3} duration={17} className="hidden lg:block" />
</div>
</div>
</div>
</section>
)
}
+18
View File
@@ -0,0 +1,18 @@
import type { Block } from 'payload'
export const TestimonialsGrid: Block = {
slug: 'testimonialsGrid',
fields: [
{
name: 'items',
type: 'array',
required: true,
minRows: 1,
fields: [
{ name: 'quote', type: 'textarea', required: true },
{ name: 'authorName', type: 'text', required: true },
{ name: 'authorRole', type: 'text' },
],
},
],
}
+2
View File
@@ -15,6 +15,7 @@ import { ContactInfo } from '../blocks/ContactInfo/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' import { Stats } from '../blocks/Stats/config'
import { TestimonialsGrid } from '../blocks/TestimonialsGrid/config'
export const Pages: CollectionConfig = { export const Pages: CollectionConfig = {
slug: 'pages', slug: 'pages',
@@ -50,6 +51,7 @@ export const Pages: CollectionConfig = {
Process, Process,
Affiliation, Affiliation,
Testimonials, Testimonials,
TestimonialsGrid,
FAQTeaser, FAQTeaser,
FinalCTA, FinalCTA,
{ {
-15
View File
@@ -12,14 +12,12 @@ export function Footer({
association = {}, association = {},
copyright, copyright,
creator, creator,
availability,
}: { }: {
navItems?: any[]; navItems?: any[];
companyInfo?: any; companyInfo?: any;
association?: any; association?: any;
copyright?: any; copyright?: any;
creator?: any; creator?: any;
availability?: any;
}) { }) {
const pathname = usePathname(); const pathname = usePathname();
@@ -50,11 +48,6 @@ export function Footer({
const creatorName = creator?.name; const creatorName = creator?.name;
const creatorUrl = creator?.url; const creatorUrl = creator?.url;
const currentAvailability = availability?.[currentLocaleCode] || availability?.pl || availability || {};
const defaultAvailText = currentLocaleCode === 'de' ? 'Ich nehme derzeit neue Mandanten an' : 'Przyjmuję obecnie nowych klientów';
const availText = currentAvailability?.text || defaultAvailText;
const isAvailable = currentAvailability?.available !== false;
return ( return (
<footer className="bg-card py-16 md:py-24 mt-auto"> <footer className="bg-card py-16 md:py-24 mt-auto">
<div className="container-premium grid grid-cols-1 md:grid-cols-3 gap-16 md:gap-12"> <div className="container-premium grid grid-cols-1 md:grid-cols-3 gap-16 md:gap-12">
@@ -157,15 +150,7 @@ export function Footer({
</div> </div>
<div className="container-premium mt-16 pt-8 border-t border-border flex flex-col md:flex-row justify-between items-center gap-6 text-sm text-foreground/50"> <div className="container-premium mt-16 pt-8 border-t border-border flex flex-col md:flex-row justify-between items-center gap-6 text-sm text-foreground/50">
<div className="flex flex-col sm:flex-row items-center gap-4 sm:gap-6">
{availText && (
<div className="inline-flex items-center gap-2 rounded-full border border-border bg-card px-3 py-1.5 text-xs font-medium text-foreground/80 shadow-xs">
<span className={`w-2 h-2 rounded-full shrink-0 ${isAvailable ? 'bg-green-500' : 'bg-foreground/40'}`} />
<span>{availText}</span>
</div>
)}
<p>{copyrightText}</p> <p>{copyrightText}</p>
</div>
{(creatorName || creatorLabel) && ( {(creatorName || creatorLabel) && (
<p className="flex items-center gap-2 font-medium"> <p className="flex items-center gap-2 font-medium">
{creatorLabel && <span>{creatorLabel}</span>} {creatorLabel && <span>{creatorLabel}</span>}
-18
View File
@@ -18,24 +18,6 @@ export const Footer: GlobalConfig = {
}, },
], ],
}, },
{
name: 'availabilityStatus',
type: 'group',
localized: true,
fields: [
{
name: 'text',
type: 'text',
required: true,
defaultValue: 'Przyjmuję obecnie nowych klientów',
},
{
name: 'available',
type: 'checkbox',
defaultValue: true,
},
],
},
{ {
name: 'association', name: 'association',
type: 'group', type: 'group',
+25 -10
View File
@@ -384,6 +384,17 @@ export interface Page {
blockName?: string | null; blockName?: string | null;
blockType: 'testimonials'; blockType: 'testimonials';
} }
| {
items: {
quote: string;
authorName: string;
authorRole?: string | null;
id?: string | null;
}[];
id?: string | null;
blockName?: string | null;
blockType: 'testimonialsGrid';
}
| { | {
eyebrow?: string | null; eyebrow?: string | null;
title: string; title: string;
@@ -783,6 +794,20 @@ export interface PagesSelect<T extends boolean = true> {
id?: T; id?: T;
blockName?: T; blockName?: T;
}; };
testimonialsGrid?:
| T
| {
items?:
| T
| {
quote?: T;
authorName?: T;
authorRole?: T;
id?: T;
};
id?: T;
blockName?: T;
};
faqTeaser?: faqTeaser?:
| T | T
| { | {
@@ -915,10 +940,6 @@ export interface Footer {
id?: string | null; id?: string | null;
}[] }[]
| null; | null;
availabilityStatus: {
text: string;
available?: boolean | null;
};
association?: { association?: {
title?: string | null; title?: string | null;
logo?: (number | null) | Media; logo?: (number | null) | Media;
@@ -1142,12 +1163,6 @@ export interface FooterSelect<T extends boolean = true> {
page?: T; page?: T;
id?: T; id?: T;
}; };
availabilityStatus?:
| T
| {
text?: T;
available?: T;
};
association?: association?:
| T | T
| { | {