// ============================================================ // REVISTA ULTRAMONTANO - codigo del sitio publico // Para modificar el sitio, edita este archivo y volve a subirlo. // La base de datos (Supabase) ya esta configurada abajo. // ============================================================ const SUPABASE_URL = "https://hqtmhlmqwliaxdjjzleb.supabase.co"; const SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhxdG1obG1xd2xpYXhkamp6bGViIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzkxNjc2NTEsImV4cCI6MjA5NDc0MzY1MX0.ToQHqMiZnGs3nQMZpM08WMxIxz0aLzafsG_ZpMQ_mvw"; const { useState, useEffect } = React; let sb = null; try { sb = supabase.createClient(SUPABASE_URL, SUPABASE_KEY); } catch (err) {} function fmtFecha(f) { try { const d = new Date(f); const m = ["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"]; return d.getDate() + " " + m[d.getMonth()] + " " + d.getFullYear(); } catch (e) { return f; } } function Etq({ t }) { return {t}; } // Convierte URL de YouTube/Vimeo/Drive/etc a la URL de embed adecuada // Convierte "Dardo Juan Calderón" a "dardo-juan-calderon" function slug(s) { return (s || "").toLowerCase() .normalize("NFD").replace(/[\u0300-\u036f]/g, "") .replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""); } function videoEmbed(url) { if (!url) return null; const u = url.trim(); // YouTube: youtube.com/watch?v=XXXX o youtu.be/XXXX o ya un embed let m = u.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{6,})/); if (m) return "https://www.youtube.com/embed/" + m[1]; // Vimeo: vimeo.com/123456789 m = u.match(/vimeo\.com\/(?:video\/)?(\d+)/); if (m) return "https://player.vimeo.com/video/" + m[1]; // Google Drive: drive.google.com/file/d/FILEID/view -> /preview m = u.match(/drive\.google\.com\/file\/d\/([a-zA-Z0-9_-]+)/); if (m) return "https://drive.google.com/file/d/" + m[1] + "/preview"; // Gloria.tv: NO devolvemos URL — el render usa el componente GloriaEmbed con oEmbed m = u.match(/gloria\.tv\/(?:post|video|embed)\/([a-zA-Z0-9]+)/); if (m) return null; // marca especial: usar GloriaEmbed // Cualquier otra URL: la usamos tal cual return u; } function esGloriaTv(url) { return /gloria\.tv\/(?:post|video|embed)\//.test(url || ""); } // Componente para embeber Gloria.tv usando su API oficial oEmbed function GloriaEmbed({ url }) { const [html, setHtml] = useState(null); const [err, setErr] = useState(false); useEffect(() => { let cancelado = false; (async () => { try { const oembedUrl = "https://gloria.tv/oembed/?url=" + encodeURIComponent(url) + "&format=json"; const r = await fetch(oembedUrl); if (!r.ok) throw new Error("oEmbed " + r.status); const d = await r.json(); if (!cancelado && d.html) setHtml(d.html); else if (!cancelado) setErr(true); } catch (e) { if (!cancelado) setErr(true); } })(); return () => { cancelado = true; }; }, [url]); if (err) { // Fallback: link a Gloria.tv si oEmbed falla return ( ▶ Ver video en Gloria.tv ); } if (!html) { return
Cargando video…
; } return
; } // Estilo de imagen de fondo según posición y modo elegidos por artículo const POS_MAP = { "center": "center center", "top-center": "center top", "bottom-center": "center bottom", "left": "left center", "right": "right center", // intermedias (porcentajes para puntos a mitad de camino) "top-mid": "center 25%", // entre arriba y centro "bottom-mid": "center 75%", // entre abajo y centro "left-mid": "25% center", // entre izquierda y centro "right-mid": "75% center", // entre derecha y centro // compatibilidad con valores viejos "top-left": "left top", "top-right": "right top", "bottom-left": "left bottom", "bottom-right": "right bottom" }; function bg(art) { // acepta el artículo (objeto) o una url string const url = typeof art === "string" ? art : (art && art.imagen_url); if (!url) return {}; const pos = (art && art.img_pos) ? (POS_MAP[art.img_pos] || "center center") : "center center"; const fit = (art && art.img_fit) || "cover"; let size = "cover"; if (fit === "alto") size = "auto 100%"; else if (fit === "ancho") size = "100% auto"; return { backgroundImage: `url(${url})`, backgroundSize: size, backgroundPosition: pos, backgroundRepeat: "no-repeat" }; } function PlayIcon() { return (
); } function Card({ a, abrir }) { const esAcad = a.categorias && a.categorias.slug === "academico"; return (
abrir(a)}>
{a.video_url && }

{a.titulo}

por {a.autores ? a.autores.nombre : ""} · {fmtFecha(a.fecha)}
); } function Home({ arts: arts0, abrir, irAutor, irAutores }) { // Presentación no se lista en el sitio (vive en Quiénes somos) const arts = arts0.filter(a => a.slug !== "presentacion"); // Destacados grande/laterales (1, 2, 3) van arriba en el bloque destacado const dest = arts.filter(a => a.destacada > 0 && a.destacada <= 3) .sort((x, y) => x.destacada - y.destacada); const grande = dest[0], lat = dest.slice(1, 3); // El resto va en Últimas, pero los de destacada=4 al tope const noDest = arts.filter(a => !a.destacada || a.destacada === 0 || a.destacada === 4); // Orden: primero los de destacada=4, después los normales por fecha noDest.sort((a, b) => { const da = a.destacada === 4 ? 1 : 0; const db = b.destacada === 4 ? 1 : 0; if (da !== db) return db - da; return 0; // ya vienen ordenados por fecha desde la query }); const resto = noDest.slice(0, 10); const mas = [...arts].sort((a, b) => (b.vistas || 0) - (a.vistas || 0)).slice(0, 5); const aus = [...new Set(arts.map(a => a.autores && a.autores.nombre).filter(Boolean))] .filter(x => x !== "Editorial").slice(0, 5); return (
{grande && (
abrir(grande)}>
{grande.video_url && }

{grande.titulo}

por {grande.autores ? grande.autores.nombre : ""} · {fmtFecha(grande.fecha)}

{grande.bajada}

{lat.map(a => (
abrir(a)}>
{a.video_url && }

{a.titulo}

por {a.autores ? a.autores.nombre : ""}
))}
)} {mas.length > 0 &&
LO MÁS LEÍDO
}
{mas.map((a, i) => (
abrir(a)}>
{i + 1}
{a.titulo}
{a.autores ? a.autores.nombre : ""}
))}
ÚLTIMAS PUBLICACIONES
{resto.map(a => )}
{aus.length > 0 && (
irAutores && irAutores()} style={{ cursor: irAutores ? "pointer" : "default" }}> POR AUTOR {irAutores && ver todos →}
)}
{aus.map(nm => (
irAutor && irAutor(nm)} style={{ cursor: "pointer" }}>
{nm}
{arts.filter(a => a.autores && a.autores.nombre === nm).length} artículo(s)
))}
TE PUEDE INTERESAR
{resto.slice(0, 4).map(a => )}
); } // Iconos SVG oficiales de redes sociales const IcoWa = () => ; const IcoX = () => ; const IcoFb = () => ; const IcoCopy = () => ; function Articulo({ art, arts, abrir, volver }) { // Registrar visita (filtra repetidos por sesión + día, y bots básicos) useEffect(() => { if (!art || !art.id) return; (async () => { try { const ua = (navigator.userAgent || "").toLowerCase(); // Detección básica de bots const esBot = /bot|crawl|spider|slurp|facebookexternalhit|whatsapp|preview|telegram/i.test(ua); // Hash simple basado en UA + un id de sesión persistente let sid = localStorage.getItem("ums_sid"); if (!sid) { sid = Math.random().toString(36).slice(2) + Date.now().toString(36); localStorage.setItem("ums_sid", sid); } const hash = btoa((ua + "|" + sid).slice(0, 80)).slice(0, 32); await sb.rpc("registrar_visita", { p_articulo_id: art.id, p_visitante_hash: hash, p_es_bot: esBot }); } catch (e) { /* si falla, no rompe la lectura */ } })(); }, [art && art.id]); function enlaceArticulo() { return window.location.origin + "/" + art.slug; } const url = art.slug ? enlaceArticulo() : window.location.href; const txt = encodeURIComponent(art.titulo); function copiar() { const ok = () => alert("Enlace copiado:\n" + url); if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(url).then(ok).catch(() => { prompt("Copiá este enlace:", url); }); } else { prompt("Copiá este enlace:", url); } } // Render del cuerpo: detecta HTML o texto plano, procesa marcadores [IMG:...] y [VIDEO:...] const parr = (() => { const cuerpo = art.cuerpo || ""; const esHtml = /<(p|h2|h3|div|ul|ol|blockquote|br)[\s>]/i.test(cuerpo); // Lista de bloques a renderizar let bloques = []; if (esHtml) { // Dividir por bloques HTML pero respetando marcadores que aparecen // dentro de párrafos o como párrafos solos. // 1. Reemplazar marcadores que están dentro de

