// Shared: Nav, Footer, WhatsApp FAB, DatePicker, BookingStrip, Toast const { useState, useEffect, useRef, useMemo, useCallback } = React; function Nav({ page, setPage }) { const links = [ { id: 'home', label: 'Home' }, { id: 'rental', label: 'Rental' }, { id: 'modification', label: 'Modification' }, { id: 'motorhomes', label: 'Motorhomes' }, { id: 'about', label: 'About' }, { id: 'contact', label: 'Contact' }, ]; return ( ); } function Footer({ setPage }) { return ( ); } function WhatsAppFab() { const [show, setShow] = useState(true); useEffect(() => { const t = setTimeout(() => setShow(false), 8000); return () => clearTimeout(t); }, []); return (
{show && (
Need help? Plan your trip with our team on WhatsApp — replies in < 5 min.
)}
); } function Toast({ message, onDone }) { useEffect(() => { const t = setTimeout(onDone, 2800); return () => clearTimeout(t); }, [message, onDone]); return (
{message}
); } // ---------- CountUp — animates number when scrolled into view ---------- function CountUp({ to, from = 0, duration = 1400, suffix = '', prefix = '', decimals = 0 }) { const ref = useRef(null); const [val, setVal] = useState(from); useEffect(() => { if (!ref.current) return; const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const obs = new IntersectionObserver((entries) => { entries.forEach(e => { if (!e.isIntersecting) return; obs.disconnect(); if (reduce) { setVal(to); return; } const start = performance.now(); const tick = () => { const elapsed = performance.now() - start; const t = Math.min(elapsed / duration, 1); const eased = 1 - Math.pow(1 - t, 3); setVal(from + (to - from) * eased); if (t < 1) requestAnimationFrame(tick); else setVal(to); }; requestAnimationFrame(tick); }); }, { threshold: 0.4 }); obs.observe(ref.current); return () => obs.disconnect(); }, [to, from, duration]); const display = decimals > 0 ? val.toFixed(decimals) : Math.round(val); return {prefix}{display}{suffix}; } // ---------- DatePicker ---------- const MONTH_NAMES = ['January','February','March','April','May','June','July','August','September','October','November','December']; const DOW = ['S','M','T','W','T','F','S']; function formatDate(d) { if (!d) return null; return `${d.getDate()} ${MONTH_NAMES[d.getMonth()].slice(0,3)}`; } function DatePicker({ start, end, onChange, onClose }) { const [view, setView] = useState(() => start || new Date(2026, 5, 1)); const ref = useRef(null); useEffect(() => { function handle(e) { if (ref.current && !ref.current.contains(e.target)) onClose && onClose(); } document.addEventListener('mousedown', handle); return () => document.removeEventListener('mousedown', handle); }, [onClose]); function navMonth(delta) { setView(v => new Date(v.getFullYear(), v.getMonth() + delta, 1)); } function pickDay(day) { const picked = new Date(view.getFullYear(), view.getMonth(), day); if (!start || (start && end)) { onChange({ start: picked, end: null }); } else if (picked < start) { onChange({ start: picked, end: null }); } else { onChange({ start, end: picked }); } } const year = view.getFullYear(); const month = view.getMonth(); const firstDow = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const today = new Date(); today.setHours(0,0,0,0); const cells = []; for (let i = 0; i < firstDow; i++) cells.push(null); for (let d = 1; d <= daysInMonth; d++) cells.push(d); function isSameDay(a, b) { return a && b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); } return (
e.stopPropagation()}>

{MONTH_NAMES[month]} {year}

{DOW.map((d, i) =>
{d}
)} {cells.map((day, i) => { if (!day) return
; const date = new Date(year, month, day); const disabled = date < today; const isStart = isSameDay(date, start); const isEnd = isSameDay(date, end); const inRange = start && end && date > start && date < end; const classes = ['cal-day']; if (disabled) classes.push('disabled'); if (isStart) classes.push('start'); if (isEnd) classes.push('end'); if (inRange) classes.push('in-range'); return (
!disabled && pickDay(day)}> {day}
); })}
{start && end ? `${Math.ceil((end - start) / 86400000)} NIGHTS` : start ? 'PICK CHECK-OUT' : 'PICK CHECK-IN'}
); } function BookingStrip({ onSubmit, initial }) { const [pickup, setPickup] = useState(initial?.pickup || 'Klang HQ'); const [dates, setDates] = useState({ start: initial?.start || null, end: initial?.end || null }); const [travellers, setTravellers] = useState(initial?.travellers || 2); const [showPicker, setShowPicker] = useState(false); const [showLoc, setShowLoc] = useState(false); const [showTrav, setShowTrav] = useState(false); return (
{ setShowLoc(s=>!s); setShowPicker(false); setShowTrav(false); }}> Location {pickup} Selangor depot {showLoc && (
e.stopPropagation()}> {['Klang HQ', 'KLIA / KLIA2', 'KL Sentral', 'Penang (delivery)'].map(loc => (
e.currentTarget.style.background='rgba(13,13,10,0.05)'} onMouseLeave={e => e.currentTarget.style.background='transparent'} onClick={() => { setPickup(loc); setShowLoc(false); }}> {loc}
))}
)}
{ setShowPicker(true); setShowLoc(false); setShowTrav(false); }}> Pick-up {dates.start ? formatDate(dates.start) : 'Select date'} From 09:00 {showPicker && ( setShowPicker(false)}/> )}
{ setShowPicker(true); setShowLoc(false); setShowTrav(false); }}> Drop-off {dates.end ? formatDate(dates.end) : 'Select date'} By 18:00
{ setShowTrav(s=>!s); setShowLoc(false); setShowPicker(false); }}> Travellers {travellers} {travellers === 1 ? 'guest' : 'guests'} 1 driver included {showTrav && (
e.stopPropagation()}>
TRAVELLERS
{travellers}
)}
); } window.Nav = Nav; window.Footer = Footer; window.WhatsAppFab = WhatsAppFab; window.Toast = Toast; window.DatePicker = DatePicker; window.BookingStrip = BookingStrip; window.formatDate = formatDate; window.CountUp = CountUp;