feat(dyvolis): add DyvoLis location page with sections
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

- New page route /lokatsii/dyvolis
- DyvoLisHero: title, subtitle, CTA, X-badge + 3 green info tips, topiary cat image
- DyvoLisGallery: quote banner + 4 Figma gallery photos
- DyvoLisWhyVisit: interactive accordion (3 items) + video cards + description
- DyvoLisTickets: working hours banner, 4 ticket cards, 4 combo cards
- Downloaded Figma assets to public/images/dyvolis/
- Fix .gitignore: scope *.png/jpg/jpeg to root-level only (was blocking public/)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-05-13 10:57:24 +01:00
parent f01f2d2698
commit d8a443fe7f
13 changed files with 499 additions and 6 deletions

8
.gitignore vendored
View file

@ -55,7 +55,7 @@ agentdb.rvf
agentdb.rvf.lock
.playwright-mcp/
# Debug screenshots (root-level)
*.png
*.jpg
*.jpeg
# Debug screenshots (root-level only)
/*.png
/*.jpg
/*.jpeg

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

View file

@ -0,0 +1,22 @@
import type { Metadata } from 'next'
import { DyvoLisHero } from '@/components/sections/DyvoLisHero'
import { DyvoLisGallery } from '@/components/sections/DyvoLisGallery'
import { DyvoLisWhyVisit } from '@/components/sections/DyvoLisWhyVisit'
import { DyvoLisTickets } from '@/components/sections/DyvoLisTickets'
export const metadata: Metadata = {
title: 'ДивоЛіс — Шуміленд',
description:
'Казковий топіарний ліс у Шуміленді: фігури з безпечних матеріалів, унікальна ландшафтна композиція та повна свобода для дітей.',
}
export default function DyvoLisPage() {
return (
<div style={{ background: '#fdf2e8' }}>
<DyvoLisHero />
<DyvoLisGallery />
<DyvoLisWhyVisit />
<DyvoLisTickets />
</div>
)
}

View file

@ -0,0 +1,54 @@
/* eslint-disable @next/next/no-img-element */
const QUOTE =
'Це місце де малеча зустрічає героїв улюблених казок. Простір справжнього дитинства.'
const GALLERY = [
'/images/dyvolis/gallery-1.jpg',
'/images/dyvolis/gallery-2.jpg',
'/images/dyvolis/gallery-3.jpg',
'/images/dyvolis/gallery-4.jpg',
]
export function DyvoLisGallery() {
return (
<section className="flex flex-col items-center overflow-hidden">
{/* Quote banner */}
<div
className="relative w-full overflow-hidden py-10 lg:rounded-[20px] 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',
}}
>
{QUOTE}
</p>
</div>
{/* Gallery row — overlaps banner on desktop */}
<div className="relative z-10 -mt-0 w-full lg:-mt-[80px]">
<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' }}
>
{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"
/>
</div>
))}
</div>
</div>
</section>
)
}

View file

@ -0,0 +1,105 @@
/* eslint-disable @next/next/no-img-element */
import { BtnPrimary } from '@/components/ui/BtnPrimary'
const HERO_IMG = '/images/dyvolis/hero-cat.png'
const TIPS = [
'Унікальна ландшафтна композиція з місцями для відпочинку',
'Повна свобода переміщення - без заборон',
]
export function DyvoLisHero() {
return (
<section
className="relative overflow-hidden"
style={{ background: '#fdf2e8' }}
>
<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]"
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 }}
>
Топіарні фігури зроблені з урахуванням важливих деталей, тому ви одразу впізнаєте в них улюблених казкових героїв. Тут можна бігати, стрибати, лазити по фігурках і ставати героями власної казки.
</p>
<BtnPrimary href="/kvytky?category=dyvolis" className="self-start">
Купити квиток
</BtnPrimary>
</div>
{/* 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"
style={{
background: '#396817',
fontFamily: 'var(--font-montserrat, Montserrat), sans-serif',
lineHeight: 1,
marginRight: '-40px',
}}
aria-hidden="true"
>
Х
</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' }}
>
атракціонів з безпечних<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' }}
>
{tip}
</div>
))}
</div>
</div>
{/* Right: hero image (desktop) */}
<div
className="pointer-events-none absolute right-0 top-0 hidden h-full w-[54%] lg:block"
aria-hidden="true"
>
<img
src={HERO_IMG}
alt="Топіарна фігура ДивоЛісу"
className="h-full w-full object-contain object-right-top"
loading="eager"
/>
</div>
</div>
</div>
{/* Mobile: hero image below text */}
<div className="relative mx-auto max-w-[420px] lg:hidden" aria-hidden="true">
<img
src={HERO_IMG}
alt="Топіарна фігура ДивоЛісу"
className="w-full object-contain"
loading="eager"
/>
</div>
</section>
)
}

