up edit
This commit is contained in:
parent
f991aee8ed
commit
bdb3728494
13 changed files with 283 additions and 20 deletions
|
@ -1,3 +0,0 @@
|
|||
html{
|
||||
font-family: "Calibri", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
|
@ -94,20 +94,23 @@
|
|||
@if (featureId()) {
|
||||
<button class="btn btn-ghost" type="button" (click)="onDelete()">Supprimer</button>
|
||||
}
|
||||
<button class="btn btn-ghost" type="button" (click)="onCancelEdit()">Quitter l’édition</button>
|
||||
</div>
|
||||
|
||||
@if (status().state !== 'idle') {
|
||||
<div class="status">
|
||||
@if (status().state === 'saving') {
|
||||
<div>{{status().message}}</div>
|
||||
} @else if (status().state === 'saved') {
|
||||
<div>
|
||||
{{status().message}}.
|
||||
<a [href]="'/demo/by-what?what=' + (status().what || form.value.what)" target="_blank">Voir d'autres évènements de ce type</a>
|
||||
</div>
|
||||
} @else if (status().state === 'error') {
|
||||
<div>{{status().message}}</div>
|
||||
}
|
||||
<div class="toast-container">
|
||||
<div class="toast" [class.is-info]="status().state==='saving'" [class.is-success]="status().state==='saved'" [class.is-error]="status().state==='error'">
|
||||
@if (status().state === 'saving') {
|
||||
<div>{{status().message}}</div>
|
||||
} @else if (status().state === 'saved') {
|
||||
<div>
|
||||
{{status().message}}.
|
||||
<a [href]="'/demo/by-what?what=' + (status().what || form.value.what)" target="_blank">Voir d'autres évènements de ce type</a>
|
||||
</div>
|
||||
} @else if (status().state === 'error') {
|
||||
<div>{{status().message}}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
|
|
|
@ -220,10 +220,12 @@ export class EditForm implements OnChanges {
|
|||
next: (res) => {
|
||||
this.status.set({ state: 'saved', what: val.what, message: 'Évènement mis à jour' });
|
||||
this.saved.emit(res);
|
||||
setTimeout(() => this.status.set({ state: 'idle' }), 3000);
|
||||
},
|
||||
error: (err) => {
|
||||
this.status.set({ state: 'error', what: val.what, message: 'Erreur lors de la mise à jour' });
|
||||
console.error(err);
|
||||
setTimeout(() => this.status.set({ state: 'idle' }), 3000);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
@ -231,10 +233,12 @@ export class EditForm implements OnChanges {
|
|||
next: (res) => {
|
||||
this.status.set({ state: 'saved', what: val.what, message: 'Évènement créé' });
|
||||
this.created.emit(res);
|
||||
setTimeout(() => this.status.set({ state: 'idle' }), 3000);
|
||||
},
|
||||
error: (err) => {
|
||||
this.status.set({ state: 'error', what: val.what, message: 'Erreur lors de la création' });
|
||||
console.error(err);
|
||||
setTimeout(() => this.status.set({ state: 'idle' }), 3000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -254,14 +258,36 @@ export class EditForm implements OnChanges {
|
|||
next: (res) => {
|
||||
this.status.set({ state: 'saved', what: this.form.value.what, message: 'Évènement supprimé' });
|
||||
this.deleted.emit(res);
|
||||
setTimeout(() => this.status.set({ state: 'idle' }), 3000);
|
||||
},
|
||||
error: (err) => {
|
||||
this.status.set({ state: 'error', what: this.form.value.what, message: 'Erreur lors de la suppression' });
|
||||
console.error(err);
|
||||
setTimeout(() => this.status.set({ state: 'idle' }), 3000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onCancelEdit() {
|
||||
this.selected = null;
|
||||
this.featureId.set(null);
|
||||
this.form.reset({
|
||||
label: '',
|
||||
description: '',
|
||||
what: '',
|
||||
where: '',
|
||||
lat: '',
|
||||
lon: '',
|
||||
wikidata: '',
|
||||
featureType: 'point',
|
||||
type: 'unscheduled',
|
||||
start: this.toLocalInputValue(new Date()),
|
||||
stop: this.toLocalInputValue(new Date(new Date().getTime() + 24 * 3600 * 1000))
|
||||
});
|
||||
this.presetValues.set({});
|
||||
this.status.set({ state: 'idle' });
|
||||
}
|
||||
|
||||
private toLocalInputValue(d: string | Date): string {
|
||||
const date = (typeof d === 'string') ? new Date(d) : d;
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
|
|
|
@ -1 +1,17 @@
|
|||
<p>osm works!</p>
|
||||
<p>
|
||||
osm works!
|
||||
|
||||
|
||||
@if(isLogginIn){
|
||||
<div class="pseudo">
|
||||
{{osmPseudo}}
|
||||
</div>
|
||||
<button (click)="logout()">logout</button>
|
||||
}
|
||||
@else{
|
||||
<div class="pseudo">
|
||||
pas connecté
|
||||
</div>
|
||||
<button (click)="login()">osm login</button>
|
||||
}
|
||||
</p>
|
|
@ -7,5 +7,14 @@ import { Component } from '@angular/core';
|
|||
styleUrl: './osm.scss'
|
||||
})
|
||||
export class Osm {
|
||||
osmPseudo: string='';
|
||||
isLogginIn: any = false;
|
||||
|
||||
logout() {
|
||||
|
||||
}
|
||||
|
||||
login() {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,4 +4,9 @@
|
|||
<input #searchBox type="text" placeholder="Chercher un lieu (Nominatim)" class="input" style="flex:1;">
|
||||
<button class="btn" type="button" (click)="searchPlace(searchBox.value)">Chercher</button>
|
||||
</div>
|
||||
@if (canRestoreOriginal) {
|
||||
<div style="position:absolute;bottom:10px;left:10px;display:flex;gap:8px;">
|
||||
<button class="btn btn-ghost" type="button" (click)="restoreOriginalCoords()">Reprendre les coordonnées initiales</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
@keyframes pulseGreen {
|
||||
0% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.4); }
|
||||
70% { box-shadow: 0 0 0 12px rgba(76, 175, 80, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0); }
|
||||
}
|
||||
@keyframes pulseRed {
|
||||
0% { box-shadow: 0 0 0 0 rgba(244, 67, 54, 0.4); }
|
||||
70% { box-shadow: 0 0 0 12px rgba(244, 67, 54, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(244, 67, 54, 0); }
|
||||
}
|
||||
|
||||
[data-feature-id].pulse-green {
|
||||
animation: pulseGreen 1.2s ease-out 1;
|
||||
border-radius: 50%;
|
||||
}
|
||||
[data-feature-id].pulse-red {
|
||||
animation: pulseRed 1.2s ease-out 1;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ import oedb_what_categories from '../../../oedb-types';
|
|||
})
|
||||
export class AllEvents {
|
||||
@Input() features: Array<any> = [];
|
||||
@Input() selected: any | null = null;
|
||||
@Input() highlight: { id: string | number, type: 'saved' | 'deleted' } | null = null;
|
||||
@Output() select = new EventEmitter<any>();
|
||||
@Output() pickCoords = new EventEmitter<[number, number]>();
|
||||
|
||||
|
@ -17,6 +19,8 @@ export class AllEvents {
|
|||
private map: any;
|
||||
private markers: any[] = [];
|
||||
private pickedMarker: any | null = null;
|
||||
private originalCoords: [number, number] | null = null;
|
||||
private currentPicked: [number, number] | null = null;
|
||||
|
||||
async ngOnInit() {
|
||||
await this.ensureMapLibre();
|
||||
|
@ -31,7 +35,28 @@ export class AllEvents {
|
|||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
// track original coordinates of the selected feature
|
||||
if (this.selected && Array.isArray(this.selected?.geometry?.coordinates)) {
|
||||
const coords = this.selected.geometry.coordinates as [number, number];
|
||||
this.originalCoords = coords;
|
||||
// If no picked marker yet, align current picked to original
|
||||
if (!this.currentPicked) this.currentPicked = coords;
|
||||
}
|
||||
this.renderFeatures();
|
||||
|
||||
// trigger animation highlight
|
||||
if (this.highlight && this.highlight.id !== undefined && this.highlight.id !== null) {
|
||||
const idStr = String(this.highlight.id);
|
||||
const el = document.querySelector(`[data-feature-id="${CSS.escape(idStr)}"]`);
|
||||
if (el) {
|
||||
el.classList.remove('pulse-green', 'pulse-red');
|
||||
if (this.highlight.type === 'saved') el.classList.add('pulse-green');
|
||||
if (this.highlight.type === 'deleted') el.classList.add('pulse-red');
|
||||
setTimeout(() => {
|
||||
el.classList.remove('pulse-green', 'pulse-red');
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private ensureMapLibre(): Promise<void> {
|
||||
|
@ -101,6 +126,7 @@ export class AllEvents {
|
|||
this.pickedMarker.remove();
|
||||
}
|
||||
this.pickedMarker = new maplibregl.Marker({ element: el }).setLngLat(coords).addTo(this.map);
|
||||
this.currentPicked = coords;
|
||||
}
|
||||
|
||||
async searchPlace(query: string) {
|
||||
|
@ -125,6 +151,18 @@ export class AllEvents {
|
|||
} catch {}
|
||||
}
|
||||
|
||||
get canRestoreOriginal(): boolean {
|
||||
if (!this.originalCoords || !this.currentPicked) return false;
|
||||
return this.originalCoords[0] !== this.currentPicked[0] || this.originalCoords[1] !== this.currentPicked[1];
|
||||
}
|
||||
|
||||
restoreOriginalCoords() {
|
||||
if (!this.originalCoords) return;
|
||||
this.showPickedMarker(this.originalCoords);
|
||||
this.pickCoords.emit(this.originalCoords);
|
||||
if (this.map) this.map.flyTo({ center: this.originalCoords, zoom: Math.max(this.map.getZoom() || 12, 12) });
|
||||
}
|
||||
|
||||
private renderFeatures() {
|
||||
if (!this.map || !Array.isArray(this.features)) return;
|
||||
// clear existing markers
|
||||
|
@ -138,11 +176,22 @@ export class AllEvents {
|
|||
const coords = f?.geometry?.coordinates;
|
||||
if (!coords || !Array.isArray(coords)) return;
|
||||
const p = f.properties || {};
|
||||
const fid = (p && (p.id ?? p.uuid)) ?? f?.id;
|
||||
const el = this.buildMarkerElement(p);
|
||||
el.style.cursor = 'pointer';
|
||||
if (typeof fid !== 'undefined') {
|
||||
el.setAttribute('data-feature-id', String(fid));
|
||||
}
|
||||
// selected styling
|
||||
const selId = this.selected?.properties?.id ?? this.selected?.properties?.uuid ?? this.selected?.id;
|
||||
if (selId !== undefined && selId !== null && String(selId) === String(fid)) {
|
||||
el.style.transform = 'scale(1.2)';
|
||||
el.style.boxShadow = '0 0 0 4px rgba(25,118,210,0.25)';
|
||||
el.style.borderRadius = '50%';
|
||||
}
|
||||
el.addEventListener('click', () => {
|
||||
this.select.emit({
|
||||
id: (p && (p.id ?? p.uuid)) ?? f?.id,
|
||||
id: fid,
|
||||
properties: p,
|
||||
geometry: { type: 'Point', coordinates: coords }
|
||||
});
|
||||
|
|
|
@ -14,8 +14,33 @@
|
|||
<app-edit-form [selected]="selected" (saved)="onSaved($event)" (created)="onCreated($event)" (deleted)="onDeleted($event)"></app-edit-form>
|
||||
</div>
|
||||
<div class="main">
|
||||
<div class="map">
|
||||
<app-all-events [features]="features" (select)="onSelect($event)" (pickCoords)="onPickCoords($event)"></app-all-events>
|
||||
</div>
|
||||
@if (!showTable) {
|
||||
<div class="map">
|
||||
<app-all-events [features]="features" [selected]="selected" (select)="onSelect($event)" (pickCoords)="onPickCoords($event)"></app-all-events>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="table-wrapper" style="overflow:auto;height:100%;">
|
||||
<table style="width:100%;border-collapse:collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align:left;padding:6px;border-bottom:1px solid #e5e7eb;">Type</th>
|
||||
<th style="text-align:left;padding:6px;border-bottom:1px solid #e5e7eb;">Label</th>
|
||||
<th style="text-align:left;padding:6px;border-bottom:1px solid #e5e7eb;">Start</th>
|
||||
<th style="text-align:left;padding:6px;border-bottom:1px solid #e5e7eb;">Stop</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (f of features; track f.id) {
|
||||
<tr (click)="onSelect({ id: f?.properties?.id ?? f?.id, properties: f.properties, geometry: f.geometry })" style="cursor:pointer;">
|
||||
<td style="padding:6px;border-bottom:1px solid #f1f5f9;">{{f?.properties?.what}}</td>
|
||||
<td style="padding:6px;border-bottom:1px solid #f1f5f9;">{{f?.properties?.label || f?.properties?.name}}</td>
|
||||
<td style="padding:6px;border-bottom:1px solid #f1f5f9;">{{f?.properties?.start || f?.properties?.when}}</td>
|
||||
<td style="padding:6px;border-bottom:1px solid #f1f5f9;">{{f?.properties?.stop}}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -19,6 +19,7 @@ export class Home {
|
|||
OedbApi = inject(OedbApi);
|
||||
features: Array<any> = [];
|
||||
selected: any | null = null;
|
||||
showTable = false;
|
||||
|
||||
constructor() {
|
||||
this.OedbApi.getEvents({ when: 'now', limit: 500 }).subscribe((events: any) => {
|
||||
|
@ -68,4 +69,49 @@ export class Home {
|
|||
this.features = Array.isArray(events?.features) ? events.features : [];
|
||||
});
|
||||
}
|
||||
|
||||
// Menu callbacks
|
||||
ngAfterViewInit() {
|
||||
// Wire menu callbacks if needed via querySelector; left simple for now
|
||||
// We keep logic here: toggling and downloads
|
||||
}
|
||||
|
||||
toggleView() {
|
||||
this.showTable = !this.showTable;
|
||||
}
|
||||
|
||||
downloadGeoJSON() {
|
||||
const blob = new Blob([JSON.stringify({ type: 'FeatureCollection', features: this.features }, null, 2)], { type: 'application/geo+json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'events.geojson';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
}
|
||||
|
||||
downloadCSV() {
|
||||
const header = ['id', 'what', 'label', 'start', 'stop', 'lon', 'lat'];
|
||||
const rows = this.features.map((f: any) => [
|
||||
JSON.stringify(f?.properties?.id ?? f?.id ?? ''),
|
||||
JSON.stringify(f?.properties?.what ?? ''),
|
||||
JSON.stringify(f?.properties?.label ?? f?.properties?.name ?? ''),
|
||||
JSON.stringify(f?.properties?.start ?? f?.properties?.when ?? ''),
|
||||
JSON.stringify(f?.properties?.stop ?? ''),
|
||||
JSON.stringify(f?.geometry?.coordinates?.[0] ?? ''),
|
||||
JSON.stringify(f?.geometry?.coordinates?.[1] ?? '')
|
||||
].join(','));
|
||||
const csv = [header.join(','), ...rows].join('\n');
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'events.csv';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,6 +72,12 @@
|
|||
<button class="button">oui, toujours là</button>
|
||||
<button class="button">non plus là</button>
|
||||
<button class="button">pouet pouet!</button>
|
||||
<hr>
|
||||
<button class="button" (click)="toggleView()">Basculer carte / tableau</button>
|
||||
<div class="downloaders">
|
||||
<button class="button" (click)="downloadGeoJSON()">Télécharger GeoJSON</button>
|
||||
<button class="button" (click)="downloadCSV()">Télécharger CSV</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="user_infos">
|
||||
login OSM:
|
||||
|
|
|
@ -10,6 +10,9 @@ import oedb_what_categories from '../../../../oedb-types';
|
|||
export class Menu {
|
||||
|
||||
public oedb_what_categories: Array<any> = [];
|
||||
public onToggleView?: () => void;
|
||||
public onDownloadGeoJSON?: () => void;
|
||||
public onDownloadCSV?: () => void;
|
||||
|
||||
constructor() {
|
||||
let keys = Object.keys(oedb_what_categories.presets.what);
|
||||
|
@ -22,4 +25,8 @@ export class Menu {
|
|||
);
|
||||
})
|
||||
}
|
||||
|
||||
toggleView() { this.onToggleView && this.onToggleView(); }
|
||||
downloadGeoJSON() { this.onDownloadGeoJSON && this.onDownloadGeoJSON(); }
|
||||
downloadCSV() { this.onDownloadCSV && this.onDownloadCSV(); }
|
||||
}
|
||||
|
|
|
@ -5,6 +5,9 @@ $color-bg: #f7fafb;
|
|||
$color-surface: #ffffff;
|
||||
$color-text: #22303a;
|
||||
$color-muted: #6b7b86;
|
||||
$color-success: #b4e5c6;
|
||||
$color-error: #f6c9c9;
|
||||
$color-info: #cfe8ff;
|
||||
$border-radius: 10px;
|
||||
$shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
$shadow-md: 0 6px 18px rgba(0, 0, 0, 0.08);
|
||||
|
@ -16,8 +19,35 @@ html, body {
|
|||
padding: 0;
|
||||
background: $color-bg;
|
||||
color: $color-text;
|
||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, sans-serif;
|
||||
// font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, Arial, sans-serif;
|
||||
}
|
||||
html, body{
|
||||
font-family: "Calibri", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
button, .button{
|
||||
border-radius: 5px;
|
||||
background-color: #79a2d1;
|
||||
padding: 1rem 0.5rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
&:hover{
|
||||
background-color: #6992c1;
|
||||
}
|
||||
&:active{
|
||||
background-color: #5982b1;
|
||||
}
|
||||
}
|
||||
input{
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.12);
|
||||
background: #ffffff;
|
||||
color: #22303a;
|
||||
box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.02);
|
||||
}
|
||||
|
||||
|
||||
app-root, app-home {
|
||||
display: block;
|
||||
|
@ -56,3 +86,27 @@ app-root, app-home {
|
|||
}
|
||||
|
||||
label { font-size: 0.85rem; color: $color-muted; }
|
||||
|
||||
/* Toasts */
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: 16px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.toast {
|
||||
min-width: 240px;
|
||||
max-width: 360px;
|
||||
padding: 10px 12px;
|
||||
border-radius: $border-radius;
|
||||
box-shadow: $shadow-md;
|
||||
border: 1px solid rgba(0,0,0,0.06);
|
||||
background: $color-surface;
|
||||
}
|
||||
|
||||
.toast.is-success { background: $color-success; }
|
||||
.toast.is-error { background: $color-error; }
|
||||
.toast.is-info { background: $color-info; }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue