2krika
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 dev

2. Créer une Branche

git checkout dev
git pull origin dev
git checkout -b feature/nom-de-la-feature

3. Développer

Suivez les patterns et conventions du projet.

4. Tester

Testez votre feature localement et vérifiez les types:

pnpm check-types
pnpm lint

5. Commit et Push

git add .
git commit -m "feat: description de la feature"
git push origin feature/nom-de-la-feature

6. 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

src/models/favorite.ts
export interface Favorite {
  id: string;
  userId: string;
  serviceId: string;
  createdAt: string;
}

export interface CreateFavoriteDto {
  serviceId: string;
}

2. Créer le Service API

src/services/favoriteService.ts
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

src/hooks/useFavorites.ts
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

src/components/features/favorites/FavoriteButton.tsx
'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

src/app/[lang]/(client)/favorites/page.tsx
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

src/locales/fr.json
{
  "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"
  }
}
src/locales/en.json
{
  "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

src/app/[lang]/(client)/new-page/page.tsx
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

src/app/[lang]/(client)/items/[id]/page.tsx
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

src/components/features/contact/ContactForm.tsx
'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

src/components/features/upload/FileUpload.tsx
'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

src/components/shared/ErrorBoundary.tsx
'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

src/app/[lang]/error.tsx
'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

src/components/ui/Button.test.tsx
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:

  1. Ouvrir l'onglet Network
  2. Filtrer par XHR/Fetch
  3. 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

Ressources

Prochaines Étapes

On this page