/* Lead funnel modals: Register Interest (3-step), Brochure (gated),
Private Viewing, Broker Enquiry. */
const { useState, useEffect, useMemo, useCallback } = React;
// ============ shared modal frame ============
function ModalFrame({ title, subtitle, onClose, step, totalSteps, children, footer }) {
useEffect(() => {
const onKey = (e) => { if (e.key === "Escape") onClose(); };
window.addEventListener("keydown", onKey);
document.body.style.overflow = "hidden";
return () => { window.removeEventListener("keydown", onKey); document.body.style.overflow = ""; };
}, [onClose]);
return (
{ if (e.target.classList.contains("modal-bg")) onClose(); }}>
{step != null && (
Step {step} of {totalSteps}
)}
{title}
{subtitle &&
{subtitle}
}
×
{step != null && (
{Array.from({ length: totalSteps }).map((_, i) => (
))}
)}
{children}
{footer &&
{footer}
}
);
}
function SecureLabel() {
return (
Encrypted · Routed by region · No call centre
);
}
// ============ REGISTER INTEREST ============
function RegisterInterestModal({ project, onClose }) {
const [step, setStep] = useState(1);
const [data, setData] = useState({
region: "AE",
firstName: "",
lastName: "",
email: "",
dial: "+971",
phone: "",
motivation: "investor",
timeframe: "3-months",
budget: "",
consent: false,
whatsappOk: true,
});
const [errors, setErrors] = useState({});
const [done, setDone] = useState(null);
const update = (k, v) => { setData({ ...data, [k]: v }); setErrors({ ...errors, [k]: null }); };
const validateStep = () => {
const e = {};
if (step === 1) {
if (!data.region) e.region = "Required";
}
if (step === 2) {
if (!data.firstName.trim()) e.firstName = "Required";
if (!data.lastName.trim()) e.lastName = "Required";
if (!data.email.match(/^[^@\s]+@[^@\s]+\.[^@\s]+$/)) e.email = "Email format";
if (!data.phone.match(/^[0-9 \-]{6,}$/)) e.phone = "Phone format";
}
if (step === 3) {
if (!data.consent) e.consent = "Required";
}
setErrors(e);
return Object.keys(e).length === 0;
};
const next = () => { if (validateStep()) setStep(step + 1); };
const submit = () => {
if (!validateStep()) return;
const ref = "REG-" + Math.random().toString(36).slice(2, 8).toUpperCase();
const region = REGIONS.find(r => r.code === data.region);
setDone({ ref, region });
};
if (done) {
return (
✓
Routed to {done.region.desk}.
Your enquiry for {project.name} is queued with the {done.region.label} desk.
A relationship manager will reach you on the phone or WhatsApp number you provided within one business day.
Reference · {done.ref}
);
}
return (
{step > 1 && setStep(step - 1)}>Back }
{step < 3 ? (
Continue
) : (
Submit enquiry
)}
>
}
>
{step === 1 && (
<>
Binghatti routes leads to a real regional desk — UAE, KSA, UK or China. Pick the country you'd like to be contacted from.
{REGIONS.map((r) => (
update("region", r.code)}>
{r.flag}
{r.label}
))}
{REGIONS.find(r => r.code === data.region).desk}
{REGIONS.find(r => r.code === data.region).line} · {REGIONS.find(r => r.code === data.region).email}
>
)}
{step === 2 && (
<>
First name *
update("firstName", e.target.value)} placeholder="Hessa" />
{errors.firstName &&
{errors.firstName}
}
Last name *
update("lastName", e.target.value)} placeholder="Al Maktoum" />
{errors.lastName &&
{errors.lastName}
}
Email *
update("email", e.target.value)} placeholder="you@domain.com" />
{errors.email &&
{errors.email}
}
Phone (WhatsApp-capable preferred) *
update("dial", e.target.value)}>
{COUNTRY_DIAL_CODES.map(c => {c.label} )}
update("phone", e.target.value)} placeholder="50 123 4567" />
{errors.phone &&
{errors.phone}
}
>
)}
{step === 3 && (
<>
I am
update("motivation", e.target.value)}>
Investor — yield / capital appreciation
End-user — personal residence
Family office / advisor on behalf of buyer
Licensed broker
Timeframe
update("timeframe", e.target.value)}>
Within 3 months
3 – 6 months
6 – 12 months
Exploring
Budget (indicative, optional)
update("budget", e.target.value)}>
Prefer not to say
AED 1M – 3M
AED 3M – 5M
AED 5M – 10M
AED 10M+
update("whatsappOk", e.target.checked)} />
Contact me on WhatsApp as well as email. (Recommended — fastest response from the regional desk.)
update("consent", e.target.checked)} />
I consent to my details being shared with the routed regional sales desk for the purpose of this enquiry only.
I understand that all project specifications, pricing, statuses, areas and handovers are provided through the signed brochure
and are not represented as audited facts on this campaign page. *
{errors.consent && Consent required
}
Summary
{project.name} · {REGIONS.find(r => r.code === data.region).desk}
{data.firstName} {data.lastName} · {data.email}
{data.dial} {data.phone}
>
)}
);
}
// ============ BROCHURE GATE ============
function BrochureModal({ project, onClose }) {
const [data, setData] = useState({ name: "", email: "", region: "AE", consent: false });
const [errors, setErrors] = useState({});
const [stage, setStage] = useState("form"); // form | sending | done
const [progress, setProgress] = useState(0);
const submit = () => {
const e = {};
if (!data.name.trim()) e.name = "Required";
if (!data.email.match(/^[^@\s]+@[^@\s]+\.[^@\s]+$/)) e.email = "Email format";
if (!data.consent) e.consent = "Consent required";
setErrors(e);
if (Object.keys(e).length) return;
setStage("sending");
let p = 0;
const t = setInterval(() => {
p += 7 + Math.random() * 10;
if (p >= 100) { p = 100; clearInterval(t); setTimeout(() => setStage("done"), 250); }
setProgress(p);
}, 110);
};
if (stage === "sending") {
return (
Building tailored PDF · {Math.floor(progress)}%
· Verifying email…
35 ? 1 : .3 }}>· Routing to {REGIONS.find(r => r.code === data.region).desk}…
65 ? 1 : .3 }}>· Compiling project pack…
90 ? 1 : .3 }}>· Attaching brochure PDF…
);
}
if (stage === "done") {
return (
✓
Brochure on the way.
We've sent the {project.name} brochure pack to {data.email} .
The {REGIONS.find(r => r.code === data.region).label} desk is copied — expect a follow-up within one business day.
Pack ID · BRO-{Math.random().toString(36).slice(2, 8).toUpperCase()}
Continue browsing
);
}
return (
Send brochure
>
}
>
The brochure is released by email — not as an anonymous download. We share campaign-specific PDFs with verified contacts so we can follow up properly.
Full name *
{ setData({ ...data, name: e.target.value }); setErrors({ ...errors, name: null }); }} placeholder="Hessa Al Maktoum" />
{errors.name &&
{errors.name}
}
Work email *
{ setData({ ...data, email: e.target.value }); setErrors({ ...errors, email: null }); }} placeholder="you@domain.com" />
{errors.email &&
{errors.email}
}
Region (routing only)
{REGIONS.map((r) => (
setData({ ...data, region: r.code })}>
{r.flag}
{r.label}
))}
{ setData({ ...data, consent: e.target.checked }); setErrors({ ...errors, consent: null }); }} />
I consent to be contacted by the routed desk for this brochure request. No spec on this page is a contractual figure. *
{errors.consent && {errors.consent}
}
);
}
// ============ PRIVATE VIEWING ============
function ViewingModal({ project, onClose }) {
const [data, setData] = useState({
mode: "experience",
name: "", email: "", phone: "", dial: "+971",
date: null, slot: null, region: "AE", consent: false,
});
const [errors, setErrors] = useState({});
const [done, setDone] = useState(false);
// Generate next 14 days
const dates = useMemo(() => {
const out = [];
const today = new Date();
for (let i = 1; i <= 12; i++) {
const d = new Date(today);
d.setDate(today.getDate() + i);
out.push({
iso: d.toISOString().slice(0, 10),
weekday: ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"][d.getDay()],
day: d.getDate(),
});
}
return out;
}, []);
const slots = ["10:00", "11:30", "13:00", "14:30", "16:00", "17:30"];
const submit = () => {
const e = {};
if (!data.name.trim()) e.name = "Required";
if (!data.email.match(/^[^@\s]+@[^@\s]+\.[^@\s]+$/)) e.email = "Email format";
if (!data.phone.match(/^[0-9 \-]{6,}$/)) e.phone = "Phone format";
if (!data.date) e.date = "Pick a date";
if (!data.slot) e.slot = "Pick a slot";
if (!data.consent) e.consent = "Required";
setErrors(e);
if (Object.keys(e).length === 0) setDone(true);
};
if (done) {
return (
✓
Viewing confirmed.
We've reserved {data.date} at {data.slot} for {project.name} ({data.mode === "experience" ? "Experience Centre" : "Video walk-through"}).
A calendar invite is on its way to {data.email} .
Booking · VW-{Math.random().toString(36).slice(2, 8).toUpperCase()}
Close
);
}
return (
Confirm booking
>
}
>
{[
{ v: "experience", t: "Experience Centre", s: "In person · Dubai" },
{ v: "video", t: "Video walk-through", s: "Zoom / WhatsApp · 45 min" },
].map(o => (
setData({ ...data, mode: o.v })}
style={{
background: data.mode === o.v ? "var(--text)" : "var(--surface)",
color: data.mode === o.v ? "var(--bg)" : "var(--text)",
border: "1px solid " + (data.mode === o.v ? "var(--text)" : "var(--border)"),
padding: "18px 16px", textAlign: "start", cursor: "pointer",
}}>
{o.s}
{o.t}
))}
Date
{dates.map(d => (
setData({ ...data, date: d.iso })}>
{d.weekday}
{d.day}
))}
{errors.date &&
{errors.date}
}
Time slot
{slots.map((s, i) => (
setData({ ...data, slot: s })}>{s}
))}
{errors.slot &&
{errors.slot}
}
Name *
setData({ ...data, name: e.target.value })}/>
{errors.name &&
{errors.name}
}
Email *
setData({ ...data, email: e.target.value })}/>
{errors.email &&
{errors.email}
}
Phone *
setData({ ...data, dial: e.target.value })}>
{COUNTRY_DIAL_CODES.map(c => {c.label} )}
setData({ ...data, phone: e.target.value })}/>
{errors.phone &&
{errors.phone}
}
setData({ ...data, consent: e.target.checked })}/>
I understand finishes, layouts and pricing shown at the viewing remain subject to the signed brochure and SPA.
{errors.consent && {errors.consent}
}
);
}
// ============ BROKER ENQUIRY ============
function BrokerModal({ project, onClose }) {
const [data, setData] = useState({ company: "", license: "", name: "", email: "", phone: "", dial: "+971", consent: false });
const [errors, setErrors] = useState({});
const [done, setDone] = useState(false);
const submit = () => {
const e = {};
if (!data.company.trim()) e.company = "Required";
if (!data.license.trim()) e.license = "RERA broker number required";
if (!data.name.trim()) e.name = "Required";
if (!data.email.match(/^[^@\s]+@[^@\s]+\.[^@\s]+$/)) e.email = "Email format";
if (!data.consent) e.consent = "Required";
setErrors(e);
if (Object.keys(e).length === 0) setDone(true);
};
if (done) {
return (
✓
Broker desk notified.
Your licensed-broker enquiry for {project.name} is now with the Binghatti broker desk. They handle commercial terms separately from the buyer funnel.
Broker enquiry · BRK-{Math.random().toString(36).slice(2, 8).toUpperCase()}
Close
);
}
return (
Submit
>
}
>
Brokerage company *
setData({ ...data, company: e.target.value })}/>
{errors.company &&
{errors.company}
}
RERA broker no. *
setData({ ...data, license: e.target.value })}/>
{errors.license &&
{errors.license}
}
Your name *
setData({ ...data, name: e.target.value })}/>
{errors.name &&
{errors.name}
}
Work email *
setData({ ...data, email: e.target.value })}/>
{errors.email &&
{errors.email}
}
setData({ ...data, consent: e.target.checked })}/>
I confirm I'm a RERA-licensed broker; commercial terms (commission, lead handling) are agreed separately with the Binghatti broker desk and are not represented on this page.
{errors.consent && {errors.consent}
}
);
}
// expose
Object.assign(window, { RegisterInterestModal, BrochureModal, ViewingModal, BrokerModal });