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.md antes 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:

  1. Pide archivos a un servidor (HTML, CSS, JS, imágenes, videos)
  2. Interpreta esos archivos y los convierte en lo que ves en pantalla
  3. Ejecuta código JavaScript para hacerlo interactivo
Ctrl+Scroll=Zoom · Arrastrar=Mover

Los 3 lenguajes fundamentales de la web#

LenguajeFunciónAnalogía
HTMLEstructura — qué elementos hay en la páginaLos huesos del cuerpo
CSSApariencia — colores, tamaños, posicionesLa piel y la ropa
JavaScriptComportamiento — qué pasa cuando haces clicLos 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:

Ctrl+Scroll=Zoom · Arrastrar=Mover

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#

ConceptoExplicación
PropsDatos que un componente padre le pasa a un hijo. Son de solo lectura
ChildrenEl 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 peticiones GET a 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#

tsx
function 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:

tsx
function 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:** useEffect se 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#

ClaseEfectoCSS equivalente
flexDisplay flex (layout horizontal/vertical)display: flex
flex-colDirección verticalflex-direction: column
items-centerCentra verticalmentealign-items: center
justify-betweenEspacio entre elementosjustify-content: space-between
p-4Padding de 16px (4 × 4px)padding: 16px
mt-2Margin top de 8pxmargin-top: 8px
text-xlTexto grande (20px)font-size: 1.25rem
font-boldTexto en negritafont-weight: 700
bg-blue-600Fondo azulbackground-color: #2563eb
text-whiteTexto blancocolor: #fff
rounded-lgBordes redondeadosborder-radius: 8px
shadow-mdSombra mediabox-shadow: ...
hiddenOcultodisplay: none
w-fullAncho 100%width: 100%
hover:bg-blue-700Cambia 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:

  1. Envía menos JavaScript al navegador (los Server Components se ejecutan en el servidor)
  2. La primera carga es HTML real, no una pantalla en blanco esperando JS
  3. 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:

Ctrl+Scroll=Zoom · Arrastrar=Mover

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:

TipoSe ejecuta enUsa interactividadCuándo usarlo
Server Component (default)El servidorNo puede usar useState, onClick, etc.Cargar datos, mostrar contenido estático, consultar DB
Client Component ("use client")El navegador del usuarioPuede usar Hooks, eventos, animacionesFormularios, 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 revalidatePath selectivos.

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#

HerramientaCuándo usarlaEjemplo en SoyDigital
Context APIDatos estáticos o que cambian pocoTema oscuro/claro, idioma, datos del usuario autenticado
ZustandEstados dinámicos complejos compartidosProgreso 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:

Ctrl+Scroll=Zoom · Arrastrar=Mover

Configuración clave:

ParámetroValor
Base URLNEXT_PUBLIC_API_URL (ej. http://localhost:7000)
TokenlocalStorage key auth_token, inyectado como Bearer
IndexedDBDB dominicana-db v4, store cachedResponses, max 1000 entradas con LRU eviction
TTL caché24 horas
Reescritura GCSURLs de storage.googleapis.com → proxy local NEXT_PUBLIC_GCS_PROXY_URL

Manejo de errores HTTP:

CódigoComportamiento
401Limpia auth_token, redirige a /
Network Error (offline)Sirve desde IndexedDB si hay caché válido
5xxRetry 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:

Ctrl+Scroll=Zoom · Arrastrar=Mover

Esto se gestiona con SWR o React Query (TanStack Query):

tsx
import { 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#

AppPuertoStackParticularidades
dominicana-front7011Next.js 15 + React 19 + IndexedDB + PlaywrightPWA offline-first, caché agresivo, reescritura GCS
dominicana-admin-front7001Next.js 15 + React 19 + Firebase AuthPanel admin: gestión de usuarios, certificados, overrides
brigades23000Next.js + GraphQL (Apollo) + PrismaMonolito: ferias, inventario, operación territorial. Auth por JWT propio
dominicana-brigades-analitics3001Next.js 15 + React 18 + Chart.jsEmbebido en brigades vía iframe + postMessage para JWT
crm-indotel3000Next.js 16 + Prisma + Twilio SDKCampañas multicanal (SMS, WhatsApp, voz). Auth S2S
dominicana-simulatorHTML/CSS/JS estáticoSimuladores por nivel, servidos desde GCS
docs-brigadeNext.js 14 + React 18 + PrismaPortal de documentación, consume docs-architecture como submódulo

Clase 4.6: Reglas de Negocio que Impactan el Frontend#

ReglaImpacto en UI
Anti-DowngradeNunca muestres un nivel inferior al actual como "disponible para reasignación"
Progresión secuencialLos 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 ManzanasSi finished_a_level === true, desbloquea la exploración libre en el mapa
Triple Click BypassEl temporizador de lectura se anula con triple clic (solo para testing)
Estados educativoslocked (candado gris), active (resaltado), done (check verde)
Mini-curso obligatorioMostrar aviso "Completa un mini-curso" antes de permitir certificar niveles 2-4
Zona horariaSiempre 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érminoSignificado
App RouterSistema de routing de Next.js basado en estructura de carpetas (src/app/)
Client ComponentComponente con "use client" que se ejecuta en el navegador (interactivo)
CSRClient-Side Rendering — la página se genera en el navegador
Custom HookFunción reutilizable que encapsula lógica con Hooks de React
E2EEnd-to-End testing — pruebas que simulan un usuario real
IndexedDBBase de datos del navegador para caché offline
JSX/TSXExtensión de JavaScript/TypeScript que permite escribir HTML en código
LRU EvictionLeast Recently Used — cuando el caché está lleno, borra lo más viejo
Optimistic UpdateMostrar el resultado esperado antes de que el servidor confirme
Prop DrillingPasar datos de padre a hijo a nieto... (antipatrón que Zustand/Context resuelven)
PWAProgressive Web App — app web que funciona como app nativa (offline, instalable)
RevalidationProceso de actualizar datos cacheados en Next.js
Server ComponentComponente que se ejecuta en el servidor (sin interactividad, menos JS enviado)
SSRServer-Side Rendering — la página se genera en el servidor antes de enviarla
SWRStale-While-Revalidate — estrategia de caché que muestra datos viejos mientras actualiza
TailwindFramework CSS utility-first (clases directas en el HTML)
ZustandLibrerí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)#

  1. 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).
  2. 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.
  3. 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)#

  1. 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).
  2. Service Workers Customizados: Ya no se depende de librerías tipo next-pwa mágicas. El Ssr domina intercepción de eventos fetch() -> 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.
  3. 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)#

  1. 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 WebP a (1024x720px) reduciéndolo a 150KB ANTES de guardarlo siquiera en IndexedDB local.
  2. 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 con navigator.serviceWorker.getRegistrations().then(r => r.forEach(reg => reg.unregister())). Reiniciando window.location.reload(true) como panacea final sin pedir al usuario despejar historial manualmente de sus tablets perimetrales.

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 = true y lo purga violentamente asegurando resiliencia del hardware local.