diff --git a/src/blocks/Hero/Component.tsx b/src/blocks/Hero/Component.tsx index 2409853..7ff3320 100644 --- a/src/blocks/Hero/Component.tsx +++ b/src/blocks/Hero/Component.tsx @@ -1,39 +1,145 @@ 'use client' -import React from 'react' +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[0], { blockType: 'hero' }> -export const HeroBlock: React.FC = ({ title, titleAccent, subtitle, primaryCta, secondaryCta, image }) => { - // Sprawdzamy czy obrazek jest obiektem Media czy tylko ID - const heroMedia = typeof image === 'object' && image !== null ? (image as Media) : null; - // PLACEHOLDER - do podmiany na zdjęcie klienta jeśli brak w CMS - 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 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) => { + 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 = ({ + 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 ( -
+
-
+
-
-

+ {/* Kolumna tekstowa z animacjami wejścia */} +
+ {title} {titleAccent && {titleAccent}} -

+ -

+ {subtitle} -

+ -
+ {primaryCta} @@ -45,24 +151,78 @@ export const HeroBlock: React.FC = ({ title, titleAccent, subtitle, prima {secondaryCta.text} )} -
+
-
-
- {imageAlt} + {/* Prawa kolumna: Kolaż zdjęć z 3D paralaksą i pływającą kartą */} + +
+ {/* Pływająca karta (floating card) */} + + + {statValue} + + + {statLabel} + + + + {secondaryUrl ? ( + /* Układ kolażu dwóch nachodzących na siebie zdjęć */ +
+ {/* Zdjęcie 1 (mniejsze, przesunięte w lewo-górę) */} +
+ {imageAlt} +
+ + {/* Zdjęcie 2 (większe, przesunięte w prawo-dół, nachodzące na pierwsze o ok. 18%) */} +
+ {secondaryAlt} +
+
+ ) : ( + /* Pojedyncze zdjęcie jeśli imageSecondary jest puste */ +
+ {imageAlt} +
+ )}
-
+
-
+ ) } diff --git a/src/blocks/Hero/config.ts b/src/blocks/Hero/config.ts index 020b05a..ce3c482 100644 --- a/src/blocks/Hero/config.ts +++ b/src/blocks/Hero/config.ts @@ -15,6 +15,15 @@ export const Hero: Block = { { name: 'url', type: 'text' }, ], }, + { + name: 'floatingStat', + type: 'group', + fields: [ + { name: 'value', type: 'text', localized: true, defaultValue: '20+' }, + { name: 'label', type: 'text', localized: true, defaultValue: 'Lat doświadczenia' }, + ], + }, { name: 'image', type: 'upload', relationTo: 'media', required: true }, + { name: 'imageSecondary', type: 'upload', relationTo: 'media' }, ], } diff --git a/src/payload-types.ts b/src/payload-types.ts index dea4b3e..15f5b0a 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -271,7 +271,12 @@ export interface Page { text?: string | null; url?: string | null; }; + floatingStat?: { + value?: string | null; + label?: string | null; + }; image: number | Media; + imageSecondary?: (number | null) | Media; id?: string | null; blockName?: string | null; blockType: 'hero'; @@ -655,7 +660,14 @@ export interface PagesSelect { text?: T; url?: T; }; + floatingStat?: + | T + | { + value?: T; + label?: T; + }; image?: T; + imageSecondary?: T; id?: T; blockName?: T; };