/* Lote «Páginas recuperadas del sitio actual». * * Nueve destinos: FIT, Helicobacter pylori, Check Up, Acerca de, Formatos de * informe médico y cuatro fichas de sucursal. * * Los datos públicos viven en `src/recuperadas-data.js`; el registro de qué se * rescató, qué se excluyó y por qué, en * docs/BITACORA-PAGINAS-RECUPERADAS-v0.1.md. Las fichas de sucursal leen * dirección, teléfonos, horario y mapa de la API, nunca de la capa local. */ const RD = () => window.RECUPERADAS_DATA; // Sólo una aprobación explícita habilita contenido clínico pendiente. const rcpPublicar = (clave) => { const d = RD(); return !!(d && d.PUBLICACION_CLINICA && d.PUBLICACION_CLINICA[clave] === true); }; function RecuperadasStyles() { return ; } /* ───────────────────────── piezas compartidas ───────────────────────── */ function RcpCrumbs({ items }) { return (
); } function RcpPasos({ pasos, azul }) { if (azul) { return (
    {pasos.map((p, i) => (
  1. {p.img ? : p.chat && }
    {p.titulo}

    {p.texto}

  2. ))}
); } return (
    {pasos.map((p) => (
  1. {p.img && (
    )}
    {p.titulo}

    {p.texto}

  2. ))}
); } /* Animación de aparición al hacer scroll. Un solo IntersectionObserver para * toda la página y sin librerías. Solo oculta contenido si el navegador puede * mostrarlo después y si el usuario no pidió reducir movimiento; los bloques * que llegan tarde de la API se detectan con un MutationObserver. */ function useRevela(ref) { React.useEffect(() => { const raiz = ref.current; if (!raiz || !("IntersectionObserver" in window)) return; if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return; raiz.classList.add("rcp-anim"); const io = new IntersectionObserver((entradas) => { entradas.forEach((e) => { if (e.isIntersecting) { e.target.classList.add("is-visible"); io.unobserve(e.target); } }); }, { rootMargin: "0px 0px -8% 0px", threshold: 0.12 }); const vigilar = () => raiz.querySelectorAll(".rcp-revela:not(.is-visible)").forEach((el) => io.observe(el)); vigilar(); const mo = new MutationObserver(vigilar); mo.observe(raiz, { childList: true, subtree: true }); return () => { io.disconnect(); mo.disconnect(); raiz.classList.remove("rcp-anim"); }; }, []); } /* Estudios en vivo desde InfectoApp. Precio, entrega, muestra y contenedor * salen de la API (fuente de verdad); la landing solo decide cuáles mostrar. * Un id que falle se omite: la sección nunca enseña un precio inventado. */ function useEstudiosApi(ids) { const [lista, setLista] = React.useState(null); const llave = (ids || []).join(","); React.useEffect(() => { if (!ids || !ids.length || !window.InfectoAPI) { setLista([]); return; } let vivo = true; const E = window.InfectoEnrich; Promise.all(ids.map((id) => window.InfectoAPI.getEstudio(id) .then((e) => (E ? E.enriquecer(e) : e)) .catch(() => null) )).then((r) => { if (vivo) setLista(r); }); return () => { vivo = false; }; }, [llave]); return lista; } function RcpPrecio({ s }) { return s.precio.consultar ? Consultar precio : <>{s.precio.texto} MXN; } /* Ficha de la prueba principal con su precio público. `nombre` es el nombre * comercial (editorial); los datos duros siguen siendo los de la API. */ function RcpFicha({ id, nombre, openStudy, lead }) { const d = RD(); const lista = useEstudiosApi([id]); const s = lista && lista[0] && (nombre ? Object.assign({}, lista[0], { titulo: nombre }) : lista[0]); if (lista && !s) return null; return (
{!s ?
Cargando precio vigente…
: ( <>
{s.code && {s.code}} {s.categorias.slice(0, 1).map((c) => {c})}

{s.titulo}

Precio particular
Incluye IVA · sin costos ocultos {lead || "Solicitar por WhatsApp"} {openStudy && ( )}
)}
); } /* Venta cruzada: tres estudios relacionados, cada uno con el porqué. */ function RcpRelacionados({ items, openStudy, titulo, intro }) { const lista = useEstudiosApi(items.map((i) => i.id)); const listos = (lista || []).map((s, i) => s && Object.assign({}, s, { porque: items[i].porque })).filter(Boolean); if (lista && !listos.length) return null; return (
Complementa tu prueba

{titulo}

{intro &&

{intro}

}
{!lista ? items.map((i) =>
) : listos.map((s, i) => (
{s.code && {s.code}} {s.categorias.slice(0, 1).map((c) => {c})}

{s.titulo}

{s.porque}

Entrega: {s.entrega.texto}
))}
); } function rcpIrA(id) { const el = document.getElementById(id); if (el) el.scrollIntoView({ behavior: "smooth", block: "start" }); } /* Hero azul de las landings. `arte` es la ilustración o foto de la derecha. */ function RcpHeroAzul({ crumbs, eyebrow, icono, titulo, enfasis, entrada, cta, arte }) { return (
{eyebrow}

{titulo}
{enfasis}

{entrada}

{cta}
{arte}
); } function RcpArteChips({ chips }) { return (chips || []).map((c, i) => ( {c.texto} )); } /* Variante con fotografía: la foto va recortada en círculo dentro de los anillos. */ function RcpArteFoto({ src, alt, chips }) { return (
{alt}
); } function RcpClaves({ items }) { return (
{items.map((c) => (
{c.titulo}{c.texto}
))}
); } /* CTA corto entre secciones: el lector no tiene que llegar al final para * encontrar cómo pedir la prueba. */ function RcpCtaLinea({ texto, lead }) { const d = RD(); return (
{texto && {texto}} {lead || "Agendar por WhatsApp"}
); } /* Conversación dibujada para los pasos sin fotografía. Es decorativa. */ function RcpChat({ yo, lab }) { return ( ); } /* Franja para médicos y clínicas: la orden de laboratorio es una herramienta * profesional, no se le ofrece al paciente. */ function RcpPro() { const d = RD(); return (
¿Eres médico o clínica? Genera la orden de laboratorio para tu paciente desde InfectoApp.
Generar orden de laboratorio
); } function RcpAviso({ children }) { return (
{children}
); } /* El iframe se marca sin permisos extra y con título: es contenido de un * tercero y carga sin consentimiento previo (pendiente del lote de cookies). */ function RcpVideo({ id, titulo }) { if (!id) return null; return (