guide de contrib

This commit is contained in:
Tykayn 2025-10-05 00:21:11 +02:00 committed by tykayn
parent e7f7e9e19e
commit 464e0e5499
12 changed files with 346 additions and 37 deletions

View file

@ -33,8 +33,15 @@ form {
border: 1px solid rgba(0,0,0,0.08);
background: #fff;
cursor: pointer;
&:hover{
background-color: #7fa8d6;
color: #fff;
}
}
.actions {
display: flex;
gap: 8px;

View file

@ -16,6 +16,7 @@ export class EditForm implements OnChanges {
@Output() saved = new EventEmitter<any>();
@Output() created = new EventEmitter<any>();
@Output() deleted = new EventEmitter<any>();
@Output() canceled = new EventEmitter<void>();
form: FormGroup;
allPresets: Array<{ key: string, label: string, emoji: string, category: string, description?: string, durationHours?: number, properties?: Record<string, { label?: string, writable?: boolean, values?: any[], default?: any, allow_custom?: boolean, allow_empty?: boolean }> }>;
@ -287,6 +288,7 @@ export class EditForm implements OnChanges {
});
this.presetValues.set({});
this.status.set({ state: 'idle' });
this.canceled.emit();
}
private toLocalInputValue(d: string | Date): string {

View file

@ -145,8 +145,8 @@ export class AllEvents implements OnInit, OnDestroy {
private showPickedMarker(coords: [number, number]) {
const maplibregl = (window as any).maplibregl;
const el = document.createElement('div');
el.style.width = '20px';
el.style.height = '20px';
el.style.width = '10px';
el.style.height = '10px';
el.style.borderRadius = '50%';
el.style.background = '#2196f3';
el.style.border = '2px solid white';

View file

@ -5,27 +5,64 @@
<p>Chargement des événements...</p>
</div>
} @else {
<app-calendar
[events]="calendarEvents"
(eventClick)="onEventClick($event)"
(dateClick)="onDateClick($event)">
</app-calendar>
}
<div class="agenda-layout">
<aside class="agenda-sidebar">
<div class="sidebar-header">
<h3>Agenda</h3>
<small>{{calendarEvents.length}} évènements</small>
</div>
<div class="day-groups">
@for (group of groupedEvents; track group.dateKey) {
<div class="day-group">
<div class="day-title">{{formatDayHeader(group.date)}}</div>
<ul class="event-list">
@for (ev of group.items; track ev.id) {
<li class="event-item" (click)="selectFromSidebar(ev)" [class.active]="selectedEvent?.id === ev.id">
<span class="event-icon">
@if (getImageForWhat(ev.properties.what)) {
<img [src]="getImageForWhat(ev.properties.what)" alt="" />
} @else if (getEmojiForWhat(ev.properties.what)) {
{{getEmojiForWhat(ev.properties.what)}}
} @else {
📌
}
</span>
<div class="event-meta">
<div class="event-title">{{ev.properties.label || ev.properties.name || 'Événement'}}</div>
<div class="event-when">{{(ev.properties.start || ev.properties.when) || '—'}}</div>
</div>
</li>
}
</ul>
</div>
}
</div>
</aside>
@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="panel-content">
<app-edit-form
[selected]="selectedEvent"
(saved)="onEventSaved()"
(created)="onEventCreated()"
(deleted)="onEventDeleted()">
</app-edit-form>
</div>
<main class="agenda-main">
<app-calendar
[events]="calendarEvents"
(eventClick)="onEventClick($event)"
(dateClick)="onDateClick($event)">
</app-calendar>
</main>
@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="panel-content">
<app-edit-form
[selected]="selectedEvent"
(saved)="onEventSaved()"
(created)="onEventCreated()"
(deleted)="onEventDeleted()">
</app-edit-form>
</div>
</div>
}
</div>
}
</div>

View file

