diff --git a/frontend/src/app/app.scss b/frontend/src/app/app.scss index 8bd32c7..e69de29 100644 --- a/frontend/src/app/app.scss +++ b/frontend/src/app/app.scss @@ -1,3 +0,0 @@ -html{ - font-family: "Calibri", "Helvetica Neue", Helvetica, Arial, sans-serif; -} diff --git a/frontend/src/app/forms/edit-form/edit-form.html b/frontend/src/app/forms/edit-form/edit-form.html index 021eb5f..db9d94a 100644 --- a/frontend/src/app/forms/edit-form/edit-form.html +++ b/frontend/src/app/forms/edit-form/edit-form.html @@ -94,20 +94,23 @@ @if (featureId()) { } + @if (status().state !== 'idle') { -
- @if (status().state === 'saving') { -
{{status().message}}
- } @else if (status().state === 'saved') { -
- {{status().message}}. - Voir d'autres évènements de ce type -
- } @else if (status().state === 'error') { -
{{status().message}}
- } +
+
+ @if (status().state === 'saving') { +
{{status().message}}
+ } @else if (status().state === 'saved') { +
+ {{status().message}}. + Voir d'autres évènements de ce type +
+ } @else if (status().state === 'error') { +
{{status().message}}
+ } +
} diff --git a/frontend/src/app/forms/edit-form/edit-form.ts b/frontend/src/app/forms/edit-form/edit-form.ts index dec440d..0a6b4c9 100644 --- a/frontend/src/app/forms/edit-form/edit-form.ts +++ b/frontend/src/app/forms/edit-form/edit-form.ts @@ -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 ''; diff --git a/frontend/src/app/forms/osm/osm.html b/frontend/src/app/forms/osm/osm.html index 199c0f0..9fedcb8 100644 --- a/frontend/src/app/forms/osm/osm.html +++ b/frontend/src/app/forms/osm/osm.html @@ -1 +1,17 @@ -

osm works!

+

+ osm works! + + + @if(isLogginIn){ +

+ {{osmPseudo}} +
+ +} +@else{ +
+ pas connecté +
+ +} +

\ No newline at end of file diff --git a/frontend/src/app/forms/osm/osm.ts b/frontend/src/app/forms/osm/osm.ts index 9d27557..1a03de3 100644 --- a/frontend/src/app/forms/osm/osm.ts +++ b/frontend/src/app/forms/osm/osm.ts @@ -7,5 +7,14 @@ import { Component } from '@angular/core'; styleUrl: './osm.scss' }) export class Osm { + osmPseudo: string=''; + isLogginIn: any = false; + logout() { + + } + + login() { + + } } diff --git a/frontend/src/app/maps/all-events/all-events.html b/frontend/src/app/maps/all-events/all-events.html index fe2fb5b..c0f8348 100644 --- a/frontend/src/app/maps/all-events/all-events.html +++ b/frontend/src/app/maps/all-events/all-events.html @@ -4,4 +4,9 @@
+ @if (canRestoreOriginal) { +
+ +
+ } diff --git a/frontend/src/app/maps/all-events/all-events.scss b/frontend/src/app/maps/all-events/all-events.scss index e69de29..1dfa672 100644 --- a/frontend/src/app/maps/all-events/all-events.scss +++ b/frontend/src/app/maps/all-events/all-events.scss @@ -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%; +} + diff --git a/frontend/src/app/maps/all-events/all-events.ts b/frontend/src/app/maps/all-events/all-events.ts index ab04eea..c2adb8e 100644 --- a/frontend/src/app/maps/all-events/all-events.ts +++ b/frontend/src/app/maps/all-events/all-events.ts @@ -9,6 +9,8 @@ import oedb_what_categories from '../../../oedb-types'; }) export class AllEvents { @Input() features: Array = []; + @Input() selected: any | null = null; + @Input() highlight: { id: string | number, type: 'saved' | 'deleted' } | null = null; @Output() select = new EventEmitter(); @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 { @@ -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 } }); diff --git a/frontend/src/app/pages/home/home.html b/frontend/src/app/pages/home/home.html index 5c75da1..b23f5ee 100644 --- a/frontend/src/app/pages/home/home.html +++ b/frontend/src/app/pages/home/home.html @@ -14,8 +14,33 @@
-
- -
+ @if (!showTable) { +
+ +
+ } @else { +
+ + + + + + + + + + + @for (f of features; track f.id) { + + + + + + + } + +
TypeLabelStartStop
{{f?.properties?.what}}{{f?.properties?.label || f?.properties?.name}}{{f?.properties?.start || f?.properties?.when}}{{f?.properties?.stop}}
+
+ }
diff --git a/frontend/src/app/pages/home/home.ts b/frontend/src/app/pages/home/home.ts index 132a08d..755ab61 100644 --- a/frontend/src/app/pages/home/home.ts +++ b/frontend/src/app/pages/home/home.ts @@ -19,6 +19,7 @@ export class Home { OedbApi = inject(OedbApi); features: Array = []; 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(); + } } diff --git a/frontend/src/app/pages/home/menu/menu.html b/frontend/src/app/pages/home/menu/menu.html index f4c3146..bd97729 100644 --- a/frontend/src/app/pages/home/menu/menu.html +++ b/frontend/src/app/pages/home/menu/menu.html @@ -72,6 +72,12 @@ +
+ +
+ + +
login OSM: diff --git a/frontend/src/app/pages/home/menu/menu.ts b/frontend/src/app/pages/home/menu/menu.ts index 1566e22..042ed40 100644 --- a/frontend/src/app/pages/home/menu/menu.ts +++ b/frontend/src/app/pages/home/menu/menu.ts @@ -10,6 +10,9 @@ import oedb_what_categories from '../../../../oedb-types'; export class Menu { public oedb_what_categories: Array = []; + 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(); } } diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 203e18c..48dec4f 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -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; }