# ============================================================================
# [SCIENCE] REACT - GUIDE ULTRA-DÉTAILLÉ POUR ÉTUDIANTS EN GÉNIE LOGICIEL
# ============================================================================
#
# [OBJECTIF] GUIDE COMPLET POUR MAÎTRISER REACT DE ZÉRO À EXPERT
#
# PARTIE 1 : FONDAMENTAUX (react_partie1.txt)
#   - Chapitre 0 : Introduction à React
#   - Chapitre 1 : Premiers Composants
#   - Chapitre 2 : JSX en Profondeur
#   - Chapitre 3 : Props - Communication entre Composants
#   - Chapitre 4 : State et Événements
#
# PARTIE 2 : HOOKS ESSENTIELS (react_partie2.txt)
#   - Chapitre 5 : useState - Gérer l'État Local
#   - Chapitre 6 : useEffect - Effets de Bord
#   - Chapitre 7 : useContext - État Global
#   - Chapitre 8 : useRef, useMemo, useCallback
#
# PARTIE 3 : FONCTIONNALITÉS AVANCÉES (react_partie3.txt)
#   - Chapitre 9  : React Router - Navigation
#   - Chapitre 10 : Formulaires Contrôlés
#   - Chapitre 11 : Appels API (fetch / axios)
#   - Chapitre 12 : Custom Hooks
#   - Chapitre 13 : Context API & Gestion d'État
#
# PARTIE 4 : PRODUCTION ET OPTIMISATION (react_partie4.txt)
#   - Chapitre 14 : Performance (memo, useMemo, useCallback)
#   - Chapitre 15 : Testing avec React Testing Library
#   - Chapitre 16 : Best Practices & Architecture
#   - Chapitre 17 : Déploiement Production
#
# [TEMPS] TEMPS DE LECTURE TOTAL : ~25-30 heures
# [DOCS] PRÉREQUIS : HTML, CSS, JavaScript de base (fonctions, tableaux, objets)
#
# ============================================================================

# ============================================================================
# [GUIDE] CHAPITRE 0 : INTRODUCTION À REACT
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Ce qu'est React et pourquoi l'utiliser
[OK] Le concept de composants et de Virtual DOM
[OK] Différence entre React, ReactDOM et React Native
[OK] Installer et créer un projet React (Vite)
[OK] Comprendre la structure d'un projet React
"""

# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QUE REACT ?
# ----------------------------------------------------------------------------

"""
DÉFINITION SIMPLE

React est une BIBLIOTHÈQUE JavaScript pour construire des interfaces
utilisateur (UI) à partir de composants réutilisables.

[IDEE] BIBLIOTHÈQUE (pas Framework) :
- React fait UNE chose : construire des UIs
- Vous choisissez le reste (routing, état global, etc.)
- Contrairement à Angular (framework complet)

CRÉÉ PAR : Facebook / Meta (2013)
UTILISÉ PAR : Meta, Airbnb, Netflix, Twitter, Shopify...


ANALOGIE SIMPLE [MODULE]

Une interface React = Un puzzle

Chaque pièce de puzzle = Un COMPOSANT
  - Réutilisable (même pièce, plusieurs endroits)
  - Indépendant (fonctionne seul)
  - Combinable (s'assemble avec d'autres)

Exemple : Un site e-commerce
┌─────────────────────────────────────────┐
│  <Header />                             │
├───────────────────────────┬─────────────┤
│  <ProductList />          │ <Cart />    │
│  ┌─────────┐ ┌─────────┐  │ <CartItem/> │
│  │<Product>│ │<Product>│  │ <CartItem/> │
│  │  Card   │ │  Card   │  │ <Total/>    │
│  └─────────┘ └─────────┘  │             │
├───────────────────────────┴─────────────┤
│  <Footer />                             │
└─────────────────────────────────────────┘

Chaque <Composant /> est une pièce indépendante !
"""

# ----------------------------------------------------------------------------
# [RECHERCHE] PROBLÈMES QUE REACT RÉSOUT
# ----------------------------------------------------------------------------

"""
SANS REACT : JavaScript Vanilla

// Page avec compteur - JavaScript classique
let count = 0;

function increment() {
  count++;
  // Mettre à jour MANUELLEMENT le DOM
  document.getElementById('counter').textContent = count;
  // Et aussi mettre à jour tout ce qui dépend de count...
  document.getElementById('status').textContent = count > 5 ? 'Haut' : 'Bas';
  document.getElementById('btn').disabled = count >= 10;
  // Et aussi recalculer si count est pair...
  // Et aussi changer la couleur...
  // Facile d'oublier quelque chose ! [!]
}

[X] PROBLÈMES :
1. Manipulation DOM manuelle -> Code fragile et répétitif
2. Synchronisation difficile -> État et UI désynchronisés
3. Code spaghetti -> Difficile à maintenir
4. Pas de réutilisabilité -> Copier-coller partout


AVEC REACT : Déclaratif

// La même chose en React
function Counter() {
  const [count, setCount] = useState(0);  // État

  return (  // Description de l'UI
    <div>
      <p>{count}</p>
      <p>{count > 5 ? 'Haut' : 'Bas'}</p>
      <button disabled={count >= 10} onClick={() => setCount(count + 1)}>
        +1
      </button>
    </div>
  );
}

[OK] SOLUTIONS :
1. Déclaratif -> "À quoi ça ressemble" pas "comment le faire"
2. Automatique -> React synchronise UI et état
3. Composants -> Code organisé et réutilisable
4. Virtual DOM -> Performance optimisée
"""

# ----------------------------------------------------------------------------
# [OUTIL] LE VIRTUAL DOM
# ----------------------------------------------------------------------------

"""
QU'EST-CE QUE LE VIRTUAL DOM ?

VRAI DOM (Browser DOM) :
- Arbre de nœuds HTML dans le navigateur
- Modifier le DOM = Lent (recalcul layout, repaint...)
- Problème : Modifier tout à chaque changement

VIRTUAL DOM (React) :
- Copie légère du DOM en mémoire (objet JavaScript)
- Rapide à créer et comparer
- React compare avant/après (algorithme de "diffing")
- Met à jour SEULEMENT ce qui a changé dans le vrai DOM


PROCESSUS :

1. Changement d'état -> React crée nouveau Virtual DOM
2. Comparaison (Diffing) : Nouveau VS Ancien Virtual DOM
3. Calcul des différences minimales (Reconciliation)
4. Mise à jour sélective du vrai DOM

Visualisation :

État change (count: 0 -> 1)
        v
Virtual DOM Ancien    Virtual DOM Nouveau
<div>                 <div>
  <p>0</p>     ->->->      <p>1</p>  <- DIFFÉRENCE !
  <button>+</button>    <button>+</button>
</div>                </div>
        v
React met à jour SEULEMENT <p>0</p> -> <p>1</p>
Pas toute la page !


RÉSUMÉ :

Sans Virtual DOM : Modifier tout le DOM à chaque changement -> Lent
Avec Virtual DOM  : Modifier seulement ce qui change -> Rapide
"""

# ----------------------------------------------------------------------------
# [OUTILS] INSTALLATION ET PREMIER PROJET
# ----------------------------------------------------------------------------

"""
PRÉREQUIS :
- Node.js ≥ 18 (nodejs.org)
- npm (installé avec Node.js) ou yarn

VÉRIFIER :
$ node --version    -> v20.x.x
$ npm --version     -> 10.x.x


MÉTHODE 1 : CREATE REACT APP (CRA) - Ancien standard
$ npx create-react-app mon-app
$ cd mon-app
$ npm start

[X] PROBLÈME : Lent, volumineux, non recommandé en 2024


MÉTHODE 2 : VITE [OK] (Recommandé)
"""

# Créer projet avec Vite
$ npm create vite@latest mon-app -- --template react
$ cd mon-app
$ npm install
$ npm run dev

"""
[IDEE] POURQUOI VITE ?
- Démarrage ultra-rapide (< 1 seconde)
- Hot Module Replacement (HMR) instantané
- Build de production optimisé
- Standard moderne en 2024


MÉTHODE 3 : NEXT.JS (Apps full-stack)
$ npx create-next-app@latest mon-app
-> Framework React avec SSR, routing, etc.


STRUCTURE DU PROJET VITE + REACT :
"""

mon-app/
├── node_modules/          # Dépendances (ne pas modifier!)
├── public/                # Fichiers statiques (images, fonts)
│   └── vite.svg
├── src/                   # [OBJECTIF] Votre code
│   ├── assets/            # Images, CSS importés par React
│   │   └── react.svg
│   ├── components/        # [PACKAGE] Vos composants (à créer)
│   ├── App.css            # CSS du composant principal
│   ├── App.jsx            # [ACCUEIL] Composant racine
│   ├── index.css          # CSS global
│   └── main.jsx           # Point d'entrée (ne pas modifier)
├── index.html             # HTML de base (React s'y injecte)
├── package.json           # Dépendances et scripts
└── vite.config.js         # Configuration Vite

"""
FICHIERS CLÉS :

index.html :
<!DOCTYPE html>
<html>
  <head>...</head>
  <body>
    <div id="root"></div>  <- React va s'y injecter !
    <script type="module" src="/src/main.jsx"></script>
  </body>
</html>


src/main.jsx (point d'entrée) :
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './index.css'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />            <- Composant racine
  </React.StrictMode>
)


src/App.jsx (composant racine) :
function App() {
  return (
    <div>
      <h1>Hello React!</h1>
    </div>
  )
}

export default App
"""

# ============================================================================
# [GUIDE] CHAPITRE 1 : PREMIERS COMPOSANTS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Créer un composant fonctionnel
[OK] Exporter et importer des composants
[OK] Comprendre les règles des composants
[OK] Nommer et organiser les composants
[OK] Composer des composants entre eux
"""

# ----------------------------------------------------------------------------
# [BRICK] QU'EST-CE QU'UN COMPOSANT ?
# ----------------------------------------------------------------------------

"""
DÉFINITION :

Un composant React = Une fonction JavaScript qui :
1. Accepte des paramètres (props)
2. Retourne du JSX (HTML-like)
3. Décrit un morceau de l'interface

RÈGLES FONDAMENTALES :
1. Nom commence par MAJUSCULE (Button, pas button)
2. Retourne UN seul élément parent (ou Fragment)
3. Doit être une fonction PURE (pas d'effets de bord au rendu)


EXEMPLE MINIMAL :
"""

// Composant simple
function Greeting() {
  return <h1>Bonjour !</h1>;
}

// Utilisation
function App() {
  return <Greeting />;  // Comme une balise HTML
}

"""
[IDEE] DIFFÉRENCE IMPORTANT

<greeting />   <- Balise HTML inconnue (ignorée)
<Greeting />   <- Composant React (MAJUSCULE obligatoire)
"""

# ----------------------------------------------------------------------------
# [NOTE] ANATOMIE D'UN COMPOSANT
# ----------------------------------------------------------------------------

"""
STRUCTURE COMPLÈTE :
"""

// src/components/UserCard.jsx

import React from 'react';  // Optionnel avec React 17+

// 1. DÉFINITION DU COMPOSANT
function UserCard() {

  // 2. LOGIQUE (variables, calculs, etc.)
  const name = "Alice Martin";
  const role = "Développeuse";
  const isActive = true;

  // 3. RETOUR JSX
  return (
    <div className="user-card">
      <h2>{name}</h2>
      <p>{role}</p>
      {isActive && <span className="badge">En ligne</span>}
    </div>
  );
}

// 4. EXPORT (obligatoire pour importer ailleurs)
export default UserCard;

"""
UTILISATION DANS App.jsx :
"""

// src/App.jsx
import UserCard from './components/UserCard';  // Import

function App() {
  return (
    <div>
      <h1>Mon Application</h1>
      <UserCard />   {/* Utiliser comme balise */}
      <UserCard />   {/* Réutilisable ! */}
      <UserCard />   {/* Encore une fois ! */}
    </div>
  );
}

export default App;

# ----------------------------------------------------------------------------
# [DOSSIER] ORGANISATION DES FICHIERS
# ----------------------------------------------------------------------------

"""
CONVENTION DE NOMMAGE :

Fichier         : UserCard.jsx  (PascalCase)
Composant       : function UserCard() {}
Import          : import UserCard from './UserCard'


STRUCTURE RECOMMANDÉE :
"""

src/
├── components/           # Composants réutilisables
│   ├── Button/
│   │   ├── Button.jsx
│   │   ├── Button.css
│   │   └── index.js     # Re-export pour import propre
│   ├── UserCard.jsx
│   └── Navbar.jsx
├── pages/                # Pages entières
│   ├── Home.jsx
│   ├── About.jsx
│   └── Contact.jsx
├── hooks/                # Custom hooks
│   └── useAuth.js
├── context/              # Context providers
│   └── AuthContext.jsx
├── utils/                # Fonctions utilitaires
│   └── helpers.js
├── App.jsx
└── main.jsx

"""
EXPORTS :

1. Export par défaut (une seule par fichier) :
   export default Button;
   import Button from './Button';  <- Nom libre

2. Export nommé (plusieurs par fichier) :
   export function Button() {}
   export function Icon() {}
   import { Button, Icon } from './components';  <- Nom exact

3. Re-export (index.js) :
   // components/index.js
   export { default as Button } from './Button/Button';
   export { default as Navbar } from './Navbar/Navbar';

   // Utilisation propre
   import { Button, Navbar } from './components';
"""

# ----------------------------------------------------------------------------
# [LIEN] COMPOSITION DE COMPOSANTS
# ----------------------------------------------------------------------------

"""
CONCEPT CLEF : Les composants peuvent contenir d'autres composants
"""

// Composants "atomiques" (petits, réutilisables)
function Avatar({ src, alt }) {
  return <img src={src} alt={alt} className="avatar" />;
}

function Badge({ children, color = "blue" }) {
  return (
    <span className={`badge badge-${color}`}>
      {children}
    </span>
  );
}

// Composant "moléculaire" (combine les atomes)
function UserInfo({ user }) {
  return (
    <div className="user-info">
      <Avatar src={user.photo} alt={user.name} />
      <div>
        <h3>{user.name}</h3>
        <Badge color={user.isAdmin ? "red" : "blue"}>
          {user.role}
        </Badge>
      </div>
    </div>
  );
}

// Composant "organisme" (combine molécules)
function UserList({ users }) {
  return (
    <ul className="user-list">
      {users.map(user => (
        <li key={user.id}>
          <UserInfo user={user} />
        </li>
      ))}
    </ul>
  );
}

"""
[IDEE] DESIGN ATOMIQUE

Atomes       -> Bouton, Input, Badge, Avatar
Molécules    -> UserCard (Avatar + Nom + Badge)
Organismes   -> UserList (liste de UserCard)
Templates    -> Layout de page
Pages        -> Contenu réel de la page

Cette hiérarchie aide à organiser le code !
"""

# ----------------------------------------------------------------------------
# [ATTENTION] RÈGLES IMPORTANTES
# ----------------------------------------------------------------------------

"""
RÈGLE 1 : Un seul élément parent
"""

// [X] ERREUR : Deux éléments frères sans parent
function Bad() {
  return (
    <h1>Titre</h1>
    <p>Paragraphe</p>   // Erreur JSX !
  );
}

// [OK] BON : Un élément parent
function Good1() {
  return (
    <div>
      <h1>Titre</h1>
      <p>Paragraphe</p>
    </div>
  );
}

// [OK] MIEUX : Fragment (pas de div inutile dans le DOM)
function Good2() {
  return (
    <>
      <h1>Titre</h1>
      <p>Paragraphe</p>
    </>
  );
}

// [OK] AUSSI : Fragment explicite (utile avec key)
function Good3() {
  return (
    <React.Fragment>
      <h1>Titre</h1>
      <p>Paragraphe</p>
    </React.Fragment>
  );
}

"""
RÈGLE 2 : Les composants sont PURS au rendu
"""

// [X] MAUVAIS : Effet de bord pendant le rendu
let count = 0;

function Impure() {
  count++;  // Modification externe pendant le rendu !
  return <p>{count}</p>;
}

// [OK] BON : Calculs locaux seulement
function Pure({ value }) {
  const doubled = value * 2;  // Juste un calcul
  return <p>{doubled}</p>;
}

"""
RÈGLE 3 : Ne pas modifier les props reçues
"""

// [X] MAUVAIS : Modifier les props
function BadComponent({ user }) {
  user.name = "Modifié !";  // Ne jamais faire ça !
  return <p>{user.name}</p>;
}

// [OK] BON : Créer une copie
function GoodComponent({ user }) {
  const displayName = user.name.toUpperCase();  // Nouvelle variable
  return <p>{displayName}</p>;
}

# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 1 : CARTE DE PROFIL
# ----------------------------------------------------------------------------

"""
OBJECTIF : Créer une carte de profil composée de plusieurs composants

CAHIER DES CHARGES :
1. Composant <Avatar /> - Affiche une image ronde
2. Composant <UserBadge /> - Affiche un badge coloré (admin/user/moderator)
3. Composant <StatItem /> - Affiche une statistique (label + valeur)
4. Composant <ProfileCard /> - Assemble tout ça

Structure visuelle attendue :
┌──────────────────────────────┐
│  [CAMERA] [Photo]                  │
│  Alice Martin    [Admin]     │
│  [EMAIL] alice@example.com        │
│                              │
│  Posts: 142  |  Followers: 891 │
└──────────────────────────────┘

DONNÉES :
const user = {
  name: "Alice Martin",
  email: "alice@example.com",
  role: "admin",
  photo: "https://i.pravatar.cc/80",
  stats: { posts: 142, followers: 891 }
}


CORRIGÉ :
"""

// src/components/Avatar.jsx
function Avatar({ src, name, size = 80 }) {
  return (
    <img
      src={src}
      alt={`Photo de ${name}`}
      style={{
        width: size,
        height: size,
        borderRadius: '50%',
        objectFit: 'cover',
        border: '3px solid #e2e8f0'
      }}
    />
  );
}

// src/components/UserBadge.jsx
function UserBadge({ role }) {
  const colors = {
    admin:     { background: '#FEE2E2', color: '#DC2626' },
    moderator: { background: '#FEF9C3', color: '#CA8A04' },
    user:      { background: '#DBEAFE', color: '#2563EB' },
  };

  const style = colors[role] || colors.user;

  return (
    <span style={{
      ...style,
      padding: '2px 10px',
      borderRadius: '9999px',
      fontSize: '12px',
      fontWeight: 'bold',
      textTransform: 'capitalize'
    }}>
      {role}
    </span>
  );
}

// src/components/StatItem.jsx
function StatItem({ label, value }) {
  return (
    <div style={{ textAlign: 'center', padding: '0 16px' }}>
      <div style={{ fontSize: '20px', fontWeight: 'bold', color: '#1e293b' }}>
        {value.toLocaleString()}
      </div>
      <div style={{ fontSize: '12px', color: '#64748b' }}>
        {label}
      </div>
    </div>
  );
}

// src/components/ProfileCard.jsx
import Avatar from './Avatar';
import UserBadge from './UserBadge';
import StatItem from './StatItem';

function ProfileCard({ user }) {
  return (
    <div style={{
      background: 'white',
      borderRadius: '12px',
      boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
      padding: '24px',
      maxWidth: '320px',
      fontFamily: 'sans-serif'
    }}>
      {/* En-tête */}
      <div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
        <Avatar src={user.photo} name={user.name} />
        <div>
          <h2 style={{ margin: 0, fontSize: '18px' }}>{user.name}</h2>
          <UserBadge role={user.role} />
        </div>
      </div>

      {/* Email */}
      <p style={{ color: '#64748b', marginTop: '12px' }}>
        [EMAIL] {user.email}
      </p>

      {/* Stats */}
      <div style={{
        display: 'flex',
        justifyContent: 'center',
        borderTop: '1px solid #e2e8f0',
        paddingTop: '16px',
        marginTop: '16px'
      }}>
        <StatItem label="Posts" value={user.stats.posts} />
        <div style={{ borderLeft: '1px solid #e2e8f0' }} />
        <StatItem label="Followers" value={user.stats.followers} />
      </div>
    </div>
  );
}

export default ProfileCard;

// src/App.jsx - Utilisation
import ProfileCard from './components/ProfileCard';

const user = {
  name: "Alice Martin",
  email: "alice@example.com",
  role: "admin",
  photo: "https://i.pravatar.cc/80",
  stats: { posts: 142, followers: 891 }
};

function App() {
  return (
    <div style={{ padding: '40px', background: '#f8fafc', minHeight: '100vh' }}>
      <ProfileCard user={user} />
    </div>
  );
}

"""
[OK] CE QUE VOUS AVEZ PRATIQUÉ :
- Créer plusieurs composants
- Les composer ensemble
- Passer des props simples
- Styles inline
- Déstructuration des props
"""

# ============================================================================
# [GUIDE] CHAPITRE 2 : JSX EN PROFONDEUR
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Ce qu'est JSX et comment il fonctionne
[OK] Différences JSX vs HTML
[OK] Insérer des expressions JavaScript dans JSX
[OK] Rendu conditionnel
[OK] Rendu de listes avec .map()
[OK] Gestion du style en React
"""

# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QUE JSX ?
# ----------------------------------------------------------------------------

"""
JSX = JavaScript XML

JSX = Syntaxe qui ressemble à du HTML dans du JavaScript
JSX N'EST PAS du HTML -> Compilé par Babel en appels React

CE QUE VOUS ÉCRIVEZ :
"""

const element = <h1 className="title">Bonjour {name} !</h1>;

"""
CE QUE BABEL GÉNÈRE :
"""

const element = React.createElement(
  'h1',
  { className: 'title' },
  'Bonjour ',
  name,
  ' !'
);

"""
[IDEE] POURQUOI JSX ?

Sans JSX -> Code illisible :
React.createElement('div', {className: 'container'},
  React.createElement('h1', null, 'Titre'),
  React.createElement('p', null, 'Contenu')
)

Avec JSX -> Code lisible :
<div className="container">
  <h1>Titre</h1>
  <p>Contenu</p>
</div>
"""

# ----------------------------------------------------------------------------
# [ATTENTION] DIFFÉRENCES JSX VS HTML
# ----------------------------------------------------------------------------

"""
1. ATTRIBUTS EN camelCase
"""

// HTML
<div class="box" onclick="handle()" tabindex="0">
<label for="input">

// JSX
<div className="box" onClick={handle} tabIndex={0}>
<label htmlFor="input">

"""
[IDEE] POURQUOI camelCase ?
JSX utilise des propriétés JavaScript (pas des attributs HTML)
Les propriétés JS sont en camelCase


2. BALISES AUTO-FERMANTES OBLIGATOIRES
"""

// HTML : OK sans fermeture
<br>
<img src="photo.jpg">
<input type="text">

// JSX : Fermeture OBLIGATOIRE
<br />
<img src="photo.jpg" />
<input type="text" />

"""
3. EXPRESSIONS ENTRE ACCOLADES {}
"""

const name = "Alice";
const age = 25;
const items = ['[ITEM]', '[BANANA]', '[TANGERINE]'];

// HTML (statique)
<p>Bonjour name !</p>    // Affiche "name" en texte

// JSX (dynamique)
<p>Bonjour {name} !</p>  // Affiche "Alice"
<p>Age : {age * 2}</p>   // Affiche "50"
<p>{items[0]}</p>        // Affiche "[ITEM]"

"""
4. COMMENTAIRES EN JSX
"""

function Component() {
  return (
    <div>
      {/* Commentaire JSX (entre accolades) */}
      <p>Contenu</p>
      // Ceci n'est PAS un commentaire JSX
      /* Ceci non plus */
    </div>
  );
}

"""
5. STYLE EN OBJET JavaScript
"""

// HTML
<div style="background: red; font-size: 16px;">

// JSX : style = objet JavaScript
<div style={{ background: 'red', fontSize: '16px' }}>
//           ^ Double accolades :
//             Extérieur { } = expression JSX
//             Intérieur { } = objet JavaScript
//           ^ camelCase : font-size -> fontSize
//           ^ Valeurs entre quotes : 'red', '16px'

# ----------------------------------------------------------------------------
# [MELANGE] EXPRESSIONS JAVASCRIPT DANS JSX
# ----------------------------------------------------------------------------

"""
CE QUE VOUS POUVEZ METTRE DANS {} :
"""

function ExpressionExamples() {
  const name = "Alice";
  const age = 25;
  const items = ['Pomme', 'Banane', 'Orange'];
  const user = { email: "alice@test.com" };

  return (
    <div>
      {/* [OK] Variables */}
      <p>{name}</p>

      {/* [OK] Expressions mathématiques */}
      <p>{age * 2 + 1}</p>

      {/* [OK] Méthodes de string */}
      <p>{name.toUpperCase()}</p>
      <p>{name.length > 5 ? 'Long' : 'Court'}</p>

      {/* [OK] Accès propriétés */}
      <p>{user.email}</p>
      <p>{items[0]}</p>

      {/* [OK] Appels de fonctions */}
      <p>{items.join(', ')}</p>
      <p>{new Date().toLocaleDateString('fr-FR')}</p>

      {/* [OK] Expressions ternaires */}
      <p>{age >= 18 ? 'Majeur' : 'Mineur'}</p>

      {/* [X] PAS de if/for (statements, pas expressions) */}
      {/* {if (true) { return 'oui'; }}  <- ERREUR ! */}
    </div>
  );
}

# ----------------------------------------------------------------------------
# [MELANGE] RENDU CONDITIONNEL
# ----------------------------------------------------------------------------

"""
4 FAÇONS DE FAIRE DU CONDITIONNEL EN JSX

1. OPÉRATEUR TERNAIRE (? :)
   Utiliser quand : Deux choix (afficher A ou B)
"""

function Greeting({ isLoggedIn }) {
  return (
    <div>
      {isLoggedIn ? (
        <h1>Bonjour, utilisateur !</h1>
      ) : (
        <h1>Veuillez vous connecter</h1>
      )}
    </div>
  );
}

"""
2. COURT-CIRCUIT && (AND)
   Utiliser quand : Afficher OU rien
"""

function Notification({ hasMessage, count }) {
  return (
    <div>
      <p>Tableau de bord</p>

      {/* Affiche seulement si hasMessage est true */}
      {hasMessage && <div className="badge">Nouveau message!</div>}

      {/* Affiche count seulement si > 0 */}
      {count > 0 && <span>({count} notifications)</span>}

      {/* [ATTENTION] PIÈGE avec 0 ! */}
      {/* count && <span>...</span>  <- Affiche "0" si count=0 ! */}
      {/* Correction : count > 0 && <span>...</span> */}
    </div>
  );
}

"""
3. VARIABLE CONDITIONNELLE (avant le return)
   Utiliser quand : Logique complexe
"""

function StatusMessage({ status }) {
  // Calculer le contenu avant le return
  let message;
  let color;

  if (status === 'success') {
    message = '[OK] Opération réussie !';
    color = 'green';
  } else if (status === 'error') {
    message = '[X] Une erreur est survenue';
    color = 'red';
  } else if (status === 'loading') {
    message = '[HOURGLASS_WITH_FLOWING_SAND] Chargement...';
    color = 'orange';
  } else {
    message = 'En attente';
    color = 'gray';
  }

  return (
    <div style={{ color }}>
      {message}
    </div>
  );
}

"""
4. RETOUR ANTICIPÉ (Early Return)
   Utiliser quand : Composant entier différent selon condition
"""

function UserProfile({ user }) {
  // Si pas d'utilisateur, afficher message
  if (!user) {
    return <p>Aucun utilisateur trouvé</p>;
  }

  // Si chargement en cours
  if (user.isLoading) {
    return <p>Chargement...</p>;
  }

  // Sinon afficher le profil complet
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

# ----------------------------------------------------------------------------
# [LISTE] RENDU DE LISTES AVEC .map()
# ----------------------------------------------------------------------------

"""
PRINCIPE :
Array de données -> Array de JSX -> React les affiche
"""

const fruits = ['[ITEM] Pomme', '[BANANA] Banane', '[TANGERINE] Orange'];

// [OK] map() transforme données -> JSX
function FruitList() {
  return (
    <ul>
      {fruits.map((fruit, index) => (
        <li key={index}>{fruit}</li>
      ))}
    </ul>
  );
}

"""
[IDEE] LA PROP KEY EST OBLIGATOIRE !

Pourquoi ?
React a besoin d'identifier chaque élément
-> Optimisation des mises à jour (savoir quoi changer)

Règles :
1. Unique parmi les frères (siblings)
2. Stable (ne change pas entre rendus)
3. Préférer les IDs des données (pas index)
"""

// [X] MAUVAIS : Index comme key (si liste réordonnée)
{items.map((item, index) => (
  <Item key={index} data={item} />
))}

// [OK] BON : ID unique et stable
{items.map(item => (
  <Item key={item.id} data={item} />
))}

// [OK] ACCEPTABLE : Index si liste statique (jamais réordonnée)
{['a', 'b', 'c'].map((letter, i) => (
  <span key={i}>{letter}</span>
))}

"""
EXEMPLE COMPLET AVEC OBJETS :
"""

const users = [
  { id: 1, name: 'Alice', role: 'Admin', active: true },
  { id: 2, name: 'Bob', role: 'User', active: false },
  { id: 3, name: 'Charlie', role: 'User', active: true },
];

function UserTable() {
  return (
    <table>
      <thead>
        <tr>
          <th>Nom</th>
          <th>Rôle</th>
          <th>Statut</th>
        </tr>
      </thead>
      <tbody>
        {users.map(user => (
          <tr key={user.id}>
            <td>{user.name}</td>
            <td>{user.role}</td>
            <td>{user.active ? '[VERT] Actif' : '[ROUGE] Inactif'}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

"""
LISTES FILTRÉES :
"""

function ActiveUsers({ users }) {
  const activeUsers = users.filter(u => u.active);

  return (
    <ul>
      {activeUsers.length === 0 ? (
        <li>Aucun utilisateur actif</li>
      ) : (
        activeUsers.map(user => (
          <li key={user.id}>{user.name}</li>
        ))
      )}
    </ul>
  );
}

// Encore plus concis :
function ActiveUsers2({ users }) {
  return (
    <ul>
      {users
        .filter(u => u.active)
        .map(u => <li key={u.id}>{u.name}</li>)
      }
    </ul>
  );
}

# ----------------------------------------------------------------------------
# [DESIGN] GESTION DU STYLE EN REACT
# ----------------------------------------------------------------------------

"""
4 APPROCHES PRINCIPALES


1. STYLE INLINE (objets JavaScript)
   Utiliser quand : Styles dynamiques
"""

function DynamicButton({ isPrimary, size }) {
  const style = {
    padding: size === 'large' ? '12px 24px' : '6px 12px',
    backgroundColor: isPrimary ? '#3b82f6' : '#e5e7eb',
    color: isPrimary ? 'white' : '#374151',
    border: 'none',
    borderRadius: '6px',
    cursor: 'pointer',
    fontSize: '14px',
  };

  return <button style={style}>Cliquer</button>;
}

"""
2. MODULES CSS (.module.css)
   Utiliser quand : Styles scoped par composant (recommandé)
"""

/* Button.module.css */
.button {
  padding: 8px 16px;
  border-radius: 6px;
  border: none;
  cursor: pointer;
}

.primary {
  background-color: #3b82f6;
  color: white;
}

.secondary {
  background-color: #e5e7eb;
  color: #374151;
}

// Button.jsx
import styles from './Button.module.css';

function Button({ variant = 'primary', children }) {
  return (
    <button className={`${styles.button} ${styles[variant]}`}>
      {children}
    </button>
  );
}

"""
[IDEE] CSS Modules = Classes scoped automatiquement
   .button -> .Button_button__aB12c (nom unique généré)
   Pas de conflits entre composants !


3. CSS CLASSIQUE (global)
   Utiliser quand : Styles globaux, reset, variables
"""

/* index.css */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: 'Inter', sans-serif; }
.container { max-width: 1200px; margin: 0 auto; }

// App.jsx
import './index.css';  // Import global

function App() {
  return <div className="container">...</div>;
}

"""
4. TAILWIND CSS (classes utilitaires)
   Utiliser quand : Développement rapide, design system
"""

function TailwindButton({ children }) {
  return (
    <button className="px-4 py-2 bg-blue-500 text-white rounded-md
                        hover:bg-blue-600 transition-colors duration-200
                        font-medium text-sm">
      {children}
    </button>
  );
}

"""
5. BIBLIOTHÈQUES CSS-IN-JS (styled-components, emotion)
   Utiliser quand : Styles très dynamiques
"""

import styled from 'styled-components';

const StyledButton = styled.button`
  padding: 8px 16px;
  background: ${props => props.primary ? '#3b82f6' : '#e5e7eb'};
  color: ${props => props.primary ? 'white' : '#374151'};
  border-radius: 6px;
  border: none;
`;

function App() {
  return <StyledButton primary>Cliquer</StyledButton>;
}

# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 2 : TABLEAU DE PRODUITS
# ----------------------------------------------------------------------------

"""
OBJECTIF : Créer un tableau de produits avec rendu conditionnel et liste

CAHIER DES CHARGES :
1. Afficher une liste de produits en tableau
2. Colorier en rouge les produits en rupture de stock (stock = 0)
3. Afficher un badge "[HOT] Promo" si le produit a une remise > 20%
4. Afficher un message si aucun produit disponible
5. Calculer et afficher le total des produits en stock

DONNÉES :
const products = [
  { id: 1, name: 'Laptop Pro', price: 999, stock: 5, discount: 10 },
  { id: 2, name: 'Souris sans fil', price: 29.99, stock: 0, discount: 25 },
  { id: 3, name: 'Clavier mécanique', price: 149, stock: 12, discount: 5 },
  { id: 4, name: 'Écran 4K', price: 799, stock: 0, discount: 30 },
  { id: 5, name: 'Webcam HD', price: 79, stock: 8, discount: 15 },
];

