/* Sea La Vie — booking widget (production v3, posts to api.php) */ const { useState, useRef, useEffect, useMemo } = React; function OrnamentRow() { return (
); } function MonthCalendar({ year, month, onPick, range, setMonth, showPrices, isUnavailable, priceUSD }) { const dim = daysInMonth(year, month); const fdow = firstDow(year, month); const today = new Date(); today.setHours(0,0,0,0); const [currency] = useCurrency(); const cells = []; for (let i = 0; i < fdow; i++) cells.push(
); for (let d = 1; d <= dim; d++) { const date = new Date(year, month, d); const key = dateKey(year, month, d); const past = date < today; const unav = !past && isUnavailable && isUnavailable(key); let cls = "cal-day"; if (past) cls += " disabled"; else if (unav) cls += " unavailable"; let inRange = false, isStart = false, isEnd = false; if (range && range[0] && range[1]) { if (date.getTime() === range[0].getTime()) isStart = true; else if (date.getTime() === range[1].getTime()) isEnd = true; else if (date > range[0] && date < range[1]) inRange = true; } else if (range && range[0] && date.getTime() === range[0].getTime()) { isStart = true; } if (inRange) cls += " in-range"; if (isStart) cls += " range-start selected"; if (isEnd) cls += " range-end selected"; const dow = date.getDay(); const isWeekend = dow === 0 || dow === 6; const baseUSD = priceUSD || 440; const dayPriceUSD = isWeekend ? Math.round(baseUSD * 1.3) : baseUSD; const price = formatPrice(dayPriceUSD, currency); cells.push(
!past && !unav && onPick && onPick(date)}> {d} {showPrices && !past && !unav && !isStart && !isEnd && !inRange && ( {price} )}
); } return (
{setMonth ? : }
{MONTHS[month]} {year}
{setMonth ? : }
{DOW.map((d, i) =>
{d}
)} {cells}
); } function DateRangePopover({ range, onChange, onClose, isUnavailable, priceUSD }) { const [year, setYear] = useState(() => range && range[0] ? range[0].getFullYear() : 2026); const [month, setMonthState] = useState(() => range && range[0] ? range[0].getMonth() : 10); const ref = useRef(null); useEffect(() => { function onClick(e) { if (ref.current && !ref.current.contains(e.target)) onClose(); } document.addEventListener("mousedown", onClick); return () => document.removeEventListener("mousedown", onClick); }, [onClose]); function shift(delta) { let m = month + delta, y = year; while (m < 0) { m += 12; y -= 1; } while (m > 11) { m -= 12; y += 1; } setMonthState(m); setYear(y); } function pick(date) { if (!range[0] || (range[0] && range[1])) onChange([date, null]); else if (date <= range[0]) onChange([date, null]); else { let hasUnav = false; for (let dt = new Date(range[0]); dt < date; dt.setDate(dt.getDate() + 1)) { if (isUnavailable && isUnavailable(dateKeyFor(dt))) { hasUnav = true; break; } } if (hasUnav) onChange([date, null]); else onChange([range[0], date]); } } const isMobile = typeof window !== "undefined" && window.innerWidth < 760; const nextMonth = month === 11 ? 0 : month + 1; const nextYear = month === 11 ? year + 1 : year; return (
e.stopPropagation()}> {!isMobile && ( )}
Available Your stay Reserved
); } function GuestPopover({ guests, onChange, onClose, maxGuests }) { const ref = useRef(null); useEffect(() => { function onClick(e) { if (ref.current && !ref.current.contains(e.target)) onClose(); } document.addEventListener("mousedown", onClick); return () => document.removeEventListener("mousedown", onClick); }, [onClose]); const totalCounting = guests.adults + guests.children; const cap = maxGuests || 4; function step(key, delta, min = 0) { if (delta > 0 && (key === "adults" || key === "children")) { if (totalCounting >= cap) return; } const v = Math.max(min, guests[key] + delta); onChange({ ...guests, [key]: v }); } const rows = [ { key: "adults", l: "Adults", s: "Ages 13+", min: 1 }, { key: "children", l: "Children", s: "Ages 4 – 12", min: 0 }, { key: "infants", l: "Infants", s: "Under 4 · doesn't count toward max", min: 0 } ]; return (
e.stopPropagation()}>
Up to {cap} guests per residence
{rows.map(r => { const isCounted = r.key === "adults" || r.key === "children"; const canInc = !isCounted || totalCounting < cap; return (
{r.l}
{r.s}
{guests[r.key]}
); })}
); } function VillaPopover({ value, onChange, onClose, availability, range }) { const ref = useRef(null); useEffect(() => { function onClick(e) { if (ref.current && !ref.current.contains(e.target)) onClose(); } document.addEventListener("mousedown", onClick); return () => document.removeEventListener("mousedown", onClick); }, [onClose]); function isVillaAvailable(villaId) { if (!range || !range[0] || !range[1]) return true; const list = availability[villaId] || []; for (let dt = new Date(range[0]); dt < range[1]; dt.setDate(dt.getDate() + 1)) { if (list.includes(dateKeyFor(dt))) return false; } return true; } return (
e.stopPropagation()}> {VILLAS.map(v => { const avail = isVillaAvailable(v.id); return ( ); })}
); } function ConfirmReservation({ booking, onClose, onConfirmed }) { const [step, setStep] = useState("review"); const [ref, setRef] = useState(""); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(""); const [form, setForm] = useState({ name: "", email: "", phone: "", message: "" }); const [currency] = useCurrency(); const { range, villaId, guests } = booking; const villa = VILLAS.find(v => v.id === villaId); const villaName = villa ? villa.name.join(" ") : "All Four Residences"; const nights = Math.max(1, Math.round((range[1] - range[0]) / 86400000)); // Estate buyout = sum of all four villas const baseUSD = villa ? villa.priceUSD : VILLAS.reduce((s, v) => s + v.priceUSD, 0); const subtotal = baseUSD * nights; const fee = Math.round(subtotal * 0.06); const tax = Math.round(subtotal * 0.16); const total = subtotal + fee + tax; const totalGuests = guests.adults + guests.children; const fp = (usd) => formatPrice(usd, currency); function up(k, v) { setForm(prev => ({ ...prev, [k]: v })); } async function confirm() { setError(""); if (!form.name.trim() || !form.email.trim()) { setError("Please share your name and email so we can confirm with you."); return; } setSubmitting(true); // Build date keys (exclusive of departure) const dateKeys = []; for (let dt = new Date(range[0]); dt < range[1]; dt.setDate(dt.getDate() + 1)) { dateKeys.push(dateKeyFor(dt)); } const payload = { villaId, arrival: dateKeyFor(range[0]), departure: dateKeyFor(range[1]), nights, guests: totalGuests, dateKeys, name: form.name.trim(), email: form.email.trim(), phone: form.phone.trim(), message: form.message.trim() }; try { const r = await apiBook(payload); if (r && r.ok) { setRef(r.ref); setStep("success"); onConfirmed(r.ref, dateKeys); } else { setError(r && r.error ? r.error : "Could not submit. Please try again or email us directly."); } } catch (e) { // Backend not available — fall back to mailto const subj = encodeURIComponent(`Reservation enquiry — ${villaName} · ${fmtDate(range[0])} → ${fmtDate(range[1])}`); const body = encodeURIComponent( `Hello Sea La Vie team,\n\nI'd like to reserve ${villaName} for ${nights} night${nights>1?"s":""}.\n\n` + `Arrival: ${fmtDate(range[0])}\nDeparture: ${fmtDate(range[1])}\nGuests: ${totalGuests}\n\n` + `Name: ${form.name}\nEmail: ${form.email}\nPhone: ${form.phone}\n\n${form.message}\n\nThank you,` ); window.location.href = `mailto:${CONTACT.email}?subject=${subj}&body=${body}`; setRef("SLV-EMAIL"); setStep("success"); onConfirmed("SLV-EMAIL", dateKeys); } finally { setSubmitting(false); } } return (
e.stopPropagation()}> {step === "review" && ( <>
Confirm reservation

Your stay at {villaName}

Tell us where to reach you and we'll hold the residence for fifteen minutes while a curator personally confirms.

Arrival
{fmtDate(range[0])}
Departure
{fmtDate(range[1])}
Residence
{villaName}
Guests
{totalGuests} · {nights} night{nights>1?"s":""}
up("name", e.target.value)} placeholder="As it appears on your passport" />
up("email", e.target.value)} placeholder="you@example.com" />
up("phone", e.target.value)} placeholder="+254 …" />
up("message", e.target.value)} placeholder="Anniversary, dietary needs, airport transfer…" />
{fp(baseUSD)} × {nights} nights{fp(subtotal)}
Concierge fee{fp(fee)}
VAT (16%){fp(tax)}
Total in {currency}
{fp(total)}
{error &&
{error}
}
)} {step === "success" && (
Reservation received

Karibu — welcome

{ref}

A confirmation has been sent to {form.email}. Your travel curator will reach out from {CONTACT.email} within twenty-four hours to finalise payment and begin planning your time on the coast.

)}
); } function BookingWidget({ initialVilla }) { const [active, setActive] = useState("Reserve"); const [open, setOpen] = useState(null); const [range, setRange] = useState([new Date(2026, 10, 14), new Date(2026, 10, 21)]); const [guests, setGuests] = useState({ adults: 2, children: 0, infants: 0 }); const [villaId, setVillaId] = useState(initialVilla || "bahari"); const [showConfirm, setShowConfirm] = useState(false); const avail = useAvailability(); const tabs = ["Reserve", "Residences", "Experiences", "Wellness", "Dining"]; const totalGuests = guests.adults + guests.children; function guestLabel() { const parts = [`${totalGuests} ${totalGuests === 1 ? "Guest" : "Guests"}`]; if (guests.infants) parts.push(`${guests.infants} Infant${guests.infants > 1 ? "s" : ""}`); return parts.join(" · "); } function isUnavailable(key) { if (villaId === "any") return avail.anyBooked(key); return avail.isBooked(villaId, key); } function handleTabClick(t) { setActive(t); const routes = { "Residences": "residences.html", "Experiences": "experiences.html", "Wellness": "wellness.html", "Dining": "cuisine.html" }; if (routes[t]) window.location.href = routes[t]; } function attemptReserve() { if (!range[0] || !range[1]) { setOpen("dates"); return; } if (villaId === "any") { // For "any" we send straight to the confirm flow as an enquiry setShowConfirm(true); return; } for (let dt = new Date(range[0]); dt < range[1]; dt.setDate(dt.getDate()+1)) { if (avail.isBooked(villaId, dateKeyFor(dt))) { alert("Those dates are no longer fully available. Please choose new dates or another residence."); setOpen("dates"); return; } } setShowConfirm(true); } function handleConfirmed(ref, dateKeys) { if (villaId !== "any") avail.addBooking(villaId, dateKeys); } const villa = VILLAS.find(v => v.id === villaId); const villaName = villa ? villa.name.join(" ") : "All residences"; const villaSub = villa ? villa.badge : "Whole estate"; return ( <>
{tabs.map(t => ( ))}
setOpen(open === "dates" ? null : "dates")}>
Arrival
{range[0] ? fmtShort(range[0]) : "Select date"}
{range[0] ? range[0].getFullYear() : "When the holiday begins"}
{open === "dates" && setOpen(null)} isUnavailable={isUnavailable} priceUSD={villa ? villa.priceUSD : 440} />}
setOpen(open === "dates" ? null : "dates")}>
Departure
{range[1] ? fmtShort(range[1]) : "Select date"}
{range[1] ? `${Math.round((range[1]-range[0])/86400000)} nights` : "Until you must return"}
setOpen(open === "guests" ? null : "guests")}>
Guests
{guestLabel()}
Ages 13 and over
{open === "guests" && setOpen(null)} maxGuests={villaId === "any" ? 16 : 4} />}
setOpen(open === "villa" ? null : "villa")}>
Residence
{villaName}
{villaSub}
{open === "villa" && setOpen(null)} availability={avail.data} range={range} />}
{showConfirm && ( setShowConfirm(false)} onConfirmed={handleConfirmed} /> )} ); } Object.assign(window, { OrnamentRow, MonthCalendar, BookingWidget, DateRangePopover, GuestPopover, VillaPopover, ConfirmReservation });