Compare commits
71
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcc9c70bfd | ||
|
|
ed3df9c653 | ||
|
|
5dbb785fc4 | ||
|
|
539d94e4e3 | ||
|
|
4e3650c55e | ||
|
|
39cad5771e | ||
|
|
7a0fd42192 | ||
|
|
e798810283 | ||
|
|
d9c7f81136 | ||
|
|
4b93f510ee | ||
|
|
506904b009 | ||
|
|
0bae7d1045 | ||
|
|
8d2ca369fc | ||
|
|
1e80e2c2af | ||
|
|
45149f6eaf | ||
|
|
a1a18776a6 | ||
|
|
4a148fa2ff | ||
|
|
6bef2f8304 | ||
|
|
a7300cad0a | ||
|
|
37ec87bf9f | ||
|
|
a578a7d22c | ||
|
|
0a01b5310c | ||
|
|
5707f5e2e3 | ||
|
|
e28c70d440 | ||
|
|
bd1e9adf13 | ||
|
|
6621ced8c5 | ||
|
|
336d92db9b | ||
|
|
b30bae07c2 | ||
|
|
af94eafc10 | ||
|
|
37364af1a2 | ||
|
|
b40a975c9c | ||
|
|
0d3855a322 | ||
|
|
ec4f14be33 | ||
|
|
b795340a99 | ||
|
|
db20a998ac | ||
|
|
7a04b631a4 | ||
|
|
785c82717f | ||
|
|
98a38b81d3 | ||
|
|
f8bc1be364 | ||
|
|
39d5e9f00b | ||
|
|
d95bcdd306 | ||
|
|
e65bf607d8 | ||
|
|
472498c56d | ||
|
|
22a1178bd7 | ||
|
|
ba66dfd5b5 | ||
|
|
4be75bfb86 | ||
|
|
eeb0e37e25 | ||
|
|
126d5adea2 | ||
|
|
c3ab560c47 | ||
|
|
5ad4bded37 | ||
|
|
19e0b777ca | ||
|
|
20a5020bc5 | ||
|
|
8230a3b80b | ||
|
|
5582234947 | ||
|
|
54fe8935a2 | ||
|
|
0075fd6c1c | ||
|
|
cad8295d1c | ||
|
|
3586e8fcad | ||
|
|
e6216b36b2 | ||
|
|
6c01a550d0 | ||
|
|
ceceb1865f | ||
|
|
d7b5df74be | ||
|
|
bb2b789d3f | ||
|
|
397272429d | ||
|
|
5314db52a0 | ||
|
|
be6c852045 | ||
|
|
d14cce5731 | ||
|
|
8910b77294 | ||
|
|
f630bbf649 | ||
|
|
2aade91105 | ||
|
|
bebc535cae |
@@ -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)
|
||||
@@ -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 (A→Z)</option>
|
||||
<option value="likes">Plus likées</option>
|
||||
</select>
|
||||
<form onSubmit={ajouter}>
|
||||
{/* ... tes inputs + bouton Ajouter ... */}
|
||||
</form>
|
||||
<input type="search" placeholder="Rechercher une trilogie…"
|
||||
value={recherche} onChange={(e) => setRecherche(e.target.value)} />
|
||||
<p>👍 Total de likes : {total}</p>
|
||||
{affichees.length === 0 && <p>Aucune trilogie trouvée</p>}
|
||||
{affichees.map((trilo) => (
|
||||
<Carte key={trilo.id} titre={trilo.nom} niveaux={trilo.niveaux}
|
||||
likes={likes[trilo.id] || 0} onLike={() => liker(trilo.id)} />
|
||||
))}
|
||||
<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.
|
||||
@@ -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.
|
||||
@@ -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). 🚀
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -1,80 +0,0 @@
|
||||
# 🔧 Correction — la recherche et le tri ne font rien
|
||||
|
||||
Tu as bien créé la fonction `afficherEquipe`, et le tableau s'affiche au chargement. Mais quand tu tapes dans la recherche ou que tu cliques « Trier », **rien ne bouge**. Voici pourquoi — c'est un bug très instructif sur les **paramètres de fonction**.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 D'abord, un indice important : la Console est *muette* cette fois
|
||||
|
||||
La fois précédente, tu avais une erreur **rouge** dans la Console (`afficherEquipe is not defined`). Là, **rien** : aucun message d'erreur.
|
||||
|
||||
> 💡 **À retenir :** il existe deux familles de bugs.
|
||||
> - Ceux qui **plantent** (message rouge dans la Console) — comme la dernière fois.
|
||||
> - Ceux qui **ne plantent pas** mais font **la mauvaise chose** — comme aujourd'hui. La Console reste silencieuse, le code « marche »… mais pas comme tu veux. Ce sont les plus sournois : il faut **raisonner**, pas juste lire les erreurs.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Le vrai problème : ta fonction ignore ce que tu lui donnes
|
||||
|
||||
Regarde **précisément** ces deux endroits dans ton `equipe.js` :
|
||||
|
||||
```js
|
||||
function afficherEquipe() { // 👈 (1) les parenthèses sont VIDES
|
||||
corps.innerHTML = "";
|
||||
for (const membre of equipe) { // 👈 (2) tu boucles toujours sur la globale "equipe"
|
||||
corps.append(creerLigne(membre));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Et plus bas, tu l'appelles avec une liste :
|
||||
|
||||
```js
|
||||
afficherEquipe(resultats); // la liste filtrée
|
||||
afficherEquipe(triee); // la liste triée
|
||||
```
|
||||
|
||||
Le souci : tu **passes** une liste (`resultats`, `triee`)… mais ta fonction ne la **reçoit nulle part**. Les parenthèses de `function afficherEquipe()` sont vides → l'argument que tu envoies tombe dans le vide. Et à l'intérieur, tu reboucles sur la variable globale `equipe`, donc **toujours les 3 membres, dans l'ordre d'origine**.
|
||||
|
||||
👉 Résultat : `filter` calcule bien la bonne liste filtrée, `sort` calcule bien la liste triée… et ta fonction **les jette** pour réafficher `equipe` en entier à chaque fois. D'où l'impression que « rien ne se passe ».
|
||||
|
||||
---
|
||||
|
||||
## 🧠 L'idée à comprendre : un *paramètre*, c'est une boîte d'entrée
|
||||
|
||||
Une fonction, c'est une recette. Un **paramètre**, c'est un ingrédient qu'on lui **tend** au moment de l'appeler.
|
||||
|
||||
- Si la recette dit *« utilise la liste que je te donne »* → tu peux lui tendre une liste différente à chaque fois, et le résultat change.
|
||||
- Ta recette actuelle dit *« ignore ce qu'on me tend, je prends toujours `equipe` sur l'étagère »* → quoi que tu lui passes, elle fait pareil.
|
||||
|
||||
Tu as **déjà** utilisé ce mécanisme sans bug ailleurs : ta fonction `creerLigne(membre)` reçoit bien un `membre` entre les parenthèses, et travaille avec **lui**. C'est exactement la même chose qu'il manque ici.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ La piste pour corriger (à toi de l'écrire)
|
||||
|
||||
1. Donne un **nom** au paramètre dans les parenthèses de `afficherEquipe` (par exemple `liste`).
|
||||
2. À l'intérieur, boucle sur **ce nom-là**, pas sur la globale `equipe`.
|
||||
3. Garde ton appel initial, mais en lui passant bien toute l'équipe : `afficherEquipe(equipe)`.
|
||||
|
||||
Ainsi :
|
||||
- au chargement, tu lui tends `equipe` → elle affiche tout le monde ;
|
||||
- quand tu tapes, tu lui tends `resultats` → elle affiche **la liste filtrée** ;
|
||||
- quand tu cliques, tu lui tends `triee` → elle affiche **la liste triée**.
|
||||
|
||||
> ⚠️ **Petit piège à éviter :** n'appelle pas ton paramètre `equipe` (le même nom que la globale). Ça « marcherait » mais ce serait très déroutant à relire. Un nom différent comme `liste` rend les choses claires : on voit tout de suite que la fonction travaille sur *ce qu'on lui donne*, pas sur une variable extérieure.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Comment vérifier que c'est réglé
|
||||
|
||||
1. Tape « mé » dans la recherche → seule Mélissa reste. Efface → les 3 reviennent.
|
||||
2. Clique « Trier A → Z » → l'ordre change (Guillaume, Matthieu, Mélissa).
|
||||
|
||||
Le reste de ton code (`filter`, `sort`) est **déjà correct** : il calcule la bonne liste. Le seul maillon cassé, c'est la fonction qui ne l'utilise pas. Répare ce maillon et tout le reste se met à marcher d'un coup.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Si tu veux aller plus loin (optionnel)
|
||||
|
||||
Une fois que ça marche, regarde la méthode **`replaceChildren`** (`MDN replaceChildren`) : elle vide et remplit un élément **en une seule ligne**, sans `innerHTML`. Combinée à `map` (Niveau 5), ta fonction pourrait tenir en deux lignes. Mais d'abord, fais marcher la version simple. 🙂
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -1,81 +0,0 @@
|
||||
# 🔧 Correction — enregistrer et afficher le prénom dans l'alerte
|
||||
|
||||
Tu veux qu'au moment de l'envoi, l'alerte affiche le **prénom** saisi. Ton alerte affiche sûrement quelque chose comme :
|
||||
|
||||
```
|
||||
merci [object HTMLInputElement], ton message a été pris en compte
|
||||
```
|
||||
|
||||
Voici **pourquoi**, et comment t'en sortir — sans te donner la solution toute faite.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Ce que tu as écrit
|
||||
|
||||
```js
|
||||
const nom = document.getElementById("prenom");
|
||||
const greeting = `merci ${nom}, ton message a été pris en compte`;
|
||||
// ...
|
||||
} else {
|
||||
alert(`${greeting}`);
|
||||
}
|
||||
```
|
||||
|
||||
Il y a **deux soucis** qui se cumulent ici. Bonne nouvelle : tu sais déjà corriger les deux, tu le fais ailleurs dans ton fichier.
|
||||
|
||||
---
|
||||
|
||||
## 🪤 Souci n°1 — il manque `.value`
|
||||
|
||||
`document.getElementById("prenom")` te donne **la case** HTML entière (l'objet `<input>`), pas le texte dedans. Pour obtenir ce que la personne a **tapé**, il faut `.value` — exactement comme tu fais avec `zoneMessage.value` pour la textarea.
|
||||
|
||||
```js
|
||||
nom // ❌ la case → s'affiche "[object HTMLInputElement]"
|
||||
nom.value // ✅ le texte tapé → "Mélissa"
|
||||
```
|
||||
|
||||
C'est ça qui produit le `[object HTMLInputElement]` dans ton alerte.
|
||||
|
||||
---
|
||||
|
||||
## 🪤 Souci n°2 — tu lis la valeur **trop tôt**
|
||||
|
||||
Regarde **où** tu construis `greeting` : tout **en haut** du fichier. Or ce code s'exécute **au chargement de la page**, à un instant où l'utilisateur n'a encore **rien tapé**. Le champ est vide à ce moment-là.
|
||||
|
||||
Du coup, même si tu ajoutes `.value`, `greeting` serait figé sur le champ **vide du démarrage**, et ne changerait jamais.
|
||||
|
||||
```js
|
||||
// en haut du fichier = au chargement → le champ est VIDE
|
||||
const greeting = `merci ${nom.value}, ...`; // ❌ figé sur "" pour toujours
|
||||
```
|
||||
|
||||
👉 La phrase doit être fabriquée **au moment du clic sur Envoyer**, c'est-à-dire **à l'intérieur** de la fonction `submit`. C'est seulement là que le champ contient ce que la personne a saisi.
|
||||
|
||||
> 🔎 Tu as déjà ce réflexe ailleurs : dans `rafraichirCompteur`, tu lis `zoneMessage.value` **au moment** où on tape, pas une fois pour toutes au début. Même logique ici.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Ce qu'il te reste à faire (à toi de l'écrire)
|
||||
|
||||
1. Garde en haut seulement l'**attrape-élément** : `const champPrenom = document.getElementById("prenom");` (sans `.value`).
|
||||
2. Dans le `else` (message non vide), **à ce moment-là**, lis `champPrenom.value` et construis ta phrase, puis `alert(...)`.
|
||||
3. Pour l'**enregistrer** : `localStorage.setItem("…", champPrenom.value)` — tu sais déjà faire, tu l'utilises pour le brouillon.
|
||||
|
||||
> 💡 Ton formulaire est en `method="get"` : quand l'envoi réussit, **la page se recharge** et l'alerte disparaît aussitôt. Comme il n'y a pas de serveur derrière, le plus simple pour ce TP est d'appeler `evenement.preventDefault()` **aussi** dans le `else`, pour que la page ne recharge pas et que tu voies ton alerte.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Comment vérifier
|
||||
|
||||
1. Tape un prénom + un message, clique **Envoyer** → l'alerte dit « merci **\<ton prénom\>**, … » (et **plus** `[object HTMLInputElement]`).
|
||||
2. Console (F12) : tape `localStorage.getItem("…")` (la clé que tu as choisie) → tu retrouves le prénom enregistré.
|
||||
3. Laisse le prénom **vide** et envoie → décide quoi faire (afficher « merci visiteur », ou exiger le prénom comme tu le fais déjà pour le message).
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Pour aller plus loin
|
||||
|
||||
- Ajouter le **nombre de caractères** du message dans l'alerte (`…value.length`).
|
||||
- Au prochain chargement, accueillir la personne : « Re-bonjour \<prénom\> » en relisant ce que tu as enregistré (`getItem`).
|
||||
|
||||
Note ta question si un point bloque encore.
|
||||
@@ -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`.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 (A→Z)</option>
|
||||
<option value="likes">Plus likées</option>
|
||||
</select>
|
||||
{affichees.map((trilo) => ( /* ... ta Carte ... */ ))}
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
> 🔎 `[...trilogies]` = une **copie** (pour ne pas modifier l'original avec `sort`). Et tu combines avec ton **filtre** de recherche : on filtre **puis** on trie **puis** on `.map()`.
|
||||
|
||||
**✅ Test :** change le tri → l'ordre des cartes change.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Module K — Plusieurs pages avec React Router
|
||||
|
||||
Jusqu'ici tu as **une seule page**. Pour avoir une page **détail** par trilogie (avec ses niveaux, son nombre de likes…), on utilise **React Router**.
|
||||
|
||||
### K.1 — Installer
|
||||
Dans le terminal, dans le dossier du projet :
|
||||
```bash
|
||||
npm install react-router-dom
|
||||
```
|
||||
|
||||
### K.2 — Envelopper l'app (dans `main.jsx`)
|
||||
```jsx
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
createRoot(document.getElementById("root")).render(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
);
|
||||
```
|
||||
|
||||
### K.3 — Définir les routes (dans `App.jsx`)
|
||||
```jsx
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Accueil />} />
|
||||
<Route path="/trilogie/:id" element={<Detail />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
```
|
||||
- `path="/"` → ta page d'accueil (la liste).
|
||||
- `path="/trilogie/:id"` → une page **dynamique** : le `:id` change selon la trilogie.
|
||||
|
||||
### K.4 — Des liens (au lieu de `<a href>`)
|
||||
Dans ta `Carte`, un lien vers le détail :
|
||||
```jsx
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
<Link to={`/trilogie/${props.id}`}>Voir les détails</Link>
|
||||
```
|
||||
> 🔎 `<Link>` navigue **sans recharger** la page (c'est tout l'intérêt d'une « single page app »). `<a href>` rechargerait tout.
|
||||
|
||||
### K.5 — Lire l'id dans la page détail
|
||||
```jsx
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function Detail() {
|
||||
const { id } = useParams(); // récupère le :id de l'URL
|
||||
// retrouve la bonne trilogie par son id, et affiche-la
|
||||
return <h1>Détails de la trilogie {id}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Test :** clique « Voir les détails » sur une carte → l'URL devient `/trilogie/3` et la page détail s'affiche, **sans rechargement**. Le bouton « précédent » du navigateur te ramène à la liste.
|
||||
|
||||
> 🧩 **Les 4 briques de React Router :** `<BrowserRouter>` (enveloppe), `<Routes>/<Route>` (quelle page pour quelle URL), `<Link>` (naviguer), `useParams()` (lire un morceau dynamique de l'URL). C'est la base de **tous** les sites multi-pages en React.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist
|
||||
- [ ] I — liste en `useState` + formulaire d'ajout (immutabilité avec `[...]`)
|
||||
- [ ] J — tri via un `<select>` contrôlé
|
||||
- [ ] K — React Router : accueil + page détail (`Link`, `useParams`)
|
||||
|
||||
## 🚀 Encore plus loin
|
||||
- Bouton **supprimer** (`filter`) et **éditer** une trilogie.
|
||||
- Un **custom hook** `useLocalStorage` pour ranger la logique de persistance.
|
||||
- Une vraie **mise en page** CSS de la page détail (image de la trilogie, etc.).
|
||||
|
||||
## 🧠 Nouveaux concepts
|
||||
Liste en état + **immutabilité** (`[...]`, `filter`) · formulaire contrôlé + `onSubmit`/`preventDefault` · tri (`select` contrôlé + `sort`) · **React Router** (`BrowserRouter`, `Routes`, `Route`, `Link`, `useParams`).
|
||||
|
||||
Bloqué ? Note ce que tu as tapé + le message exact (console F12). On débloque.
|
||||
@@ -0,0 +1,307 @@
|
||||
# 🎮 Énoncé React #2 — ton site Geometry Dash (version longue)
|
||||
|
||||
Bien joué Guillaume : tu as un Header avec ton intro, un Footer, un composant `Carte` avec des **props**, et un `Compteur` avec `useState`. Les briques sont là. 👌
|
||||
|
||||
Le souci : tes cartes affichent encore le contenu du tuto (`Suivi en temps réel`…), tu les écris **une par une à la main**, et ton compteur dit encore « Véhicules ». On va en faire un **vrai site sur tes trilogies**, et surtout apprendre **beaucoup de React** au passage.
|
||||
|
||||
> 🎯 Ce document est une **série de modules** (A → H), du plus simple au plus costaud. Fais-les **dans l'ordre**, teste à chaque fois. Je t'explique les **mécaniques** et te donne des mini-exemples ; à toi d'**appliquer sur ton thème** (je ne donne pas le site fini). Compte plusieurs séances — c'est normal.
|
||||
|
||||
| Module | Tu apprends | Niveau |
|
||||
|---|---|---|
|
||||
| A | Listes : `.map()` + `key` | ⭐ |
|
||||
| B | Affichage conditionnel + extraire un `<Badge>` | ⭐ |
|
||||
| C | État local par carte (`useState`) | ⭐ |
|
||||
| D | Champ **contrôlé** + recherche/filtre + état vide | ⭐⭐ |
|
||||
| E | **Faire remonter l'état** (lifting state up) + callbacks en props | ⭐⭐ |
|
||||
| F | **`useEffect`** : titre de page + persistance `localStorage` | ⭐⭐ |
|
||||
| G | **Charger les données avec `fetch`** (dans React) | ⭐⭐⭐ |
|
||||
| H | Re-découpage propre (`<Liste>`, `<BarreRecherche>`) | ⭐⭐ |
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Module A — Afficher une liste : `.map()` + `key`
|
||||
|
||||
Aujourd'hui tu écris chaque carte à la main. La bonne méthode : **un tableau de données** + **`.map()`**.
|
||||
|
||||
### A.1 — un tableau (au-dessus de `function App()`)
|
||||
```jsx
|
||||
const trilogies = [
|
||||
{ id: 1, nom: "Bloodbath", createur: "Riot", difficulte: "Extreme Demon" },
|
||||
{ id: 2, nom: "Cataclysm", createur: "Ggb0y", difficulte: "Extreme Demon" },
|
||||
{ id: 3, nom: "Sonic Wave", createur: "Cyclic", difficulte: "Extreme Demon" },
|
||||
// ... tes 5 trilogies
|
||||
];
|
||||
```
|
||||
|
||||
### A.2 — générer les cartes
|
||||
```jsx
|
||||
function App() {
|
||||
return (
|
||||
<div className="body">
|
||||
<Header />
|
||||
{trilogies.map((trilo) => (
|
||||
<Carte key={trilo.id} nom={trilo.nom} createur={trilo.createur} difficulte={trilo.difficulte} />
|
||||
))}
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> 🧩 **Deux nouveautés :**
|
||||
> - **`{ ... }`** dans le JSX = « insère ici du JavaScript ». `{trilogies.map(...)}` insère **une carte par élément**.
|
||||
> - **`key={...}`** = un identifiant **unique** par élément (utilise `id`, pas l'index). React en a besoin pour suivre chaque carte ; sans lui → **warning** dans la console.
|
||||
|
||||
**✅ Test :** ajoute une trilogie au tableau → une carte apparaît toute seule.
|
||||
|
||||
---
|
||||
|
||||
## 🎖️ Module B — Affichage conditionnel + un composant `<Badge>`
|
||||
|
||||
D'abord enrichis `Carte` :
|
||||
```jsx
|
||||
function Carte(props) {
|
||||
return (
|
||||
<div className="carte">
|
||||
<h2>{props.nom}</h2>
|
||||
<p>Par {props.createur}</p>
|
||||
<p>Difficulté : {props.difficulte}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Affichage conditionnel** — afficher un truc **seulement si** une condition est vraie :
|
||||
```jsx
|
||||
{props.difficulte === "Extreme Demon" && <span className="badge">🔥 Extreme</span>}
|
||||
```
|
||||
- `condition && <...>` = « si vrai, affiche, sinon rien ».
|
||||
- choix entre deux : `{props.fini ? "✅ battu" : "❌ pas battu"}`.
|
||||
|
||||
> 💡 Dans le JSX on n'écrit pas de `if` à l'intérieur du `return` : on utilise `&&` (afficher ou rien) ou `? :` (A ou B).
|
||||
|
||||
**Exercice bonus — extraire un composant `<Badge>`** (1 composant = 1 fichier) :
|
||||
```jsx
|
||||
// components/Badge.jsx
|
||||
function Badge({ difficulte }) {
|
||||
if (difficulte !== "Extreme Demon") return null; // un composant peut renvoyer "rien"
|
||||
return <span className="badge">🔥 Extreme</span>;
|
||||
}
|
||||
export default Badge;
|
||||
```
|
||||
> 🔎 Nouveau : `function Badge({ difficulte })` — on « déballe » directement la prop entre accolades au lieu d'écrire `props.difficulte` partout. Et `return null` = « n'affiche rien ». Deux outils très courants.
|
||||
|
||||
**✅ Test :** seules tes trilogies « Extreme Demon » montrent le badge.
|
||||
|
||||
---
|
||||
|
||||
## ❤️ Module C — État local : chaque `Carte` a son `useState`
|
||||
|
||||
Mets un bouton « j'aime » **dans** `Carte` :
|
||||
```jsx
|
||||
import { useState } from "react";
|
||||
|
||||
function Carte(props) {
|
||||
const [likes, setLikes] = useState(0);
|
||||
return (
|
||||
<div className="carte">
|
||||
<h2>{props.nom}</h2>
|
||||
<button onClick={() => setLikes(likes + 1)}>❤️ {likes}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> 🧩 Chaque `<Carte />` est une **instance indépendante** avec son **propre état** : liker une carte ne touche pas les autres.
|
||||
|
||||
**✅ Test :** le ❤️ d'une carte monte sans bouger les autres.
|
||||
|
||||
---
|
||||
|
||||
## 🔎 Module D — Champ **contrôlé** + recherche
|
||||
|
||||
Nouveau concept important : un champ dont la **valeur est pilotée par un `useState`** (on dit « contrôlé »).
|
||||
|
||||
```jsx
|
||||
function App() {
|
||||
const [recherche, setRecherche] = useState("");
|
||||
|
||||
const filtrees = trilogies.filter((t) =>
|
||||
t.nom.toLowerCase().includes(recherche.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="body">
|
||||
<Header />
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Rechercher une trilogie…"
|
||||
value={recherche} // l'input AFFICHE l'état
|
||||
onChange={(e) => setRecherche(e.target.value)} // toute frappe MET À JOUR l'état
|
||||
/>
|
||||
|
||||
{filtrees.length === 0 && <p>Aucune trilogie trouvée 😕</p>}
|
||||
|
||||
{filtrees.map((trilo) => (
|
||||
<Carte key={trilo.id} nom={trilo.nom} createur={trilo.createur} difficulte={trilo.difficulte} />
|
||||
))}
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> 🧩 **Le champ contrôlé**, c'est la boucle : l'état → s'affiche dans le champ (`value`) ; une frappe → met à jour l'état (`onChange`) ; l'état change → React réaffiche, donc la liste **filtrée** se recalcule. Tu reconnais ta recherche en JS pur, version React (et React refait l'affichage tout seul, plus de `innerHTML`).
|
||||
|
||||
> ⚠️ Détail : on filtre **avant** le `.map()`, et on prévoit le cas **« aucun résultat »** (l'état vide). Un bon réflexe.
|
||||
|
||||
**✅ Test :** tape « blood » → seules les trilogies correspondantes restent ; efface → tout revient ; tape n'importe quoi → message « aucune trilogie ».
|
||||
|
||||
---
|
||||
|
||||
## ⬆️ Module E — Faire remonter l'état (lifting state up)
|
||||
|
||||
Problème : et si tu veux un **total de likes** affiché en haut, qui additionne **toutes** les cartes ? Chaque carte a son like **dans son coin**, App ne les « voit » pas.
|
||||
|
||||
La solution React : **remonter l'état dans le parent** (App) et le **redescendre** aux enfants. L'enfant prévient le parent via une **fonction passée en prop** (un *callback*).
|
||||
|
||||
```jsx
|
||||
function App() {
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
return (
|
||||
<div className="body">
|
||||
<p>👍 Total de likes : {total}</p>
|
||||
{trilogies.map((trilo) => (
|
||||
<Carte
|
||||
key={trilo.id}
|
||||
nom={trilo.nom}
|
||||
onLike={() => setTotal((t) => t + 1)} // 👈 on passe une FONCTION en prop
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Carte(props) {
|
||||
return (
|
||||
<div className="carte">
|
||||
<h2>{props.nom}</h2>
|
||||
<button onClick={props.onLike}>❤️ J'aime</button> {/* l'enfant appelle le callback du parent */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> 🧩 **L'idée clé de React :** l'état « vit » **au plus haut niveau qui en a besoin**. Les données descendent en **props** ; les événements remontent via des **fonctions en props**. Ici, chaque carte ne sait pas compter le total — elle dit juste « on m'a likée » au parent, qui, lui, tient le total.
|
||||
|
||||
> 🔎 `setTotal((t) => t + 1)` (avec une fonction) est la façon **sûre** d'incrémenter quand plusieurs mises à jour peuvent s'enchaîner. Prends l'habitude.
|
||||
|
||||
**✅ Test :** liker n'importe quelle carte fait monter le **total global** en haut.
|
||||
|
||||
---
|
||||
|
||||
## 🔁 Module F — `useEffect` : agir « à côté » de l'affichage
|
||||
|
||||
`useEffect` sert à exécuter du code **après l'affichage** : changer le titre de l'onglet, lire/écrire dans `localStorage`, lancer une requête… Des choses qui ne sont pas « du JSX ».
|
||||
|
||||
### F.1 — le titre de l'onglet
|
||||
```jsx
|
||||
import { useEffect } from "react";
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${trilogies.length} trilogies cultes`;
|
||||
}, []); // 👈 tableau vide = "une seule fois, au chargement"
|
||||
```
|
||||
|
||||
### F.2 — des likes qui **survivent au rechargement** (`localStorage` + `useEffect`)
|
||||
Dans `Carte` :
|
||||
```jsx
|
||||
function Carte(props) {
|
||||
// valeur de départ : ce qui est stocké, sinon 0
|
||||
const [likes, setLikes] = useState(() => Number(localStorage.getItem("likes-" + props.id)) || 0);
|
||||
|
||||
// à chaque fois que "likes" change, on le sauvegarde
|
||||
useEffect(() => {
|
||||
localStorage.setItem("likes-" + props.id, likes);
|
||||
}, [likes]); // 👈 "rejoue cet effet quand likes change"
|
||||
|
||||
return <button onClick={() => setLikes(likes + 1)}>❤️ {likes}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
> 🧩 **À comprendre :** le **tableau de dépendances** (`[]` ou `[likes]`) dit **quand** rejouer l'effet : `[]` = une seule fois ; `[likes]` = à chaque changement de `likes`. C'est le cœur de `useEffect`.
|
||||
> Et tu retrouves ton `localStorage` (texte → `Number(...)` pour reconvertir, comme `parseInt` avant).
|
||||
|
||||
**✅ Test :** like une carte, **recharge la page (F5)** → le nombre est toujours là. Et l'onglet affiche ton titre.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Module G — Charger les données avec `fetch` (le vrai truc des apps)
|
||||
|
||||
Au lieu d'écrire `trilogies` en dur, charge-les depuis un fichier JSON — comme une vraie app va chercher ses données sur un serveur.
|
||||
|
||||
1. Crée `public/trilogies.json` (le dossier `public/` est servi à la racine) avec ton tableau de trilogies (format JSON : guillemets doubles partout).
|
||||
2. Dans `App` :
|
||||
```jsx
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
function App() {
|
||||
const [trilogies, setTrilogies] = useState([]); // au départ : liste vide
|
||||
const [chargement, setChargement] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/trilogies.json")
|
||||
.then((r) => r.json())
|
||||
.then((data) => { setTrilogies(data); setChargement(false); });
|
||||
}, []); // une seule fois, au chargement
|
||||
|
||||
if (chargement) return <p>Chargement…</p>; // tant que les données ne sont pas là
|
||||
|
||||
return (
|
||||
<div className="body">
|
||||
{trilogies.map((t) => <Carte key={t.id} nom={t.nom} /* ... */ />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> 🧩 **Le schéma à retenir :** un `useState` vide pour accueillir les données → un `useEffect([])` qui `fetch` au chargement → `setTrilogies(data)` remplit l'état → React réaffiche avec les données. Et on gère l'état **« en cours de chargement »** (très pro).
|
||||
>
|
||||
> ⚠️ Comme en JS pur : `fetch` a besoin du serveur de dev (`npm run dev` te le donne déjà). Le `public/` est servi à la racine, donc `/trilogies.json`.
|
||||
|
||||
**✅ Test :** tes cartes s'affichent à partir du JSON. Ajoute une trilogie **dans le fichier** → elle apparaît.
|
||||
|
||||
---
|
||||
|
||||
## 🧹 Module H — Re-découper proprement
|
||||
|
||||
Ton `App` commence à beaucoup faire (recherche + total + liste…). Garde-le **assembleur** : extrais des composants, **un par fichier**.
|
||||
- `components/BarreRecherche.jsx` — l'input contrôlé (reçoit `valeur` et `onChange` en props).
|
||||
- `components/Liste.jsx` — reçoit le tableau filtré et fait le `.map()`.
|
||||
- `components/Badge.jsx` — déjà fait au module B.
|
||||
|
||||
> 🧭 La règle d'or n'a pas changé : **1 composant = 1 fichier**, `App` assemble. Dès qu'un fichier « fait trop de choses », découpe.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
- [ ] A — tableau + `.map()` + `key`
|
||||
- [ ] B — affichage conditionnel + composant `<Badge>`
|
||||
- [ ] C — like local par carte
|
||||
- [ ] D — recherche avec champ **contrôlé** + message « aucun résultat »
|
||||
- [ ] E — **total de likes** remonté dans `App` (callback en prop)
|
||||
- [ ] F — `useEffect` : titre d'onglet + likes persistés en `localStorage`
|
||||
- [ ] G — données chargées via `fetch` depuis `public/trilogies.json`
|
||||
- [ ] H — re-découpage (`<BarreRecherche>`, `<Liste>`)
|
||||
|
||||
## 🚀 Encore plus loin
|
||||
- Un **tri** (par difficulté, par likes) avec un `<select>` contrôlé.
|
||||
- Une **image** par trilogie + affichage conditionnel si l'image existe.
|
||||
- Un **mode sombre** global (état dans `App`, classe sur le `<div>` racine).
|
||||
|
||||
## 🧠 Les nouveaux concepts de cet énoncé
|
||||
Listes (`map`/`key`) · affichage conditionnel · état local · **champ contrôlé** · **lifting state up** + callbacks · **`useEffect`** + dépendances · **`fetch` en React** + état de chargement · re-découpage.
|
||||
|
||||
Bloqué ? Note ce que tu as tapé + le message exact (console F12). On débloque.
|
||||
+17
-20
@@ -1,6 +1,6 @@
|
||||
# 📨 À quoi sert vraiment une balise `<form>` ?
|
||||
|
||||
Tu as un `<form>` autour de ton formulaire de contact, mais ce qu'il fait « en coulisses » n'est pas forcément clair. Ce fichier explique **à quoi il sert**, puis les deux attributs qui prêtent à confusion : **`action`** et **`method`**.
|
||||
Tu as un `<form>` autour de ton formulaire de suggestions, mais ce qu'il fait « en coulisses » n'est pas forcément clair. Ce fichier explique **à quoi il sert**, puis les deux attributs qui prêtent à confusion : **`action`** et **`method`**.
|
||||
|
||||
---
|
||||
|
||||
@@ -17,24 +17,22 @@ Sans `<form>`, tes champs s'affichent quand même… mais ce ne sont que des cas
|
||||
> 🔑 **Le détail le plus important : l'attribut `name`.**
|
||||
> Seuls les champs qui ont un **`name`** sont envoyés, et c'est ce `name` qui sert d'**étiquette** à la valeur. Dans ton formulaire :
|
||||
> ```html
|
||||
> <input type="text" name="prenom" ...>
|
||||
> <input type="email" name="email" ...>
|
||||
> <textarea name="contacter"></textarea>
|
||||
> <input type="text" name="user_name" ...>
|
||||
> <input type="email" name="user_mail" ...>
|
||||
> <textarea name="user_message"></textarea>
|
||||
> ```
|
||||
> → au moment de l'envoi, le navigateur construit quelque chose comme :
|
||||
> `prenom = Mélissa`, `email = melissa@mail.com`, `contacter = ...`
|
||||
> `user_name = Dupond`, `user_mail = dupond@mail.com`, `user_message = ...`
|
||||
> Un champ **sans `name`** est tout simplement **ignoré** à l'envoi, même s'il est rempli.
|
||||
>
|
||||
> 💬 C'est aussi pour ça que `FormData` (Niveau 5, exo 8) lit les champs par leur `name` : tout est lié.
|
||||
|
||||
> ⚠️ Rappel utile : pour que l'envoi parte, le bouton doit être **dans** le `<form>` et être un `type="submit"` (un `<button>` sans `type` précisé compte comme submit par défaut).
|
||||
> ⚠️ Rappel utile : pour que l'envoi parte, le bouton doit être **dans** le `<form>` et être un `type="submit"` (c'est le cas du tien : « Envoyer le message »).
|
||||
|
||||
---
|
||||
|
||||
## 2. L'attribut `action` : **OÙ** vont les données
|
||||
|
||||
```html
|
||||
<form method="get" action="">
|
||||
<form action="" method="post">
|
||||
```
|
||||
|
||||
`action` indique **l'adresse qui va recevoir** les données — en général un programme **côté serveur** (en PHP, Node, Python…) qui les enregistre, envoie un e-mail, etc.
|
||||
@@ -42,7 +40,7 @@ Sans `<form>`, tes champs s'affichent quand même… mais ce ne sont que des cas
|
||||
- `action="https://exemple.com/contact"` → les données partent vers cette adresse.
|
||||
- `action=""` (vide, comme chez toi) → elles sont envoyées à **la page actuelle** (la même URL).
|
||||
|
||||
> 💡 **Important pour ton cas :** vous n'avez **pas encore de serveur** derrière. Donc `action=""` ne mène nulle part d'utile : la page se contente de se recharger. C'est **normal à ce stade**, et c'est exactement pour ça que tu interceptes l'envoi en **JavaScript** (avec `addEventListener("submit", ...)` et `preventDefault()`) pour faire toi-même quelque chose d'utile (vérifier le message, afficher le prénom dans une alerte…). Tu l'as déjà fait — c'est cette logique-là qui « remplace » le serveur pour l'instant.
|
||||
> 💡 **Important pour ton cas :** vous n'avez **pas encore de serveur** derrière. Donc `action=""` ne mène nulle part d'utile : au mieux la page se recharge. C'est **normal à ce stade**, et c'est exactement pour ça que tu interceptes l'envoi en **JavaScript** (avec `addEventListener("submit", ...)` et `preventDefault()`) pour faire toi-même quelque chose d'utile (vérifier, afficher un message…).
|
||||
|
||||
---
|
||||
|
||||
@@ -50,36 +48,35 @@ Sans `<form>`, tes champs s'affichent quand même… mais ce ne sont que des cas
|
||||
|
||||
Il existe deux façons principales d'envoyer un formulaire : **GET** et **POST**.
|
||||
|
||||
### `method="get"` (ce que tu utilises)
|
||||
### `method="get"`
|
||||
Les données sont **collées dans l'URL**, après un point d'interrogation :
|
||||
```
|
||||
contact.html?prenom=Mélissa&email=melissa@mail.com&contacter=Bonjour...
|
||||
contact.html?user_name=Dupond&user_mail=dupond@mail.com
|
||||
```
|
||||
- ✅ Visible dans la barre d'adresse → on peut mettre la page en favori, la partager, la recharger.
|
||||
- ❌ Tout est visible (à éviter pour un mot de passe), et la longueur est limitée.
|
||||
- 👉 Idéal pour **demander / rechercher** des infos (un moteur de recherche utilise GET).
|
||||
|
||||
### `method="post"`
|
||||
### `method="post"` (ce que tu utilises)
|
||||
Les données sont envoyées dans le **corps** de la requête, **pas** dans l'URL.
|
||||
- ✅ N'apparaît pas dans la barre d'adresse, pas de limite pratique de taille.
|
||||
- 👉 Idéal pour **envoyer / créer / modifier** quelque chose : un message, une inscription…
|
||||
- 👉 Idéal pour **envoyer / créer / modifier** quelque chose : un message, une inscription, une suggestion…
|
||||
|
||||
> 💡 **La règle simple :**
|
||||
> - **GET** = « je **demande** des infos » (recherche, filtre, consultation).
|
||||
> - **POST** = « j'**envoie / je modifie** des infos » (formulaire de contact, inscription).
|
||||
> Pour un formulaire de **contact** où l'on **envoie un message**, `method="post"` serait en fait **plus adapté** que `get`. Essaie de le passer en `post` et regarde la différence avec l'expérience ci-dessous. 🙂
|
||||
> - **POST** = « j'**envoie / je modifie** des infos » (formulaire de contact, suggestion, inscription).
|
||||
> Pour ton formulaire de **suggestions**, `method="post"` est donc le **bon choix**. 👍
|
||||
|
||||
---
|
||||
|
||||
## 🔬 Une petite expérience pour bien voir la différence
|
||||
|
||||
1. Garde ton `method="get"` actuel. Remplis les champs, clique « Envoyer », et **regarde la barre d'adresse** : tu verras tes valeurs apparaître après le `?` (`?prenom=...&email=...`).
|
||||
2. Passe maintenant en `method="post"`, recommence : les valeurs **disparaissent** de l'URL — elles voyagent « cachées ».
|
||||
1. Change **temporairement** ton formulaire en `method="get"`.
|
||||
2. Remplis les champs, clique « Envoyer », et **regarde la barre d'adresse** : tu verras tes valeurs apparaître après le `?` (`?user_name=...&user_mail=...`).
|
||||
3. Remets `method="post"` : les valeurs **disparaissent** de l'URL — elles voyagent « cachées ».
|
||||
|
||||
Ça rend très concret ce que veulent dire GET (dans l'URL) et POST (hors de l'URL). C'est aussi pour ça que `name` est essentiel : sans lui, rien n'apparaît dans l'URL, car le navigateur ne sait pas comment étiqueter la valeur.
|
||||
|
||||
> 🔎 Astuce : pour faire l'expérience proprement, **désactive temporairement** ton `preventDefault()` dans le JS — sinon il empêche justement l'envoi par défaut, et tu ne verras pas l'URL changer.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Le résumé en une carte
|
||||
|
||||
@@ -1,620 +0,0 @@
|
||||
# 📬 Page Contact — relecture & prochaine étape
|
||||
|
||||
Salut Mélissa ! 👋
|
||||
|
||||
Ta page contact prend forme : le tableau de l'équipe est sympa, le formulaire est en place, et le bouton « contact » qui reste enfoncé pour montrer la page courante (`.instyled`) est une **très bonne idée** d'ergonomie. Bravo ! 🎉
|
||||
|
||||
Ce guide a trois objectifs :
|
||||
1. Corriger **4 bugs** (avec l'explication, pas juste la solution).
|
||||
2. T'apprendre à **définir et limiter une `<textarea>`** proprement.
|
||||
3. Te lancer sur ta **première mission JavaScript** 🚀 (dans le thème FleetZen).
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Bug 1 — L'accolade `}` oubliée (le bug le plus important)
|
||||
|
||||
Tu te souviens de la leçon « chaque `{` a son `}` » ? Elle te concerne en plein ! 😄 Regarde le tout début de `contact.css` :
|
||||
|
||||
```css
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: lightblue;
|
||||
margin: 0;
|
||||
padding-bottom: 60px;
|
||||
/* ❌ il manque le } ICI pour fermer body */
|
||||
#nav_barre { /* du coup le navigateur croit que tout ça est ENCORE dans body */
|
||||
display: flex;
|
||||
...
|
||||
```
|
||||
|
||||
Tu as **ouvert** `body {` mais tu ne l'as **jamais refermé**. Résultat : le navigateur pense que `#nav_barre`, `.styled`, `.table`… sont tous **imbriqués dans `body`**, il se perd, et **une grande partie de ton CSS est ignorée**. C'est pour ça que ta page ne ressemble pas à ce que tu attends.
|
||||
|
||||
### ✅ Correction : ferme `body` avec un `}`
|
||||
|
||||
```css
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: lightblue;
|
||||
margin: 0;
|
||||
padding-bottom: 60px;
|
||||
} /* ✅ on ferme body ici */
|
||||
|
||||
#nav_barre {
|
||||
display: flex;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
> 💡 **À retenir :** à chaque `{` correspond **un** `}`. Une accolade oubliée et **tout le reste de la feuille de style casse**. Astuce : la plupart des éditeurs colorent ou « replient » les blocs — si la coloration paraît bizarre à partir d'un endroit, c'est souvent une accolade manquante juste avant.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug 2 — Un lien de navigation cassé
|
||||
|
||||
Ta page `contact.html` est **déjà dans le dossier `pages/`**. Or ton lien vers services pointe vers :
|
||||
|
||||
```html
|
||||
<a href="pages/services.html"> <!-- ❌ cherche pages/pages/services.html → n'existe pas -->
|
||||
```
|
||||
|
||||
Comme tu es déjà dans `pages/`, tu redemandes d'entrer dans `pages/` → le chemin n'existe pas.
|
||||
|
||||
### ✅ Correction : le fichier est **juste à côté**, dans le même dossier
|
||||
|
||||
```html
|
||||
<a href="services.html"> <!-- ✅ même dossier, on met juste le nom du fichier -->
|
||||
```
|
||||
|
||||
> 💡 **À retenir :** les chemins sont **relatifs au fichier courant**.
|
||||
> - `services.html` → dans le **même dossier**
|
||||
> - `../index.html` → **remonter** d'un dossier (le `..`), puis prendre `index.html`
|
||||
> Ton lien « retour » (`../index.html`) est d'ailleurs **correct**, bravo : c'est la même logique.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug 3 — Le champ e-mail n'est pas un vrai champ e-mail
|
||||
|
||||
```html
|
||||
<input type="text" name="e-mail" id="e-mail" ...> <!-- ❌ type="text" -->
|
||||
```
|
||||
|
||||
En `type="text"`, le navigateur accepte **n'importe quoi**. Pour un e-mail, il existe un type dédié qui **vérifie tout seul** qu'il y a bien un `@` :
|
||||
|
||||
```html
|
||||
<input type="email" name="email" id="email" ...> <!-- ✅ -->
|
||||
```
|
||||
|
||||
> 💡 **À retenir :** le **bon `type`** d'`<input>` fait travailler le navigateur à ta place : `email`, `tel`, `number`, `date`… Chacun ajoute sa petite vérification automatique.
|
||||
>
|
||||
> ⚠️ Petit détail : essaie d'éviter le tiret dans les `id`/`name` (`e-mail`). Préfère `email` ou `e_mail` — c'est plus simple à réutiliser ensuite, surtout en JavaScript.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Bug 4 — L'icône SVG n'est pas centrée dans le bouton « retour »
|
||||
|
||||
Ton bouton « retour » mélange une **image** (le SVG) et du **texte** côte à côte :
|
||||
|
||||
```html
|
||||
<button class="styled"><svg ...></svg>retour</button>
|
||||
```
|
||||
|
||||
Le souci : par défaut, une icône SVG se pose **sur la ligne du texte** (sur la « ligne de base », comme une lettre). Du coup elle paraît trop haute ou trop basse, et ton `line-height: 2.5` accentue le décalage. L'icône et le mot « retour » ne sont pas alignés sur leur **milieu**.
|
||||
|
||||
### ✅ La solution : transformer le bouton en boîte flex
|
||||
|
||||
```css
|
||||
.styled {
|
||||
display: inline-flex; /* le bouton range ses enfants (icône + texte) */
|
||||
align-items: center; /* ↕ les centre verticalement, sur le même milieu */
|
||||
justify-content: center; /* ↔ les centre horizontalement */
|
||||
gap: 6px; /* un petit espace entre l'icône et le mot */
|
||||
line-height: normal; /* on enlève le 2.5 qui déréglait l'alignement */
|
||||
/* ...garde le reste de tes styles (couleur, border-radius, etc.) */
|
||||
}
|
||||
```
|
||||
|
||||
`align-items: center`, c'est **exactement** le `flex` que tu connais déjà (tu l'utilises sur `#buttons`). Ici on l'applique **au bouton lui-même**, parce que c'est lui le parent qui contient l'icône **et** le texte.
|
||||
|
||||
> 💡 **À retenir :** dès qu'un bouton (ou n'importe quelle boîte) contient **une icône + du texte** à aligner, passe-le en `display: inline-flex; align-items: center;`. C'est le réflexe pro pour centrer proprement une icône avec du texte.
|
||||
>
|
||||
> 🔎 Comme tous tes boutons partagent la classe `.styled`, cette correction les rend **tous** propres d'un coup — même ceux sans icône (le texte y reste bien centré).
|
||||
|
||||
---
|
||||
|
||||
## 📐 La leçon du jour : définir et **limiter** une `<textarea>`
|
||||
|
||||
Ta zone de message est « brute » :
|
||||
|
||||
```html
|
||||
<textarea name="contacter" id="contacter"></textarea>
|
||||
```
|
||||
|
||||
Par défaut elle est petite, et l'utilisateur peut écrire **sans aucune limite**. On va la **cadrer**.
|
||||
|
||||
### 1️⃣ Lui donner une taille de départ : `rows` et `cols`
|
||||
|
||||
```html
|
||||
<textarea id="contacter" name="contacter"
|
||||
rows="6" cols="40"></textarea>
|
||||
```
|
||||
|
||||
- `rows` = nombre de **lignes** visibles (la hauteur).
|
||||
- `cols` = largeur en **caractères**.
|
||||
|
||||
> 🔎 Ce ne sont **pas** des pixels : c'est « combien de lignes/caractères je vois » au départ.
|
||||
|
||||
### 2️⃣ Limiter le nombre de caractères : `maxlength`
|
||||
|
||||
```html
|
||||
<textarea id="contacter" name="contacter"
|
||||
rows="6" cols="40"
|
||||
maxlength="500"></textarea>
|
||||
```
|
||||
|
||||
`maxlength="500"` = impossible de taper plus de 500 caractères. Le navigateur bloque **tout seul**, sans JavaScript. 👍
|
||||
|
||||
### 3️⃣ Cadrer le redimensionnement : `resize` en CSS
|
||||
|
||||
La petite poignée en bas à droite permet à l'utilisateur d'étirer la zone et de **casser ta mise en page**. On la contrôle en CSS :
|
||||
|
||||
```css
|
||||
#contacter {
|
||||
resize: vertical; /* étirement en hauteur seulement (recommandé) */
|
||||
/* resize: none; pour l'interdire complètement */
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
}
|
||||
```
|
||||
|
||||
> 💡 **À retenir :**
|
||||
> - `rows` / `cols` → **taille de départ**.
|
||||
> - `maxlength` → **limite** le nombre de caractères (le navigateur bloque tout seul).
|
||||
> - `resize` (CSS) → contrôle la **poignée** d'étirement (`vertical`, `horizontal`, `none`, `both`).
|
||||
|
||||
### 4️⃣ 👉 Votre question : laisser l'utilisateur redimensionner, mais **entre un min et un max**
|
||||
|
||||
Tu as écrit pour l'instant :
|
||||
|
||||
```css
|
||||
textarea {
|
||||
resize: initial; /* "initial" = la valeur par défaut = "both" : étirable dans TOUS les sens, sans limite */
|
||||
width: 527px;
|
||||
height: 80px;
|
||||
}
|
||||
```
|
||||
|
||||
`resize: initial` remet la valeur **par défaut**, c'est-à-dire `both` : l'utilisateur peut tout étirer, **dans toutes les directions et sans aucune borne**. Il peut donc rendre la zone énorme et **casser ta mise en page**. Vous m'avez justement demandé comment **autoriser** l'agrandissement **sans qu'il parte trop grand (ni trop petit)**. La recette tient en 2 idées qui se combinent :
|
||||
|
||||
1. **`resize`** dit *dans quelle direction* on peut étirer.
|
||||
2. **`min-*` / `max-*`** disent *jusqu'où* on peut aller.
|
||||
|
||||
```css
|
||||
#contacter {
|
||||
resize: vertical; /* on autorise l'étirement, mais en HAUTEUR seulement */
|
||||
|
||||
min-height: 80px; /* impossible de la réduire en dessous de 80px */
|
||||
max-height: 300px; /* impossible de l'agrandir au-delà de 300px */
|
||||
|
||||
width: 100%; /* largeur pilotée en CSS, pas par l'utilisateur */
|
||||
}
|
||||
```
|
||||
|
||||
Avec ça : l'utilisateur attrape la poignée, tire vers le bas pour avoir plus de place… mais **la zone refuse de dépasser 300px** et **de descendre sous 80px**. Ta mise en page reste protégée. 🎉
|
||||
|
||||
#### Et si on veut autoriser les **deux** sens (vertical **et** horizontal) ?
|
||||
|
||||
Même principe, mais avec **4 bornes** : 2 pour la hauteur, 2 pour la largeur. On passe `resize` à `both` (c'est d'ailleurs ce que faisait ton `resize: initial`, mais cette fois **borné**) :
|
||||
|
||||
```css
|
||||
#contacter {
|
||||
resize: both; /* la poignée étire en hauteur ET en largeur */
|
||||
|
||||
min-height: 80px; /* bornes verticales */
|
||||
max-height: 300px;
|
||||
|
||||
min-width: 200px; /* bornes horizontales */
|
||||
max-width: 100%; /* ne dépasse jamais la largeur du parent → pas de scroll */
|
||||
}
|
||||
```
|
||||
|
||||
Chaque direction a sa paire `min`/`max` :
|
||||
- **hauteur** → `min-height` / `max-height`
|
||||
- **largeur** → `min-width` / `max-width`
|
||||
|
||||
L'utilisateur peut alors étirer dans tous les sens, mais **jamais au-delà** de ces 4 limites.
|
||||
|
||||
> ⚠️ Le piège classique en horizontal : mets `max-width: 100%` (et pas un grand `527px` fixe), sinon l'utilisateur peut élargir la zone **plus que l'écran** et faire apparaître une barre de défilement horizontale.
|
||||
|
||||
Récap des valeurs de `resize` :
|
||||
|
||||
| `resize` | Ce que l'utilisateur peut étirer |
|
||||
|---|---|
|
||||
| `none` | rien (verrouillé) |
|
||||
| `vertical` | la hauteur seulement (le plus confortable pour un message) |
|
||||
| `horizontal` | la largeur seulement |
|
||||
| `both` | les deux — c'est ce que donne `initial` par défaut |
|
||||
|
||||
> 🎯 Petit conseil de ciblage : vise plutôt `#contacter` (l'id de **ta** textarea) que `textarea` tout court. Le sélecteur `textarea` toucherait **toutes** les zones de texte du site, même celles que Guillaume ajoutera sur sa page.
|
||||
|
||||
> 💡 **À retenir :** `resize` = **la direction** autorisée. `min-height`/`max-height` (et `min-width`/`max-width`) = **les bornes**. Les deux ensemble = un redimensionnement libre **mais encadré**.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Petit plus — centrer un bloc sans le « pousser » avec `margin-left`
|
||||
|
||||
J'ai vu que tu centres ta table **et** ton nouveau `.form` avec `margin-left: 600px` / `620px`. Ça « marche » sur ton écran… mais sur un écran plus petit, le bloc est **poussé hors de la fenêtre** → barre de défilement horizontale. Pour **vraiment centrer** un bloc de largeur fixe :
|
||||
|
||||
```css
|
||||
.form {
|
||||
width: 35%;
|
||||
margin-left: auto; /* "auto" à gauche ET à droite = */
|
||||
margin-right: auto; /* le navigateur répartit l'espace → bloc centré */
|
||||
/* on enlève le margin-left: 620px */
|
||||
}
|
||||
```
|
||||
|
||||
> 💡 **À retenir :** `margin: auto` à gauche **et** à droite centre un bloc qui a une largeur. C'est l'outil correct ; `margin-left: 620px` ne fait que le décaler et casse l'affichage sur petit écran.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Ta première mission JavaScript : le **compteur de caractères**
|
||||
|
||||
Jusqu'ici tu as fait du HTML (la **structure**) et du CSS (l'**apparence**). Le JavaScript, c'est la **3ᵉ couche** : le **comportement**, ce qui réagit en direct. 🧠
|
||||
|
||||
On va le faire **en mode tuto** : tu construis petit bout par petit bout, et **tu testes à chaque étape**. Ne copie pas tout d'un coup — avance palier par palier, c'est comme ça qu'on comprend. 🙂
|
||||
|
||||
**Objectif final :** `maxlength` empêche de dépasser 500 caractères… mais l'utilisateur ne **voit pas** combien il lui en reste. On va afficher un **compteur en direct** : « Il vous reste 437 caractères ». C'est exactement ce que font Twitter/X, les SMS, etc.
|
||||
|
||||
---
|
||||
|
||||
### 🪜 Étape 0 — Découvrir la console (ton meilleur ami)
|
||||
|
||||
Ouvre ta page dans le navigateur, puis appuie sur **F12** → onglet **Console**. C'est là que le JS « parle ».
|
||||
|
||||
Crée un fichier `contact.js` avec une seule ligne :
|
||||
|
||||
```js
|
||||
console.log("Mon fichier JS est bien chargé !");
|
||||
```
|
||||
|
||||
Et branche-le en bas du `<body>`, **juste avant** `</body>` :
|
||||
|
||||
```html
|
||||
<script src="contact.js"></script>
|
||||
```
|
||||
|
||||
> 🔎 Pourquoi à la fin du body ? Parce que le JS s'exécute de haut en bas. S'il se lance **avant** que le HTML existe, il ne trouve rien à manipuler. À la fin, tout le HTML est déjà là. 👍
|
||||
|
||||
**✅ Teste maintenant :** recharge la page. Tu dois voir ton message dans la Console. Si oui, ton JS est branché. Sinon, vérifie le chemin du `src`.
|
||||
|
||||
---
|
||||
|
||||
### 🪜 Étape 1 — Ajouter l'affichage du compteur
|
||||
|
||||
Sous ta textarea, ajoute la ligne qui affichera le décompte :
|
||||
|
||||
```html
|
||||
<textarea id="contacter" name="contacter" rows="6" maxlength="500"></textarea>
|
||||
<p>Il vous reste <span id="restant">500</span> caractères.</p>
|
||||
```
|
||||
|
||||
**✅ Teste maintenant :** tu dois lire « Il vous reste 500 caractères ». Pour l'instant ce 500 est figé, on va le rendre vivant.
|
||||
|
||||
> 🔎 Le `<span id="restant">` est juste une petite « boîte » dans laquelle le JS viendra écrire le nombre. Son `id` est l'étiquette par laquelle on va l'attraper.
|
||||
|
||||
---
|
||||
|
||||
### 🪜 Étape 2 — Attraper les éléments depuis le JS
|
||||
|
||||
Dans `contact.js`, remplace ton `console.log` de test par :
|
||||
|
||||
```js
|
||||
const zoneMessage = document.getElementById("contacter"); // ta textarea
|
||||
const compteur = document.getElementById("restant"); // le <span>
|
||||
|
||||
console.log(zoneMessage); // pour vérifier qu'on l'a bien trouvée
|
||||
```
|
||||
|
||||
**✅ Teste maintenant :** dans la Console tu dois voir s'afficher ta `<textarea>`. Si tu vois `null`, l'id ne correspond pas → vérifie l'orthographe (`contacter`).
|
||||
|
||||
> 🧩 **Idée clé n°1 — SÉLECTIONNER.** `document.getElementById("contacter")` veut dire : « va me chercher l'élément dont l'id est *contacter* ». On le range dans une variable pour s'en servir après.
|
||||
|
||||
---
|
||||
|
||||
### 🪜 Étape 3 — Réagir quand on tape
|
||||
|
||||
Ajoute en dessous :
|
||||
|
||||
```js
|
||||
zoneMessage.addEventListener("input", function () {
|
||||
console.log("Tu viens de taper quelque chose !");
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste maintenant :** clique dans la zone et tape quelques lettres. À **chaque** touche, un message apparaît dans la Console. 🎉
|
||||
|
||||
> 🧩 **Idée clé n°2 — RÉAGIR.** `addEventListener("input", … )` se lit : « écoute les saisies dans cette zone, et à chaque frappe, exécute cette fonction ». (`"input"` = « le contenu a changé », parfait pour une textarea.)
|
||||
|
||||
---
|
||||
|
||||
### 🪜 Étape 4 — Calculer et afficher ce qu'il reste
|
||||
|
||||
Maintenant on remplace le `console.log` par le vrai calcul. Voici le fichier complet :
|
||||
|
||||
```js
|
||||
// 1. On attrape la textarea et la "boîte" où afficher le compteur
|
||||
const zoneMessage = document.getElementById("contacter");
|
||||
const compteur = document.getElementById("restant");
|
||||
const MAX = 500;
|
||||
|
||||
// 2. À chaque frappe, on recalcule ce qu'il reste
|
||||
zoneMessage.addEventListener("input", function () {
|
||||
const dejaTape = zoneMessage.value.length; // .value = le texte tapé, .length = sa longueur
|
||||
compteur.textContent = MAX - dejaTape; // on écrit le résultat dans le <span>
|
||||
});
|
||||
```
|
||||
|
||||
> 🧩 **Idée clé n°3 — MODIFIER.** `compteur.textContent = …` réécrit le texte affiché **en direct**, sans recharger la page. C'est ça, la magie du JS.
|
||||
>
|
||||
> 🔎 `zoneMessage.value`, c'est le **contenu tapé** par l'utilisateur ; `.length` en donne le **nombre de caractères**. `500 − ce nombre` = ce qu'il reste.
|
||||
|
||||
**✅ Teste maintenant :** tape du texte → le nombre doit **descendre** à chaque lettre (499, 498, …) et **s'arrêter à 0** quand tu atteins la limite. Si oui : bravo, tu viens d'écrire ta première vraie logique en JavaScript ! 🎉
|
||||
|
||||
> 🎯 **Défis bonus** (dans l'ordre de difficulté) :
|
||||
> 1. Passe le compteur en **rouge** quand il reste moins de 50 caractères :
|
||||
> ```js
|
||||
> compteur.style.color = (MAX - dejaTape) < 50 ? "red" : "black";
|
||||
> ```
|
||||
> 2. Affiche le texte « caractères » au singulier quand il reste 1 ou 0.
|
||||
> 3. Désactive le bouton « Envoyer » tant que le message est vide.
|
||||
|
||||
---
|
||||
|
||||
# 🎓 Niveau 2 — Les conditions (et de nouveaux exos JS)
|
||||
|
||||
**Bravo Mélissa** 👏 — tu as fait marcher le compteur **et** le bonus couleur (le compteur qui passe en rouge). Tu es prête pour la suite : les **conditions**, c'est-à-dire apprendre au programme à **choisir** quoi faire selon la situation.
|
||||
|
||||
> 🩹 **Petite correction au passage** (vu dans ton HTML) : ton champ e-mail est `type="e_mail"`. Ce type n'existe pas → le navigateur le traite comme du texte normal. Le bon, c'est `type="email"` (sans underscore). Avec, le navigateur vérifie tout seul qu'il y a bien un `@`. 😉
|
||||
|
||||
---
|
||||
|
||||
## 🧠 La leçon : `if` / `else` — faire **choisir** le programme
|
||||
|
||||
Une **condition**, c'est une question qui n'a que deux réponses : **vrai** ou **faux**. En JavaScript on écrit :
|
||||
|
||||
```js
|
||||
if (/* une question */) {
|
||||
// ce qu'on fait SI la réponse est "vrai"
|
||||
} else {
|
||||
// ce qu'on fait SINON
|
||||
}
|
||||
```
|
||||
|
||||
Pour poser la question, on utilise des **opérateurs de comparaison** :
|
||||
|
||||
| Opérateur | Question posée | Exemple vrai |
|
||||
|---|---|---|
|
||||
| `===` | est **égal à** ? | `restant === 1` |
|
||||
| `!==` | est **différent de** ? | `restant !== 0` |
|
||||
| `>` `<` | plus grand / plus petit ? | `restant < 50` |
|
||||
| `>=` `<=` | plus grand/petit **ou égal** ? | `restant >= 1` |
|
||||
|
||||
> ⚠️ **Le piège n°1 des débutants :** `=` et `===` ne veulent PAS dire la même chose !
|
||||
> - `=` (un seul) → **ranger** une valeur : `restant = 5` (« mets 5 dans restant »).
|
||||
> - `===` (trois) → **comparer** : `restant === 5` (« est-ce que restant vaut 5 ? »).
|
||||
> Dans un `if (...)`, c'est **toujours** `===`.
|
||||
|
||||
---
|
||||
|
||||
## ✍️ Exo : afficher « caractère » au singulier
|
||||
|
||||
Aujourd'hui ton HTML affiche toujours « caractères » au pluriel, même quand il n'en reste qu'**un**. On va corriger ça avec une condition.
|
||||
|
||||
### Étape 1 — Donner une étiquette au mot dans le HTML
|
||||
|
||||
Pour pouvoir changer le **mot** tout seul, on l'enferme dans son propre `<span>` :
|
||||
|
||||
```html
|
||||
<p class="small">Il vous reste <span id="restant">500</span> <span id="mot">caractères</span>.</p>
|
||||
```
|
||||
|
||||
### Étape 2 — Choisir le bon mot en JavaScript
|
||||
|
||||
Dans `contact.js`, attrape ce nouveau `<span>` (en haut, avec les autres) :
|
||||
|
||||
```js
|
||||
const motCaractere = document.getElementById("mot");
|
||||
```
|
||||
|
||||
Puis, **à l'intérieur** de ton `addEventListener("input", …)`, ajoute la condition :
|
||||
|
||||
```js
|
||||
zoneMessage.addEventListener("input", function () {
|
||||
const dejaTape = zoneMessage.value.length;
|
||||
const restant = MAX - dejaTape; // on range le calcul dans une variable, plus lisible
|
||||
|
||||
compteur.textContent = restant;
|
||||
compteur.style.color = restant < 50 ? "red" : "black";
|
||||
|
||||
// 👇 la nouvelle condition
|
||||
if (restant === 1) {
|
||||
motCaractere.textContent = "caractère"; // singulier
|
||||
} else {
|
||||
motCaractere.textContent = "caractères"; // pluriel
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste maintenant :** tape jusqu'à ce qu'il reste exactement **1** → tu dois lire « Il vous reste 1 **caractère** ». À 2 ou plus → « caractères ». 🎉
|
||||
|
||||
> 🔎 **Tu connaissais déjà une condition sans le savoir !** Ton bonus couleur :
|
||||
> ```js
|
||||
> compteur.style.color = restant < 50 ? "red" : "black";
|
||||
> ```
|
||||
> C'est exactement un `if`/`else`… en **version courte** (on l'appelle le *ternaire*). La version longue équivalente serait :
|
||||
> ```js
|
||||
> if (restant < 50) {
|
||||
> compteur.style.color = "red";
|
||||
> } else {
|
||||
> compteur.style.color = "black";
|
||||
> }
|
||||
> ```
|
||||
> Les deux font la même chose. Le `? :` est juste un raccourci pratique quand il n'y a qu'**une** ligne à choisir. 👍
|
||||
|
||||
---
|
||||
|
||||
## 🧪 De nouveaux exos pour découvrir plus de JavaScript
|
||||
|
||||
Chaque exo t'apprend **une nouvelle notion**. Fais-les dans l'ordre, et teste à chaque fois.
|
||||
|
||||
### 🧪 Exo 1 — Empêcher l'envoi d'un message vide
|
||||
|
||||
> 🆕 Nouvelle notion : réagir à l'**envoi du formulaire** (`"submit"`) et **bloquer** l'action par défaut.
|
||||
|
||||
Aujourd'hui, si on clique « Envoyer » avec un message vide, le formulaire part quand même. On va l'en empêcher.
|
||||
|
||||
```js
|
||||
const formulaire = document.querySelector("form"); // on attrape le <form>
|
||||
|
||||
formulaire.addEventListener("submit", function (evenement) {
|
||||
if (zoneMessage.value.trim() === "") { // .trim() enlève les espaces autour
|
||||
evenement.preventDefault(); // ⛔ on ANNULE l'envoi
|
||||
alert("Merci d'écrire un message avant d'envoyer !");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste :** clique « Envoyer » sans rien écrire → une alerte apparaît et la page ne se recharge pas. Écris un message → l'envoi repart normalement.
|
||||
|
||||
> 🔎 `evenement` est une info que le navigateur passe à ta fonction (« voici ce qui s'est passé »). `evenement.preventDefault()` veut dire « n'effectue PAS l'action habituelle » (ici : ne soumets pas le formulaire). `.trim()` sert à considérer « 3 espaces » comme un message vide.
|
||||
|
||||
### 🧪 Exo 2 — Un bouton « Effacer le message »
|
||||
|
||||
> 🆕 Nouvelle notion : **ranger du code dans une fonction** pour le réutiliser (ne pas se répéter).
|
||||
|
||||
Ajoute un bouton dans le HTML, à côté de « Envoyer » :
|
||||
|
||||
```html
|
||||
<button type="button" id="effacer">Effacer</button>
|
||||
```
|
||||
|
||||
Le souci : quand on efface, il faut aussi **remettre le compteur à 500**. On a donc besoin de réafficher le compteur à **deux endroits** (à la frappe ET à l'effacement). La bonne pratique : écrire ce calcul **une seule fois** dans une fonction, et l'appeler aux deux endroits.
|
||||
|
||||
```js
|
||||
const boutonEffacer = document.getElementById("effacer");
|
||||
|
||||
// une fonction qui remet l'affichage à jour à partir du texte actuel
|
||||
function rafraichirCompteur() {
|
||||
const restant = MAX - zoneMessage.value.length;
|
||||
compteur.textContent = restant;
|
||||
compteur.style.color = restant < 50 ? "red" : "black";
|
||||
motCaractere.textContent = restant === 1 ? "caractère" : "caractères";
|
||||
}
|
||||
|
||||
// à la frappe : on rafraîchit
|
||||
zoneMessage.addEventListener("input", rafraichirCompteur);
|
||||
|
||||
// au clic sur Effacer : on vide la zone PUIS on rafraîchit
|
||||
boutonEffacer.addEventListener("click", function () {
|
||||
zoneMessage.value = ""; // on vide la textarea
|
||||
rafraichirCompteur(); // et on remet le compteur à 500
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste :** écris du texte, clique « Effacer » → la zone se vide **et** le compteur repasse à 500.
|
||||
|
||||
> 🔎 Tu viens de **factoriser** : au lieu de recopier les mêmes lignes deux fois, tu les as mises dans `rafraichirCompteur()` et tu l'appelles. Si un jour tu changes l'affichage, tu ne le modifies **qu'à un seul endroit**. C'est exactement la même idée que le **composant réutilisable** plus bas. ♻️
|
||||
|
||||
### 🧪 Exo 3 — « FleetZen est-il joignable maintenant ? »
|
||||
|
||||
> 🆕 Nouvelle notion : lire l'**heure actuelle** (`new Date()`) et combiner deux conditions avec `&&` (ET).
|
||||
|
||||
Une page contact, ça parle d'horaires. Affichons un petit badge « ✅ Ouvert » ou « 🔴 Fermé » selon l'heure (ouvert de 9h à 18h).
|
||||
|
||||
D'abord un endroit pour l'afficher, dans le HTML :
|
||||
|
||||
```html
|
||||
<p id="statut_horaires"></p>
|
||||
```
|
||||
|
||||
Puis en JavaScript :
|
||||
|
||||
```js
|
||||
const badge = document.getElementById("statut_horaires");
|
||||
const heure = new Date().getHours(); // l'heure actuelle, de 0 à 23
|
||||
|
||||
if (heure >= 9 && heure < 18) { // && = "ET" : les DEUX doivent être vraies
|
||||
badge.textContent = "✅ Nous sommes ouverts (9h–18h)";
|
||||
badge.style.color = "green";
|
||||
} else {
|
||||
badge.textContent = "🔴 Fermé — écrivez-nous, on répond dès l'ouverture !";
|
||||
badge.style.color = "red";
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Teste :** recharge la page. Selon l'heure qu'il est, tu vois « Ouvert » ou « Fermé ». (Pour tester l'autre cas sans attendre, change temporairement `9` et `18`.)
|
||||
|
||||
> 🔎 `new Date().getHours()` donne l'heure de l'ordinateur (ex. `14`). `&&` se lit « ET » : `heure >= 9 && heure < 18` n'est vrai que si l'heure est **à la fois** ≥ 9 **et** < 18. Son cousin est `||` qui se lit « OU ».
|
||||
|
||||
---
|
||||
|
||||
> 🗺️ **Où tu en es maintenant** : tu sais **sélectionner**, **réagir**, **modifier**, et maintenant **faire choisir** ton programme avec des conditions, **factoriser** dans des fonctions, et lire des infos comme l'heure. C'est déjà le cœur du JavaScript du quotidien. La suite (boucles `for`, tableaux) viendra naturellement — demande-moi quand tu veux ! 🚀
|
||||
|
||||
---
|
||||
|
||||
## ♻️ Bonus — La notion de **composant réutilisable** (la navbar)
|
||||
|
||||
Regarde : la barre du haut (`#nav_barre`) est **recopiée à l'identique** dans `index.html`, `services.html` et `contact.html`. Le jour où on change un lien, il faut le modifier **dans 3 fichiers** → source d'erreurs (c'est d'ailleurs comme ça que le Bug 2 se glisse 😉).
|
||||
|
||||
C'est **le** problème que résolvent les **composants** : on écrit la navbar **une seule fois**, et on la réutilise partout.
|
||||
|
||||
Première approche très simple en JavaScript : une fonction qui **génère** la navbar.
|
||||
|
||||
```js
|
||||
function creerNavbar() {
|
||||
return `
|
||||
<div id="nav_barre" class="shadow">
|
||||
<h1>FleetZen</h1>
|
||||
<div id="buttons">
|
||||
<button class="instyled">contact</button>
|
||||
<a href="services.html"><button class="styled">services</button></a>
|
||||
<a href="../index.html"><button class="styled">retour</button></a>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// On l'injecte tout en haut du body :
|
||||
document.body.insertAdjacentHTML("afterbegin", creerNavbar());
|
||||
```
|
||||
|
||||
**✅ Teste :** enlève la navbar écrite « en dur » dans le HTML, garde ce JS, recharge → la navbar réapparaît, générée par la fonction. Maintenant le jour où tu changes un lien, tu le changes **à un seul endroit** pour **toutes** les pages (fini les liens cassés du Bug 2 😉).
|
||||
|
||||
> 💡 **À retenir :** un **composant** = un bout d'interface écrit **une fois** et **réutilisé** partout. C'est l'idée centrale derrière React, Vue, etc. — mais ça commence par une simple fonction comme celle-ci.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Ta checklist
|
||||
|
||||
- [ ] Fermer `body` avec un `}` dans le CSS (Bug 1 — le plus urgent)
|
||||
- [ ] Réparer le lien vers `services.html` (Bug 2)
|
||||
- [ ] Passer le champ e-mail en `type="email"` (Bug 3 — tu as mis `e_mail`, presque ! 😉)
|
||||
- [ ] Centrer l'icône SVG du bouton « retour » avec `inline-flex` (Bug 4)
|
||||
- [ ] Ajouter le redimensionnement borné (`resize: vertical` + `min/max-height`)
|
||||
- [ ] Centrer table et `.form` avec `margin: auto` (au lieu de `margin-left: 600px`)
|
||||
- [x] Ajouter `rows`, `cols`, `maxlength` à la textarea ✅
|
||||
- [x] Faire marcher le **compteur de caractères** en JavaScript ✅
|
||||
- [x] Bonus couleur : compteur en rouge sous 50 caractères ✅
|
||||
- [x] Ajouter un `<footer>` ✅
|
||||
- [ ] (Bonus) Transformer la navbar en composant réutilisable
|
||||
- [ ] Penser à fermer la balise `</html>` en fin de fichier
|
||||
|
||||
### 🎓 Niveau 2 (nouveau)
|
||||
- [ ] Afficher « caractère » au **singulier** quand il en reste 1 (leçon `if`/`else`)
|
||||
- [ ] Exo 1 — empêcher l'envoi d'un message **vide** (`submit` + `preventDefault`)
|
||||
- [ ] Exo 2 — bouton **« Effacer »** + factoriser dans une fonction
|
||||
- [ ] Exo 3 — badge **« Ouvert / Fermé »** selon l'heure (`new Date()` + `&&`)
|
||||
- [ ] Commits petits et clairs, puis `git push`
|
||||
|
||||
Pose-moi toutes tes questions. Tu gères ! 📬✨
|
||||
@@ -1,456 +0,0 @@
|
||||
# 🚀 Niveau 3 — JavaScript : tableaux, boucles & pages vivantes
|
||||
|
||||
Salut Mélissa ! 👋
|
||||
|
||||
Tu as fini tout le Niveau 2 (conditions, singulier/pluriel, envoi bloqué si vide, bouton « Effacer », badge horaires, navbar en composant). On enchaîne.
|
||||
|
||||
> 🔎 Détail au passage : ton `restant <= 1 ? "caractère" : "caractères"` est correct — en français, **0 prend le singulier** (« 0 caractère »).
|
||||
|
||||
Ce fichier est une série d'exercices **qui s'enchaînent**, chacun introduisant **une notion de plus**. Fil rouge : rendre la page **vivante et pilotée par des données**. Il y en a beaucoup, de quoi tenir l'après-midi — tu n'es pas obligée de tout finir.
|
||||
|
||||
> 🧭 **Mode d'emploi** (toujours le même) :
|
||||
> - Avance **dans l'ordre**, un exo à la fois.
|
||||
> - **Teste** (✅) avant de passer au suivant.
|
||||
> - Garde la **Console (F12)** ouverte : c'est ton tableau de bord.
|
||||
> - Bloquée ? C'est **normal** : note ta question et on en parle.
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Le programme de l'après-midi
|
||||
|
||||
| Module | Tu vas apprendre… | Exos |
|
||||
|---|---|---|
|
||||
| **A. Tableaux & boucles** | stocker une liste, la parcourir, générer du HTML automatiquement | 1 → 4 |
|
||||
| **B. Manipuler les listes** | rechercher, trier, filtrer | 5 → 6 |
|
||||
| **C. La mémoire** | sauvegarder des infos qui survivent au rechargement | 7 → 8 |
|
||||
| **D. Le style dynamique** | changer l'apparence au clic, faire un accordéon | 9 → 10 |
|
||||
| **E. Pour finir en beauté** | factoriser, paramétrer, défis libres | 11 → 12 |
|
||||
|
||||
---
|
||||
|
||||
# 🧱 Module A — Les tableaux et les boucles
|
||||
|
||||
Jusqu'ici tes variables contenaient **une seule** valeur (`const MAX = 500`). Mais comment ranger **une liste** ? Le nom de tes 2 développeurs, par exemple ? → avec un **tableau** (`array`).
|
||||
|
||||
## 🧩 La notion : un tableau, c'est une liste
|
||||
|
||||
```js
|
||||
const fruits = ["pomme", "banane", "cerise"];
|
||||
|
||||
console.log(fruits[0]); // "pomme" → on compte à partir de 0 !
|
||||
console.log(fruits.length); // 3 → combien d'éléments
|
||||
```
|
||||
|
||||
> 🔎 **Point qui surprend tout le monde au début :** on compte **à partir de 0**. Le 1ᵉ élément est `fruits[0]`, le 2ᵉ est `fruits[1]`… Le dernier est donc `fruits[fruits.length - 1]`.
|
||||
|
||||
## 🧩 La notion : une boucle, pour répéter sans copier-coller
|
||||
|
||||
Pour faire quelque chose **avec chaque** élément d'une liste, on utilise une **boucle** `for...of` :
|
||||
|
||||
```js
|
||||
for (const fruit of fruits) {
|
||||
console.log("J'aime la " + fruit);
|
||||
}
|
||||
// affiche 3 lignes, une par fruit
|
||||
```
|
||||
|
||||
> 💡 **À retenir :** `for...of` se lit « **pour chaque** *fruit* **parmi** *fruits*, fais… ». La boucle s'occupe de passer d'un élément au suivant toute seule.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 1 — Ta première liste, dans la console
|
||||
|
||||
Crée un fichier `equipe.js` (on garde ton `contact.js` pour le formulaire). Branche-le dans ta page, **avant** `contact.js`, juste avant `</body>` :
|
||||
|
||||
```html
|
||||
<script src="../equipe.js"></script>
|
||||
<script src="../contact.js"></script>
|
||||
```
|
||||
|
||||
Dans `equipe.js` :
|
||||
|
||||
```js
|
||||
const equipe = ["Mélissa", "Guillaume"];
|
||||
|
||||
for (const personne of equipe) {
|
||||
console.log("Membre de l'équipe : " + personne);
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Teste :** ouvre la Console (F12). Tu dois voir 2 lignes. Ajoute un 3ᵉ prénom au tableau, recharge → 3 lignes, **sans toucher à la boucle**.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 2 — Des données plus riches : les **objets**
|
||||
|
||||
Un prénom tout seul, c'est peu. Pour décrire une personne (nom **+** job **+** partie créée), on utilise un **objet** `{ … }` :
|
||||
|
||||
```js
|
||||
const membre = {
|
||||
nom: "Mélissa",
|
||||
job: "Stagiaire",
|
||||
partie: "Page contact"
|
||||
};
|
||||
|
||||
console.log(membre.nom); // "Mélissa"
|
||||
console.log(membre.partie); // "Page contact"
|
||||
```
|
||||
|
||||
Et une **liste d'objets** = un tableau d'objets. Remplace le contenu de `equipe.js` :
|
||||
|
||||
```js
|
||||
const equipe = [
|
||||
{ nom: "Mélissa", job: "Stagiaire", partie: "Page contact" },
|
||||
{ nom: "Guillaume", job: "Stagiaire", partie: "Page services" }
|
||||
];
|
||||
|
||||
for (const membre of equipe) {
|
||||
console.log(membre.nom + " — " + membre.job + " — " + membre.partie);
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Teste :** la Console affiche une ligne par membre, avec les 3 infos.
|
||||
|
||||
> 🧩 **À retenir :** un **tableau** `[ ]` = une liste ordonnée. Un **objet** `{ }` = une fiche avec des **étiquettes** (`nom`, `job`…). Les deux combinés (`[ {…}, {…} ]`), c'est **la** façon de stocker des données dans presque toutes les applis.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 3 — Générer ton tableau HTML **automatiquement**
|
||||
|
||||
C'est l'exercice central du module. Aujourd'hui, ton `<table>` d'équipe est **écrit à la main** dans le HTML. Problème : pour ajouter un membre, il faut retaper tout un `<tr>`. On va le faire **générer par le JavaScript** à partir du tableau `equipe`.
|
||||
|
||||
### Étape 1 — Préparer le HTML
|
||||
|
||||
Dans `contact.html`, garde l'en-tête du tableau (`<thead>`) mais **vide le corps** et donne-lui un `id` :
|
||||
|
||||
```html
|
||||
<table border="1" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>Job</th>
|
||||
<th>Parties créées</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="corps_equipe">
|
||||
<!-- vide : le JavaScript va le remplir tout seul -->
|
||||
</tbody>
|
||||
</table>
|
||||
```
|
||||
|
||||
### Étape 2 — Construire les lignes en JavaScript
|
||||
|
||||
Dans `equipe.js` :
|
||||
|
||||
```js
|
||||
const corps = document.getElementById("corps_equipe");
|
||||
let lignes = ""; // on va empiler le HTML dans ce texte
|
||||
|
||||
for (const membre of equipe) {
|
||||
lignes = lignes + `
|
||||
<tr>
|
||||
<td>${membre.nom}</td>
|
||||
<td>${membre.job}</td>
|
||||
<td>${membre.partie}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
corps.innerHTML = lignes; // on injecte tout d'un coup
|
||||
```
|
||||
|
||||
**✅ Teste :** ton tableau s'affiche comme avant, mais cette fois généré par le JS. La preuve : ajoute un 3ᵉ membre dans le tableau `equipe` (ex. `{ nom: "Matthieu", job: "Tuteur", partie: "Relecture" }`), recharge → la ligne apparaît toute seule.
|
||||
|
||||
> 🔎 Deux nouveautés ici :
|
||||
> - **Le `` `texte ${variable}` ``** (avec les accents graves `` ` ``) : ça s'appelle un *template literal*. Le `${…}` insère la valeur d'une variable **dans** le texte. (Tu l'utilises déjà dans `creerNavbar` 😉 !)
|
||||
> - **`.innerHTML = …`** remplace **tout le contenu HTML** d'un élément. Pratique, mais à réserver à **tes propres** données (jamais avec du texte tapé par un inconnu — question de sécurité, on en reparlera).
|
||||
|
||||
> 🔎 **Le principe à retenir :** on sépare les **données** (le tableau `equipe`) de l'**affichage** (la boucle qui fabrique le HTML). Change les données → l'affichage suit. C'est le mode de fonctionnement de React, Vue, etc.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 4 — Compter et afficher un total
|
||||
|
||||
Au-dessus du tableau, ajoute dans le HTML :
|
||||
|
||||
```html
|
||||
<p>👥 L'équipe compte <span id="nb_membres"></span> personne(s).</p>
|
||||
```
|
||||
|
||||
Et en JS, après ta boucle :
|
||||
|
||||
```js
|
||||
document.getElementById("nb_membres").textContent = equipe.length;
|
||||
```
|
||||
|
||||
**✅ Teste :** le nombre correspond à la taille de ton tableau. Ajoute/enlève un membre → le compte suit tout seul.
|
||||
|
||||
> 💡 `equipe.length`, tu l'avais déjà croisé avec `zoneMessage.value.length` : `.length`, c'est « **combien** ». Sur un texte = nombre de caractères ; sur un tableau = nombre d'éléments.
|
||||
|
||||
---
|
||||
|
||||
# 🔎 Module B — Rechercher et trier dans une liste
|
||||
|
||||
## 🧪 Exo 5 — Une **barre de recherche** qui filtre l'équipe en direct
|
||||
|
||||
> 🆕 Nouvelles notions : `.filter()`, `.toLowerCase()`, `.includes()`, et **ré-afficher** quand les données changent.
|
||||
|
||||
D'abord, range ton affichage du tableau dans une **fonction** (pour pouvoir le rejouer) :
|
||||
|
||||
```js
|
||||
function afficherEquipe(liste) {
|
||||
let lignes = "";
|
||||
for (const membre of liste) {
|
||||
lignes += `
|
||||
<tr>
|
||||
<td>${membre.nom}</td>
|
||||
<td>${membre.job}</td>
|
||||
<td>${membre.partie}</td>
|
||||
</tr>`;
|
||||
}
|
||||
corps.innerHTML = lignes;
|
||||
}
|
||||
|
||||
afficherEquipe(equipe); // affichage initial : toute l'équipe
|
||||
```
|
||||
|
||||
> 🔎 `lignes += "…"` est un raccourci pour `lignes = lignes + "…"`. Pratique pour empiler.
|
||||
|
||||
Ajoute un champ de recherche dans le HTML, au-dessus du tableau :
|
||||
|
||||
```html
|
||||
<input type="search" id="recherche" placeholder="Rechercher un membre…">
|
||||
```
|
||||
|
||||
Puis en JS :
|
||||
|
||||
```js
|
||||
const champRecherche = document.getElementById("recherche");
|
||||
|
||||
champRecherche.addEventListener("input", function () {
|
||||
const motCherche = champRecherche.value.toLowerCase();
|
||||
|
||||
// on garde seulement les membres dont le nom contient le texte tapé
|
||||
const resultats = equipe.filter(function (membre) {
|
||||
return membre.nom.toLowerCase().includes(motCherche);
|
||||
});
|
||||
|
||||
afficherEquipe(resultats); // on ré-affiche, mais filtré
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste :** tape « mé » → seule Mélissa reste. Efface → toute l'équipe revient.
|
||||
|
||||
> 🧩 **À retenir :**
|
||||
> - `.filter(...)` crée un **nouveau** tableau avec seulement les éléments qui passent le test.
|
||||
> - `.toLowerCase()` met en minuscules → la recherche ne tient pas compte des majuscules.
|
||||
> - `.includes("…")` répond vrai/faux : « ce texte contient-il … ? ».
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 6 — Un bouton « Trier par nom »
|
||||
|
||||
> 🆕 Nouvelle notion : `.sort()`.
|
||||
|
||||
```html
|
||||
<button type="button" id="trier">Trier A → Z</button>
|
||||
```
|
||||
|
||||
```js
|
||||
document.getElementById("trier").addEventListener("click", function () {
|
||||
const triee = [...equipe].sort(function (a, b) {
|
||||
return a.nom.localeCompare(b.nom);
|
||||
});
|
||||
afficherEquipe(triee);
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste :** clique → l'équipe s'affiche dans l'ordre alphabétique.
|
||||
|
||||
> 🔎 `[...equipe]` fait une **copie** du tableau (pour ne pas modifier l'original). `localeCompare` compare deux mots dans l'ordre alphabétique (et gère les accents français correctement). C'est un peu abstrait : retiens juste la **recette**, tu la comprendras à force de l'utiliser.
|
||||
|
||||
---
|
||||
|
||||
# 💾 Module C — Donner une mémoire à ta page
|
||||
|
||||
Recharge ta page : tout ce que l'utilisateur avait tapé **disparaît**. On va y remédier avec le **`localStorage`** : un petit coffre-fort dans le navigateur qui **survit au rechargement**.
|
||||
|
||||
## 🧪 Exo 7 — Sauvegarder le brouillon du message
|
||||
|
||||
> 🆕 Nouvelle notion : `localStorage.setItem(...)` / `localStorage.getItem(...)`.
|
||||
|
||||
Dans `contact.js`, ajoute : à **chaque frappe**, on sauvegarde le message.
|
||||
|
||||
```js
|
||||
zoneMessage.addEventListener("input", function () {
|
||||
localStorage.setItem("brouillon", zoneMessage.value);
|
||||
});
|
||||
```
|
||||
|
||||
> ⚠️ Tu as **déjà** un `addEventListener("input", rafraichirCompteur)` sur la même zone. Pas de souci : on peut en mettre plusieurs, ils s'exécutent tous les deux. (Encore mieux : ajoute la ligne `localStorage.setItem(...)` **à l'intérieur** de `rafraichirCompteur`.)
|
||||
|
||||
**✅ Teste :** tape un message, ouvre la Console et tape `localStorage.getItem("brouillon")` → tu vois ton texte.
|
||||
|
||||
## 🧪 Exo 8 — Recharger le brouillon au démarrage
|
||||
|
||||
Au tout début (au chargement de la page), on regarde si un brouillon existe :
|
||||
|
||||
```js
|
||||
const brouillon = localStorage.getItem("brouillon");
|
||||
if (brouillon) { // s'il y a quelque chose de sauvegardé
|
||||
zoneMessage.value = brouillon;
|
||||
rafraichirCompteur(); // pour que le compteur soit juste dès le départ
|
||||
}
|
||||
```
|
||||
|
||||
Et quand le message est envoyé avec succès, on vide le coffre (sinon le vieux brouillon revient) :
|
||||
|
||||
```js
|
||||
// à ajouter dans ton addEventListener("submit", …), quand le message N'EST PAS vide
|
||||
localStorage.removeItem("brouillon");
|
||||
```
|
||||
|
||||
**✅ Teste :** écris un message, **recharge la page** (F5) → ton texte est toujours là, et le compteur est correct.
|
||||
|
||||
> 💡 **À retenir :** `localStorage` garde des infos **entre les visites** (jusqu'à ce qu'on les efface). `setItem(clé, valeur)` pour ranger, `getItem(clé)` pour relire, `removeItem(clé)` pour effacer.
|
||||
|
||||
---
|
||||
|
||||
# 🎨 Module D — Changer l'apparence en JavaScript
|
||||
|
||||
## 🧪 Exo 9 — Un bouton « Mode sombre 🌙 »
|
||||
|
||||
> 🆕 Nouvelle notion : `classList.toggle(...)` — ajouter/retirer une classe CSS au clic.
|
||||
|
||||
D'abord, prépare la classe dans ton CSS (`contact.css`) :
|
||||
|
||||
```css
|
||||
body.sombre {
|
||||
background-color: #1d2733;
|
||||
color: white;
|
||||
}
|
||||
```
|
||||
|
||||
Un bouton dans le HTML :
|
||||
|
||||
```html
|
||||
<button type="button" id="theme">🌙 Mode sombre</button>
|
||||
```
|
||||
|
||||
Et le JS :
|
||||
|
||||
```js
|
||||
document.getElementById("theme").addEventListener("click", function () {
|
||||
document.body.classList.toggle("sombre");
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste :** clique → la page bascule en sombre, re-clique → elle revient. Un seul `toggle` fait les deux !
|
||||
|
||||
> 🧩 **À retenir :** `classList.toggle("sombre")` = « si la classe est là, enlève-la ; sinon, ajoute-la ». C'est **le** pont entre ton JavaScript et ton CSS : le JS gère le *quand*, le CSS gère le *à quoi ça ressemble*.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 10 — Une FAQ en accordéon
|
||||
|
||||
> 🆕 Nouvelles notions : `querySelectorAll(...)` (sélectionner **plusieurs** éléments) + une boucle `forEach`.
|
||||
|
||||
Beaucoup de pages contact ont une FAQ. Ajoute dans le HTML :
|
||||
|
||||
```html
|
||||
<section class="faq">
|
||||
<h3>Questions fréquentes</h3>
|
||||
<div class="question">
|
||||
<button class="q-titre" type="button">Comment suivre ma flotte en temps réel ?</button>
|
||||
<p class="q-reponse">Grâce à notre service de géolocalisation, accessible 24h/24.</p>
|
||||
</div>
|
||||
<div class="question">
|
||||
<button class="q-titre" type="button">Puis-je résilier à tout moment ?</button>
|
||||
<p class="q-reponse">Oui, sans frais, depuis votre espace client.</p>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
Dans le CSS, on cache les réponses au départ :
|
||||
|
||||
```css
|
||||
.q-reponse { display: none; }
|
||||
.q-reponse.ouverte { display: block; }
|
||||
```
|
||||
|
||||
En JavaScript, on veut que **chaque** bouton ouvre/ferme **sa** réponse :
|
||||
|
||||
```js
|
||||
const titres = document.querySelectorAll(".q-titre"); // TOUS les boutons FAQ
|
||||
|
||||
titres.forEach(function (titre) {
|
||||
titre.addEventListener("click", function () {
|
||||
// la réponse est le paragraphe juste après le bouton
|
||||
const reponse = titre.nextElementSibling;
|
||||
reponse.classList.toggle("ouverte");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste :** clique sur une question → sa réponse apparaît ; re-clique → elle se cache. Chaque question est indépendante.
|
||||
|
||||
> 🧩 **À retenir :**
|
||||
> - `getElementById` attrape **un** élément ; `querySelectorAll(".classe")` en attrape **plusieurs** (tous ceux qui ont cette classe).
|
||||
> - `.forEach(...)` est une boucle qui fait quelque chose **pour chaque** élément trouvé — ici : « ajoute un écouteur de clic à chacun ».
|
||||
> - `nextElementSibling` = « l'élément juste après moi ». Pratique pour relier un bouton à sa réponse.
|
||||
|
||||
---
|
||||
|
||||
# 🏁 Module E — Pour finir en beauté
|
||||
|
||||
## 🧪 Exo 11 — Une navbar qui sait quelle page est active
|
||||
|
||||
Ta `creerNavbar()` est déjà un beau composant. Rendons-la **paramétrable** : on lui dit quelle page est active, et elle met le bon bouton en surbrillance.
|
||||
|
||||
> 🆕 Nouvelle notion : une fonction qui prend un **paramètre**.
|
||||
|
||||
```js
|
||||
function creerNavbar(pageActive) { // ← pageActive est le "réglage" qu'on lui donne
|
||||
return `
|
||||
<div id="nav_barre" class="shadow">
|
||||
<h1>FleetZen</h1>
|
||||
<div id="buttons">
|
||||
<a href="contact.html"><button class="${pageActive === "contact" ? "instyled" : "styled"}">contact</button></a>
|
||||
<a href="services.html"><button class="${pageActive === "services" ? "instyled" : "styled"}">services</button></a>
|
||||
<a href="../index.html"><button class="styled">retour</button></a>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
document.body.insertAdjacentHTML("afterbegin", creerNavbar("contact"));
|
||||
```
|
||||
|
||||
**✅ Teste :** sur la page contact, le bouton « contact » apparaît enfoncé (`instyled`). Le jour où Guillaume réutilise cette fonction sur sa page, il écrira juste `creerNavbar("services")` → c'est SON bouton qui sera actif.
|
||||
|
||||
> 💡 **À retenir :** un **paramètre** (`pageActive`), c'est un réglage qu'on passe à une fonction pour qu'elle s'adapte. Une seule fonction, plusieurs résultats selon ce qu'on lui donne. C'est ÇA, un vrai composant réutilisable. ♻️
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Exo 12 — Défis libres (si tu as encore de l'énergie)
|
||||
|
||||
Choisis ceux qui t'attirent, dans l'ordre que tu veux :
|
||||
|
||||
1. **Recap avant envoi** : à la soumission, affiche « Merci {prénom}, ton message de {N} caractères a bien été pris en compte ! » en lisant la valeur du champ prénom.
|
||||
2. **Validation e-mail maison** : si l'e-mail ne contient pas de `@`, bloque l'envoi avec un message (en plus de la vérification du navigateur).
|
||||
3. **Compteur de mots** (en plus des caractères) : `zoneMessage.value.trim().split(" ").length`.
|
||||
4. **Ajouter un membre via un formulaire** : un petit champ + bouton qui fait `equipe.push({...})` puis `afficherEquipe(equipe)`.
|
||||
5. **Mémoriser le mode sombre** dans `localStorage` pour qu'il reste actif au rechargement.
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Récap — tout ce que tu sais faire maintenant
|
||||
|
||||
- **Données** : variables, **tableaux** `[ ]`, **objets** `{ }`, listes d'objets
|
||||
- **Répéter** : boucles `for...of` et `.forEach()`
|
||||
- **Transformer des listes** : `.filter()`, `.sort()`
|
||||
- **Générer du HTML** depuis des données (`innerHTML` + *template literals*)
|
||||
- **Mémoire** : `localStorage` (setItem / getItem / removeItem)
|
||||
- **Style dynamique** : `classList.toggle()`
|
||||
- **Sélection multiple** : `querySelectorAll` + `forEach`
|
||||
- **Fonctions paramétrées** : un composant qui s'adapte
|
||||
|
||||
Prochaine grande étape (pour une autre fois) : **les requêtes vers un serveur** avec `fetch`, pour aller chercher de vraies données en ligne.
|
||||
@@ -1,239 +0,0 @@
|
||||
# 🚀 Niveau 4 — DOM propre, données distantes & temps réel
|
||||
|
||||
Salut Mélissa ! 👋
|
||||
|
||||
Suite logique de ce que tu sais déjà faire (tableaux, boucles, `localStorage`, `classList`, événements…). Quatre modules, du plus simple au plus costaud. Comme d'habitude : un exo à la fois, et **✅ teste** avant de passer au suivant.
|
||||
|
||||
| Module | Notion nouvelle | Exos |
|
||||
|---|---|---|
|
||||
| **A. Construire le DOM proprement** | `createElement` / `append` (au lieu de `innerHTML`) | 1 → 2 |
|
||||
| **B. Aller chercher des données ailleurs** | `fetch`, `async`/`await`, JSON | 3 → 5 |
|
||||
| **C. Le temps réel** | `setInterval` | 6 → 7 |
|
||||
| **D. Le clavier** | événements clavier (`keydown`, `event.key`) | 8 |
|
||||
|
||||
---
|
||||
|
||||
# 🧱 Module A — Construire le HTML sans `innerHTML`
|
||||
|
||||
Pour ton tableau d'équipe, tu utilises `corps.innerHTML = lignes`. Ça marche très bien pour **tes** données. Mais `innerHTML` a deux limites :
|
||||
- si tu y mets du texte tapé par un **inconnu**, il peut injecter du code (faille de sécurité dite *XSS*) ;
|
||||
- ça remplace **tout** d'un coup, difficile de modifier une seule ligne ensuite.
|
||||
|
||||
La méthode « propre » consiste à fabriquer chaque élément **un par un**.
|
||||
|
||||
## 🧩 La notion : `createElement` + `textContent` + `append`
|
||||
|
||||
```js
|
||||
const ligne = document.createElement("tr"); // on crée une balise <tr> (vide, pas encore dans la page)
|
||||
|
||||
const caseNom = document.createElement("td");
|
||||
caseNom.textContent = "Mélissa"; // textContent = du texte pur, jamais interprété comme du HTML
|
||||
|
||||
ligne.append(caseNom); // on range la <td> dans la <tr>
|
||||
```
|
||||
|
||||
> 🔎 `createElement` fabrique l'élément **en mémoire** ; il n'apparaît à l'écran qu'une fois **rattaché** à la page avec `append`. `textContent` (et non `innerHTML`) garantit que le contenu reste du **texte**, même si quelqu'un tape `<script>`.
|
||||
|
||||
## 🧪 Exo 1 — Réécrire une ligne du tableau « proprement »
|
||||
|
||||
Dans `equipe.js`, écris une fonction qui fabrique **une** ligne à partir d'un membre :
|
||||
|
||||
```js
|
||||
function creerLigne(membre) {
|
||||
const tr = document.createElement("tr");
|
||||
|
||||
const tdNom = document.createElement("td");
|
||||
tdNom.textContent = membre.nom;
|
||||
|
||||
const tdJob = document.createElement("td");
|
||||
tdJob.textContent = membre.job;
|
||||
|
||||
const tdPartie = document.createElement("td");
|
||||
tdPartie.textContent = membre.partie;
|
||||
|
||||
tr.append(tdNom, tdJob, tdPartie); // append accepte plusieurs éléments d'un coup
|
||||
return tr;
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 Exo 2 — Remplir le tableau avec cette fonction
|
||||
|
||||
```js
|
||||
const corps = document.getElementById("corps_equipe");
|
||||
|
||||
corps.innerHTML = ""; // on vide d'abord
|
||||
for (const membre of equipe) {
|
||||
corps.append(creerLigne(membre)); // on ajoute chaque ligne fabriquée
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Teste :** le tableau s'affiche comme avant. La différence est invisible à l'écran, mais ton code est maintenant plus sûr et chaque ligne est un vrai objet manipulable.
|
||||
|
||||
> 💡 **À retenir :** `innerHTML` = rapide, pour **tes** données fixes. `createElement` + `textContent` = la méthode propre, surtout dès qu'il y a du contenu venant de l'**utilisateur**.
|
||||
|
||||
---
|
||||
|
||||
# 🌐 Module B — Aller chercher des données ailleurs (`fetch`)
|
||||
|
||||
Jusqu'ici tes données (`equipe`) sont **écrites dans le code**. Dans la vraie vie, elles arrivent **d'un serveur**, sous forme de **JSON** (un format texte très proche des objets/tableaux que tu connais déjà).
|
||||
|
||||
## ⚠️ À lire avant de commencer : il faut un petit serveur
|
||||
|
||||
`fetch` **ne fonctionne pas** si tu ouvres ta page en double-cliquant (adresse en `file://`). Il faut servir le dossier. Dans un terminal, place-toi dans le dossier du projet et lance :
|
||||
|
||||
```bash
|
||||
python3 -m http.server 8000
|
||||
```
|
||||
|
||||
Puis ouvre **http://localhost:8000/pages/contact.html** dans le navigateur. (C'est exactement ce que fait un vrai site : un serveur qui envoie les fichiers.)
|
||||
|
||||
## 🧪 Exo 3 — Créer un fichier de données JSON
|
||||
|
||||
Crée `vehicules.json` à la racine du projet :
|
||||
|
||||
```json
|
||||
[
|
||||
{ "modele": "Renault Kangoo", "immatriculation": "AB-123-CD", "km": 45200 },
|
||||
{ "modele": "Peugeot Partner", "immatriculation": "EF-456-GH", "km": 78900 },
|
||||
{ "modele": "Citroën Berlingo", "immatriculation": "IJ-789-KL", "km": 12300 }
|
||||
]
|
||||
```
|
||||
|
||||
> 🔎 Le JSON ressemble à un tableau d'objets JS, avec **deux règles strictes** : toutes les clés sont **entre guillemets doubles**, et **pas de virgule** après le dernier élément. Une erreur ici et tout casse — colle-le dans un validateur si besoin.
|
||||
|
||||
## 🧪 Exo 4 — Récupérer ce fichier avec `fetch`
|
||||
|
||||
> 🆕 Nouvelles notions : `fetch`, `async`/`await`. `fetch` va chercher un fichier/des données **sur le réseau** ; comme ça prend un peu de temps, on **attend** la réponse avec `await`.
|
||||
|
||||
Dans un nouveau fichier `flotte.js` (branché dans la page) :
|
||||
|
||||
```js
|
||||
async function chargerFlotte() {
|
||||
const reponse = await fetch("../vehicules.json"); // on demande le fichier
|
||||
const vehicules = await reponse.json(); // on le transforme en tableau JS
|
||||
console.log(vehicules); // pour vérifier
|
||||
}
|
||||
|
||||
chargerFlotte();
|
||||
```
|
||||
|
||||
**✅ Teste :** via http://localhost:8000/…, ouvre la Console → tu dois voir ton tableau de 3 véhicules. Si tu vois une erreur, relis la partie « petit serveur » ci-dessus.
|
||||
|
||||
> 🧩 **À retenir :**
|
||||
> - `async` devant une fonction = « cette fonction contient de l'attente ».
|
||||
> - `await` = « attends que ce soit prêt avant de continuer ».
|
||||
> - `fetch(...)` rapporte une **réponse**, et `reponse.json()` la transforme en données JS utilisables.
|
||||
|
||||
## 🧪 Exo 5 — Afficher la flotte + gérer les erreurs
|
||||
|
||||
Ajoute un tableau dans le HTML :
|
||||
|
||||
```html
|
||||
<h3>Notre flotte</h3>
|
||||
<p id="message_flotte">Chargement…</p>
|
||||
<table border="1"><tbody id="corps_flotte"></tbody></table>
|
||||
```
|
||||
|
||||
Puis complète `flotte.js` :
|
||||
|
||||
```js
|
||||
async function chargerFlotte() {
|
||||
const message = document.getElementById("message_flotte");
|
||||
const corps = document.getElementById("corps_flotte");
|
||||
|
||||
try {
|
||||
const reponse = await fetch("../vehicules.json");
|
||||
const vehicules = await reponse.json();
|
||||
|
||||
message.textContent = vehicules.length + " véhicules :";
|
||||
for (const v of vehicules) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.textContent = `${v.modele} — ${v.immatriculation} — ${v.km} km`;
|
||||
corps.append(tr);
|
||||
}
|
||||
} catch (erreur) {
|
||||
message.textContent = "Impossible de charger la flotte 😕";
|
||||
console.error(erreur);
|
||||
}
|
||||
}
|
||||
|
||||
chargerFlotte();
|
||||
```
|
||||
|
||||
**✅ Teste :** la liste des véhicules s'affiche. Renomme exprès `vehicules.json` en `vehicule.json` (faute) et recharge → tu dois voir le message d'erreur au lieu d'un plantage.
|
||||
|
||||
> 🧩 **À retenir :** `try { … } catch (erreur) { … }` = « **essaie** ce code ; **si** ça rate, fais ça plutôt ». Indispensable avec le réseau, où ça peut toujours échouer (pas de connexion, fichier absent…).
|
||||
|
||||
---
|
||||
|
||||
# ⏱️ Module C — Le temps réel avec `setInterval`
|
||||
|
||||
## 🧪 Exo 6 — Une horloge dans le pied de page
|
||||
|
||||
> 🆕 Nouvelle notion : `setInterval(fonction, durée)` répète une action toutes les *durée* millisecondes.
|
||||
|
||||
```html
|
||||
<span id="horloge"></span>
|
||||
```
|
||||
|
||||
```js
|
||||
const horloge = document.getElementById("horloge");
|
||||
|
||||
function afficherHeure() {
|
||||
const maintenant = new Date();
|
||||
horloge.textContent = maintenant.toLocaleTimeString(); // ex. "14:05:32"
|
||||
}
|
||||
|
||||
afficherHeure(); // une fois tout de suite
|
||||
setInterval(afficherHeure, 1000); // puis toutes les 1000 ms = 1 seconde
|
||||
```
|
||||
|
||||
**✅ Teste :** l'heure s'affiche et **avance toute seule** chaque seconde.
|
||||
|
||||
> 🔎 `1000` est en **millisecondes** (1 s). `setInterval` ne s'arrête jamais tout seul ; pour le stopper un jour, on garde son résultat (`const id = setInterval(...)`) et on fait `clearInterval(id)`.
|
||||
|
||||
## 🧪 Exo 7 — Mettre à jour le badge « Ouvert / Fermé » en direct
|
||||
|
||||
Ton badge horaires est calculé **une seule fois** au chargement. Range ce calcul dans une fonction `rafraichirBadge()` puis appelle-la dans ton `setInterval` (par exemple toutes les 60 s). Ainsi, si la page reste ouverte à 18h pile, le badge passera tout seul à « Fermé ».
|
||||
|
||||
**✅ Teste :** pour vérifier sans attendre, mets temporairement l'intervalle à `1000` et change les heures d'ouverture autour de l'heure actuelle.
|
||||
|
||||
---
|
||||
|
||||
# ⌨️ Module D — Réagir au clavier
|
||||
|
||||
## 🧪 Exo 8 — Envoyer avec Ctrl+Entrée, effacer avec Échap
|
||||
|
||||
> 🆕 Nouvelle notion : l'événement `keydown` et la propriété `event.key`.
|
||||
|
||||
```js
|
||||
zoneMessage.addEventListener("keydown", function (event) {
|
||||
// Ctrl + Entrée → on déclenche l'envoi du formulaire
|
||||
if (event.key === "Enter" && event.ctrlKey) {
|
||||
formulaire.requestSubmit(); // comme cliquer sur "Envoyer"
|
||||
}
|
||||
|
||||
// Touche Échap → on vide la zone
|
||||
if (event.key === "Escape") {
|
||||
zoneMessage.value = "";
|
||||
rafraichirCompteur();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste :** écris un message, fais **Ctrl+Entrée** → l'envoi se déclenche. Appuie sur **Échap** → la zone se vide.
|
||||
|
||||
> 🔎 `event.key` te donne le **nom** de la touche (`"Enter"`, `"Escape"`, `"a"`…). `event.ctrlKey` est vrai si Ctrl est maintenue. C'est comme ça qu'on crée des raccourcis clavier.
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Ce que ce niveau ajoute à ta boîte à outils
|
||||
|
||||
- **DOM propre** : `createElement`, `textContent`, `append`
|
||||
- **Données distantes** : `fetch`, `async`/`await`, JSON, `try`/`catch`
|
||||
- **Temps réel** : `setInterval` / `clearInterval`
|
||||
- **Clavier** : `keydown`, `event.key`, raccourcis
|
||||
|
||||
Prochaine étape possible : organiser ton code en plusieurs fichiers/modules, ou afficher des données depuis une **vraie API publique** en ligne. Dis-moi vers quoi tu veux aller.
|
||||
|
||||
Note tes questions au fur et à mesure.
|
||||
@@ -1,144 +0,0 @@
|
||||
# 🧭 Niveau 5 — aller plus loin (mode indices)
|
||||
|
||||
Salut Mélissa ! 👋
|
||||
|
||||
> 📌 À faire **une fois le Niveau 4 terminé** (`createElement`, `fetch`, `setInterval`, clavier).
|
||||
|
||||
Petit changement de format à partir d'ici : **je ne donne plus le code tout fait.** Pour chaque exo tu as un **🎯 objectif**, des **🔍 pistes** (quoi chercher, quels mots-clés, les pièges) et un **✅ test**. À toi d'écrire le code.
|
||||
|
||||
> 🔎 Tu as maintenant assez de bases pour lire la doc directement. Le réflexe : chercher **« MDN » + le nom de la chose** (ex. `MDN Array map`). Cherche d'abord **le concept**, le code viendra de toi.
|
||||
|
||||
| Module | Notions visées | Exos |
|
||||
|---|---|---|
|
||||
| **A. Tableaux, niveau 2** | `map`, `find`, `reduce` | 1 → 3 |
|
||||
| **B. Écouteurs malins** | délégation d'événements, anti-rebond | 4 → 5 |
|
||||
| **C. Organiser son code** | modules `export` / `import` | 6 |
|
||||
| **D. Une vraie API en ligne** | `fetch` vers le web réel | 7 |
|
||||
| **E. Lire un formulaire d'un coup** | `FormData` | 8 |
|
||||
|
||||
---
|
||||
|
||||
# 🧱 Module A — Les tableaux, niveau 2
|
||||
|
||||
Tu connais déjà `filter` (garder certains éléments) et `sort` (trier). Voici trois autres méthodes qui reviennent **tout le temps**.
|
||||
|
||||
## 🧪 Exo 1 — `map` : transformer une liste
|
||||
|
||||
**🎯 Objectif :** à partir de ton tableau `equipe`, fabriquer un **nouveau** tableau qui ne contient que les **noms** (`["Mélissa", "Guillaume"]`).
|
||||
|
||||
**🔍 Pistes :**
|
||||
- Cherche `MDN Array map`. L'idée : `map` applique une transformation à **chaque** élément et renvoie une **nouvelle** liste de même taille.
|
||||
- Différence avec `filter` : `filter` **garde ou jette** des éléments (la liste rétrécit) ; `map` **transforme** chacun (la liste garde la même taille).
|
||||
|
||||
**✅ Test :** un `console.log` affiche bien la liste des noms seuls.
|
||||
|
||||
## 🧪 Exo 2 — `find` : retrouver un élément précis
|
||||
|
||||
**🎯 Objectif :** retrouver dans `equipe` l'objet de la personne qui a fait la « Page contact ».
|
||||
|
||||
**🔍 Pistes :**
|
||||
- Cherche `MDN Array find`. `find` renvoie le **premier** élément qui remplit une condition (un seul objet, pas une liste).
|
||||
- Attention au cas « personne ne correspond » : `find` renvoie alors `undefined` → comment l'éviter de planter ? (cherche `optional chaining` ou un simple `if`).
|
||||
|
||||
**✅ Test :** `console.log` affiche le bon objet membre.
|
||||
|
||||
## 🧪 Exo 3 — `reduce` : calculer un total
|
||||
|
||||
**🎯 Objectif :** si tu as fait le `vehicules.json` du Niveau 4, calcule le **kilométrage total** de toute la flotte.
|
||||
|
||||
**🔍 Pistes :**
|
||||
- Cherche `MDN Array reduce`. C'est la méthode la plus abstraite des trois : elle « replie » toute une liste en **une seule valeur** (ici, une somme).
|
||||
- Repère les deux choses qu'elle attend : une fonction `(accumulateur, élément) => ...` et une **valeur de départ** (pour une somme, laquelle ?).
|
||||
- Si reduce te paraît trop dense au début, tu peux d'abord le faire avec une boucle `for...of` et une variable `total`, puis le réécrire en `reduce` pour comparer.
|
||||
|
||||
**✅ Test :** le total affiché correspond à la somme des `km` de ton JSON.
|
||||
|
||||
---
|
||||
|
||||
# 🎯 Module B — Des écouteurs d'événements plus malins
|
||||
|
||||
## 🧪 Exo 4 — La délégation d'événements
|
||||
|
||||
**🎯 Objectif :** rendre **chaque ligne** du tableau d'équipe cliquable (au clic, afficher le nom du membre dans une alerte). Le piège : tes lignes sont **générées** par JS, et parfois recréées (recherche, tri)…
|
||||
|
||||
**🔍 Pistes :**
|
||||
- L'approche naïve serait d'ajouter un écouteur sur chaque `<tr>`. Problème : à chaque ré-affichage, il faut tout réattacher, et on en oublie. Cherche pourquoi → `event delegation javascript`.
|
||||
- L'idée maligne : mettre **un seul** écouteur sur le **parent** (le `<tbody>`), qui ne bouge jamais. Quand un clic remonte jusqu'à lui, on regarde **d'où** il vient.
|
||||
- Pour savoir sur quel élément précis on a cliqué → cherche `event.target` et `closest`.
|
||||
|
||||
**✅ Test :** clique sur n'importe quelle ligne → la bonne info s'affiche, **même après** avoir filtré/trié le tableau.
|
||||
|
||||
## 🧪 Exo 5 — Anti-rebond (« debounce ») sur la recherche
|
||||
|
||||
**🎯 Objectif :** ta recherche se relance à **chaque** frappe. Sur une grosse liste, c'est du gaspillage. Attendre que l'utilisateur arrête de taper ~300 ms avant de filtrer.
|
||||
|
||||
**🔍 Pistes :**
|
||||
- Cherche `debounce javascript` et les fonctions `setTimeout` / `clearTimeout`.
|
||||
- L'idée : à chaque frappe, on **annule** le minuteur précédent et on en relance un. Le filtrage ne part que si l'utilisateur fait une pause.
|
||||
|
||||
**✅ Test :** tape vite « gui » → le filtrage ne se déclenche qu'**une** fois, à la fin, pas trois.
|
||||
|
||||
---
|
||||
|
||||
# 📦 Module C — Organiser son code en modules
|
||||
|
||||
Ton `contact.js` grossit (compteur, formulaire, FAQ, navbar, horloge, fetch…). Il est temps de **découper**.
|
||||
|
||||
## 🧪 Exo 6 — Séparer en fichiers avec `export` / `import`
|
||||
|
||||
**🎯 Objectif :** sortir, par exemple, `creerNavbar` dans son propre fichier `navbar.js`, et l'**importer** là où tu en as besoin.
|
||||
|
||||
**🔍 Pistes :**
|
||||
- Cherche `javascript export import` (les **modules ES**).
|
||||
- Côté HTML, une balise `<script>` classique ne suffit pas pour les modules : cherche `script type module`.
|
||||
- ⚠️ Comme `fetch`, les modules **ne marchent pas en `file://`** : il te faut le petit serveur (`python3 -m http.server`) vu au Niveau 4.
|
||||
- Commence petit : **un seul** export (la navbar), et fais-le marcher avant d'en découper d'autres.
|
||||
|
||||
**✅ Test :** la page fonctionne exactement comme avant, mais ton code est réparti dans plusieurs fichiers plus courts.
|
||||
|
||||
---
|
||||
|
||||
# 🌍 Module D — Des données d'une vraie API en ligne
|
||||
|
||||
Au Niveau 4 tu lisais un fichier JSON **local**. Là, on va chercher des données sur un **vrai serveur public**, comme le font les applis réelles.
|
||||
|
||||
## 🧪 Exo 7 — Afficher des données venues du web
|
||||
|
||||
**🎯 Objectif :** récupérer une liste depuis une API publique gratuite et l'afficher dans ta page (par ex. une liste d'utilisateurs fictifs, ou de publications).
|
||||
|
||||
**🔍 Pistes :**
|
||||
- Une API d'entraînement très connue, sans clé ni inscription : cherche `jsonplaceholder`. Par exemple l'adresse `https://jsonplaceholder.typicode.com/users` renvoie une liste de personnes en JSON.
|
||||
- Tu réutilises **exactement** ce que tu sais déjà : `fetch`, `await`, `.json()`, une boucle, et `createElement`/`append` pour afficher.
|
||||
- Ouvre d'abord l'adresse directement dans le navigateur pour **voir la forme** des données : quelles clés existent (`name`, `email`, `company`…) ? C'est le réflexe n°1 avant d'afficher quoi que ce soit.
|
||||
- Pense au `try/catch` : une API en ligne peut être lente ou indisponible.
|
||||
|
||||
**✅ Test :** une liste de vraies données distantes s'affiche dans ta page. Coupe ta connexion → ton message d'erreur s'affiche proprement.
|
||||
|
||||
---
|
||||
|
||||
# 📝 Module E — Lire tout un formulaire d'un coup
|
||||
|
||||
## 🧪 Exo 8 — `FormData`
|
||||
|
||||
**🎯 Objectif :** à l'envoi, récupérer **toutes** les valeurs du formulaire (prénom, e-mail, message) sans aller chercher chaque champ un par un avec `getElementById`.
|
||||
|
||||
**🔍 Pistes :**
|
||||
- Cherche `MDN FormData`. On lui passe le `<form>`, et on peut lire chaque champ par son attribut `name` (`.get("prenom")`…).
|
||||
- Pour que ça marche, vérifie que **chacun** de tes champs a bien un attribut `name` (pas seulement un `id`).
|
||||
- Bonus : cherche `Object.fromEntries(formData)` pour transformer tout le formulaire en un seul objet `{ prenom: ..., email: ..., message: ... }`.
|
||||
|
||||
**✅ Test :** à l'envoi, un `console.log` montre un objet propre avec toutes les valeurs saisies.
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Ce que ce niveau ajoute
|
||||
|
||||
- `map`, `find`, `reduce` — le trio qui complète `filter`/`sort`
|
||||
- **délégation d'événements** et **debounce** — des écouteurs propres et efficaces
|
||||
- **modules** `export`/`import` — du code organisé
|
||||
- **API en ligne** réelle — `fetch` au-delà du fichier local
|
||||
- **`FormData`** — lire un formulaire d'un coup
|
||||
|
||||
Après ça, les grandes directions possibles : un **framework** (commencer par comprendre *pourquoi* ils existent — tu as déjà recréé à la main plusieurs de leurs idées : composants, données → affichage), ou approfondir l'**accessibilité** et les **tests**. On en parlera.
|
||||
|
||||
Note tes questions, et cherche le concept avant le code.
|
||||
@@ -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 :</label>
|
||||
<input type="text" id="name" name="user_name" placeholder="Dupond" />
|
||||
</li>
|
||||
<li>
|
||||
<label for="mail">E-mail :</label>
|
||||
<input type="email" id="mail" name="user_mail" placeholder="dupond@gmail.com" />
|
||||
</li>
|
||||
<li>
|
||||
<label for="msg">Message :</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 ! 🚗💨
|
||||
@@ -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.
|
||||
@@ -1,165 +0,0 @@
|
||||
# ✅ Solutions — Niveau 5 contact (`PROCHAINE_ETAPE_contact_4.md`)
|
||||
|
||||
> 🛑 **À NE REGARDER QU'EN DERNIER RECOURS.**
|
||||
> Cherche d'abord par toi-même (au moins 10–15 min, doc MDN à l'appui). Quand tu lis une réponse : lis-la, **ferme le fichier**, et réécris-la **de mémoire**. C'est comme ça que ça rentre.
|
||||
|
||||
Le code ci-dessous est **un** exemple correct. Le tien peut être différent et tout aussi valable.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 1 — `map`
|
||||
|
||||
```js
|
||||
const noms = equipe.map(function (membre) {
|
||||
return membre.nom;
|
||||
});
|
||||
console.log(noms); // ["Mélissa", "Guillaume"]
|
||||
```
|
||||
> 💬 *« `map` ou `filter` ? »* `filter` garde/jette des éléments (la liste rétrécit). `map` transforme chaque élément (la liste garde la même taille). Ici on veut transformer chaque objet en son nom → `map`.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 2 — `find`
|
||||
|
||||
```js
|
||||
const personne = equipe.find(function (membre) {
|
||||
return membre.partie === "Page contact";
|
||||
});
|
||||
|
||||
if (personne) {
|
||||
console.log(personne.nom);
|
||||
} else {
|
||||
console.log("Personne ne correspond.");
|
||||
}
|
||||
```
|
||||
> 💬 *« Pourquoi le `if (personne)` ? »* Si aucun élément ne correspond, `find` renvoie `undefined`. Faire `personne.nom` planterait. On vérifie d'abord. (Variante moderne : `personne?.nom` — cherche `optional chaining`.)
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 3 — `reduce`
|
||||
|
||||
Avec une boucle (plus lisible pour commencer) :
|
||||
```js
|
||||
let totalKm = 0;
|
||||
for (const v of vehicules) {
|
||||
totalKm = totalKm + v.km;
|
||||
}
|
||||
```
|
||||
La même chose en `reduce` :
|
||||
```js
|
||||
const totalKm = vehicules.reduce(function (somme, v) {
|
||||
return somme + v.km;
|
||||
}, 0); // ← 0 = la valeur de départ
|
||||
console.log(totalKm);
|
||||
```
|
||||
> 💬 *« À quoi sert le `0` à la fin ? »* C'est la valeur de départ de l'accumulateur (`somme`). Pour une somme on part de 0 ; pour multiplier on partirait de 1.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 4 — Délégation d'événements
|
||||
|
||||
Quand tu génères les lignes, marque chaque ligne avec le nom (un attribut `data-…`) :
|
||||
```js
|
||||
function creerLigne(membre) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.dataset.nom = membre.nom; // devient data-nom="..." dans le HTML
|
||||
// ... (les <td> comme avant)
|
||||
return tr;
|
||||
}
|
||||
```
|
||||
Puis **un seul** écouteur, sur le parent qui ne bouge pas :
|
||||
```js
|
||||
const corps = document.getElementById("corps_equipe");
|
||||
corps.addEventListener("click", function (event) {
|
||||
const ligne = event.target.closest("tr"); // la <tr> cliquée, peu importe la <td>
|
||||
if (ligne) {
|
||||
alert("Membre : " + ligne.dataset.nom);
|
||||
}
|
||||
});
|
||||
```
|
||||
> 💬 *« Pourquoi ne pas mettre l'écouteur sur chaque `<tr>` ? »* Parce que tes lignes sont recréées à chaque recherche/tri : il faudrait tout réattacher à chaque fois. En écoutant le **parent** (qui, lui, ne change pas), ça marche même pour les lignes créées plus tard.
|
||||
> 💬 *« À quoi sert `closest("tr")` ? »* Le clic peut tomber sur une `<td>` ; `closest` remonte jusqu'à la `<tr>` qui la contient.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 5 — Debounce sur la recherche
|
||||
|
||||
```js
|
||||
let minuteur; // garde la trace du minuteur en cours
|
||||
|
||||
champRecherche.addEventListener("input", function () {
|
||||
clearTimeout(minuteur); // on annule le précédent
|
||||
minuteur = setTimeout(function () { // on en relance un
|
||||
const mot = champRecherche.value.toLowerCase();
|
||||
const resultats = equipe.filter(m => m.nom.toLowerCase().includes(mot));
|
||||
afficherEquipe(resultats);
|
||||
}, 300); // 300 ms après la DERNIÈRE frappe
|
||||
});
|
||||
```
|
||||
> 💬 *« Comment ça « attend » ? »* À chaque frappe, on **annule** le minuteur précédent (`clearTimeout`) et on en repart un de 300 ms. Tant que tu tapes, il est sans cesse annulé ; il ne se déclenche que quand tu fais une pause.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 6 — Modules `export` / `import`
|
||||
|
||||
`navbar.js` :
|
||||
```js
|
||||
export function creerNavbar(pageActive) {
|
||||
return `... ton HTML de navbar ...`;
|
||||
}
|
||||
```
|
||||
`contact.js` (tout en haut) :
|
||||
```js
|
||||
import { creerNavbar } from "./navbar.js";
|
||||
document.body.insertAdjacentHTML("afterbegin", creerNavbar("contact"));
|
||||
```
|
||||
HTML :
|
||||
```html
|
||||
<script type="module" src="../contact.js"></script>
|
||||
```
|
||||
> 💬 *« Depuis que j'utilise `type="module"`, mes autres scripts ne se voient plus ! »* Normal : chaque module a son **propre** espace. Une variable d'un fichier n'est plus visible ailleurs **sauf** si tu l'`export`es et l'`import`es. C'est justement le but : plus de variables globales qui se marchent dessus.
|
||||
> 💬 *« Erreur CORS au chargement du module. »* Comme `fetch`, les modules exigent un serveur : `python3 -m http.server` puis `http://localhost:8000/...` (pas `file://`).
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 7 — Vraie API en ligne
|
||||
|
||||
```js
|
||||
async function chargerUtilisateurs() {
|
||||
const liste = document.getElementById("liste_users");
|
||||
try {
|
||||
const reponse = await fetch("https://jsonplaceholder.typicode.com/users");
|
||||
const users = await reponse.json();
|
||||
for (const u of users) {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = `${u.name} — ${u.email}`;
|
||||
liste.append(li);
|
||||
}
|
||||
} catch (erreur) {
|
||||
console.error(erreur);
|
||||
}
|
||||
}
|
||||
chargerUtilisateurs();
|
||||
```
|
||||
> 💬 *« Comment je sais que les clés s'appellent `name` et `email` ? »* J'ai d'abord ouvert `https://jsonplaceholder.typicode.com/users` dans le navigateur pour **voir** la forme des données. Fais toujours ça avant d'afficher.
|
||||
> 💬 *« Ça marche sur le web mais mon fichier local non ? »* Une API en ligne autorise les requêtes externes (CORS) ; un fichier local ouvert en `file://` non. D'où le serveur local pour tes propres fichiers.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 8 — `FormData`
|
||||
|
||||
```js
|
||||
formulaire.addEventListener("submit", function (evenement) {
|
||||
evenement.preventDefault();
|
||||
const data = new FormData(formulaire);
|
||||
const valeurs = Object.fromEntries(data);
|
||||
console.log(valeurs); // { prenom: "...", email: "...", contacter: "..." }
|
||||
});
|
||||
```
|
||||
> 💬 *« `FormData` me renvoie du vide ! »* Vérifie que chaque champ a un attribut **`name`** (pas seulement `id`). `FormData` lit les champs par leur `name`.
|
||||
> 💬 *« C'est mieux que `getElementById` partout ? »* Pour lire **tout** un formulaire d'un coup, oui : une ligne au lieu d'un `getElementById` par champ. Pour un seul champ ponctuel, `getElementById` reste très bien.
|
||||
|
||||
---
|
||||
|
||||
### 🆘 Encore bloquée après la solution ?
|
||||
Note : *ce que tu attendais, ce qui se passe à la place, et ce qu'affiche la Console (F12)*. Avec ces 3 infos, on débloque vite.
|
||||
@@ -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 10–15 min, et regarde la doc MDN). Recopier une solution sans l'avoir cherchée, ça ne s'apprend pas. Quand tu lis une réponse ici : lis-la, **ferme le fichier**, et réécris-la **de mémoire**. Si tu y arrives, c'est gagné.
|
||||
|
||||
Le code ci-dessous est **un** exemple correct. Le tien peut être écrit autrement et être tout aussi bon.
|
||||
|
||||
---
|
||||
|
||||
## 🩹 Préambule — les 3 corrections de ton `devis.js`
|
||||
|
||||
**1. L'attribut de la textarea** : c'est `rows` (pluriel), pas `row`.
|
||||
```html
|
||||
<textarea id="msg" name="user_message" rows="6" cols="40" maxlength="500"></textarea>
|
||||
```
|
||||
|
||||
**2. Bloquer AVANT d'afficher** : on teste la limite d'abord, on affiche ensuite.
|
||||
```js
|
||||
boutonPlus.addEventListener("click", function () {
|
||||
if (nbVehicules < 100) { // on teste AVANT
|
||||
nbVehicules = nbVehicules + 1;
|
||||
}
|
||||
mettreAJour(); // on affiche APRÈS (jamais 100 puis retour)
|
||||
});
|
||||
```
|
||||
|
||||
**3. Le total se recalcule, il ne se stocke pas.** Supprime `totalargent` et `mettreAJourTOTAL`. Une seule fonction d'affichage suffit :
|
||||
```js
|
||||
function mettreAJour() {
|
||||
affichageNb.textContent = nbVehicules;
|
||||
affichageGeo.textContent = nbVehicules * PRIX_GPS;
|
||||
affichageConso.textContent = nbVehicules * PRIX_CONSOMMATION;
|
||||
}
|
||||
```
|
||||
> 💬 *« Pourquoi pas une variable `totalargent` ? »* Parce qu'elle pourrait se désynchroniser de `nbVehicules`. Une valeur qui se déduit d'une autre, on la **calcule au moment de l'afficher** : impossible qu'elle soit fausse.
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Exo 1 — Singulier / pluriel
|
||||
|
||||
Dans le HTML, mets le mot dans son propre span, à côté du nombre :
|
||||
```html
|
||||
<span id="nb_vehicules">1</span> <span id="mot_vehicule">véhicule</span>
|
||||
```
|
||||
Dans `mettreAJour()` :
|
||||
```js
|
||||
motVehicule.textContent = nbVehicules === 1 ? "véhicule" : "véhicules";
|
||||
```
|
||||
(avec `const motVehicule = document.getElementById("mot_vehicule");` en haut.)
|
||||
|
||||
> 💬 *« `=`, `==` ou `===` ? »* `=` **range** une valeur. `===` **compare** (et vérifie aussi le type). Dans un `if`/un test, utilise toujours `===`.
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Exo 2 — Compteur de caractères
|
||||
|
||||
HTML, sous la textarea :
|
||||
```html
|
||||
<p>Il vous reste <span id="restant">500</span> caractères.</p>
|
||||
```
|
||||
JS :
|
||||
```js
|
||||
const zoneMsg = document.getElementById("msg");
|
||||
const restant = document.getElementById("restant");
|
||||
const MAX = 500;
|
||||
|
||||
zoneMsg.addEventListener("input", function () {
|
||||
const reste = MAX - zoneMsg.value.length;
|
||||
restant.textContent = reste;
|
||||
restant.style.color = reste < 50 ? "red" : "black"; // bonus
|
||||
});
|
||||
```
|
||||
> 💬 *« Pourquoi `input` et pas `click` ? »* `click` ne se déclenche qu'au clic de souris ; `input` se déclenche à **chaque modification** du champ (clavier, collage…). C'est ce qu'on veut pour un compteur.
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Exo 3 — Bloquer l'envoi vide
|
||||
|
||||
```js
|
||||
const formulaire = document.querySelector("form");
|
||||
|
||||
formulaire.addEventListener("submit", function (evenement) {
|
||||
if (zoneMsg.value.trim() === "") {
|
||||
evenement.preventDefault();
|
||||
alert("Merci d'écrire une suggestion avant d'envoyer !");
|
||||
}
|
||||
});
|
||||
```
|
||||
> 💬 *« À quoi sert `trim()` ? »* Il enlève les espaces au début/à la fin. Sans lui, « 3 espaces » compterait comme un message valide.
|
||||
> 💬 *« `preventDefault`, c'est quoi ? »* Ça annule l'action par défaut du navigateur (ici : envoyer le formulaire et recharger la page).
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Exo 4 — Tableau d'objets
|
||||
|
||||
Dans un fichier `services.js` (ou en haut de `devis.js`) :
|
||||
```js
|
||||
const services = [
|
||||
{ nom: "Géolocalisation", description: "Suivi en temps réel", prix: "30€/véhicule/mois" },
|
||||
{ nom: "Odomètre", description: "Kilométrage précis", prix: "200€" },
|
||||
{ nom: "Consommation", description: "Carburant consommé", prix: "15€/véhicule/mois" },
|
||||
{ nom: "Impact carbone", description: "CO₂ rejeté", prix: "30€/véhicule/mois" }
|
||||
];
|
||||
|
||||
for (const service of services) {
|
||||
console.log(service.nom + " — " + service.prix);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Exo 5 — Générer les lignes du tableau
|
||||
|
||||
HTML : garde le `<thead>`, vide le corps et donne-lui un id :
|
||||
```html
|
||||
<tbody id="corps_services"></tbody>
|
||||
```
|
||||
JS (version `innerHTML` + template literal) :
|
||||
```js
|
||||
const corps = document.getElementById("corps_services");
|
||||
let lignes = "";
|
||||
for (const s of services) {
|
||||
lignes += `<tr><td>${s.nom}</td><td>${s.description}</td><td>${s.prix}</td></tr>`;
|
||||
}
|
||||
corps.innerHTML = lignes;
|
||||
```
|
||||
> 💬 *« Pourquoi mes `${}` ne marchent pas ? »* Vérifie que tu utilises bien les accents **graves** `` ` `` (touche AltGr+7), pas des guillemets simples `'`.
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Exo 6 — Recherche
|
||||
|
||||
Mets l'affichage dans une fonction réutilisable :
|
||||
```js
|
||||
function afficherServices(liste) {
|
||||
let lignes = "";
|
||||
for (const s of liste) {
|
||||
lignes += `<tr><td>${s.nom}</td><td>${s.description}</td><td>${s.prix}</td></tr>`;
|
||||
}
|
||||
corps.innerHTML = lignes;
|
||||
}
|
||||
afficherServices(services);
|
||||
|
||||
const champ = document.getElementById("recherche"); // <input id="recherche"> dans le HTML
|
||||
champ.addEventListener("input", function () {
|
||||
const mot = champ.value.toLowerCase();
|
||||
const resultats = services.filter(function (s) {
|
||||
return s.nom.toLowerCase().includes(mot);
|
||||
});
|
||||
afficherServices(resultats);
|
||||
});
|
||||
```
|
||||
> 💬 *« `filter` modifie-t-il `services` ? »* Non. `filter` renvoie une **nouvelle** liste ; l'originale reste intacte (c'est pour ça qu'effacer la recherche fait tout revenir).
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Exo 7 — Se souvenir du nombre de véhicules
|
||||
|
||||
```js
|
||||
// à la fin de mettreAJour(), on sauvegarde
|
||||
localStorage.setItem("nbVehicules", nbVehicules);
|
||||
|
||||
// au chargement de la page, on relit
|
||||
const sauvegarde = localStorage.getItem("nbVehicules");
|
||||
if (sauvegarde !== null) {
|
||||
nbVehicules = parseInt(sauvegarde); // ⚠️ reconvertir le TEXTE en NOMBRE
|
||||
mettreAJour();
|
||||
}
|
||||
```
|
||||
> 💬 *« Pourquoi `parseInt` ? »* `localStorage` ne stocke que du **texte**. Sans conversion, `"12" + 1` donnerait `"121"`. `parseInt("12")` redonne le nombre `12`.
|
||||
|
||||
---
|
||||
|
||||
## 🟠 Exo 8 — Mode sombre
|
||||
|
||||
CSS :
|
||||
```css
|
||||
body.sombre { background-color: #1d2733; color: white; }
|
||||
```
|
||||
JS (avec `<button id="theme">Mode sombre</button>` dans le HTML) :
|
||||
```js
|
||||
document.getElementById("theme").addEventListener("click", function () {
|
||||
document.body.classList.toggle("sombre");
|
||||
});
|
||||
```
|
||||
> 💬 *« `toggle` fait quoi exactement ? »* Si la classe est présente il l'enlève, sinon il l'ajoute. Un seul appel gère l'aller **et** le retour.
|
||||
|
||||
---
|
||||
|
||||
## 🟠 Exo 9 — Horloge
|
||||
|
||||
```html
|
||||
<span id="horloge"></span>
|
||||
```
|
||||
```js
|
||||
const horloge = document.getElementById("horloge");
|
||||
function afficherHeure() {
|
||||
horloge.textContent = new Date().toLocaleTimeString();
|
||||
}
|
||||
afficherHeure(); // tout de suite
|
||||
setInterval(afficherHeure, 1000); // puis chaque seconde
|
||||
```
|
||||
> 💬 *« Pourquoi appeler `afficherHeure()` avant le `setInterval` ? »* Sinon la page reste vide pendant la 1ʳᵉ seconde, le temps que `setInterval` se déclenche la première fois.
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Exo 10 — `fetch` d'un fichier JSON
|
||||
|
||||
`vehicules.json` (racine du projet) :
|
||||
```json
|
||||
[
|
||||
{ "modele": "Renault Kangoo", "immatriculation": "AB-123-CD", "km": 45200 },
|
||||
{ "modele": "Peugeot Partner", "immatriculation": "EF-456-GH", "km": 78900 }
|
||||
]
|
||||
```
|
||||
JS :
|
||||
```js
|
||||
async function chargerFlotte() {
|
||||
const message = document.getElementById("message_flotte");
|
||||
const corps = document.getElementById("corps_flotte");
|
||||
try {
|
||||
const reponse = await fetch("../vehicules.json");
|
||||
const vehicules = await reponse.json();
|
||||
for (const v of vehicules) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.textContent = `${v.modele} — ${v.immatriculation} — ${v.km} km`;
|
||||
corps.append(tr);
|
||||
}
|
||||
} catch (erreur) {
|
||||
message.textContent = "Impossible de charger la flotte 😕";
|
||||
console.error(erreur);
|
||||
}
|
||||
}
|
||||
chargerFlotte();
|
||||
```
|
||||
> 💬 *« J'ai l'erreur "CORS" / "Failed to fetch" ! »* Tu as ouvert la page en double-cliquant (`file://`). Lance `python3 -m http.server 8000` dans le dossier du projet et ouvre `http://localhost:8000/pages/services.html`.
|
||||
> 💬 *« Mon JSON ne charge pas. »* Vérifie : guillemets **doubles** partout, et **pas** de virgule après le dernier élément.
|
||||
|
||||
---
|
||||
|
||||
## 🏁 Exo 11 — Navbar en composant
|
||||
|
||||
```js
|
||||
function creerNavbar(pageActive) {
|
||||
return `
|
||||
<div id="nav_barre" class="shadow">
|
||||
<h1>FleetZen</h1>
|
||||
<div id="buttons">
|
||||
<a href="contact.html"><button class="${pageActive === "contact" ? "x_to_x" : "styled"}">contact</button></a>
|
||||
<a href="services.html"><button class="${pageActive === "services" ? "x_to_x" : "styled"}">services</button></a>
|
||||
<a href="../index.html"><button class="retour btn_icone">retour</button></a>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
document.body.insertAdjacentHTML("afterbegin", creerNavbar("services"));
|
||||
```
|
||||
N'oublie pas d'**enlever** la navbar écrite en dur dans ton HTML, sinon tu en auras deux.
|
||||
> 💬 *« Pourquoi un paramètre `pageActive` ? »* Pour que la **même** fonction serve sur toutes les pages : sur services tu appelles `creerNavbar("services")`, sur contact `creerNavbar("contact")`, et c'est le bon bouton qui est mis en valeur.
|
||||
|
||||
---
|
||||
|
||||
### 🆘 Encore bloqué après avoir lu la solution ?
|
||||
Note précisément : *qu'est-ce que tu attendais, qu'est-ce qui se passe à la place, et qu'affiche la Console (F12) ?* Avec ces 3 infos, on débloque n'importe quoi en deux minutes.
|
||||
+349
@@ -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.
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
const zoneMessage = document.getElementById("contacter");
|
||||
const compteur = document.getElementById("restant");
|
||||
const motCaractere = document.getElementById("mot");
|
||||
const boutonEffacer = document.getElementById("effacer");
|
||||
const formulaire = document.querySelector("form");
|
||||
const badge = document.getElementById("statut_horaires");
|
||||
const nom = document.getElementById("prenom");
|
||||
const heure = new Date().getHours();
|
||||
const MAX = 500;
|
||||
|
||||
if (heure >= 9 && heure < 18) {
|
||||
badge.textContent = "✅ Nous sommes ouverts (9h–18h)";
|
||||
badge.style.color = "green";
|
||||
} else {
|
||||
badge.textContent = "🔴 Fermé — écrivez-nous, on répond dès l'ouverture !";
|
||||
badge.style.color = "red";
|
||||
}
|
||||
|
||||
function rafraichirCompteur() {
|
||||
const restant = MAX - zoneMessage.value.length;
|
||||
compteur.textContent = restant;
|
||||
compteur.style.color = restant < 50 ? "red" : "black";
|
||||
motCaractere.textContent = restant <= 1 ? "caractère" : "caractères";
|
||||
localStorage.setItem("brouillon", zoneMessage.value);
|
||||
}
|
||||
|
||||
zoneMessage.addEventListener("input", rafraichirCompteur);
|
||||
|
||||
boutonEffacer.addEventListener("click", function () {
|
||||
zoneMessage.value = "";
|
||||
rafraichirCompteur();
|
||||
});
|
||||
|
||||
formulaire.addEventListener("submit", function (evenement) {
|
||||
if (zoneMessage.value.trim() === "") {
|
||||
evenement.preventDefault();
|
||||
alert("Merci d'écrire un message avant d'envoyer !");
|
||||
} else {
|
||||
const greeting = `merci ${nom.value}, ton message de ${zoneMessage.value.length} caractères a été pris en compte`;
|
||||
alert(`${greeting}`);
|
||||
}
|
||||
});
|
||||
|
||||
function creerNavbar(pageActive) {
|
||||
return `
|
||||
<div id="nav_barre" class="shadow">
|
||||
<h1>FleetZen</h1>
|
||||
<div id="buttons">
|
||||
<button class="${pageActive === "contact" ? "instyled" : "styled"}">contact</button>
|
||||
<a href="services.html"><button class="${pageActive === "services" ? "instyled" : "styled"}">services</button></a>
|
||||
<a href="../index.html"><button class="styled"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-corner-down-left-icon lucide-corner-down-left"><path d="M20 4v7a4 4 0 0 1-4 4H4"/><path d="m9 10-5 5 5 5"/></svg>retour</button></a>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
document.body.insertAdjacentHTML("afterbegin", creerNavbar("contact"));
|
||||
|
||||
document.getElementById("theme").addEventListener("click", function () {
|
||||
document.body.classList.toggle("sombre");
|
||||
});
|
||||
|
||||
const titres = document.querySelectorAll(".q-titre");
|
||||
|
||||
titres.forEach(function (titre) {
|
||||
titre.addEventListener("click", function () {
|
||||
const reponse = titre.nextElementSibling;
|
||||
reponse.classList.toggle("ouverte");
|
||||
});
|
||||
});
|
||||
+47
-3
@@ -42,7 +42,6 @@ h1 {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-top: 2px solid;
|
||||
}
|
||||
#nav_barre {
|
||||
display: flex;
|
||||
@@ -59,11 +58,10 @@ h1 {
|
||||
width: 130px;
|
||||
line-height: 2.5;
|
||||
padding: 0 20px;
|
||||
box-shadow: 3px 5px 3px darkslategray;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
color: white;
|
||||
text-shadow: 1px 1px 1px slategray;
|
||||
text-shadow: 1px 1px 1px black;
|
||||
border-radius: 10px;
|
||||
background-color: royalblue;
|
||||
background-image: linear-gradient(
|
||||
@@ -83,7 +81,53 @@ h1 {
|
||||
inset -2px -2px 3px rgb(255 255 255 / 0.6),
|
||||
inset 2px 2px 3px rgb(0 0 0 / 0.6);
|
||||
}
|
||||
.x_to_x {
|
||||
border: 0;
|
||||
height: 70px;
|
||||
width: 130px;
|
||||
line-height: 2.5;
|
||||
padding: 0 20px;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
color: white;
|
||||
text-shadow: 1px 1px 1px black;
|
||||
border-radius: 10px;
|
||||
background-color: midnightblue;
|
||||
background-image:
|
||||
linear-gradient to top left,
|
||||
rgb(0 0 0 / 0.2),
|
||||
rgb(0 0 0 / 0.2) 30%,
|
||||
transparent;
|
||||
}
|
||||
.styled {
|
||||
border: 0;
|
||||
height: 70px;
|
||||
width: 130px;
|
||||
line-height: 2.5;
|
||||
padding: 0 20px;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
color: white;
|
||||
text-shadow: 1px 1px 1px black;
|
||||
border-radius: 10px;
|
||||
background-color: royalblue;
|
||||
background-image: linear-gradient(
|
||||
to top left,
|
||||
rgb(0 0 0 / 0.2),
|
||||
rgb(0 0 0 / 0.2) 30%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.styled:hover {
|
||||
background-color: midnightblue;
|
||||
}
|
||||
|
||||
.styled:active {
|
||||
box-shadow:
|
||||
inset -2px -2px 3px rgb(255 255 255 / 0.6),
|
||||
inset 2px 2px 3px rgb(0 0 0 / 0.6);
|
||||
}
|
||||
#buttons {
|
||||
height: 100%;
|
||||
width: 25%;
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
const equipe = [
|
||||
{ nom: "Mélissa", job: "Stagiaire", partie: "Page contact" },
|
||||
{ nom: "Guillaume", job: "Stagiaire", partie: "Page services" },
|
||||
{ nom: "Matthieu", job: "Tuteur", partie: "Relecture" },
|
||||
];
|
||||
|
||||
for (const membre of equipe) {
|
||||
console.log(membre.nom + " — " + membre.job + " — " + membre.partie);
|
||||
}
|
||||
|
||||
function creerLigne(membre) {
|
||||
const tr = document.createElement("tr");
|
||||
|
||||
const tdNom = document.createElement("td");
|
||||
tdNom.textContent = membre.nom;
|
||||
|
||||
const tdJob = document.createElement("td");
|
||||
tdJob.textContent = membre.job;
|
||||
|
||||
const tdPartie = document.createElement("td");
|
||||
tdPartie.textContent = membre.partie;
|
||||
|
||||
tr.append(tdNom, tdJob, tdPartie);
|
||||
return tr;
|
||||
}
|
||||
|
||||
const corps = document.getElementById("corps_equipe");
|
||||
|
||||
function afficherEquipe(liste) {
|
||||
corps.innerHTML = "";
|
||||
for (const membre of liste) {
|
||||
corps.append(creerLigne(membre));
|
||||
}
|
||||
}
|
||||
afficherEquipe(equipe);
|
||||
|
||||
document.getElementById("nb_membres").textContent = equipe.length;
|
||||
|
||||
const champRecherche = document.getElementById("recherche");
|
||||
|
||||
champRecherche.addEventListener("input", function () {
|
||||
const motCherche = champRecherche.value.toLowerCase();
|
||||
|
||||
// on garde seulement les membres dont le nom contient le texte tapé
|
||||
const resultats = equipe.filter(function (membre) {
|
||||
return membre.nom.toLowerCase().includes(motCherche);
|
||||
});
|
||||
|
||||
afficherEquipe(resultats); // on ré-affiche, mais filtré
|
||||
});
|
||||
|
||||
document.getElementById("trier").addEventListener("click", function () {
|
||||
const triee = [...equipe].sort(function (a, b) {
|
||||
return a.nom.localeCompare(b.nom);
|
||||
});
|
||||
afficherEquipe(triee);
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
https://pietrafocirestaurant.com/wp-content/uploads/2025/10/PF2025SepUnique.pdf
|
||||
@@ -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?
|
||||
@@ -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.
|
||||
@@ -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 } },
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -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>
|
||||
Generated
+2515
File diff suppressed because it is too large
Load Diff
@@ -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 |
@@ -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 |
@@ -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;
|
||||
}
|
||||
@@ -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 |
@@ -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 |
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,9 @@
|
||||
function Footer() {
|
||||
return (
|
||||
<footer>
|
||||
<p>Footer toto</p>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
@@ -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 c’est un jeu, en apparence,
|
||||
très simple, il se complexifie très vite avec les différentes mécaniques
|
||||
telles que les portails qui changent la gravité du cube, ceux qui
|
||||
augmentent la vitesse, et les nombreuses transformations telles que le
|
||||
ship ou la wave. Vous l’avez compris, ce jeu ne se résume pas à un
|
||||
simple cube qui saute, et finir le plus dur des niveaux officiels,
|
||||
deadlocked, n’est pas à la portée de tous les joueurs. Cependant, finir
|
||||
deadlocked et tous les niveaux officiels n’est pas une fin en soi. En
|
||||
effet, le vrai potentiel du jeu se trouve dans l’éditeur de niveau. Ce
|
||||
dernier permet aux joueurs de créer leurs propres niveaux et de faire
|
||||
vivre la communauté (pcq oui hein robtop fait rarement des mises à
|
||||
jour). On peut donc rapidement découper cette communauté en 2 catégories
|
||||
: les buildeurs, qui créaient les niveaux, et les joueurs, qui jouent
|
||||
aux niveaux (bravo vous avez deviné), et très vite, l’objectif des
|
||||
buildeurs va être de créer les niveaux les plus durs, mais surtout les
|
||||
plus iconiques et marquant pour les joueurs. Certains niveaux sont
|
||||
tellement iconiques que des suites sont créées, créant ainsi des
|
||||
trilogies qui resteront à jamais dans l'esprit des joueurs. Aujourd'hui,
|
||||
on va parler de 5 trilogies qui ont marqué l'histoire de geometry dash,
|
||||
trilogies qui ont su nous impacter.
|
||||
</p>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header; // 👈 on l'EXPORTE pour pouvoir l'utiliser ailleurs
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>,
|
||||
);
|
||||
@@ -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 (A→Z)</option>
|
||||
<option value="likes">Plus likées</option>
|
||||
</select>
|
||||
</>
|
||||
<div className="versus">
|
||||
<select value={a} onChange={(e) => setA(e.target.value)}>
|
||||
{trilogies.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.nom}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select value={b} onChange={(e) => setB(e.target.value)}>
|
||||
{trilogies.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.nom}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<p>🏆 Gagnante : {gagnante}</p>
|
||||
</div>
|
||||
|
||||
<div className="podium">
|
||||
{podium.map((t, index) => (
|
||||
<p key={t.id}>
|
||||
{medailles[index]} {t.nom} — {likes[t.id] || 0} ❤️
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
<form onSubmit={ajouter}>
|
||||
<input
|
||||
value={nom}
|
||||
onChange={(e) => setNom(e.target.value)}
|
||||
placeholder="Nom de la trilogie"
|
||||
/>
|
||||
<input
|
||||
value={niveaux}
|
||||
onChange={(e) => setNiveaux(e.target.value)}
|
||||
placeholder="Niveaux"
|
||||
/>
|
||||
<button type="submit">Ajouter</button>
|
||||
</form>
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Rechercher une trilogie…"
|
||||
value={recherche}
|
||||
onChange={(e) => setRecherche(e.target.value)}
|
||||
/>
|
||||
<p>👍 Total de likes : {total}</p>
|
||||
{affichees.length === 0 && <p>Aucune trilogie trouvée</p>}
|
||||
{affichees.map((trilo) => (
|
||||
<Carte
|
||||
key={trilo.id}
|
||||
triloId={trilo.id}
|
||||
titre={trilo.nom}
|
||||
niveaux={trilo.niveaux}
|
||||
likes={likes[trilo.id] || 0}
|
||||
onLike={() => liker(trilo.id)}
|
||||
onDelete={() => supprimer(trilo.id)}
|
||||
/>
|
||||
))}
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Accueil;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "stagiaire",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: lightblue;
|
||||
margin: 0;
|
||||
padding-bottom: 65px;
|
||||
}
|
||||
|
||||
#nav_barre {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
background-color: steelblue;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.styled {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
line-height: normal;
|
||||
border: 0;
|
||||
height: 70px;
|
||||
width: 130px;
|
||||
padding: 0 20px;
|
||||
box-shadow: 3px 5px 3px darkslategray;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
color: white;
|
||||
text-shadow: 1px 1px 1px slategray;
|
||||
border-radius: 10px;
|
||||
background-color: royalblue;
|
||||
background-image: linear-gradient(
|
||||
to top left,
|
||||
rgb(0 0 0 / 0.2),
|
||||
rgb(0 0 0 / 0.2) 30%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.styled:hover {
|
||||
background-color: midnightblue;
|
||||
}
|
||||
|
||||
.styled:active {
|
||||
box-shadow:
|
||||
inset -2px -2px 3px rgb(255 255 255 / 0.6),
|
||||
inset 2px 2px 3px rgb(0 0 0 / 0.6);
|
||||
}
|
||||
|
||||
#buttons {
|
||||
height: 100%;
|
||||
width: 25%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-direction: row-reverse;
|
||||
margin-left: auto;
|
||||
align-items: center;
|
||||
}
|
||||
.shadow {
|
||||
box-shadow: 0px 10px 5px gray;
|
||||
}
|
||||
h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.instyled {
|
||||
border: 0;
|
||||
height: 70px;
|
||||
width: 130px;
|
||||
line-height: 2.5;
|
||||
padding: 0 20px;
|
||||
box-shadow:
|
||||
inset -2px -2px 3px rgb(255 255 255 / 0.6),
|
||||
inset 2px 2px 3px rgb(0 0 0 / 0.6);
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
color: lightgrey;
|
||||
text-shadow: 1px 1px 1px darkslategrey;
|
||||
border-radius: 10px;
|
||||
background-color: darkblue;
|
||||
background-image: linear-gradient(
|
||||
to top left,
|
||||
rgb(0 0 0 / 0.2),
|
||||
rgb(0 0 0 / 0.2) 30%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 700px;
|
||||
height: 300px;
|
||||
border-collapse: collapse;
|
||||
font-size: 35px;
|
||||
table-layout: auto;
|
||||
border: 5px solid darkblue;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: skyblue;
|
||||
box-shadow: 2px 5px 5px darkgray;
|
||||
background-image: linear-gradient(
|
||||
to top left,
|
||||
rgb(90 100 150),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
thead {
|
||||
background-color: rgb(8, 105, 85);
|
||||
text-shadow: 2px 2px 2px slategray;
|
||||
color: lightblue;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
border: 2px solid dimgray;
|
||||
padding: 10px;
|
||||
}
|
||||
#titre_tableau {
|
||||
font-size: 25px;
|
||||
color: seagreen;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
width: 570px;
|
||||
height: 380px;
|
||||
border: 3px solid green;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 100px;
|
||||
background-color: rgb(159, 199, 159);
|
||||
background-image: linear-gradient(
|
||||
to top left,
|
||||
rgb(0 0 0 / 0.001),
|
||||
rgb(0 0 0 / 0.1) 30%,
|
||||
transparent
|
||||
);
|
||||
box-shadow: 2px 5px 3px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
width: 527px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 10px;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
background-color: cadetblue;
|
||||
font-size: 25px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-top: 2px solid;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
#statut_horaires {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
body.sombre {
|
||||
background-color: #1d2733;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.q-reponse {
|
||||
display: none;
|
||||
}
|
||||
.q-reponse.ouverte {
|
||||
display: block;
|
||||
}
|
||||
.Faq {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 30 px;
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="contact.css" />
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width", initial-scame="1"/>
|
||||
<title>Contact-FleetZen</title>
|
||||
<html lang="fr">
|
||||
</head>
|
||||
<body>
|
||||
<p id= titre_tableau>
|
||||
<b>Les développeurs de ce site</b>
|
||||
</p>
|
||||
<p>L'équipe compte <span id="nb_membres"></span> personne(s).</p>
|
||||
<input type="search" id="recherche" placeholder="Rechercher un membre…">
|
||||
<button type="button" id="trier">Trier A → Z</button>
|
||||
|
||||
<table border=1 class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>Job</th>
|
||||
<th>Parties crées</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="corps_equipe">
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="form">
|
||||
<p><b>Nous contacter</b></p>
|
||||
<p id="statut_horaires"></p>
|
||||
<form method="post" action="">
|
||||
<p>
|
||||
<label for="prenom">Votre prénom :</label>
|
||||
<input type="text" name="prenom" id="prenom" placeholder="Ex. : Matthieu" size="30" maxlength="20">
|
||||
</p>
|
||||
<p>
|
||||
<label for="e_mail">Votre e-mail:</label>
|
||||
<input type="email" name="email" id="email" placeholder="Ex. : stage2@echoes.solutions" size="50" maxlength="40">
|
||||
</p>
|
||||
<div>
|
||||
<label for="contacter">Des questions a nous poser?</label><br>
|
||||
<textarea name="contacter" id="contacter" rows="6" cols="40" maxlength="500"></textarea>
|
||||
<p class=small>Il vous reste <span id="restant">500</span> <span id="mot">caractères</span>.</p>
|
||||
</div>
|
||||
<p>
|
||||
<button>Envoyer</button>
|
||||
<button type="submit" id="effacer">Effacer</button>
|
||||
</p>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="Faq">
|
||||
<section class="faq">
|
||||
<h3>Questions fréquentes</h3>
|
||||
<div class="question">
|
||||
<button class="q-titre"type="button" >Comment suivre ma flotte en temps réel ?</button>
|
||||
<p class="q-reponse">Grâce à notre service de géolocalisation, accessible 24h/24.</p>
|
||||
</div>
|
||||
<div class="question">
|
||||
<button class="q-titre"type="button">Puis-je résilier à tout moment ?</button>
|
||||
<p class="q-reponse">Oui, sans frais, depuis votre espace client.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
stagiaires rpz echoes
|
||||
<button type="button" id="theme"> Mode sombre</button>
|
||||
</footer>
|
||||
<script src="../equipe.js"></script>
|
||||
<script src="../contact.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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"));
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
[
|
||||
{
|
||||
"modele": 5,
|
||||
"immatriculation": 3,
|
||||
"km": 4
|
||||
}
|
||||
]
|
||||
@@ -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 :</label>
|
||||
<input type="text" placeholder="Dupond" id="name" name="user_name" />
|
||||
</li>
|
||||
<li>
|
||||
<label for="mail">E-mail :</label>
|
||||
<input type="email" placeholder="dupondrichard@gmail.com" id="mail" name="user_mail" />
|
||||
</li>
|
||||
<li>
|
||||
<label for="msg">Message :</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>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
[
|
||||
{ "modele": "Renault Kangoo", "immatriculation": "AB-123-CD", "km": 45200 },
|
||||
{ "modele": "Peugeot Partner", "immatriculation": "EF-456-GH", "km": 78900 },
|
||||
{ "modele": "Citroën Berlingo", "immatriculation": "IJ-789-KL", "km": 12300 }
|
||||
]
|
||||
Reference in New Issue
Block a user