CORRIGÉ :
"""

// src/components/ProductTable.jsx

const products = [
  { id: 1, name: 'Laptop Pro', price: 999, stock: 5, discount: 10 },
  { id: 2, name: 'Souris sans fil', price: 29.99, stock: 0, discount: 25 },
  { id: 3, name: 'Clavier mécanique', price: 149, stock: 12, discount: 5 },
  { id: 4, name: 'Écran 4K', price: 799, stock: 0, discount: 30 },
  { id: 5, name: 'Webcam HD', price: 79, stock: 8, discount: 15 },
];

function ProductRow({ product }) {
  const isOutOfStock = product.stock === 0;
  const isPromo = product.discount > 20;
  const finalPrice = product.price * (1 - product.discount / 100);

  return (
    <tr style={{
      backgroundColor: isOutOfStock ? '#FEF2F2' : 'white',
      color: isOutOfStock ? '#DC2626' : 'inherit'
    }}>
      <td style={{ padding: '12px', borderBottom: '1px solid #e5e7eb' }}>
        {product.name}
        {isPromo && (
          <span style={{
            marginLeft: '8px',
            background: '#FEF9C3',
            color: '#B45309',
            padding: '2px 6px',
            borderRadius: '4px',
            fontSize: '12px'
          }}>
            [HOT] Promo -{product.discount}%
          </span>
        )}
      </td>
      <td style={{ padding: '12px', borderBottom: '1px solid #e5e7eb' }}>
        <span style={{ textDecoration: isPromo ? 'line-through' : 'none', color: '#9CA3AF' }}>
          {isPromo ? `${product.price}€` : ''}
        </span>
        {' '}{finalPrice.toFixed(2)}€
      </td>
      <td style={{ padding: '12px', borderBottom: '1px solid #e5e7eb', textAlign: 'center' }}>
        {isOutOfStock ? '[X] Rupture' : `[OK] ${product.stock} unités`}
      </td>
    </tr>
  );
}

function ProductTable() {
  const availableProducts = products.filter(p => p.stock > 0);
  const totalStock = availableProducts.reduce((sum, p) => sum + p.stock, 0);

  if (products.length === 0) {
    return <p style={{ textAlign: 'center', color: '#64748b' }}>Aucun produit disponible</p>;
  }

  return (
    <div style={{ fontFamily: 'sans-serif', maxWidth: '700px' }}>
      <h2 style={{ marginBottom: '16px' }}>[PACKAGE] Catalogue Produits</h2>

      <table style={{ width: '100%', borderCollapse: 'collapse', background: 'white', borderRadius: '8px', overflow: 'hidden', boxShadow: '0 1px 3px rgba(0,0,0,0.1)' }}>
        <thead style={{ background: '#F8FAFC' }}>
          <tr>
            <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #e5e7eb' }}>Produit</th>
            <th style={{ padding: '12px', textAlign: 'left', borderBottom: '2px solid #e5e7eb' }}>Prix</th>
            <th style={{ padding: '12px', textAlign: 'center', borderBottom: '2px solid #e5e7eb' }}>Stock</th>
          </tr>
        </thead>
        <tbody>
          {products.map(product => (
            <ProductRow key={product.id} product={product} />
          ))}
        </tbody>
      </table>

      <div style={{
        marginTop: '16px',
        padding: '12px',
        background: '#EFF6FF',
        borderRadius: '8px',
        color: '#1D4ED8'
      }}>
        [GRAPHIQUE] <strong>{availableProducts.length}</strong> produits en stock •
        Total : <strong>{totalStock} unités</strong>
      </div>
    </div>
  );
}

export default ProductTable;

"""
[OK] CE QUE VOUS AVEZ PRATIQUÉ :
- .map() avec composant enfant
- Rendu conditionnel (ternaire, &&)
- Styles dynamiques basés sur données
- .filter() et .reduce() dans JSX
- Déstructuration et calculs
"""

# ============================================================================
# [GUIDE] CHAPITRE 3 : PROPS - COMMUNICATION ENTRE COMPOSANTS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Passer et recevoir des props
[OK] Déstructuration des props
[OK] Props par défaut (defaultProps et paramètre défaut)
[OK] La prop spéciale children
[OK] Propagation de props (spread)
[OK] PropTypes (validation de props)
"""

# ----------------------------------------------------------------------------
# [MESSAGE] QU'EST-CE QUE LES PROPS ?
# ----------------------------------------------------------------------------

"""
DÉFINITION :

Props = Properties = Arguments passés à un composant

Analogie :
Composant = Fonction
Props      = Arguments de la fonction

function welcome(name) {           function Welcome({ name }) {
  return "Bonjour " + name;          return <h1>Bonjour {name}</h1>;
}                                  }
welcome("Alice");                  <Welcome name="Alice" />


FLUX : TOUJOURS PARENT -> ENFANT (unidirectionnel)

App (parent)
  └─ UserCard (reçoit user={...})
       └─ Avatar (reçoit src={...}, name={...})
       └─ UserBadge (reçoit role={...})

JAMAIS le sens inverse directement !
"""

# ----------------------------------------------------------------------------
# [EMAIL] PASSER ET RECEVOIR DES PROPS
# ----------------------------------------------------------------------------

"""
PASSER (côté parent) :
"""

function App() {
  return (
    <div>
      {/* Props string */}
      <Button label="Cliquer ici" />

      {/* Props nombre */}
      <Progress value={75} max={100} />

      {/* Props booléen (présence = true) */}
      <Button disabled />          {/* disabled={true} */}
      <Button disabled={false} />  {/* Explicitement false */}

      {/* Props objet */}
      <UserCard user={{ name: 'Alice', age: 25 }} />

      {/* Props tableau */}
      <List items={['a', 'b', 'c']} />

      {/* Props fonction */}
      <Button onClick={() => alert('Cliqué!')} />

      {/* Props composant */}
      <Modal icon={<CloseIcon />} />
    </div>
  );
}

"""
RECEVOIR (côté enfant) :
"""

// Méthode 1 : Objet props complet
function Button(props) {
  console.log(props);  // { label: "Cliquer ici", disabled: true, onClick: f }
  return (
    <button disabled={props.disabled} onClick={props.onClick}>
      {props.label}
    </button>
  );
}

// Méthode 2 : Déstructuration [OK] (Recommandée)
function Button({ label, disabled, onClick }) {
  return (
    <button disabled={disabled} onClick={onClick}>
      {label}
    </button>
  );
}

// Méthode 3 : Déstructuration avec alias
function Button({ label: buttonText, className: cls }) {
  return <button className={cls}>{buttonText}</button>;
}

# ----------------------------------------------------------------------------
# [OBJECTIF] VALEURS PAR DÉFAUT
# ----------------------------------------------------------------------------

"""
MÉTHODE 1 : Paramètre par défaut [OK] (Recommandée)
"""

function Button({ label = 'Cliquer', variant = 'primary', size = 'medium', onClick }) {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      onClick={onClick}
    >
      {label}
    </button>
  );
}

// Utilisation
<Button />                              // label="Cliquer", variant="primary", size="medium"
<Button label="Envoyer" />             // variant="primary" par défaut
<Button label="Annuler" variant="secondary" />

"""
MÉTHODE 2 : defaultProps (ancienne méthode, encore utilisée)
"""

function Button({ label, variant, size, onClick }) {
  return (
    <button className={`btn btn-${variant} btn-${size}`} onClick={onClick}>
      {label}
    </button>
  );
}

Button.defaultProps = {
  label: 'Cliquer',
  variant: 'primary',
  size: 'medium',
};

# ----------------------------------------------------------------------------
# [ENFANT] LA PROP CHILDREN
# ----------------------------------------------------------------------------

"""
CONCEPT :

children = Contenu entre les balises ouvrante et fermante

<Parent>
  <Enfant />   <- Ceci est children de Parent
  <p>Texte</p> <- Ceci aussi
</Parent>
"""

// Composant "conteneur" - utilise children
function Card({ title, children }) {
  return (
    <div className="card">
      {title && <h2 className="card-title">{title}</h2>}
      <div className="card-body">
        {children}  {/* Affiche ce qui est entre <Card>...</Card> */}
      </div>
    </div>
  );
}

// Utilisation
function App() {
  return (
    <div>
      {/* Texte simple comme children */}
      <Card title="Bienvenue">
        Bonjour ! Ceci est dans la Card.
      </Card>

      {/* JSX comme children */}
      <Card title="Mon Profil">
        <img src="avatar.jpg" alt="Profil" />
        <p>Alice Martin - Développeuse</p>
        <button>Contacter</button>
      </Card>

      {/* Pas de children */}
      <Card title="Vide" />
    </div>
  );
}

"""
EXEMPLES AVANCÉS AVEC CHILDREN :
"""

// Layout avec children
function PageLayout({ children }) {
  return (
    <div>
      <header>Mon App</header>
      <main>{children}</main>
      <footer>© 2024</footer>
    </div>
  );
}

// Modal avec children
function Modal({ isOpen, onClose, title, children }) {
  if (!isOpen) return null;

  return (
    <div className="overlay" onClick={onClose}>
      <div className="modal" onClick={e => e.stopPropagation()}>
        <div className="modal-header">
          <h2>{title}</h2>
          <button onClick={onClose}>[X]</button>
        </div>
        <div className="modal-body">
          {children}
        </div>
      </div>
    </div>
  );
}

// Utilisation
<Modal isOpen={isOpen} onClose={close} title="Confirmation">
  <p>Êtes-vous sûr de vouloir supprimer ?</p>
  <button onClick={confirm}>Confirmer</button>
</Modal>

# ----------------------------------------------------------------------------
# [RESEAU] PASSAGE DE FONCTIONS COMME PROPS (Callback Pattern)
# ----------------------------------------------------------------------------

"""
CONCEPT : Passer une fonction du parent à l'enfant
-> L'enfant peut communiquer avec le parent via cette fonction
"""

// PARENT
function ParentComponent() {
  const handleAction = (data) => {
    console.log('Enfant a envoyé :', data);
  };

  return <ChildComponent onAction={handleAction} />;
}

// ENFANT
function ChildComponent({ onAction }) {
  return (
    <button onClick={() => onAction({ type: 'clicked', timestamp: Date.now() })}>
      Envoyer au parent
    </button>
  );
}

"""
EXEMPLE PRATIQUE : Formulaire et parent
"""

// Parent gère les données
function SearchPage() {
  const [results, setResults] = useState([]);

  const handleSearch = (query) => {
    // Simuler une recherche
    const filtered = allData.filter(item =>
      item.name.toLowerCase().includes(query.toLowerCase())
    );
    setResults(filtered);
  };

  return (
    <div>
      <SearchBar onSearch={handleSearch} />  {/* Passe la fonction */}
      <ResultsList items={results} />
    </div>
  );
}

// Enfant utilise la fonction
function SearchBar({ onSearch }) {
  const [value, setValue] = useState('');

  const handleSubmit = () => {
    onSearch(value);  // Appelle la fonction du parent
  };

  return (
    <div>
      <input
        value={value}
        onChange={e => setValue(e.target.value)}
        placeholder="Rechercher..."
      />
      <button onClick={handleSubmit}>Rechercher</button>
    </div>
  );
}

# ----------------------------------------------------------------------------
# [GRAPHIQUE] PROPAGATION DE PROPS (SPREAD)
# ----------------------------------------------------------------------------

"""
CONCEPT : Passer toutes les props d'un objet en une fois
"""

const buttonProps = {
  type: 'submit',
  disabled: false,
  className: 'btn-primary',
  onClick: handleClick
};

// Sans spread
<button
  type={buttonProps.type}
  disabled={buttonProps.disabled}
  className={buttonProps.className}
  onClick={buttonProps.onClick}
>

// Avec spread [OK]
<button {...buttonProps}>

"""
CAS D'USAGE : Composant "wrapper"
"""

// Bouton qui enveloppe un bouton HTML natif
function Button({ label, variant = 'primary', ...rest }) {
  // ...rest capture TOUTES les autres props (onClick, disabled, type, etc.)
  return (
    <button
      className={`btn btn-${variant}`}
      {...rest}  // Passe tout le reste au <button> HTML
    >
      {label}
    </button>
  );
}

// Utilisation : onClick, disabled, type passent via ...rest
<Button
  label="Soumettre"
  variant="success"
  onClick={handleSubmit}
  disabled={isLoading}
  type="submit"
/>

# ----------------------------------------------------------------------------
# [OK] PROPTYPES - VALIDATION DES PROPS
# ----------------------------------------------------------------------------

"""
POURQUOI VALIDER LES PROPS ?
-> Erreurs explicites si mauvais type
-> Documentation automatique du composant
-> Détection bugs en développement

INSTALLATION :
npm install prop-types

UTILISATION :
"""

import PropTypes from 'prop-types';

function UserCard({ name, age, email, role, isActive, tags, onEdit }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
      <p>Email: {email}</p>
      <span>{role}</span>
      {isActive && <span>Actif</span>}
      <ul>{tags.map(t => <li key={t}>{t}</li>)}</ul>
      <button onClick={onEdit}>Modifier</button>
    </div>
  );
}

UserCard.propTypes = {
  name: PropTypes.string.isRequired,     // String obligatoire
  age: PropTypes.number,                  // Nombre optionnel
  email: PropTypes.string.isRequired,    // String obligatoire
  role: PropTypes.oneOf(['admin', 'user', 'moderator']),  // Valeurs possibles
  isActive: PropTypes.bool,              // Booléen
  tags: PropTypes.arrayOf(PropTypes.string),  // Array de strings
  onEdit: PropTypes.func.isRequired,     // Fonction obligatoire
};

UserCard.defaultProps = {
  age: 0,
  isActive: false,
  tags: [],
};

"""
[IDEE] TYPES DISPONIBLES :

PropTypes.string          -> Chaîne de caractères
PropTypes.number          -> Nombre
PropTypes.bool            -> Booléen
PropTypes.func            -> Fonction
PropTypes.array           -> Tableau (quelconque)
PropTypes.object          -> Objet (quelconque)
PropTypes.node            -> Tout ce que React peut rendre
PropTypes.element         -> Élément React
PropTypes.any             -> N'importe quoi

PropTypes.arrayOf(type)   -> Tableau d'un type spécifique
PropTypes.objectOf(type)  -> Objet avec valeurs d'un type
PropTypes.shape({...})    -> Objet avec forme spécifique
PropTypes.oneOf([...])    -> Une des valeurs
PropTypes.oneOfType([...]) -> Un des types
PropTypes.instanceOf(Cls) -> Instance d'une classe

AVEC TypeScript -> Pas besoin de PropTypes !
"""

# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 3 : COMPOSANT BOUTON RÉUTILISABLE
# ----------------------------------------------------------------------------

"""
OBJECTIF : Créer un système de boutons ultra-réutilisable

CAHIER DES CHARGES :
1. Composant <Button /> avec :
   - variant : 'primary' | 'secondary' | 'danger' | 'ghost' (défaut: primary)
   - size : 'small' | 'medium' | 'large' (défaut: medium)
   - disabled : boolean (défaut: false)
   - isLoading : boolean (défaut: false)
   - leftIcon : élément React (optionnel)
   - rightIcon : élément React (optionnel)
   - onClick : fonction

2. Composant <IconButton /> basé sur Button, juste une icône

3. Démo montrant toutes les variantes

CONTRAINTE : Utiliser children pour le texte
CONTRAINTE : Le bouton doit être disabled pendant isLoading


CORRIGÉ :
"""

// src/components/Button/Button.jsx
import PropTypes from 'prop-types';

// Styles par variante
const variantStyles = {
  primary:   { background: '#3B82F6', color: 'white', border: 'none' },
  secondary: { background: '#E5E7EB', color: '#374151', border: 'none' },
  danger:    { background: '#EF4444', color: 'white', border: 'none' },
  ghost:     { background: 'transparent', color: '#3B82F6', border: '1px solid #3B82F6' },
};

// Styles par taille
const sizeStyles = {
  small:  { padding: '4px 10px', fontSize: '13px', borderRadius: '4px' },
  medium: { padding: '8px 16px', fontSize: '14px', borderRadius: '6px' },
  large:  { padding: '12px 24px', fontSize: '16px', borderRadius: '8px' },
};

function Button({
  children,
  variant = 'primary',
  size = 'medium',
  disabled = false,
  isLoading = false,
  leftIcon,
  rightIcon,
  onClick,
  ...rest
}) {
  const style = {
    ...variantStyles[variant],
    ...sizeStyles[size],
    display: 'inline-flex',
    alignItems: 'center',
    gap: '6px',
    cursor: disabled || isLoading ? 'not-allowed' : 'pointer',
    opacity: disabled || isLoading ? 0.6 : 1,
    transition: 'all 0.15s',
    fontWeight: '500',
  };

  return (
    <button
      style={style}
      disabled={disabled || isLoading}
      onClick={onClick}
      {...rest}
    >
      {isLoading ? (
        <span>[HOURGLASS_WITH_FLOWING_SAND]</span>
      ) : (
        <>
          {leftIcon && <span>{leftIcon}</span>}
          {children}
          {rightIcon && <span>{rightIcon}</span>}
        </>
      )}
    </button>
  );
}

Button.propTypes = {
  children: PropTypes.node.isRequired,
  variant: PropTypes.oneOf(['primary', 'secondary', 'danger', 'ghost']),
  size: PropTypes.oneOf(['small', 'medium', 'large']),
  disabled: PropTypes.bool,
  isLoading: PropTypes.bool,
  leftIcon: PropTypes.element,
  rightIcon: PropTypes.element,
  onClick: PropTypes.func,
};

// IconButton basé sur Button
function IconButton({ icon, label, ...rest }) {
  return (
    <Button {...rest} aria-label={label}>
      {icon}
    </Button>
  );
}

// Démo
function ButtonDemo() {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: '16px', padding: '24px' }}>
      <h2>Variantes</h2>
      <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
        <Button>Primary</Button>
        <Button variant="secondary">Secondary</Button>
        <Button variant="danger">Danger</Button>
        <Button variant="ghost">Ghost</Button>
      </div>

      <h2>Tailles</h2>
      <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
        <Button size="small">Petit</Button>
        <Button size="medium">Moyen</Button>
        <Button size="large">Grand</Button>
      </div>

      <h2>États</h2>
      <div style={{ display: 'flex', gap: '8px' }}>
        <Button disabled>Désactivé</Button>
        <Button isLoading>Chargement...</Button>
        <Button leftIcon={<span>[EMAIL]</span>}>Envoyer email</Button>
        <Button rightIcon={<span>-></span>} variant="ghost">Continuer</Button>
      </div>

      <h2>IconButtons</h2>
      <div style={{ display: 'flex', gap: '8px' }}>
        <IconButton icon="[HEAVY_BLACK_HEART]" label="Aimer" />
        <IconButton icon="[LIEN]" label="Partager" variant="secondary" />
        <IconButton icon="[SUPPRIMER]" label="Supprimer" variant="danger" />
      </div>
    </div>
  );
}

export { Button, IconButton, ButtonDemo };

"""
[OK] CE QUE VOUS AVEZ PRATIQUÉ :
- Props avec valeurs par défaut
- prop children
- Spread operator pour props HTML
- PropTypes
- Composant composé d'un autre (IconButton -> Button)
- Styles dynamiques basés sur props
"""

# ============================================================================
# [GUIDE] CHAPITRE 4 : STATE ET ÉVÉNEMENTS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre ce qu'est le state (état)
[OK] Utiliser useState pour gérer l'état local
[OK] Gérer les événements (click, input, submit)
[OK] Comprendre la nature asynchrone du state
[OK] Mettre à jour objets et tableaux dans le state
"""

# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QUE LE STATE ?
# ----------------------------------------------------------------------------

"""
DÉFINITION :

State = Données qui changent au fil du temps
       et dont les changements déclenchent un re-rendu

Props  = Données reçues de l'extérieur (immuables)
State  = Données gérées à l'intérieur (modifiables)

ANALOGIE :
Props = Date de naissance (ne change pas)
State = Humeur (change au fil du temps)


QUAND UTILISER LE STATE ?

[OK] Données qui changent via interactions utilisateur
   - Compteur qui s'incrémente
   - Formulaire en cours de remplissage
   - Menu ouvert/fermé
   - Onglet actif

[OK] Données récupérées de l'API
   - Liste de produits chargée
   - Profil utilisateur

