// src/pages-gestores-aula.jsx — Aula virtual del Programa de Gestores // // AUTENTICACIÓN — MAGIC LINK (sin contraseñas), INDEPENDIENTE de la de // colegios miembros: backend clonado en /plataforma/gestores-auth/ con // cookie propia (lpde_gestor). La unidad de acceso es la PERSONA (alumno // del programa), no el colegio: un mismo correo puede tener sesión aquí // y en ?page=colegios-miembros a la vez sin conflicto. // login.php → POST {correo}: si el alumno está activo, envía enlace // acceso.php → el enlace del correo; setea cookie y vuelve aquí // session.php → ¿quién soy? {alumno:{nombre}, correo} // sesiones.php→ contenido del Sheet "Aula Gestores" (requiere sesión) // logout.php → borra la cookie // El alta/baja de alumnos se administra en la pestaña "Accesos" del Sheet. // // CONTENIDO // ───────── // Todo sale del Sheet (pestaña "Sesiones": una fila por sesión con módulo, // video y hasta 3 materiales). Fallback local: la estructura pública de // GESTORES_MODULOS/GESTORES_FECHAS (pages-rest.jsx) sin videos, para que // el aula nunca aparezca vacía si el Sheet no responde. const GESTORES_AUTH_API = 'https://lpdebate.org/plataforma/gestores-auth'; /* ── Fallback: programa 2026 desde la landing pública (sin videos) ── */ function aulaFallbackSessions() { const mods = window.GESTORES_MODULOS || []; const fechas = window.GESTORES_FECHAS || {}; const out = []; for (const m of mods) { for (const s of m.sesiones) { out.push({ n: s.n, modulo: m.code, modulo_titulo: m.title, title: s.title, speaker: s.resp, date: (fechas[s.n] ? fechas[s.n][0] : '') + (fechas[s.n] ? ' 2026' : ''), duration: '2 h', summary: s.desc, video: '', materials: [], }); } } return out; } /* ── Filas del Sheet (objetos del backend) → sesiones del aula ── */ function rowsToAulaSessions(rows) { const out = []; for (const raw of rows || []) { const row = {}; for (const k of Object.keys(raw || {})) row[k.trim().toLowerCase()] = raw[k]; const get = (name) => ((row[name] === undefined || row[name] === null) ? '' : String(row[name]).trim()); const n = parseInt(get('n'), 10); if (!n || !get('title')) continue; const pub = get('published').toLowerCase(); if (pub === 'false' || pub === 'no' || pub === '0') continue; const materials = []; for (let i = 1; i <= 3; i++) { const url = get(`material_${i}_url`); if (url) materials.push({ label: get(`material_${i}_label`) || 'Material ' + i, url }); } out.push({ n, modulo: get('modulo'), modulo_titulo: get('modulo_titulo'), title: get('title'), speaker: get('speaker'), date: get('date'), duration: get('duration'), summary: get('summary'), video: get('video'), materials, }); } out.sort((a, b) => a.n - b.n); return out; } /* ── Agrupar por módulo preservando el orden de las sesiones ── */ function aulaGroupByModulo(sessions) { const groups = []; const byCode = {}; for (const s of sessions) { const code = s.modulo || 'Programa'; if (!byCode[code]) { byCode[code] = { code, titulo: s.modulo_titulo || '', sesiones: [] }; groups.push(byCode[code]); } if (!byCode[code].titulo && s.modulo_titulo) byCode[code].titulo = s.modulo_titulo; byCode[code].sesiones.push(s); } return groups; } /* ── Hook: sesiones desde sesiones.php (requiere cookie), fallback local ── */ function useAulaGestoresData() { const [sessions, setSessions] = React.useState(aulaFallbackSessions); const [status, setStatus] = React.useState('loading'); // loading | live | fallback React.useEffect(() => { let cancelled = false; fetch(`${GESTORES_AUTH_API}/sesiones.php`, { cache: 'no-store', credentials: 'include' }) .then((r) => r.ok ? r.json() : Promise.reject('http ' + r.status)) .then((data) => { if (cancelled) return; const parsed = rowsToAulaSessions(data && data.rows); if (parsed.length) { setSessions(parsed); setStatus('live'); } else { setStatus('fallback'); } }) .catch((err) => { console.warn('[aula-gestores] sesiones.php failed, using fallback:', err); if (!cancelled) setStatus('fallback'); }); return () => { cancelled = true; }; }, []); return { sessions, status }; } /* ── Convertir URL de YouTube o Google Drive en URL embebible ── */ function aulaYtEmbedUrl(url) { if (!url) return null; const yt = url.match(/(?:youtu\.be\/|v=|\/embed\/|\/live\/)([A-Za-z0-9_-]{6,})/); if (yt) return `https://www.youtube-nocookie.com/embed/${yt[1]}?rel=0&modestbranding=1&playsinline=1&iv_load_policy=3`; const dr = url.match(/drive\.google\.com\/file\/d\/([A-Za-z0-9_-]{10,})/); if (dr) return `https://drive.google.com/file/d/${dr[1]}/preview`; return null; } /* ═══════════════════════════════════════════════════════════ LOGIN ═══════════════════════════════════════════════════════════ */ const AulaGestoresLogin = ({ setPage }) => { const { t } = useLang(); const [correo, setCorreo] = React.useState(''); const [err, setErr] = React.useState(''); const [sent, setSent] = React.useState(false); const [busy, setBusy] = React.useState(false); const submit = async (e) => { if (e) e.preventDefault(); setErr(''); const c = correo.trim().toLowerCase(); if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(c)) { setErr(t('Escribe el correo con el que te inscribiste al programa.', 'Enter the email you enrolled with.')); return; } setBusy(true); try { const r = await fetch(`${GESTORES_AUTH_API}/login.php`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ correo: c }), }); const data = await r.json().catch(() => ({ ok: false })); if (data.ok && data.found) { setSent(true); } else if (data.ok && data.found === false) { setErr(t('Este correo no está registrado en el Programa de Gestores. Verifica que sea el correo con el que te inscribiste, o escríbenos a contacto@lpdebate.org.', 'This email isn’t registered in the program. Make sure it’s the one you enrolled with, or write to contacto@lpdebate.org.')); } else if (data.error === 'rate_limited') { setErr(t('Demasiados intentos seguidos. Espera unos minutos y vuelve a intentar.', 'Too many attempts in a row. Please wait a few minutes and try again.')); } else if (data.error === 'datos_no_disponibles') { setErr(t('No pudimos consultar el registro de alumnos en este momento. Intenta de nuevo en unos minutos.', 'We couldn’t check the student registry right now. Please try again in a few minutes.')); } else if (data.error === 'mail_failed') { setErr(t('No pudimos enviar el correo. Intenta de nuevo o escríbenos a contacto@lpdebate.org.', 'We couldn’t send the email. Try again or write to contacto@lpdebate.org.')); } else { setErr(t('No pudimos procesar la solicitud. Intenta de nuevo o escríbenos a contacto@lpdebate.org.', 'We couldn’t process your request. Try again or write to contacto@lpdebate.org.')); } } catch (e2) { setErr(t('No pudimos conectar con el servidor. Revisa tu conexión o escríbenos a contacto@lpdebate.org.', 'We couldn’t reach the server. Check your connection or write to contacto@lpdebate.org.')); } finally { setBusy(false); } }; return (
{/* Panel izquierdo — branding */}
{t('Aula virtual', 'Virtual classroom')}

{t(<>Programa de
Gestores, <>Program
Managers course)}

{t('Las grabaciones de las 18 sesiones y el material complementario del programa, disponibles para los alumnos durante todo el curso.', 'Recordings of all 18 sessions and the program’s supporting materials, available to students throughout the course.')}
{/* Panel derecho — form de correo (magic link) */} {sent ?
{t('Revisa tu correo', 'Check your inbox')}

{t('Enlace en camino ✓', 'Link on its way ✓')}

{t(<>Enviamos un enlace de acceso a {correo.trim().toLowerCase()}. Ábrelo en este dispositivo y entrarás directo al aula., <>We sent an access link to {correo.trim().toLowerCase()}. Open it on this device and you’ll go straight to the classroom.)}

{t('¿No llega? Revisa la carpeta de spam, o escríbenos a contacto@lpdebate.org.', 'Didn’t get it? Check your spam folder, or write to contacto@lpdebate.org.')}

:
{t('Ingreso de alumnos', 'Student log in')}

{t('Entra con tu correo', 'Log in with your email')}

{t('Sin contraseñas: escribe el correo con el que te inscribiste y te enviamos un enlace de acceso. La sesión dura 30 días en este navegador.', 'No passwords: enter the email you enrolled with and we’ll send you an access link. Your session stays active for 30 days in this browser.')}

{ setCorreo(e.target.value); setErr(''); }} autoFocus autoComplete="email" placeholder={t('tucorreo@ejemplo.com', 'you@example.com')} style={{ width: '100%', marginBottom: err ? 8 : 20 }} /> {err &&
{err}
}
}
); }; /* ═══════════════════════════════════════════════════════════ AULA — módulos con sus sesiones ═══════════════════════════════════════════════════════════ */ const AulaGestoresDashboard = ({ sess, onOpen, onLogout }) => { const { t } = useLang(); const { sessions, status } = useAulaGestoresData(); const modulos = aulaGroupByModulo(sessions); const disponibles = sessions.filter((s) => s.video).length; return ( <> {/* Barra de sesión */}
{t('Aula virtual · Programa de Gestores', 'Virtual classroom · Program managers')}
{t('Hola,', 'Hello,')} {sess.user}
{/* Header */}
{t('Material del curso', 'Course materials')}

{t(<>Tus sesiones, <>Your sessions)}

{t('Las grabaciones y el material complementario se publican aquí después de cada sesión. Las sesiones sin video aún no se dictan.', 'Recordings and supporting materials are published here after each session. Sessions without a video haven’t been taught yet.')}

{disponibles} {t('de', 'of')} {sessions.length} {t('grabaciones disponibles', 'recordings available')}

{/* Módulos */} {modulos.map((m, mi) => (
{t('MÓDULO', 'MODULE')} {mi + 1} · {m.code}

{m.titulo}

{m.sesiones.map((s) => { const ready = !!s.video; return (
onOpen(s)} style={{ display: 'grid', gridTemplateColumns: '56px 1fr auto', gap: 20, alignItems: 'center', background: 'var(--bg)', border: '1px solid var(--line)', padding: '18px 22px', cursor: 'pointer', transition: 'all 220ms ease', }} onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'var(--red)'; e.currentTarget.style.boxShadow = '0 10px 30px rgba(0,0,0,0.06)'; }} onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'var(--line)'; e.currentTarget.style.boxShadow = 'none'; }} >
{String(s.n).padStart(2, '0')}

{s.title}

{s.speaker && {t('con', 'with')} {s.speaker}} {s.date && · {s.date}} {s.materials.length > 0 && · ⬇ {s.materials.length} material{s.materials.length > 1 ? t('es', 's') : ''}}
{ready ? t('▶ Video disponible', '▶ Video available') : t('Próximamente', 'Coming soon')}
); })}
))}

{status === 'live' ? t('sincronizado con el Sheet', 'live data') : status === 'loading' ? t('cargando…', 'loading…') : t('modo offline (programa local, sin videos)', 'offline mode (local program, no videos)')}

); }; /* ═══════════════════════════════════════════════════════════ VISTA DE SESIÓN — video + descargables ═══════════════════════════════════════════════════════════ */ const AulaGestoresSessionView = ({ session, sess, onBack }) => { const { t } = useLang(); const embed = aulaYtEmbedUrl(session.video); // Bloquear menú contextual sobre el reproductor (deterrent) const blockContext = (e) => { e.preventDefault(); return false; }; return ( <> {/* Barra de sesión */}
{t('Sesión iniciada como', 'Logged in as')} {sess.user}
{/* Encabezado de sesión */}
{t('SESIÓN', 'SESSION')} {String(session.n).padStart(2, '0')} {session.modulo && {session.modulo} · {session.modulo_titulo}} {session.date && · {session.date}}

{session.title}

{session.speaker && (

{t('con', 'with')} {session.speaker}

)}
{/* Layout principal: video + sidebar de materiales */}
{/* Video con watermark */}
{embed ?