diff --git a/Dockerfile b/Dockerfile index 2f81041..8c4b6f4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,6 +26,7 @@ RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:nodejs /app/public ./public +RUN mkdir -p /app/media && chown nextjs:nodejs /app/media USER nextjs EXPOSE 3000 ENV PORT=3000 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 4eb163b..8866d17 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -24,6 +24,8 @@ services: depends_on: postgres: condition: service_healthy + volumes: + - media_uploads:/app/media restart: unless-stopped networks: - internal @@ -65,3 +67,5 @@ networks: volumes: postgres_data: name: shumiland-postgres-data + media_uploads: + name: shumiland-media-uploads diff --git a/public/videos/review-video.mp4 b/public/videos/review-video.mp4 new file mode 100644 index 0000000..6c9d68a Binary files /dev/null and b/public/videos/review-video.mp4 differ diff --git a/src/app/api/admin/seed/route.ts b/src/app/api/admin/seed/route.ts index 53d836b..5a9def5 100644 --- a/src/app/api/admin/seed/route.ts +++ b/src/app/api/admin/seed/route.ts @@ -478,7 +478,9 @@ export async function POST(req: NextRequest) { indent: 0, version: 1, direction: 'ltr', - children: [{ type: 'text', detail: 0, format: 0, mode: 'normal', style: '', text, version: 1 }], + children: [ + { type: 'text', detail: 0, format: 0, mode: 'normal', style: '', text, version: 1 }, + ], })), }, } @@ -492,9 +494,11 @@ export async function POST(req: NextRequest) { if (postCount === 0) { const postDefs = [ { - title: 'Сезон пригод відкрито: Shumiland на ВДНГ запрошує у світ, де оживають динозаври та казки!', + title: + 'Сезон пригод відкрито: Shumiland на ВДНГ запрошує у світ, де оживають динозаври та казки!', slug: 'sezon-pryhod-vidkryto', - excerpt: 'Сезон стартував із події національного масштабу — фіксації рекорду України: найбільші динозаври країни. 19 квітня у Shumiland представники Книги рекордів України офіційно зафіксували рекорд.', + excerpt: + 'Сезон стартував із події національного масштабу — фіксації рекорду України: найбільші динозаври країни. 19 квітня у Shumiland представники Книги рекордів України офіційно зафіксували рекорд.', status: 'published', publishedAt: '2026-04-19T10:00:00.000Z', imgFile: 'sezon-pryhod.webp', @@ -504,17 +508,18 @@ export async function POST(req: NextRequest) { 'Більше ніж парк: простір, де фантазія стає реальністю', 'Після яскравого старту в стилі «Аліси в Дивокраї», парк працює у повноцінному режимі, пропонуючи гостям унікальний формат відпочинку просто неба. На території ВДНГ розгорнувся масштабний всесвіт пригод, що поєднує освіту, розваги та естетику.', 'Динопарк — рекорд, який вражає: Локація в парку, де серед густої зелені «ожили» реалістичні динозаври натуральної величини, серед яких — найбільші динозаври України, офіційно зафіксовані як рекорд. Завдяки сучасним технологіям гіганти рухаються та гарчать, створюючи ефект повної присутності.', - 'Диво Ліс та Будиночок Лісовика: Казкова локація з унікальними топіарними фігурами та арт-об\'єктами. Тут розташована резиденція Лісовика, де діти можуть зануритися у світ легенд та інтерактивних історій.', + "Диво Ліс та Будиночок Лісовика: Казкова локація з унікальними топіарними фігурами та арт-об'єктами. Тут розташована резиденція Лісовика, де діти можуть зануритися у світ легенд та інтерактивних історій.", 'Дзеркальний лабіринт: Простір світла та ілюзій, що відкриває нові ракурси реальності та створює десятки ефектних фотолокацій.', 'Атракціони: У сезоні 2026 парк значно розширив зону розваг — на гостей чекає багато нових атракціонів для дітей різного віку.', - 'Shumiland — обов\'язкова точка для візиту на мапі Києва. Велика територія для активного відпочинку, продумана комфортна інфраструктура та оновлені фудкорти для всієї родини.', + "Shumiland — обов'язкова точка для візиту на мапі Києва. Велика територія для активного відпочинку, продумана комфортна інфраструктура та оновлені фудкорти для всієї родини.", '«Ми створили місце, де кожен візит перетворюється на родинне свято. Shumiland — це простір, де діти стають дослідниками, а дорослі дозволяють собі мріяти», — зазначає команда парку.', ]), }, { title: 'У Шуміленді заклали капсулу часу', slug: 'kapsula-chasu', - excerpt: 'У Шуміленді відбулася особлива подія — ми заклали капсулу часу як символ початку великого шляху, сповненого дитячого сміху, щирих емоцій, сімейних моментів і незабутніх вражень.', + excerpt: + 'У Шуміленді відбулася особлива подія — ми заклали капсулу часу як символ початку великого шляху, сповненого дитячого сміху, щирих емоцій, сімейних моментів і незабутніх вражень.', status: 'published', publishedAt: '2026-04-20T10:00:00.000Z', imgFile: 'kapsula-chasu.webp', @@ -529,7 +534,8 @@ export async function POST(req: NextRequest) { { title: 'Травень у Шуміленді — місяць пригод для всієї родини', slug: 'traven-u-shumilendt', - excerpt: 'Травень у Шуміленді обіцяє бути яскравим, веселим і сповненим незабутніх емоцій. Щовихідних на гостей чекають тематичні програми, квести, улюблені герої та атмосфера справжнього сімейного відпочинку.', + excerpt: + 'Травень у Шуміленді обіцяє бути яскравим, веселим і сповненим незабутніх емоцій. Щовихідних на гостей чекають тематичні програми, квести, улюблені герої та атмосфера справжнього сімейного відпочинку.', status: 'published', publishedAt: '2026-05-01T10:00:00.000Z', imgFile: 'traven-shymiland.webp', diff --git a/src/components/sections/Reviews.tsx b/src/components/sections/Reviews.tsx index 1364eea..fdf908b 100644 --- a/src/components/sections/Reviews.tsx +++ b/src/components/sections/Reviews.tsx @@ -7,6 +7,8 @@ import StarRating from '@/components/ui/StarRating' 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' function getMediaUrl(img: Media | string | null | undefined): string | null { if (!img) return null @@ -44,6 +46,64 @@ const STATIC_REVIEWS: ReviewCMS[] = [ }, ] +function VideoReviewCard({ pauseScroll }: { pauseScroll: () => void }) { + const [playing, setPlaying] = useState(false) + const videoRef = useRef(null) + + function handlePlay() { + pauseScroll() + setPlaying(true) + videoRef.current?.play() + } + + return ( +
+ {/* 9:16 video area */} +
+
+ {/* Label */} +
+

+ Відеовідгук +

+ +
+
+ ) +} + interface ReviewsProps { data?: ReviewCMS[] title?: string @@ -65,11 +125,15 @@ export function Reviews({ data, title }: ReviewsProps) { pauseTimer.current = setTimeout(() => setAutoPaused(false), 3000) } + function pauseForVideo() { + setAutoPaused(true) + } + return (

{title ?? 'Відгуки'} @@ -94,9 +158,12 @@ export function Reviews({ data, title }: ReviewsProps) {
+ {/* Video review card — first in each half */} + + {doubled.map((review, idx) => { const avatarUrl = getMediaUrl(review.avatarBg) ?? IMG_AVATAR_DEFAULT return ( @@ -106,7 +173,6 @@ export function Reviews({ data, title }: ReviewsProps) { >
- {/* Avatar: photo background + initial letter overlay */}
{review.ago} - +
@@ -156,6 +218,9 @@ export function Reviews({ data, title }: ReviewsProps) { ) })} + + {/* Second video card at end for seamless loop */} +