Saltar a contenido

15 — Frontend apps

Hay dos React SPAs independientes + el widget vanilla.

App Ubicación URL en producción Audiencia
Dashboard cliente apps/dashboard/ https://app.tudominio.com/ Compradores del SaaS y usuarios invitados
Superadmin apps/superadmin/ https://app.tudominio.com/superadmin/ (o subdominio) Operador del SaaS
Widget embebible static/v3/ Inyectado en webs de clientes finales Visitantes finales

Stack común:

  • React 18
  • Vite 5
  • TypeScript
  • Tailwind CSS
  • React Router v6
  • Axios
  • i18next + i18next-browser-languagedetector
  • Lucide-react (iconos)
  • @tanstack/react-query (cache de API)

apps/dashboard/

Estructura

apps/dashboard/
├── package.json
├── vite.config.ts
├── tailwind.config.ts
├── tsconfig.json
├── index.html
├── public/
│   └── favicon.ico, etc.
└── src/
    ├── main.tsx                      ← React DOM render
    ├── ClientDashboardApp.tsx        ← Router + providers (Query, Auth, i18n)
    ├── api/
    │   ├── axios-instance.ts         ← Instance con JWT interceptor + auto-refresh
    │   ├── auth.ts                   ← Llamadas a /api/auth/*
    │   ├── bots.ts                   ← Llamadas a /api/dashboard/bots/*
    │   ├── billing.ts                ← Llamadas a /api/dashboard/billing/*
    │   └── ...
    ├── auth/
    │   ├── AuthContext.tsx           ← Provider con user + tokens
    │   ├── ProtectedRoute.tsx        ← Wrapper para rutas autenticadas
    │   └── useAuth.ts                ← Hook
    ├── components/                   ← Compartidos
    │   ├── ui/                       ← Botones, inputs, modal (custom Tailwind)
    │   └── ...
    ├── layouts/
    │   └── DashboardLayout.tsx       ← Sidebar + topbar + outlet
    ├── pages/                        ← Páginas top-level
    │   ├── LoginPage.tsx
    │   ├── SignupPage.tsx
    │   ├── VerifyEmailPage.tsx
    │   ├── ForgotPasswordPage.tsx
    │   ├── OverviewPage.tsx          ← Dashboard home (KPIs)
    │   ├── BotConfigurationPage.tsx  ← Edición de un bot (3 tabs)
    │   ├── BillingPage.tsx
    │   ├── BotUsersPage.tsx          ← Invitados al bot
    │   ├── InstallationPage.tsx      ← Snippet del widget
    │   ├── AccountPage.tsx
    │   └── bots/
    │       ├── BotDetailPage.tsx
    │       └── BotFaqsPage.tsx
    ├── hooks/
    │   ├── usePlanLimits.ts          ← Lee limits del plan + contador uso
    │   ├── useBotStats.ts
    │   └── ...
    ├── i18n/
    │   ├── setup.ts                  ← Init i18next
    │   └── locales/{es,en,fr,de,pt}.json
    ├── lib/
    │   ├── apiBase.ts                ← Resuelve URL del backend (host-aware)
    │   ├── formatters.ts
    │   └── planLimits.ts
    └── types/
        ├── api.ts                    ← Tipos compartidos con el backend
        └── models.ts

Comandos

cd apps/dashboard

npm install                # Instala deps
npm run dev                # Dev server en http://localhost:5173
npm run build              # Build para producción → dist/
npm run preview            # Sirve el build localmente
npm run lint               # ESLint
npm run test               # Vitest (si añades tests)

Variables de entorno (Vite)

Crea apps/dashboard/.env.production:

VITE_API_BASE_URL=https://app.tudominio.com
VITE_UPLOADS_PUBLIC_BASE_URL=https://widget.tudominio.com

VITE_* son las únicas variables que Vite expone al bundle. NO pongas secretos — el bundle es público.

Auth flow en el frontend

// 1. Usuario se loguea
const { access, refresh, user } = await authApi.login(email, password);
localStorage.setItem('blimx_access', access);
localStorage.setItem('blimx_refresh', refresh);
authContext.setUser(user);

// 2. Cada request inyecta Authorization
axios.interceptors.request.use(config => {
  const token = localStorage.getItem('blimx_access');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// 3. En 401, intenta refresh y reintenta
axios.interceptors.response.use(
  res => res,
  async err => {
    if (err.response?.status === 401 && !err.config._retried) {
      const newAccess = await authApi.refreshToken();
      err.config._retried = true;
      err.config.headers.Authorization = `Bearer ${newAccess}`;
      return axios.request(err.config);
    }
    return Promise.reject(err);
  }
);

Routing

// ClientDashboardApp.tsx
<Routes>
  <Route path="/login" element={<LoginPage />} />
  <Route path="/signup" element={<SignupPage />} />
  <Route path="/verify-email" element={<VerifyEmailPage />} />
  <Route path="/forgot-password" element={<ForgotPasswordPage />} />
  <Route element={<ProtectedRoute />}>
    <Route element={<DashboardLayout />}>
      <Route path="/" element={<OverviewPage />} />
      <Route path="/dashboard" element={<OverviewPage />} />
      <Route path="/dashboard/bots/:slug" element={<BotDetailPage />} />
      <Route path="/dashboard/bots/:slug/configuration" element={<BotConfigurationPage />} />
      <Route path="/dashboard/bots/:slug/faqs" element={<BotFaqsPage />} />
      <Route path="/dashboard/bots/:slug/installation" element={<InstallationPage />} />
      <Route path="/dashboard/billing" element={<BillingPage />} />
      <Route path="/dashboard/account" element={<AccountPage />} />
    </Route>
  </Route>
</Routes>

Estilo

Tailwind CSS con tema custom en tailwind.config.ts. Colores principales:

  • primary — color del brand del SaaS
  • surface — fondos de tarjetas
  • border — bordes neutros

No hay storybook (decisión deliberada — el surface es manejable sin él).

apps/superadmin/

Estructura paralela, más pequeña (sin onboarding, sin auth público).

apps/superadmin/src/
├── main.tsx
├── SuperadminApp.tsx
├── modules/superadmin/
│   ├── pages/
│   │   ├── TenantsListPage.tsx
│   │   ├── TenantDetailPage.tsx
│   │   ├── PlansPage.tsx
│   │   ├── UsersPage.tsx
│   │   ├── InvoicesPage.tsx
│   │   ├── AnalyticsPage.tsx
│   │   └── ActivityFeedPage.tsx
│   ├── api/
│   │   └── (axios calls a /api/sa/*)
│   ├── components/
│   └── i18n/locales/

Detalle en 14-superadmin.md.

Widget (static/v3/)

Detalle en 13-widget-embebido.md. Resumen del build:

cd static/v3
npm install
node build.js                 # → dist/blimx-chatbot.min.{js,css} + frame.html

No es React; es JS vanilla modular para minimizar tamaño (24KB gzip).

Tareas de mantenimiento de los frontends

Actualizar deps minor

cd apps/dashboard
npx npm-check-updates -u --target minor
npm install
npm run build && npm run lint

Actualizar deps major (con cuidado)

Lee el migration guide del paquete primero. Vite 5 → 6, React 18 → 19, etc. requieren testing manual.

Auditar vulnerabilidades

npm audit
npm audit fix          # automático para fixes safe

Bundle size

npm run build
# Vite imprime el tamaño de cada chunk al final
# Si algo crece > 500KB, considera lazy-load con React.lazy()

Diferencias entre los 3 frontends

Característica Dashboard Superadmin Widget
Framework React + Vite React + Vite Vanilla JS
Bundle size (gzip) ~250 KB ~180 KB 24 KB
Auth JWT JWT (rol superadmin) Sin auth
Routing React Router React Router Sin router
State React Query + Context React Query + Context StateManager pub/sub
Tests Vitest (opcional) Vitest (opcional) Tests manuales en static/v3/tests/
Hot reload Sí (Vite) Sí (Vite) No (recompila con node build.js)