// 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 (
);
}
// ─── 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 (
EST.
1990
Reiner Modro
Founder · Senior Advisor
);
}
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 (
);
}
function Problems({ t, theme }) {
return (
{t.problems.map((p) =>
)}
);
}
function Services({ t, theme }) {
const [open, setOpen] = useState(0);
return (
{t.services.map((s, i) => {
const isOpen = open === i;
return (
setOpen(i)}>
{isOpen &&
{s.d}
{s.deliverables.map((d) =>
- {d}
)}
}
);
})}
);
}
function Results({ t, theme }) {
return (
{t.results.map((r, i) =>
)}
);
}
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) =>
-
{y}
{l}
)}
);
}
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}
)}
);
}
function Insights({ t, theme }) {
return (
);
}
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 (
);
}
// ─── Footer (multipage) ──────────────────────────────────────────────────────
function Footer({ theme, copy, navLinks }) {
return (
);
}
// ─── Tweaks panel (shared on every page) ─────────────────────────────────────
function GlobalTweaks({ t, setTweak }) {
return (
setTweak({ variant: v, accent: window.VARIANTS[v].accentDefault })} />
setTweak("accent", v)} />
setTweak("density", v)} />
setTweak("showLogos", v)} />
setTweak("lang", v)} />
);
}
// ─── Page Shell ──────────────────────────────────────────────────────────────
// Wraps every page with Nav + content + Footer.
function PageShell({ current, children }) {
const { t, setTweak, theme, copy, navLinks } = useTheme();
const showTweaks = typeof TweaksPanel !== "undefined" && new URLSearchParams(window.location.search).has("tweaks");
useScrollEffects();
return (
);
}
// Expose to window so per-page scripts can use them.
Object.assign(window, {
linkTo, asset, whatsappHref, renderModroPage, useTheme, Section, Btn, WhatsIcon, BrandMark, Nav, Hero, PageHero, PortraitFrame,
LogoBand, Problems, Services, Results, About, Testimonials, Materials, Insights,
FinalCTA, ContactForm, Footer, GlobalTweaks, PageShell
});
// ─── Couto-style sections ────────────────────────────────────────────────────
function Manifesto({ t, theme }) {
return (
"
{t.manifesto_quote}
— Reiner Modro
);
}
function Purpose({ t, theme }) {
return (
{t.purpose_blocks.map((b) =>
)}
);
}
function Differentials({ t, theme }) {
return (
{t.diff_blocks.map((b, i) =>
)}
);
}
function ClientResults({ t, theme }) {
return (
{t.clients_eyebrow}
{t.clients_title}
{t.clients_lede}
} href={linkTo("contato", "contato.html")}>
{t.clients_cta}
{t.client_stats.map((stat, index) => (
{stat.label}
))}
{window.LOGOS.map((logo) => (
))}
);
}
function TrustBand({ t, theme }) {
const loop = [...window.LOGOS, ...window.LOGOS];
return (
);
}
function InCompany({ t, theme }) {
return (
{t.incompany_eyebrow}
{t.incompany_title}
{t.incompany_lede}
} href={linkTo("contato", "contato.html")}>
{t.incompany_cta}
{t.incompany_topics.map((tp, i) =>
-
{String(i + 1).padStart(2, "0")}
{tp}
)}
);
}
function Socials({ t, theme }) {
const items = [
{ tag: "LinkedIn", h: "in/reiner-modro", d: "Notas semanais sobre indústria, sucessão e governança em PME.", href: "https://www.linkedin.com/in/reiner-modro/" },
{ tag: "Instagram", h: "@reinermodro", d: "Bastidores de visitas técnicas, fábricas e conselhos.", href: "https://www.instagram.com/reinermodro/" },
{ tag: "YouTube", h: "@rmodro", d: "Conversas longas com diretores industriais e sucessores.", href: "https://www.youtube.com/@rmodro" }];
return (
);
}
Object.assign(window, { Manifesto, Purpose, Differentials, ClientResults, TrustBand, InCompany, Socials });
// ─── Gallery & Video ──────────────────────────────────────────────────────
function VideoEmbed({ t, theme }) {
return (
);
}
function Gallery({ t, theme }) {
const photos = [
{ src: "assets/gallery/g1.jpg", c: "Visita técnica · Delta Máquinas Têxteis" },
{ src: "assets/gallery/g2.jpg", c: "Reunião na Öpller Industrial" },
{ src: "assets/gallery/g3.jpg", c: "Inauguração · linha Delta + Tronix" },
{ src: "assets/gallery/g4.jpg", c: "Registro · Reiner Modro e Werner R. Voigt" },
{ src: "assets/gallery/g5.jpg", c: "Homenagem · evento profissional" }, // (era g4 antes)
{ src: "assets/gallery/g6.jpg", c: "Palestra · formação de sucessores" },
{ src: "assets/gallery/g7.jpg", c: "Visita à Skymsen" },
{ src: "assets/gallery/g8.jpg", c: "Reunião na Delta Máquinas Têxteis" },
{ src: "assets/gallery/g9.jpg", c: "Feira · estande Skymsen" }];
return (
{photos.map((p, i) =>
{p.c}
)}
);
}
Object.assign(window, { VideoEmbed, Gallery });