feat: #3 integration map dans le profile
This commit is contained in:
@@ -30,9 +30,15 @@
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
"src/assets",
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/leaflet/dist/images",
|
||||
"output": "assets/leaflet"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"node_modules/leaflet/dist/leaflet.css",
|
||||
"node_modules/primeng/resources/themes/lara-light-blue/theme.css",
|
||||
"node_modules/primeng/resources/primeng.min.css",
|
||||
"node_modules/primeicons/primeicons.css",
|
||||
|
||||
29
package-lock.json
generated
29
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "trouve-ton-profile",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "trouve-ton-profile",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.0.0",
|
||||
"@angular/common": "^17.0.0",
|
||||
@@ -25,6 +25,7 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@ngneat/until-destroy": "^10.0.0",
|
||||
"express": "^4.18.2",
|
||||
"leaflet": "^1.9.4",
|
||||
"ng2-pdf-viewer": "^10.3.3",
|
||||
"ngx-toastr": "^17.0.2",
|
||||
"pocketbase": "^0.21.5",
|
||||
@@ -44,6 +45,7 @@
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@types/node": "^18.18.0",
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"angular-eslint": "20.4.0",
|
||||
@@ -6384,6 +6386,13 @@
|
||||
"@types/send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/graceful-fs": {
|
||||
"version": "4.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz",
|
||||
@@ -6475,6 +6484,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/leaflet": {
|
||||
"version": "1.9.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.21.tgz",
|
||||
"integrity": "sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||
@@ -14478,6 +14497,12 @@
|
||||
"shell-quote": "^1.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/leaflet": {
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/less": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "trouve-ton-profile",
|
||||
"version": "1.1.0",
|
||||
"version": "1.3.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "bash replace-prod-env.sh src/environments/environment.development.ts $ENV_URL && ng serve",
|
||||
@@ -14,7 +14,8 @@
|
||||
"format": "bash replace-prod-env.sh src/environments/environment.development.ts http://localhost:8090 && npm run prettier && npm run lint:fix",
|
||||
"lint": "ng lint",
|
||||
"lint:fix": "ng lint --fix",
|
||||
"clean:imports": "ts-unused-exports tsconfig.json --excludePathsFromReport=\"src/main.ts;src/environments;server.ts\" && npm run lint:fix","fix:all": "npm run format && npm run tsc",
|
||||
"clean:imports": "ts-unused-exports tsconfig.json --excludePathsFromReport=\"src/main.ts;src/environments;server.ts\" && npm run lint:fix",
|
||||
"fix:all": "npm run format && npm run tsc",
|
||||
"check:all": "npm run format && npm run tsc && npm run lint && npm run test",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
@@ -41,6 +42,7 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||
"@ngneat/until-destroy": "^10.0.0",
|
||||
"express": "^4.18.2",
|
||||
"leaflet": "^1.9.4",
|
||||
"ng2-pdf-viewer": "^10.3.3",
|
||||
"ngx-toastr": "^17.0.2",
|
||||
"pocketbase": "^0.21.5",
|
||||
@@ -60,6 +62,7 @@
|
||||
"@tailwindcss/line-clamp": "^0.4.4",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/leaflet": "^1.9.21",
|
||||
"@types/node": "^18.18.0",
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"angular-eslint": "20.4.0",
|
||||
|
||||
21
src/app/domain/localisation/coordinates.model.ts
Normal file
21
src/app/domain/localisation/coordinates.model.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export interface Coordinates {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}
|
||||
|
||||
export class CoordinatesValidator {
|
||||
static isValid(coords: Coordinates | null | undefined): boolean {
|
||||
if (!coords) return false;
|
||||
|
||||
return (
|
||||
coords.latitude >= -90 &&
|
||||
coords.latitude <= 90 &&
|
||||
coords.longitude >= -180 &&
|
||||
coords.longitude <= 180
|
||||
);
|
||||
}
|
||||
|
||||
static format(coords: Coordinates): string {
|
||||
return `${coords.latitude.toFixed(6)}, ${coords.longitude.toFixed(6)}`;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export interface Profile {
|
||||
cv: string;
|
||||
projets: string[];
|
||||
apropos: string;
|
||||
coordonnees?: { lat: number; lon: number };
|
||||
}
|
||||
|
||||
export interface ProfilePaginated {
|
||||
|
||||
@@ -8,4 +8,5 @@ export interface ProfileRepository {
|
||||
getByUserId(userId: string): Observable<Profile>;
|
||||
create(profile: Profile): Observable<Profile>;
|
||||
update(profileId: string, profile: Partial<Profile>): Observable<Profile>;
|
||||
updateCoordinates(profileId: string, latitude: number, longitude: number): Observable<Profile>;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,13 @@ export class PbProfileRepository implements ProfileRepository {
|
||||
return from(this.pb.collection('profiles').update<Profile>(id, data));
|
||||
}
|
||||
|
||||
updateCoordinates(profileId: string, latitude: number, longitude: number): Observable<Profile> {
|
||||
const coordinates = { lat: latitude, lon: longitude };
|
||||
return from(
|
||||
this.pb.collection('profiles').update<Profile>(profileId, { coordonnees: coordinates })
|
||||
);
|
||||
}
|
||||
|
||||
private onFilterSetting(params?: SearchFilters): string[] {
|
||||
const filters: string[] = [
|
||||
'utilisateur.verified = true',
|
||||
|
||||
@@ -324,6 +324,33 @@
|
||||
<ng-template #menu_nav>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-4 mb-6">
|
||||
<nav class="flex flex-wrap gap-2">
|
||||
<button
|
||||
class="tab-button flex items-center gap-2 px-4 py-2.5 rounded-lg font-medium text-sm transition-all duration-200"
|
||||
[class.active-tab]="menu() === 'location'"
|
||||
(click)="menu.set('location')"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z"
|
||||
/>
|
||||
</svg>
|
||||
Localisation
|
||||
</button>
|
||||
|
||||
<button
|
||||
(click)="menu.set('settings')"
|
||||
[class.active-tab]="menu() === 'settings'"
|
||||
@@ -472,6 +499,9 @@
|
||||
<ng-template #menu_content>
|
||||
<div class="tab-content">
|
||||
@switch (menu().toLowerCase()) {
|
||||
@case ('location') {
|
||||
<app-my-profile-map [profile]="profile()" />
|
||||
}
|
||||
@case ('settings') {
|
||||
<app-settings />
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { ProfileFacade } from '@app/ui/profiles/profile.facade';
|
||||
import { UserFacade } from '@app/ui/users/user.facade';
|
||||
import { LoadingComponent } from '@app/shared/components/loading/loading.component';
|
||||
import { SettingsComponent } from '@app/shared/features/settings/settings.component';
|
||||
import { MyProfileMapComponent } from '@app/shared/features/my-profile-map/my-profile-map.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-profile',
|
||||
@@ -30,6 +31,7 @@ import { SettingsComponent } from '@app/shared/features/settings/settings.compon
|
||||
LoadingComponent,
|
||||
SettingsComponent,
|
||||
NgTemplateOutlet,
|
||||
MyProfileMapComponent,
|
||||
],
|
||||
providers: [UserFacade],
|
||||
templateUrl: './my-profile.component.html',
|
||||
@@ -38,7 +40,7 @@ import { SettingsComponent } from '@app/shared/features/settings/settings.compon
|
||||
@UntilDestroy()
|
||||
export class MyProfileComponent implements OnInit {
|
||||
protected readonly environment = environment;
|
||||
protected menu = signal<string>('settings');
|
||||
protected menu = signal<string>('location');
|
||||
|
||||
protected location = inject(Location);
|
||||
protected readonly route = inject(ActivatedRoute);
|
||||
|
||||
7
src/app/shared/components/map/map.component.html
Normal file
7
src/app/shared/components/map/map.component.html
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="map-container" [style.height]="height">
|
||||
<div id="map" class="map"></div>
|
||||
|
||||
@if (!mapReady()) {
|
||||
<app-loading message="Chargement de la carte..." />
|
||||
}
|
||||
</div>
|
||||
76
src/app/shared/components/map/map.component.scss
Normal file
76
src/app/shared/components/map/map.component.scss
Normal file
@@ -0,0 +1,76 @@
|
||||
// src/app/ui/shared/components/map/map.component.scss
|
||||
|
||||
.map-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
|
||||
.map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.map-loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
z-index: 1000;
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f4f6;
|
||||
border-top-color: #6366f1;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 16px;
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Styles globaux pour les popups Leaflet
|
||||
:host ::ng-deep {
|
||||
.leaflet-popup-content {
|
||||
.map-popup {
|
||||
strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
color: #111827;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.leaflet-container {
|
||||
font-family: inherit;
|
||||
}
|
||||
}
|
||||
22
src/app/shared/components/map/map.component.spec.ts
Normal file
22
src/app/shared/components/map/map.component.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MapComponent } from './map.component';
|
||||
|
||||
describe('MapComponent', () => {
|
||||
let component: MapComponent;
|
||||
let fixture: ComponentFixture<MapComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MapComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MapComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
221
src/app/shared/components/map/map.component.ts
Normal file
221
src/app/shared/components/map/map.component.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
effect,
|
||||
EventEmitter,
|
||||
input,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { Coordinates } from '@app/domain/localisation/coordinates.model';
|
||||
import * as L from 'leaflet';
|
||||
import { LoadingComponent } from '@app/shared/components/loading/loading.component';
|
||||
|
||||
export interface MapMarker {
|
||||
id: string;
|
||||
coordinates: Coordinates;
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-map',
|
||||
standalone: true,
|
||||
imports: [LoadingComponent],
|
||||
templateUrl: './map.component.html',
|
||||
styleUrl: './map.component.scss',
|
||||
})
|
||||
export class MapComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
@Input() center: Coordinates = { latitude: 48.8566, longitude: 2.3522 }; // Paris par défaut
|
||||
@Input() zoom: number = 13;
|
||||
@Input() interactive: boolean = true;
|
||||
@Input() showUserLocation: boolean = false;
|
||||
@Input() height: string = '500px';
|
||||
|
||||
@Output() onLocationSelected = new EventEmitter<Coordinates>();
|
||||
@Output() onMarkerClick = new EventEmitter<MapMarker>();
|
||||
|
||||
markers = input<MapMarker[]>([]);
|
||||
|
||||
private map: L.Map | null = null;
|
||||
private markersLayer: L.LayerGroup | null = null;
|
||||
private userMarker: L.Marker | null = null;
|
||||
|
||||
protected mapReady = signal<boolean>(false);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const isReady = this.mapReady();
|
||||
if (isReady && this.map) {
|
||||
this.updateMarkers();
|
||||
}
|
||||
});
|
||||
|
||||
this.fixLeafletIconPath();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// Fix pour les icônes Leaflet
|
||||
this.fixLeafletIconPath();
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
setTimeout(() => this.initMap(), 100);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.map) {
|
||||
this.map.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private initMap(): void {
|
||||
// Initialiser la carte
|
||||
this.map = L.map('map', {
|
||||
center: [this.center.latitude, this.center.longitude],
|
||||
zoom: this.zoom,
|
||||
zoomControl: true,
|
||||
dragging: this.interactive,
|
||||
scrollWheelZoom: this.interactive,
|
||||
});
|
||||
|
||||
// Ajouter le layer de tuiles
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© OpenStreetMap contributors',
|
||||
maxZoom: 19,
|
||||
}).addTo(this.map);
|
||||
|
||||
// Créer le layer pour les markers
|
||||
this.markersLayer = L.layerGroup().addTo(this.map);
|
||||
|
||||
// Ajouter l'événement de clic sur la carte si interactive
|
||||
if (this.interactive) {
|
||||
this.map.on('click', (e: L.LeafletMouseEvent) => {
|
||||
this.onMapClick(e);
|
||||
});
|
||||
}
|
||||
|
||||
this.mapReady.set(true);
|
||||
this.updateMarkers();
|
||||
}
|
||||
|
||||
private onMapClick(event: L.LeafletMouseEvent): void {
|
||||
const coords: Coordinates = {
|
||||
latitude: event.latlng.lat,
|
||||
longitude: event.latlng.lng,
|
||||
};
|
||||
|
||||
this.onLocationSelected.emit(coords);
|
||||
|
||||
// Ajouter un marker temporaire à la position cliquée
|
||||
if (this.showUserLocation) {
|
||||
this.setUserLocation(coords);
|
||||
}
|
||||
}
|
||||
|
||||
private updateMarkers(): void {
|
||||
if (!this.markersLayer) return;
|
||||
|
||||
// Nettoyer les markers existants
|
||||
this.markersLayer.clearLayers();
|
||||
|
||||
// Ajouter les nouveaux markers
|
||||
this.markers().forEach((marker) => {
|
||||
const leafletMarker = L.marker([marker.coordinates.latitude, marker.coordinates.longitude], {
|
||||
icon: this.createCustomIcon(marker.icon),
|
||||
});
|
||||
|
||||
if (marker.title || marker.description) {
|
||||
leafletMarker.bindPopup(
|
||||
`<div class="map-popup">
|
||||
${marker.title ? `<strong>${marker.title}</strong>` : ''}
|
||||
${marker.description ? `<p>${marker.description}</p>` : ''}
|
||||
</div>`
|
||||
);
|
||||
}
|
||||
|
||||
leafletMarker.on('click', () => {
|
||||
this.onMarkerClick.emit(marker);
|
||||
});
|
||||
|
||||
this.markersLayer!.addLayer(leafletMarker);
|
||||
});
|
||||
|
||||
// Ajuster la vue pour montrer tous les markers
|
||||
if (this.markers().length > 0) {
|
||||
const bounds = L.latLngBounds(
|
||||
this.markers().map((m) => [m.coordinates.latitude, m.coordinates.longitude])
|
||||
);
|
||||
this.map?.fitBounds(bounds, { padding: [50, 50] });
|
||||
}
|
||||
}
|
||||
|
||||
setUserLocation(coordinates: Coordinates): void {
|
||||
if (!this.map) return;
|
||||
|
||||
// Supprimer l'ancien marker utilisateur
|
||||
if (this.userMarker) {
|
||||
this.userMarker.remove();
|
||||
}
|
||||
|
||||
// Créer un nouveau marker utilisateur
|
||||
this.userMarker = L.marker([coordinates.latitude, coordinates.longitude], {
|
||||
icon: this.createUserIcon(),
|
||||
}).addTo(this.map);
|
||||
|
||||
this.userMarker.bindPopup('Votre position').openPopup();
|
||||
|
||||
// Centrer la carte sur la position
|
||||
this.map.setView([coordinates.latitude, coordinates.longitude], this.zoom);
|
||||
}
|
||||
|
||||
recenterMap(): void {
|
||||
if (this.map) {
|
||||
this.map.setView([this.center.latitude, this.center.longitude], this.zoom);
|
||||
}
|
||||
}
|
||||
|
||||
private createCustomIcon(iconUrl?: string): L.Icon {
|
||||
return L.icon({
|
||||
iconUrl: iconUrl || 'assets/leaflet/marker-icon.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowUrl: 'assets/leaflet/marker-shadow.png',
|
||||
shadowSize: [41, 41],
|
||||
});
|
||||
}
|
||||
|
||||
private createUserIcon(): L.Icon {
|
||||
return L.icon({
|
||||
iconUrl: 'assets/leaflet/marker-icon.png',
|
||||
iconRetinaUrl: 'assets/leaflet/marker-icon-2x.png',
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
tooltipAnchor: [16, -28],
|
||||
shadowSize: [41, 41],
|
||||
});
|
||||
}
|
||||
|
||||
private fixLeafletIconPath(): void {
|
||||
const iconRetinaUrl = 'assets/leaflet/marker-icon-2x.png';
|
||||
const iconUrl = 'assets/leaflet/marker-icon.png';
|
||||
const shadowUrl = 'assets/leaflet/marker-shadow.png';
|
||||
const iconDefault = L.icon({
|
||||
iconRetinaUrl,
|
||||
iconUrl,
|
||||
shadowUrl,
|
||||
iconSize: [25, 41],
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
tooltipAnchor: [16, -28],
|
||||
shadowSize: [41, 41],
|
||||
});
|
||||
L.Marker.prototype.options.icon = iconDefault;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<div class="my-profile-map-container">
|
||||
<!-- En-tête -->
|
||||
<div class="header">
|
||||
<div>
|
||||
<h3 class="title">Ma localisation</h3>
|
||||
<p class="subtitle">Partagez votre position pour être visible sur la carte des profils</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Coordonnées actuelles -->
|
||||
<div class="coordinates-display">
|
||||
<div class="coordinates-icon">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="coordinates-label">Coordonnées</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages d'erreur ou de chargement -->
|
||||
|
||||
<!-- Boutons d'action -->
|
||||
<div class="actions">
|
||||
<button type="button" (click)="onGetCurrentLocation()" class="btn btn-secondary">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z"
|
||||
/>
|
||||
</svg>
|
||||
Me géolocaliser
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Carte interactive -->
|
||||
<div class="map-wrapper">
|
||||
<app-map
|
||||
[center]="locationState().coordinates || { latitude: 48.8566, longitude: 2.3522 }"
|
||||
[markers]="markers()"
|
||||
[interactive]="true"
|
||||
[showUserLocation]="true"
|
||||
[height]="'400px'"
|
||||
(onLocationSelected)="onLocationSelected($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Boutons de sauvegarde -->
|
||||
<div class="save-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
(click)="onSaveLocation()"
|
||||
[disabled]="!locationState().coordinates"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
Enregistrer la position
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Message d'information -->
|
||||
<div class="info-message">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
|
||||
/>
|
||||
</svg>
|
||||
<p>
|
||||
Vous pouvez cliquer sur la carte pour définir manuellement votre position ou utiliser le
|
||||
bouton "Me géolocaliser" pour détecter automatiquement votre position.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,293 @@
|
||||
// src/app/ui/shared/components/my-profile-map/my-profile-map.component.scss
|
||||
|
||||
.my-profile-map-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 1px 3px 0 rgba(0, 0, 0, 0.1),
|
||||
0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #6b7280;
|
||||
margin: 4px 0 0 0;
|
||||
}
|
||||
|
||||
.visibility-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.toggle-label {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
cursor: pointer;
|
||||
|
||||
.toggle-input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
|
||||
&:checked + .toggle-slider {
|
||||
background-color: #6366f1;
|
||||
|
||||
&::before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #d1d5db;
|
||||
border-radius: 24px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 3px;
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-text {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.coordinates-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #f9fafb;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
|
||||
.coordinates-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #eef2ff;
|
||||
border-radius: 10px;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.coordinates-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #6b7280;
|
||||
margin: 0 0 4px 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.coordinates-value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
font-family: monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.alert {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&.alert-error {
|
||||
background: #fef2f2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
&.alert-info {
|
||||
background: #eff6ff;
|
||||
color: #1e40af;
|
||||
border: 1px solid #bfdbfe;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.map-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.save-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.info-message {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: #f0f9ff;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #bae6fd;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
color: #0284c7;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #0c4a6e;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Boutons
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.btn-primary {
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #4f46e5;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-danger {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1px solid #fecaca;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #fee2e2;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-outline {
|
||||
background: transparent;
|
||||
color: #6b7280;
|
||||
border: 1px solid #d1d5db;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #f9fafb;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.spinner-small {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-top-color: #6366f1;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@media (max-width: 768px) {
|
||||
.my-profile-map-container {
|
||||
padding: 16px;
|
||||
|
||||
.header {
|
||||
flex-direction: column;
|
||||
|
||||
.visibility-toggle {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-direction: column;
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MyProfileMapComponent } from './my-profile-map.component';
|
||||
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
||||
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
||||
import { mockProfileRepo } from '@app/testing/profile.mock';
|
||||
|
||||
describe('MyProfileMapComponent', () => {
|
||||
let component: MyProfileMapComponent;
|
||||
let fixture: ComponentFixture<MyProfileMapComponent>;
|
||||
|
||||
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>> = mockProfileRepo;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [MyProfileMapComponent],
|
||||
providers: [{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository }],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(MyProfileMapComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { Component, effect, inject, input, OnInit, signal } from '@angular/core';
|
||||
import { MapComponent, MapMarker } from '@app/shared/components/map/map.component';
|
||||
import { Coordinates, CoordinatesValidator } from '@app/domain/localisation/coordinates.model';
|
||||
import { LocationFacade } from '@app/ui/location/location.facade';
|
||||
import { ProfileFacade } from '@app/ui/profiles/profile.facade';
|
||||
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-my-profile-map',
|
||||
standalone: true,
|
||||
imports: [MapComponent],
|
||||
providers: [LocationFacade],
|
||||
templateUrl: './my-profile-map.component.html',
|
||||
styleUrl: './my-profile-map.component.scss',
|
||||
})
|
||||
export class MyProfileMapComponent implements OnInit {
|
||||
readonly profile = input<ProfileViewModel>();
|
||||
private readonly locationFacade = inject(LocationFacade);
|
||||
private readonly profileFacade = inject(ProfileFacade);
|
||||
|
||||
protected readonly locationState = this.locationFacade.locationState;
|
||||
protected readonly markers = signal<MapMarker[]>([]);
|
||||
|
||||
private initializedFromProfile = false;
|
||||
|
||||
constructor() {
|
||||
effect(
|
||||
() => {
|
||||
const profile = this.profile();
|
||||
const state = this.locationState();
|
||||
|
||||
// Initialiser une seule fois quand profile est disponible
|
||||
if (!this.initializedFromProfile && profile?.coordonnees) {
|
||||
this.initializedFromProfile = true;
|
||||
this.locationState.set({
|
||||
...state,
|
||||
coordinates: profile.coordonnees,
|
||||
});
|
||||
}
|
||||
|
||||
// Mettre à jour les markers quand les coordonnées changent
|
||||
if (state.coordinates) {
|
||||
this.updateMarkers(state.coordinates);
|
||||
}
|
||||
},
|
||||
{ allowSignalWrites: true }
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
//this.locationState.set({...this.locationState(), coordinates: this.profile()!.coordonnees!})
|
||||
}
|
||||
|
||||
onSaveLocation() {
|
||||
const currentLocation = this.locationState().coordinates;
|
||||
if (currentLocation === null) return;
|
||||
this.profileFacade.updateCoordinate(
|
||||
this.profile()!.id!,
|
||||
currentLocation.latitude,
|
||||
currentLocation.longitude
|
||||
);
|
||||
}
|
||||
|
||||
onLocationSelected(coordinates: Coordinates): void {
|
||||
this.locationState.set({ ...this.locationState(), coordinates: coordinates });
|
||||
}
|
||||
|
||||
onGetCurrentLocation(): void {
|
||||
this.locationFacade.getCurrentLocation();
|
||||
}
|
||||
|
||||
private updateMarkers(coordinates: Coordinates): void {
|
||||
const marker: MapMarker[] = [
|
||||
{
|
||||
id: 'user-location',
|
||||
coordinates: coordinates,
|
||||
title: 'Ma position',
|
||||
description: CoordinatesValidator.format(coordinates),
|
||||
},
|
||||
];
|
||||
this.markers.set(marker);
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,10 @@ export class FakeProfileRepository implements ProfileRepository {
|
||||
const existing = mockProfiles.find((p) => p.id === id) ?? mockProfiles[0];
|
||||
return of({ ...existing, ...data });
|
||||
}
|
||||
|
||||
updateCoordinates(profileId: string, latitude: number, longitude: number): Observable<Profile> {
|
||||
const existing = mockProfiles.find((p) => p.id === profileId) ?? mockProfiles[0];
|
||||
const coordinate = { latitude, longitude };
|
||||
return of({ ...existing, coordinate });
|
||||
}
|
||||
}
|
||||
|
||||
54
src/app/ui/location/location.facade.ts
Normal file
54
src/app/ui/location/location.facade.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { inject, Injectable, signal } from '@angular/core';
|
||||
import { GetCurrentLocationUseCase } from '@app/usecase/location/get-current-location.use-case';
|
||||
import { Coordinates } from '@app/domain/localisation/coordinates.model';
|
||||
|
||||
export interface LocationState {
|
||||
coordinates: Coordinates | null;
|
||||
isLocationEnabled: boolean;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class LocationFacade {
|
||||
private readonly getCurrentLocationUseCase = inject(GetCurrentLocationUseCase);
|
||||
|
||||
readonly locationState = signal<LocationState>({
|
||||
coordinates: null,
|
||||
isLocationEnabled: false,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
});
|
||||
|
||||
getCurrentLocation(): void {
|
||||
this.setLoading(true);
|
||||
this.clearError();
|
||||
|
||||
this.getCurrentLocationUseCase.execute().subscribe({
|
||||
next: (coordinates) => {
|
||||
this.updateState({ coordinates });
|
||||
this.setLoading(false);
|
||||
},
|
||||
error: (error) => {
|
||||
this.setError(error.message || 'Erreur lors de la récupération de la position');
|
||||
this.setLoading(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private updateState(partial: Partial<LocationState>): void {
|
||||
this.locationState.update((state) => ({ ...state, ...partial }));
|
||||
}
|
||||
|
||||
private setLoading(isLoading: boolean): void {
|
||||
this.updateState({ isLoading });
|
||||
}
|
||||
|
||||
private setError(error: string): void {
|
||||
this.updateState({ error });
|
||||
}
|
||||
|
||||
private clearError(): void {
|
||||
this.updateState({ error: null });
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { GetProfileUseCase } from '@app/usecase/profiles/get-profile.usecase';
|
||||
import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
|
||||
import { SearchFilters } from '@app/domain/search/search-filters';
|
||||
import { SearchService } from '@app/infrastructure/search/search.service';
|
||||
import { UpdateCoordinateProfileUseCase } from '@app/usecase/profiles/update-coordinate-profile.usecase';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -25,6 +26,7 @@ export class ProfileFacade {
|
||||
private listUseCase = new ListProfilesUseCase(this.profileRepository);
|
||||
private createUseCase = new CreateProfileUseCase(this.profileRepository);
|
||||
private updateUseCase = new UpdateProfileUseCase(this.profileRepository);
|
||||
private updateCoordinateUseCase = new UpdateCoordinateProfileUseCase(this.profileRepository);
|
||||
private getUseCase = new GetProfileUseCase(this.profileRepository);
|
||||
|
||||
readonly searchFilters = this.searchService.getFilters();
|
||||
@@ -122,6 +124,20 @@ export class ProfileFacade {
|
||||
});
|
||||
}
|
||||
|
||||
updateCoordinate(profileId: string, latitude: number, longitude: number) {
|
||||
this.handleError(ActionType.UPDATE, false, null, true);
|
||||
|
||||
this.updateCoordinateUseCase.execute(profileId, latitude, longitude).subscribe({
|
||||
next: (profile: Profile) => {
|
||||
this.profile.set(this.profilePresenter.toViewModel(profile));
|
||||
this.handleError(ActionType.UPDATE, false, null, false);
|
||||
},
|
||||
error: (err) => {
|
||||
this.handleError(ActionType.UPDATE, false, err, false);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private handleError(
|
||||
action: ActionType = ActionType.NONE,
|
||||
hasError: boolean,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Coordinates } from '@app/domain/localisation/coordinates.model';
|
||||
|
||||
export interface ProfileViewModel {
|
||||
id: string;
|
||||
fullName: string;
|
||||
@@ -15,4 +17,5 @@ export interface ProfileViewModel {
|
||||
projets: string[];
|
||||
isProfileVisible?: boolean;
|
||||
missingFields?: string[];
|
||||
coordonnees?: Coordinates;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ export class ProfilePresenter {
|
||||
projets: profile.projets,
|
||||
cv: profile.cv,
|
||||
bio: profile.bio,
|
||||
coordonnees: profile.coordonnees
|
||||
? { latitude: profile!.coordonnees!.lat!, longitude: profile!.coordonnees!.lon! }
|
||||
: undefined,
|
||||
isProfileVisible,
|
||||
missingFields,
|
||||
};
|
||||
|
||||
56
src/app/usecase/location/get-current-location.use-case.ts
Normal file
56
src/app/usecase/location/get-current-location.use-case.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { Coordinates } from '@app/domain/localisation/coordinates.model';
|
||||
import { from, Observable } from 'rxjs';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class GetCurrentLocationUseCase {
|
||||
execute(): Observable<Coordinates> {
|
||||
return from(
|
||||
new Promise<Coordinates>((resolve, reject) => {
|
||||
if (!navigator.geolocation) {
|
||||
reject({
|
||||
code: 0,
|
||||
message: "La géolocalisation n'est pas supportée par votre navigateur",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
resolve({
|
||||
latitude: position.coords.latitude,
|
||||
longitude: position.coords.longitude,
|
||||
});
|
||||
},
|
||||
(error) => {
|
||||
let message = 'Erreur de géolocalisation';
|
||||
|
||||
switch (error.code) {
|
||||
case error.PERMISSION_DENIED:
|
||||
message = 'Permission de géolocalisation refusée';
|
||||
break;
|
||||
case error.POSITION_UNAVAILABLE:
|
||||
message = 'Position indisponible';
|
||||
break;
|
||||
case error.TIMEOUT:
|
||||
message = 'Délai de géolocalisation dépassé';
|
||||
break;
|
||||
}
|
||||
|
||||
reject({
|
||||
code: error.code,
|
||||
message,
|
||||
});
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
timeout: 10000,
|
||||
maximumAge: 0,
|
||||
}
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
||||
import { Profile } from '@app/domain/profiles/profile.model';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export class UpdateCoordinateProfileUseCase {
|
||||
constructor(private readonly repo: ProfileRepository) {}
|
||||
|
||||
execute(profileId: string, latitude: number, longitude: number): Observable<Profile> {
|
||||
return this.repo.updateCoordinates(profileId, latitude, longitude);
|
||||
}
|
||||
}
|
||||
BIN
src/assets/leaflet/layers-2x.png
Normal file
BIN
src/assets/leaflet/layers-2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/leaflet/layers.png
Normal file
BIN
src/assets/leaflet/layers.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 696 B |
BIN
src/assets/leaflet/marker-icon-2x.png
Normal file
BIN
src/assets/leaflet/marker-icon-2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 KiB |
BIN
src/assets/leaflet/marker-icon.png
Normal file
BIN
src/assets/leaflet/marker-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
src/assets/leaflet/marker-shadow.png
Normal file
BIN
src/assets/leaflet/marker-shadow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 618 B |
Reference in New Issue
Block a user