Pin update added

pull/26/head
Alexis Feron 2 months ago
parent a5f4ab4e99
commit 144af2e06f

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

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

@ -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<LocalStorageService>;
let routerSpy: jasmine.SpyObj<Router>;
let loginModalServiceSpy: jasmine.SpyObj<LoginModalService>;
let loginModalServiceSpy: jasmine.SpyObj<ModalService>;
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 },
],
});

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

@ -0,0 +1,174 @@
<!-- Modal toggle -->
<button
class="p-1 text-blue-500 rounded hover:bg-blue-100 focus:outline-none flex items-center"
aria-label="Edit"
(click)="openPinModal()"
>
<svg
class="w-[1.125rem] h-[1.125rem] text-gray-800"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m14.304 4.844 2.852 2.852M7 7H4a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h11a1 1 0 0 0 1-1v-4.5m2.409-9.91a2.017 2.017 0 0 1 0 2.853l-6.844 6.844L8 14l.713-3.565 6.844-6.844a2.015 2.015 0 0 1 2.852 0Z"
/>
</svg>
</button>
<!-- Fond assombri -->
<div
class="fixed inset-0 bg-gray-900 bg-opacity-50 w-full h-full transition-opacity duration-300 ease-in-out z-40"
[ngClass]="{
'opacity-0 pointer-events-none': !isPinModalOpen,
'opacity-100': isPinModalOpen
}"
(click)="closePinModal()"
></div>
<!-- Main modal -->
<div
id="pin-modal"
tabindex="-1"
aria-hidden="true"
[ngClass]="{
'opacity-0 scale-50 pointer-events-none': !isPinModalOpen,
'opacity-100 scale-100': isPinModalOpen
}"
class="fixed top-0 right-0 left-0 z-50 flex justify-center items-center w-full h-full transition-transform duration-300 ease-in-out"
>
<div class="relative p-4 w-full max-w-xl max-h-full">
<!-- Modal content -->
<div
class="relative bg-white rounded-lg shadow dark:bg-gray-700 transition-transform duration-300 ease-in-out"
>
<!-- Modal header -->
<div
class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600"
>
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">
Modifie ton souvenir
</h3>
<button
type="button"
(click)="closePinModal()"
class="end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
>
<svg
class="w-3 h-3"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 14 14"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"
/>
</svg>
<span class="sr-only">Fermer la modal</span>
</button>
</div>
<!-- Modal body -->
<div class="p-4 md:p-5">
<form class="grid gap-6 mb-1 md:grid-cols-2" [formGroup]="form">
<div>
<label
for="title"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>Titre</label
>
<input
type="text"
id="title"
formControlName="title"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
placeholder="Mont Saint-Michel"
required
/>
</div>
<div>
<label
for="localisation"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>Localisation</label
>
<input
type="text"
id="localisation"
formControlName="location"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white"
placeholder="Saisir la localisation"
(focus)="onFocus()"
(blur)="onBlur()"
/>
<ul
*ngIf="suggestions.length > 0 && inputFocused"
class="bg-white dark:bg-gray-700 border border-gray-300 mt-2 rounded shadow absolute z-10 mr-5 max-h-60 overflow-auto"
>
<li
*ngFor="let suggestion of suggestions"
(click)="selectSuggestion(suggestion)"
class="p-2 block mb-2 text-sm font-medium text-gray-900 dark:text-white hover:bg-gray-500 cursor-pointer"
>
{{ suggestion.display_name }}
</li>
</ul>
</div>
<div>
<app-drag-drop
(filesSelected)="onFilesReceived($event)"
></app-drag-drop>
</div>
<div>
<label
for="description"
class="block mb-2 text-sm font-medium text-gray-900 dark:text-white"
>Description</label
>
<textarea
id="description"
rows="4"
formControlName="description"
class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 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="Décrit ton souvenir..."
></textarea>
</div>
<div class="flex justify-between">
<button
type="reset"
(click)="closePinModal()"
class="w-full text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800"
>
Annuler
</button>
</div>
<div class="flex justify-between">
<button
type="submit"
(click)="submitForm()"
class="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Valider
</button>
</div>
</form>
</div>
</div>
</div>
</div>

@ -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<EditPinPopupComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [EditPinPopupComponent],
}).compileComponents();
fixture = TestBed.createComponent(EditPinPopupComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -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<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) {
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();
}
}

@ -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();

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

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

@ -1,7 +1,56 @@
<div class="flex mb-2 justify-end items-center">
<app-edit-pin-popup [pin]="pin"></app-edit-pin-popup>
<button
class="p-1 text-red-500 rounded hover:bg-red-100 focus:outline-none ml-2 flex items-center"
aria-label="Delete"
(click)="onDelete()"
>
<svg
class="w-[1.125rem] h-[1.125rem] text-gray-800"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 7h14m-9 3v8m4-8v8M10 3h4a1 1 0 0 1 1 1v3H9V4a1 1 0 0 1 1-1ZM6 7h12v13a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7Z"
/>
</svg>
</button>
<button
class="p-1 text-gray-500 rounded hover:bg-gray-200 focus:outline-none ml-2 flex items-center"
aria-label="Close"
(click)="onClosePopup()"
>
<svg
class="w-[1.125rem] h-[1.125rem] text-gray-800"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18 17.94 6M18 18 6.06 6"
/>
</svg>
</button>
</div>
<div class="text-center">
<strong>{{ pin.title }}</strong>
<div
*ngIf="pin.files.length > 0"
<ng-container
*ngIf="pin.files.length > 0; else noImagesPlaceholder"
class="relative carousel overflow-hidden"
>
<!-- Carousel wrapper -->
@ -15,11 +64,23 @@
(index === currentIndex ? ' opacity-100' : ' opacity-0')
"
>
<img
[src]="image"
[alt]="pin.title"
class="object-contain max-h-full max-w-full h-full w-auto mx-auto"
/>
<div
class="relative h-40 overflow-hidden rounded-lg flex items-center justify-center"
>
<img
[src]="image"
(load)="onImageLoad()"
[hidden]="!imageLoaded"
class="object-contain max-h-full max-w-full h-full w-auto mx-auto"
/>
<div
*ngIf="!imageLoaded"
class="relative inset-0 bg-gray-200 w-52 h-40 mt-2 overflow-hidden rounded-lg flex items-center justify-center"
>
<!-- animate-pulse can be use -->
<span class="text-gray-500">Image not working</span>
</div>
</div>
</div>
</div>
@ -76,6 +137,13 @@
</span>
</button>
</div>
</div>
</ng-container>
<ng-template #noImagesPlaceholder>
<div
class="relative h-40 mt-2 overflow-hidden rounded-lg flex items-center justify-center bg-gray-200"
>
<span class="text-gray-500">No images available</span>
</div>
</ng-template>
<p [innerHTML]="formattedDescription"></p>
</div>

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

@ -4,4 +4,5 @@ export interface Pin {
title: string;
files: string[];
description: string;
user_id: string;
}

@ -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', () => {

@ -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<boolean>(false);
showModal$ = this.showModalSubject.asObservable();

@ -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<any>(
url,
{
title: pin.title,
description: pin.description,
location: coords,
files: pin.files,
user_id: pin.user_id,
},
{ headers }
);
})
);
}
}

Loading…
Cancel
Save