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
Cartegardait son like dans son coin (localStorage.getItem("likes-" + id)). Maintenant c'est un seul objetlikes(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 sonuseState(jeux)et sonfetch(ils sont dansApp). Garderecherche,tri, les champs du formulaire en local.- Passe à chaque carte le bon compteur :
likes={likes[jeu.id] || 0}etonLike={() => onLike(jeu.id)}. - Dans
Carte, supprime leuseState/useEffectde likes : afficheprops.likeset appelleprops.onLikeau 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 :
classNamechange selonsombre, 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+likesremontés dansApp, page détail complète (ajouts + likes) 🔑 - M — bouton 🎲 avec
useNavigate - N — Hall of Fame Top 3 (
sort+slice+indexdumap) - 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 !