Compare commits
74
Commits
main
...
feature/contact
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
520c418df3 | ||
|
|
dcb81e0b55 | ||
|
|
642df97f7a | ||
|
|
8a52c92449 | ||
|
|
6a99a2d6eb | ||
|
|
8ee1eb1246 | ||
|
|
b7e6fa844b | ||
|
|
85d30252f3 | ||
|
|
f6da867500 | ||
|
|
e939e2c0ef | ||
|
|
8b54e57bc4 | ||
|
|
c98afaf171 | ||
|
|
8a66266a05 | ||
|
|
ef268afd15 | ||
|
|
e0fd5872cb | ||
|
|
1981386cb7 | ||
|
|
e38b05b5a5 | ||
|
|
d78d4c1468 | ||
|
|
eb52dfb63f | ||
|
|
bcf4c96e3b | ||
|
|
ddd62a2300 | ||
|
|
67d8934477 | ||
|
|
68a9645930 | ||
|
|
fce6355113 | ||
|
|
f74f9a0100 | ||
|
|
6be5ca583b | ||
|
|
dab2650776 | ||
|
|
b5906c3f92 | ||
|
|
82fe5d7085 | ||
|
|
9bb89e8280 | ||
|
|
bdd8316838 | ||
|
|
9b91627ed4 | ||
|
|
86933e15c0 | ||
|
|
b43c9989f4 | ||
|
|
e8448d229c | ||
|
|
9b04253827 | ||
|
|
17d969efe4 | ||
|
|
77f564afdb | ||
|
|
64cb4a206e | ||
|
|
0ea04988a3 | ||
|
|
0e43342e59 | ||
|
|
6150a7c045 | ||
|
|
74bc44a09c | ||
|
|
0b47b53218 | ||
|
|
e0589bf965 | ||
|
|
608cceec18 | ||
|
|
fe90e2d766 | ||
|
|
f250c06331 | ||
|
|
14286db2e0 | ||
|
|
47ec3842e3 | ||
|
|
4c8ade92a2 | ||
|
|
56d9d1e31c | ||
|
|
78768552fb | ||
|
|
d6e77de62c | ||
|
|
724e8b15f3 | ||
|
|
d4fb8696dc | ||
|
|
438e567b31 | ||
|
|
237cd9817f | ||
|
|
f42eb51b41 | ||
|
|
c2979534f1 | ||
|
|
849671b542 | ||
|
|
b9928180a9 | ||
|
|
b3507bc30f | ||
|
|
75a2305553 | ||
|
|
69eff4006a | ||
|
|
ddfd17f9af | ||
|
|
c4a5e19dc8 | ||
|
|
b49621c31d | ||
|
|
2b4d4cfa0b | ||
|
|
098a26b77f | ||
|
|
075d626ebb | ||
|
|
965efdb0cb | ||
|
|
593e363c52 | ||
|
|
c8f378970b |
@@ -0,0 +1,74 @@
|
||||
# 🔧 Correction — relier tes pages à `App` (et virer le doublon)
|
||||
|
||||
Bonne nouvelle : tes pages `pages/Accueil.jsx` et `pages/Detail.jsx` sont **bien faites** 👏 (tout ton contenu est dans `Accueil`). Le souci est uniquement dans **`App.jsx`** : tu n'as pas **branché** ces pages, et tu as gardé **tout l'ancien contenu en double** dedans.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Les 2 problèmes dans `App.jsx`
|
||||
|
||||
1. Tu utilises `<Accueil />` dans une `<Route>`, mais tu **ne l'importes pas** → `Accueil is not defined`.
|
||||
2. Tu as **recopié** toute la liste/recherche/formulaire **dans `App`** (en plus de l'avoir dans `pages/Accueil.jsx`). Du coup ce contenu s'affiche **hors des routes**, et tes pages ne servent à rien.
|
||||
|
||||
> 🧠 **L'idée du routeur :** `App` ne doit **plus** contenir le contenu des pages. Son rôle : **assembler** (Header/Footer) et **router** (« telle URL → telle page »). Le contenu vit **dans les pages** (`Accueil`, `Detail`).
|
||||
|
||||
---
|
||||
|
||||
## ✅ La correction : remplace TOUT ton `App.jsx` par ça
|
||||
|
||||
```jsx
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import Header from "./components/Header";
|
||||
import Footer from "./components/Footer";
|
||||
import Accueil from "./pages/Accueil"; // 👈 on IMPORTE la page
|
||||
import Detail from "./pages/Detail"; // 👈 et celle-ci aussi
|
||||
import "./App.css";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route path="/" element={<Accueil />} />
|
||||
<Route path="/jeu/:id" element={<Detail />} />
|
||||
</Routes>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
C'est **tout** : plus aucun `useState`, plus de `fetch`, plus de formulaire dans `App`. Tout ça vit déjà dans `pages/Accueil.jsx`.
|
||||
|
||||
> 🔎 `Header` et `Footer` sont **autour** des `<Routes>` → ils s'affichent sur **toutes** les pages. Seul ce qui est **dans** `<Routes>` change selon l'URL.
|
||||
|
||||
**✅ Test :** va sur `/` → ta page d'accueil (la liste qu'il y avait avant) **s'affiche**. Va sur `/jeu/2` → la page Détail.
|
||||
|
||||
---
|
||||
|
||||
## 🧹 Deux petits restes à nettoyer dans `pages/Accueil.jsx`
|
||||
|
||||
1. Tu as une **fonction `Detail` en double**, coincée **dans** `Accueil` :
|
||||
```jsx
|
||||
function Detail() {
|
||||
const { id } = useParams(); // ❌ useParams n'est même pas importé ici
|
||||
return <h1>Détails du jeu {id}</h1>;
|
||||
}
|
||||
```
|
||||
Elle ne sert à rien (le vrai `Detail` est dans `pages/Detail.jsx`) → **supprime-la** de `Accueil`.
|
||||
|
||||
2. Dans ton objet `nouveau`, il reste `console,` (l'ancien nom). Tu as renommé l'état en `plateforme`, donc écris :
|
||||
```jsx
|
||||
const nouveau = { id: Date.now(), titre, annee: Number(annee), console: plateforme, Type: "3d" };
|
||||
```
|
||||
(sinon tu ranges l'objet global `console`, pas la plateforme tapée.)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Récap
|
||||
- [ ] Remplacer `App.jsx` par la version courte (importe `Accueil` + `Detail`, `Header` + `<Routes>` + `Footer`)
|
||||
- [ ] Supprimer tout l'ancien contenu inline dans `App`
|
||||
- [ ] Dans `Accueil.jsx` : supprimer la fonction `Detail` en double + écrire `console: plateforme`
|
||||
|
||||
> 💬 La phrase à garder : **`App` assemble et route ; les pages contiennent le contenu.** Un composant qu'on affiche (`<Accueil/>`) doit être **importé**.
|
||||
@@ -0,0 +1,90 @@
|
||||
# 🔧 Pourquoi l'alerte « marche » en `file://` mais pas en `localhost`
|
||||
|
||||
Excellente observation 👀 — et ce n'est pas un hasard. La réponse t'apprend une vraie notion.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 La cause : ton formulaire **s'envoie vraiment**
|
||||
|
||||
Regarde le `else` de ton `submit` :
|
||||
|
||||
```js
|
||||
formulaire.addEventListener("submit", function (evenement) {
|
||||
if (zoneMessage.value.trim() === "") {
|
||||
evenement.preventDefault(); // ✅ ici tu annules l'envoi
|
||||
alert("Merci d'écrire un message avant d'envoyer !");
|
||||
} else {
|
||||
const greeting = `merci ${nom.value}, ...`;
|
||||
alert(`${greeting}`); // ❌ mais ici, PAS de preventDefault
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Dans le cas « message rempli » (le `else`), tu affiches l'alerte… mais tu **n'appelles pas** `evenement.preventDefault()`. Donc, après l'alerte, le navigateur fait ce qu'un `<form>` fait par défaut : il **envoie le formulaire** (ton form est en `method="post" action=""`, donc il poste vers la page elle-même).
|
||||
|
||||
Et c'est cet envoi qui se comporte différemment selon comment tu ouvres la page 👇
|
||||
|
||||
---
|
||||
|
||||
## 🆚 Pourquoi la différence `file://` vs `localhost`
|
||||
|
||||
| | Double-clic (`file://`) | Serveur (`http://localhost:...`) |
|
||||
|---|---|---|
|
||||
| Y a-t-il un serveur pour recevoir le POST ? | **Non** | **Oui** (celui que tu as lancé) |
|
||||
| Que devient l'envoi du formulaire ? | bloqué / ne mène nulle part | le serveur **reçoit** le POST et **répond** |
|
||||
| Ce que tu vois après l'alerte | rien ne bouge → « ça marche » | la page part sur une **page d'erreur** → « ça ne marche pas » |
|
||||
|
||||
- En **`file://`**, il n'y a **aucun serveur**. Un `<form>` ne peut pas « poster » vers un simple fichier → l'envoi est ignoré. Du coup, après ton alerte, **rien ne se passe** : tout a l'air normal.
|
||||
- En **`http://localhost`**, il y a un **vrai serveur** (celui que tu as démarré). Il **reçoit** ton POST… mais le petit serveur de dev ne sait pas traiter un POST → il renvoie une erreur (souvent **501 « Unsupported method »**), et le navigateur **t'emmène sur cette page d'erreur**, juste après l'alerte. D'où l'impression que « l'alerte ne marche pas ».
|
||||
|
||||
> 🔎 En réalité, ton alerte **s'affiche dans les deux cas** ! La différence, c'est ce qui se passe **juste après** : sur localhost, la page s'en va sur l'erreur du serveur, donc tu ne profites pas du résultat.
|
||||
|
||||
---
|
||||
|
||||
## ✅ La correction
|
||||
|
||||
Comme tu n'as **pas de vrai serveur** pour traiter l'envoi (et ce n'est pas le but ici), tu dois **empêcher l'envoi par défaut dans tous les cas**, pas seulement quand le message est vide.
|
||||
|
||||
Deux façons :
|
||||
- soit ajouter `evenement.preventDefault();` **aussi** dans le `else`,
|
||||
- soit (plus propre) l'appeler **une seule fois, tout en haut** du gestionnaire, avant le `if`. Comme ça, l'envoi est toujours annulé, et c'est ton JavaScript qui décide quoi faire (afficher l'alerte).
|
||||
|
||||
**✅ Test :** après correction, tu remplis le message, tu cliques « Envoyer » → l'alerte s'affiche **et la page ne bouge plus**, que tu sois en `file://` ou en `localhost`. Comportement identique partout.
|
||||
|
||||
> 💡 **À retenir :** tant qu'il n'y a pas de serveur qui gère vraiment l'envoi, on met **`preventDefault()`** pour garder la main et tout faire en JS. Se reposer sur « de toute façon en `file://` ça ne part pas » est trompeur : dès qu'un serveur est là (localhost, et plus tard en ligne), le comportement change.
|
||||
|
||||
---
|
||||
|
||||
## ⌨️ Et ton raccourci clavier Ctrl + Entrée ? (même cause)
|
||||
|
||||
Ton code clavier est **correct**, bonne nouvelle :
|
||||
|
||||
```js
|
||||
zoneMessage.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Enter" && event.ctrlKey) {
|
||||
formulaire.requestSubmit(); // ✅ bon choix
|
||||
}
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
> 👍 Tu as bien utilisé **`requestSubmit()`** et pas `submit()`. La différence est importante : `requestSubmit()` **déclenche ton gestionnaire `submit`** (donc ta validation et — une fois corrigé — ton `preventDefault`), alors que `form.submit()` l'aurait **court-circuité** et aurait posté direct. Tu as pris le bon.
|
||||
|
||||
Du coup, `Ctrl + Entrée` passe **exactement par le même chemin** que le clic sur « Envoyer » : il déclenche l'événement `submit` → ton gestionnaire tourne → et comme le `else` n'annule pas l'envoi, le formulaire **poste** pour de vrai → en `file://` rien ne se passe (« ça marche »), en `localhost` le serveur répond **501** et tu pars sur la page d'erreur (« ça ne marche pas »).
|
||||
|
||||
👉 **Donc il n'y a pas de bug séparé pour le clavier** : c'est le même `preventDefault()` manquant. Dès que tu l'ajoutes (point ci-dessus), **le clic ET le Ctrl + Entrée** marcheront pareil, en `file://` comme en `localhost`.
|
||||
|
||||
> 🔎 Et la **501** que tu vois ? C'est normal et attendu : le petit serveur `http.server` ne sait qu'**envoyer des fichiers** (méthode GET). Il ne sait pas **traiter** un envoi de formulaire (méthode POST) → il répond « 501 Unsupported method ». Ce n'est pas à toi de « réparer » le serveur : le bon réflexe, c'est de **ne pas poster du tout** (ton `preventDefault`), puisqu'il n'y a pas de programme derrière pour recevoir les données.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Bonus — bien comprendre `file://` vs `http://localhost`
|
||||
|
||||
C'est **deux façons différentes** d'ouvrir la même page. Regarde ta **barre d'adresse**, elle te dit dans quel mode tu es.
|
||||
|
||||
- **`file:///.../contact.html`** → **aucun serveur**. Le navigateur **lit le fichier directement sur ton disque**, comme une photo ou un PDF. Pas d'adresse web, pas d'« origine ». C'est pour ça que `fetch` y est bloqué (« CORS policy ») et qu'un `<form>` n'a personne à qui poster.
|
||||
- **`http://localhost:8000/...`** → tu as lancé un **petit serveur** qui **publie** ton dossier et **répond aux demandes**, comme un vrai site, mais sur ta machine. Là il y a une vraie adresse → `fetch` fonctionne, et un POST de formulaire est bel et bien reçu (d'où l'erreur 501 plus haut, faute de code pour le traiter).
|
||||
|
||||
Décodage de l'adresse : **`localhost`** = ta propre machine ; **`8000`** (ou `5500`, `3000`…) = le **port**, le « numéro de porte » où ton serveur écoute.
|
||||
|
||||
> 🧭 L'image à garder : `file://` = lire un papier posé sur ton bureau. `http://localhost:...` = un mini-site web monté chez toi, que tu visites comme n'importe quel site. Les vraies fonctionnalités du web (`fetch`, et le traitement d'un `<form>`) ont besoin de la **deuxième** situation — ce qui explique pourquoi ton comportement change d'un mode à l'autre.
|
||||
@@ -0,0 +1,72 @@
|
||||
# 🔧 Correction — l'API affiche « Impossible de charger la flotte 😕 »
|
||||
|
||||
Tu y es presque ! Ton `fetch` fonctionne (le fichier est bien servi, statut 200), et tu as écrit du **très bon** code propre (`createLign`, `afficherFlotte`). Le souci, c'est qu'il y a **deux versions** dans ton `flotte.js` qui ne se parlent pas, et **un id qui ne correspond pas**. On démêle.
|
||||
|
||||
---
|
||||
|
||||
## 1. 🐛 Pourquoi le message d'erreur s'affiche
|
||||
|
||||
Dans `chargerFlotte`, tu vises ce `tbody` :
|
||||
|
||||
```js
|
||||
const corps = document.getElementById("corps_flotte"); // ❌ cet id n'existe pas
|
||||
```
|
||||
|
||||
Mais dans ton HTML, ton tableau de flotte a pour id **`flotte_tableau`** (pas `corps_flotte`). Donc `corps` vaut **`null`**, et un peu plus bas `corps.append(tr)` plante (« on ne peut pas faire `.append` sur `null` »).
|
||||
|
||||
Comme ce `.append` est dans ton `try`, l'erreur est **attrapée** par le `catch` → qui affiche « Impossible de charger la flotte 😕 ».
|
||||
|
||||
> 🔎 **Donc ton API n'est PAS en cause :** le fichier est bien chargé et transformé (`vehicules` contient tes 3 véhicules). C'est juste qu'au moment de les **afficher**, tu cherches un élément qui n'existe pas. Pour le voir toi-même : dans **F12 → Console**, tu verras l'erreur exacte (`Cannot read properties of null (append)`) loggée par ton `console.error(erreur)`.
|
||||
|
||||
---
|
||||
|
||||
## 2. 🧩 Le vrai souci : tu as écrit **deux fois** l'affichage
|
||||
|
||||
Regarde ton `flotte.js`, tu as en réalité **deux mécanismes** :
|
||||
|
||||
**Version A — le `fetch`** (les vraies données du fichier) :
|
||||
```js
|
||||
async function chargerFlotte() {
|
||||
... fetch ...
|
||||
for (const v of vehicules) { corps.append(tr); } // vise corps_flotte (inexistant)
|
||||
}
|
||||
```
|
||||
|
||||
**Version B — une copie écrite à la main** (`flotte1`) avec tes belles fonctions :
|
||||
```js
|
||||
const flotte1 = [ { modele: "Renault Kangoo", ... }, ... ]; // ← une COPIE en dur
|
||||
function createLign(car) { ... }
|
||||
function afficherFlotte(list) { ... } // vise flotte_tableau (qui existe ✅)
|
||||
afficherFlotte(flotte1);
|
||||
```
|
||||
|
||||
Résultat : ce que tu **vois** à l'écran, c'est la **version B** (la copie `flotte1`), pas les données venues du fichier. Et la version A échoue dans son coin. Le `fetch` ne sert donc à rien pour l'instant : tu affiches une copie, pas le vrai fichier.
|
||||
|
||||
> 💡 Le but de l'exercice `fetch`, c'est justement de **ne plus** écrire les données en dur (`flotte1`) mais de les **charger depuis `vehicules.json`**. Sinon, autant tout écrire dans le HTML.
|
||||
|
||||
---
|
||||
|
||||
## 3. ✅ La solution : **brancher** ton fetch sur tes belles fonctions
|
||||
|
||||
Tu as déjà tout : `createLign` et `afficherFlotte` sont parfaites et visent le **bon** tableau (`flotte_tableau`). Il suffit que `chargerFlotte` **leur passe** les données qu'il a récupérées, au lieu de réécrire une boucle dans son coin.
|
||||
|
||||
L'idée (à toi de l'assembler) :
|
||||
1. Dans `chargerFlotte`, après `const vehicules = await reponse.json();`, **appelle `afficherFlotte(vehicules);`** → tes vraies données passent dans ta fonction d'affichage, qui vise le bon `tbody`.
|
||||
2. **Supprime** la boucle `for (const v of vehicules) { ... corps.append ... }` de `chargerFlotte` (c'est `afficherFlotte` qui fait ce travail maintenant) — et donc la variable `corps` au mauvais id disparaît avec.
|
||||
3. **Supprime `flotte1`** et le `afficherFlotte(flotte1)` final : tu n'as plus besoin de la copie en dur, c'est le fichier qui fournit les données.
|
||||
4. Tu peux garder le `message.textContent = vehicules.length + " véhicules :"` pour le petit compteur.
|
||||
|
||||
> 🔎 En clair : `chargerFlotte` **récupère** les données, `afficherFlotte` **les affiche**. Chacun son rôle, et ils se passent le relais avec `afficherFlotte(vehicules)`. C'est exactement le même duo que sur ton tableau d'équipe (`afficherEquipe(liste)`).
|
||||
|
||||
**✅ Test :** le message « Impossible de charger » disparaît, et le tableau affiche les véhicules **venus de `vehicules.json`**. Pour le prouver : ajoute un 4ᵉ véhicule **dans le fichier `vehicules.json`** (pas dans le JS), recharge → la 4ᵉ ligne apparaît. Si oui, c'est gagné : tu affiches bien le fichier, plus une copie. 🎉
|
||||
|
||||
---
|
||||
|
||||
## ✅ Récap
|
||||
|
||||
- [ ] Comprendre : l'erreur venait de l'**id** `corps_flotte` (inexistant), pas de l'API
|
||||
- [ ] Faire `chargerFlotte` → `afficherFlotte(vehicules)` (brancher le fetch sur ta fonction d'affichage)
|
||||
- [ ] Supprimer la copie en dur `flotte1` et la boucle en double
|
||||
- [ ] Vérifier en ajoutant une ligne **dans le JSON**
|
||||
|
||||
Honnêtement tu avais déjà écrit 90 % du bon code — il ne manquait que de **relier les deux morceaux**. C'est tout l'enjeu quand un fichier grossit : éviter d'avoir deux fois la même chose qui se marche dessus.
|
||||
@@ -0,0 +1,116 @@
|
||||
# 🔧 Correction — `BarreRecherche.jsx` (+ le Badge)
|
||||
|
||||
Super boulot : ton app **marche** (likes par carte, total qui monte, données chargées via `fetch`, persistance `localStorage`). 👏 Il te reste à **bien découper** (Module H) et à réparer le Badge. On y va.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Pourquoi `BarreRecherche.jsx` casse tout
|
||||
|
||||
Ton fichier actuel :
|
||||
```jsx
|
||||
const [jeux, setJeux] = useState([]); // ❌ hook AU NIVEAU DU FICHIER
|
||||
const [recherche, setRecherche] = useState(""); // ❌ idem
|
||||
useEffect(() => { ... }, [jeux]); // ❌ idem
|
||||
|
||||
function BarreRecherche() {
|
||||
return (
|
||||
<input ... />
|
||||
{filtres.length === 0 && <p>...</p>} // ❌ plusieurs éléments sans <> </>
|
||||
{filtres.map(...)}
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Trois erreurs :
|
||||
1. **Les hooks (`useState`, `useEffect`) doivent être DANS le composant**, pas au niveau du fichier. Hors d'une fonction composant, ils n'ont aucun sens → erreur.
|
||||
2. **Un composant doit renvoyer UN seul élément.** Là, tu renvoies un `<input>` **et** des `<p>`/`<Carte>` côte à côte → erreur JSX. (On enveloppe avec `<> … </>` quand il y en a plusieurs.)
|
||||
3. Tu essaies de remettre **tout** (état, liste, total) dans `BarreRecherche` — alors que ça marche déjà très bien dans `App`.
|
||||
|
||||
> 🧠 **L'erreur de raisonnement :** tu veux « sortir » du code dans un composant, mais tu emmènes l'**état** avec. Or l'état doit **rester dans `App`** (c'est lui qui orchestre). Un petit composant comme `BarreRecherche` doit être **« bête »** : il reçoit ce dont il a besoin en **props**, et ne décide de rien.
|
||||
|
||||
---
|
||||
|
||||
## ✅ La bonne version : un composant qui reçoit des **props**
|
||||
|
||||
`BarreRecherche` n'a besoin que de **deux choses** : la valeur à afficher, et quoi faire quand on tape. On les lui passe en props.
|
||||
|
||||
### `BarreRecherche.jsx`
|
||||
```jsx
|
||||
function BarreRecherche({ valeur, onChange }) {
|
||||
return (
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search Game…"
|
||||
value={valeur}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default BarreRecherche;
|
||||
```
|
||||
> 🔎 Pas de `useState` ici, pas de liste, pas de total. Juste un input qui **affiche** `valeur` et **remonte** les frappes via `onChange`. C'est tout son travail.
|
||||
|
||||
### Dans `App.jsx` : l'état RESTE ici, on passe les props
|
||||
```jsx
|
||||
import BarreRecherche from "./components/BarreRecherche";
|
||||
|
||||
// ... dans le return, à la place de ton <input> :
|
||||
<BarreRecherche
|
||||
valeur={recherche}
|
||||
onChange={(e) => setRecherche(e.target.value)}
|
||||
/>
|
||||
```
|
||||
|
||||
> 🧩 **À retenir :** l'état (`recherche`, `total`, `jeux`…) vit dans `App`. Les petits composants reçoivent des **props** (des données + des fonctions) et restent **simples et réutilisables**. C'est exactement la même idée que `Carte` qui reçoit `onLike`.
|
||||
|
||||
**✅ Test :** la recherche fonctionne **exactement comme avant**, mais l'`<input>` est maintenant dans son propre composant propre.
|
||||
|
||||
> 💡 Tu peux faire pareil pour la **liste** : un composant `Liste` qui reçoit le tableau filtré et fait le `.map()`. `App` ne ferait plus qu'**assembler** `<BarreRecherche/>`, `<Liste/>`, `<Footer/>`.
|
||||
|
||||
---
|
||||
|
||||
## 🎖️ Le Badge qui s'affiche partout (« Jeu 3D » sur tout)
|
||||
|
||||
Ton `Badge` attend une prop `Type` :
|
||||
```jsx
|
||||
function Badge({ Type }) {
|
||||
if (Type === "2d") return null;
|
||||
return <span className="badge"> Jeu 3D </span>;
|
||||
}
|
||||
```
|
||||
Mais dans `Carte` tu l'appelles **sans rien** : `<Badge />`. Donc `Type` vaut `undefined` → ce n'est pas `"2d"` → il affiche « Jeu 3D » **sur toutes les cartes**, même les jeux 2D.
|
||||
|
||||
### La correction : faire **descendre** `Type` jusqu'au Badge
|
||||
Il faut passer `Type` de `App` → `Carte` → `Badge`.
|
||||
|
||||
1. Dans `App.jsx`, ajoute la prop à la carte :
|
||||
```jsx
|
||||
<Carte
|
||||
key={jeu.id}
|
||||
gameId={jeu.id}
|
||||
titre={jeu.titre}
|
||||
console={jeu.console}
|
||||
annee={jeu.annee}
|
||||
Type={jeu.Type} // 👈 on transmet le type
|
||||
onLike={() => setTotal((t) => t + 1)}
|
||||
/>
|
||||
```
|
||||
2. Dans `Carte.jsx`, passe-le au Badge :
|
||||
```jsx
|
||||
<Badge Type={props.Type} />
|
||||
```
|
||||
|
||||
**✅ Test :** seuls les jeux **3D** affichent le badge ; les 2D (comme *Oracle of Ages*) n'ont rien.
|
||||
|
||||
> 🧩 **À retenir :** une prop ne « saute » pas les étages. Pour qu'un petit composant tout en bas (`Badge`) connaisse une donnée, il faut la lui **transmettre de parent en enfant** (`App` → `Carte` → `Badge`).
|
||||
|
||||
---
|
||||
|
||||
## ✅ Récap
|
||||
- [ ] `BarreRecherche` = composant simple qui reçoit `valeur` + `onChange` en props (pas de hook dedans)
|
||||
- [ ] L'état reste dans `App` ; on passe les props
|
||||
- [ ] (Bonus) extraire aussi un composant `Liste`
|
||||
- [ ] Passer `Type` jusqu'au `Badge` (`App` → `Carte` → `Badge`)
|
||||
|
||||
Quand c'est bon, tu peux enchaîner sur **`ENONCE_react_3_contact.md`** (la suite). 🚀
|
||||
@@ -0,0 +1,119 @@
|
||||
# 🔧 Corrections — `fetch` (le 200), script qui ne charge pas, et boutons
|
||||
|
||||
Tu vois un **`200`** dans la console **Python** mais tu ne vois pas la réponse. C'est normal, et il y a plusieurs petites choses à démêler. On commence par le plus important.
|
||||
|
||||
---
|
||||
|
||||
## 1. 🐛 LE bug bloquant : ta balise `<script>` est mal fermée
|
||||
|
||||
Dans `contact.html` :
|
||||
|
||||
```html
|
||||
<script src="../flotte.js"</script> <!-- ❌ il manque le > -->
|
||||
```
|
||||
|
||||
Regarde bien : après `"../flotte.js"`, il **manque le `>`** qui ferme la balise ouvrante. Du coup le navigateur ne charge **pas** `flotte.js` du tout → **rien** ne s'exécute, même pas ton `console.log`. C'est pour ça que « ça ne s'affiche pas dans la console ».
|
||||
|
||||
### ✅ Correction
|
||||
```html
|
||||
<script src="../flotte.js"></script> <!-- ✅ une balise <script> = <script ...> ... </script> -->
|
||||
```
|
||||
|
||||
> 💡 **À retenir :** une balise `<script src="...">` doit être **fermée deux fois** : le `>` de la balise ouvrante, **puis** `</script>`. Compare avec tes deux autres lignes juste au-dessus (`../equipe.js`, `../contact.js`) : elles, elles ont bien le `>`. C'est ce petit `>` qui manque.
|
||||
|
||||
---
|
||||
|
||||
## 2. 🧠 Le `200` de Python ≠ « j'ai affiché les données »
|
||||
|
||||
C'est **la** notion à comprendre ici. Quand tu lances `python3 -m http.server`, tu as **deux mondes** :
|
||||
|
||||
| Le **serveur** (terminal Python) | Le **navigateur** (ta page) |
|
||||
|---|---|
|
||||
| Reçoit les demandes de fichiers et les envoie | Reçoit les fichiers et exécute le JavaScript |
|
||||
| Affiche des lignes comme `"GET /vehicules.json" 200` | Affiche tes `console.log` dans **F12 → Console** |
|
||||
|
||||
Le **`200`** veut juste dire : *« le serveur a bien trouvé et envoyé le fichier »*. C'est **côté serveur**. Ça ne dit **rien** de ce que ta page en fait.
|
||||
|
||||
👉 Donc :
|
||||
- La **réponse** (tes données) arrive dans le **navigateur**, pas dans le terminal Python. Pour la voir, regarde la **console du navigateur (F12)**, pas le terminal.
|
||||
- Et pour l'afficher **sur la page**, ton `console.log(vehicules)` ne suffit pas : il écrit juste dans la console. Il faudra **construire des lignes** dans le HTML (boucle + `createElement`/`append`, comme pour ton tableau d'équipe).
|
||||
|
||||
> 💡 **À retenir :** `200` (terminal Python) = « fichier envoyé », côté serveur. `console.log` = côté navigateur (F12). Afficher **dans la page** = encore une autre étape (du code qui crée des éléments HTML).
|
||||
|
||||
---
|
||||
|
||||
## 3. 🐛 Le chemin du fichier dans `fetch`
|
||||
|
||||
Dans `flotte.js` :
|
||||
|
||||
```js
|
||||
const reponse = await fetch("./vehicules.json"); // ⚠️ à vérifier
|
||||
```
|
||||
|
||||
Attention : le chemin d'un `fetch` est calculé **par rapport à la page** (`pages/contact.html`), pas par rapport au fichier `flotte.js`. Or `vehicules.json` est à la **racine** du projet, pas dans `pages/`.
|
||||
|
||||
- `./vehicules.json` → cherche `pages/vehicules.json` → **n'existe pas** (erreur 404).
|
||||
- `../vehicules.json` → **remonte** d'un dossier puis prend `vehicules.json` à la racine ✅
|
||||
|
||||
C'est exactement la même logique que tes balises `<script src="../equipe.js">` : le `../` pour remonter de `pages/` vers la racine.
|
||||
|
||||
> 🔎 **Comment vérifier ?** Ouvre **F12 → onglet Réseau (Network)**, recharge, et regarde la ligne `vehicules.json` : si c'est **404 (rouge)**, c'est le chemin ; si c'est **200**, le fichier est bien chargé et le souci est ailleurs (le script mal fermé du point 1).
|
||||
|
||||
---
|
||||
|
||||
## 4. ✅ Une fois les 3 points réglés
|
||||
|
||||
Ton `console.log(vehicules)` devrait afficher ton tableau de 3 véhicules **dans la console du navigateur (F12)**.
|
||||
|
||||
Étape suivante (à toi) : au lieu de juste `console.log`, **afficher** la flotte dans la page. Tu as déjà tout le savoir-faire (c'est exactement ton tableau d'équipe) :
|
||||
- un `<tbody id="...">` dans le HTML,
|
||||
- une boucle sur `vehicules`,
|
||||
- `createElement` + `append` pour chaque ligne.
|
||||
|
||||
---
|
||||
|
||||
# 🧷 Pendant qu'on y est : deux corrections sur ton formulaire
|
||||
|
||||
## A. 🐛 Le bouton « Effacer » envoie le formulaire
|
||||
|
||||
```html
|
||||
<button>Envoyer</button>
|
||||
<button type="submit" id="effacer">Effacer</button> <!-- ❌ submit -->
|
||||
```
|
||||
|
||||
Ton bouton « Effacer » est en `type="submit"`. Du coup, quand on clique dessus, il **envoie le formulaire** (au lieu de juste vider la zone) : ça déclenche ta validation et l'alerte « Merci d'écrire un message ». Ce n'est pas ce que tu veux.
|
||||
|
||||
> 🔁 **Rappel du fichier `EXPLICATION_form.md` :** dans un `<form>`, un `<button>` est **`type="submit"` par défaut**. Pour un bouton qui fait **autre chose** qu'envoyer (effacer, basculer un thème…), il faut explicitement `type="button"`.
|
||||
|
||||
### ✅ Correction
|
||||
```html
|
||||
<button>Envoyer</button> <!-- ok : submit par défaut -->
|
||||
<button type="button" id="effacer">Effacer</button> <!-- ✅ ne soumet plus -->
|
||||
```
|
||||
|
||||
## B. 🐛 Le `<label>` de l'e-mail ne pointe pas sur le bon champ
|
||||
|
||||
```html
|
||||
<label for="e_mail">Votre e-mail:</label>
|
||||
<input ... id="email"> <!-- l'id est "email", pas "e_mail" -->
|
||||
```
|
||||
|
||||
L'attribut `for` d'un label doit correspondre **exactement** à l'`id` du champ. Là, `for="e_mail"` mais `id="email"` → cliquer sur le libellé ne place pas le curseur dans le champ.
|
||||
|
||||
### ✅ Correction
|
||||
```html
|
||||
<label for="email">Votre e-mail:</label>
|
||||
```
|
||||
|
||||
> 💡 **À retenir :** `label for="X"` doit matcher `input id="X"`, au caractère près. Bien fait, cliquer le texte du label active le champ (pratique, et important pour l'accessibilité).
|
||||
|
||||
---
|
||||
|
||||
## ✅ Récap
|
||||
|
||||
- [ ] Fermer la balise : `<script src="../flotte.js"></script>` (le `>` manquant)
|
||||
- [ ] Comprendre : `200` Python = fichier envoyé ; la réponse se voit dans **F12 → Console**
|
||||
- [ ] Corriger le chemin : `fetch("../vehicules.json")` (vérifier dans l'onglet Réseau)
|
||||
- [ ] Afficher la flotte dans la page (boucle + `createElement`), pas seulement `console.log`
|
||||
- [ ] `type="button"` sur « Effacer »
|
||||
- [ ] `label for="email"` (= l'id du champ)
|
||||
@@ -0,0 +1,80 @@
|
||||
# 🔧 Correction — la recherche et le tri ne font rien
|
||||
|
||||
Tu as bien créé la fonction `afficherEquipe`, et le tableau s'affiche au chargement. Mais quand tu tapes dans la recherche ou que tu cliques « Trier », **rien ne bouge**. Voici pourquoi — c'est un bug très instructif sur les **paramètres de fonction**.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 D'abord, un indice important : la Console est *muette* cette fois
|
||||
|
||||
La fois précédente, tu avais une erreur **rouge** dans la Console (`afficherEquipe is not defined`). Là, **rien** : aucun message d'erreur.
|
||||
|
||||
> 💡 **À retenir :** il existe deux familles de bugs.
|
||||
> - Ceux qui **plantent** (message rouge dans la Console) — comme la dernière fois.
|
||||
> - Ceux qui **ne plantent pas** mais font **la mauvaise chose** — comme aujourd'hui. La Console reste silencieuse, le code « marche »… mais pas comme tu veux. Ce sont les plus sournois : il faut **raisonner**, pas juste lire les erreurs.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Le vrai problème : ta fonction ignore ce que tu lui donnes
|
||||
|
||||
Regarde **précisément** ces deux endroits dans ton `equipe.js` :
|
||||
|
||||
```js
|
||||
function afficherEquipe() { // 👈 (1) les parenthèses sont VIDES
|
||||
corps.innerHTML = "";
|
||||
for (const membre of equipe) { // 👈 (2) tu boucles toujours sur la globale "equipe"
|
||||
corps.append(creerLigne(membre));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Et plus bas, tu l'appelles avec une liste :
|
||||
|
||||
```js
|
||||
afficherEquipe(resultats); // la liste filtrée
|
||||
afficherEquipe(triee); // la liste triée
|
||||
```
|
||||
|
||||
Le souci : tu **passes** une liste (`resultats`, `triee`)… mais ta fonction ne la **reçoit nulle part**. Les parenthèses de `function afficherEquipe()` sont vides → l'argument que tu envoies tombe dans le vide. Et à l'intérieur, tu reboucles sur la variable globale `equipe`, donc **toujours les 3 membres, dans l'ordre d'origine**.
|
||||
|
||||
👉 Résultat : `filter` calcule bien la bonne liste filtrée, `sort` calcule bien la liste triée… et ta fonction **les jette** pour réafficher `equipe` en entier à chaque fois. D'où l'impression que « rien ne se passe ».
|
||||
|
||||
---
|
||||
|
||||
## 🧠 L'idée à comprendre : un *paramètre*, c'est une boîte d'entrée
|
||||
|
||||
Une fonction, c'est une recette. Un **paramètre**, c'est un ingrédient qu'on lui **tend** au moment de l'appeler.
|
||||
|
||||
- Si la recette dit *« utilise la liste que je te donne »* → tu peux lui tendre une liste différente à chaque fois, et le résultat change.
|
||||
- Ta recette actuelle dit *« ignore ce qu'on me tend, je prends toujours `equipe` sur l'étagère »* → quoi que tu lui passes, elle fait pareil.
|
||||
|
||||
Tu as **déjà** utilisé ce mécanisme sans bug ailleurs : ta fonction `creerLigne(membre)` reçoit bien un `membre` entre les parenthèses, et travaille avec **lui**. C'est exactement la même chose qu'il manque ici.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ La piste pour corriger (à toi de l'écrire)
|
||||
|
||||
1. Donne un **nom** au paramètre dans les parenthèses de `afficherEquipe` (par exemple `liste`).
|
||||
2. À l'intérieur, boucle sur **ce nom-là**, pas sur la globale `equipe`.
|
||||
3. Garde ton appel initial, mais en lui passant bien toute l'équipe : `afficherEquipe(equipe)`.
|
||||
|
||||
Ainsi :
|
||||
- au chargement, tu lui tends `equipe` → elle affiche tout le monde ;
|
||||
- quand tu tapes, tu lui tends `resultats` → elle affiche **la liste filtrée** ;
|
||||
- quand tu cliques, tu lui tends `triee` → elle affiche **la liste triée**.
|
||||
|
||||
> ⚠️ **Petit piège à éviter :** n'appelle pas ton paramètre `equipe` (le même nom que la globale). Ça « marcherait » mais ce serait très déroutant à relire. Un nom différent comme `liste` rend les choses claires : on voit tout de suite que la fonction travaille sur *ce qu'on lui donne*, pas sur une variable extérieure.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Comment vérifier que c'est réglé
|
||||
|
||||
1. Tape « mé » dans la recherche → seule Mélissa reste. Efface → les 3 reviennent.
|
||||
2. Clique « Trier A → Z » → l'ordre change (Guillaume, Matthieu, Mélissa).
|
||||
|
||||
Le reste de ton code (`filter`, `sort`) est **déjà correct** : il calcule la bonne liste. Le seul maillon cassé, c'est la fonction qui ne l'utilise pas. Répare ce maillon et tout le reste se met à marcher d'un coup.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Si tu veux aller plus loin (optionnel)
|
||||
|
||||
Une fois que ça marche, regarde la méthode **`replaceChildren`** (`MDN replaceChildren`) : elle vide et remplit un élément **en une seule ligne**, sans `innerHTML`. Combinée à `map` (Niveau 5), ta fonction pourrait tenir en deux lignes. Mais d'abord, fais marcher la version simple. 🙂
|
||||
@@ -0,0 +1,51 @@
|
||||
# 🔧 Correction — il reste deux compteurs de likes en parallèle
|
||||
|
||||
Bravo, le boss est presque parfait : `App` centralise `jeux` et `likes`, `Detail` retrouve le bon jeu, le Hall of Fame, les stats et le filtre 2D/3D marchent. Reste **un fil débranché** dans `Carte.jsx`.
|
||||
|
||||
## 🐛 Le bug : `Carte` garde encore SON propre compteur
|
||||
|
||||
Dans `Carte.jsx`, tu as gardé l'ancienne version locale des likes :
|
||||
```jsx
|
||||
const [likes, setLikes] = useState(
|
||||
() => Number(localStorage.getItem("likes-" + props.gameId)) || 0,
|
||||
);
|
||||
useEffect(() => {
|
||||
localStorage.setItem("likes-" + props.gameId, likes);
|
||||
}, [likes]);
|
||||
// ...
|
||||
<button onClick={() => { setLikes(likes + 1); props.onLike(); }}>
|
||||
❤️ {likes} {/* ← affiche le compteur LOCAL */}
|
||||
</button>
|
||||
```
|
||||
|
||||
Du coup il existe **deux compteurs différents** pour un même jeu :
|
||||
- celui d'`App` (l'objet `likes`, rangé dans `localStorage["likes"]`) → utilisé par le Hall of Fame, les stats et la page Détail ;
|
||||
- celui de `Carte` (son `useState` + `localStorage["likes-3"]`) → affiché sur le bouton de la carte.
|
||||
|
||||
Au clic, tu incrémentes **les deux** (`setLikes` ET `props.onLike`), donc ça « a l'air » de marcher. Mais les deux peuvent **diverger** : la carte affiche son chiffre à elle, pendant que le podium affiche celui d'`App`. Et tu te retrouves avec deux clés de stockage qui font la même chose.
|
||||
|
||||
> 🧩 La règle (encore elle) : **une seule source de vérité**. Les likes vivent dans `App`. `Carte` doit juste les **afficher** (`props.likes`) et **demander** l'incrément (`props.onLike`), sans tenir son propre compte.
|
||||
|
||||
## ✅ La correction dans `Carte.jsx`
|
||||
|
||||
1. Supprime le `useState` et le `useEffect` des likes (et les imports `useState`/`useEffect` s'ils ne servent plus à rien d'autre).
|
||||
2. Affiche `props.likes` au lieu de `likes`.
|
||||
3. Au clic, appelle seulement `props.onLike`.
|
||||
|
||||
```jsx
|
||||
<button onClick={props.onLike}>❤️ {props.likes}</button>
|
||||
```
|
||||
|
||||
`Accueil` passe déjà `likes={likes[jeu.id] || 0}` et `onLike={() => onLike(jeu.id)}` → la carte a tout ce qu'il faut.
|
||||
|
||||
> 🧽 Pense aussi à supprimer dans le navigateur les vieilles clés `likes-1`, `likes-2`… (onglet *Application → Local Storage* des outils dev) : elles ne servent plus.
|
||||
|
||||
## 🧹 Petit bonus : `class` → `className`
|
||||
|
||||
Dans `Accueil.jsx` et `Carte.jsx`, tes séparateurs sont écrits `<section class="spacer">`. En React, l'attribut s'appelle **`className`** (`class` est un mot réservé en JavaScript). Le navigateur l'ignore tel quel → ouvre la console (F12), tu verras l'avertissement `Invalid DOM property 'class'. Did you mean 'className'?`.
|
||||
|
||||
```jsx
|
||||
<section className="spacer"></section>
|
||||
```
|
||||
|
||||
> 💡 À retenir : en JSX, `class` devient `className`, et `for` (sur un `<label>`) devient `htmlFor`. Ce sont les deux seuls noms qui changent par rapport au HTML.
|
||||
@@ -0,0 +1,77 @@
|
||||
# 🔧 Correction — ton `map` (et le `filter` au-dessus)
|
||||
|
||||
D'abord : 👍 tu as bien corrigé le `preventDefault` (déplacé tout en haut du `submit`) — ton histoire de formulaire sur localhost est réglée, bravo.
|
||||
|
||||
Pour le `map`, ton code de la fin de `equipe.js` :
|
||||
|
||||
```js
|
||||
const array = equipe.filter(function (stagiaire) {
|
||||
return stagiaire.nom.toLowerCase().includes(mélissa, guillaume);
|
||||
});
|
||||
const mapped = array.map((x) => x - stagiaire.job - stagiaire.partie);
|
||||
console.log(mapped);
|
||||
```
|
||||
|
||||
Il y a **3 erreurs** qui s'additionnent. On les prend une par une — ouvre la Console (F12), tu verras un message rouge `... is not defined`, c'est notre point de départ.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Erreur 1 — `mélissa` et `guillaume` sont des variables qui n'existent pas
|
||||
|
||||
```js
|
||||
.includes(mélissa, guillaume)
|
||||
```
|
||||
|
||||
Sans guillemets, `mélissa` et `guillaume` ne sont pas du **texte** : JavaScript les prend pour des **variables**… qui n'existent nulle part → **`ReferenceError: mélissa is not defined`**, et tout le fichier s'arrête là.
|
||||
|
||||
Deux choses :
|
||||
- Du texte, ça se met **entre guillemets** : `"mélissa"`.
|
||||
- `includes("...")` ne prend **qu'un seul** mot à chercher (le 2ᵉ argument n'est pas un autre mot, c'est une position de départ — pas ce que tu veux).
|
||||
|
||||
> 🔎 Mais surtout : **pour l'exo `map`, tu n'as pas besoin de `filter` du tout.** `filter` sert à *garder certains* éléments ; `map` sert à *transformer* chaque élément. Là, l'objectif (l'exo 1) est juste de transformer → donc `map` **directement sur `equipe`**, sans filtre.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Erreur 2 — `stagiaire` n'existe pas dans le `map`
|
||||
|
||||
```js
|
||||
const mapped = array.map((x) => x - stagiaire.job - stagiaire.partie);
|
||||
```
|
||||
|
||||
`stagiaire`, c'était le **nom du paramètre** de ta fonction `filter` (au-dessus). Il n'existe **que** à l'intérieur de cette fonction-là. Dans le `map`, il est **inconnu** → encore une `ReferenceError`.
|
||||
|
||||
Dans le `map`, l'élément courant, c'est **`x`** (le nom que tu as choisi entre les parenthèses). C'est **lui** qu'il faut utiliser — pas `stagiaire`.
|
||||
|
||||
> 💡 **À retenir :** le paramètre d'une fonction (`stagiaire`, `x`, `membre`…) n'existe qu'**à l'intérieur de cette fonction**. Ailleurs, il n'a jamais entendu parler de lui.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Erreur 3 — le `-` (soustraction) n'a aucun sens ici
|
||||
|
||||
```js
|
||||
x - stagiaire.job - stagiaire.partie
|
||||
```
|
||||
|
||||
`x` est un **objet** (`{ nom, job, partie }`) et `.job` / `.partie` sont du **texte**. Les **soustraire** ne veut rien dire → tu obtiendrais `NaN` (« Not a Number »).
|
||||
|
||||
Le rôle du `map`, c'est de **renvoyer**, pour chaque élément, **la nouvelle valeur que tu veux**. Donc la question à te poser : *« pour chaque membre, qu'est-ce que je veux récupérer ? »*
|
||||
- juste son nom → tu renvoies `x.nom`
|
||||
- une phrase → tu renvoies `` `${x.nom} (${x.job})` ``
|
||||
|
||||
> 💡 **À retenir :** dans un `map`, la fonction doit **`return`** quelque chose **construit à partir de l'élément** (`x.nom`, etc.). Pas une soustraction.
|
||||
|
||||
---
|
||||
|
||||
## ✅ L'objectif de l'exo, remis au clair
|
||||
|
||||
L'exo 1 (Niveau 5) voulait juste : *à partir de `equipe`, fabriquer la liste des **noms**.* C'est-à-dire **transformer** chaque membre en son nom :
|
||||
|
||||
- pas de `filter`,
|
||||
- `map` sur `equipe`,
|
||||
- dans la fonction, **`return` le `.nom`** de l'élément reçu.
|
||||
|
||||
**✅ Test :** ton `console.log` doit afficher `["Mélissa", "Guillaume", "Matthieu"]` (que des noms, dans un tableau).
|
||||
|
||||
Essaie de l'écrire **toi-même** avec ces 3 corrections en tête. Si tu bloques vraiment, la solution exacte est dans `SOLUTIONS_contact_4.md` (exo 1) — mais tente d'abord, tu es à deux doigts.
|
||||
|
||||
> 🧭 La phrase à garder : **`filter` choisit** *quels* éléments garder, **`map` transforme** *chaque* élément. Ici on transforme → c'est `map`, tout seul.
|
||||
@@ -0,0 +1,92 @@
|
||||
# 🔎 Correction — faire (enfin) vivre la page Détail
|
||||
|
||||
Salut ! 👋 Ton routeur est **nickel** : clic sur « Voir les détails » → l'URL devient `/jeu/2` → la page change **sans recharger**. C'est tout l'intérêt de React Router, bravo. 🎉
|
||||
|
||||
Sauf que ta page `Detail.jsx` ressemble encore à ça :
|
||||
|
||||
```jsx
|
||||
function Detail() {
|
||||
const { id } = useParams();
|
||||
return <h1>Détails du jeu {id}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
Elle affiche juste le **numéro**. On veut le **vrai jeu** : son titre, sa console, son année, son type. Et là, une question piège…
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Le vrai problème : « où vivent les données ? »
|
||||
|
||||
`Detail` reçoit `id` (genre `"2"`). Mais la **liste des jeux** vit dans le `useState` de… `Accueil` (tu l'as remplie avec `fetch`). 😬
|
||||
|
||||
`Detail` et `Accueil` sont **deux pages sœurs** : ni l'une dans l'autre. Donc `Detail` **ne peut pas** lire l'état de `Accueil`. C'est le mur classique de React Router.
|
||||
|
||||
> 💡 **La règle :** un composant ne voit que **ses props** et **son propre state**. Pour qu'une donnée soit partagée par deux pages, elle doit vivre **au-dessus des deux**, ou dans un endroit que les deux savent lire.
|
||||
|
||||
Comme ton appli lit déjà ses données via `fetch`, on a une solution simple **et** une solution pro. Prends la simple d'abord. 👇
|
||||
|
||||
---
|
||||
|
||||
## ✅ Solution simple — la page détail va chercher la donnée elle-même
|
||||
|
||||
`Detail` connaît l'`id` voulu. Elle peut **refaire le `fetch`** du même fichier et **retrouver** le bon jeu avec `.find()`.
|
||||
|
||||
```jsx
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
function Detail() {
|
||||
const { id } = useParams();
|
||||
const [jeu, setJeu] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/zelda.json")
|
||||
.then((r) => r.json())
|
||||
.then((data) => {
|
||||
// ⚠️ piège : useParams renvoie une CHAÎNE. Tes id du JSON sont aussi
|
||||
// des chaînes ("1","2"...) → ici on compare string === string, ok.
|
||||
// Mais reste vigilant : si un id est un nombre, fais String(j.id) === id
|
||||
const trouve = data.find((j) => j.id === id);
|
||||
setJeu(trouve);
|
||||
});
|
||||
}, [id]); // 👈 on relance si l'id change (navigation d'un détail à un autre)
|
||||
|
||||
if (!jeu) return <p>Jeu introuvable 🤷 <Link to="/">← retour</Link></p>;
|
||||
|
||||
return (
|
||||
<div className="detail">
|
||||
<Link to="/">← Retour à la liste</Link>
|
||||
<h1>{jeu.titre}</h1>
|
||||
<p>🕹️ Console : {jeu.console}</p>
|
||||
<p>📅 Année : {jeu.annee}</p>
|
||||
<p>🎮 Type : {jeu.Type}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Detail;
|
||||
```
|
||||
|
||||
> 🐛 **Le piège classique ici, c'est le type de l'`id`.** `useParams()` renvoie **toujours une chaîne** (`"2"`). Tes id dans `zelda.json` sont aussi des chaînes → `j.id === id` marche. Mais le jour où un id est un **nombre**, `2 === "2"` vaut `false` et `find` ne trouve rien → compare avec `String(j.id) === id`.
|
||||
|
||||
> 🔎 **Pourquoi `[id]` dans le `useEffect` ?** Sans lui, le fetch ne se ferait **qu'une fois**. Comme on peut naviguer d'un détail à un autre (`/jeu/2` → `/jeu/3`) **sans démonter le composant**, il faut redéclencher l'effet quand `id` change.
|
||||
|
||||
**✅ Test :** clique « détails » sur Breath of the Wild → tu vois Switch / 2017 / 3d, pas juste « 2 ». Le lien « ← Retour » te ramène à la liste.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Les limites de cette solution (et c'est voulu)
|
||||
|
||||
1. Les jeux **ajoutés au formulaire** ne sont **pas** dans `zelda.json` → leur page détail dira « introuvable ».
|
||||
2. Le **nombre de likes** (stocké dans `localStorage` par chaque `Carte`) n'apparaît pas non plus.
|
||||
|
||||
Pour que **tout** suive jusqu'au détail (jeux ajoutés + likes), il faut **remonter l'état au niveau du routeur** (le `fetch` et la liste vivent dans `App`, pas dans `Accueil`). C'est le **Module L** de l'énoncé final 👇 — le vrai « boss ». 😈
|
||||
|
||||
---
|
||||
|
||||
## 🧹 Pendant qu'on y est, 2 petits nettoyages
|
||||
|
||||
1. `console.log(props.gameId)` dans `Carte.jsx` → on enlève les logs de debug avant de livrer. 🧼
|
||||
2. Dans le formulaire d'ajout, tu crées le jeu avec `console: "?"`… mais tu as un champ `plateforme`. Mets plutôt `console: plateforme` pour que la console saisie s'affiche vraiment sur la carte.
|
||||
|
||||
> 💡 **À retenir :** une page ne peut pas piocher dans le state d'une autre page. Soit elle **va chercher** la donnée elle-même (re-`fetch` + `find`), soit on fait **remonter l'état** chez le parent commun. Et `useParams()` renvoie **toujours une chaîne** → attention au type quand tu compares aux ids.
|
||||
@@ -0,0 +1,81 @@
|
||||
# 🔧 Correction — enregistrer et afficher le prénom dans l'alerte
|
||||
|
||||
Tu veux qu'au moment de l'envoi, l'alerte affiche le **prénom** saisi. Ton alerte affiche sûrement quelque chose comme :
|
||||
|
||||
```
|
||||
merci [object HTMLInputElement], ton message a été pris en compte
|
||||
```
|
||||
|
||||
Voici **pourquoi**, et comment t'en sortir — sans te donner la solution toute faite.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 Ce que tu as écrit
|
||||
|
||||
```js
|
||||
const nom = document.getElementById("prenom");
|
||||
const greeting = `merci ${nom}, ton message a été pris en compte`;
|
||||
// ...
|
||||
} else {
|
||||
alert(`${greeting}`);
|
||||
}
|
||||
```
|
||||
|
||||
Il y a **deux soucis** qui se cumulent ici. Bonne nouvelle : tu sais déjà corriger les deux, tu le fais ailleurs dans ton fichier.
|
||||
|
||||
---
|
||||
|
||||
## 🪤 Souci n°1 — il manque `.value`
|
||||
|
||||
`document.getElementById("prenom")` te donne **la case** HTML entière (l'objet `<input>`), pas le texte dedans. Pour obtenir ce que la personne a **tapé**, il faut `.value` — exactement comme tu fais avec `zoneMessage.value` pour la textarea.
|
||||
|
||||
```js
|
||||
nom // ❌ la case → s'affiche "[object HTMLInputElement]"
|
||||
nom.value // ✅ le texte tapé → "Mélissa"
|
||||
```
|
||||
|
||||
C'est ça qui produit le `[object HTMLInputElement]` dans ton alerte.
|
||||
|
||||
---
|
||||
|
||||
## 🪤 Souci n°2 — tu lis la valeur **trop tôt**
|
||||
|
||||
Regarde **où** tu construis `greeting` : tout **en haut** du fichier. Or ce code s'exécute **au chargement de la page**, à un instant où l'utilisateur n'a encore **rien tapé**. Le champ est vide à ce moment-là.
|
||||
|
||||
Du coup, même si tu ajoutes `.value`, `greeting` serait figé sur le champ **vide du démarrage**, et ne changerait jamais.
|
||||
|
||||
```js
|
||||
// en haut du fichier = au chargement → le champ est VIDE
|
||||
const greeting = `merci ${nom.value}, ...`; // ❌ figé sur "" pour toujours
|
||||
```
|
||||
|
||||
👉 La phrase doit être fabriquée **au moment du clic sur Envoyer**, c'est-à-dire **à l'intérieur** de la fonction `submit`. C'est seulement là que le champ contient ce que la personne a saisi.
|
||||
|
||||
> 🔎 Tu as déjà ce réflexe ailleurs : dans `rafraichirCompteur`, tu lis `zoneMessage.value` **au moment** où on tape, pas une fois pour toutes au début. Même logique ici.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Ce qu'il te reste à faire (à toi de l'écrire)
|
||||
|
||||
1. Garde en haut seulement l'**attrape-élément** : `const champPrenom = document.getElementById("prenom");` (sans `.value`).
|
||||
2. Dans le `else` (message non vide), **à ce moment-là**, lis `champPrenom.value` et construis ta phrase, puis `alert(...)`.
|
||||
3. Pour l'**enregistrer** : `localStorage.setItem("…", champPrenom.value)` — tu sais déjà faire, tu l'utilises pour le brouillon.
|
||||
|
||||
> 💡 Ton formulaire est en `method="get"` : quand l'envoi réussit, **la page se recharge** et l'alerte disparaît aussitôt. Comme il n'y a pas de serveur derrière, le plus simple pour ce TP est d'appeler `evenement.preventDefault()` **aussi** dans le `else`, pour que la page ne recharge pas et que tu voies ton alerte.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Comment vérifier
|
||||
|
||||
1. Tape un prénom + un message, clique **Envoyer** → l'alerte dit « merci **\<ton prénom\>**, … » (et **plus** `[object HTMLInputElement]`).
|
||||
2. Console (F12) : tape `localStorage.getItem("…")` (la clé que tu as choisie) → tu retrouves le prénom enregistré.
|
||||
3. Laisse le prénom **vide** et envoie → décide quoi faire (afficher « merci visiteur », ou exiger le prénom comme tu le fais déjà pour le message).
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Pour aller plus loin
|
||||
|
||||
- Ajouter le **nombre de caractères** du message dans l'alerte (`…value.length`).
|
||||
- Au prochain chargement, accueillir la personne : « Re-bonjour \<prénom\> » en relisant ce que tu as enregistré (`getItem`).
|
||||
|
||||
Note ta question si un point bloque encore.
|
||||
@@ -0,0 +1,145 @@
|
||||
# 🔧 Correction — React Router : « Accueil is not defined » + 2 bugs
|
||||
|
||||
Ton erreur dit l'essentiel : **`Accueil` n'est pas défini**. Tu utilises `<Accueil />` dans une `<Route>`, mais ce composant **n'existe nulle part**. On répare ça, plus un double `render` dans `main.jsx`.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug 1 — `main.jsx` rend l'app **deux fois**
|
||||
|
||||
```jsx
|
||||
createRoot(document.getElementById("root")).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
createRoot(document.getElementById("root")).render( // ❌ un 2e render dans le même #root
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>,
|
||||
);
|
||||
```
|
||||
|
||||
Il ne doit y avoir **qu'un seul** `createRoot(...).render(...)`. Là, le second écrase le premier. On **fusionne** en emboîtant `StrictMode > BrowserRouter > App` :
|
||||
|
||||
```jsx
|
||||
import { StrictMode } from "react";
|
||||
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>,
|
||||
);
|
||||
```
|
||||
|
||||
> 🧠 **Règle :** un seul point d'entrée → un seul `render`. Et `<BrowserRouter>` doit **envelopper** ce qui contient tes `<Routes>`.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug 2 — `<Accueil />` n'existe pas (l'erreur principale)
|
||||
|
||||
Dans ton `App.jsx` :
|
||||
```jsx
|
||||
<Routes>
|
||||
<Route path="/" element={<Accueil />} /> {/* ❌ Accueil n'est défini nulle part */}
|
||||
<Route path="/jeu/:id" element={<Detail />} />
|
||||
</Routes>
|
||||
```
|
||||
Tu demandes à afficher `<Accueil />`, mais tu ne l'as **ni créé ni importé** → `Accueil is not defined`.
|
||||
|
||||
Autre souci : tu as mis ton `<Routes>` **au-dessus** de tout ton contenu (Header, recherche, liste, formulaire…), et ce contenu est **en dehors** des routes. Du coup il s'afficherait sur **toutes** les pages, et la route `/` n'a rien à montrer (à part `Accueil`, qui n'existe pas).
|
||||
|
||||
### ✅ L'idée : ta liste actuelle **devient** la page `Accueil`
|
||||
|
||||
Tu as déjà tout le code de la page d'accueil (recherche + tri + liste + formulaire + total). Il faut juste le **mettre dans un composant `Accueil`**, et laisser `App` ne gérer **que** les routes.
|
||||
|
||||
**1. Crée `src/pages/Accueil.jsx`** et **déplaces-y** tout ce contenu (avec ses `useState`, le `fetch`, `ajouter`, `affiches`, etc.) :
|
||||
```jsx
|
||||
import { useState, useEffect } from "react";
|
||||
import Carte from "../components/Carte";
|
||||
import BarreRecherche from "../components/BarreRecherche";
|
||||
|
||||
function Accueil() {
|
||||
// ⬅️ tout ton code actuel de App (les useState, le fetch, ajouter, affiches…)
|
||||
return (
|
||||
<>
|
||||
<BarreRecherche valeur={recherche} onChange={(e) => setRecherche(e.target.value)} />
|
||||
{/* le select de tri, la liste affiches.map(...), le formulaire, le total… */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Accueil;
|
||||
```
|
||||
|
||||
**2. Crée `src/pages/Detail.jsx`** (sors ta fonction `Detail` de `App`) :
|
||||
```jsx
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function Detail() {
|
||||
const { id } = useParams();
|
||||
return <h1>Détails du jeu {id}</h1>;
|
||||
}
|
||||
|
||||
export default Detail;
|
||||
```
|
||||
|
||||
**3. `App.jsx` ne garde QUE l'assemblage + les routes :**
|
||||
```jsx
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import Header from "./components/Header";
|
||||
import Footer from "./components/Footer";
|
||||
import Accueil from "./pages/Accueil";
|
||||
import Detail from "./pages/Detail";
|
||||
import "./App.css";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route path="/" element={<Accueil />} />
|
||||
<Route path="/jeu/:id" element={<Detail />} />
|
||||
</Routes>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
> 🔎 `Header` et `Footer` sont **autour** des `<Routes>` → ils s'affichent sur **toutes** les pages. Seul ce qui est **dans** `<Routes>` change selon l'URL.
|
||||
|
||||
**✅ Test :** `/` affiche ta liste (page Accueil), `/jeu/2` affiche la page Détail. Plus d'erreur « Accueil is not defined ».
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug 3 — petit reste du renommage `console`
|
||||
|
||||
Dans ton `ajouter`, tu as renommé l'état en `plateforme` 👍, mais l'objet écrit encore :
|
||||
```jsx
|
||||
const nouveau = { id: Date.now(), titre, annee: Number(annee), console, Type: "3d" };
|
||||
// ^^^^^^^ = l'objet global console !
|
||||
```
|
||||
`console,` (tout seul) veut dire `console: console` → tu ranges l'**objet global `console`** dans ton jeu, pas la plateforme tapée. Corrige :
|
||||
```jsx
|
||||
const nouveau = { id: Date.now(), titre, annee: Number(annee), console: plateforme, Type: "3d" };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Récap
|
||||
- [ ] `main.jsx` : un **seul** `render`, `StrictMode > BrowserRouter > App`
|
||||
- [ ] Créer `pages/Accueil.jsx` (y déplacer ta liste/recherche/tri/formulaire + leur état)
|
||||
- [ ] Créer `pages/Detail.jsx` (sortir la fonction `Detail`)
|
||||
- [ ] `App.jsx` = `Header` + `<Routes>` + `Footer`, rien d'autre
|
||||
- [ ] `console: plateforme` dans l'objet `nouveau`
|
||||
|
||||
> 💡 Pour naviguer vers le détail, mets un `<Link to={`/jeu/${jeu.id}`}>` (dans la `Carte` ou la liste) — pas un `<a href>`.
|
||||
|
||||
> 💬 À garder : un composant qu'on utilise (`<Accueil/>`) doit **exister et être importé** ; `App` **assemble et route**, les **pages** contiennent le contenu.
|
||||
@@ -0,0 +1,114 @@
|
||||
# 🔧 Correction — le total de likes ne baisse pas quand on supprime un jeu
|
||||
|
||||
Avant la correction, prenons 5 minutes pour **vraiment** comprendre la ligne qui calcule le total, parce que c'est elle qui est au cœur du bug :
|
||||
|
||||
```jsx
|
||||
const totalLikes = Object.values(likes).reduce((s, n) => s + n, 0);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧠 D'abord : c'est quoi `.reduce` ?
|
||||
|
||||
`reduce` sert à **transformer un tableau en UNE seule valeur** (ici : la somme de tous les likes).
|
||||
|
||||
Ton objet `likes` ressemble à ça (la clé = l'id du jeu, la valeur = son nombre de likes) :
|
||||
```js
|
||||
likes = { "1": 3, "2": 1, "4": 2 }
|
||||
```
|
||||
|
||||
**Étape 1 — `Object.values(likes)`** prend seulement les **valeurs** et en fait un tableau :
|
||||
```js
|
||||
Object.values(likes) // → [3, 1, 2]
|
||||
```
|
||||
|
||||
**Étape 2 — `.reduce(...)`** parcourt ce tableau et **accumule** au fur et à mesure.
|
||||
|
||||
```jsx
|
||||
[3, 1, 2].reduce((s, n) => s + n, 0)
|
||||
// ▲ ▲ ▲
|
||||
// | | └─ valeur de DÉPART de l'accumulateur (0)
|
||||
// | └─ n = l'élément en cours
|
||||
// └─ s = l'accumulateur (ce qu'on a additionné jusqu'ici)
|
||||
```
|
||||
|
||||
Déroulé, pas à pas — imagine une **cagnotte** `s` qui commence à 0 et dans laquelle on verse chaque nombre :
|
||||
|
||||
| tour | `s` (avant) | `n` (élément) | `s + n` (après) |
|
||||
|---|---|---|---|
|
||||
| départ | — | — | **0** |
|
||||
| 1 | 0 | 3 | 3 |
|
||||
| 2 | 3 | 1 | 4 |
|
||||
| 3 | 4 | 2 | **6** |
|
||||
|
||||
→ `totalLikes` vaut **6**.
|
||||
|
||||
> 💡 À retenir : `reduce(callback, valeurDeDépart)`. Le `callback` reçoit **(accumulateur, élément)** et renvoie le **nouvel** accumulateur. Le `0` à la fin, c'est la valeur de départ (une somme commence à 0 ; un produit commencerait à 1).
|
||||
|
||||
> 🔎 C'est exactement la même idée que ta moyenne d'année :
|
||||
> `jeux.reduce((s, j) => s + Number(j.annee), 0)` → là, `s` accumule les **années** de chaque jeu `j`.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Le bug : supprimer un jeu n'enlève pas ses likes du total
|
||||
|
||||
Regarde ta fonction `supprimer` :
|
||||
```jsx
|
||||
function supprimer(id) {
|
||||
setJeux(jeux.filter((j) => j.id !== id)); // ✅ enlève le JEU
|
||||
}
|
||||
```
|
||||
|
||||
Tu retires bien le jeu du tableau `jeux`… mais **tu ne touches pas à l'objet `likes`**. Donc l'entrée du jeu supprimé (par ex. `"2": 1`) **reste** dans `likes`.
|
||||
|
||||
Et comme `totalLikes` fait `Object.values(likes).reduce(...)`, il **continue d'additionner les likes d'un jeu qui n'existe plus**. D'où le total qui ne descend pas. 🎯
|
||||
|
||||
C'est un cas classique : tu as **deux objets à garder synchronisés** (`jeux` et `likes`). Si tu supprimes dans l'un, tu dois supprimer dans l'autre.
|
||||
|
||||
---
|
||||
|
||||
## ✅ La correction : supprimer le jeu **ET** ses likes
|
||||
|
||||
`supprimer` doit aussi modifier `likes`. Or `likes`/`setLikes` vivent dans `App`. Le plus simple : **faire passer `setLikes` en props** jusqu'à `Accueil` (comme tu passes déjà `setJeux`).
|
||||
|
||||
### 1. Dans `App.jsx`, ajoute `setLikes` aux props d'`Accueil`
|
||||
```jsx
|
||||
<Accueil
|
||||
jeux={jeux}
|
||||
setJeux={setJeux}
|
||||
likes={likes}
|
||||
setLikes={setLikes} {/* 👈 en plus */}
|
||||
onLike={liker}
|
||||
/>
|
||||
```
|
||||
|
||||
### 2. Dans `Accueil.jsx`, récupère-le et complète `supprimer`
|
||||
```jsx
|
||||
function Accueil({ jeux, setJeux, likes, setLikes, onLike }) { // 👈 setLikes ajouté
|
||||
// ...
|
||||
function supprimer(id) {
|
||||
setJeux(jeux.filter((j) => j.id !== id)); // enlève le jeu
|
||||
setLikes((prev) => {
|
||||
const copie = { ...prev }; // une COPIE (on ne modifie jamais l'ancien objet)
|
||||
delete copie[id]; // on retire l'entrée de ce jeu
|
||||
return copie; // on renvoie le nouvel objet
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 🧩 Pourquoi `{ ...prev }` puis `delete` ? Même règle d'**immutabilité** que partout en React : on ne modifie pas `prev` directement, on en fait une **copie**, on enlève la clé de la copie, et on renvoie la copie. React ne réaffiche que si on lui donne un **nouvel** objet.
|
||||
|
||||
**✅ Test :** like un jeu 3 fois (total = 3) → supprime ce jeu → le total **retombe** à 0, et le Hall of Fame ne le montre plus. F5 → toujours cohérent.
|
||||
|
||||
---
|
||||
|
||||
## 🧹 Petit bonus repéré au passage
|
||||
|
||||
Tes 5 jeux du `zelda.json` ont un champ **`console`** (`"console": "Game Boy Color"`), mais ton formulaire crée les nouveaux jeux avec un champ **`plateforme`**. Du coup :
|
||||
- sur la carte tu affiches `props.plateforme` → pour les 5 jeux d'origine ça affiche **`undefined`** (ils n'ont pas `plateforme`, ils ont `console`) ;
|
||||
- dans `Detail.jsx` tu affiches `jeu.console` → là c'est l'inverse, ça marche pour les 5 d'origine mais pas pour les jeux ajoutés.
|
||||
|
||||
👉 Choisis **un seul** nom de champ (par ex. `console` partout) et utilise-le aux 3 endroits (le `nouveau` du formulaire, `Carte`, `Detail`). Une donnée = un seul nom, du début à la fin.
|
||||
|
||||
> 💡 À retenir : quand deux états décrivent la même chose (un jeu et ses likes), une action sur l'un doit se répercuter sur l'autre — sinon ils **divergent**. Et un même champ doit porter **le même nom** partout dans l'appli.
|
||||
@@ -0,0 +1,98 @@
|
||||
# 🔧 Correction — le tri + 2 bugs discrets
|
||||
|
||||
Ton formulaire d'ajout est **bon** (immutabilité avec `[...jeux, nouveau]`, champs vidés, `preventDefault`) 👏. Trois choses à régler : le **tri**, et deux petits bugs que tu n'as peut-être pas repérés.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug 1 — tu as nommé une variable `console` (à éviter absolument)
|
||||
|
||||
```jsx
|
||||
const [console, setConsole] = useState("");
|
||||
```
|
||||
|
||||
`console`, c'est **déjà** un objet de JavaScript (celui de `console.log` !). En créant une variable qui s'appelle `console`, tu **l'écrases** dans tout ton composant : si un jour tu écris `console.log(...)` dans `App`, tu auras `console.log is not a function` (car `console` vaut maintenant `""`).
|
||||
|
||||
### ✅ Renomme-la
|
||||
```jsx
|
||||
const [plateforme, setPlateforme] = useState("");
|
||||
```
|
||||
…et adapte partout où tu l'utilises (`value={plateforme}`, `setPlateforme(...)`, et `console: plateforme` dans l'objet `nouveau`).
|
||||
|
||||
> 💡 **À retenir :** évite de nommer tes variables comme des mots **déjà pris** par JavaScript : `console`, `document`, `window`, `event`… Tu risques d'écraser un outil du langage sans t'en rendre compte.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug 2 — `BarreRecherche` ne reçoit pas la bonne prop
|
||||
|
||||
Ton composant attend une prop **`valeur`** :
|
||||
```jsx
|
||||
function BarreRecherche({ valeur, onChange }) { ... value={valeur} ... }
|
||||
```
|
||||
Mais dans `App` tu lui passes **`value`** :
|
||||
```jsx
|
||||
<BarreRecherche value={recherche} onChange={...} /> // ❌ "value", pas "valeur"
|
||||
```
|
||||
Du coup, à l'intérieur, `valeur` est `undefined` → le champ n'est plus **contrôlé** par ton état.
|
||||
|
||||
### ✅ Mets le même nom des deux côtés
|
||||
```jsx
|
||||
<BarreRecherche valeur={recherche} onChange={(e) => setRecherche(e.target.value)} />
|
||||
```
|
||||
|
||||
> 🧩 **À retenir :** une prop, c'est comme une étiquette : le nom que tu mets dans `App` (`valeur=`) doit être **exactement** celui que le composant lit (`{ valeur }`). Sinon, le composant reçoit `undefined`.
|
||||
|
||||
---
|
||||
|
||||
## 🔀 Le tri (Module J) — comment le faire marcher
|
||||
|
||||
Pour trier, il te faut **trois** choses, et bien les **enchaîner** : filtrer → trier → afficher.
|
||||
|
||||
### Étape 1 — un état pour le critère de tri
|
||||
```jsx
|
||||
const [tri, setTri] = useState("titre");
|
||||
```
|
||||
|
||||
### Étape 2 — un `<select>` contrôlé (à côté de la barre de recherche, pas « dans » le formulaire d'ajout)
|
||||
```jsx
|
||||
<select value={tri} onChange={(e) => setTri(e.target.value)}>
|
||||
<option value="titre">Titre (A→Z)</option>
|
||||
<option value="annee">Année</option>
|
||||
</select>
|
||||
```
|
||||
|
||||
### Étape 3 — filtrer PUIS trier, sur la même liste
|
||||
```jsx
|
||||
const affiches = [...jeux]
|
||||
.filter((j) => j.titre.toLowerCase().includes(recherche.toLowerCase()))
|
||||
.sort((a, b) => {
|
||||
if (tri === "titre") return a.titre.localeCompare(b.titre);
|
||||
if (tri === "annee") return a.annee - b.annee;
|
||||
return 0;
|
||||
});
|
||||
```
|
||||
|
||||
### Étape 4 — afficher **`affiches`** (et non `filtres`)
|
||||
```jsx
|
||||
{affiches.length === 0 && <p>Aucun jeu trouvé 😕</p>}
|
||||
{affiches.map((jeu) => (
|
||||
<Carte key={jeu.id} gameId={jeu.id} titre={jeu.titre} console={jeu.console}
|
||||
annee={jeu.annee} Type={jeu.Type} onLike={() => setTotal((t) => t + 1)} />
|
||||
))}
|
||||
```
|
||||
Tu peux **supprimer** l'ancien `const filtres = ...` : `affiches` fait déjà le filtre **et** le tri.
|
||||
|
||||
> 🧠 **Le piège classique du tri** (Guillaume est tombé dedans aussi !) : on calcule une liste triée mais on **affiche l'autre**. La règle : **on affiche la liste qu'on a vraiment transformée.** Ici, `affiches` fait filtre + tri, donc on `.map()` `affiches`.
|
||||
|
||||
> 🔎 Le tri **n'a rien à faire « dans le formulaire »** : le `<select>` est un élément à part (au-dessus de la liste), pas dans le `<form>` d'ajout. Garde-les séparés.
|
||||
|
||||
**✅ Test :** change le `<select>` → l'ordre des cartes change ; combiné avec la recherche, les deux marchent ensemble.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Récap
|
||||
- [ ] Renommer l'état `console` → `plateforme` (mot réservé !)
|
||||
- [ ] `BarreRecherche` : passer `valeur=` (pas `value=`)
|
||||
- [ ] Tri : état `tri` + `<select>` contrôlé + liste **filtrée puis triée** (`affiches`)
|
||||
- [ ] Afficher **`affiches`** dans le `.map()`, supprimer l'ancien `filtres`
|
||||
|
||||
> 💬 À garder : un nom de variable **jamais** pris par JS ; une prop au **même nom** des deux côtés ; et **on affiche la liste qu'on a transformée**.
|
||||
@@ -0,0 +1,335 @@
|
||||
# 🏁 Énoncé FINAL — « Boss de fin »
|
||||
|
||||
Dernier jour. Tu as tout l'arsenal : composants, props, state, lifting state up, `useEffect`, `fetch`, formulaire, tri, filtre, React Router, `localStorage`. Aujourd'hui, place à la pratique libre.
|
||||
|
||||
Un seul module est **obligatoire** : le **Module L** (la page détail, le vrai boss). Les autres forment un **menu** : pioche dans l'ordre que tu veux, fais-en le plus possible. Chacun est court et indépendant.
|
||||
|
||||
| Module | Tu fais | Niveau |
|
||||
|---|---|---|
|
||||
| **L** | Page détail **complète** (données centralisées dans `App`) — *obligatoire* | ⭐⭐⭐ |
|
||||
| M | Bouton « au hasard » (`useNavigate`) | ⭐⭐ |
|
||||
| N | Le Hall of Fame des jeux les plus likés | ⭐⭐ |
|
||||
| O | Bouton « supprimer » un jeu | ⭐⭐ |
|
||||
| P | Thème sombre qui se souvient du choix | ⭐⭐ |
|
||||
| Q | Mode Versus : deux jeux s'affrontent | ⭐⭐ |
|
||||
| R | Filtre par Type (2d / 3d) | ⭐⭐ |
|
||||
| S | Statistiques (total, moyenne d'année) | ⭐⭐ |
|
||||
| T | Page « 404 » pour les URL inconnues | ⭐⭐ |
|
||||
| U | Tri ascendant ↔ descendant | ⭐ |
|
||||
| V | Custom hook `useLocalStorage` (refacto) | ⭐⭐⭐ |
|
||||
|
||||
---
|
||||
|
||||
## 🔑 Module L — Le boss : centraliser les données dans `App` (obligatoire)
|
||||
|
||||
Dans `CORRECTION_page_detail.md` tu as fait une version simple (la page détail re-`fetch` toute seule). Ses limites : les jeux **ajoutés au formulaire** et les **likes** n'arrivent pas dans le détail, parce que chacun vit dans son coin. On corrige ça proprement.
|
||||
|
||||
L'idée : le `fetch`, la liste `jeux` **et** les likes doivent vivre **au-dessus** des deux pages → dans `App`, qui les passe ensuite **en props** aux routes.
|
||||
|
||||
### L.1 — Remonter le `fetch` et les états dans `App.jsx`
|
||||
```jsx
|
||||
import { useState, useEffect } from "react";
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import Header from "./components/Header";
|
||||
import Footer from "./components/Footer";
|
||||
import Accueil from "./pages/Accueil";
|
||||
import Detail from "./pages/Detail";
|
||||
|
||||
function App() {
|
||||
const [jeux, setJeux] = useState([]);
|
||||
const [likes, setLikes] = useState(() =>
|
||||
JSON.parse(localStorage.getItem("likes") || "{}")
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/zelda.json").then((r) => r.json()).then(setJeux);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("likes", JSON.stringify(likes));
|
||||
}, [likes]);
|
||||
|
||||
function liker(id) {
|
||||
setLikes((prev) => ({ ...prev, [id]: (prev[id] || 0) + 1 }));
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route path="/" element={
|
||||
<Accueil jeux={jeux} setJeux={setJeux} likes={likes} onLike={liker} />
|
||||
} />
|
||||
<Route path="/jeu/:id" element={
|
||||
<Detail jeux={jeux} likes={likes} />
|
||||
} />
|
||||
</Routes>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> Le déclic : une route reçoit des props comme n'importe quel composant — `element={<Detail jeux={jeux} />}`. C'est ça qui « branche » la donnée sur les deux pages.
|
||||
|
||||
> Nouveau réflexe likes : avant, chaque `Carte` gardait son like dans son coin (`localStorage.getItem("likes-" + id)`). Maintenant c'est **un seul objet** `likes` (genre `{ "2": 3, "4": 1 }`) rangé d'un coup. C'est ce qui rend possible le Hall of Fame du Module N (impossible quand chaque like est éparpillé).
|
||||
|
||||
### L.2 — `Accueil` et `Carte` lisent via les props
|
||||
- `Accueil({ jeux, setJeux, likes, onLike })` : enlève son `useState(jeux)` et son `fetch` (ils sont dans `App`). Garde `recherche`, `tri`, les champs du formulaire en local.
|
||||
- Passe à chaque carte le bon compteur : `likes={likes[jeu.id] || 0}` et `onLike={() => onLike(jeu.id)}`.
|
||||
- Dans `Carte`, supprime le `useState`/`useEffect` de likes : affiche `props.likes` et appelle `props.onLike` au clic.
|
||||
|
||||
### L.3 — `Detail` affiche le vrai jeu avec ses likes
|
||||
```jsx
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
|
||||
function Detail({ jeux, likes }) {
|
||||
const { id } = useParams();
|
||||
const jeu = jeux.find((j) => String(j.id) === id);
|
||||
if (!jeu) return <p>Introuvable — <Link to="/">retour</Link></p>;
|
||||
|
||||
return (
|
||||
<div className="detail">
|
||||
<Link to="/">← Retour</Link>
|
||||
<h1>{jeu.titre}</h1>
|
||||
<p>{jeu.console} — {jeu.annee} — {jeu.Type}</p>
|
||||
<p>❤️ Likes : {likes[jeu.id] || 0}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Test du boss :** ajoute un jeu au formulaire → clique « détails » dessus → il s'affiche (alors qu'avant il était introuvable). Like-le sur l'accueil → son détail affiche le bon nombre. Recharge la page → les likes sont toujours là.
|
||||
|
||||
> Ce que tu viens de faire s'appelle « lifting state up » poussé jusqu'au routeur : l'état monte au plus petit ancêtre commun (`App`) des composants qui en ont besoin. C'est le réflexe d'archi central en React.
|
||||
|
||||
---
|
||||
|
||||
## Module M — Bouton « au hasard »
|
||||
|
||||
Un bouton qui t'envoie sur la fiche d'un jeu au hasard. On découvre `useNavigate` (naviguer en JS, pas via un clic sur un `<Link>`).
|
||||
|
||||
```jsx
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const navigate = useNavigate();
|
||||
function hasard() {
|
||||
const i = Math.floor(Math.random() * jeux.length);
|
||||
navigate(`/jeu/${jeux[i].id}`);
|
||||
}
|
||||
// <button onClick={hasard}>🎲 Au hasard</button>
|
||||
```
|
||||
|
||||
> `<Link>` = naviguer au clic sur un lien. `useNavigate()` = naviguer depuis du code (après un calcul, un envoi de formulaire…).
|
||||
|
||||
**Test :** clique plusieurs fois → tu tombes sur des jeux différents.
|
||||
|
||||
---
|
||||
|
||||
## Module N — Le Hall of Fame
|
||||
|
||||
En haut de l'accueil, le Top 3 des jeux les plus likés, avec des médailles. (Possible uniquement parce que les likes sont centralisés depuis le Module L.)
|
||||
|
||||
```jsx
|
||||
const medailles = ["🥇", "🥈", "🥉"];
|
||||
|
||||
const top = [...jeux]
|
||||
.sort((a, b) => (likes[b.id] || 0) - (likes[a.id] || 0))
|
||||
.slice(0, 3);
|
||||
|
||||
// <div className="hall">
|
||||
// {top.map((j, index) => (
|
||||
// <p key={j.id}>{medailles[index]} {j.titre} — {likes[j.id] || 0} ❤️</p>
|
||||
// ))}
|
||||
// </div>
|
||||
```
|
||||
|
||||
> `.slice(0, 3)` garde les indices 0, 1, 2. Le 2ᵉ paramètre de `.map((j, index) => ...)`, c'est la position → parfait pour la médaille.
|
||||
|
||||
**Test :** like beaucoup un jeu → il grimpe en haut du Hall of Fame en direct.
|
||||
|
||||
---
|
||||
|
||||
## Module O — Supprimer un jeu
|
||||
|
||||
Un bouton « supprimer » sur chaque carte. La clé : l'immutabilité (un nouveau tableau **sans** l'élément).
|
||||
|
||||
```jsx
|
||||
function supprimer(id) {
|
||||
setJeux(jeux.filter((j) => j.id !== id));
|
||||
}
|
||||
// sur la Carte : <button onClick={() => props.onDelete()}>🗑️</button>
|
||||
// et dans Accueil : onDelete={() => supprimer(jeu.id)}
|
||||
```
|
||||
|
||||
> `filter` renvoie un **nouveau** tableau contenant tout sauf l'élément visé. On ne touche jamais l'ancien (immutabilité, comme pour l'ajout).
|
||||
|
||||
**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 — exactement le réflexe `localStorage` que tu connais des likes.
|
||||
|
||||
```jsx
|
||||
const [sombre, setSombre] = useState(
|
||||
() => localStorage.getItem("theme") === "sombre"
|
||||
);
|
||||
useEffect(() => {
|
||||
localStorage.setItem("theme", sombre ? "sombre" : "clair");
|
||||
}, [sombre]);
|
||||
|
||||
// <div className={sombre ? "app sombre" : "app"}>
|
||||
// <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 jeux ; on compare leurs likes (ou leurs années) et on affiche le gagnant.
|
||||
|
||||
```jsx
|
||||
const [a, setA] = useState(jeux[0].id);
|
||||
const [b, setB] = useState(jeux[1].id);
|
||||
|
||||
const likesA = likes[a] || 0;
|
||||
const likesB = likes[b] || 0;
|
||||
const gagnant = likesA === likesB ? "Égalité" :
|
||||
(likesA > likesB ? jeux.find(j => String(j.id) === String(a)).titre
|
||||
: jeux.find(j => String(j.id) === String(b)).titre);
|
||||
```
|
||||
Deux `<select>` (un pour `a`, un pour `b`) listant `jeux.map(j => <option value={j.id}>{j.titre}</option>)`, puis un `<p>` qui annonce le gagnant.
|
||||
|
||||
> Attention au type de la valeur d'un `<select>` (elle revient en chaîne) : compare avec `String(...)` des deux côtés.
|
||||
|
||||
**Test :** change les deux menus → le verdict se met à jour tout seul.
|
||||
|
||||
---
|
||||
|
||||
## Module R — Filtre par Type
|
||||
|
||||
Tes jeux ont un champ `Type` (`"2d"` / `"3d"`). Ajoute trois boutons (Tous / 2d / 3d) qui filtrent la liste, en plus de ta recherche.
|
||||
|
||||
```jsx
|
||||
const [filtreType, setFiltreType] = useState("tous");
|
||||
|
||||
// dans ta chaîne filtre → tri → map, ajoute une étape :
|
||||
const affiches = [...jeux]
|
||||
.filter((j) => j.titre.toLowerCase().includes(recherche.toLowerCase()))
|
||||
.filter((j) => filtreType === "tous" || j.Type === filtreType)
|
||||
.sort(/* ... */);
|
||||
|
||||
// <button onClick={() => setFiltreType("tous")}>Tous</button>
|
||||
// <button onClick={() => setFiltreType("2d")}>2D</button>
|
||||
// <button onClick={() => setFiltreType("3d")}>3D</button>
|
||||
```
|
||||
|
||||
> On enchaîne deux `.filter()` : recherche **puis** type. Chaque filtre renvoie un nouveau tableau, on les compose tranquillement.
|
||||
|
||||
**Test :** clique « 2D » → seuls les jeux 2d restent, et la recherche marche toujours par-dessus.
|
||||
|
||||
---
|
||||
|
||||
## Module S — Statistiques
|
||||
|
||||
Une petite barre de stats calculée à partir des données. On muscle `reduce`.
|
||||
|
||||
```jsx
|
||||
const nbJeux = jeux.length;
|
||||
const totalLikes = Object.values(likes).reduce((s, n) => s + n, 0);
|
||||
const anneeMoyenne = nbJeux
|
||||
? Math.round(jeux.reduce((s, j) => s + Number(j.annee), 0) / nbJeux)
|
||||
: 0;
|
||||
// <p>{nbJeux} jeux · {totalLikes} likes · année moyenne {anneeMoyenne}</p>
|
||||
```
|
||||
|
||||
> `reduce` réduit un tableau à **une seule** valeur en accumulant. Ici on additionne likes et années.
|
||||
|
||||
**Test :** ajoute/like des jeux → les chiffres bougent.
|
||||
|
||||
---
|
||||
|
||||
## Module T — Page « 404 »
|
||||
|
||||
Que se passe-t-il si on tape `/jeu/banane` ou une URL qui n'existe pas ? Rien d'affiché. On ajoute une route attrape-tout.
|
||||
|
||||
```jsx
|
||||
import { Link } from "react-router-dom";
|
||||
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 → la page 404 s'affiche.
|
||||
|
||||
---
|
||||
|
||||
## Module U — Tri ascendant ↔ descendant
|
||||
|
||||
Ajoute un bouton qui inverse l'ordre du tri en cours.
|
||||
|
||||
```jsx
|
||||
const [ordre, setOrdre] = useState("asc");
|
||||
// après ton .sort(...) :
|
||||
const affiches = [...maListeTriee];
|
||||
if (ordre === "desc") affiches.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 V — Custom hook `useLocalStorage` (refacto)
|
||||
|
||||
Tu répètes le même schéma `useState` + `useEffect(localStorage…)` pour les likes et le thème. On range ça dans **un hook réutilisable**.
|
||||
|
||||
```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 dans `App` : `const [likes, setLikes] = useLocalStorage("likes", {});` remplace les deux blocs likes d'un coup.
|
||||
|
||||
> Un custom hook, c'est juste une fonction qui commence par `use` et qui utilise d'autres hooks. Ça permet de **réutiliser de la logique** entre composants.
|
||||
|
||||
**Test :** like des jeux → recharge → les likes sont toujours là, mais avec deux fois moins de code.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist
|
||||
- [ ] L — `fetch` + `jeux` + `likes` remontés dans `App`, page détail complète *(obligatoire)*
|
||||
- [ ] M — au hasard · [ ] N — Hall of Fame · [ ] O — supprimer · [ ] P — thème sombre
|
||||
- [ ] Q — versus · [ ] R — filtre Type · [ ] S — stats · [ ] T — page 404
|
||||
- [ ] U — tri inversé · [ ] V — custom hook
|
||||
|
||||
## 🧠 Concepts rencontrés aujourd'hui
|
||||
`useNavigate` · `fetch` + état remontés au niveau du routeur + props passées à une `<Route>` · likes **centralisés** dans un objet · `filter` (supprimer, filtrer) · `sort`/`slice`/`index` (classement) · `reduce` (stats) · route attrape-tout `path="*"` · CSS piloté par le state · **custom hook**.
|
||||
|
||||
Bravo pour ces deux semaines. Montre-moi tes modules battus quand tu en tiens un.
|
||||
@@ -0,0 +1,166 @@
|
||||
# 💾 Énoncé — Sauvegarder les JEUX (pas que les likes)
|
||||
|
||||
Aujourd'hui, tes **likes** survivent au refresh (bravo, c'est exactement le bon réflexe `localStorage`). Mais la **liste des jeux**, elle, repart du `zelda.json` à chaque rechargement : tes ajouts disparaissent, et tu ne peux ni supprimer ni modifier durablement.
|
||||
|
||||
Objectif : faire vivre la liste `jeux` **comme** les likes — initialisée depuis `localStorage`, sauvegardée à chaque changement — pour pouvoir **ajouter, supprimer, modifier** et tout retrouver après un F5.
|
||||
|
||||
> ⚠️ On ne touche à rien d'autre que `App.jsx`, `Accueil.jsx` et `Carte.jsx`. Tu connais déjà toutes les briques : la seule **idée neuve** est « d'où viennent les jeux la première fois ? ».
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Le seul piège à comprendre d'abord
|
||||
|
||||
Pour les likes c'était simple : au tout début ils valent `{}`.
|
||||
```jsx
|
||||
const [likes] = useState(() => JSON.parse(localStorage.getItem("likes") || "{}"));
|
||||
```
|
||||
|
||||
Pour les jeux, c'est différent : la **première fois**, ils ne viennent pas de `localStorage` (vide) mais du **`fetch` du `zelda.json`**. Donc la logique est :
|
||||
|
||||
> **Si `localStorage` contient déjà des jeux → on les prend. Sinon (première visite) → on va chercher le `zelda.json`.**
|
||||
|
||||
Une fois chargés, `localStorage` devient **la source de vérité** : c'est lui qu'on relit aux prochains refresh (avec tes ajouts/suppressions/modifs), et le `json` ne ressert plus.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Étape 1 — Initialiser `jeux` depuis `localStorage` (dans `App.jsx`)
|
||||
|
||||
Remplace ton `useState([])` par une initialisation paresseuse, avec une **sentinelle `null`** qui veut dire « pas encore chargé » :
|
||||
|
||||
```jsx
|
||||
const [jeux, setJeux] = useState(() => {
|
||||
const stocke = localStorage.getItem("jeux");
|
||||
return stocke ? JSON.parse(stocke) : null; // null = jamais chargé
|
||||
});
|
||||
```
|
||||
|
||||
> 🔎 Pourquoi `null` et pas `[]` ? Parce qu'on doit **distinguer** deux cas : « la liste est vide parce que tu as tout supprimé » (`[]`, à garder !) et « on n'a encore rien chargé » (`null`, il faut aller chercher le json). Avec `[]` partout, impossible de faire la différence.
|
||||
|
||||
## 📝 Étape 2 — Aller chercher le json UNE SEULE FOIS si besoin
|
||||
|
||||
Ton `useEffect` de fetch existe déjà. Ajoute juste la condition « seulement si rien n'est encore chargé » :
|
||||
|
||||
```jsx
|
||||
useEffect(() => {
|
||||
if (jeux === null) { // rien en localStorage → 1re visite
|
||||
fetch("/zelda.json")
|
||||
.then((r) => r.json())
|
||||
.then(setJeux);
|
||||
}
|
||||
}, []); // [] : on ne tente ça qu'au montage
|
||||
```
|
||||
|
||||
## 📝 Étape 3 — Sauvegarder à chaque changement
|
||||
|
||||
Exactement le même réflexe que pour les likes, mais on **ne sauvegarde pas** tant que c'est `null` (sinon on écrirait « null » par-dessus tes données) :
|
||||
|
||||
```jsx
|
||||
useEffect(() => {
|
||||
if (jeux !== null) {
|
||||
localStorage.setItem("jeux", JSON.stringify(jeux));
|
||||
}
|
||||
}, [jeux]);
|
||||
```
|
||||
|
||||
## 📝 Étape 4 — Gérer l'écran « en cours de chargement »
|
||||
|
||||
Tant que `jeux` vaut `null`, `Accueil` ne peut pas faire `jeux.length` ou `jeux.map` (ça planterait). Affiche un petit message le temps du fetch. Dans `App.jsx`, juste avant le `return` des routes :
|
||||
|
||||
```jsx
|
||||
if (jeux === null) return <p>Chargement…</p>;
|
||||
```
|
||||
|
||||
**✅ Test de l'étape 1-4 :** recharge la page → tout s'affiche normalement. Ouvre les outils dev (F12) → *Application → Local Storage* → tu dois voir une clé **`jeux`** avec tous tes jeux dedans. 🎉
|
||||
|
||||
---
|
||||
|
||||
## ➕ Étape 5 — Ajouter (ça marche déjà… et maintenant ça PERSISTE)
|
||||
|
||||
Ta fonction `ajouter` fait déjà `setJeux([...jeux, nouveau])`. Tu n'as **rien à changer** : comme `setJeux` déclenche le `useEffect` de l'étape 3, l'ajout est **automatiquement** sauvegardé.
|
||||
|
||||
**✅ Test :** ajoute un jeu → F5 → il est **toujours là**.
|
||||
|
||||
> 🛠️ Détail : ton formulaire crée `Type: "3d"` en dur. Tu peux ajouter un `<select>` (2d / 3d) si tu veux choisir, comme pour la console.
|
||||
|
||||
---
|
||||
|
||||
## 🗑️ Étape 6 — Supprimer un jeu
|
||||
|
||||
La liste vit dans `App`, donc la fonction qui supprime doit pouvoir appeler `setJeux`. `Accueil` reçoit déjà `setJeux` en props → on définit `supprimer` dans `Accueil` et on la passe à chaque `Carte`.
|
||||
|
||||
Dans `Accueil.jsx` :
|
||||
```jsx
|
||||
function supprimer(id) {
|
||||
setJeux(jeux.filter((j) => j.id !== id)); // nouveau tableau SANS ce jeu
|
||||
}
|
||||
```
|
||||
Puis sur chaque carte, passe la fonction :
|
||||
```jsx
|
||||
<Carte
|
||||
/* ...les props existantes... */
|
||||
onDelete={() => supprimer(jeu.id)}
|
||||
/>
|
||||
```
|
||||
Et dans `Carte.jsx`, un bouton :
|
||||
```jsx
|
||||
<button onClick={props.onDelete}>🗑️ Supprimer</button>
|
||||
```
|
||||
|
||||
> 🧩 `filter` renvoie un **nouveau** tableau contenant tout **sauf** le jeu visé. On ne modifie jamais l'ancien (immutabilité, comme pour l'ajout). Et grâce à l'étape 3, la suppression est sauvegardée toute seule.
|
||||
|
||||
**✅ Test :** supprime un jeu → F5 → il **ne revient pas**.
|
||||
|
||||
---
|
||||
|
||||
## ✏️ Étape 7 — Modifier un jeu
|
||||
|
||||
Le plus costaud, même principe. On veut remplacer **un seul** jeu dans la liste en gardant les autres intacts → c'est le boulot de `map`.
|
||||
|
||||
Dans `Accueil.jsx` :
|
||||
```jsx
|
||||
function modifier(id, nouveauTitre) {
|
||||
setJeux(
|
||||
jeux.map((j) => (j.id === id ? { ...j, titre: nouveauTitre } : j)),
|
||||
);
|
||||
}
|
||||
```
|
||||
> 🔎 `map` parcourt la liste : pour le jeu visé, il renvoie une **copie modifiée** (`{ ...j, titre: nouveauTitre }`) ; pour les autres, il les renvoie tels quels. Résultat : un nouveau tableau où seul un jeu a changé.
|
||||
|
||||
Pour l'interface, le plus simple pour commencer : un bouton « ✏️ Renommer » sur la carte qui demande le nouveau titre, par exemple avec `prompt` (suffisant pour un premier jet) :
|
||||
```jsx
|
||||
<button onClick={() => {
|
||||
const t = prompt("Nouveau titre ?", props.titre);
|
||||
if (t) props.onEdit(t);
|
||||
}}>✏️ Renommer</button>
|
||||
```
|
||||
…avec `onEdit={(t) => modifier(jeu.id, t)}` passé depuis `Accueil`.
|
||||
|
||||
> 🚀 Version « propre » (bonus) : un champ `<input>` qui apparaît dans la carte quand on clique « modifier » (un état `enEdition` qui retient l'id en cours d'édition), et un bouton « Valider ». Plus joli, mais commence par la version `prompt`.
|
||||
|
||||
**✅ Test :** renomme un jeu → F5 → le nouveau nom est **conservé** (et la page Détail l'affiche aussi).
|
||||
|
||||
---
|
||||
|
||||
## 🧯 Le bouton « réinitialiser » (très utile pour debugger)
|
||||
|
||||
Quand on bricole `localStorage`, on aime pouvoir tout remettre à zéro. Un petit bouton :
|
||||
```jsx
|
||||
<button onClick={() => { localStorage.removeItem("jeux"); location.reload(); }}>
|
||||
♻️ Réinitialiser les jeux
|
||||
</button>
|
||||
```
|
||||
Il efface la clé `jeux` → au rechargement, `localStorage` est vide → on repart du `zelda.json` (retour à l'étape 2).
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist
|
||||
- [ ] 1-4 — `jeux` initialisé depuis `localStorage` (sentinelle `null`), fetch **une seule fois**, sauvegarde à chaque changement, écran « Chargement… »
|
||||
- [ ] 5 — ajout persistant (rien à changer, ça marche tout seul)
|
||||
- [ ] 6 — suppression (`filter` + bouton sur la carte)
|
||||
- [ ] 7 — modification (`map` + `prompt`, puis version `<input>` en bonus)
|
||||
- [ ] bonus — bouton « réinitialiser »
|
||||
|
||||
## 🧠 Concepts
|
||||
Persistance d'une **liste entière** dans `localStorage` · réconcilier **fetch (1re fois)** vs **localStorage (ensuite)** avec une sentinelle `null` · `filter` (supprimer) et `map` (modifier) **immuables** · une fonction d'`App` passée en props jusqu'à `Carte` pour agir sur la source de vérité.
|
||||
|
||||
> 🔁 Et si tu veux ranger tout ça proprement : le **custom hook `useLocalStorage`** de l'énoncé final fait exactement ce schéma « état + sauvegarde auto ». Une fois la persistance comprise ici, tu pourras remplacer tes blocs `useState`/`useEffect` par `const [jeux, setJeux] = useLocalStorage("jeux", null)`.
|
||||
@@ -0,0 +1,155 @@
|
||||
# 🗡️ Énoncé React #3 — formulaire, tri & plusieurs pages
|
||||
|
||||
(À attaquer une fois `BarreRecherche` et le Badge réparés via `CORRECTION_barre_recherche.md`.)
|
||||
|
||||
Tu maîtrises déjà : composants, props, état, **lifting state up**, `useEffect`, **`fetch`**. On monte d'un cran avec **3 nouveaux gros morceaux**. Explications + mini-exemples ; à toi d'appliquer sur tes jeux Zelda.
|
||||
|
||||
| Module | Tu apprends | Niveau |
|
||||
|---|---|---|
|
||||
| I | **Formulaire d'ajout** : modifier une liste en state | ⭐⭐ |
|
||||
| J | **Trier** la liste (select contrôlé + `sort`) | ⭐⭐ |
|
||||
| K | **Plusieurs pages** avec React Router | ⭐⭐⭐ |
|
||||
|
||||
---
|
||||
|
||||
## 📝 Module I — Ajouter un jeu via un formulaire
|
||||
|
||||
Ta liste `jeux` est **déjà** un état (tu l'as remplie avec `fetch`), parfait : on peut donc y **ajouter**.
|
||||
|
||||
### Un formulaire contrôlé
|
||||
```jsx
|
||||
const [titre, setTitre] = useState("");
|
||||
const [annee, setAnnee] = useState("");
|
||||
|
||||
function ajouter(e) {
|
||||
e.preventDefault(); // empêche le rechargement de page
|
||||
if (titre.trim() === "") return; // on n'ajoute pas du vide
|
||||
const nouveau = { id: Date.now(), titre, annee: Number(annee), console: "?", Type: "3d" };
|
||||
setJeux([...jeux, nouveau]); // 👈 NOUVEAU tableau = ancien + le nouveau
|
||||
setTitre(""); // on vide les champs
|
||||
setAnnee("");
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={ajouter}>
|
||||
<input value={titre} onChange={(e) => setTitre(e.target.value)} placeholder="Titre du jeu" />
|
||||
<input value={annee} onChange={(e) => setAnnee(e.target.value)} placeholder="Année" />
|
||||
<button type="submit">Ajouter</button>
|
||||
</form>
|
||||
);
|
||||
```
|
||||
|
||||
> 🧩 **Le point clé :** on ne « pousse » pas dans le tableau existant (`jeux.push(...)` ❌). On crée un **nouveau tableau** avec `[...jeux, nouveau]` et on le donne à `setJeux`. React ne réaffiche que si on lui passe une **nouvelle** valeur. C'est l'**immutabilité** : on remplace, on ne modifie pas sur place.
|
||||
|
||||
**✅ Test :** remplis le formulaire, clique « Ajouter » → la carte apparaît, les champs se vident, la page ne recharge pas.
|
||||
|
||||
> 🚀 Bonus : un bouton « 🗑️ Supprimer » par carte → `setJeux(jeux.filter((j) => j.id !== id))` (un nouveau tableau **sans** celui-là).
|
||||
|
||||
---
|
||||
|
||||
## 🔀 Module J — Trier la liste
|
||||
|
||||
Un `<select>` **contrôlé** pour choisir le tri :
|
||||
```jsx
|
||||
const [tri, setTri] = useState("titre");
|
||||
|
||||
const affiches = [...filtres].sort((a, b) => {
|
||||
if (tri === "titre") return a.titre.localeCompare(b.titre);
|
||||
if (tri === "annee") return a.annee - b.annee;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<select value={tri} onChange={(e) => setTri(e.target.value)}>
|
||||
<option value="titre">Titre (A→Z)</option>
|
||||
<option value="annee">Année</option>
|
||||
</select>
|
||||
{affiches.map((jeu) => ( /* ... ta Carte ... */ ))}
|
||||
</>
|
||||
);
|
||||
```
|
||||
|
||||
> 🔎 `[...filtres]` = une **copie** (pour ne pas modifier l'original avec `sort`). L'ordre logique : on **filtre** (recherche) → on **trie** → on `.map()`.
|
||||
|
||||
**✅ Test :** change le tri → l'ordre des cartes change.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Module K — Plusieurs pages avec React Router
|
||||
|
||||
Jusqu'ici, **une seule page**. Pour une page **détail** par jeu (sa console, son année, ses likes…), on utilise **React Router**.
|
||||
|
||||
### K.1 — Installer
|
||||
Dans le terminal, dans le dossier du projet :
|
||||
```bash
|
||||
npm install react-router-dom
|
||||
```
|
||||
|
||||
### K.2 — Envelopper l'app (dans `main.jsx`)
|
||||
```jsx
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
createRoot(document.getElementById("root")).render(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
);
|
||||
```
|
||||
|
||||
### K.3 — Définir les routes (dans `App.jsx`)
|
||||
```jsx
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/" element={<Accueil />} />
|
||||
<Route path="/jeu/:id" element={<Detail />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
```
|
||||
- `path="/"` → ta page d'accueil (la liste).
|
||||
- `path="/jeu/:id"` → page **dynamique** : `:id` change selon le jeu.
|
||||
|
||||
### K.4 — Des liens (au lieu de `<a href>`)
|
||||
Dans ta `Carte` :
|
||||
```jsx
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
<Link to={`/jeu/${props.gameId}`}>Voir les détails</Link>
|
||||
```
|
||||
> 🔎 `<Link>` navigue **sans recharger** la page. `<a href>` rechargerait tout.
|
||||
|
||||
### K.5 — Lire l'id dans la page détail
|
||||
```jsx
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
function Detail() {
|
||||
const { id } = useParams(); // récupère le :id de l'URL
|
||||
// retrouve le bon jeu par son id, et affiche-le
|
||||
return <h1>Détails du jeu {id}</h1>;
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Test :** clique « Voir les détails » → l'URL devient `/jeu/2`, la page détail s'affiche **sans rechargement**. Le bouton « précédent » du navigateur ramène à la liste.
|
||||
|
||||
> 🧩 **Les 4 briques de React Router :** `<BrowserRouter>` (enveloppe), `<Routes>/<Route>` (quelle page pour quelle URL), `<Link>` (naviguer), `useParams()` (lire un morceau dynamique de l'URL). C'est la base de **tous** les sites multi-pages en React.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist
|
||||
- [ ] I — formulaire d'ajout (immutabilité avec `[...jeux, ...]`)
|
||||
- [ ] J — tri via un `<select>` contrôlé
|
||||
- [ ] K — React Router : accueil + page détail (`Link`, `useParams`)
|
||||
|
||||
## 🚀 Encore plus loin
|
||||
- Bouton **supprimer** (`filter`) et **éditer** un jeu.
|
||||
- Un **custom hook** `useLocalStorage` pour ranger la logique de persistance des likes.
|
||||
- Une vraie **mise en page** CSS de la page détail (jaquette du jeu, etc.).
|
||||
|
||||
## 🧠 Nouveaux concepts
|
||||
Liste en état + **immutabilité** (`[...]`, `filter`) · formulaire contrôlé + `onSubmit`/`preventDefault` · tri (`select` contrôlé + `sort`) · **React Router** (`BrowserRouter`, `Routes`, `Route`, `Link`, `useParams`).
|
||||
|
||||
Bloquée ? Note ce que tu as tapé + le message exact (console F12). On débloque.
|
||||
@@ -0,0 +1,323 @@
|
||||
# 🗡️ Énoncé React #2 — ton site Zelda (version longue)
|
||||
|
||||
Bien joué Mélissa : tu as découpé en composants (`Header`, `Footer`, `Carte`, `Compteur`, et même un début de `Docs`), tu utilises `useState`… les bases sont là. 👌
|
||||
|
||||
Deux choses avant de foncer : ton `App.jsx` contient encore **tout le contenu d'exemple de Vite** (le gros bloc « Documentation / Connect with us ») — c'est le **fourre-tout** qu'on veut éviter. Et ton `Docs.jsx` est **cassé** (on le règle au Module 0). On va nettoyer, puis bâtir un **vrai site sur Zelda**, en apprenant **beaucoup de React**.
|
||||
|
||||
> 🎯 Ce document est une **série de modules** (0 → H). Fais-les **dans l'ordre**, teste à chaque fois. Je t'explique les **mécaniques** avec des mini-exemples ; à toi d'**appliquer sur ton thème Zelda**. Compte plusieurs séances — c'est normal.
|
||||
|
||||
| Module | Tu apprends | Niveau |
|
||||
|---|---|---|
|
||||
| 0 | Nettoyer le template + réparer `Docs.jsx` | ⭐ |
|
||||
| A | Listes : `.map()` + `key` | ⭐ |
|
||||
| B | Affichage conditionnel + extraire un `<Badge>` | ⭐ |
|
||||
| C | État local par carte (`useState`) | ⭐ |
|
||||
| D | Champ **contrôlé** + recherche/filtre + état vide | ⭐⭐ |
|
||||
| E | **Faire remonter l'état** (lifting state up) + callbacks | ⭐⭐ |
|
||||
| F | **`useEffect`** : titre de page + persistance `localStorage` | ⭐⭐ |
|
||||
| G | **Charger les données avec `fetch`** (dans React) | ⭐⭐⭐ |
|
||||
| H | Re-découpage propre (`<Liste>`, `<BarreRecherche>`) | ⭐⭐ |
|
||||
|
||||
---
|
||||
|
||||
## 🧹 Module 0 — Nettoyer + réparer `Docs.jsx`
|
||||
|
||||
### 0.1 — Vider le contenu d'exemple
|
||||
Dans `App.jsx`, tout le bloc `<section id="next-steps">…</section>` (Documentation, Connect with us, liens Vite/Discord…) vient du **template de démarrage**. Supprime-le : on va mettre **ton** contenu Zelda à la place. Garde juste `<Header />`, `<Footer />`, et ce que tu ajouteras.
|
||||
|
||||
### 0.2 — Le bug de `Docs.jsx`
|
||||
Ton `Docs.jsx` utilise `viteLogo` et `reactLogo`… mais **ne les importe pas** dans le fichier. Si tu l'affichais, tu aurais `viteLogo is not defined`.
|
||||
|
||||
> 🧩 **La règle :** **chaque fichier de composant doit importer ce qu'il utilise.** Un composant est isolé : il ne « voit » pas les imports faits dans `App.jsx`. Soit tu ajoutes les `import ... from "../assets/..."` en haut de `Docs.jsx`, soit (plus simple ici) tu **supprimes `Docs.jsx`** puisqu'on enlève cette section de toute façon.
|
||||
|
||||
> 💡 À retenir : un composant qui plante (import manquant, export oublié) fait **tomber tout le rendu** — tu l'as vu avec l'histoire du Footer. D'où l'intérêt de garder chaque fichier propre et autonome.
|
||||
|
||||
**✅ Test :** la page n'affiche plus que ton Header/Footer, sans erreur dans la console (F12).
|
||||
|
||||
---
|
||||
|
||||
## 🧱 Module A — Afficher une liste : `.map()` + `key`
|
||||
|
||||
On va lister des **jeux Zelda** à partir de **données**, pas à la main.
|
||||
|
||||
### A.1 — un tableau (au-dessus de `function App()`)
|
||||
```jsx
|
||||
const jeux = [
|
||||
{ id: 1, titre: "Ocarina of Time", console: "N64", annee: 1998, note: 99 },
|
||||
{ id: 2, titre: "Breath of the Wild", console: "Switch", annee: 2017, note: 97 },
|
||||
{ id: 3, titre: "Majora's Mask", console: "N64", annee: 2000, note: 95 },
|
||||
// ... tes jeux préférés
|
||||
];
|
||||
```
|
||||
|
||||
### A.2 — générer les cartes
|
||||
```jsx
|
||||
function App() {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
{jeux.map((jeu) => (
|
||||
<Carte key={jeu.id} titre={jeu.titre} console={jeu.console} annee={jeu.annee} />
|
||||
))}
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> 🧩 **Deux nouveautés :**
|
||||
> - **`{ ... }`** dans le JSX = « insère ici du JavaScript ». `{jeux.map(...)}` insère **une carte par jeu**.
|
||||
> - **`key={...}`** = identifiant **unique** par élément (utilise `id`). React en a besoin pour suivre chaque carte ; sans lui → **warning** console.
|
||||
|
||||
**✅ Test :** ajoute un jeu au tableau → une carte apparaît toute seule.
|
||||
|
||||
> 🔎 Tu reconnais ton tableau d'équipe + `.map()`/`filter()` du JS pur ? C'est la même idée, mais React refait l'affichage tout seul (plus de `innerHTML` ni `createElement` à la main).
|
||||
|
||||
---
|
||||
|
||||
## 🎖️ Module B — Affichage conditionnel + un composant `<Badge>`
|
||||
|
||||
Enrichis `Carte` :
|
||||
```jsx
|
||||
function Carte(props) {
|
||||
return (
|
||||
<div className="carte">
|
||||
<h2>{props.titre}</h2>
|
||||
<p>{props.console} — {props.annee}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Affichage conditionnel** — afficher un truc **seulement si** une condition est vraie :
|
||||
```jsx
|
||||
{props.note >= 97 && <span className="badge">⭐ Chef-d'œuvre</span>}
|
||||
```
|
||||
- `condition && <...>` = « si vrai, affiche, sinon rien ».
|
||||
- choix entre deux : `{props.console === "Switch" ? "🟢 récent" : "🕹️ rétro"}`.
|
||||
|
||||
> 💡 Dans le JSX, pas de `if` à l'intérieur du `return` : on utilise `&&` (afficher ou rien) ou `? :` (A ou B).
|
||||
|
||||
**Exercice bonus — extraire `<Badge>`** (1 composant = 1 fichier) :
|
||||
```jsx
|
||||
// components/Badge.jsx
|
||||
function Badge({ note }) {
|
||||
if (note < 97) return null; // un composant peut renvoyer "rien"
|
||||
return <span className="badge">⭐ Chef-d'œuvre</span>;
|
||||
}
|
||||
export default Badge;
|
||||
```
|
||||
> 🔎 Nouveau : `function Badge({ note })` déballe directement la prop (au lieu de `props.note`). Et `return null` = « n'affiche rien ».
|
||||
|
||||
**✅ Test :** seuls les jeux notés ≥ 97 montrent le badge.
|
||||
|
||||
---
|
||||
|
||||
## ❤️ Module C — État local : chaque `Carte` a son `useState`
|
||||
|
||||
Mets un bouton « j'aime » **dans** `Carte` :
|
||||
```jsx
|
||||
import { useState } from "react";
|
||||
|
||||
function Carte(props) {
|
||||
const [likes, setLikes] = useState(0);
|
||||
return (
|
||||
<div className="carte">
|
||||
<h2>{props.titre}</h2>
|
||||
<button onClick={() => setLikes(likes + 1)}>❤️ {likes}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> 🧩 Chaque `<Carte />` est une **instance indépendante** : liker un jeu ne touche pas les autres.
|
||||
|
||||
**✅ Test :** le ❤️ d'une carte monte sans bouger les autres.
|
||||
|
||||
---
|
||||
|
||||
## 🔎 Module D — Champ **contrôlé** + recherche
|
||||
|
||||
Nouveau concept : un champ dont la **valeur est pilotée par un `useState`** (« contrôlé »).
|
||||
|
||||
```jsx
|
||||
function App() {
|
||||
const [recherche, setRecherche] = useState("");
|
||||
|
||||
const filtres = jeux.filter((j) =>
|
||||
j.titre.toLowerCase().includes(recherche.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Rechercher un jeu…"
|
||||
value={recherche} // l'input AFFICHE l'état
|
||||
onChange={(e) => setRecherche(e.target.value)} // toute frappe MET À JOUR l'état
|
||||
/>
|
||||
|
||||
{filtres.length === 0 && <p>Aucun jeu trouvé 😕</p>}
|
||||
|
||||
{filtres.map((jeu) => (
|
||||
<Carte key={jeu.id} titre={jeu.titre} console={jeu.console} annee={jeu.annee} />
|
||||
))}
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> 🧩 **Le champ contrôlé**, c'est la boucle : l'état s'affiche dans le champ (`value`) ; une frappe met à jour l'état (`onChange`) ; l'état change → React réaffiche → la liste **filtrée** se recalcule. Exactement ta recherche du JS, version React.
|
||||
|
||||
> ⚠️ On filtre **avant** le `.map()`, et on gère le cas **« aucun résultat »** (l'état vide).
|
||||
|
||||
**✅ Test :** tape « ocarina » → seul ce jeu reste ; efface → tout revient ; tape n'importe quoi → message « aucun jeu ».
|
||||
|
||||
---
|
||||
|
||||
## ⬆️ Module E — Faire remonter l'état (lifting state up)
|
||||
|
||||
Problème : tu veux un **total de likes** affiché en haut, qui additionne **toutes** les cartes. Chaque carte a son like dans son coin → `App` ne les voit pas.
|
||||
|
||||
Solution React : **remonter l'état dans le parent** (`App`) et **redescendre** une **fonction** (callback) aux enfants.
|
||||
|
||||
```jsx
|
||||
function App() {
|
||||
const [total, setTotal] = useState(0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>👍 Total de likes : {total}</p>
|
||||
{jeux.map((jeu) => (
|
||||
<Carte
|
||||
key={jeu.id}
|
||||
titre={jeu.titre}
|
||||
onLike={() => setTotal((t) => t + 1)} // 👈 une FONCTION passée en prop
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function Carte(props) {
|
||||
return (
|
||||
<div className="carte">
|
||||
<h2>{props.titre}</h2>
|
||||
<button onClick={props.onLike}>❤️ J'aime</button> {/* l'enfant appelle le callback du parent */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> 🧩 **L'idée clé de React :** l'état vit **au plus haut niveau qui en a besoin**. Les données descendent en **props**, les événements remontent via des **fonctions en props**. La carte ne sait pas compter le total — elle dit juste « on m'a likée » au parent.
|
||||
|
||||
> 🔎 `setTotal((t) => t + 1)` (forme avec une fonction) est la façon **sûre** d'incrémenter. Prends l'habitude.
|
||||
|
||||
**✅ Test :** liker n'importe quelle carte fait monter le **total global** en haut.
|
||||
|
||||
---
|
||||
|
||||
## 🔁 Module F — `useEffect` : agir « à côté » de l'affichage
|
||||
|
||||
`useEffect` exécute du code **après l'affichage** : titre de l'onglet, `localStorage`, requêtes…
|
||||
|
||||
### F.1 — le titre de l'onglet
|
||||
```jsx
|
||||
import { useEffect } from "react";
|
||||
|
||||
useEffect(() => {
|
||||
document.title = `${jeux.length} jeux Zelda`;
|
||||
}, []); // 👈 tableau vide = "une seule fois, au chargement"
|
||||
```
|
||||
|
||||
### F.2 — des likes qui **survivent au rechargement**
|
||||
Dans `Carte` :
|
||||
```jsx
|
||||
function Carte(props) {
|
||||
const [likes, setLikes] = useState(() => Number(localStorage.getItem("likes-" + props.id)) || 0);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("likes-" + props.id, likes);
|
||||
}, [likes]); // 👈 "rejoue cet effet quand likes change"
|
||||
|
||||
return <button onClick={() => setLikes(likes + 1)}>❤️ {likes}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
> 🧩 **À comprendre :** le **tableau de dépendances** dit **quand** rejouer l'effet : `[]` = une fois ; `[likes]` = à chaque changement de `likes`. C'est le cœur de `useEffect`.
|
||||
> Et tu retrouves ton `localStorage` (texte → `Number(...)` pour reconvertir).
|
||||
|
||||
**✅ Test :** like un jeu, **recharge (F5)** → le nombre est toujours là. L'onglet affiche ton titre.
|
||||
|
||||
---
|
||||
|
||||
## 🌐 Module G — Charger les données avec `fetch` (dans React)
|
||||
|
||||
Au lieu d'écrire `jeux` en dur, charge-les depuis un JSON — comme une vraie app.
|
||||
|
||||
1. Crée `public/zelda.json` (le dossier `public/` est servi à la racine), format JSON (guillemets doubles partout).
|
||||
2. Dans `App` :
|
||||
```jsx
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
function App() {
|
||||
const [jeux, setJeux] = useState([]); // au départ : liste vide
|
||||
const [chargement, setChargement] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/zelda.json")
|
||||
.then((r) => r.json())
|
||||
.then((data) => { setJeux(data); setChargement(false); });
|
||||
}, []); // une fois, au chargement
|
||||
|
||||
if (chargement) return <p>Chargement…</p>;
|
||||
|
||||
return (
|
||||
<>
|
||||
{jeux.map((j) => <Carte key={j.id} titre={j.titre} /* ... */ />)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> 🧩 **Le schéma :** un `useState` vide → un `useEffect([])` qui `fetch` au chargement → `setJeux(data)` remplit l'état → React réaffiche. Et on gère l'état **« en cours de chargement »**.
|
||||
> ⚠️ Comme en JS pur : `fetch` a besoin du serveur (`npm run dev` te le donne). `public/` est servi à la racine → `/zelda.json`. (Tu te souviens de l'erreur CORS en `file://` ? Ici `npm run dev` est déjà un vrai serveur, donc pas de souci.)
|
||||
|
||||
**✅ Test :** tes cartes viennent du JSON ; ajoute un jeu **dans le fichier** → il apparaît.
|
||||
|
||||
---
|
||||
|
||||
## 🧹 Module H — Re-découper proprement
|
||||
|
||||
`App` commence à beaucoup faire (recherche + total + liste…). Garde-le **assembleur**, extrais des composants (un par fichier) :
|
||||
- `components/BarreRecherche.jsx` — l'input contrôlé (reçoit `valeur` + `onChange` en props).
|
||||
- `components/Liste.jsx` — reçoit le tableau filtré et fait le `.map()`.
|
||||
- `components/Badge.jsx` — déjà fait au Module B.
|
||||
|
||||
> 🧭 Règle d'or : **1 composant = 1 fichier**, `App` assemble. Dès qu'un fichier « fait trop », découpe. (C'est exactement le ménage du Module 0, mais en bien.)
|
||||
|
||||
---
|
||||
|
||||
## ✅ Checklist
|
||||
|
||||
- [ ] 0 — template nettoyé + `Docs.jsx` réparé/supprimé
|
||||
- [ ] A — tableau + `.map()` + `key`
|
||||
- [ ] B — affichage conditionnel + composant `<Badge>`
|
||||
- [ ] C — like local par carte
|
||||
- [ ] D — recherche avec champ **contrôlé** + message « aucun résultat »
|
||||
- [ ] E — **total de likes** remonté dans `App` (callback en prop)
|
||||
- [ ] F — `useEffect` : titre d'onglet + likes persistés en `localStorage`
|
||||
- [ ] G — données chargées via `fetch` depuis `public/zelda.json`
|
||||
- [ ] H — re-découpage (`<BarreRecherche>`, `<Liste>`)
|
||||
|
||||
## 🚀 Encore plus loin
|
||||
- Un **tri** (par année, par note) avec un `<select>` contrôlé.
|
||||
- Une **image** par jeu (`<img src={props.image} />`) + affichage conditionnel si elle existe.
|
||||
- Un **mode sombre** global (état dans `App`, classe sur le `<div>` racine).
|
||||
|
||||
## 🧠 Les nouveaux concepts de cet énoncé
|
||||
Listes (`map`/`key`) · affichage conditionnel · état local · **champ contrôlé** · **lifting state up** + callbacks · **`useEffect`** + dépendances · **`fetch` en React** + état de chargement · re-découpage.
|
||||
|
||||
Bloquée ? Note ce que tu as tapé + le message exact (console F12). On débloque.
|
||||
@@ -0,0 +1,95 @@
|
||||
# 📨 À 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`**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Le rôle du `<form>` : regrouper et **envoyer** des champs
|
||||
|
||||
Un `<form>` est une **enveloppe** autour de plusieurs champs (`<input>`, `<textarea>`…). Son but : permettre à l'utilisateur de **tout envoyer d'un coup**, en un seul geste (le bouton « Envoyer »).
|
||||
|
||||
Sans `<form>`, tes champs s'affichent quand même… mais ce ne sont que des cases isolées : il n'y a aucune notion d'« envoi groupé ». Le `<form>`, lui, apporte 3 choses automatiquement :
|
||||
|
||||
1. **Il rassemble** toutes les valeurs des champs qu'il contient.
|
||||
2. **Il les empaquette** en associant chaque valeur au `name` de son champ.
|
||||
3. **Il les envoie** à une adresse, quand on clique sur un bouton `type="submit"`.
|
||||
|
||||
> 🔑 **Le détail le plus important : l'attribut `name`.**
|
||||
> Seuls les champs qui ont un **`name`** sont envoyés, et c'est ce `name` qui sert d'**étiquette** à la valeur. Dans ton formulaire :
|
||||
> ```html
|
||||
> <input type="text" name="prenom" ...>
|
||||
> <input type="email" name="email" ...>
|
||||
> <textarea name="contacter"></textarea>
|
||||
> ```
|
||||
> → au moment de l'envoi, le navigateur construit quelque chose comme :
|
||||
> `prenom = Mélissa`, `email = melissa@mail.com`, `contacter = ...`
|
||||
> Un champ **sans `name`** est tout simplement **ignoré** à l'envoi, même s'il est rempli.
|
||||
>
|
||||
> 💬 C'est aussi pour ça que `FormData` (Niveau 5, exo 8) lit les champs par leur `name` : tout est lié.
|
||||
|
||||
> ⚠️ Rappel utile : pour que l'envoi parte, le bouton doit être **dans** le `<form>` et être un `type="submit"` (un `<button>` sans `type` précisé compte comme submit par défaut).
|
||||
|
||||
---
|
||||
|
||||
## 2. L'attribut `action` : **OÙ** vont les données
|
||||
|
||||
```html
|
||||
<form method="get" action="">
|
||||
```
|
||||
|
||||
`action` indique **l'adresse qui va recevoir** les données — en général un programme **côté serveur** (en PHP, Node, Python…) qui les enregistre, envoie un e-mail, etc.
|
||||
|
||||
- `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.
|
||||
|
||||
---
|
||||
|
||||
## 3. L'attribut `method` : **COMMENT** les données sont envoyées
|
||||
|
||||
Il existe deux façons principales d'envoyer un formulaire : **GET** et **POST**.
|
||||
|
||||
### `method="get"` (ce que tu utilises)
|
||||
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...
|
||||
```
|
||||
- ✅ 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"`
|
||||
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…
|
||||
|
||||
> 💡 **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. 🙂
|
||||
|
||||
---
|
||||
|
||||
## 🔬 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 ».
|
||||
|
||||
Ç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
|
||||
|
||||
| Élément | À quoi ça sert |
|
||||
|---|---|
|
||||
| `<form>` | enveloppe qui **regroupe et envoie** les champs d'un coup |
|
||||
| `name` (sur chaque champ) | l'**étiquette** de la valeur — sans lui, le champ n'est **pas** envoyé |
|
||||
| `type="submit"` (bouton) | **déclenche** l'envoi |
|
||||
| `action` | **OÙ** vont les données (l'adresse du serveur ; `""` = la même page) |
|
||||
| `method` | **COMMENT** : `get` (dans l'URL, pour demander) / `post` (caché, pour envoyer) |
|
||||
|
||||
> 🧭 Tant qu'il n'y a pas de serveur, retiens surtout : le `<form>` **structure** ta saisie, et c'est ton **JavaScript** qui décide quoi en faire à l'envoi. `action`/`method` deviendront vraiment utiles le jour où il y aura un serveur pour recevoir les données.
|
||||
@@ -0,0 +1,620 @@
|
||||
# 📬 Page Contact — relecture & prochaine étape
|
||||
|
||||
Salut Mélissa ! 👋
|
||||
|
||||
Ta page contact prend forme : le tableau de l'équipe est sympa, le formulaire est en place, et le bouton « contact » qui reste enfoncé pour montrer la page courante (`.instyled`) est une **très bonne idée** d'ergonomie. Bravo ! 🎉
|
||||
|
||||
Ce guide a trois objectifs :
|
||||
1. Corriger **4 bugs** (avec l'explication, pas juste la solution).
|
||||
2. T'apprendre à **définir et limiter une `<textarea>`** proprement.
|
||||
3. Te lancer sur ta **première mission JavaScript** 🚀 (dans le thème FleetZen).
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Bug 1 — L'accolade `}` oubliée (le bug le plus important)
|
||||
|
||||
Tu te souviens de la leçon « chaque `{` a son `}` » ? Elle te concerne en plein ! 😄 Regarde le tout début de `contact.css` :
|
||||
|
||||
```css
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: lightblue;
|
||||
margin: 0;
|
||||
padding-bottom: 60px;
|
||||
/* ❌ il manque le } ICI pour fermer body */
|
||||
#nav_barre { /* du coup le navigateur croit que tout ça est ENCORE dans body */
|
||||
display: flex;
|
||||
...
|
||||
```
|
||||
|
||||
Tu as **ouvert** `body {` mais tu ne l'as **jamais refermé**. Résultat : le navigateur pense que `#nav_barre`, `.styled`, `.table`… sont tous **imbriqués dans `body`**, il se perd, et **une grande partie de ton CSS est ignorée**. C'est pour ça que ta page ne ressemble pas à ce que tu attends.
|
||||
|
||||
### ✅ Correction : ferme `body` avec un `}`
|
||||
|
||||
```css
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: lightblue;
|
||||
margin: 0;
|
||||
padding-bottom: 60px;
|
||||
} /* ✅ on ferme body ici */
|
||||
|
||||
#nav_barre {
|
||||
display: flex;
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
> 💡 **À retenir :** à chaque `{` correspond **un** `}`. Une accolade oubliée et **tout le reste de la feuille de style casse**. Astuce : la plupart des éditeurs colorent ou « replient » les blocs — si la coloration paraît bizarre à partir d'un endroit, c'est souvent une accolade manquante juste avant.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug 2 — Un lien de navigation cassé
|
||||
|
||||
Ta page `contact.html` est **déjà dans le dossier `pages/`**. Or ton lien vers services pointe vers :
|
||||
|
||||
```html
|
||||
<a href="pages/services.html"> <!-- ❌ cherche pages/pages/services.html → n'existe pas -->
|
||||
```
|
||||
|
||||
Comme tu es déjà dans `pages/`, tu redemandes d'entrer dans `pages/` → le chemin n'existe pas.
|
||||
|
||||
### ✅ Correction : le fichier est **juste à côté**, dans le même dossier
|
||||
|
||||
```html
|
||||
<a href="services.html"> <!-- ✅ même dossier, on met juste le nom du fichier -->
|
||||
```
|
||||
|
||||
> 💡 **À retenir :** les chemins sont **relatifs au fichier courant**.
|
||||
> - `services.html` → dans le **même dossier**
|
||||
> - `../index.html` → **remonter** d'un dossier (le `..`), puis prendre `index.html`
|
||||
> Ton lien « retour » (`../index.html`) est d'ailleurs **correct**, bravo : c'est la même logique.
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Bug 3 — Le champ e-mail n'est pas un vrai champ e-mail
|
||||
|
||||
```html
|
||||
<input type="text" name="e-mail" id="e-mail" ...> <!-- ❌ type="text" -->
|
||||
```
|
||||
|
||||
En `type="text"`, le navigateur accepte **n'importe quoi**. Pour un e-mail, il existe un type dédié qui **vérifie tout seul** qu'il y a bien un `@` :
|
||||
|
||||
```html
|
||||
<input type="email" name="email" id="email" ...> <!-- ✅ -->
|
||||
```
|
||||
|
||||
> 💡 **À retenir :** le **bon `type`** d'`<input>` fait travailler le navigateur à ta place : `email`, `tel`, `number`, `date`… Chacun ajoute sa petite vérification automatique.
|
||||
>
|
||||
> ⚠️ Petit détail : essaie d'éviter le tiret dans les `id`/`name` (`e-mail`). Préfère `email` ou `e_mail` — c'est plus simple à réutiliser ensuite, surtout en JavaScript.
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Bug 4 — L'icône SVG n'est pas centrée dans le bouton « retour »
|
||||
|
||||
Ton bouton « retour » mélange une **image** (le SVG) et du **texte** côte à côte :
|
||||
|
||||
```html
|
||||
<button class="styled"><svg ...></svg>retour</button>
|
||||
```
|
||||
|
||||
Le souci : par défaut, une icône SVG se pose **sur la ligne du texte** (sur la « ligne de base », comme une lettre). Du coup elle paraît trop haute ou trop basse, et ton `line-height: 2.5` accentue le décalage. L'icône et le mot « retour » ne sont pas alignés sur leur **milieu**.
|
||||
|
||||
### ✅ La solution : transformer le bouton en boîte flex
|
||||
|
||||
```css
|
||||
.styled {
|
||||
display: inline-flex; /* le bouton range ses enfants (icône + texte) */
|
||||
align-items: center; /* ↕ les centre verticalement, sur le même milieu */
|
||||
justify-content: center; /* ↔ les centre horizontalement */
|
||||
gap: 6px; /* un petit espace entre l'icône et le mot */
|
||||
line-height: normal; /* on enlève le 2.5 qui déréglait l'alignement */
|
||||
/* ...garde le reste de tes styles (couleur, border-radius, etc.) */
|
||||
}
|
||||
```
|
||||
|
||||
`align-items: center`, c'est **exactement** le `flex` que tu connais déjà (tu l'utilises sur `#buttons`). Ici on l'applique **au bouton lui-même**, parce que c'est lui le parent qui contient l'icône **et** le texte.
|
||||
|
||||
> 💡 **À retenir :** dès qu'un bouton (ou n'importe quelle boîte) contient **une icône + du texte** à aligner, passe-le en `display: inline-flex; align-items: center;`. C'est le réflexe pro pour centrer proprement une icône avec du texte.
|
||||
>
|
||||
> 🔎 Comme tous tes boutons partagent la classe `.styled`, cette correction les rend **tous** propres d'un coup — même ceux sans icône (le texte y reste bien centré).
|
||||
|
||||
---
|
||||
|
||||
## 📐 La leçon du jour : définir et **limiter** une `<textarea>`
|
||||
|
||||
Ta zone de message est « brute » :
|
||||
|
||||
```html
|
||||
<textarea name="contacter" id="contacter"></textarea>
|
||||
```
|
||||
|
||||
Par défaut elle est petite, et l'utilisateur peut écrire **sans aucune limite**. On va la **cadrer**.
|
||||
|
||||
### 1️⃣ Lui donner une taille de départ : `rows` et `cols`
|
||||
|
||||
```html
|
||||
<textarea id="contacter" name="contacter"
|
||||
rows="6" cols="40"></textarea>
|
||||
```
|
||||
|
||||
- `rows` = nombre de **lignes** visibles (la hauteur).
|
||||
- `cols` = largeur en **caractères**.
|
||||
|
||||
> 🔎 Ce ne sont **pas** des pixels : c'est « combien de lignes/caractères je vois » au départ.
|
||||
|
||||
### 2️⃣ Limiter le nombre de caractères : `maxlength`
|
||||
|
||||
```html
|
||||
<textarea id="contacter" name="contacter"
|
||||
rows="6" cols="40"
|
||||
maxlength="500"></textarea>
|
||||
```
|
||||
|
||||
`maxlength="500"` = impossible de taper plus de 500 caractères. Le navigateur bloque **tout seul**, sans JavaScript. 👍
|
||||
|
||||
### 3️⃣ Cadrer le redimensionnement : `resize` en CSS
|
||||
|
||||
La petite poignée en bas à droite permet à l'utilisateur d'étirer la zone et de **casser ta mise en page**. On la contrôle en CSS :
|
||||
|
||||
```css
|
||||
#contacter {
|
||||
resize: vertical; /* étirement en hauteur seulement (recommandé) */
|
||||
/* resize: none; pour l'interdire complètement */
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
}
|
||||
```
|
||||
|
||||
> 💡 **À retenir :**
|
||||
> - `rows` / `cols` → **taille de départ**.
|
||||
> - `maxlength` → **limite** le nombre de caractères (le navigateur bloque tout seul).
|
||||
> - `resize` (CSS) → contrôle la **poignée** d'étirement (`vertical`, `horizontal`, `none`, `both`).
|
||||
|
||||
### 4️⃣ 👉 Votre question : laisser l'utilisateur redimensionner, mais **entre un min et un max**
|
||||
|
||||
Tu as écrit pour l'instant :
|
||||
|
||||
```css
|
||||
textarea {
|
||||
resize: initial; /* "initial" = la valeur par défaut = "both" : étirable dans TOUS les sens, sans limite */
|
||||
width: 527px;
|
||||
height: 80px;
|
||||
}
|
||||
```
|
||||
|
||||
`resize: initial` remet la valeur **par défaut**, c'est-à-dire `both` : l'utilisateur peut tout étirer, **dans toutes les directions et sans aucune borne**. Il peut donc rendre la zone énorme et **casser ta mise en page**. Vous m'avez justement demandé comment **autoriser** l'agrandissement **sans qu'il parte trop grand (ni trop petit)**. La recette tient en 2 idées qui se combinent :
|
||||
|
||||
1. **`resize`** dit *dans quelle direction* on peut étirer.
|
||||
2. **`min-*` / `max-*`** disent *jusqu'où* on peut aller.
|
||||
|
||||
```css
|
||||
#contacter {
|
||||
resize: vertical; /* on autorise l'étirement, mais en HAUTEUR seulement */
|
||||
|
||||
min-height: 80px; /* impossible de la réduire en dessous de 80px */
|
||||
max-height: 300px; /* impossible de l'agrandir au-delà de 300px */
|
||||
|
||||
width: 100%; /* largeur pilotée en CSS, pas par l'utilisateur */
|
||||
}
|
||||
```
|
||||
|
||||
Avec ça : l'utilisateur attrape la poignée, tire vers le bas pour avoir plus de place… mais **la zone refuse de dépasser 300px** et **de descendre sous 80px**. Ta mise en page reste protégée. 🎉
|
||||
|
||||
#### Et si on veut autoriser les **deux** sens (vertical **et** horizontal) ?
|
||||
|
||||
Même principe, mais avec **4 bornes** : 2 pour la hauteur, 2 pour la largeur. On passe `resize` à `both` (c'est d'ailleurs ce que faisait ton `resize: initial`, mais cette fois **borné**) :
|
||||
|
||||
```css
|
||||
#contacter {
|
||||
resize: both; /* la poignée étire en hauteur ET en largeur */
|
||||
|
||||
min-height: 80px; /* bornes verticales */
|
||||
max-height: 300px;
|
||||
|
||||
min-width: 200px; /* bornes horizontales */
|
||||
max-width: 100%; /* ne dépasse jamais la largeur du parent → pas de scroll */
|
||||
}
|
||||
```
|
||||
|
||||
Chaque direction a sa paire `min`/`max` :
|
||||
- **hauteur** → `min-height` / `max-height`
|
||||
- **largeur** → `min-width` / `max-width`
|
||||
|
||||
L'utilisateur peut alors étirer dans tous les sens, mais **jamais au-delà** de ces 4 limites.
|
||||
|
||||
> ⚠️ Le piège classique en horizontal : mets `max-width: 100%` (et pas un grand `527px` fixe), sinon l'utilisateur peut élargir la zone **plus que l'écran** et faire apparaître une barre de défilement horizontale.
|
||||
|
||||
Récap des valeurs de `resize` :
|
||||
|
||||
| `resize` | Ce que l'utilisateur peut étirer |
|
||||
|---|---|
|
||||
| `none` | rien (verrouillé) |
|
||||
| `vertical` | la hauteur seulement (le plus confortable pour un message) |
|
||||
| `horizontal` | la largeur seulement |
|
||||
| `both` | les deux — c'est ce que donne `initial` par défaut |
|
||||
|
||||
> 🎯 Petit conseil de ciblage : vise plutôt `#contacter` (l'id de **ta** textarea) que `textarea` tout court. Le sélecteur `textarea` toucherait **toutes** les zones de texte du site, même celles que Guillaume ajoutera sur sa page.
|
||||
|
||||
> 💡 **À retenir :** `resize` = **la direction** autorisée. `min-height`/`max-height` (et `min-width`/`max-width`) = **les bornes**. Les deux ensemble = un redimensionnement libre **mais encadré**.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 Petit plus — centrer un bloc sans le « pousser » avec `margin-left`
|
||||
|
||||
J'ai vu que tu centres ta table **et** ton nouveau `.form` avec `margin-left: 600px` / `620px`. Ça « marche » sur ton écran… mais sur un écran plus petit, le bloc est **poussé hors de la fenêtre** → barre de défilement horizontale. Pour **vraiment centrer** un bloc de largeur fixe :
|
||||
|
||||
```css
|
||||
.form {
|
||||
width: 35%;
|
||||
margin-left: auto; /* "auto" à gauche ET à droite = */
|
||||
margin-right: auto; /* le navigateur répartit l'espace → bloc centré */
|
||||
/* on enlève le margin-left: 620px */
|
||||
}
|
||||
```
|
||||
|
||||
> 💡 **À retenir :** `margin: auto` à gauche **et** à droite centre un bloc qui a une largeur. C'est l'outil correct ; `margin-left: 620px` ne fait que le décaler et casse l'affichage sur petit écran.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Ta première mission JavaScript : le **compteur de caractères**
|
||||
|
||||
Jusqu'ici tu as fait du HTML (la **structure**) et du CSS (l'**apparence**). Le JavaScript, c'est la **3ᵉ couche** : le **comportement**, ce qui réagit en direct. 🧠
|
||||
|
||||
On va le faire **en mode tuto** : tu construis petit bout par petit bout, et **tu testes à chaque étape**. Ne copie pas tout d'un coup — avance palier par palier, c'est comme ça qu'on comprend. 🙂
|
||||
|
||||
**Objectif final :** `maxlength` empêche de dépasser 500 caractères… mais l'utilisateur ne **voit pas** combien il lui en reste. On va afficher un **compteur en direct** : « Il vous reste 437 caractères ». C'est exactement ce que font Twitter/X, les SMS, etc.
|
||||
|
||||
---
|
||||
|
||||
### 🪜 Étape 0 — Découvrir la console (ton meilleur ami)
|
||||
|
||||
Ouvre ta page dans le navigateur, puis appuie sur **F12** → onglet **Console**. C'est là que le JS « parle ».
|
||||
|
||||
Crée un fichier `contact.js` avec une seule ligne :
|
||||
|
||||
```js
|
||||
console.log("Mon fichier JS est bien chargé !");
|
||||
```
|
||||
|
||||
Et branche-le en bas du `<body>`, **juste avant** `</body>` :
|
||||
|
||||
```html
|
||||
<script src="contact.js"></script>
|
||||
```
|
||||
|
||||
> 🔎 Pourquoi à la fin du body ? Parce que le JS s'exécute de haut en bas. S'il se lance **avant** que le HTML existe, il ne trouve rien à manipuler. À la fin, tout le HTML est déjà là. 👍
|
||||
|
||||
**✅ Teste maintenant :** recharge la page. Tu dois voir ton message dans la Console. Si oui, ton JS est branché. Sinon, vérifie le chemin du `src`.
|
||||
|
||||
---
|
||||
|
||||
### 🪜 Étape 1 — Ajouter l'affichage du compteur
|
||||
|
||||
Sous ta textarea, ajoute la ligne qui affichera le décompte :
|
||||
|
||||
```html
|
||||
<textarea id="contacter" name="contacter" rows="6" maxlength="500"></textarea>
|
||||
<p>Il vous reste <span id="restant">500</span> caractères.</p>
|
||||
```
|
||||
|
||||
**✅ Teste maintenant :** tu dois lire « Il vous reste 500 caractères ». Pour l'instant ce 500 est figé, on va le rendre vivant.
|
||||
|
||||
> 🔎 Le `<span id="restant">` est juste une petite « boîte » dans laquelle le JS viendra écrire le nombre. Son `id` est l'étiquette par laquelle on va l'attraper.
|
||||
|
||||
---
|
||||
|
||||
### 🪜 Étape 2 — Attraper les éléments depuis le JS
|
||||
|
||||
Dans `contact.js`, remplace ton `console.log` de test par :
|
||||
|
||||
```js
|
||||
const zoneMessage = document.getElementById("contacter"); // ta textarea
|
||||
const compteur = document.getElementById("restant"); // le <span>
|
||||
|
||||
console.log(zoneMessage); // pour vérifier qu'on l'a bien trouvée
|
||||
```
|
||||
|
||||
**✅ Teste maintenant :** dans la Console tu dois voir s'afficher ta `<textarea>`. Si tu vois `null`, l'id ne correspond pas → vérifie l'orthographe (`contacter`).
|
||||
|
||||
> 🧩 **Idée clé n°1 — SÉLECTIONNER.** `document.getElementById("contacter")` veut dire : « va me chercher l'élément dont l'id est *contacter* ». On le range dans une variable pour s'en servir après.
|
||||
|
||||
---
|
||||
|
||||
### 🪜 Étape 3 — Réagir quand on tape
|
||||
|
||||
Ajoute en dessous :
|
||||
|
||||
```js
|
||||
zoneMessage.addEventListener("input", function () {
|
||||
console.log("Tu viens de taper quelque chose !");
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste maintenant :** clique dans la zone et tape quelques lettres. À **chaque** touche, un message apparaît dans la Console. 🎉
|
||||
|
||||
> 🧩 **Idée clé n°2 — RÉAGIR.** `addEventListener("input", … )` se lit : « écoute les saisies dans cette zone, et à chaque frappe, exécute cette fonction ». (`"input"` = « le contenu a changé », parfait pour une textarea.)
|
||||
|
||||
---
|
||||
|
||||
### 🪜 Étape 4 — Calculer et afficher ce qu'il reste
|
||||
|
||||
Maintenant on remplace le `console.log` par le vrai calcul. Voici le fichier complet :
|
||||
|
||||
```js
|
||||
// 1. On attrape la textarea et la "boîte" où afficher le compteur
|
||||
const zoneMessage = document.getElementById("contacter");
|
||||
const compteur = document.getElementById("restant");
|
||||
const MAX = 500;
|
||||
|
||||
// 2. À chaque frappe, on recalcule ce qu'il reste
|
||||
zoneMessage.addEventListener("input", function () {
|
||||
const dejaTape = zoneMessage.value.length; // .value = le texte tapé, .length = sa longueur
|
||||
compteur.textContent = MAX - dejaTape; // on écrit le résultat dans le <span>
|
||||
});
|
||||
```
|
||||
|
||||
> 🧩 **Idée clé n°3 — MODIFIER.** `compteur.textContent = …` réécrit le texte affiché **en direct**, sans recharger la page. C'est ça, la magie du JS.
|
||||
>
|
||||
> 🔎 `zoneMessage.value`, c'est le **contenu tapé** par l'utilisateur ; `.length` en donne le **nombre de caractères**. `500 − ce nombre` = ce qu'il reste.
|
||||
|
||||
**✅ Teste maintenant :** tape du texte → le nombre doit **descendre** à chaque lettre (499, 498, …) et **s'arrêter à 0** quand tu atteins la limite. Si oui : bravo, tu viens d'écrire ta première vraie logique en JavaScript ! 🎉
|
||||
|
||||
> 🎯 **Défis bonus** (dans l'ordre de difficulté) :
|
||||
> 1. Passe le compteur en **rouge** quand il reste moins de 50 caractères :
|
||||
> ```js
|
||||
> compteur.style.color = (MAX - dejaTape) < 50 ? "red" : "black";
|
||||
> ```
|
||||
> 2. Affiche le texte « caractères » au singulier quand il reste 1 ou 0.
|
||||
> 3. Désactive le bouton « Envoyer » tant que le message est vide.
|
||||
|
||||
---
|
||||
|
||||
# 🎓 Niveau 2 — Les conditions (et de nouveaux exos JS)
|
||||
|
||||
**Bravo Mélissa** 👏 — tu as fait marcher le compteur **et** le bonus couleur (le compteur qui passe en rouge). Tu es prête pour la suite : les **conditions**, c'est-à-dire apprendre au programme à **choisir** quoi faire selon la situation.
|
||||
|
||||
> 🩹 **Petite correction au passage** (vu dans ton HTML) : ton champ e-mail est `type="e_mail"`. Ce type n'existe pas → le navigateur le traite comme du texte normal. Le bon, c'est `type="email"` (sans underscore). Avec, le navigateur vérifie tout seul qu'il y a bien un `@`. 😉
|
||||
|
||||
---
|
||||
|
||||
## 🧠 La leçon : `if` / `else` — faire **choisir** le programme
|
||||
|
||||
Une **condition**, c'est une question qui n'a que deux réponses : **vrai** ou **faux**. En JavaScript on écrit :
|
||||
|
||||
```js
|
||||
if (/* une question */) {
|
||||
// ce qu'on fait SI la réponse est "vrai"
|
||||
} else {
|
||||
// ce qu'on fait SINON
|
||||
}
|
||||
```
|
||||
|
||||
Pour poser la question, on utilise des **opérateurs de comparaison** :
|
||||
|
||||
| Opérateur | Question posée | Exemple vrai |
|
||||
|---|---|---|
|
||||
| `===` | est **égal à** ? | `restant === 1` |
|
||||
| `!==` | est **différent de** ? | `restant !== 0` |
|
||||
| `>` `<` | plus grand / plus petit ? | `restant < 50` |
|
||||
| `>=` `<=` | plus grand/petit **ou égal** ? | `restant >= 1` |
|
||||
|
||||
> ⚠️ **Le piège n°1 des débutants :** `=` et `===` ne veulent PAS dire la même chose !
|
||||
> - `=` (un seul) → **ranger** une valeur : `restant = 5` (« mets 5 dans restant »).
|
||||
> - `===` (trois) → **comparer** : `restant === 5` (« est-ce que restant vaut 5 ? »).
|
||||
> Dans un `if (...)`, c'est **toujours** `===`.
|
||||
|
||||
---
|
||||
|
||||
## ✍️ Exo : afficher « caractère » au singulier
|
||||
|
||||
Aujourd'hui ton HTML affiche toujours « caractères » au pluriel, même quand il n'en reste qu'**un**. On va corriger ça avec une condition.
|
||||
|
||||
### Étape 1 — Donner une étiquette au mot dans le HTML
|
||||
|
||||
Pour pouvoir changer le **mot** tout seul, on l'enferme dans son propre `<span>` :
|
||||
|
||||
```html
|
||||
<p class="small">Il vous reste <span id="restant">500</span> <span id="mot">caractères</span>.</p>
|
||||
```
|
||||
|
||||
### Étape 2 — Choisir le bon mot en JavaScript
|
||||
|
||||
Dans `contact.js`, attrape ce nouveau `<span>` (en haut, avec les autres) :
|
||||
|
||||
```js
|
||||
const motCaractere = document.getElementById("mot");
|
||||
```
|
||||
|
||||
Puis, **à l'intérieur** de ton `addEventListener("input", …)`, ajoute la condition :
|
||||
|
||||
```js
|
||||
zoneMessage.addEventListener("input", function () {
|
||||
const dejaTape = zoneMessage.value.length;
|
||||
const restant = MAX - dejaTape; // on range le calcul dans une variable, plus lisible
|
||||
|
||||
compteur.textContent = restant;
|
||||
compteur.style.color = restant < 50 ? "red" : "black";
|
||||
|
||||
// 👇 la nouvelle condition
|
||||
if (restant === 1) {
|
||||
motCaractere.textContent = "caractère"; // singulier
|
||||
} else {
|
||||
motCaractere.textContent = "caractères"; // pluriel
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste maintenant :** tape jusqu'à ce qu'il reste exactement **1** → tu dois lire « Il vous reste 1 **caractère** ». À 2 ou plus → « caractères ». 🎉
|
||||
|
||||
> 🔎 **Tu connaissais déjà une condition sans le savoir !** Ton bonus couleur :
|
||||
> ```js
|
||||
> compteur.style.color = restant < 50 ? "red" : "black";
|
||||
> ```
|
||||
> C'est exactement un `if`/`else`… en **version courte** (on l'appelle le *ternaire*). La version longue équivalente serait :
|
||||
> ```js
|
||||
> if (restant < 50) {
|
||||
> compteur.style.color = "red";
|
||||
> } else {
|
||||
> compteur.style.color = "black";
|
||||
> }
|
||||
> ```
|
||||
> Les deux font la même chose. Le `? :` est juste un raccourci pratique quand il n'y a qu'**une** ligne à choisir. 👍
|
||||
|
||||
---
|
||||
|
||||
## 🧪 De nouveaux exos pour découvrir plus de JavaScript
|
||||
|
||||
Chaque exo t'apprend **une nouvelle notion**. Fais-les dans l'ordre, et teste à chaque fois.
|
||||
|
||||
### 🧪 Exo 1 — Empêcher l'envoi d'un message vide
|
||||
|
||||
> 🆕 Nouvelle notion : réagir à l'**envoi du formulaire** (`"submit"`) et **bloquer** l'action par défaut.
|
||||
|
||||
Aujourd'hui, si on clique « Envoyer » avec un message vide, le formulaire part quand même. On va l'en empêcher.
|
||||
|
||||
```js
|
||||
const formulaire = document.querySelector("form"); // on attrape le <form>
|
||||
|
||||
formulaire.addEventListener("submit", function (evenement) {
|
||||
if (zoneMessage.value.trim() === "") { // .trim() enlève les espaces autour
|
||||
evenement.preventDefault(); // ⛔ on ANNULE l'envoi
|
||||
alert("Merci d'écrire un message avant d'envoyer !");
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste :** clique « Envoyer » sans rien écrire → une alerte apparaît et la page ne se recharge pas. Écris un message → l'envoi repart normalement.
|
||||
|
||||
> 🔎 `evenement` est une info que le navigateur passe à ta fonction (« voici ce qui s'est passé »). `evenement.preventDefault()` veut dire « n'effectue PAS l'action habituelle » (ici : ne soumets pas le formulaire). `.trim()` sert à considérer « 3 espaces » comme un message vide.
|
||||
|
||||
### 🧪 Exo 2 — Un bouton « Effacer le message »
|
||||
|
||||
> 🆕 Nouvelle notion : **ranger du code dans une fonction** pour le réutiliser (ne pas se répéter).
|
||||
|
||||
Ajoute un bouton dans le HTML, à côté de « Envoyer » :
|
||||
|
||||
```html
|
||||
<button type="button" id="effacer">Effacer</button>
|
||||
```
|
||||
|
||||
Le souci : quand on efface, il faut aussi **remettre le compteur à 500**. On a donc besoin de réafficher le compteur à **deux endroits** (à la frappe ET à l'effacement). La bonne pratique : écrire ce calcul **une seule fois** dans une fonction, et l'appeler aux deux endroits.
|
||||
|
||||
```js
|
||||
const boutonEffacer = document.getElementById("effacer");
|
||||
|
||||
// une fonction qui remet l'affichage à jour à partir du texte actuel
|
||||
function rafraichirCompteur() {
|
||||
const restant = MAX - zoneMessage.value.length;
|
||||
compteur.textContent = restant;
|
||||
compteur.style.color = restant < 50 ? "red" : "black";
|
||||
motCaractere.textContent = restant === 1 ? "caractère" : "caractères";
|
||||
}
|
||||
|
||||
// à la frappe : on rafraîchit
|
||||
zoneMessage.addEventListener("input", rafraichirCompteur);
|
||||
|
||||
// au clic sur Effacer : on vide la zone PUIS on rafraîchit
|
||||
boutonEffacer.addEventListener("click", function () {
|
||||
zoneMessage.value = ""; // on vide la textarea
|
||||
rafraichirCompteur(); // et on remet le compteur à 500
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste :** écris du texte, clique « Effacer » → la zone se vide **et** le compteur repasse à 500.
|
||||
|
||||
> 🔎 Tu viens de **factoriser** : au lieu de recopier les mêmes lignes deux fois, tu les as mises dans `rafraichirCompteur()` et tu l'appelles. Si un jour tu changes l'affichage, tu ne le modifies **qu'à un seul endroit**. C'est exactement la même idée que le **composant réutilisable** plus bas. ♻️
|
||||
|
||||
### 🧪 Exo 3 — « FleetZen est-il joignable maintenant ? »
|
||||
|
||||
> 🆕 Nouvelle notion : lire l'**heure actuelle** (`new Date()`) et combiner deux conditions avec `&&` (ET).
|
||||
|
||||
Une page contact, ça parle d'horaires. Affichons un petit badge « ✅ Ouvert » ou « 🔴 Fermé » selon l'heure (ouvert de 9h à 18h).
|
||||
|
||||
D'abord un endroit pour l'afficher, dans le HTML :
|
||||
|
||||
```html
|
||||
<p id="statut_horaires"></p>
|
||||
```
|
||||
|
||||
Puis en JavaScript :
|
||||
|
||||
```js
|
||||
const badge = document.getElementById("statut_horaires");
|
||||
const heure = new Date().getHours(); // l'heure actuelle, de 0 à 23
|
||||
|
||||
if (heure >= 9 && heure < 18) { // && = "ET" : les DEUX doivent être vraies
|
||||
badge.textContent = "✅ Nous sommes ouverts (9h–18h)";
|
||||
badge.style.color = "green";
|
||||
} else {
|
||||
badge.textContent = "🔴 Fermé — écrivez-nous, on répond dès l'ouverture !";
|
||||
badge.style.color = "red";
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Teste :** recharge la page. Selon l'heure qu'il est, tu vois « Ouvert » ou « Fermé ». (Pour tester l'autre cas sans attendre, change temporairement `9` et `18`.)
|
||||
|
||||
> 🔎 `new Date().getHours()` donne l'heure de l'ordinateur (ex. `14`). `&&` se lit « ET » : `heure >= 9 && heure < 18` n'est vrai que si l'heure est **à la fois** ≥ 9 **et** < 18. Son cousin est `||` qui se lit « OU ».
|
||||
|
||||
---
|
||||
|
||||
> 🗺️ **Où tu en es maintenant** : tu sais **sélectionner**, **réagir**, **modifier**, et maintenant **faire choisir** ton programme avec des conditions, **factoriser** dans des fonctions, et lire des infos comme l'heure. C'est déjà le cœur du JavaScript du quotidien. La suite (boucles `for`, tableaux) viendra naturellement — demande-moi quand tu veux ! 🚀
|
||||
|
||||
---
|
||||
|
||||
## ♻️ Bonus — La notion de **composant réutilisable** (la navbar)
|
||||
|
||||
Regarde : la barre du haut (`#nav_barre`) est **recopiée à l'identique** dans `index.html`, `services.html` et `contact.html`. Le jour où on change un lien, il faut le modifier **dans 3 fichiers** → source d'erreurs (c'est d'ailleurs comme ça que le Bug 2 se glisse 😉).
|
||||
|
||||
C'est **le** problème que résolvent les **composants** : on écrit la navbar **une seule fois**, et on la réutilise partout.
|
||||
|
||||
Première approche très simple en JavaScript : une fonction qui **génère** la navbar.
|
||||
|
||||
```js
|
||||
function creerNavbar() {
|
||||
return `
|
||||
<div id="nav_barre" class="shadow">
|
||||
<h1>FleetZen</h1>
|
||||
<div id="buttons">
|
||||
<button class="instyled">contact</button>
|
||||
<a href="services.html"><button class="styled">services</button></a>
|
||||
<a href="../index.html"><button class="styled">retour</button></a>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// On l'injecte tout en haut du body :
|
||||
document.body.insertAdjacentHTML("afterbegin", creerNavbar());
|
||||
```
|
||||
|
||||
**✅ Teste :** enlève la navbar écrite « en dur » dans le HTML, garde ce JS, recharge → la navbar réapparaît, générée par la fonction. Maintenant le jour où tu changes un lien, tu le changes **à un seul endroit** pour **toutes** les pages (fini les liens cassés du Bug 2 😉).
|
||||
|
||||
> 💡 **À retenir :** un **composant** = un bout d'interface écrit **une fois** et **réutilisé** partout. C'est l'idée centrale derrière React, Vue, etc. — mais ça commence par une simple fonction comme celle-ci.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Ta checklist
|
||||
|
||||
- [ ] Fermer `body` avec un `}` dans le CSS (Bug 1 — le plus urgent)
|
||||
- [ ] Réparer le lien vers `services.html` (Bug 2)
|
||||
- [ ] Passer le champ e-mail en `type="email"` (Bug 3 — tu as mis `e_mail`, presque ! 😉)
|
||||
- [ ] Centrer l'icône SVG du bouton « retour » avec `inline-flex` (Bug 4)
|
||||
- [ ] Ajouter le redimensionnement borné (`resize: vertical` + `min/max-height`)
|
||||
- [ ] Centrer table et `.form` avec `margin: auto` (au lieu de `margin-left: 600px`)
|
||||
- [x] Ajouter `rows`, `cols`, `maxlength` à la textarea ✅
|
||||
- [x] Faire marcher le **compteur de caractères** en JavaScript ✅
|
||||
- [x] Bonus couleur : compteur en rouge sous 50 caractères ✅
|
||||
- [x] Ajouter un `<footer>` ✅
|
||||
- [ ] (Bonus) Transformer la navbar en composant réutilisable
|
||||
- [ ] Penser à fermer la balise `</html>` en fin de fichier
|
||||
|
||||
### 🎓 Niveau 2 (nouveau)
|
||||
- [ ] Afficher « caractère » au **singulier** quand il en reste 1 (leçon `if`/`else`)
|
||||
- [ ] Exo 1 — empêcher l'envoi d'un message **vide** (`submit` + `preventDefault`)
|
||||
- [ ] Exo 2 — bouton **« Effacer »** + factoriser dans une fonction
|
||||
- [ ] Exo 3 — badge **« Ouvert / Fermé »** selon l'heure (`new Date()` + `&&`)
|
||||
- [ ] Commits petits et clairs, puis `git push`
|
||||
|
||||
Pose-moi toutes tes questions. Tu gères ! 📬✨
|
||||
@@ -0,0 +1,456 @@
|
||||
# 🚀 Niveau 3 — JavaScript : tableaux, boucles & pages vivantes
|
||||
|
||||
Salut Mélissa ! 👋
|
||||
|
||||
Tu as fini tout le Niveau 2 (conditions, singulier/pluriel, envoi bloqué si vide, bouton « Effacer », badge horaires, navbar en composant). On enchaîne.
|
||||
|
||||
> 🔎 Détail au passage : ton `restant <= 1 ? "caractère" : "caractères"` est correct — en français, **0 prend le singulier** (« 0 caractère »).
|
||||
|
||||
Ce fichier est une série d'exercices **qui s'enchaînent**, chacun introduisant **une notion de plus**. Fil rouge : rendre la page **vivante et pilotée par des données**. Il y en a beaucoup, de quoi tenir l'après-midi — tu n'es pas obligée de tout finir.
|
||||
|
||||
> 🧭 **Mode d'emploi** (toujours le même) :
|
||||
> - Avance **dans l'ordre**, un exo à la fois.
|
||||
> - **Teste** (✅) avant de passer au suivant.
|
||||
> - Garde la **Console (F12)** ouverte : c'est ton tableau de bord.
|
||||
> - Bloquée ? C'est **normal** : note ta question et on en parle.
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Le programme de l'après-midi
|
||||
|
||||
| Module | Tu vas apprendre… | Exos |
|
||||
|---|---|---|
|
||||
| **A. Tableaux & boucles** | stocker une liste, la parcourir, générer du HTML automatiquement | 1 → 4 |
|
||||
| **B. Manipuler les listes** | rechercher, trier, filtrer | 5 → 6 |
|
||||
| **C. La mémoire** | sauvegarder des infos qui survivent au rechargement | 7 → 8 |
|
||||
| **D. Le style dynamique** | changer l'apparence au clic, faire un accordéon | 9 → 10 |
|
||||
| **E. Pour finir en beauté** | factoriser, paramétrer, défis libres | 11 → 12 |
|
||||
|
||||
---
|
||||
|
||||
# 🧱 Module A — Les tableaux et les boucles
|
||||
|
||||
Jusqu'ici tes variables contenaient **une seule** valeur (`const MAX = 500`). Mais comment ranger **une liste** ? Le nom de tes 2 développeurs, par exemple ? → avec un **tableau** (`array`).
|
||||
|
||||
## 🧩 La notion : un tableau, c'est une liste
|
||||
|
||||
```js
|
||||
const fruits = ["pomme", "banane", "cerise"];
|
||||
|
||||
console.log(fruits[0]); // "pomme" → on compte à partir de 0 !
|
||||
console.log(fruits.length); // 3 → combien d'éléments
|
||||
```
|
||||
|
||||
> 🔎 **Point qui surprend tout le monde au début :** on compte **à partir de 0**. Le 1ᵉ élément est `fruits[0]`, le 2ᵉ est `fruits[1]`… Le dernier est donc `fruits[fruits.length - 1]`.
|
||||
|
||||
## 🧩 La notion : une boucle, pour répéter sans copier-coller
|
||||
|
||||
Pour faire quelque chose **avec chaque** élément d'une liste, on utilise une **boucle** `for...of` :
|
||||
|
||||
```js
|
||||
for (const fruit of fruits) {
|
||||
console.log("J'aime la " + fruit);
|
||||
}
|
||||
// affiche 3 lignes, une par fruit
|
||||
```
|
||||
|
||||
> 💡 **À retenir :** `for...of` se lit « **pour chaque** *fruit* **parmi** *fruits*, fais… ». La boucle s'occupe de passer d'un élément au suivant toute seule.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 1 — Ta première liste, dans la console
|
||||
|
||||
Crée un fichier `equipe.js` (on garde ton `contact.js` pour le formulaire). Branche-le dans ta page, **avant** `contact.js`, juste avant `</body>` :
|
||||
|
||||
```html
|
||||
<script src="../equipe.js"></script>
|
||||
<script src="../contact.js"></script>
|
||||
```
|
||||
|
||||
Dans `equipe.js` :
|
||||
|
||||
```js
|
||||
const equipe = ["Mélissa", "Guillaume"];
|
||||
|
||||
for (const personne of equipe) {
|
||||
console.log("Membre de l'équipe : " + personne);
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Teste :** ouvre la Console (F12). Tu dois voir 2 lignes. Ajoute un 3ᵉ prénom au tableau, recharge → 3 lignes, **sans toucher à la boucle**.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 2 — Des données plus riches : les **objets**
|
||||
|
||||
Un prénom tout seul, c'est peu. Pour décrire une personne (nom **+** job **+** partie créée), on utilise un **objet** `{ … }` :
|
||||
|
||||
```js
|
||||
const membre = {
|
||||
nom: "Mélissa",
|
||||
job: "Stagiaire",
|
||||
partie: "Page contact"
|
||||
};
|
||||
|
||||
console.log(membre.nom); // "Mélissa"
|
||||
console.log(membre.partie); // "Page contact"
|
||||
```
|
||||
|
||||
Et une **liste d'objets** = un tableau d'objets. Remplace le contenu de `equipe.js` :
|
||||
|
||||
```js
|
||||
const equipe = [
|
||||
{ nom: "Mélissa", job: "Stagiaire", partie: "Page contact" },
|
||||
{ nom: "Guillaume", job: "Stagiaire", partie: "Page services" }
|
||||
];
|
||||
|
||||
for (const membre of equipe) {
|
||||
console.log(membre.nom + " — " + membre.job + " — " + membre.partie);
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Teste :** la Console affiche une ligne par membre, avec les 3 infos.
|
||||
|
||||
> 🧩 **À retenir :** un **tableau** `[ ]` = une liste ordonnée. Un **objet** `{ }` = une fiche avec des **étiquettes** (`nom`, `job`…). Les deux combinés (`[ {…}, {…} ]`), c'est **la** façon de stocker des données dans presque toutes les applis.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 3 — Générer ton tableau HTML **automatiquement**
|
||||
|
||||
C'est l'exercice central du module. Aujourd'hui, ton `<table>` d'équipe est **écrit à la main** dans le HTML. Problème : pour ajouter un membre, il faut retaper tout un `<tr>`. On va le faire **générer par le JavaScript** à partir du tableau `equipe`.
|
||||
|
||||
### Étape 1 — Préparer le HTML
|
||||
|
||||
Dans `contact.html`, garde l'en-tête du tableau (`<thead>`) mais **vide le corps** et donne-lui un `id` :
|
||||
|
||||
```html
|
||||
<table border="1" class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>Job</th>
|
||||
<th>Parties créées</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="corps_equipe">
|
||||
<!-- vide : le JavaScript va le remplir tout seul -->
|
||||
</tbody>
|
||||
</table>
|
||||
```
|
||||
|
||||
### Étape 2 — Construire les lignes en JavaScript
|
||||
|
||||
Dans `equipe.js` :
|
||||
|
||||
```js
|
||||
const corps = document.getElementById("corps_equipe");
|
||||
let lignes = ""; // on va empiler le HTML dans ce texte
|
||||
|
||||
for (const membre of equipe) {
|
||||
lignes = lignes + `
|
||||
<tr>
|
||||
<td>${membre.nom}</td>
|
||||
<td>${membre.job}</td>
|
||||
<td>${membre.partie}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
corps.innerHTML = lignes; // on injecte tout d'un coup
|
||||
```
|
||||
|
||||
**✅ Teste :** ton tableau s'affiche comme avant, mais cette fois généré par le JS. La preuve : ajoute un 3ᵉ membre dans le tableau `equipe` (ex. `{ nom: "Matthieu", job: "Tuteur", partie: "Relecture" }`), recharge → la ligne apparaît toute seule.
|
||||
|
||||
> 🔎 Deux nouveautés ici :
|
||||
> - **Le `` `texte ${variable}` ``** (avec les accents graves `` ` ``) : ça s'appelle un *template literal*. Le `${…}` insère la valeur d'une variable **dans** le texte. (Tu l'utilises déjà dans `creerNavbar` 😉 !)
|
||||
> - **`.innerHTML = …`** remplace **tout le contenu HTML** d'un élément. Pratique, mais à réserver à **tes propres** données (jamais avec du texte tapé par un inconnu — question de sécurité, on en reparlera).
|
||||
|
||||
> 🔎 **Le principe à retenir :** on sépare les **données** (le tableau `equipe`) de l'**affichage** (la boucle qui fabrique le HTML). Change les données → l'affichage suit. C'est le mode de fonctionnement de React, Vue, etc.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 4 — Compter et afficher un total
|
||||
|
||||
Au-dessus du tableau, ajoute dans le HTML :
|
||||
|
||||
```html
|
||||
<p>👥 L'équipe compte <span id="nb_membres"></span> personne(s).</p>
|
||||
```
|
||||
|
||||
Et en JS, après ta boucle :
|
||||
|
||||
```js
|
||||
document.getElementById("nb_membres").textContent = equipe.length;
|
||||
```
|
||||
|
||||
**✅ Teste :** le nombre correspond à la taille de ton tableau. Ajoute/enlève un membre → le compte suit tout seul.
|
||||
|
||||
> 💡 `equipe.length`, tu l'avais déjà croisé avec `zoneMessage.value.length` : `.length`, c'est « **combien** ». Sur un texte = nombre de caractères ; sur un tableau = nombre d'éléments.
|
||||
|
||||
---
|
||||
|
||||
# 🔎 Module B — Rechercher et trier dans une liste
|
||||
|
||||
## 🧪 Exo 5 — Une **barre de recherche** qui filtre l'équipe en direct
|
||||
|
||||
> 🆕 Nouvelles notions : `.filter()`, `.toLowerCase()`, `.includes()`, et **ré-afficher** quand les données changent.
|
||||
|
||||
D'abord, range ton affichage du tableau dans une **fonction** (pour pouvoir le rejouer) :
|
||||
|
||||
```js
|
||||
function afficherEquipe(liste) {
|
||||
let lignes = "";
|
||||
for (const membre of liste) {
|
||||
lignes += `
|
||||
<tr>
|
||||
<td>${membre.nom}</td>
|
||||
<td>${membre.job}</td>
|
||||
<td>${membre.partie}</td>
|
||||
</tr>`;
|
||||
}
|
||||
corps.innerHTML = lignes;
|
||||
}
|
||||
|
||||
afficherEquipe(equipe); // affichage initial : toute l'équipe
|
||||
```
|
||||
|
||||
> 🔎 `lignes += "…"` est un raccourci pour `lignes = lignes + "…"`. Pratique pour empiler.
|
||||
|
||||
Ajoute un champ de recherche dans le HTML, au-dessus du tableau :
|
||||
|
||||
```html
|
||||
<input type="search" id="recherche" placeholder="Rechercher un membre…">
|
||||
```
|
||||
|
||||
Puis en JS :
|
||||
|
||||
```js
|
||||
const champRecherche = document.getElementById("recherche");
|
||||
|
||||
champRecherche.addEventListener("input", function () {
|
||||
const motCherche = champRecherche.value.toLowerCase();
|
||||
|
||||
// on garde seulement les membres dont le nom contient le texte tapé
|
||||
const resultats = equipe.filter(function (membre) {
|
||||
return membre.nom.toLowerCase().includes(motCherche);
|
||||
});
|
||||
|
||||
afficherEquipe(resultats); // on ré-affiche, mais filtré
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste :** tape « mé » → seule Mélissa reste. Efface → toute l'équipe revient.
|
||||
|
||||
> 🧩 **À retenir :**
|
||||
> - `.filter(...)` crée un **nouveau** tableau avec seulement les éléments qui passent le test.
|
||||
> - `.toLowerCase()` met en minuscules → la recherche ne tient pas compte des majuscules.
|
||||
> - `.includes("…")` répond vrai/faux : « ce texte contient-il … ? ».
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 6 — Un bouton « Trier par nom »
|
||||
|
||||
> 🆕 Nouvelle notion : `.sort()`.
|
||||
|
||||
```html
|
||||
<button type="button" id="trier">Trier A → Z</button>
|
||||
```
|
||||
|
||||
```js
|
||||
document.getElementById("trier").addEventListener("click", function () {
|
||||
const triee = [...equipe].sort(function (a, b) {
|
||||
return a.nom.localeCompare(b.nom);
|
||||
});
|
||||
afficherEquipe(triee);
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste :** clique → l'équipe s'affiche dans l'ordre alphabétique.
|
||||
|
||||
> 🔎 `[...equipe]` fait une **copie** du tableau (pour ne pas modifier l'original). `localeCompare` compare deux mots dans l'ordre alphabétique (et gère les accents français correctement). C'est un peu abstrait : retiens juste la **recette**, tu la comprendras à force de l'utiliser.
|
||||
|
||||
---
|
||||
|
||||
# 💾 Module C — Donner une mémoire à ta page
|
||||
|
||||
Recharge ta page : tout ce que l'utilisateur avait tapé **disparaît**. On va y remédier avec le **`localStorage`** : un petit coffre-fort dans le navigateur qui **survit au rechargement**.
|
||||
|
||||
## 🧪 Exo 7 — Sauvegarder le brouillon du message
|
||||
|
||||
> 🆕 Nouvelle notion : `localStorage.setItem(...)` / `localStorage.getItem(...)`.
|
||||
|
||||
Dans `contact.js`, ajoute : à **chaque frappe**, on sauvegarde le message.
|
||||
|
||||
```js
|
||||
zoneMessage.addEventListener("input", function () {
|
||||
localStorage.setItem("brouillon", zoneMessage.value);
|
||||
});
|
||||
```
|
||||
|
||||
> ⚠️ Tu as **déjà** un `addEventListener("input", rafraichirCompteur)` sur la même zone. Pas de souci : on peut en mettre plusieurs, ils s'exécutent tous les deux. (Encore mieux : ajoute la ligne `localStorage.setItem(...)` **à l'intérieur** de `rafraichirCompteur`.)
|
||||
|
||||
**✅ Teste :** tape un message, ouvre la Console et tape `localStorage.getItem("brouillon")` → tu vois ton texte.
|
||||
|
||||
## 🧪 Exo 8 — Recharger le brouillon au démarrage
|
||||
|
||||
Au tout début (au chargement de la page), on regarde si un brouillon existe :
|
||||
|
||||
```js
|
||||
const brouillon = localStorage.getItem("brouillon");
|
||||
if (brouillon) { // s'il y a quelque chose de sauvegardé
|
||||
zoneMessage.value = brouillon;
|
||||
rafraichirCompteur(); // pour que le compteur soit juste dès le départ
|
||||
}
|
||||
```
|
||||
|
||||
Et quand le message est envoyé avec succès, on vide le coffre (sinon le vieux brouillon revient) :
|
||||
|
||||
```js
|
||||
// à ajouter dans ton addEventListener("submit", …), quand le message N'EST PAS vide
|
||||
localStorage.removeItem("brouillon");
|
||||
```
|
||||
|
||||
**✅ Teste :** écris un message, **recharge la page** (F5) → ton texte est toujours là, et le compteur est correct.
|
||||
|
||||
> 💡 **À retenir :** `localStorage` garde des infos **entre les visites** (jusqu'à ce qu'on les efface). `setItem(clé, valeur)` pour ranger, `getItem(clé)` pour relire, `removeItem(clé)` pour effacer.
|
||||
|
||||
---
|
||||
|
||||
# 🎨 Module D — Changer l'apparence en JavaScript
|
||||
|
||||
## 🧪 Exo 9 — Un bouton « Mode sombre 🌙 »
|
||||
|
||||
> 🆕 Nouvelle notion : `classList.toggle(...)` — ajouter/retirer une classe CSS au clic.
|
||||
|
||||
D'abord, prépare la classe dans ton CSS (`contact.css`) :
|
||||
|
||||
```css
|
||||
body.sombre {
|
||||
background-color: #1d2733;
|
||||
color: white;
|
||||
}
|
||||
```
|
||||
|
||||
Un bouton dans le HTML :
|
||||
|
||||
```html
|
||||
<button type="button" id="theme">🌙 Mode sombre</button>
|
||||
```
|
||||
|
||||
Et le JS :
|
||||
|
||||
```js
|
||||
document.getElementById("theme").addEventListener("click", function () {
|
||||
document.body.classList.toggle("sombre");
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste :** clique → la page bascule en sombre, re-clique → elle revient. Un seul `toggle` fait les deux !
|
||||
|
||||
> 🧩 **À retenir :** `classList.toggle("sombre")` = « si la classe est là, enlève-la ; sinon, ajoute-la ». C'est **le** pont entre ton JavaScript et ton CSS : le JS gère le *quand*, le CSS gère le *à quoi ça ressemble*.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 10 — Une FAQ en accordéon
|
||||
|
||||
> 🆕 Nouvelles notions : `querySelectorAll(...)` (sélectionner **plusieurs** éléments) + une boucle `forEach`.
|
||||
|
||||
Beaucoup de pages contact ont une FAQ. Ajoute dans le HTML :
|
||||
|
||||
```html
|
||||
<section class="faq">
|
||||
<h3>Questions fréquentes</h3>
|
||||
<div class="question">
|
||||
<button class="q-titre" type="button">Comment suivre ma flotte en temps réel ?</button>
|
||||
<p class="q-reponse">Grâce à notre service de géolocalisation, accessible 24h/24.</p>
|
||||
</div>
|
||||
<div class="question">
|
||||
<button class="q-titre" type="button">Puis-je résilier à tout moment ?</button>
|
||||
<p class="q-reponse">Oui, sans frais, depuis votre espace client.</p>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
Dans le CSS, on cache les réponses au départ :
|
||||
|
||||
```css
|
||||
.q-reponse { display: none; }
|
||||
.q-reponse.ouverte { display: block; }
|
||||
```
|
||||
|
||||
En JavaScript, on veut que **chaque** bouton ouvre/ferme **sa** réponse :
|
||||
|
||||
```js
|
||||
const titres = document.querySelectorAll(".q-titre"); // TOUS les boutons FAQ
|
||||
|
||||
titres.forEach(function (titre) {
|
||||
titre.addEventListener("click", function () {
|
||||
// la réponse est le paragraphe juste après le bouton
|
||||
const reponse = titre.nextElementSibling;
|
||||
reponse.classList.toggle("ouverte");
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste :** clique sur une question → sa réponse apparaît ; re-clique → elle se cache. Chaque question est indépendante.
|
||||
|
||||
> 🧩 **À retenir :**
|
||||
> - `getElementById` attrape **un** élément ; `querySelectorAll(".classe")` en attrape **plusieurs** (tous ceux qui ont cette classe).
|
||||
> - `.forEach(...)` est une boucle qui fait quelque chose **pour chaque** élément trouvé — ici : « ajoute un écouteur de clic à chacun ».
|
||||
> - `nextElementSibling` = « l'élément juste après moi ». Pratique pour relier un bouton à sa réponse.
|
||||
|
||||
---
|
||||
|
||||
# 🏁 Module E — Pour finir en beauté
|
||||
|
||||
## 🧪 Exo 11 — Une navbar qui sait quelle page est active
|
||||
|
||||
Ta `creerNavbar()` est déjà un beau composant. Rendons-la **paramétrable** : on lui dit quelle page est active, et elle met le bon bouton en surbrillance.
|
||||
|
||||
> 🆕 Nouvelle notion : une fonction qui prend un **paramètre**.
|
||||
|
||||
```js
|
||||
function creerNavbar(pageActive) { // ← pageActive est le "réglage" qu'on lui donne
|
||||
return `
|
||||
<div id="nav_barre" class="shadow">
|
||||
<h1>FleetZen</h1>
|
||||
<div id="buttons">
|
||||
<a href="contact.html"><button class="${pageActive === "contact" ? "instyled" : "styled"}">contact</button></a>
|
||||
<a href="services.html"><button class="${pageActive === "services" ? "instyled" : "styled"}">services</button></a>
|
||||
<a href="../index.html"><button class="styled">retour</button></a>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
document.body.insertAdjacentHTML("afterbegin", creerNavbar("contact"));
|
||||
```
|
||||
|
||||
**✅ Teste :** sur la page contact, le bouton « contact » apparaît enfoncé (`instyled`). Le jour où Guillaume réutilise cette fonction sur sa page, il écrira juste `creerNavbar("services")` → c'est SON bouton qui sera actif.
|
||||
|
||||
> 💡 **À retenir :** un **paramètre** (`pageActive`), c'est un réglage qu'on passe à une fonction pour qu'elle s'adapte. Une seule fonction, plusieurs résultats selon ce qu'on lui donne. C'est ÇA, un vrai composant réutilisable. ♻️
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Exo 12 — Défis libres (si tu as encore de l'énergie)
|
||||
|
||||
Choisis ceux qui t'attirent, dans l'ordre que tu veux :
|
||||
|
||||
1. **Recap avant envoi** : à la soumission, affiche « Merci {prénom}, ton message de {N} caractères a bien été pris en compte ! » en lisant la valeur du champ prénom.
|
||||
2. **Validation e-mail maison** : si l'e-mail ne contient pas de `@`, bloque l'envoi avec un message (en plus de la vérification du navigateur).
|
||||
3. **Compteur de mots** (en plus des caractères) : `zoneMessage.value.trim().split(" ").length`.
|
||||
4. **Ajouter un membre via un formulaire** : un petit champ + bouton qui fait `equipe.push({...})` puis `afficherEquipe(equipe)`.
|
||||
5. **Mémoriser le mode sombre** dans `localStorage` pour qu'il reste actif au rechargement.
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Récap — tout ce que tu sais faire maintenant
|
||||
|
||||
- **Données** : variables, **tableaux** `[ ]`, **objets** `{ }`, listes d'objets
|
||||
- **Répéter** : boucles `for...of` et `.forEach()`
|
||||
- **Transformer des listes** : `.filter()`, `.sort()`
|
||||
- **Générer du HTML** depuis des données (`innerHTML` + *template literals*)
|
||||
- **Mémoire** : `localStorage` (setItem / getItem / removeItem)
|
||||
- **Style dynamique** : `classList.toggle()`
|
||||
- **Sélection multiple** : `querySelectorAll` + `forEach`
|
||||
- **Fonctions paramétrées** : un composant qui s'adapte
|
||||
|
||||
Prochaine grande étape (pour une autre fois) : **les requêtes vers un serveur** avec `fetch`, pour aller chercher de vraies données en ligne.
|
||||
@@ -0,0 +1,239 @@
|
||||
# 🚀 Niveau 4 — DOM propre, données distantes & temps réel
|
||||
|
||||
Salut Mélissa ! 👋
|
||||
|
||||
Suite logique de ce que tu sais déjà faire (tableaux, boucles, `localStorage`, `classList`, événements…). Quatre modules, du plus simple au plus costaud. Comme d'habitude : un exo à la fois, et **✅ teste** avant de passer au suivant.
|
||||
|
||||
| Module | Notion nouvelle | Exos |
|
||||
|---|---|---|
|
||||
| **A. Construire le DOM proprement** | `createElement` / `append` (au lieu de `innerHTML`) | 1 → 2 |
|
||||
| **B. Aller chercher des données ailleurs** | `fetch`, `async`/`await`, JSON | 3 → 5 |
|
||||
| **C. Le temps réel** | `setInterval` | 6 → 7 |
|
||||
| **D. Le clavier** | événements clavier (`keydown`, `event.key`) | 8 |
|
||||
|
||||
---
|
||||
|
||||
# 🧱 Module A — Construire le HTML sans `innerHTML`
|
||||
|
||||
Pour ton tableau d'équipe, tu utilises `corps.innerHTML = lignes`. Ça marche très bien pour **tes** données. Mais `innerHTML` a deux limites :
|
||||
- si tu y mets du texte tapé par un **inconnu**, il peut injecter du code (faille de sécurité dite *XSS*) ;
|
||||
- ça remplace **tout** d'un coup, difficile de modifier une seule ligne ensuite.
|
||||
|
||||
La méthode « propre » consiste à fabriquer chaque élément **un par un**.
|
||||
|
||||
## 🧩 La notion : `createElement` + `textContent` + `append`
|
||||
|
||||
```js
|
||||
const ligne = document.createElement("tr"); // on crée une balise <tr> (vide, pas encore dans la page)
|
||||
|
||||
const caseNom = document.createElement("td");
|
||||
caseNom.textContent = "Mélissa"; // textContent = du texte pur, jamais interprété comme du HTML
|
||||
|
||||
ligne.append(caseNom); // on range la <td> dans la <tr>
|
||||
```
|
||||
|
||||
> 🔎 `createElement` fabrique l'élément **en mémoire** ; il n'apparaît à l'écran qu'une fois **rattaché** à la page avec `append`. `textContent` (et non `innerHTML`) garantit que le contenu reste du **texte**, même si quelqu'un tape `<script>`.
|
||||
|
||||
## 🧪 Exo 1 — Réécrire une ligne du tableau « proprement »
|
||||
|
||||
Dans `equipe.js`, écris une fonction qui fabrique **une** ligne à partir d'un membre :
|
||||
|
||||
```js
|
||||
function creerLigne(membre) {
|
||||
const tr = document.createElement("tr");
|
||||
|
||||
const tdNom = document.createElement("td");
|
||||
tdNom.textContent = membre.nom;
|
||||
|
||||
const tdJob = document.createElement("td");
|
||||
tdJob.textContent = membre.job;
|
||||
|
||||
const tdPartie = document.createElement("td");
|
||||
tdPartie.textContent = membre.partie;
|
||||
|
||||
tr.append(tdNom, tdJob, tdPartie); // append accepte plusieurs éléments d'un coup
|
||||
return tr;
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 Exo 2 — Remplir le tableau avec cette fonction
|
||||
|
||||
```js
|
||||
const corps = document.getElementById("corps_equipe");
|
||||
|
||||
corps.innerHTML = ""; // on vide d'abord
|
||||
for (const membre of equipe) {
|
||||
corps.append(creerLigne(membre)); // on ajoute chaque ligne fabriquée
|
||||
}
|
||||
```
|
||||
|
||||
**✅ Teste :** le tableau s'affiche comme avant. La différence est invisible à l'écran, mais ton code est maintenant plus sûr et chaque ligne est un vrai objet manipulable.
|
||||
|
||||
> 💡 **À retenir :** `innerHTML` = rapide, pour **tes** données fixes. `createElement` + `textContent` = la méthode propre, surtout dès qu'il y a du contenu venant de l'**utilisateur**.
|
||||
|
||||
---
|
||||
|
||||
# 🌐 Module B — Aller chercher des données ailleurs (`fetch`)
|
||||
|
||||
Jusqu'ici tes données (`equipe`) sont **écrites dans le code**. Dans la vraie vie, elles arrivent **d'un serveur**, sous forme de **JSON** (un format texte très proche des objets/tableaux que tu connais déjà).
|
||||
|
||||
## ⚠️ À lire avant de commencer : il faut un petit serveur
|
||||
|
||||
`fetch` **ne fonctionne pas** si tu ouvres ta page en double-cliquant (adresse en `file://`). Il faut servir le dossier. Dans un terminal, place-toi dans le dossier du projet et lance :
|
||||
|
||||
```bash
|
||||
python3 -m http.server 8000
|
||||
```
|
||||
|
||||
Puis ouvre **http://localhost:8000/pages/contact.html** dans le navigateur. (C'est exactement ce que fait un vrai site : un serveur qui envoie les fichiers.)
|
||||
|
||||
## 🧪 Exo 3 — Créer un fichier de données JSON
|
||||
|
||||
Crée `vehicules.json` à la racine du projet :
|
||||
|
||||
```json
|
||||
[
|
||||
{ "modele": "Renault Kangoo", "immatriculation": "AB-123-CD", "km": 45200 },
|
||||
{ "modele": "Peugeot Partner", "immatriculation": "EF-456-GH", "km": 78900 },
|
||||
{ "modele": "Citroën Berlingo", "immatriculation": "IJ-789-KL", "km": 12300 }
|
||||
]
|
||||
```
|
||||
|
||||
> 🔎 Le JSON ressemble à un tableau d'objets JS, avec **deux règles strictes** : toutes les clés sont **entre guillemets doubles**, et **pas de virgule** après le dernier élément. Une erreur ici et tout casse — colle-le dans un validateur si besoin.
|
||||
|
||||
## 🧪 Exo 4 — Récupérer ce fichier avec `fetch`
|
||||
|
||||
> 🆕 Nouvelles notions : `fetch`, `async`/`await`. `fetch` va chercher un fichier/des données **sur le réseau** ; comme ça prend un peu de temps, on **attend** la réponse avec `await`.
|
||||
|
||||
Dans un nouveau fichier `flotte.js` (branché dans la page) :
|
||||
|
||||
```js
|
||||
async function chargerFlotte() {
|
||||
const reponse = await fetch("../vehicules.json"); // on demande le fichier
|
||||
const vehicules = await reponse.json(); // on le transforme en tableau JS
|
||||
console.log(vehicules); // pour vérifier
|
||||
}
|
||||
|
||||
chargerFlotte();
|
||||
```
|
||||
|
||||
**✅ Teste :** via http://localhost:8000/…, ouvre la Console → tu dois voir ton tableau de 3 véhicules. Si tu vois une erreur, relis la partie « petit serveur » ci-dessus.
|
||||
|
||||
> 🧩 **À retenir :**
|
||||
> - `async` devant une fonction = « cette fonction contient de l'attente ».
|
||||
> - `await` = « attends que ce soit prêt avant de continuer ».
|
||||
> - `fetch(...)` rapporte une **réponse**, et `reponse.json()` la transforme en données JS utilisables.
|
||||
|
||||
## 🧪 Exo 5 — Afficher la flotte + gérer les erreurs
|
||||
|
||||
Ajoute un tableau dans le HTML :
|
||||
|
||||
```html
|
||||
<h3>Notre flotte</h3>
|
||||
<p id="message_flotte">Chargement…</p>
|
||||
<table border="1"><tbody id="corps_flotte"></tbody></table>
|
||||
```
|
||||
|
||||
Puis complète `flotte.js` :
|
||||
|
||||
```js
|
||||
async function chargerFlotte() {
|
||||
const message = document.getElementById("message_flotte");
|
||||
const corps = document.getElementById("corps_flotte");
|
||||
|
||||
try {
|
||||
const reponse = await fetch("../vehicules.json");
|
||||
const vehicules = await reponse.json();
|
||||
|
||||
message.textContent = vehicules.length + " véhicules :";
|
||||
for (const v of vehicules) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.textContent = `${v.modele} — ${v.immatriculation} — ${v.km} km`;
|
||||
corps.append(tr);
|
||||
}
|
||||
} catch (erreur) {
|
||||
message.textContent = "Impossible de charger la flotte 😕";
|
||||
console.error(erreur);
|
||||
}
|
||||
}
|
||||
|
||||
chargerFlotte();
|
||||
```
|
||||
|
||||
**✅ Teste :** la liste des véhicules s'affiche. Renomme exprès `vehicules.json` en `vehicule.json` (faute) et recharge → tu dois voir le message d'erreur au lieu d'un plantage.
|
||||
|
||||
> 🧩 **À retenir :** `try { … } catch (erreur) { … }` = « **essaie** ce code ; **si** ça rate, fais ça plutôt ». Indispensable avec le réseau, où ça peut toujours échouer (pas de connexion, fichier absent…).
|
||||
|
||||
---
|
||||
|
||||
# ⏱️ Module C — Le temps réel avec `setInterval`
|
||||
|
||||
## 🧪 Exo 6 — Une horloge dans le pied de page
|
||||
|
||||
> 🆕 Nouvelle notion : `setInterval(fonction, durée)` répète une action toutes les *durée* millisecondes.
|
||||
|
||||
```html
|
||||
<span id="horloge"></span>
|
||||
```
|
||||
|
||||
```js
|
||||
const horloge = document.getElementById("horloge");
|
||||
|
||||
function afficherHeure() {
|
||||
const maintenant = new Date();
|
||||
horloge.textContent = maintenant.toLocaleTimeString(); // ex. "14:05:32"
|
||||
}
|
||||
|
||||
afficherHeure(); // une fois tout de suite
|
||||
setInterval(afficherHeure, 1000); // puis toutes les 1000 ms = 1 seconde
|
||||
```
|
||||
|
||||
**✅ Teste :** l'heure s'affiche et **avance toute seule** chaque seconde.
|
||||
|
||||
> 🔎 `1000` est en **millisecondes** (1 s). `setInterval` ne s'arrête jamais tout seul ; pour le stopper un jour, on garde son résultat (`const id = setInterval(...)`) et on fait `clearInterval(id)`.
|
||||
|
||||
## 🧪 Exo 7 — Mettre à jour le badge « Ouvert / Fermé » en direct
|
||||
|
||||
Ton badge horaires est calculé **une seule fois** au chargement. Range ce calcul dans une fonction `rafraichirBadge()` puis appelle-la dans ton `setInterval` (par exemple toutes les 60 s). Ainsi, si la page reste ouverte à 18h pile, le badge passera tout seul à « Fermé ».
|
||||
|
||||
**✅ Teste :** pour vérifier sans attendre, mets temporairement l'intervalle à `1000` et change les heures d'ouverture autour de l'heure actuelle.
|
||||
|
||||
---
|
||||
|
||||
# ⌨️ Module D — Réagir au clavier
|
||||
|
||||
## 🧪 Exo 8 — Envoyer avec Ctrl+Entrée, effacer avec Échap
|
||||
|
||||
> 🆕 Nouvelle notion : l'événement `keydown` et la propriété `event.key`.
|
||||
|
||||
```js
|
||||
zoneMessage.addEventListener("keydown", function (event) {
|
||||
// Ctrl + Entrée → on déclenche l'envoi du formulaire
|
||||
if (event.key === "Enter" && event.ctrlKey) {
|
||||
formulaire.requestSubmit(); // comme cliquer sur "Envoyer"
|
||||
}
|
||||
|
||||
// Touche Échap → on vide la zone
|
||||
if (event.key === "Escape") {
|
||||
zoneMessage.value = "";
|
||||
rafraichirCompteur();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**✅ Teste :** écris un message, fais **Ctrl+Entrée** → l'envoi se déclenche. Appuie sur **Échap** → la zone se vide.
|
||||
|
||||
> 🔎 `event.key` te donne le **nom** de la touche (`"Enter"`, `"Escape"`, `"a"`…). `event.ctrlKey` est vrai si Ctrl est maintenue. C'est comme ça qu'on crée des raccourcis clavier.
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Ce que ce niveau ajoute à ta boîte à outils
|
||||
|
||||
- **DOM propre** : `createElement`, `textContent`, `append`
|
||||
- **Données distantes** : `fetch`, `async`/`await`, JSON, `try`/`catch`
|
||||
- **Temps réel** : `setInterval` / `clearInterval`
|
||||
- **Clavier** : `keydown`, `event.key`, raccourcis
|
||||
|
||||
Prochaine étape possible : organiser ton code en plusieurs fichiers/modules, ou afficher des données depuis une **vraie API publique** en ligne. Dis-moi vers quoi tu veux aller.
|
||||
|
||||
Note tes questions au fur et à mesure.
|
||||
@@ -0,0 +1,144 @@
|
||||
# 🧭 Niveau 5 — aller plus loin (mode indices)
|
||||
|
||||
Salut Mélissa ! 👋
|
||||
|
||||
> 📌 À faire **une fois le Niveau 4 terminé** (`createElement`, `fetch`, `setInterval`, clavier).
|
||||
|
||||
Petit changement de format à partir d'ici : **je ne donne plus le code tout fait.** Pour chaque exo tu as un **🎯 objectif**, des **🔍 pistes** (quoi chercher, quels mots-clés, les pièges) et un **✅ test**. À toi d'écrire le code.
|
||||
|
||||
> 🔎 Tu as maintenant assez de bases pour lire la doc directement. Le réflexe : chercher **« MDN » + le nom de la chose** (ex. `MDN Array map`). Cherche d'abord **le concept**, le code viendra de toi.
|
||||
|
||||
| Module | Notions visées | Exos |
|
||||
|---|---|---|
|
||||
| **A. Tableaux, niveau 2** | `map`, `find`, `reduce` | 1 → 3 |
|
||||
| **B. Écouteurs malins** | délégation d'événements, anti-rebond | 4 → 5 |
|
||||
| **C. Organiser son code** | modules `export` / `import` | 6 |
|
||||
| **D. Une vraie API en ligne** | `fetch` vers le web réel | 7 |
|
||||
| **E. Lire un formulaire d'un coup** | `FormData` | 8 |
|
||||
|
||||
---
|
||||
|
||||
# 🧱 Module A — Les tableaux, niveau 2
|
||||
|
||||
Tu connais déjà `filter` (garder certains éléments) et `sort` (trier). Voici trois autres méthodes qui reviennent **tout le temps**.
|
||||
|
||||
## 🧪 Exo 1 — `map` : transformer une liste
|
||||
|
||||
**🎯 Objectif :** à partir de ton tableau `equipe`, fabriquer un **nouveau** tableau qui ne contient que les **noms** (`["Mélissa", "Guillaume"]`).
|
||||
|
||||
**🔍 Pistes :**
|
||||
- Cherche `MDN Array map`. L'idée : `map` applique une transformation à **chaque** élément et renvoie une **nouvelle** liste de même taille.
|
||||
- Différence avec `filter` : `filter` **garde ou jette** des éléments (la liste rétrécit) ; `map` **transforme** chacun (la liste garde la même taille).
|
||||
|
||||
**✅ Test :** un `console.log` affiche bien la liste des noms seuls.
|
||||
|
||||
## 🧪 Exo 2 — `find` : retrouver un élément précis
|
||||
|
||||
**🎯 Objectif :** retrouver dans `equipe` l'objet de la personne qui a fait la « Page contact ».
|
||||
|
||||
**🔍 Pistes :**
|
||||
- Cherche `MDN Array find`. `find` renvoie le **premier** élément qui remplit une condition (un seul objet, pas une liste).
|
||||
- Attention au cas « personne ne correspond » : `find` renvoie alors `undefined` → comment l'éviter de planter ? (cherche `optional chaining` ou un simple `if`).
|
||||
|
||||
**✅ Test :** `console.log` affiche le bon objet membre.
|
||||
|
||||
## 🧪 Exo 3 — `reduce` : calculer un total
|
||||
|
||||
**🎯 Objectif :** si tu as fait le `vehicules.json` du Niveau 4, calcule le **kilométrage total** de toute la flotte.
|
||||
|
||||
**🔍 Pistes :**
|
||||
- Cherche `MDN Array reduce`. C'est la méthode la plus abstraite des trois : elle « replie » toute une liste en **une seule valeur** (ici, une somme).
|
||||
- Repère les deux choses qu'elle attend : une fonction `(accumulateur, élément) => ...` et une **valeur de départ** (pour une somme, laquelle ?).
|
||||
- Si reduce te paraît trop dense au début, tu peux d'abord le faire avec une boucle `for...of` et une variable `total`, puis le réécrire en `reduce` pour comparer.
|
||||
|
||||
**✅ Test :** le total affiché correspond à la somme des `km` de ton JSON.
|
||||
|
||||
---
|
||||
|
||||
# 🎯 Module B — Des écouteurs d'événements plus malins
|
||||
|
||||
## 🧪 Exo 4 — La délégation d'événements
|
||||
|
||||
**🎯 Objectif :** rendre **chaque ligne** du tableau d'équipe cliquable (au clic, afficher le nom du membre dans une alerte). Le piège : tes lignes sont **générées** par JS, et parfois recréées (recherche, tri)…
|
||||
|
||||
**🔍 Pistes :**
|
||||
- L'approche naïve serait d'ajouter un écouteur sur chaque `<tr>`. Problème : à chaque ré-affichage, il faut tout réattacher, et on en oublie. Cherche pourquoi → `event delegation javascript`.
|
||||
- L'idée maligne : mettre **un seul** écouteur sur le **parent** (le `<tbody>`), qui ne bouge jamais. Quand un clic remonte jusqu'à lui, on regarde **d'où** il vient.
|
||||
- Pour savoir sur quel élément précis on a cliqué → cherche `event.target` et `closest`.
|
||||
|
||||
**✅ Test :** clique sur n'importe quelle ligne → la bonne info s'affiche, **même après** avoir filtré/trié le tableau.
|
||||
|
||||
## 🧪 Exo 5 — Anti-rebond (« debounce ») sur la recherche
|
||||
|
||||
**🎯 Objectif :** ta recherche se relance à **chaque** frappe. Sur une grosse liste, c'est du gaspillage. Attendre que l'utilisateur arrête de taper ~300 ms avant de filtrer.
|
||||
|
||||
**🔍 Pistes :**
|
||||
- Cherche `debounce javascript` et les fonctions `setTimeout` / `clearTimeout`.
|
||||
- L'idée : à chaque frappe, on **annule** le minuteur précédent et on en relance un. Le filtrage ne part que si l'utilisateur fait une pause.
|
||||
|
||||
**✅ Test :** tape vite « gui » → le filtrage ne se déclenche qu'**une** fois, à la fin, pas trois.
|
||||
|
||||
---
|
||||
|
||||
# 📦 Module C — Organiser son code en modules
|
||||
|
||||
Ton `contact.js` grossit (compteur, formulaire, FAQ, navbar, horloge, fetch…). Il est temps de **découper**.
|
||||
|
||||
## 🧪 Exo 6 — Séparer en fichiers avec `export` / `import`
|
||||
|
||||
**🎯 Objectif :** sortir, par exemple, `creerNavbar` dans son propre fichier `navbar.js`, et l'**importer** là où tu en as besoin.
|
||||
|
||||
**🔍 Pistes :**
|
||||
- Cherche `javascript export import` (les **modules ES**).
|
||||
- Côté HTML, une balise `<script>` classique ne suffit pas pour les modules : cherche `script type module`.
|
||||
- ⚠️ Comme `fetch`, les modules **ne marchent pas en `file://`** : il te faut le petit serveur (`python3 -m http.server`) vu au Niveau 4.
|
||||
- Commence petit : **un seul** export (la navbar), et fais-le marcher avant d'en découper d'autres.
|
||||
|
||||
**✅ Test :** la page fonctionne exactement comme avant, mais ton code est réparti dans plusieurs fichiers plus courts.
|
||||
|
||||
---
|
||||
|
||||
# 🌍 Module D — Des données d'une vraie API en ligne
|
||||
|
||||
Au Niveau 4 tu lisais un fichier JSON **local**. Là, on va chercher des données sur un **vrai serveur public**, comme le font les applis réelles.
|
||||
|
||||
## 🧪 Exo 7 — Afficher des données venues du web
|
||||
|
||||
**🎯 Objectif :** récupérer une liste depuis une API publique gratuite et l'afficher dans ta page (par ex. une liste d'utilisateurs fictifs, ou de publications).
|
||||
|
||||
**🔍 Pistes :**
|
||||
- Une API d'entraînement très connue, sans clé ni inscription : cherche `jsonplaceholder`. Par exemple l'adresse `https://jsonplaceholder.typicode.com/users` renvoie une liste de personnes en JSON.
|
||||
- Tu réutilises **exactement** ce que tu sais déjà : `fetch`, `await`, `.json()`, une boucle, et `createElement`/`append` pour afficher.
|
||||
- Ouvre d'abord l'adresse directement dans le navigateur pour **voir la forme** des données : quelles clés existent (`name`, `email`, `company`…) ? C'est le réflexe n°1 avant d'afficher quoi que ce soit.
|
||||
- Pense au `try/catch` : une API en ligne peut être lente ou indisponible.
|
||||
|
||||
**✅ Test :** une liste de vraies données distantes s'affiche dans ta page. Coupe ta connexion → ton message d'erreur s'affiche proprement.
|
||||
|
||||
---
|
||||
|
||||
# 📝 Module E — Lire tout un formulaire d'un coup
|
||||
|
||||
## 🧪 Exo 8 — `FormData`
|
||||
|
||||
**🎯 Objectif :** à l'envoi, récupérer **toutes** les valeurs du formulaire (prénom, e-mail, message) sans aller chercher chaque champ un par un avec `getElementById`.
|
||||
|
||||
**🔍 Pistes :**
|
||||
- Cherche `MDN FormData`. On lui passe le `<form>`, et on peut lire chaque champ par son attribut `name` (`.get("prenom")`…).
|
||||
- Pour que ça marche, vérifie que **chacun** de tes champs a bien un attribut `name` (pas seulement un `id`).
|
||||
- Bonus : cherche `Object.fromEntries(formData)` pour transformer tout le formulaire en un seul objet `{ prenom: ..., email: ..., message: ... }`.
|
||||
|
||||
**✅ Test :** à l'envoi, un `console.log` montre un objet propre avec toutes les valeurs saisies.
|
||||
|
||||
---
|
||||
|
||||
## 🗺️ Ce que ce niveau ajoute
|
||||
|
||||
- `map`, `find`, `reduce` — le trio qui complète `filter`/`sort`
|
||||
- **délégation d'événements** et **debounce** — des écouteurs propres et efficaces
|
||||
- **modules** `export`/`import` — du code organisé
|
||||
- **API en ligne** réelle — `fetch` au-delà du fichier local
|
||||
- **`FormData`** — lire un formulaire d'un coup
|
||||
|
||||
Après ça, les grandes directions possibles : un **framework** (commencer par comprendre *pourquoi* ils existent — tu as déjà recréé à la main plusieurs de leurs idées : composants, données → affichage), ou approfondir l'**accessibilité** et les **tests**. On en parlera.
|
||||
|
||||
Note tes questions, et cherche le concept avant le code.
|
||||
@@ -0,0 +1,165 @@
|
||||
# ✅ Solutions — Niveau 5 contact (`PROCHAINE_ETAPE_contact_4.md`)
|
||||
|
||||
> 🛑 **À NE REGARDER QU'EN DERNIER RECOURS.**
|
||||
> Cherche d'abord par toi-même (au moins 10–15 min, doc MDN à l'appui). Quand tu lis une réponse : lis-la, **ferme le fichier**, et réécris-la **de mémoire**. C'est comme ça que ça rentre.
|
||||
|
||||
Le code ci-dessous est **un** exemple correct. Le tien peut être différent et tout aussi valable.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 1 — `map`
|
||||
|
||||
```js
|
||||
const noms = equipe.map(function (membre) {
|
||||
return membre.nom;
|
||||
});
|
||||
console.log(noms); // ["Mélissa", "Guillaume"]
|
||||
```
|
||||
> 💬 *« `map` ou `filter` ? »* `filter` garde/jette des éléments (la liste rétrécit). `map` transforme chaque élément (la liste garde la même taille). Ici on veut transformer chaque objet en son nom → `map`.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 2 — `find`
|
||||
|
||||
```js
|
||||
const personne = equipe.find(function (membre) {
|
||||
return membre.partie === "Page contact";
|
||||
});
|
||||
|
||||
if (personne) {
|
||||
console.log(personne.nom);
|
||||
} else {
|
||||
console.log("Personne ne correspond.");
|
||||
}
|
||||
```
|
||||
> 💬 *« Pourquoi le `if (personne)` ? »* Si aucun élément ne correspond, `find` renvoie `undefined`. Faire `personne.nom` planterait. On vérifie d'abord. (Variante moderne : `personne?.nom` — cherche `optional chaining`.)
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 3 — `reduce`
|
||||
|
||||
Avec une boucle (plus lisible pour commencer) :
|
||||
```js
|
||||
let totalKm = 0;
|
||||
for (const v of vehicules) {
|
||||
totalKm = totalKm + v.km;
|
||||
}
|
||||
```
|
||||
La même chose en `reduce` :
|
||||
```js
|
||||
const totalKm = vehicules.reduce(function (somme, v) {
|
||||
return somme + v.km;
|
||||
}, 0); // ← 0 = la valeur de départ
|
||||
console.log(totalKm);
|
||||
```
|
||||
> 💬 *« À quoi sert le `0` à la fin ? »* C'est la valeur de départ de l'accumulateur (`somme`). Pour une somme on part de 0 ; pour multiplier on partirait de 1.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 4 — Délégation d'événements
|
||||
|
||||
Quand tu génères les lignes, marque chaque ligne avec le nom (un attribut `data-…`) :
|
||||
```js
|
||||
function creerLigne(membre) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.dataset.nom = membre.nom; // devient data-nom="..." dans le HTML
|
||||
// ... (les <td> comme avant)
|
||||
return tr;
|
||||
}
|
||||
```
|
||||
Puis **un seul** écouteur, sur le parent qui ne bouge pas :
|
||||
```js
|
||||
const corps = document.getElementById("corps_equipe");
|
||||
corps.addEventListener("click", function (event) {
|
||||
const ligne = event.target.closest("tr"); // la <tr> cliquée, peu importe la <td>
|
||||
if (ligne) {
|
||||
alert("Membre : " + ligne.dataset.nom);
|
||||
}
|
||||
});
|
||||
```
|
||||
> 💬 *« Pourquoi ne pas mettre l'écouteur sur chaque `<tr>` ? »* Parce que tes lignes sont recréées à chaque recherche/tri : il faudrait tout réattacher à chaque fois. En écoutant le **parent** (qui, lui, ne change pas), ça marche même pour les lignes créées plus tard.
|
||||
> 💬 *« À quoi sert `closest("tr")` ? »* Le clic peut tomber sur une `<td>` ; `closest` remonte jusqu'à la `<tr>` qui la contient.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 5 — Debounce sur la recherche
|
||||
|
||||
```js
|
||||
let minuteur; // garde la trace du minuteur en cours
|
||||
|
||||
champRecherche.addEventListener("input", function () {
|
||||
clearTimeout(minuteur); // on annule le précédent
|
||||
minuteur = setTimeout(function () { // on en relance un
|
||||
const mot = champRecherche.value.toLowerCase();
|
||||
const resultats = equipe.filter(m => m.nom.toLowerCase().includes(mot));
|
||||
afficherEquipe(resultats);
|
||||
}, 300); // 300 ms après la DERNIÈRE frappe
|
||||
});
|
||||
```
|
||||
> 💬 *« Comment ça « attend » ? »* À chaque frappe, on **annule** le minuteur précédent (`clearTimeout`) et on en repart un de 300 ms. Tant que tu tapes, il est sans cesse annulé ; il ne se déclenche que quand tu fais une pause.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 6 — Modules `export` / `import`
|
||||
|
||||
`navbar.js` :
|
||||
```js
|
||||
export function creerNavbar(pageActive) {
|
||||
return `... ton HTML de navbar ...`;
|
||||
}
|
||||
```
|
||||
`contact.js` (tout en haut) :
|
||||
```js
|
||||
import { creerNavbar } from "./navbar.js";
|
||||
document.body.insertAdjacentHTML("afterbegin", creerNavbar("contact"));
|
||||
```
|
||||
HTML :
|
||||
```html
|
||||
<script type="module" src="../contact.js"></script>
|
||||
```
|
||||
> 💬 *« Depuis que j'utilise `type="module"`, mes autres scripts ne se voient plus ! »* Normal : chaque module a son **propre** espace. Une variable d'un fichier n'est plus visible ailleurs **sauf** si tu l'`export`es et l'`import`es. C'est justement le but : plus de variables globales qui se marchent dessus.
|
||||
> 💬 *« Erreur CORS au chargement du module. »* Comme `fetch`, les modules exigent un serveur : `python3 -m http.server` puis `http://localhost:8000/...` (pas `file://`).
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 7 — Vraie API en ligne
|
||||
|
||||
```js
|
||||
async function chargerUtilisateurs() {
|
||||
const liste = document.getElementById("liste_users");
|
||||
try {
|
||||
const reponse = await fetch("https://jsonplaceholder.typicode.com/users");
|
||||
const users = await reponse.json();
|
||||
for (const u of users) {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = `${u.name} — ${u.email}`;
|
||||
liste.append(li);
|
||||
}
|
||||
} catch (erreur) {
|
||||
console.error(erreur);
|
||||
}
|
||||
}
|
||||
chargerUtilisateurs();
|
||||
```
|
||||
> 💬 *« Comment je sais que les clés s'appellent `name` et `email` ? »* J'ai d'abord ouvert `https://jsonplaceholder.typicode.com/users` dans le navigateur pour **voir** la forme des données. Fais toujours ça avant d'afficher.
|
||||
> 💬 *« Ça marche sur le web mais mon fichier local non ? »* Une API en ligne autorise les requêtes externes (CORS) ; un fichier local ouvert en `file://` non. D'où le serveur local pour tes propres fichiers.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Exo 8 — `FormData`
|
||||
|
||||
```js
|
||||
formulaire.addEventListener("submit", function (evenement) {
|
||||
evenement.preventDefault();
|
||||
const data = new FormData(formulaire);
|
||||
const valeurs = Object.fromEntries(data);
|
||||
console.log(valeurs); // { prenom: "...", email: "...", contacter: "..." }
|
||||
});
|
||||
```
|
||||
> 💬 *« `FormData` me renvoie du vide ! »* Vérifie que chaque champ a un attribut **`name`** (pas seulement `id`). `FormData` lit les champs par leur `name`.
|
||||
> 💬 *« C'est mieux que `getElementById` partout ? »* Pour lire **tout** un formulaire d'un coup, oui : une ligne au lieu d'un `getElementById` par champ. Pour un seul champ ponctuel, `getElementById` reste très bien.
|
||||
|
||||
---
|
||||
|
||||
### 🆘 Encore bloquée après la solution ?
|
||||
Note : *ce que tu attendais, ce qui se passe à la place, et ce qu'affiche la Console (F12)*. Avec ces 3 infos, on débloque vite.
|
||||
+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.
|
||||
@@ -0,0 +1,109 @@
|
||||
# 🧩 Tuto pas à pas — le parent qui additionne les likes des enfants
|
||||
|
||||
Tu cherches à faire **remonter** les likes : chaque `Carte` a son compteur, et `App` (le parent) affiche le **total**. C'est exactement le bon objectif 👍. Il te manque **une seule ligne** — et je veux surtout que tu comprennes **pourquoi**.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 D'abord, pourquoi ton total reste à 0
|
||||
|
||||
Regarde le bouton dans ta `Carte` :
|
||||
|
||||
```jsx
|
||||
<button onClick={() => setLikes(likes + 1)}>❤️ {likes}</button>
|
||||
```
|
||||
|
||||
Quand on clique, tu fais `setLikes(likes + 1)` → ça met à jour **le compteur de la carte**. ✅
|
||||
Mais… tu n'as **jamais** prévenu le parent ! La fonction `onLike` que `App` t'a donnée en prop n'est **jamais appelée**. Donc `App` ne sait pas qu'on a liké → `total` reste à 0.
|
||||
|
||||
> 🧠 **L'idée à comprendre :** un enfant **ne peut pas** modifier directement l'état du parent. Le parent lui **prête une fonction** (`onLike`), et l'enfant doit **l'appeler** au bon moment. C'est l'enfant qui dit « hé, on m'a likée ! » — le parent décide quoi en faire.
|
||||
|
||||
---
|
||||
|
||||
## 🪜 La correction, étape par étape
|
||||
|
||||
### Étape 1 — le parent a déjà tout (rien à changer)
|
||||
Dans `App.jsx`, tu as déjà :
|
||||
```jsx
|
||||
const [total, setTotal] = useState(0);
|
||||
// ...
|
||||
<Carte ... onLike={() => setTotal((t) => t + 1)} />
|
||||
// ...
|
||||
<p>Total de likes : {total}</p>
|
||||
```
|
||||
Le parent tient le `total` et passe une fonction `onLike` à **chaque** carte. ✅
|
||||
|
||||
### Étape 2 — l'enfant doit APPELER `onLike` (la ligne qui manque)
|
||||
Dans `Carte`, le bouton doit faire **deux choses** au clic : augmenter **son** compteur **et** prévenir le parent.
|
||||
|
||||
```jsx
|
||||
<button
|
||||
onClick={() => {
|
||||
setLikes(likes + 1); // 1) j'augmente MON compteur (état de la carte)
|
||||
props.onLike(); // 2) je préviens le PARENT (sa fonction)
|
||||
}}
|
||||
>
|
||||
❤️ {likes}
|
||||
</button>
|
||||
```
|
||||
|
||||
> 🔎 Deux instructions dans le `onClick` → on les met entre accolades `{ ... }` avec un `;` entre les deux. C'est une petite fonction qui fait deux choses à la suite.
|
||||
|
||||
**✅ Test :** clique sur le ❤️ d'une carte → **son** compteur monte **et** le « Total de likes » en haut monte aussi. Clique sur une autre carte → le total continue de grimper.
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Ta question : pourquoi chaque bouton démarre avec la **même** valeur ?
|
||||
|
||||
C'est une **excellente** question, et la réponse, c'est **l'initialisation** de `useState`.
|
||||
|
||||
```jsx
|
||||
const [likes, setLikes] = useState(
|
||||
() => Number(localStorage.getItem("likes-" + props.id)) || 0
|
||||
);
|
||||
```
|
||||
|
||||
### 1. La valeur dans `useState(...)` n'est qu'un **point de départ**
|
||||
L'argument que tu passes à `useState(...)` est la **valeur initiale** : React l'utilise **une seule fois**, au tout **premier affichage** de la carte, pour démarrer le compteur. Ensuite, c'est `setLikes(...)` qui prend le relais — l'initialisation **ne sert plus**.
|
||||
|
||||
> 🧠 `useState(valeurDeDépart)` = « démarre à cette valeur, **la première fois seulement** ». Après, l'état vit sa vie.
|
||||
|
||||
### 2. Chaque `<Carte>` est **indépendante** (elle ne partage rien)
|
||||
Chaque fois que tu écris `<Carte />`, React crée une **instance séparée**, avec son **propre** `likes`. Liker la carte n°1 ne touche pas la carte n°2 : ce sont des compteurs distincts.
|
||||
|
||||
### 3. Alors pourquoi elles affichent toutes la même valeur au départ ?
|
||||
Parce qu'elles exécutent toutes **le même calcul d'initialisation**, et qu'au **premier lancement**, `localStorage` est **vide** :
|
||||
|
||||
- `localStorage.getItem("likes-" + props.id)` → renvoie `null` (rien n'est encore stocké)
|
||||
- `Number(null)` → `0`, donc `|| 0` → **0**
|
||||
|
||||
→ **toutes** les cartes démarrent à **0**. Ce n'est pas qu'elles « partagent » une valeur : c'est qu'elles **partent toutes de la même situation** (rien en mémoire). C'est comme 4 personnes qui commencent un jeu : elles ont toutes 0 point au début, mais chacune marque ses propres points ensuite.
|
||||
|
||||
> 💡 **La preuve que c'est bien indépendant et par-carte :** like la carte n°1, **recharge la page (F5)**. La carte n°1 retrouve **sa** valeur (lue dans `localStorage` à la clé `likes-1`), les autres restent à 0. Chaque carte a donc bien **sa propre** initialisation, basée sur **son** `id`.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Un piège que tu as déjà senti (commit « buggy »)
|
||||
|
||||
Tu as remarqué : les likes par carte **survivent au rechargement** (ils sont dans `localStorage`), **mais le `total` repart à 0** après un F5. Normal : le `total` du parent n'est **pas** stocké, et il n'est pas **recalculé** à partir des cartes — il ne fait que s'incrémenter aux clics.
|
||||
|
||||
Pour l'instant, **c'est OK** : ton objectif était de faire **remonter** un like vers le parent (le pattern callback ci-dessus), et c'est réussi.
|
||||
> 🔭 Pour aller plus loin (plus tard) : il existe une version plus robuste où le **total se recalcule** tout seul à partir des likes (une seule « source de vérité »). On en reparlera — c'est l'étape d'après.
|
||||
|
||||
---
|
||||
|
||||
## 🧹 Petit nettoyage
|
||||
|
||||
Dans `Carte`, tu as un effet **vide** qui ne sert à rien :
|
||||
```jsx
|
||||
useEffect(() => {}, [likes]); // ❌ à supprimer
|
||||
```
|
||||
Enlève-le, il ne fait rien.
|
||||
|
||||
---
|
||||
|
||||
## ✅ Récap
|
||||
|
||||
- Le parent **prête** une fonction (`onLike`) ; l'enfant doit **l'appeler** → c'est la ligne `props.onLike()` qui te manquait.
|
||||
- Au clic : l'enfant met à jour **son** état **et** appelle `props.onLike()`.
|
||||
- `useState(x)` = valeur de départ, utilisée **une fois**. Chaque carte est **indépendante** ; elles démarrent toutes pareil parce qu'au premier lancement la mémoire est vide (toutes à 0).
|
||||
- Supprime le `useEffect` vide.
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
const zoneMessage = document.getElementById("contacter");
|
||||
const compteur = document.getElementById("restant");
|
||||
const motCaractere = document.getElementById("mot");
|
||||
const boutonEffacer = document.getElementById("effacer");
|
||||
const formulaire = document.querySelector("form");
|
||||
const badge = document.getElementById("statut_horaires");
|
||||
|
||||
const nom = document.getElementById("prenom");
|
||||
const MAX = 500;
|
||||
|
||||
function rafraichir_badge() {
|
||||
const heure = new Date().getHours();
|
||||
|
||||
if (heure >= 9 && heure < 18) {
|
||||
badge.textContent = "✅ Nous sommes ouverts (9h–18h)";
|
||||
badge.style.color = "green";
|
||||
} else {
|
||||
badge.textContent = "🔴 Fermé — écrivez-nous, on répond dès l'ouverture !";
|
||||
badge.style.color = "red";
|
||||
}
|
||||
}
|
||||
setInterval(rafraichir_badge, 60);
|
||||
|
||||
function rafraichirCompteur() {
|
||||
const restant = MAX - zoneMessage.value.length;
|
||||
compteur.textContent = restant;
|
||||
compteur.style.color = restant < 50 ? "red" : "black";
|
||||
motCaractere.textContent = restant <= 1 ? "caractère" : "caractères";
|
||||
localStorage.setItem("brouillon", zoneMessage.value);
|
||||
}
|
||||
|
||||
zoneMessage.addEventListener("input", rafraichirCompteur);
|
||||
|
||||
boutonEffacer.addEventListener("click", function () {
|
||||
zoneMessage.value = "";
|
||||
rafraichirCompteur();
|
||||
});
|
||||
|
||||
formulaire.addEventListener("submit", function (evenement) {
|
||||
evenement.preventDefault();
|
||||
if (zoneMessage.value.trim() === "") {
|
||||
alert("Merci d'écrire un message avant d'envoyer !");
|
||||
} else {
|
||||
const greeting = `merci ${nom.value}, ton message de ${zoneMessage.value.length} caractères a été pris en compte`;
|
||||
alert(`${greeting}`);
|
||||
}
|
||||
});
|
||||
|
||||
function creerNavbar(pageActive) {
|
||||
return `
|
||||
<div id="nav_barre" class="shadow">
|
||||
<h1>FleetZen</h1>
|
||||
<div id="buttons">
|
||||
<button class="${pageActive === "contact" ? "instyled" : "styled"}">contact</button>
|
||||
<a href="services.html"><button class="${pageActive === "services" ? "instyled" : "styled"}">services</button></a>
|
||||
<a href="../index.html"><button class="styled"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-corner-down-left-icon lucide-corner-down-left"><path d="M20 4v7a4 4 0 0 1-4 4H4"/><path d="m9 10-5 5 5 5"/></svg>retour</button></a>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
document.body.insertAdjacentHTML("afterbegin", creerNavbar("contact"));
|
||||
|
||||
document.getElementById("theme").addEventListener("click", function () {
|
||||
document.body.classList.toggle("sombre");
|
||||
});
|
||||
|
||||
const titres = document.querySelectorAll(".q-titre");
|
||||
|
||||
titres.forEach(function (titre) {
|
||||
titre.addEventListener("click", function () {
|
||||
const reponse = titre.nextElementSibling;
|
||||
reponse.classList.toggle("ouverte");
|
||||
});
|
||||
});
|
||||
|
||||
const horloge = document.getElementById("horloge");
|
||||
|
||||
function afficherHeure() {
|
||||
const maintenant = new Date();
|
||||
horloge.textContent = maintenant.toLocaleTimeString();
|
||||
}
|
||||
|
||||
afficherHeure();
|
||||
setInterval(afficherHeure, 1000);
|
||||
|
||||
zoneMessage.addEventListener("keydown", function (event) {
|
||||
if (event.key === "Enter" && event.ctrlKey) {
|
||||
formulaire.requestSubmit();
|
||||
}
|
||||
preventDefault();
|
||||
if (event.key === "Escape") {
|
||||
zoneMessage.value = "";
|
||||
rafraichirCompteur();
|
||||
}
|
||||
});
|
||||
+3
-1
@@ -42,6 +42,7 @@ h1 {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-top: 2px solid;
|
||||
}
|
||||
#nav_barre {
|
||||
display: flex;
|
||||
@@ -58,10 +59,11 @@ h1 {
|
||||
width: 130px;
|
||||
line-height: 2.5;
|
||||
padding: 0 20px;
|
||||
box-shadow: 3px 5px 3px darkslategray;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
color: white;
|
||||
text-shadow: 1px 1px 1px black;
|
||||
text-shadow: 1px 1px 1px slategray;
|
||||
border-radius: 10px;
|
||||
background-color: royalblue;
|
||||
background-image: linear-gradient(
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
const equipe = [
|
||||
{ nom: "Mélissa", job: "Stagiaire", partie: "Page contact" },
|
||||
{ nom: "Guillaume", job: "Stagiaire", partie: "Page services" },
|
||||
{ nom: "Matthieu", job: "Tuteur", partie: "Relecture" },
|
||||
];
|
||||
|
||||
for (const membre of equipe) {
|
||||
console.log(membre.nom + " — " + membre.job + " — " + membre.partie);
|
||||
}
|
||||
|
||||
function creerLigne(membre) {
|
||||
const tr = document.createElement("tr");
|
||||
|
||||
const tdNom = document.createElement("td");
|
||||
tdNom.textContent = membre.nom;
|
||||
|
||||
const tdJob = document.createElement("td");
|
||||
tdJob.textContent = membre.job;
|
||||
|
||||
const tdPartie = document.createElement("td");
|
||||
tdPartie.textContent = membre.partie;
|
||||
|
||||
tr.append(tdNom, tdJob, tdPartie);
|
||||
return tr;
|
||||
}
|
||||
|
||||
const corps = document.getElementById("corps_equipe");
|
||||
|
||||
function afficherEquipe(liste) {
|
||||
corps.innerHTML = "";
|
||||
for (const membre of liste) {
|
||||
corps.append(creerLigne(membre));
|
||||
}
|
||||
}
|
||||
afficherEquipe(equipe);
|
||||
|
||||
document.getElementById("nb_membres").textContent = equipe.length;
|
||||
|
||||
const champRecherche = document.getElementById("recherche");
|
||||
|
||||
champRecherche.addEventListener("input", function () {
|
||||
const motCherche = champRecherche.value.toLowerCase();
|
||||
|
||||
// on garde seulement les membres dont le nom contient le texte tapé
|
||||
const resultats = equipe.filter(function (membre) {
|
||||
return membre.nom.toLowerCase().includes(motCherche);
|
||||
});
|
||||
|
||||
afficherEquipe(resultats); // on ré-affiche, mais filtré
|
||||
});
|
||||
|
||||
document.getElementById("trier").addEventListener("click", function () {
|
||||
const triee = [...equipe].sort(function (a, b) {
|
||||
return a.nom.localeCompare(b.nom);
|
||||
});
|
||||
afficherEquipe(triee);
|
||||
});
|
||||
|
||||
const array = equipe;
|
||||
const mapped = array.map((x) => x.nom);
|
||||
console.log(mapped);
|
||||
@@ -0,0 +1,21 @@
|
||||
async function chargerFlotte() {
|
||||
const message = document.getElementById("message_flotte");
|
||||
const corps = document.getElementById("corps_flotte");
|
||||
|
||||
try {
|
||||
const reponse = await fetch("../vehicules.json");
|
||||
const vehicules = await reponse.json();
|
||||
|
||||
message.textContent = vehicules.length + " véhicules :";
|
||||
for (const v of vehicules) {
|
||||
const tr = document.createElement("tr");
|
||||
tr.textContent = `${v.modele} — ${v.immatriculation} — ${v.km} km`;
|
||||
corps.append(tr);
|
||||
}
|
||||
} catch (erreur) {
|
||||
message.textContent = "Impossible de charger la flotte";
|
||||
console.error(erreur);
|
||||
}
|
||||
}
|
||||
|
||||
chargerFlotte();
|
||||
@@ -0,0 +1 @@
|
||||
https://pietrafocirestaurant.com/wp-content/uploads/2025/10/PF2025SepUnique.pdf
|
||||
@@ -0,0 +1,193 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: lightblue;
|
||||
margin: 0;
|
||||
padding-bottom: 65px;
|
||||
}
|
||||
|
||||
#nav_barre {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
background-color: steelblue;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.styled {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
line-height: normal;
|
||||
border: 0;
|
||||
height: 70px;
|
||||
width: 130px;
|
||||
padding: 0 20px;
|
||||
box-shadow: 3px 5px 3px darkslategray;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
color: white;
|
||||
text-shadow: 1px 1px 1px slategray;
|
||||
border-radius: 10px;
|
||||
background-color: royalblue;
|
||||
background-image: linear-gradient(
|
||||
to top left,
|
||||
rgb(0 0 0 / 0.2),
|
||||
rgb(0 0 0 / 0.2) 30%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.styled:hover {
|
||||
background-color: midnightblue;
|
||||
}
|
||||
|
||||
.styled:active {
|
||||
box-shadow:
|
||||
inset -2px -2px 3px rgb(255 255 255 / 0.6),
|
||||
inset 2px 2px 3px rgb(0 0 0 / 0.6);
|
||||
}
|
||||
|
||||
#buttons {
|
||||
height: 100%;
|
||||
width: 25%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-direction: row-reverse;
|
||||
margin-left: auto;
|
||||
align-items: center;
|
||||
}
|
||||
.shadow {
|
||||
box-shadow: 0px 10px 5px gray;
|
||||
}
|
||||
h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.instyled {
|
||||
border: 0;
|
||||
height: 70px;
|
||||
width: 130px;
|
||||
line-height: 2.5;
|
||||
padding: 0 20px;
|
||||
box-shadow:
|
||||
inset -2px -2px 3px rgb(255 255 255 / 0.6),
|
||||
inset 2px 2px 3px rgb(0 0 0 / 0.6);
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
color: lightgrey;
|
||||
text-shadow: 1px 1px 1px darkslategrey;
|
||||
border-radius: 10px;
|
||||
background-color: darkblue;
|
||||
background-image: linear-gradient(
|
||||
to top left,
|
||||
rgb(0 0 0 / 0.2),
|
||||
rgb(0 0 0 / 0.2) 30%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 700px;
|
||||
height: 300px;
|
||||
border-collapse: collapse;
|
||||
font-size: 35px;
|
||||
table-layout: auto;
|
||||
border: 5px solid darkblue;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: skyblue;
|
||||
box-shadow: 2px 5px 5px darkgray;
|
||||
background-image: linear-gradient(
|
||||
to top left,
|
||||
rgb(90 100 150),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
thead {
|
||||
background-color: rgb(8, 105, 85);
|
||||
text-shadow: 2px 2px 2px slategray;
|
||||
color: lightblue;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
border: 2px solid dimgray;
|
||||
padding: 10px;
|
||||
}
|
||||
#titre_tableau {
|
||||
font-size: 25px;
|
||||
color: seagreen;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
width: 570px;
|
||||
height: 380px;
|
||||
border: 3px solid green;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 100px;
|
||||
background-color: rgb(159, 199, 159);
|
||||
background-image: linear-gradient(
|
||||
to top left,
|
||||
rgb(0 0 0 / 0.001),
|
||||
rgb(50 90 50 / 0.8) 0.1%,
|
||||
transparent
|
||||
);
|
||||
box-shadow: 2px 5px 3px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
width: 527px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 10px;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
background-color: cadetblue;
|
||||
font-size: 25px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-top: 2px solid;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
#statut_horaires {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
body.sombre {
|
||||
background-color: #1d2733;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.q-reponse {
|
||||
display: none;
|
||||
}
|
||||
.q-reponse.ouverte {
|
||||
display: block;
|
||||
}
|
||||
.Faq {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 30 px;
|
||||
}
|
||||
|
||||
#horloge {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="contact.css" />
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width", initial-scame="1"/>
|
||||
<title>Contact-FleetZen</title>
|
||||
<html lang="fr">
|
||||
</head>
|
||||
<body>
|
||||
<p id= titre_tableau>
|
||||
<b>Les développeurs de ce site</b>
|
||||
</p>
|
||||
<p>L'équipe compte <span id="nb_membres"></span> personne(s).</p>
|
||||
<input type="search" id="recherche" placeholder="Rechercher un membre…">
|
||||
<button type="button" id="trier">Trier A → Z</button>
|
||||
|
||||
<table border=1 class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>Job</th>
|
||||
<th>Parties crées</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="corps_equipe">
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="form">
|
||||
<p><b>Nous contacter</b></p>
|
||||
<p id="statut_horaires"></p>
|
||||
<form method="post" action="">
|
||||
<p>
|
||||
<label for="prenom">Votre prénom :</label>
|
||||
<input type="text" name="prenom" id="prenom" placeholder="Ex. : Matthieu" size="30" maxlength="20">
|
||||
</p>
|
||||
<p>
|
||||
<label for="email">Votre e-mail:</label>
|
||||
<input type="email" name="email" id="email" placeholder="Ex. : stage2@echoes.solutions" size="50" maxlength="40">
|
||||
</p>
|
||||
<div>
|
||||
<label for="contacter">Des questions a nous poser?</label><br>
|
||||
<textarea name="contacter" id="contacter" rows="6" cols="40" maxlength="500"></textarea>
|
||||
<p class=small>Il vous reste <span id="restant">500</span> <span id="mot">caractères</span>.</p>
|
||||
</div>
|
||||
<p>
|
||||
<button type="submit">Envoyer</button>
|
||||
<button type="button" id="effacer">Effacer</button>
|
||||
</p>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="Faq">
|
||||
<section class="faq">
|
||||
<h3>Questions fréquentes</h3>
|
||||
<div class="question">
|
||||
<button class="q-titre"type="button" >Comment suivre ma flotte en temps réel ?</button>
|
||||
<p class="q-reponse">Grâce à notre service de géolocalisation, accessible 24h/24.</p>
|
||||
</div>
|
||||
<div class="question">
|
||||
<button class="q-titre"type="button">Puis-je résilier à tout moment ?</button>
|
||||
<p class="q-reponse">Oui, sans frais, depuis votre espace client.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<h3>Notre flotte</h3>
|
||||
<p id="message_flotte">Chargement…</p>
|
||||
<table border=1>
|
||||
<tbody id="corps_flotte">
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<footer class="footer">
|
||||
<button type="button" id="theme"> Mode sombre</button>
|
||||
<b>stagiaires rpz echoes</b>
|
||||
<span id="horloge"></span>
|
||||
</footer>
|
||||
<script src="../equipe.js"></script>
|
||||
<script src="../contact.js"></script>
|
||||
<script src="../flotte.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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>react-project</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": "react-project",
|
||||
"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,42 @@
|
||||
[
|
||||
{
|
||||
"id": "1",
|
||||
"titre": "Oracle of ages",
|
||||
"plateforme": "Game Boy Color",
|
||||
"annee": "2001",
|
||||
"Type": "2d",
|
||||
"like": 0
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"titre": "Breath of the Wild",
|
||||
"plateforme": "Switch",
|
||||
"annee": "2017",
|
||||
"Type": "3d",
|
||||
"like": 0
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"titre": "Majora's Mask",
|
||||
"plateforme": "N64",
|
||||
"annee": "2000",
|
||||
"Type": "3d",
|
||||
"like": 0
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"titre": "Tears of the kingdom",
|
||||
"plateforme": "Switch",
|
||||
"annee": "2023",
|
||||
"Type": "3d",
|
||||
"like": 0
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"titre": "Echoes of Wisdom",
|
||||
"plateforme": "Switch",
|
||||
"annee": "2024",
|
||||
"Type": "3d",
|
||||
"like": 0
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,187 @@
|
||||
.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: 48px;
|
||||
@media (max-width: 1024px) {
|
||||
height: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.smallgap {
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import Header from "./components/Header";
|
||||
import Footer from "./components/Footer";
|
||||
import Accueil from "./pages/Accueil";
|
||||
import Detail from "./pages/Detail";
|
||||
import NotFound from "./pages/NotFound";
|
||||
|
||||
import "./App.css";
|
||||
|
||||
function App() {
|
||||
const [jeux, setJeux] = useState(() => {
|
||||
const stocke = localStorage.getItem("jeux");
|
||||
return stocke ? JSON.parse(stocke) : null;
|
||||
});
|
||||
|
||||
const [likes, setLikes] = useState(() =>
|
||||
JSON.parse(localStorage.getItem("likes") || "{}"),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (jeux === null) {
|
||||
fetch("/zelda.json")
|
||||
.then((r) => r.json())
|
||||
.then(setJeux);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (jeux !== null) {
|
||||
localStorage.setItem("jeux", JSON.stringify(jeux));
|
||||
}
|
||||
}, [jeux]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("likes", JSON.stringify(likes));
|
||||
}, [likes]);
|
||||
|
||||
function liker(id) {
|
||||
setLikes((prev) => ({ ...prev, [id]: (prev[id] || 0) + 1 }));
|
||||
}
|
||||
|
||||
if (jeux === null) return <p>Chargement…</p>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<Accueil
|
||||
jeux={jeux}
|
||||
setJeux={setJeux}
|
||||
likes={likes}
|
||||
onLike={liker}
|
||||
setLikes={setLikes}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route path="/jeu/:id" element={<Detail jeux={jeux} likes={likes} />} />
|
||||
<Route path="*" element={<NotFound />} />
|
||||
</Routes>
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
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,6 @@
|
||||
function Badge({ Type }) {
|
||||
if (Type === "2d") return "Jeu 2D";
|
||||
return <span className="badge"> Jeu 3D </span>;
|
||||
}
|
||||
|
||||
export default Badge;
|
||||
@@ -0,0 +1,12 @@
|
||||
function BarreRecherche({ valeur, onChange }) {
|
||||
return (
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search Game…"
|
||||
value={valeur}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default BarreRecherche;
|
||||
@@ -0,0 +1,19 @@
|
||||
import Badge from "./Badge";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
function Carte(props) {
|
||||
return (
|
||||
<div className="carte">
|
||||
<h2>{props.titre}</h2>
|
||||
<p>
|
||||
{props.plateforme} - {props.annee} - <Badge Type={props.Type} />
|
||||
</p>
|
||||
<Link to={`/jeu/${props.gameId}`}>Voir les détails</Link>
|
||||
<button onClick={props.onLike}>❤️ {props.likes}</button>
|
||||
<button onClick={() => props.onDelete()}>🗑️</button>
|
||||
<section className="smallgap"></section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Carte;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useState } from "react";
|
||||
|
||||
function Compteur() {
|
||||
const [nombre, setNombre] = useState(0);
|
||||
return (
|
||||
<div>
|
||||
<p> Vehicules : {nombre}</p>
|
||||
<button onClick={() => setNombre(nombre + 1)}>+</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Compteur;
|
||||
@@ -0,0 +1,9 @@
|
||||
function Footer() {
|
||||
return (
|
||||
<div id="foot">
|
||||
<h3>The Zelda project</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
@@ -0,0 +1,14 @@
|
||||
function Header() {
|
||||
return (
|
||||
<section id="center">
|
||||
<div>
|
||||
<h1>The Zelda project</h1>
|
||||
<p>
|
||||
<img src="https://www.greenmangaming.com/blog/wp-content/uploads/2021/06/zelda-timeline.jpg"></img>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default Header;
|
||||
@@ -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,147 @@
|
||||
import Carte from "../components/Carte";
|
||||
import BarreRecherche from "../components/BarreRecherche";
|
||||
import { useState } from "react";
|
||||
|
||||
function Accueil({ jeux, setJeux, likes, setLikes, onLike }) {
|
||||
const [recherche, setRecherche] = useState("");
|
||||
const [titre, setTitre] = useState("");
|
||||
const [annee, setAnnee] = useState("");
|
||||
const [plateforme, setPlateforme] = useState("");
|
||||
const [tri, setTri] = useState("titre");
|
||||
const medailles = ["🥇", "🥈", "🥉"];
|
||||
const [filtreType, setFiltreType] = useState("tous");
|
||||
const nbJeux = jeux.length;
|
||||
const totalLikes = Object.values(likes).reduce((s, n) => s + n, 0);
|
||||
const anneeMoyenne = nbJeux
|
||||
? Math.round(jeux.reduce((s, j) => s + Number(j.annee), 0) / nbJeux)
|
||||
: 0;
|
||||
const [ordre, setOrdre] = useState("asc");
|
||||
|
||||
function supprimer(id) {
|
||||
setJeux(jeux.filter((j) => j.id !== id));
|
||||
setLikes((prev) => {
|
||||
const copie = { ...prev }; // une COPIE (on ne modifie jamais l'ancien objet)
|
||||
delete copie[id]; // on retire l'entrée de ce jeu
|
||||
return copie; // on renvoie le nouvel objet
|
||||
});
|
||||
}
|
||||
|
||||
const top = [...jeux]
|
||||
.sort((a, b) => (likes[b.id] || 0) - (likes[a.id] || 0))
|
||||
.slice(0, 3);
|
||||
|
||||
function ajouter(e) {
|
||||
e.preventDefault();
|
||||
if (titre.trim() === "") return;
|
||||
const nouveau = {
|
||||
id: Date.now(),
|
||||
titre,
|
||||
annee: Number(annee),
|
||||
plateforme,
|
||||
Type: "3d",
|
||||
};
|
||||
setJeux([...jeux, nouveau]);
|
||||
setTitre("");
|
||||
setAnnee("");
|
||||
setPlateforme("");
|
||||
}
|
||||
|
||||
let affiches = [...jeux]
|
||||
.filter((j) => j.titre.toLowerCase().includes(recherche.toLowerCase()))
|
||||
.filter((j) => filtreType === "tous" || j.Type === filtreType)
|
||||
.sort((a, b) => {
|
||||
if (tri === "titre") return a.titre.localeCompare(b.titre);
|
||||
if (tri === "annee") return a.annee - b.annee;
|
||||
return 0;
|
||||
});
|
||||
if (ordre === "desc") affiches.reverse();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<BarreRecherche
|
||||
valeur={recherche}
|
||||
onChange={(e) => setRecherche(e.target.value)}
|
||||
/>
|
||||
<select value={tri} onChange={(e) => setTri(e.target.value)}>
|
||||
<option value="titre">Titre (A→Z)</option>
|
||||
<option value="annee">Année</option>
|
||||
</select>
|
||||
<button onClick={() => setFiltreType("tous")}>Tous</button>
|
||||
<button onClick={() => setFiltreType("2d")}>2D</button>
|
||||
<button onClick={() => setFiltreType("3d")}>3D</button>
|
||||
<button onClick={() => setOrdre(ordre === "asc" ? "desc" : "asc")}>
|
||||
{ordre === "asc" ? "A→Z" : "Z→A"}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{affiches.length === 0 && <p>Aucun jeu trouvé 😕</p>}
|
||||
|
||||
<section className="spacer"></section>
|
||||
|
||||
<div className="hall">
|
||||
{top.map((j, index) => (
|
||||
<p key={j.id}>
|
||||
{medailles[index]} {j.titre} — {likes[j.id] || 0} ❤️
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<section className="spacer"></section>
|
||||
|
||||
{affiches.map((jeu) => (
|
||||
<Carte
|
||||
key={jeu.id}
|
||||
gameId={jeu.id}
|
||||
titre={jeu.titre}
|
||||
plateforme={jeu.plateforme}
|
||||
annee={jeu.annee}
|
||||
Type={jeu.Type}
|
||||
likes={likes[jeu.id] || 0}
|
||||
onLike={() => onLike(jeu.id)}
|
||||
onDelete={() => supprimer(jeu.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
<section className="spacer"></section>
|
||||
|
||||
<p>
|
||||
{nbJeux} jeux · {totalLikes} likes · année moyenne {anneeMoyenne}
|
||||
</p>
|
||||
|
||||
<section className="spacer"></section>
|
||||
|
||||
<form onSubmit={ajouter}>
|
||||
<input
|
||||
value={titre}
|
||||
onChange={(e) => setTitre(e.target.value)}
|
||||
placeholder="Titre du jeu"
|
||||
/>
|
||||
<input
|
||||
value={annee}
|
||||
onChange={(e) => setAnnee(e.target.value)}
|
||||
placeholder="Année"
|
||||
/>
|
||||
<input
|
||||
value={plateforme}
|
||||
onChange={(e) => setPlateforme(e.target.value)}
|
||||
placeholder="Console"
|
||||
/>
|
||||
<button type="submit">Ajouter</button>
|
||||
<p>
|
||||
<button
|
||||
onClick={() => {
|
||||
localStorage.removeItem("jeux");
|
||||
location.reload();
|
||||
}}
|
||||
>
|
||||
♻️ Réinitialiser les jeux
|
||||
</button>
|
||||
</p>
|
||||
<section className="spacer"></section>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Accueil;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useParams, Link } from "react-router-dom";
|
||||
|
||||
function Detail({ jeux, likes }) {
|
||||
const { id } = useParams();
|
||||
const jeu = jeux.find((j) => String(j.id) === id);
|
||||
if (!jeu)
|
||||
return (
|
||||
<p>
|
||||
Introuvable — <Link to="/">retour</Link>
|
||||
</p>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="detail">
|
||||
<Link to="/">← Retour</Link>
|
||||
<h1>{jeu.titre}</h1>
|
||||
<p>
|
||||
{jeu.plateforme} — {jeu.annee} — {jeu.Type}
|
||||
</p>
|
||||
<p>❤️ Likes : {likes[jeu.id] || 0}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Detail;
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Link } from "react-router-dom";
|
||||
function NotFound() {
|
||||
return (
|
||||
<div>
|
||||
<h1>404 — page introuvable</h1>
|
||||
<Link to="/">Retour à l'accueil</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default NotFound;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{ "modele": "Renault Kangoo", "immatriculation": "AB-123-CD", "km": 45200 },
|
||||
{ "modele": "Peugeot Partner", "immatriculation": "EF-456-GH", "km": 78900 },
|
||||
{ "modele": "Citroën Berlingo", "immatriculation": "IJ-789-KL", "km": 12300 }
|
||||
]
|
||||
Reference in New Issue
Block a user