Merge branch 'master' into navbar

navbar
Alexis Feron 3 months ago
commit 7a93cb30a0

7
package-lock.json generated

@ -16,6 +16,7 @@
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
"exifr": "^7.1.3",
"flowbite": "^2.5.2",
"leaflet": "^1.9.4",
"rxjs": "~7.8.0",
@ -6890,6 +6891,12 @@
"node": ">=0.8.x"
}
},
"node_modules/exifr": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz",
"integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw==",
"license": "MIT"
},
"node_modules/exponential-backoff": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz",

@ -18,6 +18,7 @@
"@angular/platform-browser": "^19.0.0",
"@angular/platform-browser-dynamic": "^19.0.0",
"@angular/router": "^19.0.0",
"exifr": "^7.1.3",
"flowbite": "^2.5.2",
"leaflet": "^1.9.4",
"rxjs": "~7.8.0",

@ -16,6 +16,7 @@ import {
import { AutocompleteService } from '../../services/auto-complete.service';
import { PinService } from '../../services/pin.service';
import { DragDropComponent } from '../drag-drop/drag-drop.component';
import { ExifService } from '../../exif.service';
@Component({
selector: 'app-add-pin-popup',
@ -34,7 +35,8 @@ export class AddPinPopupComponent implements OnInit {
constructor(
private fb: FormBuilder,
private autocompleteService: AutocompleteService,
private pinService: PinService
private pinService: PinService,
private exifService: ExifService
) {
this.form = this.fb.group({
title: new FormControl(''),
@ -85,8 +87,24 @@ export class AddPinPopupComponent implements OnInit {
this.suggestions = [];
}
onFilesReceived(files: FileList): void {
async onFilesReceived(files: FileList): Promise<void> {
this.files = Array.from(files);
for (let i = 0; i < this.files.length; i++) {
try {
const data = await this.exifService.getLocation(this.files[i]);
if (data.latitude !== undefined && data.longitude !== undefined) {
const address = await this.autocompleteService.getAddressFromCoordinates(data.latitude, data.longitude).toPromise();
if (address) {
console.error("Data : " + JSON.stringify(address));
this.form.get('location')?.setValue(address.display_name);
break;
}
}
} catch (error) {
console.error("Error : " + error);
}
}
}
submitForm(): void {

@ -1,9 +1,11 @@
import { Component, OnInit, ViewContainerRef } from '@angular/core';
import * as L from 'leaflet';
import 'leaflet/dist/leaflet.css';
import { Monument } from '../../model/Monument';
import { Pin } from '../../model/Pin';
import { PinService } from '../../services/pin.service';
import { MonumentMarkerComponent } from '../monument-marker/monument-marker.component';
import { PinMarkerComponent } from '../pin-marker/pin-marker.component';
import { ActivatedRoute } from '@angular/router';
import { Router } from '@angular/router';
@Component({
selector: 'app-leaflet-map',
@ -11,10 +13,13 @@ import { MonumentMarkerComponent } from '../monument-marker/monument-marker.comp
})
export class LeafletMapComponent implements OnInit {
private map!: L.Map;
private markersMap: { [key: string]: L.Marker } = {};
constructor(
private viewContainerRef: ViewContainerRef,
private pinsService: PinService
private pinsService: PinService,
private route: ActivatedRoute,
private router: Router
) {}
ngOnInit(): void {
@ -51,27 +56,47 @@ export class LeafletMapComponent implements OnInit {
</svg>
`);
this.pinsService.getPins().subscribe((monuments: Monument[]) => {
console.log(monuments);
this.pinsService.getPins().subscribe((pins: Pin[]) => {
console.log(pins);
// Add markers
monuments.forEach((monument: Monument) => {
//const icon = monument.visited ? visitedIcon : notVisitedIcon;
pins.forEach((pin: Pin) => {
//const icon = pin.visited ? visitedIcon : notVisitedIcon;
const icon = visitedIcon;
const marker = L.marker(monument.location as [number, number], {
const marker = L.marker(pin.location as [number, number], {
icon,
}).addTo(this.map);
marker.on('popupclose', () => {
this.router.navigate(['/map']);
});
marker.on('popupopen', () => {
this.router.navigate(['/map'], { queryParams: { pin: pin.id } });
});
// Dynamically create Angular component and attach it to popup
const popupDiv = document.createElement('div');
const componentRef = this.viewContainerRef.createComponent(
MonumentMarkerComponent
);
const componentRef =
this.viewContainerRef.createComponent(PinMarkerComponent);
componentRef.instance.monument = monument;
componentRef.instance.pin = pin;
popupDiv.appendChild(componentRef.location.nativeElement);
marker.bindPopup(popupDiv);
// Stocker les marqueurs par ID
this.markersMap[pin.id] = marker;
});
// Ouvrir automatiquement la pop-up si un ID est passé dans l'URL
this.route.queryParams.subscribe((params) => {
const pinId = params['pin']; // Récupérer l'ID du Pin depuis les paramètres de requête
if (pinId && this.markersMap[pinId]) {
this.markersMap[pinId].openPopup(); // Ouvrir la pop-up du marqueur correspondant
console.log('Coords du pin : ', this.markersMap[pinId].getLatLng());
this.map.setView(this.markersMap[pinId].getLatLng(), 6); // Centrer la carte sur le marqueur
}
});
});
}

@ -61,7 +61,9 @@
</button>
</div>
<!-- Modal body -->
<div class="p-4 md:p-5">
<div
class="p-4 md:p-5"
*ngIf="isLoginModalOpen">
<form [formGroup]="userForm" class="space-y-4">
<div>
<label

@ -32,6 +32,54 @@
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"
/>
</svg>
<span class="sr-only">Search</span>
</button>
<div class="relative hidden lg:block">
<div
class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none"
>
<svg
class="w-4 h-4 text-gray-500 dark:text-gray-400"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 20 20"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"
/>
</svg>
<span class="sr-only">Search icon</span>
</div>
<form [formGroup]="searchForm">
<input
type="text"
id="search-navbar"
class="block w-full p-2 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Search..."
formControlName="searchControl"
(focus)="onFocus()"
(blur)="onBlur()"
/>
</form>
<ul
*ngIf="pinsFiltered.length > 0 && inputFocus"
class="bg-white dark:bg-gray-700 border border-gray-300 mt-2 rounded shadow absolute z-10 max-h-60 overflow-auto w-full"
>
<li
*ngFor="let suggestion of pinsFiltered"
(click)="clickSuggestion(suggestion)"
class="p-2 block text-sm font-medium text-gray-900 dark:text-white hover:bg-gray-500 cursor-pointer"
>
{{ suggestion.title }}
</li>
</ul>
</div>
<svg
*ngIf="isSearchOpen"
class="w-5 h-5"
@ -127,7 +175,7 @@
class="w-full lg:flex lg:w-auto lg:order-1"
>
<ul
class="w-full flex flex-col p-4 lg:p-0 mt-4 font-medium border border-gray-100 rounded-lg bg-gray-50 lg:space-x-8 lg:flex-row lg:mt-0 lg:border-0 lg:bg-white dark:bg-gray-800 lg:dark:bg-gray-900 dark:border-gray-700"
class="w-full flex flex-col p-0 lg:p-0 mt-4 font-medium border border-gray-100 rounded-lg bg-gray-50 lg:space-x-8 lg:flex-row lg:mt-0 lg:border-0 lg:bg-white dark:bg-gray-800 lg:dark:bg-gray-900 dark:border-gray-700"
>
<li>
<a

@ -1,12 +1,38 @@
import { CommonModule, NgIf } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { NavigationEnd, Router, RouterLink } from '@angular/router';
import { CommonModule, CommonModule, NgIf } from '@angular/common';
import { Component, input, OnInit } from '@angular/core';
import {
FormBuilder,
FormControl,
FormGroup,
ReactiveFormsModule,
} from '@angular/forms';
import {
ActivatedRoute,
NavigationEnd,
Router,
RouterLink,
} from '@angular/router';
import {
catchError,
debounceTime,
distinctUntilChanged,
of,
switchMap,
} from 'rxjs';
import { Pin } from '../../model/Pin';
import { LocalStorageService } from '../../services/localstorage.service';
import { PinService } from '../../services/pin.service';
import { AddPinPopupComponent } from '../add-pin-popup/add-pin-popup.component';
@Component({
selector: 'app-navbar',
imports: [AddPinPopupComponent, NgIf, RouterLink, CommonModule],
imports: [
AddPinPopupComponent,
NgIf,
RouterLink,
CommonModule,
ReactiveFormsModule,
],
templateUrl: './navbar.component.html',
})
export class NavbarComponent implements OnInit {
@ -21,19 +47,108 @@ export class NavbarComponent implements OnInit {
toggleSearch() {
this.isSearchOpen = !this.isSearchOpen;
}
pins: Pin[] = [];
pinsFiltered: Pin[] = [];
inputFocus: Boolean = false;
searchForm: FormGroup;
constructor(
private router: Router,
private localStorageService: LocalStorageService
) {}
private route: ActivatedRoute,
private localStorageService: LocalStorageService,
private pinService: PinService,
private fb: FormBuilder
) {
this.searchForm = this.fb.group({
searchControl: new FormControl(''),
});
}
ngOnInit(): void {
this.pins = this.pinService.getPins().subscribe((pins: Pin[]) => {
this.pins = pins;
});
this.isHome = this.router.url === '/';
this.router.events.subscribe((event) => {
if (event instanceof NavigationEnd) {
this.isHome = event.url === '/';
}
});
// Initialise la barre de recherche avec debounce et distinctUntilChanged
this.searchForm
.get('searchControl')
?.valueChanges.pipe(
debounceTime(300), // Attendre 300ms après la dernière frappe
distinctUntilChanged(), // Ignorer si la nouvelle valeur est la même que la précédente
switchMap((searchTerm) => {
console.log(
'Value change : ',
this.searchForm.get('searchControl')?.valueChanges
);
const trimmedQuery = searchTerm?.trim();
if (trimmedQuery && trimmedQuery.length > 1) {
return of(this.filterPins(trimmedQuery));
}
return of([]);
}),
catchError((error) => {
console.error(
'Erreur lors de la récupération des suggestions :',
error
);
return of([]);
})
)
.subscribe((filteredPins) => {
this.pinsFiltered = filteredPins;
console.log('Pins filtrés : ', this.pinsFiltered);
});
}
filterPins(searchTerm: string): Pin[] {
const filteredPins: Pin[] = [];
console.log('Pins : ', this.pins);
if (this.pins.length === 0) {
this.pins = this.pinService.getPins();
}
this.pins.forEach((pin: Pin) => {
if (
pin.title &&
pin.title.toLowerCase().includes(searchTerm.toLowerCase())
) {
console.log('Search term : ', searchTerm, ' / Pin trouvé : ', pin);
filteredPins.push(pin);
}
});
return filteredPins;
}
clickSuggestion(pin: Pin): void {
this.searchForm.reset();
const queryParams = { pin: pin.id }; // Remplacer "id" par la bonne propriété si nécessaire
this.router.navigate([], {
relativeTo: this.route,
queryParams: queryParams,
queryParamsHandling: 'merge', // Conserve les autres paramètres de requête
});
console.log('Redirection avec ID :', pin.id);
}
onFocus(): void {
this.inputFocus = true;
}
onBlur(): void {
setTimeout(() => {
this.inputFocus = false;
}, 200); // Petit délai pour laisser l'utilisateur cliquer
}
public logout() {

@ -1,7 +1,7 @@
<div class="text-center">
<strong>{{ monument.title }}</strong>
<strong>{{ pin.title }}</strong>
<div
*ngIf="monument.files.length > 0"
*ngIf="pin.files.length > 0"
class="relative carousel overflow-hidden"
>
<!-- Carousel wrapper -->
@ -9,7 +9,7 @@
class="relative h-40 mt-2 overflow-hidden rounded-lg flex items-center justify-center"
>
<div
*ngFor="let image of monument.files; let index = index"
*ngFor="let image of pin.files; let index = index"
[class]="
'absolute inset-0 transition-opacity duration-700 ease-in-out' +
(index === currentIndex ? ' opacity-100' : ' opacity-0')
@ -17,14 +17,14 @@
>
<img
[src]="image"
[alt]="monument.title"
[alt]="pin.title"
class="object-contain max-h-full max-w-full h-full w-auto mx-auto"
/>
</div>
</div>
<!-- Slider controls -->
<div *ngIf="monument.files.length > 1">
<div *ngIf="pin.files.length > 1">
<button
type="button"
class="absolute top-0 left-0 z-30 flex items-center justify-center h-full cursor-pointer group focus:outline-none"

@ -1,17 +1,17 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MonumentMarkerComponent } from './monument-marker.component';
import { PinMarkerComponent } from './pin-marker.component';
describe('MonumentmarkerComponent', () => {
let component: MonumentMarkerComponent;
let fixture: ComponentFixture<MonumentMarkerComponent>;
describe('PinmarkerComponent', () => {
let component: PinMarkerComponent;
let fixture: ComponentFixture<PinMarkerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MonumentMarkerComponent],
imports: [PinMarkerComponent],
}).compileComponents();
fixture = TestBed.createComponent(MonumentMarkerComponent);
fixture = TestBed.createComponent(PinMarkerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

@ -1,19 +1,19 @@
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { Monument } from '../../model/Monument';
import { Pin } from '../../model/Pin';
@Component({
selector: 'app-monument-marker',
templateUrl: './monument-marker.component.html',
selector: 'app-pin-marker',
templateUrl: './pin-marker.component.html',
imports: [CommonModule],
})
export class MonumentMarkerComponent {
@Input() monument!: Monument;
export class PinMarkerComponent {
@Input() pin!: Pin;
currentIndex: number = 0;
get formattedDescription(): string {
return this.formatDescription(this.monument.description);
return this.formatDescription(this.pin.description);
}
formatDescription(description: string): string {
@ -26,11 +26,10 @@ export class MonumentMarkerComponent {
prevSlide(): void {
this.currentIndex =
(this.currentIndex - 1 + this.monument.files.length) %
this.monument.files.length;
(this.currentIndex - 1 + this.pin.files.length) % this.pin.files.length;
}
nextSlide(): void {
this.currentIndex = (this.currentIndex + 1) % this.monument.files.length;
this.currentIndex = (this.currentIndex + 1) % this.pin.files.length;
}
}

@ -63,7 +63,9 @@
</button>
</div>
<!-- Modal body -->
<div class="p-4 md:p-5">
<div
class="p-4 md:p-5"
*ngIf="isRegisterModalOpen">
<form [formGroup]="userForm" class="space-y-4">
<div>
<label

@ -1,32 +0,0 @@
import { Monument } from '../model/Monument';
export const monuments: Monument[] = [
{
coords: [48.85837, 2.294481],
name: 'Tour Eiffel',
images: [
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQGGib245tSFiK1Qcx0cB0dZsoVyJElwsY3kA&s',
'https://encrypted-tbn2.gstatic.com/licensed-image?q=tbn:ANd9GcTLB9B0j50rJbcSbdja9_hySHS6_KATbhTK_iCeWeNKtA92hTmTX5nTW3udjjovZrnU1JxqAjMS_VqHnMwHGhTs35-sU-7B29_X_T3uLV8',
],
description: 'Visité en 2020 avec la famille, un moment inoubliable.',
visited: true,
},
{
coords: [43.296482, 5.36978],
name: 'Vieux Port de Marseille',
images: [
'https://upload.wikimedia.org/wikipedia/commons/thumb/7/74/Marseille_Old_Port.jpg/390px-Marseille_Old_Port.jpg',
],
description:
"Découvert lors d'un week-end ensoleillé en 2019 avec @John-Doe.",
visited: true,
},
{
coords: [48.636063, -1.511457],
name: 'Mont Saint-Michel',
images: [],
description: '',
visited: false,
},
// ...
];

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ExifService } from './exif.service';
describe('ExifService', () => {
let service: ExifService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ExifService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

@ -0,0 +1,87 @@
import { Injectable } from '@angular/core';
import exifr from 'exifr';
@Injectable({
providedIn: 'root'
})
export class ExifService {
private getExifData(file: File): Promise<any> {
// console.log('getExifData(file)');
// return new Promise((resolve, reject) => {
// console.log('getExifData(file) -> Promise');
// const reader = new FileReader();
// console.log('getExifData(file) -> Promise -> reader');
// reader.onload = (event: any) => {
// console.log('getExifData(file) -> Promise -> reader -> onload');
// EXIF.getData(event.target.result, function() {
// console.log('getExifData(file) -> Promise -> reader -> onload -> EXIF.getData');
// const allExifData = EXIF.getAllTags(this);
// console.log('getExifData(file) -> Promise -> reader -> onload -> EXIF.getData -> getAllTags');
// resolve(allExifData);
// });
// };
// reader.onerror = (error) => reject(error);
// reader.readAsArrayBuffer(file);
// });
return exifr.parse(file);
}
async getAllExifData(file: File): Promise<any> {
try {
return await this.getExifData(file);
} catch (error) {
console.error('Error reading EXIF data:', error);
return
}
}
async getOrientation(file: File): Promise<number | undefined> {
try {
const exifData = await this.getExifData(file);
return exifData.Orientation;
} catch (error) {
console.error('Error reading EXIF data:', error);
return undefined;
}
}
async getDeviceModel(file: File): Promise<string | undefined> {
try {
const exifData = await this.getExifData(file);
return exifData.Model;
} catch (error) {
console.error('Error reading EXIF data:', error);
return undefined;
}
}
async getLocation(file: File): Promise<{ latitude?: number; longitude?: number }> {
try {
const exifData = await this.getExifData(file);
return {
latitude: exifData.GPSLatitude ? this.convertToDecimal(exifData.GPSLatitude, exifData.GPSLatitudeRef) : undefined,
longitude: exifData.GPSLongitude ? this.convertToDecimal(exifData.GPSLongitude, exifData.GPSLongitudeRef) : undefined
};
} catch (error) {
console.error('Error reading EXIF data:', error);
return {};
}
}
async getDateTime(file: File): Promise<string | boolean> {
try {
const exifData = await this.getExifData(file);
return exifData.DateTime;
} catch (error) {
console.error('Error reading EXIF data:', error);
return false;
}
}
private convertToDecimal(coordinate: number[], direction: string): number {
if (!coordinate || coordinate.length !== 3) return NaN;
const decimal = coordinate[0] + coordinate[1] / 60 + coordinate[2] / 3600;
return (direction === 'S' || direction === 'W') ? -decimal : decimal;
}
}

@ -1,4 +1,5 @@
export interface Monument {
export interface Pin {
id: string;
location: number[];
title: string;
files: string[];

@ -31,4 +31,16 @@ export class AutocompleteService {
},
});
}
getAddressFromCoordinates(lat: number, lon: number): Observable<any> {
return this.http.get(this.apiUrl + '/reverse', {
params: {
lat: lat.toString(),
lon: lon.toString(),
format: 'json',
addressdetails: '1',
},
});
}
}

@ -3,13 +3,17 @@ import { Injectable } from '@angular/core';
import { switchMap } from 'rxjs';
import { environment } from '../../environments/environment';
import { AutocompleteService } from './auto-complete.service';
import { Pin } from '../model/Pin';
@Injectable({
providedIn: 'root',
})
export class PinService {
allPins: Pin[] = [];
filteredPins: Pin[] = [];
private apiURL = environment.apiURL;
private token = localStorage.getItem('auth_token');
constructor(
private http: HttpClient,
private autoCompleteService: AutocompleteService
@ -19,7 +23,7 @@ export class PinService {
const url = `${this.apiURL}/pins`;
const headers = new HttpHeaders({
'Content-Type': 'application/json',
Authorization: 'Bearer ' + this.token,
Authorization: 'Bearer ' + localStorage.getItem('auth_token'),
});
return this.http.get<any>(url, { headers });
}
@ -33,7 +37,7 @@ export class PinService {
const url = `${this.apiURL}/pin/add`;
const headers = new HttpHeaders({
'Content-Type': 'application/json',
Authorization: 'Bearer ' + this.token,
Authorization: 'Bearer ' + localStorage.getItem('auth_token'),
});
return this.autoCompleteService.getAdressCoordinates(pin.location).pipe(
switchMap((response: any) => {

Loading…
Cancel
Save