Compare commits
27
Commits
main
...
f250c06331
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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,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,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,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.
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
const zoneMessage = document.getElementById("contacter");
|
||||
const compteur = document.getElementById("restant");
|
||||
const motCaractere = document.getElementById("mot");
|
||||
const boutonEffacer = document.getElementById("effacer");
|
||||
const formulaire = document.querySelector("form");
|
||||
const badge = document.getElementById("statut_horaires");
|
||||
const nom = document.getElementById("prenom");
|
||||
const heure = new Date().getHours();
|
||||
const MAX = 500;
|
||||
|
||||
if (heure >= 9 && heure < 18) {
|
||||
badge.textContent = "✅ Nous sommes ouverts (9h–18h)";
|
||||
badge.style.color = "green";
|
||||
} else {
|
||||
badge.textContent = "🔴 Fermé — écrivez-nous, on répond dès l'ouverture !";
|
||||
badge.style.color = "red";
|
||||
}
|
||||
|
||||
function rafraichirCompteur() {
|
||||
const restant = MAX - zoneMessage.value.length;
|
||||
compteur.textContent = restant;
|
||||
compteur.style.color = restant < 50 ? "red" : "black";
|
||||
motCaractere.textContent = restant <= 1 ? "caractère" : "caractères";
|
||||
localStorage.setItem("brouillon", zoneMessage.value);
|
||||
}
|
||||
|
||||
zoneMessage.addEventListener("input", rafraichirCompteur);
|
||||
|
||||
boutonEffacer.addEventListener("click", function () {
|
||||
zoneMessage.value = "";
|
||||
rafraichirCompteur();
|
||||
});
|
||||
|
||||
formulaire.addEventListener("submit", function (evenement) {
|
||||
if (zoneMessage.value.trim() === "") {
|
||||
evenement.preventDefault();
|
||||
alert("Merci d'écrire un message avant d'envoyer !");
|
||||
} else {
|
||||
const greeting = `merci ${nom.value}, ton message de ${zoneMessage.value.length} caractères a été pris en compte`;
|
||||
alert(`${greeting}`);
|
||||
}
|
||||
});
|
||||
|
||||
function creerNavbar(pageActive) {
|
||||
return `
|
||||
<div id="nav_barre" class="shadow">
|
||||
<h1>FleetZen</h1>
|
||||
<div id="buttons">
|
||||
<button class="${pageActive === "contact" ? "instyled" : "styled"}">contact</button>
|
||||
<a href="services.html"><button class="${pageActive === "services" ? "instyled" : "styled"}">services</button></a>
|
||||
<a href="../index.html"><button class="styled"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-corner-down-left-icon lucide-corner-down-left"><path d="M20 4v7a4 4 0 0 1-4 4H4"/><path d="m9 10-5 5 5 5"/></svg>retour</button></a>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
document.body.insertAdjacentHTML("afterbegin", creerNavbar("contact"));
|
||||
|
||||
document.getElementById("theme").addEventListener("click", function () {
|
||||
document.body.classList.toggle("sombre");
|
||||
});
|
||||
|
||||
const titres = document.querySelectorAll(".q-titre");
|
||||
|
||||
titres.forEach(function (titre) {
|
||||
titre.addEventListener("click", function () {
|
||||
const reponse = titre.nextElementSibling;
|
||||
reponse.classList.toggle("ouverte");
|
||||
});
|
||||
});
|
||||
+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,57 @@
|
||||
const equipe = [
|
||||
{ nom: "Mélissa", job: "Stagiaire", partie: "Page contact" },
|
||||
{ nom: "Guillaume", job: "Stagiaire", partie: "Page services" },
|
||||
{ nom: "Matthieu", job: "Tuteur", partie: "Relecture" },
|
||||
];
|
||||
|
||||
for (const membre of equipe) {
|
||||
console.log(membre.nom + " — " + membre.job + " — " + membre.partie);
|
||||
}
|
||||
|
||||
function creerLigne(membre) {
|
||||
const tr = document.createElement("tr");
|
||||
|
||||
const tdNom = document.createElement("td");
|
||||
tdNom.textContent = membre.nom;
|
||||
|
||||
const tdJob = document.createElement("td");
|
||||
tdJob.textContent = membre.job;
|
||||
|
||||
const tdPartie = document.createElement("td");
|
||||
tdPartie.textContent = membre.partie;
|
||||
|
||||
tr.append(tdNom, tdJob, tdPartie);
|
||||
return tr;
|
||||
}
|
||||
|
||||
const corps = document.getElementById("corps_equipe");
|
||||
|
||||
function afficherEquipe(liste) {
|
||||
corps.innerHTML = "";
|
||||
for (const membre of liste) {
|
||||
corps.append(creerLigne(membre));
|
||||
}
|
||||
}
|
||||
afficherEquipe(equipe);
|
||||
|
||||
document.getElementById("nb_membres").textContent = equipe.length;
|
||||
|
||||
const champRecherche = document.getElementById("recherche");
|
||||
|
||||
champRecherche.addEventListener("input", function () {
|
||||
const motCherche = champRecherche.value.toLowerCase();
|
||||
|
||||
// on garde seulement les membres dont le nom contient le texte tapé
|
||||
const resultats = equipe.filter(function (membre) {
|
||||
return membre.nom.toLowerCase().includes(motCherche);
|
||||
});
|
||||
|
||||
afficherEquipe(resultats); // on ré-affiche, mais filtré
|
||||
});
|
||||
|
||||
document.getElementById("trier").addEventListener("click", function () {
|
||||
const triee = [...equipe].sort(function (a, b) {
|
||||
return a.nom.localeCompare(b.nom);
|
||||
});
|
||||
afficherEquipe(triee);
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
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();
|
||||
@@ -0,0 +1,189 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: lightblue;
|
||||
margin: 0;
|
||||
padding-bottom: 65px;
|
||||
}
|
||||
|
||||
#nav_barre {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
background-color: steelblue;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.styled {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
line-height: normal;
|
||||
border: 0;
|
||||
height: 70px;
|
||||
width: 130px;
|
||||
padding: 0 20px;
|
||||
box-shadow: 3px 5px 3px darkslategray;
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
color: white;
|
||||
text-shadow: 1px 1px 1px slategray;
|
||||
border-radius: 10px;
|
||||
background-color: royalblue;
|
||||
background-image: linear-gradient(
|
||||
to top left,
|
||||
rgb(0 0 0 / 0.2),
|
||||
rgb(0 0 0 / 0.2) 30%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.styled:hover {
|
||||
background-color: midnightblue;
|
||||
}
|
||||
|
||||
.styled:active {
|
||||
box-shadow:
|
||||
inset -2px -2px 3px rgb(255 255 255 / 0.6),
|
||||
inset 2px 2px 3px rgb(0 0 0 / 0.6);
|
||||
}
|
||||
|
||||
#buttons {
|
||||
height: 100%;
|
||||
width: 25%;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-direction: row-reverse;
|
||||
margin-left: auto;
|
||||
align-items: center;
|
||||
}
|
||||
.shadow {
|
||||
box-shadow: 0px 10px 5px gray;
|
||||
}
|
||||
h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.instyled {
|
||||
border: 0;
|
||||
height: 70px;
|
||||
width: 130px;
|
||||
line-height: 2.5;
|
||||
padding: 0 20px;
|
||||
box-shadow:
|
||||
inset -2px -2px 3px rgb(255 255 255 / 0.6),
|
||||
inset 2px 2px 3px rgb(0 0 0 / 0.6);
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
color: lightgrey;
|
||||
text-shadow: 1px 1px 1px darkslategrey;
|
||||
border-radius: 10px;
|
||||
background-color: darkblue;
|
||||
background-image: linear-gradient(
|
||||
to top left,
|
||||
rgb(0 0 0 / 0.2),
|
||||
rgb(0 0 0 / 0.2) 30%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 700px;
|
||||
height: 300px;
|
||||
border-collapse: collapse;
|
||||
font-size: 35px;
|
||||
table-layout: auto;
|
||||
border: 5px solid darkblue;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
background-color: skyblue;
|
||||
box-shadow: 2px 5px 5px darkgray;
|
||||
background-image: linear-gradient(
|
||||
to top left,
|
||||
rgb(90 100 150),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
thead {
|
||||
background-color: rgb(8, 105, 85);
|
||||
text-shadow: 2px 2px 2px slategray;
|
||||
color: lightblue;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
border: 2px solid dimgray;
|
||||
padding: 10px;
|
||||
}
|
||||
#titre_tableau {
|
||||
font-size: 25px;
|
||||
color: seagreen;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
width: 570px;
|
||||
height: 380px;
|
||||
border: 3px solid green;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 100px;
|
||||
background-color: rgb(159, 199, 159);
|
||||
background-image: linear-gradient(
|
||||
to top left,
|
||||
rgb(0 0 0 / 0.001),
|
||||
rgb(0 0 0 / 0.1) 30%,
|
||||
transparent
|
||||
);
|
||||
box-shadow: 2px 5px 3px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
width: 527px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 10px;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
background-color: cadetblue;
|
||||
font-size: 25px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-top: 2px solid;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
#statut_horaires {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
body.sombre {
|
||||
background-color: #1d2733;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.q-reponse {
|
||||
display: none;
|
||||
}
|
||||
.q-reponse.ouverte {
|
||||
display: block;
|
||||
}
|
||||
.Faq {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 30 px;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<link rel="stylesheet" href="contact.css" />
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width", initial-scame="1"/>
|
||||
<title>Contact-FleetZen</title>
|
||||
<html lang="fr">
|
||||
</head>
|
||||
<body>
|
||||
<p id= titre_tableau>
|
||||
<b>Les développeurs de ce site</b>
|
||||
</p>
|
||||
<p>L'équipe compte <span id="nb_membres"></span> personne(s).</p>
|
||||
<input type="search" id="recherche" placeholder="Rechercher un membre…">
|
||||
<button type="button" id="trier">Trier A → Z</button>
|
||||
|
||||
<table border=1 class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nom</th>
|
||||
<th>Job</th>
|
||||
<th>Parties crées</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="corps_equipe">
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="form">
|
||||
<p><b>Nous contacter</b></p>
|
||||
<p id="statut_horaires"></p>
|
||||
<form method="post" action="">
|
||||
<p>
|
||||
<label for="prenom">Votre prénom :</label>
|
||||
<input type="text" name="prenom" id="prenom" placeholder="Ex. : Matthieu" size="30" maxlength="20">
|
||||
</p>
|
||||
<p>
|
||||
<label for="e_mail">Votre e-mail:</label>
|
||||
<input type="email" name="email" id="email" placeholder="Ex. : stage2@echoes.solutions" size="50" maxlength="40">
|
||||
</p>
|
||||
<div>
|
||||
<label for="contacter">Des questions a nous poser?</label><br>
|
||||
<textarea name="contacter" id="contacter" rows="6" cols="40" maxlength="500"></textarea>
|
||||
<p class=small>Il vous reste <span id="restant">500</span> <span id="mot">caractères</span>.</p>
|
||||
</div>
|
||||
<p>
|
||||
<button>Envoyer</button>
|
||||
<button type="submit" id="effacer">Effacer</button>
|
||||
</p>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="Faq">
|
||||
<section class="faq">
|
||||
<h3>Questions fréquentes</h3>
|
||||
<div class="question">
|
||||
<button class="q-titre"type="button" >Comment suivre ma flotte en temps réel ?</button>
|
||||
<p class="q-reponse">Grâce à notre service de géolocalisation, accessible 24h/24.</p>
|
||||
</div>
|
||||
<div class="question">
|
||||
<button class="q-titre"type="button">Puis-je résilier à tout moment ?</button>
|
||||
<p class="q-reponse">Oui, sans frais, depuis votre espace client.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
stagiaires rpz echoes
|
||||
<button type="button" id="theme"> Mode sombre</button>
|
||||
</footer>
|
||||
<script src="../equipe.js"></script>
|
||||
<script src="../contact.js"></script>
|
||||
<script src="../flotte.js"</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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