Contact: correction doublon likes dans Carte + enonce guide persistance des jeux (ajout/suppression/modif au refresh)

This commit is contained in:
2026-06-26 14:48:50 +02:00
parent 85d30252f3
commit b7e6fa844b
2 changed files with 217 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
# 🔧 Correction — il reste deux compteurs de likes en parallèle
Bravo, le boss est presque parfait : `App` centralise `jeux` et `likes`, `Detail` retrouve le bon jeu, le Hall of Fame, les stats et le filtre 2D/3D marchent. Reste **un fil débranché** dans `Carte.jsx`.
## 🐛 Le bug : `Carte` garde encore SON propre compteur
Dans `Carte.jsx`, tu as gardé l'ancienne version locale des likes :
```jsx
const [likes, setLikes] = useState(
() => Number(localStorage.getItem("likes-" + props.gameId)) || 0,
);
useEffect(() => {
localStorage.setItem("likes-" + props.gameId, likes);
}, [likes]);
// ...
<button onClick={() => { setLikes(likes + 1); props.onLike(); }}>
{likes} {/* ← affiche le compteur LOCAL */}
</button>
```
Du coup il existe **deux compteurs différents** pour un même jeu :
- celui d'`App` (l'objet `likes`, rangé dans `localStorage["likes"]`) → utilisé par le Hall of Fame, les stats et la page Détail ;
- celui de `Carte` (son `useState` + `localStorage["likes-3"]`) → affiché sur le bouton de la carte.
Au clic, tu incrémentes **les deux** (`setLikes` ET `props.onLike`), donc ça « a l'air » de marcher. Mais les deux peuvent **diverger** : la carte affiche son chiffre à elle, pendant que le podium affiche celui d'`App`. Et tu te retrouves avec deux clés de stockage qui font la même chose.
> 🧩 La règle (encore elle) : **une seule source de vérité**. Les likes vivent dans `App`. `Carte` doit juste les **afficher** (`props.likes`) et **demander** l'incrément (`props.onLike`), sans tenir son propre compte.
## ✅ La correction dans `Carte.jsx`
1. Supprime le `useState` et le `useEffect` des likes (et les imports `useState`/`useEffect` s'ils ne servent plus à rien d'autre).
2. Affiche `props.likes` au lieu de `likes`.
3. Au clic, appelle seulement `props.onLike`.
```jsx
<button onClick={props.onLike}> {props.likes}</button>
```
`Accueil` passe déjà `likes={likes[jeu.id] || 0}` et `onLike={() => onLike(jeu.id)}` → la carte a tout ce qu'il faut.
> 🧽 Pense aussi à supprimer dans le navigateur les vieilles clés `likes-1`, `likes-2`… (onglet *Application → Local Storage* des outils dev) : elles ne servent plus.
## 🧹 Petit bonus : `class` → `className`
Dans `Accueil.jsx` et `Carte.jsx`, tes séparateurs sont écrits `<section class="spacer">`. En React, l'attribut s'appelle **`className`** (`class` est un mot réservé en JavaScript). Le navigateur l'ignore tel quel → ouvre la console (F12), tu verras l'avertissement `Invalid DOM property 'class'. Did you mean 'className'?`.
```jsx
<section className="spacer"></section>
```
> 💡 À retenir : en JSX, `class` devient `className`, et `for` (sur un `<label>`) devient `htmlFor`. Ce sont les deux seuls noms qui changent par rapport au HTML.
+166
View File
@@ -0,0 +1,166 @@
# 💾 Énoncé — Sauvegarder les JEUX (pas que les likes)
Aujourd'hui, tes **likes** survivent au refresh (bravo, c'est exactement le bon réflexe `localStorage`). Mais la **liste des jeux**, elle, repart du `zelda.json` à chaque rechargement : tes ajouts disparaissent, et tu ne peux ni supprimer ni modifier durablement.
Objectif : faire vivre la liste `jeux` **comme** les likes — initialisée depuis `localStorage`, sauvegardée à chaque changement — pour pouvoir **ajouter, supprimer, modifier** et tout retrouver après un F5.
> ⚠️ On ne touche à rien d'autre que `App.jsx`, `Accueil.jsx` et `Carte.jsx`. Tu connais déjà toutes les briques : la seule **idée neuve** est « d'où viennent les jeux la première fois ? ».
---
## 🧠 Le seul piège à comprendre d'abord
Pour les likes c'était simple : au tout début ils valent `{}`.
```jsx
const [likes] = useState(() => JSON.parse(localStorage.getItem("likes") || "{}"));
```
Pour les jeux, c'est différent : la **première fois**, ils ne viennent pas de `localStorage` (vide) mais du **`fetch` du `zelda.json`**. Donc la logique est :
> **Si `localStorage` contient déjà des jeux → on les prend. Sinon (première visite) → on va chercher le `zelda.json`.**
Une fois chargés, `localStorage` devient **la source de vérité** : c'est lui qu'on relit aux prochains refresh (avec tes ajouts/suppressions/modifs), et le `json` ne ressert plus.
---
## 📝 Étape 1 — Initialiser `jeux` depuis `localStorage` (dans `App.jsx`)
Remplace ton `useState([])` par une initialisation paresseuse, avec une **sentinelle `null`** qui veut dire « pas encore chargé » :
```jsx
const [jeux, setJeux] = useState(() => {
const stocke = localStorage.getItem("jeux");
return stocke ? JSON.parse(stocke) : null; // null = jamais chargé
});
```
> 🔎 Pourquoi `null` et pas `[]` ? Parce qu'on doit **distinguer** deux cas : « la liste est vide parce que tu as tout supprimé » (`[]`, à garder !) et « on n'a encore rien chargé » (`null`, il faut aller chercher le json). Avec `[]` partout, impossible de faire la différence.
## 📝 Étape 2 — Aller chercher le json UNE SEULE FOIS si besoin
Ton `useEffect` de fetch existe déjà. Ajoute juste la condition « seulement si rien n'est encore chargé » :
```jsx
useEffect(() => {
if (jeux === null) { // rien en localStorage → 1re visite
fetch("/zelda.json")
.then((r) => r.json())
.then(setJeux);
}
}, []); // [] : on ne tente ça qu'au montage
```
## 📝 Étape 3 — Sauvegarder à chaque changement
Exactement le même réflexe que pour les likes, mais on **ne sauvegarde pas** tant que c'est `null` (sinon on écrirait « null » par-dessus tes données) :
```jsx
useEffect(() => {
if (jeux !== null) {
localStorage.setItem("jeux", JSON.stringify(jeux));
}
}, [jeux]);
```
## 📝 Étape 4 — Gérer l'écran « en cours de chargement »
Tant que `jeux` vaut `null`, `Accueil` ne peut pas faire `jeux.length` ou `jeux.map` (ça planterait). Affiche un petit message le temps du fetch. Dans `App.jsx`, juste avant le `return` des routes :
```jsx
if (jeux === null) return <p>Chargement</p>;
```
**✅ Test de l'étape 1-4 :** recharge la page → tout s'affiche normalement. Ouvre les outils dev (F12) → *Application → Local Storage* → tu dois voir une clé **`jeux`** avec tous tes jeux dedans. 🎉
---
## Étape 5 — Ajouter (ça marche déjà… et maintenant ça PERSISTE)
Ta fonction `ajouter` fait déjà `setJeux([...jeux, nouveau])`. Tu n'as **rien à changer** : comme `setJeux` déclenche le `useEffect` de l'étape 3, l'ajout est **automatiquement** sauvegardé.
**✅ Test :** ajoute un jeu → F5 → il est **toujours là**.
> 🛠️ Détail : ton formulaire crée `Type: "3d"` en dur. Tu peux ajouter un `<select>` (2d / 3d) si tu veux choisir, comme pour la console.
---
## 🗑️ Étape 6 — Supprimer un jeu
La liste vit dans `App`, donc la fonction qui supprime doit pouvoir appeler `setJeux`. `Accueil` reçoit déjà `setJeux` en props → on définit `supprimer` dans `Accueil` et on la passe à chaque `Carte`.
Dans `Accueil.jsx` :
```jsx
function supprimer(id) {
setJeux(jeux.filter((j) => j.id !== id)); // nouveau tableau SANS ce jeu
}
```
Puis sur chaque carte, passe la fonction :
```jsx
<Carte
/* ...les props existantes... */
onDelete={() => supprimer(jeu.id)}
/>
```
Et dans `Carte.jsx`, un bouton :
```jsx
<button onClick={props.onDelete}>🗑 Supprimer</button>
```
> 🧩 `filter` renvoie un **nouveau** tableau contenant tout **sauf** le jeu visé. On ne modifie jamais l'ancien (immutabilité, comme pour l'ajout). Et grâce à l'étape 3, la suppression est sauvegardée toute seule.
**✅ Test :** supprime un jeu → F5 → il **ne revient pas**.
---
## ✏️ Étape 7 — Modifier un jeu
Le plus costaud, même principe. On veut remplacer **un seul** jeu dans la liste en gardant les autres intacts → c'est le boulot de `map`.
Dans `Accueil.jsx` :
```jsx
function modifier(id, nouveauTitre) {
setJeux(
jeux.map((j) => (j.id === id ? { ...j, titre: nouveauTitre } : j)),
);
}
```
> 🔎 `map` parcourt la liste : pour le jeu visé, il renvoie une **copie modifiée** (`{ ...j, titre: nouveauTitre }`) ; pour les autres, il les renvoie tels quels. Résultat : un nouveau tableau où seul un jeu a changé.
Pour l'interface, le plus simple pour commencer : un bouton « ✏️ Renommer » sur la carte qui demande le nouveau titre, par exemple avec `prompt` (suffisant pour un premier jet) :
```jsx
<button onClick={() => {
const t = prompt("Nouveau titre ?", props.titre);
if (t) props.onEdit(t);
}}> Renommer</button>
```
…avec `onEdit={(t) => modifier(jeu.id, t)}` passé depuis `Accueil`.
> 🚀 Version « propre » (bonus) : un champ `<input>` qui apparaît dans la carte quand on clique « modifier » (un état `enEdition` qui retient l'id en cours d'édition), et un bouton « Valider ». Plus joli, mais commence par la version `prompt`.
**✅ Test :** renomme un jeu → F5 → le nouveau nom est **conservé** (et la page Détail l'affiche aussi).
---
## 🧯 Le bouton « réinitialiser » (très utile pour debugger)
Quand on bricole `localStorage`, on aime pouvoir tout remettre à zéro. Un petit bouton :
```jsx
<button onClick={() => { localStorage.removeItem("jeux"); location.reload(); }}>
Réinitialiser les jeux
</button>
```
Il efface la clé `jeux` → au rechargement, `localStorage` est vide → on repart du `zelda.json` (retour à l'étape 2).
---
## ✅ Checklist
- [ ] 1-4 — `jeux` initialisé depuis `localStorage` (sentinelle `null`), fetch **une seule fois**, sauvegarde à chaque changement, écran « Chargement… »
- [ ] 5 — ajout persistant (rien à changer, ça marche tout seul)
- [ ] 6 — suppression (`filter` + bouton sur la carte)
- [ ] 7 — modification (`map` + `prompt`, puis version `<input>` en bonus)
- [ ] bonus — bouton « réinitialiser »
## 🧠 Concepts
Persistance d'une **liste entière** dans `localStorage` · réconcilier **fetch (1re fois)** vs **localStorage (ensuite)** avec une sentinelle `null` · `filter` (supprimer) et `map` (modifier) **immuables** · une fonction d'`App` passée en props jusqu'à `Carte` pour agir sur la source de vérité.
> 🔁 Et si tu veux ranger tout ça proprement : le **custom hook `useLocalStorage`** de l'énoncé final fait exactement ce schéma « état + sauvegarde auto ». Une fois la persistance comprise ici, tu pourras remplacer tes blocs `useState`/`useEffect` par `const [jeux, setJeux] = useLocalStorage("jeux", null)`.