/* ===========================================================
Halvorsen — Locations + FAQ + Contact + Booking modal + Footer
=========================================================== */
const LOCATIONS = [
{
city: "London",
addr: "33 Grosvenor Place\nLondon SW1X 7HY",
days: "Mon · Tue · Thu",
consult: "Cleveland Clinic London",
surgery: "Cleveland Clinic London",
tel: "+44 (0)20 7946 0214",
motif: "knee",
},
{
city: "Oslo",
addr: "Hansteens gate 5\n0253 Oslo, Norway",
days: "Wed · Fri",
consult: "Volvat Oslo",
surgery: "Volvat Oslo",
tel: "+47 22 95 75 00",
motif: "shoulder",
},
{
city: "Telemedicine",
addr: "Worldwide\nSecond opinions only",
days: "Friday afternoons",
consult: "Encrypted video",
surgery: "—",
tel: "—",
motif: "hip",
},
];
const Locations = () => {
const [active, setActive] = React.useState(0);
const cur = LOCATIONS[active];
const Motif = { knee: KneeMotif, shoulder: ShoulderMotif, hip: HipMotif }[cur.motif];
return (
Two clinics, one standard .
{LOCATIONS.map((l, i) => (
setActive(i)}
tabIndex={0}
onKeyDown={(e) => { if (e.key === "Enter") setActive(i); }}
style={{
padding: "32px 24px",
borderTop: "1px solid var(--line)",
borderBottom: i === LOCATIONS.length - 1 ? "1px solid var(--line)" : "none",
background: active === i ? "var(--bone)" : "transparent",
cursor: "pointer",
display: "grid",
gridTemplateColumns: "60px 1fr auto",
alignItems: "baseline",
transition: "background 280ms var(--ease)",
}}
>
{String(i + 1).padStart(2, "0")}
{l.days}
))}
);
};
/* ---------- FAQ ---------- */
const FAQS = [
{ q: "How quickly can I be seen?", a: "Initial consultations are typically available within 7–10 days in London and 3–5 days in Oslo. Urgent sports injuries are triaged within 48 hours." },
{ q: "Do you take insurance?", a: "Yes — Bupa, AXA Health, Aviva, Vitality, WPA, Cigna, Allianz Care, and most international policies. We provide pre-authorisation paperwork on your behalf." },
{ q: "Will my surgeon perform my surgery?", a: "Always. Dr. Halvorsen performs every operation she books. Trainees may assist; nobody else is the lead." },
{ q: "What about second opinions?", a: "Strongly encouraged. Friday telemedicine slots are reserved exclusively for second opinions on existing recommendations." },
{ q: "Do you publish your data?", a: "Yes. The full registry — including complications, revisions, and PROMs — is updated quarterly and available on request." },
{ q: "What is the cost?", a: "Initial consultation is £280 / NOK 3,400. Surgery is quoted in writing after consultation, with itemised theatre, implant and rehabilitation fees." },
];
const FAQ = () => {
const [open, setOpen] = React.useState(0);
return (
What patients actually ask.
{FAQS.map((f, i) => (
setOpen(open === i ? -1 : i)}
aria-expanded={open === i}
style={{ width: "100%", textAlign: "left", padding: "28px 0", background: "transparent", border: "none", cursor: "pointer", color: "var(--ink)", display: "grid", gridTemplateColumns: "40px 1fr 30px", gap: 16, alignItems: "baseline" }}
>
{String(i + 1).padStart(2, "0")}
{f.q}
+
))}
);
};
/* ---------- CONTACT / REFERRAL FORM ---------- */
const Contact = ({ onBook }) => (
);
const Detail = ({ k, v }) => (
);
const Field = ({ label, id, textarea, type = "text", placeholder }) => (
{label}
{textarea ? (
) : (
)}
);
const RadioGroup = ({ label, name, options }) => {
const [val, setVal] = React.useState(null);
return (
{label}
{options.map((o) => (
setVal(o)} style={{ position: "absolute", opacity: 0, pointerEvents: "none" }} />
{o}
))}
);
};
/* ---------- BOOKING MODAL ---------- */
const BookingModal = ({ open, onClose }) => {
const [step, setStep] = React.useState(0);
const [data, setData] = React.useState({ region: null, clinic: null, when: null, name: "", email: "" });
const dialogRef = React.useRef(null);
React.useEffect(() => {
if (!open) return;
setStep(0);
setData({ region: null, clinic: null, when: null, name: "", email: "" });
const onKey = (e) => { if (e.key === "Escape") onClose(); };
document.addEventListener("keydown", onKey);
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", onKey);
document.body.style.overflow = "";
};
}, [open, onClose]);
if (!open) return null;
const total = 4;
const next = () => setStep((s) => Math.min(total, s + 1));
const prev = () => setStep((s) => Math.max(0, s - 1));
return (
{ if (e.target === e.currentTarget) onClose(); }}
>
STEP {step + 1} / {total + 1}
×
{step === 0 && (
setData({ ...data, region: v })} options={["Knee", "Shoulder", "Hip", "Foot / Ankle", "Spine", "Unsure"]} />
)}
{step === 1 && (
setData({ ...data, clinic: v })} options={["London", "Oslo", "Telemedicine (second opinion only)"]} />
)}
{step === 2 && (
setData({ ...data, when: v })} options={["This week", "Next 2 weeks", "This month", "I'm flexible"]} />
)}
{step === 3 && (
)}
{step === 4 && (
Reference
HC-{Math.floor(Math.random() * 9000 + 1000)}
We'll be in touch shortly with available consultation slots. If you've not heard from us by tomorrow afternoon, please call the clinic directly.
)}
← Back
{step === total ? "Done" : step === total - 1 ? "Submit" : "Continue"}
);
};
const BookStep = ({ title, subtitle, children }) => (
{title}
{subtitle}
{children}
);
const ChipRow = ({ options, value, onChange }) => (
{options.map((o) => (
onChange(o)}
style={{
padding: "12px 20px",
borderRadius: 999,
border: "1px solid var(--line-strong)",
background: value === o ? "var(--ink)" : "transparent",
color: value === o ? "var(--paper)" : "var(--ink)",
fontSize: 14,
fontFamily: "var(--sans)",
cursor: "pointer",
transition: "all 220ms var(--ease)",
}}
>
{o}
))}
);
/* ---------- FOOTER ---------- */
const Footer = () => (
Consultant orthopaedic surgery — joint preservation, sports injury and complex revision. Practising in London and Oslo.
© 2009–2026 HALVORSEN ORTHOPAEDIC LTD · GMC 6041273 · CQC REGISTERED
PRIVACY · COOKIES · TERMS · ACCESSIBILITY
);
const FooterCol = ({ title, links }) => (
{title}
{links.map((l) => (
{l}
))}
);
Object.assign(window, { Locations, FAQ, Contact, BookingModal, Footer });