oedb-backend/frontend/public/embed.js

643 lines
20 KiB
JavaScript
Raw Normal View History

2025-10-12 17:19:50 +02:00
/**
* 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.openenventdatabase.org',
2025-11-03 00:37:35 +01:00
homeUrl: '/', // URL de la page home pour les liens vers les détails d'événements
theme: 'auto', // 'auto', 'light', ou 'dark' - auto détecte depuis la préférence système
2025-10-12 17:19:50 +02:00
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 };
2025-11-03 00:08:06 +01:00
// Fusionner les params si présents
2025-11-03 00:37:35 +01:00
if (config && config.params) {
2025-11-03 00:08:06 +01:00
this.config = { ...this.config, ...config.params };
}
2025-11-03 00:37:35 +01:00
// Détecter le thème système si 'auto' ou si non spécifié
const themeWasAuto = this.config.theme === 'auto' || !this.config.theme;
if (this.config.theme === 'auto' || !this.config.theme) {
this.config.theme = this.detectSystemTheme();
}
2025-10-12 17:19:50 +02:00
this.events = [];
this.isLoading = false;
this.refreshTimer = null;
2025-11-03 00:37:35 +01:00
this.map = null;
this.themeMediaQuery = null;
this.handleThemeChange = null;
2025-10-12 17:19:50 +02:00
if (!this.container) {
console.error('OEDB Embed: Container not found');
return;
}
this.init();
2025-11-03 00:37:35 +01:00
// Écouter les changements de thème si auto
if (themeWasAuto) {
this.setupThemeListener();
}
}
detectSystemTheme() {
if (typeof window !== 'undefined' && window.matchMedia) {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
return 'light';
}
setupThemeListener() {
if (typeof window !== 'undefined' && window.matchMedia) {
this.themeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
this.handleThemeChange = (e) => {
this.config.theme = e.matches ? 'dark' : 'light';
this.injectStyles();
if (this.map) {
// Mettre à jour le style de la carte si nécessaire
const mapStyle = this.config.theme === 'dark'
? 'https://tiles.openfreemap.org/styles/dark'
: 'https://tiles.openfreemap.org/styles/liberty';
try {
if (this.map.getStyle() && this.map.getStyle().name !== mapStyle) {
this.map.setStyle(mapStyle);
}
} catch (err) {
// Si on ne peut pas obtenir le style, réinitialiser la carte
if (this.map.isStyleLoaded()) {
this.map.setStyle(mapStyle);
}
}
}
};
// Support moderne et ancien
if (this.themeMediaQuery.addEventListener) {
this.themeMediaQuery.addEventListener('change', this.handleThemeChange);
} else if (this.themeMediaQuery.addListener) {
this.themeMediaQuery.addListener(this.handleThemeChange);
}
}
2025-10-12 17:19:50 +02:00
}
init() {
this.injectStyles();
this.render();
this.loadEvents();
2025-10-12 17:19:50 +02:00
if (this.config.autoRefresh) {
this.startAutoRefresh();
}
}
injectStyles() {
2025-11-03 00:37:35 +01:00
// Supprimer les anciens styles s'ils existent
const oldStyles = document.getElementById('oedb-embed-styles');
if (oldStyles) {
oldStyles.remove();
}
2025-10-12 17:19:50 +02:00
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;
}
2025-10-12 17:19:50 +02:00
.oedb-embed-header {
background: ${theme.primary};
color: white;
padding: 1rem;
text-align: center;
font-weight: 600;
}
2025-10-12 17:19:50 +02:00
.oedb-embed-content {
flex: 1;
display: flex;
overflow: hidden;
}
2025-10-12 17:19:50 +02:00
.oedb-embed-map {
flex: 1;
min-height: 200px;
2025-11-03 00:37:35 +01:00
background: ${this.config.theme === 'dark' ? '#1a1a1a' : '#f8f9fa'};
2025-10-12 17:19:50 +02:00
position: relative;
}
2025-10-12 17:19:50 +02:00
.oedb-embed-list {
width: 300px;
overflow-y: auto;
border-left: 1px solid ${theme.border};
background: ${theme.background};
}
2025-10-12 17:19:50 +02:00
.oedb-embed-list-only {
width: 100%;
}
2025-10-12 17:19:50 +02:00
.oedb-embed-map-only {
width: 100%;
}
2025-10-12 17:19:50 +02:00
.oedb-event-item {
2025-11-03 00:37:35 +01:00
display: block;
2025-10-12 17:19:50 +02:00
padding: 1rem;
border-bottom: 1px solid ${theme.border};
cursor: pointer;
transition: background-color 0.2s;
2025-11-03 00:37:35 +01:00
text-decoration: none;
color: inherit;
2025-10-12 17:19:50 +02:00
}
2025-10-12 17:19:50 +02:00
.oedb-event-item:hover {
background: ${theme.border};
2025-11-03 00:37:35 +01:00
text-decoration: none;
2025-10-12 17:19:50 +02:00
}
2025-10-12 17:19:50 +02:00
.oedb-event-title {
font-weight: 600;
margin-bottom: 0.5rem;
color: ${theme.text};
}
2025-10-12 17:19:50 +02:00
.oedb-event-meta {
font-size: 0.9rem;
color: ${theme.secondary};
display: flex;
flex-direction: column;
gap: 0.25rem;
}
2025-10-12 17:19:50 +02:00
.oedb-loading {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: ${theme.secondary};
}
2025-10-12 17:19:50 +02:00
.oedb-error {
padding: 2rem;
text-align: center;
2025-11-03 00:37:35 +01:00
color: ${this.config.theme === 'dark' ? '#ff6b6b' : '#e74c3c'};
background: ${this.config.theme === 'dark' ? '#3d2525' : '#fdf2f2'};
2025-10-12 17:19:50 +02:00
}
2025-10-12 17:19:50 +02:00
.oedb-no-events {
padding: 2rem;
text-align: center;
color: ${theme.secondary};
}
2025-10-12 17:19:50 +02:00
.oedb-map-placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
2025-11-03 00:37:35 +01:00
background: ${this.config.theme === 'dark' ? '#1a1a1a' : '#f8f9fa'};
2025-10-12 17:19:50 +02:00
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;
2025-10-12 17:19:50 +02:00
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);
2025-11-03 00:08:06 +01:00
const response = await fetch(`${this.config.apiUrl}/event?${params.toString()}`);
2025-10-12 17:19:50 +02:00
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 || '';
2025-11-03 00:37:35 +01:00
// L'ID peut être à la racine du Feature ou dans properties.id
const eventId = event.id || event.properties?.id || '';
const eventUrl = eventId ? `${this.config.homeUrl}?id=${encodeURIComponent(eventId)}` : '#';
2025-10-12 17:19:50 +02:00
return `
2025-11-03 00:37:35 +01:00
<a href="${eventUrl}" class="oedb-event-item" data-event-id="${eventId}" ${eventId ? '' : 'onclick="return false;"'}>
2025-10-12 17:19:50 +02:00
<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>
2025-11-03 00:37:35 +01:00
</a>
2025-10-12 17:19:50 +02:00
`;
}).join('');
listContainer.innerHTML = eventsHtml;
2025-11-03 00:37:35 +01:00
// Ajouter les événements de clic pour l'événement personnalisé (optionnel)
2025-10-12 17:19:50 +02:00
listContainer.querySelectorAll('.oedb-event-item').forEach(item => {
2025-11-03 00:37:35 +01:00
item.addEventListener('click', (e) => {
2025-10-12 17:19:50 +02:00
const eventId = item.dataset.eventId;
2025-11-03 00:37:35 +01:00
if (eventId) {
// Émettre un événement personnalisé pour permettre l'écoute externe
const customEvent = new CustomEvent('oedb-event-click', {
detail: { eventId, event: this.events.find(e => e.id === eventId) }
});
this.container.dispatchEvent(customEvent);
}
2025-10-12 17:19:50 +02:00
});
});
}
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;
}
2025-11-03 00:37:35 +01:00
// Initialiser MapLibre GL JS si disponible
if (typeof maplibregl === 'undefined') {
this.initMapLibre().then(() => {
this.createMap(mapContainer);
}).catch(err => {
console.error('Failed to load MapLibre GL:', err);
mapContainer.innerHTML = `
<div class="oedb-map-placeholder">
Carte interactive<br>
${this.events.length} événement(s) trouvé(s)<br>
<small>Chargement de la bibliothèque de cartes...</small>
</div>
`;
});
} else {
this.createMap(mapContainer);
}
}
async initMapLibre() {
return new Promise((resolve, reject) => {
// Vérifier si MapLibre est déjà chargé
if (typeof maplibregl !== 'undefined') {
return resolve();
}
// Charger le CSS
const css = document.createElement('link');
css.rel = 'stylesheet';
css.href = 'https://unpkg.com/maplibre-gl@3.6.0/dist/maplibre-gl.css';
document.head.appendChild(css);
// Charger le JavaScript
const script = document.createElement('script');
script.src = 'https://unpkg.com/maplibre-gl@3.6.0/dist/maplibre-gl.js';
script.onload = () => resolve();
script.onerror = () => reject(new Error('Failed to load MapLibre GL'));
document.head.appendChild(script);
});
}
createMap(mapContainer) {
// Nettoyer le conteneur
mapContainer.innerHTML = '';
// Créer la FeatureCollection GeoJSON
// Filtrer les événements qui ont des coordonnées valides
const geojson = {
type: 'FeatureCollection',
features: this.events
.filter(event => {
// Vérifier que l'événement a une géométrie valide avec des coordonnées
if (!event.geometry || !event.geometry.coordinates) return false;
const coords = event.geometry.coordinates;
return Array.isArray(coords) && coords.length >= 2 &&
typeof coords[0] === 'number' && typeof coords[1] === 'number' &&
!isNaN(coords[0]) && !isNaN(coords[1]) &&
coords[0] !== 0 && coords[1] !== 0; // Exclure les coordonnées 0,0 par défaut
})
.map(event => {
// L'ID peut être à la racine du Feature ou dans properties.id
const eventId = event.id || event.properties?.id || '';
return {
type: 'Feature',
geometry: event.geometry,
properties: {
id: eventId,
label: event.properties?.label || event.properties?.name || 'Événement',
what: event.properties?.what || '',
start: event.properties?.start || event.properties?.when || ''
}
};
})
};
// Calculer les bounds si on a des événements avec coordonnées valides
let bounds = null;
if (geojson.features.length > 0) {
const coordinates = geojson.features
.map(f => f.geometry?.coordinates)
.filter(c => c && Array.isArray(c) && c.length >= 2);
if (coordinates.length > 0) {
const lons = coordinates.map(c => c[0]);
const lats = coordinates.map(c => c[1]);
bounds = [
[Math.min(...lons), Math.min(...lats)],
[Math.max(...lons), Math.max(...lats)]
];
}
}
// Centre par défaut (Paris)
const center = bounds
? [(bounds[0][0] + bounds[1][0]) / 2, (bounds[0][1] + bounds[1][1]) / 2]
: [2.3522, 48.8566];
// Déterminer le style de carte selon le thème
const mapStyle = this.config.theme === 'dark'
? 'https://tiles.openfreemap.org/styles/dark'
: 'https://tiles.openfreemap.org/styles/liberty';
// Créer la carte
this.map = new maplibregl.Map({
container: mapContainer,
style: mapStyle,
center: center,
zoom: bounds ? this.calculateZoom(bounds) : 5
});
this.map.on('load', () => {
// Ajouter la source GeoJSON
this.map.addSource('events', {
type: 'geojson',
data: geojson
});
// Ajouter les marqueurs
this.map.addLayer({
id: 'events-points',
type: 'circle',
source: 'events',
paint: {
'circle-radius': 6,
'circle-color': '#667eea',
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff'
}
});
// Ajouter les popups avec lien vers la page home
this.map.on('click', 'events-points', (e) => {
const feature = e.features[0];
const props = feature.properties;
const eventId = props.id || '';
const eventUrl = eventId ? `${this.config.homeUrl}?id=${encodeURIComponent(eventId)}` : '#';
const popupContent = eventId
? `<div style="padding: 8px;">
<strong><a href="${eventUrl}" style="color: inherit; text-decoration: none;">${props.label || 'Événement'}</a></strong><br>
${props.what ? `<small>${props.what}</small><br>` : ''}
${props.start ? `<small>📅 ${this.formatDate(props.start)}</small>` : ''}
${eventId ? `<br><small><a href="${eventUrl}" style="color: #667eea;">Voir les détails →</a></small>` : ''}
</div>`
: `<div style="padding: 8px;">
<strong>${props.label || 'Événement'}</strong><br>
${props.what ? `<small>${props.what}</small><br>` : ''}
${props.start ? `<small>📅 ${this.formatDate(props.start)}</small>` : ''}
</div>`;
new maplibregl.Popup()
.setLngLat(e.lngLat)
.setHTML(popupContent)
.addTo(this.map);
});
// Changer le curseur au survol
this.map.on('mouseenter', 'events-points', () => {
this.map.getCanvas().style.cursor = 'pointer';
});
this.map.on('mouseleave', 'events-points', () => {
this.map.getCanvas().style.cursor = '';
});
// Ajuster la vue si on a des bounds
if (bounds) {
this.map.fitBounds(bounds, {
padding: 50,
maxZoom: 15
});
}
});
}
calculateZoom(bounds) {
const width = bounds[1][0] - bounds[0][0];
const height = bounds[1][1] - bounds[0][1];
const maxDimension = Math.max(width, height);
if (maxDimension > 10) return 4;
if (maxDimension > 5) return 5;
if (maxDimension > 2) return 6;
if (maxDimension > 1) return 7;
if (maxDimension > 0.5) return 8;
if (maxDimension > 0.2) return 9;
if (maxDimension > 0.1) return 10;
if (maxDimension > 0.05) return 11;
return 12;
2025-10-12 17:19:50 +02:00
}
onEventClick(eventId) {
2025-11-03 00:37:35 +01:00
// Émettre un événement personnalisé (déjà fait dans renderEvents)
// Cette méthode peut être utilisée pour des actions personnalisées
2025-10-12 17:19:50 +02:00
const event = new CustomEvent('oedb-event-click', {
detail: { eventId, event: this.events.find(e => e.id === eventId) }
});
this.container.dispatchEvent(event);
}
2025-11-03 00:37:35 +01:00
getEventUrl(eventId) {
if (!eventId) return '#';
return `${this.config.homeUrl}?id=${encodeURIComponent(eventId)}`;
}
2025-10-12 17:19:50 +02:00
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();
2025-11-03 00:37:35 +01:00
if (this.map) {
this.map.remove();
this.map = null;
}
// Supprimer le listener de thème
if (this.themeMediaQuery && this.handleThemeChange) {
if (this.themeMediaQuery.removeEventListener) {
this.themeMediaQuery.removeEventListener('change', this.handleThemeChange);
} else if (this.themeMediaQuery.removeListener) {
this.themeMediaQuery.removeListener(this.handleThemeChange);
}
}
2025-10-12 17:19:50 +02:00
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);
});
});
})();