add embed, research pages

This commit is contained in:
Tykayn 2025-10-12 17:19:50 +02:00 committed by tykayn
parent 2238380e80
commit 2c95bea01b
24 changed files with 2925 additions and 251 deletions

View file

@ -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&#x27;antico Mare Padano</p>
<p class="event-entry-location">Castell&#x27;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">
17th18th 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">
17th18th 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">
17th21st 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 April3rd 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
View 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);
});
});
})();

View file

@ -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>

View file

@ -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

View file

@ -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;
}
}

View file

@ -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>

View file

@ -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 {

View file

@ -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();
}
}

View 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>

View 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;
}
}
}
}

View 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;
}
}
}

View 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>

View 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;
}
}
}

View 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();
});
});

View 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>
`);
}
}
}

View file

@ -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)

View file

@ -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 {

View file

@ -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;

View 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>

View 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;
}
}
}
}

View 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();
});
});

View 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;
}
}
}

View file

@ -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
View 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>