Author SHA1 Message Date
Stage1 fcc9c70bfd the end. 2026-06-26 16:59:48 +02:00
matthieu ed3df9c653 Services: correction crash Versus (find undefined .nom), Versus a afficher, sync likes a la suppression + nettoyages 2026-06-26 16:26:06 +02:00
Stage1 5dbb785fc4 idk 2026-06-26 16:02:06 +02:00
matthieu 539d94e4e3 Correction boss L: Accueil masque la prop trilogies par un useState local (ajouts non remontes a Details) 2026-06-26 14:40:41 +02:00
Stage1 4e3650c55e boss final 1 2026-06-26 12:03:42 +02:00
Stage1 39cad5771e pages détails 2026-06-26 10:55:10 +02:00
matthieu 7a0fd42192 Ajout miammiam.txt 2026-06-26 10:31:49 +02:00
matthieu e798810283 Enonce final: plus d'exercices (versus, stats, 404, tri inverse, custom hook, edition) + ton allege 2026-06-26 10:02:43 +02:00
matthieu d9c7f81136 Correction page detail + enonce final fun (boss de fin: detail complet, random, podium, dark mode) 2026-06-26 09:32:13 +02:00
Stage1 4b93f510ee debug id 2026-06-25 16:37:48 +02:00
Stage1 506904b009 correction bugs 2026-06-25 16:21:30 +02:00
matthieu 0bae7d1045 Correction services: page d'accueil blanche car il manque le return devant le JSX dans Accueil() 2026-06-25 15:51:50 +02:00
Stage1 8d2ca369fc bug 2026-06-25 15:44:02 +02:00
matthieu 1e80e2c2af Correction services router: retirer la variante (une seule approche: BrowserRouter dans main.jsx), rester decisif 2026-06-25 14:52:13 +02:00
matthieu 45149f6eaf Correction services router: double createRoot dans main.jsx (le 2e ecrase et perd BrowserRouter) -> un seul render StrictMode>BrowserRouter>App; + variante BrowserRouter dans main OU App 2026-06-25 14:50:45 +02:00
Stage1 a1a18776a6 react router debut 2026-06-25 14:46:09 +02:00
Stage1 4a148fa2ff debug tri 2026-06-25 14:18:34 +02:00
matthieu 6bef2f8304 Correction services: le tri n'a aucun effet (affiche filtrees au lieu de affichees) -> une seule liste filtree PUIS triee, afficher affichees 2026-06-25 14:04:27 +02:00
Stage1 a7300cad0a formulaire + début tri 2026-06-25 11:53:43 +02:00
Stage1 37ec87bf9f debug like 2026-06-25 09:55:17 +02:00
matthieu a578a7d22c Services: correction Carte/likes (1 seule approche: likes dans le parent, total recalcule, fix syntaxe) + enonce React #3 (formulaire d'ajout, tri, React Router) 2026-06-25 09:11:46 +02:00
Stage1 0a01b5310c correctio bugs => + de bugs 2026-06-24 16:33:54 +02:00
Stage1 5707f5e2e3 like total 2026-06-24 15:30:30 +02:00
matthieu e28c70d440 Correction services: total de likes recalcule (source de verite = likes par trilogie dans App, total = reduce) au lieu d'un total mute; + fusionner les deux map 2026-06-24 14:47:58 +02:00
Stage1 bd1e9adf13 jsp... 2026-06-24 14:36:43 +02:00
Stage1 6621ced8c5 suite like (bug) 2026-06-24 11:56:59 +02:00
Stage1 336d92db9b début page gd 2026-06-24 11:16:47 +02:00
matthieu b30bae07c2 Enonce React #2 services (Geometry Dash): map/key, conditionnel, etat local, champ controle, lifting state up, useEffect, fetch en React, re-decoupage 2026-06-24 09:21:10 +02:00
Stage1 af94eafc10 theme ? 2026-06-23 17:02:59 +02:00
Stage1 37364af1a2 fin tuto 2026-06-23 16:36:04 +02:00
Stage1 b40a975c9c corrections bugs 2026-06-23 16:12:28 +02:00
Stage1 0d3855a322 correction bugs 2026-06-23 16:12:14 +02:00
matthieu ec4f14be33 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:49:04 +02:00
Stage1 b795340a99 début mdr... 2026-06-23 15:45:58 +02:00
Stage1 db20a998ac TOTO 2026-06-23 15:33:07 +02:00
Stage1 7a04b631a4 wlcome back 2026-06-23 15:31:56 +02:00
Stage1 785c82717f rip 2026-06-23 15:31:00 +02:00
Stage1 98a38b81d3 début projet 2026-06-23 15:21:49 +02:00
Stage1 f8bc1be364 fin 2026-06-23 15:21:36 +02:00
matthieu 39d5e9f00b Tuto React depuis zero: install Node/npm, Vite, structure, JSX, decoupage en composants (1 composant = 1 fichier), props, useState 2026-06-23 14:29:01 +02:00
Stage1 d95bcdd306 json + navbarre update 2026-06-23 12:05:50 +02:00
matthieu e65bf607d8 CORS Guillaume: recommander d'installer Python (Add to PATH) puis python -m http.server 2026-06-23 11:08:39 +02:00
matthieu 472498c56d CORS: adapter a Zed (terminal integre + npx serve/py), retirer Live Server VS Code, titres neutres 2026-06-23 11:07:35 +02:00
matthieu 22a1178bd7 CORS: solutions Windows sans python (Live Server VS Code recommande, py -m http.server, npx serve, IDE JetBrains) 2026-06-23 11:05:15 +02:00
matthieu ba66dfd5b5 Explication CORS enrichie: difference file:// (lecture disque, pas de serveur) vs http://localhost:8000 (mini-serveur, vraie origine), localhost/port 2026-06-23 11:01:50 +02:00
matthieu 4be75bfb86 Correction services CORS: ouvrir via http (python http.server) au lieu de file://, chemin fetch css/vehicules.json, tbody corps_flotte manquant 2026-06-23 10:59:03 +02:00
Stage1 eeb0e37e25 début json lol 2026-06-23 10:44:51 +02:00
Stage1 126d5adea2 mode sombre + horloge 2026-06-23 09:59:20 +02:00
Stage1 c3ab560c47 corrections/améliorations 2026-06-23 09:34:41 +02:00
matthieu 5ad4bded37 Ameliorations services: une seule fonction d'affichage, singulier/pluriel au chargement, classe virgule; rappel suite (exos 8-11) 2026-06-23 09:25:02 +02:00
Stage1 19e0b777ca correction bugs 2026-06-22 16:47:56 +02:00
Stage1 20a5020bc5 tet 2026-06-22 16:39:16 +02:00
matthieu 8230a3b80b Correction services localStorage: affichage non rafraichi au chargement, piege texte/nombre (parseInt), point-virgule manquant 2026-06-22 16:23:00 +02:00
Stage1 5582234947 début localstorage 2026-06-22 16:18:48 +02:00
matthieu 54fe8935a2 Correction services: classes HTML (espace vs virgule), menage JS (code mort/duplique), coherence message alerte 2026-06-22 15:41:36 +02:00
Stage1 0075fd6c1c table js 2026-06-22 15:22:47 +02:00
Stage1 cad8295d1c update formulaire 2026-06-22 14:48:21 +02:00
matthieuandClaude Opus 4.8 3586e8fcad Explication form: role du form, name, action (OU) et method GET/POST (exemple suggestions/POST)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:34:12 +02:00
Stage1 e6216b36b2 corrections + update js formulaire 2026-06-22 11:46:32 +02:00
matthieuandClaude Opus 4.8 6c01a550d0 Solutions services (filet de securite, a consulter en dernier recours) pour le rattrapage JS
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:42:34 +02:00
matthieuandClaude Opus 4.8 ceceb1865f Rattrapage JS services (mode indices, sans solution): conditions, tableaux/boucles, filter, localStorage, classList, setInterval, fetch, composant navbar
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 09:24:49 +02:00
Stage1 d7b5df74be simulateur de devis 2026-06-18 15:09:33 +02:00
Stage1 bb2b789d3f simulateur de devis 2026-06-18 15:09:14 +02:00
Stage1 397272429d corrections 2026-06-18 12:05:02 +02:00
matthieuandClaude Opus 4.8 5314db52a0 Guide services: relecture form, redimensionnement borne textarea (min/max), icone bouton Envoyer
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 11:44:33 +02:00
Stage1 be6c852045 update style formulaire 2026-06-18 10:55:11 +02:00
Stage1 d14cce5731 update formulaire + footer 2026-06-18 10:22:29 +02:00
Stage1 8910b77294 début formulaire 2026-06-17 16:54:23 +02:00
Stage1 f630bbf649 add style table 2026-06-17 16:04:44 +02:00
Stage1 2aade91105 update table + nav barre 2026-06-17 16:03:56 +02:00
Stage1 bebc535cae new style for the same page 2026-06-17 15:07:13 +02:00
65 changed files with 6875 additions and 2456 deletions
+82
View File
@@ -0,0 +1,82 @@
# 👍 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)
+75
View File
@@ -0,0 +1,75 @@
# 🔧 Correction — page d'accueil blanche : il manque le `return`
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.
---
## 🔍 Le bug : ton composant ne **renvoie** rien
Regarde la fin de `Accueil` :
```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.
---
## ✅ La correction : ajouter `return (` … `)`
```jsx
function Accueil() {
// ... tes useState, ajouter, liker, total ...
return ( // 👈 AJOUTE "return ("
<div className="body">
<Header />
<select value={tri} onChange={(e) => setTri(e.target.value)}>
<option value="nom">Nom (AZ)</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)} />
))}
<Footer />
</div>
); // 👈 ET la parenthèse fermante
}
```
**✅ Test :** va sur `/` → ta page d'accueil (cartes + recherche + tri + formulaire + total) **s'affiche** enfin.
---
## 🧹 Pendant que tu y es (optionnel)
- 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.
---
## ✅ 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
> 💬 La phrase à garder : **pas de `return`, pas d'affichage.** Un composant qui « calcule » du JSX sans le `return`er ne montre rien.
-72
View File
@@ -1,72 +0,0 @@
# 🔧 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.
+53
View File
@@ -0,0 +1,53 @@
# 🔧 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
@@ -0,0 +1,115 @@
# 🔧 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
@@ -0,0 +1,98 @@
# 🔧 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
@@ -0,0 +1,109 @@
# 🔧 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
@@ -1,119 +0,0 @@
# 🔧 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
@@ -1,80 +0,0 @@
# 🔧 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. 🙂
+91
View File
@@ -0,0 +1,91 @@
# 🔧 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.
+98
View File
@@ -0,0 +1,98 @@
# 🔎 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. 🎉
Mais ta page `Details.jsx` ressemble encore à ça :
```jsx
function Details() {
const { id } = useParams();
return <h1>Détails de la trilogie {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…
---
## 🧠 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`. 😬
`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.
> 💡 **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.
Il y a 2 niveaux de solution. Prends la **simple** d'abord, garde la **pro** pour l'énoncé final.
---
## ✅ Solution simple — sortir les données dans un fichier à part
Tes 5 trilogies de départ sont fixes. On les sort de `Accueil` dans un fichier que **les deux pages** peuvent importer.
### 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";
function Details() {
const { id } = useParams();
// ⚠️ 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));
if (!trilogie) return <p>Trilogie 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>
</div>
);
}
export default Details;
```
> 🐛 **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 !
**✅ 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.
---
## ⚠️ La limite 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.
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 ». 😈
---
## 🧹 Pendant qu'on y est, 3 petits nettoyages dans `Accueil.jsx`
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>`.
> 💡 **À 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.
-81
View File
@@ -1,81 +0,0 @@
# 🔧 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.
+89
View File
@@ -0,0 +1,89 @@
# 🔧 Correction — React Router : pourquoi ça plante
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. 👇
---
## 🐛 Le bug : tu rends ton app **deux fois** dans `main.jsx`
```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>,
);
```
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` :
```jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App.jsx";
createRoot(document.getElementById("root")).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
);
```
> 🔎 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`.
---
## 🔎 Vérifie aussi
Ton `App.jsx` est bon :
```jsx
<Routes>
<Route path="/" element={<Accueil />} />
<Route path="/trilogie/:id" element={<Details />} />
</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`.
Et dans `Carte`, pour aller vers le détail, utilise un `<Link>` (pas un `<a href>`) :
```jsx
import { Link } from "react-router-dom";
<Link to={`/trilogie/${props.id}`}>Voir les détails</Link>
```
(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
> 💬 À garder : `BrowserRouter` (dans `main`) = « j'allume le routeur » ; `Routes`/`Route` (dans `App`) = « voici quelle page pour quelle URL ». Et **un seul** `render`.
+101
View File
@@ -0,0 +1,101 @@
# 🔧 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).
+81
View File
@@ -0,0 +1,81 @@
# 🔧 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.
+109
View File
@@ -0,0 +1,109 @@
# 🔧 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.
+305
View File
@@ -0,0 +1,305 @@
# 🏁 É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.
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* | ⭐⭐⭐ |
| M | Bouton « au hasard » (`useNavigate`) | ⭐⭐ |
| N | Le podium des trilogies les plus likées | ⭐⭐ |
| O | Bouton « supprimer » une trilogie | ⭐⭐ |
| 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 | ⭐⭐⭐ |
---
## 🔑 Module L — Le boss : faire suivre TOUT jusqu'au détail (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.
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.1 — Remonter le state dans `App.jsx`
```jsx
import { useState } from "react";
import { Routes, Route } from "react-router-dom";
import { donneesInitiales } from "./data";
import Accueil from "./pages/Accueil";
import Details from "./pages/Details";
function App() {
const [trilogies, setTrilogies] = useState(donneesInitiales);
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>
);
}
```
> 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.
### 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}))
}
```
### L.3 — `Details` affiche la vraie trilogie avec ses likes
```jsx
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>
);
}
```
**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 ».
> 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.
---
## 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>`).
```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}`);
}
// <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.
---
## Module N — Le podium
En haut de l'accueil, le Top 3 des trilogies les plus likées, avec des médailles.
```jsx
const medailles = ["🥇", "🥈", "🥉"];
const podium = [...trilogies]
.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>
```
> `.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.
**Test :** like beaucoup une trilogie → elle grimpe sur le podium en direct.
---
## Module O — Supprimer une trilogie
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));
}
// sur la Carte : <button onClick={() => props.onDelete()}>🗑️</button>
// et dans Accueil : onDelete={() => supprimer(trilo.id)}
```
> `filter` renvoie un **nouveau** tableau contenant tout sauf l'élément visé. On ne touche jamais l'ancien (immutabilité, comme pour l'ajout).
**Test :** clique 🗑️ → la carte disparaît, les autres restent.
---
## Module P — Thème sombre (qui se souvient)
Un bouton qui bascule la page en sombre et garde le choix après un refresh (`localStorage`, comme tes likes).
```jsx
const [sombre, setSombre] = useState(
() => localStorage.getItem("theme") === "sombre"
);
useEffect(() => {
localStorage.setItem("theme", sombre ? "sombre" : "clair");
}, [sombre]);
// <div className={sombre ? "body sombre" : "body"}>
// <button onClick={() => setSombre(!sombre)}>{sombre ? "☀️" : "🌙"}</button>
```
```css
.sombre { background-color: #1a1a2e; color: #eaeaea; }
.sombre .carte { background-color: #16213e; color: #eaeaea; }
```
> Tu fais piloter du CSS par du state : `className` change selon `sombre`, le navigateur applique les bonnes règles. C'est comme ça que marchent tous les dark modes.
**Test :** passe en sombre → recharge (F5) → c'est toujours sombre.
---
## Module Q — Mode Versus
Deux `<select>` pour choisir deux trilogies ; on compare leurs likes et on affiche la gagnante.
```jsx
const [a, setA] = useState(trilogies[0].id);
const [b, setB] = useState(trilogies[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);
```
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.
> Pense à `Number(...)` quand tu relis la valeur d'un `<select>` : elle revient en chaîne.
**Test :** change les deux menus → le verdict se met à jour tout seul.
---
## Module R — Statistiques
Une petite barre de stats calculée à partir des données. On muscle `reduce`.
```jsx
const nbTrilogies = trilogies.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>
```
> `reduce` réduit un tableau à **une seule** valeur en accumulant. Ici on additionne tous les likes.
**Test :** ajoute/like des trilogies → les chiffres bougent.
---
## Module S — 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.
```jsx
import { Link } from "react-router-dom";
function NotFound() {
return <div><h1>404 page introuvable</h1><Link to="/">Retour à l'accueil</Link></div>;
}
// dans <Routes>, en DERNIER :
// <Route path="*" element={<NotFound />} />
```
> `path="*"` = « toutes les URL non reconnues ». À mettre **en dernier**, sinon il attrape tout avant les autres.
**Test :** tape une URL bidon dans la barre → la page 404 s'affiche.
---
## Module T — 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();
// <button onClick={() => setOrdre(ordre === "asc" ? "desc" : "asc")}>
// {ordre === "asc" ? "A→Z" : "Z→A"}
// </button>
```
**Test :** clique le bouton → l'ordre des cartes s'inverse.
---
## Module U — Custom hook `useLocalStorage` (refacto)
Tu répètes le même schéma `useState` + `useEffect(localStorage…)` pour les likes et le thème. On range ça dans **un hook réutilisable**.
```jsx
// src/useLocalStorage.js
import { useState, useEffect } from "react";
export function useLocalStorage(cle, valeurParDefaut) {
const [valeur, setValeur] = useState(() => {
const stockee = localStorage.getItem(cle);
return stockee !== null ? JSON.parse(stockee) : valeurParDefaut;
});
useEffect(() => {
localStorage.setItem(cle, JSON.stringify(valeur));
}, [cle, valeur]);
return [valeur, setValeur];
}
```
Puis : `const [likes, setLikes] = useLocalStorage("likes", {});` — et les likes survivent au refresh, gratuitement.
> 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.
---
## ✅ 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
## 🧠 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**.
Bravo pour ces deux semaines. Montre-moi tes modules battus quand tu en tiens un.
+164
View File
@@ -0,0 +1,164 @@
# 🎮 É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 (AZ)</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.
+307
View File
@@ -0,0 +1,307 @@
# 🎮 É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.
+17 -20
View File
@@ -1,6 +1,6 @@
# 📨 À quoi sert vraiment une balise `<form>` ?
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`**.
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`**.
---
@@ -17,24 +17,22 @@ 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="prenom" ...>
> <input type="email" name="email" ...>
> <textarea name="contacter"></textarea>
> <input type="text" name="user_name" ...>
> <input type="email" name="user_mail" ...>
> <textarea name="user_message"></textarea>
> ```
> → au moment de l'envoi, le navigateur construit quelque chose comme :
> `prenom = Mélissa`, `email = melissa@mail.com`, `contacter = ...`
> `user_name = Dupond`, `user_mail = dupond@mail.com`, `user_message = ...`
> 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"` (un `<button>` sans `type` précisé compte comme submit par défaut).
> ⚠️ 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 »).
---
## 2. L'attribut `action` : **OÙ** vont les données
```html
<form method="get" action="">
<form action="" method="post">
```
`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.
@@ -42,7 +40,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 : 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.
> 💡 **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…).
---
@@ -50,36 +48,35 @@ 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"` (ce que tu utilises)
### `method="get"`
Les données sont **collées dans l'URL**, après un point d'interrogation :
```
contact.html?prenom=Mélissa&email=melissa@mail.com&contacter=Bonjour...
contact.html?user_name=Dupond&user_mail=dupond@mail.com
```
- ✅ 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"`
### `method="post"` (ce que tu utilises)
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…
- 👉 Idéal pour **envoyer / créer / modifier** quelque chose : un message, une inscription, une suggestion
> 💡 **La règle simple :**
> - **GET** = « je **demande** des infos » (recherche, filtre, consultation).
> - **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. 🙂
> - **POST** = « j'**envoie / je modifie** des infos » (formulaire de contact, suggestion, inscription).
> Pour ton formulaire de **suggestions**, `method="post"` est donc le **bon choix**. 👍
---
## 🔬 Une petite expérience pour bien voir la différence
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 ».
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 ».
Ç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
@@ -1,620 +0,0 @@
# 📬 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 (9h18h)";
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
@@ -1,456 +0,0 @@
# 🚀 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
@@ -1,239 +0,0 @@
# 🚀 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
@@ -1,144 +0,0 @@
# 🧭 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
@@ -0,0 +1,416 @@
# 🛠️ 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
@@ -0,0 +1,200 @@
# 🧗 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
@@ -1,165 +0,0 @@
# ✅ Solutions — Niveau 5 contact (`PROCHAINE_ETAPE_contact_4.md`)
> 🛑 **À NE REGARDER QU'EN DERNIER RECOURS.**
> Cherche d'abord par toi-même (au moins 1015 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
@@ -0,0 +1,263 @@
# ✅ Solutions — rattrapage services (`PROCHAINE_ETAPE_services_2.md`)
> 🛑 **À NE REGARDER QU'EN DERNIER RECOURS.**
> Cherche d'abord par toi-même (au moins 1015 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.
+349
View File
@@ -0,0 +1,349 @@
# ⚛️ Tuto — créer ton premier projet React (depuis zéro)
Salut ! 👋 On passe à une nouvelle étape : faire un vrai **petit projet React**, dans un **sous-dossier de ce repo**. Le thème est **libre** (carte blanche !). Ce tuto part **de tout début** : on installe les outils, on explique **à quoi chacun sert**, puis on construit un mini-projet **bien rangé** (c'est le point le plus important : on découpe en plusieurs fichiers, pas un gros fourre-tout).
⏱️ Compte ~30-45 min la première fois. Avance étape par étape, ne saute rien.
---
## 0. Les outils et à quoi ils servent (lis avant d'installer)
Avant de taper des commandes « magiques », comprends **qui fait quoi** :
| Outil | C'est quoi | À quoi ça sert |
|---|---|---|
| **Node.js** | un programme qui fait tourner du **JavaScript en dehors du navigateur** (directement sur ta machine) | tous les outils de dev modernes tournent dessus. Sans Node, rien ne s'installe |
| **npm** | le **gestionnaire de paquets** de Node (installé **avec** Node) | télécharger/installer des bibliothèques (comme React) et lancer des commandes du projet |
| **Vite** | un **outil de démarrage de projet** + un **serveur de dev** ultra-rapide | crée le squelette du projet et l'affiche dans le navigateur, avec **rechargement automatique** quand tu modifies un fichier |
| **React** | une **bibliothèque** pour construire l'interface avec des **composants** réutilisables | écrire l'UI comme un assemblage de petits blocs (un bouton, une carte, une barre de nav…) |
> 💡 **Bonne nouvelle :** React, c'est l'idée que tu as **déjà** pratiquée à la main — ton `creerNavbar()` (une fonction qui renvoie du HTML réutilisable), c'était **un composant** avant l'heure. React, c'est ça, en mieux organisé.
---
## 1. Installer Node.js
React a besoin de Node. On l'installe une fois pour toutes.
### Windows
1. Va sur **https://nodejs.org** et télécharge la version **« LTS »** (la stable).
2. Lance l'installateur, **Suivant → Suivant** (laisse les options par défaut, elles ajoutent Node au PATH).
### Mac
1. Même site **https://nodejs.org**, version **« LTS »**, télécharge le `.pkg`.
2. Ouvre-le et suis l'installation.
### Vérifier que c'est bon (les deux)
Ouvre un **terminal** (sur Zed : `` Ctrl + ` `` ; sinon le Terminal de ton OS) et tape :
```bash
node -v
npm -v
```
Tu dois voir deux numéros de version (ex. `v20.11.0` et `10.2.4`). Si oui → Node **et** npm sont là. 🎉
Si « commande introuvable » : **ferme et rouvre** le terminal (parfois il faut le relancer après l'install).
> 🔎 `npm` s'installe **automatiquement avec** Node — tu n'as rien à faire de plus.
---
## 2. Créer le projet React (dans un sous-dossier du repo)
Dans le terminal, place-toi à la **racine de ce repo** (le dossier `stagiaire`). Puis lance :
```bash
npm create vite@latest
```
Vite va te poser **3 questions** :
1. **Project name** → donne un nom de dossier, ex. `mon-projet` (ce sera le sous-dossier créé ici).
2. **Select a framework** → choisis **React**.
3. **Select a variant** → choisis **JavaScript** (on garde simple ; pas TypeScript pour l'instant).
> 🔎 `npm create vite@latest` = « télécharge l'outil Vite et crée un nouveau projet ». Le `@latest` veut dire « la dernière version ».
Ça crée un dossier `mon-projet/` avec tout le squelette dedans.
---
## 3. Installer les dépendances
Entre dans le dossier et installe les bibliothèques :
```bash
cd mon-projet
npm install
```
- `cd mon-projet` = « entre dans le dossier du projet ».
- `npm install` = lit le fichier **`package.json`** (la liste des bibliothèques nécessaires) et les **télécharge** dans un dossier **`node_modules/`**.
> ⚠️ **`node_modules/` est énorme et ne se commite JAMAIS sur Git.** Vite a déjà créé un fichier `.gitignore` dans le projet qui l'ignore — laisse-le. (Si un jour tu vois des milliers de fichiers à committer, c'est que `node_modules` n'est pas ignoré : stop, on regarde ensemble.)
> 💡 **À retenir :** `package.json` = la **recette** (quelles bibliothèques, quelles commandes). `node_modules/` = les **ingrédients téléchargés**. On partage la recette (commitée), pas les ingrédients (ré-installables avec `npm install`).
---
## 4. Lancer le projet
```bash
npm run dev
```
Vite démarre un **serveur de dev** et t'affiche une adresse, du genre :
```
http://localhost:5173/
```
Ouvre-la dans le navigateur → tu vois la page React de démarrage. 🎉
> 🔎 Ça te rappelle quelque chose ? C'est le **même principe** que `localhost` qu'on a vu pour `fetch` : un petit serveur qui sert ta page en `http://`. Ici en plus, **dès que tu enregistres un fichier, la page se met à jour toute seule** (« hot reload »).
Pour **arrêter** le serveur : `Ctrl + C` dans le terminal.
---
## 5. Le tour du propriétaire (la structure des fichiers)
Voici l'essentiel de ce que Vite a créé :
```
mon-projet/
├── index.html ← la page de base (tu n'y touches presque jamais)
├── package.json ← la recette (bibliothèques + commandes)
├── .gitignore ← liste ce que Git doit ignorer (dont node_modules)
├── node_modules/ ← les bibliothèques (ignoré par Git)
├── public/ ← images/fichiers statiques
└── src/ ← 👈 TON code vit ici
├── main.jsx ← le point d'entrée : branche React sur la page
├── App.jsx ← le composant principal (ton appli)
└── App.css ← les styles
```
> 💡 **À retenir :** tu travailles **presque uniquement dans `src/`**. Et le fichier le plus important pour toi, c'est **`App.jsx`**.
---
## 6. JSX : du HTML dans le JavaScript
Ouvre `src/App.jsx`. Tu vois une fonction qui **renvoie quelque chose qui ressemble à du HTML** :
```jsx
function App() {
return (
<div>
<h1>Mon appli</h1>
</div>
);
}
```
Ce « HTML dans le JS » s'appelle du **JSX**. C'est exactement l'idée de ton `creerNavbar()` (une fonction qui renvoie du HTML), mais en plus naturel : ici React s'occupe de l'afficher.
**Essaie tout de suite :** remplace le `<h1>` par ton titre, **enregistre**, et regarde le navigateur changer **tout seul**. C'est ça, le hot reload.
> 🔎 Deux petites différences avec le HTML normal : en JSX on écrit `className` au lieu de `class`, et chaque composant doit renvoyer **un seul** élément parent (souvent un `<div>` qui enveloppe le reste).
---
## 7. ⭐ LE point clé : découper en composants (1 composant = 1 fichier)
C'est **la** chose à bien faire dès le départ : **ne pas tout entasser dans `App.jsx`**. On découpe l'interface en **petits composants**, **chacun dans son propre fichier**. `App.jsx` ne fait alors qu'**assembler** ces blocs.
### ❌ Le piège à éviter (le fourre-tout)
```jsx
// App.jsx qui grossit, grossit... tout dedans : header + cartes + formulaire + footer...
function App() {
return (
<div>
{/* 200 lignes de tout et n'importe quoi */}
</div>
);
}
```
C'est exactement le « fourre-tout » qu'on veut **éviter** : illisible, impossible à réutiliser, et on s'y perd.
### ✅ La bonne organisation
Crée un dossier **`src/components/`**, et **un fichier par composant** :
```
src/
├── App.jsx ← assemble les composants
├── main.jsx
└── components/
├── Header.jsx ← un composant = un fichier
├── Carte.jsx
└── Footer.jsx
```
**Un composant** (`src/components/Header.jsx`) :
```jsx
function Header() {
return (
<header>
<h1>FleetZen</h1>
</header>
);
}
export default Header; // 👈 on l'EXPORTE pour pouvoir l'utiliser ailleurs
```
**App.jsx qui assemble** (et reste court et lisible) :
```jsx
import Header from "./components/Header"; // 👈 on IMPORTE le composant
import Footer from "./components/Footer";
function App() {
return (
<div>
<Header />
{/* ... le reste ... */}
<Footer />
</div>
);
}
export default App;
```
> 💡 **La règle d'or :** **1 composant = 1 fichier**, rangé dans `components/`. `App.jsx` ne fait qu'**assembler** les blocs (`<Header />`, `<Footer />`…). Un composant utilisé = `<MonComposant />` (avec une **majuscule** au début, c'est obligatoire en React).
>
> 🔎 Tu reconnais le `export` / `import` ? C'est exactement les **modules** — l'idée « j'écris un truc dans un fichier, je le réutilise dans un autre ». React s'appuie là-dessus en permanence.
### ⚠️ « J'ai bien créé mes composants, mais `App` n'affiche NI le Header NI le Footer ! »
C'est **le** piège n°1 du débutant en React, et il est sournois car **il n'y a aucune erreur** dans la console. La cause la plus fréquente : la **majuscule**.
En JSX, le navigateur distingue deux choses **uniquement par la première lettre** :
| Tu écris | React comprend | Résultat |
|---|---|---|
| `<header />` (minuscule) | la **balise HTML** native `<header>` | un `<header>` **vide** → rien de visible |
| `<Header />` (Majuscule) | **TON composant** `Header` | ton vrai contenu s'affiche ✅ |
Et là est le piège : `header` et `footer` **existent pour de vrai** en HTML ! Donc si tu écris :
```jsx
function App() {
return (
<div>
<header /> {/* ❌ React rend une balise HTML <header> VIDE, pas ton composant */}
<footer /> {/* ❌ pareil : balise vide */}
</div>
);
}
```
…React ne se plaint pas (ce sont des balises HTML valides), mais il affiche des éléments **vides** → tu ne vois ni ton Header ni ton Footer. **Aucune erreur**, juste du vide. C'est exactement ton symptôme.
### ✅ La correction : une **majuscule** au début
```jsx
import Header from "./components/Header";
import Footer from "./components/Footer";
function App() {
return (
<div>
<Header /> {/* ✅ Majuscule → React utilise TON composant */}
<Footer />
</div>
);
}
```
> 💡 **À retenir :** en React, **un composant s'écrit toujours avec une Majuscule** (`<Header />`), justement pour que React le distingue d'une balise HTML (`<header />`). Minuscule = balise HTML ; Majuscule = ton composant. C'est **obligatoire**, pas une convention de style.
### Les 2 autres causes possibles (si ce n'est pas la majuscule)
1. **Oubli de l'`import`** en haut de `App.jsx` → là, tu auras une **erreur** (`Header is not defined`). Vérifie que chaque composant utilisé est bien importé.
2. **Oubli de l'`export default`** dans le fichier du composant → l'import ramène « rien » → erreur React `Element type is invalid`. Vérifie que chaque fichier de composant finit par `export default MonComposant;`.
> 🔎 Réflexe de débogage : ouvre la **Console (F12)**. **Erreur rouge** → c'est l'import ou l'export (causes 1/2). **Aucune erreur mais du vide** → c'est presque toujours la **majuscule**.
---
## 8. Rendre un composant configurable : les **props**
Un composant devient vraiment utile quand on peut lui passer des **données** (comme un argument de fonction). Ça s'appelle les **props**.
`src/components/Carte.jsx` :
```jsx
function Carte(props) {
return (
<div className="carte">
<h2>{props.titre}</h2>
<p>{props.texte}</p>
</div>
);
}
export default Carte;
```
Et dans `App.jsx`, on **réutilise** le même composant avec des contenus différents :
```jsx
<Carte titre="Géolocalisation" texte="Suivi en temps réel" />
<Carte titre="Consommation" texte="Carburant consommé" />
```
> 💡 **À retenir :** les **props**, ce sont les « réglages » qu'on passe à un composant (comme les paramètres d'une fonction — souviens-toi de `creerNavbar(pageActive)`). Un composant + des props = réutilisable à l'infini.
---
## 9. Un composant qui réagit : `useState` (l'interactivité)
Pour qu'un composant **change** quand on clique, React utilise un **état** (`state`). Exemple d'un compteur — tu vas reconnaître ton simulateur / ton compteur de caractères :
`src/components/Compteur.jsx` :
```jsx
import { useState } from "react";
function Compteur() {
const [nombre, setNombre] = useState(0); // valeur de départ : 0
return (
<div>
<p>Véhicules : {nombre}</p>
<button onClick={() => setNombre(nombre + 1)}>+</button>
</div>
);
}
export default Compteur;
```
> 💡 **L'idée :** `useState` crée une donnée **suivie par React**. Quand tu la changes avec `setNombre(...)`, React **réaffiche tout seul** le composant — plus besoin de `textContent` à la main comme avant ! C'est tout l'intérêt de React : tu décris **ce qu'on voit selon l'état**, et il s'occupe de l'affichage.
---
## 10. Et Git dans tout ça ?
- Tu peux travailler sur **ta branche habituelle** (`feature/...`), le projet React vit dans son sous-dossier.
- Tu commites **ton code** (`src/`, `package.json`, etc.) **mais pas `node_modules/`** (déjà ignoré).
- Si quelqu'un récupère ton projet, il fait juste `npm install` pour recréer `node_modules`.
---
## 🎯 À toi de jouer (carte blanche)
Thème **libre** : un mini-site sur ce que tu veux (jeux, films, recettes, sport, ta page FleetZen en React…). L'important n'est pas le sujet, mais que ce soit **bien découpé** :
- [ ] Node installé (`node -v` répond)
- [ ] Projet créé avec Vite + lancé (`npm run dev`)
- [ ] Au moins **3 composants**, **chacun dans son fichier**, dans `src/components/`
- [ ] `App.jsx` qui **assemble** (court et lisible, pas de fourre-tout)
- [ ] Un composant **réutilisé avec des props** différentes
- [ ] Un composant **interactif** avec `useState`
---
## 🧰 Récap des outils
| | Rôle | Commande clé |
|---|---|---|
| **Node.js** | faire tourner les outils JS | (installé une fois) |
| **npm** | installer les bibliothèques | `npm install` |
| **Vite** | créer + servir le projet | `npm create vite@latest`, `npm run dev` |
| **React** | construire l'UI en composants | `import` / `export`, `<Composant />` |
> 🧭 La phrase à garder : **un composant = un fichier ; `App.jsx` assemble.** Si un fichier commence à tout faire, c'est le signal de le **découper**. Mieux vaut 5 petits fichiers clairs qu'un gros illisible.
Bloqué ? Note **ce que tu as tapé**, **le message d'erreur exact** (terminal **ou** console F12), et on débloque.
-94
View File
@@ -1,94 +0,0 @@
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 (9h18h)";
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) {
if (zoneMessage.value.trim() === "") {
evenement.preventDefault();
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();
}
if (event.key === "Escape") {
zoneMessage.value = "";
rafraichirCompteur();
}
});
+47 -3
View File
@@ -42,7 +42,6 @@ h1 {
position: fixed;
bottom: 0;
width: 100%;
border-top: 2px solid;
}
#nav_barre {
display: flex;
@@ -59,11 +58,10 @@ 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 slategray;
text-shadow: 1px 1px 1px black;
border-radius: 10px;
background-color: royalblue;
background-image: linear-gradient(
@@ -83,7 +81,53 @@ 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%;
-57
View File
@@ -1,57 +0,0 @@
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);
});
-21
View File
@@ -1,21 +0,0 @@
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();
+1
View File
@@ -0,0 +1 @@
https://pietrafocirestaurant.com/wp-content/uploads/2025/10/PF2025SepUnique.pdf
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+16
View File
@@ -0,0 +1,16 @@
# React + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
+21
View File
@@ -0,0 +1,21 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{js,jsx}'],
extends: [
js.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
globals: globals.browser,
parserOptions: { ecmaFeatures: { jsx: true } },
},
},
])
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<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>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+2515
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
{
"name": "mon-projet",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router-dom": "^7.18.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
"eslint": "^10.5.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3",
"globals": "^17.6.0",
"vite": "^8.1.0"
}
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

