/* Shared components: Nav, Footer, FAB, BottomBar, BookingModal, Lightbox */
const { useState, useEffect, useRef, useMemo } = React;
const D = window.BLO_DATA;
const IMG = (p) => `media/images/${p}`;
// --- Stars row ---
const Stars = ({ n = 5 }) => (
{"★★★★★".slice(0, n)}{"☆☆☆☆☆".slice(0, 5 - n)}
);
// --- Nav ---
function Navbar({ route, setRoute, openBooking }) {
const [open, setOpen] = useState(false);
const links = [
{ id: "home", label: "Home" },
{ id: "services", label: "Services" },
{ id: "memberships", label: "Offers" },
{ id: "gallery", label: "Gallery" },
{ id: "team", label: "Team" },
{ id: "reviews", label: "Reviews" },
{ id: "book", label: "Visit" },
];
const go = (r) => { setRoute(r); setOpen(false); window.scrollTo({ top: 0, behavior: "smooth" }); };
return (
<>
BloBeauty Lounge
{links.map(l => (
))}
WhatsApp
>
);
}
// --- WhatsApp FAB ---
function WhatsAppFab() {
return (
);
}
// --- Mobile bottom action bar ---
function BottomBar({ openBooking }) {
return (
);
}
// --- Footer ---
function Footer({ setRoute, openBooking }) {
return (
);
}
// --- Booking modal (Fresha-style flow simulation) ---
function BookingModal({ open, onClose, preselect }) {
const [step, setStep] = useState(0);
const [service, setService] = useState(null);
const [staff, setStaff] = useState(null);
const [date, setDate] = useState(0);
const [time, setTime] = useState(null);
useEffect(() => {
if (open) {
setStep(0); setService(preselect || null); setStaff(null); setDate(0); setTime(null);
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => { document.body.style.overflow = ""; };
}, [open, preselect]);
if (!open) return null;
const services = [
{ id: "blowdry", name: "Blowdry & Style", meta: "45 min · from AED — (Fresha)" },
{ id: "moroccan", name: "Moroccan Bath", meta: "45 min · AED 110.25 (H/H)" },
{ id: "lash-lift", name: "Lash Lifting", meta: "60 min · AED 162.75 (H/H)" },
{ id: "lash-ext", name: "Lash Extensions", meta: "2 hr · AED 159 (promo)" },
{ id: "nails-fs", name: "Nails — Full Set", meta: "2 hr · AED 149 (promo)" },
{ id: "combo", name: "Classic Combo", meta: "2 hr · AED 156.50 (H/H)" },
{ id: "colour", name: "Hair Colour", meta: "2–3 hr · Price on Fresha" },
{ id: "facial", name: "Facial", meta: "60 min · Price on Fresha" },
];
const staffOptions = [
{ id: "any", name: "Any available", meta: "We'll match you" },
...D.team.slice(0, 6).map(t => ({ id: t.name.toLowerCase(), name: t.name, meta: `${t.role} · ${t.rating}★` })),
];
const dates = useMemo(() => {
const arr = [];
const today = new Date("2026-05-22");
const dow = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
for (let i = 0; i < 10; i++) {
const d = new Date(today.getFullYear(), today.getMonth(), today.getDate() + i);
arr.push({ dow: dow[d.getDay()], day: d.getDate(), full: d });
}
return arr;
}, []);
const times = ["10:00","10:30","11:00","11:30","12:00","12:30","13:00","13:30","14:00","14:30","15:00","15:30","16:00","16:30","17:00","17:30","18:00","18:30","19:00","19:30"];
// pseudo-busy slots vary by date
const busy = useMemo(() => {
const seed = (date * 7) % 19;
return new Set([(seed) % 20, (seed + 5) % 20, (seed + 11) % 20, (seed + 14) % 20].map(i => times[i]));
}, [date]);
const canNext = [service, staff, time, true][step];
const stepTitles = ["Choose a service", "Choose a specialist", "Pick a time", "Confirm booking"];
const next = () => setStep(s => Math.min(s + 1, 3));
const back = () => setStep(s => Math.max(s - 1, 0));
const confirm = () => setStep(4); // success
return (
e.stopPropagation()}>
{step < 4 ? stepTitles[step] : "Booking received"}
{step < 4 && (
{[0,1,2,3].map(i => )}
)}
{step === 0 && (
{services.map(s => (
))}
)}
{step === 1 && (
{staffOptions.map(s => (
))}
)}
{step === 2 && (
<>
{dates.map((d, i) => (
))}
{times.map(t => (
))}
Salon hours daily 09:00 – 21:00. Times in GST.
>
)}
{step === 3 && service && staff && time && (
<>
Service{service.name}
{service.meta}
Specialist{staff.name}
When{dates[date].dow} {dates[date].day} May · {time}
WhereVilla 710
Jumeirah Beach Rd, Umm Suqeim 2
MVP wires this confirmation to Fresha's deep-link; Phase 2 captures payment & pre-auth natively.
>
)}
{step === 4 && (
✓
See you soon.
We've held your slot. A confirmation will land on WhatsApp.
Booking ref
BLO–{(Math.random()*9000+1000)|0}
)}
{step > 0 && step < 4 && }
{step === 0 && Step 1 of 4}
{step === 4
?
: (step < 3
?
: )
}
);
}
// --- Lightbox ---
function Lightbox({ images, index, onClose, setIndex }) {
useEffect(() => {
const onKey = (e) => {
if (e.key === "Escape") onClose();
if (e.key === "ArrowRight") setIndex((i) => (i + 1) % images.length);
if (e.key === "ArrowLeft") setIndex((i) => (i - 1 + images.length) % images.length);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [images.length, onClose, setIndex]);
if (index === null) return null;
return (
})
e.stopPropagation()} />
);
}
// expose
Object.assign(window, { Navbar, Footer, WhatsAppFab, BottomBar, BookingModal, Lightbox, Stars, IMG, D });