/* global React, Arrow, BedIcon, BathIcon, AreaIcon, WhatsAppIcon, PhoneIcon */ // ============================================================ // Espace — forms + cards // ============================================================ // ── Generic form runner ─────────────────────────────────────── function useForm(initial, validators = {}) { const [values, setValues] = React.useState(initial); const [errors, setErrors] = React.useState({}); const [submitted, setSubmitted] = React.useState(false); const set = (k, v) => setValues(prev => ({ ...prev, [k]: v })); const validate = () => { const errs = {}; for (const k of Object.keys(validators)) { const msg = validators[k](values[k], values); if (msg) errs[k] = msg; } setErrors(errs); return Object.keys(errs).length === 0; }; const submit = (e) => { e.preventDefault(); if (validate()) setSubmitted(true); }; return { values, errors, submitted, set, submit }; } const required = (v) => (!v || (typeof v === "string" && !v.trim())) ? "Required" : ""; const emailRe = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const validEmail = (v) => !v ? "Required" : !emailRe.test(v) ? "Enter a valid email" : ""; const validPhone = (v) => !v ? "Required" : (v.replace(/[^0-9]/g, "").length < 7) ? "Enter a valid phone" : ""; // ── Field ───────────────────────────────────────────────────── function Field({ label, error, children, full }) { return (
{children} {error &&
{error}
}
); } // ── Viewing form ────────────────────────────────────────────── function ViewingForm({ dark, communityId, agentId, listingRef, compact }) { const f = useForm( { name: "", phone: "", email: "", listingRef: listingRef || "", date: "", time: "", msg: "", _gotcha: "" }, { name: required, phone: validPhone, email: validEmail } ); if (f.submitted) return ; return (
f.set("_gotcha", e.target.value)} />
f.set("name", e.target.value)} placeholder="Jane Doe" /> f.set("phone", e.target.value)} placeholder="+971 …" />
f.set("email", e.target.value)} placeholder="you@example.com" /> {!compact && (
f.set("date", e.target.value)} />
)} {listingRef && ( f.set("listingRef", e.target.value)} readOnly /> )}
WhatsApp instead
Leads route to the CRM and your consultant. We never share your details. Fields marked * are required.
); } // ── Valuation form ──────────────────────────────────────────── function ValuationForm({ dark }) { const f = useForm( { name: "", phone: "", email: "", type: "Villa", community: "", beds: "", size: "", purpose: "Sell", _gotcha: "" }, { name: required, phone: validPhone, email: validEmail } ); if (f.submitted) return ; return (
f.set("name", e.target.value)} placeholder="Jane Doe" /> f.set("phone", e.target.value)} placeholder="+971 …" />
f.set("email", e.target.value)} placeholder="you@example.com" />
f.set("community", e.target.value)} placeholder="e.g. Arabian Ranches – Saheel" />
f.set("size", e.target.value)} placeholder="e.g. 4,200" />
WhatsApp
We never compute or display a figure online. A consultant will come back with a realistic range — no bots, no guesswork.
); } // ── Success panel ───────────────────────────────────────────── function SuccessPanel({ dark, title, body, whatsapp, kind }) { const msg = encodeURIComponent(`Hi Espace, I just requested a ${kind}.`); return (

{title}

{body}

Confirm on WhatsApp Call instead
); } // ── Listing card (placeholder feed) ─────────────────────────── function ListingCard({ listing, onBook }) { const statusLabel = { sale: "For sale", rent: "For rent", luxury: "Luxury", offplan: "Off-plan" }[listing.status] || listing.status; return (
onBook && onBook(listing)}>
{statusLabel}
Photo loads from feed Property image · {listing.ref}
{listing.ref}

{listing.title}

{(window.COMMUNITIES.find(c => c.id === listing.community) || {}).name}
{listing.beds} bed {listing.baths} bath {listing.sizeSqft.toLocaleString()} sqft
{listing.price}
); } // ── Agent card ──────────────────────────────────────────────── function AgentCard({ agent, onOpen }) { return (
onOpen && onOpen(agent.id)}>
{agent.name}

{agent.name}

{agent.role}
{agent.langs}
); } // ── Community card ──────────────────────────────────────────── function CommunityCard({ community, span, featured, onOpen }) { const className = `community-card ${featured ? "featured" : ""} grid-span-${span}`; return (
onOpen && onOpen(community.id)} role="link" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter") onOpen && onOpen(community.id); }}> {community.name}
{community.name}
{community.type} · {community.agents} consultants
); } // ── Communities grid (editorial bento) ─────────────────────── function CommunitiesGrid({ items, onOpen, layout }) { // editorial bento pattern for 22 items // 0: 6, 1: 6, 2: 4, 3: 4, 4: 4, 5: 3, 6: 3, 7: 3, 8: 3, then 4s if (layout === "uniform") { return (
{items.map(c => ( ))}
); } const spans = [6, 6, 4, 4, 4, 3, 3, 3, 3, 4, 4, 4, 3, 3, 3, 3, 6, 6, 4, 4, 4, 4]; return (
{items.map((c, i) => ( = 6} onOpen={onOpen} /> ))}
); } Object.assign(window, { useForm, required, validEmail, validPhone, Field, ViewingForm, ValuationForm, SuccessPanel, ListingCard, AgentCard, CommunityCard, CommunitiesGrid, });