/* global React */
// ============================================================
// BOOKING — multi-step form (treatment → date/time → details → confirm)
// ============================================================
const BookPage = ({ initialTreatment }) => {
const [step, setStep] = React.useState(0);
const [data, setData] = React.useState({
treatment: initialTreatment || "",
practitioner: "any",
date: "",
time: "",
name: "",
phone: "",
email: "",
notes: "",
consent: false,
channel: "form",
});
const [errors, setErrors] = React.useState({});
const [confetti, setConfetti] = React.useState(false);
const [reference, setReference] = React.useState("");
const setField = (k, v) => {
setData(d => ({ ...d, [k]: v }));
setErrors(e => ({ ...e, [k]: undefined }));
};
const steps = [
{ label: "Treatment", short: "01" },
{ label: "Date & time", short: "02" },
{ label: "Your details", short: "03" },
{ label: "Confirm", short: "04" },
];
const validate = (s) => {
const e = {};
if (s === 0) {
if (!data.treatment) e.treatment = "Choose a treatment, or “Not sure yet”.";
}
if (s === 1) {
if (!data.date) e.date = "Pick a date.";
if (!data.time) e.time = "Pick a time.";
}
if (s === 2) {
if (!data.name.trim()) e.name = "Full name please.";
if (!/^[+\d\s]{8,}$/.test(data.phone)) e.phone = "A reachable number, please.";
if (data.email && !/^\S+@\S+\.\S+$/.test(data.email)) e.email = "That email doesn't look right.";
if (!data.consent) e.consent = "We need your consent to contact you.";
}
setErrors(e);
return Object.keys(e).length === 0;
};
const next = () => {
if (!validate(step)) return;
if (step === steps.length - 1) {
// Submit
const ref = "CC-" + Math.random().toString(36).slice(2, 7).toUpperCase();
setReference(ref);
setConfetti(true);
setStep(step + 1);
window.scrollTo({ top: 0, behavior: "smooth" });
return;
}
setStep(step + 1);
};
const back = () => setStep(Math.max(0, step - 1));
// Date options: next 14 days
const dateOptions = React.useMemo(() => {
const out = [];
const now = new Date();
for (let i = 1; i <= 14; i++) {
const d = new Date(now);
d.setDate(now.getDate() + i);
out.push({
value: d.toISOString().slice(0, 10),
day: d.toLocaleDateString("en-GB", { weekday: "short" }),
dd: d.toLocaleDateString("en-GB", { day: "2-digit" }),
mon: d.toLocaleDateString("en-GB", { month: "short" }),
sunday: d.getDay() === 0,
});
}
return out;
}, []);
const times = ["10:00", "11:00", "12:00", "14:00", "15:00", "16:00", "17:00", "18:00", "19:00", "20:00"];
const treatmentOptions = [
{ value: "", label: "Not sure yet — recommend for me" },
...window.TREATMENTS.map(t => ({ value: t.slug, label: t.title })),
];
const treatmentLabel = treatmentOptions.find(t => t.value === data.treatment)?.label || "Not sure yet";
// Final success screen
if (step === steps.length) {
return (
{confetti && }
Consultation requested .
Thank you, {data.name.split(" ")[0]} . We'll WhatsApp you on {data.phone} within the next clinic hour to confirm your slot.
Your reference
{reference}
);
}
return (
Book a consultation
Free 15-minute consultation .
No payment now. We'll confirm your slot by WhatsApp.
{/* Stepper */}
{steps.map((s, i) => {
const active = i === step;
const done = i < step;
return (
{done ? : s.short}
{s.label}
);
})}
{step === 0 && (
)}
{step === 1 && (
)}
{step === 2 && (
)}
{step === 3 && (
)}
← Back
{step === steps.length - 1 ? "Confirm request" : "Continue"}
{/* Side option */}
);
};
// ============================================================
// Step 1: treatment
// ============================================================
const StepTreatment = ({ data, setField, errors, options }) => (
What brings you in?
Choose a treatment category, or pick "Not sure yet" and we'll match you with the right doctor.
{options.map(opt => {
const selected = data.treatment === opt.value;
return (
setField("treatment", opt.value)}
style={{
padding: "20px 18px",
borderRadius: 12,
background: selected ? "var(--espresso)" : "var(--ivory)",
color: selected ? "var(--ivory)" : "var(--espresso)",
border: selected ? "1px solid var(--espresso)" : "1px solid var(--line)",
cursor: "pointer",
textAlign: "left",
fontFamily: "var(--f-display)",
fontSize: 19,
lineHeight: 1.3,
transition: "all 180ms",
}}>
{opt.label}
);
})}
Preferred practitioner
{[{ value: "any", label: "First available" }, ...window.PRACTITIONERS.map(p => ({ value: p.name, label: p.name }))].map(p => {
const selected = data.practitioner === p.value;
return (
setField("practitioner", p.value)}
style={{
padding: "10px 18px",
borderRadius: 999,
background: selected ? "var(--gold)" : "transparent",
color: selected ? "var(--charcoal)" : "var(--espresso)",
border: selected ? "1px solid var(--gold)" : "1px solid var(--line-strong)",
cursor: "pointer",
fontSize: 13,
}}>
{p.label}
);
})}
);
// ============================================================
// Step 2: date & time
// ============================================================
const StepDate = ({ data, setField, errors, dateOptions, times }) => (
When works for you?
Pick a date and a rough time — we'll confirm an exact slot when we WhatsApp you back.
Date
{dateOptions.map(d => {
const selected = data.date === d.value;
return (
!d.sunday && setField("date", d.value)}
disabled={d.sunday}
style={{
padding: "12px 8px",
borderRadius: 10,
background: selected ? "var(--espresso)" : d.sunday ? "transparent" : "var(--ivory)",
color: selected ? "var(--ivory)" : d.sunday ? "var(--line-strong)" : "var(--espresso)",
border: selected ? "1px solid var(--espresso)" : "1px solid var(--line)",
cursor: d.sunday ? "not-allowed" : "pointer",
textAlign: "center",
transition: "all 160ms",
opacity: d.sunday ? 0.5 : 1,
}}>
{d.day}
{d.dd}
{d.sunday ? "Closed" : d.mon}
);
})}
{errors.date &&
{errors.date}
}
Time
{times.map(t => {
const selected = data.time === t;
return (
setField("time", t)}
style={{
padding: "12px 8px",
borderRadius: 10,
background: selected ? "var(--gold)" : "var(--ivory)",
color: selected ? "var(--charcoal)" : "var(--espresso)",
border: selected ? "1px solid var(--gold)" : "1px solid var(--line)",
cursor: "pointer",
fontFamily: "var(--f-display)",
fontSize: 18,
transition: "all 160ms",
}}>
{t}
);
})}
{errors.time &&
{errors.time}
}
);
// ============================================================
// Step 3: details
// ============================================================
const StepDetails = ({ data, setField, errors }) => (
Your details
Just enough so we can confirm your slot. Nothing else.
setField("consent", e.target.checked)}
style={{ marginTop: 3, accentColor: "var(--gold)" }}/>
I consent to Covent Clinic contacting me about my consultation request by phone, SMS, WhatsApp or email. I understand that all treatments are subject to a clinical assessment.
{errors.consent &&
{errors.consent}
}
);
// ============================================================
// Step 4: confirm
// ============================================================
const StepConfirm = ({ data, treatmentLabel }) => (
Almost done.
Have a quick look — we'll send a confirmation to WhatsApp.
{data.email && }
{data.notes && }
Submitting this request does not confirm your booking. A team member will WhatsApp you within the next clinic hour to finalise the slot. The consultation itself is free.
);
const SummaryRow = ({ label, value }) => (
);
// ============================================================
// Confetti (gold + blush + ivory dots that fall)
// ============================================================
const Confetti = () => {
const colors = ["#C9A24B", "#E4CE9A", "#E7C9C2", "#FCFAF4", "#3B2C24"];
const pieces = React.useMemo(() => Array.from({ length: 80 }).map((_, i) => ({
id: i,
left: Math.random() * 100,
delay: Math.random() * 0.6,
duration: 2.4 + Math.random() * 1.6,
color: colors[Math.floor(Math.random() * colors.length)],
size: 6 + Math.random() * 6,
rot: Math.random() * 360,
})), []);
return (
{pieces.map(p => (
))}
);
};
Object.assign(window, { BookPage });