/* global React, ReactDOM, TRINITY */
const { useState, useEffect, useRef, useContext, createContext, useMemo, useCallback } = React;
// ============= LANGUAGE CONTEXT =============
const LangCtx = createContext({ lang: "es", setLang: () => {} });
window.LangCtx = LangCtx;
function LangProvider({ children, initial = "es" }) {
const [lang, setLang] = useState(initial);
useEffect(() => {
document.documentElement.lang = lang;
}, [lang]);
return React.createElement(LangCtx.Provider, { value: { lang, setLang } }, children);
}
// Hook → fetches localized string from {es, en} obj
function useT() {
const { lang } = useContext(LangCtx);
return (m) => {
if (!m) return "";
if (typeof m === "string") return m;
if (Array.isArray(m)) return m.map((x) => (typeof x === "string" ? x : x[lang] || x.en)).join(" ");
if (typeof m === "object" && (m.es !== undefined || m.en !== undefined)) return m[lang] || m.en || "";
return String(m);
};
}
// ============= REVEAL ON SCROLL =============
function useReveal() {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
if (typeof IntersectionObserver === "undefined") {
el.classList.add("in");
return;
}
const io = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
e.target.classList.add("in");
io.unobserve(e.target);
}
});
},
{ threshold: 0.12, rootMargin: "0px 0px -80px 0px" }
);
io.observe(el);
return () => io.disconnect();
}, []);
return ref;
}
// ============= ICONS (minimal, accessible inline SVG, no slop) =============
const Icon = ({ name, size = 20, stroke = 1.5 }) => {
const common = {
width: size, height: size, viewBox: "0 0 24 24", fill: "none",
stroke: "currentColor", strokeWidth: stroke, strokeLinecap: "round", strokeLinejoin: "round",
"aria-hidden": "true",
};
switch (name) {
case "arrow-right":
return ;
case "arrow-left":
return ;
case "phone":
return ;
case "mail":
return ;
case "map-pin":
return ;
case "clock":
return ;
case "star":
return ;
case "integrated":
return ;
case "evidence":
return ;
case "tech":
return ;
case "heart":
return ;
case "check":
return ;
case "globe":
return ;
default: return null;
}
};
// ============= LANGUAGE TOGGLE =============
function LangToggle() {
const { lang, setLang } = useContext(LangCtx);
return (
setLang("es")}
>ES
setLang("en")}
>EN
);
}
// ============= HEADER =============
function Header({ active, onJump }) {
const T = useT();
const [scrolled, setScrolled] = useState(false);
const [open, setOpen] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 12);
onScroll();
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
useEffect(() => { document.body.style.overflow = open ? "hidden" : ""; }, [open]);
const nav = [
{ id: "services", label: TRINITY.ui.services },
{ id: "about", label: TRINITY.ui.about },
{ id: "testimonials", label: TRINITY.ui.testimonials },
{ id: "faq", label: TRINITY.ui.faq },
{ id: "contact", label: TRINITY.ui.contact },
];
const handleJump = (id) => (e) => {
e.preventDefault();
setOpen(false);
const el = document.getElementById(id);
if (el) {
const top = el.getBoundingClientRect().top + window.scrollY - 72;
window.scrollTo({ top, behavior: "smooth" });
}
};
return (
<>
>
);
}
// ============= HERO =============
function Hero() {
const T = useT();
const ref = useReveal();
const { lang } = useContext(LangCtx);
const titleLines = TRINITY.hero.title[lang] || TRINITY.hero.title.en;
const emWord = (TRINITY.hero.titleEm[lang] || TRINITY.hero.titleEm.en).trim();
// Render last word as italic serif accent
const renderLine = (line, isLast) => {
if (!isLast) return line;
const idx = line.toLowerCase().lastIndexOf(emWord.toLowerCase());
if (idx === -1) return line;
return (
<>
{line.slice(0, idx)}
{line.slice(idx, idx + emWord.length)}
{line.slice(idx + emWord.length)}
>
);
};
return (
{T(TRINITY.hero.eyebrow)}
{titleLines.map((line, i) => (
{renderLine(line, i === titleLines.length - 1)}
))}
{T(TRINITY.hero.sub)}
{TRINITY.hero.meta.map((m, i) => (
{T(m)}
))}
4.9★
{T({ es: "Promedio paciente", en: "Patient avg." })}
{TRINITY.trust.map((tr, i) => (
{tr.num}
{T(tr.lbl)}
))}
);
}
// ============= SECTION HEAD helper =============
function SectionHead({ eyebrow, title, sub, split = true }) {
const T = useT();
const { lang } = useContext(LangCtx);
const lines = title?.[lang] || title?.en || [];
return (
{eyebrow && {T(eyebrow)} }
{lines.map((l, i) => {
// italicize middle line if 3 lines
if (lines.length === 3 && i === 1) return {l} ;
return {l} ;
})}
{sub &&
{T(sub)}
}
);
}
// ============= SERVICE PILLARS =============
function Pillars() {
const T = useT();
const ref = useReveal();
return (
{TRINITY.pillars.items.map((p, i) => (
{p.num}
{T(p.title)}
{T(p.body)}
{p.tags.map((tg, j) => {T(tg)} )}
))}
);
}
// ============= FEATURED TREATMENTS =============
function Featured() {
const T = useT();
const ref = useReveal();
return (
{T({ es: "Estética médica", en: "Medical aesthetics" })}
{T({ es: "Bienvenida bilingüe", en: "Bilingual welcome" })}
{TRINITY.featured.items.map((f) => (
))}
);
}
// ============= ABOUT / DOCTOR =============
function About() {
const T = useT();
const ref = useReveal();
return (
{T(TRINITY.about.eyebrow)}
{T({
es: "Una práctica donde la medicina, la regeneración y la estética se hablan.",
en: "A practice where medicine, regeneration, and aesthetics talk to each other.",
})}
{T(TRINITY.about.quote)}
{T(TRINITY.about.desc)}
{TRINITY.about.team.map((member, i) => (
{member.avatar}
{member.name}
{T(member.role)}
))}
);
}
// ============= WHY US =============
function WhyUs() {
const T = useT();
const ref = useReveal();
return (
{TRINITY.why.items.map((w, i) => (
))}
);
}
// ============= TESTIMONIALS CAROUSEL =============
function Testimonials() {
const T = useT();
const ref = useReveal();
const [idx, setIdx] = useState(0);
const items = TRINITY.testimonials.items;
const prev = () => setIdx((i) => (i - 1 + items.length) % items.length);
const next = () => setIdx((i) => (i + 1) % items.length);
useEffect(() => {
const timer = setInterval(() => setIdx((i) => (i + 1) % items.length), 7000);
return () => clearInterval(timer);
}, [items.length]);
return (
{T(TRINITY.testimonials.eyebrow)}
{(TRINITY.testimonials.title[useContext(LangCtx).lang] || TRINITY.testimonials.title.en).map((l, i, arr) => (
{arr.length === 3 && i === 1 ? {l} : l}
))}
{items.map((t2, i) => (
{Array.from({ length: t2.rating }).map((_, j) => )}
{T(t2.quote)}
{T(t2.name).charAt(0)}
{T(t2.name)}
{T(t2.treatment)} · {T(t2.loc)}
))}
{items.map((_, i) => (
setIdx(i)}
style={{
width: i === idx ? "2em" : "0.6em", height: "0.4em",
borderRadius: "999em",
background: i === idx ? "var(--surface)" : "rgba(255,255,255,.3)",
transition: "all 320ms cubic-bezier(.2,.7,.2,1)",
minHeight: "auto",
}}/>
))}
);
}
// ============= FAQ =============
function FAQ() {
const T = useT();
const ref = useReveal();
const [cat, setCat] = useState("general");
const [openIdx, setOpenIdx] = useState(0);
const items = TRINITY.faq.items.filter((it) => it.cat === cat);
return (
{TRINITY.faq.cats.map((c) => (
{ setCat(c.id); setOpenIdx(0); }}>
{T(c.label)}
))}
{items.length === 0 && (
{T({ es: "Próximamente más preguntas.", en: "More questions coming soon." })}
)}
{items.map((it, i) => {
const open = openIdx === i;
return (
setOpenIdx(open ? -1 : i)}>
{T(it.q)}
+
);
})}
);
}
// ============= CTA BANNER =============
function CTABanner() {
const T = useT();
const ref = useReveal();
const { lang } = useContext(LangCtx);
const lines = TRINITY.cta.title[lang] || TRINITY.cta.title.en;
return (
{lines.map((l, i, arr) => (
{arr.length === 3 && i === 1 ? {l} : l}
))}
{T(TRINITY.cta.sub)}
);
}
// ============= CONTACT =============
function ContactSection() {
const T = useT();
const ref = useReveal();
const [form, setForm] = useState({ name: "", email: "", phone: "", interest: "", message: "" });
const [errors, setErrors] = useState({});
const [sent, setSent] = useState(false);
const { lang } = useContext(LangCtx);
const validate = () => {
const e = {};
if (!form.name.trim()) e.name = TRINITY.contactSection.errors.name;
if (!form.email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) e.email = TRINITY.contactSection.errors.email;
if (!form.phone.trim()) e.phone = TRINITY.contactSection.errors.phone;
if (!form.interest) e.interest = TRINITY.contactSection.errors.interest;
setErrors(e);
return Object.keys(e).length === 0;
};
const submit = (ev) => {
ev.preventDefault();
if (!validate()) return;
setSent(true);
setTimeout(() => { setSent(false); setForm({ name: "", email: "", phone: "", interest: "", message: "" }); }, 4200);
};
const onField = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value }));
return (
);
}
// ============= FOOTER =============
function Footer() {
const T = useT();
const year = new Date().getFullYear();
return (
Trinity
Comprehensive Health Care
{T(TRINITY.business.tagline)}
{T({ es: "Práctica", en: "Practice" })}
{T({ es: "Visítenos", en: "Visit" })}
© {year} Trinity Comprehensive Health Care, Inc. {T({ es: "Todos los derechos reservados.", en: "All rights reserved." })}
);
}
// ============= TWEAKS PANEL =============
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"theme": "warm",
"accent": "blue",
"density": "default",
"lang": "es"
}/*EDITMODE-END*/;
function TweaksUI() {
const { setLang, lang } = useContext(LangCtx);
if (!window.useTweaks || !window.TweaksPanel) return null;
const [tw, setTweak] = window.useTweaks(TWEAK_DEFAULTS);
useEffect(() => {
document.body.dataset.theme = tw.theme;
document.body.dataset.accent = tw.accent;
document.body.dataset.density = tw.density;
}, [tw.theme, tw.accent, tw.density]);
useEffect(() => {
if (tw.lang && tw.lang !== lang) setLang(tw.lang);
// eslint-disable-next-line
}, [tw.lang]);
const { TweaksPanel, TweakSection, TweakRadio } = window;
return (
setTweak("lang", v)}
options={[
{ value: "es", label: "ES" },
{ value: "en", label: "EN" },
]}
/>
setTweak("theme", v)}
options={[
{ value: "warm", label: "Warm" },
{ value: "clinical", label: "Clinical" },
{ value: "botanical", label: "Botanic" },
]}
/>
setTweak("accent", v)}
options={[
{ value: "blue", label: "Blue" },
{ value: "teal", label: "Teal" },
{ value: "green", label: "Green" },
]}
/>
setTweak("density", v)}
options={[
{ value: "compact", label: "Compact" },
{ value: "default", label: "Default" },
{ value: "spacious", label: "Spacious" },
]}
/>
);
}
// ============= ROOT =============
function ActiveSectionTracker({ onChange }) {
useEffect(() => {
const ids = ["home", "services", "about", "testimonials", "faq", "contact"];
const els = ids.map((id) => document.getElementById(id)).filter(Boolean);
if (els.length === 0) return;
const io = new IntersectionObserver(
(entries) => {
const vis = entries.filter((e) => e.isIntersecting).sort((a, b) => b.intersectionRatio - a.intersectionRatio);
if (vis[0]) onChange(vis[0].target.id);
},
{ threshold: [0.25, 0.5, 0.75], rootMargin: "-80px 0px -50% 0px" }
);
els.forEach((el) => io.observe(el));
return () => io.disconnect();
}, [onChange]);
return null;
}
function App() {
const [active, setActive] = useState("home");
return (
Skip to main content
);
}
ReactDOM.createRoot(document.getElementById("root")).render( );