feat: blog posts, redesigned sections, video full-width, reviews hover pause
Some checks are pending
CI / Type Check (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Unit Tests (push) Waiting to run
Deploy / Build & Push Image (push) Waiting to run
Deploy / Deploy to VPS (push) Blocked by required conditions

- Blog listing: force-dynamic SSR, new photo cards with date badge + hover animation
- Blog article: custom typography (h1-h3 styling, lists, blockquotes) without plugin
- News.tsx: fix hero image mapping (direct upload, not .image sub-field)
- Reviews.tsx: hover pauses auto-scroll, resumes on mouse leave; fix poster 404
- VideoSection: full viewport width, no container constraint
- next.config.ts: add shumi.ai-impress.com to remotePatterns (fixes 400 on blog images)
- Seed: add _status published for Payload drafts; force-posts flag; real blog photos
- Locations.tsx: per-slug fallback images (fixes all cards showing dinopark photo)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-05-11 15:52:08 +01:00
parent 55d25cacac
commit f8421ed42d
11 changed files with 221 additions and 174 deletions

View file

@ -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/**' },
],
},
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 144 KiB

View file

@ -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 (
<div className="min-h-screen bg-[#f1fbeb]">
{/* Header band */}
<div className="bg-[#396817] px-8 py-16">
<div className="mx-auto max-w-[800px]">
<h1
className="text-[28px] leading-tight font-bold text-white md:text-[40px] lg:text-[48px]"
<PageHero
title={post.title}
bgSrc={post.hero?.url ?? undefined}
/>
<div className="mx-auto max-w-[800px] px-8 py-12">
{/* Back + date row */}
<div className="mb-8 flex items-center justify-between">
<Link
href="/blog"
className="flex items-center gap-2 text-[14px] font-semibold text-[#396817] transition-colors hover:text-[#f28b4a]"
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
>
{post.title}
</h1>
{post.publishedAt && (
<p className="mt-3 text-[14px] text-white/60">
{new Date(post.publishedAt).toLocaleDateString('uk-UA', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</p>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M13 8H3M7 4L3 8l4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
Усі статті
</Link>
{publishedDate && (
<span className="rounded-full bg-[#f28b4a] px-4 py-1.5 text-[13px] font-semibold text-white">
{publishedDate}
</span>
)}
</div>
</div>
{/* Cover image */}
{post.hero?.url && (
<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="max-h-[450px] w-full object-cover"
/>
</div>
</div>
)}
{/* Excerpt */}
{post.excerpt && (
<p
className="mb-8 text-[18px] leading-relaxed font-medium text-[#272727]/70 border-l-4 border-[#f28b4a] pl-5"
style={{ fontFamily: 'var(--font-poppins, Poppins), sans-serif' }}
>
{post.excerpt}
</p>
)}
{/* Body */}
<div className="mx-auto max-w-[800px] px-8 py-16">
{/* Body */}
{post.body ? (
<div
className="prose prose-lg max-w-none text-[#272727]"
className="[&_p]:mb-5 [&_p]:text-[17px] [&_p]:leading-[1.85] [&_p]:text-[#272727] [&_h1]:mb-6 [&_h1]:mt-10 [&_h1]:text-[32px] [&_h1]:font-bold [&_h1]:leading-tight [&_h1]:text-[#223e0d] [&_h2]:mb-4 [&_h2]:mt-10 [&_h2]:border-b [&_h2]:border-[#396817]/20 [&_h2]:pb-2 [&_h2]:text-[26px] [&_h2]:font-bold [&_h2]:leading-tight [&_h2]:text-[#223e0d] [&_h3]:mb-3 [&_h3]:mt-8 [&_h3]:text-[20px] [&_h3]:font-semibold [&_h3]:text-[#272727] [&_ul]:mb-5 [&_ul]:list-disc [&_ul]:pl-7 [&_ol]:mb-5 [&_ol]:list-decimal [&_ol]:pl-7 [&_li]:mb-2 [&_li]:text-[17px] [&_li]:leading-[1.75] [&_strong]:font-semibold [&_strong]:text-[#223e0d] [&_em]:italic [&_a]:text-[#396817] [&_a]:underline [&_a]:underline-offset-2 [&_blockquote]:my-6 [&_blockquote]:border-l-4 [&_blockquote]:border-[#f28b4a] [&_blockquote]:pl-5 [&_blockquote]:italic [&_blockquote]:text-[#272727]/70"
style={{ fontFamily: 'var(--font-poppins, Poppins), sans-serif' }}
>
<RichText data={post.body} />
</div>
) : (
<p
className="text-[18px] text-[#272727]/60"
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
>
<p className="text-[18px] text-[#272727]/60" style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}>
Вміст статті незабаром з&apos;явиться тут.
</p>
)}
{/* Back button bottom */}
<div className="mt-12 border-t border-[#396817]/10 pt-8">
<Link
href="/blog"
className="inline-flex items-center gap-2 rounded-[64px] bg-[#396817] px-8 py-3 text-[15px] font-semibold text-white transition-all hover:bg-[#f28b4a]"
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M13 8H3M7 4L3 8l4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
Інші статті
</Link>
</div>
</div>
</div>
)

View file

@ -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<Post[]> {
@ -27,7 +29,7 @@ async function getPosts(): Promise<Post[]> {
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<Post[]> {
}
}
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() {
<div className="mx-auto max-w-[1204px] px-8 py-16">
{posts.length === 0 ? (
<div className="py-20 text-center">
<p
className="text-[20px] text-[#272727]/60"
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
>
<p className="text-[20px] text-[#272727]/60" style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}>
Статті незабаром з&apos;являться тут.
</p>
</div>
@ -58,31 +61,55 @@ export default async function BlogPage() {
<Link
key={post.id}
href={`/blog/${post.slug}`}
className="flex flex-col gap-3 rounded-[20px] bg-white p-6 shadow-sm transition-shadow hover:shadow-md"
className="group flex flex-col overflow-hidden rounded-[24px] bg-white shadow-[0_4px_30px_0_rgba(57,104,23,0.1)] transition-all duration-300 hover:-translate-y-1 hover:shadow-[0_12px_40px_0_rgba(57,104,23,0.2)]"
>
<h2
className="text-[18px] leading-snug font-bold text-[#272727]"
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
>
{post.title}
</h2>
{post.excerpt && (
<p
className="line-clamp-3 text-[14px] leading-relaxed text-[#272727]/60"
{/* Hero image */}
<div className="relative h-[220px] overflow-hidden bg-[#396817]">
{post.hero?.url ? (
<Image
src={post.hero.url}
alt={post.hero.alt ?? post.title}
fill
sizes="(min-width: 1024px) 380px, (min-width: 640px) 50vw, 100vw"
className="object-cover transition-transform duration-500 group-hover:scale-105"
/>
) : (
<div className="h-full w-full bg-gradient-to-br from-[#396817] to-[#74d22c]">
<div className="flex h-full items-center justify-center opacity-30">
<svg width="64" height="64" viewBox="0 0 24 24" fill="white"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/></svg>
</div>
</div>
)}
{post.publishedAt && (
<div className="absolute top-4 left-4 rounded-full bg-[#f28b4a] px-3 py-1 text-[12px] font-semibold text-white shadow-md">
{formatDate(post.publishedAt)}
</div>
)}
</div>
{/* Content */}
<div className="flex flex-1 flex-col gap-3 p-6">
<h2
className="line-clamp-2 text-[18px] leading-snug font-bold text-[#272727] transition-colors duration-200 group-hover:text-[#396817]"
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
>
{post.excerpt}
</p>
)}
{post.publishedAt && (
<p className="mt-auto text-[13px] font-bold text-[#f28b4a]">
{new Date(post.publishedAt).toLocaleDateString('uk-UA', {
day: 'numeric',
month: 'long',
year: 'numeric',
})}
</p>
)}
{post.title}
</h2>
{post.excerpt && (
<p
className="line-clamp-3 text-[14px] leading-relaxed text-[#272727]/60"
style={{ fontFamily: 'var(--font-poppins, Poppins), sans-serif' }}
>
{post.excerpt}
</p>
)}
<div className="mt-auto flex items-center gap-2 pt-2 text-[14px] font-semibold text-[#396817]">
Читати далі
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" className="transition-transform duration-200 group-hover:translate-x-1">
<path d="M3 8h10M9 4l4 4-4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
</div>
</Link>
))}
</div>

View file

@ -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,

View file

@ -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

View file

@ -46,14 +46,14 @@ async function getLatestPosts(limit = 3): Promise<Article[]> {
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 {

View file

@ -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 (
<svg width="16" height="16" viewBox="0 0 24 24" aria-hidden="true">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
</svg>
)
}
function VideoReviewCard({ pauseScroll }: { pauseScroll: () => void }) {
const [playing, setPlaying] = useState(false)
const videoRef = useRef<HTMLVideoElement>(null)
@ -57,9 +68,9 @@ function VideoReviewCard({ pauseScroll }: { pauseScroll: () => void }) {
}
return (
<div className="relative flex w-full flex-none flex-col overflow-hidden rounded-[20px] bg-[#396817] shadow-[0_4px_60px_0_rgba(242,139,74,0.25)] md:w-[280px]">
{/* 9:16 video area */}
<div className="relative aspect-[9/16] w-full overflow-hidden">
<div className="relative flex w-full flex-none flex-col overflow-hidden rounded-[24px] bg-[#2d5613] shadow-[0_8px_40px_0_rgba(57,104,23,0.25)] md:w-[320px]">
{/* Video thumbnail area — fixed height */}
<div className="relative h-[240px] w-full overflow-hidden">
<video
ref={videoRef}
src={VIDEO_REVIEW_SRC}
@ -76,28 +87,25 @@ function VideoReviewCard({ pauseScroll }: { pauseScroll: () => void }) {
aria-label="Відтворити відеовідгук"
>
<div className="flex h-16 w-16 items-center justify-center rounded-full bg-white/90 shadow-lg transition-transform hover:scale-110">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true"
className="ml-1"
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true" className="ml-1">
<path d="M5 3L19 12L5 21V3Z" fill="#396817" />
</svg>
</div>
</button>
)}
{/* Badge */}
<div className="absolute top-3 left-3 rounded-full bg-[#f28b4a] px-3 py-1 text-[12px] font-semibold text-white">
Відео
</div>
</div>
{/* Label */}
<div className="flex items-center justify-between px-5 py-3">
<p
className="text-[14px] font-medium text-white/80"
style={{ fontFamily: 'var(--font-inter, Inter), sans-serif' }}
>
Відеовідгук
</p>
{/* Footer */}
<div className="flex items-center justify-between px-5 py-4">
<div>
<p className="text-[15px] font-semibold text-white" style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}>
Відеовідгук
</p>
<p className="mt-0.5 text-[12px] text-white/50">Шуміленд 2026</p>
</div>
<StarRating value={5} size={14} className="flex gap-[2px]" />
</div>
</div>
@ -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="Попередній відгук"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path
d="M13 4L7 10L13 16"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path d="M13 4L7 10L13 16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
<div
ref={trackRef}
className="flex items-start gap-5 overflow-x-auto scroll-smooth pb-2"
className="flex items-stretch gap-5 overflow-x-auto scroll-smooth pb-2"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
onMouseEnter={() => setAutoPaused(true)}
onMouseLeave={() => setAutoPaused(false)}
>
{/* Video review card — first in each half */}
{/* Video card */}
<VideoReviewCard pauseScroll={pauseForVideo} />
{doubled.map((review, idx) => {
@ -169,61 +173,58 @@ export function Reviews({ data, title }: ReviewsProps) {
return (
<article
key={`${review.id}-${idx}`}
className="flex w-full flex-none flex-col gap-2.5 rounded-[20px] bg-[#396817] px-[39px] py-[41px] shadow-[0_4px_60px_0_rgba(242,139,74,0.25)] md:w-[591px]"
className="flex w-full flex-none flex-col rounded-[24px] bg-[#2d5613] shadow-[0_8px_40px_0_rgba(57,104,23,0.25)] md:w-[380px]"
>
<div className="flex flex-col gap-6">
<div className="flex items-center gap-[27px]">
<div className="relative h-[94px] w-[94px] flex-none overflow-hidden rounded-full">
<img
src={avatarUrl}
alt=""
aria-hidden="true"
className="absolute inset-0 h-full w-full object-cover"
/>
<div className="absolute inset-0 flex items-center justify-center bg-[#f28b4a]/70">
<span
className="text-[48px] leading-none font-medium text-white"
style={{ fontFamily: 'var(--font-inter, Inter), sans-serif' }}
>
{review.initial ?? review.name[0]}
</span>
</div>
</div>
<div className="flex flex-col gap-2.5">
<p
className="text-[24px] font-medium text-white"
style={{ fontFamily: 'var(--font-inter, Inter), sans-serif' }}
>
{review.name}
</p>
<div className="flex items-center gap-[22px]">
<span
className="text-[16px] font-medium text-white"
style={{ fontFamily: 'var(--font-inter, Inter), sans-serif' }}
>
{review.ago}
</span>
<StarRating
value={review.rating ?? 5}
size={16}
className="flex gap-[2px]"
/>
</div>
{/* Quote mark decoration */}
<div className="px-7 pt-6 pb-0">
<svg width="32" height="24" viewBox="0 0 32 24" fill="none" aria-hidden="true">
<path d="M0 24V14.4C0 10.4 0.933333 7.06667 2.8 4.4C4.66667 1.6 7.46667 0 11.2 0L12.8 2.4C10.1333 3.06667 8.13333 4.33333 6.8 6.2C5.6 8.06667 5 10.2667 5 12.8H9.6V24H0ZM19.2 24V14.4C19.2 10.4 20.1333 7.06667 22 4.4C23.8667 1.6 26.6667 0 30.4 0L32 2.4C29.3333 3.06667 27.3333 4.33333 26 6.2C24.8 8.06667 24.2 10.2667 24.2 12.8H28.8V24H19.2Z" fill="white" fillOpacity="0.15"/>
</svg>
</div>
{/* Review text */}
<p
className="flex-1 px-7 pt-3 pb-5 text-[15px] leading-[1.6] text-white/85"
style={{ fontFamily: 'var(--font-poppins, Poppins), sans-serif' }}
>
{review.text}
</p>
{/* Divider */}
<div className="mx-7 h-px bg-white/10" />
{/* Author row */}
<div className="flex items-center gap-4 px-7 py-5">
<div className="relative h-12 w-12 flex-none overflow-hidden rounded-full">
<img src={avatarUrl} alt="" aria-hidden="true" className="absolute inset-0 h-full w-full object-cover" />
<div className="absolute inset-0 flex items-center justify-center bg-[#f28b4a]/75">
<span className="text-[20px] font-semibold leading-none text-white" style={{ fontFamily: 'var(--font-inter, Inter), sans-serif' }}>
{review.initial ?? review.name[0]}
</span>
</div>
</div>
<p
className="line-clamp-2 text-[16px] leading-[1.5] font-medium text-white"
style={{ fontFamily: 'var(--font-inter, Inter), sans-serif' }}
>
{review.text}
</p>
<div className="flex-1 min-w-0">
<p className="truncate text-[15px] font-semibold text-white" style={{ fontFamily: 'var(--font-inter, Inter), sans-serif' }}>
{review.name}
</p>
<div className="mt-1 flex items-center gap-3">
<StarRating value={review.rating ?? 5} size={13} className="flex gap-[2px]" />
<span className="text-[12px] text-white/50" style={{ fontFamily: 'var(--font-inter, Inter), sans-serif' }}>
{review.ago}
</span>
</div>
</div>
{review.source === 'google' && (
<div className="flex-none opacity-60">
<GoogleIcon />
</div>
)}
</div>
</article>
)
})}
{/* Second video card at end for seamless loop */}
{/* Second video card for seamless loop */}
<VideoReviewCard pauseScroll={pauseForVideo} />
</div>
@ -233,13 +234,7 @@ export function Reviews({ data, title }: ReviewsProps) {
aria-label="Наступний відгук"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path
d="M7 4L13 10L7 16"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path d="M7 4L13 10L7 16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
</div>

View file

@ -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 (
<section className="flex w-full items-center justify-center bg-[#396817] py-10 lg:py-16">
<div className="mx-auto w-full max-w-[1200px] px-4">
<div className="flex justify-center">
<video
className="aspect-square w-full max-w-[720px] rounded-[20px] object-cover"
autoPlay
muted
loop
playsInline
preload="metadata"
poster={posterUrl}
>
<source src={DEFAULT_WEBM} type="video/webm" />
<source src={DEFAULT_MP4} type="video/mp4" />
</video>
</div>
</div>
<section className="w-full overflow-hidden bg-[#396817]">
<video
className="w-full object-cover"
style={{ display: 'block', maxHeight: '90vh' }}
autoPlay
muted
loop
playsInline
preload="metadata"
poster={posterUrl}
>
<source src={DEFAULT_WEBM} type="video/webm" />
<source src={DEFAULT_MP4} type="video/mp4" />
</video>
</section>
)
}