home / skills / jpropato / ssg-santalucia / frontend-conventions

frontend-conventions skill

/.agent/skills/frontend-conventions

This skill enforces React + TypeScript frontend conventions, improves consistency, and accelerates development with Mantine, TanStack Query, and Zustand

npx playbooks add skill jpropato/ssg-santalucia --skill frontend-conventions

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
6.0 KB
---
name: frontend-conventions
description: Convenciones y patrones para el desarrollo frontend con React + Mantine
---

# Frontend Conventions

## Stack
- React 19 + TypeScript
- Vite 6.x
- Mantine v7 + TailwindCSS
- TanStack Query v5
- Zustand v5
- React Hook Form + Zod

## Estructura de Carpetas

```
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
```

## Convenciones de Código

### Componentes

```tsx
// ✅ Correcto: Functional component con TypeScript
interface ViajeCardProps {
  viaje: Viaje;
  onEdit?: (id: string) => void;
}

export function ViajeCard({ viaje, onEdit }: ViajeCardProps) {
  return (
    <Card>
      {/* ... */}
    </Card>
  );
}
```

### Hooks de TanStack Query

```tsx
// 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 (Zustand)

```tsx
// 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 }),
}));
```

### API Services

```tsx
// 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}`),
};
```

## Mantine UI Patterns

### Usar componentes de Mantine, no HTML nativo

```tsx
// ❌ Incorrecto
<button className="...">Click</button>
<input type="text" />

// ✅ Correcto
<Button>Click</Button>
<TextInput label="Nombre" />
```

### Tables con @mantine/core

```tsx
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>
```

### Modales y Drawers

```tsx
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>
    </>
  );
}
```

## Formularios

Usar React Hook Form + Zod + Mantine:

```tsx
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>
  );
}
```

## Rutas

```tsx
// 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 /> },
]);
```

Overview

This skill documents frontend conventions and patterns for building React + Mantine applications with TypeScript. It defines a recommended project structure, common stack choices, UI patterns, data fetching hooks, state stores, and form practices. The goal is consistent, maintainable code across modules.

How this skill works

It inspects and prescribes folder layout, component and hook shapes, TanStack Query and Zustand usage, API service patterns, and Mantine UI conventions. It provides concrete code examples for components, queries, mutations, stores, forms (React Hook Form + Zod), routing, and modal/drawer handling. Follow the patterns to ensure predictable data flow and UI consistency.

When to use it

  • Starting a new React + TypeScript project with Mantine and TailwindCSS.
  • Standardizing architecture and conventions across multiple frontend modules.
  • Onboarding engineers to a consistent patterns-driven frontend codebase.
  • Implementing data fetching, caching, and mutation flows with TanStack Query.
  • Designing forms and validation using React Hook Form and Zod.

Best practices

  • Organize code under app, modules, shared, and lib folders for clear separation of concerns.
  • Prefer Mantine components over raw HTML for consistent styling and accessibility.
  • Use TanStack Query hooks with stable queryKeys and queryClient.invalidateQueries on mutations.
  • Keep Zustand stores small and focused with typed interfaces.
  • Create API service objects that wrap HTTP client calls to centralize endpoints and types.
  • Validate forms with Zod and integrate with Mantine-friendly form utilities.

Example use cases

  • Create a delivery module with pages, components, services, hooks, and stores following the conventions.
  • Implement a paginated table of trips using Mantine Table and a useViajes hook backed by TanStack Query.
  • Build a modal/drawer flow to create or edit entities with React Hook Form and Zod validation.
  • Centralize HTTP client and queryClient configuration in lib for consistent request handling.
  • Manage UI state like selected shift (day/night) in a small typed Zustand store.

FAQ

Why prefer Mantine components over native HTML?

Mantine provides consistent styling, accessibility, and built-in behavior that keep UI uniform and reduce custom CSS.

How should mutations refresh data?

Use useMutation with onSuccess calling queryClient.invalidateQueries for the relevant queryKey to keep cached results in sync.