+24
View File
@@ -0,0 +1,24 @@
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="bluesky-icon" viewBox="0 0 16 17">
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
</symbol>
<symbol id="discord-icon" viewBox="0 0 20 19">
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
</symbol>
<symbol id="documentation-icon" viewBox="0 0 21 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
</symbol>
<symbol id="github-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
</symbol>
<symbol id="social-icon" viewBox="0 0 20 20">
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
</symbol>
<symbol id="x-icon" viewBox="0 0 19 19">
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
</symbol>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

+196
View File
@@ -0,0 +1,196 @@
.body {
font-family: cursive;
background-color: black;
}
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg)
rotateY(39deg) scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg)
rotateY(39deg) scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: "";
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}
.sombre {
background-color: #1a1a2e;
color: #eaeaea;
}
.sombre .carte {
background-color: #16213e;
color: #eaeaea;
}
+30
View File
@@ -0,0 +1,30 @@
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;
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

+14
View File
@@ -0,0 +1,14 @@
import { useState } from "react";
function Compteur() {
const [nombre, setNombre] = useState(0);
return (
<div>
<p>Véhicules : {nombre}</p>
<button onClick={() => setNombre(nombre + 1)}>+</button>
</div>
);
}
export default Compteur;
+14
View File
@@ -0,0 +1,14 @@
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>
<button onClick={props.onLike}> {props.likes}</button>
<button onClick={() => props.onDelete()}>🗑</button>
</div>
);
}
export default Carte;
+9
View File
@@ -0,0 +1,9 @@
function Footer() {
return (
<footer>
<p>Footer toto</p>
</footer>
);
}
export default Footer;
+31
View File
@@ -0,0 +1,31 @@
function Header() {
return (
<header>
<h1>Les Trilogies les plus Connues de Geometry Dash</h1>
<p>
Geometry dash est un jeu fascinant. Même si cest 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 lavez compris, ce jeu ne se résume pas à un
simple cube qui saute, et finir le plus dur des niveaux officiels,
deadlocked, nest pas à la portée de tous les joueurs. Cependant, finir
deadlocked et tous les niveaux officiels nest 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, lobjectif 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
@@ -0,0 +1,23 @@
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",
},
];
+111
View File
@@ -0,0 +1,111 @@
:root {
--text: #6b6375;
--text-h: #08060d;
--bg: #fff;
--border: #e5e4e7;
--code-bg: #f4f3ec;
--accent: #aa3bff;
--accent-bg: rgba(170, 59, 255, 0.1);
--accent-border: rgba(170, 59, 255, 0.5);
--social-bg: rgba(244, 243, 236, 0.5);
--shadow:
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, Consolas, monospace;
font: 18px/145% var(--sans);
letter-spacing: 0.18px;
color-scheme: light dark;
color: var(--text);
background: var(--bg);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
@media (max-width: 1024px) {
font-size: 16px;
}
}
@media (prefers-color-scheme: dark) {
:root {
--text: #9ca3af;
--text-h: #f3f4f6;
--bg: #16171d;
--border: #2e303a;
--code-bg: #1f2028;
--accent: #c084fc;
--accent-bg: rgba(192, 132, 252, 0.15);
--accent-border: rgba(192, 132, 252, 0.5);
--social-bg: rgba(47, 48, 58, 0.5);
--shadow:
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
}
#social .button-icon {
filter: invert(1) brightness(2);
}
}
body {
margin: 0;
}
#root {
width: 1126px;
max-width: 100%;
margin: 0 auto;
text-align: center;
border-inline: 1px solid var(--border);
min-height: 100svh;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
h1,
h2 {
font-family: var(--heading);
font-weight: 500;
color: var(--text-h);
}
h1 {
font-size: 56px;
letter-spacing: -1.68px;
margin: 32px 0;
@media (max-width: 1024px) {
font-size: 36px;
margin: 20px 0;
}
}
h2 {
font-size: 24px;
line-height: 118%;
letter-spacing: -0.24px;
margin: 0 0 8px;
@media (max-width: 1024px) {
font-size: 20px;
}
}
p {
margin: 0;
}
code,
.counter {
font-family: var(--mono);
display: inline-flex;
border-radius: 4px;
color: var(--text-h);
}
code {
font-size: 15px;
line-height: 135%;
padding: 4px 8px;
background: var(--code-bg);
}
+13
View File
@@ -0,0 +1,13 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App.jsx";
createRoot(document.getElementById("root")).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
);
+132
View File
@@ -0,0 +1,132 @@
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 (AZ)</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
@@ -0,0 +1,25 @@
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;
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})
+6
View File
@@ -0,0 +1,6 @@
{
"name": "stagiaire",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}
-193
View File
@@ -1,193 +0,0 @@
* {
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
@@ -1,87 +0,0 @@
<!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
@@ -0,0 +1,131 @@
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
@@ -0,0 +1,241 @@
* {
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
@@ -0,0 +1,7 @@
[
{
"modele": 5,
"immatriculation": 3,
"km": 4
}
]
+71
View File
@@ -0,0 +1,71 @@
<!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>
-5
View File
@@ -1,5 +0,0 @@
[
{ "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 }
]