This commit is contained in:
oliver committed 2026-09-25 13:41:04 -03:00
commit 86547731c8
5 files changed
+1278

No files matched your search

+526
View File
@@ -0,0 +1,526 @@
/* =========================================================
Learn & Prep — Spanish B1 News Podcast
Front-end logic: i18n, archive list, search, episode modal
========================================================= */
(function () {
"use strict";
/* ---------------------------------------------------------
Configuration
--------------------------------------------------------- */
const CONFIG = {
// List of episodes: returns [{ data: [{ topic, date, text_intro }] }]
listUrl: "https://n8n.odoo4projects.com/webhook/podcast",
// Episode detail: same webhook, date used as id
detailUrl: "https://n8n.odoo4projects.com/webhook/podcast",
// Audio download endpoint
audioUrl: "https://n8n.odoo4projects.com/webhook/podcast/audio",
// Query parameter used to pass the episode date
dateParam: "date",
telegramUrl: "https://t.me/Podcast_Learn_and_Prep_bot",
requestTimeout: 12000,
};
/* ---------------------------------------------------------
Translations
--------------------------------------------------------- */
const I18N = {
en: {
"meta.title": "Learn & Prep — Spanish B1 News Podcast",
"hero.eyebrow": "Spanish B1 · Daily news podcast",
"hero.title": "Learn Spanish with today's news — in 10 minutes a day",
"hero.subtitle":
"Every day we turn the latest headlines into a short, easy-to-follow Spanish podcast at B1 level. Three key words, the news and a clear summary — delivered straight to your Telegram inbox.",
"hero.cta": "Start on Telegram",
"hero.ctaSecondary": "Browse the archive",
"hero.badge1": "10 minutes a day",
"hero.badge2": "B1 level Spanish",
"hero.badge3": "3 new words per episode",
"archive.title": "Podcast archive",
"archive.subtitle": "Every episode, searchable by topic, keyword or date.",
"archive.search": "Search by topic, keyword or date…",
"archive.count": "{n} episodes",
"archive.countOne": "1 episode",
"archive.empty": "No episodes match your search.",
"archive.loading": "Loading episodes…",
"archive.error": "Could not load episodes. Please try again later.",
"archive.demo": "Live feed unavailable — showing sample data.",
"card.open": "Open episode",
"modal.loading": "Loading episode…",
"modal.error": "Could not load this episode. Please try again later.",
"modal.transcript": "Transcript",
"modal.sources": "Sources",
"modal.download": "Download audio",
"footer.text": "Daily Spanish B1 news podcast · Delivered via Telegram.",
},
de: {
"meta.title": "Learn & Prep — Spanisch B1 Nachrichten-Podcast",
"hero.eyebrow": "Spanisch B1 · Täglicher Nachrichten-Podcast",
"hero.title": "Spanisch lernen mit den Nachrichten von heute — in 10 Minuten pro Tag",
"hero.subtitle":
"Jeden Tag verwandeln wir die aktuellen Schlagzeilen in einen kurzen, leicht verständlichen Spanisch-Podcast auf B1-Niveau. Drei wichtige Vokabeln, die Nachrichten und eine klare Zusammenfassung — direkt in dein Telegram-Postfach.",
"hero.cta": "Auf Telegram starten",
"hero.ctaSecondary": "Zum Archiv",
"hero.badge1": "10 Minuten pro Tag",
"hero.badge2": "Spanisch Niveau B1",
"hero.badge3": "3 neue Wörter pro Folge",
"archive.title": "Podcast-Archiv",
"archive.subtitle": "Jede Folge, durchsuchbar nach Thema, Stichwort oder Datum.",
"archive.search": "Nach Thema, Stichwort oder Datum suchen…",
"archive.count": "{n} Folgen",
"archive.countOne": "1 Folge",
"archive.empty": "Keine Folgen passen zu deiner Suche.",
"archive.loading": "Folgen werden geladen…",
"archive.error": "Folgen konnten nicht geladen werden. Bitte später erneut versuchen.",
"archive.demo": "Live-Feed nicht verfügbar — Beispieldaten werden angezeigt.",
"card.open": "Folge öffnen",
"modal.loading": "Folge wird geladen…",
"modal.error": "Diese Folge konnte nicht geladen werden. Bitte später erneut versuchen.",
"modal.transcript": "Transkript",
"modal.sources": "Quellen",
"modal.download": "Audio herunterladen",
"footer.text": "Täglicher Spanisch-B1-Nachrichten-Podcast · Geliefert über Telegram.",
},
es: {
"meta.title": "Learn & Prep — Podcast de noticias en español B1",
"hero.eyebrow": "Español B1 · Podcast diario de noticias",
"hero.title": "Aprende español con las noticias de hoy — en 10 minutos al día",
"hero.subtitle":
"Cada día convertimos los titulares más recientes en un podcast corto y fácil de seguir, en español de nivel B1. Tres palabras clave, las noticias y un resumen claro — directo a tu Telegram.",
"hero.cta": "Empezar en Telegram",
"hero.ctaSecondary": "Ver el archivo",
"hero.badge1": "10 minutos al día",
"hero.badge2": "Español nivel B1",
"hero.badge3": "3 palabras nuevas por episodio",
"archive.title": "Archivo del podcast",
"archive.subtitle": "Todos los episodios, con búsqueda por tema, palabra clave o fecha.",
"archive.search": "Busca por tema, palabra clave o fecha…",
"archive.count": "{n} episodios",
"archive.countOne": "1 episodio",
"archive.empty": "Ningún episodio coincide con tu búsqueda.",
"archive.loading": "Cargando episodios…",
"archive.error": "No se pudieron cargar los episodios. Inténtalo más tarde.",
"archive.demo": "Feed no disponible — mostrando datos de ejemplo.",
"card.open": "Abrir episodio",
"modal.loading": "Cargando episodio…",
"modal.error": "No se pudo cargar este episodio. Inténtalo más tarde.",
"modal.transcript": "Transcripción",
"modal.sources": "Fuentes",
"modal.download": "Descargar audio",
"footer.text": "Podcast diario de noticias en español B1 · Enviado por Telegram.",
},
};
const LOCALES = { en: "en-GB", de: "de-DE", es: "es-ES" };
/* ---------------------------------------------------------
Sample data (used only when the live feed is unreachable,
so the page stays usable while the workflow is offline)
--------------------------------------------------------- */
const SAMPLE_LIST = [
{
data: [
{
topic:
"1. Paraguay siembra primera soja CRISPR comercial sin regulación de OMG en el mundo. \n2. Escasez crítica de insulina y biofármacos en hospitales públicos de Paraguay.",
date: "2026-09-25",
text_intro:
"Hoy en Mbokayaty, la temperatura estará entre 22 y 32 grados Celsius. No se esperan lluvias. \n\nHoy ",
},
],
},
];
const SAMPLE_DETAIL = [
{
podcast: "Learn_and_Prep",
topic:
"1. Paraguay siembra primera soja CRISPR comercial sin regulación de OMG en el mundo. \n2. Escasez crítica de insulina y biofármacos en hospitales públicos de Paraguay.",
text:
"Hoy en Mbokayaty, la temperatura estará entre 22 y 32 grados Celsius. No se esperan lluvias. \n\nHoy hablaremos de dos temas muy importantes para el futuro de Paraguay. Primero, de una innovación agrícola que está llamando la atención del mundo: la siembra de soja modificada con una tecnología llamada CRISPR, y cómo Paraguay se convirtió en el primer país en hacerlo sin aplicar las reglas habituales para organismos modificados genéticamente. Segundo, de una crisis silenciosa pero grave: la escasez de medicamentos biofarmacéuticos, como la insulina y tratamientos contra el cáncer, que está afectando directamente a miles de pacientes.\n\nAntes de entrar en los detalles, aprenderemos tres palabras clave del primer tema: **biotecnología**, **edición genética** y **trazabilidad**. \n\n*biodiversidad* no es la palabra que buscamos hoy. Vamos con las tres correctas: \n\nPrimera palabra: **biotecnología**. Es el uso de organismos vivos o sus componentes para crear productos útiles, como medicamentos o cultivos mejorados. Por ejemplo: *La biotecnología ayuda a desarrollar semillas que resisten mejor la sequía*. \n\nSegunda palabra: **edición genética**. Es una técnica que permite cambiar partes específicas del ADN de un organismo, como usar tijeras muy pequeñas para corregir errores en un libro. Por ejemplo: *Con la edición genética, los científicos pueden mejorar una planta sin introducir genes de otras especies*. \n\nTercera palabra: **trazabilidad**. Es la capacidad de seguir el camino de un producto desde su origen hasta el consumidor final. Por ejemplo: *Gracias a la trazabilidad, los compradores pueden saber si un grano vino de un cultivo con tecnología CRISPR o convencional*. \n\nAhora, pasamos al primer tema: **Paraguay registra récord de siembra de soja transgénica CRISPR: primer cultivo comercial del mundo sin regulación previa**. \n\nEste año, Paraguay comenzó a sembrar más de 250.000 hectáreas de soja modificada con la tecnología CRISPR-Cas9. ¿Por qué es especial esto? Porque Paraguay la autorizó sin considerarla un organismo modificado genéticamente (OMG), tal como lo hacen Argentina y Brasil. La razón es que esta soja —llamada *HRR2*— no tiene ADN de otras especies. En lugar de eso, se usó CRISPR, una herramienta molecular que edita el propio ADN de la planta, imitando mutaciones que podrían ocurrir naturalmente. \n\nLa empresa responsable es *Bioceres Holdings*, con sede en Argentina pero con operaciones clave en Paraguay. El Ministerio de Agricultura y Ganadería (MAG) dio su autorización en abril de 2024, sin pasar por el proceso habitual de evaluación ambiental ni por la Comisión Nacional de Biotecnología (CONABIO). Según el MAG, esta variedad ya se está cultivando en 6,8% del total de soja paraguaya —que en la última campaña llegó a 3,7 millones de hectáreas—. \n\n¿Qué ventajas tiene? Según datos oficiales, esta soja rinde 12% más que la convencional: supera los 3.400 kilogramos por hectárea. Además, usa un 20% menos de fertilizantes nitrogenados, lo cual es bueno para el suelo y para reducir emisiones de gases de efecto invernadero. Ramón Ríos, presidente de la Asociación Paraguaya de Soja, dijo que esta tecnología “permite adaptarse al cambio climático sin depender de importadores de semillas patentadas”. \n\nParaguay ya está exportando esta semilla a Colombia, Brasil y Perú, bajo licencias no exclusivas. Además, hay otros cultivos en desarrollo: arroz resistente a sequía y maíz biofortificado, ambos hechos con CRISPR por la Universidad Nacional de Asunción y el INTA de Argentina. El Ministerio de Ciencia y Tecnología asegura que Paraguay podría convertirse en un *hub* de biotecnología agrícola para América Latina. Incluso empresas globales como Corteva Agriscience y Bayer han mostrado interés en instalar laboratorios de evaluación en el país. \n\nSin embargo, la decisión no está libre de críticas. La Unión Europea advirtió que, al no exigir regulaciones estrictas, algunos países “diluyen los estándares de bioseguridad”. Greenpeace pidió al gobierno paraguayo que “revierta esta política antes de que se extienda a cultivos alimentarios para humanos”. También surgen preguntas éticas y ecológicas: ¿qué pasa si una planta CRISPR se cruza con una variedad silvestre? ¿Cómo controlar su diseminación si no hay etiquetado obligatorio? \n\nEl MAG responde que, según la ley paraguaya, los productos no regulados como OMG **no requieren etiquetado**, como ocurre en Estados Unidos, Japón y Argentina. Pero para tranquilizar a compradores, ha implementado un sistema voluntario de trazabilidad usando blockchain, desarrollado por la startup local *AgriTrace*. \n\nAun así, hay desafíos importantes. Los pequeños agricultores no tienen acceso fácil a estas semillas mejoradas, porque son más costosas y requieren acuerdos de licencia. También existe preocupación por la depLine truncated
links:
"<a href=https://www.abc.com.py/edicion-digital/agricultura/paraguay-lidera-en-america-latina-la-siembra-de-soja-crispr-sin-regulacion-de-omg-1591002.html>ABC Color: Paraguay lidera en América Latina la siembra de soja CRISPR sin regulación de OMG</a><br>\n<a href=https://hojaelectronica.com.py/mag-autoriza-250-mil-ha-de-soja-crispr-omg-o-no/>HojaElectronica: MAG autoriza 250 mil ha de soja CRISPR: ¿OMG o no?</a><br>\n<a href=https://www.clarin.com/field/agricultura/argentina-paraguay-puerta-cultivos-crispr-sin-regulacion-omg_0_1584721221.html>Clarín: Argentina y Paraguay abren la puerta a los cultivos CRISPR sin regulación de OMG</a><br>\n<a href=https://www.reuters.com/world/americas/paraguay-becomes-first-country-commercially-plant-crispr-soybean-without-gmo-rules-2024-06-05/>Reuters: Paraguay becomes first country to commercially plant CRISPR soybean without GMO rules</a><br>\n<a href=https://www.ultimahora.com/escasez-de-insulina-y-farmacos-oncologicos-ponen-en-alerta-al-sistema-de-salud-paraguayo-n557102.html>Última Hora: Escasez de insulina y fármacos oncológicos pone en alerta al sistema de salud paraguayo</a><br>\n<a href=https://www.mspbs.gov.py/boletin-medicamentos-mayo-2024/>MSPBS: Boletín de Disponibilidad de Medicamentos – Mayo 2024</a><br>\n<a href=https://www.lanacion.com.ar/mundo/mercosur-busca-mecanismo-conjunto-para-biofarmacos-tras-crisis-en-paraguay-y-uruguay-nid28062024/>La Nación (Chile): MERCOSUR busca mecanismo conjunto para biofármacos tras crisis en Paraguay y Uruguay</a><br>\n<a href=https://www.bloomberg.com/news/articles/2024-06-03/paraguay-s-drug-shortages-signal-deeper-gaps-in-health-infrastructure>Bloomberg: Paraguay’s Drug Shortages Signal Deeper Gaps in Health Infrastructure</a><br>",
date: "2026-09-25",
id: 2,
},
];
/* ---------------------------------------------------------
DOM references
--------------------------------------------------------- */
const el = {
list: document.getElementById("list"),
state: document.getElementById("state"),
count: document.getElementById("count"),
search: document.getElementById("search"),
notice: document.getElementById("notice"),
modal: document.getElementById("modal"),
modalDate: document.getElementById("modalDate"),
modalTitle: document.getElementById("modalTitle"),
modalBody: document.getElementById("modalBody"),
modalDownload: document.getElementById("modalDownload"),
langButtons: Array.from(document.querySelectorAll(".lang-switch button")),
};
/* ---------------------------------------------------------
State
--------------------------------------------------------- */
let lang = "en";
let episodes = [];
let usingDemoData = false;
let currentEpisode = null;
let lastFocused = null;
/* ---------------------------------------------------------
Helpers
--------------------------------------------------------- */
function t(key) {
const dict = I18N[lang] || I18N.en;
return dict[key] || I18N.en[key] || key;
}
function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, function (c) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c];
});
}
function formatDate(dateStr) {
if (!dateStr) return "";
const d = new Date(dateStr + "T00:00:00");
if (isNaN(d.getTime())) return dateStr;
try {
return d.toLocaleDateString(LOCALES[lang] || "en-GB", {
day: "numeric",
month: "short",
year: "numeric",
});
} catch (e) {
return dateStr;
}
}
// Renders the light markdown used by the backend (**bold**, *italic*, blank-line paragraphs)
function renderRichText(text) {
if (!text) return "";
const escaped = escapeHtml(text);
const withBold = escaped.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
const withItalic = withBold.replace(/\*(.+?)\*/g, "<em>$1</em>");
return withItalic
.split(/\n{2,}/)
.map(function (p) {
return "<p>" + p.replace(/\n/g, "<br>") + "</p>";
})
.join("");
}
async function fetchJson(url) {
const controller = new AbortController();
const timer = setTimeout(function () {
controller.abort();
}, CONFIG.requestTimeout);
try {
const res = await fetch(url, {
headers: { Accept: "application/json" },
signal: controller.signal,
});
if (!res.ok) throw new Error("HTTP " + res.status);
return await res.json();
} finally {
clearTimeout(timer);
}
}
// The list endpoint returns [{ data: [ ...episodes ] }]
function normalizeList(payload) {
if (!payload) return [];
const arr = Array.isArray(payload) ? payload : [payload];
const out = [];
arr.forEach(function (item) {
if (item && Array.isArray(item.data)) out.push.apply(out, item.data);
else if (item && typeof item === "object") out.push(item);
});
return out.filter(function (e) {
return e && (e.date || e.topic);
});
}
// The detail endpoint returns [{ podcast, topic, text, links, date, ... }]
function normalizeDetail(payload) {
if (!payload) return null;
const arr = Array.isArray(payload) ? payload : [payload];
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
if (item && Array.isArray(item.data) && item.data.length) return item.data[0];
if (item && typeof item === "object" && (item.text || item.topic)) return item;
}
return null;
}
function detailUrl(date) {
return CONFIG.detailUrl + "?" + CONFIG.dateParam + "=" + encodeURIComponent(date);
}
function audioUrl(date) {
return CONFIG.audioUrl + "?" + CONFIG.dateParam + "=" + encodeURIComponent(date);
}
/* ---------------------------------------------------------
i18n application
--------------------------------------------------------- */
function applyI18n() {
document.documentElement.lang = lang;
document.title = t("meta.title");
document.querySelectorAll("[data-i18n]").forEach(function (node) {
node.textContent = t(node.getAttribute("data-i18n"));
});
document.querySelectorAll("[data-i18n-placeholder]").forEach(function (node) {
node.setAttribute("placeholder", t(node.getAttribute("data-i18n-placeholder")));
});
el.langButtons.forEach(function (btn) {
btn.classList.toggle("is-active", btn.getAttribute("data-lang") === lang);
});
}
function setLang(next) {
if (!I18N[next] || next === lang) return;
lang = next;
try {
localStorage.setItem("lp-lang", lang);
} catch (e) {
/* storage unavailable — ignore */
}
applyI18n();
renderList();
if (currentEpisode) renderModal(currentEpisode);
}
function detectLang() {
let saved = null;
try {
saved = localStorage.getItem("lp-lang");
} catch (e) {
/* ignore */
}
if (saved && I18N[saved]) return saved;
const nav = (navigator.language || "en").slice(0, 2).toLowerCase();
return I18N[nav] ? nav : "en";
}
/* ---------------------------------------------------------
Archive list
--------------------------------------------------------- */
function setState(message, isError) {
if (!message) {
el.state.hidden = true;
el.state.textContent = "";
el.state.classList.remove("state--error");
return;
}
el.state.hidden = false;
el.state.textContent = message;
el.state.classList.toggle("state--error", !!isError);
}
function renderList() {
const query = (el.search.value || "").trim().toLowerCase();
const filtered = episodes
.slice()
.sort(function (a, b) {
return String(b.date || "").localeCompare(String(a.date || ""));
})
.filter(function (ep) {
if (!query) return true;
const haystack = [ep.topic, ep.text_intro, ep.date]
.filter(Boolean)
.join(" ")
.toLowerCase();
return haystack.indexOf(query) !== -1;
});
el.count.textContent =
filtered.length === 1
? t("archive.countOne")
: t("archive.count").replace("{n}", String(filtered.length));
if (!filtered.length) {
el.list.innerHTML = "";
setState(t("archive.empty"));
return;
}
setState("");
el.list.innerHTML = filtered
.map(function (ep) {
const date = escapeHtml(ep.date || "");
const topic = escapeHtml((ep.topic || "").trim());
const intro = escapeHtml((ep.text_intro || "").trim());
return (
'<button type="button" class="card" data-date="' +
date +
'">' +
'<span class="card__date">' +
escapeHtml(formatDate(ep.date)) +
"</span>" +
'<span class="card__topic">' +
topic +
"</span>" +
(intro ? '<span class="card__intro">' + intro + "</span>" : "") +
'<span class="card__cta">' +
escapeHtml(t("card.open")) +
'<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M13 6l6 6-6 6"/></svg>' +
"</span>" +
"</button>"
);
})
.join("");
}
async function loadEpisodes() {
setState(t("archive.loading"));
el.count.textContent = "";
try {
const payload = await fetchJson(CONFIG.listUrl);
const list = normalizeList(payload);
if (!list.length) throw new Error("empty");
episodes = list;
usingDemoData = false;
el.notice.hidden = true;
} catch (e) {
episodes = normalizeList(SAMPLE_LIST);
usingDemoData = true;
el.notice.textContent = t("archive.demo");
el.notice.hidden = false;
}
renderList();
}
/* ---------------------------------------------------------
Modal
--------------------------------------------------------- */
function section(title, innerHtml) {
return (
'<div class="modal__section"><h4>' +
escapeHtml(title) +
"</h4>" +
innerHtml +
"</div>"
);
}
function renderModal(ep) {
currentEpisode = ep;
el.modalDate.textContent = formatDate(ep.date);
el.modalTitle.textContent = (ep.topic || "").trim();
const parts = [];
if (ep.text) {
parts.push(section(t("modal.transcript"), '<div class="transcript">' + renderRichText(ep.text) + "</div>"));
}
if (ep.links) {
parts.push(section(t("modal.sources"), '<div class="sources">' + ep.links + "</div>"));
}
el.modalBody.innerHTML = parts.join("") || '<div class="state">' + escapeHtml(t("modal.error")) + "</div>";
// Harden the backend-provided source links
el.modalBody.querySelectorAll(".sources a").forEach(function (a) {
a.setAttribute("target", "_blank");
a.setAttribute("rel", "noopener noreferrer");
});
el.modalDownload.href = audioUrl(ep.date);
el.modalDownload.setAttribute("download", "learn-and-prep-" + (ep.date || "episode") + ".mp3");
}
function openModal() {
lastFocused = document.activeElement;
el.modal.hidden = false;
document.body.style.overflow = "hidden";
document.addEventListener("keydown", onKeydown);
const closeBtn = el.modal.querySelector(".modal__close");
if (closeBtn) closeBtn.focus();
}
function closeModal() {
el.modal.hidden = true;
document.body.style.overflow = "";
document.removeEventListener("keydown", onKeydown);
currentEpisode = null;
if (lastFocused && typeof lastFocused.focus === "function") lastFocused.focus();
}
function onKeydown(e) {
if (e.key === "Escape") closeModal();
}
async function openEpisode(date) {
openModal();
el.modalDate.textContent = formatDate(date);
el.modalTitle.textContent = "";
el.modalBody.innerHTML = '<div class="modal__loading">' + escapeHtml(t("modal.loading")) + "</div>";
el.modalDownload.href = audioUrl(date);
try {
const payload = await fetchJson(detailUrl(date));
const ep = normalizeDetail(payload);
if (!ep) throw new Error("empty");
renderModal(ep);
} catch (e) {
const sample = SAMPLE_DETAIL.filter(function (d) {
return d.date === date;
})[0];
if (sample) {
renderModal(sample);
} else {
el.modalBody.innerHTML = '<div class="state state--error">' + escapeHtml(t("modal.error")) + "</div>";
}
}
}
/* ---------------------------------------------------------
Events
--------------------------------------------------------- */
function bindEvents() {
el.langButtons.forEach(function (btn) {
btn.addEventListener("click", function () {
setLang(btn.getAttribute("data-lang"));
});
});
el.search.addEventListener("input", renderList);
el.list.addEventListener("click", function (e) {
const card = e.target.closest(".card");
if (card) openEpisode(card.getAttribute("data-date"));
});
el.modal.addEventListener("click", function (e) {
if (e.target.closest("[data-close]")) closeModal();
});
}
/* ---------------------------------------------------------
Init
--------------------------------------------------------- */
function init() {
lang = detectLang();
applyI18n();
const cta = document.getElementById("ctaTelegram");
if (cta) cta.href = CONFIG.telegramUrl;
bindEvents();
loadEpisodes();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();