Convenciones y patrones para el desarrollo frontend con React + Mantine
frontend/src/
βββ app/ # ConfiguraciΓ³n global
β βββ providers.tsx # MantineProvider, QueryProvider, etc.
β βββ router.tsx # React Router config
β βββ App.tsx
β
βββ modules/ # MΓ³dulos de negocio
β βββ core/ # Auth, Layout, Config
β β βββ components/
β β βββ hooks/
β β βββ services/
β β βββ stores/
β β
β βββ delivery/ # MΓ³dulo principal
β βββ components/ # Componentes del mΓ³dulo
β βββ hooks/ # Custom hooks (useViajes, etc.)
β βββ services/ # API calls
β βββ stores/ # Zustand stores
β βββ types/ # TypeScript types
β βββ pages/ # PΓ‘ginas/rutas
β
βββ shared/ # Compartido entre mΓ³dulos
β βββ components/ # Componentes genΓ©ricos
β βββ hooks/ # Hooks reutilizables
β βββ utils/ # Utilidades
β
βββ lib/ # Configuraciones
βββ api.ts # Cliente HTTP base
βββ queryClient.ts # TanStack Query config
βββ auth.ts # Better-Auth client
// β
Correcto: Functional component con TypeScript
interface ViajeCardProps {
viaje: Viaje;
onEdit?: (id: string) => void;
}
export function ViajeCard({ viaje, onEdit }: ViajeCardProps) {
return (
<Card>
{/* ... */}
</Card>
);
}
// hooks/useViajes.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { viajesApi } from '../services/viajes.api';
export function useViajes(filters?: ViajesFilters) {
return useQuery({
queryKey: ['viajes', filters],
queryFn: () => viajesApi.getAll(filters),
});
}
export function useCreateViaje() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: viajesApi.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['viajes'] });
},
});
}
// stores/delivery.store.ts
import { create } from 'zustand';
interface DeliveryState {
selectedTurno: 'DIA' | 'NOCHE' | null;
setTurno: (turno: 'DIA' | 'NOCHE') => void;
}
export const useDeliveryStore = create<DeliveryState>((set) => ({
selectedTurno: null,
setTurno: (turno) => set({ selectedTurno: turno }),
}));
// services/viajes.api.ts
import { api } from '@/lib/api';
import type { Viaje, CreateViajeDto } from '../types/viaje.types';
export const viajesApi = {
getAll: (filters?: ViajesFilters) =>
api.get<Viaje[]>('/api/delivery/viajes', { params: filters }),
getById: (id: string) =>
api.get<Viaje>(`/api/delivery/viajes/${id}`),
create: (data: CreateViajeDto) =>
api.post<Viaje>('/api/delivery/viajes', data),
update: (id: string, data: Partial<CreateViajeDto>) =>
api.patch<Viaje>(`/api/delivery/viajes/${id}`, data),
delete: (id: string) =>
api.delete(`/api/delivery/viajes/${id}`),
};
// β Incorrecto
<button className="...">Click</button>
<input type="text" />
// β
Correcto
<Button>Click</Button>
<TextInput label="Nombre" />
import { Table } from '@mantine/core';
<Table striped highlightOnHover>
<Table.Thead>
<Table.Tr>
<Table.Th>Fecha</Table.Th>
<Table.Th>Motoquero</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{viajes.map((viaje) => (
<Table.Tr key={viaje.id}>
<Table.Td>{viaje.fecha}</Table.Td>
<Table.Td>{viaje.motoquero.nombre}</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
import { useDisclosure } from '@mantine/hooks';
import { Modal, Drawer } from '@mantine/core';
function ViajesPage() {
const [opened, { open, close }] = useDisclosure(false);
return (
<>
<Button onClick={open}>Nuevo Viaje</Button>
<Drawer opened={opened} onClose={close} title="Nuevo Viaje">
<ViajeForm onSuccess={close} />
</Drawer>
</>
);
}
Usar React Hook Form + Zod + Mantine:
import { useForm, zodResolver } from '@mantine/form';
import { z } from 'zod';
const viajeSchema = z.object({
motoqueroId: z.string().min(1, 'Seleccione un motoquero'),
direcciones: z.array(z.string()).min(1, 'Ingrese al menos una direcciΓ³n'),
});
function ViajeForm() {
const form = useForm({
initialValues: { motoqueroId: '', direcciones: [''] },
validate: zodResolver(viajeSchema),
});
return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<Select
label="Motoquero"
{...form.getInputProps('motoqueroId')}
/>
{/* ... */}
</form>
);
}
// app/router.tsx
import { createBrowserRouter } from 'react-router-dom';
export const router = createBrowserRouter([
{
path: '/',
element: <MainLayout />,
children: [
{ index: true, element: <DashboardPage /> },
{
path: 'delivery',
children: [
{ path: 'viajes', element: <ViajesPage /> },
{ path: 'motoqueros', element: <MotosPage /> },
{ path: 'liquidaciones', element: <LiquidacionesPage /> },
],
},
{
path: 'config',
children: [
{ path: 'parametros', element: <ParametrosPage /> },
{ path: 'usuarios', element: <UsuariosPage /> },
],
},
],
},
{ path: '/login', element: <LoginPage /> },
]);