18 KiB
Patrones de Frontend - GAMILIT
Documento: Analisis de Patrones de Frontend React + TypeScript Proyecto de Referencia: GAMILIT Fecha: 2025-11-23 Analista: Architecture-Analyst Agent
1. VISION GENERAL
GAMILIT implementa un frontend moderno con React 18+, Vite 5+ y TypeScript 5+ siguiendo la arquitectura Feature-Sliced Design (FSD) con 180+ componentes reutilizables.
Stack tecnologico:
- Framework: React 18+ con hooks
- Build Tool: Vite 5+ (HMR rapido)
- Lenguaje: TypeScript 5+ (strict mode)
- Styling: Tailwind CSS 3+ (utility-first)
- Router: React Router v6
- State: Zustand (8 stores especializados)
- Forms: React Hook Form + Zod validation
- HTTP: Axios con interceptors
- Animations: Framer Motion
- Testing: Vitest + React Testing Library (13% coverage - GAP)
- Docs: Storybook 7+
Metricas:
- LOC: ~85,000 lineas (65% del proyecto)
- Componentes: 180+ componentes reutilizables
- Paginas: ~50 vistas
- Hooks personalizados: ~30
- Zustand Stores: 8 stores
- Tests: 15 tests (13% coverage - GAP CRITICO)
2. FEATURE-SLICED DESIGN (FSD)
2.1 Arquitectura de Capas
frontend/src/
├── shared/ # Capa compartida (180+ componentes)
│ ├── components/ # UI components genericos
│ ├── hooks/ # Custom React hooks
│ ├── utils/ # Utilidades generales
│ ├── types/ # Tipos TypeScript compartidos
│ └── constants/ # Constantes (sincronizadas con backend)
│
├── features/ # Features de negocio por dominio/rol
│ ├── student/ # Portal estudiante
│ ├── teacher/ # Portal profesor
│ └── admin/ # Portal administrador
│
├── pages/ # Paginas/Vistas (composicion de features)
│ ├── student/
│ ├── teacher/
│ └── admin/
│
├── services/ # Servicios externos
│ ├── api/ # API clients (Axios)
│ └── websocket/ # Socket.IO client
│
└── app/ # Capa de aplicacion
├── providers/ # Context providers
├── layouts/ # Layouts principales
└── router/ # Configuracion de rutas
2.2 Principios de FSD Aplicados
-
Layered Architecture:
shared- Codigo reutilizable sin dependencias de negociofeatures- Logica de negocio por dominiopages- Composicion de featuresapp- Configuracion global
-
Public API: Cada feature expone API publica via
index.ts -
Low Coupling: Features no dependen entre si
-
High Cohesion: Todo relacionado a una feature esta junto
2.3 Aplicabilidad a ERP Generico
⭐⭐⭐⭐ (ALTA)
Decision: ✅ ADOPTAR Y ADAPTAR
Propuesta para ERP:
frontend/src/
├── shared/ # Componentes reutilizables
├── features/
│ ├── administrator/ # Portal administrador
│ ├── accountant/ # Portal contador
│ ├── supervisor/ # Portal supervisor de obra
│ ├── purchaser/ # Portal comprador
│ └── hr/ # Portal RRHH
├── pages/
├── services/
└── app/
3. SHARED COMPONENTS (180+)
3.1 Categorias de Componentes
1. Atomos (Elementos basicos):
shared/components/atoms/
├── Button/
│ ├── Button.tsx
│ ├── Button.stories.tsx
│ ├── Button.test.tsx
│ └── index.ts
├── Input/
├── Label/
├── Badge/
├── Avatar/
└── Icon/
2. Moleculas (Combinaciones de atomos):
shared/components/molecules/
├── FormField/ # Label + Input + Error
├── SearchBar/ # Input + Icon + Button
├── UserCard/ # Avatar + Name + Badge
└── Notification/ # Icon + Title + Message
3. Organismos (Componentes complejos):
shared/components/organisms/
├── Navbar/
├── Sidebar/
├── DataTable/
├── Modal/
├── Dropdown/
└── Pagination/
4. Templates (Layouts):
shared/components/templates/
├── DashboardLayout/
├── AuthLayout/
├── SettingsLayout/
└── EmptyState/
3.2 Patron de Componente
Ejemplo: shared/components/atoms/Button/Button.tsx
import { ButtonHTMLAttributes, ReactNode } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
// Variants con Tailwind CSS
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
{
variants: {
variant: {
primary: 'bg-blue-600 text-white hover:bg-blue-700',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
outline: 'border border-gray-300 hover:bg-gray-100',
ghost: 'hover:bg-gray-100',
danger: 'bg-red-600 text-white hover:bg-red-700',
},
size: {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4',
lg: 'h-12 px-6 text-lg',
},
fullWidth: {
true: 'w-full',
},
},
defaultVariants: {
variant: 'primary',
size: 'md',
},
}
);
export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
children: ReactNode;
isLoading?: boolean;
}
export function Button({
children,
variant,
size,
fullWidth,
isLoading,
disabled,
className,
...props
}: ButtonProps) {
return (
<button
className={buttonVariants({ variant, size, fullWidth, className })}
disabled={disabled || isLoading}
{...props}
>
{isLoading ? <Spinner className="mr-2" /> : null}
{children}
</button>
);
}
3.3 Aplicabilidad a ERP Generico
⭐⭐⭐⭐⭐ (MAXIMA)
Decision: ✅ ADOPTAR COMPLETAMENTE
Componentes criticos para ERP:
- Button, Input, Select, Checkbox (atomos)
- FormField, SearchBar, DateRangePicker (moleculas)
- DataTable, Modal, Sidebar, Navbar (organismos)
- DashboardLayout, ReportLayout (templates)
4. PATH ALIASES FRONTEND
4.1 Configuracion
tsconfig.json:
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": ["./*"],
"@shared/*": ["shared/*"],
"@components/*": ["shared/components/*"],
"@hooks/*": ["shared/hooks/*"],
"@utils/*": ["shared/utils/*"],
"@types/*": ["shared/types/*"],
"@services/*": ["services/*"],
"@app/*": ["app/*"],
"@features/*": ["features/*"],
"@pages/*": ["pages/*"]
}
}
}
vite.config.ts:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@shared': path.resolve(__dirname, './src/shared'),
'@components': path.resolve(__dirname, './src/shared/components'),
'@hooks': path.resolve(__dirname, './src/shared/hooks'),
'@utils': path.resolve(__dirname, './src/shared/utils'),
'@types': path.resolve(__dirname, './src/shared/types'),
'@services': path.resolve(__dirname, './src/services'),
'@app': path.resolve(__dirname, './src/app'),
'@features': path.resolve(__dirname, './src/features'),
'@pages': path.resolve(__dirname, './src/pages'),
},
},
});
4.2 Uso
// ❌ Sin aliases
import { Button } from '../../../shared/components/atoms/Button';
import { useAuth } from '../../../shared/hooks/useAuth';
// ✅ Con aliases
import { Button } from '@components/atoms/Button';
import { useAuth } from '@hooks/useAuth';
4.3 Aplicabilidad a ERP Generico
⭐⭐⭐⭐⭐ (MAXIMA)
Decision: ✅ ADOPTAR COMPLETAMENTE
5. STATE MANAGEMENT CON ZUSTAND
5.1 Los 8 Stores de GAMILIT
stores/
├── useAuthStore.ts # Autenticacion (user, token, logout)
├── useGamificationStore.ts # Gamificacion (XP, coins, logros)
├── useProgressStore.ts # Progreso de aprendizaje
├── useExerciseStore.ts # Estado de ejercicios actuales
├── useNotificationStore.ts # Notificaciones en tiempo real
├── useSocialStore.ts # Features sociales
├── useTenantStore.ts # Multi-tenancy
└── useUIStore.ts # Estado UI (modals, sidebar, theme)
5.2 Patron de Store (ejemplo: useAuthStore)
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface AuthState {
// Estado
user: User | null;
token: string | null;
isAuthenticated: boolean;
// Acciones
login: (email: string, password: string) => Promise<void>;
logout: () => void;
refreshToken: () => Promise<void>;
updateProfile: (data: Partial<User>) => Promise<void>;
}
export const useAuthStore = create<AuthState>()(
persist(
(set, get) => ({
// Estado inicial
user: null,
token: null,
isAuthenticated: false,
// Acciones
login: async (email, password) => {
const response = await authApi.login({ email, password });
set({
user: response.user,
token: response.token,
isAuthenticated: true,
});
},
logout: () => {
set({
user: null,
token: null,
isAuthenticated: false,
});
},
refreshToken: async () => {
const { token } = get();
const response = await authApi.refresh(token);
set({ token: response.token });
},
updateProfile: async (data) => {
const { user } = get();
const updated = await userApi.update(user!.id, data);
set({ user: updated });
},
}),
{
name: 'auth-storage', // Key en localStorage
partialize: (state) => ({
token: state.token,
user: state.user,
}),
}
)
);
5.3 Uso en Componentes
function UserProfile() {
const { user, logout, updateProfile } = useAuthStore();
if (!user) return <LoginPrompt />;
return (
<div>
<h1>Welcome, {user.name}!</h1>
<button onClick={logout}>Logout</button>
</div>
);
}
5.4 Aplicabilidad a ERP Generico
⭐⭐⭐⭐⭐ (MAXIMA)
Decision: ✅ ADOPTAR COMPLETAMENTE
Stores propuestos para ERP:
stores/
├── useAuthStore.ts # Autenticacion
├── useCompanyStore.ts # Empresa actual (multi-tenant)
├── useProjectStore.ts # Proyecto actual
├── useBudgetStore.ts # Presupuesto actual
├── useNotificationStore.ts # Notificaciones
├── useUIStore.ts # Estado UI
└── usePermissionsStore.ts # Permisos del usuario
6. CUSTOM HOOKS (~30)
6.1 Categorias de Hooks
1. Hooks de Estado:
// hooks/useLocalStorage.ts
export function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue] as const;
}
2. Hooks de Fetch:
// hooks/useQuery.ts
export function useQuery<T>(
queryFn: () => Promise<T>,
deps: any[] = []
) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
setLoading(true);
queryFn()
.then(setData)
.catch(setError)
.finally(() => setLoading(false));
}, deps);
return { data, loading, error, refetch: () => queryFn().then(setData) };
}
3. Hooks de Utilidad:
// hooks/useDebounce.ts
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
6.2 Aplicabilidad a ERP Generico
⭐⭐⭐⭐⭐ (MAXIMA)
Decision: ✅ ADOPTAR COMPLETAMENTE
Hooks criticos para ERP:
useQuery- Fetching de datosuseMutation- Operaciones CRUDuseDebounce- Busquedas optimizadasuseLocalStorage- Persistencia localusePermissions- Verificacion de permisos
7. FORMS CON REACT HOOK FORM + ZOD
7.1 Patron de Form
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
// Schema de validacion con Zod
const loginSchema = z.object({
email: z.string().email('Email invalido'),
password: z.string().min(8, 'Minimo 8 caracteres'),
});
type LoginFormData = z.infer<typeof loginSchema>;
export function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
});
const onSubmit = async (data: LoginFormData) => {
await authApi.login(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<FormField
label="Email"
error={errors.email?.message}
{...register('email')}
/>
<FormField
label="Password"
type="password"
error={errors.password?.message}
{...register('password')}
/>
<Button type="submit" isLoading={isSubmitting}>
Login
</Button>
</form>
);
}
7.2 Aplicabilidad a ERP Generico
⭐⭐⭐⭐⭐ (MAXIMA)
Decision: ✅ ADOPTAR COMPLETAMENTE
8. API CLIENTS CON AXIOS
8.1 Configuracion de Axios Instance
// services/api/axios-instance.ts
import axios from 'axios';
import { useAuthStore } from '@stores/useAuthStore';
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
timeout: 10000,
});
// Interceptor de request (agregar token)
api.interceptors.request.use((config) => {
const { token } = useAuthStore.getState();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Interceptor de response (refresh token)
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401) {
const { refreshToken, logout } = useAuthStore.getState();
try {
await refreshToken();
return api.request(error.config);
} catch {
logout();
}
}
return Promise.reject(error);
}
);
export default api;
8.2 API Clients por Modulo
// services/api/auth.api.ts
import api from './axios-instance';
import { API_ENDPOINTS } from '@shared/constants';
export const authApi = {
login: (data: LoginDto) =>
api.post(API_ENDPOINTS.AUTH.LOGIN, data).then((r) => r.data),
register: (data: RegisterDto) =>
api.post(API_ENDPOINTS.AUTH.REGISTER, data).then((r) => r.data),
logout: () =>
api.post(API_ENDPOINTS.AUTH.LOGOUT).then((r) => r.data),
refresh: (token: string) =>
api.post(API_ENDPOINTS.AUTH.REFRESH, { token }).then((r) => r.data),
};
8.3 Aplicabilidad a ERP Generico
⭐⭐⭐⭐⭐ (MAXIMA)
Decision: ✅ ADOPTAR COMPLETAMENTE
9. TESTING PATTERNS (GAP CRITICO)
9.1 Metricas de Testing
| Metrica | Actual | Objetivo | Gap |
|---|---|---|---|
| Tests Frontend | 15 | 60 | -45 (75%) |
| Coverage | 13% | 70% | -57pp |
Critica: Coverage extremadamente bajo. NO copiar este anti-patron.
9.2 Ejemplo de Test
// components/Button/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders with children', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const onClick = vi.fn();
render(<Button onClick={onClick}>Click</Button>);
fireEvent.click(screen.getByText('Click'));
expect(onClick).toHaveBeenCalledTimes(1);
});
it('shows loading state', () => {
render(<Button isLoading>Loading</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});
9.3 Recomendacion para ERP Generico
⭐⭐⭐⭐⭐ (MAXIMA - CRITICO)
Decision: ✅ IMPLEMENTAR TESTING DESDE EL INICIO
Objetivos:
- Coverage: 70%+ desde el inicio
- Tests por componente: Unit tests
- Integration tests: Features completas
- E2E tests: Flujos criticos (Playwright/Cypress)
10. MATRIZ DE DECISION
10.1 ADOPTAR COMPLETAMENTE ✅
| Patron | Prioridad |
|---|---|
| Feature-Sliced Design (FSD) | P0 |
| Shared Components (180+) | P0 |
| Path Aliases | P0 |
| Zustand State Management | P0 |
| Custom Hooks | P0 |
| React Hook Form + Zod | P0 |
| Axios Interceptors | P0 |
| Tailwind CSS | P1 |
| Storybook | P1 |
10.2 MEJORAR 🔧
| Patron | Estado Actual | Mejora | Prioridad |
|---|---|---|---|
| Testing | 13% coverage | 70%+ coverage | P0 CRITICO |
| Type Safety | Parcial | Completa | P1 |
| E2E Tests | Inexistentes | Playwright/Cypress | P1 |
10.3 EVITAR ❌
| Patron | Razon |
|---|---|
| Coverage bajo (13%) | Inaceptable para produccion |
| Sin E2E tests | Flujos criticos sin validar |
11. PROPUESTA PARA ERP GENERICO
frontend/src/
├── shared/
│ ├── components/ # 100+ componentes reutilizables
│ ├── hooks/ # Custom hooks
│ └── constants/ # Constantes (sync con backend)
│
├── features/
│ ├── administrator/ # Portal admin
│ ├── accountant/ # Portal contador
│ ├── supervisor/ # Portal supervisor
│ ├── purchaser/ # Portal compras
│ └── hr/ # Portal RRHH
│
├── pages/
├── services/
│ └── api/
│ ├── budgets.api.ts
│ ├── purchasing.api.ts
│ └── projects.api.ts
│
└── app/
├── providers/
├── layouts/
└── router/
Documento creado: 2025-11-23
Version: 1.0
Estado: Completado
Proximo documento: ssot-system.md