// ui.jsx — shared components for Version Française Pour Elle // Logo, buttons, placeholders, trust chip, header, footer, FABs. const { useState, useEffect, useRef } = React; // ── Tiny helpers ──────────────────────────────────────────────── function cx(...xs) { return xs.filter(Boolean).join(" "); } // Pulls localised string from {en, fr} object OR returns string as-is. function L(value, lang) { if (value == null) return ""; if (typeof value === "string") return value; return value[lang] ?? value.en ?? ""; } // Intersection-observer reveal — class flips on first sight, no flicker. // Synchronously checks position on mount so above-the-fold content paints // immediately (the observer alone won't fire until a scroll event nudges it, // which left every landing view blank under the header). function Reveal({ children, as: As = "div", className = "", delay = 0, ...rest }) { const ref = useRef(null); const [seen, setSeen] = useState(false); useEffect(() => { const el = ref.current; if (!el) return; const r = el.getBoundingClientRect(); const vh = window.innerHeight || document.documentElement.clientHeight; if (r.top < vh * 0.9 && r.bottom > 0) { setSeen(true); return; } const io = new IntersectionObserver(([e]) => { if (e.isIntersecting) { setSeen(true); io.disconnect(); } }, { rootMargin: "0px 0px -10% 0px", threshold: 0.05 }); io.observe(el); return () => io.disconnect(); }, []); return ( {children} ); } // ── Logo wordmark ─────────────────────────────────────────────── function Logo({ onClick }) { return ( { e.preventDefault(); onClick?.(); }} aria-label="Version Française Pour Elle — Home" > Version Française · Pour Elle · ); } // ── Eyebrow with gold dot ─────────────────────────────────────── function Eyebrow({ children, align = "left" }) { return (
{children}
); } // ── Image placeholder ─────────────────────────────────────────── function Placeholder({ caption, tag, tone = "blush", aspect = "4 / 5", style = {}, children }) { const toneClass = ({ blush: "", cream: "ph-tone-cream", rose: "ph-tone-rose", taupe: "ph-tone-taupe", noir: "ph-tone-noir", })[tone] || ""; return (
{tag &&
{tag}
} {children} {caption &&
{caption}
}
); } // ── Trust chip (5.0 ★ Fresha · 330+) ──────────────────────────── function TrustChip({ lang }) { const t = window.VFPE_DATA.t[lang]; return ( ★★★★★ {lang === "fr" ? "5,0" : "5.0"} · {lang === "fr" ? "330+ avis · Fresha" : "330+ reviews · Fresha"} ); } // ── Buttons ───────────────────────────────────────────────────── function BookButton({ lang, variant = "primary", size = "md", className = "" }) { const t = window.VFPE_DATA.t[lang]; const cls = cx("btn", `btn-${variant}`, size === "sm" && "btn-sm", className); return ( {t.booknow} ); } function WhatsAppButton({ lang, variant = "ghost", size = "md" }) { const t = window.VFPE_DATA.t[lang]; return ( {t.whatsapp} ); } function WAIcon() { return ( ); } // ── Header / nav ──────────────────────────────────────────────── function Header({ lang, setLang, route, setRoute, accent }) { const t = window.VFPE_DATA.t[lang]; const items = [ { key: "home", label: t.home }, { key: "services", label: t.services }, { key: "about", label: t.about }, { key: "gallery", label: t.gallery }, { key: "reviews", label: t.reviews }, { key: "contact", label: t.contact }, ]; return (
setRoute("home")} />
); } function LangToggle({ lang, setLang }) { return (
); } // ── Footer ────────────────────────────────────────────────────── function Footer({ lang, setRoute }) { const t = window.VFPE_DATA.t[lang]; const links = window.VFPE_DATA.links; return ( ); } // ── Mobile bottom bar + FAB ───────────────────────────────────── function MobileBar({ lang }) { const t = window.VFPE_DATA.t[lang]; const links = window.VFPE_DATA.links; return (
{t.call} WA {t.booknow}
); } function PhoneIcon() { return ( ); } function WhatsAppFAB() { return ( ); } Object.assign(window, { cx, L, Reveal, Logo, Eyebrow, Placeholder, TrustChip, BookButton, WhatsAppButton, Header, Footer, MobileBar, WhatsAppFAB, });