Aller au contenu

Conventions Frontend

Conventions de développement React / TypeScript pour Primatch.


Règles fondamentales

Règle ✅ Correct ❌ Incorrect
Exports export const MyComponent = () => {} export default MyComponent
Imports import { foo } from '@/hooks/useGames' import { foo } from '../../hooks/useGames'
Data fetching const { data } = useGames() useEffect(() => fetch(...), [])
Types interface Game { ... } (pas any) const game: any = ...
Variables unused const _unusedVar = ... Laisser une variable non utilisée

Architecture des composants

Hiérarchie

pages/          → Assemblent les features et layouts (haut niveau)
  └── components/features/   → Logique spécifique à un domaine
        └── components/ui/   → Primitives réutilisables (sans logique métier)

Exemple de composition

// pages/games/GameDetailPage.tsx
export const GameDetailPage = () => {
  const { id } = useParams();
  const { data: game, isLoading } = useGame(Number(id));

  if (isLoading) return <LoadingSpinner />;

  return (
    <PageWrapper title={`Partie — ${game?.date}`}>
      <GameHeader game={game} />       {/* Feature component */}
      <ScoreDisplay score={game?.score} /> {/* Feature component */}
      <PlayersList players={game?.players} /> {/* Feature component */}
    </PageWrapper>
  );
};

Hooks React Query

Tous les appels API passent par des hooks dédiés dans hooks/ :

// hooks/useGames.ts
export const useGames = () => {
  return useQuery({
    queryKey: ['games'],
    queryFn: () => gameService.getAll(),
  });
};

// hooks/useCreateGame.ts
export const useCreateGame = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (dto: CreateGameDTO) => gameService.create(dto),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['games'] });
    },
  });
};

Formulaires (React Hook Form + Zod)

// Schéma Zod d'abord (source de vérité)
const createGameSchema = z.object({
  type: z.enum(['friendly', 'competitive']),
  scheduledAt: z.string().datetime(),
});

type CreateGameForm = z.infer<typeof createGameSchema>;

// Composant
export const CreateGameForm = () => {
  const { register, handleSubmit, formState: { errors } } = useForm<CreateGameForm>({
    resolver: zodResolver(createGameSchema),
  });

  const createGame = useCreateGame();

  return (
    <form onSubmit={handleSubmit((data) => createGame.mutate(data))}>
      {/* fields */}
    </form>
  );
};

Internationalisation

// Toujours utiliser useTranslation pour les textes
import { useTranslation } from 'react-i18next';

export const GameCard = ({ game }: { game: Game }) => {
  const { t } = useTranslation('games');

  return <h2>{t('game.title', { date: game.date })}</h2>;
};

Les fichiers de traduction sont dans src/i18n/locales/{fr,en}/.