From 6863f5022de3caf45a98a8c6ab05fa983ba09f7b Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Thu, 28 May 2026 16:27:24 +0100 Subject: [PATCH] feat(pages): redesign all 6 pages to match Figma designs with full CMS coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Birthday: new pricing structure (component breakdown instead of packages), packageItems with image upload per card, why/accordion CMS fields - GroupVisits: all text/prices/images CMS-editable (heroDescription, amenities, featureImages, workingHours, price, bottomImages, etc.) - DyvoLis: fixed 404 (seed location record), combo tickets now show (600/1500/1800/2000) - ThankYou: DyvoLis topiary background, 'Купити квиток' button text - Tariffs: updated to match Figma (300/150/300/50 dyno + 4 combo variants) - Seed: add DyvoLis location, correct hero text and section titles for home Co-Authored-By: Claude Sonnet 4.6 --- src/app/(frontend)/dni-narodzhennia/page.tsx | 523 +++++++++++------- .../(frontend)/grupovi-vidviduvannia/page.tsx | 224 +++++--- src/app/(frontend)/kvytky/dyakuiemo/page.tsx | 14 +- src/components/sections/DyvoLisTickets.tsx | 21 +- src/globals/BirthdayPage.ts | 250 ++++++++- src/globals/GroupVisitsPage.ts | 159 ++++-- src/seed.ts | 159 ++++-- 7 files changed, 958 insertions(+), 392 deletions(-) diff --git a/src/app/(frontend)/dni-narodzhennia/page.tsx b/src/app/(frontend)/dni-narodzhennia/page.tsx index 9090502..7f2c27d 100644 --- a/src/app/(frontend)/dni-narodzhennia/page.tsx +++ b/src/app/(frontend)/dni-narodzhennia/page.tsx @@ -5,6 +5,7 @@ import { BirthdayBookingForm } from '@/components/forms/BirthdayBookingForm' import { FormBlock, type FormData as FormBlockData } from '@/components/forms/FormBlock' import { RefreshRouteOnSave } from '@/components/cms/RefreshRouteOnSave' import { DyvoLisWhyVisit } from '@/components/sections/DyvoLisWhyVisit' +import type { Media } from '@/payload-types' export const revalidate = 60 @@ -14,28 +15,15 @@ const FONT_POPPINS = { fontFamily: 'var(--font-poppins, Poppins), sans-serif' } async function getBirthdayPageData() { try { const payload = await getPayload({ config: configPromise }) - return await payload.findGlobal({ slug: 'birthday-page', depth: 1 }) + return await payload.findGlobal({ slug: 'birthday-page', depth: 2 }) } catch { return null } } -async function getPackages() { - try { - const payload = await getPayload({ config: configPromise }) - const result = await payload.find({ - collection: 'birthday-packages', - sort: 'sort', - limit: 20, - }) - return result.docs - } catch { - return [] - } -} - -function formatPrice(price: number): string { - return price.toLocaleString('uk-UA').replace(/,/g, ' ') +function mediaUrl(m: number | Media | null | undefined): string | null { + if (!m || typeof m === 'number') return null + return (m as Media).url ?? null } export async function generateMetadata(): Promise { @@ -48,45 +36,146 @@ export async function generateMetadata(): Promise { } } -const PACKAGE_LOCATIONS = [ - { name: 'ДинопаркArk', description: 'Справжні динозаври в натуральну величину' }, - { name: 'ДивоЛіс', description: 'Казкові топіарні фігури улюблених персонажів' }, - { name: 'Дзеркальний Лабіринт', description: 'Весела гра для дітей та дорослих' }, - { name: 'Костюмованих ведучих', description: 'Аніматори в яскравих костюмах проведуть свято' }, - { name: 'Аніматорів', description: 'Конкурси, ігри та розваги для всіх гостей' }, - { name: 'Затишну альтанку', description: 'Власна зона відпочинку для вашої родини' }, -] +export default async function BirthdayPage() { + const pageData = await getBirthdayPageData() + const d = pageData as any -const WHY_ITEMS = [ - { - title: 'Свято під ключ', - description: - 'Ми беремо на себе всі деталі: аніматорів, конкурси, прикраси та окрему зону для вашої родини. Вам залишається лише насолоджуватись.', - }, - { - title: 'Простір для дітей і дорослих', - description: - 'Шуміленд — це 7 локацій, де кожен знайде щось для себе: від динозаврів до казкових лісів, від лабіринтів до фотозон.', - }, - { - title: 'Незабутні фото та спогади', - description: - 'Унікальні декорації, яскраві персонажі та щира радість дітей — ідеальний фон для фотографій, які хочеться переглядати знову і знову.', - }, -] + const heroTitle = d?.heroTitle ?? 'ДЕНЬ НАРОДЖЕННЯ У ШУМІЛЕНДІ ПІД КЛЮЧ' + const heroSubtitle = + d?.heroSubtitle ?? + 'Будьте повноцінними гостями на дні народження вашої дитини. Залиште нам усі турботи про організацію. Ваш єдиний обовʼязок — відпочивати, святкувати, фотографуватися та насолоджуватися моментами.' + const heroCta = d?.heroCta ?? 'Забронювати пригоду' -export default async function BirthdayPage({ - searchParams, -}: { - searchParams: Promise<{ package?: string }> -}) { - const params = await searchParams - const defaultPackage = params.package - const [pageData, packages] = await Promise.all([getBirthdayPageData(), getPackages()]) + const packageSectionTitle = d?.packageSectionTitle ?? 'ЩО ВХОДИТЬ У ПАКЕТ СВЯТА' + const packageSectionSubtitle = + d?.packageSectionSubtitle ?? 'Єдиний квиток для іменинника та 15-ти гостей' + const packageItems: { + title: string + description: string + imageUrl: string | null + ctaLabel: string + ctaHref?: string + }[] = ( + d?.packageItems ?? [ + { + title: 'ДинопаркArk', + description: 'Справжні динозаври в натуральну величину', + ctaLabel: 'Замовити', + }, + { + title: 'ДивоЛіс', + description: 'Казкові топіарні фігури улюблених персонажів', + ctaLabel: 'Замовити', + }, + { + title: 'Дзеркальний Лабіринт', + description: 'Весела гра для дітей та дорослих', + ctaLabel: 'Замовити', + }, + { + title: 'Костюмованих ведучих', + description: 'Аніматори в яскравих костюмах проведуть свято', + ctaLabel: 'Замовити', + }, + { + title: 'Аквагрим', + description: 'Конкурси, ігри та розваги для всіх гостей', + ctaLabel: 'Замовити', + }, + { + title: 'Затишну альтанку', + description: 'Власна зона відпочинку для вашої родини', + ctaLabel: 'Замовити', + }, + ] + ).map((item: any) => ({ + title: item.title, + description: item.description, + imageUrl: mediaUrl(item.image), + ctaLabel: item.ctaLabel ?? 'Замовити', + ctaHref: item.ctaHref, + })) + + const whyTitle = d?.whyTitle ?? 'Чому варто відвідати ДивоЛіс' + const whyItems = d?.whyItems ?? [ + { + title: 'Свято під ключ', + description: + 'Ми беремо на себе всі деталі: аніматорів, конкурси, прикраси та окрему зону для вашої родини.', + }, + { + title: 'Простір для дітей і дорослих', + description: 'Шуміленд — це 7 локацій, де кожен знайде щось для себе.', + }, + { + title: 'Незабутні фото та спогади', + description: 'Унікальні декорації та щира радість дітей — ідеальний фон для фотографій.', + }, + ] + const whyVideos = d?.whyVideos ?? [] + + const workingHours = d?.workingHours ?? "п'ятниця-субота-неділя з 11:00 до 20:00" + + const pricingSectionTitle = d?.pricingSectionTitle ?? 'ВАРТІСТЬ КВИТКІВ:' + const pricingPackages = d?.pricingPackages ?? [ + { label: 'Стандарт', price: '1 500 грн', ctaLabel: 'Купити квиток', ctaHref: '/kvytky' }, + { label: '+ 4 дитини', price: '1 800 грн', ctaLabel: 'Купити квиток', ctaHref: '/kvytky' }, + { label: '+ 4 дорослих', price: '2 000 грн', ctaLabel: 'Купити квиток', ctaHref: '/kvytky' }, + { + label: 'Додатково', + price: '400 грн', + note: 'особа', + ctaLabel: 'Купити квиток', + ctaHref: '/kvytky', + }, + ] + + const entranceSectionTitle = d?.entranceSectionTitle ?? 'Вхід на локації (для інших дітей):' + const entrancePrices = d?.entrancePrices ?? [ + { + label: 'Вхід на локації для інших дітей', + price: '600 грн', + ctaLabel: 'Забронювати пригоду', + ctaHref: '#order-form', + }, + { + label: 'ДиноПарк', + price: '300 грн', + ctaLabel: 'Забронювати пригоду', + ctaHref: '#order-form', + }, + { label: 'Диволіс', price: '250 грн', ctaLabel: 'Забронювати пригоду', ctaHref: '#order-form' }, + { + label: 'Дзеркальний Лабіринт', + price: '160 грн', + ctaLabel: 'Забронювати пригоду', + ctaHref: '#order-form', + }, + ] + + const freeInclusions = + d?.freeInclusions ?? + 'Діти до 3 років, Діти з іменинником до 18 років, VIP (за наявності запрошення), Діти-сироти' + + const entertainmentSectionTitle = d?.entertainmentSectionTitle ?? 'Розважальна програма:' + const entertainmentPackages = d?.entertainmentPackages ?? [ + { label: 'Тривалість 1 год', price: '3 000 грн', ctaLabel: 'Замовити', ctaHref: '#order-form' }, + { + label: 'Тривалість 1.5 год', + price: '4 500 грн', + ctaLabel: 'Замовити', + ctaHref: '#order-form', + }, + { label: 'Тривалість 2 год', price: '6 000 грн', ctaLabel: 'Замовити', ctaHref: '#order-form' }, + ] + + const formTitle = d?.formTitle ?? 'Замовити святкування' + const formSubtitle = + d?.formSubtitle ?? "Залиште заявку і наш менеджер зв'яжеться з вами протягом 30 хвилин" return (
- {/* ── 1. HERO ────────────────────────────────────────────────────── */} + {/* ── 1. HERO ─────────────────────────────────────────────────────── */}
- {/* dark green overlay */}
- {/* ── 2. ЩО ВХОДИТЬ У ПАКЕТ СВЯТА ──────────────────────────────── */} + {/* ── 2. ЩО ВХОДИТЬ У ПАКЕТ СВЯТА ───────────────────────────────── */}

- ЩО ВХОДИТЬ У ПАКЕТ СВЯТА + {packageSectionTitle}

- Єдиний квиток для іменинника та 15-ти гостей + {packageSectionSubtitle}

- - {/* Desktop: 2 rows × 3 cols. Mobile: horizontal scroll carousel */}
- {PACKAGE_LOCATIONS.map((loc) => ( + {packageItems.map((item) => (
- {/* image placeholder */} - - - {/* Mobile: scroll carousel */} + {/* Mobile scroll */}
- {PACKAGE_LOCATIONS.map((loc) => ( + {packageItems.map((item) => (
-
- {/* ── 3. ЧОМУ ВАРТО ВІДВІДАТИ ───────────────────────────────────── */} - + {/* ── 3. ЧОМУ ВАРТО ─────────────────────────────────────────────── */} + ({ title: i.title, description: i.description }))} + reviewVideos={ + whyVideos.length > 0 + ? whyVideos.map((v: any) => ({ + src: v.src, + poster: v.poster ?? null, + label: v.label ?? null, + })) + : undefined + } + /> - {/* ── 4. WORKING HOURS BANNER ───────────────────────────────────── */} + {/* ── 4. WORKING HOURS ──────────────────────────────────────────── */}

- п’ятниця — субота — неділя з 11:00 до - 20:00 + {workingHours}

- {/* ── 5. PRICING SECTION ────────────────────────────────────────── */} + {/* ── 5. PRICING ────────────────────────────────────────────────── */}

- ВАРТІСТЬ КВИТКІВ: + {pricingSectionTitle}

-
- {packages.length > 0 - ? packages.map((pkg) => ( -
- {pkg.featured && pkg.badge && ( - - {pkg.badge} - - )} -
-

- {pkg.priceLabel ?? formatPrice(pkg.price)}{' '} - {pkg.currency ?? '₴'} -

-

- {pkg.name} -

-
- {pkg.features && pkg.features.length > 0 && ( -
    - {pkg.features.map((f: { id?: string | null; text: string }) => ( -
  • - {f.text} -
  • - ))} -
- )} - - Купити квиток - -
- )) - : /* placeholder cards when CMS is empty */ - [1, 2, 3].map((n) => ( -
-
-

- — ₴ -

-

- Пакет {n} -

-
- - Купити квиток - -
- ))} + {/* Main packages grid */} +
+ {pricingPackages.map((pkg: any) => ( +
+

+ {pkg.label} +

+

+ {pkg.price} +

+ {pkg.note && ( +

+ {pkg.note} +

+ )} + + {pkg.ctaLabel ?? 'Купити квиток'} + +
+ ))}
- {/* Free inclusions note */} -

- Безкоштовно: До 3 дорослих (батьки та інші - супроводжуючі), Діти до 3 років, Вхід по запрошеннях для іменинника -

+ {/* Entrance prices */} +

+ {entranceSectionTitle} +

+
+ {entrancePrices.map((item: any) => ( + + ))} +
+ + {/* Free inclusions */} + {freeInclusions && ( +

+ Безкоштовно: {freeInclusions} +

+ )} + + {/* Entertainment packages */} +

+ {entertainmentSectionTitle} +

+
+ {entertainmentPackages.map((pkg: any) => ( +
+

+ {pkg.label} +

+

+ {pkg.price} +

+ + {pkg.ctaLabel ?? 'Замовити'} + +
+ ))} +
@@ -385,11 +509,10 @@ export default async function BirthdayPage({

- {pageData?.formTitle ?? 'Замовити святкування'} + {formTitle}

- {pageData?.formSubtitle ?? - "Залиште заявку і наш менеджер зв'яжеться з вами протягом 30 хвилин"} + {formSubtitle}

{pageData?.form && typeof pageData.form === 'object' ? ( ) : ( - + )}
diff --git a/src/app/(frontend)/grupovi-vidviduvannia/page.tsx b/src/app/(frontend)/grupovi-vidviduvannia/page.tsx index 30af91a..1bf8327 100644 --- a/src/app/(frontend)/grupovi-vidviduvannia/page.tsx +++ b/src/app/(frontend)/grupovi-vidviduvannia/page.tsx @@ -4,13 +4,15 @@ import configPromise from '@payload-config' import { GroupRequestForm } from '@/components/forms/GroupRequestForm' import { FormBlock, type FormData as FormBlockData } from '@/components/forms/FormBlock' import { RefreshRouteOnSave } from '@/components/cms/RefreshRouteOnSave' +import { PayloadImage } from '@/components/ui/PayloadImage' +import type { Media } from '@/payload-types' export const revalidate = 60 async function getGroupVisitsData() { try { const payload = await getPayload({ config: configPromise }) - return await payload.findGlobal({ slug: 'group-visits-page', depth: 1 }) + return await payload.findGlobal({ slug: 'group-visits-page', depth: 2 }) } catch { return null } @@ -26,14 +28,65 @@ export async function generateMetadata(): Promise { } } +function mediaUrl(m: number | Media | null | undefined): string | null { + if (!m || typeof m === 'number') return null + return (m as Media).url ?? null +} + export default async function GroupVisitsPage() { const data = await getGroupVisitsData() + const d = data as any - const formTitle = data?.formTitle ?? 'Подати заявку на групове відвідування' + const heroTitle = d?.heroTitle ?? 'Групові відвідування' + const heroSubtitle = + d?.heroSubtitle ?? + 'Спеціальні умови для організованих груп. Мінімум 10 осіб — максимум вражень.' + const heroDescription = + d?.heroDescription ?? + 'Шукаєте ідеальне місце для групового виїзду класу чи садочка? Або яскраву локацію для фотосесії? Хочете, щоб дитячий випускний альбом був дійсно унікальним? Запрошуємо провести цей захопливий і незабутній день на казковій локації.' + const heroCta = d?.heroCta ?? 'Забронювати пригоду' + + const featureText = + d?.featureText ?? + 'На дітлах чекає подорож ДинопарКом та ДивоЛісом. Це активне дозвілля на свіжому повітрі та справжні казкові пригоди, де кожен стане героєм власної історії.' + const featureImages: string[] = (d?.featureImages ?? []) + .map((i: any) => mediaUrl(i.image)) + .filter(Boolean) + + const amenitiesTitle = d?.amenitiesTitle ?? 'Ми подбали про затишок і комфорт' + const amenities: { label: string; imageUrl: string | null }[] = ( + d?.amenities ?? [ + { label: '2 локації без обмежень у часі' }, + { label: 'Вбиральні та кафе на території' }, + { label: 'Укриття поруч' }, + { label: 'Огороджено забором, є охорона' }, + ] + ).map((a: any) => ({ label: a.label, imageUrl: mediaUrl(a.image) })) + + const workingHours = d?.workingHours ?? "п'ятниця-субота-неділя з 11:00 до 20:00" + const price = d?.price ?? '350 грн' + const priceLabel = d?.priceLabel ?? 'особа' + const priceNote = d?.priceNote ?? 'Вхід для двох дорослих, що супроводжують дітей, безкоштовний.' + const priceMinPeople = d?.priceMinPeople ?? 'Пропозиція для груп від 10 людей' + const priceCta = d?.priceCta ?? 'Купити квиток' + const priceDescription = + d?.priceDescription ?? + 'У вартість входить відвідування Динопарку та ДивоЛісу.\nЧас перебування на локаціях необмежений.' + + const bottomText = + d?.bottomText ?? + 'Хочете перетворити візит на справжню маленьку експедицію з розповідями або замовити екскурсію з розповідями про динозаврів? Ми залюбки це організуємо! Телефонуйте нам — і ми все підготуємо та розрахуємо індивідуально для вашої групи.' + const bottomImages: string[] = (d?.bottomImages ?? []) + .map((i: any) => mediaUrl(i.image)) + .filter(Boolean) + + const formTitle = d?.formTitle ?? 'Подати заявку на групове відвідування' const formSubtitle = - data?.formSubtitle ?? + d?.formSubtitle ?? 'Вкажіть кількість учасників та бажану дату — менеджер зателефонує і погодить деталі.' + const FONT_MONT = { fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' } + return (
{/* 1. Hero */} @@ -49,21 +102,19 @@ export default async function GroupVisitsPage() {

- ГРУПОВІ ВІЗИТИ + {heroTitle}

- {data?.heroSubtitle ?? 'Спеціальна пропозиція для садочків та шкіл'} + {heroSubtitle}

@@ -74,21 +125,19 @@ export default async function GroupVisitsPage() {

- {} - {(data as any)?.heroDescription ?? - 'Шукаєте ідеальне місце для групового виїзду класу чи садочка? Або яскраву локацію для фотосесії? Хочете, щоб дитячий випускний альбом був дійсно унікальним? Запрошуємо провести цей захопливий і незабутній день на казковій локації.'} + {heroDescription}

- Забронювати пригоду + {heroCta}
@@ -98,15 +147,30 @@ export default async function GroupVisitsPage() {

- На дітлах чекає подорож ДинопарКом та ДивоЛісом. Це активне дозвілля на свіжому повітрі - та справжні казкові пригоди, де кожен стане героєм власної історії. + {featureText}

- {/* Tilted image placeholders */}
-
-
+ {featureImages.length >= 2 ? ( + <> + + + + ) : ( + <> +
+
+ + )}
@@ -116,22 +180,25 @@ export default async function GroupVisitsPage() {

- МИ ПОДБАЛИ ПРО ЗАТИШОК І КОМФОРТ + {amenitiesTitle}

-
- {[ - { label: '2 локації без обмежень у часі', hasPhoto: true }, - { label: 'Вбиральні та кафе на території', hasPhoto: true }, - { label: 'Укриття поруч', hasPhoto: false }, - { label: 'Огороджено забором, є охорона', hasPhoto: true }, - ].map((item) => ( +
+ {amenities.map((item) => (
- {item.hasPhoto &&
} + {item.imageUrl ? ( + {item.label} + ) : ( +
+ )}

{item.label}

@@ -148,15 +215,12 @@ export default async function GroupVisitsPage() { >

ЧАС РОБОТИ

-

- п'ятниця-субота-неділя з 11:00 до 20:00 +

+ {workingHours}

@@ -165,59 +229,53 @@ export default async function GroupVisitsPage() {

ВАРТІСТЬ ГРУПОВОГО ВІЗИТУ:

СПЕЦІАЛЬНА ЦІНА ДЛЯ ГРУП

- 350 грн + {price}

-

- особа +

+ {priceLabel}

- Вхід для двох дорослих, що супроводжують дітей, безкоштовний. + {priceNote}

-

- Пропозиція для груп від 10 людей +

+ {priceMinPeople}

- Купити квиток + {priceCta}
-

- У вартість входить відвідування ДинопаркаTM та ДивоЛісу. -
- Час перебування на локаціях необмежений. +

+ {priceDescription.split('\n').map((line, i) => ( + + {line} + {i < priceDescription.split('\n').length - 1 &&
} +
+ ))}

@@ -227,16 +285,30 @@ export default async function GroupVisitsPage() {

- Хочете перетворити візит на справжню маленьку експедицію з розповідями або замовити - екскурсію з розповідями про динозаврів? Ми залюбки це організуємо! Телефонуйте нам — і - ми все підготуємо та розрахуємо індивідуально для вашої групи. + {bottomText}

- {/* Tilted image placeholders */}
-
-
+ {bottomImages.length >= 2 ? ( + <> + + + + ) : ( + <> +
+
+ + )}
@@ -244,16 +316,10 @@ export default async function GroupVisitsPage() { {/* 8. Order form */}
-

+

{formTitle}

-

+

{formSubtitle}

{data?.form && typeof data.form === 'object' ? ( diff --git a/src/app/(frontend)/kvytky/dyakuiemo/page.tsx b/src/app/(frontend)/kvytky/dyakuiemo/page.tsx index 1537567..39fd42c 100644 --- a/src/app/(frontend)/kvytky/dyakuiemo/page.tsx +++ b/src/app/(frontend)/kvytky/dyakuiemo/page.tsx @@ -12,21 +12,19 @@ export default function DyakuiemoPage() { return (
- {/* Background texture */} + {/* Background photo — DyvoLis topiary */}