feat(dyvolis): pixel-perfect update from Figma — video reviews, quote block, ticket redesign
- Hero: bg #fdf2e8 → #f1fbeb (matches Figma page root) - WhyVisit: replace auto-scroll photo gallery with single-card video-review carousel (poster + play btn, prev/next + dots, 5s auto-advance); add closing quote block per Figma node 4:202 - Tickets: first ticket relabelled "Вхід до ДиноПарку" per Figma; card bg #f1fbeb → #fdf2e8 (cream); combo subtitle order matches Figma node 4:160 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1dab458ef9
commit
098339e23e
4 changed files with 116 additions and 77 deletions
BIN
public/images/review-video-poster.jpg
Normal file
BIN
public/images/review-video-poster.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
|
|
@ -11,7 +11,7 @@ const TIPS = [
|
|||
|
||||
export function DyvoLisHero() {
|
||||
return (
|
||||
<section className="relative overflow-hidden" style={{ background: '#fdf2e8' }}>
|
||||
<section className="relative overflow-hidden" style={{ background: '#f1fbeb' }}>
|
||||
{/* Left column — contained within max-width */}
|
||||
<div className="mx-auto max-w-[1204px] px-8">
|
||||
<div className="min-h-[600px] pt-12 pb-16 lg:min-h-[960px] lg:pt-[98px] lg:pb-[60px]">
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ interface TicketCard {
|
|||
}
|
||||
|
||||
const SINGLE_TICKETS: TicketCard[] = [
|
||||
{ label: 'Вхід до ДивоЛісу', price: '300 грн', per: 'за 1 людину' },
|
||||
{ label: 'Вхід до ДиноПарку', price: '300 грн', per: 'за 1 людину' },
|
||||
{
|
||||
label: 'Звичайна екскурсія',
|
||||
price: '150 грн',
|
||||
|
|
@ -47,7 +47,7 @@ function Ticket({ label, price, per, note }: TicketCard) {
|
|||
return (
|
||||
<div
|
||||
className="flex flex-col rounded-[20px] p-5 shadow-[0_4px_30px_rgba(242,139,74,0.25)]"
|
||||
style={{ background: '#f1fbeb' }}
|
||||
style={{ background: '#fdf2e8' }}
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-center">
|
||||
{label && (
|
||||
|
|
@ -139,7 +139,7 @@ export function DyvoLisTickets() {
|
|||
className="text-[16px] leading-[1.4] font-semibold text-white/80 lg:text-[20px]"
|
||||
style={FONT_MONT}
|
||||
>
|
||||
ДивоЛіс із казковими топіарними фігурами + ДиноПарк + Дзеркальний лабіринт
|
||||
Динопарк + Диволіс із казковими топіарними фігурами + Дзеркальний лабіринт
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
|
||||
const FONT_MONT = { fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }
|
||||
const IMG_PLAY = '/images/figma/btn-video-play.svg'
|
||||
|
||||
const ITEMS = [
|
||||
{
|
||||
title: 'Простір для спільної фантазії',
|
||||
|
|
@ -21,73 +24,59 @@ const ITEMS = [
|
|||
},
|
||||
]
|
||||
|
||||
const GALLERY = [
|
||||
'/images/dyvolis/gallery-1.jpg',
|
||||
'/images/dyvolis/gallery-2.jpg',
|
||||
'/images/dyvolis/gallery-3.jpg',
|
||||
'/images/dyvolis/gallery-4.jpg',
|
||||
const VIDEO_REVIEWS = [
|
||||
{ poster: '/images/review-video-poster.jpg' },
|
||||
{ poster: '/images/review-video-poster.jpg' },
|
||||
{ poster: '/images/review-video-poster.jpg' },
|
||||
]
|
||||
|
||||
export function DyvoLisWhyVisit() {
|
||||
const [openIndex, setOpenIndex] = useState(0)
|
||||
const galleryRef = useRef<HTMLDivElement>(null)
|
||||
const galleryPausedRef = useRef(false)
|
||||
const galleryRafRef = useRef<number | null>(null)
|
||||
const autoTimer = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
|
||||
const doubled = [...GALLERY, ...GALLERY]
|
||||
const [videoActive, setVideoActive] = useState(0)
|
||||
const videoPausedRef = useRef(false)
|
||||
const accordionTimer = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
const videoTimer = useRef<ReturnType<typeof setInterval> | null>(null)
|
||||
const vn = VIDEO_REVIEWS.length
|
||||
|
||||
useEffect(() => {
|
||||
autoTimer.current = setInterval(() => {
|
||||
accordionTimer.current = setInterval(() => {
|
||||
setOpenIndex((prev) => (prev + 1) % ITEMS.length)
|
||||
}, 4000)
|
||||
return () => {
|
||||
if (autoTimer.current) clearInterval(autoTimer.current)
|
||||
if (accordionTimer.current) clearInterval(accordionTimer.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
videoTimer.current = setInterval(() => {
|
||||
if (!videoPausedRef.current) {
|
||||
setVideoActive((prev) => (prev + 1) % vn)
|
||||
}
|
||||
}, 5000)
|
||||
return () => {
|
||||
if (videoTimer.current) clearInterval(videoTimer.current)
|
||||
}
|
||||
}, [vn])
|
||||
|
||||
function handleItemClick(i: number) {
|
||||
setOpenIndex(i)
|
||||
if (autoTimer.current) clearInterval(autoTimer.current)
|
||||
autoTimer.current = setInterval(() => {
|
||||
if (accordionTimer.current) clearInterval(accordionTimer.current)
|
||||
accordionTimer.current = setInterval(() => {
|
||||
setOpenIndex((prev) => (prev + 1) % ITEMS.length)
|
||||
}, 4000)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const el = galleryRef.current
|
||||
if (!el) return
|
||||
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
||||
let last = performance.now()
|
||||
const tick = (now: number) => {
|
||||
const dt = now - last
|
||||
last = now
|
||||
if (!galleryPausedRef.current) {
|
||||
const half = el.scrollWidth / 2
|
||||
if (half > 0) {
|
||||
el.scrollLeft += dt * 0.04
|
||||
if (el.scrollLeft >= half) el.scrollLeft -= half
|
||||
}
|
||||
}
|
||||
galleryRafRef.current = requestAnimationFrame(tick)
|
||||
}
|
||||
galleryRafRef.current = requestAnimationFrame(tick)
|
||||
return () => {
|
||||
if (galleryRafRef.current) cancelAnimationFrame(galleryRafRef.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<section className="py-[20px] md:py-[60px] lg:py-[40px]" style={{ background: '#f1fbeb' }}>
|
||||
<div className="mx-auto max-w-[1204px] px-8">
|
||||
<h2
|
||||
className="mb-[40px] text-[24px] font-bold text-[#272727] uppercase md:mb-[60px] md:text-[32px]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
style={FONT_MONT}
|
||||
>
|
||||
Чому варто відвідати ДивоЛіс
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-col items-start gap-16 lg:flex-row lg:items-stretch">
|
||||
<div className="flex flex-col items-start gap-16 lg:flex-row lg:items-start">
|
||||
{/* Left: accordion */}
|
||||
<div className="relative w-full flex-none lg:w-auto">
|
||||
<div className="absolute top-8 left-0 hidden h-[488px] w-[333px] rounded-[30px] bg-[#396817] lg:block" />
|
||||
|
|
@ -105,7 +94,7 @@ export function DyvoLisWhyVisit() {
|
|||
<div className="flex items-center gap-5">
|
||||
<span
|
||||
className="flex-1 text-[20px] leading-tight font-bold text-[#272727]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
style={FONT_MONT}
|
||||
>
|
||||
{item.title}
|
||||
</span>
|
||||
|
|
@ -144,58 +133,108 @@ export function DyvoLisWhyVisit() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: auto-scroll gallery — desktop */}
|
||||
<div className="hidden flex-1 overflow-hidden lg:block">
|
||||
{/* Right: video review carousel */}
|
||||
<div className="w-full flex-1">
|
||||
<div
|
||||
ref={galleryRef}
|
||||
className="flex gap-5 overflow-x-auto"
|
||||
style={{ scrollbarWidth: 'none' }}
|
||||
className="relative mx-auto max-w-[505px] overflow-hidden rounded-[20px] lg:mx-0"
|
||||
style={{ aspectRatio: '505/546' }}
|
||||
onMouseEnter={() => {
|
||||
galleryPausedRef.current = true
|
||||
videoPausedRef.current = true
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
galleryPausedRef.current = false
|
||||
videoPausedRef.current = false
|
||||
}}
|
||||
>
|
||||
{doubled.map((src, i) => (
|
||||
{VIDEO_REVIEWS.map((v, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="relative h-[488px] w-[340px] flex-none overflow-hidden rounded-[20px]"
|
||||
className={`absolute inset-0 transition-opacity duration-500 ${i === videoActive ? 'opacity-100' : 'opacity-0 pointer-events-none'}`}
|
||||
aria-hidden={i !== videoActive}
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover transition-transform duration-700 ease-out hover:scale-110"
|
||||
loading={i < 2 ? 'eager' : 'lazy'}
|
||||
src={v.poster}
|
||||
alt={i === videoActive ? 'Відгук про ДивоЛіс' : ''}
|
||||
className="h-full w-full object-cover"
|
||||
loading={i === 0 ? 'eager' : 'lazy'}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile: horizontal scroll */}
|
||||
<div
|
||||
className="flex w-full gap-5 overflow-x-auto pb-2 lg:hidden"
|
||||
style={{ scrollbarWidth: 'none' }}
|
||||
>
|
||||
{GALLERY.map((src, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex-none overflow-hidden rounded-[20px]"
|
||||
style={{ width: '280px', height: '200px' }}
|
||||
>
|
||||
{/* Play button overlay */}
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<img
|
||||
src={src}
|
||||
src={IMG_PLAY}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
className="h-[100px] w-[100px] drop-shadow-[0px_4px_125px_#171b24]"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Carousel navigation */}
|
||||
<div className="mt-6 flex items-center justify-center gap-6">
|
||||
<button
|
||||
onClick={() => setVideoActive((p) => (p - 1 + vn) % vn)}
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full bg-[#396817] text-white transition-all hover:scale-110 hover:bg-[#2d5414]"
|
||||
aria-label="Попереднє відео"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M11 4L6 9L11 14"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="flex gap-2.5">
|
||||
{VIDEO_REVIEWS.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setVideoActive(i)}
|
||||
className="h-2.5 w-2.5 rounded-full transition-all duration-300"
|
||||
style={{ background: i === videoActive ? '#396817' : '#b8d8a0' }}
|
||||
aria-label={`Відео ${i + 1}`}
|
||||
aria-current={i === videoActive ? true : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setVideoActive((p) => (p + 1) % vn)}
|
||||
className="flex h-10 w-10 items-center justify-center rounded-full bg-[#396817] text-white transition-all hover:scale-110 hover:bg-[#2d5414]"
|
||||
aria-label="Наступне відео"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||
<path
|
||||
d="M7 4L12 9L7 14"
|
||||
stroke="white"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quote block — Figma node 4:202 */}
|
||||
<div
|
||||
className="mt-12 w-full rounded-[20px] px-8 py-5 shadow-[0_4px_30px_rgba(242,139,74,0.25)] lg:mt-16 lg:px-10"
|
||||
style={{ background: '#f1fbeb' }}
|
||||
>
|
||||
<p
|
||||
className="text-center text-[16px] font-medium leading-[1.5] text-[#272727] lg:text-[20px]"
|
||||
style={FONT_MONT}
|
||||
>
|
||||
Це простір, де ви разом із дитиною створюєте власний магічний світ, вчитеся помічати
|
||||
дива у звичайних речах та розвиваєте творчу уяву через спільну гру. Хто знає, можлива
|
||||
саме ця прогулянка спонукає вже дорослу дитину написати казкову історію, яка стане
|
||||
бестселером.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue