Files
stagiaire/ENONCE_final_boss.md
T

8.1 KiB

🏁 Énoncé FINAL — « Boss de fin » 😈

Dernier jour ! 🎉 Tu as déjà tout l'arsenal : composants, props, state, lifting state up, useEffect, fetch, formulaire, tri, filtre, React Router, localStorage. Aujourd'hui on s'amuse : 4 modules courts et stylés. Fais-en autant que tu veux, dans l'ordre que tu veux — mais le Module L est le vrai boss, attaque-le en premier. 💪

Module Tu fais Niveau Fun
L La page détail complète (state remonté au routeur) 🔑
M Bouton 🎲 « Au hasard » (page d'un jeu aléatoire) 😄
N 🏆 Le Hall of Fame des jeux les plus likés 🥇
O 🌗 Thème sombre qui se souvient de ton choix 🌙

🔑 Module L — Le boss : centraliser les données dans App

Dans CORRECTION_page_detail.md tu as fait une version simple (la page détail re-fetch toute seule). Ses limites : les jeux ajoutés au formulaire et les likes n'arrivent pas dans le détail, parce que chacun vit dans son coin. On corrige ça proprement.

L'idée : le fetch, la liste jeux et les likes doivent vivre au-dessus des deux pages → dans App. App les passe ensuite en props aux routes.

L.1 — Remonter le fetch et les états dans App.jsx

import { useState, useEffect } from "react";
import { Routes, Route } from "react-router-dom";
import Header from "./components/Header";
import Footer from "./components/Footer";
import Accueil from "./pages/Accueil";
import Detail from "./pages/Detail";

function App() {
  const [jeux, setJeux] = useState([]);
  const [likes, setLikes] = useState(() =>
    JSON.parse(localStorage.getItem("likes") || "{}")
  );

  useEffect(() => {
    fetch("/zelda.json").then((r) => r.json()).then(setJeux);
  }, []);

  useEffect(() => {
    localStorage.setItem("likes", JSON.stringify(likes));
  }, [likes]);

  function liker(id) {
    setLikes((prev) => ({ ...prev, [id]: (prev[id] || 0) + 1 }));
  }

  return (
    <>
      <Header />
      <Routes>
        <Route path="/" element={
          <Accueil jeux={jeux} setJeux={setJeux} likes={likes} onLike={liker} />
        } />
        <Route path="/jeu/:id" element={
          <Detail jeux={jeux} likes={likes} />
        } />
      </Routes>
      <Footer />
    </>
  );
}

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

🔄 Nouveau réflexe likes : avant, chaque Carte gardait son like dans son coin (localStorage.getItem("likes-" + id)). Maintenant c'est un seul objet likes (genre { "2": 3, "4": 1 }) rangé d'un coup. C'est ce qui rend possible le podium du Module N (impossible quand chaque like est éparpillé).

L.2 — Accueil et Carte lisent via les props

  • Accueil({ jeux, setJeux, likes, onLike }) : enlève son useState(jeux) et son fetch (ils sont dans App). Garde recherche, tri, les champs du formulaire en local.
  • Passe à chaque carte le bon compteur : likes={likes[jeu.id] || 0} et onLike={() => onLike(jeu.id)}.
  • Dans Carte, supprime le useState/useEffect de likes : affiche props.likes et appelle props.onLike au clic.

L.3 — Detail affiche le vrai jeu avec ses likes

import { useParams, Link } from "react-router-dom";

function Detail({ jeux, likes }) {
  const { id } = useParams();
  const jeu = jeux.find((j) => String(j.id) === id);
  if (!jeu) return <p>Introuvable 🤷 <Link to="/"> retour</Link></p>;

  return (
    <div className="detail">
      <Link to="/"> Retour</Link>
      <h1>{jeu.titre}</h1>
      <p>🕹️ {jeu.console}  📅 {jeu.annee}  🎮 {jeu.Type}</p>
      <p>❤️ Likes : {likes[jeu.id] || 0}</p>
    </div>
  );
}

Test du boss : ajoute un jeu au formulaire → clique « détails » dessus → il s'affiche (alors qu'avant il était introuvable). Like-le sur l'accueil → son détail affiche bien le bon nombre. Recharge la page → les likes sont toujours là. 🏆

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


🎲 Module M — « Au hasard »

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

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

function Accueil({ jeux /* ... */ }) {
  const navigate = useNavigate();

  function hasard() {
    const i = Math.floor(Math.random() * jeux.length);
    navigate(`/jeu/${jeux[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…). Deux outils, deux usages.

Test : clique 🎲 plusieurs fois → tu tombes sur des jeux différents.


🏆 Module N — Le Hall of Fame

En haut de l'accueil, le Top 3 des jeux les plus likés, avec des médailles. (Possible uniquement parce que les likes sont centralisés depuis le Module L 😉)

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

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

<div className="hall">
  <h2>🏆 Hall of Fame</h2>
  {top.map((j, index) => (
    <p key={j.id}>{medailles[index]} {j.titre}  {likes[j.id] || 0} ❤️</p>
  ))}
</div>

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

Test : spamme le like sur un jeu → il grimpe en haut du Hall of Fame en direct.


🌗 Module O — Thème sombre (qui se souvient)

Un bouton 🌙 / ☀️ qui bascule toute la page en sombre et garde ton choix après un refresh — exactement le réflexe localStorage que tu connais déjà des likes.

O.1 — L'état + la mémoire (dans App)

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

O.2 — Une classe pilotée par le state

<div className={sombre ? "app sombre" : "app"}>
  <button onClick={() => setSombre(!sombre)}>
    {sombre ? "☀️ Clair" : "🌙 Sombre"}
  </button>
  {/* Header + Routes + Footer */}
</div>

O.3 — Le CSS (dans App.css)

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


Checklist du dernier jour

  • L — fetch + jeux + likes remontés dans App, page détail complète (ajouts + likes) 🔑
  • M — bouton 🎲 avec useNavigate
  • N — Hall of Fame Top 3 (sort + slice + index du map)
  • O — thème sombre persistant (localStorage + classe CSS pilotée par state)

🚀 Si tu finis tout (true endgame)

  • 🎉 Confetti au like : une petite animation CSS quand le compteur monte.
  • ⚔️ Mode Versus : 2 <select> pour choisir 2 jeux, compare leurs likes (ou leurs années), affiche le gagnant.
  • 🗂️ Filtre par Type (2d / 3d) en plus de la recherche.
  • 🗑️ Supprimer un jeu depuis sa fiche détail (filter).

🧠 Nouveaux concepts du jour

useNavigate (naviguer depuis du code) · état remonté au niveau du routeur + props passées à une <Route> · likes centralisés dans un objet plutôt qu'éparpillés · slice + index du map pour un classement · CSS piloté par le state (className conditionnel).

GG pour ces deux semaines. 🫡 Montre-moi ton boss quand il est battu !