style marqueurs events, page nouvelles catégories

This commit is contained in:
Tykayn 2025-10-04 16:14:42 +02:00 committed by tykayn
parent 20a8445a5f
commit 9fb9986a2c
15 changed files with 987 additions and 203 deletions

View file

@ -1,14 +1,20 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core'; import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
import { provideHttpClient } from '@angular/common/http'; import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import * as moment from 'moment';
import 'moment/locale/fr';
import { routes } from './app.routes'; import { routes } from './app.routes';
// Configuration du locale français pour moment
moment.locale('fr');
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideBrowserGlobalErrorListeners(), provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }), provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes), provideRouter(routes),
provideHttpClient() provideHttpClient(),
{ provide: 'moment', useValue: moment }
] ]
}; };

View file

@ -1,6 +1,7 @@
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import {Home} from './pages/home/home'; import {Home} from './pages/home/home';
import { Agenda } from './pages/agenda/agenda'; import { Agenda } from './pages/agenda/agenda';
import { NouvellesCategories } from './pages/nouvelles-categories/nouvelles-categories';
export const routes: Routes = [ export const routes: Routes = [
{ {
@ -10,5 +11,9 @@ export const routes: Routes = [
{ {
path : 'agenda', path : 'agenda',
component: Agenda component: Agenda
},
{
path : 'nouvelles-categories',
component: NouvellesCategories
} }
]; ];

View file

@ -1,4 +1,5 @@
import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; import { Component, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import oedb_what_categories from '../../../oedb-types'; import oedb_what_categories from '../../../oedb-types';
@Component({ @Component({
@ -8,7 +9,7 @@ import oedb_what_categories from '../../../oedb-types';
templateUrl: './all-events.html', templateUrl: './all-events.html',
styleUrl: './all-events.scss' styleUrl: './all-events.scss'
}) })
export class AllEvents { export class AllEvents implements OnInit, OnDestroy {
@Input() features: Array<any> = []; @Input() features: Array<any> = [];
@Input() selected: any | null = null; @Input() selected: any | null = null;
@Input() highlight: { id: string | number, type: 'saved' | 'deleted' } | null = null; @Input() highlight: { id: string | number, type: 'saved' | 'deleted' } | null = null;
@ -22,6 +23,13 @@ export class AllEvents {
private pickedMarker: any | null = null; private pickedMarker: any | null = null;
private originalCoords: [number, number] | null = null; private originalCoords: [number, number] | null = null;
private currentPicked: [number, number] | null = null; private currentPicked: [number, number] | null = null;
private isInitialLoad = true;
private mapInitialized = false;
constructor(
private route: ActivatedRoute,
private router: Router
) {}
async ngOnInit() { async ngOnInit() {
await this.ensureMapLibre(); await this.ensureMapLibre();
@ -76,11 +84,17 @@ export class AllEvents {
private initMap() { private initMap() {
const maplibregl = (window as any).maplibregl; const maplibregl = (window as any).maplibregl;
// Récupérer les paramètres de l'URL ou utiliser les valeurs par défaut (Île-de-France)
const lat = parseFloat(this.route.snapshot.queryParams['lat']) || 48.8566;
const lon = parseFloat(this.route.snapshot.queryParams['lon']) || 2.3522;
const zoom = parseFloat(this.route.snapshot.queryParams['zoom']) || 8;
this.map = new maplibregl.Map({ this.map = new maplibregl.Map({
container: this.mapContainer.nativeElement, container: this.mapContainer.nativeElement,
style: 'https://tiles.openfreemap.org/styles/liberty', style: 'https://tiles.openfreemap.org/styles/liberty',
center: [2.3522, 48.8566], center: [lon, lat],
zoom: 5 zoom: zoom
}); });
this.map.addControl(new maplibregl.NavigationControl()); this.map.addControl(new maplibregl.NavigationControl());
this.map.addControl(new maplibregl.GeolocateControl({ positionOptions: { enableHighAccuracy: true }, trackUserLocation: true })); this.map.addControl(new maplibregl.GeolocateControl({ positionOptions: { enableHighAccuracy: true }, trackUserLocation: true }));
@ -90,6 +104,21 @@ export class AllEvents {
this.showPickedMarker(coords); this.showPickedMarker(coords);
this.pickCoords.emit(coords); this.pickCoords.emit(coords);
}); });
// Écouter les changements de vue pour mettre à jour l'URL
this.map.on('moveend', () => {
if (this.mapInitialized) {
this.updateUrlFromMap();
}
});
this.map.on('zoomend', () => {
if (this.mapInitialized) {
this.updateUrlFromMap();
}
});
this.mapInitialized = true;
} }
private getEmojiForWhat(what: string): string { private getEmojiForWhat(what: string): string {
@ -164,6 +193,24 @@ export class AllEvents {
if (this.map) this.map.flyTo({ center: this.originalCoords, zoom: Math.max(this.map.getZoom() || 12, 12) }); if (this.map) this.map.flyTo({ center: this.originalCoords, zoom: Math.max(this.map.getZoom() || 12, 12) });
} }
private updateUrlFromMap() {
if (!this.map) return;
const center = this.map.getCenter();
const zoom = this.map.getZoom();
this.router.navigate([], {
relativeTo: this.route,
queryParams: {
lat: center.lat.toFixed(6),
lon: center.lng.toFixed(6),
zoom: Math.round(zoom)
},
queryParamsHandling: 'merge',
replaceUrl: true
});
}
private renderFeatures() { private renderFeatures() {
if (!this.map || !Array.isArray(this.features)) return; if (!this.map || !Array.isArray(this.features)) return;
// clear existing markers // clear existing markers
@ -225,7 +272,15 @@ export class AllEvents {
bounds.extend(coords); bounds.extend(coords);
}); });
if (!bounds.isEmpty()) { // Ne pas faire de fitBounds lors du chargement initial si on a des paramètres URL
if (!bounds.isEmpty() && this.isInitialLoad) {
const hasUrlParams = this.route.snapshot.queryParams['lat'] || this.route.snapshot.queryParams['lon'] || this.route.snapshot.queryParams['zoom'];
if (!hasUrlParams) {
this.map.fitBounds(bounds, { padding: 40, maxZoom: 12 });
}
this.isInitialLoad = false;
} else if (!bounds.isEmpty() && !this.isInitialLoad) {
// Pour les mises à jour suivantes, on peut faire un fitBounds léger
this.map.fitBounds(bounds, { padding: 40, maxZoom: 12 }); this.map.fitBounds(bounds, { padding: 40, maxZoom: 12 });
} }
} }

View file

@ -2,35 +2,59 @@
<div class="agenda-header"> <div class="agenda-header">
<h1>Agenda des événements</h1> <h1>Agenda des événements</h1>
<p>Événements des 20 derniers jours (10 jours avant et 10 jours après aujourd'hui)</p> <p>Événements des 20 derniers jours (10 jours avant et 10 jours après aujourd'hui)</p>
<div class="calendar-controls">
<button
class="btn btn-sm"
[class.btn-primary]="view === CalendarView.Month"
(click)="setView(CalendarView.Month)">
Mois
</button>
<button
class="btn btn-sm"
[class.btn-primary]="view === CalendarView.Week"
(click)="setView(CalendarView.Week)">
Semaine
</button>
<button
class="btn btn-sm"
[class.btn-primary]="view === CalendarView.Day"
(click)="setView(CalendarView.Day)">
Jour
</button>
</div>
</div> </div>
<div class="agenda-content"> <div class="agenda-content">
<div class="days-grid"> <mwl-calendar-month-view
@for (day of daysWithEvents; track day.date.getTime()) { *ngIf="view === CalendarView.Month"
<div class="day-card" [class.today]="isToday(day.date)" [class.past]="isPast(day.date)"> [viewDate]="viewDate"
<div class="day-header"> [events]="calendarEvents"
<h3>{{ formatDate(day.date) }}</h3> (eventClicked)="onEventClicked($event)"
<span class="event-count">{{ day.events.length }} événement(s)</span> (dayClicked)="dayClicked($event)"
</div> [locale]="'fr'"
[eventTitleTemplate]="eventTitleTemplate">
</mwl-calendar-month-view>
<div class="events-list"> <mwl-calendar-week-view
@if (day.events.length === 0) { *ngIf="view === CalendarView.Week"
<p class="no-events">Aucun événement</p> [viewDate]="viewDate"
} @else { [events]="calendarEvents"
@for (event of day.events; track event.id) { (eventClicked)="onEventClicked($event)"
<div class="event-item" (click)="selectEvent(event)"> (dayClicked)="dayClicked($event)"
<div class="event-time">{{ getEventTime(event) }}</div> [locale]="'fr'"
<div class="event-title">{{ getEventTitle(event) }}</div> [eventTitleTemplate]="eventTitleTemplate">
@if (event.properties.what) { </mwl-calendar-week-view>
<div class="event-type">{{ event.properties.what }}</div>
} <mwl-calendar-day-view
</div> *ngIf="view === CalendarView.Day"
} [viewDate]="viewDate"
} [events]="calendarEvents"
</div> (eventClicked)="onEventClicked($event)"
</div> (dayClicked)="dayClicked($event)"
} [locale]="'fr'"
</div> [eventTitleTemplate]="eventTitleTemplate">
</mwl-calendar-day-view>
</div> </div>
<!-- Panneau latéral pour les détails de l'événement --> <!-- Panneau latéral pour les détails de l'événement -->
@ -57,3 +81,11 @@
<div class="overlay" (click)="closeSidePanel()"></div> <div class="overlay" (click)="closeSidePanel()"></div>
} }
</div> </div>
<!-- Template personnalisé pour les événements du calendrier -->
<ng-template #eventTitleTemplate let-event="event">
<div class="custom-event">
<span class="event-emoji">{{ getEventIcon(event.meta?.preset) }}</span>
<span class="event-title">{{ event.title }}</span>
</div>
</ng-template>

View file

@ -1,6 +1,6 @@
.agenda-container { .agenda-container {
padding: 20px; padding: 20px;
max-width: 1200px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
position: relative; position: relative;
} }
@ -17,122 +17,132 @@
p { p {
color: #666; color: #666;
font-size: 14px; font-size: 14px;
margin-bottom: 20px;
}
}
.calendar-controls {
display: flex;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
.btn {
padding: 8px 16px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: #f8f9fa;
}
&.btn-primary {
background: #007bff;
color: white;
border-color: #007bff;
}
} }
} }
.agenda-content { .agenda-content {
margin-bottom: 20px; margin-bottom: 20px;
}
.days-grid { // Styles pour angular-calendar
display: grid; ::ng-deep {
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); .cal-month-view,
gap: 20px; .cal-week-view,
} .cal-day-view {
.day-card {
background: white; background: white;
border: 1px solid #e0e0e0;
border-radius: 8px; border-radius: 8px;
padding: 15px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1); box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: all 0.3s ease; overflow: hidden;
&:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
} }
&.today { .cal-header {
border-color: #007bff;
background: #f8f9ff;
.day-header h3 {
color: #007bff;
}
}
&.past {
opacity: 0.7;
background: #f5f5f5;
}
}
.day-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.event-count {
background: #007bff;
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
}
.events-list {
min-height: 50px;
}
.no-events {
color: #999;
font-style: italic;
text-align: center;
margin: 20px 0;
}
.event-item {
background: #f8f9fa; background: #f8f9fa;
border: 1px solid #e9ecef; border-bottom: 1px solid #e9ecef;
border-radius: 6px; }
padding: 12px;
margin-bottom: 8px; .cal-header .cal-cell {
padding: 15px 10px;
font-weight: 600;
color: #495057;
}
.cal-cell {
border-right: 1px solid #e9ecef;
border-bottom: 1px solid #e9ecef;
&:last-child {
border-right: none;
}
}
.cal-today {
background: #e3f2fd !important;
}
.cal-event {
border-radius: 4px;
padding: 2px 6px;
margin: 1px;
font-size: 12px;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover { &:hover {
background: #e9ecef; transform: scale(1.02);
border-color: #007bff; box-shadow: 0 2px 4px rgba(0,0,0,0.2);
transform: translateY(-1px);
}
&:last-child {
margin-bottom: 0;
} }
} }
.event-time { .cal-event-title {
font-size: 12px;
color: #666;
font-weight: 500; font-weight: 500;
margin-bottom: 4px; white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.custom-event {
display: flex;
align-items: center;
gap: 4px;
.event-emoji {
font-size: 12px;
flex-shrink: 0;
} }
.event-title { .event-title {
font-weight: 600; font-size: 11px;
color: #333; font-weight: 500;
margin-bottom: 4px; white-space: nowrap;
line-height: 1.3; overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
} }
.event-type { .cal-month-view .cal-day-number {
font-size: 12px; font-size: 14px;
color: #007bff; font-weight: 500;
background: #e7f3ff; color: #495057;
padding: 2px 6px; }
border-radius: 4px;
display: inline-block; .cal-today .cal-day-number {
background: #007bff;
color: white;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
}
} }
// Panneau latéral // Panneau latéral

View file

@ -1,9 +1,12 @@
import { Component, inject, OnInit } from '@angular/core'; import { Component, inject, OnInit, ViewChild, TemplateRef } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { OedbApi } from '../../services/oedb-api'; import { OedbApi } from '../../services/oedb-api';
import { EditForm } from '../../forms/edit-form/edit-form'; import { EditForm } from '../../forms/edit-form/edit-form';
import { CalendarModule, CalendarView, CalendarEvent } from 'angular-calendar';
import { CalendarEventAction, CalendarEventTimesChangedEvent } from 'angular-calendar';
import oedb from '../../../oedb-types';
interface Event { interface OedbEvent {
id: string; id: string;
properties: { properties: {
label?: string; label?: string;
@ -23,23 +26,31 @@ interface Event {
interface DayEvents { interface DayEvents {
date: Date; date: Date;
events: Event[]; events: OedbEvent[];
} }
@Component({ @Component({
selector: 'app-agenda', selector: 'app-agenda',
standalone: true, standalone: true,
imports: [CommonModule, EditForm], imports: [CommonModule, EditForm, CalendarModule],
templateUrl: './agenda.html', templateUrl: './agenda.html',
styleUrl: './agenda.scss' styleUrl: './agenda.scss'
}) })
export class Agenda implements OnInit { export class Agenda implements OnInit {
private oedbApi = inject(OedbApi); private oedbApi = inject(OedbApi);
events: Event[] = []; @ViewChild('eventTitleTemplate', { static: true }) eventTitleTemplate!: TemplateRef<any>;
daysWithEvents: DayEvents[] = [];
selectedEvent: Event | null = null; events: OedbEvent[] = [];
calendarEvents: CalendarEvent[] = [];
selectedEvent: OedbEvent | null = null;
showSidePanel = false; showSidePanel = false;
view: CalendarView = CalendarView.Month;
viewDate: Date = new Date();
oedbPresets = oedb.presets.what;
// Exposer CalendarView pour l'utiliser dans le template
CalendarView = CalendarView;
ngOnInit() { ngOnInit() {
this.loadEvents(); this.loadEvents();
@ -66,37 +77,24 @@ export class Agenda implements OnInit {
} }
organizeEventsByDay() { organizeEventsByDay() {
const daysMap = new Map<string, DayEvents>(); this.calendarEvents = this.events.map(event => {
// Initialiser les 20 jours
for (let i = -10; i <= 10; i++) {
const date = new Date();
date.setDate(date.getDate() + i);
const dateKey = date.toISOString().split('T')[0];
daysMap.set(dateKey, {
date: new Date(date),
events: []
});
}
// Organiser les événements par jour
this.events.forEach(event => {
const eventDate = this.getEventDate(event); const eventDate = this.getEventDate(event);
if (eventDate) { const preset = this.getEventPreset(event);
const dateKey = eventDate.toISOString().split('T')[0];
const dayEvents = daysMap.get(dateKey); return {
if (dayEvents) { id: event.id,
dayEvents.events.push(event); title: this.getEventTitle(event),
} start: eventDate || new Date(),
color: this.getEventColor(preset),
meta: {
event: event,
preset: preset
} }
};
}); });
this.daysWithEvents = Array.from(daysMap.values()).sort((a, b) =>
a.date.getTime() - b.date.getTime()
);
} }
getEventDate(event: Event): Date | null { getEventDate(event: OedbEvent): Date | null {
const startDate = event.properties.start || event.properties.when; const startDate = event.properties.start || event.properties.when;
if (startDate) { if (startDate) {
return new Date(startDate); return new Date(startDate);
@ -104,11 +102,11 @@ export class Agenda implements OnInit {
return null; return null;
} }
getEventTitle(event: Event): string { getEventTitle(event: OedbEvent): string {
return event.properties.label || event.properties.name || 'Événement sans titre'; return event.properties.label || event.properties.name || 'Événement sans titre';
} }
getEventTime(event: Event): string { getEventTime(event: OedbEvent): string {
const startDate = event.properties.start || event.properties.when; const startDate = event.properties.start || event.properties.when;
if (startDate) { if (startDate) {
const date = new Date(startDate); const date = new Date(startDate);
@ -120,11 +118,55 @@ export class Agenda implements OnInit {
return ''; return '';
} }
selectEvent(event: Event) { selectEvent(event: OedbEvent) {
this.selectedEvent = event; this.selectedEvent = event;
this.showSidePanel = true; this.showSidePanel = true;
} }
onEventClicked({ event }: { event: CalendarEvent; sourceEvent: MouseEvent | KeyboardEvent }) {
if (event.meta && event.meta.event) {
this.selectEvent(event.meta.event);
}
}
getEventPreset(event: OedbEvent): any {
const what = event.properties.what;
if (what && (this.oedbPresets as any)[what]) {
return (this.oedbPresets as any)[what];
}
return null;
}
getEventIcon(preset: any): string {
if (preset) {
return preset.emoji || '📅';
}
return '📅';
}
getEventColor(preset: any): any {
if (preset) {
// Couleurs basées sur la catégorie
const categoryColors: { [key: string]: any } = {
'Communauté': { primary: '#007bff', secondary: '#cce7ff' },
'Culture': { primary: '#28a745', secondary: '#d4edda' },
'Musique': { primary: '#ffc107', secondary: '#fff3cd' },
'Énergie': { primary: '#dc3545', secondary: '#f8d7da' },
'Commerce': { primary: '#6f42c1', secondary: '#e2d9f3' },
'Temps': { primary: '#17a2b8', secondary: '#d1ecf1' },
'Tourisme': { primary: '#fd7e14', secondary: '#ffeaa7' },
'Circulation': { primary: '#6c757d', secondary: '#e9ecef' },
'Randonnée': { primary: '#20c997', secondary: '#d1f2eb' },
'Vie sauvage': { primary: '#795548', secondary: '#efebe9' },
'Météo': { primary: '#2196f3', secondary: '#e3f2fd' }
};
const category = preset.category || 'Communauté';
return categoryColors[category] || { primary: '#6c757d', secondary: '#e9ecef' };
}
return { primary: '#6c757d', secondary: '#e9ecef' };
}
closeSidePanel() { closeSidePanel() {
this.showSidePanel = false; this.showSidePanel = false;
this.selectedEvent = null; this.selectedEvent = null;
@ -145,22 +187,19 @@ export class Agenda implements OnInit {
this.closeSidePanel(); this.closeSidePanel();
} }
isToday(date: Date): boolean { setView(view: CalendarView) {
const today = new Date(); this.view = view;
return date.toDateString() === today.toDateString();
} }
isPast(date: Date): boolean { dayClicked({ date, events }: { date: Date; events: CalendarEvent[] }): void {
const today = new Date(); console.log('Day clicked:', date, events);
today.setHours(0, 0, 0, 0);
return date < today;
} }
formatDate(date: Date): string { eventTimesChanged({
return date.toLocaleDateString('fr-FR', { event,
weekday: 'long', newStart,
day: 'numeric', newEnd,
month: 'long' }: CalendarEventTimesChangedEvent): void {
}); console.log('Event times changed:', event, newStart, newEnd);
} }
} }

View file

@ -3,7 +3,43 @@
<div class="toolbar"> <div class="toolbar">
<strong>OpenEventDatabase</strong> <strong>OpenEventDatabase</strong>
<span class="muted">{{features.length}} évènements</span> <span class="muted">{{features.length}} évènements</span>
@if (isLoading) {
<span class="loading">⏳ Chargement...</span>
}
</div> </div>
<div class="controls">
<div class="control-group">
<label>Jours à venir</label>
<input
type="number"
class="input"
[(ngModel)]="daysAhead"
(ngModelChange)="onDaysAheadChange()"
min="1"
max="30"
placeholder="7">
</div>
<div class="control-group">
<label>
<input
type="checkbox"
[(ngModel)]="autoReloadEnabled"
(change)="toggleAutoReload()">
Rechargement auto (1min)
</label>
</div>
<div class="control-group">
<button
class="btn btn-sm"
(click)="goToNewCategories()">
📋 Nouvelles catégories
</button>
</div>
</div>
<div class="filters"> <div class="filters">
<label>Filtre rapide</label> <label>Filtre rapide</label>
<input class="input" type="text" placeholder="Rechercher..."> <input class="input" type="text" placeholder="Rechercher...">

View file

@ -29,6 +29,69 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 8px 12px; padding: 8px 12px;
.loading {
color: #007bff;
font-size: 12px;
}
}
.controls {
margin: 15px 0;
padding: 15px;
background: #f8f9fa;
border-radius: 6px;
border: 1px solid #e9ecef;
.control-group {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
label {
display: block;
font-size: 12px;
font-weight: 600;
color: #495057;
margin-bottom: 4px;
input[type="checkbox"] {
margin-right: 6px;
}
}
input[type="number"] {
width: 100%;
padding: 6px 8px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 14px;
&:focus {
outline: none;
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0,123,255,0.25);
}
}
.btn {
width: 100%;
padding: 8px 12px;
background: #6c757d;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: background-color 0.2s;
&:hover {
background: #5a6268;
}
}
}
} }
.map { .map {

View file

@ -1,4 +1,6 @@
import { Component, inject } from '@angular/core'; import { Component, inject, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router';
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';
import { EditForm } from '../../forms/edit-form/edit-form'; import { EditForm } from '../../forms/edit-form/edit-form';
@ -11,24 +13,86 @@ import { UnlocatedEvents } from '../../shared/unlocated-events/unlocated-events'
Menu, Menu,
AllEvents, AllEvents,
UnlocatedEvents, UnlocatedEvents,
EditForm EditForm,
FormsModule
], ],
templateUrl: './home.html', templateUrl: './home.html',
styleUrl: './home.scss' styleUrl: './home.scss'
}) })
export class Home { export class Home implements OnInit, OnDestroy {
OedbApi = inject(OedbApi); OedbApi = inject(OedbApi);
private router = inject(Router);
features: Array<any> = []; features: Array<any> = [];
selected: any | null = null; selected: any | null = null;
showTable = false; showTable = false;
constructor() { // Nouvelles propriétés pour le rechargement automatique et la sélection de jours
this.OedbApi.getEvents({ when: 'now', limit: 500 }).subscribe((events: any) => { autoReloadEnabled = true;
autoReloadInterval: any = null;
daysAhead = 7; // Nombre de jours dans le futur par défaut
isLoading = false;
ngOnInit() {
this.loadEvents();
this.startAutoReload();
}
ngOnDestroy() {
this.stopAutoReload();
}
loadEvents() {
this.isLoading = true;
const today = new Date();
const endDate = new Date(today);
endDate.setDate(today.getDate() + this.daysAhead);
const params = {
start: today.toISOString().split('T')[0],
end: endDate.toISOString().split('T')[0],
limit: 1000
};
this.OedbApi.getEvents(params).subscribe((events: any) => {
this.features = Array.isArray(events?.features) ? events.features : []; this.features = Array.isArray(events?.features) ? events.features : [];
this.isLoading = false;
}); });
} }
startAutoReload() {
if (this.autoReloadEnabled && !this.autoReloadInterval) {
this.autoReloadInterval = setInterval(() => {
this.loadEvents();
}, 60000); // 1 minute
}
}
stopAutoReload() {
if (this.autoReloadInterval) {
clearInterval(this.autoReloadInterval);
this.autoReloadInterval = null;
}
}
toggleAutoReload() {
this.autoReloadEnabled = !this.autoReloadEnabled;
if (this.autoReloadEnabled) {
this.startAutoReload();
} else {
this.stopAutoReload();
}
}
onDaysAheadChange() {
this.loadEvents();
}
goToNewCategories() {
this.router.navigate(['/nouvelles-categories']);
}
onSelect(feature: any) { onSelect(feature: any) {
this.selected = feature; this.selected = feature;
} }
@ -52,24 +116,18 @@ export class Home {
onSaved(_res: any) { onSaved(_res: any) {
// refresh list after update // refresh list after update
this.OedbApi.getEvents({ when: 'now', limit: 500 }).subscribe((events: any) => { this.loadEvents();
this.features = Array.isArray(events?.features) ? events.features : [];
});
} }
onCreated(_res: any) { onCreated(_res: any) {
// refresh and clear selection after create // refresh and clear selection after create
this.selected = null; this.selected = null;
this.OedbApi.getEvents({ when: 'now', limit: 500 }).subscribe((events: any) => { this.loadEvents();
this.features = Array.isArray(events?.features) ? events.features : [];
});
} }
onDeleted(_res: any) { onDeleted(_res: any) {
this.selected = null; this.selected = null;
this.OedbApi.getEvents({ when: 'now', limit: 500 }).subscribe((events: any) => { this.loadEvents();
this.features = Array.isArray(events?.features) ? events.features : [];
});
} }
// Menu callbacks // Menu callbacks

View file

@ -0,0 +1,68 @@
<div class="nouvelles-categories-container">
<div class="header">
<h1>📋 Nouvelles catégories d'événements</h1>
<p>Découvrez les nouveaux types d'événements qui ne sont pas encore dans la configuration OEDB</p>
<div class="controls">
<button
class="btn btn-primary"
(click)="loadNewCategories()"
[disabled]="isLoading">
@if (isLoading) {
⏳ Chargement...
} @else {
🔄 Actualiser
}
</button>
</div>
</div>
@if (isLoading) {
<div class="loading">
<p>Analyse des événements des 30 prochains jours...</p>
</div>
} @else if (eventTypes.length === 0) {
<div class="no-data">
<p>🎉 Aucune nouvelle catégorie trouvée ! Tous les types d'événements sont déjà dans la configuration.</p>
</div>
} @else {
<div class="categories-list">
<h2>{{ eventTypes.length }} nouvelle(s) catégorie(s) trouvée(s)</h2>
@for (eventType of eventTypes; track eventType.what) {
<div class="category-card">
<div class="category-header">
<h3>{{ eventType.what }}</h3>
<span class="count">{{ eventType.count }} événement(s)</span>
</div>
<div class="examples">
<h4>Exemples :</h4>
<ul>
@for (example of eventType.examples; track example.id) {
<li>
<strong>{{ example.properties?.label || example.properties?.name || 'Sans titre' }}</strong>
@if (example.properties?.start || example.properties?.when) {
- {{ example.properties?.start || example.properties?.when }}
}
</li>
}
</ul>
</div>
<div class="code-generation">
<h4>Code à ajouter dans oedb-types.ts :</h4>
<div class="code-block">
<pre><code>{{ generateJsonCode(eventType) }}</code></pre>
<button
class="btn btn-sm copy-btn"
(click)="copyToClipboard(generateJsonCode(eventType))">
📋 Copier
</button>
</div>
</div>
</div>
}
</div>
}
</div>

View file

@ -0,0 +1,168 @@
.nouvelles-categories-container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.header {
margin-bottom: 30px;
text-align: center;
h1 {
color: #333;
margin-bottom: 10px;
}
p {
color: #666;
margin-bottom: 20px;
}
.controls {
margin-top: 20px;
}
}
.loading, .no-data {
text-align: center;
padding: 40px;
color: #666;
p {
font-size: 18px;
}
}
.categories-list {
h2 {
color: #333;
margin-bottom: 20px;
border-bottom: 2px solid #007bff;
padding-bottom: 10px;
}
}
.category-card {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
.category-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
h3 {
color: #007bff;
margin: 0;
font-family: 'Courier New', monospace;
}
.count {
background: #007bff;
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
}
.examples {
margin-bottom: 20px;
h4 {
color: #495057;
margin-bottom: 10px;
font-size: 14px;
}
ul {
margin: 0;
padding-left: 20px;
li {
margin-bottom: 5px;
color: #6c757d;
}
}
}
.code-generation {
h4 {
color: #495057;
margin-bottom: 10px;
font-size: 14px;
}
.code-block {
position: relative;
background: #2d3748;
border-radius: 6px;
padding: 15px;
pre {
margin: 0;
color: #e2e8f0;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
overflow-x: auto;
code {
color: inherit;
}
}
.copy-btn {
position: absolute;
top: 10px;
right: 10px;
background: #4a5568;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background: #2d3748;
}
}
}
}
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
&.btn-primary {
background: #007bff;
color: white;
&:hover:not(:disabled) {
background: #0056b3;
}
&:disabled {
background: #6c757d;
cursor: not-allowed;
}
}
&.btn-sm {
padding: 4px 8px;
font-size: 12px;
}
}

View file

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NouvellesCategories } from './nouvelles-categories';
describe('NouvellesCategories', () => {
let component: NouvellesCategories;
let fixture: ComponentFixture<NouvellesCategories>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [NouvellesCategories]
})
.compileComponents();
fixture = TestBed.createComponent(NouvellesCategories);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View file

@ -0,0 +1,190 @@
import { Component, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { OedbApi } from '../../services/oedb-api';
import oedb from '../../../oedb-types';
interface EventType {
what: string;
count: number;
examples: any[];
}
@Component({
selector: 'app-nouvelles-categories',
standalone: true,
imports: [CommonModule],
templateUrl: './nouvelles-categories.html',
styleUrl: './nouvelles-categories.scss'
})
export class NouvellesCategories implements OnInit {
private oedbApi = inject(OedbApi);
eventTypes: EventType[] = [];
isLoading = false;
oedbPresets = oedb.presets.what;
ngOnInit() {
this.loadNewCategories();
}
loadNewCategories() {
this.isLoading = true;
const today = new Date();
const endDate = new Date(today);
endDate.setDate(today.getDate() + 30); // 30 prochains jours
const params = {
start: today.toISOString().split('T')[0],
end: endDate.toISOString().split('T')[0],
limit: 1000
};
this.oedbApi.getEvents(params).subscribe((response: any) => {
const events = Array.isArray(response?.features) ? response.features : [];
this.analyzeEventTypes(events);
this.isLoading = false;
});
}
private analyzeEventTypes(events: any[]) {
const typeMap = new Map<string, { count: number; examples: any[] }>();
events.forEach(event => {
const what = event.properties?.what;
if (what && !this.isKnownType(what)) {
if (!typeMap.has(what)) {
typeMap.set(what, { count: 0, examples: [] });
}
const typeData = typeMap.get(what)!;
typeData.count++;
if (typeData.examples.length < 3) {
typeData.examples.push(event);
}
}
});
this.eventTypes = Array.from(typeMap.entries())
.map(([what, data]) => ({
what,
count: data.count,
examples: data.examples
}))
.sort((a, b) => b.count - a.count);
}
private isKnownType(what: string): boolean {
return !!(this.oedbPresets as any)[what];
}
generateJsonCode(eventType: EventType): string {
const what = eventType.what;
const example = eventType.examples[0];
const properties = example?.properties || {};
// Générer un emoji basé sur le type
const emoji = this.suggestEmoji(what);
// Générer une catégorie basée sur le premier mot
const category = this.suggestCategory(what);
// Générer des propriétés basées sur l'exemple
const suggestedProperties = this.suggestProperties(properties);
return `'${what}': {
emoji: '${emoji}',
label: '${this.suggestLabel(what)}',
category: '${category}',
description: '${this.suggestDescription(what)}',
durationHours: 24${suggestedProperties ? ',\n properties: {\n' + suggestedProperties + '\n }' : ''}
}`;
}
private suggestEmoji(what: string): string {
const emojiMap: { [key: string]: string } = {
'music': '🎵',
'sport': '⚽',
'food': '🍽️',
'art': '🎨',
'tech': '💻',
'health': '🏥',
'education': '📚',
'business': '💼',
'travel': '✈️',
'nature': '🌿',
'social': '👥',
'festival': '🎪',
'conference': '🎤',
'workshop': '🔧',
'exhibition': '🖼️',
'sale': '🛒',
'meeting': '🤝',
'party': '🎉',
'concert': '🎸',
'theater': '🎭'
};
const firstWord = what.split('.')[0].toLowerCase();
return emojiMap[firstWord] || '📅';
}
private suggestCategory(what: string): string {
const categoryMap: { [key: string]: string } = {
'music': 'Musique',
'sport': 'Sport',
'food': 'Gastronomie',
'art': 'Culture',
'tech': 'Technologie',
'health': 'Santé',
'education': 'Éducation',
'business': 'Commerce',
'travel': 'Tourisme',
'nature': 'Nature',
'social': 'Communauté',
'festival': 'Culture',
'conference': 'Professionnel',
'workshop': 'Formation',
'exhibition': 'Culture',
'sale': 'Commerce',
'meeting': 'Professionnel',
'party': 'Social',
'concert': 'Musique',
'theater': 'Culture'
};
const firstWord = what.split('.')[0].toLowerCase();
return categoryMap[firstWord] || 'Autre';
}
private suggestLabel(what: string): string {
return what.split('.').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
}
private suggestDescription(what: string): string {
return `Événement de type ${this.suggestLabel(what)}`;
}
private suggestProperties(properties: any): string {
const suggestedProps: string[] = [];
// Analyser les propriétés communes pour suggérer des champs
Object.keys(properties).forEach(key => {
if (key !== 'what' && key !== 'label' && key !== 'name' && key !== 'id' && key !== 'uuid') {
const value = properties[key];
if (typeof value === 'string' && value.length > 0) {
suggestedProps.push(` ${key}: { label: '${this.suggestLabel(key)}', writable: true }`);
}
}
});
return suggestedProps.join(',\n');
}
copyToClipboard(text: string) {
navigator.clipboard.writeText(text).then(() => {
// Optionnel: afficher une notification de succès
console.log('Code copié dans le presse-papiers');
});
}
}

View file

@ -186,6 +186,25 @@ const oedb = {
properties: { properties: {
lightning_count: { label: 'Nombre déclairs', writable: true } lightning_count: { label: 'Nombre déclairs', writable: true }
} }
}, 'weather.flood': {
emoji: '🌊',
label: 'Inondation',
category: 'Météo',
description: 'Inondation',
durationHours: 24,
properties: {
flood_level: { label: 'Niveau d\'inondation', writable: true }
}
},
'weather.snow': {
emoji: '❄️',
label: 'Neige',
category: 'Météo',
description: 'Neige',
durationHours: 12,
properties: {
snow_level: { label: 'Niveau de neige', writable: true }
}
}, },
'weather.earthquake': { 'weather.earthquake': {
emoji: '🌎', emoji: '🌎',

View file

@ -110,3 +110,15 @@ label { font-size: 0.85rem; color: $color-muted; }
.toast.is-success { background: $color-success; } .toast.is-success { background: $color-success; }
.toast.is-error { background: $color-error; } .toast.is-error { background: $color-error; }
.toast.is-info { background: $color-info; } .toast.is-info { background: $color-info; }
// marquerus maplibre
.maplibregl-marker{
background-color: #fff;
border-radius: 50%;
width: 16px;
height: 16px;
padding: 10px;
box-shadow: 0 0 10px 5px rgba(0,0,0,0.2);
}