feat(dyvolis): visual overhaul — bg color, 3D gallery, why-visit, compact tickets
- Change all #fdf2e8 → #f1fbeb (matches homepage light-green background) - Hero: fix badge border color, add orange decorative glow ellipse behind cat - Gallery: replace static grid with 3D coverflow carousel (auto-rotate + nav arrows) - WhyVisit: redesign to match WhyParents layout (auto-rotate accordion + RAF scroll gallery) - Tickets: compact cards with flex justify-between so CTA button is always at card bottom Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
91d11c1009
commit
3ae7ff4bad
5 changed files with 296 additions and 136 deletions
|
|
@ -12,7 +12,7 @@ export const metadata: Metadata = {
|
|||
|
||||
export default function DyvoLisPage() {
|
||||
return (
|
||||
<div style={{ background: '#fdf2e8' }}>
|
||||
<div style={{ background: '#f1fbeb' }}>
|
||||
<DyvoLisHero />
|
||||
<DyvoLisGallery />
|
||||
<DyvoLisWhyVisit />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
'use client'
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
const QUOTE =
|
||||
'Це місце – де малеча зустрічає героїв улюблених казок. Простір справжнього дитинства.'
|
||||
|
||||
|
|
@ -11,42 +14,134 @@ const GALLERY = [
|
|||
]
|
||||
|
||||
export function DyvoLisGallery() {
|
||||
const [active, setActive] = useState(0)
|
||||
const n = GALLERY.length
|
||||
|
||||
useEffect(() => {
|
||||
const t = setInterval(() => setActive((p) => (p + 1) % n), 3500)
|
||||
return () => clearInterval(t)
|
||||
}, [n])
|
||||
|
||||
function getOffset(i: number): number {
|
||||
const d = ((i - active) % n + n) % n
|
||||
return d > n / 2 ? d - n : d
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="flex flex-col items-center overflow-hidden">
|
||||
<section className="flex flex-col items-center overflow-hidden" style={{ background: '#f1fbeb' }}>
|
||||
{/* Quote banner */}
|
||||
<div
|
||||
className="relative w-full overflow-hidden py-10 lg:rounded-[20px] lg:py-14"
|
||||
style={{ background: '#396817' }}
|
||||
>
|
||||
<div className="relative w-full py-10 lg:py-14" style={{ background: '#396817' }}>
|
||||
<p
|
||||
className="relative mx-auto max-w-[900px] px-8 text-center text-[20px] font-bold leading-[1.4] text-[#fdf2e8] lg:text-[28px]"
|
||||
style={{
|
||||
fontFamily: 'var(--font-montserrat, Montserrat), sans-serif',
|
||||
}}
|
||||
className="relative mx-auto max-w-[900px] px-8 text-center text-[20px] leading-[1.4] font-bold text-white lg:text-[28px]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{QUOTE}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Gallery row — overlaps banner on desktop */}
|
||||
<div className="relative z-10 -mt-0 w-full lg:-mt-[80px]">
|
||||
{/* 3D Coverflow */}
|
||||
<div className="relative w-full overflow-hidden py-14 lg:py-20">
|
||||
<div
|
||||
className="mx-auto flex max-w-[1204px] gap-4 overflow-x-auto px-8 pb-4 pt-4 lg:overflow-x-visible lg:pb-0 lg:pt-0"
|
||||
style={{ scrollbarWidth: 'none' }}
|
||||
className="relative mx-auto h-[260px] max-w-[1204px] lg:h-[400px]"
|
||||
style={{ perspective: '1100px' }}
|
||||
>
|
||||
{GALLERY.map((src, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-[220px] w-[240px] flex-none overflow-hidden rounded-[16px] shadow-[0_8px_40px_rgba(57,104,23,0.18)] lg:h-[380px] lg:flex-1"
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt={`ДивоЛіс — фото ${i + 1}`}
|
||||
className="h-full w-full object-cover transition-transform duration-500 hover:scale-105"
|
||||
loading="lazy"
|
||||
{GALLERY.map((src, i) => {
|
||||
const d = getOffset(i)
|
||||
const absD = Math.abs(d)
|
||||
const sign = d >= 0 ? 1 : -1
|
||||
|
||||
const cfg =
|
||||
absD === 0
|
||||
? { tx: 0, ry: 0, scale: 1, opacity: 1, z: 20 }
|
||||
: absD === 1
|
||||
? { tx: sign * 270, ry: -sign * 48, scale: 0.82, opacity: 0.62, z: 10 }
|
||||
: { tx: sign * 440, ry: -sign * 62, scale: 0.6, opacity: 0.18, z: 1 }
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
role={d !== 0 ? 'button' : undefined}
|
||||
tabIndex={d !== 0 ? 0 : undefined}
|
||||
aria-label={d !== 0 ? `Показати фото ${i + 1}` : undefined}
|
||||
onClick={() => d !== 0 && setActive(i)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && d !== 0 && setActive(i)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
width: 'clamp(200px, 30vw, 370px)',
|
||||
aspectRatio: '4/3',
|
||||
transform: `translate(calc(-50% + ${cfg.tx}px), -50%) rotateY(${cfg.ry}deg) scale(${cfg.scale})`,
|
||||
opacity: cfg.opacity,
|
||||
zIndex: cfg.z,
|
||||
transition: 'transform 0.65s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.65s ease',
|
||||
borderRadius: '16px',
|
||||
overflow: 'hidden',
|
||||
cursor: d !== 0 ? 'pointer' : 'default',
|
||||
boxShadow:
|
||||
d === 0
|
||||
? '0 20px 60px rgba(57,104,23,0.28)'
|
||||
: '0 8px 24px rgba(0,0,0,0.14)',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={src}
|
||||
alt={d === 0 ? `ДивоЛіс — фото ${i + 1}` : ''}
|
||||
aria-hidden={d !== 0 ? true : undefined}
|
||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div className="mt-10 flex items-center justify-center gap-6">
|
||||
<button
|
||||
onClick={() => setActive((p) => (p - 1 + n) % n)}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div className="flex gap-2.5">
|
||||
{GALLERY.map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setActive(i)}
|
||||
className="h-2.5 w-2.5 rounded-full transition-all duration-300"
|
||||
style={{ background: i === active ? '#396817' : '#b8d8a0' }}
|
||||
aria-label={`Фото ${i + 1}`}
|
||||
aria-current={i === active ? true : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setActive((p) => (p + 1) % n)}
|
||||
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>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -10,29 +10,29 @@ const TIPS = [
|
|||
|
||||
export function DyvoLisHero() {
|
||||
return (
|
||||
<section
|
||||
className="relative overflow-hidden"
|
||||
style={{ background: '#fdf2e8' }}
|
||||
>
|
||||
<section className="relative overflow-hidden" style={{ background: '#f1fbeb' }}>
|
||||
<div className="mx-auto max-w-[1204px] px-8">
|
||||
<div className="relative flex min-h-[600px] flex-col gap-10 pt-12 pb-16 lg:min-h-[700px] lg:flex-row lg:items-start lg:gap-0 lg:pt-16 lg:pb-20">
|
||||
|
||||
{/* Left: text + info tips */}
|
||||
<div className="relative z-10 flex flex-col gap-10 lg:w-[608px] lg:flex-none">
|
||||
|
||||
{/* Text block */}
|
||||
<div className="flex flex-col gap-6">
|
||||
<h1
|
||||
className="text-[36px] font-bold uppercase leading-[1.2] text-[#272727] lg:text-[56px]"
|
||||
className="text-[36px] leading-[1.2] font-bold text-[#272727] uppercase lg:text-[56px]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
ДивоЛіс – територія магії та фантазії
|
||||
</h1>
|
||||
<p
|
||||
className="text-[16px] font-medium leading-[1.6] text-[#272727] lg:text-[20px]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif', fontWeight: 500 }}
|
||||
className="text-[16px] leading-[1.6] font-medium text-[#272727] lg:text-[20px]"
|
||||
style={{
|
||||
fontFamily: 'var(--font-montserrat, Montserrat), sans-serif',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Топіарні фігури зроблені з урахуванням важливих деталей, тому ви одразу впізнаєте в них улюблених казкових героїв. Тут можна бігати, стрибати, лазити по фігурках і ставати героями власної казки.
|
||||
Топіарні фігури зроблені з урахуванням важливих деталей, тому ви одразу впізнаєте в
|
||||
них улюблених казкових героїв. Тут можна бігати, стрибати, лазити по фігурках і
|
||||
ставати героями власної казки.
|
||||
</p>
|
||||
<BtnPrimary href="/kvytky?category=dyvolis" className="self-start">
|
||||
Купити квиток
|
||||
|
|
@ -41,11 +41,10 @@ export function DyvoLisHero() {
|
|||
|
||||
{/* Info tips */}
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
{/* Main tip with X badge */}
|
||||
<div className="relative flex items-center">
|
||||
<div
|
||||
className="z-10 flex h-[100px] w-[100px] flex-none items-center justify-center rounded-full border-[12px] border-[#fdf2e8] text-[56px] font-extrabold text-white"
|
||||
className="z-10 flex h-[100px] w-[100px] flex-none items-center justify-center rounded-full border-[12px] border-[#f1fbeb] text-[56px] font-extrabold text-white"
|
||||
style={{
|
||||
background: '#396817',
|
||||
fontFamily: 'var(--font-montserrat, Montserrat), sans-serif',
|
||||
|
|
@ -57,18 +56,26 @@ export function DyvoLisHero() {
|
|||
Х
|
||||
</div>
|
||||
<div
|
||||
className="flex-1 rounded-[20px] py-6 pl-14 pr-6 text-[16px] font-medium leading-[1.4] text-white lg:text-[20px]"
|
||||
style={{ background: '#396817', fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
className="flex-1 rounded-[20px] py-6 pr-6 pl-14 text-[16px] leading-[1.4] font-medium text-white lg:text-[20px]"
|
||||
style={{
|
||||
background: '#396817',
|
||||
fontFamily: 'var(--font-montserrat, Montserrat), sans-serif',
|
||||
}}
|
||||
>
|
||||
атракціонів з безпечних<br />для дітей матеріалів
|
||||
атракціонів з безпечних
|
||||
<br />
|
||||
для дітей матеріалів
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{TIPS.map((tip, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="rounded-[20px] px-8 py-6 text-[16px] font-medium leading-[1.4] text-white lg:text-[20px]"
|
||||
style={{ background: '#396817', fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
className="rounded-[20px] px-8 py-6 text-[16px] leading-[1.4] font-medium text-white lg:text-[20px]"
|
||||
style={{
|
||||
background: '#396817',
|
||||
fontFamily: 'var(--font-montserrat, Montserrat), sans-serif',
|
||||
}}
|
||||
>
|
||||
{tip}
|
||||
</div>
|
||||
|
|
@ -78,9 +85,14 @@ export function DyvoLisHero() {
|
|||
|
||||
{/* Right: hero image (desktop) */}
|
||||
<div
|
||||
className="pointer-events-none absolute right-0 top-0 hidden h-full w-[54%] lg:block"
|
||||
className="pointer-events-none absolute top-0 right-0 hidden h-full w-[54%] lg:block"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* Decorative orange glow behind the cat */}
|
||||
<div
|
||||
className="absolute top-[15%] right-[10%] h-[380px] w-[380px] rounded-full"
|
||||
style={{ background: '#f28b4a', opacity: 0.28, filter: 'blur(80px)' }}
|
||||
/>
|
||||
<img
|
||||
src={HERO_IMG}
|
||||
alt="Топіарна фігура ДивоЛісу"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable @next/next/no-img-element */
|
||||
import { BtnPrimary } from '@/components/ui/BtnPrimary'
|
||||
|
||||
const FONT_MONT = { fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }
|
||||
|
|
@ -47,40 +46,39 @@ const COMBO_TICKETS: TicketCard[] = [
|
|||
function Ticket({ label, price, per, note }: TicketCard) {
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col gap-6 rounded-[20px] p-5 shadow-[0_4px_30px_rgba(242,139,74,0.25)]"
|
||||
style={{ background: '#fdf2e8' }}
|
||||
className="flex flex-col rounded-[20px] p-5 shadow-[0_4px_30px_rgba(242,139,74,0.25)]"
|
||||
style={{ background: '#f1fbeb' }}
|
||||
>
|
||||
<div className="flex flex-col gap-3 text-center">
|
||||
<div className="flex flex-col gap-2 text-center">
|
||||
{label && (
|
||||
<p
|
||||
className="text-[14px] font-bold uppercase leading-[1.5] text-[#f28b4a] lg:text-[16px]"
|
||||
className="text-[13px] leading-[1.5] font-bold text-[#f28b4a] uppercase"
|
||||
style={FONT_MONT}
|
||||
>
|
||||
{label}
|
||||
</p>
|
||||
)}
|
||||
<div className="border-t border-[#e8d5c0]" />
|
||||
<div className="border-t border-[#c8e8b0]" />
|
||||
<p
|
||||
className="text-[40px] font-black leading-[1.4] text-[#272727] lg:text-[56px]"
|
||||
className="text-[32px] leading-[1.3] font-black text-[#272727] lg:text-[42px]"
|
||||
style={FONT_MONT}
|
||||
>
|
||||
{price}
|
||||
</p>
|
||||
<p className="text-[14px] font-bold leading-[1.5] text-[#272727] lg:text-[16px]" style={FONT_MONT}>
|
||||
<p className="text-[13px] leading-[1.5] font-bold text-[#272727]" style={FONT_MONT}>
|
||||
{per}
|
||||
</p>
|
||||
{note && (
|
||||
<p
|
||||
className="text-[13px] leading-[1.5] text-[#272727] lg:text-[14px]"
|
||||
style={FONT_MONT}
|
||||
>
|
||||
<p className="text-[12px] leading-[1.5] text-[#4a4a4a]" style={FONT_MONT}>
|
||||
{note}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<BtnPrimary href="/kvytky?category=dyvolis" className="w-full justify-center">
|
||||
Забронювати пригоду
|
||||
</BtnPrimary>
|
||||
<div className="mt-auto pt-5">
|
||||
<BtnPrimary href="/kvytky?category=dyvolis" className="w-full justify-center !text-[16px]">
|
||||
Забронювати пригоду
|
||||
</BtnPrimary>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -94,23 +92,22 @@ export function DyvoLisTickets() {
|
|||
style={{ background: '#396817' }}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 mx-auto max-w-[1204px] px-8 pb-20 pt-0">
|
||||
|
||||
<div className="relative z-10 mx-auto max-w-[1204px] px-8 pt-0 pb-16">
|
||||
{/* Working hours banner */}
|
||||
<div
|
||||
className="mb-14 flex flex-col items-center justify-center gap-2 overflow-hidden rounded-b-[20px] px-8 py-6 text-center lg:flex-row lg:gap-6"
|
||||
className="mb-10 flex flex-col items-center justify-center gap-2 overflow-hidden rounded-b-[20px] px-8 py-5 text-center lg:flex-row lg:gap-6"
|
||||
style={{
|
||||
background: 'linear-gradient(90deg, #f28b4a 0%, #fdcf54 50%, #f28b4a 100%)',
|
||||
}}
|
||||
>
|
||||
<p
|
||||
className="text-[22px] font-bold uppercase leading-[1.4] text-[#272727] lg:text-[28px]"
|
||||
className="text-[20px] leading-[1.4] font-bold text-[#272727] uppercase lg:text-[26px]"
|
||||
style={FONT_MONT}
|
||||
>
|
||||
Час роботи
|
||||
</p>
|
||||
<p
|
||||
className="text-[18px] font-bold leading-[1.4] text-[#272727] lg:text-[24px]"
|
||||
className="text-[16px] leading-[1.4] font-bold text-[#272727] lg:text-[22px]"
|
||||
style={FONT_MONT}
|
||||
>
|
||||
п'ятниця–субота–неділя з 11:00 до 20:00
|
||||
|
|
@ -119,33 +116,33 @@ export function DyvoLisTickets() {
|
|||
|
||||
{/* Single tickets */}
|
||||
<h3
|
||||
className="mb-8 text-[24px] font-bold uppercase leading-[1.4] text-[#fdf2e8] lg:text-[32px]"
|
||||
className="mb-6 text-[22px] leading-[1.4] font-bold text-white uppercase lg:text-[28px]"
|
||||
style={FONT_MONT}
|
||||
>
|
||||
Вартість квитка
|
||||
</h3>
|
||||
<div className="mb-14 grid grid-cols-1 gap-5 sm:grid-cols-2">
|
||||
<div className="mb-10 grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{SINGLE_TICKETS.map((t, i) => (
|
||||
<Ticket key={i} {...t} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Combo header */}
|
||||
<div className="mb-8 flex flex-col gap-2">
|
||||
<div className="mb-6 flex flex-col gap-2">
|
||||
<h3
|
||||
className="text-[24px] font-bold uppercase leading-[1.4] text-[#fdf2e8] lg:text-[32px]"
|
||||
className="text-[22px] leading-[1.4] font-bold text-white uppercase lg:text-[28px]"
|
||||
style={FONT_MONT}
|
||||
>
|
||||
Комбо
|
||||
</h3>
|
||||
<p
|
||||
className="text-[18px] font-bold leading-[1.4] text-[#fdf2e8] lg:text-[24px]"
|
||||
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-5 sm:grid-cols-2">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
{COMBO_TICKETS.map((t, i) => (
|
||||
<Ticket key={i} {...t} />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
'use client'
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
const VIDEO_THUMB = '/images/dyvolis/video-thumb.jpg'
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
|
||||
const ITEMS = [
|
||||
{
|
||||
title: 'Простір для спільної фантазії',
|
||||
description: '',
|
||||
description:
|
||||
'Вигадуйте казки та пригоди разом із дітьми — кожна топіарна фігурка стає новою сторінкою вашої власної чарівної історії.',
|
||||
},
|
||||
{
|
||||
title: 'Казковий ліс у справжньому лісі',
|
||||
|
|
@ -17,62 +16,95 @@ const ITEMS = [
|
|||
},
|
||||
{
|
||||
title: 'Магічні кадри для сімейного альбому',
|
||||
description: '',
|
||||
description:
|
||||
'Унікальні топіарні декорації та яскраві персонажі — ідеальний фон для незабутніх сімейних фотографій, які захочеться переглядати знову і знову.',
|
||||
},
|
||||
]
|
||||
|
||||
const VIDEOS = [
|
||||
{ src: VIDEO_THUMB, label: 'Відео 1' },
|
||||
{ src: VIDEO_THUMB, label: 'Відео 2' },
|
||||
{ src: VIDEO_THUMB, label: 'Відео 3' },
|
||||
const GALLERY = [
|
||||
'/images/dyvolis/gallery-1.jpg',
|
||||
'/images/dyvolis/gallery-2.jpg',
|
||||
'/images/dyvolis/gallery-3.jpg',
|
||||
'/images/dyvolis/gallery-4.jpg',
|
||||
]
|
||||
|
||||
const PLAY_BTN = (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 flex h-[72px] w-[72px] items-center justify-center rounded-full bg-white/90 shadow-[0_4px_40px_rgba(0,0,0,0.4)]"
|
||||
>
|
||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" aria-hidden="true">
|
||||
<path d="M8 5.5l16 8.5-16 8.5V5.5z" fill="#396817" />
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
|
||||
export function DyvoLisWhyVisit() {
|
||||
const [openIndex, setOpenIndex] = useState<number>(1)
|
||||
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]
|
||||
|
||||
useEffect(() => {
|
||||
autoTimer.current = setInterval(() => {
|
||||
setOpenIndex((prev) => (prev + 1) % ITEMS.length)
|
||||
}, 4000)
|
||||
return () => {
|
||||
if (autoTimer.current) clearInterval(autoTimer.current)
|
||||
}
|
||||
}, [])
|
||||
|
||||
function handleItemClick(i: number) {
|
||||
setOpenIndex(i)
|
||||
if (autoTimer.current) clearInterval(autoTimer.current)
|
||||
autoTimer.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-12 lg:py-16" style={{ background: '#fdf2e8' }}>
|
||||
<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-10 text-[24px] font-bold uppercase leading-[1.4] text-[#272727] lg:mb-14 lg:text-[32px]"
|
||||
className="mb-[40px] text-[24px] font-bold text-[#272727] uppercase md:mb-[60px] md:text-[32px]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
Чому варто відвідати ДивоЛіс
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-col gap-12 lg:flex-row lg:items-start lg:gap-16">
|
||||
<div className="flex flex-col items-start gap-16 lg:flex-row lg:items-stretch">
|
||||
{/* 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" />
|
||||
|
||||
{/* Accordion column */}
|
||||
<div className="relative flex-none lg:w-auto">
|
||||
{/* Green vertical decoration bar */}
|
||||
<div className="absolute left-0 top-8 hidden h-[420px] w-[280px] rounded-[30px] bg-[#396817] lg:block" />
|
||||
|
||||
<div className="relative flex flex-col gap-5 lg:ml-[76px]">
|
||||
<div className="relative flex flex-col gap-6 lg:ml-[76px] lg:min-h-[560px]">
|
||||
{ITEMS.map((item, i) => {
|
||||
const isOpen = openIndex === i
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
onClick={() => handleItemClick(i)}
|
||||
className="flex w-full flex-col gap-2.5 rounded-[10px] bg-[#f1fbeb] px-[50px] py-[20px] text-left shadow-[0_4px_60px_0_rgba(242,139,74,0.25)] transition-all duration-200 lg:w-[628px]"
|
||||
aria-expanded={isOpen}
|
||||
onClick={() => setOpenIndex(i)}
|
||||
className="flex w-full flex-col gap-2.5 rounded-[10px] bg-[#fdf2e8] px-10 py-5 text-left shadow-[0_4px_60px_0_rgba(242,139,74,0.25)] transition-all duration-200 lg:w-[628px]"
|
||||
>
|
||||
<div className="flex items-center gap-5">
|
||||
<span
|
||||
className="flex-1 text-[18px] font-bold leading-[1.5] text-[#272727] lg:text-[20px]"
|
||||
className="flex-1 text-[20px] leading-tight font-bold text-[#272727]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{item.title}
|
||||
|
|
@ -86,60 +118,84 @@ export function DyvoLisWhyVisit() {
|
|||
style={{ transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M1 1L9.5 9L18 1" stroke="#f28b4a" strokeWidth="2" strokeLinecap="round" />
|
||||
<path
|
||||
d="M1 1L9.5 9L18 1"
|
||||
stroke="#f28b4a"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
{item.description && (
|
||||
<div
|
||||
className={`grid transition-[grid-template-rows] duration-300 ease-out ${isOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'}`}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<p
|
||||
className="pt-1 text-[15px] leading-[1.6] font-light text-[#272727] lg:text-[16px]"
|
||||
style={{ fontFamily: 'var(--font-poppins, Poppins), sans-serif' }}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={`grid transition-[grid-template-rows] duration-300 ease-out ${isOpen ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'}`}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<p
|
||||
className="pt-2 text-[16px] leading-[1.6] font-light text-[#272727]"
|
||||
style={{ fontFamily: 'var(--font-poppins, Poppins), sans-serif' }}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Video cards */}
|
||||
{/* Right: auto-scroll gallery — desktop */}
|
||||
<div className="hidden flex-1 overflow-hidden lg:block">
|
||||
<div
|
||||
ref={galleryRef}
|
||||
className="flex gap-5 overflow-x-auto"
|
||||
style={{ scrollbarWidth: 'none' }}
|
||||
onMouseEnter={() => {
|
||||
galleryPausedRef.current = true
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
galleryPausedRef.current = false
|
||||
}}
|
||||
>
|
||||
{doubled.map((src, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="relative h-[488px] w-[340px] flex-none overflow-hidden rounded-[20px]"
|
||||
>
|
||||
<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'}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile: horizontal scroll */}
|
||||
<div
|
||||
className="flex gap-4 overflow-x-auto pb-2 lg:flex-1 lg:overflow-x-visible lg:pb-0"
|
||||
className="flex w-full gap-5 overflow-x-auto pb-2 lg:hidden"
|
||||
style={{ scrollbarWidth: 'none' }}
|
||||
>
|
||||
{VIDEOS.map((v, i) => (
|
||||
{GALLERY.map((src, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="relative h-[260px] w-[260px] flex-none cursor-pointer overflow-hidden rounded-[20px] bg-[#d6f2c0] shadow-[0_4px_30px_rgba(57,104,23,0.15)] transition-transform duration-300 hover:scale-[1.02] lg:h-[480px] lg:flex-1 lg:w-auto"
|
||||
className="flex-none overflow-hidden rounded-[20px]"
|
||||
style={{ width: '280px', height: '200px' }}
|
||||
>
|
||||
<img
|
||||
src={v.src}
|
||||
alt={v.label}
|
||||
src={src}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="h-full w-full object-cover"
|
||||
loading="lazy"
|
||||
/>
|
||||
{PLAY_BTN}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description paragraph */}
|
||||
<div className="mt-12 rounded-[20px] shadow-[0_4px_30px_rgba(242,139,74,0.25)] lg:mt-16">
|
||||
<p
|
||||
className="px-8 py-6 text-[16px] font-medium leading-[1.6] text-[#272727] lg:text-[20px]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
Це простір, де ви разом із дитиною створюєте власний магічний світ, вчитеся помічати дива у звичайних речах та розвиваєте творчу уяву через спільну гру. Хто знає, можлива саме ця прогулянка спонукає вже дорослу дитину написати казкову історію, яка стане бестселером.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue