routes doc, future, express mode thematique
This commit is contained in:
parent
eeb219cffa
commit
23c598034c
11 changed files with 228 additions and 18 deletions
|
@ -3,12 +3,22 @@ import {Home} from './pages/home/home';
|
||||||
import { Agenda } from './pages/agenda/agenda';
|
import { Agenda } from './pages/agenda/agenda';
|
||||||
import { NouvellesCategories } from './pages/nouvelles-categories/nouvelles-categories';
|
import { NouvellesCategories } from './pages/nouvelles-categories/nouvelles-categories';
|
||||||
import { UnlocatedEventsPage } from './pages/unlocated-events/unlocated-events';
|
import { UnlocatedEventsPage } from './pages/unlocated-events/unlocated-events';
|
||||||
|
import { CommunityUpcoming } from './pages/community-upcoming/community-upcoming';
|
||||||
|
import { EventsDocs } from './pages/events-docs/events-docs';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
path : '',
|
path : '',
|
||||||
component: Home
|
component: Home
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'community-upcoming',
|
||||||
|
component: CommunityUpcoming
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'events-docs',
|
||||||
|
component: EventsDocs
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path : 'agenda',
|
path : 'agenda',
|
||||||
component: Agenda
|
component: Agenda
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
<section class="toolbar">
|
||||||
|
<label>Jours à venir:
|
||||||
|
<input type="number" min="1" [ngModel]="days()" (ngModelChange)="days.set($event); load()" />
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<app-all-events [features]="features"></app-all-events>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { Component, inject, signal } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { AllEvents } from '../../maps/all-events/all-events';
|
||||||
|
import { OedbApi } from '../../services/oedb-api';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-community-upcoming',
|
||||||
|
imports: [CommonModule, FormsModule, AllEvents],
|
||||||
|
templateUrl: './community-upcoming.html',
|
||||||
|
styleUrl: './community-upcoming.scss'
|
||||||
|
})
|
||||||
|
export class CommunityUpcoming {
|
||||||
|
private api = inject(OedbApi);
|
||||||
|
|
||||||
|
days = signal<number>(7);
|
||||||
|
features: Array<any> = [];
|
||||||
|
selected: any | null = null;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
load() {
|
||||||
|
// when: NEXT7DAYS etc. Utilise le param 'when' déjà supporté par l'API
|
||||||
|
const d = Math.max(1, Number(this.days()) || 7);
|
||||||
|
const when = `NEXT${d}DAYS`;
|
||||||
|
this.api.getEvents({ when, what: 'commu', limit: 1000 }).subscribe((events: any) => {
|
||||||
|
this.features = Array.isArray(events?.features) ? events.features : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
15
frontend/src/app/pages/events-docs/events-docs.html
Normal file
15
frontend/src/app/pages/events-docs/events-docs.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<section class="docs">
|
||||||
|
<aside class="sidebar">
|
||||||
|
<h2>Types d'événements (1000 prochains)</h2>
|
||||||
|
<ul>
|
||||||
|
<li *ngFor="let c of counts">
|
||||||
|
<button (click)="filterByWhat(c.what)">{{ c.what }} <span class="badge">{{ c.count }}</span></button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
<section class="map-panel">
|
||||||
|
<app-all-events [features]="filtered"></app-all-events>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
21
frontend/src/app/pages/events-docs/events-docs.scss
Normal file
21
frontend/src/app/pages/events-docs/events-docs.scss
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
.docs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 280px 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.sidebar {
|
||||||
|
max-height: calc(100vh - 160px);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
background: #1976d2;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0 8px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
.map-panel {
|
||||||
|
min-height: 60vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
44
frontend/src/app/pages/events-docs/events-docs.ts
Normal file
44
frontend/src/app/pages/events-docs/events-docs.ts
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
import { Component, inject } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { OedbApi } from '../../services/oedb-api';
|
||||||
|
import { AllEvents } from '../../maps/all-events/all-events';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-events-docs',
|
||||||
|
imports: [CommonModule, AllEvents],
|
||||||
|
templateUrl: './events-docs.html',
|
||||||
|
styleUrl: './events-docs.scss'
|
||||||
|
})
|
||||||
|
export class EventsDocs {
|
||||||
|
private api = inject(OedbApi);
|
||||||
|
|
||||||
|
features: Array<any> = [];
|
||||||
|
counts: Array<{ what: string, count: number }> = [];
|
||||||
|
filtered: Array<any> = [];
|
||||||
|
selected: any | null = null;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
// Charger 1000 events récents
|
||||||
|
this.api.getEvents({ when: 'NEXT30DAYS', limit: 1000 }).subscribe((events: any) => {
|
||||||
|
this.features = Array.isArray(events?.features) ? events.features : [];
|
||||||
|
this.buildCounts();
|
||||||
|
this.filtered = this.features;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
buildCounts() {
|
||||||
|
const map = new Map<string, number>();
|
||||||
|
for (const f of this.features) {
|
||||||
|
const w = (f?.properties?.what || '').trim();
|
||||||
|
if (!w) continue;
|
||||||
|
map.set(w, (map.get(w) || 0) + 1);
|
||||||
|
}
|
||||||
|
this.counts = Array.from(map.entries()).sort((a,b) => b[1]-a[1]).map(([what, count]) => ({ what, count }));
|
||||||
|
}
|
||||||
|
|
||||||
|
filterByWhat(what: string) {
|
||||||
|
this.filtered = this.features.filter(f => String(f?.properties?.what || '').startsWith(what));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -135,6 +135,19 @@ lastupdate:
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="main">
|
<div class="main">
|
||||||
|
@if (theme()) {
|
||||||
|
<div class="subtheme-bar">
|
||||||
|
<div class="help">Thème: {{ theme() }} — Cliquez sur la carte pour définir des coordonnées puis créez un évènement du sous-thème choisi.</div>
|
||||||
|
<div class="chips">
|
||||||
|
@for (t of subthemes; track t.key) {
|
||||||
|
<button class="chip" [class.active]="activeSubtheme()===t.key" (click)="activeSubtheme.set(t.key)">
|
||||||
|
<span class="emoji">{{t.emoji}}</span>
|
||||||
|
<span>{{t.label}}</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@if (!showTable) {
|
@if (!showTable) {
|
||||||
<div class="map">
|
<div class="map">
|
||||||
<app-all-events [features]="filteredFeatures" [selected]="selected" (select)="onSelect($event)" (pickCoords)="onPickCoords($event)"></app-all-events>
|
<app-all-events [features]="filteredFeatures" [selected]="selected" (select)="onSelect($event)" (pickCoords)="onPickCoords($event)"></app-all-events>
|
||||||
|
|
|
@ -142,3 +142,15 @@ app-edit-form{
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
padding-bottom: 150px;
|
padding-bottom: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.subtheme-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
.help { font-size: 12px; color: #64748b; }
|
||||||
|
.chips { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||||
|
.chip { border: 1px solid #e2e8f0; border-radius: 999px; padding: 6px 10px; background: #fff; cursor: pointer; }
|
||||||
|
.chip.active { background: #e3f2fd; border-color: #90caf9; }
|
||||||
|
.emoji { margin-right: 6px; }
|
||||||
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Component, inject, signal } from '@angular/core';
|
||||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
|
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
|
@ -5,6 +6,9 @@ import {Menu} from './menu/menu';
|
||||||
import { AllEvents } from '../../maps/all-events/all-events';
|
import { AllEvents } from '../../maps/all-events/all-events';
|
||||||
import { EditForm } from '../../forms/edit-form/edit-form';
|
import { EditForm } from '../../forms/edit-form/edit-form';
|
||||||
import { OedbApi } from '../../services/oedb-api';
|
import { OedbApi } from '../../services/oedb-api';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import oedb from '../../../oedb-types';
|
||||||
|
|
||||||
import { UnlocatedEvents } from '../../shared/unlocated-events/unlocated-events';
|
import { UnlocatedEvents } from '../../shared/unlocated-events/unlocated-events';
|
||||||
import { OsmAuth } from '../../services/osm-auth';
|
import { OsmAuth } from '../../services/osm-auth';
|
||||||
import { Osm } from '../../forms/osm/osm';
|
import { Osm } from '../../forms/osm/osm';
|
||||||
|
@ -27,26 +31,30 @@ import { WhatFilterComponent } from '../../shared/what-filter/what-filter';
|
||||||
export class Home implements OnInit, OnDestroy {
|
export class Home implements OnInit, OnDestroy {
|
||||||
|
|
||||||
OedbApi = inject(OedbApi);
|
OedbApi = inject(OedbApi);
|
||||||
|
route = inject(ActivatedRoute);
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
private osmAuth = inject(OsmAuth);
|
private osmAuth = inject(OsmAuth);
|
||||||
|
|
||||||
features: Array<any> = [];
|
features: Array<any> = [];
|
||||||
filteredFeatures: Array<any> = [];
|
filteredFeatures: Array<any> = [];
|
||||||
selected: any | null = null;
|
selected: any | null = null;
|
||||||
showTable = false;
|
showTable = false;
|
||||||
showFilters = false;
|
showFilters = false;
|
||||||
showEditForm = true;
|
showEditForm = true;
|
||||||
|
|
||||||
// Nouvelles propriétés pour le rechargement automatique et la sélection de jours
|
// Nouvelles propriétés pour le rechargement automatique et la sélection de jours
|
||||||
autoReloadEnabled = true;
|
autoReloadEnabled = true;
|
||||||
autoReloadInterval: any = null;
|
autoReloadInterval: any = null;
|
||||||
daysAhead = 7; // Nombre de jours dans le futur par défaut
|
daysAhead = 7; // Nombre de jours dans le futur par défaut
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
|
|
||||||
// Propriétés pour les filtres
|
// Propriétés pour les filtres
|
||||||
searchText = '';
|
searchText = '';
|
||||||
selectedWhatFilter = '';
|
selectedWhatFilter = '';
|
||||||
availableWhatTypes: string[] = [];
|
availableWhatTypes: string[] = [];
|
||||||
|
theme = signal<string | null>(null);
|
||||||
|
subthemes: Array<{ key: string, label: string, emoji: string }> = [];
|
||||||
|
activeSubtheme = signal<string | null>(null);
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.loadEvents();
|
this.loadEvents();
|
||||||
|
@ -62,7 +70,7 @@ export class Home implements OnInit, OnDestroy {
|
||||||
//this.showTable = false;
|
//this.showTable = false;
|
||||||
//this.showFilters = true;
|
//this.showFilters = true;
|
||||||
this.showEditForm = true;
|
this.showEditForm = true;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadEvents() {
|
loadEvents() {
|
||||||
|
@ -70,7 +78,7 @@ export class Home implements OnInit, OnDestroy {
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const endDate = new Date(today);
|
const endDate = new Date(today);
|
||||||
endDate.setDate(today.getDate() + this.daysAhead);
|
endDate.setDate(today.getDate() + this.daysAhead);
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
start: today.toISOString().split('T')[0],
|
start: today.toISOString().split('T')[0],
|
||||||
end: endDate.toISOString().split('T')[0],
|
end: endDate.toISOString().split('T')[0],
|
||||||
|
@ -120,6 +128,12 @@ export class Home implements OnInit, OnDestroy {
|
||||||
whatTypes.add(feature.properties.what);
|
whatTypes.add(feature.properties.what);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.route.queryParams.subscribe(p => {
|
||||||
|
const t = (p?.['theme'] || '').trim();
|
||||||
|
this.theme.set(t || null);
|
||||||
|
this.buildSubthemes();
|
||||||
|
});
|
||||||
this.availableWhatTypes = Array.from(whatTypes).sort();
|
this.availableWhatTypes = Array.from(whatTypes).sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,7 +163,7 @@ export class Home implements OnInit, OnDestroy {
|
||||||
|
|
||||||
// Filtre par type d'événement
|
// Filtre par type d'événement
|
||||||
if (this.selectedWhatFilter) {
|
if (this.selectedWhatFilter) {
|
||||||
filtered = filtered.filter(feature =>
|
filtered = filtered.filter(feature =>
|
||||||
feature?.properties?.what === this.selectedWhatFilter
|
feature?.properties?.what === this.selectedWhatFilter
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -166,7 +180,6 @@ export class Home implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
onPickCoords(coords: [number, number]) {
|
onPickCoords(coords: [number, number]) {
|
||||||
// Autofill lat/lon in the form selection or prepare a new feature shell
|
|
||||||
const [lon, lat] = coords;
|
const [lon, lat] = coords;
|
||||||
if (this.selected && this.selected.properties) {
|
if (this.selected && this.selected.properties) {
|
||||||
this.selected = {
|
this.selected = {
|
||||||
|
@ -175,12 +188,22 @@ export class Home implements OnInit, OnDestroy {
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const osmUsername = this.osmAuth.getUsername();
|
const osmUsername = this.osmAuth.getUsername();
|
||||||
|
const whatKey = this.activeSubtheme();
|
||||||
|
let label = '';
|
||||||
|
let description = '';
|
||||||
|
if (whatKey) {
|
||||||
|
const preset = (oedb.presets.what as any)[whatKey];
|
||||||
|
if (preset) {
|
||||||
|
label = preset.label || '';
|
||||||
|
description = preset.description || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
this.selected = {
|
this.selected = {
|
||||||
id: null,
|
id: null,
|
||||||
properties: {
|
properties: {
|
||||||
label: '',
|
label: '',
|
||||||
description: '',
|
description: '',
|
||||||
what: '',
|
what: whatKey || '',
|
||||||
where: '',
|
where: '',
|
||||||
...(osmUsername && { last_modified_by: osmUsername })
|
...(osmUsername && { last_modified_by: osmUsername })
|
||||||
},
|
},
|
||||||
|
@ -195,7 +218,6 @@ export class Home implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
onCreated(_res: any) {
|
onCreated(_res: any) {
|
||||||
// refresh and clear selection after create
|
|
||||||
this.selected = null;
|
this.selected = null;
|
||||||
this.loadEvents();
|
this.loadEvents();
|
||||||
}
|
}
|
||||||
|
@ -209,16 +231,29 @@ export class Home implements OnInit, OnDestroy {
|
||||||
this.showEditForm = false;
|
this.showEditForm = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Menu callbacks
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
// Wire menu callbacks if needed via querySelector; left simple for now
|
// reserved
|
||||||
// We keep logic here: toggling and downloads
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleView() {
|
toggleView() {
|
||||||
this.showTable = !this.showTable;
|
this.showTable = !this.showTable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildSubthemes() {
|
||||||
|
const t = this.theme();
|
||||||
|
if (!t) { this.subthemes = []; this.activeSubtheme.set(null); return; }
|
||||||
|
const what = oedb.presets.what as Record<string, any>;
|
||||||
|
const list: Array<{ key: string, label: string, emoji: string }> = [];
|
||||||
|
Object.keys(what).forEach(k => {
|
||||||
|
if (k === t || k.startsWith(`${t}.`)) {
|
||||||
|
list.push({ key: k, label: what[k].label || k, emoji: what[k].emoji || '' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.subthemes = list.sort((a, b) => a.key.localeCompare(b.key));
|
||||||
|
const exact = this.subthemes.find(s => s.key === t);
|
||||||
|
this.activeSubtheme.set(exact ? exact.key : (this.subthemes[0]?.key || null));
|
||||||
|
}
|
||||||
|
|
||||||
downloadGeoJSON() {
|
downloadGeoJSON() {
|
||||||
const blob = new Blob([JSON.stringify({ type: 'FeatureCollection', features: this.filteredFeatures }, null, 2)], { type: 'application/geo+json' });
|
const blob = new Blob([JSON.stringify({ type: 'FeatureCollection', features: this.filteredFeatures }, null, 2)], { type: 'application/geo+json' });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
<menu>
|
<menu>
|
||||||
OpenEventDatabase
|
OpenEventDatabase
|
||||||
|
<nav class="nav">
|
||||||
<!--
|
<a routerLink="/" class="link">Accueil</a>
|
||||||
|
<a routerLink="/community-upcoming" class="link">Community à venir</a>
|
||||||
|
<a routerLink="/events-docs" class="link">Docs événements</a>
|
||||||
|
</nav>
|
||||||
|
<a href="/demo/stats">stats</a>
|
||||||
|
<a href="https://source.cipherbliss.com/tykayn/oedb-backend">sources</a>
|
||||||
|
|
||||||
|
(editor)
|
||||||
|
|
||||||
|
<!--
|
||||||
<div id="editor_form">
|
<div id="editor_form">
|
||||||
<div id="search_input">
|
<div id="search_input">
|
||||||
<input type="text" value="" placeholder="Rechercher une catégorie d'évènement">
|
<input type="text" value="" placeholder="Rechercher une catégorie d'évènement">
|
||||||
|
@ -52,7 +61,7 @@
|
||||||
<option value="point"></option>
|
<option value="point"></option>
|
||||||
<option value="polyline"></option>
|
<option value="polyline"></option>
|
||||||
<option value="bbox"></option>
|
<option value="bbox"></option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
-->
|
-->
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue