// shared.jsx — componentes e hooks compartilhados entre todas as páginas. // Depende de data.js (window.COPY, LOGOS, VARIANTS, ACCENTS, NAV_LINKS, TWEAK_DEFAULTS). const { useState, useEffect, useMemo, useRef } = React; function linkTo(key, fallback) { return (window.MODRO_LINKS && window.MODRO_LINKS[key]) || fallback; } function asset(path) { if (/^(https?:)?\/\//.test(path) || String(path).startsWith("data:")) return path; const base = window.MODRO_ASSET_BASE || ""; return `${base}${String(path).replace(/^\/+/, "")}`; } function whatsappHref(message) { const raw = window.MODRO_WHATSAPP || (window.COPY?.pt?.footer_phone || ""); const phone = String(raw).replace(/\D/g, ""); const text = message ? `?text=${encodeURIComponent(message)}` : ""; return `https://wa.me/${phone}${text}`; } function renderModroPage(page) { const root = document.querySelector("[data-modro-root]") || document.getElementById("root"); if (!root) return; ReactDOM.createRoot(root).render(page); } function useScrollEffects() { useEffect(() => { const nodes = Array.from(document.querySelectorAll( ".mc-app [data-reveal], .mc-app .mc-sec-head, .mc-app .mc-service, .mc-app .mc-diff-block, .mc-app .mc-purpose-block, .mc-app .mc-gal-item, .mc-app .mc-social, .mc-app .mc-client-stat" )); if (!nodes.length) return undefined; nodes.forEach((node, index) => { if (!node.hasAttribute("data-reveal")) node.setAttribute("data-reveal", "up"); node.style.setProperty("--reveal-delay", `${Math.min(index % 4, 3) * 70}ms`); }); if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { nodes.forEach((node) => node.classList.add("is-visible")); return undefined; } const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (!entry.isIntersecting) return; entry.target.classList.add("is-visible"); observer.unobserve(entry.target); }); }, { threshold: 0.16, rootMargin: "0px 0px -8% 0px" }); nodes.forEach((node) => observer.observe(node)); return () => observer.disconnect(); }, []); } function CountUp({ value, suffix = "", duration = 1100 }) { const ref = useRef(null); const [shown, setShown] = useState(0); useEffect(() => { const el = ref.current; if (!el) return undefined; let frame = 0; let started = false; const run = () => { const start = performance.now(); const tick = (now) => { const progress = Math.min((now - start) / duration, 1); const eased = 1 - Math.pow(1 - progress, 3); setShown(Math.round(value * eased)); if (progress < 1) frame = requestAnimationFrame(tick); }; frame = requestAnimationFrame(tick); }; const observer = new IntersectionObserver((entries) => { if (started || !entries.some((entry) => entry.isIntersecting)) return; started = true; run(); observer.disconnect(); }, { threshold: 0.45 }); observer.observe(el); return () => { observer.disconnect(); cancelAnimationFrame(frame); }; }, [value, duration]); return {shown.toLocaleString("pt-BR")}{suffix}; } // ─── Theming hook ──────────────────────────────────────────────────────────── function useTheme() { const [t, setTweaks] = useState(window.TWEAK_DEFAULTS); const setTweak = React.useCallback((keyOrEdits, val) => { const edits = typeof keyOrEdits === "object" && keyOrEdits !== null ? keyOrEdits : { [keyOrEdits]: val }; setTweaks((prev) => ({ ...prev, ...edits })); }, []); const variant = window.VARIANTS[t.variant] || window.VARIANTS.studio; const theme = useMemo( () => ({ ...variant, accent: t.accent || variant.accentDefault }), [variant, t.accent] ); const copy = window.COPY[t.lang] || window.COPY.pt; const navLinks = window.NAV_LINKS[t.lang] || window.NAV_LINKS.pt; // Apply theme to :root useEffect(() => { const r = document.documentElement.style; r.setProperty("--bg", theme.bg); r.setProperty("--bg-alt", theme.bgAlt); r.setProperty("--bg-raised", theme.bgRaised); r.setProperty("--fg", theme.fg); r.setProperty("--fg-muted", theme.fgMuted); r.setProperty("--fg-faint", theme.fgFaint); r.setProperty("--rule", theme.rule); r.setProperty("--accent", theme.accent); r.setProperty("--on-accent", theme.onAccent); document.body.style.background = theme.bg; document.body.style.color = theme.fg; document.documentElement.setAttribute("data-variant", t.variant); }, [theme, t.variant]); // Density useEffect(() => { const map = { compact: 40, regular: 56, comfy: 72 }; document.documentElement.style.setProperty("--sec-pad", `${map[t.density] || 56}px`); }, [t.density]); return { t, setTweak, theme, copy, navLinks }; } // ─── Primitives ────────────────────────────────────────────────────────────── function Section({ id, children, style, label, theme, eyebrow, title, kicker }) { return (
{(eyebrow || title) &&
{eyebrow}
{title &&

{title}

} {kicker &&

{kicker}

}
} {children}
); } function Btn({ children, variant = "primary", href = "#", icon, theme, big }) { const styleMap = { primary: { background: theme.accent, color: theme.onAccent, border: `1px solid ${theme.accent}` }, ghost: { background: "transparent", color: theme.fg, border: `1px solid ${theme.rule}` }, inverse: { background: theme.fg, color: theme.bg, border: `1px solid ${theme.fg}` } }; return ( {icon && {icon}} {children} ); } const WhatsIcon = () => ; function BrandMark({ theme, large }) { const size = large ? 28 : 22; return ( {[0, 1, 2, 3, 4, 5, 6].map((row) => { const y = 6 + row * 4.5; const w = Math.sqrt(Math.max(0, 18 * 18 - (y - 20) * (y - 20))) * 2; const dots = Math.max(2, Math.floor(w / 3)); return Array.from({ length: dots }).map((_, i) => { const x = 20 - w / 2 + w / (dots - 1) * i; const r = row < 3 ? 1.0 : 0.8; return ; }); })} ); } // ─── Nav (multipage) ───────────────────────────────────────────────────────── function Nav({ theme, copy, navLinks, lang, setLang, current }) { const [scrolled, setScrolled] = useState(false); useEffect(() => { const onScroll = () => setScrolled(window.scrollY > 40); window.addEventListener("scroll", onScroll); return () => window.removeEventListener("scroll", onScroll); }, []); return ( ); } // ─── Hero (home) ───────────────────────────────────────────────────────────── function Hero({ t, theme }) { return (
{t.hero_eyebrow}

{t.hero_title_a} {t.hero_title_b} {t.hero_title_c}

{t.hero_lede}

} href={linkTo("contato", "contato.html")}>{t.cta_primary} {t.cta_secondary}
{t.hero_meta_1}
{t.hero_meta_2}
{t.hero_meta_3}
{t.hero_meta_2} {t.hero_meta_3} {t.hero_meta_1}
); } function HeroBackgroundVideo() { const src = window.MODRO_HERO_VIDEO || "assets/hero-loop.mp4"; const poster = window.MODRO_HERO_POSTER || "assets/hero-bg.jpg"; return ( ); } // Page header — reusable for inner pages function PageHero({ theme, eyebrow, title, lede }) { return (
{eyebrow}

{title}

{lede &&

{lede}

}
); } function HeroLoopVideo({ theme }) { const src = window.MODRO_HERO_VIDEO || "assets/hero-loop.mp4"; const poster = window.MODRO_HERO_POSTER || "assets/hero-bg.jpg"; return (
); } function PortraitFrame({ theme }) { return (
Reiner Modro
Founder · Senior Advisor
EST. 1990
); } // ─── Sections ──────────────────────────────────────────────────────────────── function LogoBand({ t, theme, show }) { if (!show) return null; const loop = [...window.LOGOS, ...window.LOGOS]; return (
Clientes
{t.band_label}
{loop.map((l, i) =>
{l.name}
)}
); } function Problems({ t, theme }) { return (
{t.problems.map((p) =>
{p.n}

{p.t}

{p.d}

)}
); } function Services({ t, theme }) { const [open, setOpen] = useState(0); return (
{t.services.map((s, i) => { const isOpen = open === i; return (
setOpen(i)}>
{s.code}

{s.t}

{isOpen &&

{s.d}

    {s.deliverables.map((d) =>
  • {d}
  • )}
}
); })}
); } function Results({ t, theme }) { return (
{t.results.map((r, i) =>
{r.v}
{r.l}
{r.c}
)}
); } function About({ t, theme }) { return (
{t.about_eyebrow}

{t.about_title}

{t.about_p1}

{t.about_p2}

{t.about_p3 &&

{t.about_p3}

}
    {t.about_facts.map(([y, l], i) =>
  1. {y}
    {l}
  2. )}
); } function Testimonials({ t, theme }) { const [active, setActive] = useState(0); return (
{t.testimonials.map((tt, i) =>
"

{tt.q}

{tt.a}
)}
{t.testimonials.map((_, i) =>
); } function Materials({ t, theme }) { const [email, setEmail] = useState(""); const [submitted, setSubmitted] = useState(false); const submitMaterials = (e) => { e.preventDefault(); setSubmitted(true); window.open( whatsappHref(`Olá, gostaria de receber os materiais da Modro Consultoria.\nEmail: ${email}`), "_blank", "noopener,noreferrer" ); }; return (
{t.materials.map((m) =>
{m.tag} {m.n}

{m.t}

{m.d}

gated
)}
setEmail(e.target.value)} required />
{submitted &&
✓ Solicitação preparada para {email}.
}
); } function Insights({ t, theme }) { return (
{t.insights.map((i, k) =>
{i.d} {i.c}

{i.t}

{i.r}
)}
); } function FinalCTA({ t, theme }) { return (
{t.cta_secondary}

{t.cta_final_title}

{t.cta_final_lede}

} href={whatsappHref()}> {t.cta_final_button}
{t.cta_final_note}
); } // ─── Contact form (full page) ──────────────────────────────────────────────── function ContactForm({ t, theme }) { const [sent, setSent] = useState(false); const [data, setData] = useState({ name: "", company: "", email: "", phone: "", topic: t.contact_form_topics[0], msg: "" }); const upd = (k) => (e) => setData((d) => ({ ...d, [k]: e.target.value })); const submitContact = (e) => { e.preventDefault(); setSent(true); const message = [ "Olá, gostaria de conversar com a Modro Consultoria.", `Nome: ${data.name}`, `Empresa: ${data.company}`, `Email: ${data.email}`, data.phone ? `Telefone: ${data.phone}` : "", `Assunto: ${data.topic}`, `Mensagem: ${data.msg}` ].filter(Boolean).join("\n"); window.open(whatsappHref(message), "_blank", "noopener,noreferrer"); }; return (