// 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;