feat: Add frontend structure with HTML, CSS, JavaScript

Frontend Setup: - Create monorepo structure with separate frontend/ folder - Add minimalista HTML5 interface with sidebar navigation - Implement responsive CSS (desktop, tablet, mobile) - Add JavaScript utilities and API integration - Implement login/register with JWT authentication - Add form validation and error handling - Create dashboard with welcome section and stats - Add global configuration for API endpoints - Include comprehensive frontend README Files Added: - frontend/index.html - Main page with auth/dashboard - frontend/css/styles.css - Main stylesheet (minimalista) - frontend/css/responsive.css - Responsive design - frontend/js/config.js - API configuration and utilities - frontend/js/auth.js - Authentication logic - frontend/js/main.js - App initialization and control - frontend/README.md - Frontend documentation Updated: - .gitignore - Add frontend node_modules and build files Features: ✅ JWT token management in localStorage ✅ Email validation (@tecmilenio.mx domain) ✅ Password validation (8+ chars, uppercase, number) ✅ Responsive sidebar (collapsible on mobile) ✅ Form error handling ✅ API error management ✅ Auto-login after registration ✅ Session persistence ✅ Global error display Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Daniel Silva committed Aug 17, 2026 at 18:20 UTC 715d8491b70febdb1c2a8ae6a104fb429956831b
8 files changed +1884
.gitignore
+18
@@ -40,3 +40,21 @@ logs/
40 # Temp files
41 *.tmp
42 temp/
43 +
44 +# Frontend - Node (if we add npm later)
45 +frontend/node_modules/
46 +frontend/.npmrc
47 +frontend/package-lock.json
48 +frontend/yarn.lock
49 +
50 +# Frontend - IDE settings
51 +frontend/.idea/
52 +frontend/.vscode/
53 +
54 +# Frontend - Build/Dist (if we add build process)
55 +frontend/dist/
56 +frontend/build/
57 +
58 +# Frontend - Environment
59 +frontend/.env
60 +frontend/.env.local
frontend/README.md new
+294
@@ -0,0 +1,294 @@
1 +# MAPS Connect - Frontend
2 +
3 +Frontend minimalista con **HTML5**, **CSS3** y **JavaScript Vanilla** para la plataforma MAPS Connect.
4 +
5 +## 📁 Estructura del Proyecto
6 +
7 +```
8 +frontend/
9 +├── index.html # Página principal (login/dashboard)
10 +├── css/
11 +│ ├── styles.css # Estilos principales
12 +│ └── responsive.css # Estilos responsivos
13 +├── js/
14 +│ ├── config.js # Configuración y utilidades
15 +│ ├── auth.js # Lógica de autenticación
16 +│ └── main.js # Lógica principal de la app
17 +├── pages/
18 +│ ├── foro.html # Módulo de foro
19 +│ ├── tips.html # Módulo de tips
20 +│ ├── recursos.html # Módulo de recursos
21 +│ ├── circulos.html # Módulo de círculos
22 +│ ├── mensajes.html # Módulo de mensajería
23 +│ ├── empresarial.html # Módulo empresarial
24 +│ └── perfil.html # Página de perfil
25 +├── assets/ # Imágenes y recursos estáticos
26 +└── README.md # Este archivo
27 +```
28 +
29 +## 🎨 Diseño y Características
30 +
31 +### Minimalista
32 +- ✅ Contornos rectos (sin bordes redondeados)
33 +- ✅ Paleta de colores profesional
34 +- ✅ Interfaz limpia y enfocada
35 +
36 +### Responsive
37 +- ✅ Tablets (768px y menores)
38 +- ✅ Mobile (480px y menores)
39 +- ✅ Sidebar colapsable en dispositivos pequeños
40 +
41 +### Accesibilidad
42 +- ✅ Validaciones de formularios
43 +- ✅ Mensajes de error claros
44 +- ✅ Navegación intuitiva
45 +
46 +## 🚀 Cómo Usar
47 +
48 +### 1. Abrir en Visual Studio Code
49 +
50 +```bash
51 +# Desde el directorio raíz del proyecto
52 +code frontend/
53 +```
54 +
55 +### 2. Iniciar Servidor Local
56 +
57 +**Opción 1: Usar Live Server (Extensión VS Code)**
58 +- Instalar extensión "Live Server"
59 +- Hacer clic derecho en `index.html`
60 +- Seleccionar "Open with Live Server"
61 +- Se abrirá automáticamente en `http://localhost:5500`
62 +
63 +**Opción 2: Python**
64 +```bash
65 +# Python 3
66 +python -m http.server 8000
67 +
68 +# Luego acceder a http://localhost:8000
69 +```
70 +
71 +**Opción 3: Node.js**
72 +```bash
73 +# Instalar http-server
74 +npm install -g http-server
75 +
76 +# Iniciar servidor
77 +http-server
78 +```
79 +
80 +### 3. Verificar que el Backend está ejecutándose
81 +
82 +```bash
83 +curl http://localhost:8080/api/health
84 +```
85 +
86 +## 📝 Archivos Principales
87 +
88 +### index.html
89 +Página principal con:
90 +- Formulario de login y registro
91 +- Dashboard con estadísticas
92 +- Navegación principal
93 +- Manejo de autenticación
94 +
95 +### css/styles.css
96 +Estilos base:
97 +- Variables CSS (colores, espaciados)
98 +- Componentes reutilizables
99 +- Temas de formularios, botones, tarjetas
100 +- Animaciones
101 +
102 +### css/responsive.css
103 +Estilos responsivos:
104 +- Media queries para tablets
105 +- Media queries para mobile
106 +- Sidebar colapsable
107 +- Grid adaptable
108 +
109 +### js/config.js
110 +Configuración y utilidades:
111 +- Endpoints de API
112 +- Función `fetchAPI()` con manejo de errores
113 +- Manejo de JWT tokens
114 +- Validaciones (email, contraseña, nombre)
115 +- Funciones de utilidad
116 +
117 +### js/auth.js
118 +Autenticación:
119 +- Lógica de login
120 +- Lógica de registro
121 +- Toggle entre modos
122 +- Validaciones de formulario
123 +
124 +### js/main.js
125 +Lógica principal:
126 +- Inicialización de la app
127 +- Control de vistas (auth vs dashboard)
128 +- Logout
129 +- Manejo global de errores
130 +
131 +## 🔐 Autenticación
132 +
133 +### Flujo de Login
134 +
135 +1. Usuario ingresa email y contraseña
136 +2. Frontend valida formato (@tecmilenio.mx)
137 +3. Se envía POST a `/auth/login`
138 +4. Backend retorna JWT token
139 +5. Token se almacena en `localStorage`
140 +6. Se carga el dashboard
141 +
142 +### Flujo de Registro
143 +
144 +1. Usuario completa formulario de registro
145 +2. Validaciones en frontend (email, contraseña, nombre)
146 +3. Se envía POST a `/auth/register`
147 +4. Backend crea usuario y retorna token
148 +5. Auto-login posterior
149 +
150 +### Tokens JWT
151 +
152 +El token se envía en todas las requests:
153 +```javascript
154 +Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
155 +```
156 +
157 +Se valida automáticamente en cada petición y se renueva si es necesario.
158 +
159 +## 🎨 Colores y Paleta
160 +
161 +```css
162 +--primary-color: #1e40af; /* Azul profesional */
163 +--secondary-color: #7c3aed; /* Púrpura */
164 +--success-color: #10b981; /* Verde */
165 +--warning-color: #f59e0b; /* Ámbar */
166 +--danger-color: #ef4444; /* Rojo */
167 +--gray-100 a gray-900: /* Escala de grises */
168 +```
169 +
170 +## 📱 Breakpoints Responsivos
171 +
172 +- **Desktop**: > 768px (Sidebar 250px)
173 +- **Tablet**: ≤ 768px (Sidebar 70px)
174 +- **Mobile**: ≤ 480px (Sidebar 60px)
175 +
176 +## 🔧 Desarrollo
177 +
178 +### Agregar Nuevas Páginas
179 +
180 +1. Crear archivo HTML en `pages/`
181 +```html
182 +<!DOCTYPE html>
183 +<html lang="es">
184 +<head>
185 + <meta charset="UTF-8">
186 + <meta name="viewport" content="width=device-width, initial-scale=1.0">
187 + <title>Página Nueva - MAPS Connect</title>
188 + <link rel="stylesheet" href="../css/styles.css">
189 + <link rel="stylesheet" href="../css/responsive.css">
190 +</head>
191 +<body>
192 + <!-- Incluir sidebar -->
193 + <!-- Contenido -->
194 + <script src="../js/config.js"></script>
195 + <script src="../js/main.js"></script>
196 +</body>
197 +</html>
198 +```
199 +
200 +2. Actualizar link en `index.html`:
201 +```html
202 +<li><a href="pages/nueva-pagina.html" class="nav-link">Nueva Página</a></li>
203 +```
204 +
205 +### Llamar Endpoints de API
206 +
207 +```javascript
208 +// GET
209 +const data = await fetchAPI('/endpoint', 'GET');
210 +
211 +// POST
212 +const response = await fetchAPI('/endpoint', 'POST', {
213 + campo1: 'valor1',
214 + campo2: 'valor2'
215 +});
216 +
217 +// PUT
218 +const updated = await fetchAPI('/endpoint/1', 'PUT', {
219 + nombre: 'nuevo nombre'
220 +});
221 +
222 +// DELETE
223 +await fetchAPI('/endpoint/1', 'DELETE');
224 +```
225 +
226 +### Validaciones Disponibles
227 +
228 +```javascript
229 +// Email institucional
230 +isValidInstitutionalEmail('usuario@tecmilenio.mx'); // true
231 +
232 +// Contraseña (8+ chars, mayúscula, número)
233 +isValidPassword('Password123'); // true
234 +
235 +// Nombre completo
236 +isValidName('Juan Pérez'); // true
237 +```
238 +
239 +## 🐛 Debugging
240 +
241 +### Verificar Conexión con Backend
242 +
243 +```javascript
244 +// En la consola del navegador
245 +fetch('http://localhost:8080/api/health')
246 + .then(r => r.json())
247 + .then(d => console.log(d))
248 + .catch(e => console.error(e));
249 +```
250 +
251 +### Ver Token JWT
252 +
253 +```javascript
254 +// En la consola
255 +localStorage.getItem('authToken');
256 +```
257 +
258 +### Ver Datos de Usuario
259 +
260 +```javascript
261 +// En la consola
262 +localStorage.getItem('currentUser');
263 +```
264 +
265 +## 📋 Próximas Implementaciones
266 +
267 +- [ ] Módulo de Foro (dudas y respuestas)
268 +- [ ] Módulo de Tips (consejos académicos)
269 +- [ ] Repositorio de Recursos (apuntes)
270 +- [ ] Círculos de Estudio
271 +- [ ] Mensajería privada
272 +- [ ] Semestre Empresarial
273 +- [ ] Gestión de Perfil
274 +- [ ] Búsqueda y filtros
275 +- [ ] Notificaciones
276 +- [ ] Paginación
277 +
278 +## 🚦 Estado de Desarrollo
279 +
280 +**Actual**: Autenticación básica (Login/Registro)
281 +**Próximo**: Módulos principales
282 +
283 +## 📞 Soporte
284 +
285 +Para problemas o preguntas, consulta:
286 +- README.md del proyecto (raíz)
287 +- Documentación de API Backend
288 +- Especificación del Proyecto
289 +
290 +---
291 +
292 +**Última actualización**: Agosto 2024
293 +**Versión**: 1.0.0
294 +**Tecnología**: HTML5 | CSS3 | JavaScript Vanilla
frontend/css/responsive.css new
+157
@@ -0,0 +1,157 @@
1 +/* ========================
2 + RESPONSIVE DESIGN
3 + ======================== */
4 +
5 +/* Tablets (768px and below) */
6 +@media (max-width: 768px) {
7 + .sidebar {
8 + width: 70px;
9 + padding: 20px 0;
10 + }
11 +
12 + .logo {
13 + font-size: 20px;
14 + }
15 +
16 + .nav-link {
17 + padding: 12px 8px;
18 + text-align: center;
19 + font-size: 12px;
20 + }
21 +
22 + .nav-link:hover,
23 + .nav-link.active {
24 + border-left: 3px solid var(--primary-color);
25 + padding-left: 5px;
26 + }
27 +
28 + .container {
29 + margin-left: 70px;
30 + }
31 +
32 + main {
33 + padding: 20px;
34 + }
35 +
36 + .header h1 {
37 + font-size: 24px;
38 + }
39 +
40 + .subtitle {
41 + font-size: 14px;
42 + }
43 +
44 + .stats-grid {
45 + grid-template-columns: repeat(2, 1fr);
46 + }
47 +
48 + .footer {
49 + margin-left: 70px;
50 + }
51 +
52 + .global-error {
53 + right: 10px;
54 + left: 10px;
55 + max-width: none;
56 + }
57 +}
58 +
59 +/* Mobile (480px and below) */
60 +@media (max-width: 480px) {
61 + .sidebar {
62 + width: 60px;
63 + }
64 +
65 + .sidebar-header {
66 + flex-direction: column;
67 + gap: 15px;
68 + }
69 +
70 + .logo {
71 + font-size: 18px;
72 + text-align: center;
73 + width: 100%;
74 + }
75 +
76 + .toggle-btn {
77 + display: block;
78 + }
79 +
80 + .nav-link {
81 + padding: 10px 6px;
82 + font-size: 11px;
83 + }
84 +
85 + .container {
86 + margin-left: 60px;
87 + }
88 +
89 + main {
90 + padding: 15px;
91 + }
92 +
93 + .header {
94 + margin-bottom: 20px;
95 + }
96 +
97 + .header h1 {
98 + font-size: 20px;
99 + }
100 +
101 + .subtitle {
102 + font-size: 12px;
103 + }
104 +
105 + .auth-box {
106 + padding: 20px 15px;
107 + }
108 +
109 + .stats-grid {
110 + grid-template-columns: 1fr;
111 + gap: 15px;
112 + }
113 +
114 + .stat-card {
115 + padding: 20px 15px;
116 + }
117 +
118 + .stat-card h3 {
119 + font-size: 24px;
120 + }
121 +
122 + .activity-section {
123 + padding: 20px 15px;
124 + }
125 +
126 + .activity-section h3 {
127 + font-size: 16px;
128 + }
129 +
130 + .footer {
131 + margin-left: 60px;
132 + font-size: 12px;
133 + }
134 +
135 + .global-error {
136 + top: 10px;
137 + right: 10px;
138 + left: 10px;
139 + max-width: none;
140 + font-size: 12px;
141 + }
142 +}
143 +
144 +/* Toggle Sidebar on Mobile */
145 +@media (max-width: 768px) {
146 + .sidebar.collapsed {
147 + width: 60px;
148 + }
149 +
150 + .sidebar.collapsed .nav-link {
151 + font-size: 0;
152 + }
153 +
154 + .sidebar.collapsed .logo {
155 + font-size: 16px;
156 + }
157 +}
frontend/css/styles.css new
+544
@@ -0,0 +1,544 @@
1 +/* ========================
2 + MAPS CONNECT - STYLES
3 + Minimalista con contornos rectos
4 + ======================== */
5 +
6 +:root {
7 + --primary-color: #1e40af; /* Azul profesional */
8 + --secondary-color: #7c3aed; /* Púrpura */
9 + --success-color: #10b981; /* Verde */
10 + --warning-color: #f59e0b; /* Ámbar */
11 + --danger-color: #ef4444; /* Rojo */
12 + --gray-100: #f3f4f6;
13 + --gray-200: #e5e7eb;
14 + --gray-300: #d1d5db;
15 + --gray-400: #9ca3af;
16 + --gray-500: #6b7280;
17 + --gray-600: #4b5563;
18 + --gray-700: #374151;
19 + --gray-800: #1f2937;
20 + --gray-900: #111827;
21 +
22 + --border-radius: 0; /* Sin bordes redondeados */
23 + --box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
24 + --transition: all 0.3s ease;
25 +}
26 +
27 +* {
28 + margin: 0;
29 + padding: 0;
30 + box-sizing: border-box;
31 +}
32 +
33 +html {
34 + scroll-behavior: smooth;
35 +}
36 +
37 +body {
38 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
39 + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
40 + sans-serif;
41 + -webkit-font-smoothing: antialiased;
42 + -moz-osx-font-smoothing: grayscale;
43 + background-color: var(--gray-100);
44 + color: var(--gray-800);
45 + line-height: 1.6;
46 + display: flex;
47 +}
48 +
49 +/* ========================
50 + SIDEBAR NAVIGATION
51 + ======================== */
52 +
53 +.sidebar {
54 + width: 250px;
55 + background-color: var(--gray-900);
56 + color: white;
57 + position: fixed;
58 + left: 0;
59 + top: 0;
60 + height: 100vh;
61 + overflow-y: auto;
62 + padding: 20px 0;
63 + box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
64 + transition: var(--transition);
65 + z-index: 100;
66 +}
67 +
68 +.sidebar-header {
69 + display: flex;
70 + justify-content: space-between;
71 + align-items: center;
72 + padding: 0 20px 30px 20px;
73 + border-bottom: 1px solid var(--gray-700);
74 +}
75 +
76 +.logo {
77 + font-size: 28px;
78 + font-weight: 700;
79 + letter-spacing: 2px;
80 + color: var(--primary-color);
81 +}
82 +
83 +.toggle-btn {
84 + display: none;
85 + background: none;
86 + border: none;
87 + color: white;
88 + font-size: 20px;
89 + cursor: pointer;
90 +}
91 +
92 +.nav-menu {
93 + list-style: none;
94 + padding: 0 10px;
95 + margin-bottom: auto;
96 +}
97 +
98 +.nav-menu li {
99 + margin-bottom: 5px;
100 +}
101 +
102 +.nav-link {
103 + display: block;
104 + padding: 12px 15px;
105 + color: var(--gray-300);
106 + text-decoration: none;
107 + border: 1px solid transparent;
108 + transition: var(--transition);
109 +}
110 +
111 +.nav-link:hover {
112 + background-color: var(--gray-800);
113 + color: white;
114 + border-left: 3px solid var(--primary-color);
115 + padding-left: 12px;
116 +}
117 +
118 +.nav-link.active {
119 + background-color: var(--primary-color);
120 + color: white;
121 + border-left: 3px solid var(--secondary-color);
122 + padding-left: 12px;
123 +}
124 +
125 +.sidebar-footer {
126 + padding: 20px 10px;
127 + border-top: 1px solid var(--gray-700);
128 +}
129 +
130 +.logout-btn {
131 + width: 100%;
132 + padding: 10px 15px;
133 + background-color: var(--danger-color);
134 + color: white;
135 + border: none;
136 + cursor: pointer;
137 + font-size: 14px;
138 + font-weight: 600;
139 + transition: var(--transition);
140 +}
141 +
142 +.logout-btn:hover {
143 + background-color: #dc2626;
144 +}
145 +
146 +/* ========================
147 + MAIN CONTAINER
148 + ======================== */
149 +
150 +.container {
151 + flex: 1;
152 + margin-left: 250px;
153 + background-color: var(--gray-100);
154 + min-height: 100vh;
155 + display: flex;
156 + flex-direction: column;
157 +}
158 +
159 +main {
160 + flex: 1;
161 + padding: 40px;
162 + max-width: 1200px;
163 + margin: 0 auto;
164 + width: 100%;
165 +}
166 +
167 +/* ========================
168 + HEADER
169 + ======================== */
170 +
171 +.header {
172 + margin-bottom: 40px;
173 + text-align: center;
174 +}
175 +
176 +.header h1 {
177 + font-size: 32px;
178 + margin-bottom: 10px;
179 + color: var(--gray-900);
180 +}
181 +
182 +.subtitle {
183 + font-size: 16px;
184 + color: var(--gray-500);
185 +}
186 +
187 +/* ========================
188 + AUTHENTICATION SECTION
189 + ======================== */
190 +
191 +.auth-section {
192 + display: flex;
193 + align-items: center;
194 + justify-content: center;
195 + min-height: 80vh;
196 +}
197 +
198 +.auth-container {
199 + width: 100%;
200 + max-width: 400px;
201 +}
202 +
203 +.auth-box {
204 + background-color: white;
205 + padding: 40px 30px;
206 + box-shadow: var(--box-shadow);
207 + border: 1px solid var(--gray-200);
208 +}
209 +
210 +.auth-box h2 {
211 + font-size: 24px;
212 + margin-bottom: 30px;
213 + color: var(--gray-900);
214 + text-align: center;
215 +}
216 +
217 +/* ========================
218 + FORM STYLES
219 + ======================== */
220 +
221 +.form {
222 + width: 100%;
223 +}
224 +
225 +.form-group {
226 + margin-bottom: 20px;
227 + display: flex;
228 + flex-direction: column;
229 +}
230 +
231 +.form-group label {
232 + font-size: 14px;
233 + font-weight: 600;
234 + margin-bottom: 8px;
235 + color: var(--gray-700);
236 +}
237 +
238 +.form-group input,
239 +.form-group select,
240 +.form-group textarea {
241 + padding: 10px 12px;
242 + border: 1px solid var(--gray-300);
243 + background-color: white;
244 + font-size: 14px;
245 + font-family: inherit;
246 + transition: var(--transition);
247 +}
248 +
249 +.form-group input:focus,
250 +.form-group select:focus,
251 +.form-group textarea:focus {
252 + outline: none;
253 + border-color: var(--primary-color);
254 + box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
255 +}
256 +
257 +.error-message {
258 + font-size: 12px;
259 + color: var(--danger-color);
260 + margin-top: 4px;
261 + display: none;
262 +}
263 +
264 +.error-message.show {
265 + display: block;
266 +}
267 +
268 +.error-box {
269 + background-color: #fee2e2;
270 + border: 1px solid #fca5a5;
271 + color: var(--danger-color);
272 + padding: 12px;
273 + margin-bottom: 20px;
274 + font-size: 14px;
275 +}
276 +
277 +/* ========================
278 + BUTTONS
279 + ======================== */
280 +
281 +.btn {
282 + padding: 10px 16px;
283 + border: none;
284 + cursor: pointer;
285 + font-size: 14px;
286 + font-weight: 600;
287 + transition: var(--transition);
288 + border: 1px solid transparent;
289 +}
290 +
291 +.btn-primary {
292 + background-color: var(--primary-color);
293 + color: white;
294 + width: 100%;
295 + margin-top: 10px;
296 +}
297 +
298 +.btn-primary:hover {
299 + background-color: #1e3a8a;
300 +}
301 +
302 +.btn-primary:disabled {
303 + background-color: var(--gray-400);
304 + cursor: not-allowed;
305 +}
306 +
307 +.btn-secondary {
308 + background-color: var(--gray-200);
309 + color: var(--gray-800);
310 +}
311 +
312 +.btn-secondary:hover {
313 + background-color: var(--gray-300);
314 +}
315 +
316 +.btn-success {
317 + background-color: var(--success-color);
318 + color: white;
319 +}
320 +
321 +.btn-success:hover {
322 + background-color: #059669;
323 +}
324 +
325 +.link-btn {
326 + background: none;
327 + border: none;
328 + color: var(--primary-color);
329 + cursor: pointer;
330 + font-weight: 600;
331 + padding: 0;
332 + text-decoration: underline;
333 + font-size: 14px;
334 +}
335 +
336 +.link-btn:hover {
337 + color: #1e3a8a;
338 +}
339 +
340 +/* ========================
341 + AUTH TOGGLE
342 + ======================== */
343 +
344 +.auth-toggle {
345 + text-align: center;
346 + margin-top: 20px;
347 + padding-top: 20px;
348 + border-top: 1px solid var(--gray-200);
349 + font-size: 14px;
350 + color: var(--gray-600);
351 +}
352 +
353 +/* ========================
354 + LOADING SPINNER
355 + ======================== */
356 +
357 +.loading-spinner {
358 + display: flex;
359 + flex-direction: column;
360 + align-items: center;
361 + gap: 15px;
362 + margin-top: 20px;
363 +}
364 +
365 +.spinner {
366 + width: 30px;
367 + height: 30px;
368 + border: 3px solid var(--gray-200);
369 + border-top-color: var(--primary-color);
370 + border-radius: 50%;
371 + animation: spin 1s linear infinite;
372 +}
373 +
374 +@keyframes spin {
375 + to {
376 + transform: rotate(360deg);
377 + }
378 +}
379 +
380 +/* ========================
381 + DASHBOARD SECTION
382 + ======================== */
383 +
384 +.dashboard-section {
385 + padding: 40px;
386 +}
387 +
388 +.welcome-box {
389 + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
390 + color: white;
391 + padding: 30px;
392 + margin-bottom: 40px;
393 + border: 1px solid rgba(255, 255, 255, 0.1);
394 +}
395 +
396 +.welcome-box h2 {
397 + font-size: 28px;
398 + margin-bottom: 10px;
399 +}
400 +
401 +.welcome-box p {
402 + font-size: 16px;
403 + opacity: 0.9;
404 +}
405 +
406 +/* ========================
407 + STATS GRID
408 + ======================== */
409 +
410 +.stats-grid {
411 + display: grid;
412 + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
413 + gap: 20px;
414 + margin-bottom: 40px;
415 +}
416 +
417 +.stat-card {
418 + background-color: white;
419 + padding: 30px;
420 + border: 1px solid var(--gray-200);
421 + box-shadow: var(--box-shadow);
422 + text-align: center;
423 +}
424 +
425 +.stat-card h3 {
426 + font-size: 32px;
427 + color: var(--primary-color);
428 + margin-bottom: 10px;
429 +}
430 +
431 +.stat-card p {
432 + font-size: 14px;
433 + color: var(--gray-600);
434 +}
435 +
436 +/* ========================
437 + ACTIVITY SECTION
438 + ======================== */
439 +
440 +.activity-section {
441 + background-color: white;
442 + padding: 30px;
443 + border: 1px solid var(--gray-200);
444 + box-shadow: var(--box-shadow);
445 +}
446 +
447 +.activity-section h3 {
448 + margin-bottom: 20px;
449 + font-size: 18px;
450 + color: var(--gray-900);
451 + border-bottom: 2px solid var(--primary-color);
452 + padding-bottom: 10px;
453 +}
454 +
455 +.activity-list {
456 + display: flex;
457 + flex-direction: column;
458 + gap: 15px;
459 +}
460 +
461 +.activity-item {
462 + padding: 15px;
463 + background-color: var(--gray-50);
464 + border-left: 3px solid var(--primary-color);
465 +}
466 +
467 +.empty-state {
468 + text-align: center;
469 + color: var(--gray-400);
470 + padding: 40px 20px;
471 +}
472 +
473 +/* ========================
474 + FOOTER
475 + ======================== */
476 +
477 +.footer {
478 + background-color: var(--gray-900);
479 + color: white;
480 + text-align: center;
481 + padding: 20px;
482 + font-size: 14px;
483 + margin-left: 250px;
484 +}
485 +
486 +/* ========================
487 + GLOBAL ERROR
488 + ======================== */
489 +
490 +.global-error {
491 + position: fixed;
492 + top: 20px;
493 + right: 20px;
494 + background-color: var(--danger-color);
495 + color: white;
496 + padding: 15px 20px;
497 + border: 1px solid #dc2626;
498 + max-width: 400px;
499 + z-index: 1000;
500 + animation: slideIn 0.3s ease;
501 +}
502 +
503 +@keyframes slideIn {
504 + from {
505 + transform: translateX(400px);
506 + opacity: 0;
507 + }
508 + to {
509 + transform: translateX(0);
510 + opacity: 1;
511 + }
512 +}
513 +
514 +/* ========================
515 + UTILITIES
516 + ======================== */
517 +
518 +.text-center {
519 + text-align: center;
520 +}
521 +
522 +.mt-20 {
523 + margin-top: 20px;
524 +}
525 +
526 +.mb-20 {
527 + margin-bottom: 20px;
528 +}
529 +
530 +.p-20 {
531 + padding: 20px;
532 +}
533 +
534 +.text-muted {
535 + color: var(--gray-500);
536 +}
537 +
538 +.text-danger {
539 + color: var(--danger-color);
540 +}
541 +
542 +.text-success {
543 + color: var(--success-color);
544 +}
frontend/index.html new
+197
@@ -0,0 +1,197 @@
1 +<!DOCTYPE html>
2 +<html lang="es">
3 +<head>
4 + <meta charset="UTF-8">
5 + <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 + <meta name="description" content="MAPS Connect - Plataforma Académica y de Mentoría">
7 + <title>MAPS Connect - Universidad Tecmilenio</title>
8 +
9 + <link rel="stylesheet" href="css/styles.css">
10 + <link rel="stylesheet" href="css/responsive.css">
11 +</head>
12 +<body>
13 + <!-- Sidebar Navigation -->
14 + <nav class="sidebar" id="sidebar">
15 + <div class="sidebar-header">
16 + <h1 class="logo">MAPS</h1>
17 + <button class="toggle-btn" id="toggleBtn">☰</button>
18 + </div>
19 +
20 + <ul class="nav-menu">
21 + <li><a href="index.html" class="nav-link active">Home</a></li>
22 + <li><a href="pages/foro.html" class="nav-link">Foro</a></li>
23 + <li><a href="pages/tips.html" class="nav-link">Tips</a></li>
24 + <li><a href="pages/recursos.html" class="nav-link">Recursos</a></li>
25 + <li><a href="pages/circulos.html" class="nav-link">Círculos</a></li>
26 + <li><a href="pages/mensajes.html" class="nav-link">Mensajes</a></li>
27 + <li><a href="pages/empresarial.html" class="nav-link">Empresarial</a></li>
28 + <li><a href="pages/perfil.html" class="nav-link">Perfil</a></li>
29 + </ul>
30 +
31 + <div class="sidebar-footer">
32 + <button id="logoutBtn" class="logout-btn">Cerrar Sesión</button>
33 + </div>
34 + </nav>
35 +
36 + <!-- Main Content -->
37 + <main class="container">
38 + <!-- Header -->
39 + <header class="header">
40 + <h1>Bienvenido a MAPS Connect</h1>
41 + <p class="subtitle">Plataforma Académica y de Mentoría - Universidad Tecmilenio</p>
42 + </header>
43 +
44 + <!-- Auth Section (Hidden when logged in) -->
45 + <section id="authSection" class="auth-section">
46 + <div class="auth-container">
47 + <div class="auth-box">
48 + <h2 id="authTitle">Iniciar Sesión</h2>
49 +
50 + <form id="authForm" class="form">
51 + <!-- Login Fields -->
52 + <div id="loginFields">
53 + <div class="form-group">
54 + <label for="loginEmail">Email Institucional</label>
55 + <input
56 + type="email"
57 + id="loginEmail"
58 + placeholder="usuario@tecmilenio.mx"
59 + required
60 + >
61 + <span class="error-message" id="loginEmailError"></span>
62 + </div>
63 +
64 + <div class="form-group">
65 + <label for="loginPassword">Contraseña</label>
66 + <input
67 + type="password"
68 + id="loginPassword"
69 + placeholder="••••••••"
70 + required
71 + >
72 + <span class="error-message" id="loginPasswordError"></span>
73 + </div>
74 + </div>
75 +
76 + <!-- Register Fields (Hidden by default) -->
77 + <div id="registerFields" style="display: none;">
78 + <div class="form-group">
79 + <label for="registerName">Nombre Completo</label>
80 + <input
81 + type="text"
82 + id="registerName"
83 + placeholder="Juan Pérez Gómez"
84 + required
85 + >
86 + <span class="error-message" id="registerNameError"></span>
87 + </div>
88 +
89 + <div class="form-group">
90 + <label for="registerEmail">Email Institucional</label>
91 + <input
92 + type="email"
93 + id="registerEmail"
94 + placeholder="usuario@tecmilenio.mx"
95 + required
96 + >
97 + <span class="error-message" id="registerEmailError"></span>
98 + </div>
99 +
100 + <div class="form-group">
101 + <label for="registerPassword">Contraseña</label>
102 + <input
103 + type="password"
104 + id="registerPassword"
105 + placeholder="••••••••"
106 + required
107 + >
108 + <span class="error-message" id="registerPasswordError"></span>
109 + </div>
110 +
111 + <div class="form-group">
112 + <label for="registerRole">Tipo de Usuario</label>
113 + <select id="registerRole" required>
114 + <option value="">Seleccionar...</option>
115 + <option value="STUDENT">Estudiante</option>
116 + <option value="PROFESSOR">Profesor</option>
117 + </select>
118 + <span class="error-message" id="registerRoleError"></span>
119 + </div>
120 + </div>
121 +
122 + <!-- Form Error -->
123 + <div id="formError" class="error-box" style="display: none;"></div>
124 +
125 + <!-- Submit Button -->
126 + <button type="submit" class="btn btn-primary" id="authSubmitBtn">
127 + Iniciar Sesión
128 + </button>
129 +
130 + <!-- Loading State -->
131 + <div id="loadingSpinner" class="loading-spinner" style="display: none;">
132 + <div class="spinner"></div>
133 + <p>Cargando...</p>
134 + </div>
135 + </form>
136 +
137 + <!-- Toggle Auth Form -->
138 + <div class="auth-toggle">
139 + <p id="toggleText">¿No tienes cuenta?
140 + <button type="button" id="toggleAuthBtn" class="link-btn">Regístrate</button>
141 + </p>
142 + </div>
143 + </div>
144 + </div>
145 + </section>
146 +
147 + <!-- Dashboard Section (Hidden when logged out) -->
148 + <section id="dashboardSection" class="dashboard-section" style="display: none;">
149 + <div class="welcome-box">
150 + <h2>Bienvenido, <span id="userName">Usuario</span></h2>
151 + <p>Conecta con tu comunidad académica</p>
152 + </div>
153 +
154 + <!-- Quick Stats -->
155 + <div class="stats-grid">
156 + <div class="stat-card">
157 + <h3>15</h3>
158 + <p>Dudas Resueltas</p>
159 + </div>
160 + <div class="stat-card">
161 + <h3>42</h3>
162 + <p>Tips Compartidos</p>
163 + </div>
164 + <div class="stat-card">
165 + <h3>8</h3>
166 + <p>Círculos Activos</p>
167 + </div>
168 + <div class="stat-card">
169 + <h3>120</h3>
170 + <p>Puntos de Reputación</p>
171 + </div>
172 + </div>
173 +
174 + <!-- Recent Activity -->
175 + <div class="activity-section">
176 + <h3>Actividad Reciente</h3>
177 + <div id="activityContainer" class="activity-list">
178 + <p class="empty-state">Cargando actividad...</p>
179 + </div>
180 + </div>
181 + </section>
182 +
183 + <!-- Error Message -->
184 + <div id="globalError" class="global-error" style="display: none;"></div>
185 + </main>
186 +
187 + <!-- Footer -->
188 + <footer class="footer">
189 + <p>&copy; 2024 MAPS Connect - Universidad Tecmilenio. Todos los derechos reservados.</p>
190 + </footer>
191 +
192 + <!-- Scripts -->
193 + <script src="js/config.js"></script>
194 + <script src="js/auth.js"></script>
195 + <script src="js/main.js"></script>
196 +</body>
197 +</html>
frontend/js/auth.js new
+249
@@ -0,0 +1,249 @@
1 +/* ========================
2 + AUTHENTICATION LOGIC
3 + ======================== */
4 +
5 +const authForm = document.getElementById('authForm');
6 +const authTitle = document.getElementById('authTitle');
7 +const authSubmitBtn = document.getElementById('authSubmitBtn');
8 +const toggleAuthBtn = document.getElementById('toggleAuthBtn');
9 +const toggleText = document.getElementById('toggleText');
10 +const formError = document.getElementById('formError');
11 +const loadingSpinner = document.getElementById('loadingSpinner');
12 +
13 +// Form fields elements
14 +const loginFields = document.getElementById('loginFields');
15 +const registerFields = document.getElementById('registerFields');
16 +const loginEmail = document.getElementById('loginEmail');
17 +const loginPassword = document.getElementById('loginPassword');
18 +const registerName = document.getElementById('registerName');
19 +const registerEmail = document.getElementById('registerEmail');
20 +const registerPassword = document.getElementById('registerPassword');
21 +const registerRole = document.getElementById('registerRole');
22 +
23 +let isLoginMode = true;
24 +
25 +/**
26 + * Alterna entre modo login y registro
27 + */
28 +function toggleAuthMode() {
29 + isLoginMode = !isLoginMode;
30 +
31 + if (isLoginMode) {
32 + authTitle.textContent = 'Iniciar Sesión';
33 + authSubmitBtn.textContent = 'Iniciar Sesión';
34 + toggleText.innerHTML = '¿No tienes cuenta? <button type="button" id="toggleAuthBtn" class="link-btn">Regístrate</button>';
35 + show(loginFields);
36 + hide(registerFields);
37 + } else {
38 + authTitle.textContent = 'Crear Cuenta';
39 + authSubmitBtn.textContent = 'Registrarse';
40 + toggleText.innerHTML = '¿Ya tienes cuenta? <button type="button" id="toggleAuthBtn" class="link-btn">Inicia Sesión</button>';
41 + hide(loginFields);
42 + show(registerFields);
43 + }
44 +
45 + clearForm(authForm);
46 + formError.style.display = 'none';
47 +
48 + // Re-attach event listener al nuevo botón
49 + document.getElementById('toggleAuthBtn').addEventListener('click', (e) => {
50 + e.preventDefault();
51 + toggleAuthMode();
52 + });
53 +}
54 +
55 +/**
56 + * Valida formulario de login
57 + * @returns {boolean}
58 + */
59 +function validateLoginForm() {
60 + let isValid = true;
61 + const emailError = document.getElementById('loginEmailError');
62 + const passwordError = document.getElementById('loginPasswordError');
63 +
64 + // Validar email
65 + if (!loginEmail.value.trim()) {
66 + emailError.textContent = 'El email es requerido';
67 + emailError.classList.add('show');
68 + isValid = false;
69 + } else if (!isValidInstitutionalEmail(loginEmail.value)) {
70 + emailError.textContent = 'Debe usar email institucional (@tecmilenio.mx)';
71 + emailError.classList.add('show');
72 + isValid = false;
73 + } else {
74 + emailError.classList.remove('show');
75 + }
76 +
77 + // Validar password
78 + if (!loginPassword.value.trim()) {
79 + passwordError.textContent = 'La contraseña es requerida';
80 + passwordError.classList.add('show');
81 + isValid = false;
82 + } else {
83 + passwordError.classList.remove('show');
84 + }
85 +
86 + return isValid;
87 +}
88 +
89 +/**
90 + * Valida formulario de registro
91 + * @returns {boolean}
92 + */
93 +function validateRegisterForm() {
94 + let isValid = true;
95 + const nameError = document.getElementById('registerNameError');
96 + const emailError = document.getElementById('registerEmailError');
97 + const passwordError = document.getElementById('registerPasswordError');
98 + const roleError = document.getElementById('registerRoleError');
99 +
100 + // Validar nombre
101 + if (!registerName.value.trim()) {
102 + nameError.textContent = 'El nombre es requerido';
103 + nameError.classList.add('show');
104 + isValid = false;
105 + } else if (!isValidName(registerName.value)) {
106 + nameError.textContent = 'Ingresa tu nombre completo (mínimo 3 caracteres)';
107 + nameError.classList.add('show');
108 + isValid = false;
109 + } else {
110 + nameError.classList.remove('show');
111 + }
112 +
113 + // Validar email
114 + if (!registerEmail.value.trim()) {
115 + emailError.textContent = 'El email es requerido';
116 + emailError.classList.add('show');
117 + isValid = false;
118 + } else if (!isValidInstitutionalEmail(registerEmail.value)) {
119 + emailError.textContent = 'Debe usar email institucional (@tecmilenio.mx)';
120 + emailError.classList.add('show');
121 + isValid = false;
122 + } else {
123 + emailError.classList.remove('show');
124 + }
125 +
126 + // Validar password
127 + if (!registerPassword.value.trim()) {
128 + passwordError.textContent = 'La contraseña es requerida';
129 + passwordError.classList.add('show');
130 + isValid = false;
131 + } else if (!isValidPassword(registerPassword.value)) {
132 + passwordError.textContent = 'Mínimo 8 caracteres, incluir mayúscula y número';
133 + passwordError.classList.add('show');
134 + isValid = false;
135 + } else {
136 + passwordError.classList.remove('show');
137 + }
138 +
139 + // Validar rol
140 + if (!registerRole.value) {
141 + roleError.textContent = 'Selecciona un tipo de usuario';
142 + roleError.classList.add('show');
143 + isValid = false;
144 + } else {
145 + roleError.classList.remove('show');
146 + }
147 +
148 + return isValid;
149 +}
150 +
151 +/**
152 + * Realiza login
153 + * @param {string} email - Email del usuario
154 + * @param {string} password - Contraseña
155 + */
156 +async function handleLogin(email, password) {
157 + try {
158 + show(loadingSpinner);
159 + hide(authForm);
160 + formError.style.display = 'none';
161 +
162 + const response = await fetchAPI(
163 + API_CONFIG.ENDPOINTS.AUTH.LOGIN,
164 + 'POST',
165 + { email, password }
166 + );
167 +
168 + // Guardar token y datos
169 + setAuthToken(response.token, response.expiresIn);
170 + setStoredUser(response.usuario);
171 +
172 + // Mostrar dashboard
173 + showDashboard(response.usuario);
174 + } catch (error) {
175 + console.error('Error de login:', error);
176 + formError.textContent = error.message || 'Error al iniciar sesión. Verifica tus credenciales.';
177 + formError.style.display = 'block';
178 + hide(loadingSpinner);
179 + show(authForm);
180 + }
181 +}
182 +
183 +/**
184 + * Realiza registro
185 + * @param {object} data - Datos del usuario
186 + */
187 +async function handleRegister(data) {
188 + try {
189 + show(loadingSpinner);
190 + hide(authForm);
191 + formError.style.display = 'none';
192 +
193 + const response = await fetchAPI(
194 + API_CONFIG.ENDPOINTS.AUTH.REGISTER,
195 + 'POST',
196 + data
197 + );
198 +
199 + // Auto-login después de registro
200 + await handleLogin(data.email, data.password);
201 + } catch (error) {
202 + console.error('Error de registro:', error);
203 + let errorMessage = 'Error al registrarse.';
204 +
205 + if (error.status === 400) {
206 + errorMessage = error.data?.message || 'Email ya registrado o datos inválidos.';
207 + }
208 +
209 + formError.textContent = errorMessage;
210 + formError.style.display = 'block';
211 + hide(loadingSpinner);
212 + show(authForm);
213 + }
214 +}
215 +
216 +/**
217 + * Maneja el envío del formulario de autenticación
218 + */
219 +authForm.addEventListener('submit', async (e) => {
220 + e.preventDefault();
221 +
222 + if (isLoginMode) {
223 + if (validateLoginForm()) {
224 + await handleLogin(loginEmail.value, loginPassword.value);
225 + }
226 + } else {
227 + if (validateRegisterForm()) {
228 + await handleRegister({
229 + nombre: registerName.value,
230 + email: registerEmail.value,
231 + password: registerPassword.value,
232 + rol: registerRole.value
233 + });
234 + }
235 + }
236 +});
237 +
238 +/**
239 + * Toggle botón
240 + */
241 +document.addEventListener('DOMContentLoaded', () => {
242 + const btn = document.getElementById('toggleAuthBtn');
243 + if (btn) {
244 + btn.addEventListener('click', (e) => {
245 + e.preventDefault();
246 + toggleAuthMode();
247 + });
248 + }
249 +});
frontend/js/config.js new
+259
@@ -0,0 +1,259 @@
1 +/* ========================
2 + API CONFIGURATION
3 + ======================== */
4 +
5 +const API_CONFIG = {
6 + BASE_URL: 'http://localhost:8080/api',
7 + TIMEOUT: 5000,
8 + ENDPOINTS: {
9 + AUTH: {
10 + LOGIN: '/auth/login',
11 + REGISTER: '/auth/register',
12 + LOGOUT: '/auth/logout',
13 + REFRESH: '/auth/refresh'
14 + },
15 + HEALTH: '/health',
16 + USERS: '/users',
17 + FORUM: '/forum',
18 + TIPS: '/tips',
19 + RESOURCES: '/resources',
20 + CHAT: '/chat',
21 + COMMUNITIES: '/communities',
22 + COMPANIES: '/companies'
23 + }
24 +};
25 +
26 +/* ========================
27 + UTILITY FUNCTIONS
28 + ======================== */
29 +
30 +/**
31 + * Realiza peticiones HTTP a la API
32 + * @param {string} endpoint - Endpoint de la API
33 + * @param {string} method - GET, POST, PUT, DELETE
34 + * @param {object} data - Datos a enviar (opcional)
35 + * @returns {Promise}
36 + */
37 +async function fetchAPI(endpoint, method = 'GET', data = null) {
38 + const url = `${API_CONFIG.BASE_URL}${endpoint}`;
39 + const options = {
40 + method,
41 + headers: {
42 + 'Content-Type': 'application/json'
43 + }
44 + };
45 +
46 + // Añadir token JWT si existe
47 + const token = localStorage.getItem('authToken');
48 + if (token) {
49 + options.headers['Authorization'] = `Bearer ${token}`;
50 + }
51 +
52 + // Añadir body si es POST, PUT o PATCH
53 + if (data && ['POST', 'PUT', 'PATCH'].includes(method)) {
54 + options.body = JSON.stringify(data);
55 + }
56 +
57 + try {
58 + const controller = new AbortController();
59 + const timeoutId = setTimeout(() => controller.abort(), API_CONFIG.TIMEOUT);
60 +
61 + const response = await fetch(url, {
62 + ...options,
63 + signal: controller.signal
64 + });
65 +
66 + clearTimeout(timeoutId);
67 +
68 + // Parsear respuesta
69 + const contentType = response.headers.get('content-type');
70 + let result;
71 +
72 + if (contentType && contentType.includes('application/json')) {
73 + result = await response.json();
74 + } else {
75 + result = await response.text();
76 + }
77 +
78 + // Validar status code
79 + if (!response.ok) {
80 + throw {
81 + status: response.status,
82 + data: result,
83 + message: result?.message || response.statusText
84 + };
85 + }
86 +
87 + return result;
88 + } catch (error) {
89 + if (error instanceof TypeError) {
90 + throw new Error('Error de conexión. Verifica que el backend esté ejecutándose.');
91 + }
92 + throw error;
93 + }
94 +}
95 +
96 +/**
97 + * Almacena el token JWT
98 + * @param {string} token - JWT token
99 + * @param {number} expiresIn - Tiempo de expiración en milisegundos
100 + */
101 +function setAuthToken(token, expiresIn) {
102 + localStorage.setItem('authToken', token);
103 + if (expiresIn) {
104 + const expiryTime = Date.now() + expiresIn;
105 + localStorage.setItem('tokenExpiry', expiryTime);
106 + }
107 +}
108 +
109 +/**
110 + * Obtiene el token JWT almacenado
111 + * @returns {string|null}
112 + */
113 +function getAuthToken() {
114 + return localStorage.getItem('authToken');
115 +}
116 +
117 +/**
118 + * Elimina el token JWT
119 + */
120 +function clearAuthToken() {
121 + localStorage.removeItem('authToken');
122 + localStorage.removeItem('tokenExpiry');
123 +}
124 +
125 +/**
126 + * Verifica si el token es válido
127 + * @returns {boolean}
128 + */
129 +function isTokenValid() {
130 + const token = getAuthToken();
131 + const expiry = localStorage.getItem('tokenExpiry');
132 +
133 + if (!token || !expiry) return false;
134 + return Date.now() < parseInt(expiry);
135 +}
136 +
137 +/**
138 + * Obtiene datos del usuario almacenados
139 + * @returns {object|null}
140 + */
141 +function getStoredUser() {
142 + const user = localStorage.getItem('currentUser');
143 + return user ? JSON.parse(user) : null;
144 +}
145 +
146 +/**
147 + * Almacena datos del usuario
148 + * @param {object} user - Objeto usuario
149 + */
150 +function setStoredUser(user) {
151 + localStorage.setItem('currentUser', JSON.stringify(user));
152 +}
153 +
154 +/**
155 + * Elimina datos del usuario
156 + */
157 +function clearStoredUser() {
158 + localStorage.removeItem('currentUser');
159 +}
160 +
161 +/**
162 + * Muestra un mensaje de error global
163 + * @param {string} message - Mensaje de error
164 + */
165 +function showError(message) {
166 + const errorElement = document.getElementById('globalError');
167 + if (errorElement) {
168 + errorElement.textContent = message;
169 + errorElement.style.display = 'block';
170 + setTimeout(() => {
171 + errorElement.style.display = 'none';
172 + }, 5000);
173 + }
174 +}
175 +
176 +/**
177 + * Valida formato de email institucional
178 + * @param {string} email - Email a validar
179 + * @returns {boolean}
180 + */
181 +function isValidInstitutionalEmail(email) {
182 + const institutionalDomains = ['@tecmilenio.mx', '@servicios.tecmilenio.mx'];
183 + return institutionalDomains.some(domain => email.endsWith(domain));
184 +}
185 +
186 +/**
187 + * Valida contraseña (mínimo 8 caracteres, mayúscula, número)
188 + * @param {string} password - Contraseña a validar
189 + * @returns {boolean}
190 + */
191 +function isValidPassword(password) {
192 + const minLength = 8;
193 + const hasUpperCase = /[A-Z]/.test(password);
194 + const hasNumber = /[0-9]/.test(password);
195 +
196 + return password.length >= minLength && hasUpperCase && hasNumber;
197 +}
198 +
199 +/**
200 + * Valida nombre completo
201 + * @param {string} name - Nombre a validar
202 + * @returns {boolean}
203 + */
204 +function isValidName(name) {
205 + return name.trim().length >= 3 && name.trim().split(' ').length >= 2;
206 +}
207 +
208 +/**
209 + * Formatea fecha para mostrar
210 + * @param {string|Date} date - Fecha
211 + * @returns {string}
212 + */
213 +function formatDate(date) {
214 + const d = new Date(date);
215 + return d.toLocaleDateString('es-MX', {
216 + year: 'numeric',
217 + month: 'long',
218 + day: 'numeric',
219 + hour: '2-digit',
220 + minute: '2-digit'
221 + });
222 +}
223 +
224 +/**
225 + * Trunca texto a N caracteres
226 + * @param {string} text - Texto
227 + * @param {number} length - Longitud máxima
228 + * @returns {string}
229 + */
230 +function truncateText(text, length = 100) {
231 + return text.length > length ? text.substring(0, length) + '...' : text;
232 +}
233 +
234 +/**
235 + * Limpia formulario
236 + * @param {HTMLFormElement} form - Formulario
237 + */
238 +function clearForm(form) {
239 + form.reset();
240 + form.querySelectorAll('.error-message').forEach(el => {
241 + el.classList.remove('show');
242 + });
243 +}
244 +
245 +/**
246 + * Muestra elemento
247 + * @param {HTMLElement} element - Elemento
248 + */
249 +function show(element) {
250 + if (element) element.style.display = '';
251 +}
252 +
253 +/**
254 + * Oculta elemento
255 + * @param {HTMLElement} element - Elemento
256 + */
257 +function hide(element) {
258 + if (element) element.style.display = 'none';
259 +}
frontend/js/main.js new
+166
@@ -0,0 +1,166 @@
1 +/* ========================
2 + MAIN APPLICATION LOGIC
3 + ======================== */
4 +
5 +const authSection = document.getElementById('authSection');
6 +const dashboardSection = document.getElementById('dashboardSection');
7 +const userNameElement = document.getElementById('userName');
8 +const logoutBtn = document.getElementById('logoutBtn');
9 +const sidebar = document.getElementById('sidebar');
10 +const toggleBtn = document.getElementById('toggleBtn');
11 +
12 +/**
13 + * Muestra la sección de dashboard
14 + * @param {object} user - Datos del usuario
15 + */
16 +function showDashboard(user) {
17 + hide(authSection);
18 + show(dashboardSection);
19 +
20 + if (user && userNameElement) {
21 + const firstName = user.nombre ? user.nombre.split(' ')[0] : 'Usuario';
22 + userNameElement.textContent = firstName;
23 + }
24 +
25 + loadActivity();
26 +}
27 +
28 +/**
29 + * Muestra la sección de autenticación
30 + */
31 +function showAuth() {
32 + show(authSection);
33 + hide(dashboardSection);
34 + clearStoredUser();
35 + clearAuthToken();
36 +}
37 +
38 +/**
39 + * Carga actividad reciente
40 + */
41 +async function loadActivity() {
42 + try {
43 + const activityContainer = document.getElementById('activityContainer');
44 +
45 + // Simulación: En producción, esto vendría del backend
46 + const activities = [
47 + {
48 + title: 'Respondiste una duda en Matemáticas',
49 + time: 'hace 2 horas'
50 + },
51 + {
52 + title: 'Te uniste al círculo de Programación',
53 + time: 'hace 1 día'
54 + },
55 + {
56 + title: 'Compartiste un tip académico',
57 + time: 'hace 2 días'
58 + }
59 + ];
60 +
61 + activityContainer.innerHTML = activities.map(activity => `
62 + <div class="activity-item">
63 + <p>${activity.title}</p>
64 + <small class="text-muted">${activity.time}</small>
65 + </div>
66 + `).join('');
67 + } catch (error) {
68 + console.error('Error cargando actividad:', error);
69 + }
70 +}
71 +
72 +/**
73 + * Maneja logout
74 + */
75 +async function handleLogout() {
76 + try {
77 + // Opcional: notificar al backend
78 + // await fetchAPI(API_CONFIG.ENDPOINTS.AUTH.LOGOUT, 'POST');
79 + } catch (error) {
80 + console.error('Error en logout:', error);
81 + } finally {
82 + showAuth();
83 + }
84 +}
85 +
86 +/**
87 + * Verifica si el usuario está autenticado y carga la UI apropiada
88 + */
89 +function initializeApp() {
90 + if (isTokenValid() && getStoredUser()) {
91 + showDashboard(getStoredUser());
92 + } else {
93 + clearAuthToken();
94 + clearStoredUser();
95 + showAuth();
96 + }
97 +}
98 +
99 +/**
100 + * Toggle sidebar en móvil
101 + */
102 +if (toggleBtn) {
103 + toggleBtn.addEventListener('click', () => {
104 + sidebar.classList.toggle('collapsed');
105 + });
106 +}
107 +
108 +/**
109 + * Logout button
110 + */
111 +if (logoutBtn) {
112 + logoutBtn.addEventListener('click', () => {
113 + if (confirm('¿Estás seguro de que quieres cerrar sesión?')) {
114 + handleLogout();
115 + }
116 + });
117 +}
118 +
119 +/**
120 + * Actualizar enlace activo en navegación
121 + */
122 +function updateActiveNavLink() {
123 + const currentPage = window.location.pathname.split('/').pop() || 'index.html';
124 +
125 + document.querySelectorAll('.nav-link').forEach(link => {
126 + const href = link.getAttribute('href');
127 + if (href === currentPage || (currentPage === '' && href === 'index.html')) {
128 + link.classList.add('active');
129 + } else {
130 + link.classList.remove('active');
131 + }
132 + });
133 +}
134 +
135 +/**
136 + * Inicializa la aplicación cuando el DOM esté listo
137 + */
138 +document.addEventListener('DOMContentLoaded', () => {
139 + initializeApp();
140 + updateActiveNavLink();
141 +});
142 +
143 +/**
144 + * Escucha cambios en el almacenamiento local (para sincronizar entre pestañas)
145 + */
146 +window.addEventListener('storage', (e) => {
147 + if (e.key === 'authToken' && !e.newValue) {
148 + showAuth();
149 + }
150 +});
151 +
152 +/**
153 + * Manejo global de errores
154 + */
155 +window.addEventListener('error', (event) => {
156 + console.error('Error global:', event.error);
157 + showError('Ocurrió un error inesperado. Por favor, recarga la página.');
158 +});
159 +
160 +/**
161 + * Manejo de rechazos de promesas no capturadas
162 + */
163 +window.addEventListener('unhandledrejection', (event) => {
164 + console.error('Promesa rechazada:', event.reason);
165 + showError('Error de conexión. Verifica tu conexión a internet.');
166 +});