desvergue-de-alexandro
js 99 lines 6.1 KB
Raw
1 /**
2 * Directorio de Semestre Empresarial.
3 * Contrato esperado de GET /empresarial:
4 * { empresas: [{ id, nombre, ciudad, estado, modalidad, calificacion,
5 * totalResenas, descripcion, convenioActivo, tecnologias, experienciaDestacada }] }
6 */
7 document.addEventListener('DOMContentLoaded', async () => {
8 Layout.init('empresarial.html');
9 Layout.setPageTitle('Semestre Empresarial');
10
11 const content = document.getElementById('main-content');
12 if (!content) return;
13
14 content.innerHTML = '<div class="empresarial-container"><p class="loading-message">Cargando empresas vinculadas…</p></div>';
15 await loadCompanies(content);
16 });
17
18 async function loadCompanies(content, filters = {}) {
19 const params = new URLSearchParams();
20 if (filters.busqueda) params.set('busqueda', filters.busqueda);
21 if (filters.modalidad) params.set('modalidad', filters.modalidad);
22 if (filters.calificacionMinima) params.set('calificacionMinima', filters.calificacionMinima);
23
24 let response = {};
25 try {
26 response = await API.request(`/empresarial${params.size ? `?${params}` : ''}`);
27 } catch {
28 // El controlador todavía no expone datos; se conserva el estado vacío.
29 }
30
31 const companies = Array.isArray(response) ? response : (Array.isArray(response.empresas) ? response.empresas : []);
32 renderCompanies(content, companies);
33 }
34
35 function renderCompanies(content, companies) {
36 content.innerHTML = `
37 <div class="empresarial-container">
38 <header class="module-heading"><h1>Semestre Empresarial</h1><p>Explora empresas vinculadas y experiencias compartidas por la comunidad.</p></header>
39 <form class="filter-bar" id="company-filter-form">
40 <label class="visually-hidden" for="company-search">Buscar empresa</label>
41 <input class="input-search" id="company-search" name="busqueda" type="search" placeholder="Buscar por empresa, tecnología o proyecto...">
42 <select class="select-filter" name="modalidad" aria-label="Filtrar por modalidad"><option value="">Todas las modalidades</option><option value="HIBRIDO">Híbrido</option><option value="REMOTO">Remoto</option><option value="PRESENCIAL">Presencial</option></select>
43 <select class="select-filter" name="calificacionMinima" aria-label="Filtrar por calificación"><option value="">Calificación: todas</option><option value="4.5">4.5 estrellas o más</option><option value="4.0">4.0 estrellas o más</option></select>
44 <button class="btn-solid" type="submit">Buscar</button>
45 </form>
46 <section id="company-results" aria-live="polite">${companies.length ? companies.map(renderCompany).join('') : renderEmptyCompanies()}</section>
47 </div>
48 `;
49
50 document.getElementById('company-filter-form')?.addEventListener('submit', event => {
51 event.preventDefault();
52 const formData = new FormData(event.currentTarget);
53 loadCompanies(content, Object.fromEntries(formData.entries()));
54 });
55 }
56
57 function renderCompany(company) {
58 const rating = Number(company.calificacion);
59 const hasRating = Number.isFinite(rating) && rating > 0;
60 const location = [company.ciudad, company.estado].filter(Boolean).join(', ');
61 const locationAndMode = [location, company.modalidad && `Modalidad: ${formatMode(company.modalidad)}`].filter(Boolean).join(' · ');
62 const technologies = Array.isArray(company.tecnologias) ? company.tecnologias : [];
63 const experience = company.experienciaDestacada;
64
65 return `
66 <article class="card company-card">
67 <header class="company-header">
68 <div><h2 class="company-name">${escapeHtml(company.nombre || 'Empresa')}</h2>${locationAndMode ? `<span class="company-location">${escapeHtml(locationAndMode)}</span>` : ''}</div>
69 ${hasRating ? `<div class="rating-box"><span class="stars" aria-label="${rating} de 5 estrellas">${renderStars(rating)}</span><strong class="rating-number">${rating.toFixed(1)}</strong>${Number.isFinite(Number(company.totalResenas)) ? `<span class="rating-count">(${Number(company.totalResenas)} reseñas)</span>` : ''}</div>` : ''}
70 </header>
71 ${company.descripcion ? `<p class="company-desc"><strong>¿Qué hacen?</strong> ${escapeHtml(company.descripcion)}</p>` : ''}
72 ${(company.convenioActivo || technologies.length) ? `<div class="tags-group">${company.convenioActivo ? '<span class="badge badge-green">Convenio oficial activo</span>' : ''}${technologies.map(item => `<span class="badge badge-blue">${escapeHtml(item)}</span>`).join('')}</div>` : ''}
73 ${experience ? renderExperience(experience) : ''}
74 <footer class="company-footer"><button class="btn-clean" type="button" data-company-id="${escapeHtml(company.id ?? '')}">Ver experiencias${Number.isFinite(Number(company.totalResenas)) ? ` (${Number(company.totalResenas)})` : ''}</button><button class="btn-solid" type="button" data-company-id="${escapeHtml(company.id ?? '')}">Ver requisitos de vinculación</button></footer>
75 </article>
76 `;
77 }
78
79 function renderExperience(experience) {
80 const author = [experience.autor, experience.semestre && `${experience.semestre}.º semestre`].filter(Boolean).join(' · ');
81 return `<div class="experience-box">${author ? `<strong class="experience-author">${escapeHtml(author)}</strong>` : ''}${experience.texto ? `<p class="experience-text"><em>“${escapeHtml(experience.texto)}”</em></p>` : ''}</div>`;
82 }
83
84 function renderEmptyCompanies() {
85 return '<section class="card empty-companies"><h2>Aún no hay empresas disponibles</h2><p>Las empresas vinculadas y las experiencias de estudiantes aparecerán aquí cuando se registren.</p></section>';
86 }
87
88 function renderStars(rating) {
89 const filled = Math.round(rating);
90 return ''.repeat(Math.min(5, filled)) + ''.repeat(Math.max(0, 5 - filled));
91 }
92
93 function formatMode(mode) {
94 return String(mode).toLowerCase().replace(/^./, character => character.toUpperCase());
95 }
96
97 function escapeHtml(value) {
98 return String(value ?? '').replace(/[&<>'"]/g, character => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', "'": '&#39;', '"': '&quot;' })[character]);
99 }