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 SaaSsurface— fondos de tarjetasborder— 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:
No es React; es JS vanilla modular para minimizar tamaño (24KB gzip).
Tareas de mantenimiento de los frontends¶
Actualizar deps minor¶
Actualizar deps major (con cuidado)¶
Lee el migration guide del paquete primero. Vite 5 → 6, React 18 → 19, etc. requieren testing manual.
Auditar vulnerabilidades¶
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) |