ajout du md
This commit is contained in:
@@ -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
|
||||||
@@ -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).*
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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).
|
||||||
Reference in New Issue
Block a user