From 4cc6586366623b8a8a6dfa6778dca7cd17361db9 Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Thu, 28 May 2026 14:17:12 +0100 Subject: [PATCH] fix(gallery): replace CSS scroll with 3D coverflow slider everywhere; fix migration SEO columns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GallerySlider: rewrite to 3D coverflow (rotateY perspective, 3.5s auto-rotate, dots + arrows, lightbox on center click) — matches DyvoLisGallery style - migration 0006: add meta_title/description/image_id columns for seoPlugin Co-Authored-By: Claude Sonnet 4.6 --- migrations/0006_dinosaur_page.sql | 6 + src/components/sections/GallerySlider.tsx | 260 ++++++++++------------ 2 files changed, 127 insertions(+), 139 deletions(-) diff --git a/migrations/0006_dinosaur_page.sql b/migrations/0006_dinosaur_page.sql index e9bb9e7..fdaca92 100644 --- a/migrations/0006_dinosaur_page.sql +++ b/migrations/0006_dinosaur_page.sql @@ -108,3 +108,9 @@ VALUES ( true, true, false, 20 ) ON CONFLICT (slug) DO NOTHING; + +-- SEO plugin meta columns (added by seoPlugin for globals) +ALTER TABLE "dinosaur_page" ADD COLUMN IF NOT EXISTS "meta_title" varchar; +ALTER TABLE "dinosaur_page" ADD COLUMN IF NOT EXISTS "meta_description" varchar; +ALTER TABLE "dinosaur_page" ADD COLUMN IF NOT EXISTS "meta_image_id" integer REFERENCES "media"("id") ON DELETE SET NULL; +CREATE INDEX IF NOT EXISTS "dinosaur_page_meta_meta_image_idx" ON "dinosaur_page" ("meta_image_id"); diff --git a/src/components/sections/GallerySlider.tsx b/src/components/sections/GallerySlider.tsx index 064be6e..4af5f34 100644 --- a/src/components/sections/GallerySlider.tsx +++ b/src/components/sections/GallerySlider.tsx @@ -1,7 +1,7 @@ 'use client' /* eslint-disable @next/next/no-img-element */ -import { useState, useRef, useEffect } from 'react' +import { useState, useEffect } from 'react' import { ImageLightbox } from '@/components/ui/ImageLightbox' export interface GalleryImage { @@ -14,165 +14,147 @@ export interface GalleryImage { interface GallerySliderProps { images: GalleryImage[] - /** Pixels per second. Default 50. */ + /** @deprecated kept for API compatibility */ speed?: number } -export function GallerySlider({ images, speed = 50 }: GallerySliderProps) { - const scrollRef = useRef(null) - const pausedRef = useRef(false) - const rafRef = useRef(null) - const pauseTimer = useRef | null>(null) +export function GallerySlider({ images }: GallerySliderProps) { + const n = images.length + const [active, setActive] = useState(0) const [lightboxIndex, setLightboxIndex] = useState(null) - const doubled = [...images, ...images] - useEffect(() => { - const el = scrollRef.current - if (!el) return - if ( - typeof window !== 'undefined' && - window.matchMedia?.('(prefers-reduced-motion: reduce)').matches - ) - return + if (n <= 1) return + const t = setInterval(() => setActive((p) => (p + 1) % n), 3500) + return () => clearInterval(t) + }, [n]) - let last = performance.now() - const tick = (now: number) => { - const dt = now - last - last = now - if (!pausedRef.current) { - const half = el.scrollWidth / 2 - if (half > 0) { - el.scrollLeft += (dt / 1000) * speed - if (el.scrollLeft >= half) el.scrollLeft -= half - } - } - rafRef.current = requestAnimationFrame(tick) + if (n === 0) return null + + function getOffset(i: number): number { + const d = (((i - active) % n) + n) % n + return d > n / 2 ? d - n : d + } + + function handleSlideClick(i: number, d: number) { + if (d === 0) { + setLightboxIndex(i) + } else { + setActive(i) } - rafRef.current = requestAnimationFrame(tick) - return () => { - if (rafRef.current) cancelAnimationFrame(rafRef.current) - } - }, [speed]) - - function scrollByCard(dir: 1 | -1) { - const cardWidth = 380 + 26 - scrollRef.current?.scrollBy({ left: dir * cardWidth, behavior: 'smooth' }) - pausedRef.current = true - if (pauseTimer.current) clearTimeout(pauseTimer.current) - pauseTimer.current = setTimeout(() => { - pausedRef.current = false - }, 3000) - } - - function openLightbox(idx: number) { - setLightboxIndex(idx % images.length) - pausedRef.current = true - } - - function closeLightbox() { - setLightboxIndex(null) - pausedRef.current = false - } - - function prevImage() { - setLightboxIndex((i) => (i === null ? 0 : (i - 1 + images.length) % images.length)) - } - - function nextImage() { - setLightboxIndex((i) => (i === null ? 0 : (i + 1) % images.length)) } return ( <> -
- + {/* 3D Coverflow */} +
+
+ {images.map((img, i) => { + const d = getOffset(i) + const absD = Math.abs(d) + const sign = d >= 0 ? 1 : -1 -
{ - pausedRef.current = true - }} - onMouseLeave={() => { - pausedRef.current = false - }} - > -
- {doubled.map((img, i) => { - const w = img.width ?? 384 - const h = img.height ?? 280 - const r = img.radius ?? 20 - return ( -
openLightbox(i)} - role="button" - aria-label={`Відкрити фото: ${img.alt}`} - tabIndex={0} - onKeyDown={(e) => e.key === 'Enter' && openLightbox(i)} - > - {img.alt} -
- ) - })} -
+ const cfg = + absD === 0 + ? { tx: 0, ry: 0, scale: 1, opacity: 1, z: 20 } + : absD === 1 + ? { tx: sign * 270, ry: -sign * 48, scale: 0.82, opacity: 0.62, z: 10 } + : { tx: sign * 440, ry: -sign * 62, scale: 0.6, opacity: 0.18, z: 1 } + + return ( +
handleSlideClick(i, d)} + onKeyDown={(e) => e.key === 'Enter' && handleSlideClick(i, d)} + style={{ + position: 'absolute', + top: '50%', + left: '50%', + width: 'clamp(200px, 30vw, 380px)', + aspectRatio: '4/3', + transform: `translate(calc(-50% + ${cfg.tx}px), -50%) rotateY(${cfg.ry}deg) scale(${cfg.scale})`, + opacity: cfg.opacity, + zIndex: cfg.z, + transition: 'transform 0.65s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.65s ease', + borderRadius: `${img.radius ?? 16}px`, + overflow: 'hidden', + cursor: 'pointer', + boxShadow: + d === 0 ? '0 20px 60px rgba(57,104,23,0.28)' : '0 8px 24px rgba(0,0,0,0.14)', + }} + > + {d +
+ ) + })}
- + {/* Navigation */} +
+ + +
+ {images.map((_, i) => ( +
+ + +
{lightboxIndex !== null && ( setLightboxIndex(null)} + onPrev={() => setLightboxIndex((i) => (i === null ? 0 : (i - 1 + n) % n))} + onNext={() => setLightboxIndex((i) => (i === null ? 0 : (i + 1) % n))} /> )}