[X] PAS besoin de state
   - Données qui ne changent jamais
   - Données dérivées (calculables depuis d'autres)
   - Props reçues
"""

# ----------------------------------------------------------------------------
# [CONFIG] useState - LE HOOK FONDAMENTAL
# ----------------------------------------------------------------------------

"""
SYNTAXE :
"""

import { useState } from 'react';

// const [valeur, setValeur] = useState(valeurInitiale);
//        │        │                    │
//        │        │                    └── Valeur au démarrage
//        │        └─────────────────────── Fonction pour changer
//        └──────────────────────────────── Valeur actuelle

"""
EXEMPLE BASIQUE : Compteur
"""

function Counter() {
  const [count, setCount] = useState(0);  // Valeur initiale : 0

  return (
    <div>
      <p>Compteur : {count}</p>

      <button onClick={() => setCount(count + 1)}>
        +1
      </button>

      <button onClick={() => setCount(count - 1)}>
        -1
      </button>

      <button onClick={() => setCount(0)}>
        Reset
      </button>
    </div>
  );
}

"""
CE QUI SE PASSE :

1. Rendu initial : count = 0, React affiche "Compteur : 0"
2. Clic sur +1 : setCount(1) est appelé
3. React re-rend le composant avec count = 1
4. Affiche "Compteur : 1"

[IDEE] IMPORTANT : setCount() déclenche un RE-RENDU
   React appelle à nouveau la fonction Counter()
   mais count a maintenant la nouvelle valeur
"""

# ----------------------------------------------------------------------------
# [NOTIF] GESTION DES ÉVÉNEMENTS
# ----------------------------------------------------------------------------

"""
ÉVÉNEMENTS COURANTS EN REACT :
"""

// onClick - Clic
<button onClick={() => console.log('Cliqué!')}>Cliquer</button>

// onChange - Modification d'input
<input onChange={(e) => console.log(e.target.value)} />

// onSubmit - Soumission de formulaire
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>

// onMouseEnter / onMouseLeave - Survol
<div onMouseEnter={() => setHovered(true)} onMouseLeave={() => setHovered(false)}>

// onKeyDown / onKeyUp / onKeyPress
<input onKeyDown={(e) => { if (e.key === 'Enter') handleEnter(); }} />

// onFocus / onBlur
<input onFocus={() => setFocused(true)} onBlur={() => setFocused(false)} />

// onScroll
<div onScroll={(e) => setScrollTop(e.target.scrollTop)}>

"""
OBJET EVENT (e) :
"""

function InputExample() {
  const handleChange = (e) => {
    console.log(e.target.value);     // Valeur du champ
    console.log(e.target.name);      // Attribut name
    console.log(e.target.type);      // Type d'input
    console.log(e.target.checked);   // Pour checkbox
    console.log(e.type);             // Type d'événement ('change')
    e.preventDefault();              // Empêcher comportement par défaut
    e.stopPropagation();             // Arrêter la propagation
  };

  return <input name="username" onChange={handleChange} />;
}

"""
GESTIONNAIRE AVEC PARAMÈTRES :
"""

function ItemList({ items }) {
  const handleDelete = (id) => {
    console.log('Supprimer item', id);
  };

  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>
          {item.name}
          {/* Utiliser une arrow function pour passer l'id */}
          <button onClick={() => handleDelete(item.id)}>
            Supprimer
          </button>
        </li>
      ))}
    </ul>
  );
}

# ----------------------------------------------------------------------------
# [ATTENTION] NATURE ASYNCHRONE DU STATE
# ----------------------------------------------------------------------------

"""
PROBLÈME : setState est ASYNCHRONE !
"""

function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    setCount(count + 1);
    setCount(count + 1);
    setCount(count + 1);
    // On s'attendrait à count = 3...
    // MAIS React batch ces appels et count est TOUJOURS 0 ici !
    // Résultat : count = 1 seulement !
  };

  // ...
}

"""
SOLUTION : Forme fonctionnelle de setState
"""

function Counter() {
  const [count, setCount] = useState(0);

  const handleClick = () => {
    // [OK] Forme fonctionnelle : reçoit la valeur PRÉCÉDENTE
    setCount(prev => prev + 1);  // 0 -> 1
    setCount(prev => prev + 1);  // 1 -> 2
    setCount(prev => prev + 1);  // 2 -> 3
    // Résultat : count = 3 [OK]
  };

  // ...
}

"""
RÈGLE D'OR :
Quand le nouvel état dépend de l'ancien -> Forme fonctionnelle
Quand c'est une valeur fixe -> Forme directe

setCount(0)                 // Forme directe (reset à 0)
setCount(prev => prev + 1)  // Forme fonctionnelle (incrémenter)
"""

# ----------------------------------------------------------------------------
# [PACKAGE] METTRE À JOUR OBJETS ET TABLEAUX
# ----------------------------------------------------------------------------

"""
[ATTENTION] NE JAMAIS MUTER LE STATE DIRECTEMENT !
React ne détecte pas les mutations -> Pas de re-rendu !
"""

// [X] MAUVAIS : Mutation directe
const [user, setUser] = useState({ name: 'Alice', age: 25 });

user.name = 'Bob';   // React ne le sait pas !
setUser(user);       // Même référence -> React skip

// [OK] BON : Créer un nouvel objet
setUser({ ...user, name: 'Bob' });  // Spread + override

"""
METTRE À JOUR UN OBJET :
"""

const [user, setUser] = useState({
  name: 'Alice',
  age: 25,
  address: {
    city: 'Paris',
    country: 'France'
  }
});

// Modifier une propriété
const updateName = (newName) => {
  setUser({ ...user, name: newName });
};

// Modifier une propriété imbriquée
const updateCity = (newCity) => {
  setUser({
    ...user,
    address: {
      ...user.address,
      city: newCity
    }
  });
};

"""
METTRE À JOUR UN TABLEAU :
"""

const [items, setItems] = useState(['Pomme', 'Banane', 'Orange']);

// [OK] Ajouter un élément (spread)
const addItem = (newItem) => {
  setItems([...items, newItem]);
};

// [OK] Supprimer un élément (filter)
const removeItem = (indexToRemove) => {
  setItems(items.filter((_, index) => index !== indexToRemove));
};

// [OK] Modifier un élément (map)
const updateItem = (index, newValue) => {
  setItems(items.map((item, i) => i === index ? newValue : item));
};

// [OK] Insérer à une position (slice)
const insertItem = (item, position) => {
  setItems([
    ...items.slice(0, position),
    item,
    ...items.slice(position)
  ]);
};

"""
TABLEAU D'OBJETS :
"""

const [products, setProducts] = useState([
  { id: 1, name: 'Laptop', price: 999, inCart: false },
  { id: 2, name: 'Mouse', price: 29, inCart: false },
]);

// Ajouter au panier
const toggleCart = (productId) => {
  setProducts(products.map(product =>
    product.id === productId
      ? { ...product, inCart: !product.inCart }
      : product
  ));
};

// Supprimer un produit
const removeProduct = (productId) => {
  setProducts(products.filter(p => p.id !== productId));
};

# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 4 : TODO LIST COMPLÈTE
# ----------------------------------------------------------------------------

"""
OBJECTIF : Créer une Todo List complète

FONCTIONNALITÉS :
1. Afficher la liste des tâches
2. Ajouter une nouvelle tâche (input + bouton)
3. Marquer comme terminée (checkbox ou clic)
4. Supprimer une tâche
5. Filtrer : Toutes / Actives / Terminées
6. Compteur de tâches restantes
7. Bouton "Tout effacer les terminées"

ÉTAT INITIAL :
[
  { id: 1, text: 'Apprendre React', done: false },
  { id: 2, text: 'Faire du sport', done: true },
  { id: 3, text: 'Lire un livre', done: false },
]

CORRIGÉ :
"""

// src/components/TodoApp.jsx
import { useState } from 'react';

function TodoApp() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Apprendre React', done: false },
    { id: 2, text: 'Faire du sport', done: true },
    { id: 3, text: 'Lire un livre', done: false },
  ]);

  const [newTodo, setNewTodo] = useState('');
  const [filter, setFilter] = useState('all'); // 'all' | 'active' | 'done'

  // Ajouter une tâche
  const addTodo = () => {
    if (newTodo.trim() === '') return;
    setTodos([...todos, { id: Date.now(), text: newTodo.trim(), done: false }]);
    setNewTodo('');
  };

  // Basculer une tâche
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, done: !todo.done } : todo
    ));
  };

  // Supprimer une tâche
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  // Supprimer les terminées
  const clearDone = () => {
    setTodos(todos.filter(todo => !todo.done));
  };

  // Filtrer
  const filteredTodos = todos.filter(todo => {
    if (filter === 'active') return !todo.done;
    if (filter === 'done') return todo.done;
    return true;
  });

  const remainingCount = todos.filter(t => !t.done).length;

  // Gérer la touche Entrée
  const handleKeyDown = (e) => {
    if (e.key === 'Enter') addTodo();
  };

  const styles = {
    container: { maxWidth: '500px', margin: '40px auto', fontFamily: 'sans-serif' },
    title: { textAlign: 'center', color: '#E879F9', fontSize: '40px', fontWeight: '100', marginBottom: '20px' },
    inputRow: { display: 'flex', gap: '8px', marginBottom: '16px' },
    input: { flex: 1, padding: '12px', fontSize: '16px', border: '1px solid #d1d5db', borderRadius: '6px', outline: 'none' },
    addBtn: { padding: '12px 20px', background: '#3B82F6', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '16px' },
    filterRow: { display: 'flex', justifyContent: 'center', gap: '8px', marginBottom: '16px' },
    list: { listStyle: 'none', padding: 0, background: 'white', borderRadius: '8px', boxShadow: '0 2px 10px rgba(0,0,0,0.1)' },
    todoItem: { display: 'flex', alignItems: 'center', padding: '14px 16px', borderBottom: '1px solid #f3f4f6' },
    footer: { display: 'flex', justifyContent: 'space-between', padding: '12px 16px', fontSize: '13px', color: '#9ca3af' },
  };

  return (
    <div style={styles.container}>
      <h1 style={styles.title}>todos</h1>

      {/* Input */}
      <div style={styles.inputRow}>
        <input
          style={styles.input}
          value={newTodo}
          onChange={e => setNewTodo(e.target.value)}
          onKeyDown={handleKeyDown}
          placeholder="Que faire aujourd'hui ?"
        />
        <button style={styles.addBtn} onClick={addTodo}>+</button>
      </div>

      {/* Filtres */}
      <div style={styles.filterRow}>
        {['all', 'active', 'done'].map(f => (
          <button
            key={f}
            onClick={() => setFilter(f)}
            style={{
              padding: '6px 14px',
              border: filter === f ? '1px solid #3B82F6' : '1px solid #d1d5db',
              borderRadius: '4px',
              background: 'white',
              color: filter === f ? '#3B82F6' : '#374151',
              cursor: 'pointer'
            }}
          >
            {{ all: 'Toutes', active: 'Actives', done: 'Terminées' }[f]}
          </button>
        ))}
      </div>

      {/* Liste */}
      <ul style={styles.list}>
        {filteredTodos.length === 0 ? (
          <li style={{ padding: '20px', textAlign: 'center', color: '#9ca3af' }}>
            Aucune tâche [BRAVO]
          </li>
        ) : (
          filteredTodos.map(todo => (
            <li key={todo.id} style={styles.todoItem}>
              <input
                type="checkbox"
                checked={todo.done}
                onChange={() => toggleTodo(todo.id)}
                style={{ marginRight: '12px', width: '18px', height: '18px', cursor: 'pointer' }}
              />
              <span style={{
                flex: 1,
                fontSize: '16px',
                textDecoration: todo.done ? 'line-through' : 'none',
                color: todo.done ? '#9ca3af' : '#111827'
              }}>
                {todo.text}
              </span>
              <button
                onClick={() => deleteTodo(todo.id)}
                style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#d1d5db', fontSize: '18px' }}
              >
                [X]
              </button>
            </li>
          ))
        )}

        {/* Footer */}
        {todos.length > 0 && (
          <li style={styles.footer}>
            <span>{remainingCount} tâche{remainingCount !== 1 ? 's' : ''} restante{remainingCount !== 1 ? 's' : ''}</span>
            {todos.some(t => t.done) && (
              <button
                onClick={clearDone}
                style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#9ca3af' }}
              >
                Effacer terminées
              </button>
            )}
          </li>
        )}
      </ul>
    </div>
  );
}

export default TodoApp;

"""
[OK] CE QUE VOUS AVEZ PRATIQUÉ :
- useState avec tableau d'objets
- Mettre à jour tableaux (add, toggle, delete, filter)
- Gestion événements (onClick, onChange, onKeyDown)
- Forme fonctionnelle de setState
- Rendu conditionnel complexe
- Dériver de l'état (filteredTodos, remainingCount)
"""

# ============================================================================
# [DOCS] RÉCAPITULATIF PARTIE 1
# ============================================================================

"""
[BRAVO] FÉLICITATIONS ! PARTIE 1 TERMINÉE !

VOUS MAÎTRISEZ MAINTENANT :

Chapitre 0 : Introduction
[OK] React = bibliothèque pour UIs par composants
[OK] Virtual DOM pour performances
[OK] Créer projet avec Vite

Chapitre 1 : Composants
[OK] Fonction -> retourne JSX
[OK] Nom commence par Majuscule
[OK] Export / Import
[OK] Composition de composants

Chapitre 2 : JSX
[OK] Syntaxe JSX vs HTML
[OK] Expressions dans {}
[OK] Rendu conditionnel (?, &&, early return)
[OK] Listes avec .map() et key
[OK] Gestion du style

Chapitre 3 : Props
[OK] Passer et recevoir des props
[OK] Déstructuration
[OK] Valeurs par défaut
[OK] Prop children
[OK] Spread operator
[OK] PropTypes

Chapitre 4 : State et Événements
[OK] useState
[OK] Gestion événements
[OK] Nature asynchrone du state
[OK] Mise à jour objets/tableaux

-> PROCHAINE ÉTAPE : PARTIE 2 - Hooks Essentiels !
"""

# ============================================================================
# [SCIENCE] REACT - PARTIE 2 : HOOKS ESSENTIELS
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 5 : useState en profondeur
# - Chapitre 6 : useEffect - Effets de Bord
# - Chapitre 7 : useContext - État Global
# - Chapitre 8 : useRef, useMemo, useCallback
#
# [TEMPS] TEMPS : ~8-10 heures
# [DOCS] PRÉREQUIS : Partie 1 complétée
# ============================================================================

# ============================================================================
# [GUIDE] CHAPITRE 5 : useState EN PROFONDEUR
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Cas avancés de useState
[OK] Initialisation paresseuse (lazy initialization)
[OK] Gérer plusieurs états
[OK] État dérivé vs état brut
[OK] Patterns courants de gestion d'état
"""

# ----------------------------------------------------------------------------
# [NOMBRE] PLUSIEURS ÉTATS
# ----------------------------------------------------------------------------

"""
QUAND CRÉER PLUSIEURS ÉTATS ?

Règle générale :
- États qui changent ENSEMBLE -> Un seul objet
- États qui changent INDÉPENDAMMENT -> États séparés
"""

// [X] MAUVAIS : Tout dans un objet (si non liés)
const [state, setState] = useState({
  count: 0,
  isOpen: false,
  searchQuery: '',
  theme: 'dark',
  language: 'fr'
});

// Pour changer isOpen, devoir faire :
setState({ ...state, isOpen: !state.isOpen });  // Verbeux

// [OK] BON : États séparés pour choses non liées
const [count, setCount] = useState(0);
const [isOpen, setIsOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [theme, setTheme] = useState('dark');

// Simple à modifier
setIsOpen(!isOpen);

// [OK] BON : Objet pour données liées (formulaire)
const [formData, setFormData] = useState({
  firstName: '',
  lastName: '',
  email: '',
  phone: ''
});

// Mise à jour propre
const handleChange = (field, value) => {
  setFormData(prev => ({ ...prev, [field]: value }));
};

# ----------------------------------------------------------------------------
# [ATTENTE] INITIALISATION PARESSEUSE (LAZY INITIALIZATION)
# ----------------------------------------------------------------------------

"""
PROBLÈME :
"""

// [X] Calcul exécuté à CHAQUE rendu !
function App() {
  const [data, setData] = useState(expensiveCalculation()); // Recalculé à chaque rendu !
  // ...
}

"""
SOLUTION : Passer une FONCTION à useState
-> Exécutée une seule fois (au premier rendu)
"""

// [OK] Fonction passée (pas appelée !) -> Exécutée une fois
function App() {
  const [data, setData] = useState(() => expensiveCalculation()); // Une seule fois !
  // ...
}

"""
CAS D'USAGES PRATIQUES :
"""

// Lire localStorage au démarrage
function ThemeToggle() {
  const [theme, setTheme] = useState(() => {
    // Exécuté une seule fois
    return localStorage.getItem('theme') || 'light';
  });

  const toggleTheme = () => {
    const newTheme = theme === 'light' ? 'dark' : 'light';
    setTheme(newTheme);
    localStorage.setItem('theme', newTheme);  // Sauvegarder
  };

  return (
    <button onClick={toggleTheme}>
      {theme === 'light' ? '[CRESCENT_MOON] Mode nuit' : '[BLACK_SUN_WITH_RAYS] Mode jour'}
    </button>
  );
}

// Initialiser depuis props (complexe)
function DataGrid({ initialData }) {
  const [rows, setRows] = useState(() => {
    // Transformation coûteuse - exécutée une fois
    return initialData
      .filter(row => row.isValid)
      .map(row => ({ ...row, key: row.id + '_' + row.type }))
      .sort((a, b) => a.name.localeCompare(b.name));
  });

  // ...
}

# ----------------------------------------------------------------------------
# [GRAPHIQUE] ÉTAT DÉRIVÉ VS ÉTAT BRUT
# ----------------------------------------------------------------------------

"""
ÉTAT DÉRIVÉ = Calculé depuis d'autres états/props
NE PAS stocker en state -> Calculer pendant le rendu
"""

// [X] MAUVAIS : fullName stocké en state (doublon !)
function UserForm() {
  const [firstName, setFirstName] = useState('Alice');
  const [lastName, setLastName] = useState('Martin');
  const [fullName, setFullName] = useState('Alice Martin'); // Redondant !

  const handleFirstNameChange = (val) => {
    setFirstName(val);
    setFullName(val + ' ' + lastName); // Facile d'oublier / désynchroniser
  };
  // ...
}

// [OK] BON : fullName dérivé pendant le rendu
function UserForm() {
  const [firstName, setFirstName] = useState('Alice');
  const [lastName, setLastName] = useState('Martin');

  const fullName = firstName + ' ' + lastName; // Calculé, pas stocké

  return <p>Nom complet : {fullName}</p>;
}

"""
RÈGLE : Si une valeur peut être calculée depuis d'autres valeurs -> Calculer, ne pas stocker !

AUTRES EXEMPLES DE VALEURS DÉRIVÉES :
"""

const [cart, setCart] = useState([]);

// [OK] Dérivé, pas en state
const totalItems = cart.length;
const totalPrice = cart.reduce((sum, item) => sum + item.price, 0);
const isEmpty = cart.length === 0;
const hasDiscount = cart.some(item => item.discount > 0);

// [X] Ne pas faire ça
const [totalItems, setTotalItems] = useState(0); // Redondant !

# ============================================================================
# [GUIDE] CHAPITRE 6 : useEffect - EFFETS DE BORD
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Ce qu'est un "effet de bord"
[OK] Syntaxe et fonctionnement de useEffect
[OK] Tableau de dépendances
[OK] Fonction de nettoyage (cleanup)
[OK] Patterns courants : API, timers, événements
[OK] Erreurs courantes à éviter
"""

# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QU'UN EFFET DE BORD ?
# ----------------------------------------------------------------------------

"""
RENDU REACT (PUR) :
Input: props + state -> Output: JSX
Le rendu ne doit avoir AUCUN effet sur l'extérieur

EFFETS DE BORD = Interactions avec "l'extérieur" du composant :

[WEB] Appels API (fetch données)
[PACKAGE] localStorage / sessionStorage
[ALARM_CLOCK] Timers (setTimeout, setInterval)
[AUDIO] Abonnements (WebSocket, événements DOM)
[NOTE] Logs, analytics
[DESIGN] Modifier directement le DOM

-> Ces opérations ne peuvent pas être faites pendant le rendu !
-> Solution : useEffect
"""

# ----------------------------------------------------------------------------
# [CONFIG] SYNTAXE USEEFFECT
# ----------------------------------------------------------------------------

"""
FORME GÉNÉRALE :
"""

import { useState, useEffect } from 'react';

useEffect(
  () => {
    // Code de l'effet (s'exécute après le rendu)
    console.log('Effet exécuté !');

    // OPTIONNEL : Fonction de nettoyage
    return () => {
      console.log('Nettoyage !');
    };
  },
  [dep1, dep2]  // Tableau de dépendances
);

"""
LES 3 VARIANTES :


1. Sans tableau -> S'exécute après CHAQUE rendu
"""

useEffect(() => {
  console.log('Exécuté après chaque rendu');
});
// [ATTENTION] Rarement ce qu'on veut -> Risque de boucle infinie


"""
2. Tableau vide [] -> S'exécute UNE SEULE FOIS (montage)
"""

useEffect(() => {
  console.log('Exécuté au montage seulement (componentDidMount)');
  fetch('/api/init').then(/* ... */);
}, []); // <- Tableau vide


"""
3. Avec dépendances -> S'exécute quand les dépendances changent
"""

useEffect(() => {
  console.log('userId a changé, rechargement...');
  fetch(`/api/users/${userId}`).then(/* ... */);
}, [userId]); // <- S'exécute quand userId change

# ----------------------------------------------------------------------------
# [WEB] PATTERN : APPELS API
# ----------------------------------------------------------------------------

"""
PATTERN COMPLET POUR FETCH DE DONNÉES :
"""

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Reset états
    setLoading(true);
    setError(null);

    // Variable pour éviter mise à jour sur composant démonté
    let isMounted = true;

    const fetchUser = async () => {
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const data = await response.json();

        // Vérifier si composant toujours monté
        if (isMounted) {
          setUser(data);
        }
      } catch (err) {
        if (isMounted) {
          setError(err.message);
        }
      } finally {
        if (isMounted) {
          setLoading(false);
        }
      }
    };

    fetchUser();

    // Cleanup : éviter mise à jour après démontage
    return () => {
      isMounted = false;
    };
  }, [userId]); // Re-fetch quand userId change

  if (loading) return <div>[HOURGLASS_WITH_FLOWING_SAND] Chargement...</div>;
  if (error)   return <div>[X] Erreur : {error}</div>;
  if (!user)   return <div>Aucun utilisateur trouvé</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

"""
AVEC AbortController (annuler requêtes) :
"""

function SearchResults({ query }) {
  const [results, setResults] = useState([]);

  useEffect(() => {
    if (!query) {
      setResults([]);
      return;
    }

    // Créer contrôleur pour annuler requête
    const controller = new AbortController();

    const fetchResults = async () => {
      try {
        const response = await fetch(
          `/api/search?q=${encodeURIComponent(query)}`,
          { signal: controller.signal }  // <- Lier le contrôleur
        );
        const data = await response.json();
        setResults(data);
      } catch (err) {
        if (err.name !== 'AbortError') {  // Ignorer l'annulation
          console.error(err);
        }
      }
    };

    fetchResults();

    // Cleanup : annuler requête précédente si query change
    return () => {
      controller.abort();
    };
  }, [query]);

  return (
    <ul>
      {results.map(r => <li key={r.id}>{r.title}</li>)}
    </ul>
  );
}

# ----------------------------------------------------------------------------
# [TEMPS] PATTERN : TIMERS
# ----------------------------------------------------------------------------

"""
setTimeout / setInterval -> Toujours nettoyer !
"""

function Countdown({ seconds }) {
  const [remaining, setRemaining] = useState(seconds);

  useEffect(() => {
    if (remaining <= 0) return;

    // Timer qui décrémente chaque seconde
    const timer = setInterval(() => {
      setRemaining(prev => prev - 1);
    }, 1000);

    // CLEANUP : Nettoyer le timer
    return () => {
      clearInterval(timer);
    };
  }, [remaining]);

  return (
    <div>
      {remaining > 0 ? (
        <p>[ALARM_CLOCK] {remaining} secondes restantes</p>
      ) : (
        <p>[RAPIDE] Temps écoulé !</p>
      )}
    </div>
  );
}

"""
DEBOUNCE AVEC useEffect :
"""

function SearchInput() {
  const [query, setQuery] = useState('');
  const [debouncedQuery, setDebouncedQuery] = useState('');

  useEffect(() => {
    // Attendre 500ms avant de mettre à jour debouncedQuery
    const timer = setTimeout(() => {
      setDebouncedQuery(query);
    }, 500);

    // Cleanup : annuler si query change avant 500ms
    return () => clearTimeout(timer);
  }, [query]);

  // Utiliser debouncedQuery pour la recherche
  useEffect(() => {
    if (debouncedQuery) {
      console.log('Recherche :', debouncedQuery);
      // fetch(`/api/search?q=${debouncedQuery}`)
    }
  }, [debouncedQuery]);

  return (
    <input
      value={query}
      onChange={e => setQuery(e.target.value)}
      placeholder="Rechercher (debounced)..."
    />
  );
}

# ----------------------------------------------------------------------------
# [AUDIO] PATTERN : ÉVÉNEMENTS DOM
# ----------------------------------------------------------------------------

"""
Écouter des événements sur window/document -> Toujours nettoyer !
"""

function KeyboardShortcuts() {
  const [lastKey, setLastKey] = useState('');

  useEffect(() => {
    const handleKeyDown = (e) => {
      setLastKey(e.key);

      // Raccourcis clavier
      if (e.ctrlKey && e.key === 's') {
        e.preventDefault();
        console.log('Sauvegarder !');
      }
    };

    // Attacher l'événement
    window.addEventListener('keydown', handleKeyDown);

    // CLEANUP : Détacher l'événement
    return () => {
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, []); // Tableau vide = une seule fois

  return <p>Dernière touche : {lastKey || 'Aucune'}</p>;
}


// Exemple : Fermer modal avec Escape
function Modal({ isOpen, onClose }) {
  useEffect(() => {
    if (!isOpen) return;

    const handleEscape = (e) => {
      if (e.key === 'Escape') onClose();
    };

    document.addEventListener('keydown', handleEscape);
    return () => document.removeEventListener('keydown', handleEscape);
  }, [isOpen, onClose]);

  if (!isOpen) return null;
  return (
    <div className="modal">
      <button onClick={onClose}>[X]</button>
      <p>Appuyer Échap pour fermer</p>
    </div>
  );
}

# ----------------------------------------------------------------------------
# [PACKAGE] PATTERN : LOCALSTORAGE
# ----------------------------------------------------------------------------

"""
Synchroniser l'état avec localStorage :
"""

function useLocalStorage(key, initialValue) {
  // Initialisation paresseuse depuis localStorage
  const [value, setValue] = useState(() => {
    try {
      const stored = localStorage.getItem(key);
      return stored ? JSON.parse(stored) : initialValue;
    } catch {
      return initialValue;
    }
  });

  useEffect(() => {
    // Synchroniser avec localStorage à chaque changement
    try {
      localStorage.setItem(key, JSON.stringify(value));
    } catch {
      console.warn('localStorage non disponible');
    }
  }, [key, value]);

  return [value, setValue];
}

// Utilisation
function App() {
  const [theme, setTheme] = useLocalStorage('theme', 'light');
  const [favorites, setFavorites] = useLocalStorage('favorites', []);

  return (
    <div>
      <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
        Thème : {theme}
      </button>
    </div>
  );
}

# ----------------------------------------------------------------------------
# [ATTENTION] ERREURS COURANTES USEEFFECT
# ----------------------------------------------------------------------------

"""
ERREUR 1 : Dépendances manquantes
"""

// [X] MAUVAIS : url non dans les dépendances
function Fetcher({ url }) {
  useEffect(() => {
    fetch(url).then(/* ... */);  // url utilisé mais pas dans deps !
  }, []);  // ESLint warning : 'url' is missing in dependencies
}

// [OK] BON
function Fetcher({ url }) {
  useEffect(() => {
    fetch(url).then(/* ... */);
  }, [url]);  // [OK] url inclus
}

"""
ERREUR 2 : Objet/Fonction comme dépendance
"""

// [X] MAUVAIS : Boucle infinie !
function Component() {
  const options = { timeout: 1000 };  // Nouvel objet à chaque rendu !

  useEffect(() => {
    doSomething(options);
  }, [options]);  // options change à chaque rendu -> boucle infinie !
}

// [OK] SOLUTION 1 : Déplacer l'objet hors du composant
const OPTIONS = { timeout: 1000 };  // Défini une fois

function Component() {
  useEffect(() => {
    doSomething(OPTIONS);
  }, []);  // Pas de dépendance
}

// [OK] SOLUTION 2 : useMemo ou useCallback (voir Chapitre 8)

"""
ERREUR 3 : Mise à jour d'état sans cleanup
"""

// [X] Mise à jour sur composant démonté -> Warning React
function AsyncComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetch('/api/data').then(res => res.json()).then(data => {
      setData(data);  // Peut s'exécuter après démontage !
    });
  }, []);
}

// [OK] Avec cleanup isMounted (voir pattern API ci-dessus)

# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 5 : MÉTÉO EN TEMPS RÉEL
# ============================================================================

"""
OBJECTIF : Application météo avec appels API simulés

FONCTIONNALITÉS :
1. Saisir une ville
2. Fetch des données météo (API simulée)
3. Afficher : température, description, humidité, vent
4. Gérer états : chargement, erreur, succès
5. Debounce de la recherche (500ms)
6. Sauvegarder la dernière ville dans localStorage
7. Afficher l'heure locale (setInterval)

CORRIGÉ :
"""

// src/components/WeatherApp.jsx
import { useState, useEffect } from 'react';

// Simuler une API météo
const fakeWeatherAPI = (city) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const weatherData = {
        Paris:    { temp: 18, desc: '[BLACK_SUN_WITH_RAYS] Ensoleillé', humidity: 45, wind: 12 },
        Lyon:     { temp: 22, desc: '[SUN_BEHIND_CLOUD] Partiellement nuageux', humidity: 50, wind: 8 },
        Marseille:{ temp: 26, desc: '[WHITE_SUN_WITH_SMALL_CLOUD] Clair', humidity: 40, wind: 15 },
        Bordeaux: { temp: 20, desc: '[CLOUD_WITH_RAIN] Pluie', humidity: 75, wind: 20 },
        Toulouse: { temp: 24, desc: '[THUNDER_CLOUD_AND_RAIN] Orage', humidity: 80, wind: 25 },
      };

      const normalizedCity = city.charAt(0).toUpperCase() + city.slice(1).toLowerCase();
      if (weatherData[normalizedCity]) {
        resolve({ city: normalizedCity, ...weatherData[normalizedCity] });
      } else {
        reject(new Error(`Ville "${city}" non trouvée`));
      }
    }, 800); // Simuler délai réseau
  });
};

function WeatherApp() {
  const [cityInput, setCityInput] = useState(() => {
    return localStorage.getItem('lastCity') || 'Paris';
  });
  const [debouncedCity, setDebouncedCity] = useState(cityInput);
  const [weather, setWeather] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [time, setTime] = useState(new Date());

  // Horloge en temps réel
  useEffect(() => {
    const interval = setInterval(() => {
      setTime(new Date());
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  // Debounce de l'input
  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedCity(cityInput);
    }, 500);
    return () => clearTimeout(timer);
  }, [cityInput]);

  // Fetch météo quand debouncedCity change
  useEffect(() => {
    if (!debouncedCity.trim()) return;

    let isMounted = true;
    setLoading(true);
    setError(null);

    fakeWeatherAPI(debouncedCity)
      .then(data => {
        if (isMounted) {
          setWeather(data);
          localStorage.setItem('lastCity', debouncedCity); // Sauvegarder
        }
      })
      .catch(err => {
        if (isMounted) {
          setError(err.message);
          setWeather(null);
        }
      })
      .finally(() => {
        if (isMounted) setLoading(false);
      });

    return () => { isMounted = false; };
  }, [debouncedCity]);

  const suggestions = ['Paris', 'Lyon', 'Marseille', 'Bordeaux', 'Toulouse'];

  return (
    <div style={{
      minHeight: '100vh', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'sans-serif'
    }}>
      <div style={{
        background: 'rgba(255,255,255,0.15)', backdropFilter: 'blur(10px)',
        borderRadius: '20px', padding: '40px', width: '380px', color: 'white',
        boxShadow: '0 8px 32px rgba(0,0,0,0.2)'
      }}>
        {/* Horloge */}
        <div style={{ textAlign: 'center', marginBottom: '20px', fontSize: '14px', opacity: 0.8 }}>
          [HEURE] {time.toLocaleTimeString('fr-FR')}
        </div>

        {/* Input */}
        <input
          value={cityInput}
          onChange={e => setCityInput(e.target.value)}
          placeholder="Entrer une ville..."
          style={{
            width: '100%', padding: '12px 16px', borderRadius: '10px', border: 'none',
            background: 'rgba(255,255,255,0.3)', color: 'white', fontSize: '16px',
            outline: 'none', boxSizing: 'border-box', marginBottom: '8px'
          }}
        />

        {/* Suggestions */}
        <div style={{ display: 'flex', gap: '6px', flexWrap: 'wrap', marginBottom: '20px' }}>
          {suggestions.map(city => (
            <button
              key={city}
              onClick={() => setCityInput(city)}
              style={{
                padding: '4px 10px', background: 'rgba(255,255,255,0.2)', border: 'none',
                borderRadius: '20px', color: 'white', cursor: 'pointer', fontSize: '12px'
              }}
            >
              {city}
            </button>
          ))}
        </div>

        {/* Résultat */}
        {loading && (
          <div style={{ textAlign: 'center', padding: '40px', fontSize: '14px' }}>
            [HOURGLASS_WITH_FLOWING_SAND] Chargement...
          </div>
        )}

        {error && !loading && (
          <div style={{ textAlign: 'center', padding: '20px', background: 'rgba(239,68,68,0.3)', borderRadius: '10px' }}>
            [X] {error}
          </div>
        )}

        {weather && !loading && !error && (
          <div style={{ textAlign: 'center' }}>
            <h2 style={{ fontSize: '28px', margin: '0 0 8px' }}>[IMPORTANT] {weather.city}</h2>
            <div style={{ fontSize: '72px', fontWeight: 'bold', margin: '16px 0' }}>
              {weather.temp}°C
            </div>
            <p style={{ fontSize: '18px', margin: '0 0 20px' }}>{weather.desc}</p>

            <div style={{ display: 'flex', justifyContent: 'space-around', background: 'rgba(255,255,255,0.1)', borderRadius: '12px', padding: '16px' }}>
              <div>
                <div style={{ fontSize: '24px' }}>[DROPLET]</div>
                <div style={{ fontWeight: 'bold' }}>{weather.humidity}%</div>
                <div style={{ fontSize: '12px', opacity: 0.7 }}>Humidité</div>
              </div>
              <div>
                <div style={{ fontSize: '24px' }}>[DASH_SYMBOL]</div>
                <div style={{ fontWeight: 'bold' }}>{weather.wind} km/h</div>
                <div style={{ fontSize: '12px', opacity: 0.7 }}>Vent</div>
              </div>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

export default WeatherApp;

"""
[OK] CE QUE VOUS AVEZ PRATIQUÉ :
- useEffect avec tableau vide (horloge)
- useEffect avec dépendances (debounce, fetch)
- Cleanup (clearInterval, isMounted)
- Initialisation localStorage
- Gestion états loading/error/data
- async/await dans useEffect
"""

# ============================================================================
# [GUIDE] CHAPITRE 7 : useContext - ÉTAT GLOBAL
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Le problème du "prop drilling"
[OK] Créer et fournir un Context
[OK] Consommer un Context avec useContext
[OK] Pattern Provider complet
[OK] Context pour thème, authentification, langue
"""

# ----------------------------------------------------------------------------
# [REFLEXION] LE PROBLÈME : PROP DRILLING
# ----------------------------------------------------------------------------

"""
PROP DRILLING = Passer des props à travers plusieurs niveaux inutilement

App
  └─ Navbar (besoin de user)
  └─ Main
       └─ Sidebar (besoin de user)
       └─ Content
            └─ Article
                 └─ CommentSection
                      └─ Comment (besoin de user)

Sans Context :
App(user) -> Main(user) -> Content(user) -> Article(user)
-> CommentSection(user) -> Comment(user)

Main, Content, Article n'ont PAS besoin de user !
Ils ne font que le passer -> PROP DRILLING


SOLUTION : Context
-> user disponible directement dans Comment sans passer par les parents
"""

# ----------------------------------------------------------------------------
# [OUTIL] CRÉER UN CONTEXT
# ----------------------------------------------------------------------------

"""
3 ÉTAPES :
1. Créer le Context (createContext)
2. Fournir les données (Provider)
3. Consommer les données (useContext)
"""

// ===== ÉTAPE 1 : Créer ======================================================
// src/context/ThemeContext.jsx

import { createContext, useState, useContext } from 'react';

// Créer le context (valeur par défaut si pas de Provider)
const ThemeContext = createContext({
  theme: 'light',
  toggleTheme: () => {}
});

// ===== ÉTAPE 2 : Provider ===================================================
// Composant qui fournit les données

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prev => prev === 'light' ? 'dark' : 'light');
  };

  // La valeur partagée avec tous les descendants
  const value = { theme, toggleTheme };

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}

// ===== ÉTAPE 3 : Hook personnalisé (bonne pratique) =========================
export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme doit être utilisé à l\'intérieur de ThemeProvider');
  }
  return context;
}

// ===== UTILISATION ==========================================================

// main.jsx ou App.jsx : Envelopper avec le Provider
import { ThemeProvider } from './context/ThemeContext';

function App() {
  return (
    <ThemeProvider>
      <Navbar />
      <MainContent />
    </ThemeProvider>
  );
}

// N'importe où dans l'arbre :
function Navbar() {
  const { theme, toggleTheme } = useTheme();  // Direct !

  return (
    <nav style={{ background: theme === 'dark' ? '#1a1a1a' : 'white' }}>
      <button onClick={toggleTheme}>
        {theme === 'dark' ? '[BLACK_SUN_WITH_RAYS]' : '[CRESCENT_MOON]'}
      </button>
    </nav>
  );
}

// Profond dans l'arbre, sans prop drilling :
function ArticleFooter() {
  const { theme } = useTheme();  // Direct !

  return (
    <footer style={{ color: theme === 'dark' ? 'white' : 'black' }}>
      Pied de page
    </footer>
  );
}

# ----------------------------------------------------------------------------
# [SECURISE] EXEMPLE COMPLET : CONTEXTE D'AUTHENTIFICATION
# ----------------------------------------------------------------------------

// src/context/AuthContext.jsx
import { createContext, useState, useContext, useEffect } from 'react';

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  // Vérifier si l'utilisateur est connecté au démarrage
  useEffect(() => {
    const savedUser = localStorage.getItem('user');
    if (savedUser) {
      try {
        setUser(JSON.parse(savedUser));
      } catch {
        localStorage.removeItem('user');
      }
    }
    setLoading(false);
  }, []);

  const login = async (email, password) => {
    // Simuler appel API
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password })
    });
    const userData = await response.json();

    setUser(userData);
    localStorage.setItem('user', JSON.stringify(userData));
    return userData;
  };

  const logout = () => {
    setUser(null);
    localStorage.removeItem('user');
  };

  const isAuthenticated = !!user;
  const isAdmin = user?.role === 'admin';

  const value = {
    user,
    loading,
    isAuthenticated,
    isAdmin,
    login,
    logout
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
}

// Hook personnalisé
export function useAuth() {
  const context = useContext(AuthContext);
  if (!context) throw new Error('useAuth doit être dans AuthProvider');
  return context;
}

// ===== UTILISATION =====

function App() {
  return (
    <AuthProvider>
      <Router>
        <Navbar />
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
        </Routes>
      </Router>
    </AuthProvider>
  );
}

function Navbar() {
  const { user, isAuthenticated, logout } = useAuth();
  return (
    <nav>
      {isAuthenticated ? (
        <>
          <span>Bonjour {user.name}</span>
          <button onClick={logout}>Déconnexion</button>
        </>
      ) : (
        <a href="/login">Connexion</a>
      )}
    </nav>
  );
}

function ProtectedRoute({ children }) {
  const { isAuthenticated, loading } = useAuth();

  if (loading) return <div>Chargement...</div>;
  if (!isAuthenticated) return <Navigate to="/login" />;
  return children;
}

# ----------------------------------------------------------------------------
# [MONDE] EXEMPLE : CONTEXTE DE LANGUE (i18n)
# ----------------------------------------------------------------------------

// src/context/LangContext.jsx
import { createContext, useState, useContext } from 'react';

const translations = {
  fr: {
    welcome: 'Bienvenue',
    login: 'Se connecter',
    logout: 'Se déconnecter',
    save: 'Sauvegarder',
    cancel: 'Annuler',
  },
  en: {
    welcome: 'Welcome',
    login: 'Log in',
    logout: 'Log out',
    save: 'Save',
    cancel: 'Cancel',
  },
  es: {
    welcome: 'Bienvenido',
    login: 'Iniciar sesión',
    logout: 'Cerrar sesión',
    save: 'Guardar',
    cancel: 'Cancelar',
  }
};

const LangContext = createContext(null);

export function LangProvider({ children }) {
  const [lang, setLang] = useState('fr');

  const t = (key) => translations[lang][key] || key;

  return (
    <LangContext.Provider value={{ lang, setLang, t }}>
      {children}
    </LangContext.Provider>
  );
}

export const useLang = () => useContext(LangContext);

// Utilisation :
function Header() {
  const { lang, setLang, t } = useLang();
  return (
    <header>
      <h1>{t('welcome')}</h1>
      <select value={lang} onChange={e => setLang(e.target.value)}>
        <option value="fr">[FR] Français</option>
        <option value="en">[GB] English</option>
        <option value="es">[ES] Español</option>
      </select>
    </header>
  );
}

# ----------------------------------------------------------------------------
# [ATTENTION] QUAND NE PAS UTILISER CONTEXT
# ----------------------------------------------------------------------------

"""
Context NE REMPLACE PAS TOUT !

[OK] BON POUR :
- Thème (dark/light)
- Utilisateur connecté
- Langue / internationalisation
- Données vraiment globales

[X] MAUVAIS POUR :
- État qui change fréquemment (formulaire, animations)
  -> Chaque changement re-rend TOUS les consommateurs !
- État local à quelques composants proches
  -> Props suffisent pour 2-3 niveaux

[IDEE] ALTERNATIVES POUR ÉTAT COMPLEXE :
- Zustand (léger et simple)
- Redux Toolkit (complexe et puissant)
- Jotai (atomique)
"""

# ============================================================================
# [GUIDE] CHAPITRE 8 : useRef, useMemo, useCallback
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] useRef pour accéder au DOM et valeurs persistantes
[OK] useMemo pour mémoriser calculs coûteux
[OK] useCallback pour mémoriser fonctions
[OK] Quand utiliser chacun
"""

# ----------------------------------------------------------------------------
# [IMPORTANT] useRef
# ----------------------------------------------------------------------------

"""
useRef = Boîte qui PERSISTE entre les rendus, ne déclenche PAS de re-rendu

DEUX USAGES PRINCIPAUX :
1. Accéder directement à un élément DOM
2. Stocker une valeur sans déclencher de re-rendu
"""

import { useRef, useEffect, useState } from 'react';

"""
USAGE 1 : Accès au DOM
"""

function AutoFocusInput() {
  const inputRef = useRef(null);  // Référence au DOM

  // Focus automatique au montage
  useEffect(() => {
    inputRef.current.focus();  // Accès direct à l'élément DOM !
  }, []);

  return (
    <input
      ref={inputRef}  // Attacher la ref
      placeholder="Je suis focus automatiquement"
    />
  );
}

// Exemple plus complet : Player vidéo
function VideoPlayer({ src }) {
  const videoRef = useRef(null);
  const [isPlaying, setIsPlaying] = useState(false);

  const togglePlay = () => {
    if (isPlaying) {
      videoRef.current.pause();
    } else {
      videoRef.current.play();
    }
    setIsPlaying(!isPlaying);
  };

  return (
    <div>
      <video ref={videoRef} src={src} width={400} />
      <button onClick={togglePlay}>
        {isPlaying ? '[DOUBLE_VERTICAL_BAR] Pause' : '[BLACK_RIGHT-POINTING_TRIANGLE] Play'}
      </button>
    </div>
  );
}

"""
USAGE 2 : Valeur persistante SANS re-rendu
"""

// Compter les rendus
function RenderCounter() {
  const renderCount = useRef(0);  // Persiste, ne déclenche pas re-rendu
  const [count, setCount] = useState(0);

  renderCount.current += 1;  // Incrémenter à chaque rendu

  return (
    <div>
      <p>Count : {count}</p>
      <p>Rendus : {renderCount.current}</p>
      <button onClick={() => setCount(c => c + 1)}>Incrémenter</button>
    </div>
  );
}

// Stocker valeur précédente
function PreviousValue({ value }) {
  const prevValue = useRef(undefined);

  useEffect(() => {
    prevValue.current = value;  // Sauvegarder après chaque rendu
  });

  return (
    <p>
      Actuel : {value} | Précédent : {prevValue.current ?? 'aucun'}
    </p>
  );
}

// Stocker timer ID pour le nettoyer
function AutoSave({ data }) {
  const timerRef = useRef(null);
  const [saved, setSaved] = useState(false);

  useEffect(() => {
    // Annuler timer précédent
    if (timerRef.current) clearTimeout(timerRef.current);

    timerRef.current = setTimeout(() => {
      console.log('Sauvegarde...', data);
      setSaved(true);
    }, 2000);

    return () => clearTimeout(timerRef.current);
  }, [data]);

  return <span>{saved ? '[OK] Sauvegardé' : '[HOURGLASS_WITH_FLOWING_SAND] Sauvegarde en cours...'}</span>;
}

"""
RÉSUMÉ useRef :

         | Déclenche re-rendu | Persiste |
useState |        [OK]          |    [OK]    |
useRef   |        [X]          |    [OK]    |

-> Utiliser useState pour afficher la valeur
-> Utiliser useRef pour accès DOM ou valeur "interne"
"""

# ----------------------------------------------------------------------------
# [CALCUL] useMemo
# ----------------------------------------------------------------------------

"""
useMemo = Mémoriser le RÉSULTAT d'un calcul coûteux
         Recalculé seulement si les dépendances changent

SYNTAXE :
const memoValue = useMemo(() => expensiveCalc(a, b), [a, b]);
"""

"""
SANS useMemo :
"""

function ProductStats({ products, searchQuery }) {
  // [X] Recalculé à CHAQUE rendu (même si products n'a pas changé!)
  const stats = calculateStats(products);  // Supposons que c'est lent (1000 produits)

  return <div>{stats.totalRevenue}</div>;
}

"""
AVEC useMemo :
"""

import { useMemo } from 'react';

function ProductStats({ products, searchQuery }) {
  // [OK] Recalculé SEULEMENT si products change
  const stats = useMemo(() => {
    console.log('Calcul des stats...');
    return {
      total: products.length,
      totalRevenue: products.reduce((sum, p) => sum + p.price * p.quantity, 0),
      avgPrice: products.reduce((sum, p) => sum + p.price, 0) / products.length,
      outOfStock: products.filter(p => p.stock === 0).length,
    };
  }, [products]); // Recalculer seulement si products change

  // searchQuery change -> stats PAS recalculé ! [OK]

  const filteredProducts = useMemo(() => {
    return products.filter(p =>
      p.name.toLowerCase().includes(searchQuery.toLowerCase())
    );
  }, [products, searchQuery]); // Recalculer si products OU searchQuery change

  return (
    <div>
      <p>Total : {stats.total} | CA : {stats.totalRevenue}€</p>
      <ul>
        {filteredProducts.map(p => <li key={p.id}>{p.name}</li>)}
      </ul>
    </div>
  );
}

"""
QUAND UTILISER useMemo ?

