Files
stagiaire/ENONCE_final_boss.md
T

11 KiB

🏁 Énoncé FINAL — « Boss de fin »

Dernier jour. Tu as tout l'arsenal : composants, props, state, lifting state up, useEffect, formulaire, tri, filtre, React Router. Aujourd'hui, place à la pratique libre.

Un seul module est obligatoire : le Module L (la page détail, le vrai boss). Les autres forment un menu : pioche dans l'ordre que tu veux, fais-en le plus possible. Chacun est court et indépendant.

Module Tu fais Niveau
L Page détail complète (state remonté au routeur) — obligatoire
M Bouton « au hasard » (useNavigate)
N Le podium des trilogies les plus likées
O Bouton « supprimer » une trilogie
P Thème sombre qui se souvient du choix
Q Mode Versus : deux trilogies s'affrontent
R Statistiques (total, moyenne, championne)
S Page « 404 » pour les URL inconnues
T Tri ascendant ↔ descendant
U Custom hook useLocalStorage (refacto)
V Éditer une trilogie

🔑 Module L — Le boss : faire suivre TOUT jusqu'au détail (obligatoire)

Dans CORRECTION_page_detail.md tu as fait une version simple avec data.js. Sa limite : les trilogies ajoutées au formulaire et les likes n'arrivent pas dans la page détail. On corrige ça proprement.

L'idée : la donnée doit vivre au-dessus des deux pages, donc dans App, qui la passe ensuite en props aux deux routes.

L.1 — Remonter le state dans App.jsx

import { useState } from "react";
import { Routes, Route } from "react-router-dom";
import { donneesInitiales } from "./data";
import Accueil from "./pages/Accueil";
import Details from "./pages/Details";

function App() {
  const [trilogies, setTrilogies] = useState(donneesInitiales);
  const [likes, setLikes] = useState({});

  return (
    <Routes>
      <Route path="/" element={
        <Accueil trilogies={trilogies} setTrilogies={setTrilogies}
                 likes={likes} setLikes={setLikes} />
      } />
      <Route path="/trilogie/:id" element={
        <Details trilogies={trilogies} likes={likes} />
      } />
    </Routes>
  );
}

Le déclic : une route reçoit des props comme n'importe quel composant — element={<Details trilogies={trilogies} />}. C'est ça qui « branche » la donnée sur les deux pages.

L.2 — Accueil lit/écrit via les props

Enlève les useState de trilogies et likes dans Accueil : ils viennent maintenant des props. Garde en local ce qui est propre à l'accueil (recherche, tri, champs du formulaire).

function Accueil({ trilogies, setTrilogies, likes, setLikes }) {
  // ajouter() fait toujours setTrilogies([...trilogies, nouvelle])
  // liker() fait toujours setLikes(prev => ({...prev, [id]: (prev[id]||0)+1}))
}

L.3 — Details affiche la vraie trilogie avec ses likes

function Details({ trilogies, likes }) {
  const { id } = useParams();
  const trilogie = trilogies.find((t) => t.id === Number(id));
  if (!trilogie) return <p>Introuvable  <Link to="/">retour</Link></p>;

  return (
    <div className="detail">
      <Link to="/"> Retour</Link>
      <h1>{trilogie.nom}</h1>
      <p>Niveaux : {trilogie.niveaux}</p>
      <p>❤️ Likes : {likes[trilogie.id] || 0}</p>
    </div>
  );
}

Test du boss : ajoute une trilogie au formulaire → clique « détails » dessus → elle s'affiche (alors qu'avec data.js elle était introuvable). Like-la 3 fois sur l'accueil → son détail affiche « ❤️ Likes : 3 ».

Ce que tu viens de faire s'appelle « lifting state up » poussé jusqu'au routeur : l'état monte au plus petit ancêtre commun des composants qui en ont besoin. C'est le réflexe d'archi central en React.


Module M — Bouton « au hasard »

