Table des matières

Composants et pages

Documentation React

Les composants sont les éléments de base d'un projet React. Chacun représente une partie de l'interface utilisateur, réutilisable et maintenable.

Organisation des composants

Structure recommandée

src/components/
├── app/                    # Composants d'infrastructure
│   ├── Provider/
│   │   └── index.jsx
│   └── Router/
│       ├── index.jsx
│       └── Guards/
│           └── index.jsx
├── pages/                  # Pages de l'application
│   ├── public/             # Pages sans authentification
│   │   ├── LoginPage/
│   │   └── WelcomePage/
│   ├── private/            # Pages authentifiées
│   │   ├── HomePage/
│   │   └── SettingsPage/
│   └── errors/             # Pages d'erreur
│       └── Error404Page/
├── forms/                  # Composants de formulaire
│   ├── LoginForm/
│   └── ItemForm/
├── ui/                     # Composants UI réutilisables
│   ├── Card/
│   ├── Modal/
│   └── Header/
└── layouts/                # Layouts de pages
    ├── MainLayout/
    └── AuthLayout/

Convention de nommage

src/components/ui/Card/
├── index.jsx           # Composant principal
├── Card.module.css     # Styles (optionnel)
└── Card.test.jsx       # Tests (optionnel)

Créer un composant

Composant simple

// src/components/ui/Card/index.jsx

export const Card = ({ children, className = '' }) => {
  return (
    <div className={`bg-white rounded-lg shadow-md p-4 ${className}`}>
      {children}
    </div>
  );
};

Composant avec props

// src/components/ui/Button/index.jsx

export const Button = ({
  children,
  variant = 'primary',
  size = 'md',
  loading = false,
  disabled = false,
  onClick,
  type = 'button',
  className = '',
}) => {
  const variants = {
    primary: 'bg-primary text-white hover:bg-primary-dark',
    secondary: 'bg-secondary text-white hover:bg-secondary-dark',
    outline: 'border-2 border-primary text-primary hover:bg-primary/10',
    ghost: 'text-primary hover:bg-primary/10',
  };

  const sizes = {
    sm: 'px-3 py-1.5 text-sm',
    md: 'px-4 py-2',
    lg: 'px-6 py-3 text-lg',
  };

  return (
    <button
      type={type}
      onClick={onClick}
      disabled={disabled || loading}
      className={`
        inline-flex items-center justify-center
        rounded-md font-medium transition-all
        disabled:opacity-50 disabled:cursor-not-allowed
        ${variants[variant]}
        ${sizes[size]}
        ${className}
      `}
    >
      {loading && (
        <span className="mr-2 animate-spin">⟳</span>
      )}
      {children}
    </button>
  );
};

Composant Input personnalisé

// src/components/form/Input/index.jsx

export const Input = (props) => {
  const { label, id, error, ...inputProps } = props;

  return (
    <div className="flex flex-col gap-2">
      {label && (
        <label
          htmlFor={id}
          className="text-sm font-medium text-gray-700"
        >
          {label}
        </label>
      )}
      <input
        id={id}
        className={`
          bg-gray-100 p-4 rounded-lg
          outline-none focus:ring-2 focus:ring-primary
          ${error ? 'ring-2 ring-red-500' : ''}
        `}
        {...inputProps}
      />
      {error && (
        <span className="text-sm text-red-500">{error}</span>
      )}
    </div>
  );
};

Structure d'une page

Page simple

// src/components/pages/private/HomePage/index.jsx

import { useGlobalStates, useNavigation } from '@cap-rel/smartcommon';

export const HomePage = () => {
  const navigate = useNavigation();
  const [session] = useGlobalStates('session');

  return (
    <div className="min-h-screen bg-gray-100">
      {/* Header */}
      <header className="bg-white shadow p-4">
        <h1 className="text-xl font-bold">Accueil</h1>
      </header>

      {/* Content */}
      <main className="p-4">
        <p>Bienvenue {session?.user?.name}</p>
      </main>

      {/* Navigation */}
      <nav className="fixed bottom-0 left-0 right-0 bg-white shadow-lg">
        <div className="flex justify-around p-2">
          <button onClick={() => navigate('/')}>Accueil</button>
          <button onClick={() => navigate('/settings')}>Paramètres</button>
        </div>
      </nav>
    </div>
  );
};

Page avec chargement de données

// src/components/pages/private/ItemsPage/index.jsx

import { useApi, useGlobalStates, useNavigation } from '@cap-rel/smartcommon';
import { useEffect, useState } from 'react';
import { Card } from '../../ui/Card';

export const ItemsPage = () => {
  const api = useApi();
  const navigate = useNavigation();
  const [items, setItems] = useGlobalStates('items');
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchItems = async () => {
      setLoading(true);
      setError(null);

      const response = await api.private.get('items');

      if (response.success) {
        setItems(response.data);
      } else {
        setError(response.error);
      }

      setLoading(false);
    };

    fetchItems();
  }, []);

  if (loading) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <span className="animate-spin text-4xl">⟳</span>
      </div>
    );
  }

  if (error) {
    return (
      <div className="p-4 text-center text-red-500">
        Erreur: {error}
      </div>
    );
  }

  return (
    <div className="min-h-screen bg-gray-100 p-4">
      <h1 className="text-2xl font-bold mb-4">Mes items</h1>

      <div className="space-y-4">
        {items.map((item) => (
          <Card
            key={item.id}
            onClick={() => navigate(`/items/${item.id}`)}
            className="cursor-pointer hover:shadow-lg transition-shadow"
          >
            <h2 className="font-semibold">{item.label}</h2>
            <p className="text-gray-600 text-sm">{item.description}</p>
          </Card>
        ))}
      </div>
    </div>
  );
};

