| 1 | /** |
| 2 | * Círculos de estudio. |
| 3 | * Contrato esperado de GET /circulos: |
| 4 | * { sesiones, misGrupos } con sesiones [{ id, titulo, materia, fecha, enVivo, |
| 5 | * moderador, plataforma, descripcion, asistentes }] y misGrupos [{ id, nombre, |
| 6 | * descripcion, miembros }]. |
| 7 | */ |
| 8 | document.addEventListener('DOMContentLoaded', async () => { |
| 9 | Layout.init('circulos.html'); |
| 10 | Layout.setPageTitle('Círculos de Estudio'); |
| 11 | |
| 12 | const content = document.getElementById('main-content'); |
| 13 | if (!content) return; |
| 14 | content.innerHTML = '<div class="circulos-container"><p class="loading-message">Cargando círculos de estudio…</p></div>'; |
| 15 | await loadCircles(content); |
| 16 | }); |
| 17 | |
| 18 | async function loadCircles(content, search = '') { |
| 19 | let response = {}; |
| 20 | try { |
| 21 | response = await API.request(`/circulos${search ? `?busqueda=${encodeURIComponent(search)}` : ''}`); |
| 22 | } catch { |
| 23 | // El endpoint se conectará cuando el módulo esté disponible en backend. |
| 24 | } |
| 25 | |
| 26 | const sessions = Array.isArray(response.sesiones) ? response.sesiones : []; |
| 27 | const groups = Array.isArray(response.misGrupos) ? response.misGrupos : []; |
| 28 | renderCircles(content, sessions, groups); |
| 29 | } |
| 30 | |
| 31 | function renderCircles(content, sessions, groups) { |
| 32 | content.innerHTML = ` |
| 33 | <div class="circulos-container"> |
| 34 | <header class="module-heading"><h1>Círculos de estudio</h1><p>Encuentra sesiones de repaso y comunidades para aprender en conjunto.</p></header> |
| 35 | <form class="top-action-bar" id="circle-search-form"> |
| 36 | <label class="visually-hidden" for="circle-search">Buscar círculo o sesión</label> |
| 37 | <input class="search-input" id="circle-search" name="busqueda" type="search" placeholder="Buscar círculo o sesión por materia..."> |
| 38 | <button class="btn-solid" type="button" id="create-circle">+ Crear nuevo círculo</button> |
| 39 | </form> |
| 40 | <div class="circulos-grid"> |
| 41 | <section><h2 class="section-title">Próximas sesiones de repaso</h2>${sessions.length ? sessions.map(renderSession).join('') : renderEmptySessions()}</section> |
| 42 | <aside><h2 class="section-title">Tus grupos permanentes</h2>${groups.length ? `<section class="card">${groups.map(renderGroup).join('')}</section>` : ''}<section class="card rules-card"><h2>Normas del círculo</h2><p>Los círculos son espacios colaborativos. Comparte material libre de plagio y mantén el respeto en las salas de videollamada.</p></section></aside> |
| 43 | </div> |
| 44 | </div> |
| 45 | `; |
| 46 | |
| 47 | document.getElementById('circle-search-form')?.addEventListener('submit', event => { |
| 48 | event.preventDefault(); |
| 49 | loadCircles(content, new FormData(event.currentTarget).get('busqueda')?.trim()); |
| 50 | }); |
| 51 | document.getElementById('create-circle')?.addEventListener('click', () => { |
| 52 | // La pantalla de creación se conectará cuando esté disponible el endpoint POST /circulos. |
| 53 | }); |
| 54 | } |
| 55 | |
| 56 | function renderSession(session) { |
| 57 | const live = Boolean(session.enVivo); |
| 58 | return ` |
| 59 | <article class="card session-card ${live ? 'live-session' : ''}"> |
| 60 | <div class="session-header"><div class="badges-wrap">${live ? '<span class="badge badge-live">● En vivo ahora</span>' : session.fecha ? `<span class="badge badge-time">${escapeHtml(session.fecha)}</span>` : ''}${session.materia ? `<span class="badge badge-materia">${escapeHtml(session.materia)}</span>` : ''}</div>${session.sala ? `<span class="attendees-count">${escapeHtml(session.sala)}</span>` : ''}</div> |
| 61 | <h2 class="session-title">${escapeHtml(session.titulo || 'Sesión de repaso')}</h2> |
| 62 | ${(session.moderador || session.plataforma) ? `<p class="session-info">${session.moderador ? `Moderador: <strong>${escapeHtml(session.moderador)}</strong>` : ''}${session.moderador && session.plataforma ? ' · ' : ''}${session.plataforma ? `Plataforma: <strong>${escapeHtml(session.plataforma)}</strong>` : ''}</p>` : ''} |
| 63 | ${session.descripcion ? `<p class="session-desc">${escapeHtml(session.descripcion)}</p>` : ''} |
| 64 | <div class="session-footer">${Number.isFinite(Number(session.asistentes)) ? `<span class="attendees-count">👥 ${Number(session.asistentes)} ${live ? 'estudiantes conectados' : 'confirmados'}</span>` : ''}<div class="session-actions">${session.materialDisponible ? '<button class="btn-clean" type="button">Ver material de apoyo</button>' : ''}<button class="btn-solid" type="button">${live ? 'Entrar a sala' : 'Confirmar asistencia'}</button></div></div> |
| 65 | </article> |
| 66 | `; |
| 67 | } |
| 68 | |
| 69 | function renderGroup(group) { |
| 70 | return `<article class="group-item"><span class="group-name">${escapeHtml(group.nombre || 'Grupo de estudio')}</span>${group.descripcion ? `<span class="group-description">${escapeHtml(group.descripcion)}</span>` : ''}<div class="group-meta">${Number.isFinite(Number(group.miembros)) ? `<span>${Number(group.miembros)} miembros</span>` : '<span></span>'}<button class="group-link" type="button">Ver grupo →</button></div></article>`; |
| 71 | } |
| 72 | |
| 73 | function renderEmptySessions() { |
| 74 | return '<section class="card empty-circles"><h2>Aún no hay sesiones programadas</h2><p>Las sesiones de repaso y los círculos disponibles aparecerán aquí cuando sean creados.</p></section>'; |
| 75 | } |
| 76 | |
| 77 | function escapeHtml(value) { |
| 78 | return String(value ?? '').replace(/[&<>'"]/g, character => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' })[character]); |
| 79 | } |