// 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 (
{confirmed ? (
) : (
<>
{step === 0 && }
{step === 1 && }
{step === 2 && }
{step === 3 && }
{step === 4 && }
>
)}
{!confirmed && (
)}
);
}
// 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 (
);
}
// Step 4: review
function StepReview({ service, stylist, date, time, client }) {
const stylistLabel = {
any: "Any available stylist",
alaa: "Alaa",
noor: "Noor",
senior: "Senior stylist (Colour)",
}[stylist];
return (
Looks right?
No card needed yet — Fresha will request payment details only at the chair, or for colour deposits if applicable.
{service?.name}
{service?.price}
- Stylist{stylistLabel}
- Date{date?.toLocaleDateString("en-GB", { weekday: "long", day: "numeric", month: "long" })}
- Time{time}
- Duration{service?.duration}
- For{client.name || "—"}
- Contact{client.phone || client.email || "—"}
{client.notes && (
)}
By reserving you agree to the salon’s 4-hour cancellation window. We’ll text you a reminder the day before.
);
}
// Confirmed screen
function BookingConfirmed({ service, stylist, date, time, client, onClose }) {
const ref = useMemoB(() => "TMD-" + String(Math.floor(Math.random() * 90000) + 10000), []);
return (
Meet yourself on {date?.toLocaleDateString("en-GB", { day: "numeric", month: "long" })} at {time}.
Your chair is on hold for fifteen minutes while Fresha confirms. A reminder will reach
you the day before — and the kettle will already be on.
Reference{ref}
Service{service?.name}
For{client.name}
WhereBldg 6 · d3 · Dubai
Need to change? Call {window.SALON_DATA.contact.landline} — we answer Mon–Sat.
);
}
Object.assign(window, { BookingModal });