Un bouton qui t'envoie sur la page détail d'une trilogie au hasard. On découvre useNavigate (naviguer en JS, pas via un clic sur un <Link>).

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

const navigate = useNavigate();
function hasard() {
  const i = Math.floor(Math.random() * trilogies.length);
  navigate(`/trilogie/${trilogies[i].id}`);
}
// <button onClick={hasard}>🎲 Au hasard</button>

<Link> = naviguer au clic sur un lien. useNavigate() = naviguer depuis du code (après un calcul, un envoi de formulaire…).

Test : clique plusieurs fois → tu tombes sur des trilogies différentes.


Module N — Le podium

En haut de l'accueil, le Top 3 des trilogies les plus likées, avec des médailles.

const medailles = ["🥇", "🥈", "🥉"];

const podium = [...trilogies]
  .sort((a, b) => (likes[b.id] || 0) - (likes[a.id] || 0))
  .slice(0, 3);

// <div className="podium">
//   {podium.map((t, index) => (
//     <p key={t.id}>{medailles[index]} {t.nom} — {likes[t.id] || 0} ❤️</p>
//   ))}
// </div>

.slice(0, 3) garde les indices 0, 1, 2. Le 2ᵉ paramètre de .map((t, index) => ...), c'est la position dans la liste → parfait pour la médaille.

Test : like beaucoup une trilogie → elle grimpe sur le podium en direct.


Module O — Supprimer une trilogie

Un bouton « supprimer » sur chaque carte. La clé : l'immutabilité (un nouveau tableau sans l'élément).

function supprimer(id) {
  setTrilogies(trilogies.filter((t) => t.id !== id));
}
// sur la Carte : <button onClick={() => props.onDelete()}>🗑️</button>
// et dans Accueil : onDelete={() => supprimer(trilo.id)}