[OK] UTILISER quand :
- Calcul réellement coûteux (liste de milliers d'éléments)
- Tableau/objet passé comme prop à un composant mémoïsé (React.memo)
- Dépendance d'un autre useMemo ou useCallback

[X] NE PAS utiliser pour :
- Calculs simples (addition, concatenation)
- Tout par principe -> Le memoization a un coût !
"""

# ----------------------------------------------------------------------------
# [OBJECTIF] useCallback
# ----------------------------------------------------------------------------

"""
useCallback = Mémoriser une FONCTION
            Même référence si dépendances n'ont pas changé

POURQUOI ?
En JavaScript, fonctions créées au rendu = nouvelle référence à chaque fois
-> Problème avec React.memo (voir Chapitre 14)
"""

import { useCallback } from 'react';

"""
EXEMPLE :
"""

function ParentComponent({ userId }) {
  const [items, setItems] = useState([]);

  // [X] Sans useCallback : nouvelle fonction à chaque rendu
  // const handleDelete = (id) => {
  //   setItems(items.filter(item => item.id !== id));
  // };

  // [OK] Avec useCallback : même référence si items ne change pas
  const handleDelete = useCallback((id) => {
    setItems(prev => prev.filter(item => item.id !== id));
  }, []); // Tableau vide car on utilise la forme fonctionnelle

  const handleAdd = useCallback((newItem) => {
    setItems(prev => [...prev, { id: Date.now(), ...newItem }]);
  }, []); // Stable, jamais recréée

  return (
    <div>
      {items.map(item => (
        <MemoizedItem key={item.id} item={item} onDelete={handleDelete} />
      ))}
      <AddItemForm onAdd={handleAdd} />
    </div>
  );
}

// Ce composant ne re-rend PAS si ses props n'ont pas changé
const MemoizedItem = React.memo(function Item({ item, onDelete }) {
  console.log('Rendu de', item.name);
  return (
    <div>
      <span>{item.name}</span>
      <button onClick={() => onDelete(item.id)}>[X]</button>
    </div>
  );
});

"""
COMPARAISON useMemo vs useCallback :

useMemo(() => calcul(), [deps])
-> Mémorise le RÉSULTAT d'un calcul

useCallback(() => fonction(), [deps])
-> Mémorise une FONCTION (identité référentielle)

Relation :
useCallback(fn, deps) = useMemo(() => fn, deps)
"""

# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 6 : DASHBOARD DE DONNÉES
# ============================================================================

"""
OBJECTIF : Dashboard utilisant useMemo, useCallback, useRef, et Context

FONCTIONNALITÉS :
1. Context pour le thème (clair/sombre)
2. Liste de transactions avec recherche
3. Statistiques mémoïsées (useMemo)
4. Input avec focus auto (useRef)
5. Handler de suppression stable (useCallback)
6. Graphique simple des données

CORRIGÉ :
"""

// src/context/ThemeContext.jsx
import { createContext, useState, useContext } from 'react';
const ThemeCtx = createContext(null);
export const useThemeCtx = () => useContext(ThemeCtx);
export function ThemeProvider({ children }) {
  const [isDark, setIsDark] = useState(false);
  return (
    <ThemeCtx.Provider value={{ isDark, toggle: () => setIsDark(d => !d) }}>
      {children}
    </ThemeCtx.Provider>
  );
}

// src/components/Dashboard.jsx
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
import { ThemeProvider, useThemeCtx } from '../context/ThemeContext';

const TRANSACTIONS = Array.from({ length: 20 }, (_, i) => ({
  id: i + 1,
  description: ['Loyer', 'Salaire', 'Courses', 'Netflix', 'Électricité', 'Restaurant'][i % 6],
  amount: Math.round((Math.random() * 2000 - 1000) * 100) / 100,
  date: new Date(2024, Math.floor(i/4), (i % 28) + 1).toLocaleDateString('fr-FR'),
  category: ['logement', 'revenus', 'alimentation', 'loisirs', 'factures', 'restaurant'][i % 6],
}));

function DashboardContent() {
  const { isDark, toggle } = useThemeCtx();
  const [transactions, setTransactions] = useState(TRANSACTIONS);
  const [search, setSearch] = useState('');
  const searchRef = useRef(null);

  // Focus auto sur le champ de recherche
  useEffect(() => {
    searchRef.current?.focus();
  }, []);

  // Statistiques mémoïsées
  const stats = useMemo(() => {
    const income = transactions.filter(t => t.amount > 0).reduce((s, t) => s + t.amount, 0);
    const expenses = transactions.filter(t => t.amount < 0).reduce((s, t) => s + t.amount, 0);
    return {
      income: income.toFixed(2),
      expenses: Math.abs(expenses).toFixed(2),
      balance: (income + expenses).toFixed(2),
      count: transactions.length,
    };
  }, [transactions]);

  // Filtrage mémoïsé
  const filteredTransactions = useMemo(() => {
    return transactions.filter(t =>
      t.description.toLowerCase().includes(search.toLowerCase()) ||
      t.category.toLowerCase().includes(search.toLowerCase())
    );
  }, [transactions, search]);

  // Handler stable avec useCallback
  const handleDelete = useCallback((id) => {
    setTransactions(prev => prev.filter(t => t.id !== id));
  }, []);

  const bg = isDark ? '#1a1a2e' : '#f0f4f8';
  const cardBg = isDark ? '#16213e' : 'white';
  const text = isDark ? '#e0e0e0' : '#1a202c';
  const subText = isDark ? '#9ca3af' : '#718096';
  const border = isDark ? '#2d3748' : '#e2e8f0';

  return (
    <div style={{ background: bg, minHeight: '100vh', padding: '24px', color: text, transition: 'all 0.3s' }}>
      {/* Header */}
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '24px' }}>
        <h1 style={{ margin: 0, fontSize: '24px' }}>[ARGENT] Dashboard Financier</h1>
        <button onClick={toggle} style={{ background: 'none', border: '1px solid', borderColor: border, borderRadius: '8px', padding: '8px 16px', cursor: 'pointer', color: text }}>
          {isDark ? '[BLACK_SUN_WITH_RAYS] Clair' : '[CRESCENT_MOON] Sombre'}
        </button>
      </div>

      {/* Stats */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px', marginBottom: '24px' }}>
        {[
          { label: '[GREEN_HEART] Revenus', value: `+${stats.income}€`, color: '#22c55e' },
          { label: '[HEAVY_BLACK_HEART] Dépenses', value: `-${stats.expenses}€`, color: '#ef4444' },
          { label: '[BLUE_HEART] Balance', value: `${parseFloat(stats.balance) >= 0 ? '+' : ''}${stats.balance}€`, color: parseFloat(stats.balance) >= 0 ? '#3b82f6' : '#ef4444' },
        ].map(({ label, value, color }) => (
          <div key={label} style={{ background: cardBg, borderRadius: '12px', padding: '20px', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
            <div style={{ fontSize: '13px', color: subText }}>{label}</div>
            <div style={{ fontSize: '24px', fontWeight: 'bold', color, marginTop: '4px' }}>{value}</div>
          </div>
        ))}
      </div>

      {/* Recherche */}
      <input
        ref={searchRef}
        value={search}
        onChange={e => setSearch(e.target.value)}
        placeholder={`[RECHERCHE] Filtrer ${stats.count} transactions...`}
        style={{
          width: '100%', padding: '12px 16px', borderRadius: '10px',
          border: `1px solid ${border}`, background: cardBg, color: text,
          fontSize: '14px', outline: 'none', boxSizing: 'border-box', marginBottom: '16px'
        }}
      />

      {/* Transactions */}
      <div style={{ background: cardBg, borderRadius: '12px', overflow: 'hidden', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}>
        {filteredTransactions.length === 0 ? (
          <p style={{ textAlign: 'center', padding: '24px', color: subText }}>Aucune transaction trouvée</p>
        ) : (
          filteredTransactions.map(t => (
            <div key={t.id} style={{
              display: 'flex', alignItems: 'center', padding: '14px 16px',
              borderBottom: `1px solid ${border}`, gap: '12px'
            }}>
              <div style={{ flex: 1 }}>
                <div style={{ fontWeight: '500' }}>{t.description}</div>
                <div style={{ fontSize: '12px', color: subText }}>{t.date} • {t.category}</div>
              </div>
              <div style={{ fontWeight: 'bold', color: t.amount >= 0 ? '#22c55e' : '#ef4444', minWidth: '80px', textAlign: 'right' }}>
                {t.amount >= 0 ? '+' : ''}{t.amount}€
              </div>
              <button
                onClick={() => handleDelete(t.id)}
                style={{ background: 'none', border: 'none', cursor: 'pointer', color: subText, fontSize: '16px', padding: '4px 8px', borderRadius: '4px' }}
              >
                [X]
              </button>
            </div>
          ))
        )}
      </div>

      <div style={{ textAlign: 'right', marginTop: '8px', fontSize: '12px', color: subText }}>
        {filteredTransactions.length} / {stats.count} transactions affichées
      </div>
    </div>
  );
}

function Dashboard() {
  return (
    <ThemeProvider>
      <DashboardContent />
    </ThemeProvider>
  );
}

export default Dashboard;

"""
[OK] CE QUE VOUS AVEZ PRATIQUÉ :
- useContext (thème clair/sombre)
- useMemo (statistiques, filtrage)
- useCallback (handleDelete stable)
- useRef (focus automatique)
- Composition de hooks
- Performance consciente
"""

# ============================================================================
# [DOCS] RÉCAPITULATIF PARTIE 2
# ============================================================================

"""
[BRAVO] FÉLICITATIONS ! PARTIE 2 TERMINÉE !

VOUS MAÎTRISEZ MAINTENANT :

Chapitre 5 : useState avancé
[OK] Plusieurs états indépendants vs état objet
[OK] Lazy initialization (performance)
[OK] État dérivé (ne pas stocker ce qu'on peut calculer)

Chapitre 6 : useEffect
[OK] Effets de bord (API, timers, événements)
[OK] Tableau de dépendances (quand s'exécute l'effet)
[OK] Cleanup (éviter memory leaks)
[OK] Patterns : fetch, debounce, localStorage, événements DOM

Chapitre 7 : useContext
[OK] Problème prop drilling
[OK] createContext, Provider, useContext
[OK] Contexte authentification
[OK] Contexte thème, langue

Chapitre 8 : useRef, useMemo, useCallback
[OK] useRef -> Accès DOM + valeur persistante sans re-rendu
[OK] useMemo -> Mémoriser résultat de calcul coûteux
[OK] useCallback -> Mémoriser référence de fonction
[OK] Savoir QUAND les utiliser (pas tout le temps!)

-> PROCHAINE ÉTAPE : PARTIE 3 - Fonctionnalités Avancées !
"""

# ============================================================================
# [SCIENCE] REACT - PARTIE 3 : FONCTIONNALITÉS AVANCÉES
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 9  : React Router - Navigation SPA
# - Chapitre 10 : Formulaires Contrôlés & React Hook Form
# - Chapitre 11 : Appels API (fetch, axios, React Query)
# - Chapitre 12 : Custom Hooks - Abstraire la Logique
# - Chapitre 13 : Gestion d'État Avancée (useReducer + Context)
#
# [TEMPS] TEMPS : ~10-12 heures
# [DOCS] PRÉREQUIS : Parties 1 et 2 complétées
# ============================================================================

# ============================================================================
# [GUIDE] CHAPITRE 9 : REACT ROUTER - NAVIGATION SPA
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Ce qu'est le routing côté client (SPA)
[OK] Installer et configurer React Router v6
[OK] Routes imbriquées et layouts
[OK] Navigation programmatique
[OK] Routes dynamiques (paramètres URL)
[OK] Routes protégées (authentification)
[OK] Query strings et state de navigation
"""

# ----------------------------------------------------------------------------
# [REFLEXION] QU'EST-CE QU'UNE SPA ET LE ROUTING CLIENT ?
# ----------------------------------------------------------------------------

"""
SPA = Single Page Application

SITE CLASSIQUE (Multi-pages) :
Navigation -> Nouveau fichier HTML téléchargé du serveur
-> Rechargement complet de la page (lent)


SPA (React) :
Navigation -> JavaScript change le contenu sans recharger
-> Pas de requête serveur pour chaque page (rapide)
-> React Router gère QUELLE url affiche QUEL composant


ANALOGIE :
Site classique = Changer de livre physique (lent)
SPA           = Tourner une page d'un livre (rapide)
"""

# ----------------------------------------------------------------------------
# [OUTILS] INSTALLATION ET CONFIGURATION
# ----------------------------------------------------------------------------

"""
INSTALLATION :
"""

$ npm install react-router-dom

"""
CONFIGURATION DE BASE :
"""

// src/main.jsx
import { BrowserRouter } from 'react-router-dom';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <BrowserRouter>  {/* Envelopper l'app */}
      <App />
    </BrowserRouter>
  </React.StrictMode>
);

// src/App.jsx
import { Routes, Route } from 'react-router-dom';

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      <Route path="/contact" element={<Contact />} />
      <Route path="*" element={<NotFound />} />  {/* 404 */}
    </Routes>
  );
}

"""
COMPOSANTS REACT ROUTER PRINCIPAUX :

BrowserRouter      -> Fournit le contexte routing (dans main.jsx)
Routes             -> Conteneur de routes
Route              -> Associe URL -> Composant
Link               -> Lien de navigation (pas <a> !)
NavLink            -> Link avec style actif automatique
Navigate           -> Redirection programmatique
Outlet             -> Où afficher les routes enfants
useNavigate()      -> Navigation programmatique
useParams()        -> Lire paramètres URL (:id)
useSearchParams()  -> Lire query string (?q=react)
useLocation()      -> URL actuelle complète
"""

# ----------------------------------------------------------------------------
# [LIEN] NAVIGATION AVEC LINK ET NAVLINK
# ----------------------------------------------------------------------------

"""
Link = Remplacement de <a> (sans rechargement)
"""

import { Link, NavLink } from 'react-router-dom';

function Navbar() {
  return (
    <nav>
      {/* Link simple */}
      <Link to="/">Accueil</Link>
      <Link to="/products">Produits</Link>
      <Link to="/contact">Contact</Link>

      {/* NavLink avec style actif */}
      <NavLink
        to="/about"
        style={({ isActive }) => ({
          color: isActive ? '#3b82f6' : 'inherit',
          fontWeight: isActive ? 'bold' : 'normal',
        })}
      >
        À propos
      </NavLink>

      {/* NavLink avec className */}
      <NavLink
        to="/profile"
        className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
      >
        Profil
      </NavLink>
    </nav>
  );
}

"""
[ATTENTION] TOUJOURS utiliser <Link> / <NavLink> au lieu de <a>
<a href="/page"> -> Rechargement complet de la page !
<Link to="/page"> -> Navigation sans rechargement [OK]
"""

# ----------------------------------------------------------------------------
# [MELANGE] ROUTES DYNAMIQUES AVEC PARAMÈTRES
# ----------------------------------------------------------------------------

"""
URL dynamique : /products/123 -> product.id = '123'
"""

// Définir route avec paramètre
<Route path="/products/:productId" element={<ProductDetail />} />
<Route path="/users/:userId/posts/:postId" element={<PostDetail />} />

// Lire paramètre dans composant
import { useParams } from 'react-router-dom';

function ProductDetail() {
  const { productId } = useParams();  // '123' (toujours string !)

  const numericId = parseInt(productId, 10);  // Convertir si besoin

  const [product, setProduct] = useState(null);

  useEffect(() => {
    fetch(`/api/products/${productId}`)
      .then(r => r.json())
      .then(setProduct);
  }, [productId]);  // Re-fetch si productId change

  if (!product) return <div>Chargement...</div>;

  return (
    <div>
      <h1>{product.name}</h1>
      <p>ID : {productId}</p>
      <Link to="/products"><- Retour aux produits</Link>
    </div>
  );
}

# ----------------------------------------------------------------------------
# [MESURE] ROUTES IMBRIQUÉES ET LAYOUTS
# ----------------------------------------------------------------------------

"""
Routes imbriquées = Partager un layout entre plusieurs pages
"""

// App.jsx
import { Routes, Route, Outlet } from 'react-router-dom';

