Files
public-html2/parcoursup/fonctions_js_soutenance.md
T
2026-04-01 22:17:05 +02:00

368 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 📚 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).