...[IMG:...]...

// sacándolos a bloque propio. Usamos un parser DOM en el browser. try { const div = document.createElement("div"); div.innerHTML = cuerpo; // Recorremos los hijos directos Array.from(div.children).forEach(nodo => { // Si el nodo es

y su texto entero es un marcador, sacamos el marcador const textoNodo = nodo.textContent.trim(); const mImg = textoNodo.match(/^\[IMG:([^|\]]+)(?:\|([^|\]]*))?(?:\|([^\]]*))?\]$/); const mVid = textoNodo.match(/^\[VIDEO:([^|\]]+)(?:\|([^\]]*))?\]$/); if (mImg) { bloques.push({ tipo: "img", url: mImg[1].trim(), tam: (mImg[2] || "grande").trim(), pie: (mImg[3] || "").trim() }); } else if (mVid) { bloques.push({ tipo: "video", url: mVid[1].trim(), cap: (mVid[2] || "").trim() }); } else { bloques.push({ tipo: "html", html: nodo.outerHTML }); } }); } catch (e) { // Si falla el parser DOM, mostramos como bloque único bloques.push({ tipo: "html", html: cuerpo }); } } else { // Texto plano clásico: dividir por línea en blanco bloques = cuerpo.split(/\n\n+/).map(p => { const t = p.trim(); if (!t) return null; if (t.startsWith("") && t.endsWith("")) return { tipo: "h2", texto: t.replace(/<\/?strong>/g, "") }; const mImg = t.match(/^\[IMG:([^|\]]+)(?:\|([^|\]]*))?(?:\|([^\]]*))?\]$/); if (mImg) return { tipo: "img", url: mImg[1].trim(), tam: (mImg[2] || "grande").trim(), pie: (mImg[3] || "").trim() }; const mVid = t.match(/^\[VIDEO:([^|\]]+)(?:\|([^\]]*))?\]$/); if (mVid) return { tipo: "video", url: mVid[1].trim(), cap: (mVid[2] || "").trim() }; return { tipo: "p", html: t }; }).filter(Boolean); } // Renderizar cada bloque return bloques.map((b, i) => { if (b.tipo === "h2") return

{b.texto}

; if (b.tipo === "p") return

; if (b.tipo === "html") return

; if (b.tipo === "img") { let estiloImg = { display: "block", margin: "20px auto", maxWidth: "100%", height: "auto", borderRadius: 8 }; if (b.tam === "chica") { estiloImg = { float: "right", maxWidth: 280, margin: "6px 0 14px 22px", height: "auto", borderRadius: 6 }; } else if (b.tam === "mediana") { estiloImg = { display: "block", margin: "20px auto", maxWidth: 480, height: "auto", borderRadius: 8 }; } return (
{b.pie} {b.pie &&
{b.pie}
}
); } if (b.tipo === "video") { return (
{esGloriaTv(b.url) ? ( ) : (