diff --git a/next-env.d.ts b/next-env.d.ts index 9edff1c..c4e7c0e 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,6 +1,6 @@ /// /// -import "./.next/types/routes.d.ts"; +import './.next/types/routes.d.ts' // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/public/images/blog/kapsula-chasu.webp b/public/images/blog/kapsula-chasu.webp new file mode 100644 index 0000000..932d741 Binary files /dev/null and b/public/images/blog/kapsula-chasu.webp differ diff --git a/public/images/blog/sezon-pryhod.webp b/public/images/blog/sezon-pryhod.webp new file mode 100644 index 0000000..5cd2ce3 Binary files /dev/null and b/public/images/blog/sezon-pryhod.webp differ diff --git a/public/images/blog/traven-shymiland.webp b/public/images/blog/traven-shymiland.webp new file mode 100644 index 0000000..35cc975 Binary files /dev/null and b/public/images/blog/traven-shymiland.webp differ diff --git a/src/app/(frontend)/[slug]/page.tsx b/src/app/(frontend)/[slug]/page.tsx index b8074da..acc29e4 100644 --- a/src/app/(frontend)/[slug]/page.tsx +++ b/src/app/(frontend)/[slug]/page.tsx @@ -27,7 +27,10 @@ export async function generateMetadata({ params }: Props): Promise { const { slug } = await params const page = await getPage(slug) if (!page) return { title: 'Не знайдено — Шуміленд' } - const p = page as unknown as { title: string; meta?: { metaTitle?: string; metaDescription?: string } } + const p = page as unknown as { + title: string + meta?: { metaTitle?: string; metaDescription?: string } + } return { title: p.meta?.metaTitle ?? `${p.title} — Шуміленд`, description: p.meta?.metaDescription ?? '', @@ -48,8 +51,8 @@ export default async function PageRoute({ params }: Props) { {p.layout && p.layout.length > 0 ? ( [0]['blocks']} /> ) : ( -
-

Сторінка у процесі створення.

+
+

Сторінка у процесі створення.

)}
diff --git a/src/app/(frontend)/blog/[slug]/page.tsx b/src/app/(frontend)/blog/[slug]/page.tsx index e9712cc..d40b239 100644 --- a/src/app/(frontend)/blog/[slug]/page.tsx +++ b/src/app/(frontend)/blog/[slug]/page.tsx @@ -53,18 +53,18 @@ export default async function BlogPostPage({ params }: Props) { if (!post) notFound() return ( -
+
{/* Header band */} -
-
+
+

{post.title}

{post.publishedAt && ( -

+

{new Date(post.publishedAt).toLocaleDateString('uk-UA', { day: 'numeric', month: 'long', @@ -77,19 +77,19 @@ export default async function BlogPostPage({ params }: Props) { {/* Cover image */} {post.hero?.url && ( -

-
+
+
{post.hero.alt
)} {/* Body */} -
+
{post.body ? (
) : (

Вміст статті незабаром з'явиться тут. diff --git a/src/app/(frontend)/error.tsx b/src/app/(frontend)/error.tsx index dafcf99..f2a0118 100644 --- a/src/app/(frontend)/error.tsx +++ b/src/app/(frontend)/error.tsx @@ -8,23 +8,23 @@ export default function FrontendError({ reset: () => void }) { return ( -

-
+
+

Щось пішло не так

{error.message || 'Виникла помилка при завантаженні сторінки.'}

Оплата відбувається через Monobank. Ваші дані захищені. diff --git a/src/app/(frontend)/page.tsx b/src/app/(frontend)/page.tsx index ef3b2ec..1cb7a60 100644 --- a/src/app/(frontend)/page.tsx +++ b/src/app/(frontend)/page.tsx @@ -28,10 +28,7 @@ export default async function HomePage() { return (

- + - + - +
) diff --git a/src/app/api/admin/seed/route.ts b/src/app/api/admin/seed/route.ts index c4544ba..53d836b 100644 --- a/src/app/api/admin/seed/route.ts +++ b/src/app/api/admin/seed/route.ts @@ -464,6 +464,26 @@ export async function POST(req: NextRequest) { results.push('Seeded site-settings global') // === BLOG POSTS === + function makeLexical(paragraphs: string[]) { + return { + root: { + type: 'root', + format: '', + indent: 0, + version: 1, + direction: 'ltr', + children: paragraphs.map((text) => ({ + type: 'paragraph', + format: '', + indent: 0, + version: 1, + direction: 'ltr', + children: [{ type: 'text', detail: 0, format: 0, mode: 'normal', style: '', text, version: 1 }], + })), + }, + } + } + const { totalDocs: postCount } = await payload.find({ collection: 'blog-posts', limit: 1, @@ -472,33 +492,67 @@ export async function POST(req: NextRequest) { if (postCount === 0) { const postDefs = [ { - title: 'Сезон динозаврів відкрито!', - slug: 'sezon-dynozavriv-vidkryto', - excerpt: 'Шуміленд вітає нових мешканців ДиноПарку — зустрічайте 12 нових динозаврів!', + title: 'Сезон пригод відкрито: Shumiland на ВДНГ запрошує у світ, де оживають динозаври та казки!', + slug: 'sezon-pryhod-vidkryto', + excerpt: 'Сезон стартував із події національного масштабу — фіксації рекорду України: найбільші динозаври країни. 19 квітня у Shumiland представники Книги рекордів України офіційно зафіксували рекорд.', status: 'published', - publishedAt: '2025-04-01T10:00:00.000Z', - imgFile: 'news-bg1.jpg', + publishedAt: '2026-04-19T10:00:00.000Z', + imgFile: 'sezon-pryhod.webp', + imgPath: 'public/images/blog/sezon-pryhod.webp', + body: makeLexical([ + 'Сезон стартував із події національного масштабу — фіксації рекорду України: найбільші динозаври країни. 19 квітня у Shumiland представники Книги рекордів України офіційно зафіксували рекорд. Тепер Shumiland — це не просто парк розваг, а простір, де можна на власні очі побачити рекордсменів і водночас зануритися у світ пригод і казки.', + 'Більше ніж парк: простір, де фантазія стає реальністю', + 'Після яскравого старту в стилі «Аліси в Дивокраї», парк працює у повноцінному режимі, пропонуючи гостям унікальний формат відпочинку просто неба. На території ВДНГ розгорнувся масштабний всесвіт пригод, що поєднує освіту, розваги та естетику.', + 'Динопарк — рекорд, який вражає: Локація в парку, де серед густої зелені «ожили» реалістичні динозаври натуральної величини, серед яких — найбільші динозаври України, офіційно зафіксовані як рекорд. Завдяки сучасним технологіям гіганти рухаються та гарчать, створюючи ефект повної присутності.', + 'Диво Ліс та Будиночок Лісовика: Казкова локація з унікальними топіарними фігурами та арт-об\'єктами. Тут розташована резиденція Лісовика, де діти можуть зануритися у світ легенд та інтерактивних історій.', + 'Дзеркальний лабіринт: Простір світла та ілюзій, що відкриває нові ракурси реальності та створює десятки ефектних фотолокацій.', + 'Атракціони: У сезоні 2026 парк значно розширив зону розваг — на гостей чекає багато нових атракціонів для дітей різного віку.', + 'Shumiland — обов\'язкова точка для візиту на мапі Києва. Велика територія для активного відпочинку, продумана комфортна інфраструктура та оновлені фудкорти для всієї родини.', + '«Ми створили місце, де кожен візит перетворюється на родинне свято. Shumiland — це простір, де діти стають дослідниками, а дорослі дозволяють собі мріяти», — зазначає команда парку.', + ]), }, { - title: 'Весняні канікули в Шуміленді', - slug: 'vesniani-kanikuly', - excerpt: - 'Проведіть весняні канікули незабутньо! Спеціальні активності щодня з 28 березня по 6 квітня.', + title: 'У Шуміленді заклали капсулу часу', + slug: 'kapsula-chasu', + excerpt: 'У Шуміленді відбулася особлива подія — ми заклали капсулу часу як символ початку великого шляху, сповненого дитячого сміху, щирих емоцій, сімейних моментів і незабутніх вражень.', status: 'published', - publishedAt: '2025-03-20T10:00:00.000Z', - imgFile: 'news-bg2.png', + publishedAt: '2026-04-20T10:00:00.000Z', + imgFile: 'kapsula-chasu.webp', + imgPath: 'public/images/blog/kapsula-chasu.webp', + body: makeLexical([ + 'У Шуміленді відбулася особлива подія — ми заклали капсулу часу як символ початку великого шляху, сповненого дитячого сміху, щирих емоцій, сімейних моментів і незабутніх вражень.', + 'Ця капсула стала знаком нашої віри у майбутнє, у розвиток простору, де діти можуть радіти, мріяти та створювати найтепліші спогади разом із родиною.', + 'Відкриття капсули заплановане на 19 квітня 2031 року. Саме тоді ми зможемо повернутися у цей день і побачити, яким яскравим, теплим та наповненим щасливими моментами став шлях Шуміленду.', + 'Попереду — багато щасливих історій, свят і дитячих усмішок. І ми раді створювати їх разом з вами.', + ]), }, { - title: 'Нова локація: Тир з призами', - slug: 'nova-lokatsiya-tyr-z-pryzamy', - excerpt: 'Відтепер у Шуміленді є Тир з призами — точний постріл приносить реальний виграш!', + title: 'Травень у Шуміленді — місяць пригод для всієї родини', + slug: 'traven-u-shumilendt', + excerpt: 'Травень у Шуміленді обіцяє бути яскравим, веселим і сповненим незабутніх емоцій. Щовихідних на гостей чекають тематичні програми, квести, улюблені герої та атмосфера справжнього сімейного відпочинку.', status: 'published', - publishedAt: '2025-03-10T10:00:00.000Z', - imgFile: 'news-bg3.jpg', + publishedAt: '2026-05-01T10:00:00.000Z', + imgFile: 'traven-shymiland.webp', + imgPath: 'public/images/blog/traven-shymiland.webp', + body: makeLexical([ + 'Травень у Шуміленді обіцяє бути яскравим, веселим і сповненим незабутніх емоцій. Щовихідних на гостей чекають тематичні програми, квести, улюблені герої, чарівні пригоди та атмосфера справжнього сімейного відпочинку.', + 'Плануйте свої вихідні разом із Шумілендом та створюйте теплі спогади всією родиною.', + 'Календар подій:', + '09.05 — Розважальна програма «Леді Баг»: Захопливий світ пригод разом із відважною супергероїнею.', + '10.05 — День Матері: Майстер-клас «Подарунок для мами» та тематичний квест «Русалонька».', + '16.05 — Розважальна програма «Футбольний переполох»: Світ драйву та командного духу для маленьких любителів спорту.', + '17.05 — Квест «Орел і Решка»: Весела подорож навколо світу у форматі популярної тревел-пригоди.', + '23.05 — Розважальна програма «Стіч»: Яскрава гавайська пригода разом із веселим та бешкетним героєм.', + '24.05 — Квест «Аніме»: Пройдіть усі випробування та дізнайтеся, де загубився священний меч «Хігуроші».', + '30.05 — Випуск «Школа магії»: Діти поринуть у світ чаклунства, таємниць та неймовірних пригод.', + '31.05 — Тематичний день Wednesday: Загадковий і стильний світ темної естетики, натхненний атмосферою Wednesday та Академією Невермор.', + 'Щодня у Шуміленді на гостей чекають: вражаючий динопарк з динозаврами що рухаються і гарчать, магічний Диво Ліс, атракціони для всієї родини, смачний фудкорт та затишна атмосфера.', + 'Київ, пр-т Академіка Глушкова, 1 ВДНГ. Щодня: 11:00–20:00.', + ]), }, ] for (const post of postDefs) { - const heroId = await uploadMedia(payload, post.imgFile, post.title) + const heroId = await findOrUploadMedia(payload, post.imgFile, post.imgPath, post.title) await payload.create({ collection: 'blog-posts', data: { @@ -507,7 +561,8 @@ export async function POST(req: NextRequest) { excerpt: post.excerpt, status: post.status, publishedAt: post.publishedAt, - hero: heroId ? { image: heroId } : undefined, + hero: heroId ?? undefined, + body: post.body, } as never, overrideAccess: true, }) diff --git a/src/components/blocks/CTABlockComponent.tsx b/src/components/blocks/CTABlockComponent.tsx index 073af41..c870da9 100644 --- a/src/components/blocks/CTABlockComponent.tsx +++ b/src/components/blocks/CTABlockComponent.tsx @@ -12,31 +12,34 @@ export function CTABlockComponent({ title, subtitle, ctaLabel, ctaHref, variant if (!ctaHref) return null const isDark = variant === 'dark' const bg = isDark ? 'bg-[#272727]' : 'bg-[#396817]' - const btnCls = variant === 'secondary' - ? 'border-2 border-white text-white hover:bg-white hover:text-[#396817]' - : 'bg-[#f28b4a] text-white hover:shadow-[0_0_20px_0_#f28b4a]' + const btnCls = + variant === 'secondary' + ? 'border-2 border-white text-white hover:bg-white hover:text-[#396817]' + : 'bg-[#f28b4a] text-white hover:shadow-[0_0_20px_0_#f28b4a]' return (
-
+
{title && (

{title}

)} {subtitle && ( -

+

{subtitle}

)} {ctaLabel && ( {ctaLabel} diff --git a/src/components/blocks/HeroBlockComponent.tsx b/src/components/blocks/HeroBlockComponent.tsx index e506928..d26b40a 100644 --- a/src/components/blocks/HeroBlockComponent.tsx +++ b/src/components/blocks/HeroBlockComponent.tsx @@ -9,32 +9,45 @@ interface HeroBlockProps { backgroundImage?: { url?: string | null } | string | null } -export function HeroBlockComponent({ title, subtitle, ctaLabel, ctaHref, backgroundImage }: HeroBlockProps) { +export function HeroBlockComponent({ + title, + subtitle, + ctaLabel, + ctaHref, + backgroundImage, +}: HeroBlockProps) { const bgUrl = typeof backgroundImage === 'object' ? backgroundImage?.url : null return ( -
+
{bgUrl && ( - + )} -
+

{title}

{subtitle && ( -

+

{subtitle}

)} {ctaLabel && ctaHref && ( {ctaLabel} diff --git a/src/components/blocks/LeadFormBlockComponent.tsx b/src/components/blocks/LeadFormBlockComponent.tsx index 9189759..718ec7f 100644 --- a/src/components/blocks/LeadFormBlockComponent.tsx +++ b/src/components/blocks/LeadFormBlockComponent.tsx @@ -34,30 +34,35 @@ export function LeadFormBlockComponent({ await fetch('/api/leads', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name, phone: showPhone ? phone : undefined, email: showEmail ? email : undefined, source: formSource ?? 'block-form' }), + body: JSON.stringify({ + name, + phone: showPhone ? phone : undefined, + email: showEmail ? email : undefined, + source: formSource ?? 'block-form', + }), }) - } catch { /* noop */ } + } catch { + /* noop */ + } setSubmitted(true) setLoading(false) } return (
-
+
{title && (

{title}

)} - {subtitle && ( -

{subtitle}

- )} + {subtitle &&

{subtitle}

} {submitted ? ( -
-

+

+

{successMessage ?? "Дякуємо! Ми зв'яжемося з вами найближчим часом."}

@@ -69,7 +74,7 @@ export function LeadFormBlockComponent({ value={name} onChange={(e) => setName(e.target.value)} placeholder="Ваше ім'я" - className="w-full px-5 py-3 rounded-[10px] border border-[#272727]/20 text-[#272727] text-[16px] outline-none focus:ring-2 focus:ring-[#f28b4a]" + className="w-full rounded-[10px] border border-[#272727]/20 px-5 py-3 text-[16px] text-[#272727] outline-none focus:ring-2 focus:ring-[#f28b4a]" /> {showPhone && ( setPhone(e.target.value)} placeholder="Телефон" - className="w-full px-5 py-3 rounded-[10px] border border-[#272727]/20 text-[#272727] text-[16px] outline-none focus:ring-2 focus:ring-[#f28b4a]" + className="w-full rounded-[10px] border border-[#272727]/20 px-5 py-3 text-[16px] text-[#272727] outline-none focus:ring-2 focus:ring-[#f28b4a]" /> )} {showEmail && ( @@ -87,13 +92,13 @@ export function LeadFormBlockComponent({ value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" - className="w-full px-5 py-3 rounded-[10px] border border-[#272727]/20 text-[#272727] text-[16px] outline-none focus:ring-2 focus:ring-[#f28b4a]" + className="w-full rounded-[10px] border border-[#272727]/20 px-5 py-3 text-[16px] text-[#272727] outline-none focus:ring-2 focus:ring-[#f28b4a]" /> )}