@ -35,6 +35,98 @@
margin: 0;
}
.agenda-layout {
display: grid;
grid-template-columns: 320px 1fr auto;
grid-template-rows: 1fr;
height: 100vh;
}
.agenda-sidebar {
background: #ffffff;
border-right: 1px solid #e9ecef;
overflow-y: auto;
padding: 12px;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 6px 8px 12px 8px;
border-bottom: 1px solid #f1f3f5;
}
.day-group {
padding: 10px 0;
}
.day-title {
font-weight: 600;
color: #2c3e50;
font-size: 0.95rem;
margin: 8px 0;
}
.event-list {
list-style: none;
margin: 0;
padding: 0;
}
.event-item {
display: flex;
gap: 10px;
padding: 8px;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s ease;
}
.event-item:hover {
background: #f5f7fb;
}
.event-item.active {
background: #eef3ff;
border-left: 3px solid #75a0f6;
}
.event-icon {
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.event-icon img {
width: 20px;
height: 20px;
}
.event-meta {
min-width: 0;
}
.event-title {
font-size: 0.95rem;
color: #243b53;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.event-when {
font-size: 0.8rem;
color: #6b7280;
}
.agenda-main {
position: relative;
}
.event-edit-panel {
position: fixed;
top: 0;

View file

@ -4,6 +4,7 @@ import { FormsModule } from '@angular/forms';
import { OedbApi } from '../../services/oedb-api';
import { EditForm } from '../../forms/edit-form/edit-form';
import { CalendarComponent, CalendarEvent } from './calendar/calendar';
import oedb from '../../../oedb-types';
interface OedbEvent {
id: string;
@ -38,6 +39,8 @@ export class Agenda implements OnInit {
selectedEvent: OedbEvent | null = null;
isLoading = false;
groupedEvents: Array<{ dateKey: string; date: Date; items: OedbEvent[] }> = [];
ngOnInit() {
this.loadEvents();
}
@ -59,6 +62,7 @@ export class Agenda implements OnInit {
this.oedbApi.getEvents(params).subscribe((response: any) => {
this.events = Array.isArray(response?.features) ? response.features : [];
this.convertToCalendarEvents();
this.buildGroupedEvents();
this.isLoading = false;
});
}
@ -126,4 +130,57 @@ export class Agenda implements OnInit {
onEventDeleted() {
this.loadEvents();
}
// Sidebar helpers
private getEventStartDate(ev: OedbEvent): Date {
const ds = ev.properties.start || ev.properties.when;
return this.parseEventDate(ds);
}
private toDateKey(d: Date): string {
const y = d.getFullYear();
const m = (d.getMonth() + 1).toString().padStart(2, '0');
const da = d.getDate().toString().padStart(2, '0');
return `${y}-${m}-${da}`;
}
buildGroupedEvents() {
const groups: Record<string, { date: Date; items: OedbEvent[] }> = {};
for (const ev of this.events) {
const d = this.getEventStartDate(ev);
const key = this.toDateKey(d);
if (!groups[key]) groups[key] = { date: new Date(d.getFullYear(), d.getMonth(), d.getDate()), items: [] };
groups[key].items.push(ev);
}
const result = Object.keys(groups)
.sort((a, b) => groups[a].date.getTime() - groups[b].date.getTime())
.map(k => ({ dateKey: k, date: groups[k].date, items: groups[k].items.sort((a, b) => this.getEventStartDate(a).getTime() - this.getEventStartDate(b).getTime()) }));
this.groupedEvents = result;
}
formatDayHeader(d: Date): string {
const days = ['Dimanche','Lundi','Mardi','Mercredi','Jeudi','Vendredi','Samedi'];
const months = ['janvier','février','mars','avril','mai','juin','juillet','août','septembre','octobre','novembre','décembre'];
return `${days[d.getDay()]} ${d.getDate()} ${months[d.getMonth()]} ${d.getFullYear()}`;
}
getEmojiForWhat(what?: string): string | null {
if (!what) return null;
const spec: any = (oedb.presets.what as any)[what];
return spec?.emoji || null;
}
getImageForWhat(what?: string): string | null {
if (!what) return null;
const spec: any = (oedb.presets.what as any)[what];
if (spec?.image) {
const img: string = spec.image;
return img.startsWith('/') ? img : `/${img}`;
}
return null;
}
selectFromSidebar(ev: OedbEvent) {
this.selectedEvent = ev;
}
}

View file

@ -1,9 +1,9 @@
<div class="layout">
<div class="aside">
<div class="toolbar">
<!-- <span class="loading-indicator">⏳</span> -->
@if (isLoading) {
<span class="loading"> Chargement...</span>
<span class="loading-indicator"></span>
}
</div>
@ -73,9 +73,37 @@
<!-- <app-unlocated-events [events]="filteredFeatures"></app-unlocated-events> -->
<hr>
<app-edit-form [selected]="selected" (saved)="onSaved($event)" (created)="onCreated($event)" (deleted)="onDeleted($event)"></app-edit-form>
@if(showEditForm){
<div class="guide">
<h3>Guide</h3>
<ul>
<li> Créer un évènement: Cliquez sur le bouton "+" pour créer un nouvel évènement. Sélectionnez un preset, remplissez les informations, cliquez quelque part sur la carte pour définir un emplacement. Puis appuyez sur créer.</li>
<li> Mettre à jour un évènement: Sélectionnez un évènement sur la carte ou dans la liste pour le modifier.</li>
</ul>
</div>
<app-edit-form [selected]="selected" (saved)="onSaved($event)" (created)="onCreated($event)" (deleted)="onDeleted($event)" (canceled)="onCanceled()"></app-edit-form>
}
@if(selected !== null){
<div class="selected">
<h3> sélectionné:</h3>
{{selected.properties.label}}
{{selected.properties.name}}
</div>
}
<div id="fixed_actions">
<button class="button btn btn-primary" (click)="createEvent()" title="Créer un évènement">+</button>
<button class="button" (click)="toggleView()" title="Basculer carte / tableau">📊</button>
<div class="downloaders">
<button class="button" (click)="downloadGeoJSON()" title="Télécharger GeoJSON">📥 GeoJSON</button>
<button class="button" (click)="downloadCSV()" title="Télécharger CSV">📥 CSV</button>
</div>
</div>
</div>
<div class="main">
@if (!showTable) {
<div class="map">

View file

@ -1,5 +1,21 @@
:host {
display: block;
button{
background: white;
border: 1px solid rgba(0,0,0,0.06);
border-radius: 10px;
padding: 10px;
cursor: pointer;
&:hover{
background-color: #f0f0f0;
}
+ button{
margin-left: 10px;
margin-bottom: 10px;
}
}
}
.layout {
@ -99,3 +115,30 @@
flex: 1 1 auto;
min-height: 0;
}
.presets{
position: fixed;
top: 63px;
margin-left: 397px;
width: 50vw;
max-height: 80vh;
display: block;
}
app-edit-form{
position: fixed;
top: 135px;
margin-left: 397px;
width: 40vw;
max-height: 77.7vh;
display: block;
overflow: auto;
background: rgba(228, 235, 255, 0.5);
border: 1px solid rgba(0,0,0,0.06);
border-radius: 10px;
padding: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
z-index: 1000;
padding-bottom: 150px;
}

View file

@ -33,6 +33,7 @@ export class Home implements OnInit, OnDestroy {
selected: any | null = null;
showTable = false;
showFilters = false;
showEditForm = true;
// Nouvelles propriétés pour le rechargement automatique et la sélection de jours
autoReloadEnabled = true;
@ -54,6 +55,14 @@ export class Home implements OnInit, OnDestroy {
this.stopAutoReload();
}
createEvent() {
this.selected = null;
//this.showTable = false;
//this.showFilters = true;
this.showEditForm = true;
}
loadEvents() {
this.isLoading = true;
const today = new Date();
@ -194,6 +203,10 @@ export class Home implements OnInit, OnDestroy {
this.loadEvents();
}
onCanceled() {
this.showEditForm = false;
}
// Menu callbacks
ngAfterViewInit() {
// Wire menu callbacks if needed via querySelector; left simple for now

View file

@ -65,7 +65,8 @@
class="input"
[(ngModel)]="searchQuery"
placeholder="Rechercher un lieu (ex: Paris, France)"
[disabled]="isSearchingLocation">
[disabled]="isSearchingLocation"
(keyup.enter)="searchLocation()">
<button
class="btn btn-primary search-btn"
(click)="searchLocation()"

View file

@ -126,13 +126,9 @@ export class UnlocatedEventsPage implements OnInit {
// Utiliser la propriété 'where' de l'événement si disponible, sinon utiliser la recherche manuelle
const searchTerm = this.selectedEvent?.properties?.where || this.searchQuery;
const url = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(searchTerm)}&limit=10&addressdetails=1&countrycodes=fr&extratags=1`;
fetch(url, {
headers: {
'User-Agent': 'OpenEventDatabase/1.0'
}
})
const url = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(searchTerm)}&limit=10&addressdetails=1&countrycodes=fr&extratags=1&accept-language=fr`;
fetch(url)
.then(response => {
if (!response.ok) {
throw new Error(`Erreur HTTP: ${response.status}`);

View file

@ -52,6 +52,8 @@ input{
app-root, app-home {
display: block;
min-height: 100vh;
height: 100vh;
overflow: hidden;
}
/* Generic UI elements */
@ -132,11 +134,11 @@ label { font-size: 0.85rem; color: $color-muted; }
}
.actions{
position: fixed;
position: fixed;
bottom: 10px;
left: 10px;
left: 415px;
right: 10px;
width: 340px;
width: 40vw;
display: flex;
flex-direction: row;
justify-content: end;
@ -190,9 +192,40 @@ nav{
display: block;
max-width: 93%;
}
textarea{
display: block;
max-width: 93%;
}
select{
max-width: 97%;
}
}
.presets{
max-height: 300px;
overflow: auto;
}
.loading-indicator{
color: #007bff;
font-size: 12px;
position: fixed;
top: 10px;
left: 10px;
z-index: 1000;
background: #fff;
padding: 10px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.btn-primary{
background: linear-gradient( 135deg, #9fd3f6, #b9e4c9) !important;
color: #22303a;
&:hover{
background: linear-gradient( 135deg, #7fa8d6, #95c6a7);
}
&:active{
background: linear-gradient( 135deg, #5982b1, #6992c1);
}
}