filter renvoie un nouveau tableau contenant tout sauf l'élément visé. On ne touche jamais l'ancien (immutabilité, comme pour l'ajout).

Test : clique 🗑️ → la carte disparaît, les autres restent.


Module P — Thème sombre (qui se souvient)

Un bouton qui bascule la page en sombre et garde le choix après un refresh (localStorage, comme tes likes).

const [sombre, setSombre] = useState(
  () => localStorage.getItem("theme") === "sombre"
);
useEffect(() => {
  localStorage.setItem("theme", sombre ? "sombre" : "clair");
}, [sombre]);

// <div className={sombre ? "body sombre" : "body"}>
//   <button onClick={() => setSombre(!sombre)}>{sombre ? "☀️" : "🌙"}</button>
.sombre { background-color: #1a1a2e; color: #eaeaea; }
.sombre .carte { background-color: #16213e; color: #eaeaea; }

Tu fais piloter du CSS par du state : className change selon sombre, le navigateur applique les bonnes règles. C'est comme ça que marchent tous les dark modes.

Test : passe en sombre → recharge (F5) → c'est toujours sombre.


Module Q — Mode Versus

Deux <select> pour choisir deux trilogies ; on compare leurs likes et on affiche la gagnante.

const [a, setA] = useState(trilogies[0].id);
const [b, setB] = useState(trilogies[1].id);

const likesA = likes[a] || 0;
const likesB = likes[b] || 0;
const gagnante = likesA === likesB ? "Égalité" :
  (likesA > likesB ? trilogies.find(t => t.id === Number(a)).nom
                   : trilogies.find(t => t.id === Number(b)).nom);

Deux <select> (un pour a, un pour b) listant trilogies.map(t => <option value={t.id}>{t.nom}</option>), puis un <p> qui annonce la gagnante.

Pense à Number(...) quand tu relis la valeur d'un <select> : elle revient en chaîne.

Test : change les deux menus → le verdict se met à jour tout seul.


Module R — Statistiques

Une petite barre de stats calculée à partir des données. On muscle reduce.

const nbTrilogies = trilogies.length;
const totalLikes = Object.values(likes).reduce((s, n) => s + n, 0);
const moyenne = nbTrilogies ? (totalLikes / nbTrilogies).toFixed(1) : 0;
// <p>{nbTrilogies} trilogies · {totalLikes} likes · {moyenne} likes/trilogie</p>

reduce réduit un tableau à une seule valeur en accumulant. Ici on additionne tous les likes.

Test : ajoute/like des trilogies → les chiffres bougent.


Module S — Page « 404 »

Que se passe-t-il si on tape /trilogie/banane ou une URL qui n'existe pas ? Rien d'affiché. On ajoute une route attrape-tout.

import { Link } from "react-router-dom";
function NotFound() {
  return <div><h1>404  page introuvable</h1><Link to="/">Retour à l'accueil</Link></div>;
}
// dans <Routes>, en DERNIER :
// <Route path="*" element={<NotFound />} />

path="*" = « toutes les URL non reconnues ». À mettre en dernier, sinon il attrape tout avant les autres.

Test : tape une URL bidon dans la barre → la page 404 s'affiche.


Module T — Tri ascendant ↔ descendant

Ajoute un bouton qui inverse l'ordre du tri en cours.

const [ordre, setOrdre] = useState("asc");
// après ton .sort(...) :
const affichees = [...maListeTriee];
if (ordre === "desc") affichees.reverse();
// <button onClick={() => setOrdre(ordre === "asc" ? "desc" : "asc")}>
//   {ordre === "asc" ? "A→Z" : "Z→A"}
// </button>

Test : clique le bouton → l'ordre des cartes s'inverse.


Module U — Custom hook useLocalStorage (refacto)

Tu répètes le même schéma useState + useEffect(localStorage…) pour les likes et le thème. On range ça dans un hook réutilisable.

// src/useLocalStorage.js
import { useState, useEffect } from "react";

export function useLocalStorage(cle, valeurParDefaut) {
  const [valeur, setValeur] = useState(() => {
    const stockee = localStorage.getItem(cle);
    return stockee !== null ? JSON.parse(stockee) : valeurParDefaut;
  });
  useEffect(() => {
    localStorage.setItem(cle, JSON.stringify(valeur));
  }, [cle, valeur]);
  return [valeur, setValeur];
}

Puis : const [likes, setLikes] = useLocalStorage("likes", {}); — et les likes survivent au refresh, gratuitement.

Un custom hook, c'est juste une fonction qui commence par use et qui utilise d'autres hooks. Ça permet de réutiliser de la logique entre composants.

Test : like des trilogies → recharge → les likes sont toujours là.


Module V — Éditer une trilogie

Le plus dur. Un bouton « ✏️ » passe une carte en mode édition (un <input> à la place du titre), et « valider » met à jour la liste.

Piste : un état enEdition (l'id en cours d'édition, ou null), et une fonction modifier(id, nouveauNom) qui fait

setTrilogies(trilogies.map((t) => t.id === id ? { ...t, nom: nouveauNom } : t));

map ici renvoie un nouveau tableau où seule la trilogie visée est remplacée par une copie modifiée ({...t, nom}), les autres inchangées. Toujours l'immutabilité.

Test : édite un nom → valide → la carte (et sa page détail) affichent le nouveau nom.


Checklist

  • L — state remonté dans App, page détail complète (obligatoire)
  • M — bouton au hasard · [ ] N — podium · [ ] O — supprimer · [ ] P — thème sombre
  • Q — versus · [ ] R — stats · [ ] S — page 404 · [ ] T — tri inversé
  • U — custom hook · [ ] V — édition

🧠 Concepts rencontrés aujourd'hui

useNavigate · état remonté au niveau du routeur + props passées à une <Route> · filter (supprimer) et map (éditer) immuables · sort/slice/index (classement) · reduce (stats) · route attrape-tout path="*" · CSS piloté par le state · custom hook.

Bravo pour ces deux semaines. Montre-moi tes modules battus quand tu en tiens un.