add embed, research pages
This commit is contained in:
parent
2238380e80
commit
2c95bea01b
24 changed files with 2925 additions and 251 deletions
|
@ -277,150 +277,6 @@
|
|||
<h3 class="event-list-group-title"></h3>
|
||||
<ul class="event-list-group">
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4097/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Sosial samling</p>
|
||||
<p class="event-entry-location">Oslo, Norway</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
9th October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4098/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Mapatona de Estradas Rurais - Zona Sudeste de Rio Paranaíba</p>
|
||||
<p class="event-entry-location">Rio Paranaíba, Minas Gerais, Brazil</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
9th October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4059/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Mappy Hour OSM España</p>
|
||||
<p class="event-entry-location">Madrid, Community of Madrid, Spain</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
9th October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4074/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Mapathon Bliesgau, Saarpfalz-Kreis</p>
|
||||
<p class="event-entry-location">Homburg, Saarland, Germany</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
9th October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/3786/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">OpenStreetMap Midwest Meetup</p>
|
||||
<p class="event-entry-location">Ohio, United States</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
9th October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4079/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">OSM-Treffen in Bochum</p>
|
||||
<p class="event-entry-location">Bochum, North Rhine-Westphalia, Germany</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
9th October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4076/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">[Online] 🇧🇷 Oficina de validação com editor JOSM</p>
|
||||
<p class="event-entry-location">Rio de Janeiro, Rio de Janeiro, Brazil</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
9th October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/3917/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">208. OSM-Stammtisch Berlin-Brandenburg</p>
|
||||
<p class="event-entry-location">Berlin, Germany</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
10th October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4072/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Wikigita geologica Su e giù per l'antico Mare Padano</p>
|
||||
<p class="event-entry-location">Castell'Arquato, Emilia-Romagna, Italy</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
11th October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/3918/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">OSM Hackweekend Berlin-Brandenburg 10/2025</p>
|
||||
|
@ -434,38 +290,6 @@
|
|||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4080/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Cartopartie cyclable</p>
|
||||
<p class="event-entry-location">Étalle, Luxembourg, Belgium</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
11th October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4099/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">OSM Bhopal Mapping Party 0</p>
|
||||
<p class="event-entry-location">Bhopal, Madhya Pradesh, India</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
11th October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
|
@ -613,15 +437,31 @@
|
|||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/3864/" class="event-list-entry-box">
|
||||
<li class="event-list-entry"><a href="/event/4076/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Open Transport Community Conference (ÖBB)</p>
|
||||
|
||||
<p class="event-entry-name">[Online] 🇧🇷 Oficina de validação com editor JOSM</p>
|
||||
<p class="event-entry-location">Rio de Janeiro, Rio de Janeiro, Brazil</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
17th–18th October
|
||||
15th October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4120/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Mapping Party Semanal LATAM | Weekly LATAM Mapping Party</p>
|
||||
<p class="event-entry-location">Mexico City, Mexico City, Mexico</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
16th October
|
||||
|
||||
|
||||
|
||||
|
@ -642,6 +482,54 @@
|
|||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/3864/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Open Transport Community Conference (ÖBB)</p>
|
||||
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
17th–18th October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4124/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Mapathon Mini Party</p>
|
||||
<p class="event-entry-location">Albert-Eden, Auckland, New Zealand</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
17th October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4118/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Potsdamer Mappertreffen</p>
|
||||
<p class="event-entry-location">Potsdam, Brandenburg, Germany</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
17th October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
|
@ -658,6 +546,22 @@
|
|||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4119/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Sugar House / 21st South Reconstruction Survey</p>
|
||||
<p class="event-entry-location">Salt Lake City, Utah, United States</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
18th October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
|
@ -674,6 +578,22 @@
|
|||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4123/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Northwestern University Map-A-Thon</p>
|
||||
<p class="event-entry-location">Evanston, Illinois, United States</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
21st October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
|
@ -802,6 +722,22 @@
|
|||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4122/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Réunion du groupe local de Vienne</p>
|
||||
<p class="event-entry-location">Saint-Romain-en-Gal, Auvergne-Rhône-Alpes, France</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
22nd October
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
|
@ -1036,10 +972,10 @@
|
|||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/3738/" class="event-list-entry-box">
|
||||
<li class="event-list-entry"><a href="/event/3991/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Missing Maps London: (Online) Mapathon [eng]</p>
|
||||
|
||||
<p class="event-entry-name">East Midlands pub meet-up</p>
|
||||
<p class="event-entry-location">Derby, England, United Kingdom</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
@ -1052,10 +988,10 @@
|
|||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/3991/" class="event-list-entry-box">
|
||||
<li class="event-list-entry"><a href="/event/3738/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">East Midlands pub meet-up</p>
|
||||
<p class="event-entry-location">Derby, England, United Kingdom</p>
|
||||
<p class="event-entry-name">Missing Maps London: (Online) Mapathon [eng]</p>
|
||||
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
@ -1257,6 +1193,22 @@
|
|||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4125/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">FOSS4G International Conference 2025 - Auckland</p>
|
||||
<p class="event-entry-location">Waitematā, Auckland, New Zealand</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
17th–21st November
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
|
@ -1491,10 +1443,10 @@
|
|||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/3739/" class="event-list-entry-box">
|
||||
<li class="event-list-entry"><a href="/event/4104/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Missing Maps London: (Online) Mapathon [eng]</p>
|
||||
|
||||
<p class="event-entry-name">East Midlands pub meet-up</p>
|
||||
<p class="event-entry-location">Derby, England, United Kingdom</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
@ -1507,10 +1459,10 @@
|
|||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4104/" class="event-list-entry-box">
|
||||
<li class="event-list-entry"><a href="/event/3739/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">East Midlands pub meet-up</p>
|
||||
<p class="event-entry-location">Derby, England, United Kingdom</p>
|
||||
<p class="event-entry-name">Missing Maps London: (Online) Mapathon [eng]</p>
|
||||
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
@ -1619,10 +1571,10 @@
|
|||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4026/" class="event-list-entry-box">
|
||||
<li class="event-list-entry"><a href="/event/3789/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Münchner OSM-Treffen</p>
|
||||
<p class="event-entry-location">Munich, Bavaria, Germany</p>
|
||||
<p class="event-entry-name">OpenStreetMap Midwest Meetup</p>
|
||||
<p class="event-entry-location">Ohio, United States</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
@ -1635,10 +1587,10 @@
|
|||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/3789/" class="event-list-entry-box">
|
||||
<li class="event-list-entry"><a href="/event/4026/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">OpenStreetMap Midwest Meetup</p>
|
||||
<p class="event-entry-location">Ohio, United States</p>
|
||||
<p class="event-entry-name">Münchner OSM-Treffen</p>
|
||||
<p class="event-entry-location">Munich, Bavaria, Germany</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
@ -1896,6 +1848,22 @@
|
|||
<h3 class="event-list-group-title">March 2026</h3>
|
||||
<ul class="event-list-group">
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4114/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">OSM India - Monthly Online Mapathon</p>
|
||||
<p class="event-entry-location">New Delhi, India</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
1st March
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4077/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">Rencontre OpenStreetMap et Territoires</p>
|
||||
|
@ -1909,6 +1877,91 @@
|
|||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<h3 class="event-list-group-title">April 2026</h3>
|
||||
<ul class="event-list-group">
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4115/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">OSM India - Monthly Online Mapathon</p>
|
||||
<p class="event-entry-location">New Delhi, India</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
5th April
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4121/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">FOSSGIS-OSM-Communitytreffen im Linuxhotel</p>
|
||||
<p class="event-entry-location">Essen, North Rhine-Westphalia, Germany</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
30th April–3rd May
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<h3 class="event-list-group-title">May 2026</h3>
|
||||
<ul class="event-list-group">
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4116/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">OSM India - Monthly Online Mapathon</p>
|
||||
<p class="event-entry-location">New Delhi, India</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
3rd May
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<h3 class="event-list-group-title">June 2026</h3>
|
||||
<ul class="event-list-group">
|
||||
|
||||
<li class="event-list-entry"><a href="/event/4117/" class="event-list-entry-box">
|
||||
<div class="event-entry-main">
|
||||
<p class="event-entry-name">OSM India - Monthly Online Mapathon</p>
|
||||
<p class="event-entry-location">New Delhi, India</p>
|
||||
</div>
|
||||
<span class="event-entry-date">
|
||||
|
||||
|
||||
7th June
|
||||
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</a></li>
|
||||
|
||||
|
|
367
frontend/public/embed.js
Normal file
367
frontend/public/embed.js
Normal file
|
@ -0,0 +1,367 @@
|
|||
/**
|
||||
* OEDB Embed Script
|
||||
* Script d'intégration pour afficher les événements OEDB sur des sites externes
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Configuration par défaut
|
||||
const defaultConfig = {
|
||||
apiUrl: 'https://api.oedb.fr',
|
||||
theme: 'light',
|
||||
limit: 50,
|
||||
width: '100%',
|
||||
height: '400px',
|
||||
showMap: true,
|
||||
showList: true,
|
||||
autoRefresh: false,
|
||||
refreshInterval: 300000 // 5 minutes
|
||||
};
|
||||
|
||||
// Thèmes CSS
|
||||
const themes = {
|
||||
light: {
|
||||
background: '#ffffff',
|
||||
text: '#2c3e50',
|
||||
border: '#ecf0f1',
|
||||
primary: '#3498db',
|
||||
secondary: '#95a5a6'
|
||||
},
|
||||
dark: {
|
||||
background: '#2c3e50',
|
||||
text: '#ecf0f1',
|
||||
border: '#34495e',
|
||||
primary: '#3498db',
|
||||
secondary: '#7f8c8d'
|
||||
}
|
||||
};
|
||||
|
||||
class OEDBEmbed {
|
||||
constructor(container, config) {
|
||||
this.container = typeof container === 'string' ? document.querySelector(container) : container;
|
||||
this.config = { ...defaultConfig, ...config };
|
||||
this.events = [];
|
||||
this.isLoading = false;
|
||||
this.refreshTimer = null;
|
||||
|
||||
if (!this.container) {
|
||||
console.error('OEDB Embed: Container not found');
|
||||
return;
|
||||
}
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.injectStyles();
|
||||
this.render();
|
||||
this.loadEvents();
|
||||
|
||||
if (this.config.autoRefresh) {
|
||||
this.startAutoRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
injectStyles() {
|
||||
if (document.getElementById('oedb-embed-styles')) return;
|
||||
|
||||
const theme = themes[this.config.theme] || themes.light;
|
||||
const styles = `
|
||||
<style id="oedb-embed-styles">
|
||||
.oedb-embed {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: ${theme.background};
|
||||
color: ${theme.text};
|
||||
border: 1px solid ${theme.border};
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
width: ${this.config.width};
|
||||
height: ${this.config.height};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.oedb-embed-header {
|
||||
background: ${theme.primary};
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.oedb-embed-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.oedb-embed-map {
|
||||
flex: 1;
|
||||
min-height: 200px;
|
||||
background: #f8f9fa;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.oedb-embed-list {
|
||||
width: 300px;
|
||||
overflow-y: auto;
|
||||
border-left: 1px solid ${theme.border};
|
||||
background: ${theme.background};
|
||||
}
|
||||
|
||||
.oedb-embed-list-only {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.oedb-embed-map-only {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.oedb-event-item {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid ${theme.border};
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.oedb-event-item:hover {
|
||||
background: ${theme.border};
|
||||
}
|
||||
|
||||
.oedb-event-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: ${theme.text};
|
||||
}
|
||||
|
||||
.oedb-event-meta {
|
||||
font-size: 0.9rem;
|
||||
color: ${theme.secondary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.oedb-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 200px;
|
||||
color: ${theme.secondary};
|
||||
}
|
||||
|
||||
.oedb-error {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: #e74c3c;
|
||||
background: #fdf2f2;
|
||||
}
|
||||
|
||||
.oedb-no-events {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: ${theme.secondary};
|
||||
}
|
||||
|
||||
.oedb-map-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
background: #f8f9fa;
|
||||
color: ${theme.secondary};
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
document.head.insertAdjacentHTML('beforeend', styles);
|
||||
}
|
||||
|
||||
render() {
|
||||
this.container.innerHTML = `
|
||||
<div class="oedb-embed">
|
||||
<div class="oedb-embed-header">
|
||||
Événements OEDB
|
||||
</div>
|
||||
<div class="oedb-embed-content">
|
||||
${this.config.showMap ? '<div class="oedb-embed-map" id="oedb-map"></div>' : ''}
|
||||
${this.config.showList ? `<div class="oedb-embed-list ${!this.config.showMap ? 'oedb-embed-list-only' : ''}" id="oedb-list"></div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async loadEvents() {
|
||||
if (this.isLoading) return;
|
||||
|
||||
this.isLoading = true;
|
||||
this.showLoading();
|
||||
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (this.config.what) params.set('what', this.config.what);
|
||||
if (this.config.start) params.set('start', this.config.start);
|
||||
if (this.config.end) params.set('end', this.config.end);
|
||||
if (this.config.limit) params.set('limit', this.config.limit.toString());
|
||||
if (this.config.bbox) params.set('bbox', this.config.bbox);
|
||||
|
||||
const response = await fetch(`${this.config.apiUrl}/events?${params.toString()}`);
|
||||
const data = await response.json();
|
||||
|
||||
this.events = data.features || [];
|
||||
this.renderEvents();
|
||||
this.renderMap();
|
||||
} catch (error) {
|
||||
console.error('OEDB Embed: Error loading events', error);
|
||||
this.showError('Erreur lors du chargement des événements');
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
const listContainer = document.getElementById('oedb-list');
|
||||
if (listContainer) {
|
||||
listContainer.innerHTML = '<div class="oedb-loading">Chargement des événements...</div>';
|
||||
}
|
||||
}
|
||||
|
||||
showError(message) {
|
||||
const listContainer = document.getElementById('oedb-list');
|
||||
if (listContainer) {
|
||||
listContainer.innerHTML = `<div class="oedb-error">${message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
renderEvents() {
|
||||
const listContainer = document.getElementById('oedb-list');
|
||||
if (!listContainer) return;
|
||||
|
||||
if (this.events.length === 0) {
|
||||
listContainer.innerHTML = '<div class="oedb-no-events">Aucun événement trouvé</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const eventsHtml = this.events.map(event => {
|
||||
const title = event.properties?.label || event.properties?.name || 'Événement sans nom';
|
||||
const date = event.properties?.start || event.properties?.when || '';
|
||||
const location = event.properties?.where || '';
|
||||
const type = event.properties?.what || '';
|
||||
|
||||
return `
|
||||
<div class="oedb-event-item" data-event-id="${event.id || ''}">
|
||||
<div class="oedb-event-title">${this.escapeHtml(title)}</div>
|
||||
<div class="oedb-event-meta">
|
||||
${date ? `<div>📅 ${this.formatDate(date)}</div>` : ''}
|
||||
${location ? `<div>📍 ${this.escapeHtml(location)}</div>` : ''}
|
||||
${type ? `<div>🏷️ ${this.escapeHtml(type)}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
listContainer.innerHTML = eventsHtml;
|
||||
|
||||
// Ajouter les événements de clic
|
||||
listContainer.querySelectorAll('.oedb-event-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const eventId = item.dataset.eventId;
|
||||
this.onEventClick(eventId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderMap() {
|
||||
const mapContainer = document.getElementById('oedb-map');
|
||||
if (!mapContainer) return;
|
||||
|
||||
if (this.events.length === 0) {
|
||||
mapContainer.innerHTML = '<div class="oedb-map-placeholder">Aucun événement à afficher sur la carte</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Pour l'instant, afficher un placeholder
|
||||
// Dans une vraie implémentation, on utiliserait Leaflet ou une autre librairie de cartes
|
||||
mapContainer.innerHTML = `
|
||||
<div class="oedb-map-placeholder">
|
||||
Carte interactive<br>
|
||||
${this.events.length} événement(s) trouvé(s)
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
onEventClick(eventId) {
|
||||
// Émettre un événement personnalisé
|
||||
const event = new CustomEvent('oedb-event-click', {
|
||||
detail: { eventId, event: this.events.find(e => e.id === eventId) }
|
||||
});
|
||||
this.container.dispatchEvent(event);
|
||||
}
|
||||
|
||||
startAutoRefresh() {
|
||||
if (this.refreshTimer) {
|
||||
clearInterval(this.refreshTimer);
|
||||
}
|
||||
|
||||
this.refreshTimer = setInterval(() => {
|
||||
this.loadEvents();
|
||||
}, this.config.refreshInterval);
|
||||
}
|
||||
|
||||
stopAutoRefresh() {
|
||||
if (this.refreshTimer) {
|
||||
clearInterval(this.refreshTimer);
|
||||
this.refreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.stopAutoRefresh();
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
|
||||
// Utilitaires
|
||||
escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
formatDate(dateString) {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('fr-FR', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// API publique
|
||||
window.OEDBEmbed = {
|
||||
init: function(config) {
|
||||
return new OEDBEmbed(config.container, config);
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-initialisation si des éléments avec data-oedb-embed sont présents
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const embedElements = document.querySelectorAll('[data-oedb-embed]');
|
||||
embedElements.forEach(element => {
|
||||
const config = {
|
||||
container: element,
|
||||
...JSON.parse(element.dataset.oedbEmbed || '{}')
|
||||
};
|
||||
new OEDBEmbed(config.container, config);
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
<header>
|
||||
|
||||
|
||||
|
||||
<nav>
|
||||
<a [routerLink]="['/']" routerLinkActive="active"><h1>
|
||||
<img src="/static/oedb.png" alt="OEDB" style="width: 20px; height: 20px;">
|
||||
|
@ -27,6 +27,10 @@
|
|||
|
||||
<a routerLink="/agenda" routerLinkActive="active">agenda</a>
|
||||
<a routerLink="/unlocated-events" routerLinkActive="active">événements non localisés</a>
|
||||
<a routerLink="/batch-edit" routerLinkActive="active">batch edit</a>
|
||||
<a routerLink="/community-upcoming" routerLinkActive="active">community upcoming</a>
|
||||
<a routerLink="/events-docs" routerLinkActive="active">events docs</a>
|
||||
<a routerLink="/research" routerLinkActive="active">research</a>
|
||||
<a routerLink="/nouvelles-categories" routerLinkActive="active">nouvelles catégories</a>
|
||||
<a href="/demo/stats" routerLinkActive="active">stats</a>
|
||||
<a href="https://source.cipherbliss.com/tykayn/oedb-backend" routerLinkActive="active">sources</a>
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { Routes } from '@angular/router';
|
||||
import {Home} from './pages/home/home';
|
||||
import { Agenda } from './pages/agenda/agenda';
|
||||
import { Research } from './pages/research/research';
|
||||
import { Embed } from './pages/embed/embed';
|
||||
import { BatchEdit } from './pages/batch-edit/batch-edit';
|
||||
import { NouvellesCategories } from './pages/nouvelles-categories/nouvelles-categories';
|
||||
import { UnlocatedEventsPage } from './pages/unlocated-events/unlocated-events';
|
||||
import { CommunityUpcoming } from './pages/community-upcoming/community-upcoming';
|
||||
|
@ -23,6 +26,18 @@ export const routes: Routes = [
|
|||
path : 'agenda',
|
||||
component: Agenda
|
||||
},
|
||||
{
|
||||
path: 'research',
|
||||
component: Research
|
||||
},
|
||||
{
|
||||
path: 'embed',
|
||||
component: Embed
|
||||
},
|
||||
{
|
||||
path: 'batch-edit',
|
||||
component: BatchEdit
|
||||
},
|
||||
{
|
||||
path : 'nouvelles-categories',
|
||||
component: NouvellesCategories
|
||||
|
|
|
@ -1,22 +1,40 @@
|
|||
:host{
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
|
||||
a{
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
&.active{
|
||||
background-color: #d4ebff;
|
||||
border-color: #007bff;
|
||||
color: #007bff;
|
||||
}
|
||||
.container{
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* CSS global pour permettre le scroll sur toutes les pages */
|
||||
html, body {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
a{
|
||||
cursor: pointer;
|
||||
padding: 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
color: #000;
|
||||
&.active{
|
||||
background-color: #d4ebff;
|
||||
border-color: #007bff;
|
||||
color: #007bff;
|
||||
}
|
||||
}
|
|
@ -1,11 +1,17 @@
|
|||
<div class="agenda-page">
|
||||
@if (isLoading) {
|
||||
<div class="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Chargement des événements...</p>
|
||||
<div class="layout">
|
||||
<div class="aside">
|
||||
<app-menu></app-menu>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="agenda-layout">
|
||||
|
||||
<div class="main">
|
||||
@if (isLoading) {
|
||||
<div class="loading">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>Chargement des événements...</p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="agenda-layout">
|
||||
<aside class="agenda-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<h3>Agenda</h3>
|
||||
|
@ -17,10 +23,16 @@
|
|||
[available]="availableWhatTypes"
|
||||
[selected]="selectedWhatFilter"
|
||||
(selectedChange)="onWhatFilterChange($event)"></app-what-filter>
|
||||
@if (selectedDate) {
|
||||
<div class="date-filter-info">
|
||||
<small>Filtré par date: {{formatDayHeader(selectedDate)}}</small>
|
||||
<button class="btn-reset-date" (click)="clearDateFilter()">Afficher tous les jours</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="day-groups">
|
||||
@for (group of groupedEvents; track group.dateKey) {
|
||||
<div class="day-group">
|
||||
<div class="day-group" [attr.data-date-key]="group.dateKey">
|
||||
<div class="day-title">{{formatDayHeader(group.date)}}</div>
|
||||
<ul class="event-list">
|
||||
@for (ev of group.items; track ev.id) {
|
||||
|
@ -70,6 +82,8 @@
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
|
@ -1,8 +1,30 @@
|
|||
.agenda-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background: #f8f9fa;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 400px 1fr;
|
||||
grid-template-rows: minmax(100vh, auto);
|
||||
gap: 0;
|
||||
min-height: 100vh;
|
||||
|
||||
&.is-small {
|
||||
grid-template-columns: 100px 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.aside {
|
||||
background: #f8f9fa;
|
||||
border-right: 1px solid #e9ecef;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.main {
|
||||
background: white;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.loading {
|
||||
|
@ -39,7 +61,7 @@
|
|||
display: grid;
|
||||
grid-template-columns: 320px 1fr auto;
|
||||
grid-template-rows: 1fr;
|
||||
height: 100vh;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.agenda-sidebar {
|
||||
|
@ -47,6 +69,7 @@
|
|||
border-right: 1px solid #e9ecef;
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
max-height: 80vh;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
|
|
|
@ -4,9 +4,11 @@ import { FormsModule } from '@angular/forms';
|
|||
import { OedbApi } from '../../services/oedb-api';
|
||||
import { EditForm } from '../../forms/edit-form/edit-form';
|
||||
import { CalendarComponent, CalendarEvent } from './calendar/calendar';
|
||||
import { Menu } from '../home/menu/menu';
|
||||
import oedb from '../../../oedb-types';
|
||||
import { WhatFilterComponent } from '../../shared/what-filter/what-filter';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { subMonths, addMonths, format } from 'date-fns';
|
||||
|
||||
interface OedbEvent {
|
||||
id: string;
|
||||
|
@ -29,7 +31,7 @@ interface OedbEvent {
|
|||
@Component({
|
||||
selector: 'app-agenda',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, EditForm, CalendarComponent, WhatFilterComponent],
|
||||
imports: [CommonModule, FormsModule, EditForm, CalendarComponent, WhatFilterComponent, Menu],
|
||||
templateUrl: './agenda.html',
|
||||
styleUrl: './agenda.scss'
|
||||
})
|
||||
|
@ -47,20 +49,43 @@ export class Agenda implements OnInit {
|
|||
groupedEvents: Array<{ dateKey: string; date: Date; items: OedbEvent[] }> = [];
|
||||
availableWhatTypes: string[] = [];
|
||||
selectedWhatFilter = 'culture';
|
||||
selectedDate: Date | null = null;
|
||||
|
||||
ngOnInit() {
|
||||
// Gérer les paramètres de requête ET les paramètres du fragment
|
||||
this.route.queryParamMap.subscribe(map => {
|
||||
const id = (map.get('id') || '').trim();
|
||||
const what = (map.get('what') || '').trim();
|
||||
const limitParam = map.get('limit');
|
||||
const limit = limitParam ? Number(limitParam) : null;
|
||||
|
||||
// Définir le filtre what avant de charger les événements
|
||||
if (what) {
|
||||
this.selectedWhatFilter = what;
|
||||
}
|
||||
|
||||
if (id) {
|
||||
this.loadSingleEvent(id);
|
||||
} else {
|
||||
this.loadEvents({ what: what || undefined, limit: limit || undefined });
|
||||
}
|
||||
if (what) {
|
||||
this.selectedWhatFilter = what;
|
||||
});
|
||||
|
||||
// Gérer aussi les paramètres du fragment (pour les URLs avec #)
|
||||
this.route.fragment.subscribe(fragment => {
|
||||
console.log('🔗 Fragment reçu:', fragment);
|
||||
if (fragment) {
|
||||
// Nettoyer le fragment en supprimant le & initial s'il existe
|
||||
const cleanFragment = fragment.startsWith('&') ? fragment.substring(1) : fragment;
|
||||
console.log('🧹 Fragment nettoyé:', cleanFragment);
|
||||
const params = new URLSearchParams(cleanFragment);
|
||||
const what = params.get('what');
|
||||
console.log('🎯 Paramètre what extrait:', what);
|
||||
if (what) {
|
||||
this.selectedWhatFilter = what;
|
||||
console.log('✅ Filtre what défini:', this.selectedWhatFilter);
|
||||
this.loadEvents({ what: what, limit: undefined });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -68,23 +93,43 @@ export class Agenda implements OnInit {
|
|||
loadEvents(overrides: { what?: string; limit?: number } = {}) {
|
||||
this.isLoading = true;
|
||||
const today = new Date();
|
||||
const startDate = new Date(today);
|
||||
startDate.setMonth(today.getMonth() - 1); // Charger 1 mois avant
|
||||
const endDate = new Date(today);
|
||||
endDate.setMonth(today.getMonth() + 3); // Charger 3 mois après
|
||||
|
||||
// Calculer startDate : 3 mois avant aujourd'hui (plus robuste avec date-fns)
|
||||
const startDate = subMonths(today, 3);
|
||||
|
||||
// Calculer endDate : 3 mois après aujourd'hui (plus robuste avec date-fns)
|
||||
const endDate = addMonths(today, 3);
|
||||
|
||||
const params: any = {
|
||||
start: startDate.toISOString().split('T')[0],
|
||||
end: endDate.toISOString().split('T')[0],
|
||||
start: format(startDate, 'yyyy-MM-dd'),
|
||||
end: format(endDate, 'yyyy-MM-dd'),
|
||||
what: "culture",
|
||||
limit: overrides.limit ?? 1000
|
||||
};
|
||||
if (overrides.what) params.what = overrides.what;
|
||||
|
||||
console.log('🔍 Chargement des événements avec paramètres:', params);
|
||||
console.log('📅 Plage de dates:', {
|
||||
start: startDate.toISOString(),
|
||||
end: endDate.toISOString(),
|
||||
today: today.toISOString(),
|
||||
what: "culture",
|
||||
startFormatted: format(startDate, 'yyyy-MM-dd'),
|
||||
endFormatted: format(endDate, 'yyyy-MM-dd')
|
||||
});
|
||||
|
||||
this.oedbApi.getEvents(params).subscribe((response: any) => {
|
||||
console.log('📡 Réponse API reçue:', response);
|
||||
this.events = Array.isArray(response?.features) ? response.features : [];
|
||||
console.log('📊 Nombre d\'événements chargés:', this.events.length);
|
||||
this.updateAvailableWhatTypes();
|
||||
this.applyWhatFilter();
|
||||
this.isLoading = false;
|
||||
// Scroller vers le jour actuel après le chargement
|
||||
this.scrollToToday();
|
||||
}, (error) => {
|
||||
console.error('❌ Erreur lors du chargement des événements:', error);
|
||||
this.isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -160,8 +205,10 @@ export class Agenda implements OnInit {
|
|||
}
|
||||
|
||||
onDateClick(date: Date) {
|
||||
// Optionnel : gérer le clic sur une date
|
||||
console.log('Date cliquée:', date);
|
||||
this.selectedDate = date;
|
||||
this.applyDateFilter();
|
||||
this.scrollToDateInSidebar(date);
|
||||
}
|
||||
|
||||
onEventSaved() {
|
||||
|
@ -187,14 +234,23 @@ export class Agenda implements OnInit {
|
|||
}
|
||||
|
||||
applyWhatFilter() {
|
||||
console.log('🔍 Application du filtre what:', this.selectedWhatFilter);
|
||||
console.log('📊 Événements avant filtrage:', this.events.length);
|
||||
|
||||
if (this.selectedWhatFilter) {
|
||||
const prefix = this.selectedWhatFilter;
|
||||
this.filteredEvents = this.events.filter(e => String(e?.properties?.what || '').startsWith(prefix));
|
||||
console.log('✅ Événements après filtrage par', prefix + ':', this.filteredEvents.length);
|
||||
} else {
|
||||
this.filteredEvents = [...this.events];
|
||||
console.log('📋 Aucun filtre appliqué, tous les événements conservés');
|
||||
}
|
||||
this.convertToCalendarEvents();
|
||||
this.buildGroupedEvents();
|
||||
if (this.selectedDate) {
|
||||
this.applyDateFilter();
|
||||
} else {
|
||||
this.buildGroupedEvents();
|
||||
}
|
||||
}
|
||||
|
||||
onWhatFilterChange(value: string) {
|
||||
|
@ -228,6 +284,31 @@ export class Agenda implements OnInit {
|
|||
this.groupedEvents = result;
|
||||
}
|
||||
|
||||
applyDateFilter() {
|
||||
if (!this.selectedDate) {
|
||||
this.buildGroupedEvents();
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedDateKey = this.toDateKey(this.selectedDate);
|
||||
const groups: Record<string, { date: Date; items: OedbEvent[] }> = {};
|
||||
const source = this.filteredEvents.length ? this.filteredEvents : this.events;
|
||||
|
||||
for (const ev of source) {
|
||||
const d = this.getEventStartDate(ev);
|
||||
const key = this.toDateKey(d);
|
||||
if (key === selectedDateKey) {
|
||||
if (!groups[key]) groups[key] = { date: new Date(d.getFullYear(), d.getMonth(), d.getDate()), items: [] };
|
||||
groups[key].items.push(ev);
|
||||
}
|
||||
}
|
||||
|
||||
const result = Object.keys(groups)
|
||||
.sort((a, b) => groups[a].date.getTime() - groups[b].date.getTime())
|
||||
.map(k => ({ dateKey: k, date: groups[k].date, items: groups[k].items.sort((a, b) => this.getEventStartDate(a).getTime() - this.getEventStartDate(b).getTime()) }));
|
||||
this.groupedEvents = result;
|
||||
}
|
||||
|
||||
formatDayHeader(d: Date): string {
|
||||
const days = ['Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi'];
|
||||
const months = ['janvier','février','mars','avril','mai','juin','juillet','août','septembre','octobre','novembre','décembre'];
|
||||
|
@ -253,4 +334,30 @@ export class Agenda implements OnInit {
|
|||
selectFromSidebar(ev: OedbEvent) {
|
||||
this.selectedEvent = ev;
|
||||
}
|
||||
|
||||
scrollToDateInSidebar(date: Date) {
|
||||
setTimeout(() => {
|
||||
const dateKey = this.toDateKey(date);
|
||||
const dayGroupElement = document.querySelector(`[data-date-key="${dateKey}"]`);
|
||||
if (dayGroupElement) {
|
||||
dayGroupElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
scrollToToday() {
|
||||
setTimeout(() => {
|
||||
const today = new Date();
|
||||
const todayKey = this.toDateKey(today);
|
||||
const todayGroupElement = document.querySelector(`[data-date-key="${todayKey}"]`);
|
||||
if (todayGroupElement) {
|
||||
todayGroupElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
clearDateFilter() {
|
||||
this.selectedDate = null;
|
||||
this.buildGroupedEvents();
|
||||
}
|
||||
}
|
178
frontend/src/app/pages/batch-edit/batch-edit.html
Normal file
178
frontend/src/app/pages/batch-edit/batch-edit.html
Normal file
|
@ -0,0 +1,178 @@
|
|||
<div class="batch-edit-page">
|
||||
<app-menu></app-menu>
|
||||
|
||||
<div class="container">
|
||||
<header class="page-header">
|
||||
<h1>Modification en masse</h1>
|
||||
<p>Modifiez plusieurs événements en une seule opération</p>
|
||||
</header>
|
||||
|
||||
<div class="filters-section">
|
||||
<div class="filters-card">
|
||||
<h2>Filtres</h2>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="search">Recherche textuelle :</label>
|
||||
<input
|
||||
id="search"
|
||||
type="text"
|
||||
[(ngModel)]="searchText"
|
||||
placeholder="Rechercher dans les événements..."
|
||||
class="search-input">
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<label for="whatFilter">Type d'événement :</label>
|
||||
<select
|
||||
id="whatFilter"
|
||||
[(ngModel)]="selectedWhatFilter">
|
||||
<option value="">Tous les types</option>
|
||||
@for (whatType of availableWhatTypes; track whatType) {
|
||||
<option [value]="whatType">{{whatType}}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="selection-info">
|
||||
<span class="selection-count">
|
||||
{{selectedEvents().size}} événement(s) sélectionné(s)
|
||||
</span>
|
||||
<div class="selection-actions">
|
||||
<button class="btn btn-secondary" (click)="selectAll()">Tout sélectionner</button>
|
||||
<button class="btn btn-secondary" (click)="clearSelection()">Tout désélectionner</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="batch-operation-section">
|
||||
<div class="operation-card">
|
||||
<h2>Opération en masse</h2>
|
||||
|
||||
<div class="operation-form">
|
||||
<div class="form-group">
|
||||
<label for="operationType">Type d'opération :</label>
|
||||
<select
|
||||
id="operationType"
|
||||
[(ngModel)]="batchOperation().type"
|
||||
(ngModelChange)="onBatchOperationChange()">
|
||||
<option value="none">Sélectionner une opération</option>
|
||||
<option value="changeWhat">Changer le type d'événement</option>
|
||||
<option value="setField">Définir un champ</option>
|
||||
<option value="delete">Supprimer</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@if (batchOperation().type === 'changeWhat') {
|
||||
<div class="form-group">
|
||||
<label for="newWhat">Nouveau type :</label>
|
||||
<select id="newWhat" [(ngModel)]="batchOperation().what">
|
||||
<option value="">Sélectionner un type</option>
|
||||
@for (whatType of availableWhatTypes; track whatType) {
|
||||
<option [value]="whatType">{{whatType}}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (batchOperation().type === 'setField') {
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="fieldKey">Nom du champ :</label>
|
||||
<input
|
||||
id="fieldKey"
|
||||
type="text"
|
||||
[(ngModel)]="batchOperation().fieldKey"
|
||||
placeholder="ex: label, description, where">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fieldValue">Valeur :</label>
|
||||
<input
|
||||
id="fieldValue"
|
||||
type="text"
|
||||
[(ngModel)]="batchOperation().fieldValue"
|
||||
placeholder="Nouvelle valeur">
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="form-actions">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
(click)="applyBatchOperation()"
|
||||
[disabled]="isProcessing() || selectedEvents().size === 0 || batchOperation().type === 'none'">
|
||||
@if (isProcessing()) {
|
||||
⏳ Traitement en cours...
|
||||
} @else {
|
||||
⚡ Appliquer l'opération
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (batchResult()) {
|
||||
<div class="result-summary">
|
||||
<h3>Résultat de l'opération</h3>
|
||||
<div class="result-stats">
|
||||
<div class="stat success">
|
||||
<span class="stat-number">{{batchResult()!.success}}</span>
|
||||
<span class="stat-label">Succès</span>
|
||||
</div>
|
||||
<div class="stat failed">
|
||||
<span class="stat-number">{{batchResult()!.failed}}</span>
|
||||
<span class="stat-label">Échecs</span>
|
||||
</div>
|
||||
<div class="stat network-error">
|
||||
<span class="stat-number">{{batchResult()!.networkErrors}}</span>
|
||||
<span class="stat-label">Erreurs réseau</span>
|
||||
</div>
|
||||
<div class="stat total">
|
||||
<span class="stat-number">{{batchResult()!.total}}</span>
|
||||
<span class="stat-label">Total</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="events-section">
|
||||
<div class="events-header">
|
||||
<h2>Événements ({{filteredEvents.length}})</h2>
|
||||
<div class="view-options">
|
||||
<button class="btn btn-secondary">Vue carte</button>
|
||||
<button class="btn btn-secondary">Vue liste</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="events-content">
|
||||
<div class="events-list">
|
||||
@for (event of filteredEvents; track event.id) {
|
||||
<div
|
||||
class="event-card"
|
||||
[class.selected]="selectedEvents().has(event.id || event.properties?.id)"
|
||||
(click)="toggleEventSelection(event.id || event.properties?.id)">
|
||||
<div class="event-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="selectedEvents().has(event.id || event.properties?.id)"
|
||||
(change)="toggleEventSelection(event.id || event.properties?.id)">
|
||||
</div>
|
||||
<div class="event-content">
|
||||
<div class="event-header">
|
||||
<h4 class="event-title">{{getEventTitle(event)}}</h4>
|
||||
<span class="event-type">{{getEventType(event)}}</span>
|
||||
</div>
|
||||
<div class="event-meta">
|
||||
@if (getEventDate(event)) {
|
||||
<div class="event-date">📅 {{formatDate(getEventDate(event))}}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
365
frontend/src/app/pages/batch-edit/batch-edit.scss
Normal file
365
frontend/src/app/pages/batch-edit/batch-edit.scss
Normal file
|
@ -0,0 +1,365 @@
|
|||
.batch-edit-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f9fa;
|
||||
padding: 2rem 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #6c757d;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.filters-section {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.filters-card {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 2px solid #3498db;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
input, select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
font-size: 1.1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.selection-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
margin-top: 1rem;
|
||||
|
||||
.selection-count {
|
||||
font-weight: 600;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.selection-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.batch-operation-section {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
.operation-card {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 2px solid #e74c3c;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.operation-form {
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
input, select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #ecf0f1;
|
||||
}
|
||||
}
|
||||
|
||||
.result-summary {
|
||||
margin-top: 2rem;
|
||||
padding: 1.5rem;
|
||||
background: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
|
||||
h3 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.result-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 1rem;
|
||||
|
||||
.stat {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
|
||||
.stat-number {
|
||||
display: block;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
|
||||
.stat-number {
|
||||
color: #28a745;
|
||||
}
|
||||
}
|
||||
|
||||
&.failed {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
|
||||
.stat-number {
|
||||
color: #dc3545;
|
||||
}
|
||||
}
|
||||
|
||||
&.network-error {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
|
||||
.stat-number {
|
||||
color: #ffc107;
|
||||
}
|
||||
}
|
||||
|
||||
&.total {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
|
||||
.stat-number {
|
||||
color: #17a2b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.events-section {
|
||||
.events-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #3498db;
|
||||
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.view-options {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.events-content {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
|
||||
.events-list {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
|
||||
.event-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #ecf0f1;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background: #e3f2fd;
|
||||
border-left: 4px solid #3498db;
|
||||
}
|
||||
|
||||
.event-checkbox {
|
||||
margin-right: 1rem;
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.event-content {
|
||||
flex: 1;
|
||||
|
||||
.event-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.event-title {
|
||||
color: #2c3e50;
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.event-type {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.event-meta {
|
||||
.event-date {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.btn-primary {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #c0392b;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
background: #95a5a6;
|
||||
color: white;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #7f8c8d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
239
frontend/src/app/pages/batch-edit/batch-edit.ts
Normal file
239
frontend/src/app/pages/batch-edit/batch-edit.ts
Normal file
|
@ -0,0 +1,239 @@
|
|||
import { Component, inject, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { OedbApi } from '../../services/oedb-api';
|
||||
import { AllEvents } from '../../maps/all-events/all-events';
|
||||
import { Menu } from '../home/menu/menu';
|
||||
|
||||
interface BatchOperation {
|
||||
type: 'changeWhat' | 'setField' | 'delete' | 'none';
|
||||
what?: string;
|
||||
fieldKey?: string;
|
||||
fieldValue?: any;
|
||||
}
|
||||
|
||||
interface BatchResult {
|
||||
success: number;
|
||||
failed: number;
|
||||
networkErrors: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-batch-edit',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, AllEvents, Menu],
|
||||
templateUrl: './batch-edit.html',
|
||||
styleUrl: './batch-edit.scss'
|
||||
})
|
||||
export class BatchEdit {
|
||||
private oedbApi = inject(OedbApi);
|
||||
|
||||
events = signal<any[]>([]);
|
||||
selectedEvents = signal<Set<string | number>>(new Set());
|
||||
batchOperation = signal<BatchOperation>({ type: 'none' });
|
||||
batchResult = signal<BatchResult | null>(null);
|
||||
isLoading = signal<boolean>(false);
|
||||
isProcessing = signal<boolean>(false);
|
||||
|
||||
// Filtres
|
||||
searchText = '';
|
||||
selectedWhatFilter = '';
|
||||
availableWhatTypes: string[] = [];
|
||||
|
||||
constructor() {
|
||||
this.loadEvents();
|
||||
}
|
||||
|
||||
loadEvents() {
|
||||
this.isLoading.set(true);
|
||||
this.oedbApi.getEvents({ limit: 1000 }).subscribe({
|
||||
next: (response: any) => {
|
||||
this.events.set(Array.isArray(response?.features) ? response.features : []);
|
||||
this.updateAvailableWhatTypes();
|
||||
this.isLoading.set(false);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Erreur lors du chargement des événements:', error);
|
||||
this.isLoading.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateAvailableWhatTypes() {
|
||||
const whatTypes = new Set<string>();
|
||||
this.events().forEach(event => {
|
||||
if (event?.properties?.what) {
|
||||
whatTypes.add(event.properties.what);
|
||||
}
|
||||
});
|
||||
this.availableWhatTypes = Array.from(whatTypes).sort();
|
||||
}
|
||||
|
||||
get filteredEvents() {
|
||||
let filtered = this.events();
|
||||
|
||||
if (this.searchText.trim()) {
|
||||
const searchLower = this.searchText.toLowerCase();
|
||||
filtered = filtered.filter(event => {
|
||||
const label = event?.properties?.label || event?.properties?.name || '';
|
||||
const description = event?.properties?.description || '';
|
||||
const what = event?.properties?.what || '';
|
||||
return label.toLowerCase().includes(searchLower) ||
|
||||
description.toLowerCase().includes(searchLower) ||
|
||||
what.toLowerCase().includes(searchLower);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.selectedWhatFilter) {
|
||||
filtered = filtered.filter(event => {
|
||||
const what = event?.properties?.what || '';
|
||||
return what.startsWith(this.selectedWhatFilter + '.') || what === this.selectedWhatFilter;
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
toggleEventSelection(eventId: string | number) {
|
||||
const selected = new Set(this.selectedEvents());
|
||||
if (selected.has(eventId)) {
|
||||
selected.delete(eventId);
|
||||
} else {
|
||||
selected.add(eventId);
|
||||
}
|
||||
this.selectedEvents.set(selected);
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
const allIds = this.filteredEvents.map(event => event.id || event.properties?.id);
|
||||
this.selectedEvents.set(new Set(allIds));
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.selectedEvents.set(new Set());
|
||||
}
|
||||
|
||||
onBatchOperationChange() {
|
||||
this.batchResult.set(null);
|
||||
}
|
||||
|
||||
async applyBatchOperation() {
|
||||
const selectedIds = Array.from(this.selectedEvents());
|
||||
const operation = this.batchOperation();
|
||||
|
||||
if (selectedIds.length === 0 || operation.type === 'none') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isProcessing.set(true);
|
||||
this.batchResult.set(null);
|
||||
|
||||
let success = 0;
|
||||
let failed = 0;
|
||||
let networkErrors = 0;
|
||||
|
||||
try {
|
||||
if (operation.type === 'delete') {
|
||||
for (const id of selectedIds) {
|
||||
try {
|
||||
await this.oedbApi.deleteEvent(id).toPromise();
|
||||
success++;
|
||||
} catch (error: any) {
|
||||
if (error?.status === 0) {
|
||||
networkErrors++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (operation.type === 'changeWhat') {
|
||||
for (const id of selectedIds) {
|
||||
try {
|
||||
const event = this.events().find(e => (e.id || e.properties?.id) === id);
|
||||
if (event) {
|
||||
const updated = {
|
||||
...event,
|
||||
properties: { ...event.properties, what: operation.what }
|
||||
};
|
||||
await this.oedbApi.updateEvent(id, updated).toPromise();
|
||||
success++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error?.status === 0) {
|
||||
networkErrors++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (operation.type === 'setField') {
|
||||
for (const id of selectedIds) {
|
||||
try {
|
||||
const event = this.events().find(e => (e.id || e.properties?.id) === id);
|
||||
if (event && operation.fieldKey) {
|
||||
const updated = {
|
||||
...event,
|
||||
properties: { ...event.properties, [operation.fieldKey]: operation.fieldValue }
|
||||
};
|
||||
await this.oedbApi.updateEvent(id, updated).toPromise();
|
||||
success++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error?.status === 0) {
|
||||
networkErrors++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.batchResult.set({
|
||||
success,
|
||||
failed,
|
||||
networkErrors,
|
||||
total: selectedIds.length
|
||||
});
|
||||
|
||||
// Recharger les événements après l'opération
|
||||
this.loadEvents();
|
||||
this.clearSelection();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de l\'opération en masse:', error);
|
||||
} finally {
|
||||
this.isProcessing.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
getEventTitle(event: any): string {
|
||||
return event?.properties?.label || event?.properties?.name || 'Événement sans nom';
|
||||
}
|
||||
|
||||
getEventType(event: any): string {
|
||||
return event?.properties?.what || '';
|
||||
}
|
||||
|
||||
getEventDate(event: any): string {
|
||||
return event?.properties?.start || event?.properties?.when || '';
|
||||
}
|
||||
|
||||
formatDate(dateString: string): string {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('fr-FR', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
});
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
}
|
142
frontend/src/app/pages/embed/embed.html
Normal file
142
frontend/src/app/pages/embed/embed.html
Normal file
|
@ -0,0 +1,142 @@
|
|||
<div class="embed-page">
|
||||
<div class="layout">
|
||||
<div class="aside">
|
||||
<app-menu></app-menu>
|
||||
</div>
|
||||
|
||||
<div class="main">
|
||||
<div class="container">
|
||||
<header class="page-header">
|
||||
<h1>Intégration OEDB</h1>
|
||||
<p>Générez un code d'intégration pour afficher les événements OEDB sur votre site web</p>
|
||||
</header>
|
||||
|
||||
<div class="embed-config">
|
||||
<div class="config-form">
|
||||
<h2>Configuration</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="apiUrl">URL de l'API :</label>
|
||||
<input
|
||||
id="apiUrl"
|
||||
type="url"
|
||||
[(ngModel)]="config().apiUrl"
|
||||
(ngModelChange)="updateConfig()"
|
||||
placeholder="https://api.oedb.fr">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="what">Type d'événements :</label>
|
||||
<select
|
||||
id="what"
|
||||
[(ngModel)]="config().what"
|
||||
(ngModelChange)="updateConfig()">
|
||||
<option value="culture">Culture</option>
|
||||
<option value="traffic">Trafic</option>
|
||||
<option value="sport">Sport</option>
|
||||
<option value="education">Éducation</option>
|
||||
<option value="">Tous</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="start">Date de début :</label>
|
||||
<input
|
||||
id="start"
|
||||
type="date"
|
||||
[(ngModel)]="config().start"
|
||||
(ngModelChange)="updateConfig()">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="end">Date de fin :</label>
|
||||
<input
|
||||
id="end"
|
||||
type="date"
|
||||
[(ngModel)]="config().end"
|
||||
(ngModelChange)="updateConfig()">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="limit">Nombre d'événements :</label>
|
||||
<input
|
||||
id="limit"
|
||||
type="number"
|
||||
[(ngModel)]="config().limit"
|
||||
(ngModelChange)="updateConfig()"
|
||||
min="1"
|
||||
max="1000">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="width">Largeur :</label>
|
||||
<input
|
||||
id="width"
|
||||
type="text"
|
||||
[(ngModel)]="config().width"
|
||||
(ngModelChange)="updateConfig()"
|
||||
placeholder="100%">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="height">Hauteur :</label>
|
||||
<input
|
||||
id="height"
|
||||
type="text"
|
||||
[(ngModel)]="config().height"
|
||||
(ngModelChange)="updateConfig()"
|
||||
placeholder="400px">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="theme">Thème :</label>
|
||||
<select
|
||||
id="theme"
|
||||
[(ngModel)]="config().theme"
|
||||
(ngModelChange)="updateConfig()">
|
||||
<option value="light">Clair</option>
|
||||
<option value="dark">Sombre</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-output">
|
||||
<div class="code-header">
|
||||
<h2>Code d'intégration</h2>
|
||||
<div class="code-actions">
|
||||
<button class="btn btn-secondary" (click)="preview()">Aperçu</button>
|
||||
<button class="btn btn-primary" (click)="copyToClipboard()">Copier</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-container">
|
||||
<pre><code>{{generatedCode()}}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="usage-info">
|
||||
<h2>Comment utiliser</h2>
|
||||
<ol>
|
||||
<li>Configurez les paramètres ci-dessus selon vos besoins</li>
|
||||
<li>Copiez le code généré</li>
|
||||
<li>Collez-le dans votre page HTML</li>
|
||||
<li>Le script chargera automatiquement les événements depuis l'API OEDB</li>
|
||||
</ol>
|
||||
|
||||
<div class="features">
|
||||
<h3>Fonctionnalités</h3>
|
||||
<ul>
|
||||
<li>✅ Affichage responsive des événements</li>
|
||||
<li>✅ Filtrage par type et dates</li>
|
||||
<li>✅ Thèmes clair et sombre</li>
|
||||
<li>✅ Mise à jour automatique</li>
|
||||
<li>✅ Compatible avec tous les navigateurs</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
220
frontend/src/app/pages/embed/embed.scss
Normal file
220
frontend/src/app/pages/embed/embed.scss
Normal file
|
@ -0,0 +1,220 @@
|
|||
.embed-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f9fa;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
grid-template-columns: 400px 1fr;
|
||||
grid-template-rows: minmax(100vh, auto);
|
||||
gap: 0;
|
||||
min-height: 100vh;
|
||||
|
||||
&.is-small {
|
||||
grid-template-columns: 100px 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.aside {
|
||||
background: #f8f9fa;
|
||||
border-right: 1px solid #e9ecef;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.main {
|
||||
background: white;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #6c757d;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.embed-config {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 2rem;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.config-form {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 2px solid #3498db;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
input, select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.code-output {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
|
||||
.code-header {
|
||||
background: #2c3e50;
|
||||
color: white;
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.code-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.code-container {
|
||||
padding: 1.5rem;
|
||||
background: #f8f9fa;
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
background: #2c3e50;
|
||||
color: #ecf0f1;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
|
||||
code {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.usage-info {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #34495e;
|
||||
margin: 1.5rem 0 1rem 0;
|
||||
}
|
||||
|
||||
ol {
|
||||
padding-left: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
li {
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.features {
|
||||
ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid #ecf0f1;
|
||||
color: #2c3e50;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.btn-primary {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
background: #95a5a6;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: #7f8c8d;
|
||||
}
|
||||
}
|
||||
}
|
23
frontend/src/app/pages/embed/embed.spec.ts
Normal file
23
frontend/src/app/pages/embed/embed.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Embed } from './embed';
|
||||
|
||||
describe('Embed', () => {
|
||||
let component: Embed;
|
||||
let fixture: ComponentFixture<Embed>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Embed]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Embed);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
103
frontend/src/app/pages/embed/embed.ts
Normal file
103
frontend/src/app/pages/embed/embed.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
import { Component, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { Menu } from '../home/menu/menu';
|
||||
|
||||
interface EmbedConfig {
|
||||
apiUrl: string;
|
||||
what: string;
|
||||
start: string;
|
||||
end: string;
|
||||
limit: number;
|
||||
width: string;
|
||||
height: string;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-embed',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, Menu],
|
||||
templateUrl: './embed.html',
|
||||
styleUrl: './embed.scss'
|
||||
})
|
||||
export class Embed {
|
||||
config = signal<EmbedConfig>({
|
||||
apiUrl: 'https://api.oedb.fr',
|
||||
what: 'culture',
|
||||
start: '',
|
||||
end: '',
|
||||
limit: 50,
|
||||
width: '100%',
|
||||
height: '400px',
|
||||
theme: 'light'
|
||||
});
|
||||
|
||||
generatedCode = signal<string>('');
|
||||
|
||||
updateConfig() {
|
||||
const config = this.config();
|
||||
const code = this.generateEmbedCode(config);
|
||||
this.generatedCode.set(code);
|
||||
}
|
||||
|
||||
private generateEmbedCode(config: EmbedConfig): string {
|
||||
const params = new URLSearchParams();
|
||||
if (config.what) params.set('what', config.what);
|
||||
if (config.start) params.set('start', config.start);
|
||||
if (config.end) params.set('end', config.end);
|
||||
if (config.limit) params.set('limit', config.limit.toString());
|
||||
|
||||
const queryString = params.toString();
|
||||
const scriptUrl = `${window.location.origin}/embed.js`;
|
||||
|
||||
return `<!-- Intégration OEDB Embed -->
|
||||
<div id="oedb-events" style="width: ${config.width}; height: ${config.height}; border: 1px solid #ddd; border-radius: 8px; overflow: hidden;"></div>
|
||||
<script src="${scriptUrl}"></script>
|
||||
<script>
|
||||
OEDBEmbed.init({
|
||||
container: '#oedb-events',
|
||||
apiUrl: '${config.apiUrl}',
|
||||
params: {
|
||||
${queryString ? queryString.split('&').map(param => `'${param.split('=')[0]}': '${param.split('=')[1]}'`).join(',\n ') : ''}
|
||||
},
|
||||
theme: '${config.theme}'
|
||||
});
|
||||
</script>`;
|
||||
}
|
||||
|
||||
copyToClipboard() {
|
||||
const code = this.generatedCode();
|
||||
navigator.clipboard.writeText(code).then(() => {
|
||||
// Optionnel : afficher une notification de succès
|
||||
console.log('Code copié dans le presse-papiers');
|
||||
});
|
||||
}
|
||||
|
||||
preview() {
|
||||
// Ouvrir une nouvelle fenêtre avec un aperçu
|
||||
const previewWindow = window.open('', '_blank', 'width=800,height=600');
|
||||
if (previewWindow) {
|
||||
const config = this.config();
|
||||
const code = this.generateEmbedCode(config);
|
||||
previewWindow.document.write(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Aperçu OEDB Embed</title>
|
||||
<style>
|
||||
body { margin: 0; padding: 20px; font-family: Arial, sans-serif; }
|
||||
.preview-container { max-width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Aperçu de l'intégration</h2>
|
||||
<div class="preview-container">
|
||||
${code}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +1,40 @@
|
|||
<menu>
|
||||
OpenEventDatabase
|
||||
<div class="menu-header">
|
||||
<h1>OpenEventDatabase</h1>
|
||||
</div>
|
||||
|
||||
<nav class="nav">
|
||||
<a routerLink="/" class="link">Accueil</a>
|
||||
<a routerLink="/community-upcoming" class="link">Community à venir</a>
|
||||
<a routerLink="/events-docs" class="link">Docs événements</a>
|
||||
<div class="nav-section quick-tools">
|
||||
<h3>Outils rapides</h3>
|
||||
<a routerLink="/embed" class="link highlight">🔗 Intégration embarquée</a>
|
||||
<a routerLink="/batch-edit" class="link highlight">⚡ Modification en masse</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h3>Navigation principale</h3>
|
||||
<a routerLink="/" class="link">🏠 Accueil</a>
|
||||
<a routerLink="/agenda" class="link">📅 Agenda</a>
|
||||
<a routerLink="/research" class="link">🔍 Recherche</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h3>Outils d'administration</h3>
|
||||
<a routerLink="/unlocated-events" class="link">📍 Événements non localisés</a>
|
||||
<a routerLink="/nouvelles-categories" class="link">🏷️ Nouvelles catégories</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h3>Intégration & API</h3>
|
||||
<a routerLink="/events-docs" class="link">📚 Documentation API</a>
|
||||
<a href="/demo/stats" class="link">📊 Statistiques</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h3>Communauté</h3>
|
||||
<a routerLink="/community-upcoming" class="link">👥 Community à venir</a>
|
||||
<a href="https://source.cipherbliss.com/tykayn/oedb-backend" class="link" target="_blank">💻 Sources</a>
|
||||
</div>
|
||||
</nav>
|
||||
<a href="/demo/stats">stats</a>
|
||||
<a href="https://source.cipherbliss.com/tykayn/oedb-backend">sources</a>
|
||||
|
||||
(editor)
|
||||
|
||||
|
|
|
@ -1,6 +1,109 @@
|
|||
:host {
|
||||
display: block;
|
||||
background: #f8f9fa;
|
||||
border-right: 1px solid #e9ecef;
|
||||
min-height: 100vh;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.menu-header {
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #3498db;
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
h3 {
|
||||
color: #6c757d;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-left: 0.5rem;
|
||||
border-left: 3px solid #3498db;
|
||||
}
|
||||
|
||||
&.quick-tools {
|
||||
background: linear-gradient(135deg, #3498db, #2980b9);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
h3 {
|
||||
color: white;
|
||||
border-left: 3px solid white;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.link.highlight {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: translateX(4px);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
&.router-link-active {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.link, .link-button {
|
||||
display: block;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #2c3e50;
|
||||
text-decoration: none;
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 0.25rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
background: none;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
|
||||
&:hover {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
&.router-link-active {
|
||||
background: #2980b9;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.link-button {
|
||||
&:hover {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
#what_categories {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import oedb_what_categories from '../../../../oedb-types';
|
||||
import { RouterLink } from "@angular/router";
|
||||
import { RouterLink, Router } from "@angular/router";
|
||||
|
||||
@Component({
|
||||
selector: 'app-menu',
|
||||
|
@ -10,6 +10,7 @@ import { RouterLink } from "@angular/router";
|
|||
styleUrl: './menu.scss'
|
||||
})
|
||||
export class Menu {
|
||||
private router = inject(Router);
|
||||
|
||||
public oedb_what_categories: Array<any> = [];
|
||||
public onToggleView?: () => void;
|
||||
|
|
146
frontend/src/app/pages/research/research.html
Normal file
146
frontend/src/app/pages/research/research.html
Normal file
|
@ -0,0 +1,146 @@
|
|||
<div class="research-page">
|
||||
<div class="container">
|
||||
<header class="page-header">
|
||||
<h1>Recherche d'événements</h1>
|
||||
<p>Trouvez des événements et visualisez-les sur la carte</p>
|
||||
</header>
|
||||
|
||||
<div class="search-section">
|
||||
<div class="search-form">
|
||||
<h2>Critères de recherche</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="query">Recherche textuelle :</label>
|
||||
<input
|
||||
id="query"
|
||||
type="text"
|
||||
[(ngModel)]="searchParams().query"
|
||||
placeholder="Tapez votre recherche..."
|
||||
class="search-input">
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="what">Type d'événement :</label>
|
||||
<select
|
||||
id="what"
|
||||
[(ngModel)]="searchParams().what">
|
||||
<option value="">Tous les types</option>
|
||||
@for (whatType of availableWhatTypes(); track whatType) {
|
||||
<option [value]="whatType">{{whatType}}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="limit">Nombre de résultats :</label>
|
||||
<input
|
||||
id="limit"
|
||||
type="number"
|
||||
[(ngModel)]="searchParams().limit"
|
||||
min="1"
|
||||
max="1000">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="start">Date de début :</label>
|
||||
<input
|
||||
id="start"
|
||||
type="date"
|
||||
[(ngModel)]="searchParams().start">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="end">Date de fin :</label>
|
||||
<input
|
||||
id="end"
|
||||
type="date"
|
||||
[(ngModel)]="searchParams().end">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
(click)="onSearch()"
|
||||
[disabled]="isLoading() || !searchParams().query.trim()">
|
||||
@if (isLoading()) {
|
||||
🔍 Recherche en cours...
|
||||
} @else {
|
||||
🔍 Rechercher
|
||||
}
|
||||
</button>
|
||||
@if (hasSearched()) {
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
(click)="onClearSearch()">
|
||||
✕ Effacer
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (hasSearched()) {
|
||||
<div class="results-section">
|
||||
<div class="results-header">
|
||||
<h2>Résultats de recherche</h2>
|
||||
@if (searchResults()) {
|
||||
<div class="results-info">
|
||||
<span class="results-count">
|
||||
{{searchResults()!.total}} événement(s) trouvé(s)
|
||||
</span>
|
||||
@if (searchResults()!.query) {
|
||||
<span class="search-query">
|
||||
pour "{{searchResults()!.query}}"
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (searchResults() && searchResults()!.features.length > 0) {
|
||||
<div class="results-content">
|
||||
<div class="map-container">
|
||||
<app-all-events
|
||||
[features]="searchResults()!.features"
|
||||
(select)="onEventSelect($event)">
|
||||
</app-all-events>
|
||||
</div>
|
||||
|
||||
<div class="events-list">
|
||||
<h3>Liste des événements</h3>
|
||||
<div class="events-grid">
|
||||
@for (event of searchResults()!.features; track event.id) {
|
||||
<div class="event-card" (click)="onEventSelect(event)">
|
||||
<div class="event-header">
|
||||
<h4 class="event-title">{{getEventTitle(event)}}</h4>
|
||||
<span class="event-type">{{getEventType(event)}}</span>
|
||||
</div>
|
||||
<div class="event-details">
|
||||
@if (getEventDate(event)) {
|
||||
<div class="event-date">
|
||||
📅 {{formatDate(getEventDate(event))}}
|
||||
</div>
|
||||
}
|
||||
@if (getEventLocation(event)) {
|
||||
<div class="event-location">
|
||||
📍 {{getEventLocation(event)}}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (searchResults() && searchResults()!.features.length === 0) {
|
||||
<div class="no-results">
|
||||
<h3>Aucun événement trouvé</h3>
|
||||
<p>Essayez de modifier vos critères de recherche</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
279
frontend/src/app/pages/research/research.scss
Normal file
279
frontend/src/app/pages/research/research.scss
Normal file
|
@ -0,0 +1,279 @@
|
|||
.research-page {
|
||||
min-height: 100vh;
|
||||
background: #f8f9fa;
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #6c757d;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-bottom: 3rem;
|
||||
|
||||
.search-form {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 2px solid #3498db;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
input, select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.3s;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
font-size: 1.1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #ecf0f1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.results-section {
|
||||
.results-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 2rem;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 2px solid #3498db;
|
||||
|
||||
h2 {
|
||||
color: #2c3e50;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.results-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 0.25rem;
|
||||
|
||||
.results-count {
|
||||
font-weight: 600;
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
.search-query {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.results-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 400px;
|
||||
gap: 2rem;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
min-height: 500px;
|
||||
|
||||
app-all-events {
|
||||
display: block;
|
||||
height: 100%;
|
||||
min-height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.events-list {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
padding: 1.5rem;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
|
||||
h3 {
|
||||
color: #2c3e50;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid #ecf0f1;
|
||||
}
|
||||
|
||||
.events-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.event-card {
|
||||
padding: 1rem;
|
||||
border: 1px solid #ecf0f1;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #3498db;
|
||||
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.2);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.event-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
.event-title {
|
||||
color: #2c3e50;
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.event-type {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.event-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
.event-date, .event-location {
|
||||
font-size: 0.9rem;
|
||||
color: #6c757d;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.no-results {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
|
||||
h3 {
|
||||
color: #6c757d;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #95a5a6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.btn-primary {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #2980b9;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
background: #95a5a6;
|
||||
color: white;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #7f8c8d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
23
frontend/src/app/pages/research/research.spec.ts
Normal file
23
frontend/src/app/pages/research/research.spec.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { Research } from './research';
|
||||
|
||||
describe('Research', () => {
|
||||
let component: Research;
|
||||
let fixture: ComponentFixture<Research>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [Research]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(Research);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
157
frontend/src/app/pages/research/research.ts
Normal file
157
frontend/src/app/pages/research/research.ts
Normal file
|
@ -0,0 +1,157 @@
|
|||
import { Component, inject, signal } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { OedbApi } from '../../services/oedb-api';
|
||||
import { AllEvents } from '../../maps/all-events/all-events';
|
||||
|
||||
interface SearchParams {
|
||||
query: string;
|
||||
what: string;
|
||||
start: string;
|
||||
end: string;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
interface SearchResult {
|
||||
features: any[];
|
||||
total: number;
|
||||
query: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-research',
|
||||
standalone: true,
|
||||
imports: [CommonModule, FormsModule, AllEvents],
|
||||
templateUrl: './research.html',
|
||||
styleUrl: './research.scss'
|
||||
})
|
||||
export class Research {
|
||||
private oedbApi = inject(OedbApi);
|
||||
|
||||
searchParams = signal<SearchParams>({
|
||||
query: '',
|
||||
what: '',
|
||||
start: '',
|
||||
end: '',
|
||||
limit: 100
|
||||
});
|
||||
|
||||
searchResults = signal<SearchResult | null>(null);
|
||||
isLoading = signal<boolean>(false);
|
||||
hasSearched = signal<boolean>(false);
|
||||
|
||||
availableWhatTypes = signal<string[]>([]);
|
||||
|
||||
constructor() {
|
||||
this.loadAvailableWhatTypes();
|
||||
}
|
||||
|
||||
private loadAvailableWhatTypes() {
|
||||
// Charger les types d'événements disponibles
|
||||
this.oedbApi.getEvents({ limit: 1 }).subscribe((response: any) => {
|
||||
const whatTypes = new Set<string>();
|
||||
if (response?.features) {
|
||||
response.features.forEach((feature: any) => {
|
||||
if (feature?.properties?.what) {
|
||||
whatTypes.add(feature.properties.what);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.availableWhatTypes.set(Array.from(whatTypes).sort());
|
||||
});
|
||||
}
|
||||
|
||||
onSearch() {
|
||||
const params = this.searchParams();
|
||||
if (!params.query.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading.set(true);
|
||||
this.hasSearched.set(true);
|
||||
|
||||
const apiParams: any = {
|
||||
q: params.query,
|
||||
limit: params.limit
|
||||
};
|
||||
|
||||
if (params.what) {
|
||||
apiParams.what = params.what;
|
||||
}
|
||||
if (params.start) {
|
||||
apiParams.start = params.start;
|
||||
}
|
||||
if (params.end) {
|
||||
apiParams.end = params.end;
|
||||
}
|
||||
|
||||
this.oedbApi.getEvents(apiParams).subscribe({
|
||||
next: (response: any) => {
|
||||
const features = Array.isArray(response?.features) ? response.features : [];
|
||||
this.searchResults.set({
|
||||
features,
|
||||
total: features.length,
|
||||
query: params.query
|
||||
});
|
||||
this.isLoading.set(false);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Erreur lors de la recherche:', error);
|
||||
this.searchResults.set({
|
||||
features: [],
|
||||
total: 0,
|
||||
query: params.query
|
||||
});
|
||||
this.isLoading.set(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onClearSearch() {
|
||||
this.searchParams.set({
|
||||
query: '',
|
||||
what: '',
|
||||
start: '',
|
||||
end: '',
|
||||
limit: 100
|
||||
});
|
||||
this.searchResults.set(null);
|
||||
this.hasSearched.set(false);
|
||||
}
|
||||
|
||||
onEventSelect(event: any) {
|
||||
console.log('Événement sélectionné:', event);
|
||||
}
|
||||
|
||||
getEventTitle(event: any): string {
|
||||
return event?.properties?.label || event?.properties?.name || 'Événement sans nom';
|
||||
}
|
||||
|
||||
getEventDate(event: any): string {
|
||||
return event?.properties?.start || event?.properties?.when || '';
|
||||
}
|
||||
|
||||
getEventLocation(event: any): string {
|
||||
return event?.properties?.where || '';
|
||||
}
|
||||
|
||||
getEventType(event: any): string {
|
||||
return event?.properties?.what || '';
|
||||
}
|
||||
|
||||
formatDate(dateString: string): string {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('fr-FR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch {
|
||||
return dateString;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -55,9 +55,9 @@ input{
|
|||
|
||||
app-root, app-home {
|
||||
display: block;
|
||||
min-height: 100vh;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
// min-height: 100vh;
|
||||
// height: 100vh;
|
||||
// overflow: hidden;
|
||||
}
|
||||
|
||||
/* Generic UI elements */
|
||||
|
|
66
test_agenda_debug.html
Normal file
66
test_agenda_debug.html
Normal file
|
@ -0,0 +1,66 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Agenda Debug</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
.test-link { display: block; margin: 10px 0; padding: 10px; background: #f0f0f0; text-decoration: none; color: #333; border-radius: 5px; }
|
||||
.test-link:hover { background: #e0e0e0; }
|
||||
.instructions { background: #e8f4fd; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test de l'Agenda avec Debug</h1>
|
||||
|
||||
<div class="instructions">
|
||||
<h3>Instructions de test :</h3>
|
||||
<ol>
|
||||
<li>Ouvrez les outils de développement (F12)</li>
|
||||
<li>Allez dans l'onglet "Console"</li>
|
||||
<li>Cliquez sur un des liens ci-dessous</li>
|
||||
<li>Observez les logs dans la console</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<h2>Tests à effectuer :</h2>
|
||||
|
||||
<a href="http://localhost:4200/agenda" class="test-link" target="_blank">
|
||||
🔗 Test 1: Agenda sans filtre
|
||||
</a>
|
||||
|
||||
<a href="http://localhost:4200/agenda?what=culture" class="test-link" target="_blank">
|
||||
🔗 Test 2: Agenda avec paramètre de requête (?what=culture)
|
||||
</a>
|
||||
|
||||
<a href="http://localhost:4200/agenda#&what=culture" class="test-link" target="_blank">
|
||||
🔗 Test 3: Agenda avec fragment (#&what=culture)
|
||||
</a>
|
||||
|
||||
<a href="http://localhost:4200/agenda#&what=culture.community.ccpl" class="test-link" target="_blank">
|
||||
🔗 Test 4: Agenda avec fragment spécifique (#&what=culture.community.ccpl)
|
||||
</a>
|
||||
|
||||
<h3>Logs attendus dans la console :</h3>
|
||||
<ul>
|
||||
<li>🔗 Fragment reçu: [fragment]</li>
|
||||
<li>🧹 Fragment nettoyé: [fragment nettoyé]</li>
|
||||
<li>🎯 Paramètre what extrait: [valeur]</li>
|
||||
<li>✅ Filtre what défini: [valeur]</li>
|
||||
<li>🔍 Chargement des événements avec paramètres: [paramètres]</li>
|
||||
<li>📅 Plage de dates: [dates]</li>
|
||||
<li>📡 Réponse API reçue: [réponse]</li>
|
||||
<li>📊 Nombre d'événements chargés: [nombre]</li>
|
||||
<li>🔍 Application du filtre what: [filtre]</li>
|
||||
<li>✅ Événements après filtrage: [nombre]</li>
|
||||
</ul>
|
||||
|
||||
<h3>Problèmes à vérifier :</h3>
|
||||
<ul>
|
||||
<li>❌ Si aucun log n'apparaît : Le serveur Angular ne fonctionne pas</li>
|
||||
<li>❌ Si "Fragment reçu: null" : L'URL ne contient pas de fragment</li>
|
||||
<li>❌ Si "Paramètre what extrait: null" : Le parsing du fragment échoue</li>
|
||||
<li>❌ Si "Nombre d'événements chargés: 0" : L'API ne retourne pas d'événements</li>
|
||||
<li>❌ Si "Événements après filtrage: 0" : Le filtrage ne fonctionne pas</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
Loading…
Add table
Add a link
Reference in a new issue