guide de contrib
This commit is contained in:
parent
e7f7e9e19e
commit
464e0e5499
12 changed files with 346 additions and 37 deletions
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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()"
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue