import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createRoot } from 'react-dom/client'; import gsap from 'gsap'; import { ScrollTrigger } from 'gsap/ScrollTrigger'; import { ArrowLeft, CalendarDays, Check, ChevronLeft, ChevronRight, Info, Minus, Plus, ShieldCheck, UtensilsCrossed, X, Waves, Clock, Ticket, Users, Tag, CreditCard, MapPin, AlertCircle } from 'lucide-react'; import './styles.css'; import { DEMO_CARDS, MEALS } from './data/mockData'; import { cartBreakdown, getVisibleCards, groupTicketsBySection, mealBreakdown, money, offerKind, prettyDate, ticketAudience, toApiDate, todayInput, weekday } from './data/utils'; gsap.registerPlugin(ScrollTrigger); const STEP_LABELS = ['DATE & TICKETS', 'MEALS', 'REVIEW', 'PAYMENT']; const NAV_LINKS = [ 'Monsoon Mania', 'Water Rides', 'Dry Rides', 'Visitors Guide', 'Offers', 'Blogs', 'Group Bookings', 'Careers', 'Contact Us', 'Review Us' ]; const PRM = () => window.matchMedia('(prefers-reduced-motion: reduce)').matches; function makeDates(n = 60) { const out = []; const base = new Date(todayInput()); for (let i = 0; i < n; i++) { const d = new Date(base); d.setDate(base.getDate() + i); out.push(d.toISOString().slice(0, 10)); } return out; } // ───────────────────────────────────────────────────────────── // RIPPLE ENGINE (unchanged) // ───────────────────────────────────────────────────────────── class RippleEngine { constructor(canvas) { this.canvas = canvas; this.ctx = canvas.getContext('2d'); this.rings = []; this.running = false; this.raf = null; this._resize(); this._ro = new ResizeObserver(() => this._resize()); this._ro.observe(document.documentElement); } _resize() { this.canvas.width = window.innerWidth; this.canvas.height = window.innerHeight; } addRing(x, y, strength = 1) { this.rings.push({ x, y, r: 4, maxR: (100 + Math.random() * 80) * strength, opacity: 0.55 * strength, color: ['rgba(0,212,240,OP)', 'rgba(255,213,0,OP)', 'rgba(255,51,102,OP)'][Math.floor(Math.random() * 3)] }); if (!this.running) this._start(); } _start() { this.running = true; const tick = () => { const { ctx, canvas, rings } = this; ctx.clearRect(0, 0, canvas.width, canvas.height); for (let i = rings.length - 1; i >= 0; i--) { const ring = rings[i]; const alpha = ring.opacity * (1 - ring.r / ring.maxR); if (alpha < 0.005) { rings.splice(i, 1); continue; } ctx.beginPath(); ctx.arc(ring.x, ring.y, ring.r, 0, Math.PI * 2); ctx.strokeStyle = ring.color.replace('OP', alpha.toFixed(3)); ctx.lineWidth = 2.5; ctx.stroke(); ring.r += 2.8; } if (rings.length === 0) { this.running = false; return; } this.raf = requestAnimationFrame(tick); }; this.raf = requestAnimationFrame(tick); } destroy() { cancelAnimationFrame(this.raf); this._ro.disconnect(); } } let _rippleEngine = null; function initRipple() { return; } export function triggerRippleBurst(x, y) { return; } // ───────────────────────────────────────────────────────────── // SCENE BACKGROUND (mobile only) // ───────────────────────────────────────────────────────────── function SceneBackground({ mouseX, mouseY }) { const p = (factor) => ({ transform: `translate(${mouseX * factor}px, ${mouseY * factor}px)`, willChange: 'transform' }); return ( ); } // ───────────────────────────────────────────────────────────── // SPLASH BURST // ───────────────────────────────────────────────────────────── function triggerSplash(x, y) { if (PRM()) return; const container = document.getElementById('splash-container'); if (!container) return; const colors = ['#00E5FF','#FFD500','#FF3366','#00E676','#00B8D9']; for (let i = 0; i < 10; i++) { const dot = document.createElement('div'); dot.className = 'splash-dot'; dot.style.left = `${x}px`; dot.style.top = `${y}px`; dot.style.background = colors[i % colors.length]; container.appendChild(dot); const ang = (Math.PI * 2 * i) / 10; const dist = 45 + Math.random() * 55; gsap.to(dot, { x: Math.cos(ang) * dist, y: Math.sin(ang) * dist, opacity: 0, scale: 0.2, duration: 0.65 + Math.random() * 0.3, ease: 'power2.out', onComplete: () => dot.remove() }); } } // ───────────────────────────────────────────────────────────── // ORIGINAL SITE NAVBAR // ───────────────────────────────────────────────────────────── function SiteNavbar({ onHomeClick }) { return (
Wild Waters Logo { e.target.style.display = 'none'; const fallback = e.target.nextElementSibling; if (fallback) fallback.style.display = 'flex'; }} />
WILD WATERS AMUSEMENT & WATER PARK
{NAV_LINKS.map(link => ( e.preventDefault()} className="sn-link"> {link} ))} e.preventDefault()} className="sn-link sn-link--location"> Location
); } // ───────────────────────────────────────────────────────────── // FERRIS WHEEL PROGRESS ICON (MATCHING ORIGINAL VIDEO RECORDING) // ───────────────────────────────────────────────────────────── function FerrisWheelIcon({ step }) { // Smoothly rotate 90 degrees per step progress const rotationAngle = (step - 1) * 90; return (
{/* Stand Support Structure (Bottom legs and base bar) */} {/* Wheel Assembly (Rotates smoothly on step transition, paused while on slide) */} {/* Outer Wheel Circle */} {/* 4 Main Spokes */} {/* Center Hub Dot */} {/* 4 Circular Cabins (Dots) */} {[ { angle: 270, stepIdx: 1, cx: 40, cy: 12 }, // Top (12 o'clock) { angle: 0, stepIdx: 2, cx: 66, cy: 38 }, // Right (3 o'clock) { angle: 90, stepIdx: 3, cx: 40, cy: 64 }, // Bottom (6 o'clock) { angle: 180, stepIdx: 4, cx: 14, cy: 38 }, // Left (9 o'clock) ].map((seat) => { const isDone = seat.stepIdx < step; const isActive = seat.stepIdx === step; let fillColor = '#FFFFFF'; let strokeColor = '#002855'; if (isDone) { fillColor = '#00C853'; // Vibrant Green for completed step (matching recording!) strokeColor = '#003816'; } else if (isActive) { fillColor = '#E91E63'; // Pink / Magenta active seat dot (matching recording!) strokeColor = '#880e4f'; } else { fillColor = '#FFFFFF'; // White seat for upcoming steps strokeColor = '#002855'; } return ( ); })}
); } // ───────────────────────────────────────────────────────────── // WAVE PROGRESS NAV (FLOATING BELOW NAVBAR WITH FERRIS WHEEL) // ───────────────────────────────────────────────────────────── function WaveProgress({ step, onGoTo }) { return (
); } // ───────────────────────────────────────────────────────────── // TICKET THEMES // ───────────────────────────────────────────────────────────── const TICKET_IMAGES = { adult: '/ticket-bg/adult-ticket.png', child: '/ticket-bg/child-ticket.png', senior: '/ticket-bg/senior_ticket.png', group: '/ticket-bg/group-ticket.png', birthday: '/ticket-bg/birthday-ticket.png', defence: '/ticket-bg/defence-ticket.png', student: '/ticket-bg/student-ticket.png', }; const TICKET_GRADIENTS = { adult: 'linear-gradient(135deg,#00c4e8 0%,#0062a0 100%)', child: 'linear-gradient(135deg,#ff9a00 0%,#ff3d77 100%)', senior: 'linear-gradient(135deg,#f7d488 0%,#00b8d9 100%)', group: 'linear-gradient(135deg,#00e5ff 0%,#00e676 100%)', birthday: 'linear-gradient(135deg,#ff3366 0%,#ffd500 100%)', defence: 'linear-gradient(135deg,#ff8f00 0%,#1b5e20 100%)', student: 'linear-gradient(135deg,#9c27b0 0%,#ffd500 100%)', }; function getTicketThemeKey(card) { const kind = offerKind(card); if (kind === 'group') return 'group'; if (kind === 'birthday') return 'birthday'; if (kind === 'defence') return 'defence'; if (kind === 'student') return 'student'; const aud = ticketAudience(card); if (aud === 'child') return 'child'; if (aud === 'senior') return 'senior'; if (aud === 'adult') return 'adult'; return 'group'; } // ───────────────────────────────────────────────────────────── // BOOKING SUMMARY CARD // ───────────────────────────────────────────────────────────── function BookingSummaryCard({ date, cart, meals, eligibleCards, bd, md, totalValRef, onUpdateCart, onEditCart, ctaButton }) { const hasItems = bd.lineItems.length > 0 || md.lineItems.length > 0; const grandTotal = bd.total + md.total; return (
YOUR BOOKING
{onEditCart && ( )}
{prettyDate(date)}
{!hasItems && (

Add tickets to see
your booking summary

)} {bd.lineItems.length > 0 && (
ENTRY TICKETS
{bd.lineItems.map((li, i) => (
{onUpdateCart && ( )}
{li.qty} × {li.label}
{li.entries > li.qty && {li.entries} entries} {li.audience?.toUpperCase() || 'ADULT'}
{li.savings > 0 && {money(li.displayUnit)}} {money(li.total)}
))}
)} {md.lineItems.length > 0 && (
MEALS
{md.lineItems.map((li, i) => (
{li.qty} × {li.label}
{money(li.total)}
))}
)} {hasItems && ( <> {bd.savings > 0 && (
YOU SAVE {money(bd.savings)}
)}
TOTAL PAYABLE {bd.entries > 0 && {bd.entries} park {bd.entries === 1 ? 'entry' : 'entries'}}
{money(grandTotal)}
)} {ctaButton && (
{ctaButton}
)}
); } // ───────────────────────────────────────────────────────────── // TICKET CARD // ───────────────────────────────────────────────────────────── function TicketCard({ card, qty, onAdd, onInc, onDec }) { const ref = useRef(null); const themeKey = getTicketThemeKey(card); const imgSrc = TICKET_IMAGES[themeKey]; const gradient = TICKET_GRADIENTS[themeKey]; const savings = Number(card.displayTicketAmount) - Number(card.ticketAmount); const hasSavings = savings > 0; const entries = Number(card.entryCount) > 1 ? card.entryCount : null; const audience = ticketAudience(card); const onMove = useCallback((e) => { if (PRM()) return; const el = ref.current; if (!el) return; const r = el.getBoundingClientRect(); gsap.to(el, { rotateY: ((e.clientX - r.left) / r.width - 0.5) * 5, rotateX: -((e.clientY - r.top) / r.height - 0.5) * 3, duration: 0.3, ease: 'power2.out', transformPerspective: 900 }); }, []); const onLeave = useCallback(() => { if (PRM()) return; gsap.to(ref.current, { rotateY: 0, rotateX: 0, duration: 0.5, ease: 'elastic.out(1,0.5)' }); }, []); return (
0 ? 'in-cart' : ''}`} onMouseMove={onMove} onMouseLeave={onLeave} style={{ '--tc-img': `url(${imgSrc})`, '--tc-grad': gradient }}> ); } // ───────────────────────────────────────────────────────────── // CALENDAR POPUP // ───────────────────────────────────────────────────────────── function CalendarPopup({ selectedDate, onSelect, onClose }) { const today = todayInput(); const [viewMonth, setViewMonth] = useState(() => { const d = new Date(`${selectedDate}T12:00:00`); return { year: d.getFullYear(), month: d.getMonth() }; }); const daysInMonth = (year, month) => new Date(year, month + 1, 0).getDate(); const firstDayOfMonth = (year, month) => new Date(year, month, 1).getDay(); const prevMonth = () => setViewMonth(p => { const d = new Date(p.year, p.month - 1, 1); return { year: d.getFullYear(), month: d.getMonth() }; }); const nextMonth = () => setViewMonth(p => { const d = new Date(p.year, p.month + 1, 1); return { year: d.getFullYear(), month: d.getMonth() }; }); const { year, month } = viewMonth; const dim = daysInMonth(year, month); const firstDay = firstDayOfMonth(year, month); const monthName = new Date(year, month, 1).toLocaleString('default', { month: 'long', year: 'numeric' }); const maxDate = new Date(today); maxDate.setMonth(maxDate.getMonth() + 2); const days = []; for (let i = 0; i < firstDay; i++) days.push(null); for (let d = 1; d <= dim; d++) days.push(d); return (
{monthName}
{['Su','Mo','Tu','We','Th','Fr','Sa'].map(d =>
{d}
)}
{days.map((day, i) => { if (!day) return
; const mm = String(month + 1).padStart(2, '0'), dd = String(day).padStart(2, '0'), iso = `${year}-${mm}-${dd}`; const isPast = iso < today, isTooFar = new Date(`${iso}T12:00:00`) > maxDate, isSelected = iso === selectedDate, isToday = iso === today; return ( ); })}
); } // ───────────────────────────────────────────────────────────── // STEP 1 — DATE & TICKETS // ───────────────────────────────────────────────────────────── function DateAndTicketsStep({ date, setDate, dates, loading, error, cart, updateCart, onNext, bd, md, totalValRef, eligibleCards }) { const [calOpen, setCalOpen] = useState(false); const calRef = useRef(null); const ticketListRef = useRef(null); const VISIBLE_DATES = 14; const visibleDates = dates.slice(0, VISIBLE_DATES); const grouped = useMemo(() => groupTicketsBySection(eligibleCards), [eligibleCards]); const hasCart = Object.keys(cart).length > 0; useEffect(() => { const handler = (e) => { if (calRef.current && !calRef.current.contains(e.target)) setCalOpen(false); }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler); }, []); useEffect(() => { if (!ticketListRef.current || PRM()) return; const cards = ticketListRef.current.querySelectorAll('.ticket-card'); if (!cards.length) return; gsap.fromTo(cards, { opacity: 0, y: 20 }, { opacity: 1, y: 0, stagger: 0.04, duration: 0.32, ease: 'power2.out' }); return () => { ScrollTrigger.getAll().forEach(t => t.kill()); }; }, [eligibleCards]); const sectionLabelClass = (section) => { const s = section.toLowerCase(); if (s.includes('group') || s.includes('buy')) return 'group'; if (s.includes('birthday')) return 'birthday'; if (s.includes('defence') || s.includes('defense')) return 'defence'; if (s.includes('student')) return 'student'; return ''; }; return (
{/* LEFT: Eyecatcher branding panel */}
PLAN YOUR WILD DAY

WHEN
ARE WE
RIDING?

{date.split('-')[2]}
{weekday(date).toUpperCase()} · {new Date(`${date}T12:00:00`).toLocaleString('default', { month: 'short' }).toUpperCase()}
Selected park day
WEEKDAY HOURS11 AM – 6 PM WEEKEND HOURS11 AM – 7 PM
50+ Water & Dry Rides · Wave Pool · Rain Dance · Foam Party · Magic Show · 360° Dome
{/* MIDDLE: 2 Weeks Date Strip + Tickets List */}
PICK A PARK DAY (NEXT 2 WEEKS)
{calOpen && ( { setDate(d); }} onClose={() => setCalOpen(false)} /> )}
{visibleDates.map(d => ( ))}
{error &&
{error}
} {loading ?

Fetching tickets for {prettyDate(date)}…

: eligibleCards.length === 0 ?

No tickets for this date

Try selecting a different date above

:
{grouped.map(({ section, cards: sCards }) => (
{section}
{sCards.map(card => ( updateCart(card.id, 1)} onInc={() => updateCart(card.id, 1)} onDec={() => updateCart(card.id, -1)} /> ))}
))}
}
{/* RIGHT / DETAILS PANEL */} {hasCart && (
{ triggerSplash(e.clientX, e.clientY); onNext(); }} id="btn-choose-meals"> CHOOSE YOUR MEALS } />
)}
); } // ───────────────────────────────────────────────────────────── // STEP 2 — MEALS (50-50 BALANCED SPLIT) // ───────────────────────────────────────────────────────────── function MealsStep({ date, cart, meals, updateMeal, onNext, onSkip, onBack, onEditCart, bd, md, totalValRef, eligibleCards }) { return (
02 — FUEL UP FOR THE RIDES

ADD MEALS

{MEALS.map(meal => { const qty = meals[meal.id] || 0; return (
0 ? 'in-cart' : ''}`}>
{meal.name} { e.target.parentElement.style.background = 'linear-gradient(135deg,#f97316,#ef4444)'; e.target.style.display = 'none'; }} />

