finances-frontend
Finance Tracker — Frontend
Section titled “Finance Tracker — Frontend”SPA React 19 + Vite 6 para tracker de finanzas personales/familiares. Consume
la API de finances-api: login/signup
(sesión por cookie, Better-Auth), compras/recibos con OCR, ingresos/gastos,
gastos e ingresos fijos/recurrentes, ahorros, deudas, remesas familiares y
sync bancario vía Plaid.
Migración completa desde una versión vanilla JS anterior (task 034, cutover) —
todo src/ es React por dominio, sin código vanilla remanente. Historia
completa de la migración en TASKS.md (tareas #018-034) y docs/adr/.
- React 19 + Vite 6 — dev server, HMR, build
- Vitest 4 + @testing-library/react + @testing-library/jest-dom — tests
- Chart.js (CDN, script en
index.html) — gráficas de tendencia mensual - Plaid Link (CDN, script en
index.html) — conexión bancaria - Sin router (
react-routerno se usa — una sola pantalla, tabs por state interno, ver Arquitectura) - Sin librería de estado externa (Redux/Zustand) — Context API + hooks
- Auth por cookie de sesión httpOnly (Better-Auth del lado de
finances-api),credentials: 'include'en cada fetch
Arquitectura
Section titled “Arquitectura”main.jsx └─ AuthContext (login/signup, sesión) └─ App.jsx ├─ AuthGate.jsx (status === 'anon') └─ Layout.jsx (status === 'authenticated') ├─ AppStateProvider (mes actual, productos, categorías, hero stats) ├─ HeroBanner / SummaryGrid ├─ TabNav └─ tabs por dominio: PurchasesTab, ProductList, TransactionsTab, SavingsTab+DebtsTab, FamilyTab, AnalysisTab, BankTab └─ SettingsModal (montado siempre, controlado por state en Layout)Capas y responsabilidades:
src/api.js— único punto de fetch. Reescribewindow.fetchglobal: intercepta 401 (onUnauthorized), anteponeAPI_BASEa rutas/api/*, fuerzacredentials: 'include'. Ningún componente hacefetchdirecto — siempre víaapiFetch.src/AuthContext.jsx— sesión:status(loading|anon|authenticated),user,login/signup/logout.src/state/AppStateContext.jsx— estado global compartido entre tabs (mes actual, productos, miembros de familia, categorías de transacciones, datos de sync Plaid, hero stats). Un hookuse*por slice (useCurrentMonth,useProducts, etc.) — nunca se consume el context crudo fuera de este archivo.src/Layout.jsx— shell de la app autenticada: header, selector de mes,TabNav, monta todos los tabs (ocultos con CSS, nunca desmontados) y elSettingsModal.src/<dominio>/(products/,transactions/,receipts/,savings/,debts/,family-bank/,analysis/,dashboard/) — un tab o feature por carpeta:<Dominio>Tab.jsxcomo entrada,*Row.jsx/*Form.jsx/*List.jsxcomo piezas internas.src/components/— piezas reusables cross-dominio (TabNav, selects,AuthGate,SettingsModal).src/lib/— helpers puros sin estado (findProduct.js).src/analysis.js/src/state.js— funciones puras y constantes compartidas entre varios dominios (trimmedMean, categorías/unidades por defecto).
Integraciones externas:
finances-api— contrato compartido en_shared/contracts/finances/api-contract.yaml(symlink, backend es la fuente de verdad). Auth por cookie de sesión, no JWT enlocalStorage.- Plaid Link (CDN) — sync bancario,
family-bank/BankTab.jsx. - Chart.js (CDN) — gráficas de tendencia,
analysis/AnalysisTab.jsx.
Decisiones de arquitectura relevantes:
- Sin router: todo vive en un solo
Layout, tabs controlados por state local (activeTab), no por URL. - Migración incremental dominio por dominio (#018-034), no big-bang rewrite —
vanilla y React convivieron en
src/durante la transición, hasta el cutover final (#034).
Ver .claude/ARCHITECTURE.md para el detalle completo (misma fuente que esta
sección) y docs/adr/ para el historial de decisiones.
Convenciones de código
Section titled “Convenciones de código”Resumen — ver .claude/CONVENTIONS.md para el detalle completo:
- Fetch — nunca
fetchdirecto en un componente, siempreapiFetchdesrc/api.js. - Estado global vs local — estado compartido entre tabs vive en
AppStateContext.jsx(un hook por slice); estado propio de un tab/form se queda local. - Estructura por dominio — carpeta
src/<dominio>/con<Dominio>Tab.jsxcomo entry point; componentes cross-dominio encomponents/, helpers puros enlib/. - Fetch on activate, no on mount — los tabs disparan su fetch inicial
cuando la prop
activepasa atrue, no en cada montaje (todos los tabs se montan juntos enLayouty se ocultan con CSS). - Modales controlados desde el padre — visibilidad por prop
open+ callbackonClose, el padre dueño del state. - Confirmación en acciones destructivas —
window.confirm()antes del fetch, sin modal custom. - Tests — Vitest + Testing Library,
*.test.js/*.test.jsxjunto al archivo que testea, no en carpeta__tests__/separada.
Requisitos
Section titled “Requisitos”- Node.js 18+
finances-apicorriendo (por defecto se asume enlocalhost:3001)
Variables de entorno
Section titled “Variables de entorno”Copiar .env.example a .env y ajustar si hace falta:
cp .env.example .envVITE_API_BASE_URL— URL base del backend (opcional). Si no se define, enlocalhostusahttp://localhost:3001, y en cualquier otro host usahttp://<mismo-host>:3001(heurística de fallback pensada para desarrollo). Para producción con el backend en un dominio propio, definir esta variable explícitamente (ej.https://api.midominio.com).
Desarrollo
Section titled “Desarrollo”npm installnpm run devLevanta el dev server de Vite (por defecto en http://localhost:5173).
Build / preview
Section titled “Build / preview”npm run buildnpm run previewTesting
Section titled “Testing”npm testCorre la suite con Vitest + jsdom + React Testing Library. Cubre funciones
puras (trimmedMean, resolución de API_BASE, apiFetch), componentes
compartidos (CategorySelect/UnitSelect/ProductTypeSelect, findProduct)
y componentes de dominio con lógica de negocio propia (ej. DebtsTab: carga,
validación, settle, delete con confirm). No persigue cobertura exhaustiva de
render de cada tab — cada task en TASKS.md documenta qué se verificó al
portar ese dominio.
Estructura del proyecto
Section titled “Estructura del proyecto”index.html— entry point único, monta<div id="root">víasrc/main.jsxpublic/styles.css— estilos, servido como estáticosrc/main.jsx— entry:<App/>envuelto enStrictMode > AuthProvider > AppStateProvidersrc/App.jsx— switch segúnuseAuth().status(loading/anon/autenticado)src/AuthContext.jsx— sesión (login/signup/logout, cookie httpOnly)src/state/AppStateContext.jsx— estado global compartido entre tabs (mes actual, productos, categorías, hero stats), un hookuse*por slicesrc/Layout.jsx— shell autenticado: header, selector de mes,TabNav, monta todos los tabs (ocultos con CSS) ySettingsModalsrc/api.js—API_BASE,apiFetch(inyectacredentials: 'include', maneja 401)src/analysis.js/src/state.js— funciones puras y constantes compartidas entre varios dominios (trimmedMean, categorías/unidades por defecto)src/products/,src/transactions/,src/receipts/,src/savings/,src/debts/,src/family-bank/,src/analysis/,src/dashboard/— un dominio por carpeta,<Dominio>Tab.jsxcomo entradasrc/components/— piezas reusables cross-dominio (TabNav, selects,AuthGate,SettingsModal)src/lib/— helpers puros sin estadovite.config.js— config de dev server, build y tests (Vitest)
Ver .claude/ARCHITECTURE.md para más detalle de arquitectura,
.claude/CONVENTIONS.md para el detalle completo de convenciones, y
docs/adr/001-import-baseline.md / docs/adr/005-* (migración React) para
el historial de decisiones.
Deploy — Cloudflare Pages
Section titled “Deploy — Cloudflare Pages”Sitio estático (npm run build genera dist/), sin SPA-router client-side
(tabs por state en memoria, no por URL) — no hace falta _redirects ni config
de fallback.
Setup vía dashboard de Cloudflare Pages, conectado al repo
Ledder-Dev/finances-frontend:
- Build command:
npm run build - Build output directory:
dist - Variables de entorno (Settings → Environment variables, en Production
y/o Preview según haga falta):
VITE_API_BASE_URLapuntando al dominio definances-apien producción (ej.https://api.midominio.com). - Cada push a la branch conectada dispara un build/deploy automático.
El backend (finances-api) se despliega por separado (ver su propio README).