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}
|
||||
copyright={footerData.copyright}
|
||||
creator={footerData.creator}
|
||||
availability={footerData.availabilityStatus}
|
||||
/>
|
||||
|
||||
{/* Floating WhatsApp Button */}
|
||||
|
||||
@@ -109,9 +109,9 @@ export const HeroBlock: React.FC<Props> = ({
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
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">
|
||||
|
||||
{/* Kolumna tekstowa z animacjami wejścia */}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ServicesListBlock } from './ServicesList/Component'
|
||||
import { ProcessBlock } from './Process/Component'
|
||||
import { AffiliationBlock } from './Affiliation/Component'
|
||||
import { TestimonialsBlock } from './Testimonials/Component'
|
||||
import { TestimonialsGridBlock } from './TestimonialsGrid/Component'
|
||||
import { FAQTeaserBlock } from './FAQTeaser/Component'
|
||||
import { FinalCTABlock } from './FinalCTA/Component'
|
||||
|
||||
@@ -30,6 +31,7 @@ const blockComponents = {
|
||||
process: ProcessBlock,
|
||||
affiliation: AffiliationBlock,
|
||||
testimonials: TestimonialsBlock,
|
||||
testimonialsGrid: TestimonialsGridBlock,
|
||||
faqTeaser: FAQTeaserBlock,
|
||||
finalCta: FinalCTABlock,
|
||||
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 { Eligibility } from '../blocks/Eligibility/config'
|
||||
import { Stats } from '../blocks/Stats/config'
|
||||
import { TestimonialsGrid } from '../blocks/TestimonialsGrid/config'
|
||||
|
||||
export const Pages: CollectionConfig = {
|
||||
slug: 'pages',
|
||||
@@ -50,6 +51,7 @@ export const Pages: CollectionConfig = {
|
||||
Process,
|
||||
Affiliation,
|
||||
Testimonials,
|
||||
TestimonialsGrid,
|
||||
FAQTeaser,
|
||||
FinalCTA,
|
||||
{
|
||||
|
||||
@@ -12,14 +12,12 @@ export function Footer({
|
||||
association = {},
|
||||
copyright,
|
||||
creator,
|
||||
availability,
|
||||
}: {
|
||||
navItems?: any[];
|
||||
companyInfo?: any;
|
||||
association?: any;
|
||||
copyright?: any;
|
||||
creator?: any;
|
||||
availability?: any;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
|
||||
@@ -50,11 +48,6 @@ export function Footer({
|
||||
const creatorName = creator?.name;
|
||||
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 (
|
||||
<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">
|
||||
@@ -157,15 +150,7 @@ export function Footer({
|
||||
</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="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>
|
||||
</div>
|
||||
<p>{copyrightText}</p>
|
||||
{(creatorName || creatorLabel) && (
|
||||
<p className="flex items-center gap-2 font-medium">
|
||||
{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',
|
||||
type: 'group',
|
||||
|
||||
+25
-10
@@ -384,6 +384,17 @@ export interface Page {
|
||||
blockName?: string | null;
|
||||
blockType: 'testimonials';
|
||||
}
|
||||
| {
|
||||
items: {
|
||||
quote: string;
|
||||
authorName: string;
|
||||
authorRole?: string | null;
|
||||
id?: string | null;
|
||||
}[];
|
||||
id?: string | null;
|
||||
blockName?: string | null;
|
||||
blockType: 'testimonialsGrid';
|
||||
}
|
||||
| {
|
||||
eyebrow?: string | null;
|
||||
title: string;
|
||||
@@ -783,6 +794,20 @@ export interface PagesSelect<T extends boolean = true> {
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
};
|
||||
testimonialsGrid?:
|
||||
| T
|
||||
| {
|
||||
items?:
|
||||
| T
|
||||
| {
|
||||
quote?: T;
|
||||
authorName?: T;
|
||||
authorRole?: T;
|
||||
id?: T;
|
||||
};
|
||||
id?: T;
|
||||
blockName?: T;
|
||||
};
|
||||
faqTeaser?:
|
||||
| T
|
||||
| {
|
||||
@@ -915,10 +940,6 @@ export interface Footer {
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
availabilityStatus: {
|
||||
text: string;
|
||||
available?: boolean | null;
|
||||
};
|
||||
association?: {
|
||||
title?: string | null;
|
||||
logo?: (number | null) | Media;
|
||||
@@ -1142,12 +1163,6 @@ export interface FooterSelect<T extends boolean = true> {
|
||||
page?: T;
|
||||
id?: T;
|
||||
};
|
||||
availabilityStatus?:
|
||||
| T
|
||||
| {
|
||||
text?: T;
|
||||
available?: T;
|
||||
};
|
||||
association?:
|
||||
| T
|
||||
| {
|
||||
|
||||
Reference in New Issue
Block a user