diff --git a/next.config.ts b/next.config.ts
index 94d6785..9f7f4d4 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -33,6 +33,8 @@ const nextConfig: NextConfig = {
remotePatterns: [
{ protocol: 'http', hostname: 'localhost', port: '3000', pathname: '/media/**' },
{ protocol: 'https', hostname: 'shumiland.com.ua', pathname: '/media/**' },
+ { protocol: 'https', hostname: 'shumi.ai-impress.com', pathname: '/api/media/**' },
+ { protocol: 'http', hostname: 'localhost', port: '3000', pathname: '/api/media/**' },
],
},
}
diff --git a/public/images/blog/kapsula-chasu.webp b/public/images/blog/kapsula-chasu.webp
index 932d741..1f11b1f 100644
Binary files a/public/images/blog/kapsula-chasu.webp 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
index 5cd2ce3..a9923ba 100644
Binary files a/public/images/blog/sezon-pryhod.webp 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
index 35cc975..713f8c7 100644
Binary files a/public/images/blog/traven-shymiland.webp and b/public/images/blog/traven-shymiland.webp differ
diff --git a/src/app/(frontend)/blog/[slug]/page.tsx b/src/app/(frontend)/blog/[slug]/page.tsx
index d40b239..850f7de 100644
--- a/src/app/(frontend)/blog/[slug]/page.tsx
+++ b/src/app/(frontend)/blog/[slug]/page.tsx
@@ -1,9 +1,11 @@
/* eslint-disable @next/next/no-img-element */
import type { Metadata } from 'next'
+import Link from 'next/link'
import { notFound } from 'next/navigation'
import { getPayload } from 'payload'
import configPromise from '@payload-config'
import { RichText } from '@payloadcms/richtext-lexical/react'
+import { PageHero } from '@/components/ui/PageHero'
interface Props {
params: Promise<{ slug: string }>
@@ -52,59 +54,74 @@ export default async function BlogPostPage({ params }: Props) {
const post = await getPost(slug)
if (!post) notFound()
+ const publishedDate = post.publishedAt
+ ? new Date(post.publishedAt).toLocaleDateString('uk-UA', { day: 'numeric', month: 'long', year: 'numeric' })
+ : null
+
return (
- {/* Header band */}
-
-
-
+
+
+ {/* Back + date row */}
+
+
- {post.title}
-
- {post.publishedAt && (
-
- {new Date(post.publishedAt).toLocaleDateString('uk-UA', {
- day: 'numeric',
- month: 'long',
- year: 'numeric',
- })}
-
+
+ Усі статті
+
+ {publishedDate && (
+
+ {publishedDate}
+
)}
-
- {/* Cover image */}
- {post.hero?.url && (
-
-
-

-
-
- )}
+ {/* Excerpt */}
+ {post.excerpt && (
+
+ {post.excerpt}
+
+ )}
- {/* Body */}
-
+ {/* Body */}
{post.body ? (
) : (
-
+
Вміст статті незабаром з'явиться тут.
)}
+
+ {/* Back button bottom */}
+
+
+
+ Інші статті
+
+
)
diff --git a/src/app/(frontend)/blog/page.tsx b/src/app/(frontend)/blog/page.tsx
index b3f407c..650456f 100644
--- a/src/app/(frontend)/blog/page.tsx
+++ b/src/app/(frontend)/blog/page.tsx
@@ -1,5 +1,6 @@
import type { Metadata } from 'next'
import Link from 'next/link'
+import Image from 'next/image'
import { getPayload } from 'payload'
import configPromise from '@payload-config'
import { PageHero } from '@/components/ui/PageHero'
@@ -9,14 +10,15 @@ export const metadata: Metadata = {
description: 'Новини, статті та корисна інформація від парку Шуміленд.',
}
-export const revalidate = 300
+export const dynamic = "force-dynamic"
interface Post {
id: string
title: string
slug: string
- excerpt?: string
- publishedAt?: string
+ excerpt?: string | null
+ publishedAt?: string | null
+ hero?: { url?: string | null; alt?: string | null } | null
}
async function getPosts(): Promise
{
@@ -27,7 +29,7 @@ async function getPosts(): Promise {
where: { status: { equals: 'published' } },
sort: '-publishedAt',
limit: 12,
- depth: 0,
+ depth: 1,
})
return result.docs as unknown as Post[]
} catch {
@@ -35,6 +37,10 @@ async function getPosts(): Promise {
}
}
+function formatDate(iso: string) {
+ return new Date(iso).toLocaleDateString('uk-UA', { day: 'numeric', month: 'long', year: 'numeric' })
+}
+
export default async function BlogPage() {
const posts = await getPosts()
@@ -45,10 +51,7 @@ export default async function BlogPage() {
{posts.length === 0 ? (
-
+
Статті незабаром з'являться тут.
@@ -58,31 +61,55 @@ export default async function BlogPage() {
-
- {post.title}
-
- {post.excerpt && (
-
+ {post.hero?.url ? (
+
+ ) : (
+
+ )}
+ {post.publishedAt && (
+
+ {formatDate(post.publishedAt)}
+
+ )}
+
+
+ {/* Content */}
+
+
- {post.excerpt}
-
- )}
- {post.publishedAt && (
-
- {new Date(post.publishedAt).toLocaleDateString('uk-UA', {
- day: 'numeric',
- month: 'long',
- year: 'numeric',
- })}
-
- )}
+ {post.title}
+
+ {post.excerpt && (
+
+ {post.excerpt}
+
+ )}
+
+
))}
diff --git a/src/app/api/admin/seed/route.ts b/src/app/api/admin/seed/route.ts
index 5a9def5..17df126 100644
--- a/src/app/api/admin/seed/route.ts
+++ b/src/app/api/admin/seed/route.ts
@@ -65,6 +65,7 @@ async function findOrUploadMedia(
export async function POST(req: NextRequest) {
const forceLocations = req.nextUrl.searchParams.get('force') === 'locations'
+ const forcePosts = req.nextUrl.searchParams.get("force") === "posts"
const payload = await getPayload({ config })
const results: string[] = []
@@ -491,7 +492,11 @@ export async function POST(req: NextRequest) {
limit: 1,
overrideAccess: true,
})
- if (postCount === 0) {
+ if (postCount === 0 || forcePosts) {
+ if (forcePosts && postCount > 0) {
+ const { docs: existingPosts } = await payload.find({ collection: 'blog-posts', limit: 100, overrideAccess: true })
+ for (const p of existingPosts) await payload.delete({ collection: 'blog-posts', id: p.id, overrideAccess: true })
+ }
const postDefs = [
{
title:
@@ -566,6 +571,7 @@ export async function POST(req: NextRequest) {
slug: post.slug,
excerpt: post.excerpt,
status: post.status,
+ _status: 'published',
publishedAt: post.publishedAt,
hero: heroId ?? undefined,
body: post.body,
diff --git a/src/components/sections/Locations.tsx b/src/components/sections/Locations.tsx
index 398d2d0..5e8efca 100644
--- a/src/components/sections/Locations.tsx
+++ b/src/components/sections/Locations.tsx
@@ -77,7 +77,10 @@ export function Locations({ data, title }: LocationsProps) {
slug: loc.slug,
tagline: loc.tagline ?? '',
description: loc.shortDesc ?? '',
- image: getMediaUrl(loc.image) ?? FALLBACK_IMAGES[loc.slug] ?? '/images/figma/loc-dinopark.webp',
+ image:
+ getMediaUrl(loc.image) ??
+ FALLBACK_IMAGES[loc.slug] ??
+ '/images/figma/loc-dinopark.webp',
href: loc.href ?? `/lokatsii#${loc.slug}`,
}))
: STATIC_LOCATIONS
diff --git a/src/components/sections/News.tsx b/src/components/sections/News.tsx
index b01c367..a63f58a 100644
--- a/src/components/sections/News.tsx
+++ b/src/components/sections/News.tsx
@@ -46,14 +46,14 @@ async function getLatestPosts(limit = 3): Promise
{
title: string
slug: string
excerpt?: string
- hero?: { image?: { url?: string } }
+ hero?: { url?: string } | null
}
return {
id: String(doc.id),
title: d.title,
slug: d.slug,
excerpt: d.excerpt,
- heroImage: d.hero?.image ?? null,
+ heroImage: d.hero ?? null,
}
})
} catch {
diff --git a/src/components/sections/Reviews.tsx b/src/components/sections/Reviews.tsx
index dc3cb12..e8b6022 100644
--- a/src/components/sections/Reviews.tsx
+++ b/src/components/sections/Reviews.tsx
@@ -8,7 +8,7 @@ import type { ReviewCMS, Media } from '@/types/globals'
const IMG_AVATAR_DEFAULT = '/images/figma/review-avatar-bg.webp'
const VIDEO_REVIEW_SRC = '/videos/review-video.mp4'
-const VIDEO_REVIEW_POSTER = '/images/review-video-poster.jpg'
+const VIDEO_REVIEW_POSTER = '/images/figma/hero-bg2.webp'
function getMediaUrl(img: Media | string | null | undefined): string | null {
if (!img) return null
@@ -23,7 +23,7 @@ const STATIC_REVIEWS: ReviewCMS[] = [
initial: 'Ж',
ago: '2 місяці тому',
rating: 5,
- text: 'Beautiful, interesting for children and adults. Large area and different locations. Few visitors on weekdays.',
+ text: 'Гарне місце, цікаво для дітей і дорослих. Велика площа і різні локації. Мало відвідувачів у будні.',
source: 'google',
},
{
@@ -32,7 +32,7 @@ const STATIC_REVIEWS: ReviewCMS[] = [
initial: 'А',
ago: '6 місяців тому',
rating: 5,
- text: 'A wonderful dinosaur park, a park of figures made of grass. You can climb on the figures, the kids are delighted! The dinosaurs move, roar, everyone works.',
+ text: "Чудовий динозавровий парк, фігури з трав'яного покриття. Можна залізти на фігури, дітям в захваті! Динозаври рухаються і гарчать.",
source: 'google',
},
{
@@ -41,11 +41,22 @@ const STATIC_REVIEWS: ReviewCMS[] = [
initial: 'V',
ago: '10 місяців тому',
rating: 5,
- text: 'My family and I visited the open-air park at VDNH, we really liked it! It was much better than I expected. The dinosaurs were very memorable — incredible!',
+ text: "Ми з сім'єю відвідали відкритий парк, нам дуже сподобалось! Значно краще, ніж очікував. Динозаври дуже запам'яталися — неймовірно!",
source: 'google',
},
]
+function GoogleIcon() {
+ return (
+
+ )
+}
+
function VideoReviewCard({ pauseScroll }: { pauseScroll: () => void }) {
const [playing, setPlaying] = useState(false)
const videoRef = useRef(null)
@@ -57,9 +68,9 @@ function VideoReviewCard({ pauseScroll }: { pauseScroll: () => void }) {
}
return (
-
- {/* 9:16 video area */}
-
+
+ {/* Video thumbnail area — fixed height */}
+
- {/* Label */}
-
-
- Відеовідгук
-
+ {/* Footer */}
+
+
+
+ Відеовідгук
+
+
Шуміленд 2026
+
@@ -119,7 +127,7 @@ export function Reviews({ data, title }: ReviewsProps) {
useAutoScroll(trackRef, { speed: 1, intervalMs: 20, disabled: autoPaused })
function scrollByOne(dir: 1 | -1) {
- trackRef.current?.scrollBy({ left: dir * 611, behavior: 'smooth' })
+ trackRef.current?.scrollBy({ left: dir * 420, behavior: 'smooth' })
setAutoPaused(true)
if (pauseTimer.current) clearTimeout(pauseTimer.current)
pauseTimer.current = setTimeout(() => setAutoPaused(false), 3000)
@@ -146,22 +154,18 @@ export function Reviews({ data, title }: ReviewsProps) {
aria-label="Попередній відгук"
>
setAutoPaused(true)}
+ onMouseLeave={() => setAutoPaused(false)}
>
- {/* Video review card — first in each half */}
+ {/* Video card */}
{doubled.map((review, idx) => {
@@ -169,61 +173,58 @@ export function Reviews({ data, title }: ReviewsProps) {
return (
-
-
-
-

-
-
- {review.initial ?? review.name[0]}
-
-
-
-
-
- {review.name}
-
-
-
- {review.ago}
-
-
-
+ {/* Quote mark decoration */}
+
+
+ {/* Review text */}
+
+ {review.text}
+
+
+ {/* Divider */}
+
+
+ {/* Author row */}
+
+
+

+
+
+ {review.initial ?? review.name[0]}
+
-
-
- {review.text}
-
+
+
+ {review.name}
+
+
+
+
+ {review.ago}
+
+
+
+ {review.source === 'google' && (
+
+
+
+ )}
)
})}
- {/* Second video card at end for seamless loop */}
+ {/* Second video card for seamless loop */}
@@ -233,13 +234,7 @@ export function Reviews({ data, title }: ReviewsProps) {
aria-label="Наступний відгук"
>
diff --git a/src/components/sections/VideoSection.tsx b/src/components/sections/VideoSection.tsx
index 10b4d5c..8e87c66 100644
--- a/src/components/sections/VideoSection.tsx
+++ b/src/components/sections/VideoSection.tsx
@@ -65,25 +65,22 @@ export function VideoSection({ poster, src }: VideoSectionProps) {
)
}
- // Default: autoplay local video (square 1:1)
+ // Default: autoplay local video — full section width
return (
-