Enonce final: plus d'exercices (versus, filtre type, stats, 404, tri inverse, custom hook) + ton allege
This commit is contained in:
+205
-69
@@ -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`, **`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. 💪
|
||||
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.
|
||||
|
||||
| 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 | ⭐⭐ | 🌙 |
|
||||
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`
|
||||
## 🔑 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**.
|
||||
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'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
|
||||
@@ -61,68 +70,65 @@ function App() {
|
||||
}
|
||||
```
|
||||
|
||||
> 🧩 **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.
|
||||
> 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é).
|
||||
> 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**
|
||||
### 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 <p>Introuvable 🤷 <Link to="/">← retour</Link></p>;
|
||||
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>{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à**. 🏆
|
||||
**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 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 (`App`) des composants qui en ont besoin. C'est le réflexe d'archi central en React.
|
||||
|
||||
---
|
||||
|
||||
## 🎲 Module M — « Au hasard »
|
||||
## 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 `<Link>`).
|
||||
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>`).
|
||||
|
||||
```jsx
|
||||
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>
|
||||
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.
|
||||
> `<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 jeux différents.
|
||||
**Test :** clique plusieurs fois → tu tombes sur des jeux différents.
|
||||
|
||||
---
|
||||
|
||||
## 🏆 Module N — Le Hall of Fame
|
||||
## 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 😉)
|
||||
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 = ["🥇", "🥈", "🥉"];
|
||||
@@ -131,25 +137,41 @@ 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>
|
||||
// <div className="hall">
|
||||
// {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.
|
||||
> `.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 :** spamme le like sur un jeu → il grimpe en haut du Hall of Fame en direct.
|
||||
**Test :** like beaucoup un jeu → il grimpe en haut du Hall of Fame en direct.
|
||||
|
||||
---
|
||||
|
||||
## 🌗 Module O — Thème sombre (qui se souvient)
|
||||
## Module O — Supprimer un jeu
|
||||
|
||||
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.
|
||||
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 : <button onClick={() => props.onDelete()}>🗑️</button>
|
||||
// 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.
|
||||
|
||||
### O.1 — L'état + la mémoire (dans `App`)
|
||||
```jsx
|
||||
const [sombre, setSombre] = useState(
|
||||
() => localStorage.getItem("theme") === "sombre"
|
||||
@@ -157,43 +179,157 @@ const [sombre, setSombre] = useState(
|
||||
useEffect(() => {
|
||||
localStorage.setItem("theme", sombre ? "sombre" : "clair");
|
||||
}, [sombre]);
|
||||
```
|
||||
|
||||
### O.2 — Une classe pilotée par le state
|
||||
```jsx
|
||||
<div className={sombre ? "app sombre" : "app"}>
|
||||
<button onClick={() => setSombre(!sombre)}>
|
||||
{sombre ? "☀️ Clair" : "🌙 Sombre"}
|
||||
</button>
|
||||
{/* Header + Routes + Footer */}
|
||||
</div>
|
||||
// <div className={sombre ? "app sombre" : "app"}>
|
||||
// <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; }
|
||||
```
|
||||
|
||||
> 🧩 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.
|
||||
> 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. 🌙
|
||||
**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)
|
||||
## Module Q — Mode Versus
|
||||
|
||||
## 🚀 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`).
|
||||
Deux `<select>` pour choisir deux jeux ; on compare leurs likes (ou leurs années) et on affiche le gagnant.
|
||||
|
||||
## 🧠 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).
|
||||
```jsx
|
||||
const [a, setA] = useState(jeux[0].id);
|
||||
const [b, setB] = useState(jeux[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 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 `<select>` (un pour `a`, un pour `b`) listant `jeux.map(j => <option value={j.id}>{j.titre}</option>)`, puis un `<p>` qui annonce le gagnant.
|
||||
|
||||
> Attention au type de la valeur d'un `<select>` (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(/* ... */);
|
||||
|
||||
// <button onClick={() => setFiltreType("tous")}>Tous</button>
|
||||
// <button onClick={() => setFiltreType("2d")}>2D</button>
|
||||
// <button onClick={() => setFiltreType("3d")}>3D</button>
|
||||
```
|
||||
|
||||
> 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;
|
||||
// <p>{nbJeux} jeux · {totalLikes} likes · année moyenne {anneeMoyenne}</p>
|
||||
```
|
||||
|
||||
> `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 <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 → 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();
|
||||
// <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 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 `<Route>` · 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.
|
||||
|
||||
Reference in New Issue
Block a user