feat : #3 card de redirection profil
Some checks are pending
Build Check / build (push) Waiting to run
Build Check / build (pull_request) Successful in 6m42s

This commit is contained in:
styve Lioumba
2025-12-16 17:15:53 +01:00
parent 1ad97ff89d
commit 911546924c
9 changed files with 143 additions and 13 deletions

View File

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

View File

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

View File

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

View File

@@ -52,6 +52,7 @@
[interactive]="true"
[showUserLocation]="true"
[height]="'400px'"
(onMarkerClick)="onMarkerClicked($event)"
/>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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