Compare commits

..
Author SHA1 Message Date
Stage2 520c418df3 added inverse search 2026-06-26 17:14:18 +02:00
Stage2 dcb81e0b55 notFound page added 2026-06-26 16:51:49 +02:00
Stage2 642df97f7a small details added 2026-06-26 16:51:35 +02:00
matthieu 8a52c92449 Contact: correction total likes a la suppression (sync jeux/likes) + explication reduce + champ console/plateforme 2026-06-26 16:10:02 +02:00
Stage2 6a99a2d6eb addded persistent games but totallikes behave weirdly 2026-06-26 15:49:13 +02:00
Stage2 8ee1eb1246 fixed bugs 2026-06-26 15:14:38 +02:00
matthieu b7e6fa844b Contact: correction doublon likes dans Carte + enonce guide persistance des jeux (ajout/suppression/modif au refresh) 2026-06-26 14:48:50 +02:00
Stage2 85d30252f3 added total like and game number 2026-06-26 14:37:54 +02:00
Stage2 f6da867500 added some changes in Detail and accueil, added space between 2026-06-26 14:21:46 +02:00
matthieu e939e2c0ef Ajout miammiam.txt 2026-06-26 10:31:50 +02:00
matthieu 8b54e57bc4 Enonce final: plus d'exercices (versus, filtre type, stats, 404, tri inverse, custom hook) + ton allege 2026-06-26 10:03:51 +02:00
matthieu c98afaf171 Correction page detail + enonce final fun (boss de fin: detail complet, random, hall of fame, dark mode) 2026-06-26 09:33:42 +02:00
Stage2 8a66266a05 fixed bug annd linked app, accueil and detail together 2026-06-25 16:04:09 +02:00
matthieu ef268afd15 Correction contact: relier les pages a App (importer Accueil/Detail, App=Header+Routes+Footer, supprimer le contenu inline en double) + nettoyage Accueil (Detail en double, console:plateforme) 2026-06-25 15:52:23 +02:00
Stage2 e0fd5872cb created pages accueil and detail but didn't link them to app 2026-06-25 15:42:38 +02:00
matthieu 1981386cb7 Correction contact router: un seul render (StrictMode>BrowserRouter>App), creer pages Accueil/Detail (Accueil is not defined), App=Header+Routes+Footer, fix console:plateforme 2026-06-25 14:51:41 +02:00
Stage2 e38b05b5a5 tried t use react router but page and accueuil aren't defined 2026-06-25 14:46:54 +02:00
Stage2 d78d4c1468 fixed bugs and installed react-router-dom 2026-06-25 14:34:56 +02:00
matthieu eb52dfb63f Correction contact: variable d'etat nommee console (mot reserve), prop BarreRecherche value->valeur, et le tri (filtrer puis trier, afficher affiches) 2026-06-25 14:05:05 +02:00
Stage2 bcf4c96e3b tried adding the tri in form but doesnt work 2026-06-25 11:55:05 +02:00
matthieu ddd62a2300 Contact: correction BarreRecherche (composant a props, hooks dans le composant, fragment) + Badge (passer Type App->Carte->Badge) + enonce React #3 (formulaire, tri, React Router) 2026-06-25 09:13:00 +02:00
Stage2 67d8934477 started creating BarreRecherche.jsx but unfinished 2026-06-24 16:51:57 +02:00
Stage2 68a9645930 added "jeux" in json and fixed small bug 2026-06-24 16:06:57 +02:00
Stage2 fce6355113 fixed two of tree bugs (the ones about the likes buttons) 2026-06-24 15:28:10 +02:00
matthieu f74f9a0100 Tuto contact: faire remonter les likes (l'enfant doit APPELER props.onLike) + explication de l'initialisation useState (pourquoi meme valeur de depart) 2026-06-24 14:47:13 +02:00
Stage2 6be5ca583b added like that continue even with reload but buggy 2026-06-24 14:38:08 +02:00
Stage2 dab2650776 details on like total (still doesn't work) 2026-06-24 11:58:38 +02:00
Stage2 b5906c3f92 trying to understand how to add a like total that works 2026-06-24 11:41:52 +02:00
Stage2 82fe5d7085 also created baddges.jsx 2026-06-24 11:13:15 +02:00
Stage2 9bb89e8280 added likes and search bar but badges don't work 2026-06-24 11:12:40 +02:00
matthieu bdd8316838 Enonce React #2 contact (Zelda): nettoyage template + fix Docs.jsx, map/key, conditionnel, etat local, champ controle, lifting state up, useEffect, fetch en React, re-decoupage 2026-06-24 09:24:17 +02:00
Stage2 9b91627ed4 finished most of tuto 2026-06-23 16:53:02 +02:00
Stage2 86933e15c0 added footer and header 2026-06-23 16:05:51 +02:00
matthieu b43c9989f4 Tuto React: expliquer pourquoi App n'affiche ni Header ni Footer (majuscule = composant vs balise HTML, pas d'erreur), + verifs import/export 2026-06-23 15:50:25 +02:00
Stage2 e8448d229c base of react project 2026-06-23 15:19:01 +02:00
Stage2 9b04253827 fixed the map thing 2026-06-23 14:35:28 +02:00
matthieu 17d969efe4 Tuto React depuis zero: install Node/npm, Vite, structure, JSX, decoupage en composants (1 composant = 1 fichier), props, useState 2026-06-23 14:30:40 +02:00
matthieu 77f564afdb Contact: restaurer section Ctrl+Entree du fichier alerte + correction map (filter/map confondus, variables inexistantes, scope, soustraction) 2026-06-23 14:09:24 +02:00
Stage2 64cb4a206e some code with map it doesnt work 2026-06-23 12:06:58 +02:00
matthieu 0ea04988a3 Correction contact: alerte form marche en file:// pas localhost (preventDefault manquant + form qui poste, 501 du serveur) + explication file:// vs http://localhost 2026-06-23 11:10:21 +02:00
Stage2 0e43342e59 added keyboard shortcuts but they don't work on localhost page 2026-06-23 11:05:14 +02:00
Stage2 6150a7c045 added time to fotbar and badge now auto refresh 2026-06-23 10:47:08 +02:00
Stage2 74bc44a09c changed flotte tableau but it doesn't show up sometimes 2026-06-23 09:51:23 +02:00
matthieu 0b47b53218 Correction contact API flotte: id corps_flotte inexistant, deux versions qui se telescopent, brancher fetch sur afficherFlotte 2026-06-23 09:21:58 +02:00
Stage2 e0589bf965 added tableau flotte and other stuff in flotte.js 2026-06-22 16:50:28 +02:00
Stage2 608cceec18 fixed small bugs, added a table to show the flotte 2026-06-22 16:40:20 +02:00
matthieu fe90e2d766 Correction contact: script mal ferme (flotte.js), 200 serveur vs reponse navigateur, chemin fetch, bouton Effacer type submit, label for/id 2026-06-22 15:51:03 +02:00
Stage2 f250c06331 added flotte.js and linked it to contact.html but doesnt work 2026-06-22 15:42:37 +02:00
Stage2 14286db2e0 added flotte.js but doesn't show up on console 2026-06-22 15:41:42 +02:00
Stage2 47ec3842e3 aded json page and minor other changes 2026-06-22 15:24:54 +02:00
Stage2 4c8ade92a2 added numbers of caracters when sending a message 2026-06-22 14:39:12 +02:00
matthieu 56d9d1e31c Explication form: role du form, name, action (OU) et method GET/POST (exemple contact/GET) 2026-06-22 14:38:02 +02:00
Stage2 78768552fb corrections de problemes 2026-06-22 14:13:59 +02:00
matthieuandClaude Opus 4.8 d6e77de62c Correction filtre/tri: afficherEquipe ignore son argument (parametre manquant + boucle sur la globale)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 11:52:09 +02:00
Stage2 724e8b15f3 same problem as last commit, added function afficherEquipe 2026-06-22 11:30:41 +02:00
Stage2 d4fb8696dc constructed table without innerHTML but search and sort dont work 2026-06-22 10:32:53 +02:00
matthieuandClaude Opus 4.8 438e567b31 Solutions niveau 5 contact (filet de securite, a consulter en dernier recours) + FAQ erreurs frequentes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:43:39 +02:00
matthieuandClaude Opus 4.8 237cd9817f Niveau 5 contact (mode indices): map/find/reduce, delegation+debounce, modules, API en ligne, FormData
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:29:55 +02:00
matthieuandClaude Opus 4.8 f42eb51b41 Tuto JS niveau 4 contact: createElement/append, fetch+async/await+JSON, setInterval, evenements clavier
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 16:24:05 +02:00
matthieuandClaude Opus 4.8 c2979534f1 Correction: prenom dans l'alerte — expliquer les 2 bugs (.value manquant + lecture trop tot)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 16:20:25 +02:00
Stage2 849671b542 tried to answer with person name, doesn't work 2026-06-19 16:15:06 +02:00
Stage2 b9928180a9 added javascript for the table on contact page 2026-06-19 15:33:36 +02:00
Stage2 b3507bc30f Added even more JavaScript stuff: FAQ, dark mode and a navbar 2026-06-19 15:30:25 +02:00
matthieuandClaude Opus 4.8 75a2305553 Tuto JS niveau 3 contact: tableaux, boucles, recherche/tri, localStorage, classList, FAQ, fonctions parametrees
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 14:06:05 +02:00
Stage2 69eff4006a added JavaScript Stuff 2026-06-19 11:50:50 +02:00
matthieuandClaude Opus 4.8 ddfd17f9af Guide contact niveau 2: lecon conditions (if/else, singulier/pluriel) + exos JS (submit, fonction, Date)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 16:41:48 +02:00
Stage2 c4a5e19dc8 added footer to contact page without usingg Javascript 2026-06-18 15:40:46 +02:00
Stage2 b49621c31d added colorson contact page 2026-06-18 15:37:21 +02:00
Stage2 2b4d4cfa0b removing 2 copied lines 2026-06-18 15:12:39 +02:00
Stage2 098a26b77f form letter count added with some JavaScript, small corrections 2026-06-18 15:11:28 +02:00
matthieuandClaude Opus 4.8 075d626ebb Guide contact: relecture form, redimensionnement borne textarea (min/max), centrage margin auto
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 11:44:34 +02:00
Stage2 965efdb0cb continued form in contact page 2026-06-18 10:57:31 +02:00
Stage2 593e363c52 contact page start 2026-06-17 16:58:30 +02:00
Stage2 c8f378970b shadow on buttons 2026-06-17 14:46:03 +02:00
83 changed files with 4513 additions and 3303 deletions

No files matched your search

-82
View File
@@ -1,82 +0,0 @@
# 👍 Bien joué + 3 améliorations + la suite
Le `localStorage` est **réglé** : tu lis bien la valeur au chargement, tu la convertis en nombre (`parseInt`) et tu rafraîchis l'affichage avec `mettreAJourTOTAL()`. Recharge avec 12 véhicules → il affiche 12. C'était l'objectif, c'est fait. 👏
Voici 3 petites améliorations (rien de cassé, juste plus propre/correct), puis ce qu'il te reste à faire.
---
## 1. 🧹 N'avoir **qu'une seule** fonction d'affichage
Tu utilises `mettreAJourTOTAL()` dans le bouton `+` (parfait), mais le bouton `−` utilise encore l'ancienne paire :
```js
(mettreAJourGPS(), mettreAJourCONSO()); // ← dans boutonMoins
```
`mettreAJourTOTAL` fait **déjà** tout ce que font ces deux fonctions réunies. Donc :
- dans `boutonMoins`, remplace `(mettreAJourGPS(), mettreAJourCONSO());` par `mettreAJourTOTAL();`
- supprime `mettreAJourGPS` et `mettreAJourCONSO`, devenues inutiles.
> 💡 **À retenir :** une seule fonction d'affichage = un seul endroit à corriger le jour où tu changes l'affichage. C'est exactement le ménage qu'on avait commencé : finis-le, ton fichier sera plus court et plus sûr.
---
## 2. 🐛 Le mot « véhicule(s) » est faux après un rechargement
Au chargement, `mettreAJourTOTAL()` met bien à jour le **nombre**, mais **pas** le mot. Donc si tu recharges avec 12, tu lis « **12 véhicule** » (singulier, faux), parce que la mise à jour du mot n'a lieu que dans les clics `+` / `−`.
La bonne approche : mettre la logique singulier/pluriel **dans** `mettreAJourTOTAL`, pour qu'elle soit toujours correcte (au chargement **et** aux clics).
```js
function mettreAJourTOTAL() {
affichageNb.textContent = nbVehicules;
affichageGéo.textContent = nbVehicules * PRIX_GPS;
affichageConso.textContent = nbVehicules * PRIX_CONSOMMATION;
mot_vehicules.textContent = nbVehicules === 1 ? "véhicule" : "véhicules"; // ← ici
}
```
Du coup tu peux **enlever** les `if (nbVehicules === 1) ... else ...` qui sont en double dans tes deux boutons : ils sont maintenant gérés au même endroit.
> 💡 **Même idée que le point 1 :** tout ce qui concerne « afficher l'état du devis » vit dans **une** fonction. Tu l'appelles, et tout est juste — nombre, totaux, et mot.
---
## 3. 🎯 La classe avec une virgule (pas encore corrigée)
Dans `services.html` :
```html
<section class="simulateur, padding_left"> <!-- ❌ -->
```
Rappel : en HTML, les classes se séparent par un **espace**, pas une virgule (la virgule, c'est pour les sélecteurs **CSS**). Là, ta section a une classe qui s'appelle `simulateur,` (avec la virgule) → une règle `.simulateur` ne s'y appliquera jamais.
```html
<section class="simulateur padding_left"> <!-- ✅ -->
```
---
## 🚀 La suite : il te reste 4 exos dans ton rattrapage
Tu as fait : conditions ✅, compteur ✅, validation envoi ✅, tableau de services ✅, localStorage ✅. Il reste (dans `PROCHAINE_ETAPE_services_2.md`, toujours en mode indices) :
- **Exo 8 — Mode sombre** (`classList.toggle`)
- **Exo 9 — Horloge** dans le pied de page (`setInterval`)
- **Exo 10 — `fetch`** d'un `vehicules.json` (avec le mini-serveur `python3 -m http.server`)
- **Exo 11 — Navbar en composant réutilisable** (une fonction qui renvoie le HTML + `insertAdjacentHTML`)
> 🔎 Pour l'exo 10, Mélissa vient de faire le même sur sa page : si tu bloques, le concept est de **récupérer** les données avec `fetch`/`await` puis de les **afficher** avec une boucle (comme ton tableau de services, mais à partir du fichier au lieu du tableau écrit en dur).
Quand tu auras fait ces 4-là, tu seras au même niveau que la page contact. Et si un point coince, le filet `SOLUTIONS_services_2.md` est là — mais cherche d'abord. 🙂
---
## ✅ Récap
- [ ] Une seule fonction d'affichage (`mettreAJourTOTAL` partout, supprimer les 2 autres)
- [ ] Logique singulier/pluriel **dans** `mettreAJourTOTAL` (correct au chargement aussi)
- [ ] `class="simulateur padding_left"` (espace, pas virgule)
- [ ] Continuer : mode sombre, horloge, fetch, navbar (exos 8 → 11)
+48 -49
View File
@@ -1,75 +1,74 @@
# 🔧 Correction — page d'accueil blanche : il manque le `return`
# 🔧 Correction — relier tes pages à `App` (et virer le doublon)
Ton routeur est **bien configuré** maintenant (un seul `render`, `BrowserRouter` dans `main.jsx`, `Routes` dans `App.jsx`). Mais la page `/` est **vide**. La raison est dans `pages/Accueil.jsx`, et c'est une erreur très classique.
Bonne nouvelle : tes pages `pages/Accueil.jsx` et `pages/Detail.jsx` sont **bien faites** 👏 (tout ton contenu est dans `Accueil`). Le souci est uniquement dans **`App.jsx`** : tu n'as pas **branché** ces pages, et tu as gardé **tout l'ancien contenu en double** dedans.
---
## 🔍 Le bug : ton composant ne **renvoie** rien
## 🔍 Les 2 problèmes dans `App.jsx`
Regarde la fin de `Accueil` :
1. Tu utilises `<Accueil />` dans une `<Route>`, mais tu **ne l'importes pas** → `Accueil is not defined`.
2. Tu as **recopié** toute la liste/recherche/formulaire **dans `App`** (en plus de l'avoir dans `pages/Accueil.jsx`). Du coup ce contenu s'affiche **hors des routes**, et tes pages ne servent à rien.
```jsx
function Accueil() {
// ... tes useState, ajouter, liker, total ...
<div className="body"> // ❌ ce JSX n'est PAS renvoyé
<Header />
...
<Footer />
</div>; // ← juste une instruction, suivie d'un point-virgule
}
```
Tu as écrit tout ton JSX… mais **sans `return` devant**. Du coup, JavaScript **calcule** ce `<div>` puis le **jette** à la poubelle, et la fonction `Accueil` ne renvoie **rien** (`undefined`). React affiche donc… **rien** → page blanche.
> 🧠 **À retenir :** un composant React **doit `return`** le JSX qu'il veut afficher. Sans `return`, le composant ne montre rien. C'est différent d'un `if (...) return <p>Chargement…</p>` que tu fais plus haut : là tu renvoies bien quelque chose.
> 🧠 **L'idée du routeur :** `App` ne doit **plus** contenir le contenu des pages. Son rôle : **assembler** (Header/Footer) et **router** (« telle URL → telle page »). Le contenu vit **dans les pages** (`Accueil`, `Detail`).
---
## ✅ La correction : ajouter `return (` … `)`
## ✅ La correction : remplace TOUT ton `App.jsx` par ça
```jsx
function Accueil() {
// ... tes useState, ajouter, liker, total ...
import { Routes, Route } from "react-router-dom";
import Header from "./components/Header";
import Footer from "./components/Footer";
import Accueil from "./pages/Accueil"; // 👈 on IMPORTE la page
import Detail from "./pages/Detail"; // 👈 et celle-ci aussi
import "./App.css";
return ( // 👈 AJOUTE "return ("
<div className="body">
function App() {
return (
<>
<Header />
<select value={tri} onChange={(e) => setTri(e.target.value)}>
<option value="nom">Nom (A→Z)</option>
<option value="likes">Plus likées</option>
</select>
<form onSubmit={ajouter}>
{/* ... tes inputs + bouton Ajouter ... */}
</form>
<input type="search" placeholder="Rechercher une trilogie…"
value={recherche} onChange={(e) => setRecherche(e.target.value)} />
<p>👍 Total de likes : {total}</p>
{affichees.length === 0 && <p>Aucune trilogie trouvée</p>}
{affichees.map((trilo) => (
<Carte key={trilo.id} titre={trilo.nom} niveaux={trilo.niveaux}
likes={likes[trilo.id] || 0} onLike={() => liker(trilo.id)} />
))}
<Routes>
<Route path="/" element={<Accueil />} />
<Route path="/jeu/:id" element={<Detail />} />
</Routes>
<Footer />
</div>
); // 👈 ET la parenthèse fermante
</>
);
}
export default App;
```
**✅ Test :** va sur `/` → ta page d'accueil (cartes + recherche + tri + formulaire + total) **s'affiche** enfin.
C'est **tout** : plus aucun `useState`, plus de `fetch`, plus de formulaire dans `App`. Tout ça vit déjà dans `pages/Accueil.jsx`.
> 🔎 `Header` et `Footer` sont **autour** des `<Routes>` → ils s'affichent sur **toutes** les pages. Seul ce qui est **dans** `<Routes>` change selon l'URL.
**✅ Test :** va sur `/` → ta page d'accueil (la liste qu'il y avait avant) **s'affiche**. Va sur `/jeu/2` → la page Détail.
---
## 🧹 Pendant que tu y es (optionnel)
## 🧹 Deux petits restes à nettoyer dans `pages/Accueil.jsx`
- Enlève le `console.log("test")` au début de `Accueil` (c'était pour déboguer).
- Le `<>…</>` autour du **seul** `<select>` ne sert à rien (le fragment ne sert que pour grouper **plusieurs** éléments) → tu peux l'enlever.
1. Tu as une **fonction `Detail` en double**, coincée **dans** `Accueil` :
```jsx
function Detail() {
const { id } = useParams(); // ❌ useParams n'est même pas importé ici
return <h1>Détails du jeu {id}</h1>;
}
```
Elle ne sert à rien (le vrai `Detail` est dans `pages/Detail.jsx`) → **supprime-la** de `Accueil`.
2. Dans ton objet `nouveau`, il reste `console,` (l'ancien nom). Tu as renommé l'état en `plateforme`, donc écris :
```jsx
const nouveau = { id: Date.now(), titre, annee: Number(annee), console: plateforme, Type: "3d" };
```
(sinon tu ranges l'objet global `console`, pas la plateforme tapée.)
---
## ✅ Récap
- [ ] Mettre `return (` devant `<div className="body">` et `)` après `</div>`
- [ ] Vérifier que `/` affiche bien ta liste
- [ ] (Optionnel) virer le `console.log` et le fragment inutile
- [ ] Remplacer `App.jsx` par la version courte (importe `Accueil` + `Detail`, `Header` + `<Routes>` + `Footer`)
- [ ] Supprimer tout l'ancien contenu inline dans `App`
- [ ] Dans `Accueil.jsx` : supprimer la fonction `Detail` en double + écrire `console: plateforme`
> 💬 La phrase à garder : **pas de `return`, pas d'affichage.** Un composant qui « calcule » du JSX sans le `return`er ne montre rien.
> 💬 La phrase à garder : **`App` assemble et route ; les pages contiennent le contenu.** Un composant qu'on affiche (`<Accueil/>`) doit être **importé**.
+90
View File
@@ -0,0 +1,90 @@
# 🔧 Pourquoi l'alerte « marche » en `file://` mais pas en `localhost`
Excellente observation 👀 — et ce n'est pas un hasard. La réponse t'apprend une vraie notion.
---
## 🎯 La cause : ton formulaire **s'envoie vraiment**
Regarde le `else` de ton `submit` :
```js
formulaire.addEventListener("submit", function (evenement) {
if (zoneMessage.value.trim() === "") {
evenement.preventDefault(); // ✅ ici tu annules l'envoi
alert("Merci d'écrire un message avant d'envoyer !");
} else {
const greeting = `merci ${nom.value}, ...`;
alert(`${greeting}`); // ❌ mais ici, PAS de preventDefault
}
});
```
Dans le cas « message rempli » (le `else`), tu affiches l'alerte… mais tu **n'appelles pas** `evenement.preventDefault()`. Donc, après l'alerte, le navigateur fait ce qu'un `<form>` fait par défaut : il **envoie le formulaire** (ton form est en `method="post" action=""`, donc il poste vers la page elle-même).
Et c'est cet envoi qui se comporte différemment selon comment tu ouvres la page 👇
---
## 🆚 Pourquoi la différence `file://` vs `localhost`
| | Double-clic (`file://`) | Serveur (`http://localhost:...`) |
|---|---|---|
| Y a-t-il un serveur pour recevoir le POST ? | **Non** | **Oui** (celui que tu as lancé) |
| Que devient l'envoi du formulaire ? | bloqué / ne mène nulle part | le serveur **reçoit** le POST et **répond** |
| Ce que tu vois après l'alerte | rien ne bouge → « ça marche » | la page part sur une **page d'erreur** → « ça ne marche pas » |
- En **`file://`**, il n'y a **aucun serveur**. Un `<form>` ne peut pas « poster » vers un simple fichier → l'envoi est ignoré. Du coup, après ton alerte, **rien ne se passe** : tout a l'air normal.
- En **`http://localhost`**, il y a un **vrai serveur** (celui que tu as démarré). Il **reçoit** ton POST… mais le petit serveur de dev ne sait pas traiter un POST → il renvoie une erreur (souvent **501 « Unsupported method »**), et le navigateur **t'emmène sur cette page d'erreur**, juste après l'alerte. D'où l'impression que « l'alerte ne marche pas ».
> 🔎 En réalité, ton alerte **s'affiche dans les deux cas** ! La différence, c'est ce qui se passe **juste après** : sur localhost, la page s'en va sur l'erreur du serveur, donc tu ne profites pas du résultat.
---
## ✅ La correction
Comme tu n'as **pas de vrai serveur** pour traiter l'envoi (et ce n'est pas le but ici), tu dois **empêcher l'envoi par défaut dans tous les cas**, pas seulement quand le message est vide.
Deux façons :
- soit ajouter `evenement.preventDefault();` **aussi** dans le `else`,
- soit (plus propre) l'appeler **une seule fois, tout en haut** du gestionnaire, avant le `if`. Comme ça, l'envoi est toujours annulé, et c'est ton JavaScript qui décide quoi faire (afficher l'alerte).
**✅ Test :** après correction, tu remplis le message, tu cliques « Envoyer » → l'alerte s'affiche **et la page ne bouge plus**, que tu sois en `file://` ou en `localhost`. Comportement identique partout.
> 💡 **À retenir :** tant qu'il n'y a pas de serveur qui gère vraiment l'envoi, on met **`preventDefault()`** pour garder la main et tout faire en JS. Se reposer sur « de toute façon en `file://` ça ne part pas » est trompeur : dès qu'un serveur est là (localhost, et plus tard en ligne), le comportement change.
---
## ⌨️ Et ton raccourci clavier Ctrl + Entrée ? (même cause)
Ton code clavier est **correct**, bonne nouvelle :
```js
zoneMessage.addEventListener("keydown", function (event) {
if (event.key === "Enter" && event.ctrlKey) {
formulaire.requestSubmit(); // ✅ bon choix
}
...
});
```
> 👍 Tu as bien utilisé **`requestSubmit()`** et pas `submit()`. La différence est importante : `requestSubmit()` **déclenche ton gestionnaire `submit`** (donc ta validation et — une fois corrigé — ton `preventDefault`), alors que `form.submit()` l'aurait **court-circuité** et aurait posté direct. Tu as pris le bon.
Du coup, `Ctrl + Entrée` passe **exactement par le même chemin** que le clic sur « Envoyer » : il déclenche l'événement `submit` → ton gestionnaire tourne → et comme le `else` n'annule pas l'envoi, le formulaire **poste** pour de vrai → en `file://` rien ne se passe (« ça marche »), en `localhost` le serveur répond **501** et tu pars sur la page d'erreur (« ça ne marche pas »).
👉 **Donc il n'y a pas de bug séparé pour le clavier** : c'est le même `preventDefault()` manquant. Dès que tu l'ajoutes (point ci-dessus), **le clic ET le Ctrl + Entrée** marcheront pareil, en `file://` comme en `localhost`.
> 🔎 Et la **501** que tu vois ? C'est normal et attendu : le petit serveur `http.server` ne sait qu'**envoyer des fichiers** (méthode GET). Il ne sait pas **traiter** un envoi de formulaire (méthode POST) → il répond « 501 Unsupported method ». Ce n'est pas à toi de « réparer » le serveur : le bon réflexe, c'est de **ne pas poster du tout** (ton `preventDefault`), puisqu'il n'y a pas de programme derrière pour recevoir les données.
---
## 📝 Bonus — bien comprendre `file://` vs `http://localhost`
C'est **deux façons différentes** d'ouvrir la même page. Regarde ta **barre d'adresse**, elle te dit dans quel mode tu es.
- **`file:///.../contact.html`** → **aucun serveur**. Le navigateur **lit le fichier directement sur ton disque**, comme une photo ou un PDF. Pas d'adresse web, pas d'« origine ». C'est pour ça que `fetch` y est bloqué (« CORS policy ») et qu'un `<form>` n'a personne à qui poster.
- **`http://localhost:8000/...`** → tu as lancé un **petit serveur** qui **publie** ton dossier et **répond aux demandes**, comme un vrai site, mais sur ta machine. Là il y a une vraie adresse → `fetch` fonctionne, et un POST de formulaire est bel et bien reçu (d'où l'erreur 501 plus haut, faute de code pour le traiter).
Décodage de l'adresse : **`localhost`** = ta propre machine ; **`8000`** (ou `5500`, `3000`…) = le **port**, le « numéro de porte » où ton serveur écoute.
> 🧭 L'image à garder : `file://` = lire un papier posé sur ton bureau. `http://localhost:...` = un mini-site web monté chez toi, que tu visites comme n'importe quel site. Les vraies fonctionnalités du web (`fetch`, et le traitement d'un `<form>`) ont besoin de la **deuxième** situation — ce qui explique pourquoi ton comportement change d'un mode à l'autre.
+72
View File
@@ -0,0 +1,72 @@
# 🔧 Correction — l'API affiche « Impossible de charger la flotte 😕 »
Tu y es presque ! Ton `fetch` fonctionne (le fichier est bien servi, statut 200), et tu as écrit du **très bon** code propre (`createLign`, `afficherFlotte`). Le souci, c'est qu'il y a **deux versions** dans ton `flotte.js` qui ne se parlent pas, et **un id qui ne correspond pas**. On démêle.
---
## 1. 🐛 Pourquoi le message d'erreur s'affiche
Dans `chargerFlotte`, tu vises ce `tbody` :
```js
const corps = document.getElementById("corps_flotte"); // ❌ cet id n'existe pas
```
Mais dans ton HTML, ton tableau de flotte a pour id **`flotte_tableau`** (pas `corps_flotte`). Donc `corps` vaut **`null`**, et un peu plus bas `corps.append(tr)` plante (« on ne peut pas faire `.append` sur `null` »).
Comme ce `.append` est dans ton `try`, l'erreur est **attrapée** par le `catch` → qui affiche « Impossible de charger la flotte 😕 ».
> 🔎 **Donc ton API n'est PAS en cause :** le fichier est bien chargé et transformé (`vehicules` contient tes 3 véhicules). C'est juste qu'au moment de les **afficher**, tu cherches un élément qui n'existe pas. Pour le voir toi-même : dans **F12 → Console**, tu verras l'erreur exacte (`Cannot read properties of null (append)`) loggée par ton `console.error(erreur)`.
---
## 2. 🧩 Le vrai souci : tu as écrit **deux fois** l'affichage
Regarde ton `flotte.js`, tu as en réalité **deux mécanismes** :
**Version A — le `fetch`** (les vraies données du fichier) :
```js
async function chargerFlotte() {
... fetch ...
for (const v of vehicules) { corps.append(tr); } // vise corps_flotte (inexistant)
}
```
**Version B — une copie écrite à la main** (`flotte1`) avec tes belles fonctions :
```js
const flotte1 = [ { modele: "Renault Kangoo", ... }, ... ]; // ← une COPIE en dur
function createLign(car) { ... }
function afficherFlotte(list) { ... } // vise flotte_tableau (qui existe ✅)
afficherFlotte(flotte1);
```
Résultat : ce que tu **vois** à l'écran, c'est la **version B** (la copie `flotte1`), pas les données venues du fichier. Et la version A échoue dans son coin. Le `fetch` ne sert donc à rien pour l'instant : tu affiches une copie, pas le vrai fichier.
> 💡 Le but de l'exercice `fetch`, c'est justement de **ne plus** écrire les données en dur (`flotte1`) mais de les **charger depuis `vehicules.json`**. Sinon, autant tout écrire dans le HTML.
---
## 3. ✅ La solution : **brancher** ton fetch sur tes belles fonctions
Tu as déjà tout : `createLign` et `afficherFlotte` sont parfaites et visent le **bon** tableau (`flotte_tableau`). Il suffit que `chargerFlotte` **leur passe** les données qu'il a récupérées, au lieu de réécrire une boucle dans son coin.
L'idée (à toi de l'assembler) :
1. Dans `chargerFlotte`, après `const vehicules = await reponse.json();`, **appelle `afficherFlotte(vehicules);`** → tes vraies données passent dans ta fonction d'affichage, qui vise le bon `tbody`.
2. **Supprime** la boucle `for (const v of vehicules) { ... corps.append ... }` de `chargerFlotte` (c'est `afficherFlotte` qui fait ce travail maintenant) — et donc la variable `corps` au mauvais id disparaît avec.
3. **Supprime `flotte1`** et le `afficherFlotte(flotte1)` final : tu n'as plus besoin de la copie en dur, c'est le fichier qui fournit les données.
4. Tu peux garder le `message.textContent = vehicules.length + " véhicules :"` pour le petit compteur.
> 🔎 En clair : `chargerFlotte` **récupère** les données, `afficherFlotte` **les affiche**. Chacun son rôle, et ils se passent le relais avec `afficherFlotte(vehicules)`. C'est exactement le même duo que sur ton tableau d'équipe (`afficherEquipe(liste)`).
**✅ Test :** le message « Impossible de charger » disparaît, et le tableau affiche les véhicules **venus de `vehicules.json`**. Pour le prouver : ajoute un 4ᵉ véhicule **dans le fichier `vehicules.json`** (pas dans le JS), recharge → la 4ᵉ ligne apparaît. Si oui, c'est gagné : tu affiches bien le fichier, plus une copie. 🎉
---
## ✅ Récap
- [ ] Comprendre : l'erreur venait de l'**id** `corps_flotte` (inexistant), pas de l'API
- [ ] Faire `chargerFlotte` → `afficherFlotte(vehicules)` (brancher le fetch sur ta fonction d'affichage)
- [ ] Supprimer la copie en dur `flotte1` et la boucle en double
- [ ] Vérifier en ajoutant une ligne **dans le JSON**
Honnêtement tu avais déjà écrit 90 % du bon code — il ne manquait que de **relier les deux morceaux**. C'est tout l'enjeu quand un fichier grossit : éviter d'avoir deux fois la même chose qui se marche dessus.
+116
View File
@@ -0,0 +1,116 @@
# 🔧 Correction — `BarreRecherche.jsx` (+ le Badge)
Super boulot : ton app **marche** (likes par carte, total qui monte, données chargées via `fetch`, persistance `localStorage`). 👏 Il te reste à **bien découper** (Module H) et à réparer le Badge. On y va.
---
## 🐛 Pourquoi `BarreRecherche.jsx` casse tout
Ton fichier actuel :
```jsx
const [jeux, setJeux] = useState([]); // ❌ hook AU NIVEAU DU FICHIER
const [recherche, setRecherche] = useState(""); // ❌ idem
useEffect(() => { ... }, [jeux]); // ❌ idem
function BarreRecherche() {
return (
<input ... />
{filtres.length === 0 && <p>...</p>} // ❌ plusieurs éléments sans <> </>
{filtres.map(...)}
);
}
```
Trois erreurs :
1. **Les hooks (`useState`, `useEffect`) doivent être DANS le composant**, pas au niveau du fichier. Hors d'une fonction composant, ils n'ont aucun sens → erreur.
2. **Un composant doit renvoyer UN seul élément.** Là, tu renvoies un `<input>` **et** des `<p>`/`<Carte>` côte à côte → erreur JSX. (On enveloppe avec `<> … </>` quand il y en a plusieurs.)
3. Tu essaies de remettre **tout** (état, liste, total) dans `BarreRecherche` — alors que ça marche déjà très bien dans `App`.
> 🧠 **L'erreur de raisonnement :** tu veux « sortir » du code dans un composant, mais tu emmènes l'**état** avec. Or l'état doit **rester dans `App`** (c'est lui qui orchestre). Un petit composant comme `BarreRecherche` doit être **« bête »** : il reçoit ce dont il a besoin en **props**, et ne décide de rien.
---
## ✅ La bonne version : un composant qui reçoit des **props**
`BarreRecherche` n'a besoin que de **deux choses** : la valeur à afficher, et quoi faire quand on tape. On les lui passe en props.
### `BarreRecherche.jsx`
```jsx
function BarreRecherche({ valeur, onChange }) {
return (
<input
type="search"
placeholder="Search Game…"
value={valeur}
onChange={onChange}
/>
);
}
export default BarreRecherche;
```
> 🔎 Pas de `useState` ici, pas de liste, pas de total. Juste un input qui **affiche** `valeur` et **remonte** les frappes via `onChange`. C'est tout son travail.
### Dans `App.jsx` : l'état RESTE ici, on passe les props
```jsx
import BarreRecherche from "./components/BarreRecherche";
// ... dans le return, à la place de ton <input> :
<BarreRecherche
valeur={recherche}
onChange={(e) => setRecherche(e.target.value)}
/>
```
> 🧩 **À retenir :** l'état (`recherche`, `total`, `jeux`…) vit dans `App`. Les petits composants reçoivent des **props** (des données + des fonctions) et restent **simples et réutilisables**. C'est exactement la même idée que `Carte` qui reçoit `onLike`.
**✅ Test :** la recherche fonctionne **exactement comme avant**, mais l'`<input>` est maintenant dans son propre composant propre.
> 💡 Tu peux faire pareil pour la **liste** : un composant `Liste` qui reçoit le tableau filtré et fait le `.map()`. `App` ne ferait plus qu'**assembler** `<BarreRecherche/>`, `<Liste/>`, `<Footer/>`.
---
## 🎖️ Le Badge qui s'affiche partout (« Jeu 3D » sur tout)
Ton `Badge` attend une prop `Type` :
```jsx
function Badge({ Type }) {
if (Type === "2d") return null;
return <span className="badge"> Jeu 3D </span>;
}
```
Mais dans `Carte` tu l'appelles **sans rien** : `<Badge />`. Donc `Type` vaut `undefined` → ce n'est pas `"2d"` → il affiche « Jeu 3D » **sur toutes les cartes**, même les jeux 2D.
### La correction : faire **descendre** `Type` jusqu'au Badge
Il faut passer `Type` de `App` → `Carte` → `Badge`.
1. Dans `App.jsx`, ajoute la prop à la carte :
```jsx
<Carte
key={jeu.id}
gameId={jeu.id}
titre={jeu.titre}
console={jeu.console}
annee={jeu.annee}
Type={jeu.Type} // 👈 on transmet le type
onLike={() => setTotal((t) => t + 1)}
/>
```
2. Dans `Carte.jsx`, passe-le au Badge :
```jsx
<Badge Type={props.Type} />
```
**✅ Test :** seuls les jeux **3D** affichent le badge ; les 2D (comme *Oracle of Ages*) n'ont rien.
> 🧩 **À retenir :** une prop ne « saute » pas les étages. Pour qu'un petit composant tout en bas (`Badge`) connaisse une donnée, il faut la lui **transmettre de parent en enfant** (`App` → `Carte` → `Badge`).
---
## ✅ Récap
- [ ] `BarreRecherche` = composant simple qui reçoit `valeur` + `onChange` en props (pas de hook dedans)
- [ ] L'état reste dans `App` ; on passe les props
- [ ] (Bonus) extraire aussi un composant `Liste`
- [ ] Passer `Type` jusqu'au `Badge` (`App` → `Carte` → `Badge`)
Quand c'est bon, tu peux enchaîner sur **`ENONCE_react_3_contact.md`** (la suite). 🚀
-53
View File
@@ -1,53 +0,0 @@
# 🔧 Correction — le boss (Module L) est à moitié branché
Beau boulot : `App` tient bien `trilogies` et `likes`, les passe aux routes, `Details` retrouve la trilogie et affiche ses likes, et le bouton « au hasard » marche. Il reste **un fil débranché** qui casse tout le reste.
## 🐛 Le vrai bug : `Accueil` ignore les props et garde sa propre liste
Dans `App.jsx` tu passes bien la liste :
```jsx
<Accueil trilogies={trilogies} setTrilogies={setTrilogies} likes={likes} setLikes={setLikes} />
```
Mais dans `Accueil.jsx`, regarde les deux premières lignes :
```jsx
function Accueil({ likes, setLikes }) { // ❌ trilogies / setTrilogies PAS récupérés
const [trilogies, setTrilogies] = useState(initialData); // ❌ une 2e liste, locale
```
Du coup il existe **deux listes différentes** :
- celle de `App` (que voit `Details`),
- celle de `Accueil` (sa copie locale, où vont tes ajouts).
Quand tu ajoutes une trilogie, tu remplis **la liste locale d'`Accueil`** → elle s'affiche sur l'accueil, mais **`App` n'en sait rien**. Donc `Details` (qui lit la liste d'`App`) ne la trouve pas → « Introuvable ». Pareil, le bouton « au hasard » pioche dans la liste locale et t'envoie vers un id que `Details` ne connaît pas.
> 🧩 La règle : si une donnée vient des **props**, on ne la recrée pas en `useState` à côté. Sinon la prop est **masquée** (« shadowing ») et les deux copies divergent.
## ✅ La correction (3 changements dans `Accueil.jsx`)
1. **Récupère** `trilogies` et `setTrilogies` dans les props :
```jsx
function Accueil({ trilogies, setTrilogies, likes, setLikes }) {
```
2. **Supprime** la ligne qui recrée la liste en local :
```jsx
const [trilogies, setTrilogies] = useState(initialData); // ❌ à enlever
```
3. **Supprime** l'import devenu inutile (c'est `App` qui connaît `initialData`, plus `Accueil`) :
```jsx
import { initialData } from "../data"; // ❌ à enlever
```
Tout le reste de `Accueil` (`ajouter`, `liker`, `affichees`, le `.map`) marche **sans changement** : il utilisait déjà `trilogies` / `setTrilogies`, qui viennent maintenant des props.
**Test :** ajoute une trilogie → clique « Voir les détails » dessus → **elle s'affiche** (au lieu de « Introuvable »). Le bouton « au hasard » peut aussi tomber dessus et l'afficher.
## 🧹 Pendant qu'on y est — 3 petits restes à nettoyer
1. `console.log("test")` en haut d'`Accueil` → on enlève les logs de debug.
2. Le `<button> </button>` vide juste avant le `<select>` → il ne sert à rien, à supprimer.
3. Le fragment `<>...</>` autour du seul `<select>` → inutile quand il n'y a qu'un élément, tu peux laisser le `<select>` seul.
> 💡 À retenir : une prop et un `useState` du même nom = piège classique. La source de vérité doit être **unique** — ici, dans `App`. `Accueil` ne fait que lire/écrire dedans via les props.
-115
View File
@@ -1,115 +0,0 @@
# 🔧 Correction — pourquoi ton projet ne s'affiche plus
Ton commit le dit bien : « + de bugs » 😅. Pas de panique, le diagnostic est simple : tu as **mélangé deux façons** de gérer les likes, et du coup `Carte.jsx` utilise des choses qui n'existent pas chez elle.
---
## 🔍 Ce qui cloche dans `Carte.jsx`
```jsx
import { useState } from "react";
function Carte(props) {
return (
...
<button
onClick={() => {
setLikes(likes + 1); // ❌ "setLikes" et "likes" n'existent PAS dans Carte
props.onLike();
} // ❌ il manque la ) qui ferme la fonction → erreur de syntaxe
>
❤️ {likes} // ❌ "likes" non défini
</button>
```
Deux problèmes :
1. **Erreur de syntaxe** : ta fonction `() => { ... }` n'est jamais refermée par `)`. À elle seule, ça **empêche l'app de s'afficher** (page blanche / erreur de build).
2. **`likes` et `setLikes` n'existent pas dans `Carte`** : tu as déplacé les likes dans le **parent** (`App` a bien `useState({...})`), donc `Carte` n'a plus de `useState`. Elle essaie d'utiliser des variables qui ne sont plus là.
> 🧠 **La cause de fond :** tu as commencé l'approche « **les likes vivent dans le parent** » (bien !) mais tu as gardé des bouts de l'ancienne approche « les likes dans la carte ». Il faut **choisir une seule** approche. On garde celle du parent (c'est celle de la correction précédente).
---
## ✅ La règle : `Carte` ne fait qu'**afficher** ce qu'on lui donne
Dans l'approche « parent », la carte **n'a pas d'état** : elle reçoit `likes` et `onLike` en **props**, c'est tout.
### `Carte.jsx` corrigé (remplace tout le fichier)
```jsx
function Carte(props) {
return (
<div className="carte">
<h2>{props.titre}</h2>
<p>{props.niveaux}</p>
<button onClick={props.onLike}>❤️ {props.likes}</button>
</div>
);
}
export default Carte;
```
> 🔎 Plus de `import { useState }`, plus de `setLikes`, **un seul** `<h2>` (tu en avais deux : `titre` ET `nom`), et plus de `props.createur` inutile. La carte affiche `props.likes` et, au clic, appelle `props.onLike` — point.
---
## ✅ Côté `App.jsx` : finir l'approche « parent »
Tu as encore l'**ancien** `total` muté qui traîne. On le supprime et on **recalcule** le total.
```jsx
function App() {
const [likes, setLikes] = useState({}); // {} au départ (pas {1:3, 4:1})
const [recherche, setRecherche] = useState("");
// ❌ SUPPRIME : const [total, setTotal] = useState(0);
// incrémente le like d'UNE trilogie (par son id)
function liker(id) {
setLikes((prev) => ({ ...prev, [id]: (prev[id] || 0) + 1 }));
}
// le total se RECALCULE à partir des likes
const total = Object.values(likes).reduce((s, n) => s + n, 0);
const filtrees = trilogies.filter((t) =>
t.nom.toLowerCase().includes(recherche.toLowerCase())
);
return (
<div className="body">
<Header />
<input
type="search"
placeholder="Rechercher une trilogie…"
value={recherche}
onChange={(e) => setRecherche(e.target.value)}
/>
<p>👍 Total de likes : {total}</p>
{filtrees.length === 0 && <p>Aucune trilogie trouvée</p>}
{filtrees.map((trilo) => (
<Carte
key={trilo.id}
titre={trilo.nom}
niveaux={trilo.niveaux}
likes={likes[trilo.id] || 0} // SON nombre de likes
onLike={() => liker(trilo.id)} // incrémente SON like
/>
))}
<Footer />
</div>
);
}
```
**✅ Test :** l'app s'affiche de nouveau. Chaque carte montre son ❤️, et le « Total de likes » = la somme, toujours juste.
> 💡 **À retenir :** dans une appli React, **une donnée vit à UN seul endroit**. Les likes vivent dans `App` ; `Carte` ne fait que les **afficher** (props) et **signaler** un clic (`onLike`). Ne mélange jamais « état dans le parent » et « état dans l'enfant » pour la même donnée — c'est ce qui t'a mis dedans.
---
## ✅ Récap
- [ ] `Carte.jsx` : version « affichage seul » (props `titre`, `niveaux`, `likes`, `onLike`), un seul `<h2>`, plus de `useState`
- [ ] `App.jsx` : supprimer l'ancien `total`, ajouter `liker(id)`, recalculer `total` avec `reduce`
- [ ] Tester : l'app s'affiche, likes par carte + total correct
Quand c'est réparé, tu peux passer à **`ENONCE_react_3_services.md`** (la suite). 🚀
-98
View File
@@ -1,98 +0,0 @@
# 🔧 Corrections — classes CSS & ménage dans le JS
Très bon boulot Guillaume : singulier/pluriel, compteur de caractères, validation à l'envoi, tableau de services généré en JS — tout y est. Deux points à corriger, dont **une notion à bien fixer**.
---
## 1. 🎯 Notion à corriger : les classes HTML se séparent par des **espaces**, pas des virgules
Dans `services.html` :
```html
<section class="simulateur, padding_left"> <!-- ❌ la virgule -->
```
Tu as écrit `class="simulateur, padding_left"`. Le problème : dans l'attribut `class`, les noms de classes se séparent par des **espaces**. Avec ta virgule, le navigateur comprend **deux** classes :
- une classe qui s'appelle littéralement `simulateur,` (avec la virgule collée !)
- une classe `padding_left`
Donc le jour où tu écriras une règle `.simulateur { ... }` dans ton CSS, **elle ne s'appliquera pas** : ta section ne porte pas la classe `simulateur`, mais `simulateur,`.
### ✅ La correction
```html
<section class="simulateur padding_left"> <!-- ✅ un simple espace -->
```
> 🧠 **D'où vient la confusion ?** La virgule, tu l'as vue… mais en **CSS**, pas en HTML. Ce sont deux contextes différents :
> - En **CSS**, la virgule sépare plusieurs **sélecteurs** : `.a, .b { color: red }` = « applique à `.a` **et** à `.b` ».
> - En **HTML**, l'attribut `class` liste les classes d'un élément, séparées par des **espaces** : `class="a b"` = « cet élément a les classes `a` et `b` ».
>
> 💡 **À retenir :** virgule = pour lister des sélecteurs **en CSS**. Espace = pour lister des classes **dans le HTML**.
**✅ Test :** ajoute temporairement `.simulateur { border: 2px solid red; }` dans ton CSS. Avec la virgule → aucun cadre. Après correction (espace) → le cadre rouge apparaît.
---
## 2. 🧹 Ménage dans `devis.js` : du code en double et du code mort
Regarde tes trois fonctions :
```js
function mettreAJourGPS() {
affichageNb.textContent = nbVehicules;
affichageGéo.textContent = nbVehicules * PRIX_GPS;
}
function mettreAJourCONSO() {
affichageNb.textContent = nbVehicules; // ← déjà fait par GPS
affichageConso.textContent = nbVehicules * PRIX_CONSOMMATION;
}
function mettreAJourTOTAL() { // ← jamais appelée !
affichageNb.textContent = nbVehicules;
affichageGéo.textContent = nbVehicules * PRIX_GPS;
affichageConso.textContent = nbVehicules * PRIX_CONSOMMATION;
}
```
Deux soucis :
- `mettreAJourTOTAL` n'est **jamais appelée** : c'est du **code mort** (elle ne sert à rien).
- `mettreAJourGPS` et `mettreAJourCONSO` se **répètent** (toutes les deux refont `affichageNb`), et tu dois les appeler ensemble : `(mettreAJourGPS(), mettreAJourCONSO())`.
La bonne nouvelle : la fonction qui fait **tout proprement**, tu l'as déjà écrite — c'est `mettreAJourTOTAL` ! Elle met à jour le nombre, le GPS **et** la conso en une seule fois.
### ✅ La piste (à toi de l'appliquer)
- Garde **une seule** fonction d'affichage (par exemple renomme `mettreAJourTOTAL` en `mettreAJour`).
- Dans tes deux écouteurs `+` et `−`, remplace `(mettreAJourGPS(), mettreAJourCONSO())` par un simple `mettreAJour();`.
- Supprime `mettreAJourGPS` et `mettreAJourCONSO` devenues inutiles.
> 💡 **À retenir :** quand deux bouts de code font la même chose, on les **réunit dans une seule fonction** qu'on appelle où il faut. Moins de copies = moins de risques d'en oublier une le jour où tu modifies. Et le code jamais appelé, on le **supprime** : il n'apporte rien et brouille la lecture.
> 🔎 Au passage : `(mettreAJourGPS(), mettreAJourCONSO())` fonctionne, mais ces parenthèses avec une virgule sont une astuce inhabituelle. Deux instructions sur deux lignes (ou un seul `mettreAJour();`) sont plus claires.
---
## 3. 🐛 Petit détail : le message de l'alerte ne dit pas la vérité
```js
if (zoneMsg.value.trim() === "") {
evenement.preventDefault();
alert("Merci de remplir tous les champs"); // ← mais tu ne vérifies QUE le message
}
```
Ton alerte dit « remplir **tous** les champs », mais ton `if` ne teste que la **textarea** (le nom et l'e-mail peuvent être vides, ça passe quand même).
À toi de choisir :
- soit ajuster le **texte** (« Merci d'écrire un message »),
- soit **vraiment** vérifier les autres champs aussi (avec des `if` supplémentaires sur `name.value` et `mail.value`).
> 💡 **À retenir :** un message affiché à l'utilisateur doit correspondre à ce que le code vérifie **réellement**, sinon c'est trompeur.
---
## ✅ Récap
- [ ] `class="simulateur padding_left"` (espace, pas virgule)
- [ ] Une seule fonction d'affichage du devis ; supprimer le code mort et le code en double
- [ ] Faire correspondre le message de l'alerte à ce qui est vraiment vérifié
Le reste de ta page est solide. Ces corrections, c'est surtout de la **rigueur** — exactement ce qui fait la différence entre du code qui « marche » et du code propre.
-109
View File
@@ -1,109 +0,0 @@
# 🔧 L'erreur « CORS policy » sur ton `fetch`
Rassure-toi : ton code `fetch` n'est **pas** faux. Cette erreur n'a rien à voir avec ta logique — c'est juste **la façon dont tu ouvres la page**.
---
## Pourquoi ça arrive
Tu ouvres `services.html` en **double-cliquant** dessus → l'adresse commence par `file://`. Or `fetch` **refuse** de lire un fichier quand la page est en `file://` : le navigateur considère ça comme non sécurisé et bloque avec un message « CORS policy ».
`fetch` veut une adresse en **`http://`**. Il faut donc « servir » ton dossier avec un mini-serveur.
---
## 🆚 `file://` vs `http://localhost:8000` : c'est quoi la différence ?
C'est **deux façons complètement différentes** d'ouvrir la même page. Regarde toujours ta **barre d'adresse**, elle te dit dans quel mode tu es.
### Mode 1 — Double-clic = `file://`
```
file:///Users/.../stagiaire/pages/services.html
```
Là, **aucun serveur n'est impliqué**. Le navigateur va juste **lire le fichier directement sur ton disque dur**, comme quand tu ouvres une photo ou un PDF. C'est isolé : il n'y a ni « site », ni adresse web, ni « origine ».
➡️ Et c'est précisément le problème : `fetch` sert à **demander des fichiers à un serveur**. Comme il n'y a **pas de serveur** ici (juste un fichier posé sur ton disque), le navigateur bloque par sécurité → l'erreur « CORS policy ». Pareil, plus tard, pour les *modules* JS ou d'autres fonctionnalités : elles ont besoin d'un vrai `http://`.
### Mode 2 — un mini-serveur lancé par toi = `http://localhost:8000`
```
http://localhost:8000/pages/services.html
```
Là, tu as lancé un **petit programme serveur** qui « publie » ton dossier et **répond aux demandes**, exactement comme le ferait un vrai site web — sauf qu'il tourne **sur ta propre machine**. Le navigateur ne lit plus le disque en direct : il **demande** la page au serveur (« donne-moi `/pages/services.html` »), et le serveur la **renvoie**.
➡️ Du coup il y a une vraie **adresse web** (`localhost:8000`), donc une vraie « origine ». `fetch` peut alors demander `css/vehicules.json` **au même serveur** → autorisé, ça marche. C'est aussi pour ça que tu vois apparaître les lignes `"GET /..." 200` dans le terminal : ce sont les demandes que ton navigateur fait au serveur.
### Décoder l'adresse `http://localhost:8000`
- **`localhost`** = « cette machine-ci » (ton ordi). Le serveur ne sort pas sur Internet, il reste chez toi.
- **`8000`** = le **port**, une sorte de « numéro de porte » où ton serveur écoute. C'est le `8000` que tu as écrit dans la commande ; tu pourrais en mettre un autre (3000, 5500…), il faudrait juste l'utiliser dans l'adresse.
### En une image
| | Double-clic (`file://`) | Serveur (`http://localhost:8000`) |
|---|---|---|
| Ce que fait le navigateur | lit un fichier sur le disque | **demande** la page à un serveur |
| Y a-t-il un serveur ? | non | oui (lancé par toi) |
| Adresse / origine | aucune (juste un chemin) | `localhost:8000` |
| `fetch` autorisé ? | ❌ non (erreur CORS) | ✅ oui |
| Ressemble à… | ouvrir un PDF depuis un dossier | visiter un vrai site web (mais chez toi) |
> 💡 **L'image à garder :** `file://` = tu lis un papier posé sur ton bureau. `http://localhost:8000` = tu as monté un mini-site web sur ton ordi et tu le visites comme n'importe quel site. Les vraies fonctionnalités du web (dont `fetch`) ont besoin de la **deuxième** situation.
---
## La solution — installer Python, puis lancer un mini-serveur
Le but est toujours le même : ouvrir la page via `http://localhost:...` au lieu de `file://`. Le plus simple chez toi (Windows), c'est **d'installer Python**, qui fournit un serveur prêt à l'emploi en une commande.
### 1. Installe Python
- Va sur **https://www.python.org/downloads/** et installe la dernière version.
- ⚠️ **Très important** pendant l'installation : **coche la case « Add python.exe to PATH »** (en bas de la première fenêtre). Sans ça, la commande `python` ne sera pas reconnue dans le terminal.
### 2. Lance le serveur
Ouvre un terminal (celui intégré à **Zed** fait l'affaire : `` Ctrl + ` ``), place-toi à la **racine du projet**, et tape :
```powershell
python -m http.server 8000
```
> Sur Windows c'est `python` (ou `py`), **pas** `python3`. Vérifie l'install avec `python --version`.
### 3. Ouvre la page dans le navigateur
```
http://localhost:8000/pages/services.html
```
C'est maintenant du `http://` → `fetch` fonctionne, l'erreur CORS disparaît. Pour **arrêter** le serveur : `Ctrl + C` dans le terminal.
> 💡 **À retenir :** « CORS policy » sur un `fetch` de fichier local = tu as ouvert la page en `file://`. La réponse est toujours : **passe par un petit serveur** pour avoir une adresse `http://`. Une fois Python installé, c'est une seule commande à relancer à chaque session de travail.
---
## Deux détails à corriger pendant que tu y es
Une fois le CORS réglé, il te restera deux petits trucs sinon ça ne s'affichera toujours pas :
### 1. Le chemin du fichier
```js
const reponse = await fetch("vehicules.json"); // cherche pages/vehicules.json
```
Le chemin est calculé **depuis la page** (`pages/services.html`), pas depuis `devis.js`. Ton fichier est dans `pages/css/`, donc :
```js
const reponse = await fetch("css/vehicules.json"); // ✅
```
(C'est cohérent avec ta balise `<script src="css/devis.js">` : même point de départ.)
### 2. Le tableau d'accueil manque
Ton code fait `corps.append(...)` sur `document.getElementById("corps_flotte")`, mais il n'y a **pas** de `corps_flotte` dans ton HTML — juste `<p id="message_flotte">`. Ajoute un tableau pour recevoir les lignes :
```html
<table border="1">
<tbody id="corps_flotte"></tbody>
</table>
```
Sans ça, `corps` vaut `null` et `corps.append(...)` plante (c'est d'ailleurs ce plantage qui déclenche ton message « bahahaha mimimimimi » 😄).
---
## ✅ Récap
- [ ] Lancer `python3 -m http.server 8000` puis ouvrir `http://localhost:8000/pages/services.html`
- [ ] `fetch("css/vehicules.json")` (bon chemin depuis la page)
- [ ] Ajouter `<tbody id="corps_flotte">` dans un `<table>`
Avec ces trois choses, ta flotte s'affiche. Le CORS, c'est vraiment juste l'histoire du `file://` vs `http://` — rien de plus à comprendre.
+119
View File
@@ -0,0 +1,119 @@
# 🔧 Corrections — `fetch` (le 200), script qui ne charge pas, et boutons
Tu vois un **`200`** dans la console **Python** mais tu ne vois pas la réponse. C'est normal, et il y a plusieurs petites choses à démêler. On commence par le plus important.
---
## 1. 🐛 LE bug bloquant : ta balise `<script>` est mal fermée
Dans `contact.html` :
```html
<script src="../flotte.js"</script> <!-- ❌ il manque le > -->
```
Regarde bien : après `"../flotte.js"`, il **manque le `>`** qui ferme la balise ouvrante. Du coup le navigateur ne charge **pas** `flotte.js` du tout → **rien** ne s'exécute, même pas ton `console.log`. C'est pour ça que « ça ne s'affiche pas dans la console ».
### ✅ Correction
```html
<script src="../flotte.js"></script> <!-- ✅ une balise <script> = <script ...> ... </script> -->
```
> 💡 **À retenir :** une balise `<script src="...">` doit être **fermée deux fois** : le `>` de la balise ouvrante, **puis** `</script>`. Compare avec tes deux autres lignes juste au-dessus (`../equipe.js`, `../contact.js`) : elles, elles ont bien le `>`. C'est ce petit `>` qui manque.
---
## 2. 🧠 Le `200` de Python ≠ « j'ai affiché les données »
C'est **la** notion à comprendre ici. Quand tu lances `python3 -m http.server`, tu as **deux mondes** :
| Le **serveur** (terminal Python) | Le **navigateur** (ta page) |
|---|---|
| Reçoit les demandes de fichiers et les envoie | Reçoit les fichiers et exécute le JavaScript |
| Affiche des lignes comme `"GET /vehicules.json" 200` | Affiche tes `console.log` dans **F12 → Console** |
Le **`200`** veut juste dire : *« le serveur a bien trouvé et envoyé le fichier »*. C'est **côté serveur**. Ça ne dit **rien** de ce que ta page en fait.
👉 Donc :
- La **réponse** (tes données) arrive dans le **navigateur**, pas dans le terminal Python. Pour la voir, regarde la **console du navigateur (F12)**, pas le terminal.
- Et pour l'afficher **sur la page**, ton `console.log(vehicules)` ne suffit pas : il écrit juste dans la console. Il faudra **construire des lignes** dans le HTML (boucle + `createElement`/`append`, comme pour ton tableau d'équipe).
> 💡 **À retenir :** `200` (terminal Python) = « fichier envoyé », côté serveur. `console.log` = côté navigateur (F12). Afficher **dans la page** = encore une autre étape (du code qui crée des éléments HTML).
---
## 3. 🐛 Le chemin du fichier dans `fetch`
Dans `flotte.js` :
```js
const reponse = await fetch("./vehicules.json"); // ⚠️ à vérifier
```
Attention : le chemin d'un `fetch` est calculé **par rapport à la page** (`pages/contact.html`), pas par rapport au fichier `flotte.js`. Or `vehicules.json` est à la **racine** du projet, pas dans `pages/`.
- `./vehicules.json` → cherche `pages/vehicules.json` → **n'existe pas** (erreur 404).
- `../vehicules.json` → **remonte** d'un dossier puis prend `vehicules.json` à la racine ✅
C'est exactement la même logique que tes balises `<script src="../equipe.js">` : le `../` pour remonter de `pages/` vers la racine.
> 🔎 **Comment vérifier ?** Ouvre **F12 → onglet Réseau (Network)**, recharge, et regarde la ligne `vehicules.json` : si c'est **404 (rouge)**, c'est le chemin ; si c'est **200**, le fichier est bien chargé et le souci est ailleurs (le script mal fermé du point 1).
---
## 4. ✅ Une fois les 3 points réglés
Ton `console.log(vehicules)` devrait afficher ton tableau de 3 véhicules **dans la console du navigateur (F12)**.
Étape suivante (à toi) : au lieu de juste `console.log`, **afficher** la flotte dans la page. Tu as déjà tout le savoir-faire (c'est exactement ton tableau d'équipe) :
- un `<tbody id="...">` dans le HTML,
- une boucle sur `vehicules`,
- `createElement` + `append` pour chaque ligne.
---
# 🧷 Pendant qu'on y est : deux corrections sur ton formulaire
## A. 🐛 Le bouton « Effacer » envoie le formulaire
```html
<button>Envoyer</button>
<button type="submit" id="effacer">Effacer</button> <!-- ❌ submit -->
```
Ton bouton « Effacer » est en `type="submit"`. Du coup, quand on clique dessus, il **envoie le formulaire** (au lieu de juste vider la zone) : ça déclenche ta validation et l'alerte « Merci d'écrire un message ». Ce n'est pas ce que tu veux.
> 🔁 **Rappel du fichier `EXPLICATION_form.md` :** dans un `<form>`, un `<button>` est **`type="submit"` par défaut**. Pour un bouton qui fait **autre chose** qu'envoyer (effacer, basculer un thème…), il faut explicitement `type="button"`.
### ✅ Correction
```html
<button>Envoyer</button> <!-- ok : submit par défaut -->
<button type="button" id="effacer">Effacer</button> <!-- ✅ ne soumet plus -->
```
## B. 🐛 Le `<label>` de l'e-mail ne pointe pas sur le bon champ
```html
<label for="e_mail">Votre e-mail:</label>
<input ... id="email"> <!-- l'id est "email", pas "e_mail" -->
```
L'attribut `for` d'un label doit correspondre **exactement** à l'`id` du champ. Là, `for="e_mail"` mais `id="email"` → cliquer sur le libellé ne place pas le curseur dans le champ.
### ✅ Correction
```html
<label for="email">Votre e-mail:</label>
```
> 💡 **À retenir :** `label for="X"` doit matcher `input id="X"`, au caractère près. Bien fait, cliquer le texte du label active le champ (pratique, et important pour l'accessibilité).
---
## ✅ Récap
- [ ] Fermer la balise : `<script src="../flotte.js"></script>` (le `>` manquant)
- [ ] Comprendre : `200` Python = fichier envoyé ; la réponse se voit dans **F12 → Console**
- [ ] Corriger le chemin : `fetch("../vehicules.json")` (vérifier dans l'onglet Réseau)
- [ ] Afficher la flotte dans la page (boucle + `createElement`), pas seulement `console.log`
- [ ] `type="button"` sur « Effacer »
- [ ] `label for="email"` (= l'id du champ)
+80
View File
@@ -0,0 +1,80 @@
# 🔧 Correction — la recherche et le tri ne font rien
Tu as bien créé la fonction `afficherEquipe`, et le tableau s'affiche au chargement. Mais quand tu tapes dans la recherche ou que tu cliques « Trier », **rien ne bouge**. Voici pourquoi — c'est un bug très instructif sur les **paramètres de fonction**.
---
## 🔍 D'abord, un indice important : la Console est *muette* cette fois
La fois précédente, tu avais une erreur **rouge** dans la Console (`afficherEquipe is not defined`). Là, **rien** : aucun message d'erreur.
> 💡 **À retenir :** il existe deux familles de bugs.
> - Ceux qui **plantent** (message rouge dans la Console) — comme la dernière fois.
> - Ceux qui **ne plantent pas** mais font **la mauvaise chose** — comme aujourd'hui. La Console reste silencieuse, le code « marche »… mais pas comme tu veux. Ce sont les plus sournois : il faut **raisonner**, pas juste lire les erreurs.
---
## 🎯 Le vrai problème : ta fonction ignore ce que tu lui donnes
Regarde **précisément** ces deux endroits dans ton `equipe.js` :
```js
function afficherEquipe() { // 👈 (1) les parenthèses sont VIDES
corps.innerHTML = "";
for (const membre of equipe) { // 👈 (2) tu boucles toujours sur la globale "equipe"
corps.append(creerLigne(membre));
}
}
```
Et plus bas, tu l'appelles avec une liste :
```js
afficherEquipe(resultats); // la liste filtrée
afficherEquipe(triee); // la liste triée
```
Le souci : tu **passes** une liste (`resultats`, `triee`)… mais ta fonction ne la **reçoit nulle part**. Les parenthèses de `function afficherEquipe()` sont vides → l'argument que tu envoies tombe dans le vide. Et à l'intérieur, tu reboucles sur la variable globale `equipe`, donc **toujours les 3 membres, dans l'ordre d'origine**.
👉 Résultat : `filter` calcule bien la bonne liste filtrée, `sort` calcule bien la liste triée… et ta fonction **les jette** pour réafficher `equipe` en entier à chaque fois. D'où l'impression que « rien ne se passe ».
---
## 🧠 L'idée à comprendre : un *paramètre*, c'est une boîte d'entrée
Une fonction, c'est une recette. Un **paramètre**, c'est un ingrédient qu'on lui **tend** au moment de l'appeler.
- Si la recette dit *« utilise la liste que je te donne »* → tu peux lui tendre une liste différente à chaque fois, et le résultat change.
- Ta recette actuelle dit *« ignore ce qu'on me tend, je prends toujours `equipe` sur l'étagère »* → quoi que tu lui passes, elle fait pareil.
Tu as **déjà** utilisé ce mécanisme sans bug ailleurs : ta fonction `creerLigne(membre)` reçoit bien un `membre` entre les parenthèses, et travaille avec **lui**. C'est exactement la même chose qu'il manque ici.
---
## 🛠️ La piste pour corriger (à toi de l'écrire)
1. Donne un **nom** au paramètre dans les parenthèses de `afficherEquipe` (par exemple `liste`).
2. À l'intérieur, boucle sur **ce nom-là**, pas sur la globale `equipe`.
3. Garde ton appel initial, mais en lui passant bien toute l'équipe : `afficherEquipe(equipe)`.
Ainsi :
- au chargement, tu lui tends `equipe` → elle affiche tout le monde ;
- quand tu tapes, tu lui tends `resultats` → elle affiche **la liste filtrée** ;
- quand tu cliques, tu lui tends `triee` → elle affiche **la liste triée**.
> ⚠️ **Petit piège à éviter :** n'appelle pas ton paramètre `equipe` (le même nom que la globale). Ça « marcherait » mais ce serait très déroutant à relire. Un nom différent comme `liste` rend les choses claires : on voit tout de suite que la fonction travaille sur *ce qu'on lui donne*, pas sur une variable extérieure.
---
## ✅ Comment vérifier que c'est réglé
1. Tape « mé » dans la recherche → seule Mélissa reste. Efface → les 3 reviennent.
2. Clique « Trier A → Z » → l'ordre change (Guillaume, Matthieu, Mélissa).
Le reste de ton code (`filter`, `sort`) est **déjà correct** : il calcule la bonne liste. Le seul maillon cassé, c'est la fonction qui ne l'utilise pas. Répare ce maillon et tout le reste se met à marcher d'un coup.
---
## 🚀 Si tu veux aller plus loin (optionnel)
Une fois que ça marche, regarde la méthode **`replaceChildren`** (`MDN replaceChildren`) : elle vide et remplit un élément **en une seule ligne**, sans `innerHTML`. Combinée à `map` (Niveau 5), ta fonction pourrait tenir en deux lignes. Mais d'abord, fais marcher la version simple. 🙂
+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.
-91
View File
@@ -1,91 +0,0 @@
# 🔧 Correction — le nombre revient à 1 au rechargement
Bonne nouvelle d'abord : ton `localStorage` **fonctionne**. Ton `console.log("dans le local : ", ...)` au chargement te montre la valeur enregistrée → la lecture marche, tu ne passes **pas** dans le `else`.
Alors pourquoi l'écran affiche « 1 » ? Parce qu'il manque **une seule chose**.
---
## 1. 🎯 Le vrai problème : tu lis la valeur, mais tu ne l'**affiches** pas
Au chargement, tu fais bien ceci :
```js
let nbVehicules;
if (localStorage.getItem("nbVehicules") !== null) {
nbVehicules = localStorage.getItem("nbVehicules"); // ✅ la variable vaut bien, par ex. "12"
} else {
nbVehicules = 1;
}
```
Donc la **variable** `nbVehicules` vaut bien 12. **Mais** ton `<span id="nb_vehicules">` dans le HTML, lui, contient toujours le « 1 » écrit en dur :
```html
<span id="nb_vehicules">1</span>
```
Et **rien**, au chargement, ne vient remplacer ce « 1 » par la valeur de `nbVehicules`. Tes fonctions `mettreAJour…` ne sont appelées **que** dans les clics `+` et `−`. Au démarrage, aucune n'est appelée → l'écran reste sur le « 1 » du HTML.
> 🔎 C'est ça qui donne l'impression « ça passe dans le `else` ». En réalité, la lecture est bonne ; c'est juste que **la variable a changé, pas l'affichage**. La variable et ce qui est écrit à l'écran sont **deux choses différentes** : changer l'une ne change pas l'autre toute seule — il faut le dire explicitement avec `.textContent`.
### ✅ La piste
Juste **après** ton `if/else` de chargement, **appelle une fois** ta fonction d'affichage (par ex. `mettreAJourTOTAL()`), pour que l'écran se mette d'accord avec la variable.
> 💡 **À retenir :** au chargement d'une page, il faut **afficher l'état initial** soi-même. La règle générale : *« quand la donnée change (ici : au démarrage, on la relit du storage), on rappelle la fonction qui met l'affichage à jour. »*
**✅ Test :** règle 12 véhicules, recharge (F5) → l'écran affiche 12 tout de suite.
---
## 2. ⚠️ Le piège qui va te tomber dessus juste après : texte vs nombre
`localStorage` ne stocke **que du texte**. Donc après lecture, `nbVehicules` vaut la **chaîne** `"12"`, pas le **nombre** `12`.
Conséquence au prochain clic sur `+` :
```js
nbVehicules = nbVehicules + 1; // "12" + 1 → "121" (et pas 13 !)
```
Avec du texte, le `+` **colle** au lieu d'**additionner**. Tu vas voir des « 121 », « 1211 »…
### ✅ La piste
Convertis la valeur lue en **nombre** au moment de la relire :
```js
nbVehicules = parseInt(localStorage.getItem("nbVehicules"));
```
(cherche `parseInt` si besoin — ça transforme `"12"` en `12`.)
> 💡 **À retenir :** tout ce qui sort de `localStorage` est du **texte**. Si tu veux faire des calculs avec, reconvertis-le en nombre (`parseInt` pour un entier).
---
## 3. 🐛 Une vraie erreur cachée dans ton bouton `+`
Regarde ces lignes (bouton `+`) :
```js
console.log("dans le store", localStorage.getItem("nbVehicules"))(
mettreAJourGPS(),
mettreAJourCONSO(),
);
```
Il **manque un point-virgule** après le `console.log(...)`. Du coup JavaScript lit la parenthèse de la ligne suivante comme un **appel** : il essaie d'« appeler » le résultat de `console.log` (qui ne renvoie rien) → erreur dans la console : *`console.log(...) is not a function`*.
### ✅ La correction : deux instructions séparées, chacune finie par `;`
```js
console.log("dans le store", localStorage.getItem("nbVehicules"));
mettreAJourTOTAL(); // (ou tes deux fonctions, mais une seule suffit)
```
> 🔎 **Pourquoi ce bug ?** Quand une ligne **commence par une parenthèse `(`** et que la précédente n'a pas de `;`, JavaScript croit que tu enchaînes un appel. C'est exactement le piège que crée `(mettreAJourGPS(), mettreAJourCONSO())` collé à la ligne d'avant. Mets tes `;` en fin de ligne et garde **une seule** fonction d'affichage (cf. la correction précédente sur le ménage) : tout devient plus simple et plus sûr.
---
## ✅ Récap
- [ ] Appeler la fonction d'affichage **une fois au chargement**, après le `if/else` (→ corrige le « reste à 1 »)
- [ ] `parseInt(...)` sur la valeur relue du storage (→ évite « 121 »)
- [ ] Remettre le `;` manquant et n'avoir **qu'une** fonction d'affichage appelée proprement
L'idée clé de tout ça : **une variable ≠ ce qui est affiché**. Le JavaScript ne « rafraîchit » l'écran que quand tu le lui demandes (`textContent`). Une fois ce réflexe acquis, ce genre de bug disparaît tout seul.
+77
View File
@@ -0,0 +1,77 @@
# 🔧 Correction — ton `map` (et le `filter` au-dessus)
D'abord : 👍 tu as bien corrigé le `preventDefault` (déplacé tout en haut du `submit`) — ton histoire de formulaire sur localhost est réglée, bravo.
Pour le `map`, ton code de la fin de `equipe.js` :
```js
const array = equipe.filter(function (stagiaire) {
return stagiaire.nom.toLowerCase().includes(mélissa, guillaume);
});
const mapped = array.map((x) => x - stagiaire.job - stagiaire.partie);
console.log(mapped);
```
Il y a **3 erreurs** qui s'additionnent. On les prend une par une — ouvre la Console (F12), tu verras un message rouge `... is not defined`, c'est notre point de départ.
---
## 🐛 Erreur 1 — `mélissa` et `guillaume` sont des variables qui n'existent pas
```js
.includes(mélissa, guillaume)
```
Sans guillemets, `mélissa` et `guillaume` ne sont pas du **texte** : JavaScript les prend pour des **variables**… qui n'existent nulle part → **`ReferenceError: mélissa is not defined`**, et tout le fichier s'arrête là.
Deux choses :
- Du texte, ça se met **entre guillemets** : `"mélissa"`.
- `includes("...")` ne prend **qu'un seul** mot à chercher (le 2ᵉ argument n'est pas un autre mot, c'est une position de départ — pas ce que tu veux).
> 🔎 Mais surtout : **pour l'exo `map`, tu n'as pas besoin de `filter` du tout.** `filter` sert à *garder certains* éléments ; `map` sert à *transformer* chaque élément. Là, l'objectif (l'exo 1) est juste de transformer → donc `map` **directement sur `equipe`**, sans filtre.
---
## 🐛 Erreur 2 — `stagiaire` n'existe pas dans le `map`
```js
const mapped = array.map((x) => x - stagiaire.job - stagiaire.partie);
```
`stagiaire`, c'était le **nom du paramètre** de ta fonction `filter` (au-dessus). Il n'existe **que** à l'intérieur de cette fonction-là. Dans le `map`, il est **inconnu** → encore une `ReferenceError`.
Dans le `map`, l'élément courant, c'est **`x`** (le nom que tu as choisi entre les parenthèses). C'est **lui** qu'il faut utiliser — pas `stagiaire`.
> 💡 **À retenir :** le paramètre d'une fonction (`stagiaire`, `x`, `membre`…) n'existe qu'**à l'intérieur de cette fonction**. Ailleurs, il n'a jamais entendu parler de lui.
---
## 🐛 Erreur 3 — le `-` (soustraction) n'a aucun sens ici
```js
x - stagiaire.job - stagiaire.partie
```
`x` est un **objet** (`{ nom, job, partie }`) et `.job` / `.partie` sont du **texte**. Les **soustraire** ne veut rien dire → tu obtiendrais `NaN` (« Not a Number »).
Le rôle du `map`, c'est de **renvoyer**, pour chaque élément, **la nouvelle valeur que tu veux**. Donc la question à te poser : *« pour chaque membre, qu'est-ce que je veux récupérer ? »*
- juste son nom → tu renvoies `x.nom`
- une phrase → tu renvoies `` `${x.nom} (${x.job})` ``
> 💡 **À retenir :** dans un `map`, la fonction doit **`return`** quelque chose **construit à partir de l'élément** (`x.nom`, etc.). Pas une soustraction.
---
## ✅ L'objectif de l'exo, remis au clair
L'exo 1 (Niveau 5) voulait juste : *à partir de `equipe`, fabriquer la liste des **noms**.* C'est-à-dire **transformer** chaque membre en son nom :
- pas de `filter`,
- `map` sur `equipe`,
- dans la fonction, **`return` le `.nom`** de l'élément reçu.
**✅ Test :** ton `console.log` doit afficher `["Mélissa", "Guillaume", "Matthieu"]` (que des noms, dans un tableau).
Essaie de l'écrire **toi-même** avec ces 3 corrections en tête. Si tu bloques vraiment, la solution exacte est dans `SOLUTIONS_contact_4.md` (exo 1) — mais tente d'abord, tu es à deux doigts.
> 🧭 La phrase à garder : **`filter` choisit** *quels* éléments garder, **`map` transforme** *chaque* élément. Ici on transforme → c'est `map`, tout seul.
+43 -49
View File
@@ -1,98 +1,92 @@
# 🔎 Correction — faire (enfin) vivre la page Détail
Salut ! 👋 Ton routeur marche **parfaitement** : quand tu cliques « Voir les détails », l'URL devient `/trilogie/3` et la page change **sans recharger**. Bravo, c'est exactement le but de React Router. 🎉
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. 🎉
Mais ta page `Details.jsx` ressemble encore à ça :
Sauf que ta page `Detail.jsx` ressemble encore à ça :
```jsx
function Details() {
function Detail() {
const { id } = useParams();
return <h1>Détails de la trilogie {id}</h1>;
return <h1>Détails du jeu {id}</h1>;
}
```
Elle affiche juste le **numéro**. On voudrait afficher la **vraie trilogie** : son nom, ses niveaux, ses likes. Et là, une question piège…
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 ? »
`Details` reçoit `id` (genre `"3"`). Mais le **tableau des trilogies**, lui, vit dans le `useState` de… `Accueil`. 😬
`Detail` reçoit `id` (genre `"2"`). Mais la **liste des jeux** vit dans le `useState` de… `Accueil` (tu l'as remplie avec `fetch`). 😬
`Details` et `Accueil` sont **deux pages sœurs** : aucune des deux n'est dans l'autre, donc `Details` **ne peut pas** lire l'état de `Accueil`. C'est le mur classique.
`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** (chez leur parent commun), ou dans un endroit que les deux savent lire.
> 💡 **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.
Il y a 2 niveaux de solution. Prends la **simple** d'abord, garde la **pro** pour l'énoncé final.
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 — sortir les données dans un fichier à part
## ✅ Solution simple — la page détail va chercher la donnée elle-même
Tes 5 trilogies de départ sont fixes. On les sort de `Accueil` dans un fichier que **les deux pages** peuvent importer.
`Detail` connaît l'`id` voulu. Elle peut **refaire le `fetch`** du même fichier et **retrouver** le bon jeu avec `.find()`.
### 1. Crée `src/data.js`
```js
export const donneesInitiales = [
{ id: 1, nom: "The RGB Trilogy", niveaux: "Yatagarasu - Erebus - Sonic Wave" },
{ id: 2, nom: "The Greek Letter Trilogy", niveaux: "Omega - Sigma - Gamma" },
{ id: 3, nom: "The Techno Trilogy", niveaux: "Artificial Ascent - Digital Descent - Cybernetic Crescent" },
{ id: 4, nom: "The Riot's Trilogy", niveaux: "Tartarus - Acheron - Aeternus" },
{ id: 5, nom: "The Apocalytic Trilogy", niveaux: "Cataclysm - Bloodbath - Aftermath" },
];
```
### 2. Dans `Accueil.jsx`, importe-le au lieu de recopier le tableau
```jsx
import { donneesInitiales } from "../data";
// ...
const [trilogies, setTrilogies] = useState(donneesInitiales);
```
### 3. Dans `Details.jsx`, retrouve la bonne trilogie
```jsx
import { useParams, Link } from "react-router-dom";
import { donneesInitiales } from "../data";
import { useState, useEffect } from "react";
function Details() {
function Detail() {
const { id } = useParams();
const [jeu, setJeu] = useState(null);
// ⚠️ id de l'URL = une CHAÎNE ("3"), ton id de données = un NOMBRE (3).
// On compare en convertissant : Number(id)
const trilogie = donneesInitiales.find((t) => t.id === Number(id));
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 (!trilogie) return <p>Trilogie introuvable 🤷 <Link to="/">← retour</Link></p>;
if (!jeu) return <p>Jeu introuvable 🤷 <Link to="/">← retour</Link></p>;
return (
<div className="detail">
<Link to="/">← Retour à la liste</Link>
<h1>{trilogie.nom}</h1>
<p>🎮 Niveaux : {trilogie.niveaux}</p>
<h1>{jeu.titre}</h1>
<p>🕹️ Console : {jeu.console}</p>
<p>📅 Année : {jeu.annee}</p>
<p>🎮 Type : {jeu.Type}</p>
</div>
);
}
export default Details;
export default Detail;
```
> 🐛 **Le piège n°1 ici, c'est `Number(id)`.** `useParams()` te donne **toujours une chaîne**. Si tu écris `t.id === id`, tu compares `3 === "3"` → `false`, et `find` ne trouve **jamais** rien. Convertis l'un des deux !
> 🐛 **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`.
**✅ Test :** clique « Voir les détails » sur la trilogie Techno → tu vois son nom et ses niveaux, pas juste « 3 ». Le lien « ← Retour » te ramène à la liste.
> 🔎 **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.
---
## ⚠️ La limite de cette solution (et c'est voulu)
## ⚠️ Les limites de cette solution (et c'est voulu)
Avec le fichier `data.js`, la page détail connaît les **5 trilogies de départ**. Mais celles que tu **ajoutes avec le formulaire** ne seront **pas** dans `data.js` → leur page détail affichera « introuvable ». Et les **likes** non plus n'apparaissent pas dans le détail.
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.
C'est normal : `data.js` est figé, alors que le state vit dans `Accueil`. Pour que **tout** (ajouts + likes) suive jusqu'à la page détail, il faut faire **remonter l'état au niveau du routeur**. C'est le **Module L** de l'énoncé final 👇 — le vrai « boss ». 😈
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, 3 petits nettoyages dans `Accueil.jsx`
## 🧹 Pendant qu'on y est, 2 petits nettoyages
1. `import { useParams } ...` n'est pas utilisé dans `Accueil` → à supprimer (c'est dans `Details` qu'il sert).
2. `console.log("test")` → on enlève les logs de debug avant de livrer. 🧼
3. Le `<>...</>` autour du seul `<select>` ne sert à rien (un fragment, c'est utile quand on a **plusieurs** éléments à grouper sans `<div>`). Tu peux laisser juste le `<select>`.
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. On sort la donnée commune **plus haut** (fichier partagé, ou state remonté chez le parent). Et `useParams()` renvoie **toujours une chaîne** → pense à `Number()` pour comparer aux ids.
> 💡 **À 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.
+81
View File
@@ -0,0 +1,81 @@
# 🔧 Correction — enregistrer et afficher le prénom dans l'alerte
Tu veux qu'au moment de l'envoi, l'alerte affiche le **prénom** saisi. Ton alerte affiche sûrement quelque chose comme :
```
merci [object HTMLInputElement], ton message a été pris en compte
```
Voici **pourquoi**, et comment t'en sortir — sans te donner la solution toute faite.
---
## 🔍 Ce que tu as écrit
```js
const nom = document.getElementById("prenom");
const greeting = `merci ${nom}, ton message a été pris en compte`;
// ...
} else {
alert(`${greeting}`);
}
```
Il y a **deux soucis** qui se cumulent ici. Bonne nouvelle : tu sais déjà corriger les deux, tu le fais ailleurs dans ton fichier.
---
## 🪤 Souci n°1 — il manque `.value`
`document.getElementById("prenom")` te donne **la case** HTML entière (l'objet `<input>`), pas le texte dedans. Pour obtenir ce que la personne a **tapé**, il faut `.value` — exactement comme tu fais avec `zoneMessage.value` pour la textarea.
```js
nom // ❌ la case → s'affiche "[object HTMLInputElement]"
nom.value // ✅ le texte tapé → "Mélissa"
```
C'est ça qui produit le `[object HTMLInputElement]` dans ton alerte.
---
## 🪤 Souci n°2 — tu lis la valeur **trop tôt**
Regarde **où** tu construis `greeting` : tout **en haut** du fichier. Or ce code s'exécute **au chargement de la page**, à un instant où l'utilisateur n'a encore **rien tapé**. Le champ est vide à ce moment-là.
Du coup, même si tu ajoutes `.value`, `greeting` serait figé sur le champ **vide du démarrage**, et ne changerait jamais.
```js
// en haut du fichier = au chargement → le champ est VIDE
const greeting = `merci ${nom.value}, ...`; // ❌ figé sur "" pour toujours
```
👉 La phrase doit être fabriquée **au moment du clic sur Envoyer**, c'est-à-dire **à l'intérieur** de la fonction `submit`. C'est seulement là que le champ contient ce que la personne a saisi.
> 🔎 Tu as déjà ce réflexe ailleurs : dans `rafraichirCompteur`, tu lis `zoneMessage.value` **au moment** où on tape, pas une fois pour toutes au début. Même logique ici.
---
## 🧩 Ce qu'il te reste à faire (à toi de l'écrire)
1. Garde en haut seulement l'**attrape-élément** : `const champPrenom = document.getElementById("prenom");` (sans `.value`).
2. Dans le `else` (message non vide), **à ce moment-là**, lis `champPrenom.value` et construis ta phrase, puis `alert(...)`.
3. Pour l'**enregistrer** : `localStorage.setItem("…", champPrenom.value)` — tu sais déjà faire, tu l'utilises pour le brouillon.
> 💡 Ton formulaire est en `method="get"` : quand l'envoi réussit, **la page se recharge** et l'alerte disparaît aussitôt. Comme il n'y a pas de serveur derrière, le plus simple pour ce TP est d'appeler `evenement.preventDefault()` **aussi** dans le `else`, pour que la page ne recharge pas et que tu voies ton alerte.
---
## ✅ Comment vérifier
1. Tape un prénom + un message, clique **Envoyer** → l'alerte dit « merci **\<ton prénom\>**, … » (et **plus** `[object HTMLInputElement]`).
2. Console (F12) : tape `localStorage.getItem("…")` (la clé que tu as choisie) → tu retrouves le prénom enregistré.
3. Laisse le prénom **vide** et envoie → décide quoi faire (afficher « merci visiteur », ou exiger le prénom comme tu le fais déjà pour le message).
---
## 🧪 Pour aller plus loin
- Ajouter le **nombre de caractères** du message dans l'alerte (`…value.length`).
- Au prochain chargement, accueillir la personne : « Re-bonjour \<prénom\> » en relisant ce que tu as enregistré (`getItem`).
Note ta question si un point bloque encore.
+99 -43
View File
@@ -1,40 +1,25 @@
# 🔧 Correction — React Router : pourquoi ça plante
# 🔧 Correction — React Router : « Accueil is not defined » + 2 bugs
D'abord, une **bonne nouvelle** : avoir `BrowserRouter` dans `main.jsx` **et** `Routes`/`Route` dans `App.jsx`, ce **n'est pas** une erreur — c'est exactement le **bon découpage** :
- `main.jsx` **enveloppe** toute l'app dans `<BrowserRouter>` (il « allume » le routeur).
- `App.jsx` **déclare les routes** (`<Routes>`/`<Route>`).
Ton vrai bug est ailleurs. 👇
Ton erreur dit l'essentiel : **`Accueil` n'est pas défini**. Tu utilises `<Accueil />` dans une `<Route>`, mais ce composant **n'existe nulle part**. On répare ça, plus un double `render` dans `main.jsx`.
---
## 🐛 Le bug : tu rends ton app **deux fois** dans `main.jsx`
## 🐛 Bug 1 — `main.jsx` rend l'app **deux fois**
```jsx
createRoot(document.getElementById("root")).render(
<BrowserRouter>
<App />
</BrowserRouter>,
);
createRoot(document.getElementById("root")).render( // ❌ un DEUXIÈME render dans le même #root
<StrictMode>
<App />
</StrictMode>,
);
createRoot(document.getElementById("root")).render( // ❌ un 2e render dans le même #root
<BrowserRouter>
<App />
</BrowserRouter>,
);
```
Tu appelles `createRoot(...).render(...)` **deux fois** sur le **même** `#root`. Le **second** écrase le premier. Or le second n'a **pas** de `<BrowserRouter>` → ton app finit affichée **sans routeur**.
Résultat : quand `App` rencontre `<Routes>`, React ne trouve aucun routeur autour → erreur du genre **« useRoutes() may be used only in the context of a `<Router>` »**, et page blanche.
> 🧠 **À retenir :** il n'y a **qu'un seul** `createRoot(...).render(...)` dans toute l'app — c'est le point d'entrée unique. On n'en met pas deux.
---
## ✅ La correction : UN seul `render`, qui imbrique tout
On garde `StrictMode` **et** `BrowserRouter`, en les **emboîtant** (l'un dans l'autre), avec un seul `render` :
Il ne doit y avoir **qu'un seul** `createRoot(...).render(...)`. Là, le second écrase le premier. On **fusionne** en emboîtant `StrictMode > BrowserRouter > App` :
```jsx
import { StrictMode } from "react";
@@ -52,38 +37,109 @@ createRoot(document.getElementById("root")).render(
);
```
> 🔎 L'ordre habituel : `StrictMode` (un outil de dev de React) **autour de** `BrowserRouter` **autour de** `App`. Comme des poupées russes.
**✅ Test :** la page d'accueil (`/`) s'affiche, plus d'erreur de `<Router>`. Va sur une URL `/trilogie/3` → ta page `Details` s'affiche.
> 🧩 **La règle :** **un seul** `<BrowserRouter>`, et il doit **envelopper** tes `<Routes>`. Ici il est dans `main.jsx` (autour de `<App/>`) → garde-le là, n'en remets pas dans `App.jsx`.
> 🧠 **Règle :** un seul point d'entrée → un seul `render`. Et `<BrowserRouter>` doit **envelopper** ce qui contient tes `<Routes>`.
---
## 🔎 Vérifie aussi
## 🐛 Bug 2 — `<Accueil />` n'existe pas (l'erreur principale)
Ton `App.jsx` est bon :
Dans ton `App.jsx` :
```jsx
<Routes>
<Route path="/" element={<Accueil />} />
<Route path="/trilogie/:id" element={<Details />} />
<Route path="/" element={<Accueil />} /> {/* ❌ Accueil n'est défini nulle part */}
<Route path="/jeu/:id" element={<Detail />} />
</Routes>
```
Assure-toi juste que **`pages/Accueil.jsx` contient bien ta liste** (cartes + recherche + tri + formulaire + total) — c'est elle, maintenant, ta page d'accueil. Si « / » est vide, c'est que le contenu n'a pas été déplacé dans `Accueil`.
Tu demandes à afficher `<Accueil />`, mais tu ne l'as **ni créé ni importé** → `Accueil is not defined`.
Et dans `Carte`, pour aller vers le détail, utilise un `<Link>` (pas un `<a href>`) :
Autre souci : tu as mis ton `<Routes>` **au-dessus** de tout ton contenu (Header, recherche, liste, formulaire…), et ce contenu est **en dehors** des routes. Du coup il s'afficherait sur **toutes** les pages, et la route `/` n'a rien à montrer (à part `Accueil`, qui n'existe pas).
### ✅ L'idée : ta liste actuelle **devient** la page `Accueil`
Tu as déjà tout le code de la page d'accueil (recherche + tri + liste + formulaire + total). Il faut juste le **mettre dans un composant `Accueil`**, et laisser `App` ne gérer **que** les routes.
**1. Crée `src/pages/Accueil.jsx`** et **déplaces-y** tout ce contenu (avec ses `useState`, le `fetch`, `ajouter`, `affiches`, etc.) :
```jsx
import { Link } from "react-router-dom";
<Link to={`/trilogie/${props.id}`}>Voir les détails</Link>
import { useState, useEffect } from "react";
import Carte from "../components/Carte";
import BarreRecherche from "../components/BarreRecherche";
function Accueil() {
// ⬅️ tout ton code actuel de App (les useState, le fetch, ajouter, affiches…)
return (
<>
<BarreRecherche valeur={recherche} onChange={(e) => setRecherche(e.target.value)} />
{/* le select de tri, la liste affiches.map(...), le formulaire, le total… */}
</>
);
}
export default Accueil;
```
**2. Crée `src/pages/Detail.jsx`** (sors ta fonction `Detail` de `App`) :
```jsx
import { useParams } from "react-router-dom";
function Detail() {
const { id } = useParams();
return <h1>Détails du jeu {id}</h1>;
}
export default Detail;
```
**3. `App.jsx` ne garde QUE l'assemblage + les routes :**
```jsx
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";
import "./App.css";
function App() {
return (
<>
<Header />
<Routes>
<Route path="/" element={<Accueil />} />
<Route path="/jeu/:id" element={<Detail />} />
</Routes>
<Footer />
</>
);
}
export default App;
```
> 🔎 `Header` et `Footer` sont **autour** des `<Routes>` → ils s'affichent sur **toutes** les pages. Seul ce qui est **dans** `<Routes>` change selon l'URL.
**✅ Test :** `/` affiche ta liste (page Accueil), `/jeu/2` affiche la page Détail. Plus d'erreur « Accueil is not defined ».
---
## 🐛 Bug 3 — petit reste du renommage `console`
Dans ton `ajouter`, tu as renommé l'état en `plateforme` 👍, mais l'objet écrit encore :
```jsx
const nouveau = { id: Date.now(), titre, annee: Number(annee), console, Type: "3d" };
// ^^^^^^^ = l'objet global console !
```
`console,` (tout seul) veut dire `console: console` → tu ranges l'**objet global `console`** dans ton jeu, pas la plateforme tapée. Corrige :
```jsx
const nouveau = { id: Date.now(), titre, annee: Number(annee), console: plateforme, Type: "3d" };
```
(il te faudra passer `id` en prop à la `Carte` depuis `Accueil`.)
---
## ✅ Récap
- [ ] **Un seul** `createRoot(...).render(...)` dans `main.jsx`
- [ ] Emboîter `StrictMode > BrowserRouter > App`
- [ ] Vérifier que `pages/Accueil.jsx` contient bien ta liste
- [ ] `<Link to="/trilogie/:id">` dans les cartes pour naviguer
- [ ] `main.jsx` : un **seul** `render`, `StrictMode > BrowserRouter > App`
- [ ] Créer `pages/Accueil.jsx` (y déplacer ta liste/recherche/tri/formulaire + leur état)
- [ ] Créer `pages/Detail.jsx` (sortir la fonction `Detail`)
- [ ] `App.jsx` = `Header` + `<Routes>` + `Footer`, rien d'autre
- [ ] `console: plateforme` dans l'objet `nouveau`
> 💬 À garder : `BrowserRouter` (dans `main`) = « j'allume le routeur » ; `Routes`/`Route` (dans `App`) = « voici quelle page pour quelle URL ». Et **un seul** `render`.
> 💡 Pour naviguer vers le détail, mets un `<Link to={`/jeu/${jeu.id}`}>` (dans la `Carte` ou la liste) — pas un `<a href>`.
> 💬 À garder : un composant qu'on utilise (`<Accueil/>`) doit **exister et être importé** ; `App` **assemble et route**, les **pages** contiennent le contenu.
-101
View File
@@ -1,101 +0,0 @@
# 🔧 Correction — ton total de likes : le **recalculer**, pas le « mâter »
Ton total monte bien quand on clique, mais il y a un souci de fond. Chaque bouton fait ça :
```jsx
// App.jsx
onLike={() => setTotal((t) => t + 1)}
// Carte.jsx
<button onClick={props.onLike}>❤️ J'aime</button>
```
Donc chaque clic **ajoute 1 à un compteur `total`** qui vit tout seul dans son coin. Le problème : ce `total` **n'est relié à rien**. Tu ne sais pas combien de likes a **chaque** trilogie, tu ne peux pas « dé-liker », et si tu filtres/retires une carte, le total devient faux. Le total est **inventé par accumulation**, au lieu d'être **calculé à partir des vraies données**.
> 🧠 **Le principe React à retenir :** on garde **une seule source de vérité**, et on **recalcule** le reste à partir d'elle. Ici, la vérité, ce sont **les likes de chaque trilogie**. Le total n'est pas une donnée à part : c'est **la somme** des likes — donc on le **recalcule**, on ne le mute pas.
---
## 🎯 L'idée de la correction
1. La **source de vérité** = un like **par trilogie**, rangé dans le **parent** (`App`).
2. Chaque `Carte` reçoit **son** nombre de likes + une fonction pour **incrémenter le sien**.
3. Le **total** = la **somme** de tous les likes → **recalculé** à chaque affichage.
Comme ça : pas de double comptage, le total est **toujours juste**, et tu pourrais même afficher les likes par carte.
---
## 🪜 Étape par étape
### Étape 1 — ranger les likes dans le parent (un objet `{ id: nombre }`)
```jsx
function App() {
const [likes, setLikes] = useState({}); // ex. { 1: 3, 4: 1 } = 3 likes pour la trilo 1, 1 pour la 4
// incrémente le like d'UNE trilogie précise (par son id)
function liker(id) {
setLikes((prev) => ({ ...prev, [id]: (prev[id] || 0) + 1 }));
}
// ...
}
```
> 🔎 `setLikes((prev) => ({ ...prev, [id]: (prev[id] || 0) + 1 }))` se lit : « recopie tous les likes existants (`...prev`), et remplace **juste** celui de cette `id` par sa valeur + 1 ». Le `(prev[id] || 0)` = « 0 si cette trilo n'a pas encore de like ».
### Étape 2 — recalculer le total (la somme)
```jsx
const total = Object.values(likes).reduce((somme, n) => somme + n, 0);
```
> 🔎 `Object.values(likes)` = la liste des nombres (`[3, 1]`), et `.reduce(...)` les **additionne**. Tu reconnais le `reduce` — ici il **recalcule** le total à chaque rendu, à partir de la vérité. Plus aucun risque qu'il soit faux.
### Étape 3 — donner à chaque carte SON like + sa fonction
```jsx
{filtrees.map((trilo) => (
<Carte
key={trilo.id}
titre={trilo.nom}
niveaux={trilo.niveaux}
likes={likes[trilo.id] || 0} // SON nombre de likes
onLike={() => liker(trilo.id)} // incrémente SON like à elle
/>
))}
```
### Étape 4 — la `Carte` affiche son like et appelle la fonction
```jsx
function Carte(props) {
return (
<div className="carte">
<h2>{props.titre}</h2>
<p>{props.niveaux}</p>
<button onClick={props.onLike}>❤️ {props.likes}</button>
</div>
);
}
```
**✅ Test :** chaque carte affiche **son** nombre de likes ; le total en haut = la **somme** et reste juste, quoi qu'il arrive.
---
## 🧹 Pendant que tu y es : tu affiches tes cartes **deux fois**
Dans ton `App.jsx` actuel, il y a **deux** `map` qui créent des `<Carte>` :
- un premier (`trilogies.map`) avec `onLike` mais `nom=...` (pas de `titre`),
- un second (`filtrees.map`) avec `titre`/`niveaux` mais **sans** `onLike`.
Du coup tes trilogies s'affichent en double, et le bouton like n'est pas sur les bonnes cartes. **Garde un seul `map`** : celui sur **`filtrees`** (pour que la recherche marche), et passe-lui **toutes** les props (`titre`, `niveaux`, `likes`, `onLike`). Et dans `Carte`, garde **un seul** `<h2>` (tu as `props.titre` **et** `props.nom` → choisis `titre`).
> 🧭 Rappel : `App` = assembleur. Une seule liste, une seule source de vérité.
---
## ✅ Récap
- [ ] Les likes vivent dans `App`, **par trilogie** (`useState({})`)
- [ ] Une fonction `liker(id)` qui incrémente **une** trilogie
- [ ] Le **total recalculé** avec `Object.values(likes).reduce(...)` (au lieu d'un `total` muté)
- [ ] Chaque `Carte` reçoit `likes={...}` + `onLike={() => liker(id)}`
- [ ] Un **seul** `map` (sur `filtrees`), une `Carte` avec un seul `<h2>`
> 💬 La phrase à garder : **une seule source de vérité, on recalcule le reste.** Un total, un compteur affiché, une moyenne… ça se **dérive** des données, ça ne se stocke pas à part (sinon ça finit par se désynchroniser).
+114
View File
@@ -0,0 +1,114 @@
# 🔧 Correction — le total de likes ne baisse pas quand on supprime un jeu
Avant la correction, prenons 5 minutes pour **vraiment** comprendre la ligne qui calcule le total, parce que c'est elle qui est au cœur du bug :
```jsx
const totalLikes = Object.values(likes).reduce((s, n) => s + n, 0);
```
---
## 🧠 D'abord : c'est quoi `.reduce` ?
`reduce` sert à **transformer un tableau en UNE seule valeur** (ici : la somme de tous les likes).
Ton objet `likes` ressemble à ça (la clé = l'id du jeu, la valeur = son nombre de likes) :
```js
likes = { "1": 3, "2": 1, "4": 2 }
```
**Étape 1 — `Object.values(likes)`** prend seulement les **valeurs** et en fait un tableau :
```js
Object.values(likes) // → [3, 1, 2]
```
**Étape 2 — `.reduce(...)`** parcourt ce tableau et **accumule** au fur et à mesure.
```jsx
[3, 1, 2].reduce((s, n) => s + n, 0)
// ▲ ▲ ▲
// | | └─ valeur de DÉPART de l'accumulateur (0)
// | └─ n = l'élément en cours
// └─ s = l'accumulateur (ce qu'on a additionné jusqu'ici)
```
Déroulé, pas à pas — imagine une **cagnotte** `s` qui commence à 0 et dans laquelle on verse chaque nombre :
| tour | `s` (avant) | `n` (élément) | `s + n` (après) |
|---|---|---|---|
| départ | — | — | **0** |
| 1 | 0 | 3 | 3 |
| 2 | 3 | 1 | 4 |
| 3 | 4 | 2 | **6** |
→ `totalLikes` vaut **6**.
> 💡 À retenir : `reduce(callback, valeurDeDépart)`. Le `callback` reçoit **(accumulateur, élément)** et renvoie le **nouvel** accumulateur. Le `0` à la fin, c'est la valeur de départ (une somme commence à 0 ; un produit commencerait à 1).
> 🔎 C'est exactement la même idée que ta moyenne d'année :
> `jeux.reduce((s, j) => s + Number(j.annee), 0)` → là, `s` accumule les **années** de chaque jeu `j`.
---
## 🐛 Le bug : supprimer un jeu n'enlève pas ses likes du total
Regarde ta fonction `supprimer` :
```jsx
function supprimer(id) {
setJeux(jeux.filter((j) => j.id !== id)); // ✅ enlève le JEU
}
```
Tu retires bien le jeu du tableau `jeux`… mais **tu ne touches pas à l'objet `likes`**. Donc l'entrée du jeu supprimé (par ex. `"2": 1`) **reste** dans `likes`.
Et comme `totalLikes` fait `Object.values(likes).reduce(...)`, il **continue d'additionner les likes d'un jeu qui n'existe plus**. D'où le total qui ne descend pas. 🎯
C'est un cas classique : tu as **deux objets à garder synchronisés** (`jeux` et `likes`). Si tu supprimes dans l'un, tu dois supprimer dans l'autre.
---
## ✅ La correction : supprimer le jeu **ET** ses likes
`supprimer` doit aussi modifier `likes`. Or `likes`/`setLikes` vivent dans `App`. Le plus simple : **faire passer `setLikes` en props** jusqu'à `Accueil` (comme tu passes déjà `setJeux`).
### 1. Dans `App.jsx`, ajoute `setLikes` aux props d'`Accueil`
```jsx
<Accueil
jeux={jeux}
setJeux={setJeux}
likes={likes}
setLikes={setLikes} {/* 👈 en plus */}
onLike={liker}
/>
```
### 2. Dans `Accueil.jsx`, récupère-le et complète `supprimer`
```jsx
function Accueil({ jeux, setJeux, likes, setLikes, onLike }) { // 👈 setLikes ajouté
// ...
function supprimer(id) {
setJeux(jeux.filter((j) => j.id !== id)); // enlève le jeu
setLikes((prev) => {
const copie = { ...prev }; // une COPIE (on ne modifie jamais l'ancien objet)
delete copie[id]; // on retire l'entrée de ce jeu
return copie; // on renvoie le nouvel objet
});
}
}
```
> 🧩 Pourquoi `{ ...prev }` puis `delete` ? Même règle d'**immutabilité** que partout en React : on ne modifie pas `prev` directement, on en fait une **copie**, on enlève la clé de la copie, et on renvoie la copie. React ne réaffiche que si on lui donne un **nouvel** objet.
**✅ Test :** like un jeu 3 fois (total = 3) → supprime ce jeu → le total **retombe** à 0, et le Hall of Fame ne le montre plus. F5 → toujours cohérent.
---
## 🧹 Petit bonus repéré au passage
Tes 5 jeux du `zelda.json` ont un champ **`console`** (`"console": "Game Boy Color"`), mais ton formulaire crée les nouveaux jeux avec un champ **`plateforme`**. Du coup :
- sur la carte tu affiches `props.plateforme` → pour les 5 jeux d'origine ça affiche **`undefined`** (ils n'ont pas `plateforme`, ils ont `console`) ;
- dans `Detail.jsx` tu affiches `jeu.console` → là c'est l'inverse, ça marche pour les 5 d'origine mais pas pour les jeux ajoutés.
👉 Choisis **un seul** nom de champ (par ex. `console` partout) et utilise-le aux 3 endroits (le `nouveau` du formulaire, `Carte`, `Detail`). Une donnée = un seul nom, du début à la fin.
> 💡 À retenir : quand deux états décrivent la même chose (un jeu et ses likes), une action sur l'un doit se répercuter sur l'autre — sinon ils **divergent**. Et un même champ doit porter **le même nom** partout dans l'appli.
-81
View File
@@ -1,81 +0,0 @@
# 🔧 Correction — ton tri ne change rien à l'affichage
Bonne nouvelle : ton like est **réparé** (likes dans le parent, `liker(id)`, total recalculé) et ton **formulaire d'ajout marche** 👏. Le tri, lui, semble « ne rien faire ». C'est un bug très instructif.
---
## 🔍 Pourquoi le tri n'a aucun effet
Tu calcules **deux** listes dans `App` :
```jsx
const affichees = [...trilogies].sort((a, b) => { ... }); // ✅ triée... mais jamais utilisée
const filtrees = trilogies.filter((t) => ...); // filtrée (PAS triée)
```
Et dans le rendu, tu affiches… **`filtrees`** :
```jsx
{filtrees.map((trilo) => ( ... ))} // ❌ tu affiches la liste filtrée, pas la triée
```
Donc quand tu changes le `<select>`, l'état `tri` change bien, React **recalcule `affichees`**… mais comme tu n'affiches **jamais** `affichees`, rien ne bouge à l'écran. Ta liste triée part à la poubelle.
> 🧠 **La règle :** la valeur que tu **calcules** doit être celle que tu **affiches**. Si tu tries `affichees` mais que tu `.map()` `filtrees`, le tri est invisible.
---
## ✅ La correction : UNE seule liste, filtrée **puis** triée
L'ordre logique est : **filtrer** (recherche) → **trier** → **afficher**. On enchaîne les deux sur la même liste :
```jsx
const affichees = [...trilogies]
.filter((t) => t.nom.toLowerCase().includes(recherche.toLowerCase()))
.sort((a, b) => {
if (tri === "nom") return a.nom.localeCompare(b.nom);
if (tri === "likes") return (likes[b.id] || 0) - (likes[a.id] || 0);
return 0;
});
```
Puis tu **supprimes** l'ancien `const filtrees = ...` (devenu inutile) et tu affiches **`affichees`** :
```jsx
{affichees.length === 0 && <p>Aucune trilogie trouvée</p>}
{affichees.map((trilo) => (
<Carte
key={trilo.id}
titre={trilo.nom}
niveaux={trilo.niveaux}
likes={likes[trilo.id] || 0}
onLike={() => liker(trilo.id)}
/>
))}
```
> 🔎 `[...trilogies]` fait une **copie** avant de trier (sinon `.sort()` modifierait l'original sur place). On chaîne `.filter().sort()` : chacune renvoie une nouvelle liste, donc on peut les enchaîner tranquillement.
**✅ Test :** tape dans la recherche → la liste se réduit ; change le `<select>` (Nom / Plus likées) → l'**ordre** change ; like une carte puis trie par « Plus likées » → elle remonte.
---
## 🧹 Détail mineur
Tu as enveloppé ton `<select>` dans un fragment `<>…</>` :
```jsx
<>
<select ...>...</select>
</>
```
Ce n'est pas faux, mais inutile ici : le fragment `<>…</>` ne sert que pour grouper **plusieurs** éléments. Pour un `<select>` seul, tu peux l'enlever.
---
## ✅ Récap
- [ ] Une **seule** liste `affichees` = `trilogies` **filtrée puis triée**
- [ ] Supprimer l'ancien `filtrees`
- [ ] Afficher **`affichees`** dans le `.map()` (et le message « aucune trilogie »)
- [ ] (Optionnel) enlever le fragment inutile autour du `<select>`
> 💬 La phrase à garder : **on affiche la liste qu'on a vraiment transformée.** Filtre → trie → `map`, sur la même variable.
+98
View File
@@ -0,0 +1,98 @@
# 🔧 Correction — le tri + 2 bugs discrets
Ton formulaire d'ajout est **bon** (immutabilité avec `[...jeux, nouveau]`, champs vidés, `preventDefault`) 👏. Trois choses à régler : le **tri**, et deux petits bugs que tu n'as peut-être pas repérés.
---
## 🐛 Bug 1 — tu as nommé une variable `console` (à éviter absolument)
```jsx
const [console, setConsole] = useState("");
```
`console`, c'est **déjà** un objet de JavaScript (celui de `console.log` !). En créant une variable qui s'appelle `console`, tu **l'écrases** dans tout ton composant : si un jour tu écris `console.log(...)` dans `App`, tu auras `console.log is not a function` (car `console` vaut maintenant `""`).
### ✅ Renomme-la
```jsx
const [plateforme, setPlateforme] = useState("");
```
…et adapte partout où tu l'utilises (`value={plateforme}`, `setPlateforme(...)`, et `console: plateforme` dans l'objet `nouveau`).
> 💡 **À retenir :** évite de nommer tes variables comme des mots **déjà pris** par JavaScript : `console`, `document`, `window`, `event`… Tu risques d'écraser un outil du langage sans t'en rendre compte.
---
## 🐛 Bug 2 — `BarreRecherche` ne reçoit pas la bonne prop
Ton composant attend une prop **`valeur`** :
```jsx
function BarreRecherche({ valeur, onChange }) { ... value={valeur} ... }
```
Mais dans `App` tu lui passes **`value`** :
```jsx
<BarreRecherche value={recherche} onChange={...} /> // ❌ "value", pas "valeur"
```
Du coup, à l'intérieur, `valeur` est `undefined` → le champ n'est plus **contrôlé** par ton état.
### ✅ Mets le même nom des deux côtés
```jsx
<BarreRecherche valeur={recherche} onChange={(e) => setRecherche(e.target.value)} />
```
> 🧩 **À retenir :** une prop, c'est comme une étiquette : le nom que tu mets dans `App` (`valeur=`) doit être **exactement** celui que le composant lit (`{ valeur }`). Sinon, le composant reçoit `undefined`.
---
## 🔀 Le tri (Module J) — comment le faire marcher
Pour trier, il te faut **trois** choses, et bien les **enchaîner** : filtrer → trier → afficher.
### Étape 1 — un état pour le critère de tri
```jsx
const [tri, setTri] = useState("titre");
```
### Étape 2 — un `<select>` contrôlé (à côté de la barre de recherche, pas « dans » le formulaire d'ajout)
```jsx
<select value={tri} onChange={(e) => setTri(e.target.value)}>
<option value="titre">Titre (A→Z)</option>
<option value="annee">Année</option>
</select>
```
### Étape 3 — filtrer PUIS trier, sur la même liste
```jsx
const affiches = [...jeux]
.filter((j) => j.titre.toLowerCase().includes(recherche.toLowerCase()))
.sort((a, b) => {
if (tri === "titre") return a.titre.localeCompare(b.titre);
if (tri === "annee") return a.annee - b.annee;
return 0;
});
```
### Étape 4 — afficher **`affiches`** (et non `filtres`)
```jsx
{affiches.length === 0 && <p>Aucun jeu trouvé 😕</p>}
{affiches.map((jeu) => (
<Carte key={jeu.id} gameId={jeu.id} titre={jeu.titre} console={jeu.console}
annee={jeu.annee} Type={jeu.Type} onLike={() => setTotal((t) => t + 1)} />
))}
```
Tu peux **supprimer** l'ancien `const filtres = ...` : `affiches` fait déjà le filtre **et** le tri.
> 🧠 **Le piège classique du tri** (Guillaume est tombé dedans aussi !) : on calcule une liste triée mais on **affiche l'autre**. La règle : **on affiche la liste qu'on a vraiment transformée.** Ici, `affiches` fait filtre + tri, donc on `.map()` `affiches`.
> 🔎 Le tri **n'a rien à faire « dans le formulaire »** : le `<select>` est un élément à part (au-dessus de la liste), pas dans le `<form>` d'ajout. Garde-les séparés.
**✅ Test :** change le `<select>` → l'ordre des cartes change ; combiné avec la recherche, les deux marchent ensemble.
---
## ✅ Récap
- [ ] Renommer l'état `console` → `plateforme` (mot réservé !)
- [ ] `BarreRecherche` : passer `valeur=` (pas `value=`)
- [ ] Tri : état `tri` + `<select>` contrôlé + liste **filtrée puis triée** (`affiches`)
- [ ] Afficher **`affiches`** dans le `.map()`, supprimer l'ancien `filtres`
> 💬 À garder : un nom de variable **jamais** pris par JS ; une prop au **même nom** des deux côtés ; et **on affiche la liste qu'on a transformée**.
-109
View File
@@ -1,109 +0,0 @@
# 🔧 Correction — un crash caché + le Versus à finir
Beau commit : le boss est bien rebranché (tu lis `trilogies` via les props, plus de liste locale en double), le **podium** s'affiche, le bouton **🗑️ supprimer** marche. Trois choses à régler, dans l'ordre d'urgence.
---
## 1. 💥 Le bug qui fait planter la page (à corriger en premier)
Regarde ton bloc Versus, en haut d'`Accueil` :
```jsx
const [a, setA] = useState(trilogies[0].id);
const gagnante =
likesA === likesB ? "Égalité"
: likesA > likesB ? trilogies.find((t) => t.id === Number(a)).nom
: trilogies.find((t) => t.id === Number(b)).nom;
```
Ce calcul tourne **à chaque affichage** de la page. Le piège est dans `.find(...).nom` :
> `trilogies.find(...)` renvoie la trilogie trouvée… **ou `undefined` si elle n'existe pas**. Et `undefined.nom` plante (« Cannot read properties of undefined »), ce qui fait **écran blanc**.
Le scénario qui déclenche le crash est juste sous ton nez : `a` vaut l'id de la **1ʳᵉ trilogie** au démarrage. Si tu **supprimes cette trilogie** (avec ton tout nouveau bouton 🗑️), `find` ne la trouve plus → `undefined.nom` → 💥. Ton module Supprimer fait donc planter ton module Versus.
### ✅ La correction : ne jamais supposer que `find` a trouvé
Récupère d'abord les trilogies, **puis** vérifie qu'elles existent avant de lire `.nom` :
```jsx
const triloA = trilogies.find((t) => t.id === Number(a));
const triloB = trilogies.find((t) => t.id === Number(b));
const gagnante =
!triloA || !triloB ? "—" // une des deux a été supprimée
: likesA === likesB ? "Égalité"
: likesA > likesB ? triloA.nom : triloB.nom;
```
> 🔎 Autre écriture possible avec l'« optional chaining » : `trilogies.find(...)?.nom`. Le `?.` veut dire « si c'est `undefined`, ne lis pas `.nom`, renvoie `undefined` au lieu de planter ». Très pratique à retenir.
> ⚠️ Même piège au montage : `useState(trilogies[1].id)` planterait s'il y avait **moins de 2 trilogies** au démarrage. Avec tes 5 trilogies de départ, c'est OK — mais garde en tête que `trilogies[1]` n'existe pas toujours.
---
## 2. Le Versus n'est affiché nulle part
Tu calcules `a`, `b`, `likesA`, `likesB`, `gagnante`… mais il n'y a **aucun `<select>` ni verdict dans le JSX**, et `setA`/`setB` ne sont jamais appelés. Résultat : du code mort (et des warnings ESLint « `setA` is never used »).
Deux choix : **finir l'interface**, ou **supprimer le bloc**. Pour le finir, ajoute dans le `return` deux menus + le verdict :
```jsx
<div className="versus">
<select value={a} onChange={(e) => setA(e.target.value)}>
{trilogies.map((t) => (
<option key={t.id} value={t.id}>{t.nom}</option>
))}
</select>
<select value={b} onChange={(e) => setB(e.target.value)}>
{trilogies.map((t) => (
<option key={t.id} value={t.id}>{t.nom}</option>
))}
</select>
<p>🏆 Gagnante : {gagnante}</p>
</div>
```
> 🔎 La valeur d'un `<select>` est **une chaîne** ("1", "2"…). C'est pour ça que tu fais `Number(a)` dans le `find` : pour comparer un nombre (`t.id`) à un nombre. Garde bien ce `Number(...)`.
**Test :** change les deux menus → le verdict se met à jour. Supprime une des deux trilogies choisies → ça affiche « — » au lieu de planter (grâce au point 1).
---
## 3. Supprimer une trilogie ne retire pas ses likes du total
Ta fonction supprime bien la trilogie, mais **laisse ses likes** dans l'objet `likes` :
```jsx
function supprimer(id) {
setTrilogies(trilogies.filter((t) => t.id !== id)); // ✅ enlève la trilogie
// ❌ mais likes[id] reste → le "Total de likes" ne baisse pas
}
```
Comme `total = Object.values(likes).reduce((s, n) => s + n, 0)`, le total continue d'additionner les likes d'une trilogie qui n'existe plus.
### ✅ La correction : supprimer aussi dans `likes`
Tu as déjà `setLikes` dans les props, donc c'est direct :
```jsx
function supprimer(id) {
setTrilogies(trilogies.filter((t) => t.id !== id));
setLikes((prev) => {
const copie = { ...prev }; // une COPIE (on ne modifie jamais l'ancien objet)
delete copie[id]; // on retire l'entrée de cette trilogie
return copie; // on renvoie le nouvel objet
});
}
```
> 🧩 `{ ...prev }` puis `delete` : on copie l'objet, on enlève la clé de la **copie**, on renvoie la copie. Même règle d'**immutabilité** que partout — React ne réaffiche que si on lui donne un **nouvel** objet.
> 💡 La leçon générale : quand deux états décrivent la même chose (une trilogie **et** ses likes), une action sur l'un doit se répercuter sur l'autre, sinon ils **divergent**.
**Test :** like une trilogie 3 fois (total = 3) → supprime-la → le total **retombe** à 0, et elle disparaît du podium.
---
## 🧹 Petits restes
- `console.log("test")` en haut d'`Accueil` → à enlever.
- Le fragment `<>...</>` autour du seul `<select>` ne sert à rien → tu peux laisser le `<select>` seul.
- **Dark mode** : tu as ajouté le CSS `.sombre` 👍 mais rien ne l'active encore. Il te manque un état `const [sombre, setSombre] = useState(false)`, la classe sur ton `<div className={sombre ? "body sombre" : "body"}>`, et un bouton `onClick={() => setSombre(!sombre)}`. (Détaillé dans `ENONCE_final_boss.md`, module P.)
> 💡 À retenir du jour : **un `.find` peut ne rien trouver** → ne lis jamais `.quelquechose` dessus sans vérifier (ou avec `?.`). Et garde tes états synchronisés.
+135 -105
View File
@@ -1,147 +1,165 @@
# 🏁 Énoncé FINAL — « Boss de fin »
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.
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.
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* | ⭐⭐⭐ |
| **L** | Page détail **complète** (données centralisées dans `App`) — *obligatoire* | ⭐⭐⭐ |
| M | Bouton « au hasard » (`useNavigate`) | ⭐⭐ |
| N | Le podium des trilogies les plus likées | ⭐⭐ |
| O | Bouton « supprimer » une trilogie | ⭐⭐ |
| 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 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 | ⭐⭐⭐ |
| 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 : faire suivre TOUT jusqu'au détail (obligatoire)
## 🔑 Module L — Le boss : centraliser les données dans `App` (obligatoire)
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.
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 : la donnée doit vivre **au-dessus** des deux pages, donc dans `App`, qui la passe ensuite **en props** aux deux 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 state dans `App.jsx`
### L.1 — Remonter le `fetch` et les états dans `App.jsx`
```jsx
import { useState } from "react";
import { useState, useEffect } from "react";
import { Routes, Route } from "react-router-dom";
import { donneesInitiales } from "./data";
import Header from "./components/Header";
import Footer from "./components/Footer";
import Accueil from "./pages/Accueil";
import Details from "./pages/Details";
import Detail from "./pages/Detail";
function App() {
const [trilogies, setTrilogies] = useState(donneesInitiales);
const [likes, setLikes] = useState({});
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 (
<Routes>
<Route path="/" element={
<Accueil trilogies={trilogies} setTrilogies={setTrilogies}
likes={likes} setLikes={setLikes} />
} />
<Route path="/trilogie/:id" element={
<Details trilogies={trilogies} likes={likes} />
} />
</Routes>
<>
<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={<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={<Detail jeux={jeux} />}`. C'est ça qui « branche » la donnée sur les deux pages.
### 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 }) {
// ajouter() fait toujours setTrilogies([...trilogies, nouvelle])
// liker() fait toujours setLikes(prev => ({...prev, [id]: (prev[id]||0)+1}))
}
```
> 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.3 — `Details` affiche la vraie trilogie avec ses likes
### 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
function Details({ trilogies, likes }) {
import { useParams, Link } from "react-router-dom";
function Detail({ jeux, likes }) {
const { id } = useParams();
const trilogie = trilogies.find((t) => t.id === Number(id));
if (!trilogie) return <p>Introuvable — <Link to="/">retour</Link></p>;
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>{trilogie.nom}</h1>
<p>Niveaux : {trilogie.niveaux}</p>
<p>❤️ Likes : {likes[trilogie.id] || 0}</p>
<h1>{jeu.titre}</h1>
<p>{jeu.console} — {jeu.annee} — {jeu.Type}</p>
<p>❤️ Likes : {likes[jeu.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 « ❤️ Likes : 3 ».
**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 des composants qui en ont besoin. C'est le réflexe d'archi central 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 — 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 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";
const navigate = useNavigate();
function hasard() {
const i = Math.floor(Math.random() * trilogies.length);
navigate(`/trilogie/${trilogies[i].id}`);
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…).
**Test :** clique plusieurs fois → tu tombes sur des trilogies différentes.
**Test :** clique plusieurs fois → tu tombes sur des jeux différents.
---
## Module N — Le podium
## Module N — Le Hall of Fame
En haut de l'accueil, le Top 3 des trilogies les plus likées, avec des médailles.
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 podium = [...trilogies]
const top = [...jeux]
.sort((a, b) => (likes[b.id] || 0) - (likes[a.id] || 0))
.slice(0, 3);
// <div className="podium">
// {podium.map((t, index) => (
// <p key={t.id}>{medailles[index]} {t.nom} — {likes[t.id] || 0} ❤️</p>
// <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((t, index) => ...)`, c'est la position dans la liste → parfait pour 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 :** like beaucoup une trilogie → elle grimpe sur le podium en direct.
**Test :** like beaucoup un jeu → il grimpe en haut du Hall of Fame en direct.
---
## Module O — Supprimer une trilogie
## Module O — Supprimer un jeu
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));
setJeux(jeux.filter((j) => j.id !== id));
}
// sur la Carte : <button onClick={() => props.onDelete()}>🗑️</button>
// et dans Accueil : onDelete={() => supprimer(trilo.id)}
// 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).
@@ -152,7 +170,7 @@ function supprimer(id) {
## 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).
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.
```jsx
const [sombre, setSombre] = useState(
@@ -162,7 +180,7 @@ useEffect(() => {
localStorage.setItem("theme", sombre ? "sombre" : "clair");
}, [sombre]);
// <div className={sombre ? "body sombre" : "body"}>
// <div className={sombre ? "app sombre" : "app"}>
// <button onClick={() => setSombre(!sombre)}>{sombre ? "☀️" : "🌙"}</button>
```
```css
@@ -178,46 +196,72 @@ useEffect(() => {
## Module Q — Mode Versus
Deux `<select>` pour choisir deux trilogies ; on compare leurs likes et on affiche la gagnante.
Deux `<select>` pour choisir deux jeux ; on compare leurs likes (ou leurs années) et on affiche le gagnant.
```jsx
const [a, setA] = useState(trilogies[0].id);
const [b, setB] = useState(trilogies[1].id);
const [a, setA] = useState(jeux[0].id);
const [b, setB] = useState(jeux[1].id);
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);
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 `trilogies.map(t => <option value={t.id}>{t.nom}</option>)`, puis un `<p>` qui annonce la gagnante.
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.
> Pense à `Number(...)` quand tu relis la valeur d'un `<select>` : elle revient en chaîne.
> 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 — Statistiques
## 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 nbTrilogies = trilogies.length;
const nbJeux = jeux.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>
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 tous les likes.
> `reduce` réduit un tableau à **une seule** valeur en accumulant. Ici on additionne likes et années.
**Test :** ajoute/like des trilogies → les chiffres bougent.
**Test :** ajoute/like des jeux → les chiffres bougent.
---
## Module S — Page « 404 »
## Module T — 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.
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";
@@ -230,19 +274,19 @@ function 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.
**Test :** tape une URL bidon → la page 404 s'affiche.
---
## Module T — Tri ascendant ↔ descendant
## 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 affichees = [...maListeTriee];
if (ordre === "desc") affichees.reverse();
const affiches = [...maListeTriee];
if (ordre === "desc") affiches.reverse();
// <button onClick={() => setOrdre(ordre === "asc" ? "desc" : "asc")}>
// {ordre === "asc" ? "A→Z" : "Z→A"}
// </button>
@@ -252,7 +296,7 @@ if (ordre === "desc") affichees.reverse();
---
## Module U — Custom hook `useLocalStorage` (refacto)
## 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**.
@@ -271,35 +315,21 @@ export function useLocalStorage(cle, valeurParDefaut) {
return [valeur, setValeur];
}
```
Puis : `const [likes, setLikes] = useLocalStorage("likes", {});` — et les likes survivent au refresh, gratuitement.
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 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.
**Test :** like des jeux → recharge → les likes sont toujours là, mais avec deux fois moins de code.
---
## ✅ 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
- [ ] 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` · é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**.
`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.
+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)`.
+155
View File
@@ -0,0 +1,155 @@
# 🗡️ Énoncé React #3 — formulaire, tri & plusieurs pages
(À attaquer une fois `BarreRecherche` et le Badge réparés via `CORRECTION_barre_recherche.md`.)
Tu maîtrises déjà : composants, props, état, **lifting state up**, `useEffect`, **`fetch`**. On monte d'un cran avec **3 nouveaux gros morceaux**. Explications + mini-exemples ; à toi d'appliquer sur tes jeux Zelda.
| Module | Tu apprends | Niveau |
|---|---|---|
| I | **Formulaire d'ajout** : modifier une liste en state | ⭐⭐ |
| J | **Trier** la liste (select contrôlé + `sort`) | ⭐⭐ |
| K | **Plusieurs pages** avec React Router | ⭐⭐⭐ |
---
## 📝 Module I — Ajouter un jeu via un formulaire
Ta liste `jeux` est **déjà** un état (tu l'as remplie avec `fetch`), parfait : on peut donc y **ajouter**.
### Un formulaire contrôlé
```jsx
const [titre, setTitre] = useState("");
const [annee, setAnnee] = useState("");
function ajouter(e) {
e.preventDefault(); // empêche le rechargement de page
if (titre.trim() === "") return; // on n'ajoute pas du vide
const nouveau = { id: Date.now(), titre, annee: Number(annee), console: "?", Type: "3d" };
setJeux([...jeux, nouveau]); // 👈 NOUVEAU tableau = ancien + le nouveau
setTitre(""); // on vide les champs
setAnnee("");
}
return (
<form onSubmit={ajouter}>
<input value={titre} onChange={(e) => setTitre(e.target.value)} placeholder="Titre du jeu" />
<input value={annee} onChange={(e) => setAnnee(e.target.value)} placeholder="Année" />
<button type="submit">Ajouter</button>
</form>
);
```
> 🧩 **Le point clé :** on ne « pousse » pas dans le tableau existant (`jeux.push(...)` ❌). On crée un **nouveau tableau** avec `[...jeux, nouveau]` et on le donne à `setJeux`. React ne réaffiche que si on lui passe une **nouvelle** valeur. C'est l'**immutabilité** : on remplace, on ne modifie pas sur place.
**✅ Test :** remplis le formulaire, clique « Ajouter » → la carte apparaît, les champs se vident, la page ne recharge pas.
> 🚀 Bonus : un bouton « 🗑️ Supprimer » par carte → `setJeux(jeux.filter((j) => j.id !== id))` (un nouveau tableau **sans** celui-là).
---
## 🔀 Module J — Trier la liste
Un `<select>` **contrôlé** pour choisir le tri :
```jsx
const [tri, setTri] = useState("titre");
const affiches = [...filtres].sort((a, b) => {
if (tri === "titre") return a.titre.localeCompare(b.titre);
if (tri === "annee") return a.annee - b.annee;
return 0;
});
return (
<>
<select value={tri} onChange={(e) => setTri(e.target.value)}>
<option value="titre">Titre (A→Z)</option>
<option value="annee">Année</option>
</select>
{affiches.map((jeu) => ( /* ... ta Carte ... */ ))}
</>
);
```
> 🔎 `[...filtres]` = une **copie** (pour ne pas modifier l'original avec `sort`). L'ordre logique : on **filtre** (recherche) → on **trie** → on `.map()`.
**✅ Test :** change le tri → l'ordre des cartes change.
---
## 🧭 Module K — Plusieurs pages avec React Router
Jusqu'ici, **une seule page**. Pour une page **détail** par jeu (sa console, son année, ses likes…), on utilise **React Router**.
### K.1 — Installer
Dans le terminal, dans le dossier du projet :
```bash
npm install react-router-dom
```
### K.2 — Envelopper l'app (dans `main.jsx`)
```jsx
import { BrowserRouter } from "react-router-dom";
createRoot(document.getElementById("root")).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
```
### K.3 — Définir les routes (dans `App.jsx`)
```jsx
import { Routes, Route } from "react-router-dom";
function App() {
return (
<Routes>
<Route path="/" element={<Accueil />} />
<Route path="/jeu/:id" element={<Detail />} />
</Routes>
);
}
```
- `path="/"` → ta page d'accueil (la liste).
- `path="/jeu/:id"` → page **dynamique** : `:id` change selon le jeu.
### K.4 — Des liens (au lieu de `<a href>`)
Dans ta `Carte` :
```jsx
import { Link } from "react-router-dom";
<Link to={`/jeu/${props.gameId}`}>Voir les détails</Link>
```
> 🔎 `<Link>` navigue **sans recharger** la page. `<a href>` rechargerait tout.
### K.5 — Lire l'id dans la page détail
```jsx
import { useParams } from "react-router-dom";
function Detail() {
const { id } = useParams(); // récupère le :id de l'URL
// retrouve le bon jeu par son id, et affiche-le
return <h1>Détails du jeu {id}</h1>;
}
```
**✅ Test :** clique « Voir les détails » → l'URL devient `/jeu/2`, la page détail s'affiche **sans rechargement**. Le bouton « précédent » du navigateur ramène à la liste.
> 🧩 **Les 4 briques de React Router :** `<BrowserRouter>` (enveloppe), `<Routes>/<Route>` (quelle page pour quelle URL), `<Link>` (naviguer), `useParams()` (lire un morceau dynamique de l'URL). C'est la base de **tous** les sites multi-pages en React.
---
## ✅ Checklist
- [ ] I — formulaire d'ajout (immutabilité avec `[...jeux, ...]`)
- [ ] J — tri via un `<select>` contrôlé
- [ ] K — React Router : accueil + page détail (`Link`, `useParams`)
## 🚀 Encore plus loin
- Bouton **supprimer** (`filter`) et **éditer** un jeu.
- Un **custom hook** `useLocalStorage` pour ranger la logique de persistance des likes.
- Une vraie **mise en page** CSS de la page détail (jaquette du jeu, etc.).
## 🧠 Nouveaux concepts
Liste en état + **immutabilité** (`[...]`, `filter`) · formulaire contrôlé + `onSubmit`/`preventDefault` · tri (`select` contrôlé + `sort`) · **React Router** (`BrowserRouter`, `Routes`, `Route`, `Link`, `useParams`).
Bloquée ? Note ce que tu as tapé + le message exact (console F12). On débloque.
-164
View File
@@ -1,164 +0,0 @@
# 🎮 Énoncé React #3 — formulaire, tri & plusieurs pages
(À attaquer une fois ton projet réparé via `CORRECTION_carte_likes.md`.)
Tu sais maintenant : composants, props, état local, **lifting state up**, `useEffect`. On monte d'un cran avec **3 nouveaux gros morceaux**. Comme avant : explications + mini-exemples, à toi d'appliquer sur tes trilogies.
| Module | Tu apprends | Niveau |
|---|---|---|
| I | **Formulaire d'ajout** : modifier une liste en state | ⭐⭐ |
| J | **Trier** la liste (select contrôlé + `sort`) | ⭐⭐ |
| K | **Plusieurs pages** avec React Router | ⭐⭐⭐ |
---
## 📝 Module I — Ajouter une trilogie via un formulaire
### I.1 — D'abord, rendre la liste **modifiable**
Aujourd'hui `trilogies` est un `const` figé. Pour pouvoir **ajouter**, il doit devenir un **état** :
```jsx
const donneesInitiales = [ /* tes 5 trilogies */ ];
function App() {
const [trilogies, setTrilogies] = useState(donneesInitiales);
// ...
}
```
### I.2 — Un formulaire contrôlé
```jsx
const [nom, setNom] = useState("");
const [niveaux, setNiveaux] = useState("");
function ajouter(e) {
e.preventDefault(); // empêche le rechargement de page
if (nom.trim() === "") return; // on n'ajoute pas du vide
const nouvelle = { id: Date.now(), nom, niveaux }; // id unique simple
setTrilogies([...trilogies, nouvelle]); // 👈 NOUVEAU tableau = ancien + la nouvelle
setNom(""); // on vide les champs
setNiveaux("");
}
return (
<form onSubmit={ajouter}>
<input value={nom} onChange={(e) => setNom(e.target.value)} placeholder="Nom de la trilogie" />
<input value={niveaux} onChange={(e) => setNiveaux(e.target.value)} placeholder="Niveaux" />
<button type="submit">Ajouter</button>
</form>
);
```
> 🧩 **Le point clé :** on ne « pousse » pas dans le tableau existant (`trilogies.push(...)` ❌). On crée un **nouveau tableau** avec `[...trilogies, nouvelle]` et on le donne à `setTrilogies`. React ne réaffiche que si on lui passe une **nouvelle** valeur. C'est la règle de l'**immutabilité** : on remplace, on ne modifie pas sur place.
**✅ Test :** remplis le formulaire, clique « Ajouter » → la carte apparaît en bas, les champs se vident, la page ne recharge pas.
> 🚀 Bonus : un bouton « 🗑️ Supprimer » sur chaque carte → `setTrilogies(trilogies.filter((t) => t.id !== id))` (un nouveau tableau **sans** celle-là).
---
## 🔀 Module J — Trier la liste
Un `<select>` **contrôlé** pour choisir le tri :
```jsx
const [tri, setTri] = useState("nom");
const affichees = [...trilogies].sort((a, b) => {
if (tri === "nom") return a.nom.localeCompare(b.nom);
if (tri === "likes") return (likes[b.id] || 0) - (likes[a.id] || 0);
return 0;
});
return (
<>
<select value={tri} onChange={(e) => setTri(e.target.value)}>
<option value="nom">Nom (A→Z)</option>
<option value="likes">Plus likées</option>
</select>
{affichees.map((trilo) => ( /* ... ta Carte ... */ ))}
</>
);
```
> 🔎 `[...trilogies]` = une **copie** (pour ne pas modifier l'original avec `sort`). Et tu combines avec ton **filtre** de recherche : on filtre **puis** on trie **puis** on `.map()`.
**✅ Test :** change le tri → l'ordre des cartes change.
---
## 🧭 Module K — Plusieurs pages avec React Router
Jusqu'ici tu as **une seule page**. Pour avoir une page **détail** par trilogie (avec ses niveaux, son nombre de likes…), on utilise **React Router**.
### K.1 — Installer
Dans le terminal, dans le dossier du projet :
```bash
npm install react-router-dom
```
### K.2 — Envelopper l'app (dans `main.jsx`)
```jsx
import { BrowserRouter } from "react-router-dom";
createRoot(document.getElementById("root")).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
```
### K.3 — Définir les routes (dans `App.jsx`)
```jsx
import { Routes, Route } from "react-router-dom";
function App() {
return (
<Routes>
<Route path="/" element={<Accueil />} />
<Route path="/trilogie/:id" element={<Detail />} />
</Routes>
);
}
```
- `path="/"` → ta page d'accueil (la liste).
- `path="/trilogie/:id"` → une page **dynamique** : le `:id` change selon la trilogie.
### K.4 — Des liens (au lieu de `<a href>`)
Dans ta `Carte`, un lien vers le détail :
```jsx
import { Link } from "react-router-dom";
<Link to={`/trilogie/${props.id}`}>Voir les détails</Link>
```
> 🔎 `<Link>` navigue **sans recharger** la page (c'est tout l'intérêt d'une « single page app »). `<a href>` rechargerait tout.
### K.5 — Lire l'id dans la page détail
```jsx
import { useParams } from "react-router-dom";
function Detail() {
const { id } = useParams(); // récupère le :id de l'URL
// retrouve la bonne trilogie par son id, et affiche-la
return <h1>Détails de la trilogie {id}</h1>;
}
```
**✅ Test :** clique « Voir les détails » sur une carte → l'URL devient `/trilogie/3` et la page détail s'affiche, **sans rechargement**. Le bouton « précédent » du navigateur te ramène à la liste.
> 🧩 **Les 4 briques de React Router :** `<BrowserRouter>` (enveloppe), `<Routes>/<Route>` (quelle page pour quelle URL), `<Link>` (naviguer), `useParams()` (lire un morceau dynamique de l'URL). C'est la base de **tous** les sites multi-pages en React.
---
## ✅ Checklist
- [ ] I — liste en `useState` + formulaire d'ajout (immutabilité avec `[...]`)
- [ ] J — tri via un `<select>` contrôlé
- [ ] K — React Router : accueil + page détail (`Link`, `useParams`)
## 🚀 Encore plus loin
- Bouton **supprimer** (`filter`) et **éditer** une trilogie.
- Un **custom hook** `useLocalStorage` pour ranger la logique de persistance.
- Une vraie **mise en page** CSS de la page détail (image de la trilogie, etc.).
## 🧠 Nouveaux concepts
Liste en état + **immutabilité** (`[...]`, `filter`) · formulaire contrôlé + `onSubmit`/`preventDefault` · tri (`select` contrôlé + `sort`) · **React Router** (`BrowserRouter`, `Routes`, `Route`, `Link`, `useParams`).
Bloqué ? Note ce que tu as tapé + le message exact (console F12). On débloque.
+323
View File
@@ -0,0 +1,323 @@
# 🗡️ Énoncé React #2 — ton site Zelda (version longue)
Bien joué Mélissa : tu as découpé en composants (`Header`, `Footer`, `Carte`, `Compteur`, et même un début de `Docs`), tu utilises `useState`… les bases sont là. 👌
Deux choses avant de foncer : ton `App.jsx` contient encore **tout le contenu d'exemple de Vite** (le gros bloc « Documentation / Connect with us ») — c'est le **fourre-tout** qu'on veut éviter. Et ton `Docs.jsx` est **cassé** (on le règle au Module 0). On va nettoyer, puis bâtir un **vrai site sur Zelda**, en apprenant **beaucoup de React**.
> 🎯 Ce document est une **série de modules** (0 → H). Fais-les **dans l'ordre**, teste à chaque fois. Je t'explique les **mécaniques** avec des mini-exemples ; à toi d'**appliquer sur ton thème Zelda**. Compte plusieurs séances — c'est normal.
| Module | Tu apprends | Niveau |
|---|---|---|
| 0 | Nettoyer le template + réparer `Docs.jsx` | ⭐ |
| A | Listes : `.map()` + `key` | ⭐ |
| B | Affichage conditionnel + extraire un `<Badge>` | ⭐ |
| C | État local par carte (`useState`) | ⭐ |
| D | Champ **contrôlé** + recherche/filtre + état vide | ⭐⭐ |
| E | **Faire remonter l'état** (lifting state up) + callbacks | ⭐⭐ |
| F | **`useEffect`** : titre de page + persistance `localStorage` | ⭐⭐ |
| G | **Charger les données avec `fetch`** (dans React) | ⭐⭐⭐ |
| H | Re-découpage propre (`<Liste>`, `<BarreRecherche>`) | ⭐⭐ |
---
## 🧹 Module 0 — Nettoyer + réparer `Docs.jsx`
### 0.1 — Vider le contenu d'exemple
Dans `App.jsx`, tout le bloc `<section id="next-steps">…</section>` (Documentation, Connect with us, liens Vite/Discord…) vient du **template de démarrage**. Supprime-le : on va mettre **ton** contenu Zelda à la place. Garde juste `<Header />`, `<Footer />`, et ce que tu ajouteras.
### 0.2 — Le bug de `Docs.jsx`
Ton `Docs.jsx` utilise `viteLogo` et `reactLogo`… mais **ne les importe pas** dans le fichier. Si tu l'affichais, tu aurais `viteLogo is not defined`.
> 🧩 **La règle :** **chaque fichier de composant doit importer ce qu'il utilise.** Un composant est isolé : il ne « voit » pas les imports faits dans `App.jsx`. Soit tu ajoutes les `import ... from "../assets/..."` en haut de `Docs.jsx`, soit (plus simple ici) tu **supprimes `Docs.jsx`** puisqu'on enlève cette section de toute façon.
> 💡 À retenir : un composant qui plante (import manquant, export oublié) fait **tomber tout le rendu** — tu l'as vu avec l'histoire du Footer. D'où l'intérêt de garder chaque fichier propre et autonome.
**✅ Test :** la page n'affiche plus que ton Header/Footer, sans erreur dans la console (F12).
---
## 🧱 Module A — Afficher une liste : `.map()` + `key`
On va lister des **jeux Zelda** à partir de **données**, pas à la main.
### A.1 — un tableau (au-dessus de `function App()`)
```jsx
const jeux = [
{ id: 1, titre: "Ocarina of Time", console: "N64", annee: 1998, note: 99 },
{ id: 2, titre: "Breath of the Wild", console: "Switch", annee: 2017, note: 97 },
{ id: 3, titre: "Majora's Mask", console: "N64", annee: 2000, note: 95 },
// ... tes jeux préférés
];
```
### A.2 — générer les cartes
```jsx
function App() {
return (
<>
<Header />
{jeux.map((jeu) => (
<Carte key={jeu.id} titre={jeu.titre} console={jeu.console} annee={jeu.annee} />
))}
<Footer />
</>
);
}
```
> 🧩 **Deux nouveautés :**
> - **`{ ... }`** dans le JSX = « insère ici du JavaScript ». `{jeux.map(...)}` insère **une carte par jeu**.
> - **`key={...}`** = identifiant **unique** par élément (utilise `id`). React en a besoin pour suivre chaque carte ; sans lui → **warning** console.
**✅ Test :** ajoute un jeu au tableau → une carte apparaît toute seule.
> 🔎 Tu reconnais ton tableau d'équipe + `.map()`/`filter()` du JS pur ? C'est la même idée, mais React refait l'affichage tout seul (plus de `innerHTML` ni `createElement` à la main).
---
## 🎖️ Module B — Affichage conditionnel + un composant `<Badge>`
Enrichis `Carte` :
```jsx
function Carte(props) {
return (
<div className="carte">
<h2>{props.titre}</h2>
<p>{props.console} — {props.annee}</p>
</div>
);
}
```
**Affichage conditionnel** — afficher un truc **seulement si** une condition est vraie :
```jsx
{props.note >= 97 && <span className="badge">⭐ Chef-d'œuvre</span>}
```
- `condition && <...>` = « si vrai, affiche, sinon rien ».
- choix entre deux : `{props.console === "Switch" ? "🟢 récent" : "🕹️ rétro"}`.
> 💡 Dans le JSX, pas de `if` à l'intérieur du `return` : on utilise `&&` (afficher ou rien) ou `? :` (A ou B).
**Exercice bonus — extraire `<Badge>`** (1 composant = 1 fichier) :
```jsx
// components/Badge.jsx
function Badge({ note }) {
if (note < 97) return null; // un composant peut renvoyer "rien"
return <span className="badge">⭐ Chef-d'œuvre</span>;
}
export default Badge;
```
> 🔎 Nouveau : `function Badge({ note })` déballe directement la prop (au lieu de `props.note`). Et `return null` = « n'affiche rien ».
**✅ Test :** seuls les jeux notés ≥ 97 montrent le badge.
---
## ❤️ Module C — État local : chaque `Carte` a son `useState`
Mets un bouton « j'aime » **dans** `Carte` :
```jsx
import { useState } from "react";
function Carte(props) {
const [likes, setLikes] = useState(0);
return (
<div className="carte">
<h2>{props.titre}</h2>
<button onClick={() => setLikes(likes + 1)}>❤️ {likes}</button>
</div>
);
}
```
> 🧩 Chaque `<Carte />` est une **instance indépendante** : liker un jeu ne touche pas les autres.
**✅ Test :** le ❤️ d'une carte monte sans bouger les autres.
---
## 🔎 Module D — Champ **contrôlé** + recherche
Nouveau concept : un champ dont la **valeur est pilotée par un `useState`** (« contrôlé »).
```jsx
function App() {
const [recherche, setRecherche] = useState("");
const filtres = jeux.filter((j) =>
j.titre.toLowerCase().includes(recherche.toLowerCase())
);
return (
<>
<Header />
<input
type="search"
placeholder="Rechercher un jeu…"
value={recherche} // l'input AFFICHE l'état
onChange={(e) => setRecherche(e.target.value)} // toute frappe MET À JOUR l'état
/>
{filtres.length === 0 && <p>Aucun jeu trouvé 😕</p>}
{filtres.map((jeu) => (
<Carte key={jeu.id} titre={jeu.titre} console={jeu.console} annee={jeu.annee} />
))}
<Footer />
</>
);
}
```
> 🧩 **Le champ contrôlé**, c'est la boucle : l'état s'affiche dans le champ (`value`) ; une frappe met à jour l'état (`onChange`) ; l'état change → React réaffiche → la liste **filtrée** se recalcule. Exactement ta recherche du JS, version React.
> ⚠️ On filtre **avant** le `.map()`, et on gère le cas **« aucun résultat »** (l'état vide).
**✅ Test :** tape « ocarina » → seul ce jeu reste ; efface → tout revient ; tape n'importe quoi → message « aucun jeu ».
---
## ⬆️ Module E — Faire remonter l'état (lifting state up)
Problème : tu veux un **total de likes** affiché en haut, qui additionne **toutes** les cartes. Chaque carte a son like dans son coin → `App` ne les voit pas.
Solution React : **remonter l'état dans le parent** (`App`) et **redescendre** une **fonction** (callback) aux enfants.
```jsx
function App() {
const [total, setTotal] = useState(0);
return (
<>
<p>👍 Total de likes : {total}</p>
{jeux.map((jeu) => (
<Carte
key={jeu.id}
titre={jeu.titre}
onLike={() => setTotal((t) => t + 1)} // 👈 une FONCTION passée en prop
/>
))}
</>
);
}
function Carte(props) {
return (
<div className="carte">
<h2>{props.titre}</h2>
<button onClick={props.onLike}>❤️ J'aime</button> {/* l'enfant appelle le callback du parent */}
</div>
);
}
```
> 🧩 **L'idée clé de React :** l'état vit **au plus haut niveau qui en a besoin**. Les données descendent en **props**, les événements remontent via des **fonctions en props**. La carte ne sait pas compter le total — elle dit juste « on m'a likée » au parent.
> 🔎 `setTotal((t) => t + 1)` (forme avec une fonction) est la façon **sûre** d'incrémenter. Prends l'habitude.
**✅ Test :** liker n'importe quelle carte fait monter le **total global** en haut.
---
## 🔁 Module F — `useEffect` : agir « à côté » de l'affichage
`useEffect` exécute du code **après l'affichage** : titre de l'onglet, `localStorage`, requêtes…
### F.1 — le titre de l'onglet
```jsx
import { useEffect } from "react";
useEffect(() => {
document.title = `${jeux.length} jeux Zelda`;
}, []); // 👈 tableau vide = "une seule fois, au chargement"
```
### F.2 — des likes qui **survivent au rechargement**
Dans `Carte` :
```jsx
function Carte(props) {
const [likes, setLikes] = useState(() => Number(localStorage.getItem("likes-" + props.id)) || 0);
useEffect(() => {
localStorage.setItem("likes-" + props.id, likes);
}, [likes]); // 👈 "rejoue cet effet quand likes change"
return <button onClick={() => setLikes(likes + 1)}>❤️ {likes}</button>;
}
```
> 🧩 **À comprendre :** le **tableau de dépendances** dit **quand** rejouer l'effet : `[]` = une fois ; `[likes]` = à chaque changement de `likes`. C'est le cœur de `useEffect`.
> Et tu retrouves ton `localStorage` (texte → `Number(...)` pour reconvertir).
**✅ Test :** like un jeu, **recharge (F5)** → le nombre est toujours là. L'onglet affiche ton titre.
---
## 🌐 Module G — Charger les données avec `fetch` (dans React)
Au lieu d'écrire `jeux` en dur, charge-les depuis un JSON — comme une vraie app.
1. Crée `public/zelda.json` (le dossier `public/` est servi à la racine), format JSON (guillemets doubles partout).
2. Dans `App` :
```jsx
import { useState, useEffect } from "react";
function App() {
const [jeux, setJeux] = useState([]); // au départ : liste vide
const [chargement, setChargement] = useState(true);
useEffect(() => {
fetch("/zelda.json")
.then((r) => r.json())
.then((data) => { setJeux(data); setChargement(false); });
}, []); // une fois, au chargement
if (chargement) return <p>Chargement…</p>;
return (
<>
{jeux.map((j) => <Carte key={j.id} titre={j.titre} /* ... */ />)}
</>
);
}
```
> 🧩 **Le schéma :** un `useState` vide → un `useEffect([])` qui `fetch` au chargement → `setJeux(data)` remplit l'état → React réaffiche. Et on gère l'état **« en cours de chargement »**.
> ⚠️ Comme en JS pur : `fetch` a besoin du serveur (`npm run dev` te le donne). `public/` est servi à la racine → `/zelda.json`. (Tu te souviens de l'erreur CORS en `file://` ? Ici `npm run dev` est déjà un vrai serveur, donc pas de souci.)
**✅ Test :** tes cartes viennent du JSON ; ajoute un jeu **dans le fichier** → il apparaît.
---
## 🧹 Module H — Re-découper proprement
`App` commence à beaucoup faire (recherche + total + liste…). Garde-le **assembleur**, extrais des composants (un par fichier) :
- `components/BarreRecherche.jsx` — l'input contrôlé (reçoit `valeur` + `onChange` en props).
- `components/Liste.jsx` — reçoit le tableau filtré et fait le `.map()`.
- `components/Badge.jsx` — déjà fait au Module B.
> 🧭 Règle d'or : **1 composant = 1 fichier**, `App` assemble. Dès qu'un fichier « fait trop », découpe. (C'est exactement le ménage du Module 0, mais en bien.)
---
## ✅ Checklist
- [ ] 0 — template nettoyé + `Docs.jsx` réparé/supprimé
- [ ] A — tableau + `.map()` + `key`
- [ ] B — affichage conditionnel + composant `<Badge>`
- [ ] C — like local par carte
- [ ] D — recherche avec champ **contrôlé** + message « aucun résultat »
- [ ] E — **total de likes** remonté dans `App` (callback en prop)
- [ ] F — `useEffect` : titre d'onglet + likes persistés en `localStorage`
- [ ] G — données chargées via `fetch` depuis `public/zelda.json`
- [ ] H — re-découpage (`<BarreRecherche>`, `<Liste>`)
## 🚀 Encore plus loin
- Un **tri** (par année, par note) avec un `<select>` contrôlé.
- Une **image** par jeu (`<img src={props.image} />`) + affichage conditionnel si elle existe.
- Un **mode sombre** global (état dans `App`, classe sur le `<div>` racine).
## 🧠 Les nouveaux concepts de cet énoncé
Listes (`map`/`key`) · affichage conditionnel · état local · **champ contrôlé** · **lifting state up** + callbacks · **`useEffect`** + dépendances · **`fetch` en React** + état de chargement · re-découpage.
Bloquée ? Note ce que tu as tapé + le message exact (console F12). On débloque.
-307
View File
@@ -1,307 +0,0 @@
# 🎮 Énoncé React #2 — ton site Geometry Dash (version longue)
Bien joué Guillaume : tu as un Header avec ton intro, un Footer, un composant `Carte` avec des **props**, et un `Compteur` avec `useState`. Les briques sont là. 👌
Le souci : tes cartes affichent encore le contenu du tuto (`Suivi en temps réel`…), tu les écris **une par une à la main**, et ton compteur dit encore « Véhicules ». On va en faire un **vrai site sur tes trilogies**, et surtout apprendre **beaucoup de React** au passage.
> 🎯 Ce document est une **série de modules** (A → H), du plus simple au plus costaud. Fais-les **dans l'ordre**, teste à chaque fois. Je t'explique les **mécaniques** et te donne des mini-exemples ; à toi d'**appliquer sur ton thème** (je ne donne pas le site fini). Compte plusieurs séances — c'est normal.
| Module | Tu apprends | Niveau |
|---|---|---|
| A | Listes : `.map()` + `key` | ⭐ |
| B | Affichage conditionnel + extraire un `<Badge>` | ⭐ |
| C | État local par carte (`useState`) | ⭐ |
| D | Champ **contrôlé** + recherche/filtre + état vide | ⭐⭐ |
| E | **Faire remonter l'état** (lifting state up) + callbacks en props | ⭐⭐ |
| F | **`useEffect`** : titre de page + persistance `localStorage` | ⭐⭐ |
| G | **Charger les données avec `fetch`** (dans React) | ⭐⭐⭐ |
| H | Re-découpage propre (`<Liste>`, `<BarreRecherche>`) | ⭐⭐ |
---
## 🧱 Module A — Afficher une liste : `.map()` + `key`
Aujourd'hui tu écris chaque carte à la main. La bonne méthode : **un tableau de données** + **`.map()`**.
### A.1 — un tableau (au-dessus de `function App()`)
```jsx
const trilogies = [
{ id: 1, nom: "Bloodbath", createur: "Riot", difficulte: "Extreme Demon" },
{ id: 2, nom: "Cataclysm", createur: "Ggb0y", difficulte: "Extreme Demon" },
{ id: 3, nom: "Sonic Wave", createur: "Cyclic", difficulte: "Extreme Demon" },
// ... tes 5 trilogies
];
```
### A.2 — générer les cartes
```jsx
function App() {
return (
<div className="body">
<Header />
{trilogies.map((trilo) => (
<Carte key={trilo.id} nom={trilo.nom} createur={trilo.createur} difficulte={trilo.difficulte} />
))}
<Footer />
</div>
);
}
```
> 🧩 **Deux nouveautés :**
> - **`{ ... }`** dans le JSX = « insère ici du JavaScript ». `{trilogies.map(...)}` insère **une carte par élément**.
> - **`key={...}`** = un identifiant **unique** par élément (utilise `id`, pas l'index). React en a besoin pour suivre chaque carte ; sans lui → **warning** dans la console.
**✅ Test :** ajoute une trilogie au tableau → une carte apparaît toute seule.
---
## 🎖️ Module B — Affichage conditionnel + un composant `<Badge>`
D'abord enrichis `Carte` :
```jsx
function Carte(props) {
return (
<div className="carte">
<h2>{props.nom}</h2>
<p>Par {props.createur}</p>
<p>Difficulté : {props.difficulte}</p>
</div>
);
}
```
**Affichage conditionnel** — afficher un truc **seulement si** une condition est vraie :
```jsx
{props.difficulte === "Extreme Demon" && <span className="badge">🔥 Extreme</span>}
```
- `condition && <...>` = « si vrai, affiche, sinon rien ».
- choix entre deux : `{props.fini ? "✅ battu" : "❌ pas battu"}`.
> 💡 Dans le JSX on n'écrit pas de `if` à l'intérieur du `return` : on utilise `&&` (afficher ou rien) ou `? :` (A ou B).
**Exercice bonus — extraire un composant `<Badge>`** (1 composant = 1 fichier) :
```jsx
// components/Badge.jsx
function Badge({ difficulte }) {
if (difficulte !== "Extreme Demon") return null; // un composant peut renvoyer "rien"
return <span className="badge">🔥 Extreme</span>;
}
export default Badge;
```
> 🔎 Nouveau : `function Badge({ difficulte })` — on « déballe » directement la prop entre accolades au lieu d'écrire `props.difficulte` partout. Et `return null` = « n'affiche rien ». Deux outils très courants.
**✅ Test :** seules tes trilogies « Extreme Demon » montrent le badge.
---
## ❤️ Module C — État local : chaque `Carte` a son `useState`
Mets un bouton « j'aime » **dans** `Carte` :
```jsx
import { useState } from "react";
function Carte(props) {
const [likes, setLikes] = useState(0);
return (
<div className="carte">
<h2>{props.nom}</h2>
<button onClick={() => setLikes(likes + 1)}>❤️ {likes}</button>
</div>
);
}
```
> 🧩 Chaque `<Carte />` est une **instance indépendante** avec son **propre état** : liker une carte ne touche pas les autres.
**✅ Test :** le ❤️ d'une carte monte sans bouger les autres.
---
## 🔎 Module D — Champ **contrôlé** + recherche
Nouveau concept important : un champ dont la **valeur est pilotée par un `useState`** (on dit « contrôlé »).
```jsx
function App() {
const [recherche, setRecherche] = useState("");
const filtrees = trilogies.filter((t) =>
t.nom.toLowerCase().includes(recherche.toLowerCase())
);
return (
<div className="body">
<Header />
<input
type="search"
placeholder="Rechercher une trilogie…"
value={recherche} // l'input AFFICHE l'état
onChange={(e) => setRecherche(e.target.value)} // toute frappe MET À JOUR l'état
/>
{filtrees.length === 0 && <p>Aucune trilogie trouvée 😕</p>}
{filtrees.map((trilo) => (
<Carte key={trilo.id} nom={trilo.nom} createur={trilo.createur} difficulte={trilo.difficulte} />
))}
<Footer />
</div>
);
}
```
> 🧩 **Le champ contrôlé**, c'est la boucle : l'état → s'affiche dans le champ (`value`) ; une frappe → met à jour l'état (`onChange`) ; l'état change → React réaffiche, donc la liste **filtrée** se recalcule. Tu reconnais ta recherche en JS pur, version React (et React refait l'affichage tout seul, plus de `innerHTML`).
> ⚠️ Détail : on filtre **avant** le `.map()`, et on prévoit le cas **« aucun résultat »** (l'état vide). Un bon réflexe.
**✅ Test :** tape « blood » → seules les trilogies correspondantes restent ; efface → tout revient ; tape n'importe quoi → message « aucune trilogie ».
---
## ⬆️ Module E — Faire remonter l'état (lifting state up)
Problème : et si tu veux un **total de likes** affiché en haut, qui additionne **toutes** les cartes ? Chaque carte a son like **dans son coin**, App ne les « voit » pas.
La solution React : **remonter l'état dans le parent** (App) et le **redescendre** aux enfants. L'enfant prévient le parent via une **fonction passée en prop** (un *callback*).
```jsx
function App() {
const [total, setTotal] = useState(0);
return (
<div className="body">
<p>👍 Total de likes : {total}</p>
{trilogies.map((trilo) => (
<Carte
key={trilo.id}
nom={trilo.nom}
onLike={() => setTotal((t) => t + 1)} // 👈 on passe une FONCTION en prop
/>
))}
</div>
);
}
function Carte(props) {
return (
<div className="carte">
<h2>{props.nom}</h2>
<button onClick={props.onLike}>❤️ J'aime</button> {/* l'enfant appelle le callback du parent */}
</div>
);
}
```
> 🧩 **L'idée clé de React :** l'état « vit » **au plus haut niveau qui en a besoin**. Les données descendent en **props** ; les événements remontent via des **fonctions en props**. Ici, chaque carte ne sait pas compter le total — elle dit juste « on m'a likée » au parent, qui, lui, tient le total.
> 🔎 `setTotal((t) => t + 1)` (avec une fonction) est la façon **sûre** d'incrémenter quand plusieurs mises à jour peuvent s'enchaîner. Prends l'habitude.
**✅ Test :** liker n'importe quelle carte fait monter le **total global** en haut.
---
## 🔁 Module F — `useEffect` : agir « à côté » de l'affichage
`useEffect` sert à exécuter du code **après l'affichage** : changer le titre de l'onglet, lire/écrire dans `localStorage`, lancer une requête… Des choses qui ne sont pas « du JSX ».
### F.1 — le titre de l'onglet
```jsx
import { useEffect } from "react";
useEffect(() => {
document.title = `${trilogies.length} trilogies cultes`;
}, []); // 👈 tableau vide = "une seule fois, au chargement"
```
### F.2 — des likes qui **survivent au rechargement** (`localStorage` + `useEffect`)
Dans `Carte` :
```jsx
function Carte(props) {
// valeur de départ : ce qui est stocké, sinon 0
const [likes, setLikes] = useState(() => Number(localStorage.getItem("likes-" + props.id)) || 0);
// à chaque fois que "likes" change, on le sauvegarde
useEffect(() => {
localStorage.setItem("likes-" + props.id, likes);
}, [likes]); // 👈 "rejoue cet effet quand likes change"
return <button onClick={() => setLikes(likes + 1)}>❤️ {likes}</button>;
}
```
> 🧩 **À comprendre :** le **tableau de dépendances** (`[]` ou `[likes]`) dit **quand** rejouer l'effet : `[]` = une seule fois ; `[likes]` = à chaque changement de `likes`. C'est le cœur de `useEffect`.
> Et tu retrouves ton `localStorage` (texte → `Number(...)` pour reconvertir, comme `parseInt` avant).
**✅ Test :** like une carte, **recharge la page (F5)** → le nombre est toujours là. Et l'onglet affiche ton titre.
---
## 🌐 Module G — Charger les données avec `fetch` (le vrai truc des apps)
Au lieu d'écrire `trilogies` en dur, charge-les depuis un fichier JSON — comme une vraie app va chercher ses données sur un serveur.
1. Crée `public/trilogies.json` (le dossier `public/` est servi à la racine) avec ton tableau de trilogies (format JSON : guillemets doubles partout).
2. Dans `App` :
```jsx
import { useState, useEffect } from "react";
function App() {
const [trilogies, setTrilogies] = useState([]); // au départ : liste vide
const [chargement, setChargement] = useState(true);
useEffect(() => {
fetch("/trilogies.json")
.then((r) => r.json())
.then((data) => { setTrilogies(data); setChargement(false); });
}, []); // une seule fois, au chargement
if (chargement) return <p>Chargement…</p>; // tant que les données ne sont pas là
return (
<div className="body">
{trilogies.map((t) => <Carte key={t.id} nom={t.nom} /* ... */ />)}
</div>
);
}
```
> 🧩 **Le schéma à retenir :** un `useState` vide pour accueillir les données → un `useEffect([])` qui `fetch` au chargement → `setTrilogies(data)` remplit l'état → React réaffiche avec les données. Et on gère l'état **« en cours de chargement »** (très pro).
>
> ⚠️ Comme en JS pur : `fetch` a besoin du serveur de dev (`npm run dev` te le donne déjà). Le `public/` est servi à la racine, donc `/trilogies.json`.
**✅ Test :** tes cartes s'affichent à partir du JSON. Ajoute une trilogie **dans le fichier** → elle apparaît.
---
## 🧹 Module H — Re-découper proprement
Ton `App` commence à beaucoup faire (recherche + total + liste…). Garde-le **assembleur** : extrais des composants, **un par fichier**.
- `components/BarreRecherche.jsx` — l'input contrôlé (reçoit `valeur` et `onChange` en props).
- `components/Liste.jsx` — reçoit le tableau filtré et fait le `.map()`.
- `components/Badge.jsx` — déjà fait au module B.
> 🧭 La règle d'or n'a pas changé : **1 composant = 1 fichier**, `App` assemble. Dès qu'un fichier « fait trop de choses », découpe.
---
## ✅ Checklist
- [ ] A — tableau + `.map()` + `key`
- [ ] B — affichage conditionnel + composant `<Badge>`
- [ ] C — like local par carte
- [ ] D — recherche avec champ **contrôlé** + message « aucun résultat »
- [ ] E — **total de likes** remonté dans `App` (callback en prop)
- [ ] F — `useEffect` : titre d'onglet + likes persistés en `localStorage`
- [ ] G — données chargées via `fetch` depuis `public/trilogies.json`
- [ ] H — re-découpage (`<BarreRecherche>`, `<Liste>`)
## 🚀 Encore plus loin
- Un **tri** (par difficulté, par likes) avec un `<select>` contrôlé.
- Une **image** par trilogie + affichage conditionnel si l'image existe.
- Un **mode sombre** global (état dans `App`, classe sur le `<div>` racine).
## 🧠 Les nouveaux concepts de cet énoncé
Listes (`map`/`key`) · affichage conditionnel · état local · **champ contrôlé** · **lifting state up** + callbacks · **`useEffect`** + dépendances · **`fetch` en React** + état de chargement · re-découpage.
Bloqué ? Note ce que tu as tapé + le message exact (console F12). On débloque.
+20 -17
View File
@@ -1,6 +1,6 @@
# 📨 À quoi sert vraiment une balise `<form>` ?
Tu as un `<form>` autour de ton formulaire de suggestions, mais ce qu'il fait « en coulisses » n'est pas forcément clair. Ce fichier explique **à quoi il sert**, puis les deux attributs qui prêtent à confusion : **`action`** et **`method`**.
Tu as un `<form>` autour de ton formulaire de contact, mais ce qu'il fait « en coulisses » n'est pas forcément clair. Ce fichier explique **à quoi il sert**, puis les deux attributs qui prêtent à confusion : **`action`** et **`method`**.
---
@@ -17,22 +17,24 @@ Sans `<form>`, tes champs s'affichent quand même… mais ce ne sont que des cas
> 🔑 **Le détail le plus important : l'attribut `name`.**
> Seuls les champs qui ont un **`name`** sont envoyés, et c'est ce `name` qui sert d'**étiquette** à la valeur. Dans ton formulaire :
> ```html
> <input type="text" name="user_name" ...>
> <input type="email" name="user_mail" ...>
> <textarea name="user_message"></textarea>
> <input type="text" name="prenom" ...>
> <input type="email" name="email" ...>
> <textarea name="contacter"></textarea>
> ```
> → au moment de l'envoi, le navigateur construit quelque chose comme :
> `user_name = Dupond`, `user_mail = dupond@mail.com`, `user_message = ...`
> `prenom = Mélissa`, `email = melissa@mail.com`, `contacter = ...`
> Un champ **sans `name`** est tout simplement **ignoré** à l'envoi, même s'il est rempli.
>
> 💬 C'est aussi pour ça que `FormData` (Niveau 5, exo 8) lit les champs par leur `name` : tout est lié.
> ⚠️ Rappel utile : pour que l'envoi parte, le bouton doit être **dans** le `<form>` et être un `type="submit"` (c'est le cas du tien : « Envoyer le message »).
> ⚠️ Rappel utile : pour que l'envoi parte, le bouton doit être **dans** le `<form>` et être un `type="submit"` (un `<button>` sans `type` précisé compte comme submit par défaut).
---
## 2. L'attribut `action` : **OÙ** vont les données
```html
<form action="" method="post">
<form method="get" action="">
```
`action` indique **l'adresse qui va recevoir** les données — en général un programme **côté serveur** (en PHP, Node, Python…) qui les enregistre, envoie un e-mail, etc.
@@ -40,7 +42,7 @@ Sans `<form>`, tes champs s'affichent quand même… mais ce ne sont que des cas
- `action="https://exemple.com/contact"` → les données partent vers cette adresse.
- `action=""` (vide, comme chez toi) → elles sont envoyées à **la page actuelle** (la même URL).
> 💡 **Important pour ton cas :** vous n'avez **pas encore de serveur** derrière. Donc `action=""` ne mène nulle part d'utile : au mieux la page se recharge. C'est **normal à ce stade**, et c'est exactement pour ça que tu interceptes l'envoi en **JavaScript** (avec `addEventListener("submit", ...)` et `preventDefault()`) pour faire toi-même quelque chose d'utile (vérifier, afficher un message…).
> 💡 **Important pour ton cas :** vous n'avez **pas encore de serveur** derrière. Donc `action=""` ne mène nulle part d'utile : la page se contente de se recharger. C'est **normal à ce stade**, et c'est exactement pour ça que tu interceptes l'envoi en **JavaScript** (avec `addEventListener("submit", ...)` et `preventDefault()`) pour faire toi-même quelque chose d'utile (vérifier le message, afficher le prénom dans une alerte…). Tu l'as déjà fait — c'est cette logique-là qui « remplace » le serveur pour l'instant.
---
@@ -48,35 +50,36 @@ Sans `<form>`, tes champs s'affichent quand même… mais ce ne sont que des cas
Il existe deux façons principales d'envoyer un formulaire : **GET** et **POST**.
### `method="get"`
### `method="get"` (ce que tu utilises)
Les données sont **collées dans l'URL**, après un point d'interrogation :
```
contact.html?user_name=Dupond&user_mail=dupond@mail.com
contact.html?prenom=Mélissa&email=melissa@mail.com&contacter=Bonjour...
```
- ✅ Visible dans la barre d'adresse → on peut mettre la page en favori, la partager, la recharger.
- ❌ Tout est visible (à éviter pour un mot de passe), et la longueur est limitée.
- 👉 Idéal pour **demander / rechercher** des infos (un moteur de recherche utilise GET).
### `method="post"` (ce que tu utilises)
### `method="post"`
Les données sont envoyées dans le **corps** de la requête, **pas** dans l'URL.
- ✅ N'apparaît pas dans la barre d'adresse, pas de limite pratique de taille.
- 👉 Idéal pour **envoyer / créer / modifier** quelque chose : un message, une inscription, une suggestion…
- 👉 Idéal pour **envoyer / créer / modifier** quelque chose : un message, une inscription…
> 💡 **La règle simple :**
> - **GET** = « je **demande** des infos » (recherche, filtre, consultation).
> - **POST** = « j'**envoie / je modifie** des infos » (formulaire de contact, suggestion, inscription).
> Pour ton formulaire de **suggestions**, `method="post"` est donc le **bon choix**. 👍
> - **POST** = « j'**envoie / je modifie** des infos » (formulaire de contact, inscription).
> Pour un formulaire de **contact** où l'on **envoie un message**, `method="post"` serait en fait **plus adapté** que `get`. Essaie de le passer en `post` et regarde la différence avec l'expérience ci-dessous. 🙂
---
## 🔬 Une petite expérience pour bien voir la différence
1. Change **temporairement** ton formulaire en `method="get"`.
2. Remplis les champs, clique « Envoyer », et **regarde la barre d'adresse** : tu verras tes valeurs apparaître après le `?` (`?user_name=...&user_mail=...`).
3. Remets `method="post"` : les valeurs **disparaissent** de l'URL — elles voyagent « cachées ».
1. Garde ton `method="get"` actuel. Remplis les champs, clique « Envoyer », et **regarde la barre d'adresse** : tu verras tes valeurs apparaître après le `?` (`?prenom=...&email=...`).
2. Passe maintenant en `method="post"`, recommence : les valeurs **disparaissent** de l'URL — elles voyagent « cachées ».
Ça rend très concret ce que veulent dire GET (dans l'URL) et POST (hors de l'URL). C'est aussi pour ça que `name` est essentiel : sans lui, rien n'apparaît dans l'URL, car le navigateur ne sait pas comment étiqueter la valeur.
> 🔎 Astuce : pour faire l'expérience proprement, **désactive temporairement** ton `preventDefault()` dans le JS — sinon il empêche justement l'envoi par défaut, et tu ne verras pas l'URL changer.
---
## 📝 Le résumé en une carte
+620
View File
@@ -0,0 +1,620 @@
# 📬 Page Contact — relecture & prochaine étape
Salut Mélissa ! 👋
Ta page contact prend forme : le tableau de l'équipe est sympa, le formulaire est en place, et le bouton « contact » qui reste enfoncé pour montrer la page courante (`.instyled`) est une **très bonne idée** d'ergonomie. Bravo ! 🎉
Ce guide a trois objectifs :
1. Corriger **4 bugs** (avec l'explication, pas juste la solution).
2. T'apprendre à **définir et limiter une `<textarea>`** proprement.
3. Te lancer sur ta **première mission JavaScript** 🚀 (dans le thème FleetZen).
---
## 🚨 Bug 1 — L'accolade `}` oubliée (le bug le plus important)
Tu te souviens de la leçon « chaque `{` a son `}` » ? Elle te concerne en plein ! 😄 Regarde le tout début de `contact.css` :
```css
body {
width: 100%;
height: 100%;
background-color: lightblue;
margin: 0;
padding-bottom: 60px;
/* ❌ il manque le } ICI pour fermer body */
#nav_barre { /* du coup le navigateur croit que tout ça est ENCORE dans body */
display: flex;
...
```
Tu as **ouvert** `body {` mais tu ne l'as **jamais refermé**. Résultat : le navigateur pense que `#nav_barre`, `.styled`, `.table`… sont tous **imbriqués dans `body`**, il se perd, et **une grande partie de ton CSS est ignorée**. C'est pour ça que ta page ne ressemble pas à ce que tu attends.
### ✅ Correction : ferme `body` avec un `}`
```css
body {
width: 100%;
height: 100%;
background-color: lightblue;
margin: 0;
padding-bottom: 60px;
} /* ✅ on ferme body ici */
#nav_barre {
display: flex;
...
}
```
> 💡 **À retenir :** à chaque `{` correspond **un** `}`. Une accolade oubliée et **tout le reste de la feuille de style casse**. Astuce : la plupart des éditeurs colorent ou « replient » les blocs — si la coloration paraît bizarre à partir d'un endroit, c'est souvent une accolade manquante juste avant.
---
## 🐛 Bug 2 — Un lien de navigation cassé
Ta page `contact.html` est **déjà dans le dossier `pages/`**. Or ton lien vers services pointe vers :
```html
<a href="pages/services.html"> <!-- ❌ cherche pages/pages/services.html → n'existe pas -->
```
Comme tu es déjà dans `pages/`, tu redemandes d'entrer dans `pages/` → le chemin n'existe pas.
### ✅ Correction : le fichier est **juste à côté**, dans le même dossier
```html
<a href="services.html"> <!-- ✅ même dossier, on met juste le nom du fichier -->
```
> 💡 **À retenir :** les chemins sont **relatifs au fichier courant**.
> - `services.html` → dans le **même dossier**
> - `../index.html` → **remonter** d'un dossier (le `..`), puis prendre `index.html`
> Ton lien « retour » (`../index.html`) est d'ailleurs **correct**, bravo : c'est la même logique.
---
## 🐛 Bug 3 — Le champ e-mail n'est pas un vrai champ e-mail
```html
<input type="text" name="e-mail" id="e-mail" ...> <!-- ❌ type="text" -->
```
En `type="text"`, le navigateur accepte **n'importe quoi**. Pour un e-mail, il existe un type dédié qui **vérifie tout seul** qu'il y a bien un `@` :
```html
<input type="email" name="email" id="email" ...> <!-- ✅ -->
```
> 💡 **À retenir :** le **bon `type`** d'`<input>` fait travailler le navigateur à ta place : `email`, `tel`, `number`, `date`… Chacun ajoute sa petite vérification automatique.
>
> ⚠️ Petit détail : essaie d'éviter le tiret dans les `id`/`name` (`e-mail`). Préfère `email` ou `e_mail` — c'est plus simple à réutiliser ensuite, surtout en JavaScript.
---
## 🎯 Bug 4 — L'icône SVG n'est pas centrée dans le bouton « retour »
Ton bouton « retour » mélange une **image** (le SVG) et du **texte** côte à côte :
```html
<button class="styled"><svg ...></svg>retour</button>
```
Le souci : par défaut, une icône SVG se pose **sur la ligne du texte** (sur la « ligne de base », comme une lettre). Du coup elle paraît trop haute ou trop basse, et ton `line-height: 2.5` accentue le décalage. L'icône et le mot « retour » ne sont pas alignés sur leur **milieu**.
### ✅ La solution : transformer le bouton en boîte flex
```css
.styled {
display: inline-flex; /* le bouton range ses enfants (icône + texte) */
align-items: center; /* ↕ les centre verticalement, sur le même milieu */
justify-content: center; /* ↔ les centre horizontalement */
gap: 6px; /* un petit espace entre l'icône et le mot */
line-height: normal; /* on enlève le 2.5 qui déréglait l'alignement */
/* ...garde le reste de tes styles (couleur, border-radius, etc.) */
}
```
`align-items: center`, c'est **exactement** le `flex` que tu connais déjà (tu l'utilises sur `#buttons`). Ici on l'applique **au bouton lui-même**, parce que c'est lui le parent qui contient l'icône **et** le texte.
> 💡 **À retenir :** dès qu'un bouton (ou n'importe quelle boîte) contient **une icône + du texte** à aligner, passe-le en `display: inline-flex; align-items: center;`. C'est le réflexe pro pour centrer proprement une icône avec du texte.
>
> 🔎 Comme tous tes boutons partagent la classe `.styled`, cette correction les rend **tous** propres d'un coup — même ceux sans icône (le texte y reste bien centré).
---
## 📐 La leçon du jour : définir et **limiter** une `<textarea>`
Ta zone de message est « brute » :
```html
<textarea name="contacter" id="contacter"></textarea>
```
Par défaut elle est petite, et l'utilisateur peut écrire **sans aucune limite**. On va la **cadrer**.
### 1️⃣ Lui donner une taille de départ : `rows` et `cols`
```html
<textarea id="contacter" name="contacter"
rows="6" cols="40"></textarea>
```
- `rows` = nombre de **lignes** visibles (la hauteur).
- `cols` = largeur en **caractères**.
> 🔎 Ce ne sont **pas** des pixels : c'est « combien de lignes/caractères je vois » au départ.
### 2️⃣ Limiter le nombre de caractères : `maxlength`
```html
<textarea id="contacter" name="contacter"
rows="6" cols="40"
maxlength="500"></textarea>
```
`maxlength="500"` = impossible de taper plus de 500 caractères. Le navigateur bloque **tout seul**, sans JavaScript. 👍
### 3️⃣ Cadrer le redimensionnement : `resize` en CSS
La petite poignée en bas à droite permet à l'utilisateur d'étirer la zone et de **casser ta mise en page**. On la contrôle en CSS :
```css
#contacter {
resize: vertical; /* étirement en hauteur seulement (recommandé) */
/* resize: none; pour l'interdire complètement */
width: 100%;
min-height: 120px;
}
```
> 💡 **À retenir :**
> - `rows` / `cols` → **taille de départ**.
> - `maxlength` → **limite** le nombre de caractères (le navigateur bloque tout seul).
> - `resize` (CSS) → contrôle la **poignée** d'étirement (`vertical`, `horizontal`, `none`, `both`).
### 4️⃣ 👉 Votre question : laisser l'utilisateur redimensionner, mais **entre un min et un max**
Tu as écrit pour l'instant :
```css
textarea {
resize: initial; /* "initial" = la valeur par défaut = "both" : étirable dans TOUS les sens, sans limite */
width: 527px;
height: 80px;
}
```
`resize: initial` remet la valeur **par défaut**, c'est-à-dire `both` : l'utilisateur peut tout étirer, **dans toutes les directions et sans aucune borne**. Il peut donc rendre la zone énorme et **casser ta mise en page**. Vous m'avez justement demandé comment **autoriser** l'agrandissement **sans qu'il parte trop grand (ni trop petit)**. La recette tient en 2 idées qui se combinent :
1. **`resize`** dit *dans quelle direction* on peut étirer.
2. **`min-*` / `max-*`** disent *jusqu'où* on peut aller.
```css
#contacter {
resize: vertical; /* on autorise l'étirement, mais en HAUTEUR seulement */
min-height: 80px; /* impossible de la réduire en dessous de 80px */
max-height: 300px; /* impossible de l'agrandir au-delà de 300px */
width: 100%; /* largeur pilotée en CSS, pas par l'utilisateur */
}
```
Avec ça : l'utilisateur attrape la poignée, tire vers le bas pour avoir plus de place… mais **la zone refuse de dépasser 300px** et **de descendre sous 80px**. Ta mise en page reste protégée. 🎉
#### Et si on veut autoriser les **deux** sens (vertical **et** horizontal) ?
Même principe, mais avec **4 bornes** : 2 pour la hauteur, 2 pour la largeur. On passe `resize` à `both` (c'est d'ailleurs ce que faisait ton `resize: initial`, mais cette fois **borné**) :
```css
#contacter {
resize: both; /* la poignée étire en hauteur ET en largeur */
min-height: 80px; /* bornes verticales */
max-height: 300px;
min-width: 200px; /* bornes horizontales */
max-width: 100%; /* ne dépasse jamais la largeur du parent → pas de scroll */
}
```
Chaque direction a sa paire `min`/`max` :
- **hauteur** → `min-height` / `max-height`
- **largeur** → `min-width` / `max-width`
L'utilisateur peut alors étirer dans tous les sens, mais **jamais au-delà** de ces 4 limites.
> ⚠️ Le piège classique en horizontal : mets `max-width: 100%` (et pas un grand `527px` fixe), sinon l'utilisateur peut élargir la zone **plus que l'écran** et faire apparaître une barre de défilement horizontale.
Récap des valeurs de `resize` :
| `resize` | Ce que l'utilisateur peut étirer |
|---|---|
| `none` | rien (verrouillé) |
| `vertical` | la hauteur seulement (le plus confortable pour un message) |
| `horizontal` | la largeur seulement |
| `both` | les deux — c'est ce que donne `initial` par défaut |
> 🎯 Petit conseil de ciblage : vise plutôt `#contacter` (l'id de **ta** textarea) que `textarea` tout court. Le sélecteur `textarea` toucherait **toutes** les zones de texte du site, même celles que Guillaume ajoutera sur sa page.
> 💡 **À retenir :** `resize` = **la direction** autorisée. `min-height`/`max-height` (et `min-width`/`max-width`) = **les bornes**. Les deux ensemble = un redimensionnement libre **mais encadré**.
---
## 🧭 Petit plus — centrer un bloc sans le « pousser » avec `margin-left`
J'ai vu que tu centres ta table **et** ton nouveau `.form` avec `margin-left: 600px` / `620px`. Ça « marche » sur ton écran… mais sur un écran plus petit, le bloc est **poussé hors de la fenêtre** → barre de défilement horizontale. Pour **vraiment centrer** un bloc de largeur fixe :
```css
.form {
width: 35%;
margin-left: auto; /* "auto" à gauche ET à droite = */
margin-right: auto; /* le navigateur répartit l'espace → bloc centré */
/* on enlève le margin-left: 620px */
}
```
> 💡 **À retenir :** `margin: auto` à gauche **et** à droite centre un bloc qui a une largeur. C'est l'outil correct ; `margin-left: 620px` ne fait que le décaler et casse l'affichage sur petit écran.
---
## 🚀 Ta première mission JavaScript : le **compteur de caractères**
Jusqu'ici tu as fait du HTML (la **structure**) et du CSS (l'**apparence**). Le JavaScript, c'est la **3ᵉ couche** : le **comportement**, ce qui réagit en direct. 🧠
On va le faire **en mode tuto** : tu construis petit bout par petit bout, et **tu testes à chaque étape**. Ne copie pas tout d'un coup — avance palier par palier, c'est comme ça qu'on comprend. 🙂
**Objectif final :** `maxlength` empêche de dépasser 500 caractères… mais l'utilisateur ne **voit pas** combien il lui en reste. On va afficher un **compteur en direct** : « Il vous reste 437 caractères ». C'est exactement ce que font Twitter/X, les SMS, etc.
---
### 🪜 Étape 0 — Découvrir la console (ton meilleur ami)
Ouvre ta page dans le navigateur, puis appuie sur **F12** → onglet **Console**. C'est là que le JS « parle ».
Crée un fichier `contact.js` avec une seule ligne :
```js
console.log("Mon fichier JS est bien chargé !");
```
Et branche-le en bas du `<body>`, **juste avant** `</body>` :
```html
<script src="contact.js"></script>
```
> 🔎 Pourquoi à la fin du body ? Parce que le JS s'exécute de haut en bas. S'il se lance **avant** que le HTML existe, il ne trouve rien à manipuler. À la fin, tout le HTML est déjà là. 👍
**✅ Teste maintenant :** recharge la page. Tu dois voir ton message dans la Console. Si oui, ton JS est branché. Sinon, vérifie le chemin du `src`.
---
### 🪜 Étape 1 — Ajouter l'affichage du compteur
Sous ta textarea, ajoute la ligne qui affichera le décompte :
```html
<textarea id="contacter" name="contacter" rows="6" maxlength="500"></textarea>
<p>Il vous reste <span id="restant">500</span> caractères.</p>
```
**✅ Teste maintenant :** tu dois lire « Il vous reste 500 caractères ». Pour l'instant ce 500 est figé, on va le rendre vivant.
> 🔎 Le `<span id="restant">` est juste une petite « boîte » dans laquelle le JS viendra écrire le nombre. Son `id` est l'étiquette par laquelle on va l'attraper.
---
### 🪜 Étape 2 — Attraper les éléments depuis le JS
Dans `contact.js`, remplace ton `console.log` de test par :
```js
const zoneMessage = document.getElementById("contacter"); // ta textarea
const compteur = document.getElementById("restant"); // le <span>
console.log(zoneMessage); // pour vérifier qu'on l'a bien trouvée
```
**✅ Teste maintenant :** dans la Console tu dois voir s'afficher ta `<textarea>`. Si tu vois `null`, l'id ne correspond pas → vérifie l'orthographe (`contacter`).
> 🧩 **Idée clé n°1 — SÉLECTIONNER.** `document.getElementById("contacter")` veut dire : « va me chercher l'élément dont l'id est *contacter* ». On le range dans une variable pour s'en servir après.
---
### 🪜 Étape 3 — Réagir quand on tape
Ajoute en dessous :
```js
zoneMessage.addEventListener("input", function () {
console.log("Tu viens de taper quelque chose !");
});
```
**✅ Teste maintenant :** clique dans la zone et tape quelques lettres. À **chaque** touche, un message apparaît dans la Console. 🎉
> 🧩 **Idée clé n°2 — RÉAGIR.** `addEventListener("input", … )` se lit : « écoute les saisies dans cette zone, et à chaque frappe, exécute cette fonction ». (`"input"` = « le contenu a changé », parfait pour une textarea.)
---
### 🪜 Étape 4 — Calculer et afficher ce qu'il reste
Maintenant on remplace le `console.log` par le vrai calcul. Voici le fichier complet :
```js
// 1. On attrape la textarea et la "boîte" où afficher le compteur
const zoneMessage = document.getElementById("contacter");
const compteur = document.getElementById("restant");
const MAX = 500;
// 2. À chaque frappe, on recalcule ce qu'il reste
zoneMessage.addEventListener("input", function () {
const dejaTape = zoneMessage.value.length; // .value = le texte tapé, .length = sa longueur
compteur.textContent = MAX - dejaTape; // on écrit le résultat dans le <span>
});
```
> 🧩 **Idée clé n°3 — MODIFIER.** `compteur.textContent = …` réécrit le texte affiché **en direct**, sans recharger la page. C'est ça, la magie du JS.
>
> 🔎 `zoneMessage.value`, c'est le **contenu tapé** par l'utilisateur ; `.length` en donne le **nombre de caractères**. `500 − ce nombre` = ce qu'il reste.
**✅ Teste maintenant :** tape du texte → le nombre doit **descendre** à chaque lettre (499, 498, …) et **s'arrêter à 0** quand tu atteins la limite. Si oui : bravo, tu viens d'écrire ta première vraie logique en JavaScript ! 🎉
> 🎯 **Défis bonus** (dans l'ordre de difficulté) :
> 1. Passe le compteur en **rouge** quand il reste moins de 50 caractères :
> ```js
> compteur.style.color = (MAX - dejaTape) < 50 ? "red" : "black";
> ```
> 2. Affiche le texte « caractères » au singulier quand il reste 1 ou 0.
> 3. Désactive le bouton « Envoyer » tant que le message est vide.
---
# 🎓 Niveau 2 — Les conditions (et de nouveaux exos JS)
**Bravo Mélissa** 👏 — tu as fait marcher le compteur **et** le bonus couleur (le compteur qui passe en rouge). Tu es prête pour la suite : les **conditions**, c'est-à-dire apprendre au programme à **choisir** quoi faire selon la situation.
> 🩹 **Petite correction au passage** (vu dans ton HTML) : ton champ e-mail est `type="e_mail"`. Ce type n'existe pas → le navigateur le traite comme du texte normal. Le bon, c'est `type="email"` (sans underscore). Avec, le navigateur vérifie tout seul qu'il y a bien un `@`. 😉
---
## 🧠 La leçon : `if` / `else` — faire **choisir** le programme
Une **condition**, c'est une question qui n'a que deux réponses : **vrai** ou **faux**. En JavaScript on écrit :
```js
if (/* une question */) {
// ce qu'on fait SI la réponse est "vrai"
} else {
// ce qu'on fait SINON
}
```
Pour poser la question, on utilise des **opérateurs de comparaison** :
| Opérateur | Question posée | Exemple vrai |
|---|---|---|
| `===` | est **égal à** ? | `restant === 1` |
| `!==` | est **différent de** ? | `restant !== 0` |
| `>` `<` | plus grand / plus petit ? | `restant < 50` |
| `>=` `<=` | plus grand/petit **ou égal** ? | `restant >= 1` |
> ⚠️ **Le piège n°1 des débutants :** `=` et `===` ne veulent PAS dire la même chose !
> - `=` (un seul) → **ranger** une valeur : `restant = 5` (« mets 5 dans restant »).
> - `===` (trois) → **comparer** : `restant === 5` (« est-ce que restant vaut 5 ? »).
> Dans un `if (...)`, c'est **toujours** `===`.
---
## ✍️ Exo : afficher « caractère » au singulier
Aujourd'hui ton HTML affiche toujours « caractères » au pluriel, même quand il n'en reste qu'**un**. On va corriger ça avec une condition.
### Étape 1 — Donner une étiquette au mot dans le HTML
Pour pouvoir changer le **mot** tout seul, on l'enferme dans son propre `<span>` :
```html
<p class="small">Il vous reste <span id="restant">500</span> <span id="mot">caractères</span>.</p>
```
### Étape 2 — Choisir le bon mot en JavaScript
Dans `contact.js`, attrape ce nouveau `<span>` (en haut, avec les autres) :
```js
const motCaractere = document.getElementById("mot");
```
Puis, **à l'intérieur** de ton `addEventListener("input", …)`, ajoute la condition :
```js
zoneMessage.addEventListener("input", function () {
const dejaTape = zoneMessage.value.length;
const restant = MAX - dejaTape; // on range le calcul dans une variable, plus lisible
compteur.textContent = restant;
compteur.style.color = restant < 50 ? "red" : "black";
// 👇 la nouvelle condition
if (restant === 1) {
motCaractere.textContent = "caractère"; // singulier
} else {
motCaractere.textContent = "caractères"; // pluriel
}
});
```
**✅ Teste maintenant :** tape jusqu'à ce qu'il reste exactement **1** → tu dois lire « Il vous reste 1 **caractère** ». À 2 ou plus → « caractères ». 🎉
> 🔎 **Tu connaissais déjà une condition sans le savoir !** Ton bonus couleur :
> ```js
> compteur.style.color = restant < 50 ? "red" : "black";
> ```
> C'est exactement un `if`/`else`… en **version courte** (on l'appelle le *ternaire*). La version longue équivalente serait :
> ```js
> if (restant < 50) {
> compteur.style.color = "red";
> } else {
> compteur.style.color = "black";
> }
> ```
> Les deux font la même chose. Le `? :` est juste un raccourci pratique quand il n'y a qu'**une** ligne à choisir. 👍
---
## 🧪 De nouveaux exos pour découvrir plus de JavaScript
Chaque exo t'apprend **une nouvelle notion**. Fais-les dans l'ordre, et teste à chaque fois.
### 🧪 Exo 1 — Empêcher l'envoi d'un message vide
> 🆕 Nouvelle notion : réagir à l'**envoi du formulaire** (`"submit"`) et **bloquer** l'action par défaut.
Aujourd'hui, si on clique « Envoyer » avec un message vide, le formulaire part quand même. On va l'en empêcher.
```js
const formulaire = document.querySelector("form"); // on attrape le <form>
formulaire.addEventListener("submit", function (evenement) {
if (zoneMessage.value.trim() === "") { // .trim() enlève les espaces autour
evenement.preventDefault(); // ⛔ on ANNULE l'envoi
alert("Merci d'écrire un message avant d'envoyer !");
}
});
```
**✅ Teste :** clique « Envoyer » sans rien écrire → une alerte apparaît et la page ne se recharge pas. Écris un message → l'envoi repart normalement.
> 🔎 `evenement` est une info que le navigateur passe à ta fonction (« voici ce qui s'est passé »). `evenement.preventDefault()` veut dire « n'effectue PAS l'action habituelle » (ici : ne soumets pas le formulaire). `.trim()` sert à considérer « 3 espaces » comme un message vide.
### 🧪 Exo 2 — Un bouton « Effacer le message »
> 🆕 Nouvelle notion : **ranger du code dans une fonction** pour le réutiliser (ne pas se répéter).
Ajoute un bouton dans le HTML, à côté de « Envoyer » :
```html
<button type="button" id="effacer">Effacer</button>
```
Le souci : quand on efface, il faut aussi **remettre le compteur à 500**. On a donc besoin de réafficher le compteur à **deux endroits** (à la frappe ET à l'effacement). La bonne pratique : écrire ce calcul **une seule fois** dans une fonction, et l'appeler aux deux endroits.
```js
const boutonEffacer = document.getElementById("effacer");
// une fonction qui remet l'affichage à jour à partir du texte actuel
function rafraichirCompteur() {
const restant = MAX - zoneMessage.value.length;
compteur.textContent = restant;
compteur.style.color = restant < 50 ? "red" : "black";
motCaractere.textContent = restant === 1 ? "caractère" : "caractères";
}
// à la frappe : on rafraîchit
zoneMessage.addEventListener("input", rafraichirCompteur);
// au clic sur Effacer : on vide la zone PUIS on rafraîchit
boutonEffacer.addEventListener("click", function () {
zoneMessage.value = ""; // on vide la textarea
rafraichirCompteur(); // et on remet le compteur à 500
});
```
**✅ Teste :** écris du texte, clique « Effacer » → la zone se vide **et** le compteur repasse à 500.
> 🔎 Tu viens de **factoriser** : au lieu de recopier les mêmes lignes deux fois, tu les as mises dans `rafraichirCompteur()` et tu l'appelles. Si un jour tu changes l'affichage, tu ne le modifies **qu'à un seul endroit**. C'est exactement la même idée que le **composant réutilisable** plus bas. ♻️
### 🧪 Exo 3 — « FleetZen est-il joignable maintenant ? »
> 🆕 Nouvelle notion : lire l'**heure actuelle** (`new Date()`) et combiner deux conditions avec `&&` (ET).
Une page contact, ça parle d'horaires. Affichons un petit badge « ✅ Ouvert » ou « 🔴 Fermé » selon l'heure (ouvert de 9h à 18h).
D'abord un endroit pour l'afficher, dans le HTML :
```html
<p id="statut_horaires"></p>
```
Puis en JavaScript :
```js
const badge = document.getElementById("statut_horaires");
const heure = new Date().getHours(); // l'heure actuelle, de 0 à 23
if (heure >= 9 && heure < 18) { // && = "ET" : les DEUX doivent être vraies
badge.textContent = "✅ Nous sommes ouverts (9h–18h)";
badge.style.color = "green";
} else {
badge.textContent = "🔴 Fermé — écrivez-nous, on répond dès l'ouverture !";
badge.style.color = "red";
}
```
**✅ Teste :** recharge la page. Selon l'heure qu'il est, tu vois « Ouvert » ou « Fermé ». (Pour tester l'autre cas sans attendre, change temporairement `9` et `18`.)
> 🔎 `new Date().getHours()` donne l'heure de l'ordinateur (ex. `14`). `&&` se lit « ET » : `heure >= 9 && heure < 18` n'est vrai que si l'heure est **à la fois** ≥ 9 **et** < 18. Son cousin est `||` qui se lit « OU ».
---
> 🗺️ **Où tu en es maintenant** : tu sais **sélectionner**, **réagir**, **modifier**, et maintenant **faire choisir** ton programme avec des conditions, **factoriser** dans des fonctions, et lire des infos comme l'heure. C'est déjà le cœur du JavaScript du quotidien. La suite (boucles `for`, tableaux) viendra naturellement — demande-moi quand tu veux ! 🚀
---
## ♻️ Bonus — La notion de **composant réutilisable** (la navbar)
Regarde : la barre du haut (`#nav_barre`) est **recopiée à l'identique** dans `index.html`, `services.html` et `contact.html`. Le jour où on change un lien, il faut le modifier **dans 3 fichiers** → source d'erreurs (c'est d'ailleurs comme ça que le Bug 2 se glisse 😉).
C'est **le** problème que résolvent les **composants** : on écrit la navbar **une seule fois**, et on la réutilise partout.
Première approche très simple en JavaScript : une fonction qui **génère** la navbar.
```js
function creerNavbar() {
return `
<div id="nav_barre" class="shadow">
<h1>FleetZen</h1>
<div id="buttons">
<button class="instyled">contact</button>
<a href="services.html"><button class="styled">services</button></a>
<a href="../index.html"><button class="styled">retour</button></a>
</div>
</div>`;
}
// On l'injecte tout en haut du body :
document.body.insertAdjacentHTML("afterbegin", creerNavbar());
```
**✅ Teste :** enlève la navbar écrite « en dur » dans le HTML, garde ce JS, recharge → la navbar réapparaît, générée par la fonction. Maintenant le jour où tu changes un lien, tu le changes **à un seul endroit** pour **toutes** les pages (fini les liens cassés du Bug 2 😉).
> 💡 **À retenir :** un **composant** = un bout d'interface écrit **une fois** et **réutilisé** partout. C'est l'idée centrale derrière React, Vue, etc. — mais ça commence par une simple fonction comme celle-ci.
---
## ✅ Ta checklist
- [ ] Fermer `body` avec un `}` dans le CSS (Bug 1 — le plus urgent)
- [ ] Réparer le lien vers `services.html` (Bug 2)
- [ ] Passer le champ e-mail en `type="email"` (Bug 3 — tu as mis `e_mail`, presque ! 😉)
- [ ] Centrer l'icône SVG du bouton « retour » avec `inline-flex` (Bug 4)
- [ ] Ajouter le redimensionnement borné (`resize: vertical` + `min/max-height`)
- [ ] Centrer table et `.form` avec `margin: auto` (au lieu de `margin-left: 600px`)
- [x] Ajouter `rows`, `cols`, `maxlength` à la textarea ✅
- [x] Faire marcher le **compteur de caractères** en JavaScript ✅
- [x] Bonus couleur : compteur en rouge sous 50 caractères ✅
- [x] Ajouter un `<footer>` ✅
- [ ] (Bonus) Transformer la navbar en composant réutilisable
- [ ] Penser à fermer la balise `</html>` en fin de fichier
### 🎓 Niveau 2 (nouveau)
- [ ] Afficher « caractère » au **singulier** quand il en reste 1 (leçon `if`/`else`)
- [ ] Exo 1 — empêcher l'envoi d'un message **vide** (`submit` + `preventDefault`)
- [ ] Exo 2 — bouton **« Effacer »** + factoriser dans une fonction
- [ ] Exo 3 — badge **« Ouvert / Fermé »** selon l'heure (`new Date()` + `&&`)
- [ ] Commits petits et clairs, puis `git push`
Pose-moi toutes tes questions. Tu gères ! 📬✨
+456
View File
@@ -0,0 +1,456 @@
# 🚀 Niveau 3 — JavaScript : tableaux, boucles & pages vivantes
Salut Mélissa ! 👋
Tu as fini tout le Niveau 2 (conditions, singulier/pluriel, envoi bloqué si vide, bouton « Effacer », badge horaires, navbar en composant). On enchaîne.
> 🔎 Détail au passage : ton `restant <= 1 ? "caractère" : "caractères"` est correct — en français, **0 prend le singulier** (« 0 caractère »).
Ce fichier est une série d'exercices **qui s'enchaînent**, chacun introduisant **une notion de plus**. Fil rouge : rendre la page **vivante et pilotée par des données**. Il y en a beaucoup, de quoi tenir l'après-midi — tu n'es pas obligée de tout finir.
> 🧭 **Mode d'emploi** (toujours le même) :
> - Avance **dans l'ordre**, un exo à la fois.
> - **Teste** (✅) avant de passer au suivant.
> - Garde la **Console (F12)** ouverte : c'est ton tableau de bord.
> - Bloquée ? C'est **normal** : note ta question et on en parle.
---
## 🗺️ Le programme de l'après-midi
| Module | Tu vas apprendre… | Exos |
|---|---|---|
| **A. Tableaux & boucles** | stocker une liste, la parcourir, générer du HTML automatiquement | 1 → 4 |
| **B. Manipuler les listes** | rechercher, trier, filtrer | 5 → 6 |
| **C. La mémoire** | sauvegarder des infos qui survivent au rechargement | 7 → 8 |
| **D. Le style dynamique** | changer l'apparence au clic, faire un accordéon | 9 → 10 |
| **E. Pour finir en beauté** | factoriser, paramétrer, défis libres | 11 → 12 |
---
# 🧱 Module A — Les tableaux et les boucles
Jusqu'ici tes variables contenaient **une seule** valeur (`const MAX = 500`). Mais comment ranger **une liste** ? Le nom de tes 2 développeurs, par exemple ? → avec un **tableau** (`array`).
## 🧩 La notion : un tableau, c'est une liste
```js
const fruits = ["pomme", "banane", "cerise"];
console.log(fruits[0]); // "pomme" → on compte à partir de 0 !
console.log(fruits.length); // 3 → combien d'éléments
```
> 🔎 **Point qui surprend tout le monde au début :** on compte **à partir de 0**. Le 1ᵉ élément est `fruits[0]`, le 2ᵉ est `fruits[1]`… Le dernier est donc `fruits[fruits.length - 1]`.
## 🧩 La notion : une boucle, pour répéter sans copier-coller
Pour faire quelque chose **avec chaque** élément d'une liste, on utilise une **boucle** `for...of` :
```js
for (const fruit of fruits) {
console.log("J'aime la " + fruit);
}
// affiche 3 lignes, une par fruit
```
> 💡 **À retenir :** `for...of` se lit « **pour chaque** *fruit* **parmi** *fruits*, fais… ». La boucle s'occupe de passer d'un élément au suivant toute seule.
---
## 🧪 Exo 1 — Ta première liste, dans la console
Crée un fichier `equipe.js` (on garde ton `contact.js` pour le formulaire). Branche-le dans ta page, **avant** `contact.js`, juste avant `</body>` :
```html
<script src="../equipe.js"></script>
<script src="../contact.js"></script>
```
Dans `equipe.js` :
```js
const equipe = ["Mélissa", "Guillaume"];
for (const personne of equipe) {
console.log("Membre de l'équipe : " + personne);
}
```
**✅ Teste :** ouvre la Console (F12). Tu dois voir 2 lignes. Ajoute un 3ᵉ prénom au tableau, recharge → 3 lignes, **sans toucher à la boucle**.
---
## 🧪 Exo 2 — Des données plus riches : les **objets**
Un prénom tout seul, c'est peu. Pour décrire une personne (nom **+** job **+** partie créée), on utilise un **objet** `{ … }` :
```js
const membre = {
nom: "Mélissa",
job: "Stagiaire",
partie: "Page contact"
};
console.log(membre.nom); // "Mélissa"
console.log(membre.partie); // "Page contact"
```
Et une **liste d'objets** = un tableau d'objets. Remplace le contenu de `equipe.js` :
```js
const equipe = [
{ nom: "Mélissa", job: "Stagiaire", partie: "Page contact" },
{ nom: "Guillaume", job: "Stagiaire", partie: "Page services" }
];
for (const membre of equipe) {
console.log(membre.nom + " — " + membre.job + " — " + membre.partie);
}
```
**✅ Teste :** la Console affiche une ligne par membre, avec les 3 infos.
> 🧩 **À retenir :** un **tableau** `[ ]` = une liste ordonnée. Un **objet** `{ }` = une fiche avec des **étiquettes** (`nom`, `job`…). Les deux combinés (`[ {…}, {…} ]`), c'est **la** façon de stocker des données dans presque toutes les applis.
---
## 🧪 Exo 3 — Générer ton tableau HTML **automatiquement**
C'est l'exercice central du module. Aujourd'hui, ton `<table>` d'équipe est **écrit à la main** dans le HTML. Problème : pour ajouter un membre, il faut retaper tout un `<tr>`. On va le faire **générer par le JavaScript** à partir du tableau `equipe`.
### Étape 1 — Préparer le HTML
Dans `contact.html`, garde l'en-tête du tableau (`<thead>`) mais **vide le corps** et donne-lui un `id` :
```html
<table border="1" class="table">
<thead>
<tr>
<th>Nom</th>
<th>Job</th>
<th>Parties créées</th>
</tr>
</thead>
<tbody id="corps_equipe">
<!-- vide : le JavaScript va le remplir tout seul -->
</tbody>
</table>
```
### Étape 2 — Construire les lignes en JavaScript
Dans `equipe.js` :
```js
const corps = document.getElementById("corps_equipe");
let lignes = ""; // on va empiler le HTML dans ce texte
for (const membre of equipe) {
lignes = lignes + `
<tr>
<td>${membre.nom}</td>
<td>${membre.job}</td>
<td>${membre.partie}</td>
</tr>`;
}
corps.innerHTML = lignes; // on injecte tout d'un coup
```
**✅ Teste :** ton tableau s'affiche comme avant, mais cette fois généré par le JS. La preuve : ajoute un 3ᵉ membre dans le tableau `equipe` (ex. `{ nom: "Matthieu", job: "Tuteur", partie: "Relecture" }`), recharge → la ligne apparaît toute seule.
> 🔎 Deux nouveautés ici :
> - **Le `` `texte ${variable}` ``** (avec les accents graves `` ` ``) : ça s'appelle un *template literal*. Le `${…}` insère la valeur d'une variable **dans** le texte. (Tu l'utilises déjà dans `creerNavbar` 😉 !)
> - **`.innerHTML = …`** remplace **tout le contenu HTML** d'un élément. Pratique, mais à réserver à **tes propres** données (jamais avec du texte tapé par un inconnu — question de sécurité, on en reparlera).
> 🔎 **Le principe à retenir :** on sépare les **données** (le tableau `equipe`) de l'**affichage** (la boucle qui fabrique le HTML). Change les données → l'affichage suit. C'est le mode de fonctionnement de React, Vue, etc.
---
## 🧪 Exo 4 — Compter et afficher un total
Au-dessus du tableau, ajoute dans le HTML :
```html
<p>👥 L'équipe compte <span id="nb_membres"></span> personne(s).</p>
```
Et en JS, après ta boucle :
```js
document.getElementById("nb_membres").textContent = equipe.length;
```
**✅ Teste :** le nombre correspond à la taille de ton tableau. Ajoute/enlève un membre → le compte suit tout seul.
> 💡 `equipe.length`, tu l'avais déjà croisé avec `zoneMessage.value.length` : `.length`, c'est « **combien** ». Sur un texte = nombre de caractères ; sur un tableau = nombre d'éléments.
---
# 🔎 Module B — Rechercher et trier dans une liste
## 🧪 Exo 5 — Une **barre de recherche** qui filtre l'équipe en direct
> 🆕 Nouvelles notions : `.filter()`, `.toLowerCase()`, `.includes()`, et **ré-afficher** quand les données changent.
D'abord, range ton affichage du tableau dans une **fonction** (pour pouvoir le rejouer) :
```js
function afficherEquipe(liste) {
let lignes = "";
for (const membre of liste) {
lignes += `
<tr>
<td>${membre.nom}</td>
<td>${membre.job}</td>
<td>${membre.partie}</td>
</tr>`;
}
corps.innerHTML = lignes;
}
afficherEquipe(equipe); // affichage initial : toute l'équipe
```
> 🔎 `lignes += "…"` est un raccourci pour `lignes = lignes + "…"`. Pratique pour empiler.
Ajoute un champ de recherche dans le HTML, au-dessus du tableau :
```html
<input type="search" id="recherche" placeholder="Rechercher un membre…">
```
Puis en JS :
```js
const champRecherche = document.getElementById("recherche");
champRecherche.addEventListener("input", function () {
const motCherche = champRecherche.value.toLowerCase();
// on garde seulement les membres dont le nom contient le texte tapé
const resultats = equipe.filter(function (membre) {
return membre.nom.toLowerCase().includes(motCherche);
});
afficherEquipe(resultats); // on ré-affiche, mais filtré
});
```
**✅ Teste :** tape « mé » → seule Mélissa reste. Efface → toute l'équipe revient.
> 🧩 **À retenir :**
> - `.filter(...)` crée un **nouveau** tableau avec seulement les éléments qui passent le test.
> - `.toLowerCase()` met en minuscules → la recherche ne tient pas compte des majuscules.
> - `.includes("…")` répond vrai/faux : « ce texte contient-il … ? ».
---
## 🧪 Exo 6 — Un bouton « Trier par nom »
> 🆕 Nouvelle notion : `.sort()`.
```html
<button type="button" id="trier">Trier A → Z</button>
```
```js
document.getElementById("trier").addEventListener("click", function () {
const triee = [...equipe].sort(function (a, b) {
return a.nom.localeCompare(b.nom);
});
afficherEquipe(triee);
});
```
**✅ Teste :** clique → l'équipe s'affiche dans l'ordre alphabétique.
> 🔎 `[...equipe]` fait une **copie** du tableau (pour ne pas modifier l'original). `localeCompare` compare deux mots dans l'ordre alphabétique (et gère les accents français correctement). C'est un peu abstrait : retiens juste la **recette**, tu la comprendras à force de l'utiliser.
---
# 💾 Module C — Donner une mémoire à ta page
Recharge ta page : tout ce que l'utilisateur avait tapé **disparaît**. On va y remédier avec le **`localStorage`** : un petit coffre-fort dans le navigateur qui **survit au rechargement**.
## 🧪 Exo 7 — Sauvegarder le brouillon du message
> 🆕 Nouvelle notion : `localStorage.setItem(...)` / `localStorage.getItem(...)`.
Dans `contact.js`, ajoute : à **chaque frappe**, on sauvegarde le message.
```js
zoneMessage.addEventListener("input", function () {
localStorage.setItem("brouillon", zoneMessage.value);
});
```
> ⚠️ Tu as **déjà** un `addEventListener("input", rafraichirCompteur)` sur la même zone. Pas de souci : on peut en mettre plusieurs, ils s'exécutent tous les deux. (Encore mieux : ajoute la ligne `localStorage.setItem(...)` **à l'intérieur** de `rafraichirCompteur`.)
**✅ Teste :** tape un message, ouvre la Console et tape `localStorage.getItem("brouillon")` → tu vois ton texte.
## 🧪 Exo 8 — Recharger le brouillon au démarrage
Au tout début (au chargement de la page), on regarde si un brouillon existe :
```js
const brouillon = localStorage.getItem("brouillon");
if (brouillon) { // s'il y a quelque chose de sauvegardé
zoneMessage.value = brouillon;
rafraichirCompteur(); // pour que le compteur soit juste dès le départ
}
```
Et quand le message est envoyé avec succès, on vide le coffre (sinon le vieux brouillon revient) :
```js
// à ajouter dans ton addEventListener("submit", …), quand le message N'EST PAS vide
localStorage.removeItem("brouillon");
```
**✅ Teste :** écris un message, **recharge la page** (F5) → ton texte est toujours là, et le compteur est correct.
> 💡 **À retenir :** `localStorage` garde des infos **entre les visites** (jusqu'à ce qu'on les efface). `setItem(clé, valeur)` pour ranger, `getItem(clé)` pour relire, `removeItem(clé)` pour effacer.
---
# 🎨 Module D — Changer l'apparence en JavaScript
## 🧪 Exo 9 — Un bouton « Mode sombre 🌙 »
> 🆕 Nouvelle notion : `classList.toggle(...)` — ajouter/retirer une classe CSS au clic.
D'abord, prépare la classe dans ton CSS (`contact.css`) :
```css
body.sombre {
background-color: #1d2733;
color: white;
}
```
Un bouton dans le HTML :
```html
<button type="button" id="theme">🌙 Mode sombre</button>
```
Et le JS :
```js
document.getElementById("theme").addEventListener("click", function () {
document.body.classList.toggle("sombre");
});
```
**✅ Teste :** clique → la page bascule en sombre, re-clique → elle revient. Un seul `toggle` fait les deux !
> 🧩 **À retenir :** `classList.toggle("sombre")` = « si la classe est là, enlève-la ; sinon, ajoute-la ». C'est **le** pont entre ton JavaScript et ton CSS : le JS gère le *quand*, le CSS gère le *à quoi ça ressemble*.
---
## 🧪 Exo 10 — Une FAQ en accordéon
> 🆕 Nouvelles notions : `querySelectorAll(...)` (sélectionner **plusieurs** éléments) + une boucle `forEach`.
Beaucoup de pages contact ont une FAQ. Ajoute dans le HTML :
```html
<section class="faq">
<h3>Questions fréquentes</h3>
<div class="question">
<button class="q-titre" type="button">Comment suivre ma flotte en temps réel ?</button>
<p class="q-reponse">Grâce à notre service de géolocalisation, accessible 24h/24.</p>
</div>
<div class="question">
<button class="q-titre" type="button">Puis-je résilier à tout moment ?</button>
<p class="q-reponse">Oui, sans frais, depuis votre espace client.</p>
</div>
</section>
```
Dans le CSS, on cache les réponses au départ :
```css
.q-reponse { display: none; }
.q-reponse.ouverte { display: block; }
```
En JavaScript, on veut que **chaque** bouton ouvre/ferme **sa** réponse :
```js
const titres = document.querySelectorAll(".q-titre"); // TOUS les boutons FAQ
titres.forEach(function (titre) {
titre.addEventListener("click", function () {
// la réponse est le paragraphe juste après le bouton
const reponse = titre.nextElementSibling;
reponse.classList.toggle("ouverte");
});
});
```
**✅ Teste :** clique sur une question → sa réponse apparaît ; re-clique → elle se cache. Chaque question est indépendante.
> 🧩 **À retenir :**
> - `getElementById` attrape **un** élément ; `querySelectorAll(".classe")` en attrape **plusieurs** (tous ceux qui ont cette classe).
> - `.forEach(...)` est une boucle qui fait quelque chose **pour chaque** élément trouvé — ici : « ajoute un écouteur de clic à chacun ».
> - `nextElementSibling` = « l'élément juste après moi ». Pratique pour relier un bouton à sa réponse.
---
# 🏁 Module E — Pour finir en beauté
## 🧪 Exo 11 — Une navbar qui sait quelle page est active
Ta `creerNavbar()` est déjà un beau composant. Rendons-la **paramétrable** : on lui dit quelle page est active, et elle met le bon bouton en surbrillance.
> 🆕 Nouvelle notion : une fonction qui prend un **paramètre**.
```js
function creerNavbar(pageActive) { // ← pageActive est le "réglage" qu'on lui donne
return `
<div id="nav_barre" class="shadow">
<h1>FleetZen</h1>
<div id="buttons">
<a href="contact.html"><button class="${pageActive === "contact" ? "instyled" : "styled"}">contact</button></a>
<a href="services.html"><button class="${pageActive === "services" ? "instyled" : "styled"}">services</button></a>
<a href="../index.html"><button class="styled">retour</button></a>
</div>
</div>`;
}
document.body.insertAdjacentHTML("afterbegin", creerNavbar("contact"));
```
**✅ Teste :** sur la page contact, le bouton « contact » apparaît enfoncé (`instyled`). Le jour où Guillaume réutilise cette fonction sur sa page, il écrira juste `creerNavbar("services")` → c'est SON bouton qui sera actif.
> 💡 **À retenir :** un **paramètre** (`pageActive`), c'est un réglage qu'on passe à une fonction pour qu'elle s'adapte. Une seule fonction, plusieurs résultats selon ce qu'on lui donne. C'est ÇA, un vrai composant réutilisable. ♻️
---
## 🎯 Exo 12 — Défis libres (si tu as encore de l'énergie)
Choisis ceux qui t'attirent, dans l'ordre que tu veux :
1. **Recap avant envoi** : à la soumission, affiche « Merci {prénom}, ton message de {N} caractères a bien été pris en compte ! » en lisant la valeur du champ prénom.
2. **Validation e-mail maison** : si l'e-mail ne contient pas de `@`, bloque l'envoi avec un message (en plus de la vérification du navigateur).
3. **Compteur de mots** (en plus des caractères) : `zoneMessage.value.trim().split(" ").length`.
4. **Ajouter un membre via un formulaire** : un petit champ + bouton qui fait `equipe.push({...})` puis `afficherEquipe(equipe)`.
5. **Mémoriser le mode sombre** dans `localStorage` pour qu'il reste actif au rechargement.
---
## 🗺️ Récap — tout ce que tu sais faire maintenant
- **Données** : variables, **tableaux** `[ ]`, **objets** `{ }`, listes d'objets
- **Répéter** : boucles `for...of` et `.forEach()`
- **Transformer des listes** : `.filter()`, `.sort()`
- **Générer du HTML** depuis des données (`innerHTML` + *template literals*)
- **Mémoire** : `localStorage` (setItem / getItem / removeItem)
- **Style dynamique** : `classList.toggle()`
- **Sélection multiple** : `querySelectorAll` + `forEach`
- **Fonctions paramétrées** : un composant qui s'adapte
Prochaine grande étape (pour une autre fois) : **les requêtes vers un serveur** avec `fetch`, pour aller chercher de vraies données en ligne.
+239
View File
@@ -0,0 +1,239 @@
# 🚀 Niveau 4 — DOM propre, données distantes & temps réel
Salut Mélissa ! 👋
Suite logique de ce que tu sais déjà faire (tableaux, boucles, `localStorage`, `classList`, événements…). Quatre modules, du plus simple au plus costaud. Comme d'habitude : un exo à la fois, et **✅ teste** avant de passer au suivant.
| Module | Notion nouvelle | Exos |
|---|---|---|
| **A. Construire le DOM proprement** | `createElement` / `append` (au lieu de `innerHTML`) | 1 → 2 |
| **B. Aller chercher des données ailleurs** | `fetch`, `async`/`await`, JSON | 3 → 5 |
| **C. Le temps réel** | `setInterval` | 6 → 7 |
| **D. Le clavier** | événements clavier (`keydown`, `event.key`) | 8 |
---
# 🧱 Module A — Construire le HTML sans `innerHTML`
Pour ton tableau d'équipe, tu utilises `corps.innerHTML = lignes`. Ça marche très bien pour **tes** données. Mais `innerHTML` a deux limites :
- si tu y mets du texte tapé par un **inconnu**, il peut injecter du code (faille de sécurité dite *XSS*) ;
- ça remplace **tout** d'un coup, difficile de modifier une seule ligne ensuite.
La méthode « propre » consiste à fabriquer chaque élément **un par un**.
## 🧩 La notion : `createElement` + `textContent` + `append`
```js
const ligne = document.createElement("tr"); // on crée une balise <tr> (vide, pas encore dans la page)
const caseNom = document.createElement("td");
caseNom.textContent = "Mélissa"; // textContent = du texte pur, jamais interprété comme du HTML
ligne.append(caseNom); // on range la <td> dans la <tr>
```
> 🔎 `createElement` fabrique l'élément **en mémoire** ; il n'apparaît à l'écran qu'une fois **rattaché** à la page avec `append`. `textContent` (et non `innerHTML`) garantit que le contenu reste du **texte**, même si quelqu'un tape `<script>`.
## 🧪 Exo 1 — Réécrire une ligne du tableau « proprement »
Dans `equipe.js`, écris une fonction qui fabrique **une** ligne à partir d'un membre :
```js
function creerLigne(membre) {
const tr = document.createElement("tr");
const tdNom = document.createElement("td");
tdNom.textContent = membre.nom;
const tdJob = document.createElement("td");
tdJob.textContent = membre.job;
const tdPartie = document.createElement("td");
tdPartie.textContent = membre.partie;
tr.append(tdNom, tdJob, tdPartie); // append accepte plusieurs éléments d'un coup
return tr;
}
```
## 🧪 Exo 2 — Remplir le tableau avec cette fonction
```js
const corps = document.getElementById("corps_equipe");
corps.innerHTML = ""; // on vide d'abord
for (const membre of equipe) {
corps.append(creerLigne(membre)); // on ajoute chaque ligne fabriquée
}
```
**✅ Teste :** le tableau s'affiche comme avant. La différence est invisible à l'écran, mais ton code est maintenant plus sûr et chaque ligne est un vrai objet manipulable.
> 💡 **À retenir :** `innerHTML` = rapide, pour **tes** données fixes. `createElement` + `textContent` = la méthode propre, surtout dès qu'il y a du contenu venant de l'**utilisateur**.
---
# 🌐 Module B — Aller chercher des données ailleurs (`fetch`)
Jusqu'ici tes données (`equipe`) sont **écrites dans le code**. Dans la vraie vie, elles arrivent **d'un serveur**, sous forme de **JSON** (un format texte très proche des objets/tableaux que tu connais déjà).
## ⚠️ À lire avant de commencer : il faut un petit serveur
`fetch` **ne fonctionne pas** si tu ouvres ta page en double-cliquant (adresse en `file://`). Il faut servir le dossier. Dans un terminal, place-toi dans le dossier du projet et lance :
```bash
python3 -m http.server 8000
```
Puis ouvre **http://localhost:8000/pages/contact.html** dans le navigateur. (C'est exactement ce que fait un vrai site : un serveur qui envoie les fichiers.)
## 🧪 Exo 3 — Créer un fichier de données JSON
Crée `vehicules.json` à la racine du projet :
```json
[
{ "modele": "Renault Kangoo", "immatriculation": "AB-123-CD", "km": 45200 },
{ "modele": "Peugeot Partner", "immatriculation": "EF-456-GH", "km": 78900 },
{ "modele": "Citroën Berlingo", "immatriculation": "IJ-789-KL", "km": 12300 }
]
```
> 🔎 Le JSON ressemble à un tableau d'objets JS, avec **deux règles strictes** : toutes les clés sont **entre guillemets doubles**, et **pas de virgule** après le dernier élément. Une erreur ici et tout casse — colle-le dans un validateur si besoin.
## 🧪 Exo 4 — Récupérer ce fichier avec `fetch`
> 🆕 Nouvelles notions : `fetch`, `async`/`await`. `fetch` va chercher un fichier/des données **sur le réseau** ; comme ça prend un peu de temps, on **attend** la réponse avec `await`.
Dans un nouveau fichier `flotte.js` (branché dans la page) :
```js
async function chargerFlotte() {
const reponse = await fetch("../vehicules.json"); // on demande le fichier
const vehicules = await reponse.json(); // on le transforme en tableau JS
console.log(vehicules); // pour vérifier
}
chargerFlotte();
```
**✅ Teste :** via http://localhost:8000/…, ouvre la Console → tu dois voir ton tableau de 3 véhicules. Si tu vois une erreur, relis la partie « petit serveur » ci-dessus.
> 🧩 **À retenir :**
> - `async` devant une fonction = « cette fonction contient de l'attente ».
> - `await` = « attends que ce soit prêt avant de continuer ».
> - `fetch(...)` rapporte une **réponse**, et `reponse.json()` la transforme en données JS utilisables.
## 🧪 Exo 5 — Afficher la flotte + gérer les erreurs
Ajoute un tableau dans le HTML :
```html
<h3>Notre flotte</h3>
<p id="message_flotte">Chargement…</p>
<table border="1"><tbody id="corps_flotte"></tbody></table>
```
Puis complète `flotte.js` :
```js
async function chargerFlotte() {
const message = document.getElementById("message_flotte");
const corps = document.getElementById("corps_flotte");
try {
const reponse = await fetch("../vehicules.json");
const vehicules = await reponse.json();
message.textContent = vehicules.length + " véhicules :";
for (const v of vehicules) {
const tr = document.createElement("tr");
tr.textContent = `${v.modele} — ${v.immatriculation} — ${v.km} km`;
corps.append(tr);
}
} catch (erreur) {
message.textContent = "Impossible de charger la flotte 😕";
console.error(erreur);
}
}
chargerFlotte();
```
**✅ Teste :** la liste des véhicules s'affiche. Renomme exprès `vehicules.json` en `vehicule.json` (faute) et recharge → tu dois voir le message d'erreur au lieu d'un plantage.
> 🧩 **À retenir :** `try { … } catch (erreur) { … }` = « **essaie** ce code ; **si** ça rate, fais ça plutôt ». Indispensable avec le réseau, où ça peut toujours échouer (pas de connexion, fichier absent…).
---
# ⏱️ Module C — Le temps réel avec `setInterval`
## 🧪 Exo 6 — Une horloge dans le pied de page
> 🆕 Nouvelle notion : `setInterval(fonction, durée)` répète une action toutes les *durée* millisecondes.
```html
<span id="horloge"></span>
```
```js
const horloge = document.getElementById("horloge");
function afficherHeure() {
const maintenant = new Date();
horloge.textContent = maintenant.toLocaleTimeString(); // ex. "14:05:32"
}
afficherHeure(); // une fois tout de suite
setInterval(afficherHeure, 1000); // puis toutes les 1000 ms = 1 seconde
```
**✅ Teste :** l'heure s'affiche et **avance toute seule** chaque seconde.
> 🔎 `1000` est en **millisecondes** (1 s). `setInterval` ne s'arrête jamais tout seul ; pour le stopper un jour, on garde son résultat (`const id = setInterval(...)`) et on fait `clearInterval(id)`.
## 🧪 Exo 7 — Mettre à jour le badge « Ouvert / Fermé » en direct
Ton badge horaires est calculé **une seule fois** au chargement. Range ce calcul dans une fonction `rafraichirBadge()` puis appelle-la dans ton `setInterval` (par exemple toutes les 60 s). Ainsi, si la page reste ouverte à 18h pile, le badge passera tout seul à « Fermé ».
**✅ Teste :** pour vérifier sans attendre, mets temporairement l'intervalle à `1000` et change les heures d'ouverture autour de l'heure actuelle.
---
# ⌨️ Module D — Réagir au clavier
## 🧪 Exo 8 — Envoyer avec Ctrl+Entrée, effacer avec Échap
> 🆕 Nouvelle notion : l'événement `keydown` et la propriété `event.key`.
```js
zoneMessage.addEventListener("keydown", function (event) {
// Ctrl + Entrée → on déclenche l'envoi du formulaire
if (event.key === "Enter" && event.ctrlKey) {
formulaire.requestSubmit(); // comme cliquer sur "Envoyer"
}
// Touche Échap → on vide la zone
if (event.key === "Escape") {
zoneMessage.value = "";
rafraichirCompteur();
}
});
```
**✅ Teste :** écris un message, fais **Ctrl+Entrée** → l'envoi se déclenche. Appuie sur **Échap** → la zone se vide.
> 🔎 `event.key` te donne le **nom** de la touche (`"Enter"`, `"Escape"`, `"a"`…). `event.ctrlKey` est vrai si Ctrl est maintenue. C'est comme ça qu'on crée des raccourcis clavier.
---
## 🗺️ Ce que ce niveau ajoute à ta boîte à outils
- **DOM propre** : `createElement`, `textContent`, `append`
- **Données distantes** : `fetch`, `async`/`await`, JSON, `try`/`catch`
- **Temps réel** : `setInterval` / `clearInterval`
- **Clavier** : `keydown`, `event.key`, raccourcis
Prochaine étape possible : organiser ton code en plusieurs fichiers/modules, ou afficher des données depuis une **vraie API publique** en ligne. Dis-moi vers quoi tu veux aller.
Note tes questions au fur et à mesure.
+144
View File
@@ -0,0 +1,144 @@
# 🧭 Niveau 5 — aller plus loin (mode indices)
Salut Mélissa ! 👋
> 📌 À faire **une fois le Niveau 4 terminé** (`createElement`, `fetch`, `setInterval`, clavier).
Petit changement de format à partir d'ici : **je ne donne plus le code tout fait.** Pour chaque exo tu as un **🎯 objectif**, des **🔍 pistes** (quoi chercher, quels mots-clés, les pièges) et un **✅ test**. À toi d'écrire le code.
> 🔎 Tu as maintenant assez de bases pour lire la doc directement. Le réflexe : chercher **« MDN » + le nom de la chose** (ex. `MDN Array map`). Cherche d'abord **le concept**, le code viendra de toi.
| Module | Notions visées | Exos |
|---|---|---|
| **A. Tableaux, niveau 2** | `map`, `find`, `reduce` | 1 → 3 |
| **B. Écouteurs malins** | délégation d'événements, anti-rebond | 4 → 5 |
| **C. Organiser son code** | modules `export` / `import` | 6 |
| **D. Une vraie API en ligne** | `fetch` vers le web réel | 7 |
| **E. Lire un formulaire d'un coup** | `FormData` | 8 |
---
# 🧱 Module A — Les tableaux, niveau 2
Tu connais déjà `filter` (garder certains éléments) et `sort` (trier). Voici trois autres méthodes qui reviennent **tout le temps**.
## 🧪 Exo 1 — `map` : transformer une liste
**🎯 Objectif :** à partir de ton tableau `equipe`, fabriquer un **nouveau** tableau qui ne contient que les **noms** (`["Mélissa", "Guillaume"]`).
**🔍 Pistes :**
- Cherche `MDN Array map`. L'idée : `map` applique une transformation à **chaque** élément et renvoie une **nouvelle** liste de même taille.
- Différence avec `filter` : `filter` **garde ou jette** des éléments (la liste rétrécit) ; `map` **transforme** chacun (la liste garde la même taille).
**✅ Test :** un `console.log` affiche bien la liste des noms seuls.
## 🧪 Exo 2 — `find` : retrouver un élément précis
**🎯 Objectif :** retrouver dans `equipe` l'objet de la personne qui a fait la « Page contact ».
**🔍 Pistes :**
- Cherche `MDN Array find`. `find` renvoie le **premier** élément qui remplit une condition (un seul objet, pas une liste).
- Attention au cas « personne ne correspond » : `find` renvoie alors `undefined` → comment l'éviter de planter ? (cherche `optional chaining` ou un simple `if`).
**✅ Test :** `console.log` affiche le bon objet membre.
## 🧪 Exo 3 — `reduce` : calculer un total
**🎯 Objectif :** si tu as fait le `vehicules.json` du Niveau 4, calcule le **kilométrage total** de toute la flotte.
**🔍 Pistes :**
- Cherche `MDN Array reduce`. C'est la méthode la plus abstraite des trois : elle « replie » toute une liste en **une seule valeur** (ici, une somme).
- Repère les deux choses qu'elle attend : une fonction `(accumulateur, élément) => ...` et une **valeur de départ** (pour une somme, laquelle ?).
- Si reduce te paraît trop dense au début, tu peux d'abord le faire avec une boucle `for...of` et une variable `total`, puis le réécrire en `reduce` pour comparer.
**✅ Test :** le total affiché correspond à la somme des `km` de ton JSON.
---
# 🎯 Module B — Des écouteurs d'événements plus malins
## 🧪 Exo 4 — La délégation d'événements
**🎯 Objectif :** rendre **chaque ligne** du tableau d'équipe cliquable (au clic, afficher le nom du membre dans une alerte). Le piège : tes lignes sont **générées** par JS, et parfois recréées (recherche, tri)…
**🔍 Pistes :**
- L'approche naïve serait d'ajouter un écouteur sur chaque `<tr>`. Problème : à chaque ré-affichage, il faut tout réattacher, et on en oublie. Cherche pourquoi → `event delegation javascript`.
- L'idée maligne : mettre **un seul** écouteur sur le **parent** (le `<tbody>`), qui ne bouge jamais. Quand un clic remonte jusqu'à lui, on regarde **d'où** il vient.
- Pour savoir sur quel élément précis on a cliqué → cherche `event.target` et `closest`.
**✅ Test :** clique sur n'importe quelle ligne → la bonne info s'affiche, **même après** avoir filtré/trié le tableau.
## 🧪 Exo 5 — Anti-rebond (« debounce ») sur la recherche
**🎯 Objectif :** ta recherche se relance à **chaque** frappe. Sur une grosse liste, c'est du gaspillage. Attendre que l'utilisateur arrête de taper ~300 ms avant de filtrer.
**🔍 Pistes :**
- Cherche `debounce javascript` et les fonctions `setTimeout` / `clearTimeout`.
- L'idée : à chaque frappe, on **annule** le minuteur précédent et on en relance un. Le filtrage ne part que si l'utilisateur fait une pause.
**✅ Test :** tape vite « gui » → le filtrage ne se déclenche qu'**une** fois, à la fin, pas trois.
---
# 📦 Module C — Organiser son code en modules
Ton `contact.js` grossit (compteur, formulaire, FAQ, navbar, horloge, fetch…). Il est temps de **découper**.
## 🧪 Exo 6 — Séparer en fichiers avec `export` / `import`
**🎯 Objectif :** sortir, par exemple, `creerNavbar` dans son propre fichier `navbar.js`, et l'**importer** là où tu en as besoin.
**🔍 Pistes :**
- Cherche `javascript export import` (les **modules ES**).
- Côté HTML, une balise `<script>` classique ne suffit pas pour les modules : cherche `script type module`.
- ⚠️ Comme `fetch`, les modules **ne marchent pas en `file://`** : il te faut le petit serveur (`python3 -m http.server`) vu au Niveau 4.
- Commence petit : **un seul** export (la navbar), et fais-le marcher avant d'en découper d'autres.
**✅ Test :** la page fonctionne exactement comme avant, mais ton code est réparti dans plusieurs fichiers plus courts.
---
# 🌍 Module D — Des données d'une vraie API en ligne
Au Niveau 4 tu lisais un fichier JSON **local**. Là, on va chercher des données sur un **vrai serveur public**, comme le font les applis réelles.
## 🧪 Exo 7 — Afficher des données venues du web
**🎯 Objectif :** récupérer une liste depuis une API publique gratuite et l'afficher dans ta page (par ex. une liste d'utilisateurs fictifs, ou de publications).
**🔍 Pistes :**
- Une API d'entraînement très connue, sans clé ni inscription : cherche `jsonplaceholder`. Par exemple l'adresse `https://jsonplaceholder.typicode.com/users` renvoie une liste de personnes en JSON.
- Tu réutilises **exactement** ce que tu sais déjà : `fetch`, `await`, `.json()`, une boucle, et `createElement`/`append` pour afficher.
- Ouvre d'abord l'adresse directement dans le navigateur pour **voir la forme** des données : quelles clés existent (`name`, `email`, `company`…) ? C'est le réflexe n°1 avant d'afficher quoi que ce soit.
- Pense au `try/catch` : une API en ligne peut être lente ou indisponible.
**✅ Test :** une liste de vraies données distantes s'affiche dans ta page. Coupe ta connexion → ton message d'erreur s'affiche proprement.
---
# 📝 Module E — Lire tout un formulaire d'un coup
## 🧪 Exo 8 — `FormData`
**🎯 Objectif :** à l'envoi, récupérer **toutes** les valeurs du formulaire (prénom, e-mail, message) sans aller chercher chaque champ un par un avec `getElementById`.
**🔍 Pistes :**
- Cherche `MDN FormData`. On lui passe le `<form>`, et on peut lire chaque champ par son attribut `name` (`.get("prenom")`…).
- Pour que ça marche, vérifie que **chacun** de tes champs a bien un attribut `name` (pas seulement un `id`).
- Bonus : cherche `Object.fromEntries(formData)` pour transformer tout le formulaire en un seul objet `{ prenom: ..., email: ..., message: ... }`.
**✅ Test :** à l'envoi, un `console.log` montre un objet propre avec toutes les valeurs saisies.
---
## 🗺️ Ce que ce niveau ajoute
- `map`, `find`, `reduce` — le trio qui complète `filter`/`sort`
- **délégation d'événements** et **debounce** — des écouteurs propres et efficaces
- **modules** `export`/`import` — du code organisé
- **API en ligne** réelle — `fetch` au-delà du fichier local
- **`FormData`** — lire un formulaire d'un coup
Après ça, les grandes directions possibles : un **framework** (commencer par comprendre *pourquoi* ils existent — tu as déjà recréé à la main plusieurs de leurs idées : composants, données → affichage), ou approfondir l'**accessibilité** et les **tests**. On en parlera.
Note tes questions, et cherche le concept avant le code.
-416
View File
@@ -1,416 +0,0 @@
# 🛠️ Page Services — relecture & prochaine étape
Salut Guillaume ! 👋
Ta page services avance super bien : le tableau des prestations est clair, les boutons de navigation sont stylés, et tu as même ajouté une petite icône SVG sur le bouton « retour ». Bravo, c'est du bon travail ! 🎉
Ce guide a deux objectifs :
1. Te montrer **3 bugs** à corriger (avec l'explication, pas juste la solution).
2. T'apprendre à **définir et limiter une `<textarea>`** proprement.
3. Te lancer sur ta **première mission JavaScript** 🚀 (un vrai morceau de programmation, dans le thème FleetZen).
---
## 🐛 Bug 1 — Ton formulaire ne contient (presque) aucun champ
Regarde bien comment tu as écrit ton formulaire :
```html
<form action="" method="post">Si vous avez des suggestions :</form> <!-- ❌ le </form> ferme ICI -->
<ul>
<li><input type="text" ... /></li> <!-- ces champs sont DEHORS du <form> ! -->
<li><textarea ...></textarea></li>
<button type="submit">Envoyer le message</button>
</ul>
```
Le problème : tu **ouvres** `<form>` puis tu le **refermes** (`</form>`) tout de suite, juste après la phrase d'intro. Du coup, tous tes champs (`<input>`, `<textarea>`, le bouton « Envoyer ») se retrouvent **à l'extérieur** du formulaire.
👉 Conséquence : le jour où le formulaire enverra vraiment les données, **rien ne partira**, parce que le navigateur n'envoie que ce qui est **à l'intérieur** des balises `<form>…</form>`.
### ✅ La correction : le `<form>` doit englober TOUS les champs
```html
<form action="" method="post">
<p>Si vous avez des suggestions :</p>
<ul>
<li>
<label for="name">Nom&nbsp;:</label>
<input type="text" id="name" name="user_name" placeholder="Dupond" />
</li>
<li>
<label for="mail">E-mail&nbsp;:</label>
<input type="email" id="mail" name="user_mail" placeholder="dupond@gmail.com" />
</li>
<li>
<label for="msg">Message&nbsp;:</label>
<textarea id="msg" name="user_message" placeholder="Je pense qu'il faudrait..."></textarea>
</li>
</ul>
<div id="boutton_formulaire">
<button type="submit">Envoyer le message</button>
</div>
</form>
```
> 💡 **À retenir :** un `<form>` est une **boîte**. Tout ce qu'on veut envoyer (champs + bouton « Envoyer ») doit être **dedans**, entre `<form>` et `</form>`.
---
## 🐛 Bug 2 — Un dégradé qui ne s'affiche pas (parenthèses oubliées)
Dans `style_services.css`, ton bouton de la page courante `.x_to_x` :
```css
.x_to_x {
background-image:
linear-gradient to top left, /* ❌ il manque les parenthèses */
rgb(0 0 0 / 0.2),
rgb(0 0 0 / 0.2) 30%,
transparent;
}
```
`linear-gradient` est une **fonction** : comme en maths, ses arguments vont **entre parenthèses**. Sans elles, le navigateur ne comprend pas la ligne et **ignore tout le dégradé**. C'est pour ça que ce bouton n'a pas le même rendu que les autres.
### ✅ Correction (compare avec ta classe `.styled` qui, elle, est correcte)
```css
.x_to_x {
background-image: linear-gradient(
to top left,
rgb(0 0 0 / 0.2),
rgb(0 0 0 / 0.2) 30%,
transparent
);
}
```
> 💡 **À retenir :** `linear-gradient( … )`, `rgb( … )`, `calc( … )` … toutes les **fonctions** CSS prennent leurs arguments **entre parenthèses**. Une fonction sans parenthèses = ligne ignorée.
---
## 🎯 Bug 3 — L'icône SVG n'est pas centrée dans le bouton « retour »
Ton bouton mélange une **image** (le SVG) et du **texte** côte à côte :
```html
<button class="retour"><svg ...></svg>retour</button>
```
Le souci : par défaut, une icône SVG se pose **sur la ligne du texte** (sur la « ligne de base », comme une lettre). Du coup elle paraît trop haute ou trop basse, et ton `line-height: 2.5` accentue le décalage. L'icône et le mot « retour » ne sont pas alignés sur leur **milieu**.
### ✅ La solution : transformer le bouton en boîte flex
```css
.retour {
display: inline-flex; /* le bouton range ses enfants (icône + texte) */
align-items: center; /* ↕ les centre verticalement, sur le même milieu */
justify-content: center; /* ↔ les centre horizontalement */
gap: 6px; /* un petit espace entre l'icône et le mot */
line-height: normal; /* on enlève le 2.5 qui déréglait l'alignement */
/* ...garde le reste de tes styles (couleur, border-radius, etc.) */
}
```
`align-items: center`, c'est **exactement** le `flex` que tu connais déjà (tu l'utilises sur `#buttons` et `.align_x`). Ici on l'applique **au bouton lui-même**, parce que c'est lui le parent qui contient l'icône **et** le texte.
> 💡 **À retenir :** dès qu'un bouton (ou n'importe quelle boîte) contient **une icône + du texte** à aligner, passe-le en `display: inline-flex; align-items: center;`. C'est le réflexe pro pour centrer proprement une icône avec du texte.
> ⚠️ **Ça vaut aussi pour ton nouveau bouton « Envoyer le message »** : tu viens d'y ajouter l'icône SVG « avion en papier », il a donc exactement le même décalage. Applique-lui la même recette flex (par ex. une classe commune `.btn-icone` que tu mets sur les deux boutons, plutôt que de répéter le CSS).
---
## 📐 La leçon du jour : définir et **limiter** une `<textarea>`
Pour l'instant ta `<textarea>` est « brute » :
```html
<textarea id="msg" name="user_message"></textarea>
```
Par défaut elle est petite, et surtout l'utilisateur peut écrire **un roman de 10 000 mots** sans aucune limite. On va apprendre à la **cadrer**.
### 1️⃣ Lui donner une taille de départ : `rows` et `cols`
```html
<textarea id="msg" name="user_message"
rows="6" cols="40"></textarea>
```
- `rows` = nombre de **lignes** visibles (la hauteur).
- `cols` = nombre de **colonnes** = caractères en largeur.
> 🔎 `rows`/`cols` ne sont **pas** des pixels : c'est « combien de lignes/caractères je vois ». C'est la taille **de départ**.
### 2️⃣ Limiter le nombre de caractères : `maxlength`
```html
<textarea id="msg" name="user_message"
rows="6" cols="40"
maxlength="500"></textarea>
```
`maxlength="500"` = l'utilisateur **ne peut pas** taper plus de 500 caractères. C'est le navigateur qui bloque tout seul, sans JavaScript. 👍
### 3️⃣ Empêcher (ou cadrer) le redimensionnement : `resize` en CSS
Tu as remarqué la petite poignée en bas à droite d'une textarea ? L'utilisateur peut l'étirer et **casser ta mise en page**. On contrôle ça en CSS :
```css
#msg {
resize: vertical; /* il peut agrandir en hauteur seulement (recommandé) */
/* resize: none; pour interdire complètement le redimensionnement */
width: 100%; /* la largeur, on la pilote en CSS, pas avec cols */
min-height: 120px;
}
```
> 💡 **À retenir :**
> - `rows` / `cols` → **taille de départ** (lignes / caractères).
> - `maxlength` → **limite** le nombre de caractères (le navigateur bloque tout seul).
> - `resize` (CSS) → contrôle la **poignée** d'étirement (`vertical`, `horizontal`, `none`, `both`).
> - Astuce de pro : on règle souvent la **largeur en CSS** (`width`) et on garde `rows` pour la hauteur de départ.
### 4️⃣ 👉 Votre question : laisser l'utilisateur redimensionner, mais **entre un min et un max**
Tu as choisi pour l'instant :
```css
#msg {
resize: none; /* la poignée est désactivée : l'utilisateur ne peut RIEN étirer */
width: 200px;
}
```
`resize: none` est un **choix valable** (la zone est verrouillée). Mais vous m'avez demandé l'inverse : **autoriser** l'agrandissement, tout en **empêchant que ça parte trop grand ou trop petit**. La recette tient en 2 idées qui se combinent :
1. **`resize`** dit *dans quelle direction* on peut étirer.
2. **`min-*` / `max-*`** disent *jusqu'où* on peut aller.
```css
#msg {
resize: vertical; /* on autorise l'étirement, mais en HAUTEUR seulement */
min-height: 80px; /* impossible de la réduire en dessous de 80px */
max-height: 300px; /* impossible de l'agrandir au-delà de 300px */
width: 100%; /* largeur pilotée en CSS, pas par l'utilisateur */
}
```
Avec ça : l'utilisateur attrape la poignée, tire vers le bas pour avoir plus de place… mais **la zone refuse de dépasser 300px** et **de descendre sous 80px**. Ta mise en page reste protégée. 🎉
#### Et si on veut autoriser les **deux** sens (vertical **et** horizontal) ?
Même principe, mais avec **4 bornes** : 2 pour la hauteur, 2 pour la largeur. On passe `resize` à `both` :
```css
#msg {
resize: both; /* la poignée étire en hauteur ET en largeur */
min-height: 80px; /* bornes verticales */
max-height: 300px;
min-width: 200px; /* bornes horizontales */
max-width: 100%; /* ne dépasse jamais la largeur du parent → pas de scroll */
}
```
Chaque direction a sa paire `min`/`max` :
- **hauteur** → `min-height` / `max-height`
- **largeur** → `min-width` / `max-width`
L'utilisateur peut alors étirer dans tous les sens, mais **jamais au-delà** de ces 4 limites.
> ⚠️ Le piège classique en horizontal : mets `max-width: 100%` (et pas une grande valeur en `px`), sinon l'utilisateur peut élargir la zone **plus que l'écran** et faire réapparaître la barre de défilement horizontale (souviens-toi du bug du scroll 😉).
Récap des valeurs de `resize` :
| `resize` | Ce que l'utilisateur peut étirer |
|---|---|
| `none` | rien (verrouillé) — ton choix actuel |
| `vertical` | la hauteur seulement (le plus confortable pour un message) |
| `horizontal` | la largeur seulement |
| `both` | les deux |
> 💡 **À retenir :** `resize` = **la direction** autorisée. `min-height`/`max-height` (et `min-width`/`max-width`) = **les bornes**. Les deux ensemble = un redimensionnement libre **mais encadré**.
---
## 🚀 Ta première mission JavaScript : le **simulateur de devis FleetZen**
Jusqu'ici tu as fait du HTML (la **structure**) et du CSS (l'**apparence**). Le JavaScript, c'est la **3ᵉ couche** : le **comportement**, ce qui réagit quand on clique. 🧠
On va le faire **en mode tuto** : tu construis petit bout par petit bout, et **tu testes à chaque étape**. Ne copie pas tout d'un coup — avance palier par palier, c'est comme ça qu'on comprend. 🙂
**Objectif final :** un petit **simulateur** où le client choisit **combien de véhicules** il a, et le prix total se met à jour **tout seul**, sans recharger la page.
---
### 🪜 Étape 0 — Découvrir la console (ton meilleur ami)
Avant tout : ouvre ta page dans le navigateur, puis appuie sur **F12** → onglet **Console**. C'est là que le JS « parle ».
Crée un fichier `css/devis.js` avec une seule ligne :
```js
console.log("Mon fichier JS est bien chargé !");
```
Et branche-le en bas du `<body>`, **juste avant** `</body>` :
```html
<script src="css/devis.js"></script>
```
> 🔎 Pourquoi à la fin du body ? Parce que le JS s'exécute de haut en bas. S'il se lance **avant** que le HTML existe, il ne trouve rien à manipuler. À la fin, tout le HTML est déjà là. 👍
**✅ Teste maintenant :** recharge la page. Tu dois voir ton message dans la Console. Si oui, ton JS est branché. Sinon, vérifie le chemin du `src`.
---
### 🪜 Étape 1 — Ajouter le HTML du simulateur
Sous ton tableau, ajoute :
```html
<section class="simulateur">
<h2>Estimez votre devis</h2>
<p>Nombre de véhicules dans votre flotte :</p>
<button id="moins" type="button">−</button>
<span id="nb_vehicules">1</span>
<button id="plus" type="button">+</button>
<p>Total géolocalisation : <strong id="total">30</strong> €/mois</p>
</section>
```
**✅ Teste maintenant :** tu dois voir les boutons `−` et `+` et le chiffre `1`. Pour l'instant ils ne font rien, c'est normal : on n'a pas encore écrit le comportement.
> 🔎 Remarque les **`id`** (`moins`, `plus`, `nb_vehicules`, `total`). Un `id` est une **étiquette unique** : c'est par là que le JS va « attraper » chaque élément à l'étape suivante.
---
### 🪜 Étape 2 — Attraper les éléments depuis le JS
Dans `devis.js`, remplace ton `console.log` de test par :
```js
// "Attraper" un élément du HTML grâce à son id
const boutonPlus = document.getElementById("plus");
console.log(boutonPlus); // pour vérifier qu'on l'a bien trouvé
```
**✅ Teste maintenant :** dans la Console tu dois voir s'afficher ton `<button id="plus">`. Si tu vois `null`, c'est que l'id ne correspond pas → vérifie l'orthographe.
> 🧩 **Idée clé n°1 — SÉLECTIONNER.** `document.getElementById("plus")` veut dire : « dans le document, va me chercher l'élément dont l'id est *plus* ». On le range dans une variable pour s'en servir après.
---
### 🪜 Étape 3 — Réagir à un clic
Ajoute en dessous :
```js
boutonPlus.addEventListener("click", function () {
console.log("Clic sur + détecté !");
});
```
**✅ Teste maintenant :** clique sur le bouton `+`. À **chaque** clic, un message apparaît dans la Console. 🎉
> 🧩 **Idée clé n°2 — RÉAGIR.** `addEventListener("click", … )` se lit : « écoute les clics sur ce bouton, et quand il y en a un, exécute cette fonction ». La fonction entre `{ }` est le « que faire quand ça arrive ».
---
### 🪜 Étape 4 — Faire vraiment changer le compteur
Maintenant on remplace le `console.log` par une vraie logique. Voici le fichier complet :
```js
// 1. On attrape tous les éléments dont on a besoin
const affichageNb = document.getElementById("nb_vehicules");
const affichageTotal = document.getElementById("total");
const boutonPlus = document.getElementById("plus");
const boutonMoins = document.getElementById("moins");
const PRIX_PAR_VEHICULE = 30; // 30 €/véhicule/mois (cf. ton tableau)
let nbVehicules = 1; // "let" = une variable qui va CHANGER
// 2. Une fonction qui réécrit l'affichage à partir de nbVehicules
function mettreAJour() {
affichageNb.textContent = nbVehicules;
affichageTotal.textContent = nbVehicules * PRIX_PAR_VEHICULE;
}
// 3. Au clic sur "+", on augmente puis on réaffiche
boutonPlus.addEventListener("click", function () {
nbVehicules = nbVehicules + 1;
mettreAJour();
});
// 4. Au clic sur "−", on diminue (mais jamais en dessous de 1)
boutonMoins.addEventListener("click", function () {
if (nbVehicules > 1) {
nbVehicules = nbVehicules - 1;
}
mettreAJour();
});
```
> 🧩 **Idée clé n°3 — MODIFIER.** `element.textContent = …` réécrit le texte affiché **en direct**, sans recharger la page. C'est ça, la magie du JS.
>
> 🔎 Pourquoi une fonction `mettreAJour()` ? Pour ne pas se répéter : le `+` et le `−` ont tous les deux besoin de réafficher. On écrit le « comment afficher » **une seule fois** et on l'appelle aux deux endroits (c'est le même esprit que le composant réutilisable, juste après 👇).
**✅ Teste maintenant :** clique 3 fois sur `+` → tu dois lire **4 véhicules** et **120 €**. Clique sur `−` jusqu'en bas → ça doit **bloquer à 1**. Si oui : bravo, tu viens d'écrire ta première vraie logique en JavaScript ! 🎉
> 🎯 **Défis bonus** (dans l'ordre de difficulté) :
> 1. Empêcher aussi de dépasser 100 véhicules.
> 2. Afficher le total avec un séparateur de milliers.
> 3. Ajouter un 2ᵉ service (ex. consommation à 15 €) et calculer un total combiné.
---
## ♻️ Bonus — La notion de **composant réutilisable** (la navbar)
Regarde : la barre du haut (`#nav_barre`) est **recopiée à l'identique** dans `index.html`, `services.html` et `contact.html`. Le jour où tu changes un lien, tu dois le changer **dans 3 fichiers** → source d'erreurs (d'ailleurs, vérifie tes liens entre pages 😉).
C'est **le** problème que résolvent les **composants** : on écrit la navbar **une seule fois**, et on la réutilise partout.
Première approche très simple en JavaScript : une fonction qui **génère** la navbar.
```js
function creerNavbar() {
return `
<div id="nav_barre" class="shadow">
<h1>FleetZen</h1>
<div id="buttons">
<a href="contact.html"><button class="styled">contact</button></a>
<a href="services.html"><button class="styled">services</button></a>
<a href="../index.html"><button class="retour">retour</button></a>
</div>
</div>`;
}
// On l'injecte tout en haut du body :
document.body.insertAdjacentHTML("afterbegin", creerNavbar());
```
**✅ Teste :** enlève la navbar écrite « en dur » dans le HTML, garde ce JS, recharge → la navbar réapparaît, générée par la fonction. Maintenant le jour où tu changes un lien, tu le changes **à un seul endroit** pour **toutes** les pages.
> 💡 **À retenir :** un **composant** = un bout d'interface écrit **une fois** et **réutilisé** partout. C'est l'idée centrale derrière React, Vue, etc. — mais ça commence par une simple fonction comme celle-ci.
---
## ✅ Ta checklist
- [ ] Remettre tous les champs **dans** le `<form>` (Bug 1)
- [ ] Réparer le dégradé de `.x_to_x` avec les parenthèses (Bug 2)
- [ ] Centrer l'icône SVG des boutons « retour » **et** « Envoyer » avec `inline-flex` (Bug 3)
- [ ] Ajouter `rows`, `maxlength` + redimensionnement borné (`resize: vertical` + `min/max-height`)
- [ ] Faire marcher le **simulateur de devis** en JavaScript
- [ ] (Bonus) Transformer la navbar en composant réutilisable
- [ ] Commits petits et clairs, puis `git push`
Pose-moi toutes tes questions. Tu gères ! 🚗💨
-200
View File
@@ -1,200 +0,0 @@
# 🧗 Rattrapage JavaScript — page services
Salut Guillaume ! 👋
Tu as raté une journée, ce guide est là pour rattraper. Il couvre tout ce que le projet a avancé côté JavaScript, **dans l'ordre**, sur **ta** page services (le simulateur, le tableau des services, le formulaire de suggestions).
> ⚠️ **Ce guide est différent des précédents : il ne donne PAS le code tout fait.**
> Pour chaque exo tu as : un **🎯 objectif**, des **🔍 pistes** (quoi chercher, quels mots-clés, les pièges), et un **✅ test**. À toi d'écrire le code. C'est en cherchant que ça rentre — et tu retiendras dix fois mieux qu'en copiant-collant.
>
> 🔎 Ton meilleur réflexe : taper dans Google **« MDN » + le nom de la chose** (ex. `MDN addEventListener`). MDN, c'est la doc de référence du web, en français, avec des exemples.
---
## 🩹 Avant de continuer : 3 choses à revoir dans ton `devis.js`
Tu as déjà bien attaqué le simulateur. Trois petits points à corriger toi-même (cherche, ne devine pas) :
1. Dans ton HTML, ta textarea a `row="6"`. Regarde bien l'attribut : il existe au **pluriel**. Lequel est le bon ? (cherche `MDN textarea rows`).
2. Ton blocage à 100 véhicules : tu ajoutes 1, tu **affiches**, *puis* tu testes `> 99`. Dans quel ordre faudrait-il faire pour ne jamais afficher 100 une fraction de seconde ? (indice : teste **avant** d'afficher).
3. Tu as une fonction `mettreAJourTOTAL` et une variable `totalargent` qui ne servent pas vraiment (le total affiché est toujours 0). Demande-toi : *d'où devrait venir le total ?* (indice : il se **calcule** à partir de `nbVehicules`, il ne se stocke pas dans une variable à part).
> 💡 Règle générale que tu retrouveras partout : une valeur qui **se déduit** d'une autre (le total se déduit du nombre de véhicules) ne se **stocke** pas — on la **recalcule** au moment de l'afficher.
---
# 🟢 Étape 1 — Les conditions (`if` / `else`)
## 🧪 Exo 1 — Singulier / pluriel dans le simulateur
**🎯 Objectif :** quand il y a **1** véhicule, afficher « 1 véhicule » ; à partir de 2, « N véhicules ».
**🔍 Pistes :**
- La structure à chercher : `if (condition) { ... } else { ... }` → cherche `MDN if else`.
- Pour comparer deux valeurs : l'opérateur d'égalité. Attention, en JS **`=` et `===` ne font pas la même chose** → cherche `difference == === javascript`. Dans un `if`, c'est presque toujours le triple.
- Où mettre ce code ? Là où tu mets déjà à jour l'affichage du nombre.
**✅ Test :** descends à 1 → « véhicule » ; monte à 2+ → « véhicules ».
---
# 🟢 Étape 2 — Réagir à la saisie et à l'envoi
## 🧪 Exo 2 — Un compteur de caractères sur le formulaire de suggestions
**🎯 Objectif :** sous ta textarea, afficher « Il vous reste X caractères » qui se met à jour **à chaque frappe** (ta limite est `maxlength="500"`).
**🔍 Pistes :**
- Il te faut un endroit où afficher le nombre : ajoute un `<span>` avec un `id` dans ton HTML.
- L'événement qui se déclenche à chaque frappe dans un champ → cherche `MDN input event` (ce n'est pas `"click"`).
- Pour savoir combien de caractères ont été tapés : sur un champ, `.value` donne le texte, et il existe une propriété qui donne sa **longueur** (la même que pour compter les éléments d'une liste) → cherche `javascript string length`.
- Le reste = 500 moins ce nombre.
**✅ Test :** tape du texte → le nombre descend (499, 498…) et s'arrête à 0.
**Bonus :** passe le compteur en rouge sous 50 caractères → cherche `element.style.color` et l'opérateur ternaire `condition ? ... : ...`.
## 🧪 Exo 3 — Empêcher l'envoi d'un message vide
**🎯 Objectif :** si on clique « Envoyer » avec une suggestion vide, bloquer l'envoi et afficher un message.
**🔍 Pistes :**
- L'événement déclenché quand un formulaire est envoyé → cherche `MDN form submit event`.
- Pour **annuler** l'envoi par défaut → cherche `preventDefault`.
- Pour considérer « que des espaces » comme vide → cherche `javascript trim`.
- Afficher un message rapide → la fonction `alert(...)`.
**✅ Test :** envoi à vide → message + pas de rechargement. Avec du texte → ça passe.
---
# 🟡 Étape 3 — Les tableaux et les boucles (le gros morceau)
Jusqu'ici tes variables contiennent **une** valeur. Pour une **liste**, il y a les tableaux. C'est la base de presque tout.
## 🧪 Exo 4 — Décrire tes services dans un tableau d'objets
**🎯 Objectif :** au lieu d'avoir tes 4 services écrits « en dur » dans le `<table>`, range-les dans une **liste** en JavaScript.
**🔍 Pistes :**
- Une **liste** s'écrit avec des crochets `[ ]` → cherche `javascript array`.
- Chaque service a plusieurs infos (nom, description, prix) : ça, c'est un **objet** `{ }` avec des étiquettes → cherche `javascript object`.
- Combine les deux : une liste d'objets `[ { ... }, { ... } ]`.
- Pour parcourir la liste et tout afficher dans la console → cherche `javascript for of`.
**✅ Test :** un `console.log` dans la boucle affiche tes 4 services, un par ligne.
## 🧪 Exo 5 — Générer les lignes du tableau automatiquement
**🎯 Objectif :** que les lignes `<tr>` de ton tableau soient **fabriquées par le JS** à partir de la liste de l'exo 4. Ajouter un service = juste ajouter un objet à la liste.
**🔍 Pistes :**
- Vide le corps de ton tableau dans le HTML et donne-lui un `id` (garde l'en-tête `<thead>`). Cherche `html tbody`.
- Deux approches existent, regarde les deux et choisis :
- assembler du texte HTML et l'injecter → cherche `innerHTML` et `template literals` (les accents graves `` ` `` avec `${...}`) ;
- fabriquer chaque élément proprement → cherche `document.createElement` et `append`.
- Une boucle parcourt la liste et ajoute une ligne par service.
**✅ Test :** le tableau s'affiche comme avant. Ajoute un 5ᵉ service dans la liste → la ligne apparaît sans toucher au HTML.
> 💡 Tu viens de toucher au principe des sites modernes : **les données d'un côté, l'affichage de l'autre**. On change les données, l'affichage suit.
## 🧪 Exo 6 — Une barre de recherche qui filtre les services
**🎯 Objectif :** un champ où l'on tape, et le tableau ne garde que les services dont le nom contient le texte.
**🔍 Pistes :**
- Mets ton « affichage du tableau » dans une **fonction** que tu peux rappeler avec une liste différente.
- Pour ne garder que certains éléments d'une liste → cherche `MDN Array filter`.
- Pour ignorer les majuscules → cherche `toLowerCase`, et pour tester « contient » → cherche `string includes`.
- À chaque frappe (`input`), filtre puis ré-affiche.
**✅ Test :** tape « géo » → seul le service de géolocalisation reste ; efface → tout revient.
---
# 🟡 Étape 4 — Donner une mémoire à la page (`localStorage`)
## 🧪 Exo 7 — Se souvenir du nombre de véhicules choisi
**🎯 Objectif :** si on règle le simulateur sur 12 véhicules et qu'on recharge la page, il doit revenir à **12**, pas à 1.
**🔍 Pistes :**
- Le petit coffre-fort du navigateur qui survit au rechargement → cherche `MDN localStorage` (`setItem` pour ranger, `getItem` pour relire).
- Sauvegarde à chaque changement ; relis au chargement de la page.
- ⚠️ **Piège classique :** ce qui sort de `localStorage` est toujours du **texte**, pas un nombre. `"12" + 1` donne `"121"`, pas `13` ! Cherche `parseInt javascript` pour reconvertir en nombre.
**✅ Test :** règle un nombre, recharge (F5) → le simulateur repart sur ce nombre.
---
# 🟠 Étape 5 — Le style dynamique et le temps réel
## 🧪 Exo 8 — Un bouton « Mode sombre »
**🎯 Objectif :** un bouton qui bascule la page en thème sombre, et re-clic pour revenir.
**🔍 Pistes :**
- Prépare une classe CSS (ex. `body.sombre { ... }`) avec un fond foncé et un texte clair.
- En JS, pour ajouter/enlever une classe en un seul geste → cherche `classList toggle`.
**✅ Test :** clic → sombre, re-clic → clair.
## 🧪 Exo 9 — Une horloge qui tourne dans le pied de page
**🎯 Objectif :** afficher l'heure actuelle, mise à jour **chaque seconde**.
**🔍 Pistes :**
- Pour obtenir l'heure : cherche `MDN Date` et `toLocaleTimeString`.
- Pour répéter une action à intervalle régulier → cherche `MDN setInterval` (la durée est en **millisecondes** : 1 seconde = ?).
**✅ Test :** l'heure s'affiche et avance toute seule.
---
# 🔴 Étape 6 — Aller chercher des données ailleurs (`fetch`)
C'est l'étape la plus avancée, prends ton temps.
## 🧪 Exo 10 — Charger une liste de véhicules depuis un fichier JSON
**🎯 Objectif :** créer un fichier `vehicules.json` (quelques véhicules : modèle, immatriculation, km) et l'afficher dans un tableau via JavaScript, au lieu de l'écrire en dur.
**🔍 Pistes :**
- Le format JSON ressemble à une liste d'objets, mais avec des règles strictes (guillemets doubles partout, pas de virgule finale) → cherche `JSON format`.
- Pour aller chercher un fichier → cherche `MDN fetch`. Comme ça prend du temps, on utilise `async` / `await` → cherche `async await javascript`.
- ⚠️ **Important :** `fetch` ne marche pas si tu ouvres la page en double-cliquant (`file://`). Il faut un mini-serveur : dans un terminal, dans le dossier du projet, lance `python3 -m http.server 8000`, puis ouvre `http://localhost:8000/...`. Cherche `python http server` si besoin.
- Pour réagir si le fichier est introuvable → cherche `try catch javascript`.
**✅ Test :** la liste des véhicules s'affiche. Renomme le fichier exprès → un message d'erreur s'affiche au lieu d'un plantage.
---
# 🏁 Étape 7 — Pour être au niveau du reste du projet
## 🧪 Exo 11 — La navbar en composant réutilisable
**🎯 Objectif :** ta barre de navigation est recopiée à l'identique dans `index.html`, `services.html`, `contact.html`. L'écrire **une seule fois** en JavaScript et l'injecter sur la page.
**🔍 Pistes :**
- Une **fonction** qui **renvoie** le HTML de la navbar (avec `return` et un *template literal*) → cherche `javascript return` et `template literals`.
- Pour insérer ce HTML en haut du `<body>` → cherche `insertAdjacentHTML`.
- Pour mettre en valeur le bouton de la page courante, passe à la fonction un **paramètre** (ex. la page active) et utilise une condition pour choisir la classe du bouton.
**✅ Test :** supprime la navbar écrite en dur dans le HTML, garde le JS → elle réapparaît. Changer un lien = un seul endroit à modifier.
---
## 🗺️ Quand tu auras fait tout ça, tu sauras :
- les **conditions** (`if`/`else`, ternaire) et les **événements** (`input`, `submit`, `click`)
- les **tableaux** et **objets**, les **boucles**, `filter`
- **générer du HTML** depuis des données
- la **mémoire** (`localStorage`) et le piège du texte vs nombre
- le **style dynamique** (`classList`) et le **temps réel** (`setInterval`)
- les **données distantes** (`fetch`, `async`/`await`, JSON)
- le **composant réutilisable**
C'est exactement le niveau où en est la page contact, tu seras à jour.
Note tes questions au fur et à mesure — et résiste à l'envie de chercher « la réponse toute faite » : cherche d'abord **le concept**, le code viendra de toi.
+165
View File
@@ -0,0 +1,165 @@
# ✅ Solutions — Niveau 5 contact (`PROCHAINE_ETAPE_contact_4.md`)
> 🛑 **À NE REGARDER QU'EN DERNIER RECOURS.**
> Cherche d'abord par toi-même (au moins 10–15 min, doc MDN à l'appui). Quand tu lis une réponse : lis-la, **ferme le fichier**, et réécris-la **de mémoire**. C'est comme ça que ça rentre.
Le code ci-dessous est **un** exemple correct. Le tien peut être différent et tout aussi valable.
---
## 🧪 Exo 1 — `map`
```js
const noms = equipe.map(function (membre) {
return membre.nom;
});
console.log(noms); // ["Mélissa", "Guillaume"]
```
> 💬 *« `map` ou `filter` ? »* `filter` garde/jette des éléments (la liste rétrécit). `map` transforme chaque élément (la liste garde la même taille). Ici on veut transformer chaque objet en son nom → `map`.
---
## 🧪 Exo 2 — `find`
```js
const personne = equipe.find(function (membre) {
return membre.partie === "Page contact";
});
if (personne) {
console.log(personne.nom);
} else {
console.log("Personne ne correspond.");
}
```
> 💬 *« Pourquoi le `if (personne)` ? »* Si aucun élément ne correspond, `find` renvoie `undefined`. Faire `personne.nom` planterait. On vérifie d'abord. (Variante moderne : `personne?.nom` — cherche `optional chaining`.)
---
## 🧪 Exo 3 — `reduce`
Avec une boucle (plus lisible pour commencer) :
```js
let totalKm = 0;
for (const v of vehicules) {
totalKm = totalKm + v.km;
}
```
La même chose en `reduce` :
```js
const totalKm = vehicules.reduce(function (somme, v) {
return somme + v.km;
}, 0); // ← 0 = la valeur de départ
console.log(totalKm);
```
> 💬 *« À quoi sert le `0` à la fin ? »* C'est la valeur de départ de l'accumulateur (`somme`). Pour une somme on part de 0 ; pour multiplier on partirait de 1.
---
## 🧪 Exo 4 — Délégation d'événements
Quand tu génères les lignes, marque chaque ligne avec le nom (un attribut `data-…`) :
```js
function creerLigne(membre) {
const tr = document.createElement("tr");
tr.dataset.nom = membre.nom; // devient data-nom="..." dans le HTML
// ... (les <td> comme avant)
return tr;
}
```
Puis **un seul** écouteur, sur le parent qui ne bouge pas :
```js
const corps = document.getElementById("corps_equipe");
corps.addEventListener("click", function (event) {
const ligne = event.target.closest("tr"); // la <tr> cliquée, peu importe la <td>
if (ligne) {
alert("Membre : " + ligne.dataset.nom);
}
});
```
> 💬 *« Pourquoi ne pas mettre l'écouteur sur chaque `<tr>` ? »* Parce que tes lignes sont recréées à chaque recherche/tri : il faudrait tout réattacher à chaque fois. En écoutant le **parent** (qui, lui, ne change pas), ça marche même pour les lignes créées plus tard.
> 💬 *« À quoi sert `closest("tr")` ? »* Le clic peut tomber sur une `<td>` ; `closest` remonte jusqu'à la `<tr>` qui la contient.
---
## 🧪 Exo 5 — Debounce sur la recherche
```js
let minuteur; // garde la trace du minuteur en cours
champRecherche.addEventListener("input", function () {
clearTimeout(minuteur); // on annule le précédent
minuteur = setTimeout(function () { // on en relance un
const mot = champRecherche.value.toLowerCase();
const resultats = equipe.filter(m => m.nom.toLowerCase().includes(mot));
afficherEquipe(resultats);
}, 300); // 300 ms après la DERNIÈRE frappe
});
```
> 💬 *« Comment ça « attend » ? »* À chaque frappe, on **annule** le minuteur précédent (`clearTimeout`) et on en repart un de 300 ms. Tant que tu tapes, il est sans cesse annulé ; il ne se déclenche que quand tu fais une pause.
---
## 🧪 Exo 6 — Modules `export` / `import`
`navbar.js` :
```js
export function creerNavbar(pageActive) {
return `... ton HTML de navbar ...`;
}
```
`contact.js` (tout en haut) :
```js
import { creerNavbar } from "./navbar.js";
document.body.insertAdjacentHTML("afterbegin", creerNavbar("contact"));
```
HTML :
```html
<script type="module" src="../contact.js"></script>
```
> 💬 *« Depuis que j'utilise `type="module"`, mes autres scripts ne se voient plus ! »* Normal : chaque module a son **propre** espace. Une variable d'un fichier n'est plus visible ailleurs **sauf** si tu l'`export`es et l'`import`es. C'est justement le but : plus de variables globales qui se marchent dessus.
> 💬 *« Erreur CORS au chargement du module. »* Comme `fetch`, les modules exigent un serveur : `python3 -m http.server` puis `http://localhost:8000/...` (pas `file://`).
---
## 🧪 Exo 7 — Vraie API en ligne
```js
async function chargerUtilisateurs() {
const liste = document.getElementById("liste_users");
try {
const reponse = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await reponse.json();
for (const u of users) {
const li = document.createElement("li");
li.textContent = `${u.name} — ${u.email}`;
liste.append(li);
}
} catch (erreur) {
console.error(erreur);
}
}
chargerUtilisateurs();
```
> 💬 *« Comment je sais que les clés s'appellent `name` et `email` ? »* J'ai d'abord ouvert `https://jsonplaceholder.typicode.com/users` dans le navigateur pour **voir** la forme des données. Fais toujours ça avant d'afficher.
> 💬 *« Ça marche sur le web mais mon fichier local non ? »* Une API en ligne autorise les requêtes externes (CORS) ; un fichier local ouvert en `file://` non. D'où le serveur local pour tes propres fichiers.
---
## 🧪 Exo 8 — `FormData`
```js
formulaire.addEventListener("submit", function (evenement) {
evenement.preventDefault();
const data = new FormData(formulaire);
const valeurs = Object.fromEntries(data);
console.log(valeurs); // { prenom: "...", email: "...", contacter: "..." }
});
```
> 💬 *« `FormData` me renvoie du vide ! »* Vérifie que chaque champ a un attribut **`name`** (pas seulement `id`). `FormData` lit les champs par leur `name`.
> 💬 *« C'est mieux que `getElementById` partout ? »* Pour lire **tout** un formulaire d'un coup, oui : une ligne au lieu d'un `getElementById` par champ. Pour un seul champ ponctuel, `getElementById` reste très bien.
---
### 🆘 Encore bloquée après la solution ?
Note : *ce que tu attendais, ce qui se passe à la place, et ce qu'affiche la Console (F12)*. Avec ces 3 infos, on débloque vite.
-263
View File
@@ -1,263 +0,0 @@
# ✅ Solutions — rattrapage services (`PROCHAINE_ETAPE_services_2.md`)
> 🛑 **À NE REGARDER QU'EN DERNIER RECOURS.**
> Cherche d'abord par toi-même (au moins 10–15 min, et regarde la doc MDN). Recopier une solution sans l'avoir cherchée, ça ne s'apprend pas. Quand tu lis une réponse ici : lis-la, **ferme le fichier**, et réécris-la **de mémoire**. Si tu y arrives, c'est gagné.
Le code ci-dessous est **un** exemple correct. Le tien peut être écrit autrement et être tout aussi bon.
---
## 🩹 Préambule — les 3 corrections de ton `devis.js`
**1. L'attribut de la textarea** : c'est `rows` (pluriel), pas `row`.
```html
<textarea id="msg" name="user_message" rows="6" cols="40" maxlength="500"></textarea>
```
**2. Bloquer AVANT d'afficher** : on teste la limite d'abord, on affiche ensuite.
```js
boutonPlus.addEventListener("click", function () {
if (nbVehicules < 100) { // on teste AVANT
nbVehicules = nbVehicules + 1;
}
mettreAJour(); // on affiche APRÈS (jamais 100 puis retour)
});
```
**3. Le total se recalcule, il ne se stocke pas.** Supprime `totalargent` et `mettreAJourTOTAL`. Une seule fonction d'affichage suffit :
```js
function mettreAJour() {
affichageNb.textContent = nbVehicules;
affichageGeo.textContent = nbVehicules * PRIX_GPS;
affichageConso.textContent = nbVehicules * PRIX_CONSOMMATION;
}
```
> 💬 *« Pourquoi pas une variable `totalargent` ? »* Parce qu'elle pourrait se désynchroniser de `nbVehicules`. Une valeur qui se déduit d'une autre, on la **calcule au moment de l'afficher** : impossible qu'elle soit fausse.
---
## 🟢 Exo 1 — Singulier / pluriel
Dans le HTML, mets le mot dans son propre span, à côté du nombre :
```html
<span id="nb_vehicules">1</span> <span id="mot_vehicule">véhicule</span>
```
Dans `mettreAJour()` :
```js
motVehicule.textContent = nbVehicules === 1 ? "véhicule" : "véhicules";
```
(avec `const motVehicule = document.getElementById("mot_vehicule");` en haut.)
> 💬 *« `=`, `==` ou `===` ? »* `=` **range** une valeur. `===` **compare** (et vérifie aussi le type). Dans un `if`/un test, utilise toujours `===`.
---
## 🟢 Exo 2 — Compteur de caractères
HTML, sous la textarea :
```html
<p>Il vous reste <span id="restant">500</span> caractères.</p>
```
JS :
```js
const zoneMsg = document.getElementById("msg");
const restant = document.getElementById("restant");
const MAX = 500;
zoneMsg.addEventListener("input", function () {
const reste = MAX - zoneMsg.value.length;
restant.textContent = reste;
restant.style.color = reste < 50 ? "red" : "black"; // bonus
});
```
> 💬 *« Pourquoi `input` et pas `click` ? »* `click` ne se déclenche qu'au clic de souris ; `input` se déclenche à **chaque modification** du champ (clavier, collage…). C'est ce qu'on veut pour un compteur.
---
## 🟢 Exo 3 — Bloquer l'envoi vide
```js
const formulaire = document.querySelector("form");
formulaire.addEventListener("submit", function (evenement) {
if (zoneMsg.value.trim() === "") {
evenement.preventDefault();
alert("Merci d'écrire une suggestion avant d'envoyer !");
}
});
```
> 💬 *« À quoi sert `trim()` ? »* Il enlève les espaces au début/à la fin. Sans lui, « 3 espaces » compterait comme un message valide.
> 💬 *« `preventDefault`, c'est quoi ? »* Ça annule l'action par défaut du navigateur (ici : envoyer le formulaire et recharger la page).
---
## 🟡 Exo 4 — Tableau d'objets
Dans un fichier `services.js` (ou en haut de `devis.js`) :
```js
const services = [
{ nom: "Géolocalisation", description: "Suivi en temps réel", prix: "30€/véhicule/mois" },
{ nom: "Odomètre", description: "Kilométrage précis", prix: "200€" },
{ nom: "Consommation", description: "Carburant consommé", prix: "15€/véhicule/mois" },
{ nom: "Impact carbone", description: "CO₂ rejeté", prix: "30€/véhicule/mois" }
];
for (const service of services) {
console.log(service.nom + " — " + service.prix);
}
```
---
## 🟡 Exo 5 — Générer les lignes du tableau
HTML : garde le `<thead>`, vide le corps et donne-lui un id :
```html
<tbody id="corps_services"></tbody>
```
JS (version `innerHTML` + template literal) :
```js
const corps = document.getElementById("corps_services");
let lignes = "";
for (const s of services) {
lignes += `<tr><td>${s.nom}</td><td>${s.description}</td><td>${s.prix}</td></tr>`;
}
corps.innerHTML = lignes;
```
> 💬 *« Pourquoi mes `${}` ne marchent pas ? »* Vérifie que tu utilises bien les accents **graves** `` ` `` (touche AltGr+7), pas des guillemets simples `'`.
---
## 🟡 Exo 6 — Recherche
Mets l'affichage dans une fonction réutilisable :
```js
function afficherServices(liste) {
let lignes = "";
for (const s of liste) {
lignes += `<tr><td>${s.nom}</td><td>${s.description}</td><td>${s.prix}</td></tr>`;
}
corps.innerHTML = lignes;
}
afficherServices(services);
const champ = document.getElementById("recherche"); // <input id="recherche"> dans le HTML
champ.addEventListener("input", function () {
const mot = champ.value.toLowerCase();
const resultats = services.filter(function (s) {
return s.nom.toLowerCase().includes(mot);
});
afficherServices(resultats);
});
```
> 💬 *« `filter` modifie-t-il `services` ? »* Non. `filter` renvoie une **nouvelle** liste ; l'originale reste intacte (c'est pour ça qu'effacer la recherche fait tout revenir).
---
## 🟡 Exo 7 — Se souvenir du nombre de véhicules
```js
// à la fin de mettreAJour(), on sauvegarde
localStorage.setItem("nbVehicules", nbVehicules);
// au chargement de la page, on relit
const sauvegarde = localStorage.getItem("nbVehicules");
if (sauvegarde !== null) {
nbVehicules = parseInt(sauvegarde); // ⚠️ reconvertir le TEXTE en NOMBRE
mettreAJour();
}
```
> 💬 *« Pourquoi `parseInt` ? »* `localStorage` ne stocke que du **texte**. Sans conversion, `"12" + 1` donnerait `"121"`. `parseInt("12")` redonne le nombre `12`.
---
## 🟠 Exo 8 — Mode sombre
CSS :
```css
body.sombre { background-color: #1d2733; color: white; }
```
JS (avec `<button id="theme">Mode sombre</button>` dans le HTML) :
```js
document.getElementById("theme").addEventListener("click", function () {
document.body.classList.toggle("sombre");
});
```
> 💬 *« `toggle` fait quoi exactement ? »* Si la classe est présente il l'enlève, sinon il l'ajoute. Un seul appel gère l'aller **et** le retour.
---
## 🟠 Exo 9 — Horloge
```html
<span id="horloge"></span>
```
```js
const horloge = document.getElementById("horloge");
function afficherHeure() {
horloge.textContent = new Date().toLocaleTimeString();
}
afficherHeure(); // tout de suite
setInterval(afficherHeure, 1000); // puis chaque seconde
```
> 💬 *« Pourquoi appeler `afficherHeure()` avant le `setInterval` ? »* Sinon la page reste vide pendant la 1ʳᵉ seconde, le temps que `setInterval` se déclenche la première fois.
---
## 🔴 Exo 10 — `fetch` d'un fichier JSON
`vehicules.json` (racine du projet) :
```json
[
{ "modele": "Renault Kangoo", "immatriculation": "AB-123-CD", "km": 45200 },
{ "modele": "Peugeot Partner", "immatriculation": "EF-456-GH", "km": 78900 }
]
```
JS :
```js
async function chargerFlotte() {
const message = document.getElementById("message_flotte");
const corps = document.getElementById("corps_flotte");
try {
const reponse = await fetch("../vehicules.json");
const vehicules = await reponse.json();
for (const v of vehicules) {
const tr = document.createElement("tr");
tr.textContent = `${v.modele} — ${v.immatriculation} — ${v.km} km`;
corps.append(tr);
}
} catch (erreur) {
message.textContent = "Impossible de charger la flotte 😕";
console.error(erreur);
}
}
chargerFlotte();
```
> 💬 *« J'ai l'erreur "CORS" / "Failed to fetch" ! »* Tu as ouvert la page en double-cliquant (`file://`). Lance `python3 -m http.server 8000` dans le dossier du projet et ouvre `http://localhost:8000/pages/services.html`.
> 💬 *« Mon JSON ne charge pas. »* Vérifie : guillemets **doubles** partout, et **pas** de virgule après le dernier élément.
---
## 🏁 Exo 11 — Navbar en composant
```js
function creerNavbar(pageActive) {
return `
<div id="nav_barre" class="shadow">
<h1>FleetZen</h1>
<div id="buttons">
<a href="contact.html"><button class="${pageActive === "contact" ? "x_to_x" : "styled"}">contact</button></a>
<a href="services.html"><button class="${pageActive === "services" ? "x_to_x" : "styled"}">services</button></a>
<a href="../index.html"><button class="retour btn_icone">retour</button></a>
</div>
</div>`;
}
document.body.insertAdjacentHTML("afterbegin", creerNavbar("services"));
```
N'oublie pas d'**enlever** la navbar écrite en dur dans ton HTML, sinon tu en auras deux.
> 💬 *« Pourquoi un paramètre `pageActive` ? »* Pour que la **même** fonction serve sur toutes les pages : sur services tu appelles `creerNavbar("services")`, sur contact `creerNavbar("contact")`, et c'est le bon bouton qui est mis en valeur.
---
### 🆘 Encore bloqué après avoir lu la solution ?
Note précisément : *qu'est-ce que tu attendais, qu'est-ce qui se passe à la place, et qu'affiche la Console (F12) ?* Avec ces 3 infos, on débloque n'importe quoi en deux minutes.
+109
View File
@@ -0,0 +1,109 @@
# 🧩 Tuto pas à pas — le parent qui additionne les likes des enfants
Tu cherches à faire **remonter** les likes : chaque `Carte` a son compteur, et `App` (le parent) affiche le **total**. C'est exactement le bon objectif 👍. Il te manque **une seule ligne** — et je veux surtout que tu comprennes **pourquoi**.
---
## 🔍 D'abord, pourquoi ton total reste à 0
Regarde le bouton dans ta `Carte` :
```jsx
<button onClick={() => setLikes(likes + 1)}>❤️ {likes}</button>
```
Quand on clique, tu fais `setLikes(likes + 1)` → ça met à jour **le compteur de la carte**. ✅
Mais… tu n'as **jamais** prévenu le parent ! La fonction `onLike` que `App` t'a donnée en prop n'est **jamais appelée**. Donc `App` ne sait pas qu'on a liké → `total` reste à 0.
> 🧠 **L'idée à comprendre :** un enfant **ne peut pas** modifier directement l'état du parent. Le parent lui **prête une fonction** (`onLike`), et l'enfant doit **l'appeler** au bon moment. C'est l'enfant qui dit « hé, on m'a likée ! » — le parent décide quoi en faire.
---
## 🪜 La correction, étape par étape
### Étape 1 — le parent a déjà tout (rien à changer)
Dans `App.jsx`, tu as déjà :
```jsx
const [total, setTotal] = useState(0);
// ...
<Carte ... onLike={() => setTotal((t) => t + 1)} />
// ...
<p>Total de likes : {total}</p>
```
Le parent tient le `total` et passe une fonction `onLike` à **chaque** carte. ✅
### Étape 2 — l'enfant doit APPELER `onLike` (la ligne qui manque)
Dans `Carte`, le bouton doit faire **deux choses** au clic : augmenter **son** compteur **et** prévenir le parent.
```jsx
<button
onClick={() => {
setLikes(likes + 1); // 1) j'augmente MON compteur (état de la carte)
props.onLike(); // 2) je préviens le PARENT (sa fonction)
}}
>
❤️ {likes}
</button>
```
> 🔎 Deux instructions dans le `onClick` → on les met entre accolades `{ ... }` avec un `;` entre les deux. C'est une petite fonction qui fait deux choses à la suite.
**✅ Test :** clique sur le ❤️ d'une carte → **son** compteur monte **et** le « Total de likes » en haut monte aussi. Clique sur une autre carte → le total continue de grimper.
---
## 🎓 Ta question : pourquoi chaque bouton démarre avec la **même** valeur ?
C'est une **excellente** question, et la réponse, c'est **l'initialisation** de `useState`.
```jsx
const [likes, setLikes] = useState(
() => Number(localStorage.getItem("likes-" + props.id)) || 0
);
```
### 1. La valeur dans `useState(...)` n'est qu'un **point de départ**
L'argument que tu passes à `useState(...)` est la **valeur initiale** : React l'utilise **une seule fois**, au tout **premier affichage** de la carte, pour démarrer le compteur. Ensuite, c'est `setLikes(...)` qui prend le relais — l'initialisation **ne sert plus**.
> 🧠 `useState(valeurDeDépart)` = « démarre à cette valeur, **la première fois seulement** ». Après, l'état vit sa vie.
### 2. Chaque `<Carte>` est **indépendante** (elle ne partage rien)
Chaque fois que tu écris `<Carte />`, React crée une **instance séparée**, avec son **propre** `likes`. Liker la carte n°1 ne touche pas la carte n°2 : ce sont des compteurs distincts.
### 3. Alors pourquoi elles affichent toutes la même valeur au départ ?
Parce qu'elles exécutent toutes **le même calcul d'initialisation**, et qu'au **premier lancement**, `localStorage` est **vide** :
- `localStorage.getItem("likes-" + props.id)` → renvoie `null` (rien n'est encore stocké)
- `Number(null)` → `0`, donc `|| 0` → **0**
→ **toutes** les cartes démarrent à **0**. Ce n'est pas qu'elles « partagent » une valeur : c'est qu'elles **partent toutes de la même situation** (rien en mémoire). C'est comme 4 personnes qui commencent un jeu : elles ont toutes 0 point au début, mais chacune marque ses propres points ensuite.
> 💡 **La preuve que c'est bien indépendant et par-carte :** like la carte n°1, **recharge la page (F5)**. La carte n°1 retrouve **sa** valeur (lue dans `localStorage` à la clé `likes-1`), les autres restent à 0. Chaque carte a donc bien **sa propre** initialisation, basée sur **son** `id`.
---
## ⚠️ Un piège que tu as déjà senti (commit « buggy »)
Tu as remarqué : les likes par carte **survivent au rechargement** (ils sont dans `localStorage`), **mais le `total` repart à 0** après un F5. Normal : le `total` du parent n'est **pas** stocké, et il n'est pas **recalculé** à partir des cartes — il ne fait que s'incrémenter aux clics.
Pour l'instant, **c'est OK** : ton objectif était de faire **remonter** un like vers le parent (le pattern callback ci-dessus), et c'est réussi.
> 🔭 Pour aller plus loin (plus tard) : il existe une version plus robuste où le **total se recalcule** tout seul à partir des likes (une seule « source de vérité »). On en reparlera — c'est l'étape d'après.
---
## 🧹 Petit nettoyage
Dans `Carte`, tu as un effet **vide** qui ne sert à rien :
```jsx
useEffect(() => {}, [likes]); // ❌ à supprimer
```
Enlève-le, il ne fait rien.
---
## ✅ Récap
- Le parent **prête** une fonction (`onLike`) ; l'enfant doit **l'appeler** → c'est la ligne `props.onLike()` qui te manquait.
- Au clic : l'enfant met à jour **son** état **et** appelle `props.onLike()`.
- `useState(x)` = valeur de départ, utilisée **une fois**. Chaque carte est **indépendante** ; elles démarrent toutes pareil parce qu'au premier lancement la mémoire est vide (toutes à 0).
- Supprime le `useEffect` vide.
+95
View File
@@ -0,0 +1,95 @@
const zoneMessage = document.getElementById("contacter");
const compteur = document.getElementById("restant");
const motCaractere = document.getElementById("mot");
const boutonEffacer = document.getElementById("effacer");
const formulaire = document.querySelector("form");
const badge = document.getElementById("statut_horaires");
const nom = document.getElementById("prenom");
const MAX = 500;
function rafraichir_badge() {
const heure = new Date().getHours();
if (heure >= 9 && heure < 18) {
badge.textContent = "✅ Nous sommes ouverts (9h–18h)";
badge.style.color = "green";
} else {
badge.textContent = "🔴 Fermé — écrivez-nous, on répond dès l'ouverture !";
badge.style.color = "red";
}
}
setInterval(rafraichir_badge, 60);
function rafraichirCompteur() {
const restant = MAX - zoneMessage.value.length;
compteur.textContent = restant;
compteur.style.color = restant < 50 ? "red" : "black";
motCaractere.textContent = restant <= 1 ? "caractère" : "caractères";
localStorage.setItem("brouillon", zoneMessage.value);
}
zoneMessage.addEventListener("input", rafraichirCompteur);
boutonEffacer.addEventListener("click", function () {
zoneMessage.value = "";
rafraichirCompteur();
});
formulaire.addEventListener("submit", function (evenement) {
evenement.preventDefault();
if (zoneMessage.value.trim() === "") {
alert("Merci d'écrire un message avant d'envoyer !");
} else {
const greeting = `merci ${nom.value}, ton message de ${zoneMessage.value.length} caractères a été pris en compte`;
alert(`${greeting}`);
}
});
function creerNavbar(pageActive) {
return `
<div id="nav_barre" class="shadow">
<h1>FleetZen</h1>
<div id="buttons">
<button class="${pageActive === "contact" ? "instyled" : "styled"}">contact</button>
<a href="services.html"><button class="${pageActive === "services" ? "instyled" : "styled"}">services</button></a>
<a href="../index.html"><button class="styled"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-corner-down-left-icon lucide-corner-down-left"><path d="M20 4v7a4 4 0 0 1-4 4H4"/><path d="m9 10-5 5 5 5"/></svg>retour</button></a>
</div>
</div>`;
}
document.body.insertAdjacentHTML("afterbegin", creerNavbar("contact"));
document.getElementById("theme").addEventListener("click", function () {
document.body.classList.toggle("sombre");
});
const titres = document.querySelectorAll(".q-titre");
titres.forEach(function (titre) {
titre.addEventListener("click", function () {
const reponse = titre.nextElementSibling;
reponse.classList.toggle("ouverte");
});
});
const horloge = document.getElementById("horloge");
function afficherHeure() {
const maintenant = new Date();
horloge.textContent = maintenant.toLocaleTimeString();
}
afficherHeure();
setInterval(afficherHeure, 1000);
zoneMessage.addEventListener("keydown", function (event) {
if (event.key === "Enter" && event.ctrlKey) {
formulaire.requestSubmit();
}
preventDefault();
if (event.key === "Escape") {
zoneMessage.value = "";
rafraichirCompteur();
}
});
+3 -47
View File
@@ -42,6 +42,7 @@ h1 {
position: fixed;
bottom: 0;
width: 100%;
border-top: 2px solid;
}
#nav_barre {
display: flex;
@@ -58,10 +59,11 @@ h1 {
width: 130px;
line-height: 2.5;
padding: 0 20px;
box-shadow: 3px 5px 3px darkslategray;
font-size: 1rem;
text-align: center;
color: white;
text-shadow: 1px 1px 1px black;
text-shadow: 1px 1px 1px slategray;
border-radius: 10px;
background-color: royalblue;
background-image: linear-gradient(
@@ -81,53 +83,7 @@ h1 {
inset -2px -2px 3px rgb(255 255 255 / 0.6),
inset 2px 2px 3px rgb(0 0 0 / 0.6);
}
.x_to_x {
border: 0;
height: 70px;
width: 130px;
line-height: 2.5;
padding: 0 20px;
font-size: 1rem;
text-align: center;
color: white;
text-shadow: 1px 1px 1px black;
border-radius: 10px;
background-color: midnightblue;
background-image:
linear-gradient to top left,
rgb(0 0 0 / 0.2),
rgb(0 0 0 / 0.2) 30%,
transparent;
}
.styled {
border: 0;
height: 70px;
width: 130px;
line-height: 2.5;
padding: 0 20px;
font-size: 1rem;
text-align: center;
color: white;
text-shadow: 1px 1px 1px black;
border-radius: 10px;
background-color: royalblue;
background-image: linear-gradient(
to top left,
rgb(0 0 0 / 0.2),
rgb(0 0 0 / 0.2) 30%,
transparent
);
}
.styled:hover {
background-color: midnightblue;
}
.styled:active {
box-shadow:
inset -2px -2px 3px rgb(255 255 255 / 0.6),
inset 2px 2px 3px rgb(0 0 0 / 0.6);
}
#buttons {
height: 100%;
width: 25%;
+61
View File
@@ -0,0 +1,61 @@
const equipe = [
{ nom: "Mélissa", job: "Stagiaire", partie: "Page contact" },
{ nom: "Guillaume", job: "Stagiaire", partie: "Page services" },
{ nom: "Matthieu", job: "Tuteur", partie: "Relecture" },
];
for (const membre of equipe) {
console.log(membre.nom + " — " + membre.job + " — " + membre.partie);
}
function creerLigne(membre) {
const tr = document.createElement("tr");
const tdNom = document.createElement("td");
tdNom.textContent = membre.nom;
const tdJob = document.createElement("td");
tdJob.textContent = membre.job;
const tdPartie = document.createElement("td");
tdPartie.textContent = membre.partie;
tr.append(tdNom, tdJob, tdPartie);
return tr;
}
const corps = document.getElementById("corps_equipe");
function afficherEquipe(liste) {
corps.innerHTML = "";
for (const membre of liste) {
corps.append(creerLigne(membre));
}
}
afficherEquipe(equipe);
document.getElementById("nb_membres").textContent = equipe.length;
const champRecherche = document.getElementById("recherche");
champRecherche.addEventListener("input", function () {
const motCherche = champRecherche.value.toLowerCase();
// on garde seulement les membres dont le nom contient le texte tapé
const resultats = equipe.filter(function (membre) {
return membre.nom.toLowerCase().includes(motCherche);
});
afficherEquipe(resultats); // on ré-affiche, mais filtré
});
document.getElementById("trier").addEventListener("click", function () {
const triee = [...equipe].sort(function (a, b) {
return a.nom.localeCompare(b.nom);
});
afficherEquipe(triee);
});
const array = equipe;
const mapped = array.map((x) => x.nom);
console.log(mapped);
+21
View File
@@ -0,0 +1,21 @@
async function chargerFlotte() {
const message = document.getElementById("message_flotte");
const corps = document.getElementById("corps_flotte");
try {
const reponse = await fetch("../vehicules.json");
const vehicules = await reponse.json();
message.textContent = vehicules.length + " véhicules :";
for (const v of vehicules) {
const tr = document.createElement("tr");
tr.textContent = `${v.modele} — ${v.immatriculation} — ${v.km} km`;
corps.append(tr);
}
} catch (erreur) {
message.textContent = "Impossible de charger la flotte";
console.error(erreur);
}
}
chargerFlotte();
-30
View File
@@ -1,30 +0,0 @@
import { useState } from "react";
import { Routes, Route } from "react-router-dom";
import { initialData } from "./data";
import Accueil from "./pages/Accueil";
import Details from "./pages/Details";
function App() {
const [trilogies, setTrilogies] = useState(initialData);
const [likes, setLikes] = useState({});
return (
<Routes>
<Route
path="/"
element={
<Accueil
trilogies={trilogies}
setTrilogies={setTrilogies}
likes={likes}
setLikes={setLikes}
/>
}
/>
<Route
path="/trilogie/:id"
element={<Details trilogies={trilogies} likes={likes} />}
/>
</Routes>
);
}
export default App;
-9
View File
@@ -1,9 +0,0 @@
function Footer() {
return (
<footer>
<p>Footer toto</p>
</footer>
);
}
export default Footer;
-31
View File
@@ -1,31 +0,0 @@
function Header() {
return (
<header>
<h1>Les Trilogies les plus Connues de Geometry Dash</h1>
<p>
Geometry dash est un jeu fascinant. Même si c’est un jeu, en apparence,
très simple, il se complexifie très vite avec les différentes mécaniques
telles que les portails qui changent la gravité du cube, ceux qui
augmentent la vitesse, et les nombreuses transformations telles que le
ship ou la wave. Vous l’avez compris, ce jeu ne se résume pas à un
simple cube qui saute, et finir le plus dur des niveaux officiels,
deadlocked, n’est pas à la portée de tous les joueurs. Cependant, finir
deadlocked et tous les niveaux officiels n’est pas une fin en soi. En
effet, le vrai potentiel du jeu se trouve dans l’éditeur de niveau. Ce
dernier permet aux joueurs de créer leurs propres niveaux et de faire
vivre la communauté (pcq oui hein robtop fait rarement des mises à
jour). On peut donc rapidement découper cette communauté en 2 catégories
: les buildeurs, qui créaient les niveaux, et les joueurs, qui jouent
aux niveaux (bravo vous avez deviné), et très vite, l’objectif des
buildeurs va être de créer les niveaux les plus durs, mais surtout les
plus iconiques et marquant pour les joueurs. Certains niveaux sont
tellement iconiques que des suites sont créées, créant ainsi des
trilogies qui resteront à jamais dans l'esprit des joueurs. Aujourd'hui,
on va parler de 5 trilogies qui ont marqué l'histoire de geometry dash,
trilogies qui ont su nous impacter.
</p>
</header>
);
}
export default Header; // 👈 on l'EXPORTE pour pouvoir l'utiliser ailleurs
-23
View File
@@ -1,23 +0,0 @@
export const initialData = [
{
id: 1,
nom: "The RGB Trilogy",
niveaux: "Yatagarasu - Erebus - Sonic Wave",
},
{ id: 2, nom: "The Greek Letter Trilogy", niveaux: "Omega - Sigma - Gamma" },
{
id: 3,
nom: "The Techno Trilogy",
niveaux: "Artificial Ascent - Digital Descent - Cybernetic Crescent",
},
{
id: 4,
nom: "The Riot's Trilogy",
niveaux: "Tartarus - Acheron - Aeternus",
},
{
id: 5,
nom: "The Apocalytic Trilogy",
niveaux: "Cataclysm - Bloodbath - Aftermath",
},
];
-132
View File
@@ -1,132 +0,0 @@
import { useState } from "react";
import Carte from "../components/carte";
import Header from "../components/Header";
import Footer from "../components/Footer";
function Accueil({ trilogies, setTrilogies, likes, setLikes }) {
console.log("test");
const [a, setA] = useState(trilogies[0].id);
const [b, setB] = useState(trilogies[1].id);
const triloA = trilogies.find((t) => t.id === Number(a));
const triloB = trilogies.find((t) => t.id === Number(b));
const likesA = likes[a] || 0;
const likesB = likes[b] || 0;
const gagnante =
!triloA || !triloB
? "—" // une des deux a été supprimée
: likesA === likesB
? "Égalité"
: likesA > likesB
? triloA.nom
: triloB.nom;
const [recherche, setRecherche] = useState("");
const [nom, setNom] = useState("");
const [niveaux, setNiveaux] = useState("");
const [tri, setTri] = useState("nom");
const affichees = [...trilogies]
.filter((t) => t.nom.toLowerCase().includes(recherche.toLowerCase()))
.sort((a, b) => {
if (tri === "nom") return a.nom.localeCompare(b.nom);
if (tri === "likes") return (likes[b.id] || 0) - (likes[a.id] || 0);
return 0;
});
const medailles = ["🥇", "🥈", "🥉"];
const podium = [...trilogies]
.sort((a, b) => (likes[b.id] || 0) - (likes[a.id] || 0))
.slice(0, 3);
function ajouter(e) {
e.preventDefault();
if (nom.trim() === "") return;
const nouvelle = { id: Date.now(), nom, niveaux };
setTrilogies([...trilogies, nouvelle]);
setNom("");
setNiveaux("");
}
function liker(id) {
setLikes((prev) => ({ ...prev, [id]: (prev[id] || 0) + 1 }));
}
function supprimer(id) {
setTrilogies(trilogies.filter((t) => t.id !== id));
setLikes((prev) => {
const copie = { ...prev }; // une COPIE (on ne modifie jamais l'ancien objet)
delete copie[id]; // on retire l'entrée de cette trilogie
return copie; // on renvoie le nouvel objet
});
}
const total = Object.values(likes).reduce((s, n) => s + n, 0);
return (
<div className="body">
<Header />
<>
<select value={tri} onChange={(e) => setTri(e.target.value)}>
<option value="nom">Nom (A→Z)</option>
<option value="likes">Plus likées</option>
</select>
</>
<div className="versus">
<select value={a} onChange={(e) => setA(e.target.value)}>
{trilogies.map((t) => (
<option key={t.id} value={t.id}>
{t.nom}
</option>
))}
</select>
<select value={b} onChange={(e) => setB(e.target.value)}>
{trilogies.map((t) => (
<option key={t.id} value={t.id}>
{t.nom}
</option>
))}
</select>
<p>🏆 Gagnante : {gagnante}</p>
</div>
<div className="podium">
{podium.map((t, index) => (
<p key={t.id}>
{medailles[index]} {t.nom} — {likes[t.id] || 0} ❤️
</p>
))}
</div>
<form onSubmit={ajouter}>
<input
value={nom}
onChange={(e) => setNom(e.target.value)}
placeholder="Nom de la trilogie"
/>
<input
value={niveaux}
onChange={(e) => setNiveaux(e.target.value)}
placeholder="Niveaux"
/>
<button type="submit">Ajouter</button>
</form>
<input
type="search"
placeholder="Rechercher une trilogie…"
value={recherche}
onChange={(e) => setRecherche(e.target.value)}
/>
<p>👍 Total de likes : {total}</p>
{affichees.length === 0 && <p>Aucune trilogie trouvée</p>}
{affichees.map((trilo) => (
<Carte
key={trilo.id}
triloId={trilo.id}
titre={trilo.nom}
niveaux={trilo.niveaux}
likes={likes[trilo.id] || 0}
onLike={() => liker(trilo.id)}
onDelete={() => supprimer(trilo.id)}
/>
))}
<Footer />
</div>
);
}
export default Accueil;
-25
View File
@@ -1,25 +0,0 @@
import { useParams, Link } from "react-router-dom";
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>
);
return (
<div className="detail">
<Link to="/">← Retour</Link>
<h1>{trilogie.nom}</h1>
<p>Niveaux : {trilogie.niveaux}</p>
<p>❤️ Likes : {likes[trilogie.id] || 0}</p>
</div>
);
}
export default Details;
-6
View File
@@ -1,6 +0,0 @@
{
"name": "stagiaire",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
+193
View File
@@ -0,0 +1,193 @@
* {
box-sizing: border-box;
}
body {
width: 100%;
height: 100%;
background-color: lightblue;
margin: 0;
padding-bottom: 65px;
}
#nav_barre {
display: flex;
width: 100%;
height: 100px;
background-color: steelblue;
padding-left: 10px;
padding-right: 10px;
}
.styled {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
line-height: normal;
border: 0;
height: 70px;
width: 130px;
padding: 0 20px;
box-shadow: 3px 5px 3px darkslategray;
font-size: 1rem;
text-align: center;
color: white;
text-shadow: 1px 1px 1px slategray;
border-radius: 10px;
background-color: royalblue;
background-image: linear-gradient(
to top left,
rgb(0 0 0 / 0.2),
rgb(0 0 0 / 0.2) 30%,
transparent
);
}
.styled:hover {
background-color: midnightblue;
}
.styled:active {
box-shadow:
inset -2px -2px 3px rgb(255 255 255 / 0.6),
inset 2px 2px 3px rgb(0 0 0 / 0.6);
}
#buttons {
height: 100%;
width: 25%;
display: flex;
justify-content: space-around;
flex-direction: row-reverse;
margin-left: auto;
align-items: center;
}
.shadow {
box-shadow: 0px 10px 5px gray;
}
h1 {
font-size: 40px;
}
.instyled {
border: 0;
height: 70px;
width: 130px;
line-height: 2.5;
padding: 0 20px;
box-shadow:
inset -2px -2px 3px rgb(255 255 255 / 0.6),
inset 2px 2px 3px rgb(0 0 0 / 0.6);
font-size: 1rem;
text-align: center;
color: lightgrey;
text-shadow: 1px 1px 1px darkslategrey;
border-radius: 10px;
background-color: darkblue;
background-image: linear-gradient(
to top left,
rgb(0 0 0 / 0.2),
rgb(0 0 0 / 0.2) 30%,
transparent
);
}
.table {
width: 700px;
height: 300px;
border-collapse: collapse;
font-size: 35px;
table-layout: auto;
border: 5px solid darkblue;
margin-left: auto;
margin-right: auto;
background-color: skyblue;
box-shadow: 2px 5px 5px darkgray;
background-image: linear-gradient(
to top left,
rgb(90 100 150),
transparent
);
}
thead {
background-color: rgb(8, 105, 85);
text-shadow: 2px 2px 2px slategray;
color: lightblue;
}
th,
td {
border: 2px solid dimgray;
padding: 10px;
}
#titre_tableau {
font-size: 25px;
color: seagreen;
padding-left: 10px;
}
.form {
display: flex;
width: 570px;
height: 380px;
border: 3px solid green;
align-items: center;
flex-direction: column;
margin-left: auto;
margin-right: auto;
margin-top: 100px;
background-color: rgb(159, 199, 159);
background-image: linear-gradient(
to top left,
rgb(0 0 0 / 0.001),
rgb(50 90 50 / 0.8) 0.1%,
transparent
);
box-shadow: 2px 5px 3px;
}
textarea {
resize: none;
width: 527px;
height: 80px;
}
.small {
font-size: 10px;
}
.footer {
text-align: center;
background-color: cadetblue;
font-size: 25px;
position: fixed;
bottom: 0;
width: 100%;
border-top: 2px solid;
padding-bottom: 10px;
}
#statut_horaires {
font-size: 15px;
}
body.sombre {
background-color: #1d2733;
color: white;
}
.q-reponse {
display: none;
}
.q-reponse.ouverte {
display: block;
}
.Faq {
display: flex;
align-items: center;
justify-content: center;
padding-top: 30 px;
}
#horloge {
text-align: right;
}
+87
View File
@@ -0,0 +1,87 @@
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="contact.css" />
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width", initial-scame="1"/>
<title>Contact-FleetZen</title>
<html lang="fr">
</head>
<body>
<p id= titre_tableau>
<b>Les développeurs de ce site</b>
</p>
<p>L'équipe compte <span id="nb_membres"></span> personne(s).</p>
<input type="search" id="recherche" placeholder="Rechercher un membre…">
<button type="button" id="trier">Trier A → Z</button>
<table border=1 class="table">
<thead>
<tr>
<th>Nom</th>
<th>Job</th>
<th>Parties crées</th>
</tr>
</thead>
<tbody id="corps_equipe">
</tbody>
</table>
<div class="form">
<p><b>Nous contacter</b></p>
<p id="statut_horaires"></p>
<form method="post" action="">
<p>
<label for="prenom">Votre prénom :</label>
<input type="text" name="prenom" id="prenom" placeholder="Ex. : Matthieu" size="30" maxlength="20">
</p>
<p>
<label for="email">Votre e-mail:</label>
<input type="email" name="email" id="email" placeholder="Ex. : stage2@echoes.solutions" size="50" maxlength="40">
</p>
<div>
<label for="contacter">Des questions a nous poser?</label><br>
<textarea name="contacter" id="contacter" rows="6" cols="40" maxlength="500"></textarea>
<p class=small>Il vous reste <span id="restant">500</span> <span id="mot">caractères</span>.</p>
</div>
<p>
<button type="submit">Envoyer</button>
<button type="button" id="effacer">Effacer</button>
</p>
</form>
</div>
<div class="Faq">
<section class="faq">
<h3>Questions fréquentes</h3>
<div class="question">
<button class="q-titre"type="button" >Comment suivre ma flotte en temps réel ?</button>
<p class="q-reponse">Grâce à notre service de géolocalisation, accessible 24h/24.</p>
</div>
<div class="question">
<button class="q-titre"type="button">Puis-je résilier à tout moment ?</button>
<p class="q-reponse">Oui, sans frais, depuis votre espace client.</p>
</div>
</section>
</div>
<h3>Notre flotte</h3>
<p id="message_flotte">Chargement…</p>
<table border=1>
<tbody id="corps_flotte">
</tbody>
</table>
<footer class="footer">
<button type="button" id="theme"> Mode sombre</button>
<b>stagiaires rpz echoes</b>
<span id="horloge"></span>
</footer>
<script src="../equipe.js"></script>
<script src="../contact.js"></script>
<script src="../flotte.js"></script>
</body>
</html>
-131
View File
@@ -1,131 +0,0 @@
const affichageNb = document.getElementById("nb_vehicules");
const affichageGéo = document.getElementById("géolocalisation");
const affichageConso = document.getElementById("consommation");
const boutonPlus = document.getElementById("plus");
const boutonMoins = document.getElementById("moins");
const mot_vehicules = document.getElementById("mot_vehic");
const PRIX_GPS = 30;
const PRIX_CONSOMMATION = 15;
const zoneMsg = document.getElementById("msg");
const txt_restant = document.getElementById("txt_restant");
const MAX = 500;
const formulaire = document.querySelector("form");
console.log("dans le local : ", localStorage.getItem("nbVehicules"));
const services = [
{
nom: "Géolocalisation",
description: "Suivi en temps réel",
prix: "30€/véhicule/mois",
},
{ nom: "Odomètre", description: "Kilométrage précis", prix: "200€" },
{
nom: "Consommation",
description: "Carburant consommé",
prix: "15€/véhicule/mois",
},
{
nom: "Impact carbone",
description: "CO₂ rejeté",
prix: "30€/véhicule/mois",
},
];
const corps = document.getElementById("services");
const horloge = document.getElementById("horloge");
let lignes = "";
let nbVehicules;
if (localStorage.getItem("nbVehicules") !== null) {
nbVehicules = parseInt(localStorage.getItem("nbVehicules"));
mettreAJourTOTAL();
} else {
nbVehicules = 1;
}
function mettreAJourTOTAL() {
affichageNb.textContent = nbVehicules;
affichageGéo.textContent = nbVehicules * PRIX_GPS;
affichageConso.textContent = nbVehicules * PRIX_CONSOMMATION;
mot_vehicules.textContent = nbVehicules === 1 ? "véhicule" : "véhicules";
}
boutonPlus.addEventListener("click", function () {
nbVehicules = nbVehicules + 1;
if (nbVehicules > 99) {
nbVehicules = nbVehicules - 1;
}
console.log("rajout en cours :", nbVehicules);
localStorage.setItem("nbVehicules", nbVehicules);
mettreAJourTOTAL();
});
boutonMoins.addEventListener("click", function () {
if (nbVehicules > 1) {
nbVehicules = nbVehicules - 1;
}
localStorage.setItem("nbVehicules", nbVehicules);
mettreAJourTOTAL();
});
zoneMsg.addEventListener("input", function () {
const reste = MAX - zoneMsg.value.length;
txt_restant.textContent = reste;
txt_restant.style.color = reste < 50 ? "red" : "black";
});
formulaire.addEventListener("submit", function (evenement) {
if (zoneMsg.value.trim() === "") {
evenement.preventDefault();
alert("Merci d'écrire un message");
}
});
for (const service of services) {
console.log(service.nom + " _ " + service.description + " _ " + service.prix);
}
for (const service of services) {
lignes =
lignes +
`
<tr>
<td>${service.nom}</td>
<td>${service.description}</td>
<td>${service.prix}</td>
</tr>`;
}
corps.innerHTML = lignes;
document.getElementById("color").addEventListener("click", function () {
document.body.classList.toggle("sombre");
});
function heure() {
horloge.textContent = new Date().toLocaleTimeString();
}
{
heure();
setInterval(heure, 1000);
}
{
async function Flotte() {
const message = document.getElementById("message_flotte");
const corps = document.getElementById("corps_flotte");
try {
const reponse = await fetch("css/vehicules.json");
let toto = reponse.map((x) => parseInt(x.km) + 2);
const vehicules = await toto.json();
for (const v of vehicules) {
const tr = document.createElement("tr");
tr.textContent = `${v.modele} — ${v.immatriculation} — ${v.km} km`;
corps.append(tr);
}
} catch (erreur) {
message.textContent =
"Impossible de charger la flotte (bahahaha mimimimimi)";
console.error(erreur);
}
}
Flotte();
}
function creerNavbar(pageActive) {
return `
<div id="nav_barre" class="shadow">
<h1>FleetZen</h1>
<div id="buttons">
<a href="contact.html"><button class="${pageActive === "contact" ? "x_to_x" : "styled"}">contact</button></a>
<a href="services.html"><button class="${pageActive === "services" ? "x_to_x" : "styled"}">services</button></a>
<a href="../index.html"><button class="retour btn_icone"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-undo2-icon lucide-undo-2"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"/></svg>retour</button></a>
</div>
</div>`;
}
document.body.insertAdjacentHTML("afterbegin", creerNavbar("services"));
-241
View File
@@ -1,241 +0,0 @@
* {
box-sizing: border-box;
}
body {
width: 100%;
height: 100%;
background-color: rgb(190, 255, 255);
margin: 0;
padding-bottom: 167px;
}
#img_1 {
width: 20%;
height: auto;
}
.image_footer {
display: flex;
width: 30%;
height: auto;
}
.align_x {
display: flex;
width: 100%;
height: 100%;
flex-direction: row;
justify-content: space-between;
padding-top: 20px;
padding-left: 10px;
padding-right: 10px;
}
#text_1 {
width: 65%;
font-size: 200%;
}
h1 {
background-color: steelblue;
font-size: 40px;
}
.footer {
text-align: center;
background-color: cadetblue;
font-size: 25px;
position: fixed;
bottom: 0;
width: 100%;
display: flex;
justify-content: space-around;
}
#nav_barre {
display: flex;
width: 100%;
height: 100px;
background-color: steelblue;
padding-left: 10px;
padding-right: 10px;
}
.styled {
border: 0;
height: 70px;
width: 130px;
line-height: 2.5;
padding: 0 20px;
font-size: 1rem;
text-align: center;
color: white;
text-shadow: 1px 1px 1px black;
border-radius: 10px;
background-color: royalblue;
background-image: linear-gradient(
to top left,
rgb(0 0 0 / 0.2),
rgb(0 0 0 / 0.2) 30%,
transparent
);
}
.styled:hover {
background-color: midnightblue;
}
.styled:active {
box-shadow:
inset -2px -2px 3px rgb(255 255 255 / 0.6),
inset 2px 2px 3px rgb(0 0 0 / 0.6);
}
.x_to_x {
border: 0;
height: 70px;
width: 130px;
line-height: 2.5;
padding: 0 20px;
font-size: 1rem;
text-align: center;
color: white;
text-shadow: 1px 1px 1px black;
border-radius: 10px;
background-color: midnightblue;
background-image: linear-gradient(
to top left,
rgb(0 0 0 / 0.2),
rgb(0 0 0 / 0.2) 30%,
transparent
);
}
.retour {
border: 0;
height: 50px;
width: 130px;
line-height: 2.5;
padding: 0 20px;
font-size: 1rem;
text-align: center;
color: black;
border-radius: 10px;
background-color: white;
background-image: linear-gradient(
to top left,
rgb(0 0 0 / 0.2),
rgb(0 0 0 / 0.2) 30%,
transparent
);
}
.retour:hover {
background-color: gray;
}
.retour:active {
box-shadow:
inset -2px -2px 3px rgb(255 255 255 / 0.6),
inset 2px 2px 3px rgb(0 0 0 / 0.6);
}
#buttons {
height: 100%;
width: 37%;
display: flex;
justify-content: space-around;
flex-direction: row-reverse;
margin-left: auto;
align-items: center;
}
.shadow {
box-shadow: 0px 10px 5px gray;
}
p {
font-family: cursive;
}
.sous-titre {
font-size: 25px;
}
.table {
margin-bottom: 30px;
width: 70%;
height: auto;
background-color: dodgerblue;
border-collapse: collapse;
font-size: 25px;
table-layout: auto;
border: 2px solid;
th,
td {
padding: 10px;
text-align: center;
}
td {
background-color: lightskyblue;
}
}
.padding_left {
padding-left: 20px;
}
#boutton_formulaire {
width: 200px;
height: 50px;
display: flex;
justify-content: space-around;
flex-direction: row;
margin-left: auto;
align-items: center;
padding: 2px;
text-align: center;
}
.formulaire {
background-color: lightskyblue;
padding: 10px;
width: 40%;
height: 300px;
border: solid;
display: flex;
font-size: 20px;
margin: 30px;
li {
padding: 15px;
}
ul {
list-style-type: none;
}
}
.center {
width: 100%;
display: flex;
justify-content: center;
}
#msg {
resize: horizontal;
min-width: 50px;
max-width: 300px;
}
.btn_icone {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
line-height: normal;
}
body.sombre {
background-color: #1d2733;
color: white;
}
.table_2 {
margin: 30px;
margin-top: 120px;
width: 70%;
height: auto;
background-color: lightskyblue;
border-collapse: collapse;
font-size: 20px;
table-layout: auto;
border: 2px solid;
th,
td {
padding: 10px;
text-align: center;
}
td {
background-color: lightcoral;
}
}
-7
View File
@@ -1,7 +0,0 @@
[
{
"modele": 5,
"immatriculation": 3,
"km": 4
}
]
-71
View File
@@ -1,71 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="css/style_services.css" />
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width", initial-scame="1"/>
<title>FleetZen/services</title>
<html lang="fr">
</head>
<body>
<div class="padding_left">
<p class="sous-titre">Découvrez nos services tous plus utiles les uns que les autres !</p>
<table border="1" class="table">
<thead>
<tr>
<th>Nom</th>
<th>Description</th>
<th>Prix</th>
</tr>
</thead>
<tbody id="services">
</tbody>
</table>
</div>
<section class="simulateur padding_left" >
<h2>Estimez votre devis</h2>
<p>Nombre de véhicules dans votre flotte :</p>
<button id="moins" type="button">−</button>
<span id="nb_vehicules">1</span> <span id="mot_vehic">véhicule</span>
<button id="plus" type="button">+</button>
<p>Total géolocalisation : <strong id="géolocalisation">30</strong> €/mois</p>
<p>Total consommation : <strong id="consommation">15</strong> €/mois</p>
</section>
<div class="center">
<div class="formulaire">
<form action="" method="post">
<p> Si vous avez des suggestions :</p>
<ul>
<li>
<label for="name">Nom&nbsp;:</label>
<input type="text" placeholder="Dupond" id="name" name="user_name" />
</li>
<li>
<label for="mail">E-mail&nbsp;:</label>
<input type="email" placeholder="dupondrichard@gmail.com" id="mail" name="user_mail" />
</li>
<li>
<label for="msg">Message&nbsp;:</label>
<textarea id="msg" placeholder="Je pense qu'il faudrait..." name="user_message"
rows="5" cols="40" maxlength="500"></textarea>
<p>Il vous reste <span id="txt_restant">500</span> caractères.</p>
</li>
<div id="boutton_formulaire">
<button type="submit"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-send-icon lucide-send"><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"/><path d="m21.854 2.147-10.94 10.939"/></svg>Envoyer le message</button>
</div>
</ul>
</form>
</div>
</div>
<table border="1" class="table_2">
<tbody id="corps_flotte"></tbody>
</table>
<footer class="footer">
<button id="color">Mode sombre</button>
stagiaires rpz echoes
<span id="horloge"></span>
</footer>
<script src="css/devis.js"></script>
</body>
</html>
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>mon-projet</title>
<title>react-project</title>
</head>
<body>
<div id="root"></div>
@@ -1,11 +1,11 @@
{
"name": "mon-projet",
"name": "react-project",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mon-projet",
"name": "react-project",
"version": "0.0.0",
"dependencies": {
"react": "^19.2.7",
@@ -543,14 +543,14 @@
}
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
"integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==",
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz",
"integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@tybys/wasm-util": "^0.10.3"
"@tybys/wasm-util": "^0.10.2"
},
"funding": {
"type": "github",
@@ -572,9 +572,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz",
"integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.2.tgz",
"integrity": "sha512-2cZ+7xRS+DBcuJBJKnfzsbleumJhBqSlJVpuzHC0nTqfd3QQ7Vx2/x5YR/D7cBamKSeWplwo82Fn9lqYUDEMfA==",
"cpu": [
"arm64"
],
@@ -589,9 +589,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz",
"integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.2.tgz",
"integrity": "sha512-RkPMJnygxsgOYdkfqgpwY0/Fzm8d0VQe6HGU2/B00Xa9eqdLbrII+DOKAodbJAn3ZL1AJxGHkZRPYazgGY6Ljw==",
"cpu": [
"arm64"
],
@@ -606,9 +606,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz",
"integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.2.tgz",
"integrity": "sha512-Uiczh6vFhwyfd7WNe7Q7mCA4KxAiLdz7jPE/WGizfRpIieoyFuNVMmM8HqZ9HwudTkY6/AeMQwlNJ9NJijguWw==",
"cpu": [
"x64"
],
@@ -623,9 +623,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz",
"integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.2.tgz",
"integrity": "sha512-+TpdtTRgHiJFjCVFbw311SuLk3KfytPOQQn+VlAEv+gBxYPtL7E6JS9e/tk+8CwxhIZvemJKo4rTKgfWNsKkkA==",
"cpu": [
"x64"
],
@@ -640,9 +640,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz",
"integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.2.tgz",
"integrity": "sha512-4lv1/tkmi7ueIVHnyreaOeUpiZP26BH9rRy6hoYfR9310A2B9nUEVRDvBx69vx64Nr3eTPPRkyciqJJs+j9Jmw==",
"cpu": [
"arm"
],
@@ -657,9 +657,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz",
"integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.2.tgz",
"integrity": "sha512-gBSUVO0eaWgw1JMjK3gB8BMlX2Mk148s2lTiVT3e9vjVxbl7UDfMWWY8CfIaaqiXuM9fVTMxIpUz6CAo/B6Vlw==",
"cpu": [
"arm64"
],
@@ -677,9 +677,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz",
"integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.2.tgz",
"integrity": "sha512-LjQP/iZLBu8o8PjIfk4x3At0/mT6h282pvz8Z5LAyhGbu/kDezyO7ea62rF5uoqmgnIYqbN/MqJ3Si3Aymi7xQ==",
"cpu": [
"arm64"
],
@@ -697,9 +697,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz",
"integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.2.tgz",
"integrity": "sha512-X/7bVLWelEsbyWDUSXt7zVsTniLLPIY2n1rH58qr78l9i7MNbbxBWD8gI2vRfBWf4NUXJCUuQnfZDsp32LqsfQ==",
"cpu": [
"ppc64"
],
@@ -717,9 +717,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz",
"integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.2.tgz",
"integrity": "sha512-gb6dYKW/1KDorGXyy48glEBJs/sxVSC5pcVrox/pFGV4mvwSFeg2sK5L2tRkVsVlh7kueqOgg4GEcuipJcGuKg==",
"cpu": [
"s390x"
],
@@ -737,9 +737,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz",
"integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.2.tgz",
"integrity": "sha512-JY4w85pU3iAiJVMh5nuk4/Mh9GjMsupe8MrIN53rwxAZW64GKrWeJBuN6SxQg9QTU5uB1cxyhDzW8jqRn1EABw==",
"cpu": [
"x64"
],
@@ -757,9 +757,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz",
"integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.2.tgz",
"integrity": "sha512-xvpA7o5KCYLB0Rwscmuylb1/zHHSUx4g4xilm4prC5jP76pEUlzBmMbgpbh7bVDbId4NcfT96gN5i6mE6UDaiw==",
"cpu": [
"x64"
],
@@ -777,9 +777,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz",
"integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.2.tgz",
"integrity": "sha512-p/ts6KBLjuk49Bp21XH77poQGt02iNz7ChgHep7tudPOaLinR/De/RHdxF8w8Yj4r/bF/bqXwH6PZrB2sA+Nvw==",
"cpu": [
"arm64"
],
@@ -794,9 +794,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz",
"integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.2.tgz",
"integrity": "sha512-VMu/wmrZ9hJzYlRhbw7jK5PODlugyKZ5mOdX78+lS8OvuFkWNQdz1pFLrI2p3P0pjXOmUZ7B48o5VnMH9QOGtg==",
"cpu": [
"wasm32"
],
@@ -806,16 +806,16 @@
"dependencies": {
"@emnapi/core": "1.11.1",
"@emnapi/runtime": "1.11.1",
"@napi-rs/wasm-runtime": "^1.1.6"
"@napi-rs/wasm-runtime": "^1.1.5"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz",
"integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.2.tgz",
"integrity": "sha512-xtUJqs8qEkuSviS0n1tsohaPuz3a1SPhZywOji4Oo+sgrJs8daEDMZ0QtqL0OS7dx8PoVpg2J/ZZycPY5I2+Zg==",
"cpu": [
"arm64"
],
@@ -830,9 +830,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz",
"integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.2.tgz",
"integrity": "sha512-85YiLQqjUKgSO/Zjnf9e0XIn5Ymrh1fLDWBeAkZqpuBR/3R8TpfoHXuyblqyQrftSSgWO9qpcHN8mkyKsLraoA==",
"cpu": [
"x64"
],
@@ -1140,9 +1140,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.378",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.378.tgz",
"integrity": "sha512-VinvOAuuPmdD1guEgGv5f2Qp7/vlfqOrUOMYNnOD4wj3pit8kRsQHzfIf6teyUGWo15Tg5+bOJaRunvyltpVWQ==",
"version": "1.5.377",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.377.tgz",
"integrity": "sha512-cH1jZgJHoezfTnKfKwnScpHywTFVnJUNITDPREFdhNjiuD502+QFpG0Qk7G8jhsV/f+CEAFlIrzP1fT+IMb92g==",
"dev": true,
"license": "ISC"
},
@@ -1989,9 +1989,9 @@
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.50",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz",
"integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==",
"version": "2.0.48",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz",
"integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2197,9 +2197,9 @@
}
},
"node_modules/rolldown": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz",
"integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==",
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.2.tgz",
"integrity": "sha512-x0CrQQqCXWGeI8dTvFfN/Dnv3yMKT9hv5jFjlOreKAx9wqLq9wz7VvLLHyaAXC90/CpggTu9SisSbsJJTPSjNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2213,21 +2213,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.1.3",
"@rolldown/binding-darwin-arm64": "1.1.3",
"@rolldown/binding-darwin-x64": "1.1.3",
"@rolldown/binding-freebsd-x64": "1.1.3",
"@rolldown/binding-linux-arm-gnueabihf": "1.1.3",
"@rolldown/binding-linux-arm64-gnu": "1.1.3",
"@rolldown/binding-linux-arm64-musl": "1.1.3",
"@rolldown/binding-linux-ppc64-gnu": "1.1.3",
"@rolldown/binding-linux-s390x-gnu": "1.1.3",
"@rolldown/binding-linux-x64-gnu": "1.1.3",
"@rolldown/binding-linux-x64-musl": "1.1.3",
"@rolldown/binding-openharmony-arm64": "1.1.3",
"@rolldown/binding-wasm32-wasi": "1.1.3",
"@rolldown/binding-win32-arm64-msvc": "1.1.3",
"@rolldown/binding-win32-x64-msvc": "1.1.3"
"@rolldown/binding-android-arm64": "1.1.2",
"@rolldown/binding-darwin-arm64": "1.1.2",
"@rolldown/binding-darwin-x64": "1.1.2",
"@rolldown/binding-freebsd-x64": "1.1.2",
"@rolldown/binding-linux-arm-gnueabihf": "1.1.2",
"@rolldown/binding-linux-arm64-gnu": "1.1.2",
"@rolldown/binding-linux-arm64-musl": "1.1.2",
"@rolldown/binding-linux-ppc64-gnu": "1.1.2",
"@rolldown/binding-linux-s390x-gnu": "1.1.2",
"@rolldown/binding-linux-x64-gnu": "1.1.2",
"@rolldown/binding-linux-x64-musl": "1.1.2",
"@rolldown/binding-openharmony-arm64": "1.1.2",
"@rolldown/binding-wasm32-wasi": "1.1.2",
"@rolldown/binding-win32-arm64-msvc": "1.1.2",
"@rolldown/binding-win32-x64-msvc": "1.1.2"
}
},
"node_modules/scheduler": {
@@ -1,5 +1,5 @@
{
"name": "mon-projet",
"name": "react-project",
"private": true,
"version": "0.0.0",
"type": "module",
File renamed without changes.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

+42
View File
@@ -0,0 +1,42 @@
[
{
"id": "1",
"titre": "Oracle of ages",
"plateforme": "Game Boy Color",
"annee": "2001",
"Type": "2d",
"like": 0
},
{
"id": "2",
"titre": "Breath of the Wild",
"plateforme": "Switch",
"annee": "2017",
"Type": "3d",
"like": 0
},
{
"id": "3",
"titre": "Majora's Mask",
"plateforme": "N64",
"annee": "2000",
"Type": "3d",
"like": 0
},
{
"id": "4",
"titre": "Tears of the kingdom",
"plateforme": "Switch",
"annee": "2023",
"Type": "3d",
"like": 0
},
{
"id": "5",
"titre": "Echoes of Wisdom",
"plateforme": "Switch",
"annee": "2024",
"Type": "3d",
"like": 0
}
]
@@ -1,7 +1,3 @@
.body {
font-family: cursive;
background-color: black;
}
.counter {
font-size: 16px;
padding: 5px 10px;
@@ -157,14 +153,17 @@
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
.spacer {
height: 48px;
@media (max-width: 1024px) {
height: 48px;
height: 28px;
}
}
.smallgap {
height: 20px;
}
.ticks {
position: relative;
width: 100%;
@@ -186,11 +185,3 @@
border-right-color: var(--border);
}
}
.sombre {
background-color: #1a1a2e;
color: #eaeaea;
}
.sombre .carte {
background-color: #16213e;
color: #eaeaea;
}
+69
View File
@@ -0,0 +1,69 @@
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";
import NotFound from "./pages/NotFound";
import "./App.css";
function App() {
const [jeux, setJeux] = useState(() => {
const stocke = localStorage.getItem("jeux");
return stocke ? JSON.parse(stocke) : null;
});
const [likes, setLikes] = useState(() =>
JSON.parse(localStorage.getItem("likes") || "{}"),
);
useEffect(() => {
if (jeux === null) {
fetch("/zelda.json")
.then((r) => r.json())
.then(setJeux);
}
}, []);
useEffect(() => {
if (jeux !== null) {
localStorage.setItem("jeux", JSON.stringify(jeux));
}
}, [jeux]);
useEffect(() => {
localStorage.setItem("likes", JSON.stringify(likes));
}, [likes]);
function liker(id) {
setLikes((prev) => ({ ...prev, [id]: (prev[id] || 0) + 1 }));
}
if (jeux === null) return <p>Chargement…</p>;
return (
<>
<Header />
<Routes>
<Route
path="/"
element={
<Accueil
jeux={jeux}
setJeux={setJeux}
likes={likes}
onLike={liker}
setLikes={setLikes}
/>
}
/>
<Route path="/jeu/:id" element={<Detail jeux={jeux} likes={likes} />} />
<Route path="*" element={<NotFound />} />
</Routes>
<Footer />
</>
);
}
export default App;
File renamed without changes.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

File renamed without changes.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

+6
View File
@@ -0,0 +1,6 @@
function Badge({ Type }) {
if (Type === "2d") return "Jeu 2D";
return <span className="badge"> Jeu 3D </span>;
}
export default Badge;
@@ -0,0 +1,12 @@
function BarreRecherche({ valeur, onChange }) {
return (
<input
type="search"
placeholder="Search Game…"
value={valeur}
onChange={onChange}
/>
);
}
export default BarreRecherche;
@@ -1,12 +1,17 @@
import Badge from "./Badge";
import { Link } from "react-router-dom";
function Carte(props) {
return (
<div className="carte">
<Link to={`/trilogie/${props.triloId}`}>Voir les détails</Link>
<h2>{props.titre}</h2>
<p>{props.niveaux}</p>
<p>
{props.plateforme} - {props.annee} - <Badge Type={props.Type} />
</p>
<Link to={`/jeu/${props.gameId}`}>Voir les détails</Link>
<button onClick={props.onLike}>❤️ {props.likes}</button>
<button onClick={() => props.onDelete()}>🗑️</button>
<section className="smallgap"></section>
</div>
);
}
@@ -2,10 +2,9 @@ import { useState } from "react";
function Compteur() {
const [nombre, setNombre] = useState(0);
return (
<div>
<p>Véhicules : {nombre}</p>
<p> Vehicules : {nombre}</p>
<button onClick={() => setNombre(nombre + 1)}>+</button>
</div>
);
+9
View File
@@ -0,0 +1,9 @@
function Footer() {
return (
<div id="foot">
<h3>The Zelda project</h3>
</div>
);
}
export default Footer;
Whitespace-only changes.
+14
View File
@@ -0,0 +1,14 @@
function Header() {
return (
<section id="center">
<div>
<h1>The Zelda project</h1>
<p>
<img src="https://www.greenmangaming.com/blog/wp-content/uploads/2021/06/zelda-timeline.jpg"></img>
</p>
</div>
</section>
);
}
export default Header;
File renamed without changes.
File renamed without changes.
+147
View File
@@ -0,0 +1,147 @@
import Carte from "../components/Carte";
import BarreRecherche from "../components/BarreRecherche";
import { useState } from "react";
function Accueil({ jeux, setJeux, likes, setLikes, onLike }) {
const [recherche, setRecherche] = useState("");
const [titre, setTitre] = useState("");
const [annee, setAnnee] = useState("");
const [plateforme, setPlateforme] = useState("");
const [tri, setTri] = useState("titre");
const medailles = ["🥇", "🥈", "🥉"];
const [filtreType, setFiltreType] = useState("tous");
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;
const [ordre, setOrdre] = useState("asc");
function supprimer(id) {
setJeux(jeux.filter((j) => j.id !== id));
setLikes((prev) => {
const copie = { ...prev }; // une COPIE (on ne modifie jamais l'ancien objet)
delete copie[id]; // on retire l'entrée de ce jeu
return copie; // on renvoie le nouvel objet
});
}
const top = [...jeux]
.sort((a, b) => (likes[b.id] || 0) - (likes[a.id] || 0))
.slice(0, 3);
function ajouter(e) {
e.preventDefault();
if (titre.trim() === "") return;
const nouveau = {
id: Date.now(),
titre,
annee: Number(annee),
plateforme,
Type: "3d",
};
setJeux([...jeux, nouveau]);
setTitre("");
setAnnee("");
setPlateforme("");
}
let affiches = [...jeux]
.filter((j) => j.titre.toLowerCase().includes(recherche.toLowerCase()))
.filter((j) => filtreType === "tous" || j.Type === filtreType)
.sort((a, b) => {
if (tri === "titre") return a.titre.localeCompare(b.titre);
if (tri === "annee") return a.annee - b.annee;
return 0;
});
if (ordre === "desc") affiches.reverse();
return (
<>
<div>
<BarreRecherche
valeur={recherche}
onChange={(e) => setRecherche(e.target.value)}
/>
<select value={tri} onChange={(e) => setTri(e.target.value)}>
<option value="titre">Titre (A→Z)</option>
<option value="annee">Année</option>
</select>
<button onClick={() => setFiltreType("tous")}>Tous</button>
<button onClick={() => setFiltreType("2d")}>2D</button>
<button onClick={() => setFiltreType("3d")}>3D</button>
<button onClick={() => setOrdre(ordre === "asc" ? "desc" : "asc")}>
{ordre === "asc" ? "A→Z" : "Z→A"}
</button>
</div>
{affiches.length === 0 && <p>Aucun jeu trouvé 😕</p>}
<section className="spacer"></section>
<div className="hall">
{top.map((j, index) => (
<p key={j.id}>
{medailles[index]} {j.titre} — {likes[j.id] || 0} ❤️
</p>
))}
</div>
<section className="spacer"></section>
{affiches.map((jeu) => (
<Carte
key={jeu.id}
gameId={jeu.id}
titre={jeu.titre}
plateforme={jeu.plateforme}
annee={jeu.annee}
Type={jeu.Type}
likes={likes[jeu.id] || 0}
onLike={() => onLike(jeu.id)}
onDelete={() => supprimer(jeu.id)}
/>
))}
<section className="spacer"></section>
<p>
{nbJeux} jeux · {totalLikes} likes · année moyenne {anneeMoyenne}
</p>
<section className="spacer"></section>
<form onSubmit={ajouter}>
<input
value={titre}
onChange={(e) => setTitre(e.target.value)}
placeholder="Titre du jeu"
/>
<input
value={annee}
onChange={(e) => setAnnee(e.target.value)}
placeholder="Année"
/>
<input
value={plateforme}
onChange={(e) => setPlateforme(e.target.value)}
placeholder="Console"
/>
<button type="submit">Ajouter</button>
<p>
<button
onClick={() => {
localStorage.removeItem("jeux");
location.reload();
}}
>
♻️ Réinitialiser les jeux
</button>
</p>
<section className="spacer"></section>
</form>
</>
);
}
export default Accueil;
+25
View File
@@ -0,0 +1,25 @@
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.plateforme} — {jeu.annee} — {jeu.Type}
</p>
<p>❤️ Likes : {likes[jeu.id] || 0}</p>
</div>
);
}
export default Detail;
+11
View File
@@ -0,0 +1,11 @@
import { Link } from "react-router-dom";
function NotFound() {
return (
<div>
<h1>404 — page introuvable</h1>
<Link to="/">Retour à l'accueil</Link>
</div>
);
}
export default NotFound;
File renamed without changes.
+5
View File
@@ -0,0 +1,5 @@
[
{ "modele": "Renault Kangoo", "immatriculation": "AB-123-CD", "km": 45200 },
{ "modele": "Peugeot Partner", "immatriculation": "EF-456-GH", "km": 78900 },
{ "modele": "Citroën Berlingo", "immatriculation": "IJ-789-KL", "km": 12300 }
]