diff --git a/frontend/public/embed.js b/frontend/public/embed.js index 1c9ff82..fc087ff 100644 --- a/frontend/public/embed.js +++ b/frontend/public/embed.js @@ -41,6 +41,10 @@ constructor(container, config) { this.container = typeof container === 'string' ? document.querySelector(container) : container; this.config = { ...defaultConfig, ...config }; + // Fusionner les params si présents + if (config.params) { + this.config = { ...this.config, ...config.params }; + } this.events = []; this.isLoading = false; this.refreshTimer = null; @@ -207,7 +211,7 @@ 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 response = await fetch(`${this.config.apiUrl}/event?${params.toString()}`); const data = await response.json(); this.events = data.features || []; diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index 10b7184..a5401ba 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -3,6 +3,7 @@ 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 { EmbedView } from './pages/embed/embed-view/embed-view'; import { BatchEdit } from './pages/batch-edit/batch-edit'; import { NouvellesCategories } from './pages/nouvelles-categories/nouvelles-categories'; import { UnlocatedEventsPage } from './pages/unlocated-events/unlocated-events'; @@ -35,6 +36,10 @@ export const routes: Routes = [ path: 'embed', component: Embed }, + { + path: 'embed/view', + component: EmbedView + }, { path: 'stats', component: Stats diff --git a/frontend/src/app/forms/edit-form/edit-form.html b/frontend/src/app/forms/edit-form/edit-form.html index 937412a..52add9a 100644 --- a/frontend/src/app/forms/edit-form/edit-form.html +++ b/frontend/src/app/forms/edit-form/edit-form.html @@ -32,7 +32,7 @@
- +
@@ -84,9 +84,8 @@
} -
-    {{currentPreset() | json}}
-  
+ +
@@ -109,6 +108,16 @@
+
+
+ + @if (form.get('lat')?.value && form.get('lon')?.value) { + 📍 Coordonnées définies + } +
+
@@ -117,6 +126,41 @@
+ + + @if (showMapPicker) { +
+
+
+

Sélectionner un lieu sur la carte

+ +
+
+
+
+

Cliquez sur la carte pour sélectionner un lieu

+ @if (selectedLocation) { +
+ Coordonnées sélectionnées :
+ Latitude: {{selectedLocation.lat.toFixed(6)}}
+ Longitude: {{selectedLocation.lon.toFixed(6)}} +
+ } +
+
+
+ + +
+
+
+ } @@ -131,7 +175,7 @@ @if (status().state !== 'idle') {
-
+
@if (status().state === 'saving') {
{{status().message}}
} @else if (status().state === 'saved') { @@ -139,6 +183,19 @@ {{status().message}}. Voir d'autres évènements de ce type
+ } @else if (status().state === 'created') { +
+ ✅ {{status().message}} +

Merci d'avoir créé cet événement !

+
+ @if (status().createdEvent) { + + } + ou continuez à modifier ci-dessus +
+
} @else if (status().state === 'error') {
{{status().message}}
} diff --git a/frontend/src/app/forms/edit-form/edit-form.scss b/frontend/src/app/forms/edit-form/edit-form.scss index 664338c..e5d6378 100644 --- a/frontend/src/app/forms/edit-form/edit-form.scss +++ b/frontend/src/app/forms/edit-form/edit-form.scss @@ -68,3 +68,170 @@ form { transform: translateY(-1px); } +// Styles pour le sélecteur de carte +.location-controls { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 8px; +} + +.btn-map-picker { + padding: 10px 16px; + font-size: 0.95rem; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.location-status { + color: #667eea; + font-size: 0.9rem; + font-weight: 500; +} + +// Modale de sélection de carte +.map-picker-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + backdrop-filter: blur(2px); +} + +.map-picker-modal { + background: white; + border-radius: 12px; + width: 90vw; + max-width: 900px; + max-height: 90vh; + display: flex; + flex-direction: column; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); +} + +.map-picker-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 1px solid #e9ecef; + + h3 { + margin: 0; + font-size: 1.3rem; + color: #2c3e50; + } +} + +.btn-close-map { + background: none; + border: none; + font-size: 2rem; + color: #6c757d; + cursor: pointer; + padding: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + transition: all 0.2s ease; + + &:hover { + background: #f8f9fa; + color: #2c3e50; + } +} + +.map-picker-content { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; + position: relative; +} + +.map-picker-map { + flex: 1; + min-height: 400px; + position: relative; +} + +.map-picker-info { + padding: 16px 24px; + background: #f8f9fa; + border-top: 1px solid #e9ecef; + + p { + margin: 0 0 12px 0; + color: #6c757d; + font-size: 0.9rem; + } +} + +.selected-coords { + background: white; + padding: 12px; + border-radius: 8px; + border: 1px solid #e9ecef; + font-size: 0.9rem; + line-height: 1.6; + + strong { + color: #2c3e50; + display: block; + margin-bottom: 8px; + } +} + +.map-picker-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 20px 24px; + border-top: 1px solid #e9ecef; + background: #f8f9fa; +} + +// Styles pour le message de succès après création +.success-message { + text-align: center; + + strong { + display: block; + margin-bottom: 8px; + font-size: 1.1rem; + } + + p { + margin: 0 0 16px 0; + color: #6c757d; + } +} + +.success-actions { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + + .btn-sm { + padding: 8px 16px; + font-size: 0.9rem; + } +} + +.or-text { + color: #6c757d; + font-size: 0.85rem; + font-style: italic; +} + diff --git a/frontend/src/app/forms/edit-form/edit-form.ts b/frontend/src/app/forms/edit-form/edit-form.ts index e09b74d..0c08a51 100644 --- a/frontend/src/app/forms/edit-form/edit-form.ts +++ b/frontend/src/app/forms/edit-form/edit-form.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Input, Output, OnChanges, SimpleChanges, computed, effect, signal } from '@angular/core'; +import { Component, EventEmitter, Input, Output, OnChanges, SimpleChanges, computed, effect, signal, ViewChild, ElementRef, AfterViewInit, OnDestroy } from '@angular/core'; import { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; import oedb from '../../../oedb-types'; import { OedbApi } from '../../services/oedb-api'; @@ -11,12 +11,15 @@ import { JsonPipe } from '@angular/common'; templateUrl: './edit-form.html', styleUrl: './edit-form.scss' }) -export class EditForm implements OnChanges { +export class EditForm implements OnChanges, AfterViewInit, OnDestroy { @Input() selected: any | null = null; @Output() saved = new EventEmitter(); @Output() created = new EventEmitter(); @Output() deleted = new EventEmitter(); @Output() canceled = new EventEmitter(); + + @ViewChild('labelInput', { static: false }) labelInput?: ElementRef; + @ViewChild('mapContainer', { static: false }) mapContainer?: ElementRef; form: FormGroup; allPresets: Array<{ key: string, label: string, emoji: string, category: string, description?: string, durationHours?: number, properties?: Record }>; @@ -41,10 +44,17 @@ export class EditForm implements OnChanges { 'label','description','what','where','lat','lon','noLocation','wikidata','featureType','type','start','stop' ]); - status = signal<{ state: 'idle' | 'saving' | 'saved' | 'error', message?: string, what?: string }>({ state: 'idle' }); + status = signal<{ state: 'idle' | 'saving' | 'saved' | 'created' | 'error', message?: string, what?: string, createdEvent?: any }>({ state: 'idle' }); featureId = signal(null); durationHuman = signal(''); + + // Carte pour sélection de lieu + showMapPicker = false; + map: any = null; + mapInitialized = false; + selectedLocation: { lat: number; lon: number } | null = null; + mapMarker: any = null; constructor(private fb: FormBuilder, private api: OedbApi) { this.form = this.fb.group({ @@ -103,6 +113,8 @@ export class EditForm implements OnChanges { this.featureId.set((propId ?? sel.id) ?? null); const p = sel.properties || {}; const coords = sel?.geometry?.coordinates || []; + // Si on a un addMode (filterByPrefix) et pas d'id, c'est une création, mettre type=scheduled + const eventType = this._filterByPrefix && !this.featureId() ? 'scheduled' : (p.type || this.form.value.type || 'unscheduled'); this.form.patchValue({ label: p.label || p.name || '', id: p.id || '', @@ -114,7 +126,7 @@ export class EditForm implements OnChanges { lon: coords[0] ?? '', wikidata: p.wikidata || '', featureType: 'point', - type: p.type || this.form.value.type || 'unscheduled', + type: eventType, start: this.toLocalInputValue(p.start || p.when || new Date()), stop: this.toLocalInputValue(p.stop || new Date(new Date().getTime() + 24 * 3600 * 1000)) }, { emitEvent: false }); @@ -329,11 +341,14 @@ export class EditForm implements OnChanges { } else { this.api.createEvent(feature).subscribe({ next: (res) => { - this.status.set({ state: 'saved', what: val.what, message: 'Évènement créé' }); + this.status.set({ + state: 'created', + what: val.what, + message: 'Évènement créé avec succès !', + createdEvent: res + }); this.created.emit(res); - // Quitter l'édition après succès - this.canceled.emit(); - setTimeout(() => this.status.set({ state: 'idle' }), 3000); + // Ne pas quitter l'édition après création, laisser l'utilisateur voir/modifier }, error: (err) => { this.status.set({ state: 'error', what: val.what, message: 'Erreur lors de la création' }); @@ -384,6 +399,7 @@ export class EditForm implements OnChanges { resetPresetFilter() { // Réinitialise le champ what et le filtre de préfixe pour afficher tous les presets + this._filterByPrefix = null; this.presetFilterPrefix = null; this.form.patchValue({ what: '' }); } @@ -391,17 +407,187 @@ export class EditForm implements OnChanges { private _filterByPrefix: string | null = null; @Input() set filterByPrefix(prefix: string | null) { + const previousPrefix = this._filterByPrefix; this._filterByPrefix = prefix; this.presetFilterPrefix = prefix; - // Si un préfixe est défini et que le champ what est vide, pré-remplir avec le préfixe - if (prefix && !this.form.get('what')?.value) { - this.form.patchValue({ what: prefix }, { emitEvent: false }); + // Si un préfixe est défini et que le champ what est vide ou qu'on change de préfixe, appliquer le preset correspondant + if (prefix && (!this.form.get('what')?.value || previousPrefix !== prefix)) { + // Vérifier si le préfixe correspond à un preset exact + const what = oedb.presets.what as Record; + if (what[prefix]) { + // C'est un preset exact, l'appliquer + setTimeout(() => { + // Si c'est une nouvelle création (pas d'id), mettre type=scheduled + if (!this.featureId()) { + this.form.patchValue({ type: 'scheduled' }, { emitEvent: false }); + } + this.applyPreset(prefix); + // Mettre le focus sur le champ label après un court délai + setTimeout(() => { + if (this.labelInput) { + this.labelInput.nativeElement.focus(); + } + }, 150); + }, 0); + } else { + // Sinon, juste pré-remplir avec le préfixe + this.form.patchValue({ what: prefix }, { emitEvent: false }); + // Si c'est une nouvelle création, mettre type=scheduled + if (!this.featureId()) { + this.form.patchValue({ type: 'scheduled' }, { emitEvent: false }); + } + } + } + } + + ngAfterViewInit() { + // Si on est en mode add et que le champ what est déjà rempli, mettre le focus sur label + if (this._filterByPrefix && this.form.get('what')?.value) { + setTimeout(() => { + if (this.labelInput) { + this.labelInput.nativeElement.focus(); + } + }, 200); } } get filterByPrefix(): string | null { return this._filterByPrefix; } + + ngOnDestroy() { + if (this.map) { + this.map.remove(); + } + } + + openMapPicker() { + this.showMapPicker = true; + // Initialiser la carte après que la vue soit mise à jour + setTimeout(() => { + if (!this.mapInitialized && this.mapContainer) { + this.initMap(); + } else if (this.mapContainer && this.map) { + // Si la carte existe déjà, restaurer le point sélectionné s'il y en a un + const currentLat = parseFloat(this.form.get('lat')?.value); + const currentLon = parseFloat(this.form.get('lon')?.value); + if (!isNaN(currentLat) && !isNaN(currentLon)) { + this.map.flyTo({ center: [currentLon, currentLat], zoom: 13 }); + this.selectedLocation = { lat: currentLat, lon: currentLon }; + this.updateMarker(); + } + } + }, 100); + } + + closeMapPicker() { + this.showMapPicker = false; + } + + async initMap() { + if (!this.mapContainer || this.mapInitialized) return; + + await this.ensureMapLibre(); + const maplibregl = (window as any).maplibregl; + if (!maplibregl) { + console.error('MapLibre GL n\'a pas pu être chargé'); + return; + } + + // Récupérer les coordonnées actuelles ou utiliser Paris par défaut + const currentLat = parseFloat(this.form.get('lat')?.value); + const currentLon = parseFloat(this.form.get('lon')?.value); + const center: [number, number] = + (!isNaN(currentLat) && !isNaN(currentLon)) + ? [currentLon, currentLat] + : [2.3522, 48.8566]; // Paris par défaut + + this.map = new maplibregl.Map({ + container: this.mapContainer.nativeElement, + style: 'https://tiles.openfreemap.org/styles/liberty', + center: center, + zoom: currentLat && currentLon ? 13 : 5 + }); + + this.map.addControl(new maplibregl.NavigationControl()); + + // Ajouter un marqueur initial si des coordonnées existent + if (!isNaN(currentLat) && !isNaN(currentLon)) { + this.selectedLocation = { lat: currentLat, lon: currentLon }; + this.map.on('load', () => { + this.updateMarker(); + }); + } + + // Gérer les clics sur la carte + this.map.on('click', (e: any) => { + const { lng, lat } = e.lngLat; + this.selectedLocation = { lat, lon: lng }; + this.updateMarker(); + }); + + this.mapInitialized = true; + } + + updateMarker() { + if (!this.map || !this.selectedLocation) return; + + const maplibregl = (window as any).maplibregl; + + // Supprimer l'ancien marqueur + if (this.mapMarker) { + this.mapMarker.remove(); + } + + // Créer un nouveau marqueur + const el = document.createElement('div'); + el.className = 'custom-marker'; + el.style.width = '30px'; + el.style.height = '30px'; + el.style.borderRadius = '50%'; + el.style.backgroundColor = '#667eea'; + el.style.border = '3px solid white'; + el.style.boxShadow = '0 2px 8px rgba(0,0,0,0.3)'; + el.style.cursor = 'pointer'; + + this.mapMarker = new maplibregl.Marker({ element: el }) + .setLngLat([this.selectedLocation.lon, this.selectedLocation.lat]) + .addTo(this.map); + } + + validateLocation() { + if (this.selectedLocation) { + this.form.patchValue({ + lat: this.selectedLocation.lat.toFixed(6), + lon: this.selectedLocation.lon.toFixed(6) + }); + this.closeMapPicker(); + } + } + + private async ensureMapLibre(): Promise { + return new Promise((resolve) => { + if ((window as any).maplibregl) return resolve(); + 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); + const s = document.createElement('script'); + s.src = 'https://unpkg.com/maplibre-gl@3.6.0/dist/maplibre-gl.js'; + s.onload = () => resolve(); + document.head.appendChild(s); + }); + } + + viewCreatedEvent() { + const createdEvent = this.status().createdEvent; + if (createdEvent) { + const eventId = createdEvent?.id || createdEvent?.properties?.id || createdEvent?.properties?.uuid; + if (eventId) { + window.open(`/agenda?id=${eventId}`, '_blank'); + } + } + } onCancelEdit() { this.selected = null; diff --git a/frontend/src/app/pages/agenda/agenda.html b/frontend/src/app/pages/agenda/agenda.html index d38720c..09ba4c6 100644 --- a/frontend/src/app/pages/agenda/agenda.html +++ b/frontend/src/app/pages/agenda/agenda.html @@ -119,8 +119,8 @@ @if (selectedEvent || (showEditForm && addMode)) {
-

@if (selectedEvent && selectedEvent.id) { Modifier l'événement } @else { Créer un événement }

- +

@if (addMode) { Créer un événement } @else if (selectedEvent && selectedEvent.id) { Modifier l'événement } @else { Modifier l'événement }

+
@if (selectedEvent && selectedEvent.id) {
@@ -147,7 +147,8 @@ [filterByPrefix]="addMode" (saved)="onEventSaved()" (created)="onEventCreated()" - (deleted)="onEventDeleted()"> + (deleted)="onEventDeleted()" + (canceled)="onEditFormCanceled()">
diff --git a/frontend/src/app/pages/agenda/agenda.scss b/frontend/src/app/pages/agenda/agenda.scss index f0eba3a..e2d54f3 100644 --- a/frontend/src/app/pages/agenda/agenda.scss +++ b/frontend/src/app/pages/agenda/agenda.scss @@ -440,34 +440,34 @@ justify-content: space-between; align-items: center; padding: 20px; - border-bottom: 1px solid #e9ecef; - background: #f8f9fa; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; } .panel-header h3 { margin: 0; - color: #2c3e50; + color: white; font-size: 1.2rem; + font-weight: 600; } .btn-close { - background: none; + background: rgba(255, 255, 255, 0.2); border: none; - font-size: 1.5rem; - color: #6c757d; - cursor: pointer; - padding: 5px; + color: white; + font-size: 24px; + width: 32px; + height: 32px; border-radius: 50%; - width: 30px; - height: 30px; + cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s ease; &:hover { - background: #e9ecef; - color: #495057; + background: rgba(255, 255, 255, 0.3); + transform: scale(1.1); } } @@ -511,6 +511,78 @@ flex: 1; padding: 20px; overflow-y: auto; + background: white; +} + +// Appliquer les couleurs de la modale au formulaire d'édition +.panel-content :deep(form) { + .row label { + color: #495057; + font-weight: 500; + } + + .input { + border: 1px solid #e9ecef; + border-radius: 6px; + + &:focus { + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); + } + } + + .presets { + background: rgba(102, 126, 234, 0.05); + border: 1px dashed rgba(102, 126, 234, 0.3); + border-radius: 10px; + } + + .preset-list button:hover { + background-color: #667eea; + color: white; + border-color: #667eea; + } + + .actions { + .btn-primary { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + border: none; + + &:hover { + opacity: 0.9; + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(102, 126, 234, 0.3); + } + } + + .btn-ghost { + border: 1px solid #667eea; + color: #667eea; + background: transparent; + + &:hover { + background: rgba(102, 126, 234, 0.1); + } + + &.btn-danger { + border-color: #dc3545; + color: #dc3545; + + &:hover { + background: rgba(220, 53, 69, 0.1); + } + } + } + } + + .prop-row { + background: #f8f9fa; + padding: 12px; + border-radius: 8px; + border-left: 4px solid #667eea; + margin-bottom: 12px; + } } // Responsive diff --git a/frontend/src/app/pages/agenda/agenda.ts b/frontend/src/app/pages/agenda/agenda.ts index 7ba0582..4544184 100644 --- a/frontend/src/app/pages/agenda/agenda.ts +++ b/frontend/src/app/pages/agenda/agenda.ts @@ -23,6 +23,7 @@ interface OedbEvent { stop?: string; description?: string; where?: string; + type?: string; }; geometry?: { type: string; @@ -106,13 +107,14 @@ export class Agenda implements OnInit, OnDestroy, AfterViewInit { if (add) { this.addMode = add; this.showEditForm = true; - // Créer un événement temporaire avec le type what défini + // Créer un événement temporaire avec le type what défini et type=scheduled this.selectedEvent = { id: '', properties: { what: add, label: '', description: '', + type: 'scheduled', start: new Date().toISOString(), stop: new Date(Date.now() + 24 * 3600 * 1000).toISOString() } @@ -465,6 +467,11 @@ export class Agenda implements OnInit, OnDestroy, AfterViewInit { this.showEditForm = true; this.addMode = null; } + + // Handler pour le bouton "Quitter l'édition" du formulaire + onEditFormCanceled() { + this.closeEditForm(); + } scrollToDateInSidebar(date: Date) { setTimeout(() => { @@ -619,18 +626,14 @@ export class Agenda implements OnInit, OnDestroy, AfterViewInit { const currentLimit = this.route.snapshot.queryParamMap.get('limit'); const limitValue = currentLimit ? Number(currentLimit) : this.defaultLimit; this.loadEvents({ what: this.selectedWhatFilter, limit: limitValue }); - this.selectedEvent = null; - this.showEditForm = false; - this.addMode = null; + this.closeEditForm(); } onEventCreated() { const currentLimit = this.route.snapshot.queryParamMap.get('limit'); const limitValue = currentLimit ? Number(currentLimit) : this.defaultLimit; this.loadEvents({ what: this.selectedWhatFilter, limit: limitValue }); - this.selectedEvent = null; - this.showEditForm = false; - this.addMode = null; + this.closeEditForm(); // Retirer le paramètre add de l'URL this.router.navigate([], { relativeTo: this.route, @@ -643,8 +646,7 @@ export class Agenda implements OnInit, OnDestroy, AfterViewInit { const currentLimit = this.route.snapshot.queryParamMap.get('limit'); const limitValue = currentLimit ? Number(currentLimit) : this.defaultLimit; this.loadEvents({ what: this.selectedWhatFilter, limit: limitValue }); - this.selectedEvent = null; - this.showEditForm = false; + this.closeEditForm(); } ngOnDestroy() { diff --git a/frontend/src/app/pages/agenda/calendar/calendar.ts b/frontend/src/app/pages/agenda/calendar/calendar.ts index 4ca7927..a2700f1 100644 --- a/frontend/src/app/pages/agenda/calendar/calendar.ts +++ b/frontend/src/app/pages/agenda/calendar/calendar.ts @@ -137,8 +137,7 @@ export class CalendarComponent implements OnInit, OnDestroy, OnChanges { onEventClick(event: CalendarEvent, $event: Event) { $event.stopPropagation(); - this.selectedEvent = event; - this.showEventDetails = true; + // Ne plus afficher la modale, juste émettre l'événement pour que le parent ouvre le panel de droite this.eventClick.emit(event); } diff --git a/frontend/src/app/pages/embed/embed-view/embed-view.html b/frontend/src/app/pages/embed/embed-view/embed-view.html new file mode 100644 index 0000000..3360834 --- /dev/null +++ b/frontend/src/app/pages/embed/embed-view/embed-view.html @@ -0,0 +1,51 @@ +
+
+

Événements OEDB

+
+ + @if (isLoading) { +
+
+

Chargement des événements...

+
+ } @else if (error) { +
+

{{error}}

+
+ } @else if (events.length === 0) { +
+

Aucun événement trouvé

+
+ } @else { +
+ @for (event of events; track event.id) { +
+
+

+ {{event.properties?.label || event.properties?.name || 'Événement sans nom'}} +

+ @if (event.properties?.what) { + {{event.properties.what}} + } +
+
+
+ 📅 {{formatDate(event.properties?.start || event.properties?.when)}} +
+ @if (event.properties?.where) { +
+ 📍 {{event.properties.where}} +
+ } + @if (event.properties?.description) { +
+ {{event.properties.description}} +
+ } +
+
+ } +
+ } +
+ diff --git a/frontend/src/app/pages/embed/embed-view/embed-view.scss b/frontend/src/app/pages/embed/embed-view/embed-view.scss new file mode 100644 index 0000000..b2393d2 --- /dev/null +++ b/frontend/src/app/pages/embed/embed-view/embed-view.scss @@ -0,0 +1,136 @@ +.embed-view-container { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: #ffffff; + color: #2c3e50; + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.embed-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 16px 20px; + text-align: center; + + h2 { + margin: 0; + font-size: 1.2rem; + font-weight: 600; + } +} + +.embed-loading, +.embed-error, +.embed-empty { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px 20px; + text-align: center; +} + +.spinner { + width: 40px; + height: 40px; + border: 4px solid #e9ecef; + border-top: 4px solid #667eea; + border-radius: 50%; + animation: spin 1s linear infinite; + margin-bottom: 16px; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +.embed-error { + color: #e74c3c; + background: #fdf2f2; +} + +.embed-empty { + color: #6c757d; +} + +.embed-events-list { + flex: 1; + overflow-y: auto; + padding: 20px; + display: flex; + flex-direction: column; + gap: 16px; +} + +.embed-event-item { + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 8px; + padding: 16px; + transition: all 0.2s ease; + border-left: 4px solid #667eea; +} + +.embed-event-item:hover { + background: #ffffff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transform: translateX(2px); +} + +.embed-event-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 12px; + gap: 12px; +} + +.embed-event-title { + margin: 0; + font-size: 1.1rem; + font-weight: 600; + color: #2c3e50; + flex: 1; +} + +.embed-event-type { + background: #667eea; + color: white; + padding: 4px 10px; + border-radius: 12px; + font-size: 0.75rem; + white-space: nowrap; +} + +.embed-event-details { + display: flex; + flex-direction: column; + gap: 8px; + font-size: 0.9rem; +} + +.embed-event-date, +.embed-event-location { + color: #6c757d; +} + +.embed-event-description { + color: #495057; + margin-top: 8px; + line-height: 1.5; +} + +/* Responsive */ +@media (max-width: 768px) { + .embed-event-header { + flex-direction: column; + } + + .embed-event-type { + align-self: flex-start; + } +} + diff --git a/frontend/src/app/pages/embed/embed-view/embed-view.ts b/frontend/src/app/pages/embed/embed-view/embed-view.ts new file mode 100644 index 0000000..4b113c1 --- /dev/null +++ b/frontend/src/app/pages/embed/embed-view/embed-view.ts @@ -0,0 +1,83 @@ +import { Component, OnInit, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ActivatedRoute } from '@angular/router'; +import { OedbApi } from '../../../services/oedb-api'; + +interface OedbEvent { + id: string; + properties: { + label?: string; + name?: string; + what?: string; + start?: string; + when?: string; + stop?: string; + description?: string; + where?: string; + }; +} + +@Component({ + selector: 'app-embed-view', + standalone: true, + imports: [CommonModule], + templateUrl: './embed-view.html', + styleUrl: './embed-view.scss' +}) +export class EmbedView implements OnInit { + private oedbApi = inject(OedbApi); + private route = inject(ActivatedRoute); + + events: OedbEvent[] = []; + isLoading = true; + error: string | null = null; + + ngOnInit() { + this.route.queryParams.subscribe(params => { + this.loadEvents(params); + }); + } + + loadEvents(params: any) { + this.isLoading = true; + this.error = null; + + const apiParams: any = { + when: "NEXT365DAYS", + limit: params.limit ? Number(params.limit) : 50 + }; + + 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) => { + this.events = Array.isArray(response?.features) ? response.features : []; + this.isLoading = false; + }, + error: (err) => { + console.error('Error loading events:', err); + this.error = 'Erreur lors du chargement des événements'; + this.isLoading = false; + } + }); + } + + formatDate(dateString: string | undefined): string { + if (!dateString) return '—'; + 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; + } + } +} + diff --git a/frontend/src/app/pages/embed/embed.html b/frontend/src/app/pages/embed/embed.html index 4a41411..f802834 100644 --- a/frontend/src/app/pages/embed/embed.html +++ b/frontend/src/app/pages/embed/embed.html @@ -116,6 +116,17 @@
+ +
+ + +
diff --git a/frontend/src/app/pages/embed/embed.ts b/frontend/src/app/pages/embed/embed.ts index 75a8d48..848f3d4 100644 --- a/frontend/src/app/pages/embed/embed.ts +++ b/frontend/src/app/pages/embed/embed.ts @@ -12,6 +12,7 @@ interface EmbedConfig { width: string; height: string; theme: string; + embedType: 'script' | 'iframe'; } @Component({ @@ -23,14 +24,15 @@ interface EmbedConfig { }) export class Embed { config = signal({ - apiUrl: 'https://api.openenventdatabase.org', + apiUrl: 'https://api.openeventdatabase.org', what: 'culture', start: '', end: '', limit: 50, width: '100%', height: '400px', - theme: 'light' + theme: 'light', + embedType: 'iframe' }); generatedCode = signal(''); @@ -49,29 +51,46 @@ export class Embed { 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()); + if (config.limit) params.set('limit', config.limit.toString()); const queryString = params.toString(); - const scriptUrl = `${window.location.origin}/embed.js`; - return ` + if (config.embedType === 'iframe') { + // Générer du code iframe qui pointe vers une page embed spéciale + const embedUrl = `${window.location.origin}/embed/view?${queryString}`; + return ` + +`; + } else { + // Code script (ancienne méthode) + const scriptUrl = `${window.location.origin}/embed.js`; + const paramsObj = queryString ? + queryString.split('&').map(param => { + const [key, value] = param.split('='); + return ` '${key}': '${decodeURIComponent(value || '')}'`; + }).join(',\n') : ''; + + const paramsCode = paramsObj ? ` params: {\n${paramsObj}\n },\n` : ''; + + return `
- - -`; - - +`; + } } copyToClipboard() { @@ -93,19 +112,37 @@ export class Embed { Aperçu OEDB Embed + +

Aperçu de l'intégration

+

Voici comment l'intégration apparaîtra sur votre site :

${code}
+ ${config.embedType === 'script' ? ` + + ` : ''} `); + // Si c'est un iframe, le charger après que le document soit écrit + if (config.embedType === 'iframe') { + previewWindow.document.close(); + } } } } diff --git a/frontend/src/app/pages/home/home.html b/frontend/src/app/pages/home/home.html index c251e1c..9952473 100644 --- a/frontend/src/app/pages/home/home.html +++ b/frontend/src/app/pages/home/home.html @@ -5,6 +5,9 @@ @if (isLoading) { } + + 🔗 Partager +
@if(showOptions){ diff --git a/frontend/src/app/pages/home/home.scss b/frontend/src/app/pages/home/home.scss index 0acb839..466e4a7 100644 --- a/frontend/src/app/pages/home/home.scss +++ b/frontend/src/app/pages/home/home.scss @@ -63,8 +63,29 @@ .toolbar { display: flex; align-items: center; + gap: 12px; justify-content: space-between; padding: 8px 12px; + + .btn-share { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + text-decoration: none; + border-radius: 8px; + font-size: 0.9rem; + font-weight: 500; + transition: all 0.2s ease; + white-space: nowrap; + + &:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); + } + } .loading { color: #007bff; diff --git a/frontend/src/app/pages/home/home.ts b/frontend/src/app/pages/home/home.ts index e9a0543..5049e22 100644 --- a/frontend/src/app/pages/home/home.ts +++ b/frontend/src/app/pages/home/home.ts @@ -1,5 +1,5 @@ import {Component, inject, signal, OnDestroy, OnInit} from '@angular/core'; -import {Router} from '@angular/router'; +import {Router, RouterLink} from '@angular/router'; import {FormsModule} from '@angular/forms'; import {Menu} from './menu/menu'; import {AllEvents} from '../../maps/all-events/all-events'; @@ -25,7 +25,8 @@ import {NgClass} from '@angular/common'; Osm, FormsModule, WhatFilterComponent, - NgClass + NgClass, + RouterLink ], templateUrl: './home.html', styleUrl: './home.scss' diff --git a/frontend/src/app/pages/home/menu/menu.html b/frontend/src/app/pages/home/menu/menu.html index d339c06..f8aabc9 100644 --- a/frontend/src/app/pages/home/menu/menu.html +++ b/frontend/src/app/pages/home/menu/menu.html @@ -1,38 +1,45 @@ + + -