# đ ĂnoncĂ© FINAL â « Boss de fin »
Dernier jour. Tu as tout l'arsenal : composants, props, state, lifting state up, `useEffect`, `fetch`, formulaire, tri, filtre, React Router, `localStorage`. 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** (donnĂ©es centralisĂ©es dans `App`) â *obligatoire* | âââ |
| M | Bouton « au hasard » (`useNavigate`) | ââ |
| N | Le Hall of Fame des jeux les plus likĂ©s | ââ |
| O | Bouton « supprimer » un jeu | ââ |
| P | ThĂšme sombre qui se souvient du choix | ââ |
| Q | Mode Versus : deux jeux s'affrontent | ââ |
| R | Filtre par Type (2d / 3d) | ââ |
| S | Statistiques (total, moyenne d'annĂ©e) | ââ |
| T | Page « 404 » pour les URL inconnues | ââ |
| U | Tri ascendant â descendant | â |
| V | Custom hook `useLocalStorage` (refacto) | âââ |
---
## đ Module L â Le boss : centraliser les donnĂ©es dans `App` (obligatoire)
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`, qui les passe ensuite **en props** aux routes.
### L.1 â Remonter le `fetch` et les Ă©tats dans `App.jsx`
```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 (
<>
} />
} />
>
);
}
```
> Le dĂ©clic : une route reçoit des props comme n'importe quel composant â `element={ }`. 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 Hall of Fame 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
```jsx
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
Introuvable â retour
;
return (
â Retour
{jeu.titre}
{jeu.console} â {jeu.annee} â {jeu.Type}
â€ïž Likes : {likes[jeu.id] || 0}
);
}
```
**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 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 central en React.
---
## Module M â Bouton « 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 ` `).
```jsx
import { useNavigate } from "react-router-dom";
const navigate = useNavigate();
function hasard() {
const i = Math.floor(Math.random() * jeux.length);
navigate(`/jeu/${jeux[i].id}`);
}
// đČ Au hasard
```
> ` ` = 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 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.)
```jsx
const medailles = ["đ„", "đ„", "đ„"];
const top = [...jeux]
.sort((a, b) => (likes[b.id] || 0) - (likes[a.id] || 0))
.slice(0, 3);
//
// {top.map((j, index) => (
//
{medailles[index]} {j.titre} â {likes[j.id] || 0} â€ïž
// ))}
//
```
> `.slice(0, 3)` garde les indices 0, 1, 2. Le 2á” paramĂštre de `.map((j, index) => ...)`, c'est la position â parfait pour la mĂ©daille.
**Test :** like beaucoup un jeu â il grimpe en haut du Hall of Fame en direct.
---
## Module O â Supprimer un jeu
Un bouton « supprimer » sur chaque carte. La clé : l'immutabilité (un nouveau tableau **sans** l'élément).
```jsx
function supprimer(id) {
setJeux(jeux.filter((j) => j.id !== id));
}
// sur la Carte : props.onDelete()}>đïž
// et dans Accueil : onDelete={() => supprimer(jeu.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 â exactement le rĂ©flexe `localStorage` que tu connais des likes.
```jsx
const [sombre, setSombre] = useState(
() => localStorage.getItem("theme") === "sombre"
);
useEffect(() => {
localStorage.setItem("theme", sombre ? "sombre" : "clair");
}, [sombre]);
//
//
setSombre(!sombre)}>{sombre ? "âïž" : "đ"}
```
```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.
---
## Module Q â Mode Versus
Deux `
` pour choisir deux jeux ; on compare leurs likes (ou leurs années) et on affiche le gagnant.
```jsx
const [a, setA] = useState(jeux[0].id);
const [b, setB] = useState(jeux[1].id);
const likesA = likes[a] || 0;
const likesB = likes[b] || 0;
const gagnant = likesA === likesB ? "ĂgalitĂ©" :
(likesA > likesB ? jeux.find(j => String(j.id) === String(a)).titre
: jeux.find(j => String(j.id) === String(b)).titre);
```
Deux `` (un pour `a`, un pour `b`) listant `jeux.map(j => {j.titre} )`, puis un `` qui annonce le gagnant.
> Attention au type de la valeur d'un `` (elle revient en chaßne) : compare avec `String(...)` des deux cÎtés.
**Test :** change les deux menus â le verdict se met Ă jour tout seul.
---
## Module R â Filtre par Type
Tes jeux ont un champ `Type` (`"2d"` / `"3d"`). Ajoute trois boutons (Tous / 2d / 3d) qui filtrent la liste, en plus de ta recherche.
```jsx
const [filtreType, setFiltreType] = useState("tous");
// dans ta chaĂźne filtre â tri â map, ajoute une Ă©tape :
const affiches = [...jeux]
.filter((j) => j.titre.toLowerCase().includes(recherche.toLowerCase()))
.filter((j) => filtreType === "tous" || j.Type === filtreType)
.sort(/* ... */);
// setFiltreType("tous")}>Tous
// setFiltreType("2d")}>2D
// setFiltreType("3d")}>3D
```
> On enchaĂźne deux `.filter()` : recherche **puis** type. Chaque filtre renvoie un nouveau tableau, on les compose tranquillement.
**Test :** clique « 2D » â seuls les jeux 2d restent, et la recherche marche toujours par-dessus.
---
## Module S â Statistiques
Une petite barre de stats calculée à partir des données. On muscle `reduce`.
```jsx
const nbJeux = jeux.length;
const totalLikes = Object.values(likes).reduce((s, n) => s + n, 0);
const anneeMoyenne = nbJeux
? Math.round(jeux.reduce((s, j) => s + Number(j.annee), 0) / nbJeux)
: 0;
// {nbJeux} jeux · {totalLikes} likes · année moyenne {anneeMoyenne}
```
> `reduce` réduit un tableau à **une seule** valeur en accumulant. Ici on additionne likes et années.
**Test :** ajoute/like des jeux â les chiffres bougent.
---
## Module T â Page « 404 »
Que se passe-t-il si on tape `/jeu/banane` ou une URL qui n'existe pas ? Rien d'affiché. On ajoute une route attrape-tout.
```jsx
import { Link } from "react-router-dom";
function NotFound() {
return
404 â page introuvable Retour Ă l'accueil
;
}
// dans , en DERNIER :
// } />
```
> `path="*"` = « toutes les URL non reconnues ». à mettre **en dernier**, sinon il attrape tout avant les autres.
**Test :** tape une URL bidon â la page 404 s'affiche.
---
## Module U â Tri ascendant â descendant
Ajoute un bouton qui inverse l'ordre du tri en cours.
```jsx
const [ordre, setOrdre] = useState("asc");
// aprĂšs ton .sort(...) :
const affiches = [...maListeTriee];
if (ordre === "desc") affiches.reverse();
// setOrdre(ordre === "asc" ? "desc" : "asc")}>
// {ordre === "asc" ? "AâZ" : "ZâA"}
//
```
**Test :** clique le bouton â l'ordre des cartes s'inverse.
---
## Module V â 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**.
```jsx
// 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 dans `App` : `const [likes, setLikes] = useLocalStorage("likes", {});` remplace les deux blocs likes d'un coup.
> 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 jeux â recharge â les likes sont toujours lĂ , mais avec deux fois moins de code.
---
## â
Checklist
- [ ] L â `fetch` + `jeux` + `likes` remontĂ©s dans `App`, page dĂ©tail complĂšte *(obligatoire)*
- [ ] M â au hasard · [ ] N â Hall of Fame · [ ] O â supprimer · [ ] P â thĂšme sombre
- [ ] Q â versus · [ ] R â filtre Type · [ ] S â stats · [ ] T â page 404
- [ ] U â tri inversĂ© · [ ] V â custom hook
## đ§ Concepts rencontrĂ©s aujourd'hui
`useNavigate` · `fetch` + état remontés au niveau du routeur + props passées à une `` · likes **centralisés** dans un objet · `filter` (supprimer, filtrer) · `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.