ajout des elements

This commit is contained in:
2026-03-20 01:51:08 +01:00
parent 83020dcb5b
commit 70aed67ba6
9 changed files with 986 additions and 412 deletions
+92 -4
View File
@@ -1,19 +1,45 @@
export function buildURL(query, limit = 20, offset = 0) { export function buildURL(query, limit = 20, offset = 0, filters = {}) {
let url = let url =
"https://data.enseignementsup-recherche.gouv.fr/api/explore/v2.1/catalog/datasets/fr-esr-parcoursup/records?" "https://data.enseignementsup-recherche.gouv.fr/api/explore/v2.1/catalog/datasets/fr-esr-parcoursup/records?"
url += "limit=" + limit url += "limit=" + limit
url += "&offset=" + offset url += "&offset=" + offset
var conditions = []
if (query && query.trim() !== "") { if (query && query.trim() !== "") {
url += "&where=search(lib_for_voe_ins, '" + query + "')" conditions.push("search(lib_for_voe_ins, '" + query + "')")
}
if (filters.filiere && filters.filiere !== "") {
conditions.push("fili='" + filters.filiere + "'")
}
if (filters.selectivite && filters.selectivite !== "") {
conditions.push("select_form='" + filters.selectivite + "'")
}
if (filters.region && filters.region !== "") {
conditions.push("region_etab_aff='" + filters.region + "'")
}
if (filters.tauxMin && filters.tauxMin > 0) {
conditions.push("taux_acces_ens>=" + filters.tauxMin)
}
if (filters.tauxMax && filters.tauxMax < 100) {
conditions.push("taux_acces_ens<=" + filters.tauxMax)
}
if (conditions.length > 0) {
url += "&where=" + conditions.join(" AND ")
} }
return url return url
} }
export async function fetchFormations(query, limit = 20, offset = 0) { export async function fetchFormations(query, limit = 20, offset = 0, filters = {}) {
const url = buildURL(query, limit, offset) const url = buildURL(query, limit, offset, filters)
const response = await fetch(url) const response = await fetch(url)
if (!response.ok) { if (!response.ok) {
@@ -22,3 +48,65 @@ export async function fetchFormations(query, limit = 20, offset = 0) {
return await response.json() return await response.json()
} }
export async function fetchFormationHistory(codUai, nomFormation) {
var datasets = {
2020: "fr-esr-parcoursup_2020",
2021: "fr-esr-parcoursup_2021",
2022: "fr-esr-parcoursup_2022",
2023: "fr-esr-parcoursup_2023",
2024: "fr-esr-parcoursup_2024",
2025: "fr-esr-parcoursup"
}
var history = []
var searchName = nomFormation.substring(0, 40).replace(/'/g, "\\'")
var years = [2020, 2021, 2022, 2023, 2024, 2025]
for (var i = 0; i < years.length; i++) {
var year = years[i]
var dataset = datasets[year]
try {
var url = "https://data.enseignementsup-recherche.gouv.fr/api/explore/v2.1/catalog/datasets/"
+ dataset + "/records?"
+ "limit=5"
+ "&where=cod_uai%3D'" + codUai + "' AND search(lib_for_voe_ins, '" + searchName + "')"
+ "&select=cod_uai,lib_for_voe_ins,voe_tot,acc_tot,pct_sansmention,pct_ab,pct_b,pct_tb,pct_tbf,pct_bg,pct_bt,pct_bp"
var response = await fetch(url)
if (response.ok) {
var data = await response.json()
if (data.results && data.results.length > 0) {
var r = data.results[0]
var taux = 0
if (r.voe_tot && r.voe_tot > 0) {
taux = Math.round((r.acc_tot / r.voe_tot) * 100)
}
history.push({
annee: year,
tauxAcces: taux,
candidats: r.voe_tot || 0,
admis: r.acc_tot || 0,
pctSansMention: r.pct_sansmention || 0,
pctAB: r.pct_ab || 0,
pctB: r.pct_b || 0,
pctTB: r.pct_tb || 0,
pctTBF: r.pct_tbf || 0,
pctGeneral: r.pct_bg || 0,
pctTechno: r.pct_bt || 0,
pctPro: r.pct_bp || 0
})
}
}
} catch (e) {
console.warn("Erreur pour " + year + ":", e)
}
}
return history
}
+237 -121
View File
File diff suppressed because it is too large Load Diff
+209 -115
View File
@@ -1,9 +1,7 @@
<detail-view> <detail-view>
<div if={ props.formation } class="detail-page"> <div if={ props.formation } class="detail-page">
<h2>Formation</h2> <h2>Formation</h2>
<h1 class="formation-title">{ props.formation.etablissement } - { props.formation.nom }</h1> <h1 class="formation-title">{ props.formation.etablissement } - { props.formation.nom }</h1>
<div class="formation-meta"> <div class="formation-meta">
<p><b>Ville :</b> { props.formation.ville }</p> <p><b>Ville :</b> { props.formation.ville }</p>
<p><b>Département :</b> { props.formation.departement } { props.formation.departementLib }</p> <p><b>Département :</b> { props.formation.departement } { props.formation.departementLib }</p>
@@ -15,7 +13,6 @@
<div class="detail-grid"> <div class="detail-grid">
<div> <div>
<h2>Phase principale d'admission</h2> <h2>Phase principale d'admission</h2>
<table class="detail-table"> <table class="detail-table">
<thead> <thead>
<tr> <tr>
@@ -68,7 +65,6 @@
<div class="timeline-box"> <div class="timeline-box">
<h3>Vitesse de remplissage</h3> <h3>Vitesse de remplissage</h3>
<div class="timeline"> <div class="timeline">
<div class="timeline-item"> <div class="timeline-item">
<div class="timeline-dot"></div> <div class="timeline-dot"></div>
@@ -77,7 +73,6 @@
{ props.formation.pctDebutPhase }% { props.formation.pctDebutPhase }%
</div> </div>
</div> </div>
<div class="timeline-item"> <div class="timeline-item">
<div class="timeline-dot"></div> <div class="timeline-dot"></div>
<div> <div>
@@ -85,7 +80,6 @@
{ props.formation.pctDateBac }% { props.formation.pctDateBac }%
</div> </div>
</div> </div>
<div class="timeline-item"> <div class="timeline-item">
<div class="timeline-dot"></div> <div class="timeline-dot"></div>
<div> <div>
@@ -98,7 +92,6 @@
</div> </div>
<h2>Phase complémentaire d'admission</h2> <h2>Phase complémentaire d'admission</h2>
<table class="detail-table"> <table class="detail-table">
<thead> <thead>
<tr> <tr>
@@ -153,127 +146,49 @@
<h2 class="charts-heading">Profil des admis</h2> <h2 class="charts-heading">Profil des admis</h2>
<div class="charts-section"> <div class="charts-section">
<!-- Graphique 1 : Répartition par type de bac -->
<div class="chart-wrapper"> <div class="chart-wrapper">
<h3>Répartition par type de bac</h3> <h3>Répartition par type de bac</h3>
<div id="chart-bac" ref="chartBac"></div>
<div id="chart-bac">
<table class="charts-css column show-labels show-primary-axis show-4-secondary-axes data-spacing-10">
<thead>
<tr>
<th scope="col">Type</th>
<th scope="col">Pourcentage</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Général</th>
<td style="--size: { safe(props.formation.pctGeneral) }; --color: #3d7fff;">
<span class="data">{ props.formation.pctGeneral || 0 }%</span>
</td>
</tr>
<tr>
<th scope="row">Techno</th>
<td style="--size: { safe(props.formation.pctTechno) }; --color: #f59e0b;">
<span class="data">{ props.formation.pctTechno || 0 }%</span>
</td>
</tr>
<tr>
<th scope="row">Pro</th>
<td style="--size: { safe(props.formation.pctPro) }; --color: #10b981;">
<span class="data">{ props.formation.pctPro || 0 }%</span>
</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
<!-- Graphique 2 : Mentions au bac -->
<div class="chart-wrapper"> <div class="chart-wrapper">
<h3>Mentions au bac des admis</h3> <h3>Mentions au bac des admis</h3>
<div id="chart-mentions" ref="chartMentions"></div>
<div id="chart-mentions">
<table class="charts-css column show-labels show-primary-axis show-4-secondary-axes data-spacing-10">
<thead>
<tr>
<th scope="col">Mention</th>
<th scope="col">Pourcentage</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Sans</th>
<td style="--size: { safe(props.formation.pctSansMention) }; --color: #94a3b8;">
<span class="data">{ props.formation.pctSansMention || 0 }%</span>
</td>
</tr>
<tr>
<th scope="row">AB</th>
<td style="--size: { safe(props.formation.pctAB) }; --color: #60a5fa;">
<span class="data">{ props.formation.pctAB || 0 }%</span>
</td>
</tr>
<tr>
<th scope="row">Bien</th>
<td style="--size: { safe(props.formation.pctB) }; --color: #34d399;">
<span class="data">{ props.formation.pctB || 0 }%</span>
</td>
</tr>
<tr>
<th scope="row">TB</th>
<td style="--size: { safe(props.formation.pctTB) }; --color: #fbbf24;">
<span class="data">{ props.formation.pctTB || 0 }%</span>
</td>
</tr>
<tr>
<th scope="row">TB Féli.</th>
<td style="--size: { safe(props.formation.pctTBF) }; --color: #f472b6;">
<span class="data">{ props.formation.pctTBF || 0 }%</span>
</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
<!-- Graphique 3 : Profil sociologique (barres horizontales, pleine largeur) -->
<div class="chart-wrapper chart-full"> <div class="chart-wrapper chart-full">
<h3>Profil sociologique</h3> <h3>Profil sociologique</h3>
<div id="chart-profil" ref="chartProfil"></div>
<div id="chart-profil">
<table class="charts-css bar show-labels show-primary-axis show-4-secondary-axes data-spacing-14">
<thead>
<tr>
<th scope="col">Catégorie</th>
<th scope="col">Pourcentage</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Femmes</th>
<td style="--size: { safe(props.formation.pctFemmes) }; --color: #a78bfa;">
<span class="data">{ props.formation.pctFemmes || 0 }%</span>
</td>
</tr>
<tr>
<th scope="row">Boursiers</th>
<td style="--size: { safe(props.formation.pctBoursiers) }; --color: #fb923c;">
<span class="data">{ props.formation.pctBoursiers || 0 }%</span>
</td>
</tr>
<tr>
<th scope="row">Néo-bac</th>
<td style="--size: { safe(props.formation.pctNeoBac) }; --color: #2dd4bf;">
<span class="data">{ props.formation.pctNeoBac || 0 }%</span>
</td>
</tr>
</tbody>
</table>
</div> </div>
</div> </div>
<!-- ===================== ÉVOLUTION HISTORIQUE ===================== -->
<h2 class="charts-heading">Évolution depuis 2020</h2>
<div if={ state.loadingHistory } class="message">
Chargement de l'historique...
</div>
<div if={ !state.loadingHistory && state.history.length === 0 } class="message">
Aucune donnée historique disponible pour cette formation.
</div>
<div class="charts-section" if={ state.history.length > 0 }>
<div class="chart-wrapper">
<h3>Taux d'accès par année</h3>
<div id="chart-evolution-taux" ref="chartTaux"></div>
</div>
<div class="chart-wrapper">
<h3>Nombre de candidats et admis</h3>
<div id="chart-evolution-candidats" ref="chartCandidats"></div>
</div>
<div class="chart-wrapper chart-full">
<h3>Évolution des mentions au bac</h3>
<div ref="chartMentionsHist"></div>
</div>
</div> </div>
<button onclick={ () => props.onback() } class="btn-retour">Retour à la liste</button> <button onclick={ () => props.onback() } class="btn-retour">Retour à la liste</button>
@@ -281,12 +196,191 @@
<script> <script>
export default { export default {
state: {
history: [],
loadingHistory: false
},
safe(val) { safe(val) {
if (val === null || val === undefined || isNaN(val)) return 0 if (val === null || val === undefined || isNaN(val)) return 0
var v = val / 100 var v = val / 100
if (v > 1) return 1 if (v > 1) return 1
if (v < 0) return 0 if (v < 0) return 0
return Math.round(v * 100) / 100 return Math.round(v * 100) / 100
},
onMounted() {
this.renderCharts()
this.loadHistory()
},
onUpdated() {
this.renderCharts()
if (this.state.history.length > 0) {
this.renderHistoryCharts()
}
},
renderCharts() {
var f = this.props.formation
if (!f) return
var chartBac = this.$('[ref="chartBac"]')
var chartMentions = this.$('[ref="chartMentions"]')
var chartProfil = this.$('[ref="chartProfil"]')
if (chartBac) {
chartBac.innerHTML = this.buildColumnChart([
{ label: 'Général', value: f.pctGeneral, color: '#3d7fff' },
{ label: 'Techno', value: f.pctTechno, color: '#f59e0b' },
{ label: 'Pro', value: f.pctPro, color: '#10b981' }
])
}
if (chartMentions) {
chartMentions.innerHTML = this.buildColumnChart([
{ label: 'Sans', value: f.pctSansMention, color: '#94a3b8' },
{ label: 'AB', value: f.pctAB, color: '#60a5fa' },
{ label: 'Bien', value: f.pctB, color: '#34d399' },
{ label: 'TB', value: f.pctTB, color: '#fbbf24' },
{ label: 'TB Féli.', value: f.pctTBF, color: '#f472b6' }
])
}
if (chartProfil) {
chartProfil.innerHTML = this.buildBarChart([
{ label: 'Femmes', value: f.pctFemmes, color: '#a78bfa' },
{ label: 'Boursiers', value: f.pctBoursiers, color: '#fb923c' },
{ label: 'Néo-bac', value: f.pctNeoBac, color: '#2dd4bf' }
])
}
},
async loadHistory() {
var f = this.props.formation
if (!f) return
// Extraire le cod_uai de l'id (format: codUai-nomFormation)
var codUai = f.id.split('-')[0]
if (!codUai || !window.fetchFormationHistory) return
this.update({ loadingHistory: true })
try {
var history = await window.fetchFormationHistory(codUai, f.nom)
this.update({ history: history, loadingHistory: false })
} catch (e) {
console.error('Erreur historique:', e)
this.update({ history: [], loadingHistory: false })
}
},
renderHistoryCharts() {
var hist = this.state.history
if (!hist || hist.length === 0) return
var chartTaux = this.$('[ref="chartTaux"]')
var chartCandidats = this.$('[ref="chartCandidats"]')
var chartMentionsHist = this.$('[ref="chartMentionsHist"]')
// Graphique : taux d'accès par année
if (chartTaux) {
var items = []
for (var i = 0; i < hist.length; i++) {
items.push({
label: '' + hist[i].annee,
value: hist[i].tauxAcces,
color: '#1a936f'
})
}
chartTaux.innerHTML = this.buildColumnChart(items)
}
// Graphique : candidats vs admis (normalisé sur le max)
if (chartCandidats) {
var maxCandidats = 0
for (var i = 0; i < hist.length; i++) {
if (hist[i].candidats > maxCandidats) maxCandidats = hist[i].candidats
}
var rows = ''
for (var i = 0; i < hist.length; i++) {
var h = hist[i]
var sizeCand = maxCandidats > 0 ? Math.round((h.candidats / maxCandidats) * 100) / 100 : 0
var sizeAdmis = maxCandidats > 0 ? Math.round((h.admis / maxCandidats) * 100) / 100 : 0
rows += '<tr>'
rows += '<th scope="row">' + h.annee + '</th>'
rows += '<td style="--size: ' + sizeCand + '; --color: #2a5298;">'
rows += '<span class="data">' + h.candidats + '</span></td>'
rows += '<td style="--size: ' + sizeAdmis + '; --color: #1a936f;">'
rows += '<span class="data">' + h.admis + '</span></td>'
rows += '</tr>'
}
chartCandidats.innerHTML = '<table class="charts-css column multiple show-labels show-primary-axis show-4-secondary-axes data-spacing-10">'
+ '<thead><tr><th scope="col">Année</th><th scope="col">Candidats</th><th scope="col">Admis</th></tr></thead>'
+ '<tbody>' + rows + '</tbody></table>'
}
// Tableau : évolution des mentions
if (chartMentionsHist) {
var table = '<table class="detail-table">'
table += '<thead><tr><th>Année</th><th>Sans mention</th><th>AB</th><th>Bien</th><th>TB</th><th>TB Féli.</th></tr></thead>'
table += '<tbody>'
for (var i = 0; i < hist.length; i++) {
var h = hist[i]
table += '<tr>'
table += '<td><b>' + h.annee + '</b></td>'
table += '<td>' + h.pctSansMention + '%</td>'
table += '<td>' + h.pctAB + '%</td>'
table += '<td>' + h.pctB + '%</td>'
table += '<td>' + h.pctTB + '%</td>'
table += '<td>' + h.pctTBF + '%</td>'
table += '</tr>'
}
table += '</tbody></table>'
chartMentionsHist.innerHTML = table
}
},
buildColumnChart(items) {
var rows = ''
for (var i = 0; i < items.length; i++) {
var item = items[i]
var val = this.safe(item.value)
var display = item.value || 0
rows += '<tr>'
rows += '<th scope="row">' + item.label + '</th>'
rows += '<td style="--size: ' + val + '; --color: ' + item.color + ';">'
rows += '<span class="data">' + display + '%</span>'
rows += '</td></tr>'
}
return '<table class="charts-css column show-labels show-primary-axis show-4-secondary-axes data-spacing-10">'
+ '<thead><tr><th scope="col">Type</th><th scope="col">%</th></tr></thead>'
+ '<tbody>' + rows + '</tbody></table>'
},
buildBarChart(items) {
var rows = ''
for (var i = 0; i < items.length; i++) {
var item = items[i]
var val = this.safe(item.value)
var display = item.value || 0
rows += '<tr>'
rows += '<th scope="row">' + item.label + '</th>'
rows += '<td style="--size: ' + val + '; --color: ' + item.color + ';">'
rows += '<span class="data">' + display + '%</span>'
rows += '</td></tr>'
}
return '<table class="charts-css bar show-labels show-primary-axis show-4-secondary-axes data-spacing-14">'
+ '<thead><tr><th scope="col">Catégorie</th><th scope="col">%</th></tr></thead>'
+ '<tbody>' + rows + '</tbody></table>'
} }
} }
</script> </script>
+52 -14
View File
@@ -7,7 +7,7 @@
<script> <script>
export default { export default {
onMounted() { onMounted() {
const mapElement = this.$('div[ref="map"]') var mapElement = this.$('div[ref="map"]')
this.map = L.map(mapElement).setView([46.8, 2.5], 6) this.map = L.map(mapElement).setView([46.8, 2.5], 6)
@@ -16,21 +16,37 @@
}).addTo(this.map) }).addTo(this.map)
this.markersLayer = L.layerGroup().addTo(this.map) this.markersLayer = L.layerGroup().addTo(this.map)
this.markersById = {}
this.refreshMarkers() this.refreshMarkers()
// important : Leaflet calcule parfois mal la taille au montage var self = this
setTimeout(() => {
this.map.invalidateSize() setTimeout(function() {
}, 100) if (self.map) self.map.invalidateSize()
}, 200)
setTimeout(function() {
if (self.map) self.map.invalidateSize()
}, 500)
window.mapFocus = function(id) {
self.focusFormation(id)
}
}, },
onUpdated() { onUpdated() {
this.refreshMarkers() this.refreshMarkers()
var self = this
if (this.map) { if (this.map) {
setTimeout(() => { setTimeout(function() {
this.map.invalidateSize() self.map.invalidateSize()
}, 50) }, 100)
setTimeout(function() {
self.map.invalidateSize()
}, 300)
} }
}, },
@@ -39,6 +55,7 @@
this.map.remove() this.map.remove()
this.map = null this.map = null
} }
window.mapFocus = null
}, },
refreshMarkers() { refreshMarkers() {
@@ -47,18 +64,20 @@
} }
this.markersLayer.clearLayers() this.markersLayer.clearLayers()
this.markersById = {}
const points = [] var points = []
const results = this.props.results || [] var results = this.props.results || []
for (let i = 0; i < results.length; i++) { for (var i = 0; i < results.length; i++) {
const f = results[i] var f = results[i]
if (f.latitude != null && f.longitude != null) { if (f.latitude != null && f.longitude != null) {
const marker = L.marker([f.latitude, f.longitude]) var marker = L.marker([f.latitude, f.longitude])
marker.bindPopup(`<b>${f.nom}</b><br>${f.ville}`) marker.bindPopup('<b>' + f.nom + '</b><br>' + f.ville)
marker.addTo(this.markersLayer) marker.addTo(this.markersLayer)
this.markersById[f.id] = marker
points.push([f.latitude, f.longitude]) points.push([f.latitude, f.longitude])
} }
} }
@@ -68,6 +87,25 @@
} else { } else {
this.map.setView([46.8, 2.5], 6) this.map.setView([46.8, 2.5], 6)
} }
},
focusFormation(id) {
var marker = this.markersById[id]
if (marker && this.map) {
var mapEl = this.$('div[ref="map"]')
if (mapEl) {
mapEl.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
var self = this
setTimeout(function() {
self.map.invalidateSize()
self.map.setView(marker.getLatLng(), 13, { animate: true })
marker.openPopup()
}, 400)
}
} }
} }
</script> </script>
+11
View File
@@ -17,6 +17,17 @@
<button onclick={ () => props.ondetail(i) }>Voir détail</button> <button onclick={ () => props.ondetail(i) }>Voir détail</button>
<button onclick={ () => props.onselect(i) }>Ajouter à la sélection</button> <button onclick={ () => props.onselect(i) }>Ajouter à la sélection</button>
<button onclick={ () => locateOnMap(f) } if={ f.latitude != null }>Localiser</button>
</div> </div>
</div> </div>
<script>
export default {
locateOnMap(f) {
if (window.mapFocus) {
window.mapFocus(f.id)
}
}
}
</script>
</result-list> </result-list>
+110 -3
View File
@@ -2,25 +2,132 @@
<div class="search-bar"> <div class="search-bar">
<input <input
type="text" type="text"
placeholder="Ex : BUT informatique" placeholder="Ex : BUT informatique, Licence droit..."
oninput={ updateQuery } oninput={ updateQuery }
value={ state.query } value={ state.query }
onkeydown={ handleKey }
/> />
<button onclick={ submitSearch }>Rechercher</button> <button onclick={ submitSearch }>Rechercher</button>
</div> </div>
<div class="filters-toggle">
<button class="btn btn-small btn-outline" onclick={ toggleFilters }>
{ state.showFilters ? 'Masquer les filtres' : 'Filtres avancés' }
</button>
</div>
<div class="filters-panel" if={ state.showFilters }>
<div class="filter-row">
<div class="filter-item">
<label>Type de formation</label>
<select onchange={ updateFiliere }>
<option value="">Tous</option>
<option value="BTS">BTS</option>
<option value="BUT">BUT</option>
<option value="Licence">Licence</option>
<option value="Licence_Las">Licence - L.AS</option>
<option value="CPGE">CPGE</option>
<option value="BTS Agricole">BTS Agricole</option>
<option value="DN MADE">DN MADE</option>
<option value="DCG">DCG</option>
<option value="Ecole de Commerce">École de Commerce</option>
<option value="Ecole d'Ingénieurs">École d'Ingénieurs</option>
<option value="IFSI">IFSI</option>
<option value="PASS">PASS</option>
<option value="EFTS">EFTS</option>
</select>
</div>
<div class="filter-item">
<label>Sélectivité</label>
<select onchange={ updateSelectivite }>
<option value="">Toutes</option>
<option value="formation sélective">Sélective</option>
<option value="formation non sélective">Non sélective</option>
</select>
</div>
<div class="filter-item">
<label>Région</label>
<select onchange={ updateRegion }>
<option value="">Toutes</option>
<option value="Auvergne-Rhône-Alpes">Auvergne-Rhône-Alpes</option>
<option value="Bourgogne-Franche-Comté">Bourgogne-Franche-Comté</option>
<option value="Bretagne">Bretagne</option>
<option value="Centre-Val de Loire">Centre-Val de Loire</option>
<option value="Corse">Corse</option>
<option value="Grand Est">Grand Est</option>
<option value="Guadeloupe">Guadeloupe</option>
<option value="Guyane">Guyane</option>
<option value="Hauts-de-France">Hauts-de-France</option>
<option value="Ile-de-France">Île-de-France</option>
<option value="La Réunion">La Réunion</option>
<option value="Martinique">Martinique</option>
<option value="Mayotte">Mayotte</option>
<option value="Normandie">Normandie</option>
<option value="Nouvelle-Aquitaine">Nouvelle-Aquitaine</option>
<option value="Occitanie">Occitanie</option>
<option value="Pays de la Loire">Pays de la Loire</option>
<option value="Provence-Alpes-Côte d'Azur">PACA</option>
</select>
</div>
</div>
<div class="filter-row">
<div class="filter-item">
<label>Taux d'accès min (%)</label>
<input type="number" min="0" max="100" value={ state.tauxMin } oninput={ updateTauxMin } placeholder="0" />
</div>
<div class="filter-item">
<label>Taux d'accès max (%)</label>
<input type="number" min="0" max="100" value={ state.tauxMax } oninput={ updateTauxMax } placeholder="100" />
</div>
</div>
</div>
<script> <script>
export default { export default {
state: { state: {
query: '' query: '',
showFilters: false,
filiere: '',
selectivite: '',
region: '',
tauxMin: 0,
tauxMax: 100
}, },
updateQuery(e) { updateQuery(e) {
this.update({ query: e.target.value }) this.update({ query: e.target.value })
}, },
handleKey(e) {
if (e.key === 'Enter') {
this.submitSearch()
}
},
toggleFilters() {
this.update({ showFilters: !this.state.showFilters })
},
updateFiliere(e) { this.update({ filiere: e.target.value }) },
updateSelectivite(e) { this.update({ selectivite: e.target.value }) },
updateRegion(e) { this.update({ region: e.target.value }) },
updateTauxMin(e) { this.update({ tauxMin: Number(e.target.value) }) },
updateTauxMax(e) { this.update({ tauxMax: Number(e.target.value) }) },
submitSearch() { submitSearch() {
this.props.onsearch(this.state.query) var filters = {
filiere: this.state.filiere,
selectivite: this.state.selectivite,
region: this.state.region,
tauxMin: this.state.tauxMin,
tauxMax: this.state.tauxMax
}
this.props.onsearch(this.state.query, filters)
} }
} }
</script> </script>
View File
+26 -7
View File
@@ -12,7 +12,7 @@
<script src="https://cdn.jsdelivr.net/npm/riot@9/riot+compiler.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/riot@9/riot+compiler.min.js"></script>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
</head> </head>
<body> <body>
<app></app> <app></app>
<script src="./components/search-bar.riot" type="riot"></script> <script src="./components/search-bar.riot" type="riot"></script>
@@ -22,17 +22,36 @@
<script src="./app.riot" type="riot"></script> <script src="./app.riot" type="riot"></script>
<script type="module"> <script type="module">
import { fetchFormations } from './api.js' import { fetchFormations, fetchFormationHistory } from './api.js'
import { createFormation } from './formation.js' import { createFormation } from './formation.js'
import {
auth,
db,
createAccount,
login,
logout,
onUserChanged,
saveUserData,
loadUserData
} from './firebase.js'
window.fetchFormations = fetchFormations window.fetchFormations = fetchFormations
window.createFormation = createFormation window.createFormation = createFormation
</script> window.fetchFormationHistory = fetchFormationHistory
<script> window.firebaseServices = {
riot.compile().then(() => { auth,
db,
createAccount,
login,
logout,
onUserChanged,
saveUserData,
loadUserData
}
await riot.compile()
riot.mount('app') riot.mount('app')
})
</script> </script>
</body> </body>
</html> </html>
+232 -131
View File
@@ -20,7 +20,6 @@
--rayon: 6px; --rayon: 6px;
} }
/* --- Reset & base --- */
*, *::before, *::after { *, *::before, *::after {
box-sizing: border-box; box-sizing: border-box;
} }
@@ -35,7 +34,7 @@
} }
/* =========================================================== /* ===========================================================
HEADER — Bandeau Parcoursup HEADER
=========================================================== */ =========================================================== */
.site-header { .site-header {
@@ -66,9 +65,7 @@
gap: 10px; gap: 10px;
} }
.logo-icon { .logo-icon { font-size: 22px; }
font-size: 22px;
}
.logo-text { .logo-text {
font-size: 18px; font-size: 18px;
@@ -92,6 +89,46 @@
border: 1px solid #b8e0cd; border: 1px solid #b8e0cd;
} }
/* --- Header nav --- */
.header-nav {
display: flex;
align-items: center;
gap: 4px;
}
.nav-link {
color: var(--gris-500);
text-decoration: none;
padding: 6px 14px;
border-radius: var(--rayon);
font-size: 14px;
font-weight: 600;
transition: all 0.15s;
display: inline-flex;
align-items: center;
gap: 6px;
}
.nav-link:hover {
color: var(--vert);
background: var(--vert-clair);
}
.nav-active {
color: var(--vert-fonce);
background: var(--vert-clair);
border: 1px solid #b8e0cd;
}
.nav-badge {
background: var(--vert);
color: white;
font-size: 11px;
font-weight: 800;
padding: 1px 7px;
border-radius: 10px;
}
/* =========================================================== /* ===========================================================
PAGE PAGE
=========================================================== */ =========================================================== */
@@ -119,41 +156,18 @@
transition: all 0.15s; transition: all 0.15s;
} }
.btn:disabled { .btn:disabled { opacity: 0.4; cursor: not-allowed; }
opacity: 0.4;
cursor: not-allowed;
}
.btn-primary { .btn-primary { background: var(--vert); color: white; }
background: var(--vert); .btn-primary:hover:not(:disabled) { background: var(--vert-fonce); }
color: white;
}
.btn-primary:hover:not(:disabled) {
background: var(--vert-fonce);
}
.btn-outline { .btn-outline { background: white; color: var(--vert); border: 1.5px solid var(--vert); }
background: white; .btn-outline:hover:not(:disabled) { background: var(--vert-clair); }
color: var(--vert);
border: 1.5px solid var(--vert);
}
.btn-outline:hover:not(:disabled) {
background: var(--vert-clair);
}
.btn-danger { .btn-danger { background: #fef2f2; color: var(--rouge); border: 1px solid #f5c6cb; }
background: #fef2f2; .btn-danger:hover { background: #fde8e8; }
color: var(--rouge);
border: 1px solid #f5c6cb;
}
.btn-danger:hover {
background: #fde8e8;
}
.btn-small { .btn-small { padding: 5px 12px; font-size: 13px; }
padding: 5px 12px;
font-size: 13px;
}
/* =========================================================== /* ===========================================================
SEARCH BAR SEARCH BAR
@@ -193,13 +207,7 @@
transition: background 0.15s; transition: background 0.15s;
} }
.search-bar button:hover { .search-bar button:hover { background: var(--vert-fonce); }
background: var(--vert-fonce);
}
/* ===========================================================
RESULT COUNT
=========================================================== */
.result-count { .result-count {
font-size: 14px; font-size: 14px;
@@ -208,7 +216,7 @@
} }
/* =========================================================== /* ===========================================================
CARDS — Résultats + comparateur items CARDS
=========================================================== */ =========================================================== */
.card { .card {
@@ -219,9 +227,14 @@
transition: border-color 0.15s; transition: border-color 0.15s;
} }
.card:hover { .card:hover { border-color: var(--vert); }
border-color: var(--vert);
} /* Pas de hover vert sur les cards colorées du comparateur */
.card-tres-favorable:hover { border-color: #6ec89b; }
.card-favorable:hover { border-color: #a3d9b8; }
.card-possible:hover { border-color: #f6e05e; }
.card-difficile:hover { border-color: #f5c6cb; }
.card-tres-difficile:hover { border-color: #f5a3a3; }
.card h3, .card h4 { .card h3, .card h4 {
margin-top: 0; margin-top: 0;
@@ -249,12 +262,91 @@
transition: all 0.15s; transition: all 0.15s;
} }
.card button:hover { .card button:hover { background: var(--vert-clair); }
background: var(--vert-clair); .card button + button { margin-left: 8px; }
/* --- Cards colorées du comparateur --- */
.card-tres-favorable {
background: #e8f5ef;
border-color: #6ec89b;
border-left: 4px solid #147a5c;
} }
.card button + button { .card-favorable {
margin-left: 8px; background: #eaf7ed;
border-color: #a3d9b8;
border-left: 4px solid #1a936f;
}
.card-possible {
background: #fef9e7;
border-color: #f6e05e;
border-left: 4px solid #d69e2e;
}
.card-difficile {
background: #fef2f2;
border-color: #f5c6cb;
border-left: 4px solid #e67e22;
}
.card-tres-difficile {
background: #fde8e8;
border-color: #f5a3a3;
border-left: 4px solid #c0392b;
}
/* ===========================================================
BADGES ESTIMATION — 5 niveaux
=========================================================== */
.estimate {
display: inline-block;
padding: 4px 14px;
border-radius: 4px;
font-size: 13px;
font-weight: 700;
}
.tres-favorable {
background: #147a5c;
color: white;
}
.favorable {
background: var(--vert-clair);
color: var(--vert-fonce);
border: 1px solid #a3d9c0;
}
.possible {
background: #fef9e7;
color: #b7791f;
border: 1px solid #f6e05e;
}
.difficile {
background: #fef2f2;
color: #c05621;
border: 1px solid #f5c6cb;
}
.tres-difficile {
background: var(--rouge);
color: white;
}
.estimation-result {
margin-top: 10px;
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}
.estimation-detail {
font-size: 12px;
color: var(--gris-500);
} }
/* =========================================================== /* ===========================================================
@@ -342,36 +434,6 @@
color: var(--gris-500); color: var(--gris-500);
} }
/* ===========================================================
BADGES ESTIMATION
=========================================================== */
.estimate {
display: inline-block;
padding: 3px 12px;
border-radius: 4px;
font-size: 13px;
font-weight: 700;
}
.favorable {
background: var(--vert-clair);
color: var(--vert-fonce);
border: 1px solid #a3d9c0;
}
.possible {
background: #fef9e7;
color: #b7791f;
border: 1px solid #f6e05e;
}
.difficile {
background: #fef2f2;
color: var(--rouge);
border: 1px solid #f5c6cb;
}
/* =========================================================== /* ===========================================================
LAYOUT LAYOUT
=========================================================== */ =========================================================== */
@@ -387,10 +449,6 @@
gap: 12px; gap: 12px;
} }
/* ===========================================================
MESSAGE (loading / no result)
=========================================================== */
.message { .message {
padding: 16px; padding: 16px;
background: white; background: white;
@@ -423,12 +481,10 @@
} }
/* =========================================================== /* ===========================================================
DETAIL VIEW — Fiche formation DETAIL VIEW
=========================================================== */ =========================================================== */
.detail-page { .detail-page { background: transparent; }
background: transparent;
}
.formation-title { .formation-title {
color: var(--bleu); color: var(--bleu);
@@ -461,7 +517,6 @@
margin-bottom: 28px; margin-bottom: 28px;
} }
/* --- Tableaux --- */
.detail-table { .detail-table {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
@@ -490,7 +545,6 @@
background: var(--gris-100); background: var(--gris-100);
} }
/* --- Timeline --- */
.timeline-box { .timeline-box {
background: white; background: white;
padding: 18px 22px; padding: 18px 22px;
@@ -498,10 +552,7 @@
border: 1px solid var(--gris-200); border: 1px solid var(--gris-200);
} }
.timeline-box h3 { .timeline-box h3 { margin-top: 0; color: var(--gris-900); }
margin-top: 0;
color: var(--gris-900);
}
.timeline { .timeline {
position: relative; position: relative;
@@ -510,10 +561,7 @@
border-left: 3px solid #b8e0cd; border-left: 3px solid #b8e0cd;
} }
.timeline-item { .timeline-item { position: relative; margin-bottom: 32px; }
position: relative;
margin-bottom: 32px;
}
.timeline-dot { .timeline-dot {
position: absolute; position: absolute;
@@ -557,9 +605,7 @@
font-weight: 600; font-weight: 600;
} }
.chart-full { .chart-full { grid-column: 1 / -1; }
grid-column: 1 / -1;
}
#chart-bac .column, #chart-bac .column,
#chart-mentions .column { #chart-mentions .column {
@@ -579,6 +625,18 @@
font-size: 13px; font-size: 13px;
} }
#chart-evolution-taux .column {
height: 220px;
max-width: 100%;
margin: 0 auto;
}
#chart-evolution-candidats .column {
height: 220px;
max-width: 100%;
margin: 0 auto;
}
.chart-wrapper .data { .chart-wrapper .data {
font-size: 11px; font-size: 11px;
font-weight: 700; font-weight: 700;
@@ -586,7 +644,6 @@
text-shadow: 0 1px 2px rgba(0,0,0,0.25); text-shadow: 0 1px 2px rgba(0,0,0,0.25);
} }
/* --- Bouton retour --- */
.btn-retour { .btn-retour {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
@@ -603,42 +660,86 @@
transition: background 0.15s; transition: background 0.15s;
} }
.btn-retour:hover { .btn-retour:hover { background: var(--vert-fonce); }
background: var(--vert-fonce);
}
/* =========================================================== /* ===========================================================
RESPONSIVE RESPONSIVE
=========================================================== */ =========================================================== */
@media (max-width: 900px) { /* --- Badge cliquable --- */
.detail-grid { .badge-clickable {
grid-template-columns: 1fr; cursor: pointer;
transition: all 0.15s;
} }
.badge-clickable:hover {
background: #c6f0dc;
border-color: #6ec89b;
}
/* --- Filtres avancés --- */
.filters-toggle {
margin: -10px 0 16px;
}
.filters-panel {
background: white;
border: 1px solid var(--gris-200);
border-radius: var(--rayon);
padding: 16px 20px;
margin-bottom: 20px;
}
.filter-row {
display: flex;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.filter-row:last-child {
margin-bottom: 0;
}
.filter-item {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 160px;
flex: 1;
}
.filter-item label {
font-size: 12px;
font-weight: 600;
color: var(--gris-500);
}
.filter-item select,
.filter-item input {
padding: 8px 10px;
font-size: 14px;
border: 1.5px solid var(--gris-300);
border-radius: var(--rayon);
background: white;
outline: none;
}
.filter-item select:focus,
.filter-item input:focus {
border-color: var(--vert);
}
@media (max-width: 900px) {
.detail-grid { grid-template-columns: 1fr; }
} }
@media (max-width: 700px) { @media (max-width: 700px) {
.charts-section { .charts-section { grid-template-columns: 1fr; }
grid-template-columns: 1fr; .site-header { padding: 0 14px; }
.logo-text { font-size: 16px; }
.page { padding: 16px 12px 30px; }
.search-bar { flex-direction: column; }
.search-bar button { width: 100%; }
} }
.site-header {
padding: 0 14px;
}
.logo-text {
font-size: 16px;
}
.page {
padding: 16px 12px 30px;
}
.search-bar {
flex-direction: column;
}
.search-bar button {
width: 100%;
}
}