diff --git a/ENONCE_final_boss.md b/ENONCE_final_boss.md index 9f7eea1..1e25aa0 100644 --- a/ENONCE_final_boss.md +++ b/ENONCE_final_boss.md @@ -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={}`. 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={}`. 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

Introuvable đŸ€· ← retour

; + if (!jeu) return

Introuvable — retour

; return (
← Retour

{jeu.titre}

-

đŸ•č {jeu.console} — 📅 {jeu.annee} — 🎼 {jeu.Type}

+

{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 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 ``). +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"; -function Accueil({ jeux /* ... */ }) { - const navigate = useNavigate(); - - function hasard() { - const i = Math.floor(Math.random() * jeux.length); - navigate(`/jeu/${jeux[i].id}`); - } - // ... +const navigate = useNavigate(); +function hasard() { + const i = Math.floor(Math.random() * jeux.length); + navigate(`/jeu/${jeux[i].id}`); } +// ``` -> 🔎 `` = naviguer **au clic** sur un lien. `useNavigate()` = naviguer **depuis du code** (aprĂšs un calcul, un envoi de formulaire
). Deux outils, deux usages. +> `` = 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); -
-

🏆 Hall of Fame

- {top.map((j, index) => ( -

{medailles[index]} {j.titre} — {likes[j.id] || 0} ❀

- ))} -
+//
+// {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 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 : +// 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 -
- - {/* Header + Routes + Footer */} -
+//
+// ``` - -### 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 `` 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 `` · 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 `` (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(/* ... */); + +// +// +// +``` + +> 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(); +// +``` + +**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.