ajout page calendrier

This commit is contained in:
Tykayn 2025-10-04 12:58:44 +02:00 committed by tykayn
parent 8aa4e107ac
commit 20a8445a5f
14 changed files with 617 additions and 5 deletions

View file

@ -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"
]
}

View file

@ -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",

View file

@ -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"
}
}
}

View file

@ -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
}
];

View file

@ -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');

View file

@ -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'

View file

@ -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'

View file

@ -0,0 +1,59 @@
<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>
<div class="agenda-content">
<div class="days-grid">
@for (day of daysWithEvents; track day.date.getTime()) {
<div class="day-card" [class.today]="isToday(day.date)" [class.past]="isPast(day.date)">
<div class="day-header">
<h3>{{ formatDate(day.date) }}</h3>
<span class="event-count">{{ day.events.length }} événement(s)</span>
</div>
<div class="events-list">
@if (day.events.length === 0) {
<p class="no-events">Aucun événement</p>
} @else {
@for (event of day.events; track event.id) {
<div class="event-item" (click)="selectEvent(event)">
<div class="event-time">{{ getEventTime(event) }}</div>
<div class="event-title">{{ getEventTitle(event) }}</div>
@if (event.properties.what) {
<div class="event-type">{{ event.properties.what }}</div>
}
</div>
}
}
</div>
</div>
}
</div>
</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 class="side-panel-content">
<app-edit-form
[selected]="selectedEvent"
(saved)="onEventSaved($event)"
(created)="onEventCreated($event)"
(deleted)="onEventDeleted($event)">
</app-edit-form>
</div>
</div>
}
<!-- Overlay pour fermer le panneau latéral -->
@if (showSidePanel) {
<div class="overlay" (click)="closeSidePanel()"></div>
}
</div>

View file

@ -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;
}
}

View file

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

View file

@ -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<string, DayEvents>();
// 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'
});
}
}

View file

@ -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,

View file

@ -1,5 +1,6 @@
<menu>
OpenEventDatabase
<a routerLink="/agenda">agenda</a>
<a href="/demo/stats">stats</a>
<a href="https://source.cipherbliss.com/tykayn/oedb-backend">sources</a>

View file

@ -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'
})