'use client'; import React, { useState, useEffect, useRef, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { gsap } from 'gsap'; import { getMenuPosterCandidates, getMenuPosterSrc, getMenuVideoSources, applyNextImageFallback, } from '@/lib/assets'; import { menuCategories, allMenuItems, type MenuItem, type MenuCategory } from '@/lib/menu-data'; import { useLanguage } from '@/lib/language-context'; import { getTranslation } from '@/lib/translations'; interface ScreenDisplayProps { screen: 1 | 2; } const SCREEN1_CATEGORY_IDS = ['street-food', 'vegetarian', 'meat', 'chicken', 'burger-sandwich']; const SCREEN2_CATEGORY_IDS = ['pizza', 'naan-roll', 'sweets', 'drinks']; function SpiceParticles() { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d', { alpha: true }); if (!ctx) return; let width = window.innerWidth; let height = window.innerHeight; canvas.width = width; canvas.height = height; const particles = Array.from({ length: 42 }, () => ({ x: Math.random() * width, y: Math.random() * height * 0.9, vx: (Math.random() - 0.5) * 0.22, vy: -0.12 - Math.random() * 0.22, size: 1.1 + Math.random() * 1.9, alpha: 0.12 + Math.random() * 0.22, phase: Math.random() * Math.PI * 2, speed: 0.018 + Math.random() * 0.012, })); const onResize = () => { width = window.innerWidth; height = window.innerHeight; canvas.width = width; canvas.height = height; }; window.addEventListener('resize', onResize); let rafId: number; const draw = () => { ctx.clearRect(0, 0, width, height); for (let i = 0; i < particles.length; i++) { const p = particles[i]; p.x += p.vx + Math.sin(p.phase) * 0.07; p.y += p.vy; p.phase += p.speed; if (p.y < -10) { p.y = height * 0.92 + Math.random() * 40; p.x = Math.random() * width; } if (p.x < 0) p.x = width; if (p.x > width) p.x = 0; ctx.fillStyle = `hsla(38, 68%, 52%, ${p.alpha})`; ctx.beginPath(); ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); ctx.fill(); // occasional tiny sparkle if (Math.random() < 0.014) { ctx.fillStyle = `hsla(42, 90%, 78%, ${p.alpha * 0.9})`; ctx.fillRect(p.x - 0.6, p.y - 0.6, 1.2, 1.2); } } rafId = requestAnimationFrame(draw); }; draw(); return () => { cancelAnimationFrame(rafId); window.removeEventListener('resize', onResize); }; }, []); return ( ); } function MenuCard({ item, isHighlighted, t }: { item: MenuItem; isHighlighted?: boolean; t: any }) { const poster = getMenuPosterSrc(item, 'optimized'); const desc = (t as any).menuDescriptions?.[item.id] || item.description || ''; return (
{ applyNextImageFallback( e.currentTarget, getMenuPosterCandidates(item), e.currentTarget.src ); }} />
{/* Gold price pill */}
{item.price} KR
{item.isVegetarian && (
VEG
)}
{item.name}

{desc}

); } export default function ScreenDisplay({ screen }: ScreenDisplayProps) { const { language } = useLanguage(); const t = getTranslation(language); const screenLabel = screen === 1 ? t.screen.display1 : t.screen.display2; const isScreen1 = screen === 1; // Filtered data for this screen const screenCategories = useMemo(() => { const ids = isScreen1 ? SCREEN1_CATEGORY_IDS : SCREEN2_CATEGORY_IDS; return menuCategories.filter((c) => ids.includes(c.id)); }, [isScreen1]); // Cycle through ALL items for this screen, one by one in the large hero (not just signatures) const cyclingItems = useMemo(() => { return screenCategories.flatMap((category) => category.items); }, [screenCategories]); // Cycling through all menu items (large top animation) const [currentIndex, setCurrentIndex] = useState(0); const current = cyclingItems[currentIndex] || cyclingItems[0]; const heroTiltRef = useRef(null); // Auto cycle through every item (large top hero shows all, one by one) const CYCLE_INTERVAL_MS = 8200; useEffect(() => { const id = setInterval(() => { setCurrentIndex((i) => (i + 1) % cyclingItems.length); }, CYCLE_INTERVAL_MS); return () => clearInterval(id); }, [cyclingItems.length]); // Beautiful 3D tilt on hero (like signature cards on homepage) useEffect(() => { const el = heroTiltRef.current; if (!el) return; const onMouseMove = (e: MouseEvent) => { const rect = el.getBoundingClientRect(); const x = ((e.clientX - rect.left) / rect.width - 0.5) * 2; const y = ((e.clientY - rect.top) / rect.height - 0.5) * 2; gsap.to(el, { rotationY: x * 7, rotationX: -y * 5.5, transformPerspective: 1400, duration: 0.36, ease: 'power2.out', overwrite: true, }); }; const onMouseLeave = () => { gsap.to(el, { rotationY: 0, rotationX: 0, duration: 1.35, ease: 'elastic.out(1, 0.48)', }); }; el.addEventListener('mousemove', onMouseMove); el.addEventListener('mouseleave', onMouseLeave); return () => { el.removeEventListener('mousemove', onMouseMove); el.removeEventListener('mouseleave', onMouseLeave); }; }, []); // Professional kiosk mode for screen1/screen2: // Force clean fullscreen-like window (no side scrollbars) even in F11 or kiosk browser. // Applies to html/body so the window itself has no scrollbar. // User zooms the browser in/out to change reel density (# visible items) instead of scrolling. // Cleanup restores previous state (good if ever navigating away in dev). useEffect(() => { const htmlEl = document.documentElement; const bodyEl = document.body; const prevHtmlClass = htmlEl.className; const prevBodyClass = bodyEl.className; const prevHtmlOverflow = htmlEl.style.overflow; const prevBodyOverflow = bodyEl.style.overflow; htmlEl.classList.add('screen-kiosk'); bodyEl.classList.add('screen-kiosk'); htmlEl.style.overflow = 'hidden'; bodyEl.style.overflow = 'hidden'; return () => { // restore htmlEl.style.overflow = prevHtmlOverflow; bodyEl.style.overflow = prevBodyOverflow; // remove only if we added (simple: toggle off) htmlEl.classList.remove('screen-kiosk'); bodyEl.classList.remove('screen-kiosk'); }; }, []); const translatedDesc = current ? ((t as any).menuDescriptions?.[current.id] || current.description) : ''; // ===================================================== // CONTINUOUS MENU REEL (one line, loop like a film reel) - items move LEFT // Enter from RIGHT, exit LEFT. Number of visible items is dynamic (more when zoom out, fewer when zoom in). // The reel viewport is always full width (edge to edge, no max-w), no side whitespace. // Posters only (no videos) for seamless movement. Numbers for easy ordering (screen1:1-26, screen2:27+). // CSS marquee for stable seamless poster reel. // Card sizes fixed px for consistent distance viewing; # visible auto-adjusts to current container width on zoom. // Sized large for 43" TV distance viewing in restaurant. // ===================================================== const allMenuItemsForReel = screenCategories.flatMap((c) => c.items); const numItems = allMenuItemsForReel.length; const startNumber = isScreen1 ? 1 : 27; // Duplicate for seamless infinite loop (scroll 50% to repeat perfectly) const reelItems = [...allMenuItemsForReel, ...allMenuItemsForReel]; // Card width tuned for 43" screen (~1920px), 3 visible + gaps ~1872px at default zoom. // Poster 320px + bottom ~70px (py-5 + one-line name+right price) so cards fill the ~390px reel band vertically. const REEL_CARD_WIDTH = 620; // px const REEL_CARD_GAP = 16; // px (Tailwind gap-4) // ReelCard: large poster + number+name on left, price on right (opposite side, same line) so price is always clearly visible as cards move left in the reel. Sized big for 43" TV distance viewing. function ReelCard({ item, number }: { item: MenuItem; number: number }) { const numStr = String(number).padStart(2, '0'); return (
{/* Prominent number badge for easy ordering by number e.g. "give me number 13" */}
{numStr}
{numStr}. {item.name}
{item.price} kr
); } return (
{/* Elegant top bar */}
SHAHI KITCHEN
{screenLabel}
ASKIM • BACKAPLAN
{t.screen.prepared.toUpperCase()}
{/* HERO — Large cycling animation showing ALL items one by one (not just signatures). flex-1 so it absorbs extra viewport height on tall displays / when zoomed out. On short effective viewports (zoomed in) it shrinks gracefully; text sized for fit. */}
{/* Media switches with crossfade + fresh start each cycle. Video when available, else large animated poster (Ken Burns). */} {current && ( current.video ? ( ) : ( { applyNextImageFallback( e.currentTarget, getMenuPosterCandidates(current), e.currentTarget.src ); }} /> ) )} {/* Rich overlays for text legibility + luxury feel (stay on top of changing media) */}
{/* Animated dish info */}
{current && (
{t.screen.menuTitle}

{current.name}

{current.price}
KR

{translatedDesc}

{t.screen.prepared}
)}
{/* Cycle progress + counter (clean for many items) */}
{currentIndex + 1} / {cyclingItems.length}
{/* MENU REEL - continuous LEFT-moving reel (items move left: enter from right, exit left), entire menu in one line. Dynamic # of items visible based on zoom (more when zoomed out). Always full width edge-to-edge (no side whitespace). ~8s per item, seamless loop with posters only. Fixed band height so layout sums <=100vh with flex-1 hero. */}
THE MENU • REEL
{/* Full-width viewport for the reel; cards will fill based on container width. Height tuned to match card (poster + bottom text area) so items fill the band vertically with no large empty space under as they rotate left. */}
{reelItems.map((item, index) => { const displayNumber = ((index % numItems) + startNumber); return ( ); })}
{/* COMBO MEALS & DISCOUNT OFFERS - compact vertical footprint so total page height always <= 100vh in fullscreen (no scrollbar) */}
COMBO MEALS & OFFERS
Ask at counter by offer number
{/* Offer 1 */}
OFFER 1
BEST VALUE
SHAHI FAMILY COMBO
2 Chicken Curries + 2 Veg Curries + 4 Naan + 2 Rice + 4 Drinks
799 kr 999 kr
SAVE 200 KR • Serves 4
{/* Offer 2 */}
OFFER 2
LUNCH THALI SPECIAL
Any Curry + Rice + 2 Naan + Salad + Drink (11am–3pm)
149 kr 189 kr
SAVE 40 KR • Mon–Fri only
{/* Offer 3 */}
OFFER 3
PIZZA & ROLL DEAL
Any 2 Pizzas + Any 2 Rolls + 2 Drinks
229 kr 299 kr
SAVE 70 KR • Great for sharing
{/* Offer 4 */}
OFFER 4
VEG
VEG FEAST FOR 4
4 Veg Curries + 4 Naan + 2 Rice + 4 Drinks
599 kr 749 kr
SAVE 150 KR • Pure veg delight
{/* Very subtle footer branding */}
SHAHI KITCHEN • GOTHENBURG • FRESH EVERY DAY
); }