diff --git a/angular.json b/angular.json index 1765036..766c605 100644 --- a/angular.json +++ b/angular.json @@ -3,7 +3,7 @@ "version": 1, "newProjectRoot": "projects", "projects": { - "frontv2": { + "front": { "projectType": "application", "schematics": {}, "root": "", @@ -13,7 +13,7 @@ "build": { "builder": "@angular-devkit/build-angular:application", "options": { - "outputPath": "dist/frontv2", + "outputPath": "dist/front", "index": "src/index.html", "browser": "src/main.ts", "polyfills": ["zone.js"], @@ -64,10 +64,10 @@ "builder": "@angular-devkit/build-angular:dev-server", "configurations": { "production": { - "buildTarget": "frontv2:build:production" + "buildTarget": "front:build:production" }, "development": { - "buildTarget": "frontv2:build:development" + "buildTarget": "front:build:development" } }, "defaultConfiguration": "development" diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index a287845..3f7dfb1 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -14,16 +14,16 @@ describe('AppComponent', () => { expect(app).toBeTruthy(); }); - it(`should have the 'frontv2' title`, () => { + it(`should have the 'front' title`, () => { const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; - expect(app.title).toEqual('frontv2'); + expect(app.title).toEqual('front'); }); it('should render title', () => { const fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); const compiled = fixture.nativeElement as HTMLElement; - expect(compiled.querySelector('h1')?.textContent).toContain('Hello, frontv2'); + expect(compiled.querySelector('h1')?.textContent).toContain('Hello, front'); }); }); diff --git a/src/app/auth.guard.spec.ts b/src/app/auth.guard.spec.ts index bbabc44..884e432 100644 --- a/src/app/auth.guard.spec.ts +++ b/src/app/auth.guard.spec.ts @@ -2,13 +2,13 @@ import { TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; import { AuthGuard } from './auth.guard'; import { LocalStorageService } from './services/local-storage/local-storage.service'; -import { LoginModalService } from './services/login-modal/login-modal.service'; +import { ModalService } from './services/modal/modal.service'; describe('AuthGuard', () => { let guard: AuthGuard; let localStorageServiceSpy: jasmine.SpyObj; let routerSpy: jasmine.SpyObj; - let loginModalServiceSpy: jasmine.SpyObj; + let loginModalServiceSpy: jasmine.SpyObj; beforeEach(() => { localStorageServiceSpy = jasmine.createSpyObj('LocalStorageService', [ @@ -24,7 +24,7 @@ describe('AuthGuard', () => { AuthGuard, { provide: LocalStorageService, useValue: localStorageServiceSpy }, { provide: Router, useValue: routerSpy }, - { provide: LoginModalService, useValue: loginModalServiceSpy }, + { provide: ModalService, useValue: loginModalServiceSpy }, ], }); diff --git a/src/app/auth.guard.ts b/src/app/auth.guard.ts index ae712d0..700f502 100644 --- a/src/app/auth.guard.ts +++ b/src/app/auth.guard.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { CanActivate, Router } from '@angular/router'; import { LocalStorageService } from './services/local-storage/local-storage.service'; -import { LoginModalService } from './services/login-modal/login-modal.service'; +import { ModalService } from './services/modal/modal.service'; @Injectable({ providedIn: 'root', @@ -10,7 +10,7 @@ export class AuthGuard implements CanActivate { constructor( private localStorageService: LocalStorageService, private router: Router, - private loginModalService: LoginModalService + private loginModalService: ModalService ) {} canActivate(): boolean { diff --git a/src/app/components/edit-pin-popup/edit-pin-popup.component.html b/src/app/components/edit-pin-popup/edit-pin-popup.component.html new file mode 100644 index 0000000..9d50c32 --- /dev/null +++ b/src/app/components/edit-pin-popup/edit-pin-popup.component.html @@ -0,0 +1,174 @@ + + + + +
+ + + diff --git a/src/app/components/edit-pin-popup/edit-pin-popup.component.spec.ts b/src/app/components/edit-pin-popup/edit-pin-popup.component.spec.ts new file mode 100644 index 0000000..dc64c00 --- /dev/null +++ b/src/app/components/edit-pin-popup/edit-pin-popup.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditPinPopupComponent } from './edit-pin-popup.component'; + +describe('EditPinPopupComponent', () => { + let component: EditPinPopupComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [EditPinPopupComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(EditPinPopupComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/edit-pin-popup/edit-pin-popup.component.ts b/src/app/components/edit-pin-popup/edit-pin-popup.component.ts new file mode 100644 index 0000000..418d855 --- /dev/null +++ b/src/app/components/edit-pin-popup/edit-pin-popup.component.ts @@ -0,0 +1,268 @@ +import { CommonModule } from '@angular/common'; +import { + AfterViewInit, + Component, + Input, + OnDestroy, + OnInit, +} from '@angular/core'; +import { + FormBuilder, + FormControl, + FormGroup, + ReactiveFormsModule, +} from '@angular/forms'; +import { NavigationEnd, Router } from '@angular/router'; +import { of, Subscription } from 'rxjs'; +import { + catchError, + debounceTime, + distinctUntilChanged, + filter, + switchMap, + take, +} from 'rxjs/operators'; +import { Pin } from '../../model/Pin'; +import { AutocompleteService } from '../../services/auto-complete/auto-complete.service'; +import { ExifService } from '../../services/exif/exif.service'; +import { ModalService } from '../../services/modal/modal.service'; +import { PinService } from '../../services/pin/pin.service'; +import { DragDropComponent } from '../drag-drop/drag-drop.component'; + +@Component({ + selector: 'app-edit-pin-popup', + standalone: true, + imports: [ReactiveFormsModule, CommonModule, DragDropComponent], + templateUrl: './edit-pin-popup.component.html', +}) +export class EditPinPopupComponent implements OnInit, AfterViewInit, OnDestroy { + @Input() isHomePage: boolean = false; + @Input() pin!: Pin; + + form!: FormGroup; + suggestions: any[] = []; + inputFocused: boolean = false; + files: any[] = []; + isPinModalOpen: boolean = false; + + private modalOpenSubscription!: Subscription; + private routerSubscription!: Subscription; + private locationSubscription!: Subscription; + + constructor( + private fb: FormBuilder, + private autocompleteService: AutocompleteService, + private pinService: PinService, + private exifService: ExifService, + private modalService: ModalService, + private router: Router + ) { + // Initialiser le formulaire avec des valeurs par défaut + this.form = this.fb.group({ + title: new FormControl(''), + description: new FormControl(''), + location: new FormControl(''), + files: new FormControl(null), + }); + } + + onFocus(): void { + this.inputFocused = true; + } + + onBlur(): void { + setTimeout(() => { + this.inputFocused = false; // Désactiver le focus après un petit délai pour permettre un clic sur la liste + }, 200); + } + + ngOnInit(): void { + // Initialiser le formulaire avec les valeurs de base + this.form.patchValue({ + title: this.pin?.title || 'vide', + description: this.pin?.description || '', + location: "Chargement de l'adresse...", + }); + + // Vérifier si nous avons des coordonnées valides dans pin.location + if ( + this.pin?.location && + Array.isArray(this.pin.location) && + this.pin.location.length >= 2 + ) { + const lat = this.pin.location[0]; + const lon = this.pin.location[1]; + + if (lat !== undefined && lon !== undefined) { + // Récupérer l'adresse à partir des coordonnées + this.locationSubscription = this.autocompleteService + .getAddressFromCoordinates(lat, lon) + .pipe(take(1)) + .subscribe( + (address) => { + console.log('Adresse récupérée:', address); + if (address && address.display_name) { + this.form.patchValue({ location: address.display_name }); + } else { + this.form.patchValue({ location: `${lat}, ${lon}` }); + } + }, + (error) => { + console.error( + "Erreur lors de la récupération de l'adresse:", + error + ); + this.form.patchValue({ location: `${lat}, ${lon}` }); + } + ); + } + } + + // S'abonner aux changements d'état du modal + this.modalOpenSubscription = this.modalService.showModal$.subscribe( + (state) => { + this.isPinModalOpen = state; + + // Lorsque le modal s'ouvre, s'assurer qu'il est dans le body + if (state) { + setTimeout(() => this.moveModalToBody(), 0); + } + } + ); + + // S'abonner aux événements de navigation du router + this.routerSubscription = this.router.events + .pipe(filter((event) => event instanceof NavigationEnd)) + .subscribe(() => { + // Attendre que le DOM soit mis à jour après la navigation + setTimeout(() => this.moveModalToBody(), 0); + }); + + // Configuration de l'autocomplétion pour le champ d'adresse + this.form + .get('location') + ?.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((query) => { + // Vérifier que query est une chaîne de caractères + if (typeof query !== 'string') { + return of([]); + } + + const trimmedQuery = query.trim(); + if (trimmedQuery.length > 2) { + return this.autocompleteService.getAddressSuggestions(trimmedQuery); + } + return of([]); + }), + catchError((error) => { + console.error('Error fetching suggestions:', error); + return of([]); + }) + ) + .subscribe((data) => { + this.suggestions = data; + }); + } + + ngAfterViewInit() { + // Appel initial pour déplacer le modal + this.moveModalToBody(); + } + + ngOnDestroy() { + // Nettoyage des abonnements pour éviter les fuites de mémoire + if (this.modalOpenSubscription) { + this.modalOpenSubscription.unsubscribe(); + } + + if (this.routerSubscription) { + this.routerSubscription.unsubscribe(); + } + + if (this.locationSubscription) { + this.locationSubscription.unsubscribe(); + } + } + + // Méthode dédiée pour déplacer le modal vers le body + private moveModalToBody(): void { + const modal = document.getElementById('pin-modal'); + if (modal && modal.parentElement !== document.body) { + document.body.appendChild(modal); + } + } + + selectSuggestion(suggestion: any): void { + const locationControl = this.form.get('location'); + if (locationControl instanceof FormControl) { + locationControl.setValue(suggestion.display_name); + } + this.suggestions = []; + } + + async onFilesReceived(files: FileList): Promise { + 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) { + try { + // Utiliser pipe(take(1)) pour s'assurer que l'observable se termine + const address = await this.autocompleteService + .getAddressFromCoordinates(data.latitude, data.longitude) + .pipe(take(1)) + .toPromise(); + + if (address) { + console.log("Données d'adresse :", JSON.stringify(address)); + this.form.get('location')?.setValue(address.display_name); + break; + } + } catch (addressError) { + console.error( + "Erreur lors de la récupération de l'adresse:", + addressError + ); + // Utiliser les coordonnées brutes en cas d'échec + this.form + .get('location') + ?.setValue(`${data.latitude}, ${data.longitude}`); + } + } + } catch (error) { + console.error('Erreur :', error); + } + } + } + + submitForm(): void { + if (this.form.valid) { + this.files = this.files.map((file) => { + return file.name; //TODO: Mettre le hash du fichier + }); + + const pinData = { + ...this.form.value, + files: this.files, + user_id: this.pin.user_id, + }; + + this.pinService.updatePin(this.pin.id, pinData).subscribe(() => { + this.closePinModal(); + }); + } else { + console.error('Le formulaire est invalide'); + } + } + + openPinModal() { + this.modalService.openModal(); + } + + closePinModal() { + this.modalService.closeModal(); + } +} diff --git a/src/app/components/home-page/home-page.component.ts b/src/app/components/home-page/home-page.component.ts index f5706a5..1e5ba94 100644 --- a/src/app/components/home-page/home-page.component.ts +++ b/src/app/components/home-page/home-page.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core'; -import { LoginModalService } from '../../services/login-modal/login-modal.service'; +import { ModalService } from '../../services/modal/modal.service'; @Component({ selector: 'app-home-page', @@ -8,7 +8,7 @@ import { LoginModalService } from '../../services/login-modal/login-modal.servic export class HomePageComponent { currentYear = new Date().getFullYear(); - constructor(private loginModalService: LoginModalService) {} + constructor(private loginModalService: ModalService) {} openLogin() { this.loginModalService.openModal(); diff --git a/src/app/components/leaflet-map/leaflet-map.component.ts b/src/app/components/leaflet-map/leaflet-map.component.ts index 10306f7..380f18a 100644 --- a/src/app/components/leaflet-map/leaflet-map.component.ts +++ b/src/app/components/leaflet-map/leaflet-map.component.ts @@ -79,9 +79,10 @@ export class LeafletMapComponent implements OnInit { this.viewContainerRef.createComponent(PinMarkerComponent); componentRef.instance.pin = pin; + componentRef.instance.marker = marker; popupDiv.appendChild(componentRef.location.nativeElement); - marker.bindPopup(popupDiv); + marker.bindPopup(popupDiv, { closeButton: false, minWidth: 150 }); // Stocker les marqueurs par ID this.markersMap[pin.id] = marker; @@ -94,7 +95,7 @@ export class LeafletMapComponent implements OnInit { this.markersMap[pinId].openPopup(); // Ouvrir la pop-up du marqueur correspondant const latlng = this.markersMap[pinId].getLatLng(); const zoom = this.map.getZoom(); - const offsetLat = 0.02 / Math.pow(2, zoom - 10); + const offsetLat = 0.05 / Math.pow(2, zoom - 10); this.map.setView(L.latLng(latlng.lat + offsetLat, latlng.lng), zoom); } }); diff --git a/src/app/components/login-page/login-page.component.ts b/src/app/components/login-page/login-page.component.ts index 7e22ba2..11ab803 100644 --- a/src/app/components/login-page/login-page.component.ts +++ b/src/app/components/login-page/login-page.component.ts @@ -11,8 +11,8 @@ import { Router } from '@angular/router'; import { Subscription } from 'rxjs'; import { User } from '../../model/User'; import { LocalStorageService } from '../../services/local-storage/local-storage.service'; -import { LoginModalService } from '../../services/login-modal/login-modal.service'; import { LoginService } from '../../services/login/login.service'; +import { ModalService } from '../../services/modal/modal.service'; @Component({ selector: 'app-login-page', @@ -31,7 +31,7 @@ export class LoginPageComponent { private loginService: LoginService, private fb: FormBuilder, private router: Router, - private loginModalService: LoginModalService, + private loginModalService: ModalService, private localStorageService: LocalStorageService ) { this.userForm = this.fb.group({ diff --git a/src/app/components/pin-marker/pin-marker.component.html b/src/app/components/pin-marker/pin-marker.component.html index 1f88f42..341ab9e 100644 --- a/src/app/components/pin-marker/pin-marker.component.html +++ b/src/app/components/pin-marker/pin-marker.component.html @@ -1,7 +1,56 @@ +
+ + + +
{{ pin.title }} -
@@ -76,6 +137,13 @@ - + + +
+ No images available +
+

diff --git a/src/app/components/pin-marker/pin-marker.component.ts b/src/app/components/pin-marker/pin-marker.component.ts index 8d10c16..cdb03d6 100644 --- a/src/app/components/pin-marker/pin-marker.component.ts +++ b/src/app/components/pin-marker/pin-marker.component.ts @@ -1,17 +1,34 @@ import { CommonModule } from '@angular/common'; import { Component, Input } from '@angular/core'; +import * as L from 'leaflet'; import { Pin } from '../../model/Pin'; +import { EditPinPopupComponent } from '../edit-pin-popup/edit-pin-popup.component'; @Component({ selector: 'app-pin-marker', templateUrl: './pin-marker.component.html', - imports: [CommonModule], + imports: [CommonModule, EditPinPopupComponent], }) export class PinMarkerComponent { @Input() pin!: Pin; + @Input() marker!: L.Marker; currentIndex: number = 0; + onClosePopup() { + this.marker.closePopup(); + } + + imageLoaded = false; + + onImageLoad() { + this.imageLoaded = true; + } + + onDelete() { + // TODO: Implémenter l'action de suppression + } + get formattedDescription(): string { return this.formatDescription(this.pin.description); } diff --git a/src/app/model/Pin.ts b/src/app/model/Pin.ts index bb4ce29..cc6a067 100644 --- a/src/app/model/Pin.ts +++ b/src/app/model/Pin.ts @@ -4,4 +4,5 @@ export interface Pin { title: string; files: string[]; description: string; + user_id: string; } diff --git a/src/app/services/login-modal/login-modal.service.spec.ts b/src/app/services/modal/modal.service.spec.ts similarity index 52% rename from src/app/services/login-modal/login-modal.service.spec.ts rename to src/app/services/modal/modal.service.spec.ts index 97fea24..0ad21ad 100644 --- a/src/app/services/login-modal/login-modal.service.spec.ts +++ b/src/app/services/modal/modal.service.spec.ts @@ -1,13 +1,13 @@ import { TestBed } from '@angular/core/testing'; -import { LoginModalService } from './login-modal.service'; +import { ModalService } from './modal.service'; -describe('LoginModalService', () => { - let service: LoginModalService; +describe('ModalService', () => { + let service: ModalService; beforeEach(() => { TestBed.configureTestingModule({}); - service = TestBed.inject(LoginModalService); + service = TestBed.inject(ModalService); }); it('should be created', () => { diff --git a/src/app/services/login-modal/login-modal.service.ts b/src/app/services/modal/modal.service.ts similarity index 91% rename from src/app/services/login-modal/login-modal.service.ts rename to src/app/services/modal/modal.service.ts index 6c63e3c..574b7f5 100644 --- a/src/app/services/login-modal/login-modal.service.ts +++ b/src/app/services/modal/modal.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; @Injectable({ providedIn: 'root' }) -export class LoginModalService { +export class ModalService { private showModalSubject = new BehaviorSubject(false); showModal$ = this.showModalSubject.asObservable(); diff --git a/src/app/services/pin/pin.service.ts b/src/app/services/pin/pin.service.ts index c1ed0dd..35c862b 100644 --- a/src/app/services/pin/pin.service.ts +++ b/src/app/services/pin/pin.service.ts @@ -56,4 +56,39 @@ export class PinService { }) ); } + + updatePin( + id: string, + pin: { + title: string; + description: string; + location: string; + files: any[]; + user_id: string; + } + ) { + const url = `${this.apiURL}/pin/${id}`; + const headers = new HttpHeaders({ + 'Content-Type': 'application/json', + Authorization: 'Bearer ' + localStorage.getItem('auth_token'), + }); + + // Obtenir les coordonnées GPS à partir de l'adresse + return this.autoCompleteService.getAdressCoordinates(pin.location).pipe( + switchMap((response: any) => { + const coords: [string, string] = [response[0].lat, response[0].lon]; + return this.http.patch( + url, + { + title: pin.title, + description: pin.description, + location: coords, + files: pin.files, + user_id: pin.user_id, + }, + { headers } + ); + }) + ); + } }