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 (

{
e.target.style.display = 'none';
const fallback = e.target.nextElementSibling;
if (fallback) fallback.style.display = 'flex';
}}
/>
WILD WATERS
AMUSEMENT & WATER PARK
);
}
// ─────────────────────────────────────────────────────────────
// FERRIS WHEEL PROGRESS ICON (MATCHING ORIGINAL VIDEO RECORDING)
// ─────────────────────────────────────────────────────────────
function FerrisWheelIcon({ step }) {
// Smoothly rotate 90 degrees per step progress
const rotationAngle = (step - 1) * 90;
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) => (
))}
)}
{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 }}>
{card.subclass || 'REGULAR'}
{hasSavings && {Math.round((savings / Number(card.displayTicketAmount)) * 100)}% OFF}
{hasSavings && {money(Number(card.displayTicketAmount))}}
{money(card.ticketAmount)}
{card.name}
{card.description &&
{card.description}
}
{audience.toUpperCase()}
{entries && {entries} ENTRIES}
{qty === 0
?
:
{qty}
}
);
}
// ─────────────────────────────────────────────────────────────
// 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' : ''}`}>

{ 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
By continuing, I agree to the Terms & Conditions
Secure Payment · Instant Confirmation
Non-refundable · Non-transferable · No re-entry
Senior / Defence / Student tickets require valid Govt. ID at gate
Queries: +91-77997 38111
{/* 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 (
{Array.from({ length: 24 }).map((_, i) => (
))}
THANK YOU!
YOUR BOOKING IS CONFIRMED
Get ready to ride wild at Wild Waters, Hyderabad!
You paid
{money(grandTotal)}
Your tickets will be sent to your registered email. Show them at the gate.
);
}
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 (
);
}
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)} />}
{/* ORIGINAL SITE NAVBAR */}
goTo(1)} />
{/* FLOATING WAVE PROGRESS BELOW NAVBAR WITH ROTATING FERRIS WHEEL */}
{step === 1 && (
{ setDate(d); setCart({}); setMeals({}); }}
dates={dates} loading={loading} error={error}
cart={cart} updateCart={updateCart} onNext={() => goTo(2)}
bd={bd} md={md} totalValRef={totalValRef} eligibleCards={eligibleCards}
/>
)}
{step === 2 && (
goTo(3)} onSkip={() => { setMeals({}); goTo(3); }}
onBack={() => goTo(1)} onEditCart={() => goTo(1)}
bd={bd} md={md} totalValRef={totalValRef} eligibleCards={eligibleCards}
/>
)}
{step === 3 && (
goTo(4)} onBack={() => goTo(2)} onEditCart={() => goTo(1)}
paymentError={paymentError} onClearError={() => setPaymentError('')}
/>
)}
{step === 4 && (
goTo(3)} bd={bd} md={md} onSimulateFail={handleSimulatePaymentFail} />
)}
);
}
createRoot(document.getElementById('root')).render();