/* Components for Allsopp & Allsopp — Darren Murphy microsite. All components attach to window for global scope sharing. */ const { useState, useEffect, useRef, useMemo, useCallback } = React; // ============================================================ // SVG icons — minimal stroke set // ============================================================ const Icon = { Menu: (p) => , X: (p) => , Arrow: (p) => , Phone: (p) => , WA: (p) => , Calendar: (p) => , Star: (p) => , Check: (p) => , Bed: (p) => , Bath: (p) => , Ruler: (p) => , MapPin: (p) => , Award: (p) => , }; window.Icon = Icon; // ============================================================ // Ampersand mark (extracted from logo SVG) // ============================================================ function Ampersand({ size = 32, color = "currentColor" }) { return ( ); } window.Ampersand = Ampersand; // ============================================================ // Brand logo (inline allsopp wordmark) // ============================================================ function BrandMark({ color = "var(--navy)" }) { return ( Allsopp & Allsopp ); } window.BrandMark = BrandMark; // ============================================================ // Top navigation // ============================================================ function TopNav({ t, lang, onLangToggle, onBook, dark = false }) { const [open, setOpen] = useState(false); const [scrolled, setScrolled] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 12); window.addEventListener('scroll', onScroll); return () => window.removeEventListener('scroll', onScroll); }, []); return (
{open && (
)}
); } window.TopNav = TopNav; // ============================================================ // Hero — 3 layout variants // ============================================================ function Hero({ t, heroLayout, onBook }) { const eyebrow = (
{t.heroEyebrow}
); const stats = (
10+
{t.statsLabel.years}
6
{t.statsLabel.communities}
2
{t.statsLabel.languages}
<1h
{t.statsLabel.response}
); const ctas = (
{t.cta2} {t.cta3}
); if (heroLayout === "cinematic") { return (
Darren Murphy
{eyebrow}

{t.heroPre} Darren Murphy.

{t.heroSub}

{ctas}
{AGENT.honour} · {AGENT.honourIssuer} 2025 — {AGENT.honourNote.toLowerCase()}
{stats}
); } if (heroLayout === "magazine") { return (
{eyebrow}
VOL. XVII · 2026 · DUBAI · ISSUE №1815
{t.heroPre}

Darren
Murphy

{AGENT.role}
Darren Murphy
— Portrait, Motor City Head Office, Dubai

{t.heroSub}

{ctas}
{AGENT.honour}
{AGENT.honourIssuer} 2025 · {AGENT.honourNote}
{stats}
); } // default: editorial split return (
{eyebrow}

{t.heroPre}{" "} Darren Murphy.

{t.heroSub}

{ctas}
{AGENT.honour} · {AGENT.honourIssuer} 2025 — {AGENT.honourNote.toLowerCase()}
Darren Murphy
Darren Murphy
{AGENT.role}
{stats}
); } window.Hero = Hero; // ============================================================ // About / approach // ============================================================ function About({ t }) { return (
{t.aboutTitle}

A consultant, not a closer-of-deals.

{AGENT.bio}

RERA
{AGENT.brnNote}
Languages
{AGENT.speaks.join(", ")}
Office
Motor City HQ · JGE
{AGENT.approach.map((a, i) => (
0{i + 1}

{a.h}

{a.b}

))}
); } window.About = About; // ============================================================ // Listing card + grid (filterable) // ============================================================ function ListingCard({ l, t }) { const statusLabel = l.status === "sale" ? t.forSale : l.status === "rent" ? t.toRent : t.offPlan; return (
{l.community}
{t.sampleWatermark}
{statusLabel}
{l.community}

{l.title}

{l.price} {l.priceQualifier && l.priceQualifier !== "Asking" && · {l.priceQualifier}}
{l.beds} {t.bedsLabel} {l.baths} {t.bathsLabel} {l.sizeSqft.toLocaleString()} {t.sqftLabel}
{l.tags.map((x, i) => {x})}
Ref. {l.ref}
); } function ListingsGrid({ t }) { const [filter, setFilter] = useState("all"); const filters = [ { k: "all", l: t.filterAll }, { k: "sale", l: t.filterSale }, { k: "rent", l: t.filterRent }, { k: "offplan", l: t.filterOffplan } ]; const filtered = filter === "all" ? LISTINGS : LISTINGS.filter(l => l.status === filter); return (
{t.listingsTitle}

{filtered.length} {filtered.length === 1 ? 'property' : 'properties'} currently with Darren.

{t.listingsSub}

{filters.map(f => ( ))}
{filtered.map(l => )}
Production listings sync live from Strapi CMS → Property Finder, Bayut and dubizzle feeds. Demo cards above are non-published sample data.
); } window.ListingsGrid = ListingsGrid; // ============================================================ // Communities // ============================================================ function Communities({ t }) { const items = [ { name: "Palm Jumeirah", img: "media/community-palm-jumeirah.webp", tag: "Waterfront luxury" }, { name: "Downtown Dubai", img: "media/community-downtown.webp", tag: "Burj Khalifa district" }, { name: "Dubai Hills Estate", img: "media/community-dubai-hills.webp", tag: "Family villas" }, { name: "Business Bay", img: "media/community-business-bay.webp", tag: "Canal-side towers" }, { name: "Arabian Ranches", img: "media/community-arabian-ranches.webp", tag: "Established community" }, { name: "City Walk", img: "media/community-city-walk.webp", tag: "Urban resort" }, ]; return (
{t.communitiesTitle}

