main
js 247 lines 10.4 KB
Raw
1 /**
2 * MAPS Connect - Perfil público de otro usuario (alumno o docente).
3 * Contrato esperado de GET /usuarios/{id}:
4 * { id, nombre, foto, rol, esYo, reputacion, seguidores, siguiendo, loSigo }
5 * ESTUDIANTE: { carrera, semestre, proposito, certificados[{titulo,descripcion}],
6 * dudas, respuestas, aportes[{titulo,detalle,tipo,meta}] }
7 * PROFESOR: { especialidad, biografia, horarioAsesorias, disponibleChat,
8 * materias[], respuestas, aportes[{titulo,detalle,tipo,meta}] }
9 */
10
11 const UsuarioPub = {
12 data: null,
13
14 async init() {
15 Layout.init('usuario.html');
16 Layout.setPageTitle('Perfil');
17
18 const root = document.getElementById('usuario-root');
19 if (!root) return;
20
21 const id = new URLSearchParams(window.location.search).get('id');
22 if (!id) {
23 this.renderError(root, 'No se indicó un usuario.');
24 return;
25 }
26
27 try {
28 const data = await API.request(`/usuarios/${encodeURIComponent(id)}`);
29 if (!data) {
30 this.renderError(root, 'No se pudo cargar el perfil.');
31 return;
32 }
33 this.data = data;
34 this.render();
35 } catch (e) {
36 this.renderError(root, (e && e.message) || 'No se pudo cargar el perfil.');
37 }
38 },
39
40 num(v) {
41 const n = Number(v);
42 return Number.isFinite(n) ? n : null;
43 },
44
45 /* ---------- Render principal ---------- */
46
47 render() {
48 const d = this.data;
49 const esDocente = d.rol === 'PROFESOR';
50
51 document.getElementById('usuario-nombre').textContent = d.nombre || 'Usuario';
52 const trigger = document.getElementById('avatarTrigger');
53 trigger.innerHTML = Utils.avatarHtml(d.nombre, d.foto, 'profile-avatar trigger', d.nombre);
54
55 const sub = document.getElementById('usuario-sub');
56 const sub2 = document.getElementById('usuario-sub2');
57 if (esDocente) {
58 sub.innerHTML = d.especialidad
59 ? `Docente &bull; <strong>${Utils.esc(d.especialidad)}</strong>`
60 : 'Docente';
61 sub2.innerHTML = `Reputaci&oacute;n: <strong>${this.neto(d.reputacion)}</strong>`;
62 } else {
63 sub.innerHTML = d.carrera
64 ? `Estudiante &bull; <strong>${Utils.esc(d.carrera)}</strong>`
65 : 'Estudiante';
66 sub2.innerHTML = (d.semestre
67 ? `Semestre Vigente: <strong>${this.neto(d.semestre)}&deg; Semestre</strong> &bull; `
68 : '')
69 + `Reputaci&oacute;n: <strong>${this.neto(d.reputacion)}</strong>`;
70 }
71
72 this.renderActions(d);
73 this.renderIzquierda(d);
74 this.renderDerecha(d);
75 },
76
77 neto(v) {
78 const n = this.num(v);
79 return n == null ? '' : n;
80 },
81
82 renderActions(d) {
83 const actions = document.getElementById('usuario-actions');
84 if (!actions) return;
85
86 if (d.esYo) {
87 actions.innerHTML = '<a class="btn-clean" href="perfil.html">Este es tu perfil</a>';
88 return;
89 }
90
91 const loSigo = d.loSigo === true || d.loSigo === 'true';
92 actions.innerHTML = `
93 <button class="follow-btn${loSigo ? ' siguiendo' : ''}" type="button" id="usuario-follow" data-siguiendo="${loSigo}">${loSigo ? 'Siguiendo' : 'Seguir'}</button>
94 <button class="btn-clean" type="button" id="usuario-invitar">Invitar a C&iacute;rculo</button>
95 <button class="btn-solid" type="button" id="usuario-mensaje">Enviar Mensaje</button>
96 `;
97
98 document.getElementById('usuario-follow')?.addEventListener('click', async e => {
99 const siguiente = await Utils.alternarSeguir(e.target, d.id);
100 const box = document.getElementById('stat-seg');
101 if (box) {
102 const base = this.num(box.dataset.base);
103 if (base != null) box.textContent = String(base + (siguiente ? 1 : -1));
104 }
105 });
106
107 document.getElementById('usuario-invitar')?.addEventListener('click', () => {
108 Utils.toast('La invitación a círculos de estudio se habilita próximamente.', 'info');
109 });
110
111 document.getElementById('usuario-mensaje')?.addEventListener('click', async () => {
112 try {
113 await API.request(`/mensajes/nuevo/${encodeURIComponent(d.id)}`, { method: 'POST' });
114 } catch (e) {
115 // La conversación suele existir; se navega igual.
116 }
117 window.location.href = 'mensajes.html';
118 });
119 },
120
121 renderIzquierda(d) {
122 const host = document.getElementById('col-izquierda');
123 if (!host) return;
124
125 if (d.rol === 'ESTUDIANTE') {
126 const certs = Array.isArray(d.certificados) ? d.certificados : [];
127 const proposito = d.proposito
128 ? `<div class="purpose-box"><strong>Prop&oacute;sito Declarado</strong><p>${Utils.esc(d.proposito)}</p></div>`
129 : '';
130 const listaCerts = certs.length ? certs.map((c, i) => `
131 <div class="certificate-item${i === 0 ? ' active' : ''}">
132 <div class="cert-header">
133 <span class="cert-title">${Utils.esc(c.titulo || 'Certificado MAPS')}</span>
134 <span class="badge badge-green">En Curso</span>
135 </div>
136 <p class="cert-desc">${Utils.esc(c.descripcion)}</p>
137 </div>`).join('')
138 : '<p class="empty-note">Aún no ha seleccionado certificados MAPS.</p>';
139
140 host.innerHTML = `
141 <h3 class="section-heading">Ruta Acad&eacute;mica MAPS</h3>
142 ${proposito}
143 <h4 class="subsection-heading">Certificados en Curso y Obtenidos</h4>
144 ${listaCerts}`;
145 return;
146 }
147
148 if (d.rol === 'PROFESOR') {
149 const bio = d.biografia
150 ? `<div class="purpose-box"><strong>Sobre ${Utils.esc(d.nombre)}</strong><p>${Utils.esc(d.biografia)}</p></div>`
151 : '';
152 const materias = (Array.isArray(d.materias) && d.materias.length)
153 ? `<li><strong>Materias asignadas:</strong><span style="display:block;margin-top:4px;">${d.materias.map(m => `<span class="materia-tag">${Utils.esc(m)}</span>`).join('')}</span></li>`
154 : '';
155
156 host.innerHTML = `
157 <h3 class="section-heading">Perfil Docente</h3>
158 ${bio}
159 <ul class="prof-detail-list">
160 ${d.especialidad ? `<li><strong>Especialidad:</strong> ${Utils.esc(d.especialidad)}</li>` : ''}
161 ${materias}
162 ${d.horarioAsesorias ? `<li><strong>Horario de asesor&iacute;as:</strong> ${Utils.esc(d.horarioAsesorias)}</li>` : ''}
163 <li><strong>Disponible en chat:</strong> <span class="${d.disponibleChat ? 'ok' : 'off'}">${d.disponibleChat ? 'S&iacute;' : 'No'}</span></li>
164 </ul>`;
165 return;
166 }
167
168 host.innerHTML = '<p class="empty-note">Perfil sin información académica adicional.</p>';
169 },
170
171 renderDerecha(d) {
172 const esDocente = d.rol === 'PROFESOR';
173 const stats = esDocente ? [
174 { key: 'seg', label: 'Seguidores', val: d.seguidores },
175 { key: 'sig', label: 'Siguiendo', val: d.siguiendo },
176 { key: 'res', label: 'Respuestas Aportadas', val: d.respuestas },
177 { key: 'mat', label: 'Materias', val: Array.isArray(d.materias) ? d.materias.length : 0 }
178 ] : [
179 { key: 'seg', label: 'Seguidores', val: d.seguidores },
180 { key: 'sig', label: 'Siguiendo', val: d.siguiendo },
181 { key: 'dud', label: 'Dudas en el Foro', val: d.dudas },
182 { key: 'res', label: 'Respuestas Aportadas', val: d.respuestas }
183 ];
184
185 const statsHost = document.getElementById('usuario-stats');
186 if (statsHost) {
187 statsHost.innerHTML = stats.map(s => `
188 <div class="stat-box">
189 <span class="number" id="stat-${s.key}"${s.key === 'seg' ? ` data-base="${this.neto(s.val)}"` : ''}>${this.neto(s.val)}</span>
190 <span class="label">${Utils.esc(s.label)}</span>
191 </div>`).join('');
192 }
193
194 const aportes = Array.isArray(d.aportes) ? d.aportes : [];
195 const aportesHost = document.getElementById('usuario-aportes');
196 if (aportesHost) {
197 aportesHost.innerHTML = aportes.length ? aportes.map(a => `
198 <div class="public-item">
199 <strong>${Utils.esc(a.titulo || 'Aporte')}</strong>
200 <p>${Utils.esc(a.detalle)}</p>
201 <div class="item-meta">
202 <span>${a.tipo === 'respuesta' ? 'Respuesta en foro' : 'Duda publicada'}</span>
203 <span>${Utils.esc(a.meta)}</span>
204 </div>
205 </div>`).join('')
206 : '<p class="empty-note">Aún no hay aportaciones públicas.</p>';
207 }
208 },
209
210 /* ---------- Modal para ampliar avatar ---------- */
211
212 renderError(root, message) {
213 root.innerHTML = `<div class="card empty-state"><h2>Perfil no disponible</h2><p>${Utils.esc(message)}</p><a class="side-link" href="inicio.html">Volver al inicio →</a></div>`;
214 }
215 };
216
217 document.addEventListener('DOMContentLoaded', () => {
218
219 const backdrop = document.getElementById('avatarModal');
220 const trigger = document.getElementById('avatarTrigger');
221 const closeBtn = document.getElementById('closeModalBtn');
222 const expanded = document.getElementById('avatarExpanded');
223
224 function abrirModal() {
225 const d = UsuarioPub.data;
226 if (!d || !backdrop || !expanded) return;
227 if (d.foto) {
228 expanded.innerHTML = `<img class="avatar-expanded-box" src="${Utils.esc(d.foto)}" alt="Foto de ${Utils.esc(d.nombre)}">`;
229 } else {
230 expanded.innerHTML = `<span class="avatar-expanded-box">${Utils.esc(Utils.iniciales(d.nombre))}</span>`;
231 }
232 backdrop.classList.add('show');
233 }
234
235 function cerrarModal() {
236 if (backdrop) backdrop.classList.remove('show');
237 }
238
239 trigger?.addEventListener('click', abrirModal);
240 closeBtn?.addEventListener('click', cerrarModal);
241 backdrop?.addEventListener('click', e => { if (e.target === backdrop) cerrarModal(); });
242 document.addEventListener('keydown', e => {
243 if (e.key === 'Escape' && backdrop.classList.contains('show')) cerrarModal();
244 });
245
246 UsuarioPub.init();
247 });