// ============================================================ // TERREXO — Shared components (nav, footer, marks, helpers) // Loaded after React + data/site.js // ============================================================ const { useState, useEffect, useRef, useMemo } = React; // --- Responsive breakpoint hook --- function useMobile(bp = 768) { const [m, setM] = useState(window.innerWidth < bp); useEffect(() => { const h = () => setM(window.innerWidth < bp); window.addEventListener("resize", h, { passive: true }); return () => window.removeEventListener("resize", h); }, [bp]); return m; } // --- Logo / wordmark --- function Wordmark({ size = 22, dark = false, lockup = true }) { const color = dark ? "#FAFAF7" : "#161A18"; return ( {lockup && ( Terrexo. )} ); } // --- Top nav --- function Nav({ active = "", dark = false }) { const [scrolled, setScrolled] = useState(false); const [progress, setProgress] = useState(0); const [lang, setLang] = useState("FR"); const [open, setOpen] = useState(false); const isMobile = useMobile(960); useEffect(() => { const onScroll = () => { setScrolled(window.scrollY > 30); const h = document.documentElement.scrollHeight - window.innerHeight; setProgress(h > 0 ? (window.scrollY / h) * 100 : 0); }; window.addEventListener("scroll", onScroll, { passive: true }); onScroll(); return () => window.removeEventListener("scroll", onScroll); }, []); useEffect(() => { if (!isMobile && open) setOpen(false); }, [isMobile]); const inkColor = dark ? "#FAFAF7" : "#161A18"; const bg = (scrolled || open) ? (dark ? "rgba(22,26,24,.94)" : "rgba(250,250,247,.96)") : "transparent"; return ( <>
{/* Desktop nav */} {!isMobile && ( )}
{!isMobile && (
{window.TERREXO.languages.map((l) => ( ))}
)} {!isMobile && ( Demander un devis )} {/* Hamburger button */} {isMobile && ( )}
{/* Mobile drawer */} {isMobile && (
Demander un devis
{window.TERREXO.languages.map((l) => ( ))}
)}
); } function ArrowIcon({ size = 14 }) { return ( ); } // --- Footer --- function Footer() { const T = window.TERREXO; const isMobile = useMobile(768); const isTablet = useMobile(1024); return ( ); } // --- Section header --- function SectionHead({ eyebrow, title, lead, action }) { return (
{eyebrow}

{title}

{lead &&

{lead}

} {action}
); } // --- Stat counter --- function Stat({ value, label, suffix = "" }) { const ref = useRef(null); const [n, setN] = useState(0); useEffect(() => { const el = ref.current; if (!el) return; const obs = new IntersectionObserver(entries => { entries.forEach(e => { if (e.isIntersecting) { const start = performance.now(); const dur = 1600; const isFloat = !Number.isInteger(value); const tick = (t) => { const p = Math.min((t - start) / dur, 1); const eased = 1 - Math.pow(1 - p, 3); setN(isFloat ? +(value * eased).toFixed(1) : Math.round(value * eased)); if (p < 1) requestAnimationFrame(tick); }; requestAnimationFrame(tick); obs.disconnect(); } }); }, { threshold: 0.4 }); obs.observe(el); return () => obs.disconnect(); }, [value]); return (
{n}{suffix}
{label}
); } // --- Project card --- function ProjectCard({ p, large = false }) { return (
{p.title}
{p.title}
{p.location} · {p.serviceLabel}
{p.year}
); } // --- Scroll reveal hook (init for whole page) --- function useScrollReveal() { useEffect(() => { const els = document.querySelectorAll(".sr"); const obs = new IntersectionObserver(entries => { entries.forEach((e) => { if (e.isIntersecting) { const idx = [...e.target.parentElement.children].indexOf(e.target); e.target.style.transitionDelay = (Math.min(idx, 6) * 0.06) + "s"; e.target.classList.add("in"); obs.unobserve(e.target); } }); }, { threshold: 0.12 }); els.forEach(el => obs.observe(el)); return () => obs.disconnect(); }, []); } // --- Final CTA (small, used across many pages) --- function FinalCTASmall() { return (

Vous savez ce que vous voulez ?
Ou pas du tout ?

Dans les deux cas, on commence par une visite. Gratuite, sans engagement.

Prendre rendez-vous
); } // Expose globally Object.assign(window, { Wordmark, Nav, Footer, SectionHead, Stat, ProjectCard, ArrowIcon, useScrollReveal, FinalCTASmall, useMobile, });