feat: make Hero full viewport, add TestimonialsGrid block mechanism, and remove footer availability pill
This commit is contained in:
@@ -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 */}
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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">
|
||||||
|
“{item.quote}”
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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">
|
<p>{copyrightText}</p>
|
||||||
{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>
|
|
||||||
</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,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
@@ -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
|
||||||
| {
|
| {
|
||||||
|
|||||||
Reference in New Issue
Block a user