# Estructura de Código Compartido Frontend **Versión:** 1.0.0 **Última Actualización:** 2025-11-28 **Aplica a:** apps/frontend/src/shared/ --- ## Resumen La carpeta `shared/` contiene código reutilizable que no pertenece a ninguna feature específica. Incluye componentes UI base, hooks genéricos, utilidades, tipos comunes y configuración. --- ## Estructura de Carpetas ``` shared/ ├── components/ # Componentes UI reutilizables │ ├── ui/ # Componentes base (Button, Input, Modal...) │ ├── layout/ # Layout components (Header, Sidebar, Footer) │ ├── feedback/ # Loading, Error, Empty states │ └── index.ts ├── hooks/ # Custom hooks genéricos │ ├── useLocalStorage.ts │ ├── useMediaQuery.ts │ ├── useDebounce.ts │ └── index.ts ├── lib/ # Configuración de librerías │ ├── axios.ts # Instancia configurada de axios │ ├── queryClient.ts # React Query client │ └── i18n.ts # Internacionalización ├── types/ # Tipos TypeScript compartidos │ ├── api.types.ts # Tipos de respuestas API │ ├── common.types.ts # Tipos genéricos │ └── index.ts ├── utils/ # Funciones utilitarias │ ├── format.ts # Formateo de fechas, números │ ├── validation.ts # Validadores │ ├── storage.ts # LocalStorage helpers │ └── index.ts ├── constants/ # Constantes globales │ ├── routes.ts # Rutas de la aplicación │ ├── api.ts # Endpoints base │ └── index.ts └── styles/ # Estilos globales ├── globals.css └── variables.css ``` --- ## Componentes UI Base ### components/ui/ ``` components/ui/ ├── Button/ │ ├── Button.tsx │ ├── Button.test.tsx │ └── index.ts ├── Input/ │ ├── Input.tsx │ ├── Input.test.tsx │ └── index.ts ├── Modal/ │ ├── Modal.tsx │ ├── ModalHeader.tsx │ ├── ModalBody.tsx │ ├── ModalFooter.tsx │ └── index.ts ├── Card/ ├── Badge/ ├── Avatar/ ├── Tooltip/ ├── Dropdown/ ├── Tabs/ ├── Table/ ├── Pagination/ └── index.ts ``` ### Ejemplo: Button ```typescript // shared/components/ui/Button/Button.tsx import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/shared/utils/cn'; const buttonVariants = cva( 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2', { variants: { variant: { primary: 'bg-primary text-white hover:bg-primary-dark', secondary: 'bg-secondary text-white hover:bg-secondary-dark', outline: 'border border-gray-300 bg-transparent hover:bg-gray-50', ghost: 'hover:bg-gray-100', danger: 'bg-red-500 text-white hover:bg-red-600', }, size: { sm: 'h-8 px-3 text-sm', md: 'h-10 px-4 text-base', lg: 'h-12 px-6 text-lg', }, }, defaultVariants: { variant: 'primary', size: 'md', }, } ); interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { isLoading?: boolean; } export const Button: React.FC = ({ className, variant, size, isLoading, children, disabled, ...props }) => { return ( ); }; ``` --- ## Componentes de Layout ### components/layout/ ```typescript // shared/components/layout/MainLayout.tsx interface MainLayoutProps { children: React.ReactNode; } export const MainLayout: React.FC = ({ children }) => { return (
{children}
); }; ``` ```typescript // shared/components/layout/Header.tsx import { useAuth } from '@/features/auth'; import { MlCoinsDisplay, RankBadge } from '@/features/gamification'; export const Header: React.FC = () => { const { user } = useAuth(); return (
); }; ``` --- ## Componentes de Feedback ### components/feedback/ ```typescript // shared/components/feedback/LoadingSpinner.tsx interface LoadingSpinnerProps { size?: 'sm' | 'md' | 'lg'; className?: string; } export const LoadingSpinner: React.FC = ({ size = 'md', className, }) => { const sizeClasses = { sm: 'w-4 h-4', md: 'w-8 h-8', lg: 'w-12 h-12', }; return (
); }; // shared/components/feedback/ErrorMessage.tsx interface ErrorMessageProps { error: Error | string; onRetry?: () => void; } export const ErrorMessage: React.FC = ({ error, onRetry }) => { const message = typeof error === 'string' ? error : error.message; return (

{message}

{onRetry && ( )}
); }; // shared/components/feedback/EmptyState.tsx interface EmptyStateProps { title: string; description?: string; action?: React.ReactNode; icon?: React.ReactNode; } export const EmptyState: React.FC = ({ title, description, action, icon, }) => { return (
{icon &&
{icon}
}

