App Customers
Guide de Développement
Guide pratique pour développer sur l'application Customers
Guide de Développement
Ce guide vous accompagne dans le développement de nouvelles fonctionnalités pour l'application Customers.
Workflow de Développement
1. Configuration de l'Environnement
# Cloner le repo
git clone [repository-url]
cd krika-ui/apps/customers
# Installer les dépendances
pnpm install
# Copier et configurer .env
cp .env.example .env
# Démarrer le serveur
pnpm dev2. Créer une Branche
git checkout dev
git pull origin dev
git checkout -b feature/nom-de-la-feature3. Développer
Suivez les patterns et conventions du projet.
4. Tester
Testez votre feature localement et vérifiez les types:
pnpm check-types
pnpm lint5. Commit et Push
git add .
git commit -m "feat: description de la feature"
git push origin feature/nom-de-la-feature6. Pull Request
Créez une PR vers dev avec une description claire.
Ajouter une Nouvelle Feature
Exemple: Ajouter un Système de Favoris
1. Définir le Modèle
export interface Favorite {
id: string;
userId: string;
serviceId: string;
createdAt: string;
}
export interface CreateFavoriteDto {
serviceId: string;
}2. Créer le Service API
import api from './api';
import { Favorite, CreateFavoriteDto } from '@/models/favorite';
export const favoriteService = {
getAll: async (): Promise<Favorite[]> => {
const { data } = await api.get('/favorites');
return data;
},
add: async (dto: CreateFavoriteDto): Promise<Favorite> => {
const { data } = await api.post('/favorites', dto);
return data;
},
remove: async (serviceId: string): Promise<void> => {
await api.delete(`/favorites/${serviceId}`);
},
isFavorite: async (serviceId: string): Promise<boolean> => {
const { data } = await api.get(`/favorites/check/${serviceId}`);
return data.isFavorite;
},
};3. Créer le Hook
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { favoriteService } from '@/services/favoriteService';
import { CreateFavoriteDto } from '@/models/favorite';
import { toast } from '@mantine/core';
export function useFavorites() {
return useQuery({
queryKey: ['favorites'],
queryFn: favoriteService.getAll,
});
}
export function useIsFavorite(serviceId: string) {
return useQuery({
queryKey: ['favorite', serviceId],
queryFn: () => favoriteService.isFavorite(serviceId),
enabled: !!serviceId,
});
}
export function useAddFavorite() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (dto: CreateFavoriteDto) => favoriteService.add(dto),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['favorites'] });
toast.success('Ajouté aux favoris');
},
onError: () => {
toast.error('Erreur lors de l\'ajout');
},
});
}
export function useRemoveFavorite() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (serviceId: string) => favoriteService.remove(serviceId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['favorites'] });
toast.success('Retiré des favoris');
},
});
}4. Créer le Composant
'use client';
import { ActionIcon } from '@mantine/core';
import { IconHeart, IconHeartFilled } from '@tabler/icons-react';
import { useIsFavorite, useAddFavorite, useRemoveFavorite } from '@/hooks/useFavorites';
interface FavoriteButtonProps {
serviceId: string;
}
export function FavoriteButton({ serviceId }: FavoriteButtonProps) {
const { data: isFavorite, isLoading } = useIsFavorite(serviceId);
const addMutation = useAddFavorite();
const removeMutation = useRemoveFavorite();
const handleClick = () => {
if (isFavorite) {
removeMutation.mutate(serviceId);
} else {
addMutation.mutate({ serviceId });
}
};
return (
<ActionIcon
variant="subtle"
color={isFavorite ? 'red' : 'gray'}
onClick={handleClick}
loading={isLoading || addMutation.isPending || removeMutation.isPending}
>
{isFavorite ? <IconHeartFilled size={20} /> : <IconHeart size={20} />}
</ActionIcon>
);
}5. Créer la Page Favoris
import { Suspense } from 'react';
import { FavoritesList } from '@/components/features/favorites/FavoritesList';
import { LoadingState } from '@/components/shared/LoadingState';
export default function FavoritesPage() {
return (
<div className="container mx-auto py-8">
<h1 className="text-3xl font-bold mb-8">Mes Favoris</h1>
<Suspense fallback={<LoadingState />}>
<FavoritesList />
</Suspense>
</div>
);
}6. Ajouter les Traductions
{
"Favorites": {
"title": "Mes Favoris",
"empty": "Aucun favori pour le moment",
"add": "Ajouter aux favoris",
"remove": "Retirer des favoris",
"added": "Ajouté aux favoris",
"removed": "Retiré des favoris"
}
}{
"Favorites": {
"title": "My Favorites",
"empty": "No favorites yet",
"add": "Add to favorites",
"remove": "Remove from favorites",
"added": "Added to favorites",
"removed": "Removed from favorites"
}
}Travailler avec le Routing
Créer une Nouvelle Page
import { useTranslations } from 'next-intl';
import { Metadata } from 'next';
export async function generateMetadata(): Promise<Metadata> {
return {
title: 'New Page - 2Krika',
description: 'Description de la page',
};
}
export default function NewPage() {
return (
<div>
<h1>New Page</h1>
</div>
);
}Créer une Page Dynamique
interface PageProps {
params: {
lang: string;
id: string;
};
}
export default async function ItemPage({ params }: PageProps) {
const { id } = params;
return (
<div>
<h1>Item {id}</h1>
</div>
);
}Travailler avec les Formulaires
Formulaire avec Mantine Forms
'use client';
import { useForm } from '@mantine/form';
import { TextInput, Textarea, Button } from '@mantine/core';
import { useMutation } from '@tanstack/react-query';
import { contactService } from '@/services/contactService';
import { toast } from '@mantine/core';
export function ContactForm() {
const form = useForm({
initialValues: {
name: '',
email: '',
message: '',
},
validate: {
name: (value) => (value.length < 2 ? 'Nom trop court' : null),
email: (value) => (/^\S+@\S+$/.test(value) ? null : 'Email invalide'),
message: (value) => (value.length < 10 ? 'Message trop court' : null),
},
});
const mutation = useMutation({
mutationFn: contactService.send,
onSuccess: () => {
toast.success('Message envoyé');
form.reset();
},
onError: () => {
toast.error('Erreur lors de l\'envoi');
},
});
return (
<form onSubmit={form.onSubmit((values) => mutation.mutate(values))}>
<TextInput
label="Nom"
placeholder="Votre nom"
{...form.getInputProps('name')}
required
/>
<TextInput
label="Email"
placeholder="votre@email.com"
{...form.getInputProps('email')}
required
/>
<Textarea
label="Message"
placeholder="Votre message"
{...form.getInputProps('message')}
rows={5}
required
/>
<Button type="submit" loading={mutation.isPending}>
Envoyer
</Button>
</form>
);
}Upload de Fichiers
Composant d'Upload
'use client';
import { Dropzone } from '@mantine/dropzone';
import { IconUpload, IconX, IconFile } from '@tabler/icons-react';
import { useState } from 'react';
interface FileUploadProps {
onUpload: (files: File[]) => void;
accept?: string[];
maxSize?: number;
}
export function FileUpload({
onUpload,
accept = ['image/*'],
maxSize = 5 * 1024 * 1024, // 5MB
}: FileUploadProps) {
const [files, setFiles] = useState<File[]>([]);
const handleDrop = (newFiles: File[]) => {
setFiles([...files, ...newFiles]);
onUpload(newFiles);
};
return (
<Dropzone
onDrop={handleDrop}
accept={accept}
maxSize={maxSize}
>
<div className="flex flex-col items-center justify-center p-8">
<IconUpload size={50} />
<p className="mt-4">Glissez vos fichiers ici ou cliquez pour sélectionner</p>
<p className="text-sm text-gray-500 mt-2">
Max {maxSize / 1024 / 1024}MB
</p>
</div>
</Dropzone>
);
}Gestion des Erreurs
Error Boundary
'use client';
import { Component, ReactNode } from 'react';
import { Button } from '@mantine/core';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error?: Error;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return (
<div className="flex flex-col items-center justify-center p-8">
<h2 className="text-2xl font-bold mb-4">Une erreur est survenue</h2>
<p className="text-gray-600 mb-4">{this.state.error?.message}</p>
<Button onClick={() => window.location.reload()}>
Recharger la page
</Button>
</div>
);
}
return this.props.children;
}
}Error Page
'use client';
import { useEffect } from 'react';
import { Button } from '@mantine/core';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="flex flex-col items-center justify-center min-h-screen">
<h2 className="text-2xl font-bold mb-4">Une erreur est survenue</h2>
<Button onClick={reset}>Réessayer</Button>
</div>
);
}Testing
Test d'un Composant
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('is disabled when loading', () => {
render(<Button loading>Click me</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});Performance
Optimiser les Images
import Image from 'next/image';
export function ServiceImage({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={400}
height={300}
className="rounded-lg"
priority={false}
loading="lazy"
/>
);
}Lazy Loading
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
loading: () => <p>Chargement...</p>,
ssr: false,
});Memoization
import { memo } from 'react';
export const ServiceCard = memo(function ServiceCard({ service }) {
return (
<div>
<h3>{service.title}</h3>
<p>{service.price}</p>
</div>
);
});Debugging
Console Logs
console.log('Debug:', data);
console.error('Error:', error);
console.table(users);React DevTools
Utilisez React DevTools pour inspecter:
- Component tree
- Props et state
- Hooks
- Performance
Network Inspector
Dans Chrome DevTools:
- Ouvrir l'onglet Network
- Filtrer par XHR/Fetch
- Inspecter les requêtes API
Bonnes Pratiques
Code Quality
- Écrire du code lisible et maintenable
- Commenter le code complexe
- Utiliser des noms descriptifs
- Respecter les conventions du projet
Performance
- Éviter les re-renders inutiles
- Utiliser React.memo pour les composants lourds
- Lazy load les composants non critiques
- Optimiser les images
Sécurité
- Valider toutes les entrées utilisateur
- Sanitizer les données avant affichage
- Ne jamais exposer de secrets
- Utiliser HTTPS en production
Accessibilité
- Utiliser des balises sémantiques
- Ajouter des alt texts aux images
- Gérer le focus clavier
- Tester avec un screen reader