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 && ( -
-
- {post.hero.alt -
-
- )} + {/* 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.hero.alt + ) : ( +

+
+ +
+
+ )} + {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 ( -
-
-
- -
-
+
+
) }