style agenda

This commit is contained in:
Tykayn 2025-10-04 23:36:37 +02:00 committed by tykayn
parent ba6ec93860
commit e7f7e9e19e
11 changed files with 928 additions and 677 deletions

View file

@ -1,134 +1,31 @@
<div class="agenda-container"> <div class="agenda-page">
<div class="agenda-header"> @if (isLoading) {
<h1>Agenda des événements</h1> <div class="loading">
<p>Événements des 20 derniers jours (10 jours avant et 10 jours après aujourd'hui)</p> <div class="loading-spinner"></div>
<p>Chargement des événements...</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>
<button
class="btn btn-sm"
(click)="toggleFiltersPanel()">
{{showFiltersPanel ? 'Masquer' : 'Afficher'}} les filtres
</button>
</div>
</div>
<!-- Panneau de filtres latéral -->
@if (showFiltersPanel) {
<div class="filters-panel">
<h3>Filtres d'événements</h3>
<div class="filter-group">
<label>
<input
type="checkbox"
[(ngModel)]="hideTrafficEvents"
(change)="onHideTrafficChange()">
Masquer les événements de circulation
</label>
</div>
<div class="filter-group">
<h4>Types d'événements</h4>
<div class="event-types-list">
@for (eventType of availableEventTypes; track eventType) {
<label class="event-type-item">
<input
type="checkbox"
[checked]="isEventTypeSelected(eventType)"
(change)="onEventTypeChange(eventType, $event.target.checked)">
{{eventType}}
</label>
}
</div>
</div>
<div class="filter-actions">
<button class="btn btn-sm" (click)="clearAllFilters()">
Effacer tous les filtres
</button>
</div>
</div> </div>
} @else {
<app-calendar
[events]="calendarEvents"
(eventClick)="onEventClick($event)"
(dateClick)="onDateClick($event)">
</app-calendar>
} }
<div class="agenda-content"> @if (selectedEvent) {
<mwl-calendar-month-view <div class="event-edit-panel">
*ngIf="view === CalendarView.Month" <div class="panel-header">
[viewDate]="viewDate" <h3>Modifier l'événement</h3>
[events]="calendarEvents" <button class="btn-close" (click)="selectedEvent = null">×</button>
(eventClicked)="onEventClicked($event)"
(dayClicked)="dayClicked($event)"
[locale]="'fr'"
[eventTitleTemplate]="eventTitleTemplate">
</mwl-calendar-month-view>
<mwl-calendar-week-view
*ngIf="view === CalendarView.Week"
[viewDate]="viewDate"
[events]="calendarEvents"
(eventClicked)="onEventClicked($event)"
(dayClicked)="dayClicked($event)"
[locale]="'fr'"
[eventTitleTemplate]="eventTitleTemplate">
</mwl-calendar-week-view>
<mwl-calendar-day-view
*ngIf="view === CalendarView.Day"
[viewDate]="viewDate"
[events]="calendarEvents"
(eventClicked)="onEventClicked($event)"
(dayClicked)="dayClicked($event)"
[locale]="'fr'"
[eventTitleTemplate]="eventTitleTemplate">
</mwl-calendar-day-view>
</div>
<!-- Panneau latéral pour les détails de l'événement -->
@if (showSidePanel && selectedEvent) {
<div class="side-panel">
<div class="side-panel-header">
<h2>Détails de l'événement</h2>
<button class="close-btn" (click)="closeSidePanel()">×</button>
</div> </div>
<div class="panel-content">
<div class="side-panel-content">
<app-edit-form <app-edit-form
[selected]="selectedEvent" [selected]="selectedEvent"
(saved)="onEventSaved($event)" (saved)="onEventSaved()"
(created)="onEventCreated($event)" (created)="onEventCreated()"
(deleted)="onEventDeleted($event)"> (deleted)="onEventDeleted()">
</app-edit-form> </app-edit-form>
</div> </div>
</div> </div>
} }
</div>
<!-- Overlay pour fermer le panneau latéral -->
@if (showSidePanel) {
<div class="overlay" (click)="closeSidePanel()"></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,294 +1,108 @@
.agenda-container { .agenda-page {
padding: 20px; height: 100vh;
max-width: 1400px;
margin: 0 auto;
position: relative;
}
.agenda-header {
text-align: center;
margin-bottom: 30px;
h1 {
color: #333;
margin-bottom: 10px;
}
p {
color: #666;
font-size: 14px;
margin-bottom: 20px;
}
}
.calendar-controls {
display: flex; display: flex;
justify-content: center; flex-direction: column;
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;
}
}
}
// Panneau de filtres
.filters-panel {
background: #f8f9fa; background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
h3 {
margin: 0 0 20px 0;
color: #333;
font-size: 18px;
}
.filter-group {
margin-bottom: 20px;
h4 {
margin: 0 0 10px 0;
color: #555;
font-size: 14px;
font-weight: 600;
}
label {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
cursor: pointer;
font-size: 14px;
input[type="checkbox"] {
margin: 0;
}
}
}
.event-types-list {
max-height: 200px;
overflow-y: auto;
border: 1px solid #e9ecef;
border-radius: 4px;
padding: 10px;
background: white;
.event-type-item {
display: block;
padding: 4px 0;
border-bottom: 1px solid #f1f5f9;
&:last-child {
border-bottom: none;
}
&:hover {
background: #f8f9fa;
}
}
}
.filter-actions {
border-top: 1px solid #e9ecef;
padding-top: 15px;
text-align: center;
}
} }
.agenda-content { .loading {
margin-bottom: 20px; display: flex;
flex-direction: column;
// Styles pour angular-calendar align-items: center;
::ng-deep { justify-content: center;
.cal-month-view, height: 100vh;
.cal-week-view, background: #f8f9fa;
.cal-day-view {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
.cal-header {
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.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;
transition: all 0.2s ease;
&:hover {
transform: scale(1.02);
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
}
.cal-event-title {
font-weight: 500;
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 {
font-size: 11px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
}
}
.cal-month-view .cal-day-number {
font-size: 14px;
font-weight: 500;
color: #495057;
}
.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 .loading-spinner {
.side-panel { width: 40px;
height: 40px;
border: 4px solid #e9ecef;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading p {
color: #6c757d;
font-size: 1.1rem;
margin: 0;
}
.event-edit-panel {
position: fixed; position: fixed;
top: 0; top: 0;
right: 0; right: 0;
width: 400px; width: 400px;
height: 100vh; height: 100vh;
background: white; background: white;
box-shadow: -2px 0 10px rgba(0,0,0,0.1); box-shadow: -4px 0 12px rgba(0,0,0,0.15);
z-index: 1000; z-index: 1000;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
animation: slideIn 0.3s ease-out;
} }
.side-panel-header { @keyframes slideIn {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
.panel-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 20px; padding: 20px;
border-bottom: 1px solid #eee; border-bottom: 1px solid #e9ecef;
background: #f8f9fa; background: #f8f9fa;
}
h2 {
margin: 0; .panel-header h3 {
font-size: 18px; margin: 0;
color: #333; color: #2c3e50;
} font-size: 1.2rem;
}
.close-btn {
background: none; .btn-close {
border: none; background: none;
font-size: 24px; border: none;
cursor: pointer; font-size: 1.5rem;
color: #666; color: #6c757d;
padding: 0; cursor: pointer;
width: 30px; padding: 5px;
height: 30px; border-radius: 50%;
display: flex; width: 30px;
align-items: center; height: 30px;
justify-content: center; display: flex;
border-radius: 50%; align-items: center;
justify-content: center;
&:hover { transition: all 0.2s ease;
background: #e9ecef;
color: #333; &:hover {
} background: #e9ecef;
color: #495057;
} }
} }
.side-panel-content { .panel-content {
flex: 1; flex: 1;
overflow-y: auto;
padding: 20px; padding: 20px;
} overflow-y: auto;
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.5);
z-index: 999;
} }
// Responsive // Responsive
@media (max-width: 768px) { @media (max-width: 768px) {
.agenda-container { .event-edit-panel {
padding: 10px; width: 100%;
} }
}
.days-grid {
grid-template-columns: 1fr;
}
.side-panel {
width: 100vw;
}
}

View file

@ -1,11 +1,9 @@
import { Component, inject, OnInit, ViewChild, TemplateRef } from '@angular/core'; import { Component, inject, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
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 { CalendarComponent, CalendarEvent } from './calendar/calendar';
import { CalendarEventAction, CalendarEventTimesChangedEvent } from 'angular-calendar';
import oedb from '../../../oedb-types';
interface OedbEvent { interface OedbEvent {
id: string; id: string;
@ -25,268 +23,107 @@ interface OedbEvent {
}; };
} }
interface DayEvents {
date: Date;
events: OedbEvent[];
}
@Component({ @Component({
selector: 'app-agenda', selector: 'app-agenda',
standalone: true, standalone: true,
imports: [CommonModule, FormsModule, EditForm, CalendarModule], imports: [CommonModule, FormsModule, EditForm, CalendarComponent],
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);
@ViewChild('eventTitleTemplate', { static: true }) eventTitleTemplate!: TemplateRef<any>;
events: OedbEvent[] = []; events: OedbEvent[] = [];
filteredEvents: OedbEvent[] = [];
calendarEvents: CalendarEvent[] = []; calendarEvents: CalendarEvent[] = [];
selectedEvent: OedbEvent | null = null; selectedEvent: OedbEvent | null = null;
showSidePanel = false; isLoading = false;
showFiltersPanel = false;
view: CalendarView = CalendarView.Month;
viewDate: Date = new Date();
oedbPresets = oedb.presets.what;
// Propriétés pour les filtres
hideTrafficEvents = true; // Par défaut, masquer les événements de type traffic
selectedEventTypes: string[] = [];
availableEventTypes: string[] = [];
// Exposer CalendarView pour l'utiliser dans le template
CalendarView = CalendarView;
ngOnInit() { ngOnInit() {
this.loadEvents(); this.loadEvents();
} }
loadEvents() { loadEvents() {
this.isLoading = true;
const today = new Date(); const today = new Date();
const startDate = new Date(today); const startDate = new Date(today);
startDate.setDate(today.getDate() - 10); startDate.setMonth(today.getMonth() - 1); // Charger 1 mois avant
const endDate = new Date(today); const endDate = new Date(today);
endDate.setDate(today.getDate() + 10); endDate.setMonth(today.getMonth() + 3); // Charger 3 mois après
const params = { const params = {
start: `${startDate.toISOString().split('T')[0]}`, start: startDate.toISOString().split('T')[0],
end: `${endDate.toISOString().split('T')[0]}`, end: endDate.toISOString().split('T')[0],
limit: 1000 limit: 1000
}; };
this.oedbApi.getEvents(params).subscribe((response: any) => { this.oedbApi.getEvents(params).subscribe((response: any) => {
this.events = Array.isArray(response?.features) ? response.features : []; this.events = Array.isArray(response?.features) ? response.features : [];
this.updateAvailableEventTypes(); this.convertToCalendarEvents();
this.applyFilters(); this.isLoading = false;
}); });
} }
updateAvailableEventTypes() { convertToCalendarEvents() {
const eventTypes = new Set<string>(); this.calendarEvents = this.events.map(event => {
this.events.forEach(event => { const startDate = this.parseEventDate(event.properties.start || event.properties.when);
if (event?.properties?.what) { const endDate = event.properties.stop ? this.parseEventDate(event.properties.stop) : null;
eventTypes.add(event.properties.what);
}
});
this.availableEventTypes = Array.from(eventTypes).sort();
}
applyFilters() {
let filtered = [...this.events];
// Filtre par défaut : masquer les événements de type traffic
if (this.hideTrafficEvents) {
filtered = filtered.filter(event =>
!event?.properties?.what?.startsWith('traffic.')
);
}
// Filtre par types d'événements sélectionnés
if (this.selectedEventTypes.length > 0) {
filtered = filtered.filter(event =>
this.selectedEventTypes.includes(event?.properties?.what || '')
);
}
this.filteredEvents = filtered;
this.organizeEventsByDay();
}
organizeEventsByDay() {
this.calendarEvents = this.filteredEvents.map(event => {
const eventDate = this.getEventDate(event);
const preset = this.getEventPreset(event);
return { return {
id: event.id, id: event.id || Math.random().toString(36).substr(2, 9),
title: this.getEventTitle(event), title: event.properties.label || event.properties.name || 'Événement sans nom',
start: eventDate || new Date(), start: startDate,
color: this.getEventColor(preset), end: endDate || undefined,
meta: { description: event.properties.description || '',
event: event, location: event.properties.where || '',
preset: preset type: event.properties.what || 'default',
} properties: event.properties
}; };
}); });
} }
getEventDate(event: OedbEvent): Date | null { parseEventDate(dateString: string | undefined): Date {
const startDate = event.properties.start || event.properties.when; if (!dateString) return new Date();
if (startDate) {
return new Date(startDate);
}
return null;
}
getEventTitle(event: OedbEvent): string {
return event.properties.label || event.properties.name || 'Événement sans titre';
}
getEventTime(event: OedbEvent): string {
const startDate = event.properties.start || event.properties.when;
if (startDate) {
const date = new Date(startDate);
return date.toLocaleTimeString('fr-FR', {
hour: '2-digit',
minute: '2-digit'
});
}
return '';
}
selectEvent(event: OedbEvent) {
this.selectedEvent = event;
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() {
this.showSidePanel = false;
this.selectedEvent = null;
}
onEventSaved(event: any) {
this.loadEvents(); // Recharger les événements après modification
this.closeSidePanel();
}
onEventCreated(event: any) {
this.loadEvents(); // Recharger les événements après création
this.closeSidePanel();
}
onEventDeleted(event: any) {
this.loadEvents(); // Recharger les événements après suppression
this.closeSidePanel();
}
setView(view: CalendarView) {
this.view = view;
}
dayClicked(event: any): void {
// Gérer les différents types d'événements selon la vue
let date: Date;
let events: CalendarEvent[] = [];
if (event.day) { // Essayer différents formats de date
// Vue mois : { day: MonthViewDay, sourceEvent: MouseEvent | KeyboardEvent } const date = new Date(dateString);
date = event.day.date; if (isNaN(date.getTime())) {
events = event.day.events || []; // Si la date n'est pas valide, essayer de parser manuellement
} else if (event.date) { const parts = dateString.split(/[-T:]/);
// Vue semaine/jour : { date: Date, events: CalendarEvent[], sourceEvent: MouseEvent | KeyboardEvent } if (parts.length >= 3) {
date = event.date; const year = parseInt(parts[0]);
events = event.events || []; const month = parseInt(parts[1]) - 1; // Les mois commencent à 0
} else { const day = parseInt(parts[2]);
// Fallback pour les autres cas const hour = parts[3] ? parseInt(parts[3]) : 0;
console.warn('Type d\'événement dayClicked non reconnu:', event); const minute = parts[4] ? parseInt(parts[4]) : 0;
return; return new Date(year, month, day, hour, minute);
}
console.log('Day clicked:', date, events);
}
eventTimesChanged({
event,
newStart,
newEnd,
}: CalendarEventTimesChangedEvent): void {
console.log('Event times changed:', event, newStart, newEnd);
}
toggleFiltersPanel() {
this.showFiltersPanel = !this.showFiltersPanel;
}
onHideTrafficChange() {
this.applyFilters();
}
onEventTypeChange(eventType: string, checked: boolean) {
if (checked) {
if (!this.selectedEventTypes.includes(eventType)) {
this.selectedEventTypes.push(eventType);
} }
} else {
this.selectedEventTypes = this.selectedEventTypes.filter(type => type !== eventType);
} }
this.applyFilters();
return date;
} }
isEventTypeSelected(eventType: string): boolean { onEventClick(event: CalendarEvent) {
return this.selectedEventTypes.includes(eventType); // Trouver l'événement OEDB correspondant
this.selectedEvent = this.events.find(e =>
(e.id && e.id === event.id) ||
(e.properties.label === event.title)
) || null;
} }
clearAllFilters() { onDateClick(date: Date) {
this.selectedEventTypes = []; // Optionnel : gérer le clic sur une date
this.hideTrafficEvents = true; console.log('Date cliquée:', date);
this.applyFilters(); }
onEventSaved() {
this.loadEvents();
}
onEventCreated() {
this.loadEvents();
}
onEventDeleted() {
this.loadEvents();
} }
} }

View file

@ -0,0 +1,132 @@
<div class="calendar-container">
<!-- En-tête du calendrier -->
<div class="calendar-header">
<div class="calendar-controls">
<button class="btn btn-nav" (click)="previousMonth()"></button>
<h2 class="calendar-title">{{getMonthName()}} {{currentYear}}</h2>
<button class="btn btn-nav" (click)="nextMonth()"></button>
</div>
<button class="btn btn-today" (click)="goToToday()">Aujourd'hui</button>
</div>
<!-- Statistiques -->
<div class="calendar-stats">
<div class="stat-item">
<span class="stat-number">{{getTotalEventsCount()}}</span>
<span class="stat-label">Total événements</span>
</div>
<div class="stat-item">
<span class="stat-number">{{getEventsThisMonth()}}</span>
<span class="stat-label">Ce mois</span>
</div>
</div>
<!-- Grille du calendrier -->
<div class="calendar-grid">
<!-- En-têtes des jours -->
<div class="calendar-weekdays">
@for (day of weekDays; track day) {
<div class="weekday-header">{{day}}</div>
}
</div>
<!-- Jours du calendrier -->
<div class="calendar-days">
@for (day of calendarDays; track day.getTime()) {
<div
class="calendar-day"
[class.today]="isToday(day)"
[class.other-month]="!isCurrentMonth(day)"
[class.weekend]="isWeekend(day)"
[class.selected]="selectedDate?.toDateString() === day.toDateString()"
(click)="onDateClick(day)">
<div class="day-number">{{day.getDate()}}</div>
@if (getEventCountForDate(day) > 0) {
<div class="event-indicator">
<span class="event-count">{{getEventCountForDate(day)}}</span>
</div>
}
<div class="day-events">
@for (event of getEventsForDate(day).slice(0, 3); track event.id) {
<div
class="event-preview"
[class]="'event-type-' + (event.type || 'default')"
(click)="onEventClick(event, $event)"
[title]="event.title">
{{event.title}}
</div>
}
@if (getEventsForDate(day).length > 3) {
<div class="more-events">+{{getEventsForDate(day).length - 3}} autres</div>
}
</div>
</div>
}
</div>
</div>
<!-- Panel de détails de l'événement -->
@if (showEventDetails && selectedEvent) {
<div class="event-details-panel">
<div class="panel-header">
<h3>Détails de l'événement</h3>
<button class="btn-close" (click)="closeEventDetails()">×</button>
</div>
<div class="panel-content">
<div class="event-title">{{selectedEvent.title}}</div>
@if (selectedEvent.description) {
<div class="event-description">
<strong>Description :</strong>
<p>{{selectedEvent.description}}</p>
</div>
}
@if (selectedEvent.location) {
<div class="event-location">
<strong>📍 Lieu :</strong>
<span>{{selectedEvent.location}}</span>
</div>
}
<div class="event-datetime">
<strong>📅 Date :</strong>
<span>{{selectedEvent.start | date:'dd/MM/yyyy à HH:mm'}}</span>
</div>
@if (selectedEvent.end) {
<div class="event-end">
<strong>⏰ Fin :</strong>
<span>{{selectedEvent.end | date:'dd/MM/yyyy à HH:mm'}}</span>
</div>
}
@if (selectedEvent.type) {
<div class="event-type">
<strong>Type :</strong>
<span class="type-badge">{{selectedEvent.type}}</span>
</div>
}
@if (selectedEvent.properties) {
<div class="event-properties">
<strong>Propriétés :</strong>
<div class="properties-list">
@for (prop of getObjectKeys(selectedEvent.properties); track prop) {
<div class="property-item">
<span class="property-key">{{prop}} :</span>
<span class="property-value">{{selectedEvent.properties[prop]}}</span>
</div>
}
</div>
</div>
}
</div>
</div>
}
</div>

View file

@ -0,0 +1,563 @@
:host {
display: block;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f8f9fa;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.calendar-container {
background: white;
border-radius: 12px;
overflow: hidden;
}
/* En-tête du calendrier */
.calendar-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}
.calendar-controls {
display: flex;
align-items: center;
gap: 15px;
}
.btn {
background: rgba(255, 255, 255, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
padding: 8px 12px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 14px;
font-weight: 500;
}
.btn:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateY(-1px);
}
.btn-nav {
font-size: 18px;
padding: 8px 12px;
min-width: 40px;
}
.btn-today {
background: rgba(255, 255, 255, 0.9);
color: #667eea;
font-weight: 600;
}
.btn-today:hover {
background: white;
transform: translateY(-1px);
}
.calendar-title {
margin: 0;
font-size: 24px;
font-weight: 600;
text-align: center;
min-width: 200px;
}
/* Statistiques */
.calendar-stats {
background: #f8f9fa;
padding: 15px 20px;
display: flex;
gap: 30px;
border-bottom: 1px solid #e9ecef;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.stat-number {
font-size: 24px;
font-weight: 700;
color: #667eea;
line-height: 1;
}
.stat-label {
font-size: 12px;
color: #6c757d;
margin-top: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Grille du calendrier */
.calendar-grid {
background: white;
}
.calendar-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
background: #f8f9fa;
border-bottom: 2px solid #e9ecef;
}
.weekday-header {
padding: 15px 8px;
text-align: center;
font-weight: 600;
color: #495057;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.5px;
border-right: 1px solid #e9ecef;
}
.weekday-header:last-child {
border-right: none;
}
.calendar-days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0;
}
.calendar-day {
min-height: 120px;
padding: 8px;
border-right: 1px solid #e9ecef;
border-bottom: 1px solid #e9ecef;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
background: white;
display: flex;
flex-direction: column;
}
.calendar-day:nth-child(7n) {
border-right: none;
}
.calendar-day:hover {
background: #f8f9fa;
transform: scale(1.02);
z-index: 1;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.calendar-day.today {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
border: 2px solid #2196f3;
}
.calendar-day.today .day-number {
background: #2196f3;
color: white;
font-weight: 700;
}
.calendar-day.other-month {
background: #f8f9fa;
color: #adb5bd;
}
.calendar-day.other-month .day-number {
color: #adb5bd;
}
.calendar-day.weekend {
background: #f8f9fa;
}
.calendar-day.selected {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.calendar-day.selected .day-number {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.day-number {
font-size: 16px;
font-weight: 600;
color: #495057;
background: #f8f9fa;
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
transition: all 0.2s ease;
}
/* Indicateurs d'événements */
.event-indicator {
position: absolute;
top: 8px;
right: 8px;
background: #ff4757;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 700;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.day-events {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 4px;
}
.event-preview {
background: #667eea;
color: white;
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
transition: all 0.2s ease;
border-left: 3px solid #5a67d8;
max-width: 150px;
overflow: auto;
text-overflow: ellipsis;
}
.event-preview:hover {
background: #5a67d8;
transform: translateX(2px);
}
.event-type-conference {
background: #4caf50;
border-left-color: #388e3c;
}
.event-type-conference:hover {
background: #388e3c;
}
.event-type-workshop {
background: #ff9800;
border-left-color: #f57c00;
}
.event-type-workshop:hover {
background: #f57c00;
}
.event-type-meeting {
background: #9c27b0;
border-left-color: #7b1fa2;
}
.event-type-meeting:hover {
background: #7b1fa2;
}
.event-type-default {
background: #6c757d;
border-left-color: #495057;
}
.event-type-default:hover {
background: #495057;
}
.more-events {
background: #e9ecef;
color: #6c757d;
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
text-align: center;
margin-top: auto;
}
/* Panel de détails d'événement */
.event-details-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border-radius: 12px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
z-index: 1000;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translate(-50%, -60%);
}
to {
opacity: 1;
transform: translate(-50%, -50%);
}
}
.panel-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 12px 12px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.panel-header h3 {
margin: 0;
font-size: 20px;
font-weight: 600;
}
.btn-close {
background: rgba(255, 255, 255, 0.2);
border: none;
color: white;
font-size: 24px;
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.btn-close:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
.panel-content {
padding: 20px;
}
.event-title {
font-size: 24px;
font-weight: 700;
color: #2c3e50;
margin-bottom: 20px;
line-height: 1.3;
}
.event-description,
.event-location,
.event-datetime,
.event-end,
.event-type {
margin-bottom: 15px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #667eea;
}
.event-description strong,
.event-location strong,
.event-datetime strong,
.event-end strong,
.event-type strong {
display: block;
color: #495057;
font-size: 14px;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.event-description p {
margin: 0;
color: #6c757d;
line-height: 1.5;
}
.event-location span,
.event-datetime span,
.event-end span {
color: #2c3e50;
font-weight: 500;
}
.type-badge {
background: #667eea;
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.event-properties {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.event-properties strong {
display: block;
color: #495057;
font-size: 14px;
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.properties-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.property-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: white;
border-radius: 6px;
border: 1px solid #e9ecef;
}
.property-key {
font-weight: 600;
color: #495057;
font-size: 13px;
}
.property-value {
color: #6c757d;
font-size: 13px;
text-align: right;
max-width: 200px;
word-break: break-word;
}
/* Responsive Design */
@media (max-width: 768px) {
.calendar-header {
flex-direction: column;
text-align: center;
gap: 10px;
}
.calendar-controls {
order: 2;
}
.calendar-title {
order: 1;
font-size: 20px;
min-width: auto;
}
.btn-today {
order: 3;
}
.calendar-stats {
justify-content: center;
gap: 20px;
}
.calendar-day {
min-height: 80px;
padding: 4px;
}
.day-number {
width: 24px;
height: 24px;
font-size: 14px;
}
.event-preview {
font-size: 10px;
padding: 1px 4px;
}
.event-details-panel {
width: 95%;
max-height: 90vh;
}
.panel-content {
padding: 15px;
}
.event-title {
font-size: 20px;
}
}
@media (max-width: 480px) {
.calendar-day {
min-height: 60px;
padding: 2px;
}
.weekday-header {
padding: 10px 4px;
font-size: 12px;
}
.day-number {
width: 20px;
height: 20px;
font-size: 12px;
}
.event-preview {
font-size: 9px;
padding: 1px 2px;
}
.more-events {
font-size: 8px;
}
}

View file

@ -16,8 +16,8 @@ export interface CalendarEvent {
selector: 'app-calendar', selector: 'app-calendar',
standalone: true, standalone: true,
imports: [CommonModule], imports: [CommonModule],
templateUrl: './calendar.html', templateUrl: 'calendar.html',
styleUrl: './calendar.scss' styleUrl: 'calendar.scss'
}) })
export class CalendarComponent implements OnInit, OnDestroy { export class CalendarComponent implements OnInit, OnDestroy {
@Input() events: CalendarEvent[] = []; @Input() events: CalendarEvent[] = [];
@ -157,4 +157,8 @@ export class CalendarComponent implements OnInit, OnDestroy {
eventDate.getFullYear() === this.currentYear; eventDate.getFullYear() === this.currentYear;
}).length; }).length;
} }
getObjectKeys(obj: any): string[] {
return Object.keys(obj || {});
}
} }

