// 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 (
);
}
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 (
);
}
function PhoneIcon() {
return (
);
}
function WhatsAppFAB() {
return (
);
}
Object.assign(window, {
cx, L, Reveal, Logo, Eyebrow, Placeholder, TrustChip,
BookButton, WhatsAppButton, Header, Footer, MobileBar, WhatsAppFAB,
});