View file

@ -0,0 +1,156 @@
/* eslint-disable @next/next/no-img-element */
import { BtnPrimary } from '@/components/ui/BtnPrimary'
const FONT_MONT = { fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }
interface TicketCard {
label: string
price: string
per: string
note?: string
}
const SINGLE_TICKETS: TicketCard[] = [
{ label: 'Вхід до ДивоЛісу', price: '300 грн', per: 'за 1 людину' },
{
label: 'Звичайна екскурсія',
price: '150 грн',
per: 'за 1 людину',
note: 'Екскурсійна група — від 5 людей\nПриблизний час екскурсії — 30 хвилин',
},
{ label: 'Палеонтологічна екскурсія', price: '300 грн', per: 'за 1 людину' },
{ label: 'Динородео', price: '50 грн', per: 'сеанс' },
]
const COMBO_TICKETS: TicketCard[] = [
{ label: '', price: '600 грн', per: 'Комбо на людину' },
{
label: '',
price: '1500 грн',
per: 'Комбо на 3 людини',
note: '(мама та/або тато та їхні діти до 14 років)',
},
{
label: '',
price: '1800 грн',
per: 'Комбо на 4 людини',
note: '(мама та/або тато та їхні діти до 14 років)',
},
{
label: '',
price: '2000 грн',
per: 'Комбо на 5 людей',
note: '(мама та/або тато та їхні діти до 14 років)',
},
]
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' }}
>
<div className="flex flex-col gap-3 text-center">
{label && (
<p
className="text-[14px] font-bold uppercase leading-[1.5] text-[#f28b4a] lg:text-[16px]"
style={FONT_MONT}
>
{label}
</p>
)}
<div className="border-t border-[#e8d5c0]" />
<p
className="text-[40px] font-black leading-[1.4] text-[#272727] lg:text-[56px]"
style={FONT_MONT}
>
{price}
</p>
<p className="text-[14px] font-bold leading-[1.5] text-[#272727] lg:text-[16px]" style={FONT_MONT}>
{per}
</p>
{note && (
<p
className="text-[13px] leading-[1.5] text-[#272727] lg:text-[14px]"
style={FONT_MONT}
>
{note}
</p>
)}
</div>
<BtnPrimary href="/kvytky?category=dyvolis" className="w-full justify-center">
Забронювати пригоду
</BtnPrimary>
</div>
)
}
export function DyvoLisTickets() {
return (
<section className="relative overflow-hidden">
{/* Dark green background */}
<div
className="absolute inset-0 z-0 rounded-tl-[20px] rounded-tr-[20px]"
style={{ background: '#396817' }}
/>
<div className="relative z-10 mx-auto max-w-[1204px] px-8 pb-20 pt-0">
{/* 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"
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]"
style={FONT_MONT}
>
Час роботи
</p>
<p
className="text-[18px] font-bold leading-[1.4] text-[#272727] lg:text-[24px]"
style={FONT_MONT}
>
п&apos;ятницясуботанеділя з 11:00 до 20:00
</p>
</div>
{/* Single tickets */}
<h3
className="mb-8 text-[24px] font-bold uppercase leading-[1.4] text-[#fdf2e8] lg:text-[32px]"
style={FONT_MONT}
>
Вартість квитка
</h3>
<div className="mb-14 grid grid-cols-1 gap-5 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">
<h3
className="text-[24px] font-bold uppercase leading-[1.4] text-[#fdf2e8] lg:text-[32px]"
style={FONT_MONT}
>
Комбо
</h3>
<p
className="text-[18px] font-bold leading-[1.4] text-[#fdf2e8] lg:text-[24px]"
style={FONT_MONT}
>
ДивоЛіс із казковими топіарними фігурами + ДиноПарк + Дзеркальний лабіринт
</p>
</div>
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
{COMBO_TICKETS.map((t, i) => (
<Ticket key={i} {...t} />
))}
</div>
</div>
</section>
)
}

