Enonce final: plus d'exercices (versus, stats, 404, tri inverse, custom hook, edition) + ton allege

This commit is contained in:
2026-06-26 10:02:43 +02:00
parent d9c7f81136
commit e798810283
+204 -103
View File
@@ -1,21 +1,30 @@
# 🏁 Énoncé FINAL — « Boss de fin » 😈
# 🏁 Énoncé FINAL — « Boss de fin »
Dernier jour ! 🎉 Tu as déjà tout l'arsenal : composants, props, state, lifting state up, `useEffect`, formulaire, tri, filtre, **React Router**. Aujourd'hui on **s'amuse** : 4 modules courts et stylés, du plus utile au plus fun. Fais-en autant que tu veux, dans l'ordre que tu veux — mais le **Module L** est le vrai boss, attaque-le en premier. 💪
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.
| Module | Tu fais | Niveau | Fun |
|---|---|---|---|
| L | La page détail **complète** (state remonté au routeur) | ⭐⭐⭐ | 🔑 |
| M | Bouton **🎲 « Je tente ma chance »** (page au hasard) | ⭐⭐ | 😄 |
| N | **🏆 Le Podium** des trilogies les plus likées | ⭐⭐ | 🥇 |
| O | **🌗 Thème sombre** qui se souvient de ton choix | ⭐⭐ | 🌙 |
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
## 🔑 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`. Le souci : les trilogies **ajoutées au formulaire** et les **likes** n'arrivent pas dans la page détail. On corrige ça **proprement**.
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`. `App` la passe ensuite **en props** aux deux routes.
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
@@ -31,174 +40,266 @@ function App() {
return (
<Routes>
<Route
path="/"
element={
<Accueil
trilogies={trilogies}
setTrilogies={setTrilogies}
likes={likes}
setLikes={setLikes}
/>
}
/>
<Route
path="/trilogie/:id"
element={<Details trilogies={trilogies} likes={likes} />}
/>
<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 peut recevoir des props comme n'importe quel composant — `element={<Details trilogies={trilogies} />}`. C'est ça qui « branche » la donnée sur les deux pages.
> 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 (au lieu de son state local)
Enlève les `useState` de `trilogies` et `likes` dans `Accueil` : ils viennent maintenant de `props`. Garde en local ce qui est **propre à la page d'accueil** (`recherche`, `tri`, les champs du formulaire).
### 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 }) {
// recherche, tri, nom, niveaux restent en useState local
// 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**
### 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 <p>Introuvable 🤷 <Link to="/"> retour</Link></p>;
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>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 bien « ❤️ Likes : 3 ». 🏆
**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 en React.
> 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 — « Je tente ma chance »
## 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 avec un clic sur un `<Link>`).
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>`).
```jsx
import { useNavigate } from "react-router-dom";
function Accueil({ trilogies, /* ... */ }) {
const navigate = useNavigate();
function hasard() {
const i = Math.floor(Math.random() * trilogies.length);
navigate(`/trilogie/${trilogies[i].id}`);
}
// ...
return (
<>
<button onClick={hasard}>🎲 Je tente ma chance</button>
{/* ... reste de la page ... */}
</>
);
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…). Deux outils, deux usages.
> `<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.
**Test :** clique plusieurs fois → tu tombes sur des trilogies différentes.
---
## 🏆 Module N — Le Podium
## Module N — Le podium
En haut de l'accueil, affiche le **Top 3** des trilogies les plus likées, avec des médailles.
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); // on ne garde que les 3 premières
.slice(0, 3);
// dans le JSX :
<div className="podium">
<h2>🏆 Podium</h2>
{podium.map((t, index) => (
<p key={t.id}>{medailles[index]} {t.nom} {likes[t.id] || 0} </p>
))}
</div>
// <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 éléments d'indice 0, 1, 2 ». Et le **2ᵉ paramètre** de `.map((t, index) => ...)`, c'est la **position** dans la liste → parfait pour choisir la médaille.
> `.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 temps réel.
**Test :** like beaucoup une trilogie → elle grimpe sur le podium en direct.
---
## 🌗 Module O — Thème sombre (qui se souvient)
## Module O — Supprimer une trilogie
Un bouton 🌙 / ☀️ qui bascule toute la page en sombre, **et garde ton choix** même après un refresh (`localStorage`, comme les likes du Module H).
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 : <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).
### O.1 — L'état + la mémoire
```jsx
const [sombre, setSombre] = useState(
() => localStorage.getItem("theme") === "sombre"
);
useEffect(() => {
localStorage.setItem("theme", sombre ? "sombre" : "clair");
}, [sombre]);
```
### O.2 — Appliquer une classe sur le conteneur
```jsx
<div className={sombre ? "body sombre" : "body"}>
<button onClick={() => setSombre(!sombre)}>
{sombre ? "☀️ Clair" : "🌙 Sombre"}
</button>
{/* ... */}
</div>
// <div className={sombre ? "body sombre" : "body"}>
// <button onClick={() => setSombre(!sombre)}>{sombre ? "☀️" : "🌙"}</button>
```
### O.3 — Le CSS (dans `App.css`)
```css
.sombre {
background-color: #1a1a2e;
color: #eaeaea;
}
.sombre .carte {
background-color: #16213e;
color: #eaeaea;
}
.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`, et le navigateur applique les bonnes règles. C'est comme ça que marchent **tous** les dark modes que tu connais.
> 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 la page (F5) → c'est **toujours** sombre. 🌙
**Test :** passe en sombre → recharge (F5) → c'est toujours sombre.
---
## ✅ Checklist du dernier jour
- [ ] L — state remonté dans `App`, page détail complète (ajouts + likes inclus) 🔑
- [ ] M — bouton 🎲 avec `useNavigate`
- [ ] N — podium Top 3 (`sort` + `slice` + `index` dans `map`)
- [ ] O — thème sombre persistant (`localStorage` + classe CSS pilotée par state)
## Module Q — Mode Versus
## 🚀 Si tu finis tout (true endgame)
- 🎉 **Confetti au like** : une petite animation CSS quand le compteur monte.
- ⚔️ **Mode Versus** : choisis 2 trilogies dans 2 `<select>`, compare leurs likes, affiche la gagnante.
- 🗑️ **Supprimer / éditer** une trilogie depuis sa page détail.
- 📱 Soigner le **responsive** (flex/grid) pour que ce soit beau sur téléphone.
Deux `<select>` pour choisir deux trilogies ; on compare leurs likes et on affiche la gagnante.
## 🧠 Nouveaux concepts du jour
`useNavigate` (naviguer depuis du code) · état remonté **au niveau du routeur** + props passées à une `<Route>` · `slice` + `index` du `map` pour un classement · **CSS piloté par le state** (`className` conditionnel) · persistance `localStorage`.
```jsx
const [a, setA] = useState(trilogies[0].id);
const [b, setB] = useState(trilogies[1].id);
GG pour ces deux semaines. 🫡 Montre-moi ton boss quand il est battu !
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`.
```jsx
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.
```jsx
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.
```jsx
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**.
```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 `<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
```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 `<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.