parent
a5f4ab4e99
commit
144af2e06f
@ -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,13 +1,13 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
import { LoginModalService } from './login-modal.service';
|
import { ModalService } from './modal.service';
|
||||||
|
|
||||||
describe('LoginModalService', () => {
|
describe('ModalService', () => {
|
||||||
let service: LoginModalService;
|
let service: ModalService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({});
|
TestBed.configureTestingModule({});
|
||||||
service = TestBed.inject(LoginModalService);
|
service = TestBed.inject(ModalService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
Loading…
Reference in new issue