embed share
This commit is contained in:
parent
5d636b0027
commit
f8abb4d11a
20 changed files with 1040 additions and 72 deletions
|
|
@ -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 || [];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
</div>
|
||||
<div class="row">
|
||||
<label>Label</label>
|
||||
<input class="input" type="text" formControlName="label" />
|
||||
<input #labelInput class="input" type="text" formControlName="label" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Description</label>
|
||||
|
|
@ -84,9 +84,8 @@
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
<pre>
|
||||
{{currentPreset() | json}}
|
||||
</pre>
|
||||
<!-- Debug preset caché -->
|
||||
<!-- <pre>{{currentPreset() | json}}</pre> -->
|
||||
|
||||
<div class="row">
|
||||
<label>Type</label>
|
||||
|
|
@ -109,6 +108,16 @@
|
|||
<label>Wikidata</label>
|
||||
<input class="input" type="text" formControlName="wikidata" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="location-controls">
|
||||
<button type="button" class="btn btn-secondary btn-map-picker" (click)="openMapPicker()">
|
||||
🗺️ Sélectionner sur la carte
|
||||
</button>
|
||||
@if (form.get('lat')?.value && form.get('lon')?.value) {
|
||||
<span class="location-status">📍 Coordonnées définies</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Latitude</label>
|
||||
<input class="input" type="number" step="any" formControlName="lat" />
|
||||
|
|
@ -118,6 +127,41 @@
|
|||
<input class="input" type="number" step="any" formControlName="lon" />
|
||||
</div>
|
||||
|
||||
<!-- Modale de sélection de carte -->
|
||||
@if (showMapPicker) {
|
||||
<div class="map-picker-overlay" (click)="closeMapPicker()">
|
||||
<div class="map-picker-modal" (click)="$event.stopPropagation()">
|
||||
<div class="map-picker-header">
|
||||
<h3>Sélectionner un lieu sur la carte</h3>
|
||||
<button class="btn-close-map" (click)="closeMapPicker()">×</button>
|
||||
</div>
|
||||
<div class="map-picker-content">
|
||||
<div #mapContainer class="map-picker-map"></div>
|
||||
<div class="map-picker-info">
|
||||
<p>Cliquez sur la carte pour sélectionner un lieu</p>
|
||||
@if (selectedLocation) {
|
||||
<div class="selected-coords">
|
||||
<strong>Coordonnées sélectionnées :</strong><br>
|
||||
Latitude: {{selectedLocation.lat.toFixed(6)}}<br>
|
||||
Longitude: {{selectedLocation.lon.toFixed(6)}}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="map-picker-actions">
|
||||
<button type="button" class="btn btn-ghost" (click)="closeMapPicker()">Annuler</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
(click)="validateLocation()"
|
||||
[disabled]="!selectedLocation">
|
||||
Valider
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
||||
<div class="actions">
|
||||
|
|
@ -131,7 +175,7 @@
|
|||
|
||||
@if (status().state !== 'idle') {
|
||||
<div class="toast-container">
|
||||
<div class="toast" [class.is-info]="status().state==='saving'" [class.is-success]="status().state==='saved'" [class.is-error]="status().state==='error'">
|
||||
<div class="toast" [class.is-info]="status().state==='saving'" [class.is-success]="status().state==='saved' || status().state==='created'" [class.is-error]="status().state==='error'">
|
||||
@if (status().state === 'saving') {
|
||||
<div>{{status().message}}</div>
|
||||
} @else if (status().state === 'saved') {
|
||||
|
|
@ -139,6 +183,19 @@
|
|||
{{status().message}}.
|
||||
<a [href]="'/demo/by-what?what=' + (status().what || form.value.what)" target="_blank">Voir d'autres évènements de ce type</a>
|
||||
</div>
|
||||
} @else if (status().state === 'created') {
|
||||
<div class="success-message">
|
||||
<strong>✅ {{status().message}}</strong>
|
||||
<p>Merci d'avoir créé cet événement !</p>
|
||||
<div class="success-actions">
|
||||
@if (status().createdEvent) {
|
||||
<button type="button" class="btn btn-primary btn-sm" (click)="viewCreatedEvent()">
|
||||
Voir l'événement en détail
|
||||
</button>
|
||||
}
|
||||
<span class="or-text">ou continuez à modifier ci-dessus</span>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (status().state === 'error') {
|
||||
<div>{{status().message}}</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,13 +11,16 @@ 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<any>();
|
||||
@Output() created = new EventEmitter<any>();
|
||||
@Output() deleted = new EventEmitter<any>();
|
||||
@Output() canceled = new EventEmitter<void>();
|
||||
|
||||
@ViewChild('labelInput', { static: false }) labelInput?: ElementRef<HTMLInputElement>;
|
||||
@ViewChild('mapContainer', { static: false }) mapContainer?: ElementRef<HTMLDivElement>;
|
||||
|
||||
form: FormGroup;
|
||||
allPresets: Array<{ key: string, label: string, emoji: string, category: string, description?: string, durationHours?: number, properties?: Record<string, { label?: string, writable?: boolean, values?: any[], default?: any, allow_custom?: boolean, allow_empty?: boolean }> }>;
|
||||
filteredGroups = computed(() => {
|
||||
|
|
@ -41,11 +44,18 @@ 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<string | number | null>(null);
|
||||
durationHuman = signal<string>('');
|
||||
|
||||
// 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({
|
||||
label: ['', Validators.required],
|
||||
|
|
@ -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,11 +407,47 @@ 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<string, any>;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -403,6 +455,140 @@ export class EditForm implements OnChanges {
|
|||
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<void> {
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -119,8 +119,8 @@
|
|||
@if (selectedEvent || (showEditForm && addMode)) {
|
||||
<div class="event-edit-panel">
|
||||
<div class="panel-header">
|
||||
<h3>@if (selectedEvent && selectedEvent.id) { Modifier l'événement } @else { Créer un événement }</h3>
|
||||
<button class="btn-close" (click)="selectedEvent = null; showEditForm = false; addMode = null">×</button>
|
||||
<h3>@if (addMode) { Créer un événement } @else if (selectedEvent && selectedEvent.id) { Modifier l'événement } @else { Modifier l'événement }</h3>
|
||||
<button class="btn-close" (click)="closeEditForm()">×</button>
|
||||
</div>
|
||||
@if (selectedEvent && selectedEvent.id) {
|
||||
<div class="selected">
|
||||
|
|
@ -147,7 +147,8 @@
|
|||
[filterByPrefix]="addMode"
|
||||
(saved)="onEventSaved()"
|
||||
(created)="onEventCreated()"
|
||||
(deleted)="onEventDeleted()">
|
||||
(deleted)="onEventDeleted()"
|
||||
(canceled)="onEditFormCanceled()">
|
||||
</app-edit-form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -466,6 +468,11 @@ export class Agenda implements OnInit, OnDestroy, AfterViewInit {
|
|||
this.addMode = null;
|
||||
}
|
||||
|
||||
// Handler pour le bouton "Quitter l'édition" du formulaire
|
||||
onEditFormCanceled() {
|
||||
this.closeEditForm();
|
||||
}
|
||||
|
||||
scrollToDateInSidebar(date: Date) {
|
||||
setTimeout(() => {
|
||||
const dateKey = this.toDateKey(date);
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
51
frontend/src/app/pages/embed/embed-view/embed-view.html
Normal file
51
frontend/src/app/pages/embed/embed-view/embed-view.html
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<div class="embed-view-container">
|
||||
<div class="embed-header">
|
||||
<h2>Événements OEDB</h2>
|
||||
</div>
|
||||
|
||||
@if (isLoading) {
|
||||
<div class="embed-loading">
|
||||
<div class="spinner"></div>
|
||||
<p>Chargement des événements...</p>
|
||||
</div>
|
||||
} @else if (error) {
|
||||
<div class="embed-error">
|
||||
<p>{{error}}</p>
|
||||
</div>
|
||||
} @else if (events.length === 0) {
|
||||
<div class="embed-empty">
|
||||
<p>Aucun événement trouvé</p>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="embed-events-list">
|
||||
@for (event of events; track event.id) {
|
||||
<div class="embed-event-item">
|
||||
<div class="embed-event-header">
|
||||
<h3 class="embed-event-title">
|
||||
{{event.properties?.label || event.properties?.name || 'Événement sans nom'}}
|
||||
</h3>
|
||||
@if (event.properties?.what) {
|
||||
<span class="embed-event-type">{{event.properties.what}}</span>
|
||||
}
|
||||
</div>
|
||||
<div class="embed-event-details">
|
||||
<div class="embed-event-date">
|
||||
📅 {{formatDate(event.properties?.start || event.properties?.when)}}
|
||||
</div>
|
||||
@if (event.properties?.where) {
|
||||
<div class="embed-event-location">
|
||||
📍 {{event.properties.where}}
|
||||
</div>
|
||||
}
|
||||
@if (event.properties?.description) {
|
||||
<div class="embed-event-description">
|
||||
{{event.properties.description}}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
136
frontend/src/app/pages/embed/embed-view/embed-view.scss
Normal file
136
frontend/src/app/pages/embed/embed-view/embed-view.scss
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
83
frontend/src/app/pages/embed/embed-view/embed-view.ts
Normal file
83
frontend/src/app/pages/embed/embed-view/embed-view.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -116,6 +116,17 @@
|
|||
<option value="dark">Sombre</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="embedType">Type d'intégration :</label>
|
||||
<select
|
||||
id="embedType"
|
||||
[(ngModel)]="config().embedType"
|
||||
(ngModelChange)="updateConfig()">
|
||||
<option value="iframe">Iframe (recommandé pour intégration simple)</option>
|
||||
<option value="script">Script JavaScript (plus flexible)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="code-output">
|
||||
|
|
|
|||
|
|
@ -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<EmbedConfig>({
|
||||
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<string>('');
|
||||
|
|
@ -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 `<!-- OpenEventDatabase Embed start-->
|
||||
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 `<!-- OpenEventDatabase Embed (iframe) -->
|
||||
<iframe
|
||||
src="${embedUrl}"
|
||||
width="${config.width}"
|
||||
height="${config.height}"
|
||||
frameborder="0"
|
||||
scrolling="auto"
|
||||
style="border: 1px solid #ddd; border-radius: 8px;">
|
||||
</iframe>
|
||||
<!-- OpenEventDatabase Embed end -->`;
|
||||
} 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 `<!-- OpenEventDatabase Embed (script) -->
|
||||
<div id="oedb-events" style="width: ${config.width}; height: ${config.height}; border: 1px solid #ddd; border-radius: 8px; overflow: hidden;"></div>
|
||||
<script src="${scriptUrl}"></script>
|
||||
<script>
|
||||
OEDBEmbed.init({
|
||||
container: '#oedb-events',
|
||||
apiUrl: '${config.apiUrl}',
|
||||
params: {
|
||||
${queryString ? queryString.split('&').map(param => `'${param.split('=')[0]}': '${param.split('=')[1]}'`).join(',\n ') : ''}
|
||||
},
|
||||
theme: '${config.theme}'
|
||||
${paramsCode} theme: '${config.theme}'
|
||||
});
|
||||
</script>
|
||||
|
||||
<!--OpenEventDatabase Embed end-->
|
||||
`;
|
||||
|
||||
|
||||
<!-- OpenEventDatabase Embed end -->`;
|
||||
}
|
||||
}
|
||||
|
||||
copyToClipboard() {
|
||||
|
|
@ -93,19 +112,37 @@ export class Embed {
|
|||
<html>
|
||||
<head>
|
||||
<title>Aperçu OEDB Embed</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body { margin: 0; padding: 20px; font-family: Arial, sans-serif; }
|
||||
.preview-container { max-width: 100%; }
|
||||
.preview-container { max-width: 100%; margin-top: 20px; }
|
||||
h2 { color: #2c3e50; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Aperçu de l'intégration</h2>
|
||||
<p>Voici comment l'intégration apparaîtra sur votre site :</p>
|
||||
<div class="preview-container">
|
||||
${code}
|
||||
</div>
|
||||
${config.embedType === 'script' ? `
|
||||
<script>
|
||||
// Attendre que le DOM soit chargé pour les scripts
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Le script embed.js se chargera automatiquement
|
||||
});
|
||||
}
|
||||
</script>
|
||||
` : ''}
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
// Si c'est un iframe, le charger après que le document soit écrit
|
||||
if (config.embedType === 'iframe') {
|
||||
previewWindow.document.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@
|
|||
@if (isLoading) {
|
||||
<span class="loading-indicator">⏳</span>
|
||||
}
|
||||
<a routerLink="/embed" class="btn-share" title="Partager et intégrer les événements">
|
||||
🔗 Partager
|
||||
</a>
|
||||
</div>
|
||||
@if(showOptions){
|
||||
|
||||
|
|
|
|||
|
|
@ -63,9 +63,30 @@
|
|||
.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;
|
||||
font-size: 12px;
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -1,38 +1,45 @@
|
|||
<menu>
|
||||
<div class="menu-overlay" [class.overlay-active]="isMenuOpen" (click)="closeMenu()"></div>
|
||||
|
||||
<div class="menu-header">
|
||||
<h1>OpenEventDatabase</h1>
|
||||
<button class="burger-menu-toggle" (click)="toggleMenu()" [attr.aria-expanded]="isMenuOpen" aria-label="Toggle menu">
|
||||
<span class="burger-line"></span>
|
||||
<span class="burger-line"></span>
|
||||
<span class="burger-line"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav class="nav">
|
||||
<nav class="nav" [class.nav-open]="isMenuOpen">
|
||||
<div class="nav-section quick-tools">
|
||||
<h3>Outils rapides</h3>
|
||||
<a routerLink="/embed" class="link highlight">🔗 Intégration embarquée</a>
|
||||
<a routerLink="/batch-edit" class="link highlight">⚡ Modification en masse</a>
|
||||
<a routerLink="/batch-edit" class="link highlight" (click)="closeMenu()">⚡ Modification en masse</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h3>Navigation principale</h3>
|
||||
<a routerLink="/" class="link">🏠 Accueil</a>
|
||||
<a routerLink="/agenda" class="link">📅 Agenda</a>
|
||||
<a routerLink="/research" class="link">🔍 Recherche</a>
|
||||
<a routerLink="/" class="link" (click)="closeMenu()">🏠 Accueil</a>
|
||||
<a routerLink="/agenda" class="link" (click)="closeMenu()">📅 Agenda</a>
|
||||
<a routerLink="/research" class="link" (click)="closeMenu()">🔍 Recherche</a>
|
||||
<a routerLink="/embed" class="link" (click)="closeMenu()">🔗 Intégration embarquée</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h3>Outils d'administration</h3>
|
||||
<a routerLink="/unlocated-events" class="link">📍 Événements non localisés</a>
|
||||
<a routerLink="/nouvelles-categories" class="link">🏷️ Nouvelles catégories</a>
|
||||
<a routerLink="/unlocated-events" class="link" (click)="closeMenu()">📍 Événements non localisés</a>
|
||||
<a routerLink="/nouvelles-categories" class="link" (click)="closeMenu()">🏷️ Nouvelles catégories</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h3>Intégration & API</h3>
|
||||
<a routerLink="/events-docs" class="link">📚 Documentation API</a>
|
||||
<a href="/stats" class="link">📊 Statistiques</a>
|
||||
<a routerLink="/events-docs" class="link" (click)="closeMenu()">📚 Documentation API</a>
|
||||
<a href="/stats" class="link" (click)="closeMenu()">📊 Statistiques</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h3>Communauté</h3>
|
||||
<a routerLink="/community-upcoming" class="link">👥 Community à venir</a>
|
||||
<a href="https://source.cipherbliss.com/tykayn/oedb-backend" class="link" target="_blank">💻 Sources</a>
|
||||
<a routerLink="/community-upcoming" class="link" (click)="closeMenu()">👥 Community à venir</a>
|
||||
<a href="https://source.cipherbliss.com/tykayn/oedb-backend" class="link" target="_blank" (click)="closeMenu()">💻 Sources</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,18 +4,111 @@
|
|||
border-right: 1px solid #e9ecef;
|
||||
min-height: 100vh;
|
||||
padding: 1rem;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
position: relative;
|
||||
min-height: auto;
|
||||
padding: 0.75rem;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
}
|
||||
}
|
||||
|
||||
// Overlay pour mobile (masqué par défaut, montré quand le menu est ouvert)
|
||||
.menu-overlay {
|
||||
display: none;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 999;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
|
||||
&.overlay-active {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-header {
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 2px solid #3498db;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
|
||||
h1 {
|
||||
color: #2c3e50;
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
flex: 1;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Bouton burger menu
|
||||
.burger-menu-toggle {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
z-index: 1001;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 2px solid #3498db;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.burger-line {
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background: #2c3e50;
|
||||
border-radius: 2px;
|
||||
transition: all 0.3s ease;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.burger-menu-toggle[aria-expanded="true"] {
|
||||
.burger-line:nth-child(1) {
|
||||
transform: rotate(45deg) translate(7px, 7px);
|
||||
}
|
||||
|
||||
.burger-line:nth-child(2) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.burger-line:nth-child(3) {
|
||||
transform: rotate(-45deg) translate(7px, -7px);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -23,6 +116,28 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
position: relative;
|
||||
z-index: 1000;
|
||||
background: #f8f9fa;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
gap: 1.5rem;
|
||||
border-radius: 0 0 8px 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
|
||||
&.nav-open {
|
||||
max-height: 2000px;
|
||||
opacity: 1;
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ export class Menu {
|
|||
public onDownloadCSV?: () => void;
|
||||
protected whats: any = [];
|
||||
|
||||
isMenuOpen = false;
|
||||
|
||||
constructor() {
|
||||
let keys = Object.keys(oedb_what_categories.presets.what);
|
||||
|
||||
|
|
@ -36,4 +38,12 @@ export class Menu {
|
|||
toggleView() { this.onToggleView && this.onToggleView(); }
|
||||
downloadGeoJSON() { this.onDownloadGeoJSON && this.onDownloadGeoJSON(); }
|
||||
downloadCSV() { this.onDownloadCSV && this.onDownloadCSV(); }
|
||||
|
||||
toggleMenu() {
|
||||
this.isMenuOpen = !this.isMenuOpen;
|
||||
}
|
||||
|
||||
closeMenu() {
|
||||
this.isMenuOpen = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue