Correction page detail + enonce final fun (boss de fin: detail complet, random, hall of fame, dark mode)
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# 🔎 Correction — faire (enfin) vivre la page Détail
|
||||
|
||||
Salut ! 👋 Ton routeur est **nickel** : clic sur « Voir les détails » → l'URL devient `/jeu/2` → la page change **sans recharger**. C'est tout l'intérêt de React Router, bravo. 🎉
|
||||
|
||||
Sauf que ta page `Detail.jsx` ressemble encore à ça :
|
||||
|
||||
```jsx
|
||||
function Detail() {
|
||||
const { id } = useParams();
|
||||
return <h1>Détails du jeu {id}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
Elle affiche juste le **numéro**. On veut le **vrai jeu** : son titre, sa console, son année, son type. Et là, une question piège…
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Le vrai problème : « où vivent les données ? »
|
||||
|
||||
`Detail` reçoit `id` (genre `"2"`). Mais la **liste des jeux** vit dans le `useState` de… `Accueil` (tu l'as remplie avec `fetch`). 😬
|
||||
|
||||
`Detail` et `Accueil` sont **deux pages sœurs** : ni l'une dans l'autre. Donc `Detail` **ne peut pas** lire l'état de `Accueil`. C'est le mur classique de React Router.
|
||||
|
||||
> 💡 **La règle :** un composant ne voit que **ses props** et **son propre state**. Pour qu'une donnée soit partagée par deux pages, elle doit vivre **au-dessus des deux**, ou dans un endroit que les deux savent lire.
|
||||
|
||||
Comme ton appli lit déjà ses données via `fetch`, on a une solution simple **et** une solution pro. Prends la simple d'abord. 👇
|
||||
|
||||
---
|
||||
|
||||
## ✅ Solution simple — la page détail va chercher la donnée elle-même
|
||||
|
||||
`Detail` connaît l'`id` voulu. Elle peut **refaire le `fetch`** du même fichier et **retrouver** le bon jeu avec `.find()`.
|
||||
|
||||
```jsx
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
function Detail() {
|
||||
const { id } = useParams();
|
||||
const [jeu, setJeu] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/zelda.json")
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
// ⚠️ piège : useParams renvoie une CHAÎNE. Tes id du JSON sont aussi
|
||||
// des chaînes ("1","2"...) → ici on compare string === string, ok.
|
||||
// Mais reste vigilant : si un id est un nombre, fais String(j.id) === id
|
||||
const trouve = data.find((j) => j.id === id);
|
||||
setJeu(trouve);
|
||||
});
|
||||
}, [id]); // 👈 on relance si l'id change (navigation d'un détail à un autre)
|
||||
|
||||
if (!jeu) return <p>Jeu introuvable 🤷 <Link to="/">← retour</Link></p>;
|
||||
|
||||
return (
|
||||
<div className="detail">
|
||||
<Link to="/">← Retour à la liste</Link>
|
||||
<h1>{jeu.titre}</h1>
|
||||
<p>🕹️ Console : {jeu.console}</p>
|
||||
<p>📅 Année : {jeu.annee}</p>
|
||||
<p>🎮 Type : {jeu.Type}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Detail;
|
||||
```
|
||||
|
||||
> 🐛 **Le piège classique ici, c'est le type de l'`id`.** `useParams()` renvoie **toujours une chaîne** (`"2"`). Tes id dans `zelda.json` sont aussi des chaînes → `j.id === id` marche. Mais le jour où un id est un **nombre**, `2 === "2"` vaut `false` et `find` ne trouve rien → compare avec `String(j.id) === id`.
|
||||
|
||||
> 🔎 **Pourquoi `[id]` dans le `useEffect` ?** Sans lui, le fetch ne se ferait **qu'une fois**. Comme on peut naviguer d'un détail à un autre (`/jeu/2` → `/jeu/3`) **sans démonter le composant**, il faut redéclencher l'effet quand `id` change.
|
||||
|
||||
**✅ Test :** clique « détails » sur Breath of the Wild → tu vois Switch / 2017 / 3d, pas juste « 2 ». Le lien « ← Retour » te ramène à la liste.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Les limites de cette solution (et c'est voulu)
|
||||
|
||||
1. Les jeux **ajoutés au formulaire** ne sont **pas** dans `zelda.json` → leur page détail dira « introuvable ».
|
||||
2. Le **nombre de likes** (stocké dans `localStorage` par chaque `Carte`) n'apparaît pas non plus.
|
||||
|
||||
Pour que **tout** suive jusqu'au détail (jeux ajoutés + likes), il faut **remonter l'état au niveau du routeur** (le `fetch` et la liste vivent dans `App`, pas dans `Accueil`). C'est le **Module L** de l'énoncé final 👇 — le vrai « boss ». 😈
|
||||
|
||||
---
|
||||
|
||||
## 🧹 Pendant qu'on y est, 2 petits nettoyages
|
||||
|
||||
1. `console.log(props.gameId)` dans `Carte.jsx` → on enlève les logs de debug avant de livrer. 🧼
|
||||
2. Dans le formulaire d'ajout, tu crées le jeu avec `console: "?"`… mais tu as un champ `plateforme`. Mets plutôt `console: plateforme` pour que la console saisie s'affiche vraiment sur la carte.
|
||||
|
||||
> 💡 **À retenir :** une page ne peut pas piocher dans le state d'une autre page. Soit elle **va chercher** la donnée elle-même (re-`fetch` + `find`), soit on fait **remonter l'état** chez le parent commun. Et `useParams()` renvoie **toujours une chaîne** → attention au type quand tu compares aux ids.
|
||||
@@ -0,0 +1,199 @@
|
||||
# 🏁 É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. 💪
|
||||
|
||||
| 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 | ⭐⭐ | 🌙 |
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Module L — Le boss : centraliser les données dans `App`
|
||||
|
||||
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.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 (
|
||||
<>
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route path="/" element={
|
||||
<Accueil jeux={jeux} setJeux={setJeux} likes={likes} onLike={liker} />
|
||||
} />
|
||||
<Route path="/jeu/:id" element={
|
||||
<Detail jeux={jeux} likes={likes} />
|
||||
} />
|
||||
</Routes>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> 🧩 **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é).
|
||||
|
||||
### 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 <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>❤️ 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à**. 🏆
|
||||
|
||||
> 🧠 **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.
|
||||
|
||||
---
|
||||
|
||||
## 🎲 Module M — « 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>`).
|
||||
|
||||
```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>
|
||||
}
|
||||
```
|
||||
|
||||
> 🔎 `<Link>` = naviguer **au clic** sur un lien. `useNavigate()` = naviguer **depuis du code** (après un calcul, un envoi de formulaire…). Deux outils, deux usages.
|
||||
|
||||
**✅ 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);
|
||||
|
||||
<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>
|
||||
```
|
||||
|
||||
> 🔎 `.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.
|
||||
|
||||
**✅ Test :** spamme le like sur un jeu → il grimpe en haut du Hall of Fame en direct.
|
||||
|
||||
---
|
||||
|
||||
## 🌗 Module O — Thème sombre (qui se souvient)
|
||||
|
||||
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.
|
||||
|
||||
### O.1 — L'état + la mémoire (dans `App`)
|
||||
```jsx
|
||||
const [sombre, setSombre] = useState(
|
||||
() => localStorage.getItem("theme") === "sombre"
|
||||
);
|
||||
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>
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
**✅ 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)
|
||||
|
||||
## 🚀 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`).
|
||||
|
||||
## 🧠 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).
|
||||
|
||||
GG pour ces deux semaines. 🫡 Montre-moi ton boss quand il est battu !
|
||||
Reference in New Issue
Block a user