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 `
-
+
`;
}).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)}` : ''}
+
`;
+
+ 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 @@
-
+
@@ -18,8 +18,8 @@
} @else {
- @for (event of events; track event.id) {
-
+ @for (event of events; track event.id || event.properties?.id) {
+
}
-
+
}
}
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: () => {