function App() {
  return (
    <Routes>
      {/* Route layout - affiche Layout + Outlet */}
      <Route path="/" element={<Layout />}>
        {/* Routes enfants - affichées dans Outlet */}
        <Route index element={<Home />} />           {/* "/" */}
        <Route path="about" element={<About />} />   {/* "/about" */}
        <Route path="products" element={<Products />} />

        {/* Routes imbriquées */}
        <Route path="products/:id" element={<ProductDetail />} />

        {/* Zone admin avec son propre layout */}
        <Route path="admin" element={<AdminLayout />}>
          <Route index element={<AdminDashboard />} />
          <Route path="users" element={<AdminUsers />} />
          <Route path="settings" element={<AdminSettings />} />
        </Route>
      </Route>

      {/* Route sans layout */}
      <Route path="/login" element={<LoginPage />} />
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

// Layout principal
function Layout() {
  return (
    <div>
      <Navbar />    {/* Affiché sur toutes les pages */}
      <main>
        <Outlet />  {/* Ici s'affiche la page active */}
      </main>
      <Footer />    {/* Affiché sur toutes les pages */}
    </div>
  );
}

// Layout Admin
function AdminLayout() {
  const { isAdmin } = useAuth();
  if (!isAdmin) return <Navigate to="/" />;  // Redirection si pas admin

  return (
    <div style={{ display: 'flex' }}>
      <AdminSidebar />
      <div style={{ flex: 1 }}>
        <Outlet />  {/* Routes admin ici */}
      </div>
    </div>
  );
}

# ----------------------------------------------------------------------------
# [RAPIDE] NAVIGATION PROGRAMMATIQUE
# ----------------------------------------------------------------------------

"""
useNavigate = Naviguer depuis du code (pas un clic)
"""

import { useNavigate } from 'react-router-dom';

function LoginForm() {
  const navigate = useNavigate();

  const handleLogin = async (credentials) => {
    const success = await login(credentials);
    if (success) {
      navigate('/dashboard');            // Aller vers /dashboard
      navigate('/dashboard', { replace: true }); // Remplacer historique (pas de "retour")
      navigate(-1);                      // Retour (comme bouton retour)
      navigate(1);                       // Avancer
    }
  };

  // Passer des données via state (invisible dans l'URL)
  const goToProduct = (product) => {
    navigate(`/products/${product.id}`, {
      state: { product, fromCart: true }  // Données supplémentaires
    });
  };

  return <form onSubmit={handleLogin}>...</form>;
}

// Lire le state à la destination
function ProductDetail() {
  const location = useLocation();
  const fromCart = location.state?.fromCart;  // Optionnel, peut être undefined

  return (
    <div>
      {fromCart && <p><- Retour depuis le panier</p>}
    </div>
  );
}

# ----------------------------------------------------------------------------
# [RECHERCHE] QUERY STRINGS
# ----------------------------------------------------------------------------

"""
Query string = Paramètres dans l'URL après ?
Ex: /products?category=tech&sort=price&page=2
"""

import { useSearchParams } from 'react-router-dom';

function ProductsPage() {
  const [searchParams, setSearchParams] = useSearchParams();

  // Lire les paramètres
  const category = searchParams.get('category') || 'all';
  const sort = searchParams.get('sort') || 'name';
  const page = parseInt(searchParams.get('page') || '1');

  // Modifier les paramètres
  const handleCategoryChange = (newCategory) => {
    setSearchParams(prev => {
      prev.set('category', newCategory);
      prev.set('page', '1');  // Reset page
      return prev;
    });
  };

  return (
    <div>
      <select
        value={category}
        onChange={e => handleCategoryChange(e.target.value)}
      >
        <option value="all">Toutes catégories</option>
        <option value="tech">Technologie</option>
        <option value="books">Livres</option>
      </select>

      {/* Les filtres sont dans l'URL -> Partageable ! */}
      <p>URL: /products?category={category}&sort={sort}&page={page}</p>
    </div>
  );
}

# ----------------------------------------------------------------------------
# [VERROUILLE] ROUTES PROTÉGÉES
# ----------------------------------------------------------------------------

"""
Routes qui nécessitent d'être connecté
"""

function ProtectedRoute({ children }) {
  const { isAuthenticated, loading } = useAuth();
  const location = useLocation();

  if (loading) return <LoadingSpinner />;

  if (!isAuthenticated) {
    // Sauvegarder l'URL demandée pour rediriger après login
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return children;
}

// Route admin
function AdminRoute({ children }) {
  const { isAuthenticated, isAdmin } = useAuth();

  if (!isAuthenticated) return <Navigate to="/login" replace />;
  if (!isAdmin) return <Navigate to="/unauthorized" replace />;

  return children;
}

// Utilisation
function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/login" element={<Login />} />

      {/* Routes protégées */}
      <Route path="/dashboard" element={
        <ProtectedRoute>
          <Dashboard />
        </ProtectedRoute>
      } />

      <Route path="/admin/*" element={
        <AdminRoute>
          <AdminPanel />
        </AdminRoute>
      } />
    </Routes>
  );
}

// Login qui redirige vers l'url d'origine
function LoginPage() {
  const navigate = useNavigate();
  const location = useLocation();
  const from = location.state?.from?.pathname || '/dashboard';

  const handleLogin = async (credentials) => {
    await login(credentials);
    navigate(from, { replace: true });  // Retourner là où on voulait aller
  };

  return <LoginForm onLogin={handleLogin} />;
}

# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 7 : APPLICATION MULTI-PAGES
# ============================================================================

"""
OBJECTIF : Application e-commerce avec navigation complète

PAGES À CRÉER :
1. / -> HomePage (liste des produits avec filtre)
2. /products/:id -> ProductPage (détail produit)
3. /cart -> CartPage (panier)
4. /login -> LoginPage
5. /profile -> ProfilePage (protégée)

FONCTIONNALITÉS :
- Navigation avec NavLink (actif coloré)
- Compteur dans l'icône panier (Context)
- Route protégée /profile
- Paramètre URL pour le produit
- Query string pour le filtre

CORRIGÉ SIMPLIFIÉ :
"""

// src/context/CartContext.jsx
import { createContext, useState, useContext } from 'react';
const CartCtx = createContext(null);
export const useCart = () => useContext(CartCtx);

export function CartProvider({ children }) {
  const [cartItems, setCartItems] = useState([]);

  const addToCart = (product) => {
    setCartItems(prev => {
      const existing = prev.find(item => item.id === product.id);
      if (existing) {
        return prev.map(item => item.id === product.id
          ? { ...item, quantity: item.quantity + 1 }
          : item
        );
      }
      return [...prev, { ...product, quantity: 1 }];
    });
  };

  const removeFromCart = (productId) => {
    setCartItems(prev => prev.filter(item => item.id !== productId));
  };

  const totalItems = cartItems.reduce((sum, item) => sum + item.quantity, 0);
  const totalPrice = cartItems.reduce((sum, item) => sum + item.price * item.quantity, 0);

  return (
    <CartCtx.Provider value={{ cartItems, addToCart, removeFromCart, totalItems, totalPrice }}>
      {children}
    </CartCtx.Provider>
  );
}

// src/App.jsx
import { BrowserRouter, Routes, Route, Link, NavLink, useParams, useSearchParams, Navigate } from 'react-router-dom';
import { CartProvider, useCart } from './context/CartContext';

const PRODUCTS = [
  { id: 1, name: 'Laptop Pro 16"', price: 1299, category: 'tech', rating: 4.8, image: '[CODE]' },
  { id: 2, name: 'AirPods Pro', price: 249, category: 'tech', rating: 4.6, image: '[AUDIO]' },
  { id: 3, name: 'Clean Code', price: 35, category: 'books', rating: 4.9, image: '[DOCS]' },
  { id: 4, name: 'Clavier Mécanique', price: 149, category: 'tech', rating: 4.7, image: '[KEYBOARD]' },
  { id: 5, name: 'React Design Patterns', price: 45, category: 'books', rating: 4.5, image: '[GUIDE]' },
  { id: 6, name: 'Moniteur 4K', price: 599, category: 'tech', rating: 4.4, image: '[ECRAN]' },
];

// Navbar
function Navbar() {
  const { totalItems } = useCart();
  const navStyle = { display: 'flex', gap: '24px', alignItems: 'center', padding: '16px 32px', background: '#1e293b', color: 'white' };
  const linkStyle = ({ isActive }) => ({
    color: isActive ? '#60a5fa' : '#e2e8f0', textDecoration: 'none', fontWeight: isActive ? '600' : '400'
  });
  return (
    <nav style={navStyle}>
      <span style={{ fontWeight: 'bold', fontSize: '18px' }}>[SCIENCE] ReactShop</span>
      <div style={{ display: 'flex', gap: '20px', marginLeft: 'auto' }}>
        <NavLink to="/" end style={linkStyle}>[ACCUEIL] Accueil</NavLink>
        <NavLink to="/cart" style={linkStyle}>[SHOPPING_TROLLEY] Panier {totalItems > 0 && <sup style={{ background: '#ef4444', color: 'white', borderRadius: '50%', padding: '2px 6px', fontSize: '11px' }}>{totalItems}</sup>}</NavLink>
        <NavLink to="/profile" style={linkStyle}>[UTILISATEUR] Profil</NavLink>
        <NavLink to="/login" style={linkStyle}>[CLE] Login</NavLink>
      </div>
    </nav>
  );
}

// HomePage avec filtres
function HomePage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const category = searchParams.get('category') || 'all';
  const { addToCart } = useCart();

  const filtered = category === 'all' ? PRODUCTS : PRODUCTS.filter(p => p.category === category);

  return (
    <div style={{ padding: '32px', maxWidth: '900px', margin: '0 auto' }}>
      <h1>Nos Produits</h1>
      <div style={{ display: 'flex', gap: '8px', marginBottom: '24px' }}>
        {['all', 'tech', 'books'].map(cat => (
          <button key={cat} onClick={() => setSearchParams({ category: cat })}
            style={{ padding: '6px 14px', borderRadius: '20px', border: 'none', cursor: 'pointer',
              background: category === cat ? '#3b82f6' : '#e5e7eb', color: category === cat ? 'white' : '#374151' }}>
            {{ all: '[MONDE] Tous', tech: '[CODE] Tech', books: '[DOCS] Livres' }[cat]}
          </button>
        ))}
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(250px, 1fr))', gap: '16px' }}>
        {filtered.map(p => (
          <div key={p.id} style={{ background: 'white', borderRadius: '12px', padding: '20px', boxShadow: '0 2px 8px rgba(0,0,0,0.08)' }}>
            <div style={{ fontSize: '48px', textAlign: 'center', marginBottom: '12px' }}>{p.image}</div>
            <h3 style={{ margin: '0 0 4px' }}>{p.name}</h3>
            <p style={{ color: '#64748b', fontSize: '13px' }}>* {p.rating} | {p.category}</p>
            <p style={{ fontWeight: 'bold', fontSize: '20px', color: '#3b82f6' }}>{p.price}€</p>
            <div style={{ display: 'flex', gap: '8px' }}>
              <Link to={`/products/${p.id}`} style={{ flex: 1, textAlign: 'center', padding: '8px', background: '#f1f5f9', borderRadius: '8px', textDecoration: 'none', color: '#374151', fontSize: '13px' }}>Détails</Link>
              <button onClick={() => addToCart(p)} style={{ flex: 1, padding: '8px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer', fontSize: '13px' }}>+ Panier</button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ProductPage
function ProductPage() {
  const { id } = useParams();
  const { addToCart } = useCart();
  const product = PRODUCTS.find(p => p.id === parseInt(id));

  if (!product) return <div style={{ padding: '32px' }}>Produit non trouvé ! <Link to="/"><- Retour</Link></div>;

  return (
    <div style={{ padding: '32px', maxWidth: '600px', margin: '0 auto' }}>
      <Link to="/"><- Retour</Link>
      <div style={{ textAlign: 'center', fontSize: '100px', margin: '24px 0' }}>{product.image}</div>
      <h1>{product.name}</h1>
      <p>* {product.rating} | Catégorie : {product.category}</p>
      <p style={{ fontSize: '32px', fontWeight: 'bold', color: '#3b82f6' }}>{product.price}€</p>
      <button onClick={() => addToCart(product)} style={{ padding: '14px 32px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '10px', cursor: 'pointer', fontSize: '16px', width: '100%' }}>
        [SHOPPING_TROLLEY] Ajouter au panier
      </button>
    </div>
  );
}

// CartPage
function CartPage() {
  const { cartItems, removeFromCart, totalPrice } = useCart();
  if (cartItems.length === 0) return <div style={{ padding: '32px', textAlign: 'center' }}><p>[SHOPPING_TROLLEY] Panier vide</p><Link to="/">Continuer les achats</Link></div>;
  return (
    <div style={{ padding: '32px', maxWidth: '600px', margin: '0 auto' }}>
      <h1>Mon Panier</h1>
      {cartItems.map(item => (
        <div key={item.id} style={{ display: 'flex', alignItems: 'center', gap: '16px', padding: '16px 0', borderBottom: '1px solid #e5e7eb' }}>
          <span style={{ fontSize: '32px' }}>{item.image}</span>
          <div style={{ flex: 1 }}><p style={{ margin: 0, fontWeight: '500' }}>{item.name}</p><p style={{ margin: 0, color: '#64748b', fontSize: '13px' }}>x{item.quantity}</p></div>
          <span style={{ fontWeight: 'bold' }}>{(item.price * item.quantity).toFixed(2)}€</span>
          <button onClick={() => removeFromCart(item.id)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#ef4444' }}>[X]</button>
        </div>
      ))}
      <div style={{ marginTop: '24px', textAlign: 'right' }}>
        <p style={{ fontSize: '24px', fontWeight: 'bold' }}>Total : {totalPrice.toFixed(2)}€</p>
        <button style={{ padding: '14px 32px', background: '#22c55e', color: 'white', border: 'none', borderRadius: '10px', cursor: 'pointer', fontSize: '16px' }}>[OK] Commander</button>
      </div>
    </div>
  );
}

// Fake auth
const isLoggedIn = false;
function ProtectedRoute({ children }) {
  return isLoggedIn ? children : <Navigate to="/login" replace />;
}

function App() {
  return (
    <CartProvider>
      <BrowserRouter>
        <Navbar />
        <Routes>
          <Route path="/" element={<HomePage />} />
          <Route path="/products/:id" element={<ProductPage />} />
          <Route path="/cart" element={<CartPage />} />
          <Route path="/login" element={<div style={{ padding: '32px', textAlign: 'center' }}><h1>[CLE] Connexion</h1><p>Formulaire ici...</p></div>} />
          <Route path="/profile" element={<ProtectedRoute><div style={{ padding: '32px' }}><h1>[UTILISATEUR] Profil</h1><p>Contenu protégé</p></div></ProtectedRoute>} />
          <Route path="*" element={<div style={{ padding: '32px', textAlign: 'center' }}><h1>404 [SMILING_FACE_WITH_OPEN_MOUTH_AND_COLD_SWEAT]</h1><Link to="/">Accueil</Link></div>} />
        </Routes>
      </BrowserRouter>
    </CartProvider>
  );
}

export default App;

# ============================================================================
# [GUIDE] CHAPITRE 10 : FORMULAIRES
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Formulaires contrôlés vs non contrôlés
[OK] Gérer tous types d'inputs
[OK] Validation manuelle
[OK] React Hook Form (bibliothèque)
[OK] Validation avec Zod
"""

# ----------------------------------------------------------------------------
# [VIDEO_GAME] FORMULAIRES CONTRÔLÉS
# ----------------------------------------------------------------------------

"""
Formulaire CONTRÔLÉ = React contrôle la valeur de l'input
-> La valeur est dans le state React
-> À chaque frappe, le state est mis à jour
-> React "dirige" l'input (source de vérité = React)
"""

function ControlledForm() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [message, setMessage] = useState('');
  const [agreed, setAgreed] = useState(false);
  const [country, setCountry] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();  // Empêcher rechargement !
    console.log({ name, email, message, agreed, country });
  };

  return (
    <form onSubmit={handleSubmit}>
      {/* Text input */}
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
        placeholder="Nom"
      />

      {/* Email input */}
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
      />

      {/* Textarea */}
      <textarea
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        rows={4}
      />

      {/* Checkbox */}
      <input
        type="checkbox"
        checked={agreed}
        onChange={(e) => setAgreed(e.target.checked)}  // <- .checked, pas .value !
      />

      {/* Select */}
      <select value={country} onChange={(e) => setCountry(e.target.value)}>
        <option value="">Choisir...</option>
        <option value="fr">France</option>
        <option value="be">Belgique</option>
        <option value="ch">Suisse</option>
      </select>

      <button type="submit">Envoyer</button>
    </form>
  );
}

"""
GÉRER PLUSIEURS INPUTS AVEC UN SEUL ÉTAT :
"""

function MultiInputForm() {
  const [formData, setFormData] = useState({
    firstName: '',
    lastName: '',
    email: '',
    phone: '',
    newsletter: false,
  });

  const handleChange = (e) => {
    const { name, value, type, checked } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: type === 'checkbox' ? checked : value  // Gérer checkbox
    }));
  };

  return (
    <form>
      {/* Utiliser 'name' correspondant à la clé du state */}
      <input name="firstName" value={formData.firstName} onChange={handleChange} />
      <input name="lastName"  value={formData.lastName}  onChange={handleChange} />
      <input name="email"     value={formData.email}     onChange={handleChange} />
      <input name="phone"     value={formData.phone}     onChange={handleChange} />
      <input name="newsletter" type="checkbox" checked={formData.newsletter} onChange={handleChange} />
    </form>
  );
}

# ----------------------------------------------------------------------------
# [OK] VALIDATION MANUELLE
# ----------------------------------------------------------------------------

function ValidatedForm() {
  const [formData, setFormData] = useState({ email: '', password: '', age: '' });
  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});  // Suivre les champs touchés

  const validate = (data) => {
    const newErrors = {};

    if (!data.email) {
      newErrors.email = 'Email requis';
    } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
      newErrors.email = 'Email invalide';
    }

    if (!data.password) {
      newErrors.password = 'Mot de passe requis';
    } else if (data.password.length < 8) {
      newErrors.password = 'Minimum 8 caractères';
    } else if (!/[A-Z]/.test(data.password)) {
      newErrors.password = 'Doit contenir une majuscule';
    }

    if (data.age && (isNaN(data.age) || parseInt(data.age) < 18)) {
      newErrors.age = 'Doit être majeur (18+)';
    }

    return newErrors;
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setFormData(prev => ({ ...prev, [name]: value }));

    // Valider en temps réel si champ déjà touché
    if (touched[name]) {
      const newErrors = validate({ ...formData, [name]: value });
      setErrors(prev => ({ ...prev, [name]: newErrors[name] }));
    }
  };

  const handleBlur = (e) => {
    const { name } = e.target;
    setTouched(prev => ({ ...prev, [name]: true }));
    const newErrors = validate(formData);
    setErrors(prev => ({ ...prev, [name]: newErrors[name] }));
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    const newErrors = validate(formData);
    setErrors(newErrors);
    setTouched({ email: true, password: true, age: true });

    if (Object.keys(newErrors).length === 0) {
      console.log('[OK] Formulaire valide :', formData);
    }
  };

  const isValid = Object.keys(validate(formData)).length === 0;

  return (
    <form onSubmit={handleSubmit} style={{ maxWidth: '400px' }}>
      {['email', 'password', 'age'].map(field => (
        <div key={field} style={{ marginBottom: '16px' }}>
          <label style={{ display: 'block', marginBottom: '4px', fontWeight: '500' }}>
            {field.charAt(0).toUpperCase() + field.slice(1)}
          </label>
          <input
            name={field}
            type={field === 'password' ? 'password' : field === 'age' ? 'number' : 'text'}
            value={formData[field]}
            onChange={handleChange}
            onBlur={handleBlur}
            style={{
              width: '100%', padding: '10px', borderRadius: '8px',
              border: `1px solid ${errors[field] && touched[field] ? '#ef4444' : '#d1d5db'}`,
              outline: 'none', boxSizing: 'border-box'
            }}
          />
          {errors[field] && touched[field] && (
            <p style={{ color: '#ef4444', fontSize: '12px', margin: '4px 0 0' }}>
              [ATTENTION] {errors[field]}
            </p>
          )}
        </div>
      ))}
      <button
        type="submit"
        disabled={!isValid}
        style={{ width: '100%', padding: '12px', background: isValid ? '#3b82f6' : '#94a3b8', color: 'white', border: 'none', borderRadius: '8px', cursor: isValid ? 'pointer' : 'not-allowed' }}
      >
        Soumettre
      </button>
    </form>
  );
}

# ----------------------------------------------------------------------------
# [DOCS] REACT HOOK FORM (Bibliothèque)
# ----------------------------------------------------------------------------

"""
POURQUOI REACT HOOK FORM ?

[OK] Moins de re-rendus (formulaires non contrôlés avec ref)
[OK] Validation intégrée
[OK] API simple et puissante
[OK] Compatible TypeScript
[OK] < 9KB gzippé

INSTALLATION :
"""

$ npm install react-hook-form
$ npm install zod @hookform/resolvers  // Pour validation Zod

"""
UTILISATION DE BASE :
"""

import { useForm } from 'react-hook-form';

function RegisterForm() {
  const {
    register,      // Enregistrer un champ
    handleSubmit,  // Gérer la soumission
    formState: { errors, isSubmitting, isValid },
    watch,         // Observer une valeur
    reset,         // Réinitialiser le formulaire
    setValue,      // Modifier programmatiquement
  } = useForm({
    defaultValues: {
      username: '',
      email: '',
      password: '',
      confirmPassword: '',
    },
    mode: 'onChange',  // Valider à chaque frappe ('onBlur', 'onSubmit')
  });

  const password = watch('password');  // Observer la valeur de password

  const onSubmit = async (data) => {
    // data = { username, email, password, confirmPassword }
    try {
      await registerUser(data);
      reset();  // Vider le formulaire
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>

      {/* Champ avec validation intégrée */}
      <input
        {...register('username', {
          required: 'Nom d\'utilisateur requis',
          minLength: { value: 3, message: 'Minimum 3 caractères' },
          maxLength: { value: 20, message: 'Maximum 20 caractères' },
          pattern: { value: /^[a-zA-Z0-9_]+$/, message: 'Lettres, chiffres, _ seulement' },
        })}
        placeholder="Nom d'utilisateur"
      />
      {errors.username && <p>{errors.username.message}</p>}

      {/* Email */}
      <input
        {...register('email', {
          required: 'Email requis',
          pattern: { value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: 'Email invalide' },
          validate: async (email) => {
            const taken = await checkEmailTaken(email);
            return !taken || 'Email déjà utilisé';
          }
        })}
        type="email"
        placeholder="Email"
      />
      {errors.email && <p>{errors.email.message}</p>}

      {/* Password */}
      <input
        {...register('password', {
          required: 'Mot de passe requis',
          minLength: { value: 8, message: 'Min 8 caractères' },
        })}
        type="password"
      />
      {errors.password && <p>{errors.password.message}</p>}

      {/* Confirmer password */}
      <input
        {...register('confirmPassword', {
          required: 'Confirmation requise',
          validate: value => value === password || 'Les mots de passe ne correspondent pas',
        })}
        type="password"
      />
      {errors.confirmPassword && <p>{errors.confirmPassword.message}</p>}

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Inscription...' : 'S\'inscrire'}
      </button>
    </form>
  );
}

"""
AVEC ZOD (Validation schema) :
"""

import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';

// Définir le schéma de validation
const registerSchema = z.object({
  username: z.string()
    .min(3, 'Min 3 caractères')
    .max(20, 'Max 20 caractères')
    .regex(/^[a-zA-Z0-9_]+$/, 'Lettres, chiffres, _ seulement'),

  email: z.string()
    .email('Email invalide'),

  password: z.string()
    .min(8, 'Min 8 caractères')
    .regex(/[A-Z]/, 'Une majuscule requise')
    .regex(/[0-9]/, 'Un chiffre requis'),

  confirmPassword: z.string(),

  age: z.number()
    .min(18, 'Doit être majeur'),

}).refine(data => data.password === data.confirmPassword, {
  message: 'Mots de passe différents',
  path: ['confirmPassword'],
});

// Utilisation avec React Hook Form
function ZodForm() {
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(registerSchema),
  });

  return (
    <form onSubmit={handleSubmit(data => console.log(data))}>
      <input {...register('username')} />
      {errors.username && <p>{errors.username.message}</p>}

      <input {...register('email')} type="email" />
      {errors.email && <p>{errors.email.message}</p>}

      <button type="submit">S'inscrire</button>
    </form>
  );
}

# ============================================================================
# [GUIDE] CHAPITRE 11 : APPELS API
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Fetch API avec useEffect
[OK] axios (bibliothèque populaire)
[OK] Gestion d'erreurs robuste
[OK] TanStack Query (React Query) - bibliothèque recommandée
[OK] Patterns : loading, error, pagination
"""

# ----------------------------------------------------------------------------
# [WEB] FETCH AVEC USEEFFECT (Rappel + Avancé)
# ----------------------------------------------------------------------------

"""
SERVICE API (Abstraction) :
"""

// src/services/api.js - Centraliser les appels API
const BASE_URL = import.meta.env.VITE_API_URL || 'https://jsonplaceholder.typicode.com';

const api = {
  // GET simple
  get: async (endpoint) => {
    const res = await fetch(`${BASE_URL}${endpoint}`);
    if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`);
    return res.json();
  },

  // POST
  post: async (endpoint, data) => {
    const res = await fetch(`${BASE_URL}${endpoint}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });
    if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`);
    return res.json();
  },

  // PUT
  put: async (endpoint, data) => {
    const res = await fetch(`${BASE_URL}${endpoint}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });
    if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`);
    return res.json();
  },

  // DELETE
  delete: async (endpoint) => {
    const res = await fetch(`${BASE_URL}${endpoint}`, { method: 'DELETE' });
    if (!res.ok) throw new Error(`${res.status}: ${res.statusText}`);
    return res.json();
  },
};

export default api;

// Utilisation
import api from '../services/api';

function UserList() {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let isMounted = true;
    api.get('/users')
      .then(data => { if (isMounted) setUsers(data); })
      .catch(err => { if (isMounted) setError(err.message); })
      .finally(() => { if (isMounted) setLoading(false); });
    return () => { isMounted = false; };
  }, []);

  if (loading) return <LoadingSpinner />;
  if (error) return <ErrorMessage message={error} />;
  return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

# ----------------------------------------------------------------------------
# [PACKAGE] AXIOS
# ----------------------------------------------------------------------------

"""
INSTALLATION :
$ npm install axios

AVANTAGES vs fetch :
[OK] Timeout intégré
[OK] Annulation de requêtes
[OK] Transformation automatique JSON
[OK] Intercepteurs (logs, auth tokens)
[OK] Meilleure gestion d'erreurs
"""

import axios from 'axios';

// Instance configurée
const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Intercepteur : Ajouter token automatiquement
apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

// Intercepteur : Gérer erreurs globalement
apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      // Token expiré -> déconnecter
      logout();
      window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

// Utilisation
const getUsers = () => apiClient.get('/users').then(r => r.data);
const createUser = (data) => apiClient.post('/users', data).then(r => r.data);
const updateUser = (id, data) => apiClient.put(`/users/${id}`, data).then(r => r.data);
const deleteUser = (id) => apiClient.delete(`/users/${id}`);

# ----------------------------------------------------------------------------
# [RAPIDE] TANSTACK QUERY (React Query)
# ----------------------------------------------------------------------------

"""
POURQUOI TANSTACK QUERY ?

Sans React Query, à chaque composant :
[X] Gérer loading, error, data manuellement
[X] Pas de cache (re-fetch inutile)
[X] Pas de synchronisation en arrière-plan
[X] Beaucoup de code boilerplate


AVEC TANSTACK QUERY :
[OK] Cache automatique (même données = pas de re-fetch)
[OK] Refetch automatique (focus fenêtre, reconnexion)
[OK] Loading/error states intégrés
[OK] Mutations (create, update, delete)
[OK] Pagination et scroll infini


INSTALLATION :
$ npm install @tanstack/react-query @tanstack/react-query-devtools


CONFIGURATION :
"""

// main.jsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,   // 5 minutes avant re-fetch
      cacheTime: 10 * 60 * 1000,  // 10 minutes dans le cache
      retry: 3,                    // 3 tentatives si erreur
    },
  },
});

ReactDOM.createRoot(document.getElementById('root')).render(
  <QueryClientProvider client={queryClient}>
    <App />
    <ReactQueryDevtools />  {/* Outil de débogage */}
  </QueryClientProvider>
);

"""
LIRE DES DONNÉES : useQuery
"""

import { useQuery } from '@tanstack/react-query';

// Fonction fetcher
const fetchUsers = () =>
  fetch('https://jsonplaceholder.typicode.com/users').then(r => r.json());

function UserList() {
  const {
    data: users,         // Les données
    isLoading,          // Premier chargement
    isError,            // Erreur
    error,              // Objet erreur
    isFetching,         // Re-fetch en arrière-plan
    refetch,            // Relancer manuellement
  } = useQuery({
    queryKey: ['users'],  // Clé unique du cache
    queryFn: fetchUsers,  // Fonction de fetch
  });

  if (isLoading) return <p>[HOURGLASS_WITH_FLOWING_SAND] Chargement...</p>;
  if (isError) return <p>[X] {error.message}</p>;

  return (
    <div>
      {isFetching && <span>Actualisation...</span>}
      <button onClick={refetch}>[SYNC] Actualiser</button>
      <ul>
        {users.map(user => <li key={user.id}>{user.name}</li>)}
      </ul>
    </div>
  );
}

"""
PARAMÈTRES DYNAMIQUES :
"""

function UserDetail({ userId }) {
  const { data: user, isLoading } = useQuery({
    queryKey: ['user', userId],  // userId dans la clé -> re-fetch si change
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
    enabled: !!userId,  // Ne pas fetcher si userId est null/undefined
  });

  if (isLoading) return <p>Chargement...</p>;
  return <div>{user?.name}</div>;
}

"""
MUTATIONS (Créer, modifier, supprimer) :
"""

import { useMutation, useQueryClient } from '@tanstack/react-query';

const createUser = (newUser) =>
  fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(newUser),
  }).then(r => r.json());

function CreateUserForm() {
  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: createUser,

    onSuccess: (newUser) => {
      // Invalider le cache pour re-fetcher la liste
      queryClient.invalidateQueries({ queryKey: ['users'] });

      // OU: Mise à jour optimiste du cache (plus rapide)
      queryClient.setQueryData(['users'], (old) => [...old, newUser]);
    },

    onError: (error) => {
      console.error('Erreur création :', error);
    },
  });

  const handleSubmit = (e) => {
    e.preventDefault();
    mutation.mutate({
      name: 'Nouveau Utilisateur',
      email: 'nouveau@example.com',
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? 'Création...' : 'Créer utilisateur'}
      </button>
      {mutation.isError && <p>Erreur : {mutation.error.message}</p>}
      {mutation.isSuccess && <p>[OK] Utilisateur créé !</p>}
    </form>
  );
}

# ============================================================================
# [GUIDE] CHAPITRE 12 : CUSTOM HOOKS
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Ce qu'est un custom hook et pourquoi l'utiliser
[OK] Règles des hooks
[OK] Créer des custom hooks utiles
[OK] Extraire logique réutilisable
"""

# ----------------------------------------------------------------------------
# [REFLEXION] POURQUOI DES CUSTOM HOOKS ?
# ----------------------------------------------------------------------------

"""
PROBLÈME : Logique dupliquée dans plusieurs composants
"""

// Composant A et Composant B ont la même logique de fetch...
function UserList() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetch('/api/users').then(r => r.json())
      .then(setData).catch(setError)
      .finally(() => setLoading(false));
  }, []);
  // ...
}

function ProductList() {
  const [data, setData] = useState(null);  // Même logique ! [!]
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  // ...
}

"""
SOLUTION : Extraire dans un custom hook
"""

# ----------------------------------------------------------------------------
# [OUTIL] CRÉER UN CUSTOM HOOK
# ----------------------------------------------------------------------------

"""
RÈGLES :
1. Nom commence par "use" -> useFetch, useLocalStorage...
2. Peut utiliser d'autres hooks (useState, useEffect...)
3. S'utilise comme n'importe quel hook
"""

// src/hooks/useFetch.js
import { useState, useEffect } from 'react';

function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    if (!url) return;

    let isMounted = true;
    setLoading(true);
    setError(null);

    const controller = new AbortController();

    fetch(url, { signal: controller.signal })
      .then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
      .then(data => { if (isMounted) setData(data); })
      .catch(err => {
        if (isMounted && err.name !== 'AbortError') setError(err.message);
      })
      .finally(() => { if (isMounted) setLoading(false); });

    return () => {
      isMounted = false;
      controller.abort();
    };
  }, [url]);

  return { data, loading, error };
}

// Utilisation propre !
function UserList() {
  const { data: users, loading, error } = useFetch('/api/users');
  if (loading) return <p>Chargement...</p>;
  if (error) return <p>Erreur : {error}</p>;
  return <ul>{users?.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}

function ProductList() {
  const { data: products, loading } = useFetch('/api/products');
  // Même logique, zéro duplication !
}

"""
CUSTOM HOOKS UTILES À CONNAÎTRE :
"""

// 1. useLocalStorage
function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch { return initialValue; }
  });

  const setValue = (value) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) { console.error(error); }
  };

  return [storedValue, setValue];
}

// 2. useDebounce
function useDebounce(value, delay = 500) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// 3. useToggle
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = useCallback(() => setValue(v => !v), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);
  return [value, toggle, setTrue, setFalse];
}

// 4. useWindowSize
function useWindowSize() {
  const [size, setSize] = useState({
    width: window.innerWidth,
    height: window.innerHeight,
  });

  useEffect(() => {
    const handleResize = () => setSize({ width: window.innerWidth, height: window.innerHeight });
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return size;
}

// 5. useMediaQuery
function useMediaQuery(query) {
  const [matches, setMatches] = useState(() => window.matchMedia(query).matches);

  useEffect(() => {
    const media = window.matchMedia(query);
    const listener = (e) => setMatches(e.matches);
    media.addEventListener('change', listener);
    return () => media.removeEventListener('change', listener);
  }, [query]);

  return matches;
}

// 6. useClickOutside
function useClickOutside(ref, callback) {
  useEffect(() => {
    const handleClick = (event) => {
      if (ref.current && !ref.current.contains(event.target)) {
        callback();
      }
    };
    document.addEventListener('mousedown', handleClick);
    return () => document.removeEventListener('mousedown', handleClick);
  }, [ref, callback]);
}

// 7. usePrevious
function usePrevious(value) {
  const ref = useRef(undefined);
  useEffect(() => { ref.current = value; });
  return ref.current;
}

// 8. useAsync (générique)
function useAsync(asyncFn, dependencies = []) {
  const [status, setStatus] = useState('idle'); // 'idle' | 'pending' | 'success' | 'error'
  const [value, setValue] = useState(null);
  const [error, setError] = useState(null);

  const execute = useCallback(() => {
    setStatus('pending');
    setValue(null);
    setError(null);

    return asyncFn()
      .then(data => { setValue(data); setStatus('success'); })
      .catch(err => { setError(err); setStatus('error'); });
  }, [asyncFn]);

  useEffect(() => { execute(); }, [...dependencies, execute]);

  return { execute, status, value, error,
    isIdle: status === 'idle',
    isPending: status === 'pending',
    isSuccess: status === 'success',
    isError: status === 'error',
  };
}

# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 8 : HOOKS PERSONNALISÉS
# ============================================================================

"""
OBJECTIF : Créer et utiliser plusieurs custom hooks dans une app réelle

HOOKS À CRÉER :
1. useSearch(items, fields) -> Filtrage de recherche
2. usePagination(data, itemsPerPage) -> Pagination
3. useSortable(data, defaultField) -> Tri de tableau
4. useClipboard() -> Copier dans le presse-papier

DEMO : Table d'utilisateurs avec recherche, tri, pagination

CORRIGÉ :
"""

// src/hooks/useSearch.js
function useSearch(items, fields) {
  const [query, setQuery] = useState('');

  const filtered = useMemo(() => {
    if (!query.trim()) return items;
    const q = query.toLowerCase();
    return items.filter(item =>
      fields.some(field => String(item[field]).toLowerCase().includes(q))
    );
  }, [items, fields, query]);

  return { query, setQuery, filtered, count: filtered.length };
}

// src/hooks/usePagination.js
function usePagination(data, itemsPerPage = 10) {
  const [currentPage, setCurrentPage] = useState(1);

  const totalPages = Math.ceil(data.length / itemsPerPage);
  const paginatedData = useMemo(() => {
    const start = (currentPage - 1) * itemsPerPage;
    return data.slice(start, start + itemsPerPage);
  }, [data, currentPage, itemsPerPage]);

  useEffect(() => {
    if (currentPage > totalPages) setCurrentPage(1);
  }, [data.length, currentPage, totalPages]);

  return {
    currentPage, setCurrentPage,
    totalPages, paginatedData,
    hasPrev: currentPage > 1,
    hasNext: currentPage < totalPages,
    prevPage: () => setCurrentPage(p => Math.max(1, p - 1)),
    nextPage: () => setCurrentPage(p => Math.min(totalPages, p + 1)),
  };
}

// src/hooks/useSortable.js
function useSortable(data, defaultField = 'id') {
  const [sortField, setSortField] = useState(defaultField);
  const [sortDir, setSortDir] = useState('asc');

  const sorted = useMemo(() => {
    return [...data].sort((a, b) => {
      if (a[sortField] < b[sortField]) return sortDir === 'asc' ? -1 : 1;
      if (a[sortField] > b[sortField]) return sortDir === 'asc' ? 1 : -1;
      return 0;
    });
  }, [data, sortField, sortDir]);

  const sort = (field) => {
    if (field === sortField) {
      setSortDir(d => d === 'asc' ? 'desc' : 'asc');
    } else {
      setSortField(field);
      setSortDir('asc');
    }
  };

  return { sorted, sort, sortField, sortDir };
}

// src/hooks/useClipboard.js
function useClipboard() {
  const [copied, setCopied] = useState(false);

  const copy = async (text) => {
    try {
      await navigator.clipboard.writeText(text);
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch (err) {
      console.error('Clipboard error:', err);
    }
  };

  return { copy, copied };
}

// Demo complète
const USERS_DATA = Array.from({ length: 50 }, (_, i) => ({
  id: i + 1,
  name: ['Alice', 'Bob', 'Charlie', 'Diana', 'Éric'][i % 5] + ` ${i + 1}`,
  email: `user${i + 1}@example.com`,
  role: ['Admin', 'User', 'Moderator'][i % 3],
  score: Math.floor(Math.random() * 1000),
}));

function UserDataTable() {
  const { query, setQuery, filtered } = useSearch(USERS_DATA, ['name', 'email', 'role']);
  const { sorted, sort, sortField, sortDir } = useSortable(filtered, 'name');
  const { currentPage, totalPages, paginatedData, prevPage, nextPage, hasPrev, hasNext } = usePagination(sorted, 8);
  const { copy, copied } = useClipboard();

  const SortIcon = ({ field }) => {
    if (sortField !== field) return <span style={{ color: '#9ca3af' }}> ^v</span>;
    return <span> {sortDir === 'asc' ? '^' : 'v'}</span>;
  };

  const colStyle = { padding: '12px 16px', textAlign: 'left', borderBottom: '1px solid #e5e7eb', cursor: 'pointer' };

  return (
    <div style={{ fontFamily: 'sans-serif', maxWidth: '900px', margin: '32px auto' }}>
      <h1>[UTILISATEURS] Gestion Utilisateurs</h1>

      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
        <input
          value={query}
          onChange={e => setQuery(e.target.value)}
          placeholder={`[RECHERCHE] Rechercher dans ${USERS_DATA.length} utilisateurs...`}
          style={{ padding: '10px 16px', width: '300px', borderRadius: '8px', border: '1px solid #d1d5db', fontSize: '14px' }}
        />
        <span style={{ color: '#64748b', fontSize: '14px' }}>
          {filtered.length} résultat{filtered.length !== 1 ? 's' : ''}
        </span>
      </div>

      <div style={{ background: 'white', borderRadius: '12px', boxShadow: '0 1px 3px rgba(0,0,0,0.1)', overflow: 'hidden' }}>
        <table style={{ width: '100%', borderCollapse: 'collapse' }}>
          <thead style={{ background: '#f8fafc' }}>
            <tr>
              {[['name', 'Nom'], ['email', 'Email'], ['role', 'Rôle'], ['score', 'Score']].map(([field, label]) => (
                <th key={field} style={{ ...colStyle, fontWeight: '600', color: '#374151', userSelect: 'none' }} onClick={() => sort(field)}>
                  {label}<SortIcon field={field} />
                </th>
              ))}
              <th style={{ ...colStyle, cursor: 'default' }}>Copier email</th>
            </tr>
          </thead>
          <tbody>
            {paginatedData.map(user => (
              <tr key={user.id} style={{ transition: 'background 0.1s' }} onMouseEnter={e => e.currentTarget.style.background = '#f8fafc'} onMouseLeave={e => e.currentTarget.style.background = 'white'}>
                <td style={colStyle}><strong>{user.name}</strong></td>
                <td style={{ ...colStyle, color: '#64748b' }}>{user.email}</td>
                <td style={colStyle}>
                  <span style={{ padding: '2px 8px', borderRadius: '9999px', fontSize: '12px', background: { Admin: '#FEE2E2', User: '#DBEAFE', Moderator: '#D1FAE5' }[user.role], color: { Admin: '#991B1B', User: '#1E40AF', Moderator: '#065F46' }[user.role] }}>
                    {user.role}
                  </span>
                </td>
                <td style={{ ...colStyle, fontWeight: 'bold', color: user.score > 500 ? '#22c55e' : '#374151' }}>{user.score}</td>
                <td style={colStyle}>
                  <button onClick={() => copy(user.email)} style={{ padding: '4px 10px', background: copied ? '#22c55e' : '#f1f5f9', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '12px', color: copied ? 'white' : '#374151' }}>
                    {copied ? '[OK] Copié !' : '[LISTE] Copier'}
                  </button>
                </td>
              </tr>
            ))}
          </tbody>
        </table>

        {/* Pagination */}
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', background: '#f8fafc', borderTop: '1px solid #e5e7eb' }}>
          <span style={{ fontSize: '13px', color: '#64748b' }}>Page {currentPage} / {totalPages}</span>
          <div style={{ display: 'flex', gap: '8px' }}>
            <button onClick={prevPage} disabled={!hasPrev} style={{ padding: '6px 12px', borderRadius: '6px', border: 'none', background: hasPrev ? '#3b82f6' : '#e5e7eb', color: hasPrev ? 'white' : '#9ca3af', cursor: hasPrev ? 'pointer' : 'not-allowed' }}><- Préc</button>
            <button onClick={nextPage} disabled={!hasNext} style={{ padding: '6px 12px', borderRadius: '6px', border: 'none', background: hasNext ? '#3b82f6' : '#e5e7eb', color: hasNext ? 'white' : '#9ca3af', cursor: hasNext ? 'pointer' : 'not-allowed' }}>Suiv -></button>
          </div>
        </div>
      </div>
    </div>
  );
}

export default UserDataTable;

# ============================================================================
# [GUIDE] CHAPITRE 13 : useReducer ET GESTION D'ÉTAT AVANCÉE
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] useReducer - Quand et Comment
[OK] Pattern Flux/Redux
[OK] Combiner useReducer + Context
[OK] Middleware et side-effects
"""

# ----------------------------------------------------------------------------
# [SYNC] useReducer
# ----------------------------------------------------------------------------

"""
QUAND useReducer vs useState ?

useState    -> État simple, peu de transitions
useReducer  -> État complexe, logique de transition riche
            -> Plusieurs valeurs liées
            -> Actions nommées (plus lisible)

ANALOGIE :
useState   = Interrupteur ON/OFF
useReducer = Panneau de commande avec boutons nommés
"""

import { useReducer } from 'react';

"""
SYNTAXE :
"""

// 1. Définir le REDUCER (état + action -> nouvel état)
function reducer(state, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };

    case 'DECREMENT':
      return { ...state, count: state.count - 1 };

    case 'RESET':
      return { ...state, count: 0 };

    case 'SET':
      return { ...state, count: action.payload };

    default:
      return state;  // Important : toujours avoir un default !
  }
}

// 2. État initial
const initialState = { count: 0, history: [] };

// 3. Utiliser useReducer
function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      <p>{state.count}</p>
      <button onClick={() => dispatch({ type: 'INCREMENT' })}>+</button>
      <button onClick={() => dispatch({ type: 'DECREMENT' })}>-</button>
      <button onClick={() => dispatch({ type: 'RESET' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'SET', payload: 10 })}>-> 10</button>
    </div>
  );
}

"""
EXEMPLE AVANCÉ : Panier d'achat avec useReducer
"""

// Reducer du panier
function cartReducer(state, action) {
  switch (action.type) {
    case 'ADD_ITEM': {
      const exists = state.items.find(item => item.id === action.payload.id);
      if (exists) {
        return {
          ...state,
          items: state.items.map(item =>
            item.id === action.payload.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          ),
        };
      }
      return {
        ...state,
        items: [...state.items, { ...action.payload, quantity: 1 }],
      };
    }

    case 'REMOVE_ITEM':
      return {
        ...state,
        items: state.items.filter(item => item.id !== action.payload),
      };

    case 'UPDATE_QUANTITY':
      return {
        ...state,
        items: state.items.map(item =>
          item.id === action.payload.id
            ? { ...item, quantity: Math.max(0, action.payload.quantity) }
            : item
        ).filter(item => item.quantity > 0),
      };

    case 'CLEAR_CART':
      return { ...state, items: [] };

    case 'APPLY_COUPON':
      return { ...state, coupon: action.payload, discount: action.payload.discount };

    default:
      return state;
  }
}

const cartInitialState = {
  items: [],
  coupon: null,
  discount: 0,
};

function ShoppingCart() {
  const [cart, dispatch] = useReducer(cartReducer, cartInitialState);

  const totalPrice = cart.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
  const discountedPrice = totalPrice * (1 - cart.discount / 100);

  return (
    <div>
      {cart.items.map(item => (
        <div key={item.id}>
          <span>{item.name}</span>
          <button onClick={() => dispatch({ type: 'UPDATE_QUANTITY', payload: { id: item.id, quantity: item.quantity - 1 } })}>-</button>
          <span>{item.quantity}</span>
          <button onClick={() => dispatch({ type: 'UPDATE_QUANTITY', payload: { id: item.id, quantity: item.quantity + 1 } })}>+</button>
          <button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: item.id })}>[X]</button>
        </div>
      ))}

      <button onClick={() => dispatch({ type: 'CLEAR_CART' })}>Vider le panier</button>
      <p>Total : {discountedPrice.toFixed(2)}€</p>
    </div>
  );
}

"""
COMBINER useReducer + Context = Mini Redux :
"""

// store/appStore.jsx
import { createContext, useContext, useReducer } from 'react';

// Reducer global
function appReducer(state, action) {
  switch (action.type) {
    case 'SET_USER':
      return { ...state, user: action.payload };
    case 'LOGOUT':
      return { ...state, user: null };
    case 'SET_THEME':
      return { ...state, theme: action.payload };
    case 'ADD_NOTIFICATION':
      return { ...state, notifications: [...state.notifications, action.payload] };
    case 'REMOVE_NOTIFICATION':
      return { ...state, notifications: state.notifications.filter(n => n.id !== action.payload) };
    default:
      return state;
  }
}

const initialAppState = {
  user: null,
  theme: 'light',
  notifications: [],
};

const AppStateContext = createContext(null);
const AppDispatchContext = createContext(null);

export function AppProvider({ children }) {
  const [state, dispatch] = useReducer(appReducer, initialAppState);

  return (
    <AppStateContext.Provider value={state}>
      <AppDispatchContext.Provider value={dispatch}>
        {children}
      </AppDispatchContext.Provider>
    </AppStateContext.Provider>
  );
}

export const useAppState = () => useContext(AppStateContext);
export const useAppDispatch = () => useContext(AppDispatchContext);

// Action creators (pour ne pas répéter { type: ... })
export const actions = {
  setUser: (user) => ({ type: 'SET_USER', payload: user }),
  logout: () => ({ type: 'LOGOUT' }),
  setTheme: (theme) => ({ type: 'SET_THEME', payload: theme }),
  addNotification: (message, type = 'info') => ({
    type: 'ADD_NOTIFICATION',
    payload: { id: Date.now(), message, type }
  }),
  removeNotification: (id) => ({ type: 'REMOVE_NOTIFICATION', payload: id }),
};

// Utilisation propre :
function Header() {
  const { user, theme } = useAppState();
  const dispatch = useAppDispatch();

  return (
    <header>
      {user ? (
        <button onClick={() => dispatch(actions.logout())}>
          Déconnexion ({user.name})
        </button>
      ) : (
        <button onClick={() => dispatch(actions.setUser({ name: 'Alice' }))}>
          Connexion
        </button>
      )}
      <button onClick={() => dispatch(actions.setTheme(theme === 'light' ? 'dark' : 'light'))}>
        Thème
      </button>
    </header>
  );
}

# ============================================================================
# [DOCS] RÉCAPITULATIF PARTIE 3
# ============================================================================

"""
[BRAVO] FÉLICITATIONS ! PARTIE 3 TERMINÉE !

VOUS MAÎTRISEZ MAINTENANT :

Chapitre 9 : React Router
[OK] BrowserRouter, Routes, Route
[OK] Link et NavLink
[OK] Routes dynamiques (:id)
[OK] Routes imbriquées et layouts (Outlet)
[OK] Navigation programmatique (useNavigate)
[OK] Query strings (useSearchParams)
[OK] Routes protégées

Chapitre 10 : Formulaires
[OK] Formulaires contrôlés
[OK] Validation manuelle (touched/errors)
[OK] React Hook Form
[OK] Validation Zod

Chapitre 11 : Appels API
[OK] Service API avec fetch
[OK] Axios avec intercepteurs
[OK] TanStack Query (cache, mutations)

Chapitre 12 : Custom Hooks
[OK] Règles des hooks
[OK] useFetch, useLocalStorage, useDebounce
[OK] useSearch, usePagination, useSortable

Chapitre 13 : useReducer
[OK] Reducer pattern
[OK] Actions nommées
[OK] useReducer + Context = Store global

-> PROCHAINE ÉTAPE : PARTIE 4 - Production et Optimisation !
"""

# ============================================================================
# [SCIENCE] REACT - PARTIE 4 : PRODUCTION ET OPTIMISATION
# ============================================================================
#
# [OBJECTIF] CETTE PARTIE COUVRE :
# - Chapitre 14 : Performance (memo, useMemo, useCallback, Profiler)
# - Chapitre 15 : Testing avec React Testing Library
# - Chapitre 16 : Best Practices & Architecture
# - Chapitre 17 : Déploiement Production
#
# [TEMPS] TEMPS : ~8-10 heures
# [DOCS] PRÉREQUIS : Parties 1, 2 et 3 complétées
#
# ============================================================================

# ============================================================================
# [GUIDE] CHAPITRE 14 : PERFORMANCE
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Comprendre POURQUOI et QUAND React re-rend un composant
[OK] Utiliser React.memo pour éviter les re-rendus inutiles
[OK] Maîtriser useMemo et useCallback (rappel + cas avancés)
[OK] Lire le React Profiler (DevTools)
[OK] Lazy loading et Code Splitting
[OK] Virtualisation de longues listes
[OK] Optimiser les images et ressources
"""

# ----------------------------------------------------------------------------
# [REFLEXION] QUAND REACT RE-REND UN COMPOSANT ?
# ----------------------------------------------------------------------------

"""
RÈGLE FONDAMENTALE :

Un composant se RE-REND (recalcule son JSX) quand :
  1. Son STATE change (useState, useReducer)
  2. Ses PROPS changent
  3. Son PARENT re-rend (même si ses props n'ont pas changé !)
  4. Un CONTEXT qu'il consomme change

[ATTENTION] PIÈGE N°1 : Quand le parent re-rend, TOUS ses enfants re-rendent !
   Même si leurs props n'ont pas changé !
"""

// EXEMPLE DU PROBLÈME :
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>+1 ({count})</button>

      {/* Ces composants re-rendent à CHAQUE clic, même si leur props ne changent pas ! */}
      <ExpensiveComponent title="Titre fixe" />
      <AnotherComponent data={[1, 2, 3]} />
    </div>
  );
}

// [X] ExpensiveComponent re-rend à chaque clic alors que title="Titre fixe" ne change jamais !
function ExpensiveComponent({ title }) {
  console.log('[!] Re-rendu de ExpensiveComponent !');

  // Imaginez un calcul très coûteux ici...
  return <h1>{title}</h1>;
}

"""
VISUALISER LES RE-RENDUS :

Dans React DevTools :
-> Onglet "Profiler"
-> Enregistrer une session
-> Voir quels composants ont re-rendu et pourquoi
-> Les composants qui re-rendent sont surlignés

OU activer les highlights :
React DevTools -> [CONFIG] Settings -> "Highlight updates when components render"
-> Les composants flashent en orange/rouge à chaque rendu
"""

# ----------------------------------------------------------------------------
# [LOGIQUE] REACT.MEMO - MÉMORISER UN COMPOSANT
# ----------------------------------------------------------------------------

"""
React.memo = HOC (Higher-Order Component) qui mémorise un composant

Comportement :
-> React.memo enveloppe votre composant
-> Avant de re-rendre, React compare les PROPS (shallow comparison)
-> Si les props sont identiques -> PAS de re-rendu !
-> Si les props ont changé -> Re-rendu normal

ANALOGIE :
Sans memo : "Je refais tout le repas à chaque fois !"
Avec memo : "Je compare la commande, si c'est la même, je sers le même plat."
"""

import React, { useState, memo } from 'react';

// [OK] SOLUTION : Envelopper avec React.memo
const ExpensiveComponent = memo(function ExpensiveComponent({ title }) {
  console.log('[OK] Re-rendu seulement si title change !');
  return <h1>{title}</h1>;
});

// OU : Syntaxe avec export
function UserCard({ user }) {
  return <div>{user.name}</div>;
}
export default memo(UserCard);  // Exporter la version mémorisée


// RÉSULTAT :
function Parent() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>+1 ({count})</button>

      {/* [OK] Ne re-rend PAS car title="Titre fixe" est constant */}
      <ExpensiveComponent title="Titre fixe" />
    </div>
  );
}

"""
[ATTENTION] PIÈGE MAJEUR : Les objets et fonctions créés dans le parent !

Même avec React.memo, si vous passez un OBJET ou une FONCTION créé
dans le parent -> Nouvelle référence à chaque rendu -> React.memo échoue !
"""

// [X] PROBLÈME : Objet recréé à chaque rendu du parent !
function Parent() {
  const [count, setCount] = useState(0);

  const style = { color: 'red', fontSize: 16 };     // <- Nouvel objet à chaque rendu !
  const handleClick = () => console.log('cliqué'); // <- Nouvelle fonction à chaque rendu !

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      {/* memo ne sert à rien ici ! style et handleClick sont toujours "différents" */}
      <MemoizedChild style={style} onClick={handleClick} />
    </div>
  );
}

// [OK] SOLUTION : useMemo pour l'objet, useCallback pour la fonction
function Parent() {
  const [count, setCount] = useState(0);

  // [OK] Même référence si rien ne change
  const style = useMemo(() => ({ color: 'red', fontSize: 16 }), []);
  const handleClick = useCallback(() => console.log('cliqué'), []);

  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      {/* [OK] MemoizedChild ne re-rend pas */}
      <MemoizedChild style={style} onClick={handleClick} />
    </div>
  );
}

"""
QUAND UTILISER React.memo ?

[OK] BON CAS D'USAGE :
- Composant coûteux à rendre (grandes listes, graphiques, calculs)
- Composant qui reçoit des props stables
- Composant qui re-rend souvent à cause de son parent

[X] INUTILE (même contre-productif) :
- Composants simples (juste du texte, un bouton)
- Composants qui re-rendent TOUJOURS quand le parent re-rend
  (car memo fait une comparaison supplémentaire pour rien)
- Props qui changent à chaque rendu de toute façon

[IDEE] RÈGLE PRATIQUE :
D'abord, mesurez avec le Profiler.
N'optimisez que si vous voyez un problème réel.
"Premature optimization is the root of all evil" - Knuth
"""

"""
COMPARAISON PERSONNALISÉE :

Par défaut, React.memo fait une comparaison superficielle (shallow).
Vous pouvez fournir votre propre fonction de comparaison.
"""

// Comparaison custom : ne re-rendre que si userId ou role change
const UserProfile = memo(
  function UserProfile({ user, settings }) {
    return <div>{user.name} - {user.role}</div>;
  },
  // Fonction de comparaison : true = "même" = PAS de re-rendu
  (prevProps, nextProps) => {
    return (
      prevProps.user.id === nextProps.user.id &&
      prevProps.user.role === nextProps.user.role
      // On ignore settings -> si settings change, on ne re-rend pas !
    );
  }
);

// [ATTENTION] Utiliser avec précaution ! Peut cacher des bugs.

# ----------------------------------------------------------------------------
# [CALCUL] useMemo - RAPPEL ET CAS AVANCÉS
# ----------------------------------------------------------------------------

"""
useMemo = Mémoriser le RÉSULTAT d'un calcul

const valeur = useMemo(() => calcul(), [dépendances]);

Recalcule SEULEMENT si les dépendances changent.

QUAND L'UTILISER ?
1. Calcul réellement coûteux (filtrer/trier des milliers d'éléments)
2. Stabiliser une référence d'objet pour React.memo
3. Dépendance d'un autre useMemo ou useCallback
"""

// 1. CALCUL COÛTEUX
import { useMemo } from 'react';

function DataAnalysis({ transactions }) {
  // [X] Recalculé à chaque rendu
  // const stats = analyzeData(transactions);

  // [OK] Recalculé seulement si transactions change
  const stats = useMemo(() => {
    console.log('[GRAPHIQUE] Calcul des statistiques... (coûteux)');
    return {
      total: transactions.length,
      sum: transactions.reduce((s, t) => s + t.amount, 0),
      average: transactions.reduce((s, t) => s + t.amount, 0) / transactions.length,
      max: Math.max(...transactions.map(t => t.amount)),
      min: Math.min(...transactions.map(t => t.amount)),
      byCategory: transactions.reduce((acc, t) => {
        acc[t.category] = (acc[t.category] || 0) + t.amount;
        return acc;
      }, {}),
    };
  }, [transactions]);

  return (
    <div>
      <p>Transactions : {stats.total}</p>
      <p>Total : {stats.sum.toFixed(2)}€</p>
      <p>Moyenne : {stats.average.toFixed(2)}€</p>
    </div>
  );
}

// 2. STABILISER RÉFÉRENCE D'OBJET
function Parent({ userId, theme }) {
  // Sans useMemo : Nouvel objet à chaque rendu -> MemoizedChild re-rend toujours
  // const userConfig = { userId, theme, timestamp: Date.now() };

  // Avec useMemo : Même référence si userId et theme n'ont pas changé
  const userConfig = useMemo(() => ({
    userId,
    theme,
    permissions: ['read', 'write'],  // Tableau recréé sinon
  }), [userId, theme]);

  return <MemoizedChild config={userConfig} />;
}

// 3. FILTRAGE ET TRI MÉMOÏSÉS (pattern courant)
function ProductList({ products, searchQuery, sortBy, filterCategory }) {
  const processedProducts = useMemo(() => {
    let result = [...products];

    // Filtrer par catégorie
    if (filterCategory !== 'all') {
      result = result.filter(p => p.category === filterCategory);
    }

    // Filtrer par recherche
    if (searchQuery.trim()) {
      const q = searchQuery.toLowerCase();
      result = result.filter(p =>
        p.name.toLowerCase().includes(q) ||
        p.description.toLowerCase().includes(q)
      );
    }

    // Trier
    result.sort((a, b) => {
      if (sortBy === 'price-asc') return a.price - b.price;
      if (sortBy === 'price-desc') return b.price - a.price;
      if (sortBy === 'name') return a.name.localeCompare(b.name);
      return 0;
    });

    return result;
  }, [products, searchQuery, sortBy, filterCategory]);
  // <- Recalcule SEULEMENT si une de ces 4 dépendances change

  return (
    <div>
      <p>{processedProducts.length} produits trouvés</p>
      {processedProducts.map(p => <ProductCard key={p.id} product={p} />)}
    </div>
  );
}

# ----------------------------------------------------------------------------
# [OBJECTIF] useCallback - RAPPEL ET CAS AVANCÉS
# ----------------------------------------------------------------------------

"""
useCallback = Mémoriser une FONCTION (sa référence)

const maFonction = useCallback(() => { ... }, [dépendances]);

Retourne la MÊME référence de fonction si dépendances n'ont pas changé.
Utile pour :
1. Passer une fonction à un composant mémoïsé (memo)
2. Dépendance stable pour useEffect
3. Éviter les boucles infinies
"""

import { useCallback, useState, memo } from 'react';

// EXEMPLE COMPLET : Liste avec ajout/suppression
function TaskManager() {
  const [tasks, setTasks] = useState([
    { id: 1, text: 'Apprendre React', done: false },
    { id: 2, text: 'Faire les courses', done: true },
  ]);

  // [OK] useCallback avec forme fonctionnelle -> dépendances vides !
  const addTask = useCallback((text) => {
    setTasks(prev => [...prev, { id: Date.now(), text, done: false }]);
  }, []);  // [] = jamais recréée

  const toggleTask = useCallback((id) => {
    setTasks(prev => prev.map(t => t.id === id ? { ...t, done: !t.done } : t));
  }, []);

  const deleteTask = useCallback((id) => {
    setTasks(prev => prev.filter(t => t.id !== id));
  }, []);

  return (
    <div>
      <AddTaskForm onAdd={addTask} />    {/* <- Reçoit une fonction stable */}
      {tasks.map(task => (
        <TaskItem
          key={task.id}
          task={task}
          onToggle={toggleTask}
          onDelete={deleteTask}
        />
      ))}
    </div>
  );
}

// Mémorisé : ne re-rend que si task, onToggle ou onDelete changent
const TaskItem = memo(function TaskItem({ task, onToggle, onDelete }) {
  console.log('Rendu de TaskItem:', task.text);
  return (
    <div>
      <input
        type="checkbox"
        checked={task.done}
        onChange={() => onToggle(task.id)}
      />
      <span style={{ textDecoration: task.done ? 'line-through' : 'none' }}>
        {task.text}
      </span>
      <button onClick={() => onDelete(task.id)}>[X]</button>
    </div>
  );
});

const AddTaskForm = memo(function AddTaskForm({ onAdd }) {
  const [text, setText] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim()) {
      onAdd(text.trim());
      setText('');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input value={text} onChange={e => setText(e.target.value)} placeholder="Nouvelle tâche..." />
      <button type="submit">Ajouter</button>
    </form>
  );
});

"""
TABLEAU RÉCAPITULATIF :

┌──────────────────┬──────────────────────────────┬──────────────────┐
│ Hook             │ Ce qu'il mémorise            │ Utiliser quand   │
├──────────────────┼──────────────────────────────┼──────────────────┤
│ React.memo       │ Un composant entier          │ Composant coûteux│
│ useMemo          │ Résultat d'un calcul         │ Calcul lent      │
│ useCallback      │ Référence d'une fonction     │ Prop de fonction │
└──────────────────┴──────────────────────────────┴──────────────────┘

[ATTENTION] RAPPEL : Ne pas sur-optimiser !
  Ces outils ont un coût (mémoire, comparaison).
  Utilisez le Profiler pour identifier les vrais problèmes.
"""

# ----------------------------------------------------------------------------
# [RECHERCHE] REACT PROFILER - DIAGNOSTIQUER LES PERFORMANCES
# ----------------------------------------------------------------------------

"""
REACT DEVELOPER TOOLS - ONGLET PROFILER

1. Installer l'extension :
   Chrome : "React Developer Tools"
   Firefox : "React Developer Tools"

2. Ouvrir DevTools (F12) -> Onglet "Profiler" [SCIENCE]

3. UTILISATION :
   a) Cliquer [BLACK_RIGHT-POINTING_TRIANGLE] (Record)
   b) Interagir avec l'app
   c) Cliquer [NOIR] (Stop)
   d) Analyser les résultats

LECTURE DES RÉSULTATS :

Flame Chart (Flamme) :
- Axe horizontal = Temps
- Axe vertical = Pile des composants
- Plus c'est large -> Plus le rendu prend du temps
- Gris = Pas de re-rendu (optimisé)
- Couleur (jaune->rouge) = A re-rendu (plus rouge = plus lent)

Ranked Chart :
- Composants classés par temps de rendu
- Le plus lent en haut
- Idéal pour trouver les goulots d'étranglement
"""

// PROFILER PROGRAMMATIQUE (dans le code)
import { Profiler } from 'react';

function onRenderCallback(
  id,          // ID du Profiler
  phase,       // "mount" ou "update"
  actualDuration,  // Temps de rendu (ms)
  baseDuration,    // Temps estimé sans mémo
  startTime,       // Début du rendu
  commitTime,      // Fin du commit
) {
  console.log(`[RECHERCHE] [${id}] ${phase}: ${actualDuration.toFixed(2)}ms`);

  // Envoyer à un service de monitoring
  if (actualDuration > 10) {
    console.warn(`[ATTENTION] Rendu lent (${actualDuration.toFixed(2)}ms) dans ${id}`);
  }
}

function App() {
  return (
    <Profiler id="Dashboard" onRender={onRenderCallback}>
      <Dashboard />
    </Profiler>
  );
}

// Profiler imbriqué pour précision
function Dashboard() {
  return (
    <div>
      <Profiler id="Sidebar" onRender={onRenderCallback}>
        <Sidebar />
      </Profiler>
      <Profiler id="MainContent" onRender={onRenderCallback}>
        <MainContent />
      </Profiler>
    </div>
  );
}

"""
[IDEE] MÉTRIQUES IMPORTANTES :

actualDuration : Temps réel de rendu
-> < 1ms    : Excellent
-> 1-10ms   : Bon
-> 10-50ms  : Acceptable (à surveiller)
-> > 50ms   : Problème ! (l'utilisateur ressent un lag)
-> > 100ms  : Très lent ! À optimiser absolument

baseDuration : Temps sans mémoization
-> Si baseDuration >> actualDuration -> Mémoization efficace [OK]
-> Si baseDuration ≈ actualDuration  -> Mémoization inutile [ATTENTION]
"""

# ----------------------------------------------------------------------------
# [SLOTH] LAZY LOADING ET CODE SPLITTING
# ----------------------------------------------------------------------------

"""
PROBLÈME :
Par défaut, React bundle TOUT le code en un seul fichier.
-> bundle.js = 2MB -> L'utilisateur attend 5 secondes au premier chargement !

SOLUTION : Code Splitting
-> Diviser le bundle en petits morceaux (chunks)
-> Charger seulement ce qui est nécessaire
-> Les autres pages chargent quand on les visite

ANALOGIE :
Sans code splitting : Télécharger tout un livre avant de le lire
Avec code splitting : Télécharger chapitre par chapitre
"""

import { lazy, Suspense } from 'react';

// [X] Import statique : tout est dans le bundle principal
// import Dashboard from './pages/Dashboard';
// import Analytics from './pages/Analytics';
// import Settings from './pages/Settings';

// [OK] Import dynamique : chargé seulement quand nécessaire
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Analytics = lazy(() => import('./pages/Analytics'));
const Settings  = lazy(() => import('./pages/Settings'));
const UserProfile = lazy(() => import('./pages/UserProfile'));

// Suspense : Affiche un fallback pendant le chargement
function App() {
  return (
    <BrowserRouter>
      {/* Le Suspense DOIT envelopper les composants lazy */}
      <Suspense fallback={<LoadingSpinner />}>
        <Routes>
          <Route path="/"           element={<Home />} />        {/* Pas de lazy - page principale */}
          <Route path="/dashboard"  element={<Dashboard />} />   {/* Lazy - chargé à la demande */}
          <Route path="/analytics"  element={<Analytics />} />   {/* Lazy */}
          <Route path="/settings"   element={<Settings />} />    {/* Lazy */}
          <Route path="/profile/:id" element={<UserProfile />} /> {/* Lazy */}
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

// Fallback sophistiqué
function LoadingSpinner() {
  return (
    <div style={{
      display: 'flex', flexDirection: 'column',
      alignItems: 'center', justifyContent: 'center',
      minHeight: '200px', gap: '16px'
    }}>
      <div style={{
        width: '40px', height: '40px',
        border: '4px solid #e5e7eb',
        borderTop: '4px solid #3b82f6',
        borderRadius: '50%',
        animation: 'spin 1s linear infinite'
      }} />
      <p style={{ color: '#64748b', fontSize: '14px' }}>Chargement...</p>

      <style>{`
        @keyframes spin { to { transform: rotate(360deg); } }
      `}</style>
    </div>
  );
}

"""
LAZY LOADING DE COMPOSANTS LOURDS :

Pas seulement pour les pages ! Aussi pour les composants lourds.
"""

// Composant avec bibliothèque lourde (ex: graphiques)
const HeavyChart = lazy(() => import('./components/HeavyChart'));
const MapComponent = lazy(() => import('./components/MapComponent'));
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));

function Dashboard() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <h1>Dashboard</h1>
      <button onClick={() => setShowChart(true)}>Afficher le graphique</button>

      {/* Le code du graphique N'EST PAS téléchargé tant que showChart est false */}
      {showChart && (
        <Suspense fallback={<p>Chargement du graphique...</p>}>
          <HeavyChart data={chartData} />
        </Suspense>
      )}
    </div>
  );
}

"""
PRÉCHARGEMENT (Preloading) :

Charger en avance quand on sait que l'utilisateur va probablement aller quelque part.
"""

// Précharger au survol d'un lien
const SettingsPage = lazy(() => import('./pages/Settings'));

function NavLink() {
  const handleMouseEnter = () => {
    // Déclencher le chargement au survol (avant le clic)
    import('./pages/Settings');
  };

  return (
    <Link to="/settings" onMouseEnter={handleMouseEnter}>
      [CONFIG] Paramètres
    </Link>
  );
}

# ----------------------------------------------------------------------------
# [DOC] VIRTUALISATION DE LONGUES LISTES
# ----------------------------------------------------------------------------

"""
PROBLÈME : Afficher 10 000 éléments dans une liste

[X] Sans virtualisation :
-> React crée 10 000 nœuds DOM
-> Le navigateur doit les gérer tous
-> Scroll haché, lag, mémoire saturée

[OK] Avec virtualisation :
-> Seulement les éléments VISIBLES sont dans le DOM
-> Généralement 20-50 éléments max, peu importe la taille de la liste
-> Scroll fluide même avec 1 000 000 éléments !

ANALOGIE :
Un livre de 1000 pages : Vous ne lisez qu'une page à la fois.
La virtualisation ne "rend" que les pages visibles.

BIBLIOTHÈQUE : react-window (recommandée, légère)
INSTALLATION : npm install react-window
"""

import { FixedSizeList, VariableSizeList } from 'react-window';

// 1. LISTE D'ÉLÉMENTS DE TAILLE FIXE
const ITEMS = Array.from({ length: 10000 }, (_, i) => ({
  id: i + 1,
  name: `Utilisateur ${i + 1}`,
  email: `user${i + 1}@example.com`,
  score: Math.floor(Math.random() * 1000),
}));

// Composant pour CHAQUE ligne (reçoit index et style OBLIGATOIRE)
function Row({ index, style }) {
  const item = ITEMS[index];
  return (
    <div style={{
      ...style,  // [ATTENTION] OBLIGATOIRE : contient top, height, etc.
      display: 'flex',
      alignItems: 'center',
      padding: '0 16px',
      borderBottom: '1px solid #e5e7eb',
      backgroundColor: index % 2 === 0 ? 'white' : '#f9fafb',
    }}>
      <span style={{ width: 40, color: '#9ca3af', fontSize: 12 }}>{index + 1}</span>
      <span style={{ flex: 1, fontWeight: 500 }}>{item.name}</span>
      <span style={{ color: '#64748b', fontSize: 13 }}>{item.email}</span>
      <span style={{ width: 80, textAlign: 'right', fontWeight: 'bold', color: item.score > 500 ? '#22c55e' : '#374151' }}>
        {item.score}
      </span>
    </div>
  );
}

function VirtualList() {
  return (
    <div>
      <h2>[UTILISATEURS] {ITEMS.length.toLocaleString()} utilisateurs</h2>

      {/* Header */}
      <div style={{ display: 'flex', padding: '8px 16px', background: '#f8fafc', fontWeight: 'bold', fontSize: 13, borderBottom: '2px solid #e5e7eb' }}>
        <span style={{ width: 40 }}>#</span>
        <span style={{ flex: 1 }}>Nom</span>
        <span>Email</span>
        <span style={{ width: 80, textAlign: 'right' }}>Score</span>
      </div>

      {/* Liste virtualisée */}
      <FixedSizeList
        height={400}       // Hauteur de la "fenêtre" visible
        itemCount={ITEMS.length}  // Nombre TOTAL d'éléments
        itemSize={48}      // Hauteur de chaque ligne (en px)
        width="100%"       // Largeur
      >
        {Row}  {/* Composant à rendre pour chaque ligne */}
      </FixedSizeList>
    </div>
  );
}

// 2. LISTE D'ÉLÉMENTS DE TAILLE VARIABLE
const COMMENTS = Array.from({ length: 1000 }, (_, i) => ({
  id: i + 1,
  author: `Auteur ${i + 1}`,
  text: 'Lorem ipsum '.repeat(Math.floor(Math.random() * 5) + 1),
}));

// Calculer la hauteur de chaque élément
const getItemSize = (index) => {
  const textLength = COMMENTS[index].text.length;
  return Math.ceil(textLength / 80) * 20 + 60;  // Estimation basée sur le texte
};

function Comment({ index, style }) {
  const comment = COMMENTS[index];
  return (
    <div style={{ ...style, padding: '12px 16px', borderBottom: '1px solid #e5e7eb' }}>
      <strong>{comment.author}</strong>
      <p style={{ margin: '4px 0 0', fontSize: 13, color: '#374151' }}>{comment.text}</p>
    </div>
  );
}

function VirtualCommentList() {
  const listRef = useRef(null);

  return (
    <VariableSizeList
      ref={listRef}
      height={500}
      itemCount={COMMENTS.length}
      itemSize={getItemSize}  // <- Fonction au lieu de nombre fixe
      width="100%"
    >
      {Comment}
    </VariableSizeList>
  );
}

"""
[IDEE] REACT-WINDOW vs REACT-VIRTUAL (TanStack Virtual)

react-window :
  [OK] Léger (~2KB)
  [OK] Simple à utiliser
  [X] Fonctionnalités limitées
  -> Bon pour la plupart des cas

@tanstack/react-virtual :
  [OK] Plus flexible
  [OK] Grilles, listes horizontales
  [OK] Meilleur support tailles dynamiques
  [X] Plus complexe
  -> Pour des besoins avancés
"""

# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 9 : OPTIMISER UN DASHBOARD LENT
# ----------------------------------------------------------------------------

"""
OBJECTIF : Transformer un dashboard lent en dashboard rapide

PROBLÈME INITIAL :
- 5000 produits à afficher
- Recherche qui filtre en temps réel
- Statistiques recalculées à chaque rendu
- Graphique coûteux qui se recharge inutilement

OPTIMISATIONS À APPLIQUER :
1. useMemo pour les filtres et statistiques
2. useCallback pour les gestionnaires d'événements
3. React.memo pour les composants de ligne
4. Virtualisation pour la liste de 5000 produits
5. Lazy loading pour le graphique (composant fictif)

CORRIGÉ COMMENTÉ :
"""

import { useState, useMemo, useCallback, memo, lazy, Suspense, useRef } from 'react';
import { FixedSizeList } from 'react-window';

// Simuler 5000 produits
const generateProducts = () =>
  Array.from({ length: 5000 }, (_, i) => ({
    id: i + 1,
    name: ['Laptop', 'Souris', 'Clavier', 'Écran', 'Webcam', 'Hub USB', 'SSD', 'RAM'][i % 8] + ` Pro ${i + 1}`,
    category: ['tech', 'peripherals', 'storage', 'networking'][i % 4],
    price: Math.round((Math.random() * 2000 + 10) * 100) / 100,
    stock: Math.floor(Math.random() * 100),
    rating: Math.round((Math.random() * 2 + 3) * 10) / 10,
  }));

const ALL_PRODUCTS = generateProducts();  // <- Hors du composant, créé UNE FOIS

// [OK] Composant de ligne mémorisé
const ProductRow = memo(function ProductRow({ index, style, data }) {
  const { products, onDelete } = data;
  const product = products[index];

  return (
    <div style={{
      ...style,
      display: 'flex',
      alignItems: 'center',
      padding: '0 16px',
      borderBottom: '1px solid #f0f0f0',
      background: index % 2 === 0 ? 'white' : '#fafafa',
      gap: '12px',
    }}>
      <span style={{ width: 40, color: '#9ca3af', fontSize: 11 }}>{product.id}</span>
      <span style={{ flex: 2, fontWeight: 500, fontSize: 13 }}>{product.name}</span>
      <span style={{ flex: 1, fontSize: 12, color: '#64748b', textTransform: 'capitalize' }}>
        {product.category}
      </span>
      <span style={{ width: 80, fontWeight: 'bold', color: '#3b82f6' }}>
        {product.price.toFixed(2)}€
      </span>
      <span style={{
        width: 70, textAlign: 'center', fontSize: 12,
        color: product.stock === 0 ? '#ef4444' : '#22c55e'
      }}>
        {product.stock === 0 ? '[X] Rupture' : `[OK] ${product.stock}`}
      </span>
      <span style={{ width: 50, textAlign: 'center', fontSize: 12 }}>
        {'*'.repeat(Math.round(product.rating))} {product.rating}
      </span>
      <button
        onClick={() => onDelete(product.id)}
        style={{
          background: 'none', border: 'none', cursor: 'pointer',
          color: '#9ca3af', fontSize: 16, padding: '4px'
        }}
      >[X]</button>
    </div>
  );
});

// Lazy loading du graphique fictif
const StatsChart = lazy(() => new Promise(resolve => {
  // Simuler un composant lourd qui prend du temps à charger
  setTimeout(() => resolve({
    default: function StatsChart({ stats }) {
      return (
        <div style={{ padding: '20px', background: '#eff6ff', borderRadius: '12px' }}>
          <h3>[GRAPHIQUE] Répartition par catégorie</h3>
          {Object.entries(stats.byCategory).map(([cat, count]) => (
            <div key={cat} style={{ display: 'flex', alignItems: 'center', gap: '8px', margin: '8px 0' }}>
              <span style={{ width: 100, textTransform: 'capitalize', fontSize: 13 }}>{cat}</span>
              <div style={{ flex: 1, background: '#dbeafe', borderRadius: '4px', height: '8px' }}>
                <div style={{
                  width: `${(count / stats.total) * 100}%`,
                  background: '#3b82f6', height: '100%', borderRadius: '4px'
                }} />
              </div>
              <span style={{ width: 40, textAlign: 'right', fontSize: 12, color: '#64748b' }}>
                {count}
              </span>
            </div>
          ))}
        </div>
      );
    }
  }), 1500);  // Simuler 1.5s de chargement
}));

function OptimizedDashboard() {
  const [products, setProducts] = useState(ALL_PRODUCTS);
  const [search, setSearch] = useState('');
  const [category, setCategory] = useState('all');
  const [minPrice, setMinPrice] = useState('');
  const [showChart, setShowChart] = useState(false);
  const renderCount = useRef(0);
  renderCount.current++;

  // [OK] useMemo : Filtrage (calcul coûteux sur 5000 items)
  const filteredProducts = useMemo(() => {
    let result = products;

    if (search.trim()) {
      const q = search.toLowerCase();
      result = result.filter(p => p.name.toLowerCase().includes(q));
    }

    if (category !== 'all') {
      result = result.filter(p => p.category === category);
    }

    if (minPrice !== '') {
      const min = parseFloat(minPrice);
      if (!isNaN(min)) result = result.filter(p => p.price >= min);
    }

    return result;
  }, [products, search, category, minPrice]);

  // [OK] useMemo : Statistiques (dépend de filteredProducts)
  const stats = useMemo(() => ({
    total: filteredProducts.length,
    totalRevenue: filteredProducts.reduce((s, p) => s + p.price * p.stock, 0),
    avgPrice: filteredProducts.length > 0
      ? filteredProducts.reduce((s, p) => s + p.price, 0) / filteredProducts.length
      : 0,
    inStock: filteredProducts.filter(p => p.stock > 0).length,
    byCategory: filteredProducts.reduce((acc, p) => {
      acc[p.category] = (acc[p.category] || 0) + 1;
      return acc;
    }, {}),
  }), [filteredProducts]);

  // [OK] useCallback : Handlers stables
  const handleDelete = useCallback((id) => {
    setProducts(prev => prev.filter(p => p.id !== id));
  }, []);

  const handleSearch = useCallback((e) => setSearch(e.target.value), []);
  const handleCategory = useCallback((e) => setCategory(e.target.value), []);
  const handleMinPrice = useCallback((e) => setMinPrice(e.target.value), []);

  // [OK] useMemo : itemData stable pour FixedSizeList
  const itemData = useMemo(() => ({
    products: filteredProducts,
    onDelete: handleDelete,
  }), [filteredProducts, handleDelete]);

  return (
    <div style={{ fontFamily: 'Inter, sans-serif', maxWidth: '1100px', margin: '0 auto', padding: '24px' }}>

      {/* Debug : compteur de rendus */}
      <div style={{ position: 'fixed', top: 10, right: 10, background: '#fef3c7', padding: '4px 10px', borderRadius: '20px', fontSize: 12, zIndex: 999 }}>
        [SYNC] Rendus : {renderCount.current}
      </div>

      <h1 style={{ margin: '0 0 24px' }}>[RAPIDE] Dashboard Optimisé</h1>

      {/* Stats Cards */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '12px', marginBottom: '24px' }}>
        {[
          { label: '[PACKAGE] Produits', value: stats.total.toLocaleString(), color: '#3b82f6' },
          { label: '[OK] En stock', value: stats.inStock.toLocaleString(), color: '#22c55e' },
          { label: '[ARGENT] Prix moy.', value: `${stats.avgPrice.toFixed(2)}€`, color: '#f59e0b' },
          { label: '[HAUSSE] Valeur stock', value: `${(stats.totalRevenue / 1000).toFixed(0)}k€`, color: '#8b5cf6' },
        ].map(({ label, value, color }) => (
          <div key={label} style={{ background: 'white', borderRadius: '12px', padding: '16px', boxShadow: '0 1px 3px rgba(0,0,0,0.1)', borderLeft: `4px solid ${color}` }}>
            <div style={{ fontSize: 12, color: '#64748b' }}>{label}</div>
            <div style={{ fontSize: 22, fontWeight: 'bold', color, marginTop: 4 }}>{value}</div>
          </div>
        ))}
      </div>

      {/* Graphique lazy */}
      <div style={{ marginBottom: '24px' }}>
        {!showChart ? (
          <button
            onClick={() => setShowChart(true)}
            style={{ padding: '10px 20px', background: '#eff6ff', border: '1px solid #bfdbfe', borderRadius: '8px', cursor: 'pointer', color: '#3b82f6', fontSize: 14 }}
          >
            [GRAPHIQUE] Afficher le graphique (chargement différé)
          </button>
        ) : (
          <Suspense fallback={<div style={{ padding: '20px', textAlign: 'center', color: '#64748b' }}>[HOURGLASS_WITH_FLOWING_SAND] Chargement du graphique...</div>}>
            <StatsChart stats={stats} />
          </Suspense>
        )}
      </div>

      {/* Filtres */}
      <div style={{ display: 'flex', gap: '12px', marginBottom: '16px', flexWrap: 'wrap' }}>
        <input
          value={search}
          onChange={handleSearch}
          placeholder="[RECHERCHE] Rechercher..."
          style={{ flex: 1, minWidth: '200px', padding: '10px 14px', borderRadius: '8px', border: '1px solid #d1d5db', fontSize: 14 }}
        />
        <select
          value={category}
          onChange={handleCategory}
          style={{ padding: '10px 14px', borderRadius: '8px', border: '1px solid #d1d5db', fontSize: 14 }}
        >
          <option value="all">Toutes catégories</option>
          <option value="tech">Tech</option>
          <option value="peripherals">Périphériques</option>
          <option value="storage">Stockage</option>
          <option value="networking">Réseau</option>
        </select>
        <input
          value={minPrice}
          onChange={handleMinPrice}
          placeholder="Prix min (€)"
          type="number"
          style={{ width: '130px', padding: '10px 14px', borderRadius: '8px', border: '1px solid #d1d5db', fontSize: 14 }}
        />
      </div>

      {/* Info résultats */}
      <div style={{ marginBottom: '8px', fontSize: 13, color: '#64748b' }}>
        {filteredProducts.length.toLocaleString()} produits (sur {ALL_PRODUCTS.length.toLocaleString()})
      </div>

      {/* Header du tableau */}
      <div style={{
        display: 'flex', alignItems: 'center', padding: '10px 16px',
        background: '#f8fafc', borderRadius: '8px 8px 0 0',
        border: '1px solid #e5e7eb', fontSize: 12, fontWeight: '600', color: '#374151', gap: '12px'
      }}>
        <span style={{ width: 40 }}>#</span>
        <span style={{ flex: 2 }}>PRODUIT</span>
        <span style={{ flex: 1 }}>CATÉGORIE</span>
        <span style={{ width: 80 }}>PRIX</span>
        <span style={{ width: 70, textAlign: 'center' }}>STOCK</span>
        <span style={{ width: 50, textAlign: 'center' }}>NOTE</span>
        <span style={{ width: 24 }}></span>
      </div>

      {/* [OK] Liste virtualisée : seulement ~12 lignes rendues dans le DOM */}
      <div style={{ border: '1px solid #e5e7eb', borderTop: 'none', borderRadius: '0 0 8px 8px', overflow: 'hidden' }}>
        {filteredProducts.length === 0 ? (
          <div style={{ padding: '40px', textAlign: 'center', color: '#9ca3af' }}>
            Aucun produit trouvé [RECHERCHE]
          </div>
        ) : (
          <FixedSizeList
            height={480}
            itemCount={filteredProducts.length}
            itemSize={44}
            width="100%"
            itemData={itemData}  // <- Données passées via itemData (stable grâce à useMemo)
          >
            {ProductRow}
          </FixedSizeList>
        )}
      </div>
    </div>
  );
}

export default OptimizedDashboard;

"""
[OK] CE QUE VOUS AVEZ PRATIQUÉ :
- Identifier les problèmes de performance (renderCount)
- useMemo pour filtrage et statistiques coûteux
- useCallback pour gestionnaires stables
- React.memo pour les lignes de liste
- FixedSizeList pour la virtualisation
- Lazy loading du graphique avec Suspense
- Stabiliser itemData pour react-window
"""

# ============================================================================
# [GUIDE] CHAPITRE 15 : TESTING AVEC REACT TESTING LIBRARY
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Philosophie du testing en React
[OK] Configurer Vitest + React Testing Library
[OK] Écrire des tests unitaires pour composants
[OK] Tester les interactions utilisateur
[OK] Tester des hooks personnalisés
[OK] Mocker des appels API
[OK] Coverage et bonnes pratiques
"""

# ----------------------------------------------------------------------------
# [REFLEXION] POURQUOI TESTER ?
# ----------------------------------------------------------------------------

"""
SANS TESTS :
- Chaque modification risque de casser autre chose
- On doit tout tester manuellement à chaque fois
- Pas de filet de sécurité
- Peur de refactorer
- Bugs en production que les utilisateurs trouvent

AVEC TESTS :
[OK] Confiance pour modifier/refactorer du code
[OK] Documentation vivante du comportement
[OK] Détection des régressions automatique
[OK] Développement plus rapide sur le long terme

LES 3 TYPES DE TESTS :

1. Tests UNITAIRES : Tester un composant / hook isolément
   -> Rapide, nombreux, fondation

2. Tests d'INTÉGRATION : Tester plusieurs composants ensemble
   -> Moyen, important, vérifie les interactions

3. Tests E2E (End-to-End) : Tester l'app complète dans un navigateur
   -> Lent, rare, vérifie le parcours utilisateur complet

PYRAMIDE DES TESTS :
         /\
        /E2E\    -> Peu (5-10)
       /──────\
      /Intégration\ -> Quelques-uns (20-50)
     /──────────────\
    /    Unitaires   \ -> Beaucoup (100+)
   /──────────────────\
"""

# ----------------------------------------------------------------------------
# [OUTILS] INSTALLATION ET CONFIGURATION
# ----------------------------------------------------------------------------

"""
STACK RECOMMANDÉE (2024) :
- Vitest (runner de tests, compatible Vite)
- React Testing Library (tester les composants)
- @testing-library/user-event (simuler interactions)
- @testing-library/jest-dom (matchers supplémentaires)

INSTALLATION :
"""

$ npm install -D vitest @vitest/coverage-v8 jsdom
$ npm install -D @testing-library/react @testing-library/user-event @testing-library/jest-dom

"""
CONFIGURATION vite.config.js :
"""

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,           // describe, it, expect disponibles sans import
    environment: 'jsdom',    // Simuler un navigateur
    setupFiles: ['./src/setupTests.js'],  // Fichier de config global
    coverage: {
      reporter: ['text', 'html'],  // Rapport de couverture
      include: ['src/**/*.{js,jsx}'],
    },
  },
});

// src/setupTests.js
import '@testing-library/jest-dom';  // Matchers comme toBeInTheDocument()

"""
SCRIPTS dans package.json :
"""

// package.json
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "test": "vitest",
    "test:ui": "vitest --ui",        // Interface graphique
    "test:run": "vitest run",         // Exécuter une fois (CI)
    "test:coverage": "vitest run --coverage"
  }
}

"""
LANCER LES TESTS :
$ npm test              -> Mode watch (re-teste à chaque modification)
$ npm run test:run      -> Exécuter une fois et quitter
$ npm run test:coverage -> Avec rapport de couverture
$ npm run test:ui       -> Interface graphique dans le navigateur
"""

# ----------------------------------------------------------------------------
# [TEST] PREMIERS TESTS : COMPOSANTS SIMPLES
# ----------------------------------------------------------------------------

"""
PHILOSOPHIE DE REACT TESTING LIBRARY :

"Tester votre application comme vos utilisateurs l'utilisent"

-> Ne pas tester les détails d'implémentation (état interne, noms de variables)
-> Tester ce que l'UTILISATEUR voit et fait
-> Utiliser des sélecteurs accessibles (rôle, label, texte)

SÉLECTEURS RECOMMANDÉS (ordre de priorité) :
1. getByRole        -> Rôle ARIA ('button', 'textbox', 'heading'...)
2. getByLabelText   -> Label associé à un input
3. getByPlaceholderText -> Placeholder
4. getByText        -> Texte visible
5. getByDisplayValue -> Valeur affichée d'un input
6. getByAltText     -> Attribut alt d'une image
7. getByTitle       -> Attribut title
8. getByTestId      -> data-testid (DERNIER RECOURS)
"""

// src/components/Button.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import Button from './Button';

describe('Button', () => {

  // Test 1 : Rendu de base
  it('affiche le texte du bouton', () => {
    render(<Button>Cliquer ici</Button>);

    // getByText : Trouver l'élément avec ce texte
    const button = screen.getByText('Cliquer ici');
    expect(button).toBeInTheDocument();
  });

  // Test 2 : Attribut disabled
  it('est désactivé quand disabled est true', () => {
    render(<Button disabled>Désactivé</Button>);

    // getByRole : Trouver par rôle ARIA
    const button = screen.getByRole('button', { name: 'Désactivé' });
    expect(button).toBeDisabled();
  });

  // Test 3 : Gestion du clic
  it('appelle onClick quand cliqué', async () => {
    const handleClick = vi.fn();  // Fonction mock
    render(<Button onClick={handleClick}>Cliquer</Button>);

    const user = userEvent.setup();
    await user.click(screen.getByRole('button'));

    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  // Test 4 : Pas de clic si disabled
  it('n\'appelle pas onClick quand disabled', async () => {
    const handleClick = vi.fn();
    render(<Button onClick={handleClick} disabled>Désactivé</Button>);

    const user = userEvent.setup();
    await user.click(screen.getByRole('button'));

    expect(handleClick).not.toHaveBeenCalled();
  });

  // Test 5 : Classe CSS variant
  it('applique la classe variant correcte', () => {
    const { rerender } = render(<Button variant="primary">Primary</Button>);
    expect(screen.getByRole('button')).toHaveClass('btn-primary');

    rerender(<Button variant="danger">Danger</Button>);
    expect(screen.getByRole('button')).toHaveClass('btn-danger');
  });

});

"""
MATCHERS DISPONIBLES (jest-dom) :

// Présence
expect(element).toBeInTheDocument()
expect(element).not.toBeInTheDocument()

// Visibilité
expect(element).toBeVisible()
expect(element).toBeHidden()

// État
expect(element).toBeEnabled()
expect(element).toBeDisabled()
expect(element).toBeChecked()
expect(element).not.toBeChecked()

// Contenu
expect(element).toHaveTextContent('Bonjour')
expect(element).toHaveTextContent(/Bon/)     // Regex
expect(element).toHaveValue('alice@example.com')
expect(element).toHaveDisplayValue('Option 2')

// Attributs & classes
expect(element).toHaveAttribute('href', '/home')
expect(element).toHaveClass('active')
expect(element).toHaveStyle({ color: 'red' })

// Focus
expect(element).toHaveFocus()

// Mock
expect(mockFn).toHaveBeenCalled()
expect(mockFn).toHaveBeenCalledTimes(3)
expect(mockFn).toHaveBeenCalledWith('argument')
"""

# ----------------------------------------------------------------------------
# [SOURIS] TESTER LES INTERACTIONS
# ----------------------------------------------------------------------------

// src/components/Counter.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';

describe('Counter', () => {

  it('démarre à 0', () => {
    render(<Counter />);
    expect(screen.getByText('0')).toBeInTheDocument();
  });

  it('incrémente au clic sur +1', async () => {
    render(<Counter />);
    const user = userEvent.setup();

    await user.click(screen.getByRole('button', { name: '+1' }));

    expect(screen.getByText('1')).toBeInTheDocument();
  });

  it('décrémente au clic sur -1', async () => {
    render(<Counter />);
    const user = userEvent.setup();

    // D'abord monter à 5
    for (let i = 0; i < 5; i++) {
      await user.click(screen.getByRole('button', { name: '+1' }));
    }
    expect(screen.getByText('5')).toBeInTheDocument();

    // Puis descendre
    await user.click(screen.getByRole('button', { name: '-1' }));
    expect(screen.getByText('4')).toBeInTheDocument();
  });

  it('se réinitialise au clic sur Reset', async () => {
    render(<Counter />);
    const user = userEvent.setup();

    await user.click(screen.getByRole('button', { name: '+1' }));
    await user.click(screen.getByRole('button', { name: '+1' }));
    await user.click(screen.getByRole('button', { name: 'Reset' }));

    expect(screen.getByText('0')).toBeInTheDocument();
  });

});

// src/components/LoginForm.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginForm from './LoginForm';

describe('LoginForm', () => {

  it('soumet le formulaire avec email et mot de passe', async () => {
    const handleSubmit = vi.fn();
    render(<LoginForm onSubmit={handleSubmit} />);

    const user = userEvent.setup();

    // Remplir les champs
    await user.type(screen.getByLabelText('Email'), 'alice@example.com');
    await user.type(screen.getByLabelText('Mot de passe'), 'MonMotDePasse123');

    // Soumettre
    await user.click(screen.getByRole('button', { name: 'Se connecter' }));

    expect(handleSubmit).toHaveBeenCalledWith({
      email: 'alice@example.com',
      password: 'MonMotDePasse123',
    });
  });

  it('affiche une erreur si email invalide', async () => {
    render(<LoginForm />);
    const user = userEvent.setup();

    await user.type(screen.getByLabelText('Email'), 'pas-un-email');
    await user.tab();  // Déclencher onBlur

    expect(await screen.findByText('Email invalide')).toBeInTheDocument();
  });

  it('le bouton Submit est désactivé si formulaire vide', () => {
    render(<LoginForm />);
    expect(screen.getByRole('button', { name: 'Se connecter' })).toBeDisabled();
  });

  it('affiche/cache le mot de passe au clic sur l\'oeil', async () => {
    render(<LoginForm />);
    const user = userEvent.setup();
    const passwordInput = screen.getByLabelText('Mot de passe');

    expect(passwordInput).toHaveAttribute('type', 'password');

    await user.click(screen.getByRole('button', { name: 'Voir le mot de passe' }));

    expect(passwordInput).toHaveAttribute('type', 'text');
  });

});

# ----------------------------------------------------------------------------
# [WEB] TESTER DES APPELS API
# ----------------------------------------------------------------------------

"""
Pour les tests, on ne fait PAS de vrais appels API !
-> Trop lent, dépend du réseau, données qui changent
-> On MOQUE (mock) les appels

BIBLIOTHÈQUES :
- vi.fn() : Mock simple
- vi.mock() : Mock un module entier
- msw (Mock Service Worker) : Simuler un vrai serveur HTTP
"""

// 1. Mocker fetch directement
// src/components/UserList.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import UserList from './UserList';

// Données de test
const MOCK_USERS = [
  { id: 1, name: 'Alice Martin', email: 'alice@example.com' },
  { id: 2, name: 'Bob Dupont', email: 'bob@example.com' },
];

describe('UserList', () => {

  beforeEach(() => {
    // Mocker fetch avant chaque test
    global.fetch = vi.fn();
  });

  afterEach(() => {
    vi.restoreAllMocks();  // Nettoyer après chaque test
  });

  it('affiche la liste des utilisateurs', async () => {
    // Configurer le mock pour retourner des données
    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => MOCK_USERS,
    });

    render(<UserList />);

    // D'abord le loading
    expect(screen.getByText('Chargement...')).toBeInTheDocument();

    // Attendre que les données soient affichées
    await waitFor(() => {
      expect(screen.getByText('Alice Martin')).toBeInTheDocument();
    });
    expect(screen.getByText('Bob Dupont')).toBeInTheDocument();
  });

  it('affiche un message d\'erreur si l\'API échoue', async () => {
    fetch.mockRejectedValueOnce(new Error('Network error'));

    render(<UserList />);

    await waitFor(() => {
      expect(screen.getByText(/Erreur/i)).toBeInTheDocument();
    });
  });

  it('affiche un message si aucun utilisateur', async () => {
    fetch.mockResolvedValueOnce({
      ok: true,
      json: async () => [],
    });

    render(<UserList />);

    await waitFor(() => {
      expect(screen.getByText('Aucun utilisateur trouvé')).toBeInTheDocument();
    });
  });

});


// 2. MOCKER UN MODULE (vi.mock)
// Exemple : Mocker le fichier api.js
vi.mock('../services/api', () => ({
  default: {
    get: vi.fn(),
    post: vi.fn(),
    delete: vi.fn(),
  },
}));

import api from '../services/api';

describe('UserManager', () => {
  it('supprime un utilisateur', async () => {
    api.delete.mockResolvedValueOnce({ success: true });

    render(<UserManager />);

    // Cliquer sur Supprimer
    await userEvent.setup().click(screen.getByRole('button', { name: 'Supprimer Alice' }));

    expect(api.delete).toHaveBeenCalledWith('/users/1');
    await waitFor(() => {
      expect(screen.queryByText('Alice Martin')).not.toBeInTheDocument();
    });
  });
});

# ----------------------------------------------------------------------------
# [HOOK] TESTER DES HOOKS PERSONNALISÉS
# ----------------------------------------------------------------------------

"""
Pour tester des hooks, on utilise renderHook de Testing Library
"""

import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
import useLocalStorage from './useLocalStorage';
import useFetch from './useFetch';

// Test d'un hook simple : useCounter
describe('useCounter', () => {

  it('initialise avec la valeur fournie', () => {
    const { result } = renderHook(() => useCounter(10));
    expect(result.current.count).toBe(10);
  });

  it('incrémente correctement', () => {
    const { result } = renderHook(() => useCounter(0));

    act(() => {
      result.current.increment();
    });

    expect(result.current.count).toBe(1);
  });

  it('décrémente correctement', () => {
    const { result } = renderHook(() => useCounter(5));

    act(() => {
      result.current.decrement();
    });

    expect(result.current.count).toBe(4);
  });

  it('se réinitialise à 0', () => {
    const { result } = renderHook(() => useCounter(42));

    act(() => {
      result.current.reset();
    });

    expect(result.current.count).toBe(0);
  });

});

// Test de useLocalStorage
describe('useLocalStorage', () => {

  beforeEach(() => {
    localStorage.clear();
  });

  it('retourne la valeur initiale si rien en localStorage', () => {
    const { result } = renderHook(() => useLocalStorage('test-key', 'valeur initiale'));
    expect(result.current[0]).toBe('valeur initiale');
  });

  it('sauvegarde la valeur dans localStorage', () => {
    const { result } = renderHook(() => useLocalStorage('test-key', ''));

    act(() => {
      result.current[1]('nouvelle valeur');
    });

    expect(result.current[0]).toBe('nouvelle valeur');
    expect(localStorage.getItem('test-key')).toBe('"nouvelle valeur"');
  });

  it('charge une valeur existante du localStorage', () => {
    localStorage.setItem('existing-key', JSON.stringify(42));
    const { result } = renderHook(() => useLocalStorage('existing-key', 0));
    expect(result.current[0]).toBe(42);
  });

});

// Test de useFetch (avec mock)
describe('useFetch', () => {

  it('démarre en loading', () => {
    global.fetch = vi.fn(() => new Promise(() => {}));  // Jamais résolu

    const { result } = renderHook(() => useFetch('/api/data'));

    expect(result.current.loading).toBe(true);
    expect(result.current.data).toBeNull();
    expect(result.current.error).toBeNull();
  });

  it('retourne les données après succès', async () => {
    const mockData = { id: 1, name: 'Test' };
    global.fetch = vi.fn().mockResolvedValueOnce({
      ok: true,
      json: async () => mockData,
    });

    const { result } = renderHook(() => useFetch('/api/data'));

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.data).toEqual(mockData);
    expect(result.current.error).toBeNull();
  });

  it('retourne une erreur en cas d\'échec', async () => {
    global.fetch = vi.fn().mockRejectedValueOnce(new Error('Network error'));

    const { result } = renderHook(() => useFetch('/api/data'));

    await waitFor(() => {
      expect(result.current.loading).toBe(false);
    });

    expect(result.current.error).toBe('Network error');
    expect(result.current.data).toBeNull();
  });

});

# ----------------------------------------------------------------------------
# [CONSTRUCTION] TESTER AVEC UN CONTEXTE (Context Provider)
# ----------------------------------------------------------------------------

"""
Si votre composant utilise useContext, il faut l'envelopper avec le Provider dans les tests.
"""

// Utilitaire : Wrapper avec tous les providers
function AllProviders({ children }) {
  return (
    <BrowserRouter>
      <AuthProvider>
        <CartProvider>
          {children}
        </CartProvider>
      </AuthProvider>
    </BrowserRouter>
  );
}

// Fonction render personnalisée
function renderWithProviders(ui, options = {}) {
  return render(ui, { wrapper: AllProviders, ...options });
}

// Utilisation dans les tests
describe('UserProfile (avec Context)', () => {

  it('affiche le nom de l\'utilisateur connecté', () => {
    renderWithProviders(<UserProfile />);
    // Si AuthContext fournit un user...
    expect(screen.getByText('Bonjour, Alice !')).toBeInTheDocument();
  });

  it('redirige si non connecté', () => {
    // Mock du contexte pour simuler un utilisateur non connecté
    vi.mock('../context/AuthContext', () => ({
      useAuth: () => ({ user: null, isAuthenticated: false }),
    }));

    renderWithProviders(<ProtectedPage />);
    expect(screen.getByText('Veuillez vous connecter')).toBeInTheDocument();
  });

});

# ----------------------------------------------------------------------------
# [GRAPHIQUE] COVERAGE (COUVERTURE DE CODE)
# ----------------------------------------------------------------------------

"""
COVERAGE = Pourcentage du code testé

MÉTRIQUES :
- Statements  : Lignes exécutées
- Branches    : if/else couverts (vrai ET faux)
- Functions   : Fonctions appelées
- Lines       : Lignes de code couvertes

LANCER :
$ npm run test:coverage

RAPPORT :
----------|---------|----------|---------|---------|
File      | % Stmts | % Branch | % Funcs | % Lines |
----------|---------|----------|---------|---------|
Button.jsx|   100   |   100    |   100   |   100   |
UserCard  |    80   |    60    |    75   |    80   |
----------|---------|----------|---------|---------|

OBJECTIFS RAISONNABLES :
- 80% de coverage global -> Très bon
- 100% -> Parfois counter-productif (tests superficiels pour remplir le score)

NE PAS TESTER :
- main.jsx (point d'entrée)
- Fichiers CSS
- Fichiers de configuration
- Code tiers (bibliothèques)
"""

// vitest.config.js : Exclure certains fichiers du coverage
export default defineConfig({
  test: {
    coverage: {
      exclude: [
        'src/main.jsx',
        'src/**/*.d.ts',
        'src/**/*.test.{js,jsx}',
        'src/mocks/**',
      ],
      thresholds: {
        lines: 80,      // Échoue si < 80% de lignes couvertes
        functions: 80,
        branches: 70,
        statements: 80,
      },
    },
  },
});

# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE 10 : TESTER UNE TODO APP
# ----------------------------------------------------------------------------

"""
OBJECTIF : Écrire une suite de tests complète pour la TodoApp du Chapitre 4

TESTS À ÉCRIRE :
1. Affichage initial de la liste
2. Ajouter une tâche
3. Marquer une tâche comme terminée
4. Supprimer une tâche
5. Filtres (Toutes / Actives / Terminées)
6. Compteur de tâches restantes
7. Bouton "Effacer terminées"
8. Ne pas ajouter une tâche vide

CORRIGÉ COMMENTÉ :
"""

// src/components/TodoApp.test.jsx
import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import TodoApp from './TodoApp';

describe('TodoApp', () => {
  let user;

  beforeEach(() => {
    user = userEvent.setup();
  });

  // ----- AFFICHAGE INITIAL -----

  it('affiche les tâches initiales', () => {
    render(<TodoApp />);

    expect(screen.getByText('Apprendre React')).toBeInTheDocument();
    expect(screen.getByText('Faire du sport')).toBeInTheDocument();
    expect(screen.getByText('Lire un livre')).toBeInTheDocument();
  });

  it('affiche le bon nombre de tâches restantes', () => {
    render(<TodoApp />);
    // 'Faire du sport' est done=true -> 2 restantes
    expect(screen.getByText(/2 tâches restantes/)).toBeInTheDocument();
  });

  // ----- AJOUTER UNE TÂCHE -----

  it('ajoute une nouvelle tâche', async () => {
    render(<TodoApp />);

    const input = screen.getByPlaceholderText(/que faire/i);
    await user.type(input, 'Nouvelle tâche super importante');
    await user.click(screen.getByRole('button', { name: '+' }));

    expect(screen.getByText('Nouvelle tâche super importante')).toBeInTheDocument();
    expect(input).toHaveValue('');  // Input vidé après ajout
  });

  it('ajoute une tâche avec la touche Entrée', async () => {
    render(<TodoApp />);

    const input = screen.getByPlaceholderText(/que faire/i);
    await user.type(input, 'Tâche via Entrée{Enter}');

    expect(screen.getByText('Tâche via Entrée')).toBeInTheDocument();
  });

  it('n\'ajoute pas une tâche vide', async () => {
    render(<TodoApp />);
    const initialCount = screen.getAllByRole('checkbox').length;

    await user.click(screen.getByRole('button', { name: '+' }));

    expect(screen.getAllByRole('checkbox')).toHaveLength(initialCount);
  });

  it('n\'ajoute pas une tâche avec seulement des espaces', async () => {
    render(<TodoApp />);
    const initialCount = screen.getAllByRole('checkbox').length;

    await user.type(screen.getByPlaceholderText(/que faire/i), '   ');
    await user.click(screen.getByRole('button', { name: '+' }));

    expect(screen.getAllByRole('checkbox')).toHaveLength(initialCount);
  });

  // ----- MARQUER COMME TERMINÉE -----

  it('marque une tâche comme terminée', async () => {
    render(<TodoApp />);

    // Trouver la checkbox de "Apprendre React"
    const taskItem = screen.getByText('Apprendre React').closest('li');
    const checkbox = within(taskItem).getByRole('checkbox');

    expect(checkbox).not.toBeChecked();
    await user.click(checkbox);
    expect(checkbox).toBeChecked();

    // Le texte doit être barré (vérifier la classe ou le style)
    expect(screen.getByText('Apprendre React')).toHaveStyle({
      textDecoration: 'line-through'
    });
  });

  it('met à jour le compteur quand une tâche est complétée', async () => {
    render(<TodoApp />);

    expect(screen.getByText(/2 tâches restantes/)).toBeInTheDocument();

    const apprendreCbx = within(
      screen.getByText('Apprendre React').closest('li')
    ).getByRole('checkbox');

    await user.click(apprendreCbx);

    expect(screen.getByText(/1 tâche restante/)).toBeInTheDocument();
  });

  // ----- SUPPRIMER -----

  it('supprime une tâche', async () => {
    render(<TodoApp />);

    const taskItem = screen.getByText('Lire un livre').closest('li');
    const deleteBtn = within(taskItem).getByRole('button');

    await user.click(deleteBtn);

    expect(screen.queryByText('Lire un livre')).not.toBeInTheDocument();
  });

  // ----- FILTRES -----

  it('filtre pour afficher seulement les tâches actives', async () => {
    render(<TodoApp />);

    await user.click(screen.getByRole('button', { name: 'Actives' }));

    expect(screen.getByText('Apprendre React')).toBeInTheDocument();
    expect(screen.getByText('Lire un livre')).toBeInTheDocument();
    expect(screen.queryByText('Faire du sport')).not.toBeInTheDocument();
  });

  it('filtre pour afficher seulement les tâches terminées', async () => {
    render(<TodoApp />);

    await user.click(screen.getByRole('button', { name: 'Terminées' }));

    expect(screen.queryByText('Apprendre React')).not.toBeInTheDocument();
    expect(screen.getByText('Faire du sport')).toBeInTheDocument();
  });

  it('affiche toutes les tâches avec le filtre "Toutes"', async () => {
    render(<TodoApp />);

    await user.click(screen.getByRole('button', { name: 'Terminées' }));
    await user.click(screen.getByRole('button', { name: 'Toutes' }));

    expect(screen.getByText('Apprendre React')).toBeInTheDocument();
    expect(screen.getByText('Faire du sport')).toBeInTheDocument();
    expect(screen.getByText('Lire un livre')).toBeInTheDocument();
  });

  // ----- EFFACER TERMINÉES -----

  it('efface toutes les tâches terminées', async () => {
    render(<TodoApp />);

    await user.click(screen.getByRole('button', { name: 'Effacer terminées' }));

    expect(screen.queryByText('Faire du sport')).not.toBeInTheDocument();
    expect(screen.getByText('Apprendre React')).toBeInTheDocument();
    expect(screen.getByText('Lire un livre')).toBeInTheDocument();
  });

  it('masque le bouton "Effacer terminées" si aucune tâche terminée', async () => {
    render(<TodoApp />);

    // Supprimer la tâche terminée
    const taskItem = screen.getByText('Faire du sport').closest('li');
    await user.click(within(taskItem).getByRole('button'));

    expect(screen.queryByRole('button', { name: 'Effacer terminées' }))
      .not.toBeInTheDocument();
  });

});

"""
[OK] BONNES PRATIQUES DE TEST :

1. AAA : Arrange, Act, Assert (Préparer, Agir, Vérifier)
2. Un test = Un comportement (pas plusieurs assertions non liées)
3. Nommer clairement : 'quand X alors Y'
4. Nettoyer après chaque test (beforeEach/afterEach)
5. Tester les edge cases (liste vide, valeurs limites)
6. Ne pas tester les détails d'implémentation
7. Préférer findBy à waitFor pour les éléments asynchrones
"""

# ============================================================================
# [GUIDE] CHAPITRE 16 : BEST PRACTICES & ARCHITECTURE
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Structurer un projet React à grande échelle
[OK] Conventions de nommage professionnelles
[OK] Patterns d'architecture (Feature-based)
[OK] Gestion des erreurs (Error Boundaries)
[OK] Accessibilité (a11y)
[OK] TypeScript avec React (introduction)
[OK] Variables d'environnement
"""

# ----------------------------------------------------------------------------
# [DOSSIER] ARCHITECTURE D'UN PROJET À GRANDE ÉCHELLE
# ----------------------------------------------------------------------------

"""
STRUCTURE NAIVE (Mauvaise pour les grands projets) :
"""

src/
├── components/    # 200 fichiers mélangés !
├── hooks/         # Tous les hooks
├── pages/         # Toutes les pages
└── utils/         # Tout le reste

"""
STRUCTURE RECOMMANDÉE : Feature-based (orientée fonctionnalités)

Principe : "Tout ce qui concerne une fonctionnalité est au même endroit"
-> Facilite : Trouver du code, comprendre le contexte, supprimer une feature
"""

src/
├── features/                    # [OBJECTIF] Cœur de l'app - par fonctionnalité
│   ├── auth/                    # Authentification
│   │   ├── components/          # Composants spécifiques auth
│   │   │   ├── LoginForm.jsx
│   │   │   ├── RegisterForm.jsx
│   │   │   └── PasswordReset.jsx
│   │   ├── hooks/               # Hooks spécifiques auth
│   │   │   └── useAuth.js
│   │   ├── context/
│   │   │   └── AuthContext.jsx
│   │   ├── services/            # Appels API auth
│   │   │   └── authService.js
│   │   ├── types/               # Types TypeScript
│   │   │   └── auth.types.ts
│   │   └── index.js             # Point d'entrée (exports publics)
│   │
│   ├── products/                # Catalogue produits
│   │   ├── components/
│   │   │   ├── ProductCard.jsx
│   │   │   ├── ProductList.jsx
│   │   │   └── ProductDetail.jsx
│   │   ├── hooks/
│   │   │   ├── useProducts.js
│   │   │   └── useProductSearch.js
│   │   ├── services/
│   │   │   └── productService.js
│   │   └── index.js
│   │
│   └── cart/                    # Panier
│       ├── components/
│       │   ├── CartSummary.jsx
│       │   └── CartItem.jsx
│       ├── context/
│       │   └── CartContext.jsx
│       └── index.js
│
├── components/                  # Composants GÉNÉRIQUES/PARTAGÉS
│   ├── ui/                      # Composants "atomiques" (système de design)
│   │   ├── Button/
│   │   │   ├── Button.jsx
│   │   │   ├── Button.test.jsx
│   │   │   ├── Button.css
│   │   │   └── index.js
│   │   ├── Input/
│   │   ├── Modal/
│   │   ├── Card/
│   │   └── index.js             # Exporter tous les composants UI
│   │
│   └── layout/                  # Layouts réutilisables
│       ├── Header.jsx
│       ├── Footer.jsx
│       ├── Sidebar.jsx
│       └── PageLayout.jsx
│
├── hooks/                       # Hooks GLOBAUX réutilisables
│   ├── useWindowSize.js
│   ├── useDebounce.js
│   ├── useLocalStorage.js
│   └── usePrevious.js
│
├── services/                    # Services API génériques
│   ├── api.js                   # Instance axios configurée
│   └── websocket.js
│
├── context/                     # Contextes GLOBAUX
│   ├── ThemeContext.jsx
│   └── NotificationContext.jsx
│
├── utils/                       # Fonctions utilitaires
│   ├── formatters.js            # Formater dates, prix, etc.
│   ├── validators.js            # Fonctions de validation
│   └── constants.js             # Constantes globales
│
├── types/                       # Types TypeScript globaux
│   └── index.ts
│
├── pages/                       # Pages = Routes (juste de la composition)
│   ├── HomePage.jsx
│   ├── ProductsPage.jsx
│   ├── CartPage.jsx
│   └── NotFoundPage.jsx
│
├── App.jsx                      # Composant racine + Routing
└── main.jsx                     # Point d'entrée

"""
PRINCIPE DES INDEX.JS (Barrel exports) :
-> Contrôler ce qui est "public" depuis chaque feature
"""

// features/products/index.js
export { ProductCard } from './components/ProductCard';
export { ProductList } from './components/ProductList';
export { useProducts } from './hooks/useProducts';
// [X] On n'exporte PAS productService directement (interne à la feature)

// Utilisation propre depuis une autre feature :
import { ProductCard, useProducts } from '@/features/products';
// Pas : import { ProductCard } from '@/features/products/components/ProductCard'

"""
ALIAS DE CHEMINS (Vite) :
-> Remplacer '../../../components/Button' par '@/components/Button'
"""

// vite.config.js
import path from 'path';
import { defineConfig } from 'vite';

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),  // @ = dossier src/
    },
  },
});

// Utilisation :
import Button from '@/components/ui/Button';          // Au lieu de '../../../../components/ui/Button'
import { useAuth } from '@/features/auth';
import api from '@/services/api';

# ----------------------------------------------------------------------------
# [SECURITE] ERROR BOUNDARIES - GÉRER LES ERREURS
# ----------------------------------------------------------------------------

"""
PROBLÈME :
Une erreur JavaScript dans un composant plante TOUTE l'application !
L'utilisateur voit un écran blanc... [!]

SOLUTION : Error Boundary
-> Composant qui "capture" les erreurs de ses enfants
-> Affiche un message d'erreur élégant à la place
-> Le reste de l'app continue de fonctionner

[ATTENTION] Error Boundaries = SEULS composants qui doivent être des classes !
   (React n'a pas encore d'hooks pour ça)
"""

import { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  // Appelé quand une erreur est capturée
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  // Pour logger l'erreur (analytics, Sentry, etc.)
  componentDidCatch(error, info) {
    console.error('[X] ErrorBoundary a capturé :', error);
    console.error('Component stack :', info.componentStack);

    // Envoyer à un service de monitoring
    // Sentry.captureException(error, { contexts: { react: info } });
  }

  render() {
    if (this.state.hasError) {
      // Afficher le fallback
      if (this.props.fallback) {
        return this.props.fallback;
      }

      return (
        <div style={{
          padding: '40px', textAlign: 'center', maxWidth: '500px', margin: '0 auto'
        }}>
          <h2>[DIZZY_FACE] Oups ! Quelque chose s'est cassé.</h2>
          <p style={{ color: '#64748b', marginBottom: '20px' }}>
            Une erreur inattendue s'est produite. Notre équipe en a été notifiée.
          </p>
          <pre style={{
            background: '#fef2f2', padding: '16px', borderRadius: '8px',
            fontSize: '12px', textAlign: 'left', overflow: 'auto', color: '#dc2626'
          }}>
            {this.state.error.message}
          </pre>
          <button
            onClick={() => this.setState({ hasError: false, error: null })}
            style={{ marginTop: '20px', padding: '10px 20px', background: '#3b82f6', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer' }}
          >
            [SYNC] Réessayer
          </button>
          <button
            onClick={() => window.location.reload()}
            style={{ marginTop: '20px', marginLeft: '10px', padding: '10px 20px', background: '#f1f5f9', border: 'none', borderRadius: '8px', cursor: 'pointer' }}
          >
            [ACCUEIL] Recharger la page
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// UTILISATION :
function App() {
  return (
    <ErrorBoundary>  {/* Global */}
      <Navbar />
      <ErrorBoundary fallback={<p>Le dashboard est indisponible</p>}>
        <Dashboard />  {/* Erreur ici = seulement Dashboard affecté */}
      </ErrorBoundary>
      <Sidebar />
    </ErrorBoundary>
  );
}

"""
BIBLIOTHÈQUE : react-error-boundary
-> Composant Error Boundary + hooks

$ npm install react-error-boundary
"""

import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div>
      <p>Erreur : {error.message}</p>
      <button onClick={resetErrorBoundary}>Réessayer</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onError={(error, info) => console.error(error, info)}
      onReset={() => window.location.reload()}
    >
      <MyComponent />
    </ErrorBoundary>
  );
}

# ----------------------------------------------------------------------------
# [WHEELCHAIR_SYMBOL] ACCESSIBILITÉ (A11Y)
# ----------------------------------------------------------------------------

"""
L'ACCESSIBILITÉ = Rendre votre app utilisable par tous :
- Personnes malvoyantes (lecteurs d'écran)
- Personnes avec mobilité réduite (navigation clavier)
- Personnes avec troubles cognitifs

[IDEE] BONUS : Une app accessible est meilleur pour le SEO aussi !

RÈGLES FONDAMENTALES :
"""

// 1. SÉMANTIQUE HTML CORRECTE
// [X] Mauvais : div et span partout
<div className="nav">
  <span onClick={handleClick}>Accueil</span>
</div>

// [OK] Bon : Éléments sémantiques
<nav>
  <a href="/">Accueil</a>
  <button onClick={handleClick}>Menu</button>
</nav>

// 2. ATTRIBUTS ARIA
function Modal({ isOpen, title, children, onClose }) {
  return (
    <div
      role="dialog"          // <- Rôle ARIA
      aria-modal="true"      // <- C'est une modale
      aria-labelledby="modal-title"  // <- Titre de la modale
      aria-describedby="modal-desc"  // <- Description
    >
      <h2 id="modal-title">{title}</h2>
      <p id="modal-desc">{children}</p>
      <button
        onClick={onClose}
        aria-label="Fermer la modale"  // <- Label pour les lecteurs d'écran
      >
        [X]
      </button>
    </div>
  );
}

// 3. IMAGES AVEC ALT
// [X] Mauvais
<img src="logo.png" />
<img src="decorative.png" alt="image" />  // Alt inutile

// [OK] Bon
<img src="logo.png" alt="Logo de React Shop" />       // Image informative
<img src="decorative.png" alt="" />                   // Image décorative : alt vide

// 4. LABELS POUR FORMULAIRES
// [X] Mauvais
<p>Email</p>
<input type="email" />

// [OK] Bon : Associer label et input
<label htmlFor="email">Email</label>
<input id="email" type="email" />

// OU avec aria-label si pas de label visible
<input
  type="search"
  aria-label="Rechercher des produits"
  placeholder="Rechercher..."
/>

// 5. FOCUS VISIBLE
// [X] Mauvais : Supprimer l'outline
button:focus { outline: none; }  // Problème pour navigation clavier !

// [OK] Bon : Garder un focus visible
button:focus-visible {
  outline: 3px solid #3b82f6;
  outline-offset: 2px;
}

// 6. NAVIGATION CLAVIER
function Dropdown({ items }) {
  const [isOpen, setIsOpen] = useState(false);
  const [focusIndex, setFocusIndex] = useState(-1);

  const handleKeyDown = (e) => {
    if (e.key === 'Escape') setIsOpen(false);
    if (e.key === 'ArrowDown') setFocusIndex(i => Math.min(i + 1, items.length - 1));
    if (e.key === 'ArrowUp') setFocusIndex(i => Math.max(i - 1, 0));
    if (e.key === 'Enter' && focusIndex >= 0) handleSelect(items[focusIndex]);
  };

  return (
    <div onKeyDown={handleKeyDown}>
      <button
        onClick={() => setIsOpen(!isOpen)}
        aria-expanded={isOpen}     // <- État ouvert/fermé
        aria-haspopup="listbox"    // <- Indique qu'il ouvre une liste
      >
        Choisir [BLACK_DOWN-POINTING_TRIANGLE]
      </button>
      {isOpen && (
        <ul role="listbox" tabIndex={-1}>
          {items.map((item, i) => (
            <li
              key={item.id}
              role="option"
              aria-selected={i === focusIndex}
              tabIndex={0}
              onClick={() => handleSelect(item)}
            >
              {item.name}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

"""
OUTILS DE VÉRIFICATION :
- Extension "axe DevTools" (navigateur) -> Scan automatique
- Lighthouse (Chrome DevTools -> Lighthouse -> Accessibility)
- eslint-plugin-jsx-a11y -> Erreurs dans le code
  $ npm install -D eslint-plugin-jsx-a11y
"""

# ----------------------------------------------------------------------------
# - TYPESCRIPT AVEC REACT (INTRODUCTION)
# ----------------------------------------------------------------------------

"""
POURQUOI TYPESCRIPT ?
[OK] Détection d'erreurs à la compilation (avant l'exécution)
[OK] Auto-complétion intelligente dans l'IDE
[OK] Documentation automatique du code
[OK] Refactoring plus sûr

CRÉER UN PROJET REACT + TYPESCRIPT :
$ npm create vite@latest mon-app -- --template react-ts

MIGRER UN PROJET EXISTANT :
$ npm install -D typescript @types/react @types/react-dom
-> Renommer .jsx en .tsx
"""

// COMPOSANTS TYPÉS
interface ButtonProps {
  label: string;
  variant?: 'primary' | 'secondary' | 'danger';  // Valeurs possibles
  size?: 'small' | 'medium' | 'large';
  disabled?: boolean;
  onClick?: () => void;
  children: React.ReactNode;  // Tout ce que React peut rendre
}

// TypeScript vérifie automatiquement les props !
function Button({ label, variant = 'primary', size = 'medium', disabled = false, onClick, children }: ButtonProps) {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      disabled={disabled}
      onClick={onClick}
    >
      {children}
    </button>
  );
}

// INTERFACES POUR LES DONNÉES
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'moderator';
  createdAt: Date;
  address?: {           // Optionnel
    street: string;
    city: string;
    country: string;
  };
}

// Composant typé avec interface
function UserCard({ user }: { user: User }) {
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
      {user.address && <p>{user.address.city}</p>}
    </div>
  );
}

// HOOKS TYPÉS
function useToggle(initial: boolean = false): [boolean, () => void] {
  const [value, setValue] = useState<boolean>(initial);
  const toggle = useCallback(() => setValue(v => !v), []);
  return [value, toggle];
}

// useState avec type explicite
const [users, setUsers] = useState<User[]>([]);
const [selectedId, setSelectedId] = useState<number | null>(null);
const [error, setError] = useState<string | null>(null);

// EVENTS TYPÉS
function SearchInput() {
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
  };

  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    console.log('Position :', e.clientX, e.clientY);
  };

  return (
    <form onSubmit={handleSubmit}>
      <input onChange={handleChange} />
      <button onClick={handleClick}>Chercher</button>
    </form>
  );
}

// GENERIC HOOKS
function useFetch<T>(url: string): { data: T | null; loading: boolean; error: string | null } {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    fetch(url)
      .then(r => r.json())
      .then((d: T) => setData(d))
      .catch((e: Error) => setError(e.message))
      .finally(() => setLoading(false));
  }, [url]);

  return { data, loading, error };
}

// Utilisation typée
const { data: users } = useFetch<User[]>('/api/users');
const { data: product } = useFetch<Product>(`/api/products/${id}`);

# ----------------------------------------------------------------------------
# [SECURISE] VARIABLES D'ENVIRONNEMENT
# ----------------------------------------------------------------------------

"""
POURQUOI ?
-> Ne pas mettre les clés API dans le code source (GitHub public !)
-> Avoir des configurations différentes (dev/prod)

AVEC VITE, les variables doivent commencer par VITE_ :
"""

// .env (local, JAMAIS commité)
VITE_API_URL=http://localhost:3000
VITE_STRIPE_KEY=pk_test_xxxxx
VITE_GOOGLE_MAPS_KEY=AIza_xxxxx

// .env.production (commité, sans valeurs sensibles)
VITE_API_URL=https://api.monapp.com
// VITE_STRIPE_KEY et VITE_GOOGLE_MAPS_KEY -> À définir sur le serveur

// .env.example (commité, template pour l'équipe)
VITE_API_URL=
VITE_STRIPE_KEY=
VITE_GOOGLE_MAPS_KEY=

"""
.gitignore - Ne JAMAIS commiter les .env avec données réelles :
"""

.env
.env.local
.env.*.local

"""
ACCÈS DANS LE CODE :
"""

// [OK] Accès via import.meta.env
const API_URL = import.meta.env.VITE_API_URL;
const STRIPE_KEY = import.meta.env.VITE_STRIPE_KEY;

// Valeur par défaut si non défini
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000';

// Vérification au démarrage (src/config.js)
const config = {
  apiUrl: import.meta.env.VITE_API_URL,
  stripeKey: import.meta.env.VITE_STRIPE_KEY,
  isDev: import.meta.env.DEV,         // Boolean : vrai en dev
  isProd: import.meta.env.PROD,       // Boolean : vrai en prod
  mode: import.meta.env.MODE,         // 'development' | 'production'
};

// Valider les variables critiques
if (!config.apiUrl) {
  throw new Error('VITE_API_URL est requis ! Vérifiez votre fichier .env');
}

export default config;

// Utilisation dans les services
import config from './config';
const api = axios.create({ baseURL: config.apiUrl });

# ============================================================================
# [GUIDE] CHAPITRE 17 : DÉPLOIEMENT PRODUCTION
# ============================================================================

"""
[OBJECTIF] OBJECTIFS D'APPRENTISSAGE

À la fin de ce chapitre, vous saurez :
[OK] Préparer une app React pour la production
[OK] Déployer sur Vercel (le plus simple)
[OK] Déployer sur Netlify
[OK] Déployer sur GitHub Pages
[OK] CI/CD avec GitHub Actions
[OK] Optimisations de production
[OK] Monitoring et analytics
"""

# ----------------------------------------------------------------------------
# [CONSTRUCTION] BUILD DE PRODUCTION
# ----------------------------------------------------------------------------

"""
DIFFÉRENCE DEV vs PRODUCTION :

DÉVELOPPEMENT :
- Code source lisible
- Source maps (déboggage)
- Hot reload
- Warnings React actifs
- Pas d'optimisation -> Démarrage rapide

PRODUCTION :
- Code minifié et compressé (bundle.min.js)
- Code tree-shaken (code mort supprimé)
- Images optimisées
- Cache agressif
- Warnings supprimés -> App plus rapide

COMMANDES :
"""

# Build de production
$ npm run build

# Prévisualiser le build localement
$ npm run preview

"""
RÉSULTAT DU BUILD (dossier dist/) :
"""

dist/
├── index.html                    # HTML de base (petit)
├── assets/
│   ├── index-Cd5klcQ5.js        # Bundle JS principal (minifié)
│   ├── vendor-DXqAdFzN.js       # Bibliothèques tierces (React, etc.)
│   ├── Dashboard-BsNkG7aM.js   # Chunk lazy (Dashboard)
│   ├── index-DEpxQ3RK.css      # CSS minifié
│   └── logo-BYGrfFxF.svg       # Assets
└── favicon.ico

"""
ANALYSER LA TAILLE DU BUNDLE :
"""

# Installer l'analyseur
$ npm install -D rollup-plugin-visualizer

# vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    react(),
    visualizer({
      open: true,        // Ouvrir automatiquement dans le navigateur
      gzipSize: true,    // Afficher tailles gzippées
      brotliSize: true,  // Afficher tailles brotli
    }),
  ],
});

# Puis : npm run build
# -> Ouvre stats.html avec un graphique de la taille de chaque package

"""
OPTIMISATIONS DU BUNDLE :
"""

// vite.config.js - Configuration avancée
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // Diviser les vendors en chunks séparés
        manualChunks: {
          'react-vendor': ['react', 'react-dom'],
          'router': ['react-router-dom'],
          'query': ['@tanstack/react-query'],
          'forms': ['react-hook-form', 'zod'],
        },
      },
    },
    // Seuil d'avertissement pour les chunks (en KB)
    chunkSizeWarningLimit: 1000,
    // Minifier
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,  // Supprimer tous les console.log en prod !
        drop_debugger: true,
      },
    },
  },
});

# ----------------------------------------------------------------------------
# [RAPIDE] DÉPLOIEMENT SUR VERCEL
# ----------------------------------------------------------------------------

"""
VERCEL = Plateforme de déploiement conçue pour les apps JavaScript
-> Déploiement gratuit pour projets personnels
-> HTTPS automatique
-> CDN mondial
-> Previews pour chaque PR (Pull Request)
-> Re-déploiement automatique à chaque git push

MÉTHODE 1 : Interface web (La plus simple)
"""

# 1. Pousser le code sur GitHub
$ git init
$ git add .
$ git commit -m "Initial commit"
$ git remote add origin https://github.com/votre-user/votre-repo.git
$ git push -u origin main

# 2. Aller sur vercel.com
# 3. "Import Project" -> Sélectionner votre repo GitHub
# 4. Configuration automatique détectée (Vite + React)
# 5. Cliquer "Deploy"
# 6. En 2 minutes -> URL publique !

"""
MÉTHODE 2 : CLI Vercel
"""

# Installer Vercel CLI
$ npm install -g vercel

# Se connecter
$ vercel login

# Déployer depuis le dossier du projet
$ vercel

# Déployer en production
$ vercel --prod

"""
CONFIGURATION vercel.json (si nécessaire) :
"""

// vercel.json - Pour SPA avec React Router
{
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
}

// [ATTENTION] IMPORTANT pour React Router !
// Sans ça, /products/123 -> 404 si l'utilisateur rafraîchit la page
// Le serveur cherche un fichier /products/123.html qui n'existe pas
// Cette règle redirige tout vers index.html -> React Router gère la route

"""
VARIABLES D'ENVIRONNEMENT SUR VERCEL :
-> Dashboard Vercel -> Project -> Settings -> Environment Variables
-> Ajouter chaque VITE_* variable
"""

# ----------------------------------------------------------------------------
# [WEB] DÉPLOIEMENT SUR NETLIFY
# ----------------------------------------------------------------------------

"""
NETLIFY = Alternative à Vercel, similaire en fonctionnalités
-> Formulaires gratuits intégrés
-> Identity (authentification basique)
-> Fonctions serverless

MÉTHODE 1 : Drag & Drop (Ultra-simple)
"""

# 1. Builder le projet
$ npm run build

# 2. Aller sur netlify.com -> "Sites" -> "Add new site"
# 3. Faire glisser le dossier dist/ dans la zone
# 4. URL publique immédiate !

"""
MÉTHODE 2 : Connecter GitHub (Recommandé)
"""

# 1. netlify.com -> "Add new site" -> "Import from Git"
# 2. Sélectionner votre repo
# 3. Configuration :
#    Build command  : npm run build
#    Publish directory : dist
# 4. "Deploy site"

"""
CONFIGURATION netlify.toml :
"""

# netlify.toml (à la racine du projet)
[build]
  command = "npm run build"
  publish = "dist"

[build.environment]
  NODE_VERSION = "20"

# Redirections pour React Router
[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

# En-têtes de cache pour les assets
[[headers]]
  for = "/assets/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

# ----------------------------------------------------------------------------
# [PACKAGE] DÉPLOIEMENT SUR GITHUB PAGES
# ----------------------------------------------------------------------------

"""
GITHUB PAGES = Hébergement gratuit directement depuis GitHub
-> Parfait pour les projets open source
-> Gratuit, mais limité (pas de serverless, pas de backend)
-> URL : votre-user.github.io/votre-repo
"""

# 1. Installer gh-pages
$ npm install -D gh-pages

# 2. Modifier package.json
{
  "homepage": "https://votre-user.github.io/votre-repo",
  "scripts": {
    "predeploy": "npm run build",
    "deploy": "gh-pages -d dist"
  }
}

# 3. Modifier vite.config.js pour le sous-dossier
export default defineConfig({
  base: '/votre-repo/',  // <- Nom de votre repo !
  // ...
});

# 4. Déployer
$ npm run deploy
# -> Crée une branche gh-pages et y pousse le build

# 5. Sur GitHub : Settings -> Pages -> Source : Branch "gh-pages"

"""
AVEC GitHub Actions (Automatique) :
"""

# .github/workflows/deploy.yml
name: Deploy to GitHub Pages

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build
        env:
          VITE_API_URL: ${{ secrets.VITE_API_URL }}  # Variables secrètes

      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./dist

# ----------------------------------------------------------------------------
# [CONFIG] CI/CD COMPLET AVEC GITHUB ACTIONS
# ----------------------------------------------------------------------------

"""
CI/CD = Continuous Integration / Continuous Deployment

CI : À chaque PR/push -> Exécuter tests, lint, build
CD : À chaque merge sur main -> Déployer automatiquement

PIPELINE COMPLET :
"""

# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  # ========== JOB 1 : Tests et qualité ==========
  test:
    name: [TEST] Tests & Qualité
    runs-on: ubuntu-latest

    steps:
      - name: [ENTREE] Checkout code
        uses: actions/checkout@v4

      - name: [PACKAGE] Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'  # Cache des node_modules

      - name: [DOCS] Installer les dépendances
        run: npm ci

      - name: [RECHERCHE] Lint (ESLint)
        run: npm run lint

      - name: [TEST] Exécuter les tests
        run: npm run test:run

      - name: [GRAPHIQUE] Coverage
        run: npm run test:coverage

      - name: [CLOUD] Upload Coverage
        uses: codecov/codecov-action@v3  # Envoyer à codecov.io

  # ========== JOB 2 : Build ==========
  build:
    name: [CONSTRUCTION] Build
    needs: test  # Lance seulement si test réussit
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci

      - name: [CONSTRUCTION] Build de production
        run: npm run build
        env:
          VITE_API_URL: ${{ secrets.VITE_API_URL }}
          VITE_STRIPE_KEY: ${{ secrets.VITE_STRIPE_KEY }}

      - name: [SORTIE] Upload artifact
        uses: actions/upload-artifact@v3
        with:
          name: dist
          path: dist/

  # ========== JOB 3 : Déploiement (seulement sur main) ==========
  deploy:
    name: [RAPIDE] Déploiement
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'  # Seulement sur la branche main

    steps:
      - name: [ENTREE] Télécharger le build
        uses: actions/download-artifact@v3
        with:
          name: dist
          path: dist/

      - name: [RAPIDE] Déployer sur Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'

      - name: [SPEECH_BALLOON] Commenter la PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v6
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '[OK] Déploiement réussi ! Voir : https://votre-app.vercel.app'
            })

# ----------------------------------------------------------------------------
# [HAUSSE] MONITORING ET ANALYTICS
# ----------------------------------------------------------------------------

"""
EN PRODUCTION, vous voulez savoir :
- Combien d'utilisateurs ? D'où viennent-ils ?
- Quelles pages visitent-ils ?
- Y a-t-il des erreurs JavaScript ?
- Les performances sont-elles bonnes ?
"""

"""
1. SENTRY (Monitoring d'erreurs) - RECOMMANDÉ
   -> Capture les erreurs en production avec contexte
   $ npm install @sentry/react
"""

// src/main.jsx
import * as Sentry from '@sentry/react';

Sentry.init({
  dsn: import.meta.env.VITE_SENTRY_DSN,
  environment: import.meta.env.MODE,   // 'development' ou 'production'
  tracesSampleRate: 0.1,               // 10% des transactions (pour les perfs)

  // Intégrations
  integrations: [
    Sentry.browserTracingIntegration(), // Suivre les performances
    Sentry.replayIntegration({          // Enregistrer les sessions (erreurs)
      maskAllText: true,               // Masquer les données sensibles
      blockAllMedia: false,
    }),
  ],
});

// Composant d'erreur Sentry
const SentryErrorBoundary = Sentry.withErrorBoundary(App, {
  fallback: <ErrorFallback />,
});

// Capturer manuellement
try {
  await riskyOperation();
} catch (error) {
  Sentry.captureException(error, {
    tags: { section: 'checkout' },
    user: { id: user.id, email: user.email },
  });
}

// Ajouter du contexte
Sentry.setUser({ id: user.id, username: user.name });
Sentry.setTag('subscription', 'premium');
Sentry.addBreadcrumb({ message: 'User clicked checkout', level: 'info' });

"""
2. WEB VITALS (Métriques de performance)
   -> Google mesure ça pour le référencement !
"""

// src/reportWebVitals.js
import { getCLS, getFID, getLCP, getFCP, getTTFB } from 'web-vitals';

function reportWebVitals(onPerfEntry) {
  if (onPerfEntry && onPerfEntry instanceof Function) {
    getCLS(onPerfEntry);   // Cumulative Layout Shift
    getFID(onPerfEntry);   // First Input Delay
    getLCP(onPerfEntry);   // Largest Contentful Paint
    getFCP(onPerfEntry);   // First Contentful Paint
    getTTFB(onPerfEntry);  // Time to First Byte
  }
}

// Envoyer à un service d'analytics
reportWebVitals((metric) => {
  console.log(metric);

  // Envoyer à Google Analytics
  if (window.gtag) {
    window.gtag('event', metric.name, {
      value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
      metric_id: metric.id,
      metric_delta: metric.delta,
    });
  }

  // Envoyer à votre propre backend
  fetch('/api/metrics', {
    method: 'POST',
    body: JSON.stringify(metric),
    headers: { 'Content-Type': 'application/json' },
  });
});

"""
3. GOOGLE ANALYTICS 4
"""

// index.html - Ajouter dans <head>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'G-XXXXXXXXXX');
</script>

// OU avec react-ga4
$ npm install react-ga4

import ReactGA from 'react-ga4';

ReactGA.initialize('G-XXXXXXXXXX');

// Suivre les changements de page (React Router)
function App() {
  const location = useLocation();

  useEffect(() => {
    ReactGA.send({ hitType: 'pageview', page: location.pathname });
  }, [location]);

  // ...
}

// Événements personnalisés
ReactGA.event({
  category: 'User',
  action: 'Added to Cart',
  label: product.name,
  value: product.price,
});

# ----------------------------------------------------------------------------
# [COURS] EXERCICE PRATIQUE FINAL : DÉPLOYER VOTRE PORTFOLIO
# ----------------------------------------------------------------------------

"""
OBJECTIF : Créer et déployer un portfolio React professionnel

CONTENU :
1. Page d'accueil avec votre présentation
2. Section Skills (liste de vos compétences)
3. Section Projects (vos projets React réalisés)
4. Formulaire de contact (validation + envoi simulé)
5. Thème clair/sombre (localStorage)
6. Animations CSS
7. Responsive (mobile-first)
8. Déploiement sur Vercel avec domaine personnalisé

ÉTAPES :
1. Créer le projet : npm create vite@latest portfolio -- --template react
2. Développer les composants
3. Tester avec npm run build && npm run preview
4. git push sur GitHub
5. Importer dans Vercel
6. Configurer les variables d'environnement si besoin
7. Partager votre URL !

STRUCTURE SUGGÉRÉE :
"""

// src/App.jsx
import { lazy, Suspense } from 'react';
import { useLocalStorage } from './hooks/useLocalStorage';
import Navbar from './components/layout/Navbar';
import Hero from './components/sections/Hero';
import LoadingSpinner from './components/ui/LoadingSpinner';

const Skills = lazy(() => import('./components/sections/Skills'));
const Projects = lazy(() => import('./components/sections/Projects'));
const Contact = lazy(() => import('./components/sections/Contact'));
const Footer = lazy(() => import('./components/layout/Footer'));

function App() {
  const [isDark, setIsDark] = useLocalStorage('portfolio-theme', false);

  return (
    <div className={isDark ? 'dark' : 'light'}>
      <Navbar isDark={isDark} onToggleTheme={() => setIsDark(!isDark)} />
      <main>
        <Hero />
        <Suspense fallback={<LoadingSpinner />}>
          <Skills />
          <Projects />
          <Contact />
        </Suspense>
      </main>
      <Suspense fallback={null}>
        <Footer />
      </Suspense>
    </div>
  );
}

// src/components/sections/Projects.jsx
const PROJECTS = [
  {
    id: 1,
    title: 'TodoApp',
    description: 'Application de gestion de tâches avec filtres et localStorage',
    tech: ['React', 'useState', 'CSS Modules'],
    github: 'https://github.com/...',
    demo: 'https://...',
    image: '[NOTE]',
  },
  {
    id: 2,
    title: 'Dashboard Météo',
    description: 'Application météo avec appels API, debounce et animations',
    tech: ['React', 'useEffect', 'API REST'],
    github: 'https://github.com/...',
    demo: 'https://...',
    image: '[WHITE_SUN_WITH_SMALL_CLOUD]',
  },
  {
    id: 3,
    title: 'E-commerce',
    description: 'Shop complet avec React Router, Context et panier',
    tech: ['React Router', 'Context API', 'TanStack Query'],
    github: 'https://github.com/...',
    demo: 'https://...',
    image: '[SHOPPING_TROLLEY]',
  },
];

function Projects() {
  return (
    <section id="projects" style={{ padding: '80px 24px', maxWidth: '1100px', margin: '0 auto' }}>
      <h2 style={{ textAlign: 'center', fontSize: '36px', marginBottom: '48px' }}>
        [RAPIDE] Mes Projets
      </h2>

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: '24px' }}>
        {PROJECTS.map(project => (
          <article
            key={project.id}
            style={{
              background: 'white', borderRadius: '16px', overflow: 'hidden',
              boxShadow: '0 4px 20px rgba(0,0,0,0.08)',
              transition: 'transform 0.2s, box-shadow 0.2s',
            }}
            onMouseEnter={e => {
              e.currentTarget.style.transform = 'translateY(-4px)';
              e.currentTarget.style.boxShadow = '0 8px 30px rgba(0,0,0,0.15)';
            }}
            onMouseLeave={e => {
              e.currentTarget.style.transform = 'translateY(0)';
              e.currentTarget.style.boxShadow = '0 4px 20px rgba(0,0,0,0.08)';
            }}
          >
            <div style={{ background: 'linear-gradient(135deg, #667eea, #764ba2)', padding: '40px', textAlign: 'center', fontSize: '64px' }}>
              {project.image}
            </div>
            <div style={{ padding: '24px' }}>
              <h3 style={{ margin: '0 0 8px', fontSize: '20px' }}>{project.title}</h3>
              <p style={{ color: '#64748b', fontSize: '14px', marginBottom: '16px', lineHeight: 1.6 }}>
                {project.description}
              </p>
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', marginBottom: '20px' }}>
                {project.tech.map(t => (
                  <span key={t} style={{ background: '#eff6ff', color: '#3b82f6', padding: '4px 10px', borderRadius: '20px', fontSize: '12px', fontWeight: '500' }}>
                    {t}
                  </span>
                ))}
              </div>
              <div style={{ display: 'flex', gap: '10px' }}>
                <a href={project.github} target="_blank" rel="noopener noreferrer"
                  style={{ flex: 1, textAlign: 'center', padding: '10px', background: '#1e293b', color: 'white', borderRadius: '8px', textDecoration: 'none', fontSize: '14px' }}>
                  [PACKAGE] GitHub
                </a>
                <a href={project.demo} target="_blank" rel="noopener noreferrer"
                  style={{ flex: 1, textAlign: 'center', padding: '10px', background: '#3b82f6', color: 'white', borderRadius: '8px', textDecoration: 'none', fontSize: '14px' }}>
                  [RAPIDE] Demo
                </a>
              </div>
            </div>
          </article>
        ))}
      </div>
    </section>
  );
}

export default Projects;

"""
[OK] CE QUE VOUS AVEZ APPRIS DANS CE CHAPITRE :
- Build de production avec Vite
- Analyser et optimiser la taille du bundle
- Déployer sur Vercel (recommandé), Netlify, GitHub Pages
- CI/CD avec GitHub Actions
- Monitoring avec Sentry et Web Vitals
- Variables d'environnement sécurisées
"""

# ============================================================================
# [DOCS] RÉCAPITULATIF PARTIE 4 - LA TOTALITÉ DU GUIDE
# ============================================================================

"""
[BRAVO][BRAVO][BRAVO] FÉLICITATIONS ! VOUS AVEZ TERMINÉ LE GUIDE COMPLET REACT ! [BRAVO][BRAVO][BRAVO]

══════════════════════════════════════════════════════════════════════════════
                    TOUT CE QUE VOUS MAÎTRISEZ MAINTENANT
══════════════════════════════════════════════════════════════════════════════

PARTIE 1 - FONDAMENTAUX
  [OK] React = bibliothèque pour UIs par composants (Virtual DOM)
  [OK] Composants fonctionnels, JSX, export/import
  [OK] Props : passer, recevoir, déstructurer, valeurs par défaut
  [OK] Prop children, spread operator, PropTypes
  [OK] State avec useState
  [OK] Événements : onClick, onChange, onSubmit
  [OK] Mettre à jour tableaux et objets dans le state

PARTIE 2 - HOOKS ESSENTIELS
  [OK] useState avancé : lazy init, état dérivé, plusieurs états
  [OK] useEffect : fetch API, timers, événements DOM, localStorage
  [OK] Tableau de dépendances et cleanup
  [OK] useContext : créer/fournir/consommer un Context
  [OK] Contextes thème, auth, langue
  [OK] useRef : accès DOM et valeurs persistantes
  [OK] useMemo : mémoriser résultats coûteux
  [OK] useCallback : mémoriser fonctions

PARTIE 3 - FONCTIONNALITÉS AVANCÉES
  [OK] React Router v6 : routes, navigation, paramètres URL
  [OK] Routes imbriquées, layouts, routes protégées
  [OK] Query strings, navigation programmatique
  [OK] Formulaires contrôlés et validation
  [OK] React Hook Form + Zod
  [OK] Appels API : fetch, axios avec intercepteurs
  [OK] TanStack Query : cache, mutations, optimisme
  [OK] Custom hooks : useFetch, useDebounce, usePagination...
  [OK] useReducer et pattern Redux-like

PARTIE 4 - PRODUCTION ET OPTIMISATION
  [OK] React.memo : éviter les re-rendus inutiles
  [OK] useMemo et useCallback avancés
  [OK] React Profiler : diagnostiquer les performances
  [OK] Lazy loading et Code Splitting avec Suspense
  [OK] Virtualisation de longues listes (react-window)
  [OK] Tests avec Vitest + React Testing Library
  [OK] Tester composants, interactions, API, hooks
  [OK] Architecture feature-based à grande échelle
  [OK] Error Boundaries
  [OK] Accessibilité (a11y)
  [OK] Introduction à TypeScript
  [OK] Variables d'environnement
  [OK] Déploiement Vercel/Netlify/GitHub Pages
  [OK] CI/CD avec GitHub Actions
  [OK] Monitoring avec Sentry et Web Vitals

══════════════════════════════════════════════════════════════════════════════
                         PROCHAINES ÉTAPES RECOMMANDÉES
══════════════════════════════════════════════════════════════════════════════

NIVEAU 1 - CONSOLIDER (1-2 mois)
  -> Construire 3-5 projets personnels de A à Z
  -> Reproduire des UIs existantes (clone Airbnb, clone Twitter)
  -> Contribuer à des projets open source React
  -> Coder sans regarder le guide !

NIVEAU 2 - APPROFONDIR (2-3 mois)
  -> TypeScript complet avec React
  -> Next.js (SSR, SSG, App Router)
  -> Zustand ou Redux Toolkit (gestion d'état avancée)
  -> Storybook (design system)
  -> Tests E2E avec Playwright

NIVEAU 3 - SPÉCIALISER (3-6 mois)
  -> React Native (apps mobiles)
  -> Three.js / R3F (3D dans React)
  -> Animations avec Framer Motion
  -> Microfrontends
  -> Architecture hexagonale

══════════════════════════════════════════════════════════════════════════════
                              RESSOURCES UTILES
══════════════════════════════════════════════════════════════════════════════

Documentation officielle :
  -> https://react.dev (nouvelle doc, avec exemples interactifs)
  -> https://tanstack.com (React Query, Router, Virtual)
  -> https://reactrouter.com

Communauté :
  -> r/reactjs (Reddit)
  -> Reactiflux (Discord)
  -> Twitter/X : @reactjs, @dan_abramov, @kentcdodds

Pratique :
  -> Frontend Mentor (challenges UI)
  -> The Odin Project (curriculum complet)
  -> Exercism.io (exercices JavaScript/TypeScript)

Newsletters :
  -> This Week In React
  -> React Status
  -> bytes.dev

══════════════════════════════════════════════════════════════════════════════

[IDEE] CONSEIL FINAL :

"La meilleure façon d'apprendre React est de CONSTRUIRE des choses.
Pas de regarder des tutoriels. Pas de lire des guides (comme celui-ci !).
CONSTRUIRE. Faire des erreurs. Débogger. Reconstruire.

Prenez chaque concept de ce guide et créez quelque chose avec.
L'erreur la plus courante des débutants est de consommer trop de contenu
et de trop peu pratiquer.

Maintenant, fermez ce guide et allez coder ! [RAPIDE]"

══════════════════════════════════════════════════════════════════════════════
"""

# ============================================================================
# [LISTE] ANNEXE : CHEAT SHEET RAPIDE
# ============================================================================

"""
╔════════════════════════════════════════════════════════════════════════════╗
║                     [SCIENCE]  REACT CHEAT SHEET                                  ║
╚════════════════════════════════════════════════════════════════════════════╝

CRÉER UN PROJET :
  npm create vite@latest mon-app -- --template react
  npm create vite@latest mon-app -- --template react-ts (TypeScript)

HOOKS ESSENTIELS :
  const [val, setVal] = useState(init)        -> État local
  useEffect(() => { ... return cleanup }, [])  -> Effets de bord
  const val = useContext(MonContext)           -> Contexte
  const ref = useRef(null)                    -> Réf DOM / valeur
  const memo = useMemo(() => calc(), [deps])  -> Calcul mémoïsé
  const fn = useCallback(() => {}, [deps])    -> Fonction mémoïsée
  const [state, dispatch] = useReducer(fn, init) -> État complexe

RENDU CONDITIONNEL :
  {condition && <Composant />}                 -> Afficher ou rien
  {condition ? <A /> : <B />}                 -> A ou B
  {!data && <Loading />}                      -> Avant les données

LISTES :
  {items.map(item => <Item key={item.id} {...item} />)}

PATTERNS COURANTS :
  // Mettre à jour un tableau
  setItems(prev => [...prev, newItem])         -> Ajouter
  setItems(prev => prev.filter(i => i.id !== id)) -> Supprimer
  setItems(prev => prev.map(i => i.id === id ? {...i, ...update} : i)) -> Modifier

  // Mettre à jour un objet
  setUser(prev => ({ ...prev, name: 'Alice' }))

STYLE :
  <div className="ma-classe">                  -> CSS classique
  <div style={{ color: 'red', fontSize: 16 }}>  -> Style inline
  import styles from './Comp.module.css';
  <div className={styles.maClasse}>             -> CSS Modules

MEMO :
  const Comp = memo(function Comp({ prop }) { ... })
  -> Ne re-rend pas si props identiques

LAZY LOADING :
  const Page = lazy(() => import('./Page'));
  <Suspense fallback={<Loading />}><Page /></Suspense>

CONTEXT :
  const Ctx = createContext(null);
  <Ctx.Provider value={data}><App /></Ctx.Provider>
  const data = useContext(Ctx);

CUSTOM HOOK :
  function useMyHook(param) {
    const [state, setState] = useState(null);
    useEffect(() => { /* logique */ }, [param]);
    return { state, setState };
  }

REACT ROUTER :
  <BrowserRouter><Routes><Route path="/" element={<Home />} /></Routes></BrowserRouter>
  <Link to="/page">Lien</Link>
  const { id } = useParams();
  const navigate = useNavigate(); navigate('/page');
  const [params, setParams] = useSearchParams();

TESTS :
  render(<Comp prop="val" />)                  -> Monter
  screen.getByRole('button', { name: 'Clic' }) -> Trouver
  await userEvent.setup().click(element)        -> Interagir
  expect(element).toBeInTheDocument()           -> Vérifier
  vi.fn()                                       -> Mock
  await waitFor(() => expect(...))              -> Async

TYPESCRIPT COURANT :
  interface Props { name: string; age?: number; onClick: () => void; }
  function Comp({ name, age = 0 }: Props) { ... }
  const [val, setVal] = useState<string | null>(null);
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { ... }

PERFORMANCES :
  React.memo(Comp)       -> Éviter re-rendus
  useMemo(fn, deps)      -> Mémoriser calcul
  useCallback(fn, deps)  -> Mémoriser fonction
  lazy() + Suspense      -> Code splitting
  FixedSizeList          -> Virtualisation (react-window)

BUILD ET DÉPLOIEMENT :
  npm run build          -> Créer dist/
  npm run preview        -> Tester le build
  vercel                 -> Déployer sur Vercel
  netlify deploy --prod  -> Déployer sur Netlify
"""