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-header">
<h1>Agenda des événements</h1>
<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>
<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 class="agenda-page">
@if (isLoading) {
<div class="loading">
<div class="loading-spinner"></div>
<p>Chargement des événements...</p>
</div>
} @else {
<app-calendar
[events]="calendarEvents"
(eventClick)="onEventClick($event)"
(dateClick)="onDateClick($event)">
</app-calendar>
}
<div class="agenda-content">
<mwl-calendar-month-view
*ngIf="view === CalendarView.Month"
[viewDate]="viewDate"
[events]="calendarEvents"
(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>
@if (selectedEvent) {
<div class="event-edit-panel">
<div class="panel-header">
<h3>Modifier l'événement</h3>
<button class="btn-close" (click)="selectedEvent = null">×</button>
</div>
<div class="side-panel-content">
<div class="panel-content">
<app-edit-form
[selected]="selectedEvent"
(saved)="onEventSaved($event)"
(created)="onEventCreated($event)"
(deleted)="onEventDeleted($event)">
[selected]="selectedEvent"
(saved)="onEventSaved()"
(created)="onEventCreated()"
(deleted)="onEventDeleted()">
</app-edit-form>
</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>
</div>

View file

@ -1,294 +1,108 @@
.agenda-container {
padding: 20px;
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 {
.agenda-page {
height: 100vh;
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;
}
}
}
// Panneau de filtres
.filters-panel {
flex-direction: column;
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 {
margin-bottom: 20px;
// Styles pour angular-calendar
::ng-deep {
.cal-month-view,
.cal-week-view,
.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;
}
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
background: #f8f9fa;
}
// Panneau latéral
.side-panel {
.loading-spinner {
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;
top: 0;
right: 0;
width: 400px;
height: 100vh;
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;
display: flex;
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;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #eee;
border-bottom: 1px solid #e9ecef;
background: #f8f9fa;
h2 {
margin: 0;
font-size: 18px;
color: #333;
}
.close-btn {
background: none;
border: none;
font-size: 24px;
cursor: pointer;
color: #666;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
&:hover {
background: #e9ecef;
color: #333;
}
}
.panel-header h3 {
margin: 0;
color: #2c3e50;
font-size: 1.2rem;
}
.btn-close {
background: none;
border: none;
font-size: 1.5rem;
color: #6c757d;
cursor: pointer;
padding: 5px;
border-radius: 50%;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
&:hover {
background: #e9ecef;
color: #495057;
}
}
.side-panel-content {
.panel-content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.5);
z-index: 999;
overflow-y: auto;
}
// Responsive
@media (max-width: 768px) {
.agenda-container {
padding: 10px;
.event-edit-panel {
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 { FormsModule } from '@angular/forms';
import { OedbApi } from '../../services/oedb-api';
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';
import { CalendarComponent, CalendarEvent } from './calendar/calendar';
interface OedbEvent {
id: string;
@ -25,268 +23,107 @@ interface OedbEvent {
};
}
interface DayEvents {
date: Date;
events: OedbEvent[];
}
@Component({
selector: 'app-agenda',
standalone: true,
imports: [CommonModule, FormsModule, EditForm, CalendarModule],
imports: [CommonModule, FormsModule, EditForm, CalendarComponent],
templateUrl: './agenda.html',
styleUrl: './agenda.scss'
})
export class Agenda implements OnInit {
private oedbApi = inject(OedbApi);
@ViewChild('eventTitleTemplate', { static: true }) eventTitleTemplate!: TemplateRef<any>;
events: OedbEvent[] = [];
filteredEvents: OedbEvent[] = [];
calendarEvents: CalendarEvent[] = [];
selectedEvent: OedbEvent | null = null;
showSidePanel = 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;
isLoading = false;
ngOnInit() {
this.loadEvents();
}
loadEvents() {
this.isLoading = true;
const today = new Date();
const startDate = new Date(today);
startDate.setDate(today.getDate() - 10);
startDate.setMonth(today.getMonth() - 1); // Charger 1 mois avant
const endDate = new Date(today);
endDate.setDate(today.getDate() + 10);
endDate.setMonth(today.getMonth() + 3); // Charger 3 mois après
const params = {
start: `${startDate.toISOString().split('T')[0]}`,
end: `${endDate.toISOString().split('T')[0]}`,
start: startDate.toISOString().split('T')[0],
end: endDate.toISOString().split('T')[0],
limit: 1000
};
this.oedbApi.getEvents(params).subscribe((response: any) => {
this.events = Array.isArray(response?.features) ? response.features : [];
this.updateAvailableEventTypes();
this.applyFilters();
this.convertToCalendarEvents();
this.isLoading = false;
});
}
updateAvailableEventTypes() {
const eventTypes = new Set<string>();
this.events.forEach(event => {
if (event?.properties?.what) {
eventTypes.add(event.properties.what);
}
});
this.availableEventTypes = Array.from(eventTypes).sort();
}
convertToCalendarEvents() {
this.calendarEvents = this.events.map(event => {
const startDate = this.parseEventDate(event.properties.start || event.properties.when);
const endDate = event.properties.stop ? this.parseEventDate(event.properties.stop) : null;
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 {
id: event.id,
title: this.getEventTitle(event),
start: eventDate || new Date(),
color: this.getEventColor(preset),
meta: {
event: event,
preset: preset
}
id: event.id || Math.random().toString(36).substr(2, 9),
title: event.properties.label || event.properties.name || 'Événement sans nom',
start: startDate,
end: endDate || undefined,
description: event.properties.description || '',
location: event.properties.where || '',
type: event.properties.what || 'default',
properties: event.properties
};
});
}
getEventDate(event: OedbEvent): Date | null {
const startDate = event.properties.start || event.properties.when;
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[] = [];
parseEventDate(dateString: string | undefined): Date {
if (!dateString) return new Date();
if (event.day) {
// Vue mois : { day: MonthViewDay, sourceEvent: MouseEvent | KeyboardEvent }
date = event.day.date;
events = event.day.events || [];
} else if (event.date) {
// Vue semaine/jour : { date: Date, events: CalendarEvent[], sourceEvent: MouseEvent | KeyboardEvent }
date = event.date;
events = event.events || [];
} else {
// Fallback pour les autres cas
console.warn('Type d\'événement dayClicked non reconnu:', event);
return;
}
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);
// Essayer différents formats de date
const date = new Date(dateString);
if (isNaN(date.getTime())) {
// Si la date n'est pas valide, essayer de parser manuellement
const parts = dateString.split(/[-T:]/);
if (parts.length >= 3) {
const year = parseInt(parts[0]);
const month = parseInt(parts[1]) - 1; // Les mois commencent à 0
const day = parseInt(parts[2]);
const hour = parts[3] ? parseInt(parts[3]) : 0;
const minute = parts[4] ? parseInt(parts[4]) : 0;
return new Date(year, month, day, hour, minute);
}
} else {
this.selectedEventTypes = this.selectedEventTypes.filter(type => type !== eventType);
}
this.applyFilters();
return date;
}
isEventTypeSelected(eventType: string): boolean {
return this.selectedEventTypes.includes(eventType);
onEventClick(event: CalendarEvent) {
// 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() {
this.selectedEventTypes = [];
this.hideTrafficEvents = true;
this.applyFilters();
onDateClick(date: Date) {
// Optionnel : gérer le clic sur une date
console.log('Date cliquée:', date);
}
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',
standalone: true,
imports: [CommonModule],
templateUrl: './calendar.html',
styleUrl: './calendar.scss'
templateUrl: 'calendar.html',
styleUrl: 'calendar.scss'
})
export class CalendarComponent implements OnInit, OnDestroy {
@Input() events: CalendarEvent[] = [];
@ -157,4 +157,8 @@ export class CalendarComponent implements OnInit, OnDestroy {
eventDate.getFullYear() === this.currentYear;
}).length;
}
getObjectKeys(obj: any): string[] {
return Object.keys(obj || {});
}
}

View file

@ -1,64 +1,79 @@
<div class="layout">
<div class="aside">
<div class="toolbar">
<strong>OpenEventDatabase</strong>
<span class="muted">{{filteredFeatures.length}} évènements</span>
@if (isLoading) {
<span class="loading">⏳ Chargement...</span>
}
</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">
<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()">
<div class="control-group">
<label>Filtrer par type d'événement</label>
<select class="input" [(ngModel)]="selectedWhatFilter" (ngModelChange)="onWhatFilterChange()">
<option value="">Tous les types</option>
@for (whatType of availableWhatTypes; track whatType) {
<option [value]="whatType">{{whatType}}</option>
<option value="">Tous les types</option>
@for (whatType of availableWhatTypes; track whatType) {
<option [value]="whatType">{{whatType}}</option>
}
</select>
</div>
<app-osm></app-osm>
<app-menu></app-menu>
<hr>
}
</div>
</div>
<!-- <app-unlocated-events [events]="filteredFeatures"></app-unlocated-events> -->
<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>
</div>
<div class="main">

View file

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

View file

@ -1,16 +1,9 @@
<menu>
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="search_input">
<div id="search_input">
<input type="text" value="" placeholder="Rechercher une catégorie d'évènement">
</div>
<div id="what_categories">
@ -56,10 +49,10 @@
<option value="point"></option>
<option value="polyline"></option>
<option value="bbox"></option>
</select> -->
</select>
</div>
-->
<!-- <div id="found_list">
<h2>données</h2>
(liste des éléments trouvés)

View file

@ -1,17 +1,6 @@
:host {
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 {

View file

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