From 3226789bd1e85fa69a905234781e6d19ee308524 Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Sun, 10 May 2026 18:41:46 +0100 Subject: [PATCH] feat: refactor frontend components and add PageHero Refactored slider components (Gallery, Locations), Hero section with data-driven approach, WhyParents layout, and frontend page layouts. Added PageHero reusable component and ignored debug screenshots/playwright runtime in .gitignore. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 6 ++ next.config.ts | 7 ++ src/app/(frontend)/blog/page.tsx | 18 +--- src/app/(frontend)/dni-narodzhennia/page.tsx | 21 +--- .../(frontend)/grupovi-vidviduvannia/page.tsx | 21 +--- src/app/(frontend)/kvytky/page.tsx | 22 +--- src/app/(frontend)/layout.tsx | 1 + src/app/(frontend)/lokatsii/page.tsx | 21 +--- src/app/(frontend)/page.tsx | 3 +- src/app/(payload)/layout.tsx | 7 +- src/app/layout.tsx | 6 +- src/components/sections/Gallery.tsx | 21 ++-- src/components/sections/GallerySlider.tsx | 76 ++++++------- src/components/sections/Hero.test.tsx | 4 +- src/components/sections/Hero.tsx | 46 ++++---- src/components/sections/Locations.tsx | 14 +-- src/components/sections/LocationsSlider.tsx | 100 +++++++++--------- src/components/sections/WhyParents.tsx | 94 +++++++++------- src/components/ui/PageHero.tsx | 32 ++++++ 19 files changed, 263 insertions(+), 257 deletions(-) create mode 100644 src/components/ui/PageHero.tsx diff --git a/.gitignore b/.gitignore index 5306610..71edd68 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,9 @@ playwright-report/ # AI tooling runtime files agentdb.rvf agentdb.rvf.lock +.playwright-mcp/ + +# Debug screenshots (root-level) +*.png +*.jpg +*.jpeg diff --git a/next.config.ts b/next.config.ts index 9dd077a..de1ccf6 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,6 +4,13 @@ import type { NextConfig } from 'next' const nextConfig: NextConfig = { output: 'standalone', reactStrictMode: true, + webpack: (config) => { + config.watchOptions = { + ...config.watchOptions, + ignored: /node_modules|importMap\.js|\.next/, + } + return config + }, images: { remotePatterns: [ { diff --git a/src/app/(frontend)/blog/page.tsx b/src/app/(frontend)/blog/page.tsx index 6d3c460..bca0094 100644 --- a/src/app/(frontend)/blog/page.tsx +++ b/src/app/(frontend)/blog/page.tsx @@ -2,6 +2,7 @@ import type { Metadata } from 'next' import Link from 'next/link' import { getPayload } from 'payload' import configPromise from '@payload-config' +import { PageHero } from '@/components/ui/PageHero' export const metadata: Metadata = { title: 'Блог — Шуміленд', @@ -39,22 +40,7 @@ export default async function BlogPage() { return (
-
-
-

- Блог -

-

- Новини та статті від Шуміленду -