{meal.name}

{meal.description}

{meal.note && {meal.note}}
{money(meal.price)} {qty === 0 ? :
{qty}
}
); })}
); } // ───────────────────────────────────────────────────────────── // STEP 3 — REVIEW (50-50 SPLIT & YELLOW STRIKE AMOUNTS) // ───────────────────────────────────────────────────────────── function ReviewStep({ date, cart, meals, eligibleCards, bd, md, totalValRef, onPay, onBack, onEditCart, paymentError, onClearError }) { const [personal, setPersonal] = useState({ name: '', email: '', mobile: '' }); const [errors, setErrors] = useState({}); const grandTotal = bd.total + md.total; const validate = () => { const e = {}; if (!personal.name.trim()) e.name = true; if (!personal.email.trim() || !/\S+@\S+\.\S+/.test(personal.email)) e.email = true; if (!personal.mobile.trim() || !/^\d{10}$/.test(personal.mobile.replace(/\s/g, ''))) e.mobile = true; setErrors(e); return Object.keys(e).length === 0; }; const handlePay = (e) => { if (!validate()) return; if (onClearError) onClearError(); triggerSplash(e.clientX, e.clientY); onPay(personal); }; return (
{/* LEFT: Customer Details Form (50%) */}
{paymentError && (
Payment Failed {paymentError}
)}
03 — ALMOST THERE