{t.communitiesTitle}.

{t.communitiesSub}

{items.map((c, i) => (
{c.name}
{c.tag}
{c.name}
View listings
))}
); } window.Communities = Communities; // ============================================================ // Awards strip // ============================================================ function Press({ t }) { return (
{t.pressTitle}

{t.pressTitle}.

{t.pressSub}

{PRESS.map((p, i) => (
{p.year}
{p.title}
{p.issuer}
{p.note}
Verified
))}
Source attributions per research/reviews-ratings.md. Logo rights to be cleared with each issuer before public display.
); } window.Press = Press; // ============================================================ // Testimonials // ============================================================ function Testimonials({ t }) { return (
{t.testimonialsTitle}

{t.testimonialsTitle}.

{t.testimonialsSub}

{TESTIMONIALS.map((tm, i) => (
{tm.q}
{tm.name} — {tm.detail}
))}
); } window.Testimonials = Testimonials; // ============================================================ // Book viewing modal (multi-step) // ============================================================ function BookViewingModal({ open, onClose, t }) { const [step, setStep] = useState(1); const [form, setForm] = useState({ name: "", phone: "", email: "", property: "", date: "", time: "10:00", notes: "" }); const [errors, setErrors] = useState({}); const upd = (k, v) => setForm(f => ({ ...f, [k]: v })); useEffect(() => { if (!open) { setTimeout(() => { setStep(1); setForm({ name: "", phone: "", email: "", property: "", date: "", time: "10:00", notes: "" }); setErrors({}); }, 300); } }, [open]); useEffect(() => { document.body.style.overflow = open ? 'hidden' : ''; return () => { document.body.style.overflow = ''; }; }, [open]); const validate1 = () => { const e = {}; if (!form.name.trim()) e.name = "Required"; if (!form.phone.trim()) e.phone = "Required"; if (!form.email.trim() || !form.email.includes('@')) e.email = "Valid email required"; setErrors(e); return Object.keys(e).length === 0; }; const validate2 = () => { const e = {}; if (!form.property) e.property = "Pick a property"; if (!form.date) e.date = "Pick a date"; setErrors(e); return Object.keys(e).length === 0; }; if (!open) return null; return (
e.stopPropagation()} role="dialog" aria-modal="true">
Darren Murphy
Darren Murphy
{AGENT.role}
{AGENT.honour} · {AGENT.honourIssuer} 2025
+971 4 429 4444
wa.me/97144294444
Motor City, Dubai
{step <= 2 && (
= 1 ? 'on' : ''}>01 Your details = 2 ? 'on' : ''}>02 Property & time 03 Confirm
)} {step === 1 && ( <>

{t.bookTitle}

{t.bookSub}

WhatsApp instead
)} {step === 2 && ( <>

Pick a property and a time.

Darren confirms every appointment by phone — you'll never get a silent calendar invite.

)} {step === 3 && (

Request received.

Darren will call {form.phone || "your number"} within the hour during working hours. You'll also get a confirmation at {form.email}.

Property{LISTINGS.find(l => l.ref === form.property)?.title || form.property || "—"}
When{form.date || "—"} · {form.time}
AgentDarren Murphy
ReferenceVW-{Math.random().toString(36).slice(2,8).toUpperCase()}

Demo flow — production submits to Formspree/Netlify + mirrors to the CRM webhook so the lead lands in Darren's pipeline.

)}
); } window.BookViewingModal = BookViewingModal; // ============================================================ // Valuation block (inline strip) // ============================================================ function ValuationStrip({ t, onBook }) { const [v, setV] = useState({ type: "Apartment", area: "", beds: "2" }); const [sent, setSent] = useState(false); return (
Free valuation

Thinking of selling or letting?

A RERA-registered consultant who actually knows your community will come back with a realistic range — no bots, no guesswork, no instant-quote nonsense.

  • Comparable evidence from the last 90 days
  • Marketing plan + photography spec
  • Honest "list or wait" recommendation
{!sent ? ( <>

We'll come back with a realistic range — never a machine-generated figure.

) : (

Thanks — Darren has it.

Expect a call within the hour. In the meantime, WhatsApp him directly if it's urgent.

)}
); } window.ValuationStrip = ValuationStrip; // ============================================================ // WhatsApp FAB // ============================================================ function WhatsAppFab() { const [hint, setHint] = useState(false); useEffect(() => { const t = setTimeout(() => setHint(true), 3500); const t2 = setTimeout(() => setHint(false), 9000); return () => { clearTimeout(t); clearTimeout(t2); }; }, []); return ( {hint && Message Darren on WhatsApp} ); } window.WhatsAppFab = WhatsAppFab; // ============================================================ // Sticky mobile bottom bar // ============================================================ function StickyBar({ t, onBook }) { return (
{t.nav.call} {t.nav.whatsapp}
); } window.StickyBar = StickyBar; // ============================================================ // Footer // ============================================================ function Footer({ t }) { return ( ); } window.Footer = Footer;