From 20a8445a5f6a14992459e0b632736261b21e74a0 Mon Sep 17 00:00:00 2001 From: Tykayn Date: Sat, 4 Oct 2025 12:58:44 +0200 Subject: [PATCH] ajout page calendrier --- frontend/angular.json | 2 + frontend/package-lock.json | 118 ++++++++++ frontend/package.json | 7 +- frontend/src/app/app.routes.ts | 5 + frontend/src/app/app.ts | 17 +- frontend/src/app/forms/edit-form/edit-form.ts | 2 +- .../src/app/maps/all-events/all-events.ts | 1 + frontend/src/app/pages/agenda/agenda.html | 59 +++++ frontend/src/app/pages/agenda/agenda.scss | 216 ++++++++++++++++++ frontend/src/app/pages/agenda/agenda.spec.ts | 23 ++ frontend/src/app/pages/agenda/agenda.ts | 166 ++++++++++++++ frontend/src/app/pages/home/home.ts | 1 + frontend/src/app/pages/home/menu/menu.html | 1 + frontend/src/app/pages/home/menu/menu.ts | 4 +- 14 files changed, 617 insertions(+), 5 deletions(-) create mode 100644 frontend/src/app/pages/agenda/agenda.html create mode 100644 frontend/src/app/pages/agenda/agenda.scss create mode 100644 frontend/src/app/pages/agenda/agenda.spec.ts create mode 100644 frontend/src/app/pages/agenda/agenda.ts diff --git a/frontend/angular.json b/frontend/angular.json index 22b8cd0..d9b8121 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -30,6 +30,7 @@ } ], "styles": [ + "node_modules/angular-calendar/css/angular-calendar.css", "src/styles.scss" ] }, @@ -88,6 +89,7 @@ } ], "styles": [ + "node_modules/angular-calendar/css/angular-calendar.css", "src/styles.scss" ] } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ed0345e..2ac82c2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,11 @@ "@angular/forms": "^20.3.0", "@angular/platform-browser": "^20.3.0", "@angular/router": "^20.3.0", + "angular-calendar": "^0.32.0", + "angular-draggable-droppable": "^9.0.1", + "angular-resizable-element": "^8.0.0", + "date-fns": "^4.1.0", + "moment": "^2.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -1952,6 +1957,12 @@ "win32" ] }, + "node_modules/@mattlewis92/dom-autoscroller": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@mattlewis92/dom-autoscroller/-/dom-autoscroller-2.4.2.tgz", + "integrity": "sha512-YbrUWREPGEjE/FU6foXcAT1YbVwqD/jkYnY1dFb0o4AxtP3s4xKBthlELjndZih8uwsDWgQZx1eNskRNe2BgZQ==", + "license": "MIT" + }, "node_modules/@modelcontextprotocol/sdk": { "version": "1.17.3", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.17.3.tgz", @@ -3354,6 +3365,13 @@ "win32" ] }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@schematics/angular": { "version": "20.3.4", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.3.4.tgz", @@ -3657,6 +3675,60 @@ "node": ">= 14.0.0" } }, + "node_modules/angular-calendar": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/angular-calendar/-/angular-calendar-0.32.0.tgz", + "integrity": "sha512-+iQ4j04SCxFv75bp8psx0Q9S7eefREE2IbwbeaSA3uqJcl7Rm1uo7mBwF9Pcg9gu6+U1NDVYlWzDsBpL9b4u3w==", + "dependencies": { + "@scarf/scarf": "^1.1.1", + "calendar-utils": "^0.12.3", + "positioning": "^3.0.0", + "tslib": "^2.4.1" + }, + "funding": { + "url": "https://github.com/sponsors/mattlewis92" + }, + "peerDependencies": { + "@angular/core": ">=20.2.0", + "angular-draggable-droppable": "^9.0.1", + "angular-resizable-element": "^8.0.0", + "date-fns": "^4.0.0", + "moment": "^2.0.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/angular-draggable-droppable": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/angular-draggable-droppable/-/angular-draggable-droppable-9.0.1.tgz", + "integrity": "sha512-nxxFzBMEzB6RsRUqnHWelt9G7QXG2wc18czYL75YhJ9IkBJOBkxXBd0ZOTeDgV9C3mmkkw+PMLJOayx0GH6gXA==", + "license": "MIT", + "dependencies": { + "@mattlewis92/dom-autoscroller": "^2.4.2", + "tslib": "^2.4.1" + }, + "peerDependencies": { + "@angular/core": ">=20.0.0" + } + }, + "node_modules/angular-resizable-element": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/angular-resizable-element/-/angular-resizable-element-8.0.0.tgz", + "integrity": "sha512-cHCfz4y/G8GiKS4WHDeRPv5NPZA4BnsnEgn+z/l7wGhAQv28g7fFRRVyWQu4ZFM5YGu/d7Irv1kUGZ02pCHVdQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/core": ">=20.0.0" + } + }, "node_modules/ansi-escapes": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.1.1.tgz", @@ -4017,6 +4089,28 @@ "node": ">=18" } }, + "node_modules/calendar-utils": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/calendar-utils/-/calendar-utils-0.12.4.tgz", + "integrity": "sha512-OYhqJdeDRRVUdMYJMUrXtR7Go+80oEcA6VK4T3zjcUloQqcOVQKs/kp7j8Yw0HiLOcGwzF50WhLmzZoUNfkM7A==", + "license": "MIT", + "peerDependencies": { + "date-fns": "^4.0.0", + "luxon": "^3.0.0", + "moment": "^2.0.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -4439,6 +4533,15 @@ "dev": true, "license": "MIT" }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/date-format": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", @@ -7009,6 +7112,15 @@ "mkdirp": "bin/cmd.js" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -7795,6 +7907,12 @@ "node": ">=16.20.0" } }, + "node_modules/positioning": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/positioning/-/positioning-3.0.1.tgz", + "integrity": "sha512-cqg00fwFtEu14YwlLUuvFih5ztTd9RYUguJA55lWjeIGFQTpik7ca+TMU87YhAgwWjyjcGIG6l80eUh7X6uHog==", + "license": "MIT" + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", diff --git a/frontend/package.json b/frontend/package.json index 51181aa..eedaf8b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,11 @@ "@angular/forms": "^20.3.0", "@angular/platform-browser": "^20.3.0", "@angular/router": "^20.3.0", + "angular-calendar": "^0.32.0", + "angular-draggable-droppable": "^9.0.1", + "angular-resizable-element": "^8.0.0", + "date-fns": "^4.1.0", + "moment": "^2.0.0", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.15.0" @@ -45,4 +50,4 @@ "karma-jasmine-html-reporter": "~2.1.0", "typescript": "~5.9.2" } -} \ No newline at end of file +} diff --git a/frontend/src/app/app.routes.ts b/frontend/src/app/app.routes.ts index ecf12f9..36a6d49 100644 --- a/frontend/src/app/app.routes.ts +++ b/frontend/src/app/app.routes.ts @@ -1,9 +1,14 @@ import { Routes } from '@angular/router'; import {Home} from './pages/home/home'; +import { Agenda } from './pages/agenda/agenda'; export const routes: Routes = [ { path : '', component: Home + }, + { + path : 'agenda', + component: Agenda } ]; diff --git a/frontend/src/app/app.ts b/frontend/src/app/app.ts index d560a9f..c6ef36f 100644 --- a/frontend/src/app/app.ts +++ b/frontend/src/app/app.ts @@ -1,11 +1,24 @@ import { Component, signal } from '@angular/core'; import { RouterOutlet } from '@angular/router'; +import { CalendarPreviousViewDirective, CalendarTodayDirective, CalendarNextViewDirective, CalendarMonthViewComponent, CalendarWeekViewComponent, CalendarDayViewComponent, CalendarDatePipe, DateAdapter, provideCalendar } from 'angular-calendar'; +import { adapterFactory } from 'angular-calendar/date-adapters/moment'; +import * as moment from 'moment'; + +export function momentAdapterFactory() { + return adapterFactory(moment); +}; @Component({ selector: 'app-root', - imports: [RouterOutlet], + imports: [RouterOutlet, CalendarPreviousViewDirective, CalendarTodayDirective, CalendarNextViewDirective, CalendarMonthViewComponent, CalendarWeekViewComponent, CalendarDayViewComponent, CalendarDatePipe], templateUrl: './app.html', - styleUrl: './app.scss' + styleUrl: './app.scss', + providers: [ + provideCalendar({ + provide: DateAdapter, + useFactory: momentAdapterFactory, + }), + ], }) export class App { protected readonly title = signal('frontend'); diff --git a/frontend/src/app/forms/edit-form/edit-form.ts b/frontend/src/app/forms/edit-form/edit-form.ts index 11a5365..fdf7434 100644 --- a/frontend/src/app/forms/edit-form/edit-form.ts +++ b/frontend/src/app/forms/edit-form/edit-form.ts @@ -1,12 +1,12 @@ import { Component, EventEmitter, Input, Output, OnChanges, SimpleChanges, computed, effect, signal } from '@angular/core'; import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms'; -import { NgFor, NgIf } from '@angular/common'; import oedb from '../../../oedb-types'; import { OedbApi } from '../../services/oedb-api'; import { JsonPipe } from '@angular/common'; @Component({ selector: 'app-edit-form', + standalone: true, imports: [ReactiveFormsModule, JsonPipe], templateUrl: './edit-form.html', styleUrl: './edit-form.scss' diff --git a/frontend/src/app/maps/all-events/all-events.ts b/frontend/src/app/maps/all-events/all-events.ts index c2adb8e..9e1be60 100644 --- a/frontend/src/app/maps/all-events/all-events.ts +++ b/frontend/src/app/maps/all-events/all-events.ts @@ -3,6 +3,7 @@ import oedb_what_categories from '../../../oedb-types'; @Component({ selector: 'app-all-events', + standalone: true, imports: [], templateUrl: './all-events.html', styleUrl: './all-events.scss' diff --git a/frontend/src/app/pages/agenda/agenda.html b/frontend/src/app/pages/agenda/agenda.html new file mode 100644 index 0000000..88626e3 --- /dev/null +++ b/frontend/src/app/pages/agenda/agenda.html @@ -0,0 +1,59 @@ +
+
+

Agenda des événements

+

Événements des 20 derniers jours (10 jours avant et 10 jours après aujourd'hui)

+
+ +
+
+ @for (day of daysWithEvents; track day.date.getTime()) { +
+
+

{{ formatDate(day.date) }}

+ {{ day.events.length }} événement(s) +
+ +
+ @if (day.events.length === 0) { +

Aucun événement

+ } @else { + @for (event of day.events; track event.id) { +
+
{{ getEventTime(event) }}
+
{{ getEventTitle(event) }}
+ @if (event.properties.what) { +
{{ event.properties.what }}
+ } +
+ } + } +
+
+ } +
+
+ + + @if (showSidePanel && selectedEvent) { +
+
+

Détails de l'événement

+ +
+ +
+ + +
+
+ } + + + @if (showSidePanel) { +
+ } +
\ No newline at end of file diff --git a/frontend/src/app/pages/agenda/agenda.scss b/frontend/src/app/pages/agenda/agenda.scss new file mode 100644 index 0000000..acdff08 --- /dev/null +++ b/frontend/src/app/pages/agenda/agenda.scss @@ -0,0 +1,216 @@ +.agenda-container { + padding: 20px; + max-width: 1200px; + 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; + } +} + +.agenda-content { + margin-bottom: 20px; +} + +.days-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 20px; +} + +.day-card { + background: white; + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 15px; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + transition: all 0.3s ease; + + &:hover { + box-shadow: 0 4px 8px rgba(0,0,0,0.15); + } + + &.today { + 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; + border: 1px solid #e9ecef; + border-radius: 6px; + padding: 12px; + margin-bottom: 8px; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background: #e9ecef; + border-color: #007bff; + transform: translateY(-1px); + } + + &:last-child { + margin-bottom: 0; + } +} + +.event-time { + font-size: 12px; + color: #666; + font-weight: 500; + margin-bottom: 4px; +} + +.event-title { + font-weight: 600; + color: #333; + margin-bottom: 4px; + line-height: 1.3; +} + +.event-type { + font-size: 12px; + color: #007bff; + background: #e7f3ff; + padding: 2px 6px; + border-radius: 4px; + display: inline-block; +} + +// Panneau latéral +.side-panel { + position: fixed; + top: 0; + right: 0; + width: 400px; + height: 100vh; + background: white; + box-shadow: -2px 0 10px rgba(0,0,0,0.1); + z-index: 1000; + display: flex; + flex-direction: column; +} + +.side-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px; + border-bottom: 1px solid #eee; + 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; + } + } +} + +.side-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; +} + +// Responsive +@media (max-width: 768px) { + .agenda-container { + padding: 10px; + } + + .days-grid { + grid-template-columns: 1fr; + } + + .side-panel { + width: 100vw; + } +} diff --git a/frontend/src/app/pages/agenda/agenda.spec.ts b/frontend/src/app/pages/agenda/agenda.spec.ts new file mode 100644 index 0000000..d8b0be5 --- /dev/null +++ b/frontend/src/app/pages/agenda/agenda.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Agenda } from './agenda'; + +describe('Agenda', () => { + let component: Agenda; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Agenda] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Agenda); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/pages/agenda/agenda.ts b/frontend/src/app/pages/agenda/agenda.ts new file mode 100644 index 0000000..1ed065d --- /dev/null +++ b/frontend/src/app/pages/agenda/agenda.ts @@ -0,0 +1,166 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { OedbApi } from '../../services/oedb-api'; +import { EditForm } from '../../forms/edit-form/edit-form'; + +interface Event { + id: string; + properties: { + label?: string; + name?: string; + what?: string; + start?: string; + when?: string; + stop?: string; + description?: string; + where?: string; + }; + geometry?: { + type: string; + coordinates: [number, number]; + }; +} + +interface DayEvents { + date: Date; + events: Event[]; +} + +@Component({ + selector: 'app-agenda', + standalone: true, + imports: [CommonModule, EditForm], + templateUrl: './agenda.html', + styleUrl: './agenda.scss' +}) +export class Agenda implements OnInit { + private oedbApi = inject(OedbApi); + + events: Event[] = []; + daysWithEvents: DayEvents[] = []; + selectedEvent: Event | null = null; + showSidePanel = false; + + ngOnInit() { + this.loadEvents(); + } + + loadEvents() { + const today = new Date(); + const startDate = new Date(today); + startDate.setDate(today.getDate() - 10); + + const endDate = new Date(today); + endDate.setDate(today.getDate() + 10); + + const params = { + 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.organizeEventsByDay(); + }); + } + + organizeEventsByDay() { + const daysMap = new Map(); + + // 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); + if (eventDate) { + const dateKey = eventDate.toISOString().split('T')[0]; + const dayEvents = daysMap.get(dateKey); + if (dayEvents) { + dayEvents.events.push(event); + } + } + }); + + this.daysWithEvents = Array.from(daysMap.values()).sort((a, b) => + a.date.getTime() - b.date.getTime() + ); + } + + getEventDate(event: Event): Date | null { + const startDate = event.properties.start || event.properties.when; + if (startDate) { + return new Date(startDate); + } + return null; + } + + getEventTitle(event: Event): string { + return event.properties.label || event.properties.name || 'Événement sans titre'; + } + + getEventTime(event: Event): 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: Event) { + this.selectedEvent = event; + this.showSidePanel = true; + } + + 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(); + } + + isToday(date: Date): boolean { + const today = new Date(); + return date.toDateString() === today.toDateString(); + } + + isPast(date: Date): boolean { + const today = new Date(); + today.setHours(0, 0, 0, 0); + return date < today; + } + + formatDate(date: Date): string { + return date.toLocaleDateString('fr-FR', { + weekday: 'long', + day: 'numeric', + month: 'long' + }); + } +} \ No newline at end of file diff --git a/frontend/src/app/pages/home/home.ts b/frontend/src/app/pages/home/home.ts index 82437d8..33cf0dc 100644 --- a/frontend/src/app/pages/home/home.ts +++ b/frontend/src/app/pages/home/home.ts @@ -6,6 +6,7 @@ import { OedbApi } from '../../services/oedb-api'; import { UnlocatedEvents } from '../../shared/unlocated-events/unlocated-events'; @Component({ selector: 'app-home', + standalone: true, imports: [ Menu, AllEvents, diff --git a/frontend/src/app/pages/home/menu/menu.html b/frontend/src/app/pages/home/menu/menu.html index 17aca62..26b06d3 100644 --- a/frontend/src/app/pages/home/menu/menu.html +++ b/frontend/src/app/pages/home/menu/menu.html @@ -1,5 +1,6 @@ OpenEventDatabase + agenda stats sources diff --git a/frontend/src/app/pages/home/menu/menu.ts b/frontend/src/app/pages/home/menu/menu.ts index 042ed40..b7b7f7d 100644 --- a/frontend/src/app/pages/home/menu/menu.ts +++ b/frontend/src/app/pages/home/menu/menu.ts @@ -1,9 +1,11 @@ import { Component } from '@angular/core'; import oedb_what_categories from '../../../../oedb-types'; +import { RouterLink } from "@angular/router"; @Component({ selector: 'app-menu', - imports: [], + standalone: true, + imports: [RouterLink], templateUrl: './menu.html', styleUrl: './menu.scss' })