Utiliser les layouts

Créer un layout

// src/components/layouts/MainLayout/index.jsx

import { useNavigation } from '@cap-rel/smartcommon';

export const MainLayout = ({ children, title, showBack = true }) => {
  const navigate = useNavigation();

  return (
    <div className="min-h-screen bg-gray-100 flex flex-col">
      {/* Header */}
      <header className="bg-white shadow p-4 flex items-center gap-4">
        {showBack && (
          <button onClick={() => navigate(-1)} className="text-primary">
            ← Retour
          </button>
        )}
        <h1 className="text-xl font-bold">{title}</h1>
      </header>

      {/* Content */}
      <main className="flex-1 p-4">
        {children}
      </main>

      {/* Bottom navigation */}
      <nav className="bg-white shadow-lg p-2">
        <div className="flex justify-around">
          <button onClick={() => navigate('/')}>🏠</button>
          <button onClick={() => navigate('/search')}>🔍</button>
          <button onClick={() => navigate('/profile')}>👤</button>
        </div>
      </nav>
    </div>
  );
};

Utiliser un layout

// src/components/pages/private/SettingsPage/index.jsx

import { MainLayout } from '../../layouts/MainLayout';
import { useGlobalStates, useIntl } from '@cap-rel/smartcommon';

export const SettingsPage = () => {
  const { t, lng, setLng } = useIntl();
  const [theme, setTheme] = useGlobalStates('settings.theme');

  return (
    <MainLayout title={t('settings.title')}>
      <div className="space-y-6">
        {/* Langue */}
        <section className="bg-white rounded-lg p-4">
          <h2 className="font-semibold mb-2">{t('settings.language')}</h2>
          <select
            value={lng}
            onChange={(e) => setLng(e.target.value)}
            className="w-full p-2 border rounded"
          >
            <option value="fr">Français</option>
            <option value="en">English</option>
          </select>
        </section>

        {/* Thème */}
        <section className="bg-white rounded-lg p-4">
          <h2 className="font-semibold mb-2">{t('settings.theme')}</h2>
          <div className="flex gap-4">
            <button
              onClick={() => setTheme('light')}
              className={`p-4 rounded ${theme === 'light' ? 'ring-2 ring-primary' : ''}`}
            >
              ☀️ Clair
            </button>
            <button
              onClick={() => setTheme('dark')}
              className={`p-4 rounded ${theme === 'dark' ? 'ring-2 ring-primary' : ''}`}
            >
              🌙 Sombre
            </button>
          </div>
        </section>
      </div>
    </MainLayout>
  );
};

Patterns courants

Formulaire avec validation Zod

import { useForm, useApi, useNavigation } from '@cap-rel/smartcommon';
import { Form, Input, Button } from '@cap-rel/smartcommon';
import { z } from 'zod';

const schema = z.object({
  name: z.string().min(2, "Minimum 2 caractères"),
  email: z.string().email("Email invalide"),
});

const ContactForm = () => {
  const api = useApi();
  const navigate = useNavigation();
  const form = useForm({ schema });

  const handleSubmit = async (data) => {
    const response = await api.private.post('contacts', { json: data });
    if (response.success) {
      navigate('/contacts');
    }
  };

  return (
    <Form form={form} onSubmit={handleSubmit} className="space-y-4">
      <Input name="name" label="Nom" />
      <Input name="email" type="email" label="Email" />
      <Button type="submit" loading={form.formState.isSubmitting}>
        Envoyer
      </Button>
    </Form>
  );
};

Modal/Drawer avec animation

import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';

const Modal = ({ isOpen, onClose, children }) => {
  return (
    <AnimatePresence>
      {isOpen && (
        <>
          {/* Backdrop */}
          <motion.div
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            onClick={onClose}
            className="fixed inset-0 bg-black/50 z-40"
          />

          {/* Content */}
          <motion.div
            initial={{ y: '100%' }}
            animate={{ y: 0 }}
            exit={{ y: '100%' }}
            className="fixed bottom-0 left-0 right-0 bg-white rounded-t-2xl p-4 z-50"
          >
            {children}
          </motion.div>
        </>
      )}
    </AnimatePresence>
  );
};

Liste avec état vide

const ItemsList = ({ items }) => {
  if (items.length === 0) {
    return (
      <div className="text-center py-12 text-gray-500">
        <p className="text-4xl mb-4">📭</p>
        <p>Aucun élément pour le moment</p>
      </div>
    );
  }

  return (
    <div className="space-y-4">
      {items.map(item => (
        <ItemCard key={item.id} item={item} />
      ))}
    </div>
  );
};

Utiliser SmartCommon

Préférez les composants SmartCommon aux composants customs quand c'est possible :

import {
  Form,
  Input,
  Select,
  Checkbox,
  Button,
  Card,
  Modal,
} from '@cap-rel/smartcommon';

Voir SmartCommon pour la liste complète.

Voir aussi