+
@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) {
@@ -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 @@
+
+
+
+ @if (isLoading) {
+
+
+
Chargement des événements...
+
+ } @else if (error) {
+
+ } @else if (events.length === 0) {
+
+
Aucun événement trouvé
+
+ } @else {
+
+ @for (event of events; track event.id) {
+
+
+
+
+ 📅 {{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 @@