# đ Ă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`
```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 (
} />
} />
);
}
```
> 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.
### 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).
```jsx
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
```jsx
function Details({ trilogies, likes }) {
const { id } = useParams();
const trilogie = trilogies.find((t) => t.id === Number(id));
if (!trilogie) return
Introuvable â retour
;
return (
â Retour
{trilogie.nom}
Niveaux : {trilogie.niveaux}
â€ïž Likes : {likes[trilogie.id] || 0}
);
}
```
**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 ` `).
```jsx
import { useNavigate } from "react-router-dom";
const navigate = useNavigate();
function hasard() {
const i = Math.floor(Math.random() * trilogies.length);
navigate(`/trilogie/${trilogies[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 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.
```jsx
const medailles = ["đ„", "đ„", "đ„"];
const podium = [...trilogies]
.sort((a, b) => (likes[b.id] || 0) - (likes[a.id] || 0))
.slice(0, 3);
//
// {podium.map((t, index) => (
//
{medailles[index]} {t.nom} â {likes[t.id] || 0} â€ïž
// ))}
//
```
> `.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).
```jsx
function supprimer(id) {
setTrilogies(trilogies.filter((t) => t.id !== id));
}
// sur la Carte : props.onDelete()}>đïž
// 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).
```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 trilogies ; on compare leurs likes et on affiche la gagnante.
```jsx
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 `` (un pour `a`, un pour `b`) listant `trilogies.map(t => {t.nom} )`, puis un `` qui annonce la gagnante.
> Pense Ă `Number(...)` quand tu relis la valeur d'un `` : 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`.
```jsx
const nbTrilogies = trilogies.length;
const totalLikes = Object.values(likes).reduce((s, n) => s + n, 0);
const moyenne = nbTrilogies ? (totalLikes / nbTrilogies).toFixed(1) : 0;
// {nbTrilogies} trilogies · {totalLikes} likes · {moyenne} likes/trilogie
```
> `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.
```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 dans la barre â la page 404 s'affiche.
---
## Module T â 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 affichees = [...maListeTriee];
if (ordre === "desc") affichees.reverse();
// setOrdre(ordre === "asc" ? "desc" : "asc")}>
// {ordre === "asc" ? "AâZ" : "ZâA"}
//
```
**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**.
```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 : `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 ` ` Ă 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
```jsx
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 `` · `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.