| 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 | mostrarBienvenida(); |
| 25 | }); |
| 26 | |
| 27 | function mostrarBienvenida() { |
| 28 | // Limpia cualquier flag viejo que pudiera haber quedado de versiones anteriores. |
| 29 | try { localStorage.removeItem('bienvenida-pendiente'); } catch (e) {} |
| 30 | |
| 31 | let pendiente = false; |
| 32 | try { |
| 33 | pendiente = |
| 34 | localStorage.getItem('bienvenida-pendiente') === '1' || |
| 35 | sessionStorage.getItem('bienvenida-pendiente') === '1'; |
| 36 | } catch (e) {} |
| 37 | if (!pendiente) return; |
| 38 | |
| 39 | try { |
| 40 | sessionStorage.removeItem('bienvenida-pendiente'); |
| 41 | localStorage.removeItem('bienvenida-pendiente'); |
| 42 | } catch (e) {} |
| 43 | |
| 44 | const nombre = (Auth.getUser() && Auth.getUser().nombre) || ''; |
| 45 | |
| 46 | const overlay = document.createElement('div'); |
| 47 | overlay.className = 'welcome-overlay'; |
| 48 | overlay.id = 'welcomeOverlay'; |
| 49 | overlay.setAttribute('aria-hidden', 'true'); |
| 50 | overlay.innerHTML = ` |
| 51 | <div class="welcome-popup"> |
| 52 | <div class="popup-tag"> |
| 53 | <span class="pulse-mini"></span> |
| 54 | <span>MAPS CONNECT // ACCESS GRANTED</span> |
| 55 | </div> |
| 56 | <h1 class="popup-title">BIENVENIDO</h1> |
| 57 | <div class="popup-username" id="popupUserName">${escapeHtml(nombre || 'Usuario').toUpperCase()}</div> |
| 58 | <p class="popup-status">SESIÓN INICIADA • CARGANDO ENTORNO HUD...</p> |
| 59 | </div> |
| 60 | `; |
| 61 | document.body.appendChild(overlay); |
| 62 | |
| 63 | setTimeout(() => { |
| 64 | overlay.classList.add('fade-out'); |
| 65 | // Elimina el overlay del DOM al terminar la transición para que nunca se quede pegado. |
| 66 | setTimeout(() => { |
| 67 | if (overlay.parentNode) overlay.parentNode.removeChild(overlay); |
| 68 | }, 550); |
| 69 | }, 1500); |
| 70 | } |
| 71 | |
| 72 | function renderDashboard(content, data) { |
| 73 | const perfil = data.perfil || null; |
| 74 | const esDocente = perfil?.rol === 'PROFESOR'; |
| 75 | const materias = Array.isArray(data.materias) ? data.materias : []; |
| 76 | const publicaciones = Array.isArray(data.publicaciones) ? data.publicaciones : []; |
| 77 | const accesos = renderAccessCards(data); |
| 78 | |
| 79 | content.innerHTML = ` |
| 80 | <div class="inicio-container"> |
| 81 | ${perfil ? (esDocente ? renderDocenteBanner(perfil) : renderBanner(perfil)) : ''} |
| 82 | ${data.carreraFiltrada && !esDocente ? `<p class="filter-note">Mostrando solo contenido de tu carrera: <strong>${escapeHtml(data.carreraFiltrada)}</strong></p>` : ''} |
| 83 | <div class="main-layout"> |
| 84 | <section class="feed-column" aria-label="Actividad reciente"> |
| 85 | ${!esDocente && materias.length ? renderQuickQuestion(materias) : ''} |
| 86 | ${publicaciones.length ? publicaciones.map(renderPost).join('') : renderEmptyFeed()} |
| 87 | </section> |
| 88 | ${accesos ? `<aside class="sidebar-section" aria-label="Accesos directos">${accesos}</aside>` : ''} |
| 89 | </div> |
| 90 | </div> |
| 91 | `; |
| 92 | |
| 93 | document.getElementById('quick-question-form')?.addEventListener('submit', event => { |
| 94 | event.preventDefault(); |
| 95 | window.location.href = 'comunidad.html#foro-dudas'; |
| 96 | }); |
| 97 | |
| 98 | content.addEventListener('click', event => { |
| 99 | const followBtn = event.target.closest('.follow-btn'); |
| 100 | if (!followBtn) return; |
| 101 | event.preventDefault(); |
| 102 | const id = followBtn.getAttribute('data-id'); |
| 103 | if (!id) return; |
| 104 | Utils.alternarSeguir(followBtn, id); |
| 105 | }); |
| 106 | } |
| 107 | |
| 108 | function renderDocenteBanner(perfil) { |
| 109 | return ` |
| 110 | <section class="card maps-banner prof-card" aria-labelledby="prof-title"> |
| 111 | <div class="banner-content"> |
| 112 | <div> |
| 113 | <h2 id="prof-title">Bienvenido, Docente</h2> |
| 114 | <p>Panel de docencia MAPS. Aquí aparecerán los avisos y materiales que compartes con tu comunidad.</p> |
| 115 | </div> |
| 116 | <span class="badge badge-verified">Perfil Docente</span> |
| 117 | </div> |
| 118 | </section> |
| 119 | `; |
| 120 | } |
| 121 | |
| 122 | function renderBanner(perfil) { |
| 123 | const semestre = Number(perfil.semestre) || 0; |
| 124 | const totalSemestres = Number(perfil.totalSemestres) || 0; |
| 125 | const avance = totalSemestres ? Math.min(100, Math.round((semestre / totalSemestres) * 100)) : 0; |
| 126 | |
| 127 | return ` |
| 128 | <section class="card maps-banner" aria-labelledby="maps-title"> |
| 129 | <div class="banner-content"> |
| 130 | <div> |
| 131 | <h2 id="maps-title">${escapeHtml(perfil.carrera || 'Mi trayectoria MAPS')}</h2> |
| 132 | <p>${perfil.certificado ? `Certificado Activo: <strong>${escapeHtml(perfil.certificado)}</strong>` : 'Sin certificado activo'}${semestre ? ` • Semestre Vigente: <strong>${semestre}°</strong>` : ''}</p> |
| 133 | </div> |
| 134 | <span class="badge badge-materia">Plan MAPS Oficial</span> |
| 135 | </div> |
| 136 | ${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>` : ''} |
| 137 | </section> |
| 138 | `; |
| 139 | } |
| 140 | |
| 141 | function renderQuickQuestion(materias) { |
| 142 | const options = materias.map(materia => `<option value="${escapeHtml(materia.id ?? '')}">${escapeHtml(materia.nombre || materia)}</option>`).join(''); |
| 143 | return ` |
| 144 | <form class="card create-post" id="quick-question-form"> |
| 145 | <label class="visually-hidden" for="quick-question">Nueva duda académica</label> |
| 146 | <input id="quick-question" name="question" type="text" maxlength="180" placeholder="¿Tienes una duda académica? Pregunta a tu comunidad..." required> |
| 147 | <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> |
| 148 | </form> |
| 149 | `; |
| 150 | } |
| 151 | |
| 152 | function renderPost(post) { |
| 153 | const author = (post.autor && typeof post.autor === 'object') ? post.autor : {}; |
| 154 | const esDocente = author.tipo === 'profesor' || author.tipo === 'docente' || Boolean(post.verificadoDocente); |
| 155 | const esMaterial = post.tipoContenido === 'material'; |
| 156 | const nombre = author.nombre || 'Usuario'; |
| 157 | const meta = esMaterial |
| 158 | ? ['Docente', post.materia ? `de ${post.materia}` : ''].filter(Boolean).join(' ') |
| 159 | : (author.semestre ? `${author.semestre}° Semestre` : 'Estudiante'); |
| 160 | const tiempo = post.fechaRelativa ? ` • ${post.fechaRelativa}` : ''; |
| 161 | const meId = Auth.getUser() && Auth.getUser().id; |
| 162 | const clickeable = author.id && author.id !== meId; |
| 163 | const hrefAutor = `usuario.html?id=${encodeURIComponent(author.id)}`; |
| 164 | const avatarWrap = clickeable |
| 165 | ? `<a class="user-link" href="${hrefAutor}" aria-label="Ver perfil de ${escapeHtml(nombre)}">${Utils.avatarHtml(nombre, author.foto, `avatar ${esDocente ? 'prof' : ''}`, nombre)}</a>` |
| 166 | : Utils.avatarHtml(nombre, author.foto, `avatar ${esDocente ? 'prof' : ''}`, nombre); |
| 167 | const nombreWrap = clickeable |
| 168 | ? `<a class="user-link" href="${hrefAutor}"><strong>${escapeHtml(nombre)}</strong></a>` |
| 169 | : `<strong>${escapeHtml(nombre)}</strong>`; |
| 170 | const seguirBoton = (author.id && author.id !== meId) |
| 171 | ? `<button class="follow-btn${author.seguido ? ' siguiendo' : ''}" type="button" data-id="${author.id}" data-siguiendo="${author.seguido ? 'true' : 'false'}">${author.seguido ? 'Siguiendo' : 'Seguir'}</button>` |
| 172 | : ''; |
| 173 | |
| 174 | const tags = [ |
| 175 | post.materia && `<span class="badge badge-materia">${escapeHtml(post.materia)}</span>`, |
| 176 | post.participado && '<span class="badge badge-interacted">Has participado</span>', |
| 177 | post.solucionAceptada && '<span class="badge badge-solved">Solución Aceptada</span>', |
| 178 | esDocente && '<span class="badge badge-verified">Aviso Docente</span>', |
| 179 | ].filter(Boolean).join(''); |
| 180 | |
| 181 | const embed = esMaterial && post.archivo |
| 182 | ? `<div class="embed-box prof-box"><strong style="color:#0284c7;">Archivo disponible:</strong><p>${escapeHtml(post.archivo.nombre)} (${escapeHtml(post.archivo.peso)}) • ${escapeHtml(post.archivo.descargas)} descargas de tu carrera</p></div>` |
| 183 | : (post.respuestaUsuario ? `<div class="embed-box"><strong>Tu respuesta:</strong><p>${escapeHtml(post.respuestaUsuario)}</p></div>` : ''); |
| 184 | |
| 185 | const votos = Number.isFinite(Number(post.votos)) ? Number(post.votos) : 0; |
| 186 | const botonVotos = post.participado |
| 187 | ? `<button class="btn-action voted" type="button">Votado (${votos})</button>` |
| 188 | : `<button class="btn-action" type="button">${votos} Votos</button>`; |
| 189 | const tercerBoton = esMaterial |
| 190 | ? '<button class="btn-action" type="button">Descargar</button>' |
| 191 | : '<button class="btn-action" type="button">Guardar</button>'; |
| 192 | const status = esMaterial ? 'Material Oficial' : (post.solucionAceptada ? 'Resuelto' : ''); |
| 193 | |
| 194 | return ` |
| 195 | <article class="card post-card"> |
| 196 | <div class="post-header"><div class="user-block">${avatarWrap}<div class="user-data"><div class="user-data-line">${nombreWrap}${seguirBoton}</div><span>${escapeHtml(meta + tiempo)}</span></div></div><div class="badges-wrap">${tags}</div></div> |
| 197 | <h3 class="post-title">${escapeHtml(post.titulo || '')}</h3> |
| 198 | ${post.contenido ? `<p class="post-body">${escapeHtml(post.contenido)}</p>` : ''} |
| 199 | ${embed} |
| 200 | <div class="post-footer"><div class="action-links">${botonVotos}<button class="btn-action" type="button">${Number(post.respuestas) || 0} ${esMaterial ? 'Comentarios' : 'Respuestas'}</button>${tercerBoton}</div>${status ? `<span class="post-status">${escapeHtml(status)}</span>` : ''}</div> |
| 201 | </article> |
| 202 | `; |
| 203 | } |
| 204 | |
| 205 | function renderAccessCards(data) { |
| 206 | const cards = []; |
| 207 | const circulo = data.circuloActual; |
| 208 | const asesoria = data.asesoria; |
| 209 | const empresa = data.empresaDestacada; |
| 210 | |
| 211 | if (circulo) cards.push(`<section class="card side-card"><span class="badge badge-materia">${escapeHtml(circulo.fecha || 'Próximamente')}</span><h4>${escapeHtml(circulo.nombre || 'Círculo de estudio')}</h4>${circulo.descripcion ? `<p>${escapeHtml(circulo.descripcion)}</p>` : ''}<a class="btn-solid" style="width:100%;" href="circulos.html">${circulo.plataforma ? `Entrar a Sala (${escapeHtml(circulo.plataforma)})` : 'Ver círculo'}</a></section>`); |
| 212 | if (asesoria) cards.push(`<section class="card side-card prof-card"><span class="badge badge-verified">Asesoría Disponible</span><h4>${escapeHtml(asesoria.docente || 'Docente')}</h4>${asesoria.horario ? `<p>Horario de atención para dudas del certificado: <strong>${escapeHtml(asesoria.horario)}</strong></p>` : ''}<a class="btn-clean" href="mensajes.html">Enviar Mensaje Privado</a></section>`); |
| 213 | if (empresa) cards.push(`<section class="card side-card"><h4>Radar Semestre Empresarial</h4><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>`); |
| 214 | |
| 215 | return cards.join(''); |
| 216 | } |
| 217 | |
| 218 | function renderEmptyFeed() { |
| 219 | 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="comunidad.html#foro-dudas">Ir al foro →</a></section>'; |
| 220 | } |
| 221 | |
| 222 | function escapeHtml(value) { |
| 223 | return String(value ?? '').replace(/[&<>'"]/g, character => ({ '&': '&', '<': '<', '>': '>', "'": ''', '"': '"' })[character]); |
| 224 | } |