View file

@ -0,0 +1,146 @@
'use client'
/* eslint-disable @next/next/no-img-element */
import { useState } from 'react'
const VIDEO_THUMB = '/images/dyvolis/video-thumb.jpg'
const ITEMS = [
{
title: 'Простір для спільної фантазії',
description: '',
},
{
title: 'Казковий ліс у справжньому лісі',
description:
'Ми створили локацію, в якій гармонійно поєднуються казкові фігури та жива природа. Прогулянка лісом ще не була такою захопливою.',
},
{
title: 'Магічні кадри для сімейного альбому',
description: '',
},
]
const VIDEOS = [
{ src: VIDEO_THUMB, label: 'Відео 1' },
{ src: VIDEO_THUMB, label: 'Відео 2' },
{ src: VIDEO_THUMB, label: 'Відео 3' },
]
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)
return (
<section className="py-12 lg:py-16" style={{ background: '#fdf2e8' }}>
<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]"
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">
{/* 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]">
{ITEMS.map((item, i) => {
const isOpen = openIndex === i
return (
<button
key={i}
type="button"
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]"
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>
{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>
)}
</button>
)
})}
</div>
</div>
{/* Video cards */}
<div
className="flex gap-4 overflow-x-auto pb-2 lg:flex-1 lg:overflow-x-visible lg:pb-0"
style={{ scrollbarWidth: 'none' }}
>
{VIDEOS.map((v, 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"
>
<img
src={v.src}
alt={v.label}
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>
)
}

View file

@ -99,11 +99,21 @@ export function VideoSection({ poster, src }: VideoSectionProps) {
className="absolute right-4 bottom-4 flex h-10 w-10 items-center justify-center rounded-full bg-black/50 text-white backdrop-blur-sm transition-colors hover:bg-black/70"
>
{muted ? (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="h-5 w-5">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="h-5 w-5"
>
<path d="M13.5 4.06c0-1.336-1.616-2.005-2.56-1.06l-4.5 4.5H4.508c-1.141 0-2.318.664-2.66 1.905A9.76 9.76 0 0 0 1.5 12c0 .898.121 1.768.35 2.595.341 1.24 1.518 1.905 2.659 1.905h1.93l4.5 4.5c.945.945 2.561.276 2.561-1.06V4.06ZM17.78 9.22a.75.75 0 1 0-1.06 1.06L18.44 12l-1.72 1.72a.75.75 0 1 0 1.06 1.06l1.72-1.72 1.72 1.72a.75.75 0 1 0 1.06-1.06L20.56 12l1.72-1.72a.75.75 0 1 0-1.06-1.06l-1.72 1.72-1.72-1.72Z" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="h-5 w-5">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="h-5 w-5"
>
<path d="M13.5 4.06c0-1.336-1.616-2.005-2.56-1.06l-4.5 4.5H4.508c-1.141 0-2.318.664-2.66 1.905A9.76 9.76 0 0 0 1.5 12c0 .898.121 1.768.35 2.595.341 1.24 1.518 1.905 2.659 1.905h1.93l4.5 4.5c.945.945 2.561.276 2.561-1.06V4.06ZM18.584 5.106a.75.75 0 0 1 1.06 0c3.808 3.807 3.808 9.98 0 13.788a.75.75 0 0 1-1.06-1.06 8.25 8.25 0 0 0 0-11.668.75.75 0 0 1 0-1.06Z" />
<path d="M15.932 7.757a.75.75 0 0 1 1.061 0 6 6 0 0 1 0 8.486.75.75 0 0 1-1.06-1.061 4.5 4.5 0 0 0 0-6.364.75.75 0 0 1 0-1.06Z" />
</svg>