-
-
+
{posts.length === 0 ? ( diff --git a/src/app/(frontend)/dni-narodzhennia/page.tsx b/src/app/(frontend)/dni-narodzhennia/page.tsx index 810f667..053450a 100644 --- a/src/app/(frontend)/dni-narodzhennia/page.tsx +++ b/src/app/(frontend)/dni-narodzhennia/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' import Link from 'next/link' +import { PageHero } from '@/components/ui/PageHero' export const metadata: Metadata = { title: 'Дні народження — Шуміленд', @@ -30,22 +31,10 @@ const PACKAGES = [ export default function BirthdayPage() { return (
-
-
-

- Дні народження -

-

- Зробіть свято незабутнім! Оберіть пакет і наші менеджери зв'яжуться з вами для уточнення деталей. -

-
-
+
diff --git a/src/app/(frontend)/grupovi-vidviduvannia/page.tsx b/src/app/(frontend)/grupovi-vidviduvannia/page.tsx index 1fd8a93..b29315d 100644 --- a/src/app/(frontend)/grupovi-vidviduvannia/page.tsx +++ b/src/app/(frontend)/grupovi-vidviduvannia/page.tsx @@ -1,5 +1,6 @@ import type { Metadata } from 'next' import Link from 'next/link' +import { PageHero } from '@/components/ui/PageHero' export const metadata: Metadata = { title: 'Групові відвідування — Шуміленд', @@ -33,22 +34,10 @@ const GROUPS = [ export default function GroupVisitsPage() { return (
-
-
-

- Групові відвідування -

-

- Спеціальні умови для організованих груп. Мінімум 10 осіб — максимум вражень. -

-
-
+
diff --git a/src/app/(frontend)/kvytky/page.tsx b/src/app/(frontend)/kvytky/page.tsx index bd006f4..790113f 100644 --- a/src/app/(frontend)/kvytky/page.tsx +++ b/src/app/(frontend)/kvytky/page.tsx @@ -1,6 +1,7 @@ /* eslint-disable @next/next/no-img-element */ import type { Metadata } from 'next' import Link from 'next/link' +import { PageHero } from '@/components/ui/PageHero' export const metadata: Metadata = { title: 'Купити квиток — Шуміленд', @@ -53,23 +54,10 @@ export default async function TicketsPage() { return (
- {/* Hero */} -
-
-

- Купити квиток -

-

- Оберіть квиток та придбайте онлайн — без черги на касі -

-
-
+
{hasWarning && ( diff --git a/src/app/(frontend)/layout.tsx b/src/app/(frontend)/layout.tsx index e60a141..5c47926 100644 --- a/src/app/(frontend)/layout.tsx +++ b/src/app/(frontend)/layout.tsx @@ -1,6 +1,7 @@ import React from 'react' import type { Metadata } from 'next' import { Montserrat, Inter, Poppins } from 'next/font/google' +import '@/app/globals.css' import { Header } from '@/components/layout/Header' import { Footer } from '@/components/layout/Footer' diff --git a/src/app/(frontend)/lokatsii/page.tsx b/src/app/(frontend)/lokatsii/page.tsx index 7d31b5b..4a62f3e 100644 --- a/src/app/(frontend)/lokatsii/page.tsx +++ b/src/app/(frontend)/lokatsii/page.tsx @@ -1,6 +1,7 @@ /* eslint-disable @next/next/no-img-element */ import type { Metadata } from 'next' import Link from 'next/link' +import { PageHero } from '@/components/ui/PageHero' export const metadata: Metadata = { title: 'Локації — Шуміленд', @@ -36,22 +37,10 @@ const LOCATIONS = [ export default function LocationsPage() { return (
-
-
-

- Локації -

-

- Три неповторні зони розваг для всієї родини -

-
-
+
diff --git a/src/app/(frontend)/page.tsx b/src/app/(frontend)/page.tsx index 7f68bcc..6229f75 100644 --- a/src/app/(frontend)/page.tsx +++ b/src/app/(frontend)/page.tsx @@ -27,7 +27,8 @@ export default async function HomePage() { // Fail gracefully — DB may not be available during static build } - const hero = home?.hero ?? STATIC_HERO + const heroData = home?.hero + const hero = heroData?.title ? heroData : STATIC_HERO return (
diff --git a/src/app/(payload)/layout.tsx b/src/app/(payload)/layout.tsx index f353bd8..49934ba 100644 --- a/src/app/(payload)/layout.tsx +++ b/src/app/(payload)/layout.tsx @@ -1,3 +1,4 @@ +import '@payloadcms/next/css' import React from 'react' import { RootLayout, handleServerFunctions } from '@payloadcms/next/layouts' import type { ServerFunctionClient } from 'payload' @@ -19,11 +20,7 @@ const serverFunction: ServerFunctionClient = async function (args) { export default function PayloadLayout({ children }: { children: React.ReactNode }) { return ( - + {children} ) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f1a0385..8c5c674 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,5 @@ import React from 'react' import type { Metadata } from 'next' -import './globals.css' export const metadata: Metadata = { title: 'Shumiland', @@ -8,7 +7,10 @@ export const metadata: Metadata = { export default function RootLayout({ children }: { children: React.ReactNode }) { return ( - + + + + {children} ) diff --git a/src/components/sections/Gallery.tsx b/src/components/sections/Gallery.tsx index 7556c91..752b4d5 100644 --- a/src/components/sections/Gallery.tsx +++ b/src/components/sections/Gallery.tsx @@ -2,15 +2,16 @@ import { GallerySlider } from './GallerySlider' import type { GalleryImage } from './GallerySlider' const IMAGES: GalleryImage[] = [ - { src: '/images/figma/gallery-2.png', alt: 'Шуміленд — фото 1' }, - { src: '/images/figma/gallery-4.png', alt: 'Шуміленд — фото 2' }, - { src: '/images/figma/gallery-6.png', alt: 'Шуміленд — фото 3' }, - { src: '/images/figma/gallery-7.png', alt: 'Шуміленд — фото 4' }, - { src: '/images/figma/loc-map.jpg', alt: 'Шуміленд — фото 5', width: 592, height: 427, radius: 30 }, - { src: '/images/figma/gallery-8.png', alt: 'Шуміленд — фото 6' }, - { src: '/images/figma/hero-bg-family.png', alt: 'Шуміленд — фото 7' }, - { src: '/images/figma/why-parents-1.png', alt: 'Шуміленд — фото 8' }, - { src: '/images/figma/video-preview.png', alt: 'Шуміленд — фото 9' }, + { src: '/images/figma/why-parents-1.png', alt: 'Шуміленд — сімейні враження', width: 320, height: 420, radius: 20 }, + { src: '/images/figma/gallery-6.png', alt: 'Шуміленд — атракціони', width: 380, height: 420, radius: 20 }, + { src: '/images/figma/gallery-7.png', alt: 'Шуміленд — фото 3', width: 320, height: 420, radius: 20 }, + { src: '/images/figma/why-parents-2.png', alt: 'Шуміленд — прогулянка', width: 380, height: 420, radius: 20 }, + { src: '/images/figma/news-bg2.png', alt: 'Шуміленд — зони парку', width: 320, height: 420, radius: 20 }, + { src: '/images/figma/gallery-4.png', alt: 'Шуміленд — фото 5', width: 380, height: 420, radius: 20 }, + { src: '/images/figma/why-parents-3.png', alt: 'Шуміленд — відпочинок', width: 320, height: 420, radius: 20 }, + { src: '/images/figma/gallery-2.png', alt: 'Шуміленд — фото 7', width: 380, height: 420, radius: 20 }, + { src: '/images/figma/news-bg6.png', alt: 'Шуміленд — Шумі Кафе', width: 320, height: 420, radius: 20 }, + { src: '/images/figma/why-parents-4.png', alt: 'Шуміленд — карта парку', width: 380, height: 420, radius: 20 }, ] export function Gallery() { @@ -26,7 +27,7 @@ export function Gallery() {
- +
) diff --git a/src/components/sections/GallerySlider.tsx b/src/components/sections/GallerySlider.tsx index 7069625..ee4cf66 100644 --- a/src/components/sections/GallerySlider.tsx +++ b/src/components/sections/GallerySlider.tsx @@ -1,8 +1,7 @@ 'use client' /* eslint-disable @next/next/no-img-element */ -import { useRef } from 'react' -import { useAutoScroll } from '@/hooks/useAutoScroll' +import { useState } from 'react' export interface GalleryImage { src: string @@ -14,62 +13,65 @@ export interface GalleryImage { interface GallerySliderProps { images: GalleryImage[] + speed?: number } -export function GallerySlider({ images }: GallerySliderProps) { - const trackRef = useRef(null) - useAutoScroll(trackRef, { speed: 0.5, intervalMs: 16 }) +export function GallerySlider({ images, speed = 35 }: GallerySliderProps) { + const [paused, setPaused] = useState(false) - function scrollByOne(dir: 1 | -1) { - trackRef.current?.scrollBy({ left: dir * 710, behavior: 'smooth' }) - } + // Duplicate for seamless infinite loop + const doubled = [...images, ...images] return ( -
- +
setPaused(true)} + onMouseLeave={() => setPaused(false)} + > +
- {images.map((img, i) => { - const w = img.width ?? 684 - const h = img.height ?? 494 - const r = img.radius ?? 35 + {doubled.map((img, i) => { + const w = img.width ?? 384 + const h = img.height ?? 280 + const r = img.radius ?? 20 return (
{img.alt}
) })}
- -
) } diff --git a/src/components/sections/Hero.test.tsx b/src/components/sections/Hero.test.tsx index fb64a69..8256c47 100644 --- a/src/components/sections/Hero.test.tsx +++ b/src/components/sections/Hero.test.tsx @@ -60,7 +60,9 @@ describe('Hero', () => { it('renders title when provided', () => { const hero: HomePageHero = { title: 'Welcome to Shumiland' } render() - expect(screen.getByRole('heading', { level: 1, name: 'Welcome to Shumiland' })).toBeInTheDocument() + expect( + screen.getByRole('heading', { level: 1, name: 'Welcome to Shumiland' }) + ).toBeInTheDocument() }) it('does not render a heading when title is not provided', () => { diff --git a/src/components/sections/Hero.tsx b/src/components/sections/Hero.tsx index 9dd10f8..fbdaf46 100644 --- a/src/components/sections/Hero.tsx +++ b/src/components/sections/Hero.tsx @@ -3,7 +3,6 @@ import type { HomePageHero, Media } from '@/types/globals' import { BtnPrimary } from '@/components/ui/BtnPrimary' const IMG_BG2 = '/images/figma/hero-bg2.png' -const IMG_BG1 = '/images/figma/hero-bg1.png' const IMG_FAMILY = '/images/figma/hero-bg-family.png' interface HeroProps { @@ -27,7 +26,7 @@ export function Hero({ hero }: HeroProps) { return (
{/* Background layers */} @@ -39,19 +38,28 @@ export function Hero({ hero }: HeroProps) { muted playsInline aria-hidden="true" - className="absolute inset-0 w-full h-full object-cover pointer-events-none" + className="pointer-events-none absolute inset-0 h-full w-full object-cover" /> ) : bgMedia ? ( {bgMedia.alt ) : ( <> - - - + + )} @@ -59,14 +67,14 @@ export function Hero({ hero }: HeroProps) { The mobile layout is always in the DOM; CSS hides it visually on desktop. */}