diff --git a/_generateGuide.mjs b/_generateGuide.mjs new file mode 100644 index 0000000..afeb1c5 --- /dev/null +++ b/_generateGuide.mjs @@ -0,0 +1,23 @@ +import { chromium } from './node_modules/.pnpm/playwright-core@1.60.0/node_modules/playwright-core/index.js' +import { readFileSync, writeFileSync, mkdirSync } from 'fs' + +const guideHtml = readFileSync('/tmp/shumiland_guide.html', 'utf8') +mkdirSync('docs', { recursive: true }) + +let browser +try { + browser = await chromium.launch({ + executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, + }) + const page = await browser.newPage() + await page.setContent(guideHtml, { waitUntil: 'networkidle' }) + await page.pdf({ + path: 'docs/shumiland-admin-guide.pdf', + format: 'A4', + margin: { top: '20mm', right: '15mm', bottom: '20mm', left: '15mm' }, + printBackground: true, + }) + console.log('PDF generated: docs/shumiland-admin-guide.pdf') +} finally { + await browser?.close() +} diff --git a/docs/shumiland-admin-guide.pdf b/docs/shumiland-admin-guide.pdf index 849d572..85ff759 100644 Binary files a/docs/shumiland-admin-guide.pdf and b/docs/shumiland-admin-guide.pdf differ diff --git a/src/components/sections/DinoPageContent.tsx b/src/components/sections/DinoPageContent.tsx index 208118a..e161264 100644 --- a/src/components/sections/DinoPageContent.tsx +++ b/src/components/sections/DinoPageContent.tsx @@ -1,8 +1,9 @@ 'use client' /* eslint-disable @next/next/no-img-element */ -import { useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useState } from 'react' import { useCart } from '@/context/CartContext' +import { ImageLightbox } from '@/components/ui/ImageLightbox' // ─── Types ──────────────────────────────────────────────────────────────────── @@ -515,7 +516,7 @@ const DYNO_CSS = ` .dino-page .hero-text h1{font-size:52px;} } @media(max-width:820px){ - .dino-page .hero-dino{width:min(620px,90vw);right:-40px;top:0;opacity:.85;} + .dino-page .hero-dino{width:min(520px,80vw);height:auto;max-height:75vh;right:-20px;top:0;opacity:.8;} .dino-page .hero-text{padding-top:40px;} .dino-page .hero-text h1{font-size:40px;} .dino-page .hero-text p{font-size:18px;} @@ -526,15 +527,21 @@ const DYNO_CSS = ` } @media(max-width:560px){ .dino-page{--gutter:18px;} + /* Hero dino: smaller, clipped by hero overflow:hidden */ + .dino-page .hero-dino{width:min(320px,70vw);height:auto;max-height:60vh;right:-10px;top:20px;opacity:.7;} .dino-page .hero-text h1{font-size:32px;} + .dino-page .hero-text p{font-size:16px;} .dino-page .tip{font-size:18px;padding:18px 24px;} .dino-page .tip-badge{width:120px;height:120px;font-size:60px;border-width:10px;} .dino-page .tip-main .tip{padding-left:60px;} .dino-page .section-head h2,.dino-page .price-title,.dino-page .time-bar h2{font-size:24px;} .dino-page .gallery{grid-template-columns:1fr 1fr;gap:10px;} + .dino-page .gallery .exhibit{cursor:pointer;} .dino-page .ticket .t-price{font-size:48px;} - .dino-page .dino-wheel{height:400px;} + .dino-page .dino-wheel{height:400px;overflow:hidden;} .dino-page .wheel-info h3{font-size:24px;} + /* Prevent SVG wheel from creating horizontal scroll */ + .dino-page .slider-section{overflow:hidden;} } ` @@ -567,6 +574,22 @@ export function DinoPageContent({ () => (galleryImages && galleryImages.length >= 4 ? galleryImages : FB_GALLERY), [galleryImages] ) + // Lightbox state for gallery + const [lightboxIdx, setLightboxIdx] = useState(null) + const lbImages = galleryList.slice(0, 4).map((src, i) => ({ + src, + alt: FB_GALLERY_ALTS[i] ?? `Динопарк — фото ${i + 1}`, + })) + const openLightbox = useCallback((i: number) => setLightboxIdx(i), []) + const closeLightbox = useCallback(() => setLightboxIdx(null), []) + const prevLb = useCallback( + () => setLightboxIdx((p) => (p != null ? (p - 1 + lbImages.length) % lbImages.length : 0)), + [lbImages.length] + ) + const nextLb = useCallback( + () => setLightboxIdx((p) => (p != null ? (p + 1) % lbImages.length : 0)), + [lbImages.length] + ) const activityList = useMemo( () => (activities && activities.length > 0 ? activities : FB_ACTIVITIES), [activities] @@ -951,17 +974,31 @@ export function DinoPageContent({
- {galleryList.slice(0, 4).map((src, i) => ( -
- {FB_GALLERY_ALTS[i] + {lbImages.map((photo, i) => ( +
openLightbox(i)} + onKeyDown={(e) => e.key === 'Enter' && openLightbox(i)} + > + {photo.alt}
))}
+ {lightboxIdx !== null && ( + + )} {/* ── ATTRACTIONS ── */} diff --git a/src/components/sections/LocationsSlider.tsx b/src/components/sections/LocationsSlider.tsx index 155a2e0..ef26f06 100644 --- a/src/components/sections/LocationsSlider.tsx +++ b/src/components/sections/LocationsSlider.tsx @@ -18,22 +18,22 @@ export function LocationsSlider({ locations }: { locations: LocationData[] }) { src: loc.image, alt: loc.name, overlay: ( -
-
+
+

{loc.name}

{loc.tagline}

{loc.description} diff --git a/src/components/ui/CoverflowSlider.tsx b/src/components/ui/CoverflowSlider.tsx index c3dcc1e..8f9f03b 100644 --- a/src/components/ui/CoverflowSlider.tsx +++ b/src/components/ui/CoverflowSlider.tsx @@ -12,9 +12,9 @@ export interface CoverflowSlide { interface CoverflowSliderProps { slides: CoverflowSlide[] - /** px width of each card (default 420) */ + /** px width of each card at full desktop size (default 420) */ cardWidth?: number - /** px height of each card (default 300) */ + /** px height of each card at full desktop size (default 300) */ cardHeight?: number /** Border radius of each card (default 20) */ radius?: number @@ -23,7 +23,6 @@ interface CoverflowSliderProps { autoplay?: number } -const SLOT = 230 // px between card centers const ANGLE = 42 // deg rotation per slot const SCALE_STEP = 0.14 const OPACITY_STEP = 0.22 @@ -39,12 +38,36 @@ export function CoverflowSlider({ }: CoverflowSliderProps) { const [active, setActive] = useState(0) const autoTimer = useRef | null>(null) + const containerRef = useRef(null) + + // Responsive: scale card + slot to fit the container + const [effectiveCard, setEffectiveCard] = useState({ w: cardWidth, h: cardHeight }) + + useEffect(() => { + function measure() { + const vw = containerRef.current?.offsetWidth ?? window.innerWidth + if (vw < cardWidth * 0.9) { + // On small screens cap card width at ~85% of viewport + const ratio = (vw * 0.85) / cardWidth + setEffectiveCard({ w: Math.round(cardWidth * ratio), h: Math.round(cardHeight * ratio) }) + } else { + setEffectiveCard({ w: cardWidth, h: cardHeight }) + } + } + measure() + window.addEventListener('resize', measure) + return () => window.removeEventListener('resize', measure) + }, [cardWidth, cardHeight]) + + const cw = effectiveCard.w + const ch = effectiveCard.h + // Slot scales with card size so side cards don't overflow + const SLOT = Math.round(cw * 0.72) const n = slides.length const prev = useCallback(() => setActive((a) => (a - 1 + n) % n), [n]) const next = useCallback(() => setActive((a) => (a + 1) % n), [n]) - // Keyboard navigation useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === 'ArrowLeft') prev() @@ -54,7 +77,6 @@ export function CoverflowSlider({ return () => window.removeEventListener('keydown', onKey) }, [prev, next]) - // Autoplay useEffect(() => { if (!autoplay) return autoTimer.current = setInterval(next, autoplay) @@ -63,7 +85,6 @@ export function CoverflowSlider({ } }, [autoplay, next]) - // Touch/drag support const dragStart = useRef(null) const onTouchStart = (e: React.TouchEvent) => { dragStart.current = e.touches[0]?.clientX ?? null @@ -75,20 +96,19 @@ export function CoverflowSlider({ dragStart.current = null } - const containerHeight = Math.round(cardHeight * 1.25) + const containerHeight = Math.round(ch * 1.25) return ( -

- {/* 3D stage */} +
+ {/* 3D stage — overflow-hidden prevents side cards from creating scrollbar */}
{slides.map((slide, i) => { const offset = i - active - // Wrap-around distance (shortest path) const wrapped = offset > n / 2 ? offset - n : offset < -n / 2 ? offset + n : offset const abs = Math.abs(wrapped) @@ -110,8 +130,8 @@ export function CoverflowSlider({ onKeyDown={(e) => e.key === 'Enter' && setActive(i)} className="absolute cursor-pointer overflow-hidden" style={{ - width: cardWidth, - height: cardHeight, + width: cw, + height: ch, borderRadius: radius, left: '50%', top: '50%', @@ -136,9 +156,8 @@ export function CoverflowSlider({ })}
- {/* Controls row */} + {/* Controls */}
- {/* Prev button */} - {/* Dots */}
{slides.map((_, i) => (