View file

@ -1,64 +1,79 @@
<div class="layout"> <div class="layout">
<div class="aside"> <div class="aside">
<div class="toolbar"> <div class="toolbar">
<strong>OpenEventDatabase</strong>
<span class="muted">{{filteredFeatures.length}} évènements</span>
@if (isLoading) { @if (isLoading) {
<span class="loading">⏳ Chargement...</span> <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 (click)="showFilters = !showFilters">
Filtre rapide
@if (showFilters) {
<span></span>
} @else {
<span></span>
}
</label>
<div class="filters-group">
@if (showFilters) {
<span class="muted">{{filteredFeatures.length}} évènements chargés</span>
<hr>
<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>
<input class="input" type="text" placeholder="Rechercher..." [(ngModel)]="searchText" (ngModelChange)="onSearchChange()"> <input class="input" type="text" placeholder="Rechercher..." [(ngModel)]="searchText" (ngModelChange)="onSearchChange()">
<div class="control-group"> <div class="control-group">
<label>Filtrer par type d'événement</label> <label>Filtrer par type d'événement</label>
<select class="input" [(ngModel)]="selectedWhatFilter" (ngModelChange)="onWhatFilterChange()"> <select class="input" [(ngModel)]="selectedWhatFilter" (ngModelChange)="onWhatFilterChange()">
<option value="">Tous les types</option> <option value="">Tous les types</option>
@for (whatType of availableWhatTypes; track whatType) { @for (whatType of availableWhatTypes; track whatType) {
<option [value]="whatType">{{whatType}}</option> <option [value]="whatType">{{whatType}}</option>
} }
</select> </select>
</div> </div>
<app-osm></app-osm>
<app-menu></app-menu>
<hr>
}
</div> </div>
</div>
<!-- <app-unlocated-events [events]="filteredFeatures"></app-unlocated-events> -->
<hr> <hr>
<app-unlocated-events [events]="filteredFeatures"></app-unlocated-events>
<app-menu></app-menu>
<hr>
<app-osm></app-osm>
<app-edit-form [selected]="selected" (saved)="onSaved($event)" (created)="onCreated($event)" (deleted)="onDeleted($event)"></app-edit-form> <app-edit-form [selected]="selected" (saved)="onSaved($event)" (created)="onCreated($event)" (deleted)="onDeleted($event)"></app-edit-form>
</div> </div>
<div class="main"> <div class="main">

View file

@ -32,6 +32,7 @@ export class Home implements OnInit, OnDestroy {
filteredFeatures: Array<any> = []; filteredFeatures: Array<any> = [];
selected: any | null = null; selected: any | null = null;
showTable = false; showTable = false;
showFilters = false;
// Nouvelles propriétés pour le rechargement automatique et la sélection de jours // Nouvelles propriétés pour le rechargement automatique et la sélection de jours
autoReloadEnabled = true; autoReloadEnabled = true;

View file

@ -1,16 +1,9 @@
<menu> <menu>
OpenEventDatabase OpenEventDatabase
<nav>
<!--
<a routerLink="/agenda">agenda</a>
<a routerLink="/unlocated-events">événements non localisés</a>
<a href="/demo/stats">stats</a>
<a href="https://source.cipherbliss.com/tykayn/oedb-backend">sources</a>
</nav>
<div id="editor_form"> <div id="editor_form">
<!-- <div id="search_input"> <div id="search_input">
<input type="text" value="" placeholder="Rechercher une catégorie d'évènement"> <input type="text" value="" placeholder="Rechercher une catégorie d'évènement">
</div> </div>
<div id="what_categories"> <div id="what_categories">
@ -56,10 +49,10 @@
<option value="point"></option> <option value="point"></option>
<option value="polyline"></option> <option value="polyline"></option>
<option value="bbox"></option> <option value="bbox"></option>
</select> --> </select>
</div> </div>
-->
<!-- <div id="found_list"> <!-- <div id="found_list">
<h2>données</h2> <h2>données</h2>
(liste des éléments trouvés) (liste des éléments trouvés)

View file

@ -1,17 +1,6 @@
:host { :host {
display: block; display: block;
nav{
a {
padding: 10px;
border-radius: 10px;
border: 1px solid rgba(0,0,0,0.08);
display: block;
margin-bottom: 10px;
&:hover{
background-color: #f0f0f0;
}
}
}
} }
#what_categories { #what_categories {

View file

@ -110,6 +110,9 @@
font-size: 1.1rem; font-size: 1.1rem;
flex: 1; flex: 1;
line-height: 1.3; line-height: 1.3;
max-width: 200px;
overflow: auto;
text-overflow: ellipsis;
} }
.event-type { .event-type {
@ -120,6 +123,9 @@
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 500; font-weight: 500;
margin-left: 10px; margin-left: 10px;
max-width: 100px;
overflow: auto;
text-overflow: ellipsis;
} }
} }