feat : #3 card de redirection profil
This commit is contained in:
@@ -122,7 +122,11 @@
|
||||
d="M9 6a3 3 0 11-6 0 3 3 0 016 0zM17 6a3 3 0 11-6 0 3 3 0 016 0zM12.93 17c.046-.327.07-.66.07-1a6.97 6.97 0 00-1.5-4.33A5 5 0 0119 16v1h-6.07zM6 11a5 5 0 015 5v1H1v-1a5 5 0 015-5z"
|
||||
/>
|
||||
</svg>
|
||||
<span>{{ profilePaginated().items.length }} profil(s)</span>
|
||||
@if (profilePaginated().items) {
|
||||
<span>{{ profilePaginated().items.length }} profil(s)</span>
|
||||
} @else {
|
||||
<span> 0 profil(s)</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { Coordinates } from '@app/domain/localisation/coordinates.model';
|
||||
import * as L from 'leaflet';
|
||||
import { LoadingComponent } from '@app/shared/components/loading/loading.component';
|
||||
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
|
||||
|
||||
export interface MapMarker {
|
||||
id: string;
|
||||
@@ -20,6 +21,7 @@ export interface MapMarker {
|
||||
title?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
profile?: ProfileViewModel;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -117,7 +119,7 @@ export class MapComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
private updateMarkers(): void {
|
||||
/*private updateMarkers(): void {
|
||||
if (!this.markersLayer) return;
|
||||
|
||||
// Nettoyer les markers existants
|
||||
@@ -152,6 +154,88 @@ export class MapComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
);
|
||||
this.map?.fitBounds(bounds, { padding: [50, 50] });
|
||||
}
|
||||
}*/
|
||||
|
||||
private updateMarkers(): void {
|
||||
if (!this.markersLayer) return;
|
||||
|
||||
this.markersLayer.clearLayers();
|
||||
|
||||
this.markers().forEach((marker) => {
|
||||
// 1. Création du Marker
|
||||
const leafletMarker = L.marker([marker.coordinates.latitude, marker.coordinates.longitude], {
|
||||
icon: this.createCustomIcon(),
|
||||
});
|
||||
|
||||
// --- A. GESTION DU SURVOL (HOVER) ---
|
||||
// On utilise un Tooltip pour l'affichage au survol
|
||||
if (marker.profile!.fullName) {
|
||||
leafletMarker.bindTooltip(marker.profile!.fullName!, {
|
||||
permanent: false, // false = n'apparaît qu'au survol
|
||||
direction: 'top', // s'affiche au-dessus du marker
|
||||
className: 'custom-map-tooltip', // Pour le style CSS si besoin
|
||||
offset: [0, -35], // Décalage pour ne pas chevaucher le marker
|
||||
});
|
||||
}
|
||||
|
||||
// --- B. GESTION DU CLIC (POPUP) ---
|
||||
// On construit la Card complète pour le clic
|
||||
const popupContainer = document.createElement('div');
|
||||
popupContainer.className =
|
||||
'custom-popup-card flex flex-col items-center gap-3 min-w-[160px] text-center p-1';
|
||||
|
||||
// Image (Avatar)
|
||||
if (marker.profile!.avatarUrl) {
|
||||
const img = document.createElement('img');
|
||||
img.src = marker.profile!.avatarUrl;
|
||||
img.className = 'w-16 h-16 rounded-full object-cover border-2 border-indigo-500 shadow-sm';
|
||||
popupContainer.appendChild(img);
|
||||
} else {
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.className =
|
||||
'w-16 h-16 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-500 font-bold text-xl';
|
||||
placeholder.innerText = marker.profile!.fullName
|
||||
? marker.profile!.fullName.charAt(0).toUpperCase()
|
||||
: '?';
|
||||
popupContainer.appendChild(placeholder);
|
||||
}
|
||||
|
||||
// Nom (Titre)
|
||||
if (marker.profile!.fullName) {
|
||||
const title = document.createElement('h3');
|
||||
title.className = 'font-bold text-gray-800 text-sm m-0';
|
||||
title.innerText = marker.profile!.fullName;
|
||||
popupContainer.appendChild(title);
|
||||
}
|
||||
|
||||
// Bouton "Voir le profil"
|
||||
const btn = document.createElement('button');
|
||||
btn.className =
|
||||
'bg-indigo-600 text-white text-xs px-4 py-1.5 rounded-full hover:bg-indigo-700 transition-colors w-full mt-1';
|
||||
btn.innerText = 'Voir le profil';
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
this.onMarkerClick.emit(marker);
|
||||
});
|
||||
popupContainer.appendChild(btn);
|
||||
|
||||
// On lie la popup au marker (Par défaut, bindPopup s'active au clic)
|
||||
leafletMarker.bindPopup(popupContainer, {
|
||||
closeButton: true, // On remet la croix pour pouvoir fermer si besoin
|
||||
offset: [0, -30],
|
||||
className: 'custom-leaflet-popup',
|
||||
});
|
||||
|
||||
this.markersLayer!.addLayer(leafletMarker);
|
||||
});
|
||||
|
||||
// Recentrage automatique (optionnel, selon votre goût)
|
||||
if (this.markers().length > 0 && this.map) {
|
||||
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 {
|
||||
|
||||
@@ -67,9 +67,7 @@ export class MyProfileMapComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
//this.locationState.set({...this.locationState(), coordinates: this.profile()!.coordonnees!})
|
||||
}
|
||||
ngOnInit() {}
|
||||
|
||||
onSaveLocation() {
|
||||
const currentLocation = this.locationState().coordinates;
|
||||
@@ -92,10 +90,11 @@ export class MyProfileMapComponent implements OnInit {
|
||||
private updateMarkers(coordinates: Coordinates): void {
|
||||
const marker: MapMarker[] = [
|
||||
{
|
||||
id: 'user-location',
|
||||
id: this.profile()!.id,
|
||||
coordinates: coordinates,
|
||||
title: 'Ma position',
|
||||
title: this.profile()!.fullName,
|
||||
description: CoordinatesValidator.format(coordinates),
|
||||
profile: this.profile()!,
|
||||
},
|
||||
];
|
||||
this.markers.set(marker);
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
[interactive]="true"
|
||||
[showUserLocation]="true"
|
||||
[height]="'400px'"
|
||||
(onMarkerClick)="onMarkerClicked($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Component, effect, inject, input, OnInit, signal } from '@angular/core'
|
||||
import { MapComponent, MapMarker } from '@app/shared/components/map/map.component';
|
||||
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
|
||||
import { LocationFacade } from '@app/ui/location/location.facade';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-profile-list-map',
|
||||
@@ -14,6 +15,7 @@ import { LocationFacade } from '@app/ui/location/location.facade';
|
||||
export class ProfileListMapComponent implements OnInit {
|
||||
readonly profiles = input<ProfileViewModel[]>();
|
||||
private readonly locationFacade = inject(LocationFacade);
|
||||
private readonly router = inject(Router); // Injection du Router
|
||||
|
||||
protected readonly locationState = this.locationFacade.locationState;
|
||||
protected readonly markers = signal<MapMarker[]>([]);
|
||||
@@ -42,6 +44,14 @@ export class ProfileListMapComponent implements OnInit {
|
||||
this.locationFacade.getCurrentLocation();
|
||||
}
|
||||
|
||||
// Nouvelle méthode pour gérer le clic sur "Voir plus"
|
||||
onMarkerClicked(marker: MapMarker): void {
|
||||
// Redirection vers la page détail
|
||||
this.router.navigate(['/profiles', marker.profile!.slug], {
|
||||
state: { user: marker.profile!.userViewModel, profile: marker.profile! },
|
||||
});
|
||||
}
|
||||
|
||||
private toMapMarker(profiles: ProfileViewModel[]): MapMarker[] {
|
||||
return profiles
|
||||
.filter((profile: ProfileViewModel) => {
|
||||
@@ -58,7 +68,7 @@ export class ProfileListMapComponent implements OnInit {
|
||||
coordinates: currentProfile.coordonnees!,
|
||||
title: currentProfile.fullName,
|
||||
description: currentProfile.bio,
|
||||
icon: currentProfile.avatarUrl,
|
||||
profile: currentProfile,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export const mockProfiles: Profile[] = [
|
||||
created: '2024-01-17T10:00:00Z',
|
||||
updated: '2024-01-17T12:00:00Z',
|
||||
profession: 'Développeur Web',
|
||||
coordonnees: { lat: 0, lon: 0 },
|
||||
utilisateur: 'user_abc',
|
||||
estVerifier: true,
|
||||
secteur: 'Informatique',
|
||||
@@ -22,6 +23,7 @@ export const mockProfiles: Profile[] = [
|
||||
updated: '2024-01-18T09:00:00Z',
|
||||
profession: 'Designer UI/UX',
|
||||
utilisateur: 'user_xyz',
|
||||
coordonnees: { lat: 0, lon: 0 },
|
||||
estVerifier: false,
|
||||
secteur: 'Design',
|
||||
reseaux: JSON.parse('{"dribbble": "https://dribbble.com/test"}'),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ProfilePresenter } from '@app/ui/profiles/profile.presenter';
|
||||
import { Profile } from '@app/domain/profiles/profile.model';
|
||||
import { mockProfiles } from '@app/testing/profile.mock';
|
||||
import { Coordinates } from '@app/domain/localisation/coordinates.model';
|
||||
|
||||
describe('ProfilePresenter', () => {
|
||||
it('devrait transformer un Profile en ProfileViewModel', () => {
|
||||
@@ -11,9 +12,10 @@ describe('ProfilePresenter', () => {
|
||||
|
||||
expect(viewModel).toEqual({
|
||||
id: profile.id,
|
||||
fullName: profile.profession.toUpperCase(), // transformation OK
|
||||
fullName: '', // transformation OK
|
||||
isVerifiedLabel: '✅ Vérifié',
|
||||
estVerifier: true,
|
||||
coordonnees: { latitude: 0, longitude: 0 } as Coordinates,
|
||||
profession: 'Développeur Web',
|
||||
reseaux: { linkedin: 'https://linkedin.com/in/test' },
|
||||
secteur: 'Informatique',
|
||||
@@ -49,6 +51,6 @@ describe('ProfilePresenter', () => {
|
||||
const result = profilePresenter.toViewModels(mockProfiles);
|
||||
|
||||
expect(result.length).toBe(2);
|
||||
expect(result[0].fullName).toBe(mockProfiles[0].profession.toUpperCase());
|
||||
expect(result[0].fullName).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Coordinates } from '@app/domain/localisation/coordinates.model';
|
||||
import { SettingsProfileDto } from '@app/domain/profiles/dto/settings-profile.dto';
|
||||
import { UserViewModel } from '@app/ui/users/user.presenter.model';
|
||||
|
||||
export interface ProfileViewModel {
|
||||
id: string;
|
||||
@@ -20,6 +21,8 @@ export interface ProfileViewModel {
|
||||
missingFields?: string[];
|
||||
coordonnees?: Coordinates;
|
||||
settings?: SettingsProfileDto;
|
||||
slug?: string;
|
||||
userViewModel?: UserViewModel;
|
||||
}
|
||||
|
||||
export interface ProfileViewModelPaginated {
|
||||
|
||||
@@ -4,8 +4,13 @@ import {
|
||||
} from '@app/ui/profiles/profile.presenter.model';
|
||||
import { Profile, ProfilePaginated } from '@app/domain/profiles/profile.model';
|
||||
import { SettingsProfileDto } from '@app/domain/profiles/dto/settings-profile.dto';
|
||||
import { User } from '@app/domain/users/user.model';
|
||||
import { environment } from '@env/environment';
|
||||
import { UserPresenter } from '@app/ui/users/user.presenter';
|
||||
|
||||
export class ProfilePresenter {
|
||||
private readonly userPresenter = new UserPresenter();
|
||||
|
||||
toViewModelPaginated(profilePaginated: ProfilePaginated): ProfileViewModelPaginated {
|
||||
return { ...profilePaginated, items: this.toViewModels(profilePaginated.items) };
|
||||
}
|
||||
@@ -20,12 +25,12 @@ export class ProfilePresenter {
|
||||
isProfilePublic: profile.estVisible ?? false,
|
||||
};
|
||||
|
||||
return {
|
||||
let profileViewModel: ProfileViewModel = {
|
||||
id: profile.id,
|
||||
fullName: profile.profession.toUpperCase(), // ❗ exemple volontaire
|
||||
fullName: '', // ❗ exemple volontaire
|
||||
isVerifiedLabel: profile.estVerifier ? '✅ Vérifié' : '❌ Non vérifié',
|
||||
createdAtFormatted: new Date(profile.created).toLocaleDateString(),
|
||||
avatarUrl: '',
|
||||
avatarUrl: ``,
|
||||
estVerifier: profile.estVerifier,
|
||||
utilisateur: profile.utilisateur,
|
||||
profession: profile.profession,
|
||||
@@ -42,6 +47,26 @@ export class ProfilePresenter {
|
||||
isProfileVisible,
|
||||
missingFields,
|
||||
};
|
||||
|
||||
const profileExpand = (profile as any) ? (profile as any).expand : { utilisateur: {} as User };
|
||||
const userExpand = profileExpand ? (profileExpand.utilisateur as User) : undefined;
|
||||
if (userExpand !== undefined) {
|
||||
const userViewModel = this.userPresenter.toViewModel(userExpand);
|
||||
|
||||
const userSlug = userViewModel.slug ?? '';
|
||||
const profileId = profile.id ? profile.id : '';
|
||||
const slug = userSlug === '' ? profileId : userSlug.concat('-', profileId);
|
||||
|
||||
profileViewModel = {
|
||||
...profileViewModel,
|
||||
userViewModel,
|
||||
slug,
|
||||
fullName: userExpand.name,
|
||||
avatarUrl: `${environment.baseUrl}/api/files/users/${profile.utilisateur}/${userExpand.avatar}`,
|
||||
};
|
||||
}
|
||||
|
||||
return profileViewModel;
|
||||
}
|
||||
|
||||
toViewModels(profiles: Profile[]): ProfileViewModel[] {
|
||||
|
||||
Reference in New Issue
Block a user