feat : #14 partage de profil

This commit is contained in:
styve Lioumba
2025-12-04 12:42:06 +01:00
parent 6afb13b2e9
commit 65ecc516c4
37 changed files with 309 additions and 74 deletions

View File

@@ -30,7 +30,7 @@ describe('AppComponent', () => {
create: jest.fn(),
list: jest.fn(),
update: jest.fn(),
getByUserId: jest.fn(),
getById: jest.fn(),
};
await TestBed.configureTestingModule({

View File

@@ -2,6 +2,7 @@ import { ApplicationConfig } from '@angular/core';
import {
PreloadAllModules,
provideRouter,
withComponentInputBinding,
withInMemoryScrolling,
withPreloading,
withViewTransitions,
@@ -21,6 +22,8 @@ import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository
import { PbUserRepository } from '@app/infrastructure/users/pb-user.repository';
import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token';
import { PbAuthRepository } from '@app/infrastructure/authentification/pb-auth.repository';
import { WEB_SHARE_SERVICE_TOKEN } from '@app/infrastructure/shareData/web-share.service.token';
import { WebShareService } from '@app/infrastructure/shareData/web-share.service';
export const appConfig: ApplicationConfig = {
providers: [
@@ -31,7 +34,8 @@ export const appConfig: ApplicationConfig = {
withInMemoryScrolling({
scrollPositionRestoration: 'enabled',
anchorScrolling: 'enabled',
})
}),
withComponentInputBinding()
),
provideAnimations(),
provideHttpClient(withFetch()),
@@ -40,6 +44,7 @@ export const appConfig: ApplicationConfig = {
{ provide: SECTOR_REPOSITORY_TOKEN, useExisting: PbSectorRepository },
{ provide: USER_REPOSITORY_TOKEN, useExisting: PbUserRepository },
{ provide: AUTH_REPOSITORY_TOKEN, useExisting: PbAuthRepository },
{ provide: WEB_SHARE_SERVICE_TOKEN, useExisting: WebShareService },
provideToastr({
timeOut: 10000,
positionClass: 'toast-top-right',

View File

@@ -4,7 +4,7 @@ import { SearchFilters } from '@app/domain/search/search-filters';
export interface ProfileRepository {
list(params?: SearchFilters): Observable<ProfilePaginated>;
getByUserId(userId: string): Observable<Profile>;
getById(profileId: string): Observable<Profile>;
create(profile: Profile): Observable<Profile>;
update(profileId: string, profile: Partial<Profile>): Observable<Profile>;
}

View File

@@ -0,0 +1,3 @@
export abstract class ShareDataRepository {
abstract share(shareData: ShareData): void;
}

View File

@@ -0,0 +1,5 @@
export interface ShareData {
title: string;
text: string;
url: string;
}

View File

@@ -29,9 +29,9 @@ export class PbProfileRepository implements ProfileRepository {
);
}
getByUserId(userId: string): Observable<Profile> {
getById(userId: string): Observable<Profile> {
return from(
this.pb.collection('profiles').getFirstListItem<Profile>(`utilisateur="${userId}"`)
this.pb.collection('profiles').getOne<Profile>(`${userId}`, { expand: 'utilisateur' })
);
}

View File

@@ -0,0 +1,6 @@
import { InjectionToken } from '@angular/core';
import { ShareDataRepository } from '@app/domain/shareData/share-data.repository';
export const WEB_SHARE_SERVICE_TOKEN = new InjectionToken<ShareDataRepository>(
'ShareDataRepository'
);

View File

@@ -0,0 +1,36 @@
import { inject, Injectable } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { ShareDataRepository } from '@app/domain/shareData/share-data.repository';
import { ToastrService } from 'ngx-toastr';
@Injectable({
providedIn: 'root',
})
export class WebShareService implements ShareDataRepository {
private document = inject(DOCUMENT);
private toastr = inject(ToastrService);
async share(shareData: ShareData) {
const navigator = this.document.defaultView?.navigator;
if (navigator && navigator.canShare && navigator.canShare(shareData)) {
try {
await navigator.share(shareData);
return;
} catch (error) {
return;
}
}
this.copyToClipboard(shareData.url!);
}
private copyToClipboard(text: string) {
navigator.clipboard.writeText(text).then(() => {
this.toastr.info(`Le lien du profil est copié dans le presse papier !`, `Partage de profil`, {
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
});
});
}
}

View File

@@ -21,7 +21,7 @@ describe('HomeComponent', () => {
mockProfileRepo = {
create: jest.fn().mockReturnValue(of({} as Profile)),
list: jest.fn().mockReturnValue(of([])),
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
getById: jest.fn().mockReturnValue(of({} as Profile)),
update: jest.fn().mockReturnValue(of({} as Profile)),
};

View File

@@ -26,7 +26,7 @@ describe('MyProfileComponent', () => {
create: jest.fn(),
list: jest.fn(),
update: jest.fn(),
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
getById: jest.fn().mockReturnValue(of({} as Profile)),
};
mockUserRepo = {

View File

@@ -9,11 +9,13 @@
<!-- Overlay gradient -->
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-black/30"></div>
<!-- Bouton retour -->
<!-- Boutons en haut -->
<div class="relative z-10 p-4 flex justify-between items-start">
<!-- Bouton retour -->
<a
[routerLink]="['/profiles']"
class="group flex items-center justify-center w-10 h-10 md:w-12 md:h-12 bg-white/20 backdrop-blur-md rounded-full hover:bg-white/30 transition-all duration-300 hover:scale-110"
aria-label="Retour à la liste des profils"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -30,27 +32,53 @@
</svg>
</a>
<!-- Badge vérifié -->
@if (profile().estVerifier) {
<div
class="flex items-center gap-2 bg-purple-500/20 backdrop-blur-md px-3 py-2 rounded-full animate-pulse-slow"
<!-- Conteneur pour badge vérifié et bouton partage -->
<div class="flex items-center gap-2">
<!-- Badge vérifié -->
@if (profile()!.estVerifier) {
<div
class="flex items-center gap-2 bg-purple-500/20 backdrop-blur-md px-3 py-2 rounded-full animate-pulse-slow"
data-testid="verified-badge"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-5 h-5 md:w-6 md:h-6 text-purple-300"
>
<title>Profil vérifié</title>
<path
fill-rule="evenodd"
d="M8.603 3.799A4.49 4.49 0 0 1 12 2.25c1.357 0 2.573.6 3.397 1.549a4.49 4.49 0 0 1 3.498 1.307 4.491 4.491 0 0 1 1.307 3.497A4.49 4.49 0 0 1 21.75 12a4.49 4.49 0 0 1-1.549 3.397 4.491 4.491 0 0 1-1.307 3.497 4.491 4.491 0 0 1-3.497 1.307A4.49 4.49 0 0 1 12 21.75a4.49 4.49 0 0 1-3.397-1.549 4.49 4.49 0 0 1-3.498-1.306 4.491 4.491 0 0 1-1.307-3.498A4.49 4.49 0 0 1 2.25 12c0-1.357.6-2.573 1.549-3.397a4.49 4.49 0 0 1 1.307-3.497 4.49 4.49 0 0 1 3.497-1.307Zm7.007 6.387a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
clip-rule="evenodd"
/>
</svg>
<span class="text-white text-sm font-medium hidden md:inline">Vérifié</span>
</div>
}
<!-- Bouton partage -->
<button
(click)="onShare()"
class="group flex items-center justify-center w-10 h-10 md:w-12 md:h-12 bg-indigo-500/20 backdrop-blur-md rounded-full hover:bg-indigo-500/30 transition-all duration-300 hover:scale-110"
aria-label="Partager ce profil"
data-testid="share-button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-5 h-5 md:w-6 md:h-6 text-purple-300"
class="w-5 h-5 md:w-6 md:h-6 text-white"
>
<title>Profil vérifié</title>
<title>Partager</title>
<path
fill-rule="evenodd"
d="M8.603 3.799A4.49 4.49 0 0 1 12 2.25c1.357 0 2.573.6 3.397 1.549a4.49 4.49 0 0 1 3.498 1.307 4.491 4.491 0 0 1 1.307 3.497A4.49 4.49 0 0 1 21.75 12a4.49 4.49 0 0 1-1.549 3.397 4.491 4.491 0 0 1-1.307 3.497 4.491 4.491 0 0 1-3.497 1.307A4.49 4.49 0 0 1 12 21.75a4.49 4.49 0 0 1-3.397-1.549 4.49 4.49 0 0 1-3.498-1.306 4.491 4.491 0 0 1-1.307-3.498A4.49 4.49 0 0 1 2.25 12c0-1.357.6-2.573 1.549-3.397a4.49 4.49 0 0 1 1.307-3.497 4.49 4.49 0 0 1 3.497-1.307Zm7.007 6.387a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
d="M15.75 4.5a3 3 0 11.825 2.066l-8.421 4.679a3.002 3.002 0 010 1.51l8.421 4.679a3 3 0 11-.729 1.31l-8.421-4.678a3 3 0 110-4.132l8.421-4.679a3 3 0 01-.096-.755z"
clip-rule="evenodd"
/>
</svg>
<span class="text-white text-sm font-medium hidden md:inline">Vérifié</span>
</div>
}
</button>
</div>
</div>
</div>
@@ -103,7 +131,7 @@
}
<p class="text-lg md:text-xl text-indigo-600 dark:text-indigo-400 font-semibold">
{{ profile().profession | uppercase }}
{{ profile()!.profession | uppercase }}
</p>
</div>
</div>
@@ -135,9 +163,9 @@
</svg>
Biographie
</h3>
@if (profile().bio) {
@if (profile()!.bio) {
<p class="text-gray-600 dark:text-gray-300 text-sm leading-relaxed">
{{ profile().bio }}
{{ profile()!.bio }}
</p>
} @else {
<p class="text-gray-600 dark:text-gray-300 text-sm leading-relaxed">
@@ -149,7 +177,7 @@
</div>
<!-- Card Secteur -->
@if (profile().secteur) {
@if (profile()!.secteur) {
<div
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow duration-300"
>
@@ -168,12 +196,12 @@
</svg>
Secteur
</h3>
<app-chips [sectorId]="profile().secteur" />
<app-chips [sectorId]="profile()!.secteur" />
</div>
}
<!-- Card Réseaux -->
@if (profile().reseaux) {
@if (profile()!.reseaux) {
<div
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow duration-300"
>
@@ -195,7 +223,7 @@
</svg>
Réseaux
</h3>
<app-reseaux [reseaux]="profile().reseaux" />
<app-reseaux [reseaux]="profile()!.reseaux" />
</div>
}
</div>
@@ -203,7 +231,7 @@
<!-- Colonne droite - À propos et Projets -->
<div class="lg:col-span-2 space-y-6 animate-slide-up animation-delay-300">
<!-- Card À propos -->
@if (profile().apropos) {
@if (profile()!.apropos) {
<div
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 md:p-8 hover:shadow-xl transition-shadow duration-300"
>
@@ -225,7 +253,7 @@
À propos
</h2>
<p class="text-gray-700 dark:text-gray-300 leading-relaxed text-base">
{{ profile().apropos }}
{{ profile()!.apropos }}
</p>
</div>
}

View File

@@ -9,12 +9,22 @@ import { Project } from '@app/domain/projects/project.model';
import { Sector } from '@app/domain/sectors/sector.model';
import { SectorRepository } from '@app/domain/sectors/sector.repository';
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
import { ToastrService } from 'ngx-toastr';
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
import { Profile } from '@app/domain/profiles/profile.model';
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository.token';
import { UserRepository } from '@app/domain/users/user.repository';
import { User } from '@app/domain/users/user.model';
describe('ProfileDetailComponent', () => {
let component: ProfileDetailComponent;
let fixture: ComponentFixture<ProfileDetailComponent>;
let mockProjectRepository: jest.Mocked<ProjectRepository>;
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>>;
let mockSectorRepo: SectorRepository;
let mockToastr: Partial<ToastrService>;
let mockUserRepo: jest.Mocked<Partial<UserRepository>>;
beforeEach(async () => {
mockProjectRepository = {
@@ -29,12 +39,34 @@ describe('ProfileDetailComponent', () => {
getOne: jest.fn().mockReturnValue(of({} as Sector)),
};
mockToastr = {
warning: jest.fn(),
success: jest.fn(),
info: jest.fn(),
error: jest.fn(),
};
mockProfileRepository = {
create: jest.fn().mockReturnValue(of({} as Profile)),
list: jest.fn().mockReturnValue(of([])),
getById: jest.fn().mockReturnValue(of({} as Profile)),
update: jest.fn().mockReturnValue(of({} as Profile)),
};
mockUserRepo = {
getUserById: jest.fn().mockReturnValue(of({} as User)),
update: jest.fn(),
};
await TestBed.configureTestingModule({
imports: [ProfileDetailComponent],
providers: [
provideRouter([]),
{ provide: PROJECT_REPOSITORY_TOKEN, useValue: mockProjectRepository },
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository },
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepo },
{ provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepo },
{ provide: ToastrService, useValue: mockToastr },
],
}).compileComponents();

View File

@@ -1,4 +1,4 @@
import { Component, computed, inject } from '@angular/core';
import { Component, computed, effect, inject, OnInit } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { UpperCasePipe } from '@angular/common';
import { User } from '@app/domain/users/user.model';
@@ -7,29 +7,86 @@ import { ReseauxComponent } from '@app/shared/components/reseaux/reseaux.compone
import { UntilDestroy } from '@ngneat/until-destroy';
import { ProjectListComponent } from '@app/shared/components/project-list/project-list.component';
import { environment } from '@env/environment';
import { Profile } from '@app/domain/profiles/profile.model';
import { WebShareService } from '@app/infrastructure/shareData/web-share.service';
import { ProfileFacade } from '@app/ui/profiles/profile.facade';
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
import { ActionType } from '@app/domain/action-type.util';
import { UserFacade } from '@app/ui/users/user.facade';
@Component({
selector: 'app-profile-detail',
standalone: true,
imports: [ChipsComponent, ReseauxComponent, RouterLink, UpperCasePipe, ProjectListComponent],
providers: [UserFacade],
templateUrl: './profile-detail.component.html',
styleUrl: './profile-detail.component.scss',
})
@UntilDestroy()
export class ProfileDetailComponent {
export class ProfileDetailComponent implements OnInit {
private readonly webShare = inject(WebShareService);
private readonly profileFacade = inject(ProfileFacade);
private readonly userFacade = inject(UserFacade);
protected readonly environment = environment;
protected readonly ActionType = ActionType;
private readonly route = inject(ActivatedRoute);
protected extraData: { user: User; profile: Profile } = this.route.snapshot.data['profile'];
protected extraData: { user: User; profile: ProfileViewModel } | undefined =
this.route.snapshot.data['profile'];
protected user = computed(() => {
if (this.extraData != undefined) return this.extraData.user;
return {} as User;
});
slug = computed(() => this.route.snapshot.params['name'] ?? '');
protected profile = computed(() => {
if (this.extraData != undefined) return this.extraData.profile;
return {} as Profile;
});
protected user = this.userFacade.user;
protected readonly userLoading = this.userFacade.loading;
protected readonly userError = this.userFacade.error;
protected profile = this.profileFacade.profile;
protected readonly profileLoading = this.profileFacade.loading;
protected readonly profileError = this.profileFacade.error;
constructor() {
effect(() => {
if (!this.profileLoading().isLoading) {
switch (this.profileLoading().action) {
case ActionType.READ:
if (!this.profileError().hasError) {
this.profile = this.profileFacade.profile;
}
break;
}
}
if (!this.userLoading().isLoading) {
switch (this.userLoading().action) {
case ActionType.READ:
if (!this.userError().hasError) {
this.user = this.userFacade.user;
}
break;
}
}
});
}
ngOnInit() {
if (this.extraData === undefined) {
const extractSlug = this.slug().split('-');
const profileId = extractSlug[extractSlug.length - 1];
const userId = extractSlug[extractSlug.length - 2];
this.profileFacade.loadOne(profileId);
this.userFacade.loadOne(userId);
} else {
this.profile.set(this.extraData.profile);
this.user.set(this.extraData.user);
}
}
async onShare() {
if (!this.profile) return;
const fullUrl = `${window.location.origin}/profiles/${this.slug()}`;
await this.webShare.share({
title: `Découvrez le profil de ${this.profile.name}`,
text: `Jette un œil à ce profil intéressant sur notre application !`,
url: fullUrl,
});
}
}

View File

@@ -20,7 +20,7 @@ describe('ProfileListComponent', () => {
mockProfileRepository = {
create: jest.fn().mockReturnValue(of({} as Profile)),
list: jest.fn().mockReturnValue(of([])),
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
getById: jest.fn().mockReturnValue(of({} as Profile)),
update: jest.fn().mockReturnValue(of({} as Profile)),
};

View File

@@ -26,7 +26,7 @@ describe('MyProfileUpdateCvFormComponent', () => {
create: jest.fn(),
list: jest.fn(),
update: jest.fn().mockReturnValue(of({} as Profile)),
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
getById: jest.fn().mockReturnValue(of({} as Profile)),
};
await TestBed.configureTestingModule({

View File

@@ -39,7 +39,7 @@ describe('MyProfileUpdateFormComponent', () => {
create: jest.fn(),
list: jest.fn(),
update: jest.fn().mockReturnValue(of({} as Profile)),
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
getById: jest.fn().mockReturnValue(of({} as Profile)),
};
mockSectorRepo = {

View File

@@ -51,7 +51,7 @@ describe('MyProfileUpdateProjectFormComponent', () => {
create: jest.fn(),
list: jest.fn(),
update: jest.fn(),
getByUserId: jest.fn(),
getById: jest.fn(),
};
await TestBed.configureTestingModule({

View File

@@ -7,12 +7,12 @@
<a
[routerLink]="['/']"
class="flex items-center space-x-2 group"
aria-label="Accueil TrouveTonProfile"
aria-label="Accueil TrouveTonProfil"
>
<span
class="text-lg sm:text-xl font-semibold text-gray-900 dark:text-white group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors"
>
TrouveTonProfile
TrouveTonProfil
</span>
</a>

View File

@@ -4,7 +4,6 @@ import { NavBarComponent } from './nav-bar.component';
import { ThemeService } from '@app/core/services/theme/theme.service';
import { provideRouter } from '@angular/router';
import { signal } from '@angular/core';
import { AuthModel } from '@app/domain/authentification/auth.model';
import { User } from '@app/domain/users/user.model';
import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token';
import { AuthRepository } from '@app/domain/authentification/auth.repository';
@@ -53,7 +52,7 @@ describe('NavBarComponent', () => {
create: jest.fn(),
list: jest.fn(),
update: jest.fn(),
getByUserId: jest.fn(),
getById: jest.fn(),
};
await TestBed.configureTestingModule({

View File

@@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core';
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
import { UntilDestroy } from '@ngneat/until-destroy';
import { ProjectItemComponent } from '@app/shared/components/project-item/project-item.component';
import { ProjectFacade } from '@app/ui/projects/project.facade';
@@ -11,14 +11,14 @@ import { ProjectFacade } from '@app/ui/projects/project.facade';
styleUrl: './project-list.component.scss',
})
@UntilDestroy()
export class ProjectListComponent implements OnInit {
export class ProjectListComponent implements OnChanges {
@Input({ required: true }) userProjectId = '';
private readonly projectFacade = new ProjectFacade();
protected projects = this.projectFacade.projects;
ngOnInit(): void {
ngOnChanges(changes: SimpleChanges) {
this.projectFacade.load(this.userProjectId);
}
}

View File

@@ -1,5 +1,5 @@
@if (user() !== undefined) {
<a [routerLink]="[user().slug]" [state]="{ user: user(), profile }" class="block group">
<a [routerLink]="[slug()]" [state]="{ user: user(), profile }" class="block group">
<!-- Card du profil -->
<div
class="relative bg-white dark:bg-gray-800 rounded-xl shadow-lg overflow-hidden transition-all duration-300 hover:shadow-2xl hover:-translate-y-2"

View File

@@ -1,4 +1,4 @@
import { Component, inject, Input, OnInit } from '@angular/core';
import { Component, computed, inject, Input, OnInit } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { UntilDestroy } from '@ngneat/until-destroy';
import { environment } from '@env/environment';
@@ -23,6 +23,12 @@ export class VerticalProfileItemComponent implements OnInit {
protected readonly loading = this.facade.loading;
protected readonly error = this.facade.error;
protected slug = computed(() => {
const slug = this.user().slug ?? '';
const profileId = this.profile.id ? this.profile.id : '';
return slug === '' ? profileId : slug.concat('-', profileId);
});
ngOnInit(): void {
this.facade.loadOne(this.profile.utilisateur);
}

View File

@@ -20,7 +20,7 @@ describe('FilterComponent', () => {
mockProfileRepository = {
create: jest.fn().mockReturnValue(of({} as Profile)),
list: jest.fn().mockReturnValue(of([])),
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
getById: jest.fn().mockReturnValue(of({} as Profile)),
update: jest.fn().mockReturnValue(of({} as Profile)),
};

View File

@@ -42,7 +42,7 @@ describe('LoginComponent', () => {
create: jest.fn(),
list: jest.fn(),
update: jest.fn(),
getByUserId: jest.fn(),
getById: jest.fn(),
};
await TestBed.configureTestingModule({

View File

@@ -21,7 +21,7 @@ describe('RegisterComponent', () => {
create: jest.fn(),
list: jest.fn(),
update: jest.fn(),
getByUserId: jest.fn(),
getById: jest.fn(),
};
mockToastrService = {

View File

@@ -21,7 +21,7 @@ describe('SearchComponent', () => {
mockProfileRepo = {
create: jest.fn().mockReturnValue(of({} as Profile)),
list: jest.fn().mockReturnValue(of([])),
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
getById: jest.fn().mockReturnValue(of({} as Profile)),
update: jest.fn().mockReturnValue(of({} as Profile)),
};

View File

@@ -8,8 +8,8 @@ export class FakeProfileRepository implements ProfileRepository {
return of(mockProfilePaginated);
}
getByUserId(userId: string): Observable<Profile> {
const profile = mockProfiles.find((p) => p.utilisateur === userId) ?? ({} as Profile);
getById(profileId: string): Observable<Profile> {
const profile = mockProfiles.find((p) => p.utilisateur === profileId) ?? ({} as Profile);
return of(profile);
}

View File

@@ -15,7 +15,7 @@ describe('PbProfileRepository', () => {
// Création dun faux client PocketBase avec les méthodes dont on a besoin
mockCollection = {
getList: jest.fn(),
getFirstListItem: jest.fn(),
getOne: jest.fn(),
create: jest.fn(),
update: jest.fn(),
};
@@ -54,13 +54,13 @@ describe('PbProfileRepository', () => {
// ------------------------------------------
// 🔹 TEST : getByUserId()
// ------------------------------------------
it('devrait appeler pb.collection("profiles").getFirstListItem() avec le bon filtre utilisateur', () => {
const userId = '1';
it('devrait appeler pb.collection("profiles").getOne() avec le bon filtre utilisateur', () => {
const profileId = '1';
mockCollection.getFirstListItem.mockResolvedValue(mockProfiles);
mockCollection.getOne.mockResolvedValue(mockProfiles);
repo.getByUserId(userId).subscribe((result) => {
expect(mockCollection.getFirstListItem).toHaveBeenCalledWith(`utilisateur="${userId}"`);
repo.getById(profileId).subscribe((result) => {
expect(mockCollection.getOne).toHaveBeenCalledWith(`${profileId}`, { expand: 'utilisateur' });
expect(result).toEqual(mockProfiles[0]);
});
});

View File

@@ -67,9 +67,9 @@ export class ProfileFacade {
});
}
loadOne(userId: string) {
loadOne(profileId: string) {
this.handleError(ActionType.READ, false, null, true);
this.getUseCase.execute(userId).subscribe({
this.getUseCase.execute(profileId).subscribe({
next: (profile: Profile) => {
this.profile.set(ProfilePresenter.toViewModel(profile));
this.handleError(ActionType.READ, false, null, false);

View File

@@ -4,8 +4,8 @@ import { User } from '@app/domain/users/user.model';
export class UserPresenter {
toViewModel(user: User): UserViewModel {
const slug = user.name
? user.name.toLowerCase().replace(/\s/g, '-')
: user.email.split('@')[0].toLowerCase().trim();
? this.generateProfileSlug(user.name, user.id)
: this.generateProfileSlug('Non renséigné');
let userViewModel: UserViewModel = {
id: user.id,
@@ -30,4 +30,16 @@ export class UserPresenter {
toViewModels(users: User[]): UserViewModel[] {
return users.map(this.toViewModel);
}
private generateProfileSlug(name: string, id?: string): string {
return name
.concat(id ? ` ${id}` : '')
.toLowerCase()
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '') // Enlève les accents
.trim()
.replace(/[^a-z0-9 -]/g, '') // Enlève les caractères spéciaux
.replace(/\s+/g, '-') // Remplace les espaces par des tirets
.replace(/-+/g, '-'); // Évite les tirets multiples
}
}

View File

@@ -5,7 +5,7 @@ import { Profile } from '@app/domain/profiles/profile.model';
export class GetProfileUseCase {
constructor(private readonly repo: ProfileRepository) {}
execute(userId: string): Observable<Profile> {
return this.repo.getByUserId(userId);
execute(profileId: string): Observable<Profile> {
return this.repo.getById(profileId);
}
}

View File

@@ -1,8 +1,10 @@
import { UserRepository } from '@app/domain/users/user.repository';
import { Observable } from 'rxjs';
import { User } from '@app/domain/users/user.model';
export class GetUserUseCase {
constructor(private readonly repo: UserRepository) {}
execute(userId: string) {
execute(userId: string): Observable<User> {
return this.repo.getUserById(userId);
}
}