246 lines
9.8 KiB
TypeScript
246 lines
9.8 KiB
TypeScript
'use client'
|
||
/* eslint-disable @next/next/no-img-element */
|
||
|
||
import { useState, useRef, useEffect } from 'react'
|
||
import type { HomePageWhyParentsItem, Media } from '@/types/globals'
|
||
|
||
function getMediaUrl(img: Media | string | null | undefined): string | null {
|
||
if (!img) return null
|
||
if (typeof img === 'string') return img
|
||
return img.url ?? null
|
||
}
|
||
|
||
const STATIC_GALLERY = [
|
||
'/images/figma/why-parents-1.webp',
|
||
'/images/figma/why-parents-2.webp',
|
||
'/images/figma/why-parents-3.webp',
|
||
'/images/figma/why-parents-4.webp',
|
||
'/images/figma/gallery-1.webp',
|
||
'/images/figma/gallery-3.webp',
|
||
]
|
||
|
||
const STATIC_ITEMS: HomePageWhyParentsItem[] = [
|
||
{
|
||
title: 'Подорож кількома світами за один день',
|
||
description:
|
||
'ДиноПарк, Диво Ліс, Дзеркальний лабіринт — кожна локація це окремий всесвіт пригод для дітей і батьків.',
|
||
},
|
||
{
|
||
title: 'Свіже повітря та затишок лісу',
|
||
description:
|
||
"Ми оновлюємо тематику та декорації до кожного сезону, тому тут буде цікаво кожного візиту. А у вас з'являться нові яскраві фото у сімейному альбомі.",
|
||
},
|
||
{
|
||
title: 'Нова казка кожної пори року',
|
||
description:
|
||
'Зима, весна, літо, осінь — кожен сезон у парку неповторний. Святкові декорації, сезонні активності та тематичні заходи чекають на вас.',
|
||
},
|
||
{
|
||
title: 'Безпека понад усе',
|
||
description:
|
||
'Всі атракції та зони проходять регулярну перевірку. Охоронці, медичний персонал та чіткі правила безпеки забезпечують спокій для батьків.',
|
||
},
|
||
{
|
||
title: 'Все необхідне — поруч і без пошуків',
|
||
description:
|
||
'Паркування, вбиральні, зона для годування немовлят, укриття, фудкорт — все на місці, щоб ваш відпочинок був справді комфортним.',
|
||
},
|
||
{
|
||
title: 'Фудкорт — смачно для всієї родини',
|
||
description:
|
||
'Хот-доги, піца, кава, лимонади та багато іншого. Є дитяче меню та здорові перекуси — ніхто не залишиться голодним.',
|
||
},
|
||
]
|
||
|
||
interface WhyParentsProps {
|
||
items?: HomePageWhyParentsItem[] | null
|
||
sideGallery?: { image?: Media | string | null }[] | null
|
||
title?: string
|
||
}
|
||
|
||
export function WhyParents({ items, sideGallery, title }: WhyParentsProps) {
|
||
const [openIndex, setOpenIndex] = useState<number>(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 activeItems = items && items.length > 0 ? items : STATIC_ITEMS
|
||
|
||
useEffect(() => {
|
||
autoTimer.current = setInterval(() => {
|
||
setOpenIndex((prev) => (prev + 1) % activeItems.length)
|
||
}, 4000)
|
||
return () => {
|
||
if (autoTimer.current) clearInterval(autoTimer.current)
|
||
}
|
||
}, [activeItems.length])
|
||
|
||
function handleItemClick(i: number) {
|
||
setOpenIndex(i)
|
||
if (autoTimer.current) clearInterval(autoTimer.current)
|
||
autoTimer.current = setInterval(() => {
|
||
setOpenIndex((prev) => (prev + 1) % activeItems.length)
|
||
}, 4000)
|
||
}
|
||
|
||
const galleryUrls: string[] =
|
||
sideGallery && sideGallery.length > 0
|
||
? sideGallery.map((g) => getMediaUrl(g.image) ?? '/images/figma/gallery-1.webp')
|
||
: STATIC_GALLERY
|
||
|
||
const doubled = [...galleryUrls, ...galleryUrls]
|
||
|
||
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]">
|
||
<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' }}
|
||
>
|
||
{title ?? 'Чому батьки обирають Шуміленд'}
|
||
</h2>
|
||
|
||
<div className="flex flex-col items-start gap-16 lg:flex-row lg:items-stretch">
|
||
<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" />
|
||
|
||
<div className="relative flex flex-col gap-6 lg:ml-[76px] lg:min-h-[600px]">
|
||
{activeItems.map((item, i) => {
|
||
const isOpen = openIndex === i
|
||
return (
|
||
<button
|
||
key={i}
|
||
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}
|
||
>
|
||
<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' }}
|
||
>
|
||
{item.title}
|
||
</span>
|
||
<svg
|
||
width="19"
|
||
height="10"
|
||
viewBox="0 0 19 10"
|
||
fill="none"
|
||
className="flex-none transition-transform duration-200"
|
||
style={{ transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}
|
||
aria-hidden="true"
|
||
>
|
||
<path
|
||
d="M1 1L9.5 9L18 1"
|
||
stroke="#f28b4a"
|
||
strokeWidth="2"
|
||
strokeLinecap="round"
|
||
/>
|
||
</svg>
|
||
</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>
|
||
|
||
{/* Horizontal auto-scroll gallery — desktop */}
|
||
<div className="hidden flex-1 overflow-hidden lg:block">
|
||
<div
|
||
className="relative"
|
||
style={{
|
||
maskImage: 'linear-gradient(90deg, transparent 0%, black 8%)',
|
||
WebkitMaskImage: 'linear-gradient(90deg, transparent 0%, black 8%)',
|
||
}}
|
||
>
|
||
<div
|
||
ref={galleryRef}
|
||
className="flex gap-5 overflow-x-auto"
|
||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||
onMouseEnter={() => {
|
||
galleryPausedRef.current = true
|
||
}}
|
||
onMouseLeave={() => {
|
||
galleryPausedRef.current = false
|
||
}}
|
||
>
|
||
{doubled.map((src, i) => (
|
||
<div
|
||
key={i}
|
||
className={`relative h-[488px] w-[505px] flex-none overflow-hidden rounded-[20px] ${i === 0 ? 'bg-[#d6f2c0]' : ''}`}
|
||
>
|
||
<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>
|
||
</div>
|
||
|
||
{/* Mobile: horizontal scroll */}
|
||
<div
|
||
className="flex w-full gap-5 overflow-x-auto pb-2 lg:hidden"
|
||
style={{ scrollbarWidth: 'none' }}
|
||
>
|
||
{galleryUrls.map((src, i) => (
|
||
<div
|
||
key={i}
|
||
className="flex-none overflow-hidden rounded-[20px]"
|
||
style={{ width: '280px', height: '200px' }}
|
||
>
|
||
<img
|
||
src={src}
|
||
alt=""
|
||
aria-hidden="true"
|
||
className="h-full w-full object-cover"
|
||
loading="lazy"
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
)
|
||
}
|