{title}

{description &&

{description}

} {action &&
{action}
}
); }; ``` --- ## Hooks Compartidos ### hooks/ ```typescript // shared/hooks/useLocalStorage.ts export function useLocalStorage( key: string, initialValue: T ): [T, (value: T) => void] { const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch { return initialValue; } }); const setValue = (value: T) => { setStoredValue(value); window.localStorage.setItem(key, JSON.stringify(value)); }; return [storedValue, setValue]; } // shared/hooks/useDebounce.ts export function useDebounce(value: T, delay: number): T { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const handler = setTimeout(() => setDebouncedValue(value), delay); return () => clearTimeout(handler); }, [value, delay]); return debouncedValue; } // shared/hooks/useMediaQuery.ts export function useMediaQuery(query: string): boolean { const [matches, setMatches] = useState( () => window.matchMedia(query).matches ); useEffect(() => { const mediaQuery = window.matchMedia(query); const handler = (e: MediaQueryListEvent) => setMatches(e.matches); mediaQuery.addEventListener('change', handler); return () => mediaQuery.removeEventListener('change', handler); }, [query]); return matches; } ``` --- ## Configuración de Librerías ### lib/axios.ts ```typescript import axios from 'axios'; import { getAuthToken, clearAuthToken } from '@/features/auth'; export const api = axios.create({ baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1', headers: { 'Content-Type': 'application/json', }, }); // Request interceptor - añadir token api.interceptors.request.use((config) => { const token = getAuthToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); // Response interceptor - manejar errores api.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { clearAuthToken(); window.location.href = '/login'; } return Promise.reject(error); } ); ``` ### lib/queryClient.ts ```typescript import { QueryClient } from '@tanstack/react-query'; export const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, // 5 minutos gcTime: 10 * 60 * 1000, // 10 minutos retry: 1, refetchOnWindowFocus: false, }, mutations: { retry: 0, }, }, }); ``` --- ## Utilidades ### utils/format.ts ```typescript // Formatear fecha export const formatDate = (date: Date | string, format = 'short'): string => { const d = new Date(date); const options: Intl.DateTimeFormatOptions = format === 'short' ? { day: '2-digit', month: 'short', year: 'numeric' } : { day: '2-digit', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' }; return d.toLocaleDateString('es-MX', options); }; // Formatear número con separadores export const formatNumber = (num: number): string => { return num.toLocaleString('es-MX'); }; // Formatear XP export const formatXp = (xp: number): string => { if (xp >= 1000) { return `${(xp / 1000).toFixed(1)}K XP`; } return `${xp} XP`; }; ``` ### utils/cn.ts (classNames helper) ```typescript import { clsx, type ClassValue } from 'clsx'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } ``` --- ## Tipos Compartidos ### types/api.types.ts ```typescript // Respuesta paginada genérica export interface PaginatedResponse { data: T[]; meta: { total: number; page: number; limit: number; totalPages: number; }; } // Respuesta de error export interface ApiError { statusCode: number; code: string; message: string; errors?: Array<{ field: string; messages: string[]; }>; } // Query params de paginación export interface PaginationParams { page?: number; limit?: number; sortBy?: string; sortOrder?: 'asc' | 'desc'; } ``` --- ## Constantes ### constants/routes.ts ```typescript export const ROUTES = { HOME: '/', LOGIN: '/login', REGISTER: '/register', DASHBOARD: '/dashboard', EXERCISES: '/exercises', EXERCISE: (id: string) => `/exercises/${id}`, ACHIEVEMENTS: '/achievements', LEADERBOARD: '/leaderboard', PROFILE: '/profile', ADMIN: '/admin', TEACHER: '/teacher', } as const; ``` --- ## Buenas Prácticas 1. **Componentes genéricos**: Sin lógica de negocio 2. **Props tipadas**: Interfaces claras para cada componente 3. **Variantes con CVA**: Para componentes con múltiples estilos 4. **Hooks puros**: Sin efectos secundarios de negocio 5. **Utils sin estado**: Funciones puras 6. **Barrel exports**: index.ts en cada carpeta --- ## Ver También - [ESTRUCTURA-FEATURES.md](./ESTRUCTURA-FEATURES.md) - Estructura de features - [COMPONENTES-UI.md](./COMPONENTES-UI.md) - Guía de componentes UI - [STATE-MANAGEMENT.md](./STATE-MANAGEMENT.md) - Gestión de estado