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) {
|
constructor(container, config) {
|
||||||
this.container = typeof container === 'string' ? document.querySelector(container) : container;
|
this.container = typeof container === 'string' ? document.querySelector(container) : container;
|
||||||
this.config = { ...defaultConfig, ...config };
|
this.config = { ...defaultConfig, ...config };
|
||||||
|
// Fusionner les params si présents
|
||||||
|
if (config.params) {
|
||||||
|
this.config = { ...this.config, ...config.params };
|
||||||
|
}
|
||||||
this.events = [];
|
this.events = [];
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.refreshTimer = null;
|
this.refreshTimer = null;
|
||||||
|
|
@ -207,7 +211,7 @@
|
||||||
if (this.config.limit) params.set('limit', this.config.limit.toString());
|
if (this.config.limit) params.set('limit', this.config.limit.toString());
|
||||||
if (this.config.bbox) params.set('bbox', this.config.bbox);
|
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();
|
const data = await response.json();
|
||||||
|
|
||||||
this.events = data.features || [];
|
this.events = data.features || [];
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {Home} from './pages/home/home';
|
||||||
import { Agenda } from './pages/agenda/agenda';
|
import { Agenda } from './pages/agenda/agenda';
|
||||||
import { Research } from './pages/research/research';
|
import { Research } from './pages/research/research';
|
||||||
import { Embed } from './pages/embed/embed';
|
import { Embed } from './pages/embed/embed';
|
||||||
|
import { EmbedView } from './pages/embed/embed-view/embed-view';
|
||||||
import { BatchEdit } from './pages/batch-edit/batch-edit';
|
import { BatchEdit } from './pages/batch-edit/batch-edit';
|
||||||
import { NouvellesCategories } from './pages/nouvelles-categories/nouvelles-categories';
|
import { NouvellesCategories } from './pages/nouvelles-categories/nouvelles-categories';
|
||||||
import { UnlocatedEventsPage } from './pages/unlocated-events/unlocated-events';
|
import { UnlocatedEventsPage } from './pages/unlocated-events/unlocated-events';
|
||||||
|
|
@ -35,6 +36,10 @@ export const routes: Routes = [
|
||||||
path: 'embed',
|
path: 'embed',
|
||||||
component: Embed
|
component: Embed
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'embed/view',
|
||||||
|
component: EmbedView
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'stats',
|
path: 'stats',
|
||||||
component: Stats
|
component: Stats
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Label</label>
|
<label>Label</label>
|
||||||
<input class="input" type="text" formControlName="label" />
|
<input #labelInput class="input" type="text" formControlName="label" />
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Description</label>
|
<label>Description</label>
|
||||||
|
|
@ -84,9 +84,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<pre>
|
<!-- Debug preset caché -->
|
||||||
{{currentPreset() | json}}
|
<!-- <pre>{{currentPreset() | json}}</pre> -->
|
||||||
</pre>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<label>Type</label>
|
<label>Type</label>
|
||||||
|
|
@ -109,6 +108,16 @@
|
||||||
<label>Wikidata</label>
|
<label>Wikidata</label>
|
||||||
<input class="input" type="text" formControlName="wikidata" />
|
<input class="input" type="text" formControlName="wikidata" />
|
||||||
</div>
|
</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">
|
<div class="row">
|
||||||
<label>Latitude</label>
|
<label>Latitude</label>
|
||||||
<input class="input" type="number" step="any" formControlName="lat" />
|
<input class="input" type="number" step="any" formControlName="lat" />
|
||||||
|
|
@ -118,6 +127,41 @@
|
||||||
<input class="input" type="number" step="any" formControlName="lon" />
|
<input class="input" type="number" step="any" formControlName="lon" />
|
||||||
</div>
|
</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">
|
<div class="actions">
|
||||||
|
|
@ -131,7 +175,7 @@
|
||||||
|
|
||||||
@if (status().state !== 'idle') {
|
@if (status().state !== 'idle') {
|
||||||
<div class="toast-container">
|
<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') {
|
@if (status().state === 'saving') {
|
||||||
<div>{{status().message}}</div>
|
<div>{{status().message}}</div>
|
||||||
} @else if (status().state === 'saved') {
|
} @else if (status().state === 'saved') {
|
||||||
|
|
@ -139,6 +183,19 @@
|
||||||
{{status().message}}.
|
{{status().message}}.
|
||||||
<a [href]="'/demo/by-what?what=' + (status().what || form.value.what)" target="_blank">Voir d'autres évènements de ce type</a>
|
<a [href]="'/demo/by-what?what=' + (status().what || form.value.what)" target="_blank">Voir d'autres évènements de ce type</a>
|
||||||
</div>
|
</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') {
|
} @else if (status().state === 'error') {
|
||||||
<div>{{status().message}}</div>
|
<div>{{status().message}}</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,3 +68,170 @@ form {
|
||||||
transform: translateY(-1px);
|
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 { FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import oedb from '../../../oedb-types';
|
import oedb from '../../../oedb-types';
|
||||||
import { OedbApi } from '../../services/oedb-api';
|
import { OedbApi } from '../../services/oedb-api';
|
||||||
|
|
@ -11,13 +11,16 @@ import { JsonPipe } from '@angular/common';
|
||||||
templateUrl: './edit-form.html',
|
templateUrl: './edit-form.html',
|
||||||
styleUrl: './edit-form.scss'
|
styleUrl: './edit-form.scss'
|
||||||
})
|
})
|
||||||
export class EditForm implements OnChanges {
|
export class EditForm implements OnChanges, AfterViewInit, OnDestroy {
|
||||||
@Input() selected: any | null = null;
|
@Input() selected: any | null = null;
|
||||||
@Output() saved = new EventEmitter<any>();
|
@Output() saved = new EventEmitter<any>();
|
||||||
@Output() created = new EventEmitter<any>();
|
@Output() created = new EventEmitter<any>();
|
||||||
@Output() deleted = new EventEmitter<any>();
|
@Output() deleted = new EventEmitter<any>();
|
||||||
@Output() canceled = new EventEmitter<void>();
|
@Output() canceled = new EventEmitter<void>();
|
||||||
|
|
||||||
|
@ViewChild('labelInput', { static: false }) labelInput?: ElementRef<HTMLInputElement>;
|
||||||
|
@ViewChild('mapContainer', { static: false }) mapContainer?: ElementRef<HTMLDivElement>;
|
||||||
|
|
||||||
form: FormGroup;
|
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 }> }>;
|
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(() => {
|
filteredGroups = computed(() => {
|
||||||
|
|
@ -41,11 +44,18 @@ export class EditForm implements OnChanges {
|
||||||
'label','description','what','where','lat','lon','noLocation','wikidata','featureType','type','start','stop'
|
'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);
|
featureId = signal<string | number | null>(null);
|
||||||
durationHuman = signal<string>('');
|
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) {
|
constructor(private fb: FormBuilder, private api: OedbApi) {
|
||||||
this.form = this.fb.group({
|
this.form = this.fb.group({
|
||||||
label: ['', Validators.required],
|
label: ['', Validators.required],
|
||||||
|
|
@ -103,6 +113,8 @@ export class EditForm implements OnChanges {
|
||||||
this.featureId.set((propId ?? sel.id) ?? null);
|
this.featureId.set((propId ?? sel.id) ?? null);
|
||||||
const p = sel.properties || {};
|
const p = sel.properties || {};
|
||||||
const coords = sel?.geometry?.coordinates || [];
|
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({
|
this.form.patchValue({
|
||||||
label: p.label || p.name || '',
|
label: p.label || p.name || '',
|
||||||
id: p.id || '',
|
id: p.id || '',
|
||||||
|
|
@ -114,7 +126,7 @@ export class EditForm implements OnChanges {
|
||||||
lon: coords[0] ?? '',
|
lon: coords[0] ?? '',
|
||||||
wikidata: p.wikidata || '',
|
wikidata: p.wikidata || '',
|
||||||
featureType: 'point',
|
featureType: 'point',
|
||||||
type: p.type || this.form.value.type || 'unscheduled',
|
type: eventType,
|
||||||
start: this.toLocalInputValue(p.start || p.when || new Date()),
|
start: this.toLocalInputValue(p.start || p.when || new Date()),
|
||||||
stop: this.toLocalInputValue(p.stop || new Date(new Date().getTime() + 24 * 3600 * 1000))
|
stop: this.toLocalInputValue(p.stop || new Date(new Date().getTime() + 24 * 3600 * 1000))
|
||||||
}, { emitEvent: false });
|
}, { emitEvent: false });
|
||||||
|
|
@ -329,11 +341,14 @@ export class EditForm implements OnChanges {
|
||||||
} else {
|
} else {
|
||||||
this.api.createEvent(feature).subscribe({
|
this.api.createEvent(feature).subscribe({
|
||||||
next: (res) => {
|
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);
|
this.created.emit(res);
|
||||||
// Quitter l'édition après succès
|
// Ne pas quitter l'édition après création, laisser l'utilisateur voir/modifier
|
||||||
this.canceled.emit();
|
|
||||||
setTimeout(() => this.status.set({ state: 'idle' }), 3000);
|
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
this.status.set({ state: 'error', what: val.what, message: 'Erreur lors de la création' });
|
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() {
|
resetPresetFilter() {
|
||||||
// Réinitialise le champ what et le filtre de préfixe pour afficher tous les presets
|
// Réinitialise le champ what et le filtre de préfixe pour afficher tous les presets
|
||||||
|
this._filterByPrefix = null;
|
||||||
this.presetFilterPrefix = null;
|
this.presetFilterPrefix = null;
|
||||||
this.form.patchValue({ what: '' });
|
this.form.patchValue({ what: '' });
|
||||||
}
|
}
|
||||||
|
|
@ -391,11 +407,47 @@ export class EditForm implements OnChanges {
|
||||||
private _filterByPrefix: string | null = null;
|
private _filterByPrefix: string | null = null;
|
||||||
|
|
||||||
@Input() set filterByPrefix(prefix: string | null) {
|
@Input() set filterByPrefix(prefix: string | null) {
|
||||||
|
const previousPrefix = this._filterByPrefix;
|
||||||
this._filterByPrefix = prefix;
|
this._filterByPrefix = prefix;
|
||||||
this.presetFilterPrefix = prefix;
|
this.presetFilterPrefix = prefix;
|
||||||
// Si un préfixe est défini et que le champ what est vide, pré-remplir avec le préfixe
|
// 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) {
|
if (prefix && (!this.form.get('what')?.value || previousPrefix !== prefix)) {
|
||||||
this.form.patchValue({ what: prefix }, { emitEvent: false });
|
// 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;
|
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() {
|
onCancelEdit() {
|
||||||
this.selected = null;
|
this.selected = null;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,8 +119,8 @@
|
||||||
@if (selectedEvent || (showEditForm && addMode)) {
|
@if (selectedEvent || (showEditForm && addMode)) {
|
||||||
<div class="event-edit-panel">
|
<div class="event-edit-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h3>@if (selectedEvent && selectedEvent.id) { Modifier l'événement } @else { Créer un événement }</h3>
|
<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)="selectedEvent = null; showEditForm = false; addMode = null">×</button>
|
<button class="btn-close" (click)="closeEditForm()">×</button>
|
||||||
</div>
|
</div>
|
||||||
@if (selectedEvent && selectedEvent.id) {
|
@if (selectedEvent && selectedEvent.id) {
|
||||||
<div class="selected">
|
<div class="selected">
|
||||||
|
|
@ -147,7 +147,8 @@
|
||||||
[filterByPrefix]="addMode"
|
[filterByPrefix]="addMode"
|
||||||
(saved)="onEventSaved()"
|
(saved)="onEventSaved()"
|
||||||
(created)="onEventCreated()"
|
(created)="onEventCreated()"
|
||||||
(deleted)="onEventDeleted()">
|
(deleted)="onEventDeleted()"
|
||||||
|
(canceled)="onEditFormCanceled()">
|
||||||
</app-edit-form>
|
</app-edit-form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -440,34 +440,34 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-bottom: 1px solid #e9ecef;
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
background: #f8f9fa;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header h3 {
|
.panel-header h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #2c3e50;
|
color: white;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-close {
|
.btn-close {
|
||||||
background: none;
|
background: rgba(255, 255, 255, 0.2);
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 1.5rem;
|
color: white;
|
||||||
color: #6c757d;
|
font-size: 24px;
|
||||||
cursor: pointer;
|
width: 32px;
|
||||||
padding: 5px;
|
height: 32px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
width: 30px;
|
cursor: pointer;
|
||||||
height: 30px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #e9ecef;
|
background: rgba(255, 255, 255, 0.3);
|
||||||
color: #495057;
|
transform: scale(1.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -511,6 +511,78 @@
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
overflow-y: auto;
|
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
|
// Responsive
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ interface OedbEvent {
|
||||||
stop?: string;
|
stop?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
where?: string;
|
where?: string;
|
||||||
|
type?: string;
|
||||||
};
|
};
|
||||||
geometry?: {
|
geometry?: {
|
||||||
type: string;
|
type: string;
|
||||||
|
|
@ -106,13 +107,14 @@ export class Agenda implements OnInit, OnDestroy, AfterViewInit {
|
||||||
if (add) {
|
if (add) {
|
||||||
this.addMode = add;
|
this.addMode = add;
|
||||||
this.showEditForm = true;
|
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 = {
|
this.selectedEvent = {
|
||||||
id: '',
|
id: '',
|
||||||
properties: {
|
properties: {
|
||||||
what: add,
|
what: add,
|
||||||
label: '',
|
label: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
type: 'scheduled',
|
||||||
start: new Date().toISOString(),
|
start: new Date().toISOString(),
|
||||||
stop: new Date(Date.now() + 24 * 3600 * 1000).toISOString()
|
stop: new Date(Date.now() + 24 * 3600 * 1000).toISOString()
|
||||||
}
|
}
|
||||||
|
|
@ -466,6 +468,11 @@ export class Agenda implements OnInit, OnDestroy, AfterViewInit {
|
||||||
this.addMode = null;
|
this.addMode = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handler pour le bouton "Quitter l'édition" du formulaire
|
||||||
|
onEditFormCanceled() {
|
||||||
|
this.closeEditForm();
|
||||||
|
}
|
||||||
|
|
||||||
scrollToDateInSidebar(date: Date) {
|
scrollToDateInSidebar(date: Date) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const dateKey = this.toDateKey(date);
|
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 currentLimit = this.route.snapshot.queryParamMap.get('limit');
|
||||||
const limitValue = currentLimit ? Number(currentLimit) : this.defaultLimit;
|
const limitValue = currentLimit ? Number(currentLimit) : this.defaultLimit;
|
||||||
this.loadEvents({ what: this.selectedWhatFilter, limit: limitValue });
|
this.loadEvents({ what: this.selectedWhatFilter, limit: limitValue });
|
||||||
this.selectedEvent = null;
|
this.closeEditForm();
|
||||||
this.showEditForm = false;
|
|
||||||
this.addMode = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onEventCreated() {
|
onEventCreated() {
|
||||||
const currentLimit = this.route.snapshot.queryParamMap.get('limit');
|
const currentLimit = this.route.snapshot.queryParamMap.get('limit');
|
||||||
const limitValue = currentLimit ? Number(currentLimit) : this.defaultLimit;
|
const limitValue = currentLimit ? Number(currentLimit) : this.defaultLimit;
|
||||||
this.loadEvents({ what: this.selectedWhatFilter, limit: limitValue });
|
this.loadEvents({ what: this.selectedWhatFilter, limit: limitValue });
|
||||||
this.selectedEvent = null;
|
this.closeEditForm();
|
||||||
this.showEditForm = false;
|
|
||||||
this.addMode = null;
|
|
||||||
// Retirer le paramètre add de l'URL
|
// Retirer le paramètre add de l'URL
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
|
|
@ -643,8 +646,7 @@ export class Agenda implements OnInit, OnDestroy, AfterViewInit {
|
||||||
const currentLimit = this.route.snapshot.queryParamMap.get('limit');
|
const currentLimit = this.route.snapshot.queryParamMap.get('limit');
|
||||||
const limitValue = currentLimit ? Number(currentLimit) : this.defaultLimit;
|
const limitValue = currentLimit ? Number(currentLimit) : this.defaultLimit;
|
||||||
this.loadEvents({ what: this.selectedWhatFilter, limit: limitValue });
|
this.loadEvents({ what: this.selectedWhatFilter, limit: limitValue });
|
||||||
this.selectedEvent = null;
|
this.closeEditForm();
|
||||||
this.showEditForm = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
|
|
||||||
|
|
@ -137,8 +137,7 @@ export class CalendarComponent implements OnInit, OnDestroy, OnChanges {
|
||||||
|
|
||||||
onEventClick(event: CalendarEvent, $event: Event) {
|
onEventClick(event: CalendarEvent, $event: Event) {
|
||||||
$event.stopPropagation();
|
$event.stopPropagation();
|
||||||
this.selectedEvent = event;
|
// Ne plus afficher la modale, juste émettre l'événement pour que le parent ouvre le panel de droite
|
||||||
this.showEventDetails = true;
|
|
||||||
this.eventClick.emit(event);
|
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>
|
<option value="dark">Sombre</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div class="code-output">
|
<div class="code-output">
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ interface EmbedConfig {
|
||||||
width: string;
|
width: string;
|
||||||
height: string;
|
height: string;
|
||||||
theme: string;
|
theme: string;
|
||||||
|
embedType: 'script' | 'iframe';
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
|
|
@ -23,14 +24,15 @@ interface EmbedConfig {
|
||||||
})
|
})
|
||||||
export class Embed {
|
export class Embed {
|
||||||
config = signal<EmbedConfig>({
|
config = signal<EmbedConfig>({
|
||||||
apiUrl: 'https://api.openenventdatabase.org',
|
apiUrl: 'https://api.openeventdatabase.org',
|
||||||
what: 'culture',
|
what: 'culture',
|
||||||
start: '',
|
start: '',
|
||||||
end: '',
|
end: '',
|
||||||
limit: 50,
|
limit: 50,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '400px',
|
height: '400px',
|
||||||
theme: 'light'
|
theme: 'light',
|
||||||
|
embedType: 'iframe'
|
||||||
});
|
});
|
||||||
|
|
||||||
generatedCode = signal<string>('');
|
generatedCode = signal<string>('');
|
||||||
|
|
@ -49,29 +51,46 @@ export class Embed {
|
||||||
if (config.what) params.set('what', config.what);
|
if (config.what) params.set('what', config.what);
|
||||||
if (config.start) params.set('start', config.start);
|
if (config.start) params.set('start', config.start);
|
||||||
if (config.end) params.set('end', config.end);
|
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 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>
|
<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 src="${scriptUrl}"></script>
|
||||||
<script>
|
<script>
|
||||||
OEDBEmbed.init({
|
OEDBEmbed.init({
|
||||||
container: '#oedb-events',
|
container: '#oedb-events',
|
||||||
apiUrl: '${config.apiUrl}',
|
apiUrl: '${config.apiUrl}',
|
||||||
params: {
|
${paramsCode} theme: '${config.theme}'
|
||||||
${queryString ? queryString.split('&').map(param => `'${param.split('=')[0]}': '${param.split('=')[1]}'`).join(',\n ') : ''}
|
|
||||||
},
|
|
||||||
theme: '${config.theme}'
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<!-- OpenEventDatabase Embed end -->`;
|
||||||
<!--OpenEventDatabase Embed end-->
|
}
|
||||||
`;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
copyToClipboard() {
|
copyToClipboard() {
|
||||||
|
|
@ -93,19 +112,37 @@ export class Embed {
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Aperçu OEDB Embed</title>
|
<title>Aperçu OEDB Embed</title>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<style>
|
<style>
|
||||||
body { margin: 0; padding: 20px; font-family: Arial, sans-serif; }
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>Aperçu de l'intégration</h2>
|
<h2>Aperçu de l'intégration</h2>
|
||||||
|
<p>Voici comment l'intégration apparaîtra sur votre site :</p>
|
||||||
<div class="preview-container">
|
<div class="preview-container">
|
||||||
${code}
|
${code}
|
||||||
</div>
|
</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>
|
</body>
|
||||||
</html>
|
</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) {
|
@if (isLoading) {
|
||||||
<span class="loading-indicator">⏳</span>
|
<span class="loading-indicator">⏳</span>
|
||||||
}
|
}
|
||||||
|
<a routerLink="/embed" class="btn-share" title="Partager et intégrer les événements">
|
||||||
|
🔗 Partager
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@if(showOptions){
|
@if(showOptions){
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,30 @@
|
||||||
.toolbar {
|
.toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 8px 12px;
|
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 {
|
.loading {
|
||||||
color: #007bff;
|
color: #007bff;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import {Component, inject, signal, OnDestroy, OnInit} from '@angular/core';
|
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 {FormsModule} from '@angular/forms';
|
||||||
import {Menu} from './menu/menu';
|
import {Menu} from './menu/menu';
|
||||||
import {AllEvents} from '../../maps/all-events/all-events';
|
import {AllEvents} from '../../maps/all-events/all-events';
|
||||||
|
|
@ -25,7 +25,8 @@ import {NgClass} from '@angular/common';
|
||||||
Osm,
|
Osm,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
WhatFilterComponent,
|
WhatFilterComponent,
|
||||||
NgClass
|
NgClass,
|
||||||
|
RouterLink
|
||||||
],
|
],
|
||||||
templateUrl: './home.html',
|
templateUrl: './home.html',
|
||||||
styleUrl: './home.scss'
|
styleUrl: './home.scss'
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,45 @@
|
||||||
<menu>
|
<menu>
|
||||||
|
<div class="menu-overlay" [class.overlay-active]="isMenuOpen" (click)="closeMenu()"></div>
|
||||||
|
|
||||||
<div class="menu-header">
|
<div class="menu-header">
|
||||||
<h1>OpenEventDatabase</h1>
|
<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>
|
</div>
|
||||||
|
|
||||||
<nav class="nav">
|
<nav class="nav" [class.nav-open]="isMenuOpen">
|
||||||
<div class="nav-section quick-tools">
|
<div class="nav-section quick-tools">
|
||||||
<h3>Outils rapides</h3>
|
<h3>Outils rapides</h3>
|
||||||
<a routerLink="/embed" class="link highlight">🔗 Intégration embarquée</a>
|
<a routerLink="/batch-edit" class="link highlight" (click)="closeMenu()">⚡ Modification en masse</a>
|
||||||
<a routerLink="/batch-edit" class="link highlight">⚡ Modification en masse</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav-section">
|
<div class="nav-section">
|
||||||
<h3>Navigation principale</h3>
|
<h3>Navigation principale</h3>
|
||||||
<a routerLink="/" class="link">🏠 Accueil</a>
|
<a routerLink="/" class="link" (click)="closeMenu()">🏠 Accueil</a>
|
||||||
<a routerLink="/agenda" class="link">📅 Agenda</a>
|
<a routerLink="/agenda" class="link" (click)="closeMenu()">📅 Agenda</a>
|
||||||
<a routerLink="/research" class="link">🔍 Recherche</a>
|
<a routerLink="/research" class="link" (click)="closeMenu()">🔍 Recherche</a>
|
||||||
|
<a routerLink="/embed" class="link" (click)="closeMenu()">🔗 Intégration embarquée</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav-section">
|
<div class="nav-section">
|
||||||
<h3>Outils d'administration</h3>
|
<h3>Outils d'administration</h3>
|
||||||
<a routerLink="/unlocated-events" class="link">📍 Événements non localisés</a>
|
<a routerLink="/unlocated-events" class="link" (click)="closeMenu()">📍 Événements non localisés</a>
|
||||||
<a routerLink="/nouvelles-categories" class="link">🏷️ Nouvelles catégories</a>
|
<a routerLink="/nouvelles-categories" class="link" (click)="closeMenu()">🏷️ Nouvelles catégories</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav-section">
|
<div class="nav-section">
|
||||||
<h3>Intégration & API</h3>
|
<h3>Intégration & API</h3>
|
||||||
<a routerLink="/events-docs" class="link">📚 Documentation API</a>
|
<a routerLink="/events-docs" class="link" (click)="closeMenu()">📚 Documentation API</a>
|
||||||
<a href="/stats" class="link">📊 Statistiques</a>
|
<a href="/stats" class="link" (click)="closeMenu()">📊 Statistiques</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav-section">
|
<div class="nav-section">
|
||||||
<h3>Communauté</h3>
|
<h3>Communauté</h3>
|
||||||
<a routerLink="/community-upcoming" class="link">👥 Community à venir</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">💻 Sources</a>
|
<a href="https://source.cipherbliss.com/tykayn/oedb-backend" class="link" target="_blank" (click)="closeMenu()">💻 Sources</a>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,111 @@
|
||||||
border-right: 1px solid #e9ecef;
|
border-right: 1px solid #e9ecef;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 1rem;
|
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 {
|
.menu-header {
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
border-bottom: 2px solid #3498db;
|
border-bottom: 2px solid #3498db;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 700;
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2rem;
|
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 {
|
.nav-section {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,8 @@ export class Menu {
|
||||||
public onDownloadCSV?: () => void;
|
public onDownloadCSV?: () => void;
|
||||||
protected whats: any = [];
|
protected whats: any = [];
|
||||||
|
|
||||||
|
isMenuOpen = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
let keys = Object.keys(oedb_what_categories.presets.what);
|
let keys = Object.keys(oedb_what_categories.presets.what);
|
||||||
|
|
||||||
|
|
@ -36,4 +38,12 @@ export class Menu {
|
||||||
toggleView() { this.onToggleView && this.onToggleView(); }
|
toggleView() { this.onToggleView && this.onToggleView(); }
|
||||||
downloadGeoJSON() { this.onDownloadGeoJSON && this.onDownloadGeoJSON(); }
|
downloadGeoJSON() { this.onDownloadGeoJSON && this.onDownloadGeoJSON(); }
|
||||||
downloadCSV() { this.onDownloadCSV && this.onDownloadCSV(); }
|
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