feat(blog): add 3 real articles + blog image placeholders to seed
- Add seed data for 3 Shumiland articles (Сезон пригод, Капсула часу, Травень) - Create public/images/blog/ with placeholder hero images - Full Lexical body content for each post - Add makeLexical() helper for paragraph formatting in seed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
574b125626
commit
9562db84e3
22 changed files with 273 additions and 150 deletions
2
next-env.d.ts
vendored
2
next-env.d.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
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.
|
||||
|
|
|
|||
BIN
public/images/blog/kapsula-chasu.webp
Normal file
BIN
public/images/blog/kapsula-chasu.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 181 KiB |
BIN
public/images/blog/sezon-pryhod.webp
Normal file
BIN
public/images/blog/sezon-pryhod.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
BIN
public/images/blog/traven-shymiland.webp
Normal file
BIN
public/images/blog/traven-shymiland.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
|
|
@ -27,7 +27,10 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|||
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 ? (
|
||||
<RenderBlocks blocks={p.layout as Parameters<typeof RenderBlocks>[0]['blocks']} />
|
||||
) : (
|
||||
<div className="max-w-[1204px] mx-auto px-8 py-16">
|
||||
<p className="text-[#272727]/60 text-[18px]">Сторінка у процесі створення.</p>
|
||||
<div className="mx-auto max-w-[1204px] px-8 py-16">
|
||||
<p className="text-[18px] text-[#272727]/60">Сторінка у процесі створення.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -53,18 +53,18 @@ export default async function BlogPostPage({ params }: Props) {
|
|||
if (!post) notFound()
|
||||
|
||||
return (
|
||||
<div className="bg-[#f1fbeb] min-h-screen">
|
||||
<div className="min-h-screen bg-[#f1fbeb]">
|
||||
{/* Header band */}
|
||||
<div className="bg-[#396817] py-16 px-8">
|
||||
<div className="max-w-[800px] mx-auto">
|
||||
<div className="bg-[#396817] px-8 py-16">
|
||||
<div className="mx-auto max-w-[800px]">
|
||||
<h1
|
||||
className="text-white font-bold text-[28px] md:text-[40px] lg:text-[48px] leading-tight"
|
||||
className="text-[28px] leading-tight font-bold text-white md:text-[40px] lg:text-[48px]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{post.title}
|
||||
</h1>
|
||||
{post.publishedAt && (
|
||||
<p className="text-white/60 text-[14px] mt-3">
|
||||
<p className="mt-3 text-[14px] text-white/60">
|
||||
{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 && (
|
||||
<div className="max-w-[800px] mx-auto px-8 -mt-8">
|
||||
<div className="rounded-[20px] overflow-hidden shadow-[0_4px_60px_0_rgba(242,139,74,0.25)]">
|
||||
<div className="mx-auto -mt-8 max-w-[800px] px-8">
|
||||
<div className="overflow-hidden rounded-[20px] shadow-[0_4px_60px_0_rgba(242,139,74,0.25)]">
|
||||
<img
|
||||
src={post.hero.url}
|
||||
alt={post.hero.alt ?? post.title}
|
||||
className="w-full max-h-[450px] object-cover"
|
||||
className="max-h-[450px] w-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Body */}
|
||||
<div className="max-w-[800px] mx-auto px-8 py-16">
|
||||
<div className="mx-auto max-w-[800px] px-8 py-16">
|
||||
{post.body ? (
|
||||
<div
|
||||
className="prose prose-lg max-w-none text-[#272727]"
|
||||
|
|
@ -99,7 +99,7 @@ export default async function BlogPostPage({ params }: Props) {
|
|||
</div>
|
||||
) : (
|
||||
<p
|
||||
className="text-[#272727]/60 text-[18px]"
|
||||
className="text-[18px] text-[#272727]/60"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
Вміст статті незабаром з'явиться тут.
|
||||
|
|
|
|||
|
|
@ -8,23 +8,23 @@ export default function FrontendError({
|
|||
reset: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-[#f1fbeb] min-h-screen flex items-center justify-center px-8">
|
||||
<div className="text-center max-w-[500px]">
|
||||
<div className="flex min-h-screen items-center justify-center bg-[#f1fbeb] px-8">
|
||||
<div className="max-w-[500px] text-center">
|
||||
<h1
|
||||
className="text-[#272727] font-bold text-[32px] mb-4"
|
||||
className="mb-4 text-[32px] font-bold text-[#272727]"
|
||||
style={{ fontFamily: 'Montserrat, sans-serif' }}
|
||||
>
|
||||
Щось пішло не так
|
||||
</h1>
|
||||
<p
|
||||
className="text-[#272727]/60 text-[16px] mb-8"
|
||||
className="mb-8 text-[16px] text-[#272727]/60"
|
||||
style={{ fontFamily: 'Montserrat, sans-serif' }}
|
||||
>
|
||||
{error.message || 'Виникла помилка при завантаженні сторінки.'}
|
||||
</p>
|
||||
<button
|
||||
onClick={reset}
|
||||
className="bg-[#f28b4a] text-white font-bold px-8 py-3 rounded-[64px] hover:shadow-[0_0_20px_0_#f28b4a] transition-shadow"
|
||||
className="rounded-[64px] bg-[#f28b4a] px-8 py-3 font-bold text-white transition-shadow hover:shadow-[0_0_20px_0_#f28b4a]"
|
||||
style={{ fontFamily: 'Montserrat, sans-serif' }}
|
||||
>
|
||||
Спробувати знову
|
||||
|
|
|
|||
|
|
@ -37,17 +37,17 @@ function CheckoutForm() {
|
|||
|
||||
window.location.href = data.url
|
||||
} catch {
|
||||
setError('Помилка мережі. Перевірте з\'єднання та спробуйте ще раз.')
|
||||
setError("Помилка мережі. Перевірте з'єднання та спробуйте ще раз.")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-[#f1fbeb] min-h-screen">
|
||||
<div className="bg-[#396817] py-12 px-8">
|
||||
<div className="max-w-[600px] mx-auto">
|
||||
<div className="min-h-screen bg-[#f1fbeb]">
|
||||
<div className="bg-[#396817] px-8 py-12">
|
||||
<div className="mx-auto max-w-[600px]">
|
||||
<h1
|
||||
className="text-white font-bold text-[clamp(28px,4vw,48px)] uppercase"
|
||||
className="text-[clamp(28px,4vw,48px)] font-bold text-white uppercase"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
Оформлення
|
||||
|
|
@ -55,12 +55,12 @@ function CheckoutForm() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-[600px] mx-auto px-8 py-16">
|
||||
<div className="mx-auto max-w-[600px] px-8 py-16">
|
||||
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
|
||||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="font-bold text-[16px] text-[#272727]"
|
||||
className="text-[16px] font-bold text-[#272727]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
Email для підтвердження
|
||||
|
|
@ -72,7 +72,7 @@ function CheckoutForm() {
|
|||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="your@email.com"
|
||||
className="w-full border-2 border-[#e8d5c0] rounded-[12px] px-4 py-3 text-[16px] text-[#272727] bg-white focus:outline-none focus:border-[#f28b4a]"
|
||||
className="w-full rounded-[12px] border-2 border-[#e8d5c0] bg-white px-4 py-3 text-[16px] text-[#272727] focus:border-[#f28b4a] focus:outline-none"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -80,7 +80,7 @@ function CheckoutForm() {
|
|||
<div className="flex flex-col gap-2">
|
||||
<label
|
||||
htmlFor="count"
|
||||
className="font-bold text-[16px] text-[#272727]"
|
||||
className="text-[16px] font-bold text-[#272727]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
Кількість квитків
|
||||
|
|
@ -93,13 +93,13 @@ function CheckoutForm() {
|
|||
required
|
||||
value={count}
|
||||
onChange={(e) => setCount(Number(e.target.value))}
|
||||
className="w-full border-2 border-[#e8d5c0] rounded-[12px] px-4 py-3 text-[16px] text-[#272727] bg-white focus:outline-none focus:border-[#f28b4a]"
|
||||
className="w-full rounded-[12px] border-2 border-[#e8d5c0] bg-white px-4 py-3 text-[16px] text-[#272727] focus:border-[#f28b4a] focus:outline-none"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl px-4 py-3 text-red-700 text-[14px]">
|
||||
<div className="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-[14px] text-red-700">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -107,14 +107,14 @@ function CheckoutForm() {
|
|||
<button
|
||||
type="submit"
|
||||
disabled={isPending || !tariffId}
|
||||
className="bg-[#f28b4a] text-white font-bold text-[18px] py-4 rounded-[64px] disabled:opacity-50 hover:shadow-[0_0_20px_0_#f28b4a] transition-shadow"
|
||||
className="rounded-[64px] bg-[#f28b4a] py-4 text-[18px] font-bold text-white transition-shadow hover:shadow-[0_0_20px_0_#f28b4a] disabled:opacity-50"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{isPending ? 'Зачекайте...' : 'Перейти до оплати'}
|
||||
</button>
|
||||
|
||||
<p
|
||||
className="text-[#272727]/60 text-[13px] text-center"
|
||||
className="text-center text-[13px] text-[#272727]/60"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
Оплата відбувається через Monobank. Ваші дані захищені.
|
||||
|
|
|
|||
|
|
@ -28,10 +28,7 @@ export default async function HomePage() {
|
|||
return (
|
||||
<div className="bg-[#f1fbeb]">
|
||||
<Hero hero={hero} />
|
||||
<Locations
|
||||
data={locations}
|
||||
title={home?.sectionTitles?.locations ?? undefined}
|
||||
/>
|
||||
<Locations data={locations} title={home?.sectionTitles?.locations ?? undefined} />
|
||||
<WhyParents
|
||||
items={home?.whyParents?.items ?? undefined}
|
||||
sideGallery={home?.whyParents?.sideGallery ?? undefined}
|
||||
|
|
@ -42,18 +39,12 @@ export default async function HomePage() {
|
|||
title={home?.sectionTitles?.birthday ?? undefined}
|
||||
intro={home?.birthdayIntro?.text ?? undefined}
|
||||
/>
|
||||
<VideoSection
|
||||
poster={home?.video?.poster ?? undefined}
|
||||
src={home?.video?.src ?? undefined}
|
||||
/>
|
||||
<VideoSection poster={home?.video?.poster ?? undefined} src={home?.video?.src ?? undefined} />
|
||||
<Gallery
|
||||
images={home?.gallery?.images ?? undefined}
|
||||
title={home?.sectionTitles?.gallery ?? undefined}
|
||||
/>
|
||||
<Reviews
|
||||
data={reviews}
|
||||
title={home?.sectionTitles?.reviews ?? undefined}
|
||||
/>
|
||||
<Reviews data={reviews} title={home?.sectionTitles?.reviews ?? undefined} />
|
||||
<News />
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<section className={`${bg} py-[40px] md:py-[80px]`}>
|
||||
<div className="max-w-[1204px] mx-auto px-8 text-center">
|
||||
<div className="mx-auto max-w-[1204px] px-8 text-center">
|
||||
{title && (
|
||||
<h2
|
||||
className="font-bold text-[24px] md:text-[40px] text-white uppercase mb-4"
|
||||
className="mb-4 text-[24px] font-bold text-white uppercase md:text-[40px]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{subtitle && (
|
||||
<p className="text-white/80 text-[16px] md:text-[20px] mb-8 max-w-[600px] mx-auto"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}>
|
||||
<p
|
||||
className="mx-auto mb-8 max-w-[600px] text-[16px] text-white/80 md:text-[20px]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
{ctaLabel && (
|
||||
<Link
|
||||
href={ctaHref}
|
||||
className={`inline-flex items-center justify-center font-bold text-[20px] px-[30px] py-[10px] rounded-[64px] transition-all ${btnCls}`}
|
||||
className={`inline-flex items-center justify-center rounded-[64px] px-[30px] py-[10px] text-[20px] font-bold transition-all ${btnCls}`}
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{ctaLabel}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<section className="relative overflow-hidden bg-[#396817] min-h-[400px] flex items-center">
|
||||
<section className="relative flex min-h-[400px] items-center overflow-hidden bg-[#396817]">
|
||||
{bgUrl && (
|
||||
<img src={bgUrl} alt="" aria-hidden="true" className="absolute inset-0 w-full h-full object-cover pointer-events-none" />
|
||||
<img
|
||||
src={bgUrl}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="relative z-10 w-full max-w-[1204px] mx-auto px-8 py-[80px]">
|
||||
<div className="relative z-10 mx-auto w-full max-w-[1204px] px-8 py-[80px]">
|
||||
<div className="flex flex-col gap-[38px]">
|
||||
<h1
|
||||
className="text-white font-bold uppercase leading-[1.2] text-[32px] md:text-[48px] lg:text-[80px]"
|
||||
className="text-[32px] leading-[1.2] font-bold text-white uppercase md:text-[48px] lg:text-[80px]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
{subtitle && (
|
||||
<p className="text-white font-medium text-[16px] lg:text-[20px] leading-[1.5] max-w-[629px]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}>
|
||||
<p
|
||||
className="max-w-[629px] text-[16px] leading-[1.5] font-medium text-white lg:text-[20px]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
{ctaLabel && ctaHref && (
|
||||
<Link
|
||||
href={ctaHref}
|
||||
className="self-start bg-[#f28b4a] text-white font-bold text-[20px] px-[30px] py-[10px] rounded-[64px] transition-shadow hover:shadow-[0_0_20px_0_#f28b4a]"
|
||||
className="self-start rounded-[64px] bg-[#f28b4a] px-[30px] py-[10px] text-[20px] font-bold text-white transition-shadow hover:shadow-[0_0_20px_0_#f28b4a]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{ctaLabel}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<section className="py-[20px] md:py-[60px] lg:py-[40px]">
|
||||
<div className="max-w-[600px] mx-auto px-8">
|
||||
<div className="mx-auto max-w-[600px] px-8">
|
||||
{title && (
|
||||
<h2
|
||||
className="font-bold text-[24px] md:text-[32px] text-[#272727] uppercase mb-4"
|
||||
className="mb-4 text-[24px] font-bold text-[#272727] uppercase md:text-[32px]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{subtitle && (
|
||||
<p className="text-[#272727] text-[16px] mb-8">{subtitle}</p>
|
||||
)}
|
||||
{subtitle && <p className="mb-8 text-[16px] text-[#272727]">{subtitle}</p>}
|
||||
{submitted ? (
|
||||
<div className="bg-[#d6f2c0] rounded-[20px] p-8 text-center">
|
||||
<p className="text-[#396817] font-bold text-[18px]">
|
||||
<div className="rounded-[20px] bg-[#d6f2c0] p-8 text-center">
|
||||
<p className="text-[18px] font-bold text-[#396817]">
|
||||
{successMessage ?? "Дякуємо! Ми зв'яжемося з вами найближчим часом."}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -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 && (
|
||||
<input
|
||||
|
|
@ -78,7 +83,7 @@ export function LeadFormBlockComponent({
|
|||
value={phone}
|
||||
onChange={(e) => 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]"
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="bg-[#f28b4a] text-white font-bold text-[20px] px-[30px] py-[10px] rounded-[64px] hover:shadow-[0_0_20px_0_#f28b4a] transition-shadow disabled:opacity-50"
|
||||
className="rounded-[64px] bg-[#f28b4a] px-[30px] py-[10px] text-[20px] font-bold text-white transition-shadow hover:shadow-[0_0_20px_0_#f28b4a] disabled:opacity-50"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{loading ? '...' : (ctaLabel ?? 'Відправити')}
|
||||
|
|
|
|||
|
|
@ -19,10 +19,10 @@ export function LocationsTeaserBlockComponent({ title, locations }: LocationsTea
|
|||
|
||||
return (
|
||||
<section className="py-[20px] md:py-[60px] lg:py-[40px]">
|
||||
<div className="max-w-[1204px] mx-auto px-8">
|
||||
<div className="mx-auto max-w-[1204px] px-8">
|
||||
{title && (
|
||||
<h2
|
||||
className="font-bold text-[24px] md:text-[32px] text-[#272727] uppercase mb-[40px] md:mb-[60px]"
|
||||
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}
|
||||
|
|
@ -32,18 +32,31 @@ export function LocationsTeaserBlockComponent({ title, locations }: LocationsTea
|
|||
{locations.map((loc, i) => {
|
||||
const imgUrl = typeof loc.image === 'object' ? loc.image?.url : null
|
||||
return (
|
||||
<article key={i} className="flex-none w-full md:w-[min(694px,90vw)] rounded-[20px] overflow-hidden">
|
||||
<div className="relative flex items-center h-[491px]">
|
||||
<article
|
||||
key={i}
|
||||
className="w-full flex-none overflow-hidden rounded-[20px] md:w-[min(694px,90vw)]"
|
||||
>
|
||||
<div className="relative flex h-[491px] items-center">
|
||||
{imgUrl && (
|
||||
<img src={imgUrl} alt={loc.name ?? ''} className="absolute inset-0 w-full h-full object-cover" />
|
||||
<img
|
||||
src={imgUrl}
|
||||
alt={loc.name ?? ''}
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<div className="absolute right-0 top-0 h-full w-[327px] bg-[rgba(57,104,23,0.8)] flex flex-col justify-center gap-7 px-[30px]">
|
||||
<div className="absolute top-0 right-0 flex h-full w-[327px] flex-col justify-center gap-7 bg-[rgba(57,104,23,0.8)] px-[30px]">
|
||||
<div className="flex flex-col gap-3 text-white">
|
||||
<h3 className="font-bold text-[24px]" style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}>
|
||||
<h3
|
||||
className="text-[24px] font-bold"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{loc.name}
|
||||
</h3>
|
||||
{loc.description && (
|
||||
<p className="text-[16px]" style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}>
|
||||
<p
|
||||
className="text-[16px]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{loc.description}
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -51,7 +64,7 @@ export function LocationsTeaserBlockComponent({ title, locations }: LocationsTea
|
|||
{loc.href && (
|
||||
<Link
|
||||
href={loc.href}
|
||||
className="self-start inline-flex items-center gap-2.5 bg-gradient-to-r from-[#f28b4a] via-[#fdcf54] to-[#f28d4b] text-[#272727] font-bold text-[16px] uppercase px-[30px] py-[10px] rounded-[20px] hover:shadow-[0_0_20px_0_#f28b4a] transition-shadow"
|
||||
className="inline-flex items-center gap-2.5 self-start rounded-[20px] bg-gradient-to-r from-[#f28b4a] via-[#fdcf54] to-[#f28d4b] px-[30px] py-[10px] text-[16px] font-bold text-[#272727] uppercase transition-shadow hover:shadow-[0_0_20px_0_#f28b4a]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{loc.ctaLabel ?? 'Детальніше'}
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ export async function NewsBlockComponent({ title, limit }: NewsBlockProps) {
|
|||
|
||||
return (
|
||||
<section className="py-[20px] md:py-[60px] lg:py-[40px]">
|
||||
<div className="max-w-[1204px] mx-auto px-8">
|
||||
<div className="mx-auto max-w-[1204px] px-8">
|
||||
{title && (
|
||||
<h2
|
||||
className="font-bold text-[24px] md:text-[32px] text-[#272727] uppercase mb-[40px] md:mb-[60px]"
|
||||
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}
|
||||
|
|
@ -50,24 +50,35 @@ export async function NewsBlockComponent({ title, limit }: NewsBlockProps) {
|
|||
{posts.length === 0 ? (
|
||||
<p className="text-[#272727]/60">Публікацій ще немає.</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{posts.map((post) => {
|
||||
const heroUrl = typeof post.hero === 'object' ? post.hero?.url : null
|
||||
return (
|
||||
<Link key={post.id} href={`/blog/${post.slug}`} className="flex flex-col gap-4 group">
|
||||
<div className="h-[221px] rounded-[20px] overflow-hidden bg-[#396817]/10">
|
||||
<Link
|
||||
key={post.id}
|
||||
href={`/blog/${post.slug}`}
|
||||
className="group flex flex-col gap-4"
|
||||
>
|
||||
<div className="h-[221px] overflow-hidden rounded-[20px] bg-[#396817]/10">
|
||||
{heroUrl && (
|
||||
<img src={heroUrl} alt={post.title} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" />
|
||||
<img
|
||||
src={heroUrl}
|
||||
alt={post.title}
|
||||
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<h3
|
||||
className="font-bold text-[20px] text-[#272727] line-clamp-2"
|
||||
className="line-clamp-2 text-[20px] font-bold text-[#272727]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{post.title}
|
||||
</h3>
|
||||
{post.excerpt && (
|
||||
<p className="text-[#272727] text-[16px] line-clamp-3" style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}>
|
||||
<p
|
||||
className="line-clamp-3 text-[16px] text-[#272727]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{post.excerpt}
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,11 @@ interface NewsletterFormBlockProps {
|
|||
ctaLabel?: string | null
|
||||
}
|
||||
|
||||
export function NewsletterFormBlockComponent({ title, subtitle, ctaLabel }: NewsletterFormBlockProps) {
|
||||
export function NewsletterFormBlockComponent({
|
||||
title,
|
||||
subtitle,
|
||||
ctaLabel,
|
||||
}: NewsletterFormBlockProps) {
|
||||
const [email, setEmail] = useState('')
|
||||
const [submitted, setSubmitted] = useState(false)
|
||||
|
||||
|
|
@ -18,36 +22,42 @@ export function NewsletterFormBlockComponent({ title, subtitle, ctaLabel }: News
|
|||
}
|
||||
|
||||
return (
|
||||
<section className="py-[20px] md:py-[60px] lg:py-[40px] bg-[#396817]">
|
||||
<div className="max-w-[1204px] mx-auto px-8 text-center">
|
||||
<section className="bg-[#396817] py-[20px] md:py-[60px] lg:py-[40px]">
|
||||
<div className="mx-auto max-w-[1204px] px-8 text-center">
|
||||
{title && (
|
||||
<h2
|
||||
className="font-bold text-[24px] md:text-[32px] text-white uppercase mb-4"
|
||||
className="mb-4 text-[24px] font-bold text-white uppercase md:text-[32px]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{subtitle && (
|
||||
<p className="text-white/80 text-[16px] mb-8" style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}>
|
||||
<p
|
||||
className="mb-8 text-[16px] text-white/80"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{subtitle}
|
||||
</p>
|
||||
)}
|
||||
{submitted ? (
|
||||
<p className="text-[#fdcf54] font-bold text-[18px]">Дякуємо за підписку!</p>
|
||||
<p className="text-[18px] font-bold text-[#fdcf54]">Дякуємо за підписку!</p>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="flex flex-col sm:flex-row gap-4 max-w-[500px] mx-auto">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="mx-auto flex max-w-[500px] flex-col gap-4 sm:flex-row"
|
||||
>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Ваш email"
|
||||
className="flex-1 px-5 py-3 rounded-[64px] text-[#272727] text-[16px] outline-none focus:ring-2 focus:ring-[#f28b4a]"
|
||||
className="flex-1 rounded-[64px] px-5 py-3 text-[16px] text-[#272727] outline-none focus:ring-2 focus:ring-[#f28b4a]"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-[#f28b4a] text-white font-bold text-[16px] px-[30px] py-[10px] rounded-[64px] hover:shadow-[0_0_20px_0_#f28b4a] transition-shadow whitespace-nowrap"
|
||||
className="rounded-[64px] bg-[#f28b4a] px-[30px] py-[10px] text-[16px] font-bold whitespace-nowrap text-white transition-shadow hover:shadow-[0_0_20px_0_#f28b4a]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{ctaLabel ?? 'Підписатися'}
|
||||
|
|
|
|||
|
|
@ -36,45 +36,60 @@ async function fetchTariffs(showOnlyVisible: boolean): Promise<Tariff[]> {
|
|||
}
|
||||
}
|
||||
|
||||
export async function PricingBlockComponent({ title, subtitle, showOnlyVisible, ctaLabel }: PricingBlockProps) {
|
||||
export async function PricingBlockComponent({
|
||||
title,
|
||||
subtitle,
|
||||
showOnlyVisible,
|
||||
ctaLabel,
|
||||
}: PricingBlockProps) {
|
||||
const tariffs = await fetchTariffs(showOnlyVisible ?? true)
|
||||
|
||||
return (
|
||||
<section className="py-[20px] md:py-[60px] lg:py-[40px]">
|
||||
<div className="max-w-[1204px] mx-auto px-8">
|
||||
<div className="mx-auto max-w-[1204px] px-8">
|
||||
{title && (
|
||||
<h2
|
||||
className="font-bold text-[24px] md:text-[32px] text-[#272727] uppercase mb-4"
|
||||
className="mb-4 text-[24px] font-bold text-[#272727] uppercase md:text-[32px]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
{subtitle && (
|
||||
<p className="text-[#272727] text-[16px] mb-10">{subtitle}</p>
|
||||
)}
|
||||
{subtitle && <p className="mb-10 text-[16px] text-[#272727]">{subtitle}</p>}
|
||||
{tariffs.length === 0 ? (
|
||||
<p className="text-[#272727]/60">Тарифи незабаром з'являться.</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{tariffs.map((t) => (
|
||||
<div key={t.id} className="bg-[#396817] rounded-[20px] p-8 shadow-[0_4px_60px_0_rgba(242,139,74,0.25)] flex flex-col gap-4">
|
||||
<div
|
||||
key={t.id}
|
||||
className="flex flex-col gap-4 rounded-[20px] bg-[#396817] p-8 shadow-[0_4px_60px_0_rgba(242,139,74,0.25)]"
|
||||
>
|
||||
<div>
|
||||
<p className="text-[#f28b4a] font-bold text-[14px] uppercase mb-2" style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}>
|
||||
<p
|
||||
className="mb-2 text-[14px] font-bold text-[#f28b4a] uppercase"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{t.category_tag}
|
||||
</p>
|
||||
<h3 className="text-white font-bold text-[20px]" style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}>
|
||||
<h3
|
||||
className="text-[20px] font-bold text-white"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{t.display_name ?? t.last_synced_name}
|
||||
</h3>
|
||||
</div>
|
||||
{t.last_synced_price != null && (
|
||||
<p className="text-white font-black text-[48px] leading-none" style={{ fontFamily: 'var(--font-inter, Inter), sans-serif' }}>
|
||||
<p
|
||||
className="text-[48px] leading-none font-black text-white"
|
||||
style={{ fontFamily: 'var(--font-inter, Inter), sans-serif' }}
|
||||
>
|
||||
{t.last_synced_price} <span className="text-[24px] font-bold">грн</span>
|
||||
</p>
|
||||
)}
|
||||
<Link
|
||||
href="/kvytky"
|
||||
className="mt-auto text-center bg-[#f28b4a] text-white font-bold text-[16px] px-[30px] py-[10px] rounded-[56px] hover:shadow-[0_0_20px_0_#f28b4a] transition-shadow"
|
||||
className="mt-auto rounded-[56px] bg-[#f28b4a] px-[30px] py-[10px] text-center text-[16px] font-bold text-white transition-shadow hover:shadow-[0_0_20px_0_#f28b4a]"
|
||||
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
|
||||
>
|
||||
{ctaLabel ?? 'Купити квиток'}
|
||||
|
|
|
|||
|
|
@ -22,12 +22,15 @@ export function VideoBlockComponent({ videoUrl, caption, autoplay }: VideoBlockP
|
|||
|
||||
return (
|
||||
<section className="py-[20px] md:py-[40px]">
|
||||
<div className="max-w-[1204px] mx-auto px-8">
|
||||
<div className="relative w-full rounded-[20px] overflow-hidden" style={{ paddingTop: '56.25%' }}>
|
||||
<div className="mx-auto max-w-[1204px] px-8">
|
||||
<div
|
||||
className="relative w-full overflow-hidden rounded-[20px]"
|
||||
style={{ paddingTop: '56.25%' }}
|
||||
>
|
||||
{playing ? (
|
||||
<iframe
|
||||
src={toEmbedUrl(videoUrl, true)}
|
||||
className="absolute inset-0 w-full h-full"
|
||||
className="absolute inset-0 h-full w-full"
|
||||
allow="autoplay; fullscreen"
|
||||
allowFullScreen
|
||||
title={caption ?? 'Video'}
|
||||
|
|
@ -35,10 +38,10 @@ export function VideoBlockComponent({ videoUrl, caption, autoplay }: VideoBlockP
|
|||
) : (
|
||||
<button
|
||||
onClick={() => setPlaying(true)}
|
||||
className="absolute inset-0 w-full h-full bg-[#396817] flex items-center justify-center group"
|
||||
className="group absolute inset-0 flex h-full w-full items-center justify-center bg-[#396817]"
|
||||
aria-label="Відтворити відео"
|
||||
>
|
||||
<span className="w-20 h-20 rounded-full bg-[#f28b4a] flex items-center justify-center group-hover:shadow-[0_0_40px_0_#f28b4a] transition-shadow">
|
||||
<span className="flex h-20 w-20 items-center justify-center rounded-full bg-[#f28b4a] transition-shadow group-hover:shadow-[0_0_40px_0_#f28b4a]">
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" aria-hidden="true">
|
||||
<path d="M10 6L26 16L10 26V6Z" fill="white" />
|
||||
</svg>
|
||||
|
|
@ -46,9 +49,7 @@ export function VideoBlockComponent({ videoUrl, caption, autoplay }: VideoBlockP
|
|||
</button>
|
||||
)}
|
||||
</div>
|
||||
{caption && (
|
||||
<p className="text-center text-[14px] text-[#272727]/60 mt-3">{caption}</p>
|
||||
)}
|
||||
{caption && <p className="mt-3 text-center text-[14px] text-[#272727]/60">{caption}</p>}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export function BirthdayPricing({ packages, title, intro }: BirthdayPricingProps
|
|||
const activePackages = packages && packages.length > 0 ? packages : STATIC_PACKAGES
|
||||
|
||||
return (
|
||||
<section className="py-[20px] md:py-[60px] lg:py-[40px] bg-[#fefaf6]">
|
||||
<section className="bg-[#fefaf6] py-[20px] md:py-[60px] lg:py-[40px]">
|
||||
<div className="mx-auto max-w-[1204px] px-8">
|
||||
<div className="mb-[40px] flex flex-col gap-5 md:mb-[60px]">
|
||||
<h2
|
||||
|
|
|
|||
|
|
@ -60,7 +60,9 @@ export function GallerySlider({ images, speed = 50 }: GallerySliderProps) {
|
|||
scrollRef.current?.scrollBy({ left: dir * cardWidth, behavior: 'smooth' })
|
||||
pausedRef.current = true
|
||||
if (pauseTimer.current) clearTimeout(pauseTimer.current)
|
||||
pauseTimer.current = setTimeout(() => { pausedRef.current = false }, 3000)
|
||||
pauseTimer.current = setTimeout(() => {
|
||||
pausedRef.current = false
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
function openLightbox(idx: number) {
|
||||
|
|
@ -108,8 +110,12 @@ export function GallerySlider({ images, speed = 50 }: GallerySliderProps) {
|
|||
WebkitMaskImage:
|
||||
'linear-gradient(90deg, transparent 0%, black 8%, black 92%, transparent 100%)',
|
||||
}}
|
||||
onMouseEnter={() => { pausedRef.current = true }}
|
||||
onMouseLeave={() => { pausedRef.current = false }}
|
||||
onMouseEnter={() => {
|
||||
pausedRef.current = true
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
pausedRef.current = false
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={scrollRef}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,11 @@ export function BtnPrimary({ children, className = '', ...rest }: BtnPrimaryProp
|
|||
}
|
||||
|
||||
return (
|
||||
<button className={cls} style={BTN_STYLE} {...(rest as ButtonHTMLAttributes<HTMLButtonElement>)}>
|
||||
<button
|
||||
className={cls}
|
||||
style={BTN_STYLE}
|
||||
{...(rest as ButtonHTMLAttributes<HTMLButtonElement>)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,14 +19,7 @@ export function PageHero({ title, subtitle, bgSrc, children }: PageHeroProps) {
|
|||
const src = bgSrc ?? DEFAULT_BG
|
||||
return (
|
||||
<div className="relative -mt-[60px] overflow-hidden px-8 pt-[calc(60px+48px)] pb-16 lg:-mt-[120px] lg:pt-[calc(120px+64px)]">
|
||||
<Image
|
||||
src={src}
|
||||
alt=""
|
||||
fill
|
||||
priority
|
||||
sizes="100vw"
|
||||
className="object-cover"
|
||||
/>
|
||||
<Image src={src} alt="" fill priority sizes="100vw" className="object-cover" />
|
||||
<div className="absolute inset-0 bg-[rgba(57,104,23,0.55)]" />
|
||||
<div className="relative z-10 mx-auto max-w-[1140px] text-white">
|
||||
<h1
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue