Shumiland/src/components/sections/WhyParents.tsx
Vadym Samoilenko ce064bce7a
Some checks are pending
CI / Type Check (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Unit Tests (push) Waiting to run
Deploy / Build & Push Image (push) Waiting to run
Deploy / Deploy to VPS (push) Blocked by required conditions
fix(whyparents): remove right-edge fade from gallery mask
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 16:41:19 +01:00

246 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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>
)
}