527 lines
30 KiB
JavaScript
527 lines
30 KiB
JavaScript
/* =========================================================
|
||
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 dependencia tecnológica: las patentes de CRISPR están en manos de universidades de EE.UU. y Europa. Y si Paraguay quiere exportar a la Unión Europea —que tiene las reglas más estrictas del mundo—, tendrá que adaptarse a nuevas exigencias. \n\nEn resumen: Paraguay está apostando fuerte por la innovación científica. Pero la pregunta es: ¿será este un modelo sostenible… o un riesgo que aún no se ha evaluado del todo? \n\nAhora pasamos a la segunda noticia: **Escasez crítica de medicamentos biofarmacéuticos en Paraguay: insulina, anticancerígenos y terapias avanzadas en riesgo**. \n\nDesde abril de 2024, Paraguay enfrenta una grave escasez de medicamentos biofarmacéuticos, también llamados *biofármacos*. Estos son medicamentos complejos, hechos a partir de células vivas, usados para tratar enfermedades graves como la diabetes, el cáncer y enfermedades raras. Según el Ministerio de Salud (MSPBS), la disponibilidad de estos productos cayó del 88% en mayo de 2023 al 62% en mayo de 2024. \n\nEntre los fármacos que faltan más frecuentemente están: **insulina glargina y aspartada**, necesaria para personas con diabetes tipo 1 y tipo 2; *bevacizumab* y *trastuzumab*, usados en tratamientos de cáncer de mama y colorrectal; y terapias enzymáticas para enfermedades como la fibrosis quística. En el Hospital de Clínicas, el 40% de los pacientes diabéticos reciben dosis reducidas o espaciadas, lo que aumenta el riesgo de comas hiperglucémicos o amputaciones. En el Instituto de Oncología, se han postergado tratamientos programados por falta de *trastuzumab*. \n\nSegún la Sociedad Paraguaya de Endocrinología, “la escasez no es puntual: es estructural y ya está afectando la mortalidad”. \n\n¿Por qué ocurre esto? Hay tres causas principales. Primero, la reconfiguración de cadenas globales de suministro tras la pandemia: grandes laboratorios como Novo Nordisk, Roche y AbbVie priorizan mercados con mayor capacidad de pago. Segundo, la devaluación del guaraní, que en 2024 alcanzó cerca del 20%, encareciendo drásticamente las importaciones. Tercero, problemas logísticos: en mayo, el puerto de Nueva Palmira en Uruguay —por donde entra gran parte de los medicamentos a Paraguay— registró retrasos de hasta 21 días en el despacho. \n\nEl MSPBS reaccionó con medidas de emergencia. Implementó el *Plan Nacional de Racionamiento de Medicamentos*, priorizando hospitales públicos y pacientes en situación crítica. Negoció con laboratorios argentinos como Bagó y Procare para distribuir biosimilares —copias no idénticas de biofármacos patentados, que cuestan hasta un 30% menos—. También aprobó la importación temporal de medicamentos de India y China, aunque solo si cumplen con normas internacionales de calidad. \n\nPero persisten problemas serios. Muchos biofármacos requieren cadena de frío entre 2°C y 8°C. El 60% de los centros de salud rural no tienen cámaras termorreguladas. Además, el sistema público no puede acceder a créditos internacionales por su baja calificación crediticia (B+), y los precios negociados aún superan el presupuesto hospitalario. El 75% de los medicamentos llegan a través de intermediarios extranjeros, lo que eleva el precio final hasta un 400%. \n\nLa situación ya llevó a acciones legales. La Defensora del Pueblo abrió una investigación por “negligencia en el acceso a la salud”, y un grupo de pacientes con esclerosis múltiple presentó un amparo constitucional, argumentando que se viola el derecho a la salud, protegido por el artículo 87 de la Constitución Nacional. \n\nA nivel regional, Paraguay se unió a Chile y Uruguay para proponer un *Mecanismo de Abastecimiento conjunto de Biofármacos en MERCOSUR*, que permitiría consolidar compras y reducir precios. La iniciativa está siendo evaluada por el Consejo del Mercado Común. \n\nA largo plazo, Paraguay necesita invertir en producción local. El laboratorio público *Farmaceutica Paraguaya S.A.* (FP-S.A.) ya inició ensayos piloto de insulina humana en asociación con la UNA y el CONACYT. Pero los expertos advierten que tardará al menos 4 años en ser viable comercialmente. Mientras tanto, el país sigue vulnerable a choques externos. \n\nEn conclusión: esta no es solo una crisis de medicamentos, sino de **soberanía sanitaria**. Sin políticas activas de fomento industrial, regulación de precios y cooperación regional, Paraguay no podrá garantizar el acceso a terapias avanzadas —ni a personas diabéticas, ni a pacientes oncológicos, ni a niños con enfermedades genéticas raras. \n\nLa transcripción y las fuentes están disponibles en nuestra página web. El enlace está en el mensaje de Telegram. \n\nGracias por escuchar *Noticias Global*, el podcast que te ayuda a entender lo que sucede en tu país y en el mundo.",
|
||
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 { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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();
|
||
}
|
||
})();
|