YOUR DETAILS

We'll send your tickets to this contact

setPersonal(p => ({ ...p, name: e.target.value }))} className={errors.name ? 'error' : ''} /> {errors.name && Please enter your name}
setPersonal(p => ({ ...p, email: e.target.value }))} className={errors.email ? 'error' : ''} /> {errors.email && Please enter a valid email}
+91 setPersonal(p => ({ ...p, mobile: e.target.value.replace(/\D/g, '').slice(0, 10) }))} className={errors.mobile ? 'error' : ''} maxLength={10} />
{errors.mobile && Please enter a valid 10-digit mobile}

By continuing, I agree to the Terms & Conditions

{/* RIGHT: Detailed Review Summary Card (50%) */}
); } // ───────────────────────────────────────────────────────────── // STEP 4 — PAYMENT + THANK YOU SCREEN // ───────────────────────────────────────────────────────────── function PaymentStep({ onBack, bd, md, onSimulateFail }) { const [paid, setPaid] = useState(false); const grandTotal = bd.total + md.total; if (paid) { return (
); } return (

PAYMENT

Amount: {money(grandTotal)}

Choose simulation action below:

); } // ───────────────────────────────────────────────────────────── // VIDEO PRELOADER & EFFECTS // ───────────────────────────────────────────────────────────── function VideoPreloader({ onReady }) { const [fade, setFade] = useState(false); useEffect(() => { if (PRM()) { onReady(); return; } const check = () => { const v = document.getElementById('ride-video'); if (v && v.readyState >= 3) { setFade(true); setTimeout(onReady, 600); } else setTimeout(check, 100); }; check(); }, [onReady]); if (PRM()) return null; return (
WW
Loading your ride…
); } function WaterParticles({ active }) { if (PRM()) return null; const particles = Array.from({ length: 12 }).map((_, i) => { const angle = Math.random() * Math.PI * 2, distance = 100 + Math.random() * 200; return
; }); return
{particles}
; } function MotionBlurOverlay() { return
; } // ───────────────────────────────────────────────────────────── // APP // ───────────────────────────────────────────────────────────── function App() { const [step, setStep] = useState(1); const [date, setDate] = useState(todayInput()); const [cards, setCards] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [paymentError, setPaymentError] = useState(''); const [cart, setCart] = useState({}); const [meals, setMeals] = useState({}); const [mouse, setMouse] = useState({ x: 0, y: 0 }); const [videoReady, setVideoReady] = useState(false); const [particlesActive, setParticlesActive] = useState(false); const containerRef = useRef(null); const totalValRef = useRef(null); const prevTotal = useRef(0); const stepTimes = useMemo(() => ({ 1: 0.5, 2: 2.5, 3: 5.5, 4: 9.0 }), []); const dates = useMemo(() => makeDates(60), []); const eligibleCards = useMemo(() => getVisibleCards(cards), [cards]); const bd = useMemo(() => cartBreakdown(cart, eligibleCards), [cart, eligibleCards]); const md = useMemo(() => mealBreakdown(meals, MEALS), [meals]); const grandTotal = bd.total + md.total; const hasCart = Object.keys(cart).length > 0; useEffect(() => { initRipple(); }, []); useEffect(() => { let tid; const h = (e) => { clearTimeout(tid); tid = setTimeout(() => setMouse({ x: e.clientX - window.innerWidth / 2, y: e.clientY - window.innerHeight / 2 }), 16); }; window.addEventListener('mousemove', h, { passive: true }); return () => window.removeEventListener('mousemove', h); }, []); useEffect(() => { let cancelled = false; setLoading(true); setError(''); setCart({}); setMeals({}); fetch(`/api/price-cards?date=${encodeURIComponent(toApiDate(date))}`) .then(r => r.json().then(d => ({ ok: r.ok, d }))) .then(({ ok, d }) => { if (!cancelled) { if (!ok) throw new Error(d.error || 'Error'); setCards(d.priceCards || []); } }) .catch(e => { if (!cancelled) { setCards(DEMO_CARDS); setError(`${e.message}. Showing demo data.`); } }) .finally(() => { if (!cancelled) setLoading(false); }); return () => { cancelled = true; }; }, [date]); useEffect(() => { if (!totalValRef.current || grandTotal === prevTotal.current || PRM()) return; prevTotal.current = grandTotal; gsap.fromTo(totalValRef.current, { scale: 1.18, color: '#00E5FF' }, { scale: 1, color: '#002855', duration: 0.5, ease: 'elastic.out(1,0.6)' }); }, [grandTotal]); useEffect(() => { if (videoReady && !PRM() && window.innerWidth >= 1024) { gsap.to('#ride-video', { scale: 1.025, duration: 4, ease: 'sine.inOut', yoyo: true, repeat: -1 }); const v = document.getElementById('ride-video'); if (v) v.currentTime = stepTimes[step]; } }, [videoReady, step, stepTimes]); const goTo = useCallback((next) => { if (next === step) return; if (PRM()) { setStep(next); window.scrollTo(0, 0); return; } const el = containerRef.current; if (!el) return; const isForward = next > step; if (isForward && window.innerWidth >= 1024) { const v = document.getElementById('ride-video'); const targetTime = stepTimes[next]; gsap.fromTo(el, { y: 0 }, { y: -3, duration: 0.08, yoyo: true, repeat: 1 }); gsap.to(el, { opacity: 0, filter: 'blur(10px)', duration: 0.3, onComplete: () => { if (v) { const timeDiff = targetTime - v.currentTime; const animDuration = 1.4; if (timeDiff > 0) { v.playbackRate = timeDiff / animDuration; v.play().catch(() => {}); } gsap.to({}, { duration: animDuration, onUpdate: () => { if (v.currentTime >= targetTime) v.pause(); }, onComplete: () => { v.pause(); v.currentTime = targetTime; v.playbackRate = 1; setStep(next); window.scrollTo(0, 0); gsap.fromTo(el, { opacity: 0, filter: 'blur(8px)', y: 12 }, { opacity: 1, filter: 'blur(0px)', y: 0, duration: 0.4, ease: 'power2.out' }); } }); gsap.to('#motion-blur-overlay', { opacity: 0.6, backdropFilter: 'blur(6px)', duration: 0.7, yoyo: true, repeat: 1, ease: 'power1.inOut' }); setTimeout(() => setParticlesActive(true), 400); setTimeout(() => setParticlesActive(false), 1200); } else { setStep(next); window.scrollTo(0, 0); gsap.fromTo(el, { opacity: 0, y: 24 }, { opacity: 1, y: 0, duration: 0.32 }); } } }); } else { gsap.to(el, { opacity: 0, y: isForward ? -24 : 24, duration: 0.22, ease: 'power2.in', onComplete: () => { setStep(next); window.scrollTo(0, 0); if (window.innerWidth >= 1024) { const v = document.getElementById('ride-video'); if (v) { v.pause(); v.currentTime = stepTimes[next]; } } gsap.fromTo(el, { opacity: 0, filter: 'blur(0px)', y: isForward ? 24 : -24 }, { opacity: 1, y: 0, duration: 0.32, ease: 'power2.out' }); } }); } }, [step, stepTimes]); const updateCart = (id, d) => setCart(p => { const n = Math.max(0, (p[id] || 0) + d); const c = { ...p }; if (n === 0) delete c[id]; else c[id] = n; return c; }); const updateMeal = (id, d) => setMeals(p => { const n = Math.max(0, (p[id] || 0) + d); const c = { ...p }; if (n === 0) delete c[id]; else c[id] = n; return c; }); const handleSimulatePaymentFail = () => { setPaymentError('Sorry, transaction failed! Payment was declined by bank. Please check your details and try again.'); goTo(3); }; const isThankyou = step === 4; return (
{!videoReady && setVideoReady(true)} />}