ajout du md

This commit is contained in:
2026-04-01 22:17:05 +02:00
parent 20bdddd729
commit 21205fd2cc
4 changed files with 471 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
# Fichiers de présentation et documentation temporaire
fonctions_js_soutenance.md
# Autres fichiers courants à ignorer
node_modules/
*.log
.DS_Store
Thumbs.db
+49
View File
@@ -88,5 +88,54 @@ L'application utilise un système de routage manuel natif basé sur l'API `windo
* Affichage conditionnel des composants via les directives natives `if={ state.view === ... }`. * Affichage conditionnel des composants via les directives natives `if={ state.view === ... }`.
*** ***
## Cycle de vie de l'application
### Initialisation de la carte dans map-view.riot
```javascript
onMounted() {
this.map = L.map(element).setView([46.8, 2.5], 6) // crée la carte Leaflet
L.tileLayer('https://...').addTo(this.map) // ajoute les tuiles OpenStreetMap
}
```
**Étape 9 — L'application est prête, elle attend l'utilisateur**
À ce stade le navigateur affiche la page avec la barre de recherche, la carte vide centrée sur la France, et aucun résultat. Tout est en mémoire, prêt à réagir.
**Étape 10 — L'utilisateur interagit**
```
L'utilisateur tape "BUT informatique" et clique Rechercher
→ le navigateur déclenche l'événement onclick sur le bouton
→ Riot appelle submitSearch() dans search-bar.riot
→ qui appelle props.onsearch(requete, filtres)
→ qui appelle lancerRecherche() dans app.riot
→ qui appelle chargerPage(1)
→ qui appelle fetch() vers l'API
→ l'API retourne le JSON
→ app.riot fait this.update({ resultats: formations })
→ Riot détecte que le state a changé
→ Riot re-rend le HTML automatiquement
→ les cards de résultats apparaissent
→ map-view reçoit les nouvelles props
→ refreshMarkers() place les marqueurs
```
**Résumé de l'ordre d'exécution :**
```
1. index.html chargé
2. CSS téléchargés (style.css, leaflet.css, charts.css)
3. JS externes téléchargés (riot, leaflet)
4. Fichiers .riot téléchargés (pas exécutés)
5. Modules JS importés (api.js, formation.js, firebase.js, estimation.js, selection.js)
6. Firebase se connecte
7. Tout exposé sur window
8. riot.compile() → compile les .riot en JS
9. riot.mount('app') → monte le composant principal
10. onMounted() → charge localStorage, écoute Firebase, écoute hashchange
11. gererRoute() → lit le hash → affiche la vue recherche
12. Composants enfants montés (search-bar, map-view, result-list)
13. Application prête → attend les interactions utilisateur
```
***
*Réalisé par Aylane Sehl, Jenson Val et Séri-Khane Yolou à l'IUT Sénart-Fontainebleau (UPEC).* *Réalisé par Aylane Sehl, Jenson Val et Séri-Khane Yolou à l'IUT Sénart-Fontainebleau (UPEC).*
+47
View File
@@ -1,24 +1,59 @@
// Échapper les apostrophes dans les valeurs injectées dans la clause where // Échapper les apostrophes dans les valeurs injectées dans la clause where
// ---------------------------------------------------------------
// Cette fonction est utilisée pour prévenir les problèmes de syntaxe
// quand on insère une valeur dans une requête Parcoursup qui nécessite
// des guillemets simples.
function echapperValeur(valeur) { function echapperValeur(valeur) {
// force en string (undefined/null -> "undefined"/"null"), puis remplace toutes
// les apostrophes par une version échappée (\').
return String(valeur).replace(/'/g, "\\'"); return String(valeur).replace(/'/g, "\\'");
} }
// Construire l'URL de requête vers l'API Parcoursup // Construire l'URL de requête vers l'API Parcoursup
// --------------------------------------------------
// - filtre par recherche texte + plusieurs paramètres de filtre
// - limit / offset pour pagination
// - where encodé (via encodeURIComponent)
export function construireURL(requete, limite = 20, decalage = 0, filtres = {}) { export function construireURL(requete, limite = 20, decalage = 0, filtres = {}) {
var url = "https://data.enseignementsup-recherche.gouv.fr/api/explore/v2.1/catalog/datasets/fr-esr-parcoursup/records?"; var url = "https://data.enseignementsup-recherche.gouv.fr/api/explore/v2.1/catalog/datasets/fr-esr-parcoursup/records?";
// pagination simple
url += "limit=" + limite; url += "limit=" + limite;
url += "&offset=" + decalage; url += "&offset=" + decalage;
var conditions = []; var conditions = [];
// recherche libre
if (requete && requete.trim() !== "") { if (requete && requete.trim() !== "") {
conditions.push("search(lib_for_voe_ins, '" + echapperValeur(requete.trim()) + "')"); conditions.push("search(lib_for_voe_ins, '" + echapperValeur(requete.trim()) + "')");
} }
// ===== FILTRE FILIÈRE =====
// Ce bloc filtre les formations par type (BTS, BUT, Licence, CPGE, etc.)
// Il est OPTIONNEL : si l'user ne choisit pas de filière, on le saute.
// CONDITION : "Si l'user a choisi une filière ET qu'elle n'est pas vide"
if (filtres.filiere && filtres.filiere !== "") { if (filtres.filiere && filtres.filiere !== "") {
// if (filtres.filiere && ...)
// ↑ vérifie que filtres.filiere EXISTE (n'est pas undefined/null)
//
// if (... && filtres.filiere !== "")
// ↑ ET vérifie que la filière NE S'PAS VIDE (pas "")
//
// RÉSUMÉ : si les 2 conditions sont vraies, on rentre dans le bloc
// ACTION : construire et ajouter le filtre filière
conditions.push("fili='" + echapperValeur(filtres.filiere) + "'"); conditions.push("fili='" + echapperValeur(filtres.filiere) + "'");
//
// Exemple si l'user choisit "BTS" :
// - filtres.filiere = "BTS"
// - echapperValeur("BTS") = "BTS" (pas d'apostrophe, inchangé)
// - condition.push ajoute : "fili='BTS'" au tableau conditions
// - Résultat : conditions = [..., "fili='BTS'"]
//
// Cette condition sera fusionnée avec les autres via AND
// Exemple d'URL finale : ...where=search(...) AND fili='BTS' AND region='IDF'
} }
if (filtres.selectivite && filtres.selectivite !== "") { if (filtres.selectivite && filtres.selectivite !== "") {
@@ -37,6 +72,7 @@ export function construireURL(requete, limite = 20, decalage = 0, filtres = {})
conditions.push("taux_acces_ens<=" + filtres.tauxMax); conditions.push("taux_acces_ens<=" + filtres.tauxMax);
} }
// si on a des conditions, on les ajoute dans paramètre where encodé
if (conditions.length > 0) { if (conditions.length > 0) {
url += "&where=" + encodeURIComponent(conditions.join(" AND ")); url += "&where=" + encodeURIComponent(conditions.join(" AND "));
} }
@@ -45,11 +81,14 @@ export function construireURL(requete, limite = 20, decalage = 0, filtres = {})
} }
// Charger les formations depuis l'API Parcoursup // Charger les formations depuis l'API Parcoursup
// ----------------------------------------------
// Appelle l'URL construite ci-dessus via fetch, parse le JSON.
export async function chargerFormations(requete, limite = 20, decalage = 0, filtres = {}) { export async function chargerFormations(requete, limite = 20, decalage = 0, filtres = {}) {
var url = construireURL(requete, limite, decalage, filtres); var url = construireURL(requete, limite, decalage, filtres);
var reponse = await fetch(url); var reponse = await fetch(url);
// vérifie le code HTTP; si erreur, lève une exception pour le remonté à l'appelant
if (!reponse.ok) { if (!reponse.ok) {
throw new Error("Erreur HTTP " + reponse.status); throw new Error("Erreur HTTP " + reponse.status);
} }
@@ -58,6 +97,10 @@ export async function chargerFormations(requete, limite = 20, decalage = 0, filt
} }
// Charger l'historique d'une formation sur plusieurs années // Charger l'historique d'une formation sur plusieurs années
// ---------------------------------------------------------
// - appelle plusieurs versions du dataset (2020..2025)
// - récupère le premier résultat valide pour chaque année
// - calcule un taux d'accès (acc_tot / voe_tot)
export async function chargerHistoriqueFormation(codUai, nomFormation) { export async function chargerHistoriqueFormation(codUai, nomFormation) {
var jeuDeDonnees = { var jeuDeDonnees = {
@@ -70,6 +113,8 @@ export async function chargerHistoriqueFormation(codUai, nomFormation) {
}; };
var historique = []; var historique = [];
// on échappe aussi cod_uai et nom pour la requête (sécurité sintaxe)
var nomCourt = echapperValeur((nomFormation || "").substring(0, 40)); var nomCourt = echapperValeur((nomFormation || "").substring(0, 40));
var codeUai = echapperValeur(codUai); var codeUai = echapperValeur(codUai);
var annees = [2020, 2021, 2022, 2023, 2024, 2025]; var annees = [2020, 2021, 2022, 2023, 2024, 2025];
@@ -105,6 +150,7 @@ export async function chargerHistoriqueFormation(codUai, nomFormation) {
taux = Math.round((ligne.acc_tot / ligne.voe_tot) * 100); taux = Math.round((ligne.acc_tot / ligne.voe_tot) * 100);
} }
// ajoute un objet historique pour cette année
historique.push({ historique.push({
annee: annee, annee: annee,
tauxAcces: taux, tauxAcces: taux,
@@ -123,6 +169,7 @@ export async function chargerHistoriqueFormation(codUai, nomFormation) {
} }
} catch (e) { } catch (e) {
// si un appel échoue, on affiche l'erreur et on continue la boucle
console.warn("Erreur pour l'année " + annee + " :", e); console.warn("Erreur pour l'année " + annee + " :", e);
} }
} }
+367
View File
@@ -0,0 +1,367 @@
# 📚 Fiche de Révision Fonctions JS — Parcoursup Explorer
> Préparée pour la soutenance. Tous les fichiers `.js` et `.riot` (partie `<script>`) ont été analysés.
---
## 🗂️ Vue d'ensemble des fichiers JS
| Fichier | Rôle |
|---|---|
| `api.js` | Appels HTTP à l'API Parcoursup (Open Data) |
| `firebase.js` | Authentification et base de données Firebase |
| `formation.js` | Transformation des données brutes de l'API en objet propre |
| `main.js` | *(ancien fichier de test, remplacé par app.riot)* |
| `app.riot` | Composant principal — état global, routing, comparateur |
| `components/auth-panel.riot` | Connexion / inscription / déconnexion |
| `components/search-bar.riot` | Barre de recherche et filtres avancés |
| `components/result-list.riot` | Affichage de la liste de résultats |
| `components/detail-view.riot` | Page détail d'une formation + graphiques |
| `components/map-view.riot` | Carte interactive Leaflet |
---
## 📄 `api.js` — Appels à l'API Parcoursup
### `echapperValeur(valeur)` *(fonction privée, non exportée)*
- **Rôle** : Sécurise une valeur avant de l'injecter dans une clause `where` de l'URL.
- **Comment** : Convertit en `String`, puis remplace les apostrophes `'` par `\'` avec une **regex**.
- **Concepts clés** : `String()`, `.replace()`, **expression régulière** `/'/g`
```js
function echapperValeur(valeur) {
return String(valeur).replace(/'/g, "\\'");
}
```
---
### `construireURL(requete, limite, decalage, filtres)` *(exportée)*
- **Rôle** : Construit l'URL complète de requête vers l'API Open Data.
- **Paramètres** :
- `requete` → texte tapé par l'utilisateur
- `limite` → nombre de résultats (défaut : 20)
- `decalage` → offset pour la pagination (défaut : 0)
- `filtres` → objet `{ filiere, selectivite, region, tauxMin, tauxMax }`
- **Retourne** : une URL (string)
- **Concepts clés** : **valeurs par défaut** (`limite = 20`), `encodeURIComponent()`, tableau `conditions`, `.join(" AND ")`
---
### `chargerFormations(requete, limite, decalage, filtres)` *(async, exportée)*
- **Rôle** : Appelle l'URL construite via `fetch()` et parse le JSON.
- **Retourne** : l'objet JSON de l'API (contient `results`, `total_count`)
- **Concepts clés** :
- `async` / `await`
- `fetch()` — requête HTTP
- `reponse.ok` — vérifie le statut HTTP
- `reponse.json()` — parse le corps de la réponse
- `throw new Error()` — lève une exception si erreur
---
### `chargerHistoriqueFormation(codUai, nomFormation)` *(async, exportée)*
- **Rôle** : Charge les données de la même formation sur plusieurs années (2020 → 2025), en appelant 6 datasets différents.
- **Retourne** : un tableau d'objets `{ annee, tauxAcces, candidats, admis, pctAB, ... }`
- **Concepts clés** :
- Boucle `for` sur un tableau d'années
- `try / catch` pour gérer les erreurs par année sans bloquer le reste
- `Math.round()` pour calculer le taux d'accès
- `.substring(0, 40)` — tronque le nom de la formation
- `historique.push(...)` — accumule les résultats
- `console.warn()` — affiche un avertissement sans bloquer
---
## 📄 `firebase.js` — Authentification & Base de données
### Imports Firebase (SDK modulaire)
```js
import { initializeApp } from "firebase-app.js";
import { getAuth, createUserWithEmailAndPassword, ... } from "firebase-auth.js";
import { getFirestore, doc, setDoc, getDoc } from "firebase-firestore.js";
```
> **À savoir** : Firebase est importé directement depuis une URL CDN (pas de npm ici).
### `createAccount(email, password)` *(async)*
- Crée un compte avec `createUserWithEmailAndPassword(auth, email, password)`
### `login(email, password)` *(async)*
- Connecte un utilisateur avec `signInWithEmailAndPassword(auth, email, password)`
### `logout()` *(async)*
- Déconnecte avec `signOut(auth)`
### `onUserChanged(callback)` *(sync)*
- **Rôle** : Écoute les changements d'état de connexion (connecté / déconnecté).
- Utilise `onAuthStateChanged(auth, callback)` — appelle `callback` à chaque changement.
- **Concept clé** : **callback** / **écouteur d'évènement**
### `saveUserData(uid, data)` *(async)*
- Sauvegarde des données dans Firestore avec `setDoc(doc(db, "users", uid), data, { merge: true })`
- `merge: true` → ne **pas écraser** les champs existants, juste **fusionner**
### `loadUserData(uid)` *(async)*
- Lit un document Firestore avec `getDoc(doc(db, "users", uid))`
- Vérifie avec `snap.exists()` avant de retourner `snap.data()`
---
## 📄 `formation.js` — Transformation des données
### `creerFormation(brut)` *(exportée)*
- **Rôle** : Prend un enregistrement brut de l'API (avec des noms de champs techniques comme `lib_for_voe_ins`, `voe_tot`, `acc_tot`…) et retourne un objet **propre et lisible**.
- **Concepts clés** :
- Objet littéral `{}`
- Accès aux propriétés imbriquées : `brut.g_olocalisation_des_formations.lat`
- `Math.round()` pour calculer `tauxAcces`
- Valeurs par défaut avec `||` : `brut.voe_tot || 0`
---
## 📄 `app.riot` — Composant principal (Riot.js)
### Cycle de vie Riot.js
| Méthode | Déclenchement |
|---|---|
| `onMounted()` | Quand le composant est ajouté au DOM |
| `onUpdated()` | Après chaque `this.update()` |
| `onBeforeUnmount()` | Juste avant la suppression du composant |
### `onMounted()`
- Lit la sélection sauvegardée avec `localStorage.getItem('selectionFormations')`
- Parse le JSON avec `JSON.parse(saved)` (dans un `try/catch`)
- Appelle `window.firebaseServices.onUserChanged(...)` pour surveiller la connexion
- Écoute les changements de hash URL avec `window.addEventListener('hashchange', ...)`
- Appelle `this.gererRoute()` pour démarrer le bon affichage
### `gererRoute()`
- **Rôle** : Système de **routing par hash** (`#/`, `#/formation/id`, `#/comparateur`)
- Lit `window.location.hash`
- Utilise `chemin.startsWith('/formation/')` et `decodeURIComponent()`
- Met à jour `state.view` pour afficher le bon écran
### `chargerFormationParId(id)` *(async)*
- Cherche d'abord dans `state.results`, puis dans `state.selectedFormations`
- Si non trouvé → appelle `window.chargerFormations()` pour récupérer depuis l'API
### `lancerRecherche(requete, filtres)` *(async)*
- Remet `page` à 1, met `loading: true`, puis appelle `this.chargerPage(1)`
### `chargerPage(page)` *(async)*
- Calcule `decalage = (page - 1) * this.state.limit`
- Appelle `window.chargerFormations(...)` et transforme les résultats avec `window.creerFormation()`
- Met à jour `results`, `total`, `page`, `loading`
### `nombreTotalPages()`
- **Formule** : `Math.ceil(total / limit)` — arrondit au supérieur
### `pageSuivante()` / `pagePrecedente()` *(async)*
- Vérifient les bornes (`page < nombreTotalPages()`, `page > 1`) avant d'appeler `chargerPage()`
### `ajouterSelection(index)` / `retirerSelection(id)` / `viderSelection()`
- Gèrent un tableau `selectedFormations`
- `.slice()` → copie le tableau avant modification (évite mutation directe du state)
- Appellent `this.sauvegarderSelection(selection)` après chaque modification
### `sauvegarderSelection(selection)` *(async)*
- `localStorage.setItem(...)` — persistance locale (dans navigateur)
- `window.firebaseServices.saveUserData(...)` — persistance cloud si connecté
### `obtenirSelectionTriee()`
- Trie avec `Array.sort()` et des **fonctions de comparaison**
- `localeCompare()` — compare des chaînes alphabétiquement (avec accents)
- `calculerScore()` — tri par score d'estimation
### `calculerScore(f)`
- Algorithme de scoring en 3 critères : taux d'accès + note de l'étudiant + % de sa série
- Retourne un score numérique (0 à 100)
### `estimerFormation(f)`
- Appelle `calculerScore()` et retourne un libellé : `"Très favorable"`, `"Favorable"`, `"Possible"`, `"Difficile"`, `"Très difficile"`
### `classeEstimation(f)` / `classeCarte(f)`
- Retournent une **classe CSS** selon l'estimation → pour coloriser les cartes
### `surDeconnexion()`
- Vide `selectedFormations` et supprime de localStorage avec `localStorage.removeItem(...)`
---
## 📄 `components/auth-panel.riot`
### `ouvrirModale()` / `fermerModale()`
- `this.update({ visible: true })` / `this.update({ visible: false })` — affiche/cache la modale
### `cliquerFond(e)`
- `e.target === e.currentTarget` → vérifie qu'on a cliqué sur le **fond** et non sur la modale elle-même
### `afficherConnexion()` / `afficherInscription()`
- Changent `state.mode` (entre `'connexion'` et `'inscription'`) et les labels affichés
### `validerFormulaire(e)` *(async)*
- `e.preventDefault()` — empêche le rechargement de page (comportement par défaut du formulaire)
- Récupère email et password depuis `e.target.email.value`
- Attrape les erreurs Firebase avec `err.code` pour afficher des messages lisibles
- Codes gérés : `auth/email-already-in-use`, `auth/wrong-password`, `auth/weak-password`, `auth/user-not-found`, `auth/invalid-email`
### `seDeconnecter()` *(async)*
- Appelle `window.firebaseServices.logout()`
---
## 📄 `components/search-bar.riot`
### `updateQuery(e)` / `updateFiliere(e)` / `updateSelectivite(e)` / `updateRegion(e)` / `updateTauxMin(e)` / `updateTauxMax(e)`
- Tous liés à des événements `oninput` ou `onchange`
- Mettent à jour le `state` avec `this.update({ champ: e.target.value })`
- `Number(e.target.value)` pour les taux (convertit string → nombre)
### `handleKey(e)`
- `e.key === 'Enter'` → déclenche `this.submitSearch()` si on appuie Entrée
### `toggleFilters()`
- Inverse `state.showFilters` et change le label du bouton
### `submitSearch()`
- Construit l'objet `filters` depuis le state et appelle `this.props.onsearch(...)`**communication enfant → parent** via props
---
## 📄 `components/result-list.riot`
### `afficherDetail(index)`
- Appelle `this.props.ondetail(index)`**communication vers le parent**
### `ajouterALaSelection(index)`
- Appelle `this.props.onselect(index)`
### `localiserSurCarte(formation)`
- Appelle `window.mapFocus(formation.id)`**communication globale via window** entre deux composants frères
---
## 📄 `components/detail-view.riot`
### `onMounted()` / `onUpdated()`
- Appellent `afficherGraphiques()` et `chargerHistorique()` lors du montage/mise à jour
### `retourListe()`
- `this.props.onback()` — dit au parent de revenir à la liste
### `limiterValeur(val)`
- **Rôle** : Ramène une valeur entre 0 et 1 pour Charts.css (qui utilise des variables CSS `--size`)
- Gère `null`, `undefined`, `NaN` avec `isNaN()`
- Formule : `val / 100`, clampé entre 0 et 1
### `afficherGraphiques()`
- Sélectionne les éléments DOM avec `this.$('[ref="graphBac"]')` — sélecteur Riot
- Appelle `construireGraphiqueColonnes()` et `construireGraphiqueBarres()` pour injecter du HTML
### `chargerHistorique()` *(async)*
- Extrait le `codUai` depuis `f.id.split('-')[0]`
- Appelle `window.chargerHistoriqueFormation(codUai, f.nom)`
- Met à jour `state.historique`
### `afficherGraphiquesHistoriques()`
- Construit les graphiques historiques en HTML (charts CSS) en itérant sur `state.historique`
- Pour candidats vs admis : calcule une taille relative `(valeur / max) * 100`
### `construireGraphiqueColonnes(elements)` / `construireGraphiqueBarres(elements)`
- **Rôle** : Génèrent du HTML de tableau `<table>` pour la lib **Charts.css**
- Paramètre : tableau `[{ label, valeur, couleur }]`
- Retournent un string HTML avec variables CSS `--size` et `--color`
---
## 📄 `components/map-view.riot` — Carte Leaflet
### `onMounted()`
- `L.map(divCarte).setView([46.8, 2.5], 6)` — initialise la carte centrée sur la France
- `L.layerGroup().addTo(this.carte)` — groupe de marqueurs pour les vider facilement
- `L.tileLayer(...)` — ajoute les tuiles OpenStreetMap
- `setTimeout(..., 200)` et `setTimeout(..., 500)` — force un recalcul de taille après rendu (bug Leaflet classique)
- `window.mapFocus = function(id) {...}` — expose une fonction globale pour que `result-list` puisse y accéder
### `onUpdated()`
- Recharge les marqueurs et invalide la taille de la carte
### `onBeforeUnmount()`
- `this.carte.remove()` — détruit la carte proprement pour éviter les **memory leaks**
- `this.carte = null`
### `afficherMarqueurs()`
- `this.groupeMarqueurs.clearLayers()` — vide tous les marqueurs existants avant d'en re-créer
- Crée un `L.marker([lat, lon])` pour chaque formation avec coordonnées
- `.bindPopup(...)` — popup d'info au clic
- `.addTo(this.groupeMarqueurs)` — ajoute au groupe
- `this.carte.fitBounds(coordonnees, { padding: [20, 20] })` — ajuste le zoom pour tout voir
### `centrerSurFormation(id)`
- Récupère le marqueur dans `this.marqueursIndex[id]`
- `divCarte.scrollIntoView({ behavior: 'smooth' })` — scrolle vers la carte
- `this.carte.setView(marqueur.getLatLng(), 13, { animate: true })` — centre et zoom
- `marqueur.openPopup()` — ouvre la bulle d'info
---
## 🔑 Concepts JavaScript clés à bien connaître
| Concept | Où dans le projet |
|---|---|
| `async` / `await` | `chargerFormations`, `chargerHistoriqueFormation`, `validerFormulaire`, etc. |
| `fetch()` + `.json()` | `api.js` — appels API |
| `try / catch` | Partout pour gérer les erreurs réseau et Firebase |
| `Promise` + `.then()` + `.catch()` | `app.riot``loadUserData().then(...)` |
| `localStorage` | `app.riot` — sauvegarder la sélection |
| `JSON.parse()` / `JSON.stringify()` | Lecture/écriture dans `localStorage` |
| `encodeURIComponent()` | Encoder les paramètres dans l'URL API |
| `Array.push()`, `.slice()`, `.sort()` | Gestion de la sélection et du tri |
| `Math.round()`, `Math.ceil()` | Calcul taux d'accès, pagination |
| `String.localeCompare()` | Tri alphabétique avec accents |
| `String.split('-')`, `.startsWith()`, `.substring()` | Parsing d'ID, routing |
| `e.preventDefault()` | Formulaire auth (empêche rechargement) |
| `e.target === e.currentTarget` | Détection clic sur fond de modale |
| `window.addEventListener('hashchange', ...)` | Routing SPA par hash |
| `setTimeout()` | Délai pour corriger bug affichage Leaflet |
| `isNaN()` | Vérification valeur numérique dans Charts.css |
| Regex `/'/g` | Échapper les apostrophes dans les requêtes API |
---
## 🌐 Bibliothèques et APIs externes utilisées
| Bibliothèque/API | Usage | Comment importé |
|---|---|---|
| **Firebase Auth** | Connexion / inscription / déconnexion | CDN (`gstatic.com`) |
| **Firebase Firestore** | Stockage de la sélection en cloud | CDN (`gstatic.com`) |
| **Riot.js** | Framework composants UI | Via `index.html` |
| **Leaflet.js (`L`)** | Carte interactive | Via `index.html` (variable globale `L`) |
| **Charts.css** | Graphiques en CSS pur | Via `index.html` |
| **API Open Data Parcoursup** | Données des formations | `fetch()` vers `data.enseignementsup-recherche.gouv.fr` |
| **OpenStreetMap** | Tuiles de fond de carte | URL `tile.openstreetmap.org` |
---
## ❓ Questions probables du prof — Réponses préparées
**Q : Pourquoi utilises-tu `async/await` ?**
→ Parce que `fetch()` et les appels Firebase sont **asynchrones** (ils prennent du temps). Sans `await`, le code continuerait sans attendre la réponse, ce qui rendrait le résultat `undefined`.
**Q : À quoi sert `encodeURIComponent()` ?**
→ L'URL ne peut pas contenir certains caractères spéciaux (espaces, apostrophes, `&`...). Cette fonction les convertit en codes URL sûrs (ex: `%20` pour l'espace).
**Q : Pourquoi utilises-tu `localStorage` ?**
→ Pour **persister** la sélection de formations entre les visites, même sans être connecté. C'est un stockage côté navigateur, sans serveur.
**Q : Qu'est-ce que `e.preventDefault()` ?**
→ Empêche le **comportement par défaut** du navigateur. Sur un `<form>`, le comportement par défaut est de recharger la page. On l'annule pour gérer la soumission en JavaScript.
**Q : Pourquoi `this.carte.remove()` dans `onBeforeUnmount()` ?**
→ Leaflet attache des **event listeners** et occupe de la mémoire. Si on ne détruit pas proprement la carte, cela crée des **memory leaks** (fuites mémoire).
**Q : Qu'est-ce que `merge: true` dans Firestore `setDoc(...)` ?**
→ Cela **fusionne** les données avec celles existantes au lieu de tout écraser. Utile pour ne mettre à jour qu'un seul champ (la sélection) sans toucher aux autres.
**Q : Comment fonctionne le routing de ton app ?**
→ On utilise les **hash URL** (`#/`, `#/formation/id`, `#/comparateur`). On écoute l'événement `hashchange` pour détecter les changements, et on affiche le bon écran via `state.view`. C'est un routing côté client, sans rechargement de page — c'est ce qu'on appelle une **SPA** (Single Page Application).