routes doc, future, express mode thematique

This commit is contained in:
Tykayn 2025-10-07 12:14:51 +02:00 committed by tykayn
parent eeb219cffa
commit 23c598034c
11 changed files with 228 additions and 18 deletions

View file

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

View file

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

View file

@ -0,0 +1,8 @@
.toolbar {
display: flex;
gap: 12px;
align-items: center;
padding: 8px 0;
}

View file

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

View 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>

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

View 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));
}
}

View file

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

View file

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

View file

@ -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,6 +31,7 @@ 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);
@ -47,6 +52,9 @@ export class Home implements OnInit, OnDestroy {
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();
@ -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();
} }
@ -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);

View file

@ -1,5 +1,14 @@
<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">