feat: complete UI rebuild, responsive fixes, dark mode UI, and Team block overhaul
This commit is contained in:
+80
-155
@@ -4,7 +4,7 @@ import { motion } from 'framer-motion'
|
||||
import type { Page, Media } from '../../payload-types'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { ImageIcon } from 'lucide-react'
|
||||
import { ImagePlaceholder } from '../../components/ImagePlaceholder'
|
||||
|
||||
type BaseProps = Extract<NonNullable<Page['layout']>[0], { blockType: 'hero' }>
|
||||
type Props = BaseProps & { whatsappNumber?: string | null }
|
||||
@@ -16,6 +16,7 @@ const containerVariants = {
|
||||
visible: {
|
||||
transition: {
|
||||
staggerChildren: 0.14,
|
||||
delayChildren: 0.2
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -26,63 +27,12 @@ const itemVariants = {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
transition: {
|
||||
duration: 0.5,
|
||||
duration: 0.7,
|
||||
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,
|
||||
@@ -90,22 +40,15 @@ export const HeroBlock: React.FC<Props> = ({
|
||||
primaryCta,
|
||||
secondaryCta,
|
||||
image,
|
||||
imageSecondary,
|
||||
floatingStat,
|
||||
whatsappNumber,
|
||||
}) => {
|
||||
const [imagesLoaded, setImagesLoaded] = useState(false)
|
||||
const [imageLoaded, setImageLoaded] = useState(false)
|
||||
const whatsappLink = whatsappNumber ? `https://wa.me/${whatsappNumber}` : '#'
|
||||
|
||||
const heroMedia = typeof image === 'object' && image !== null ? (image as Media) : null
|
||||
const imageUrl = heroMedia?.url
|
||||
const imageAlt = heroMedia?.alt || ''
|
||||
|
||||
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 imageAlt = heroMedia?.alt || 'Tło hero'
|
||||
|
||||
const statValue = floatingStat?.value || '20+'
|
||||
const statLabel = floatingStat?.label || 'Lat doświadczenia'
|
||||
@@ -114,21 +57,53 @@ export const HeroBlock: React.FC<Props> = ({
|
||||
<motion.section
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={containerVariants}
|
||||
className="w-full bg-paper-noise text-foreground min-h-[calc(100svh-6rem)] flex items-center py-12 lg:py-0 overflow-hidden"
|
||||
className="relative w-full min-h-[70vh] lg:min-h-[85vh] flex flex-col justify-end 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">
|
||||
{/* Background Layer */}
|
||||
<div className="absolute inset-0 z-0">
|
||||
{imageUrl ? (
|
||||
<>
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={imageAlt}
|
||||
fill
|
||||
priority
|
||||
sizes="100vw"
|
||||
onLoad={() => setImageLoaded(true)}
|
||||
className={`object-cover object-center transition-all duration-1000 ${
|
||||
imageLoaded ? 'opacity-100 scale-100' : 'opacity-0 scale-105'
|
||||
}`}
|
||||
/>
|
||||
{/* Gradient Overlay for Text Readability - Mocniejszy na dole/lewo */}
|
||||
{/* Z uwagi na "od ciemnego do przezroczystego" stosujemy from-background (zależne od motywu),
|
||||
ale upewniamy się, że w jasnym motywie ciemny tekst nadal ma sens - gradient na jasnym tle to od białego,
|
||||
a na ciemnym od czarnego. Jeśli chcesz wymusić zawsze ciemny overlay to musielibyśmy
|
||||
nadpisać text-foreground na stałe na biało. Zakładamy, że bg-background daje optymalny kontrast
|
||||
(jasny gradient z ciemnym tekstem / ciemny gradient z jasnym tekstem). */}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-background via-background/70 to-transparent lg:bg-gradient-to-r lg:from-background/90 lg:via-background/50 lg:to-transparent" />
|
||||
<div className="absolute inset-0 bg-background/20" /> {/* bazowe lekkie przyciemnienie reszty zdjęcia */}
|
||||
</>
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-card bg-paper-noise border-b border-border/50" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content Layer */}
|
||||
<div className="container-premium relative z-10 w-full pb-16 lg:pb-24 pt-32">
|
||||
<div className="flex flex-col lg:flex-row items-end justify-between gap-12">
|
||||
|
||||
{/* Kolumna tekstowa z animacjami wejścia */}
|
||||
<div className="w-full lg:w-1/2 max-w-xl flex flex-col">
|
||||
{/* Main Content (Bottom Left) */}
|
||||
<motion.div
|
||||
variants={containerVariants}
|
||||
className="w-full lg:max-w-3xl flex flex-col"
|
||||
>
|
||||
<motion.h1
|
||||
variants={itemVariants}
|
||||
className="font-heading text-4xl sm:text-5xl md:text-6xl font-bold leading-tight mb-8 text-balance"
|
||||
className={`font-heading text-4xl sm:text-5xl md:text-6xl lg:text-[4.5rem] font-bold leading-[1.05] mb-6 text-balance text-foreground break-words hyphens-auto ${imageUrl ? 'drop-shadow-sm' : ''}`}
|
||||
>
|
||||
<span className="block text-foreground">{title}</span>
|
||||
<span className="block">{title}</span>
|
||||
{titleAccent && (
|
||||
<span className="block text-accent">
|
||||
<span className="block text-accent mt-2">
|
||||
{titleAccent.split(/(po polsku)/i).map((part, i) =>
|
||||
part.toLowerCase() === 'po polsku' ? <i key={i} className="italic font-medium">{part}</i> : part
|
||||
)}
|
||||
@@ -138,21 +113,21 @@ export const HeroBlock: React.FC<Props> = ({
|
||||
|
||||
<motion.p
|
||||
variants={itemVariants}
|
||||
className="text-xl sm:text-2xl text-foreground/80 mb-12 leading-relaxed font-light max-w-[65ch]"
|
||||
className={`text-lg sm:text-xl md:text-2xl text-foreground/90 mb-10 leading-relaxed font-medium max-w-[55ch] ${imageUrl ? 'drop-shadow-sm' : ''}`}
|
||||
>
|
||||
{subtitle}
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
variants={itemVariants}
|
||||
className="flex flex-col sm:flex-row items-start sm:items-center gap-8"
|
||||
className="flex flex-col sm:flex-row items-start sm:items-center gap-6 sm:gap-8"
|
||||
>
|
||||
{whatsappNumber && (
|
||||
<a
|
||||
href={whatsappLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-8 py-4 bg-[#25D366] hover:bg-[#20bd5a] text-[#1C1B1A] font-bold text-base sm:text-lg transition-colors flex items-center justify-center rounded-none shadow-sm cursor-pointer"
|
||||
className="px-8 py-4 bg-[#25D366] hover:bg-[#20bd5a] text-[#1C1B1A] font-bold text-base sm:text-lg transition-colors flex items-center justify-center rounded-none shadow-md cursor-pointer border border-[#1C1B1A]/10"
|
||||
>
|
||||
{primaryCta}
|
||||
</a>
|
||||
@@ -161,102 +136,52 @@ export const HeroBlock: React.FC<Props> = ({
|
||||
secondaryCta.url.startsWith('#') ? (
|
||||
<a
|
||||
href={secondaryCta.url}
|
||||
className="text-foreground font-semibold text-lg hover:text-accent hover:underline underline-offset-4 transition-colors"
|
||||
className="text-foreground font-semibold text-lg hover:text-accent hover:underline underline-offset-4 transition-colors drop-shadow-sm"
|
||||
>
|
||||
{secondaryCta.text}
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
href={secondaryCta.url}
|
||||
className="text-foreground font-semibold text-lg hover:text-accent hover:underline underline-offset-4 transition-colors"
|
||||
className="text-foreground font-semibold text-lg hover:text-accent hover:underline underline-offset-4 transition-colors drop-shadow-sm"
|
||||
>
|
||||
{secondaryCta.text}
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Prawa kolumna: Kolaż zdjęć z 3D paralaksą i pływającą kartą */}
|
||||
{/* Floating Stat "Seal" (Bottom Right) */}
|
||||
<motion.div
|
||||
variants={collageVariants}
|
||||
className="w-full lg:w-1/2 flex justify-center lg:justify-end"
|
||||
initial={{ opacity: 0, scale: 0.9, rotate: -5 }}
|
||||
animate={{ opacity: 1, scale: 1, rotate: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.6, ease: [0.34, 1.56, 0.64, 1] }}
|
||||
className="hidden lg:flex flex-col items-center justify-center bg-card border border-border/50 p-6 lg:p-8 shadow-2xl lg:-translate-y-4 max-w-[200px] aspect-square rounded-sm relative shrink-0"
|
||||
>
|
||||
<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 bg-card">
|
||||
{imageUrl ? (
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={imageAlt}
|
||||
fill
|
||||
priority
|
||||
sizes="(max-width: 1024px) 55vw, 25vw"
|
||||
onLoad={() => setImagesLoaded(true)}
|
||||
className={`object-cover object-center grayscale hover:grayscale-0 transition-all duration-700 ease-out ${imagesLoaded ? 'opacity-100 scale-100' : 'opacity-0 scale-105'}`}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex items-center justify-center text-border border border-border">
|
||||
<ImageIcon size={32} strokeWidth={1} />
|
||||
</div>
|
||||
)}
|
||||
</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 bg-border/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 ease-out ${imagesLoaded ? 'opacity-100 scale-100' : 'opacity-0 scale-105'}`}
|
||||
/>
|
||||
</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 bg-card">
|
||||
{imageUrl ? (
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={imageAlt}
|
||||
fill
|
||||
priority
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
onLoad={() => setImagesLoaded(true)}
|
||||
className={`object-cover object-center grayscale hover:grayscale-0 transition-all duration-700 ease-out ${imagesLoaded ? 'opacity-100 scale-100' : 'opacity-0 scale-105'}`}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center text-border gap-4 p-8 text-center border border-border">
|
||||
<ImageIcon size={64} strokeWidth={1} />
|
||||
<span className="text-xs tracking-widest uppercase font-semibold text-foreground/40">ZDJĘCIE GŁÓWNE</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Dekoracyjna krawędź przypominająca stempel */}
|
||||
<div className="absolute inset-2 border border-border/20 pointer-events-none" />
|
||||
<span className="font-heading text-4xl lg:text-5xl font-extrabold tracking-tight text-accent mb-2 drop-shadow-sm relative z-10 text-center">
|
||||
{statValue}
|
||||
</span>
|
||||
<span className="text-sm uppercase tracking-widest text-foreground/80 font-medium text-center relative z-10 leading-snug">
|
||||
{statLabel}
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
{/* Mobile version of the seal - more inline */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.6, ease: customEase }}
|
||||
className="flex lg:hidden items-center gap-4 bg-card/90 backdrop-blur-sm border border-border/50 shadow-md p-4 w-full mt-4"
|
||||
>
|
||||
<span className="font-heading text-3xl font-extrabold tracking-tight text-accent drop-shadow-sm">
|
||||
{statValue}
|
||||
</span>
|
||||
<span className="text-xs uppercase tracking-widest text-foreground/80 font-medium leading-snug">
|
||||
{statLabel}
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user