Files
public-html2/parcoursup/app.riot
T

271 lines
9.5 KiB
Plaintext
Raw Normal View History

<app>
2026-04-02 14:15:26 +02:00
<!--
Composant racine — routeur SPA basé sur les ancres URL (#/)
3 vues : 'search' | 'detail' | 'comparateur'
-->
2026-03-19 14:37:08 +01:00
<header class="site-header">
<div class="header-inner">
2026-03-20 01:51:08 +01:00
<a class="logo" href="#/">
<span class="logo-icon"></span>
2026-03-19 14:37:08 +01:00
<span class="logo-text">Parcoursup <span class="logo-light">Explorer</span></span>
2026-03-20 01:51:08 +01:00
</a>
<div class="header-right">
2026-04-02 14:15:26 +02:00
<!-- Badge cliquable vers le comparateur, visible si au moins 1 formation sélectionnée -->
<a href="#/comparateur" class="header-badge badge-clickable" if={ state.selectedFormations.length > 0 }>
{ state.selectedFormations.length } sélection(s)
</a>
2026-03-21 13:47:09 +01:00
<auth-panel user={ state.user } onauth={ surConnexion } onlogout={ surDeconnexion }></auth-panel>
</div>
2026-03-19 14:37:08 +01:00
</div>
</header>
2026-03-19 14:37:08 +01:00
<div class="page">
2026-04-02 14:15:26 +02:00
<!-- VUE RECHERCHE -->
2026-03-20 01:51:08 +01:00
<div if={ state.view === 'search' }>
2026-03-21 13:47:09 +01:00
<search-bar onsearch={ lancerRecherche }></search-bar>
2026-03-20 01:51:08 +01:00
<p class="result-count" if={ state.hasSearched && !state.loading }>
<b>{ state.query }</b> — { state.total } résultat(s)
</p>
2026-03-20 01:51:08 +01:00
<map-view results={ state.results }></map-view>
<div class="layout">
<div>
<result-list
results={ state.results }
hasSearched={ state.hasSearched }
loading={ state.loading }
2026-03-21 13:47:09 +01:00
ondetail={ afficherDetail }
onselect={ ajouterSelection }>
2026-03-20 01:51:08 +01:00
</result-list>
2026-04-02 14:15:26 +02:00
<!-- Pagination (visible si plus de résultats que la limite) -->
2026-03-20 01:51:08 +01:00
<div class="pagination" if={ state.total > state.limit }>
2026-03-21 13:47:09 +01:00
<button class="btn btn-outline" onclick={ pagePrecedente } disabled={ state.page === 1 }>
2026-03-20 01:51:08 +01:00
← Précédent
</button>
2026-03-21 13:47:09 +01:00
<span class="page-info">Page { state.page } / { nombreTotalPages() }</span>
<button class="btn btn-outline" onclick={ pageSuivante } disabled={ state.page === nombreTotalPages() }>
2026-03-20 01:51:08 +01:00
Suivant →
</button>
</div>
</div>
</div>
</div>
2026-04-02 14:15:26 +02:00
<!-- VUE DÉTAIL -->
2026-03-20 01:51:08 +01:00
<div if={ state.view === 'detail' && state.selected }>
2026-04-02 14:15:26 +02:00
<detail-view formation={ state.selected } onback={ retourRecherche }></detail-view>
2026-03-20 01:51:08 +01:00
</div>
<div if={ state.view === 'detail' && !state.selected } class="message">
Chargement de la formation...
</div>
2026-04-02 14:15:26 +02:00
<!-- VUE COMPARATEUR -->
2026-03-20 01:51:08 +01:00
<div if={ state.view === 'comparateur' }>
2026-03-21 13:47:09 +01:00
<button class="btn btn-outline" onclick={ retourRecherche } style="margin-bottom: 16px;">← Retour à la recherche</button>
2026-04-02 00:12:35 +02:00
<comparateur-view
formations={ state.selectedFormations }
onretirer={ retirerSelection }
onvider={ viderSelection }>
</comparateur-view>
</div>
</div>
<script>
export default {
2026-04-02 14:15:26 +02:00
state: {
2026-04-02 00:12:35 +02:00
view: 'search',
loading: false,
hasSearched: false,
results: [],
selected: null,
selectedFormations: [],
2026-04-02 00:12:35 +02:00
query: '',
filters: {},
page: 1,
limit: 20,
total: 0,
user: null
},
onMounted() {
2026-03-30 16:33:11 +02:00
var saved = localStorage.getItem('selectionFormations');
var soi = this;
2026-04-02 14:15:26 +02:00
// Restaurer la sélection depuis le localStorage
if (saved) {
2026-04-02 14:15:26 +02:00
try { this.state.selectedFormations = JSON.parse(saved); }
catch (e) { console.error('Erreur localStorage :', e); }
}
2026-03-20 01:51:08 +01:00
2026-04-02 14:15:26 +02:00
// Écouter les changements de session Firebase
window.firebaseServices.onUserChanged(function(user) {
2026-03-30 16:33:11 +02:00
soi.update({ user: user });
2026-04-02 14:15:26 +02:00
// À la connexion, on charge la sélection depuis Firestore
if (user) {
2026-03-20 03:06:30 +01:00
window.firebaseServices.loadUserData(user.uid)
2026-03-21 13:47:09 +01:00
.then(function(donnees) {
if (donnees && donnees.selection) {
2026-03-30 16:33:11 +02:00
soi.update({ selectedFormations: donnees.selection });
localStorage.setItem('selectionFormations', JSON.stringify(donnees.selection));
2026-03-20 03:06:30 +01:00
}
})
2026-04-02 14:15:26 +02:00
.catch(function(err) { console.error('Erreur Firestore :', err); });
}
2026-03-30 16:33:11 +02:00
});
2026-03-20 01:51:08 +01:00
2026-04-02 14:15:26 +02:00
// Écouter les changements d'URL pour le routage
window.addEventListener('hashchange', function() { soi.gererRoute(); });
2026-03-30 16:33:11 +02:00
this.gererRoute();
},
2026-03-21 13:47:09 +01:00
surConnexion() {
2026-04-02 14:15:26 +02:00
// Géré automatiquement par onUserChanged ci-dessus
},
2026-03-21 13:47:09 +01:00
surDeconnexion() {
2026-03-30 16:33:11 +02:00
this.update({ selectedFormations: [] });
localStorage.removeItem('selectionFormations');
},
2026-04-02 14:15:26 +02:00
// Sauvegarde la sélection en local ET dans Firestore si connecté
2026-03-21 13:47:09 +01:00
async sauvegarderSelection(selection) {
2026-03-30 16:33:11 +02:00
localStorage.setItem('selectionFormations', JSON.stringify(selection));
if (this.state.user) {
2026-04-02 14:15:26 +02:00
try { await window.firebaseServices.saveUserData(this.state.user.uid, { selection }); }
catch (err) { console.error('Erreur sauvegarde :', err); }
}
},
2026-04-02 14:15:26 +02:00
// Lit l'ancre URL et affiche la vue correspondante
2026-03-21 13:47:09 +01:00
gererRoute() {
2026-04-02 14:15:26 +02:00
var ancre = window.location.hash || '#/';
2026-03-30 16:33:11 +02:00
var chemin = ancre.slice(1) || '/';
2026-03-20 01:51:08 +01:00
2026-03-21 13:47:09 +01:00
if (chemin.startsWith('/formation/')) {
2026-03-30 16:33:11 +02:00
var id = decodeURIComponent(chemin.replace('/formation/', ''));
this.update({ view: 'detail' });
this.chargerFormationParId(id);
2026-03-21 13:47:09 +01:00
} else if (chemin === '/comparateur') {
2026-03-30 16:33:11 +02:00
this.update({ view: 'comparateur', selected: null });
2026-03-20 01:51:08 +01:00
} else {
2026-03-30 16:33:11 +02:00
this.update({ view: 'search', selected: null });
2026-03-20 01:51:08 +01:00
}
},
2026-04-02 14:15:26 +02:00
// Cherche la formation dans le cache (résultats / sélection) avant de faire un appel API
2026-03-21 13:47:09 +01:00
async chargerFormationParId(id) {
2026-03-30 16:33:11 +02:00
var i;
2026-03-20 01:51:08 +01:00
for (i = 0; i < this.state.results.length; i++) {
2026-04-02 14:15:26 +02:00
if (this.state.results[i].id === id) { this.update({ selected: this.state.results[i] }); return; }
2026-03-20 01:51:08 +01:00
}
for (i = 0; i < this.state.selectedFormations.length; i++) {
2026-04-02 14:15:26 +02:00
if (this.state.selectedFormations[i].id === id) { this.update({ selected: this.state.selectedFormations[i] }); return; }
2026-03-20 01:51:08 +01:00
}
2026-04-02 14:15:26 +02:00
// En dernier recours : appel API avec un mot-clé extrait de l'ID
2026-03-20 01:51:08 +01:00
try {
2026-04-02 14:15:26 +02:00
var motCle = id.split('-').slice(1).join(' ').substring(0, 30);
2026-03-21 13:47:09 +01:00
if (motCle) {
2026-03-30 16:33:11 +02:00
var donnees = await window.chargerFormations(motCle, 10, 0);
2026-03-21 13:47:09 +01:00
if (donnees.results && donnees.results.length > 0) {
for (i = 0; i < donnees.results.length; i++) {
2026-04-02 14:15:26 +02:00
var f = window.creerFormation(donnees.results[i]);
if (f.id === id) { this.update({ selected: f }); return; }
2026-03-20 01:51:08 +01:00
}
2026-03-30 16:33:11 +02:00
this.update({ selected: window.creerFormation(donnees.results[0]) });
2026-03-20 01:51:08 +01:00
}
}
2026-04-02 14:15:26 +02:00
} catch (erreur) { console.error('Erreur chargement formation :', erreur); }
2026-03-20 01:51:08 +01:00
},
2026-03-21 13:47:09 +01:00
afficherDetail(index) {
2026-03-30 16:33:11 +02:00
var formation = this.state.results[index];
this.update({ selected: formation, view: 'detail' });
window.location.hash = '#/formation/' + encodeURIComponent(formation.id);
2026-03-20 01:51:08 +01:00
},
2026-03-21 13:47:09 +01:00
retourRecherche() {
2026-03-30 16:33:11 +02:00
window.location.hash = '#/';
2026-03-20 01:51:08 +01:00
},
2026-03-21 13:47:09 +01:00
async lancerRecherche(requete, filtres) {
2026-04-02 14:15:26 +02:00
this.update({ loading: true, hasSearched: true, selected: null, view: 'search', query: requete, filters: filtres || {}, page: 1 });
2026-03-30 16:33:11 +02:00
window.location.hash = '#/';
await this.chargerPage(1);
},
2026-04-02 14:15:26 +02:00
// Charge la page demandée en calculant l'offset (décalage) correspondant
2026-03-21 13:47:09 +01:00
async chargerPage(page) {
2026-03-30 16:33:11 +02:00
this.update({ loading: true });
try {
2026-03-30 16:33:11 +02:00
var decalage = (page - 1) * this.state.limit;
2026-04-02 14:15:26 +02:00
var donnees = await window.chargerFormations(this.state.query, this.state.limit, decalage, this.state.filters);
2026-03-30 16:33:11 +02:00
var formations = [];
2026-03-21 13:47:09 +01:00
if (donnees.results) {
for (var i = 0; i < donnees.results.length; i++) {
2026-04-02 14:15:26 +02:00
formations.push(window.creerFormation(donnees.results[i]));
}
}
2026-04-02 14:15:26 +02:00
this.update({ results: formations, total: donnees.total_count || 0, page, loading: false });
2026-03-21 13:47:09 +01:00
} catch (erreur) {
2026-03-30 16:33:11 +02:00
console.error(erreur);
2026-04-02 14:15:26 +02:00
this.update({ results: [], total: 0, loading: false });
}
},
2026-03-21 13:47:09 +01:00
nombreTotalPages() {
2026-03-30 16:33:11 +02:00
return Math.ceil(this.state.total / this.state.limit);
},
2026-03-21 13:47:09 +01:00
async pageSuivante() {
2026-04-02 14:15:26 +02:00
if (this.state.page < this.nombreTotalPages()) { await this.chargerPage(this.state.page + 1); }
},
2026-03-21 13:47:09 +01:00
async pagePrecedente() {
2026-04-02 14:15:26 +02:00
if (this.state.page > 1) { await this.chargerPage(this.state.page - 1); }
},
2026-04-02 14:15:26 +02:00
// Ajoute la formation au comparateur si elle n'y est pas déjà
2026-03-21 13:47:09 +01:00
ajouterSelection(index) {
2026-04-02 14:15:26 +02:00
var formation = this.state.results[index];
var selection = this.state.selectedFormations.slice();
2026-03-18 13:44:30 +01:00
2026-03-20 01:51:08 +01:00
for (var i = 0; i < selection.length; i++) {
2026-04-02 14:15:26 +02:00
if (selection[i].id === formation.id) { return; } // doublon
}
2026-03-18 13:44:30 +01:00
2026-04-02 14:15:26 +02:00
selection.push(formation);
this.update({ selectedFormations: selection });
this.sauvegarderSelection(selection);
},
2026-03-18 13:44:30 +01:00
2026-04-02 14:15:26 +02:00
// Retire la formation avec cet id de la sélection
2026-03-21 13:47:09 +01:00
retirerSelection(id) {
2026-04-02 14:15:26 +02:00
var nouvelle = this.state.selectedFormations.filter(function(f) { return f.id !== id; });
this.update({ selectedFormations: nouvelle });
this.sauvegarderSelection(nouvelle);
},
2026-03-21 13:47:09 +01:00
viderSelection() {
2026-03-30 16:33:11 +02:00
this.update({ selectedFormations: [] });
this.sauvegarderSelection([]);
}
2026-04-02 00:12:35 +02:00
2026-03-30 16:33:11 +02:00
};
</script>
2026-04-02 00:12:35 +02:00
2026-03-19 14:37:08 +01:00
</app>