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
50 changed files with 6949 additions and 0 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.
+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.
+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.
+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.
+92
View File
@@ -0,0 +1,92 @@
# 📨 À quoi sert vraiment une balise `<form>` ?
Tu as un `<form>` autour de ton formulaire de suggestions, mais ce qu'il fait « en coulisses » n'est pas forcément clair. Ce fichier explique **à quoi il sert**, puis les deux attributs qui prêtent à confusion : **`action`** et **`method`**.
---
## 1. Le rôle du `<form>` : regrouper et **envoyer** des champs
Un `<form>` est une **enveloppe** autour de plusieurs champs (`<input>`, `<textarea>`…). Son but : permettre à l'utilisateur de **tout envoyer d'un coup**, en un seul geste (le bouton « Envoyer »).
Sans `<form>`, tes champs s'affichent quand même… mais ce ne sont que des cases isolées : il n'y a aucune notion d'« envoi groupé ». Le `<form>`, lui, apporte 3 choses automatiquement :
1. **Il rassemble** toutes les valeurs des champs qu'il contient.
2. **Il les empaquette** en associant chaque valeur au `name` de son champ.
3. **Il les envoie** à une adresse, quand on clique sur un bouton `type="submit"`.
> 🔑 **Le détail le plus important : l'attribut `name`.**
> Seuls les champs qui ont un **`name`** sont envoyés, et c'est ce `name` qui sert d'**étiquette** à la valeur. Dans ton formulaire :
> ```html
> <input type="text" name="user_name" ...>
> <input type="email" name="user_mail" ...>
> <textarea name="user_message"></textarea>
> ```
> → au moment de l'envoi, le navigateur construit quelque chose comme :
> `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.
> ⚠️ 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 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.
- `action="https://exemple.com/contact"` → les données partent vers cette adresse.
- `action=""` (vide, comme chez toi) → elles sont envoyées à **la page actuelle** (la même URL).
> 💡 **Important pour ton cas :** vous n'avez **pas encore de serveur** derrière. Donc `action=""` ne mène nulle part d'utile : au mieux la page se recharge. C'est **normal à ce stade**, et c'est exactement pour ça que tu interceptes l'envoi en **JavaScript** (avec `addEventListener("submit", ...)` et `preventDefault()`) pour faire toi-même quelque chose d'utile (vérifier, afficher un message…).
---
## 3. L'attribut `method` : **COMMENT** les données sont envoyées
Il existe deux façons principales d'envoyer un formulaire : **GET** et **POST**.
### `method="get"`
Les données sont **collées dans l'URL**, après un point d'interrogation :
```
contact.html?user_name=Dupond&user_mail=dupond@mail.com
```
- ✅ Visible dans la barre d'adresse → on peut mettre la page en favori, la partager, la recharger.
- ❌ Tout est visible (à éviter pour un mot de passe), et la longueur est limitée.
- 👉 Idéal pour **demander / rechercher** des infos (un moteur de recherche utilise GET).
### `method="post"` (ce que tu utilises)
Les données sont envoyées dans le **corps** de la requête, **pas** dans l'URL.
- ✅ N'apparaît pas dans la barre d'adresse, pas de limite pratique de taille.
- 👉 Idéal pour **envoyer / créer / modifier** quelque chose : un message, une inscription, une suggestion…
> 💡 **La règle simple :**
> - **GET** = « je **demande** des infos » (recherche, filtre, consultation).
> - **POST** = « j'**envoie / je modifie** des infos » (formulaire de contact, suggestion, inscription).
> Pour ton formulaire de **suggestions**, `method="post"` est donc le **bon choix**. 👍
---
## 🔬 Une petite expérience pour bien voir la différence
1. Change **temporairement** ton formulaire en `method="get"`.
2. Remplis les champs, clique « Envoyer », et **regarde la barre d'adresse** : tu verras tes valeurs apparaître après le `?` (`?user_name=...&user_mail=...`).
3. Remets `method="post"` : les valeurs **disparaissent** de l'URL — elles voyagent « cachées ».
Ç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.
---
## 📝 Le résumé en une carte
| Élément | À quoi ça sert |
|---|---|
| `<form>` | enveloppe qui **regroupe et envoie** les champs d'un coup |
| `name` (sur chaque champ) | l'**étiquette** de la valeur — sans lui, le champ n'est **pas** envoyé |
| `type="submit"` (bouton) | **déclenche** l'envoi |
| `action` | **OÙ** vont les données (l'adresse du serveur ; `""` = la même page) |
| `method` | **COMMENT** : `get` (dans l'URL, pour demander) / `post` (caché, pour envoyer) |
> 🧭 Tant qu'il n'y a pas de serveur, retiens surtout : le `<form>` **structure** ta saisie, et c'est ton **JavaScript** qui décide quoi en faire à l'envoi. `action`/`method` deviendront vraiment utiles le jour où il y aura un serveur pour recevoir les données.
+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.
+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.
+46
View File
@@ -81,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%;
+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": {}
}
+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>