| 1 | /** |
| 2 | * Panel de inicio. |
| 3 | * Contrato esperado de GET /inicio: |
| 4 | * { perfil, materias, publicaciones, circuloActual, asesoria, empresaDestacada } |
| 5 | */ |
| 6 | document.addEventListener('DOMContentLoaded', async () => { |
| 7 | Layout.init('inicio.html'); |
| 8 | Layout.setPageTitle('Inicio'); |
| 9 | |
| 10 | const content = document.getElementById('main-content'); |
| 11 | if (!content) return; |
| 12 | |
| 13 | content.innerHTML = '<div class="inicio-container"><p class="loading-message">Cargando tu información académica…</p></div>'; |
| 14 | |
| 15 | let dashboard = {}; |
| 16 | try { |
| 17 | dashboard = await API.request('/inicio'); |
| 18 | } catch { |
| 19 | // El endpoint se implementará en el backend. La UI mantiene estados vacíos. |
| 20 | } |
| 21 | |
| 22 | renderDashboard(content, dashboard || {}); |
| 23 | }); |
| 24 | |
| 25 | function renderDashboard(content, data) { |
| 26 | const perfil = data.perfil || null; |
| 27 | const materias = Array.isArray(data.materias) ? data.materias : []; |
| 28 | const publicaciones = Array.isArray(data.publicaciones) ? data.publicaciones : []; |
| 29 | const accesos = renderAccessCards(data); |
| 30 | |
| 31 | content.innerHTML = ` |
| 32 | <div class="inicio-container"> |
| 33 | ${perfil ? renderBanner(perfil) : ''} |
| 34 | <div class="inicio-layout"> |
| 35 | <section class="feed-column" aria-label="Actividad reciente"> |
| 36 | ${materias.length ? renderQuickQuestion(materias) : ''} |
| 37 | ${publicaciones.length ? publicaciones.map(renderPost).join('') : renderEmptyFeed()} |
| 38 | </section> |
| 39 | ${accesos ? `<aside class="sidebar-section" aria-label="Accesos directos">${accesos}</aside>` : ''} |
| 40 | </div> |
| 41 | </div> |
| 42 | `; |
| 43 | |
| 44 | document.getElementById('quick-question-form')?.addEventListener('submit', event => { |
| 45 | event.preventDefault(); |
| 46 | window.location.href = 'foro.html'; |
| 47 | }); |
| 48 | } |
| 49 | |
| 50 | function renderBanner(perfil) { |
| 51 | const semestre = Number(perfil.semestre) || 0; |
| 52 | const totalSemestres = Number(perfil.totalSemestres) || 0; |
| 53 | const avance = totalSemestres ? Math.min(100, Math.round((semestre / totalSemestres) * 100)) : 0; |
| 54 | |
| 55 | return ` |
| 56 | <section class="card maps-banner" aria-labelledby="maps-title"> |
| 57 | <div class="banner-content"> |
| 58 | <div> |
| 59 | <h1 id="maps-title">${escapeHtml(perfil.carrera || 'Mi trayectoria MAPS')}</h1> |
| 60 | <p>${perfil.certificado ? `Certificado activo: <strong>${escapeHtml(perfil.certificado)}</strong>` : 'Sin certificado activo'}${semestre ? ` · Semestre vigente: <strong>${semestre}.º</strong>` : ''}</p> |
| 61 | </div> |
| 62 | <span class="badge badge-materia">Plan MAPS oficial</span> |
| 63 | </div> |
| 64 | ${totalSemestres ? `<div class="progress-container"><div class="progress-labels"><span>Avance de carrera</span><span>Semestre ${semestre} de ${totalSemestres} · ${avance} %</span></div><div class="progress-bar" role="progressbar" aria-label="Avance de carrera" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${avance}"><div class="progress-fill" style="width: ${avance}%"></div></div></div>` : ''} |
| 65 | </section> |
| 66 | `; |
| 67 | } |
| 68 | |
| 69 | function renderQuickQuestion(materias) { |
| 70 | const options = materias.map(materia => `<option value="${escapeHtml(materia.id ?? '')}">${escapeHtml(materia.nombre || materia)}</option>`).join(''); |
| 71 | return ` |
| 72 | <form class="card create-post" id="quick-question-form"> |
| 73 | <label class="visually-hidden" for="quick-question">Nueva duda académica</label> |
| 74 | <input id="quick-question" name="question" type="text" maxlength="180" placeholder="¿Tienes una duda académica? Pregunta a tu comunidad..." required> |
| 75 | <div class="create-post-actions"><select aria-label="Materia de la duda"><option value="">Seleccionar materia...</option>${options}</select><button class="btn-solid" type="submit">Publicar duda</button></div> |
| 76 | </form> |
| 77 | `; |
| 78 | } |
| 79 | |
| 80 | function renderPost(post) { |
| 81 | const author = post.autor || {}; |
| 82 | const initials = (author.iniciales || author.nombre || '?').trim().split(/\s+/).map(word => word[0]).slice(0, 2).join('').toUpperCase(); |
| 83 | const tags = [post.materia && `<span class="badge badge-materia">${escapeHtml(post.materia)}</span>`, post.solucionAceptada && '<span class="badge badge-solved">✓ Solución aceptada</span>', post.verificadoDocente && '<span class="badge badge-verified">Verificado por docente</span>'].filter(Boolean).join(''); |
| 84 | |
| 85 | return ` |
| 86 | <article class="card post-card"> |
| 87 | <div class="post-header"><div class="user-block"><div class="avatar ${post.verificadoDocente ? 'prof' : ''}">${escapeHtml(initials)}</div><div class="user-data"><strong>${escapeHtml(author.nombre || 'Usuario')}</strong><span>${escapeHtml(post.fechaRelativa || '')}</span></div></div><div class="badges-wrap">${tags}</div></div> |
| 88 | <h2 class="post-title">${escapeHtml(post.titulo || '')}</h2> |
| 89 | ${post.contenido ? `<p class="post-body">${escapeHtml(post.contenido)}</p>` : ''} |
| 90 | <div class="post-footer"><div class="action-links"><a class="btn-action" href="foro.html">💬 ${Number(post.respuestas) || 0} respuestas</a></div><span class="post-status">${post.solucionAceptada ? 'Resuelto' : ''}</span></div> |
| 91 | </article> |
| 92 | `; |
| 93 | } |
| 94 | |
| 95 | function renderAccessCards(data) { |
| 96 | const cards = []; |
| 97 | const circulo = data.circuloActual; |
| 98 | const asesoria = data.asesoria; |
| 99 | const empresa = data.empresaDestacada; |
| 100 | |
| 101 | if (circulo) cards.push(`<section class="card side-card"><span class="badge badge-materia">${escapeHtml(circulo.fecha || 'Próximamente')}</span><h3>${escapeHtml(circulo.nombre || 'Círculo de estudio')}</h3>${circulo.descripcion ? `<p>${escapeHtml(circulo.descripcion)}</p>` : ''}<a class="btn-solid" href="circulos.html">Ver círculo</a></section>`); |
| 102 | if (asesoria) cards.push(`<section class="card side-card prof-card"><span class="badge badge-verified">Asesoría disponible</span><h3>${escapeHtml(asesoria.docente || 'Docente')}</h3>${asesoria.horario ? `<p>${escapeHtml(asesoria.horario)}</p>` : ''}<a class="btn-clean" href="mensajes.html">Enviar mensaje privado</a></section>`); |
| 103 | if (empresa) cards.push(`<section class="card side-card"><h3>Radar Semestre Empresarial</h3><p><strong>${escapeHtml(empresa.nombre || '')}${empresa.calificacion ? ` · ${escapeHtml(empresa.calificacion)} ★` : ''}</strong>${empresa.resena ? `<br>${escapeHtml(empresa.resena)}` : ''}</p><a class="side-link" href="empresarial.html">Ver directorio de empresas →</a></section>`); |
| 104 | |
| 105 | return cards.join(''); |
| 106 | } |
| 107 | |
| 108 | function renderEmptyFeed() { |
| 109 | return '<section class="card empty-state"><h2>Aún no hay actividad</h2><p>Las dudas, avisos y recursos de tu comunidad aparecerán aquí cuando estén disponibles.</p><a class="side-link" href="foro.html">Ir al foro →</a></section>'; |
| 110 | } |
| 111 | |
| 112 | function escapeHtml(value) { |
| 113 | return String(value ?? '').replace(/[&<>'"]/g, character => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' })[character]); |
| 114 | } |