// THE MOST — booking modal flow (Fresha-style multi-step) const { useState: useStateB, useEffect: useEffectB, useMemo: useMemoB, useRef: useRefB } = React; function BookingModal({ open, onClose, initialService }) { const [step, setStep] = useStateB(0); const [service, setService] = useStateB(initialService || null); const [stylist, setStylist] = useStateB("any"); const [date, setDate] = useStateB(null); const [time, setTime] = useStateB(null); const [client, setClient] = useStateB({ name: "", phone: "", email: "", notes: "" }); const [confirmed, setConfirmed] = useStateB(false); useEffectB(() => { if (open) { if (initialService) { setService(initialService); setStep(1); } else { setStep(0); } setStylist("any"); setDate(null); setTime(null); setConfirmed(false); document.body.style.overflow = "hidden"; } else { document.body.style.overflow = ""; } return () => { document.body.style.overflow = ""; }; }, [open, initialService]); if (!open) return null; const steps = ["Service", "Stylist", "Date & time", "Your details", "Confirm"]; const next = () => setStep((s) => Math.min(s + 1, steps.length - 1)); const back = () => setStep((s) => Math.max(s - 1, 0)); const canNext = () => { if (step === 0) return !!service; if (step === 1) return !!stylist; if (step === 2) return !!date && !!time; if (step === 3) return client.name.trim() && (client.phone.trim() || client.email.trim()); return true; }; const submit = () => { setConfirmed(true); }; return (
{steps.map((s, i) => (
{String(i + 1).padStart(2, "0")} {s}
))}
{confirmed ? ( ) : ( <> {step === 0 && } {step === 1 && } {step === 2 && } {step === 3 && } {step === 4 && } )}
{!confirmed && (
{service && (
{service.name} {service.duration && {service.duration}} {service.price && {service.price}}
)}
{step === steps.length - 1 ? ( ) : ( )}
)}
); } // Step 0: choose service function StepService({ selected, onSelect }) { const sig = window.SALON_DATA.signature; const menu = window.SALON_DATA.menu; const [cat, setCat] = useStateB("Signature"); const cats = ["Signature", ...Object.keys(menu)]; const items = cat === "Signature" ? sig.map((s) => ({ id: s.id, name: s.name, duration: s.duration, price: s.price, category: s.category, })) : menu[cat].map(([n, d, p]) => ({ id: `${cat}-${n}`, name: n, duration: d, price: `AED ${p}`, category: cat, })); return (

Choose a service

Single appointment, one service. Add more after the first is held.

{cats.map((c) => ( ))}
{items.map((it) => ( ))}
); } // Step 1: stylist function StepStylist({ service, stylist, onSelect }) { const options = [ { id: "any", name: "Any available stylist", note: "We'll match you to the next free chair.", flag: "Fastest" }, { id: "alaa", name: "Alaa", note: "Named in five-star reviews.", flag: "Awaiting consent" }, { id: "noor", name: "Noor", note: "Named in five-star reviews.", flag: "Awaiting consent" }, { id: "senior", name: "Senior stylist (Colour)", note: "Specialist for balayage & complex colour.", flag: "On request" }, ]; return (

Pick a stylist

Final team names show once each stylist has approved their card. Until then, "any" is the safe pick.

{options.map((o) => ( ))}
); } // Step 2: date & time function StepWhen({ date, time, onDate, onTime }) { const today = new Date(2026, 4, 25); // May 25 2026 (anchored) const [monthOffset, setMonthOffset] = useStateB(0); const monthStart = new Date(today.getFullYear(), today.getMonth() + monthOffset, 1); const monthLabel = monthStart.toLocaleDateString("en-GB", { month: "long", year: "numeric" }); const daysInMonth = new Date(monthStart.getFullYear(), monthStart.getMonth() + 1, 0).getDate(); const firstDow = (monthStart.getDay() + 6) % 7; // Mon=0 const dates = []; for (let i = 0; i < firstDow; i++) dates.push(null); for (let d = 1; d <= daysInMonth; d++) { dates.push(new Date(monthStart.getFullYear(), monthStart.getMonth(), d)); } const isPast = (d) => d && d < new Date(today.getFullYear(), today.getMonth(), today.getDate()); const isSunday = (d) => d && d.getDay() === 0; const dateKey = (d) => d ? `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}` : null; // Pseudo-deterministic time slots const slots = useMemoB(() => { if (!date) return []; const all = []; for (let h = 10; h < 21; h++) { for (const m of [0, 30]) { const taken = ((date.getDate() * 13 + h * 7 + m) % 11) < 4; // ~36% taken all.push({ label: `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`, taken }); } } return all; }, [date]); return (

Choose a date and time

Live availability syncs with Fresha at confirmation. Sundays are marked as confirming until the salon resolves listed hours.

{monthLabel}
{["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map((d) => ( {d} ))}
{dates.map((d, i) => { if (!d) return ; const past = isPast(d); const sunday = isSunday(d); const selected = dateKey(d) === dateKey(date); return ( ); })}
Selected Sunday — confirming Past
{date ? date.toLocaleDateString("en-GB", { weekday: "long", day: "numeric", month: "long" }) : "Pick a date first"}
{date &&
{slots.filter((s) => !s.taken).length} slots open
}
{date ? (
{slots.map((s, i) => ( ))}
) : (
Choose a date on the left to see open slots.
)}
); } // Step 3: client details function StepDetails({ value, onChange }) { const set = (k, v) => onChange({ ...value, [k]: v }); return (

Your details

A first name is enough; we’ll only call if a stylist needs to confirm a colour consultation.