// ============================================================
// 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 →}
);
}
// 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
Revista Ultramontano es una publicación de actualidad y de pensamiento contrarrevolucionario, fiel al Magisterio perenne de la Iglesia y a la sana doctrina.
El nombre Ultramontano reivindica aquella postura que, frente a los nacionalismos eclesiásticos y las tentaciones de adaptar la fe a los poderes del mundo, mira siempre «más allá de los montes»: hacia Roma y la doctrina inmutable que la Iglesia ha custodiado a lo largo de los siglos.
Dirección
{retrato && (
)}
La revista es dirigida por el Dr. Dardo Juan Calderón, abogado en ejercicio del foro en la Provincia de Mendoza, Argentina, donde nació en 1958. Titulado en la Universidad de Mendoza y padre de numerosa familia. Es hijo de don Rubén Calderón Bouchet (1918-2012), destacado filósofo argentino dedicado al estudio de las ideas políticas y del pensamiento tradicional.
{pres && (
{pres.titulo}
{pres.bajada &&
{pres.bajada}
}
{parr}
)}
);
}
const PHP_URL = "enviar.php";
function Suscribirse({ emailInicial }) {
const [f, setF] = useState({ nombre: "", apellido: "", email: emailInicial || "" });
const [estado, setEstado] = useState(null);
function set(k, v) { setF(p => ({ ...p, [k]: v })); }
async function enviar() {
if (!f.nombre || !f.apellido || !f.email) { setEstado("incompleto"); return; }
if (!/^[^@]+@[^@]+\.[^@]+$/.test(f.email)) { setEstado("invalido"); return; }
setEstado("enviando");
let guardoEnBase = false;
let mandoMail = false;
// 1. Guardar en la base
try {
if (sb) {
const { error } = await sb.from("suscriptores").insert({
nombre: f.nombre, apellido: f.apellido, email: f.email
});
if (error) { console.error("Error guardando suscriptor:", error); }
else { guardoEnBase = true; }
}
} catch (e) { console.error("Excepción guardando suscriptor:", e); }
// 2. Mandar aviso por mail
try {
const r = await fetch(PHP_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
tipo: "suscripcion", email: f.email,
nombre: f.nombre + " " + f.apellido
})
});
const d = await r.json().catch(() => null);
if (d && d.ok) { mandoMail = true; }
else { console.error("PHP devolvió:", d); }
} catch (e) { console.error("Excepción enviando mail:", e); }
// Si al menos una de las dos funcionó, damos por ok
if (guardoEnBase || mandoMail) {
setEstado("ok");
setF({ nombre: "", apellido: "", email: "" });
} else {
setEstado("error");
}
}
const inp = { width: "100%", padding: "13px 14px", fontSize: 15,
border: "1px solid #ccc", borderRadius: 6, fontFamily: "inherit", marginBottom: 14 };
return (