From b18c2c5aece0996604be02b283cf39484c97c08e Mon Sep 17 00:00:00 2001 From: Tykayn Date: Mon, 3 Nov 2025 00:37:35 +0100 Subject: [PATCH] up embed, add export docs --- frontend/public/embed.js | 313 ++++++++++++++++-- .../src/app/maps/all-events/all-events.ts | 15 + .../pages/embed/embed-view/embed-view.html | 8 +- .../pages/embed/embed-view/embed-view.scss | 57 +++- .../app/pages/embed/embed-view/embed-view.ts | 30 +- frontend/src/app/pages/home/home.html | 2 +- frontend/src/app/pages/home/home.ts | 22 +- 7 files changed, 414 insertions(+), 33 deletions(-) diff --git a/frontend/public/embed.js b/frontend/public/embed.js index fc087ff..d4ddcc2 100644 --- a/frontend/public/embed.js +++ b/frontend/public/embed.js @@ -9,7 +9,8 @@ // Configuration par défaut const defaultConfig = { apiUrl: 'https://api.openenventdatabase.org', - theme: 'light', + 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 limit: 50, width: '100%', height: '400px', @@ -42,12 +43,22 @@ this.container = typeof container === 'string' ? document.querySelector(container) : container; this.config = { ...defaultConfig, ...config }; // Fusionner les params si présents - if (config.params) { + if (config && config.params) { this.config = { ...this.config, ...config.params }; } + + // 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(); + } + this.events = []; this.isLoading = false; this.refreshTimer = null; + this.map = null; + this.themeMediaQuery = null; + this.handleThemeChange = null; if (!this.container) { console.error('OEDB Embed: Container not found'); @@ -55,6 +66,51 @@ } this.init(); + + // É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); + } + } } init() { @@ -68,7 +124,11 @@ } injectStyles() { - if (document.getElementById('oedb-embed-styles')) return; + // Supprimer les anciens styles s'ils existent + const oldStyles = document.getElementById('oedb-embed-styles'); + if (oldStyles) { + oldStyles.remove(); + } const theme = themes[this.config.theme] || themes.light; const styles = ` @@ -103,7 +163,7 @@ .oedb-embed-map { flex: 1; min-height: 200px; - background: #f8f9fa; + background: ${this.config.theme === 'dark' ? '#1a1a1a' : '#f8f9fa'}; position: relative; } @@ -123,14 +183,18 @@ } .oedb-event-item { + display: block; padding: 1rem; border-bottom: 1px solid ${theme.border}; cursor: pointer; transition: background-color 0.2s; + text-decoration: none; + color: inherit; } .oedb-event-item:hover { background: ${theme.border}; + text-decoration: none; } .oedb-event-title { @@ -158,8 +222,8 @@ .oedb-error { padding: 2rem; text-align: center; - color: #e74c3c; - background: #fdf2f2; + color: ${this.config.theme === 'dark' ? '#ff6b6b' : '#e74c3c'}; + background: ${this.config.theme === 'dark' ? '#3d2525' : '#fdf2f2'}; } .oedb-no-events { @@ -173,7 +237,7 @@ align-items: center; justify-content: center; height: 100%; - background: #f8f9fa; + background: ${this.config.theme === 'dark' ? '#1a1a1a' : '#f8f9fa'}; color: ${theme.secondary}; font-style: italic; } @@ -253,26 +317,35 @@ const date = event.properties?.start || event.properties?.when || ''; const location = event.properties?.where || ''; const type = event.properties?.what || ''; + // 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)}` : '#'; return ` -
+
${this.escapeHtml(title)}
${date ? `
📅 ${this.formatDate(date)}
` : ''} ${location ? `
📍 ${this.escapeHtml(location)}
` : ''} ${type ? `
🏷️ ${this.escapeHtml(type)}
` : ''}
-
+ `; }).join(''); listContainer.innerHTML = eventsHtml; - // Ajouter les événements de clic + // Ajouter les événements de clic pour l'événement personnalisé (optionnel) listContainer.querySelectorAll('.oedb-event-item').forEach(item => { - item.addEventListener('click', () => { + item.addEventListener('click', (e) => { const eventId = item.dataset.eventId; - this.onEventClick(eventId); + 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); + } }); }); } @@ -286,23 +359,209 @@ return; } - // Pour l'instant, afficher un placeholder - // Dans une vraie implémentation, on utiliserait Leaflet ou une autre librairie de cartes - mapContainer.innerHTML = ` -
- Carte interactive
- ${this.events.length} événement(s) trouvé(s) -
- `; + // 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 = ` +
+ Carte interactive
+ ${this.events.length} événement(s) trouvé(s)
+ Chargement de la bibliothèque de cartes... +
+ `; + }); + } 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 + ? `
+ ${props.label || 'Événement'}
+ ${props.what ? `${props.what}
` : ''} + ${props.start ? `📅 ${this.formatDate(props.start)}` : ''} + ${eventId ? `
Voir les détails →` : ''} +
` + : `
+ ${props.label || 'Événement'}
+ ${props.what ? `${props.what}
` : ''} + ${props.start ? `📅 ${this.formatDate(props.start)}` : ''} +
`; + + 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; } onEventClick(eventId) { - // Émettre un événement personnalisé + // Émettre un événement personnalisé (déjà fait dans renderEvents) + // Cette méthode peut être utilisée pour des actions personnalisées const event = new CustomEvent('oedb-event-click', { detail: { eventId, event: this.events.find(e => e.id === eventId) } }); this.container.dispatchEvent(event); } + + getEventUrl(eventId) { + if (!eventId) return '#'; + return `${this.config.homeUrl}?id=${encodeURIComponent(eventId)}`; + } startAutoRefresh() { if (this.refreshTimer) { @@ -323,6 +582,18 @@ destroy() { this.stopAutoRefresh(); + 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); + } + } this.container.innerHTML = ''; } diff --git a/frontend/src/app/maps/all-events/all-events.ts b/frontend/src/app/maps/all-events/all-events.ts index 2bbf5af..d17dfac 100644 --- a/frontend/src/app/maps/all-events/all-events.ts +++ b/frontend/src/app/maps/all-events/all-events.ts @@ -406,6 +406,21 @@ export class AllEvents implements OnInit, OnDestroy { if (this.map) this.map.flyTo({ center: this.originalCoords, zoom: Math.max(this.map.getZoom() || 12, 12) }); } + /** + * Centre la carte sur un point avec zoom + * @param coords Coordonnées [longitude, latitude] + * @param zoom Niveau de zoom (défaut: 14) + */ + centerOn(coords: [number, number], zoom: number = 14) { + if (this.map && coords && coords.length >= 2) { + this.map.flyTo({ + center: coords, + zoom: zoom, + duration: 1000 + }); + } + } + private updateUrlFromMap() { if (!this.map) return; diff --git a/frontend/src/app/pages/embed/embed-view/embed-view.html b/frontend/src/app/pages/embed/embed-view/embed-view.html index 3360834..79b4ab0 100644 --- a/frontend/src/app/pages/embed/embed-view/embed-view.html +++ b/frontend/src/app/pages/embed/embed-view/embed-view.html @@ -1,4 +1,4 @@ -
+

Événements OEDB

@@ -18,8 +18,8 @@
} @else {
- @for (event of events; track event.id) { -
+ @for (event of events; track event.id || event.properties?.id) { +

{{event.properties?.label || event.properties?.name || 'Événement sans nom'}} @@ -43,7 +43,7 @@

}
-
+ }
} diff --git a/frontend/src/app/pages/embed/embed-view/embed-view.scss b/frontend/src/app/pages/embed/embed-view/embed-view.scss index b2393d2..479e137 100644 --- a/frontend/src/app/pages/embed/embed-view/embed-view.scss +++ b/frontend/src/app/pages/embed/embed-view/embed-view.scss @@ -5,6 +5,12 @@ min-height: 100vh; display: flex; flex-direction: column; + transition: background-color 0.3s ease, color 0.3s ease; + + &.dark-theme { + background: #2c3e50; + color: #ecf0f1; + } } .embed-header { @@ -40,6 +46,11 @@ border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 16px; + + .dark-theme & { + border-color: #34495e; + border-top-color: #667eea; + } } @keyframes spin { @@ -50,10 +61,19 @@ .embed-error { color: #e74c3c; background: #fdf2f2; + + .dark-theme & { + color: #ff6b6b; + background: #3d2525; + } } .embed-empty { color: #6c757d; + + .dark-theme & { + color: #95a5a6; + } } .embed-events-list { @@ -66,18 +86,33 @@ } .embed-event-item { + display: block; background: #f8f9fa; border: 1px solid #e9ecef; border-radius: 8px; padding: 16px; transition: all 0.2s ease; border-left: 4px solid #667eea; -} + text-decoration: none; + color: inherit; -.embed-event-item:hover { - background: #ffffff; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - transform: translateX(2px); + .dark-theme & { + background: #34495e; + border-color: #4a5568; + + &:hover { + background: #3d4a5e; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + } + } + + &:hover { + background: #ffffff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transform: translateX(2px); + text-decoration: none; + color: inherit; + } } .embed-event-header { @@ -94,6 +129,10 @@ font-weight: 600; color: #2c3e50; flex: 1; + + .dark-theme & { + color: #ecf0f1; + } } .embed-event-type { @@ -115,12 +154,20 @@ .embed-event-date, .embed-event-location { color: #6c757d; + + .dark-theme & { + color: #95a5a6; + } } .embed-event-description { color: #495057; margin-top: 8px; line-height: 1.5; + + .dark-theme & { + color: #bdc3c7; + } } /* Responsive */ diff --git a/frontend/src/app/pages/embed/embed-view/embed-view.ts b/frontend/src/app/pages/embed/embed-view/embed-view.ts index 98f5fd0..e96ac17 100644 --- a/frontend/src/app/pages/embed/embed-view/embed-view.ts +++ b/frontend/src/app/pages/embed/embed-view/embed-view.ts @@ -4,8 +4,9 @@ import { ActivatedRoute } from '@angular/router'; import { OedbApi } from '../../../services/oedb-api'; interface OedbEvent { - id: string; + id?: string; properties: { + id?: string; label?: string; name?: string; what?: string; @@ -31,13 +32,40 @@ export class EmbedView implements OnInit { events: OedbEvent[] = []; isLoading = true; error: string | null = null; + isDarkTheme = false; + homeUrl = '/'; ngOnInit() { this.route.queryParams.subscribe(params => { + // Détecter le thème depuis les query params ou préférence système + this.detectTheme(params); this.loadEvents(params); }); } + detectTheme(params: any) { + if (params.theme) { + this.isDarkTheme = params.theme === 'dark'; + } else { + // Détecter depuis la préférence système + if (typeof window !== 'undefined' && window.matchMedia) { + this.isDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; + } + } + + // Appliquer le thème au body + if (typeof document !== 'undefined') { + document.body.classList.toggle('dark-theme', this.isDarkTheme); + } + } + + getEventUrl(event: OedbEvent): string { + // L'ID peut être à la racine du Feature ou dans properties.id + const eventId = event.id || event.properties?.id; + if (!eventId) return '#'; + return `${this.homeUrl}?id=${encodeURIComponent(eventId)}`; + } + loadEvents(params: any) { this.isLoading = true; this.error = null; diff --git a/frontend/src/app/pages/home/home.html b/frontend/src/app/pages/home/home.html index 3de9b10..3d5bbf9 100644 --- a/frontend/src/app/pages/home/home.html +++ b/frontend/src/app/pages/home/home.html @@ -337,7 +337,7 @@ } @if (!showTable && !showUnlocatedList) {
-
diff --git a/frontend/src/app/pages/home/home.ts b/frontend/src/app/pages/home/home.ts index f603395..3a83d2f 100644 --- a/frontend/src/app/pages/home/home.ts +++ b/frontend/src/app/pages/home/home.ts @@ -1,4 +1,4 @@ -import {Component, inject, signal, OnDestroy, OnInit} from '@angular/core'; +import {Component, inject, signal, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {Router, RouterLink} from '@angular/router'; import {FormsModule} from '@angular/forms'; import {Menu} from './menu/menu'; @@ -38,6 +38,8 @@ export class Home implements OnInit, OnDestroy { router = inject(Router); private osmAuth = inject(OsmAuth); + @ViewChild('allEventsMap') allEventsMap!: AllEvents; + features: Array = []; filteredFeatures: Array = []; selected: any | null = null; @@ -273,6 +275,24 @@ export class Home implements OnInit, OnDestroy { this.features = f ? [f] : []; this.filteredFeatures = this.features; this.updateAvailableWhatTypes(); + // Sélectionner automatiquement l'événement et ouvrir le panel d'édition + if (f) { + this.selected = f; + this.showEditForm = true; + this.showOptions = true; // Afficher aussi le panel d'options + + // Zoomer sur le marqueur de l'événement si la géométrie est disponible + const geometry = f.geometry; + if (geometry && geometry.type === 'Point' && geometry.coordinates && geometry.coordinates.length >= 2) { + const [lng, lat] = geometry.coordinates; + // Attendre que la carte soit initialisée avant de zoomer + setTimeout(() => { + if (this.allEventsMap) { + this.allEventsMap.centerOn([lng, lat], 16); // Zoom élevé pour voir le marqueur de près + } + }, 500); + } + } this.isLoading = false; }, error: () => {