03 Desarrollo Frontend
Actualizado: 17 de marzo de 2026
Ruta de Aprendizaje y Onboarding: Desarrollo Frontend#
La arquitectura Frontend del proyecto SoyDigital abarca múltiples interfaces: la app de estudiantes (dominicana-front), el panel administrativo (dominicana-admin-front), el CRM (crm-indotel), el sistema de brigadas (brigades), y las analíticas (dominicana-brigades-analitics). El stack principal compartido es React, Next.js, TypeScript y TailwindCSS.
Tiempo estimado de estudio: 40–60 horas
Prerrequisitos: Conocimientos básicos de HTML y lógica de programación. Si nunca has hecho una página web, empieza por aquí.
Índice de Contenidos y Mapa de Profundidad#
- Nivel 1 (Junior): Fundamentos de Frontend
- Clase 1.1: El DOM y Límites de Navegadores Móviles
- Clase 1.2: Tipados Estrictos y DTOs Compartidos
- Clase 1.3: Ciclo de Vida React y Render Virtual
- Clase 1.4: Multiplicación de Hooks y Dependency Arrays
- Clase 1.5: Tailwind y Renderizados Atómicos
- Nivel 2 (Semi-Senior): Next.js App Router
- Clase 2.1: SSR vs CSR en Entornos Limitados
- Clase 2.2: Estructuras Basadas en Archivos (Rutas)
- Clase 2.3: Límites entre Cliente y Servidor (Boundary)
- Clase 2.4: Mutación y Caché Avanzado (revalidate)
- Nivel 3 (Senior): Estado Complejo y Formularios
- Clase 3.1: Rendimiento RHF y Zod en Offline
- Clase 3.2: Redux vs Zustand (Stores ligeros)
- Nivel 4 (Arquitecto/Experto): Local-First y Edge Cases
- Clase 4.1: Interceptores Axios y Refresh Tokens
- Clase 4.2: SWR y Feedback Latencia-0 (Optimistic Updates)
- Clase 4.3: Destrozos LRU en IndexedDB bajo Quota Evasions
- Clase 4.4: Contenedores Chromium para E2E y Regresiones
- Clase 4.5: Mapeo de Aplicaciones SoyDigital
- Clase 4.6: Reglas de Negocio Desacopladas
Exigencias y Requisitos Técnicos del Rol#
Este rol NO es "pintar botones". Debes entender el ciclo de vida de React, la diferencia Cliente/Servidor, y optimizar para dispositivos lentos (campo con mala red).
1. Dominio de Linux (OBLIGATORIO)#
Debes saber levantar tus entornos locales, manejar variables de entorno y entender por qué falla un build en Linux (CI/CD) aunque funcione en tu Mac/Windows.
Acción: Completa el manual
00-Fundamentos-Linux-Esenciales.mdantes de seguir.
2. React Avanzado y Hooks#
Debes dominar useEffect, useCallback, useMemo y las reglas de los Hooks. No puedes escribir componentes que rendericen innecesariamente.
3. Next.js y Server Actions#
Debes entender la diferencia entre Server Components y Client Components, y cuándo usar cada uno para optimizar la carga inicial (FCP).
4. TypeScript Estricto#
No se permite any. Debes tipar props, estados y respuestas de API correctamente.
5. Rendimiento y Offline-First#
Debes saber implementar Service Workers, IndexedDB y estrategias de caché (SWR/React Query) para que la app funcione sin internet.
Nivel 1 (Junior) - Fundamentos Web — Cómo Funciona Internet en tu Navegador#
Antes de tocar React o Next.js, debes entender cómo funciona la web desde cero.
Clase 1.1: ¿Qué es un Navegador Web?#
➤ Profundización Técnica / Casos de Borde:
- Asfixia del DOM (Row Virtualization): En una tablet Samsung de bajo costo, tener miles de nodos en un listado de "10,000 Estudiantes" causará que el scroll se caiga a 10 FPS. El desarrollador debe virtualizar (renderizar solo los 10 divs que la pantalla ve) ahorrando la RAM destructiva.
Un navegador (Chrome, Safari, Firefox) es un programa que:
- Pide archivos a un servidor (HTML, CSS, JS, imágenes, videos)
- Interpreta esos archivos y los convierte en lo que ves en pantalla
- Ejecuta código JavaScript para hacerlo interactivo
Los 3 lenguajes fundamentales de la web#
| Lenguaje | Función | Analogía |
|---|---|---|
| HTML | Estructura — qué elementos hay en la página | Los huesos del cuerpo |
| CSS | Apariencia — colores, tamaños, posiciones | La piel y la ropa |
| JavaScript | Comportamiento — qué pasa cuando haces clic | Los músculos y reflejos |
HTML: La estructura#
html<!-- Un formulario de login simplificado --> <div> <h1>Bienvenido a SoyDigital</h1> <form> <label>Cédula:</label> <input type="text" placeholder="Ingrese su cédula" /> <button type="submit">Ingresar</button> </form> </div>
CSS: La apariencia#
css/* Hace que el título sea azul y centrado */ h1 { color: #1a56db; text-align: center; font-size: 24px; } /* Hace que el botón sea verde con bordes redondeados */ button { background-color: #10b981; color: white; padding: 12px 24px; border-radius: 8px; }
JavaScript: El comportamiento#
javascript// Cuando el usuario hace clic en "Ingresar" document.querySelector('form').addEventListener('submit', (event) => { event.preventDefault(); // No recargar la página const cedula = document.querySelector('input').value; if (cedula.length !== 11) { alert('La cédula debe tener 11 dígitos'); } else { // Enviar al servidor... } });
Clase 1.2: TypeScript para Frontend#
TypeScript es JavaScript con tipos estáticos — detecta errores antes de que el usuario los vea:
typescript// Sin tipos (JavaScript puro) — el error se descubre EN PRODUCCIÓN function mostrarNota(nota) { return nota.toFixed(2); // Si nota es "hola", explota } // Con tipos (TypeScript) — el error se descubre AL ESCRIBIR CÓDIGO function mostrarNota(nota: number): string { return nota.toFixed(2); // TypeScript garantiza que nota es un número }
Tipos que verás constantemente en el proyecto:
typescript// Interfaz de usuario (lo que devuelve la API) interface User { id: string; full_name: string; cedula: string; current_level: number; finished_a_level: boolean; certificates: Certificate[]; } // Props de un componente React interface ButtonProps { label: string; onClick: () => void; disabled?: boolean; // El ? significa que es opcional variant?: 'primary' | 'secondary' | 'danger'; // Solo estos 3 valores } // Estado del progreso educativo type ProgressState = 'locked' | 'active' | 'done';
Clase 1.3: ¿Qué es React? (Desde Cero)#
React es una librería de JavaScript para construir interfaces de usuario a partir de componentes reutilizables.
¿Qué es un Componente?#
Un componente es un bloque de interfaz que tiene su propia estructura (HTML), apariencia (CSS) y comportamiento (JS). En vez de tener un HTML gigante, dividimos la pantalla en piezas:
Tu primer componente React:#
tsx// components/Button.tsx interface ButtonProps { label: string; onClick: () => void; variant?: 'primary' | 'danger'; } export function Button({ label, onClick, variant = 'primary' }: ButtonProps) { const colors = { primary: 'bg-blue-600 text-white', danger: 'bg-red-600 text-white', }; return ( <button onClick={onClick} className={`px-4 py-2 rounded-lg ${colors[variant]}`} > {label} </button> ); } // Uso: <Button label="Continuar" onClick={() => avanzarModulo()} /> <Button label="Eliminar" onClick={() => borrar()} variant="danger" />
Props = los "parámetros" del componente#
| Concepto | Explicación |
|---|---|
| Props | Datos que un componente padre le pasa a un hijo. Son de solo lectura |
| Children | El contenido que va entre las etiquetas del componente |
tsx// El padre le pasa datos al hijo: <UserCard name="María" level={2} certified={true} /> // El hijo los recibe: function UserCard({ name, level, certified }: UserCardProps) { return ( <div> <h2>{name}</h2> <p>Nivel: {level}</p> {certified && <span> Certificada</span>} </div> ); }
Clase 1.4: Hooks — El Cerebro de los Componentes#
➤ Profundización Técnica / Casos de Borde:
- Ataque DDoS Local (useEffect loop): Si olvidas pasarle el array vacío
[]a un efecto que hace data-fetching, el Frontend hará 50 peticionesGETa la API local del Mini-PC por cada letra que el usuario escriba, colapsando tanto la tablet como el Servidor instantáneamente.
Los Hooks son funciones especiales que le dan "memoria" y "reflejos" a los componentes.
useState — Memoria del componente#
tsxfunction Contador() { // Declara una variable "count" que React recuerda entre re-renders const [count, setCount] = useState(0); return ( <div> <p>Has hecho clic {count} veces</p> <button onClick={() => setCount(count + 1)}> Incrementar </button> </div> ); }
useEffect — Efectos secundarios#
Se ejecuta cuando el componente se monta o cuando cambian ciertas dependencias:
tsxfunction UserProfile({ userId }: { userId: string }) { const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState(true); useEffect(() => { // Se ejecuta cuando el componente aparece en pantalla async function fetchUser() { const response = await fetch(`/api/users/${userId}`); const data = await response.json(); setUser(data); setLoading(false); } fetchUser(); }, [userId]); // ← Solo se re-ejecuta si userId cambia if (loading) return <p>Cargando...</p>; if (!user) return <p>Usuario no encontrado</p>; return <h1>{user.full_name}</h1>; }
** Regla de oro:**
useEffectse usa SOLO cuando es estrictamente necesario. Abusar de él causa problemas de rendimiento. En Next.js App Router, preferimos cargar datos desde Server Components.
Custom Hooks — Encapsular lógica repetitiva#
tsx// hooks/useAuth.ts function useAuth() { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const token = localStorage.getItem('auth_token'); if (token) { // Validar token y obtener usuario... } setIsLoading(false); }, []); return { user, isLoading, isAuthenticated: !!user }; } // Uso en cualquier componente: function Dashboard() { const { user, isLoading, isAuthenticated } = useAuth(); if (isLoading) return <Spinner />; if (!isAuthenticated) return <Redirect to="/login" />; return <h1>Bienvenido, {user.name}</h1>; }
Clase 1.5: TailwindCSS — Estilos Utility-First#
En SoyDigital no usamos archivos CSS separados. Usamos TailwindCSS, donde los estilos se aplican directamente en las clases HTML:
tsx// CSS tradicional (NO hacemos esto) <div className="mi-tarjeta"> <h2 className="mi-titulo">Módulo 1</h2> </div> // + archivo CSS separado con .mi-tarjeta { padding: 16px; } etc. // Tailwind (SÍ hacemos esto) <div className="flex flex-col items-center p-4 bg-white rounded-lg shadow-md"> <h2 className="text-xl font-bold text-gray-800">Módulo 1</h2> </div>
Clases Tailwind que verás constantemente#
| Clase | Efecto | CSS equivalente |
|---|---|---|
flex | Display flex (layout horizontal/vertical) | display: flex |
flex-col | Dirección vertical | flex-direction: column |
items-center | Centra verticalmente | align-items: center |
justify-between | Espacio entre elementos | justify-content: space-between |
p-4 | Padding de 16px (4 × 4px) | padding: 16px |
mt-2 | Margin top de 8px | margin-top: 8px |
text-xl | Texto grande (20px) | font-size: 1.25rem |
font-bold | Texto en negrita | font-weight: 700 |
bg-blue-600 | Fondo azul | background-color: #2563eb |
text-white | Texto blanco | color: #fff |
rounded-lg | Bordes redondeados | border-radius: 8px |
shadow-md | Sombra media | box-shadow: ... |
hidden | Oculto | display: none |
w-full | Ancho 100% | width: 100% |
hover:bg-blue-700 | Cambia color al pasar mouse | :hover { background-color } |
¿Por qué Tailwind en SoyDigital? Garantiza que el panel de administración en Santo Domingo y el simulador en campo en Monte Plata tengan colores y dimensiones idénticos sin conflictos de CSS.
Nivel 2 (Semi-Senior) - Next.js — El Framework del Proyecto#
Clase 2.1: ¿Qué es Next.js y por qué lo usamos?#
Next.js es un framework basado en React que añade:
- Server-Side Rendering (SSR): La página se genera en el servidor, no en el navegador → carga más rápido
- Routing automático: La estructura de carpetas define las URLs
- API Routes: Puedes tener endpoints backend dentro del mismo proyecto
- Optimización automática: Imágenes, fuentes, código dividido automáticamente
¿Por qué Next.js en vez de React puro?#
En SoyDigital, los ciudadanos usan teléfonos básicos con conexiones lentas. Next.js:
- Envía menos JavaScript al navegador (los Server Components se ejecutan en el servidor)
- La primera carga es HTML real, no una pantalla en blanco esperando JS
- El caché de datos reduce peticiones a la API
Clase 2.2: App Router — Estructura de Carpetas = URLs#
En Next.js App Router (directorio src/app), cada carpeta es una ruta:
Clase 2.3: Server Components vs Client Components#
➤ Profundización Técnica / Casos de Borde:
- Client Boundary Leakage: Ocurre cuando importas un módulo super pesado de gráficos (
Chart.js) en un archivo que debió ser exclusivo de Servidor. Esto inyecta 2 MegaBytes innecesarios al JavaScript bundle final de la tablet, saturando la red satelital de los brigadistas en el primer pantallazo.
Esta es la distinción MÁS IMPORTANTE de Next.js App Router:
| Tipo | Se ejecuta en | Usa interactividad | Cuándo usarlo |
|---|---|---|---|
| Server Component (default) | El servidor | No puede usar useState, onClick, etc. | Cargar datos, mostrar contenido estático, consultar DB |
Client Component ("use client") | El navegador del usuario | Puede usar Hooks, eventos, animaciones | Formularios, botones interactivos, estados dinámicos |
tsx// Server Component (por defecto) — se ejecuta en el servidor // Puede hacer fetch directo, no envía JS al navegador async function LevelPage({ params }: { params: { levelId: string } }) { const level = await fetch(`${API_URL}/levels/${params.levelId}`); const data = await level.json(); return ( <div> <h1>{data.name}</h1> <p>{data.description}</p> <ModuleList modules={data.modules} /> {/* Server Component */} <ProgressTracker userId={data.userId} /> {/* Client Component */} </div> ); } // Client Component — necesita "use client" en la primera línea "use client"; function ProgressTracker({ userId }: { userId: string }) { const [progress, setProgress] = useState(0); return ( <div> <p>Progreso: {progress}%</p> <button onClick={() => setProgress(p => p + 10)}>Avanzar</button> </div> ); }
Regla de oro del proyecto: Empuja el
"use client"lo más "abajo" del árbol de componentes posible. Si solo un botón necesita interactividad, solo ese botón debe ser Client Component, no la página entera.
Clase 2.4: Fetching de Datos y Caché#
➤ Profundización Técnica / Casos de Borde:
- Next.js Aggressive Static Cache: Next.js por defecto intentará cachear eternamente cada llamada a la API en producción. Si un ciudadano termina un examen pero sigue viéndolo "No Iniciado" al recargar, es por este caché agresivo; debe controlarse con tags
revalidatePathselectivos.
En Server Components (la forma preferida):
tsx// Fetch directo dentro del componente — Next.js lo cachea automáticamente async function DashboardStats() { const res = await fetch(`${API_URL}/stats`, { next: { revalidate: 60 }, // Recachea cada 60 segundos }); const stats = await res.json(); return <StatsDisplay data={stats} />; }
Invalidar el caché cuando los datos cambian:
tsx// En una Server Action (tras una mutación) import { revalidatePath } from 'next/cache'; async function completarRecurso(recursoId: string) { await fetch(`${API_URL}/progress/complete`, { method: 'POST', body: ... }); revalidatePath('/dashboard'); // Le dice a Next.js: "actualiza esta página" }
Nivel 3 (Senior) - Gestión de Formularios y Estados Globales#
Clase 3.1: React Hook Form + Zod#
➤ Profundización Técnica / Casos de Borde:
- Validación Pre-Emptiva Offline: Validar únicamente en Backend es un pecado en Indotel. Si el Frontend permite enviar una cédula con letras bajo conexión cortada de red, el payload va a IndexedDB. Cuando la red vuelva y se intente sincronizar silente, saltará un 400 Bad Request invisible y la nota de ese alumno quedará perdida en el limbo.
El CRM y las brigadas manejan formularios complejos (registros, encuestas, reportes). Prohibido usar useState para cada campo:
tsx"use client"; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; // Esquema de validación con Zod const registroSchema = z.object({ cedula: z.string().length(11, 'La cédula debe tener 11 dígitos').regex(/^\d+$/, 'Solo números'), full_name: z.string().min(3, 'Mínimo 3 caracteres'), birth_date: z.string().refine(val => { const age = new Date().getFullYear() - new Date(val).getFullYear(); return age >= 16; }, 'Debe tener al menos 16 años'), gender: z.enum(['M', 'F', 'O'], { message: 'Seleccione un género' }), }); type RegistroForm = z.infer<typeof registroSchema>; function FormularioRegistro() { const { register, handleSubmit, formState: { errors } } = useForm<RegistroForm>({ resolver: zodResolver(registroSchema), }); const onSubmit = async (data: RegistroForm) => { await fetch('/api/users', { method: 'POST', body: JSON.stringify(data) }); }; return ( <form onSubmit={handleSubmit(onSubmit)}> <input {...register('cedula')} placeholder="Cédula" /> {errors.cedula && <span className="text-red-500">{errors.cedula.message}</span>} <input {...register('full_name')} placeholder="Nombre completo" /> {errors.full_name && <span className="text-red-500">{errors.full_name.message}</span>} <input {...register('birth_date')} type="date" /> {errors.birth_date && <span className="text-red-500">{errors.birth_date.message}</span>} <select {...register('gender')}> <option value="">Seleccione...</option> <option value="M">Masculino</option> <option value="F">Femenino</option> <option value="O">Otro</option> </select> <button type="submit">Registrar</button> </form> ); }
Clase 3.2: Estados Globales — Zustand y Context API#
| Herramienta | Cuándo usarla | Ejemplo en SoyDigital |
|---|---|---|
| Context API | Datos estáticos o que cambian poco | Tema oscuro/claro, idioma, datos del usuario autenticado |
| Zustand | Estados dinámicos complejos compartidos | Progreso temporal del formulario multietapa, bandeja de errores |
tsx// store/useProgressStore.ts (Zustand) import { create } from 'zustand'; interface ProgressStore { currentResource: string | null; completedResources: string[]; setCurrentResource: (id: string) => void; markCompleted: (id: string) => void; } export const useProgressStore = create<ProgressStore>((set) => ({ currentResource: null, completedResources: [], setCurrentResource: (id) => set({ currentResource: id }), markCompleted: (id) => set((state) => ({ completedResources: [...state.completedResources, id], })), })); // Uso en CUALQUIER componente (sin necesidad de prop drilling): function ResourceViewer() { const { currentResource, markCompleted } = useProgressStore(); // ... }
Nivel 4 (Arquitecto/Experto) - Modo Offline, Consumo de API y Testing#
Clase 4.1: El Cliente HTTP de dominicana-front#
El frontend usa una instancia Axios centralizada (src/utils/axios.ts) con capacidades offline:
Configuración clave:
| Parámetro | Valor |
|---|---|
| Base URL | NEXT_PUBLIC_API_URL (ej. http://localhost:7000) |
| Token | localStorage key auth_token, inyectado como Bearer |
| IndexedDB | DB dominicana-db v4, store cachedResponses, max 1000 entradas con LRU eviction |
| TTL caché | 24 horas |
| Reescritura GCS | URLs de storage.googleapis.com → proxy local NEXT_PUBLIC_GCS_PROXY_URL |
Manejo de errores HTTP:
| Código | Comportamiento |
|---|---|
401 | Limpia auth_token, redirige a / |
| Network Error (offline) | Sirve desde IndexedDB si hay caché válido |
5xx | Retry via service worker (Workbox) |
Clase 4.2: Actualizaciones Optimistas (Optimistic Updates)#
➤ Profundización Técnica / Casos de Borde:
- Rollback Asíncrono de UI: Engañas al cerebro del usuario dándole un check verde instantáneo antes de mandar a red. Pero, si 30 segundos después el Backend lo rechaza por Duplicado, el Frontend debe revertir silenciosamente el Check Verde en UI sin destruir el "scroll position" actual del profesor (ej. SWR
rollbackOnError).
En zonas rurales con internet lento, no podemos hacer que el usuario espere 5 segundos por cada acción:
Esto se gestiona con SWR o React Query (TanStack Query):
tsximport { useSWRConfig } from 'swr'; function CompletarRecurso({ recursoId }: { recursoId: string }) { const { mutate } = useSWRConfig(); const handleComplete = async () => { // 1. Actualiza la UI inmediatamente (optimista) mutate('/api/progress', (current) => ({ ...current, completed: [...current.completed, recursoId], }), false); // false = no revalidar todavía try { // 2. Envía al servidor en segundo plano await fetch('/api/progress/complete', { method: 'POST', body: JSON.stringify({ recourse_id: recursoId }), }); } catch (error) { // 3. Si falla, revierte la UI mutate('/api/progress'); // Re-fetch real toast.error('Error al guardar. Inténtalo de nuevo.'); } }; }
Clase 4.3: Offline Progress — Guardado Local#
➤ Profundización Técnica / Casos de Borde:
- Apple IndexedDB Quota Wipe: Safari en iPads "limpia secretamente" la Cache y el IndexedDB si el dispositivo físico se queda sin disco o no has visitado la PWA en 7 días para ahorrar megas. El Frontend debe medir APIs de cuotas (StorageManager API) y avisar mediante una alerta roja si se está al borde de un barrido no deseado.
Cuando no hay internet, el progreso se guarda en localStorage:
typescript// Estructura de offlineProgress en localStorage interface OfflineEvent { clientRequestId: string; // UUID v4 — previene duplicados al sincronizar recourse_id: string; action: 'view' | 'complete'; time_spent: number; timestamp: string; // America/Santo_Domingo } // Al detectar que estamos offline: const events: OfflineEvent[] = JSON.parse(localStorage.getItem('offlineProgress') || '[]'); events.push({ clientRequestId: crypto.randomUUID(), recourse_id: 'rec-001', action: 'complete', time_spent: 300, timestamp: new Date().toISOString(), }); localStorage.setItem('offlineProgress', JSON.stringify(events)); // Al volver online: se envían en lotes de 50 a la API
Clase 4.4: Playwright — Tests End-to-End#
Antes de mergear código de interacción central, es obligatorio tener tests E2E:
typescript// tests/e2e/login.spec.ts import { test, expect } from '@playwright/test'; test('Un ciudadano puede hacer login con cédula válida', async ({ page }) => { // 1. Navegar a la página de login await page.goto('/login'); // 2. Escribir la cédula await page.fill('input[name="cedula"]', '40200000000'); // 3. Hacer clic en Ingresar await page.click('button[type="submit"]'); // 4. Verificar que llegamos al dashboard await expect(page).toHaveURL('/dashboard'); await expect(page.locator('h1')).toContainText('Bienvenido'); }); test('Rechaza cédulas inválidas', async ({ page }) => { await page.goto('/login'); await page.fill('input[name="cedula"]', '123'); await page.click('button[type="submit"]'); // Debe mostrar error de validación await expect(page.locator('.text-red-500')).toBeVisible(); });
Clase 4.5: Las Aplicaciones Frontend del Ecosistema#
| App | Puerto | Stack | Particularidades |
|---|---|---|---|
| dominicana-front | 7011 | Next.js 15 + React 19 + IndexedDB + Playwright | PWA offline-first, caché agresivo, reescritura GCS |
| dominicana-admin-front | 7001 | Next.js 15 + React 19 + Firebase Auth | Panel admin: gestión de usuarios, certificados, overrides |
| brigades | 23000 | Next.js + GraphQL (Apollo) + Prisma | Monolito: ferias, inventario, operación territorial. Auth por JWT propio |
| dominicana-brigades-analitics | 3001 | Next.js 15 + React 18 + Chart.js | Embebido en brigades vía iframe + postMessage para JWT |
| crm-indotel | 3000 | Next.js 16 + Prisma + Twilio SDK | Campañas multicanal (SMS, WhatsApp, voz). Auth S2S |
| dominicana-simulator | — | HTML/CSS/JS estático | Simuladores por nivel, servidos desde GCS |
| docs-brigade | — | Next.js 14 + React 18 + Prisma | Portal de documentación, consume docs-architecture como submódulo |
Clase 4.6: Reglas de Negocio que Impactan el Frontend#
| Regla | Impacto en UI |
|---|---|
| Anti-Downgrade | Nunca muestres un nivel inferior al actual como "disponible para reasignación" |
| Progresión secuencial | Los módulos/actividades futuros aparecen como locked (candado) |
| Tolerancia LAN (≤3) | No muestres "error" si faltan ≤3 recursos y el examen está aprobado |
| Regla del Árbol de Manzanas | Si finished_a_level === true, desbloquea la exploración libre en el mapa |
| Triple Click Bypass | El temporizador de lectura se anula con triple clic (solo para testing) |
| Estados educativos | locked (candado gris), active (resaltado), done (check verde) |
| Mini-curso obligatorio | Mostrar aviso "Completa un mini-curso" antes de permitir certificar niveles 2-4 |
| Zona horaria | Siempre usar America/Santo_Domingo. Datos pre-2026-02-05 requieren conversión UTC |
Práctica Obligatoria Front-End#
bash# 1. Clonar el proyecto git clone <url> dominicana-front cd dominicana-front # 2. Instalar dependencias npm install # 3. Configurar variables de entorno cp .env.example .env.local # Editar .env.local: # NEXT_PUBLIC_API_URL=http://localhost:7000 # NEXT_PUBLIC_GCS_PROXY_URL=http://localhost:9000 # 4. Iniciar en modo desarrollo npm run dev # Acceder a http://localhost:7011 # 5. Verificar linter npm run lint # 6. Ejecutar tests E2E (si están configurados) npx playwright test
Glosario del Desarrollador Frontend#
| Término | Significado |
|---|---|
| App Router | Sistema de routing de Next.js basado en estructura de carpetas (src/app/) |
| Client Component | Componente con "use client" que se ejecuta en el navegador (interactivo) |
| CSR | Client-Side Rendering — la página se genera en el navegador |
| Custom Hook | Función reutilizable que encapsula lógica con Hooks de React |
| E2E | End-to-End testing — pruebas que simulan un usuario real |
| IndexedDB | Base de datos del navegador para caché offline |
| JSX/TSX | Extensión de JavaScript/TypeScript que permite escribir HTML en código |
| LRU Eviction | Least Recently Used — cuando el caché está lleno, borra lo más viejo |
| Optimistic Update | Mostrar el resultado esperado antes de que el servidor confirme |
| Prop Drilling | Pasar datos de padre a hijo a nieto... (antipatrón que Zustand/Context resuelven) |
| PWA | Progressive Web App — app web que funciona como app nativa (offline, instalable) |
| Revalidation | Proceso de actualizar datos cacheados en Next.js |
| Server Component | Componente que se ejecuta en el servidor (sin interactividad, menos JS enviado) |
| SSR | Server-Side Rendering — la página se genera en el servidor antes de enviarla |
| SWR | Stale-While-Revalidate — estrategia de caché que muestra datos viejos mientras actualiza |
| Tailwind | Framework CSS utility-first (clases directas en el HTML) |
| Zustand | Librería minimalista de estado global para React |
Ruta de Dominio y Escalafón Profesional (Matriz de Habilidades Frontend)#
El Frontend Indotel se aloja tanto en un CDN remoto como (crucialmente) directamente en Tablets Android y iPads operando al 100% offline dentro de PWA's con proxies locales.
Nivel Junior (Next.js, UX y Componentes)#
- Next.js App Router: Arquitectura fundamental (Rutas, Layouts, Client vs Server components). Generar UIs limpias y componentes tolerantes con Tailwind CSS e inyección de datos (SWR/React Query básico).
- Validación de Formularios Rígida: Integración con React Hook Form o Zod para garantizar que, cuando no haya internet, el formulario no se guíe de validaciones de backend sino de tipados client-side perfectos.
- PWA Base (Manifest & Installation): Saber implementar un
manifest.json, iconografía exigente de PWA para ser anclada al "HomeScreen" de iOS(Safari) o Android (Chrome Chrome/Brave).
Nivel Semi-Senior (Persistencia IndexedDB & Service Worker Lifecycles)#
- IndexedDB Interaccional Avanzada: Dominio total de herramientas en el cliente para bases de datos complejas (
idb,dexie). Capacidad de manejar tablas de relaciones interconectadas en el navegador simulando la DB Real (Tablas: Encuestas, Fotos, Geofences). - Service Workers Customizados: Ya no se depende de librerías tipo
next-pwamágicas. El Ssr domina intercepción de eventosfetch()-> Estrategias de Cache First, "Stale While Revalidate" offline para el render de los assets y Network First a mutaciones, cacheando con precisión de cirujano. - Optimist UI & UI Transitions: Crear interfaces con "Optimistic Updates". Si se enfrían las rutas, el usuario ve como "Terminado verde" su formulario inmediatamente, gestionando el verdadero Sync asíncrono detrás de escena silenciosamente.
Nivel Senior/Arquitecto (Rendimiento Edge y WebWorkers)#
- Compresión Pesada Client-side: Tablets antiguas abren la cámara tomando fotos 4K (4MB-10MB por archivo). Grabar esto en local y dispararlo por satélite colapsa la Red (Netbird MTU o RAM de IndexedDB). El Senior usa Web Workers en threads secundarios para leer el BLOB/Base64 original, trazarlo en un Canvas in-memory, interpolar formato
WebPa (1024x720px) reduciéndolo a 150KB ANTES de guardarlo siquiera en IndexedDB local. - Soluciones Hydration & Local Cache Proxy (CORS/Proxy): A nivel sistema global se lidia agresivamente con Mismatch de Hidratación Next.js por estadios de Cache y domina la integración de proxies inversos integrados GCS (Gateway Cache System) en el Nginx del edge local sin afectar el Frontend.
Escenarios Críticos y Troubleshooting Frontend (Edge Cases)#
1. Service Worker Zombie o Corrupción Catastrófica de Caché
- Síntoma: El sistema despachó a producción la vista v1.2. Pero los Brigadistas, tras conectarse y descargar datos, siguen viendo la UI con bugs de hace 2 semanas (v1.1) porque su navegador se niega asolutar a instalar el nuevo Service Worker que está "Esperando activación".
- Mitigación (Técnica Obligatoria):
- Desarrollar botón "Hard Resync" oculto (7 clicks en el logo). Al gatillarlo invoca limpiar IndexedDB de colisiones mediante JS, invoca APIs de navegador:
caches.keys().then(... => caches.delete(key))e ignora el ciclo vital connavigator.serviceWorker.getRegistrations().then(r => r.forEach(reg => reg.unregister())). Reiniciandowindow.location.reload(true)como panacea final sin pedir al usuario despejar historial manualmente de sus tablets perimetrales.
- Desarrollar botón "Hard Resync" oculto (7 clicks en el logo). Al gatillarlo invoca limpiar IndexedDB de colisiones mediante JS, invoca APIs de navegador:
2. Colapso de Espacio del Almacenamiento (QuotaExceededError)
- Síntoma: Tras 3 semanas en el monte profundo con el Mini-PC del carro bloqueado, las bases del navegaor local en el Ipad acumulan gigas de evidencia. Al darle "Guardar" el sistema lanza un error DOM de límite de almacenamiento superado.
- Mitigación: Aplicar rutinas LRU (Least Recently Used) o "Destructive Offload". Las fotos sincronizadas al backend Edge-local tienen un "TTL" en el Ipad de sólo 24h. Un Cron del frontend en un WebWorker revisa el store persistente del usuario, comparando
sync_status = truey lo purga violentamente asegurando resiliencia del hardware local.