238 lines
8.6 KiB
TypeScript
238 lines
8.6 KiB
TypeScript
'use client'
|
|
import React, { useState } from 'react'
|
|
import { motion } from 'framer-motion'
|
|
import type { Page, Media } from '../../payload-types'
|
|
import Link from 'next/link'
|
|
import Image from 'next/image'
|
|
|
|
type Props = Extract<NonNullable<Page['layout']>[0], { blockType: 'hero' }>
|
|
|
|
const customEase = [0.22, 1, 0.36, 1] as const
|
|
|
|
const containerVariants = {
|
|
hidden: {},
|
|
visible: {
|
|
transition: {
|
|
staggerChildren: 0.14,
|
|
},
|
|
},
|
|
}
|
|
|
|
const itemVariants = {
|
|
hidden: { opacity: 0, y: 22 },
|
|
visible: {
|
|
opacity: 1,
|
|
y: 0,
|
|
transition: {
|
|
duration: 0.5,
|
|
ease: customEase,
|
|
},
|
|
},
|
|
}
|
|
|
|
const collageVariants = {
|
|
hidden: { opacity: 0, scale: 0.96, y: 24 },
|
|
visible: {
|
|
opacity: 1,
|
|
scale: 1,
|
|
y: 0,
|
|
transition: {
|
|
duration: 0.6,
|
|
ease: customEase,
|
|
delay: 0.25,
|
|
},
|
|
},
|
|
}
|
|
|
|
function useParallax3D(maxAngle = 4.5) {
|
|
const [rotation, setRotation] = useState({ rotateX: 0, rotateY: 0 })
|
|
const [isHovered, setIsHovered] = useState(false)
|
|
|
|
const handlePointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
const rect = e.currentTarget.getBoundingClientRect()
|
|
const x = e.clientX - rect.left
|
|
const y = e.clientY - rect.top
|
|
const centerX = rect.width / 2
|
|
const centerY = rect.height / 2
|
|
const rotateX = -((y - centerY) / centerY) * maxAngle
|
|
const rotateY = ((x - centerX) / centerX) * maxAngle
|
|
setRotation({ rotateX, rotateY })
|
|
}
|
|
|
|
const handlePointerLeave = () => {
|
|
setIsHovered(false)
|
|
setRotation({ rotateX: 0, rotateY: 0 })
|
|
}
|
|
|
|
const handlePointerEnter = () => {
|
|
setIsHovered(true)
|
|
}
|
|
|
|
return {
|
|
bind: {
|
|
onPointerMove: handlePointerMove,
|
|
onPointerEnter: handlePointerEnter,
|
|
onPointerLeave: handlePointerLeave,
|
|
},
|
|
style: {
|
|
transform: `perspective(1000px) rotateX(${rotation.rotateX.toFixed(2)}deg) rotateY(${rotation.rotateY.toFixed(2)}deg)`,
|
|
transition: isHovered ? 'transform 0.12s ease-out' : 'transform 0.55s ease-out',
|
|
},
|
|
}
|
|
}
|
|
|
|
export const HeroBlock: React.FC<Props> = ({
|
|
title,
|
|
titleAccent,
|
|
subtitle,
|
|
primaryCta,
|
|
secondaryCta,
|
|
image,
|
|
imageSecondary,
|
|
floatingStat,
|
|
}) => {
|
|
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 secondaryMedia = typeof imageSecondary === 'object' && imageSecondary !== null ? (imageSecondary as Media) : null
|
|
const secondaryUrl = secondaryMedia?.url
|
|
const secondaryAlt = secondaryMedia?.alt || 'Profesjonalne rozliczenia podatkowe'
|
|
|
|
const { bind: parallaxBind, style: parallaxStyle } = useParallax3D(4.5)
|
|
|
|
const statValue = floatingStat?.value || '20+'
|
|
const statLabel = floatingStat?.label || 'Lat doświadczenia'
|
|
|
|
return (
|
|
<motion.section
|
|
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"
|
|
>
|
|
<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 */}
|
|
<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"
|
|
>
|
|
<span className="block text-foreground">{title}</span>
|
|
{titleAccent && <span className="block text-accent">{titleAccent}</span>}
|
|
</motion.h1>
|
|
|
|
<motion.p
|
|
variants={itemVariants}
|
|
className="text-xl sm:text-2xl text-foreground/80 mb-12 leading-relaxed font-light"
|
|
>
|
|
{subtitle}
|
|
</motion.p>
|
|
|
|
<motion.div
|
|
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>
|
|
{secondaryCta?.text && secondaryCta?.url && (
|
|
secondaryCta.url.startsWith('#') ? (
|
|
<a
|
|
href={secondaryCta.url}
|
|
className="text-foreground font-semibold text-lg hover:text-accent hover:underline underline-offset-4 transition-colors"
|
|
>
|
|
{secondaryCta.text}
|
|
</a>
|
|
) : (
|
|
<Link
|
|
href={secondaryCta.url}
|
|
className="text-foreground font-semibold text-lg hover:text-accent hover:underline underline-offset-4 transition-colors"
|
|
>
|
|
{secondaryCta.text}
|
|
</Link>
|
|
)
|
|
)}
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Prawa kolumna: Kolaż zdjęć z 3D paralaksą i pływającą kartą */}
|
|
<motion.div
|
|
variants={collageVariants}
|
|
className="w-full lg:w-1/2 flex justify-center lg:justify-end"
|
|
>
|
|
<div
|
|
{...parallaxBind}
|
|
style={parallaxStyle}
|
|
className="relative w-full max-w-md sm:max-w-lg select-none will-change-transform"
|
|
>
|
|
{/* Pływająca karta (floating card) */}
|
|
<motion.div
|
|
animate={{ y: [0, -8, 0] }}
|
|
transition={{ duration: 4, repeat: Infinity, ease: 'easeInOut' }}
|
|
className="absolute -top-6 -left-3 sm:-top-8 sm:-left-6 z-30 bg-foreground text-background px-5 py-4 shadow-xl border border-border/20 flex flex-col gap-0.5 pointer-events-none"
|
|
>
|
|
<span className="font-heading text-2xl sm:text-3xl font-extrabold tracking-tight text-accent">
|
|
{statValue}
|
|
</span>
|
|
<span className="text-xs uppercase tracking-widest text-background/80 font-medium">
|
|
{statLabel}
|
|
</span>
|
|
</motion.div>
|
|
|
|
{secondaryUrl ? (
|
|
/* 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>
|
|
|
|
{/* 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">
|
|
<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"
|
|
/>
|
|
</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>
|
|
)}
|
|
</div>
|
|
</motion.div>
|
|
|
|
</div>
|
|
</div>
|
|
</motion.section>
|
|
)
|
|
}
|