From 65ecc516c4763d319b7c85906aafb4d4d8738851 Mon Sep 17 00:00:00 2001 From: styve Lioumba Date: Thu, 4 Dec 2025 12:42:06 +0100 Subject: [PATCH] feat : #14 partage de profil --- .act.secrets.example | 2 + .actrc | 8 +- .env.example | 1 + .gitea/workflows/trigger-deploy.yaml | 31 +++++++ .gitignore | 8 ++ src/app/app.component.spec.ts | 2 +- src/app/app.config.ts | 7 +- src/app/domain/profiles/profile.repository.ts | 2 +- .../domain/shareData/share-data.repository.ts | 3 + src/app/domain/shareData/share-data.ts | 5 ++ .../profiles/pb-profile.repository.ts | 4 +- .../shareData/web-share.service.token.ts | 6 ++ .../shareData/web-share.service.ts | 36 +++++++++ src/app/routes/home/home.component.spec.ts | 2 +- .../my-profile/my-profile.component.spec.ts | 2 +- .../profile-detail.component.html | 68 +++++++++++----- .../profile-detail.component.spec.ts | 32 ++++++++ .../profile-detail.component.ts | 81 ++++++++++++++++--- .../profile-list.component.spec.ts | 2 +- ...y-profile-update-cv-form.component.spec.ts | 2 +- .../my-profile-update-form.component.spec.ts | 2 +- ...file-update-project-form.component.spec.ts | 2 +- .../components/nav-bar/nav-bar.component.html | 4 +- .../nav-bar/nav-bar.component.spec.ts | 3 +- .../project-list/project-list.component.ts | 6 +- .../vertical-profile-item.component.html | 2 +- .../vertical-profile-item.component.ts | 8 +- .../features/filter/filter.component.spec.ts | 2 +- .../features/login/login.component.spec.ts | 2 +- .../register/register.component.spec.ts | 2 +- .../features/search/search.component.spec.ts | 2 +- .../profiles/fake-profile.repository.ts | 4 +- .../profiles/pb-profile.repository.spec.ts | 12 +-- src/app/ui/profiles/profile.facade.ts | 4 +- src/app/ui/users/user.presenter.ts | 16 +++- .../usecase/profiles/get-profile.usecase.ts | 4 +- src/app/usecase/users/get-user.usecase.ts | 4 +- 37 files changed, 309 insertions(+), 74 deletions(-) create mode 100644 .act.secrets.example create mode 100644 .env.example create mode 100644 .gitea/workflows/trigger-deploy.yaml create mode 100644 src/app/domain/shareData/share-data.repository.ts create mode 100644 src/app/domain/shareData/share-data.ts create mode 100644 src/app/infrastructure/shareData/web-share.service.token.ts create mode 100644 src/app/infrastructure/shareData/web-share.service.ts diff --git a/.act.secrets.example b/.act.secrets.example new file mode 100644 index 0000000..14cf500 --- /dev/null +++ b/.act.secrets.example @@ -0,0 +1,2 @@ +ROBOT_TOKEN=fake_gitea_token +ENV_URL=fake_env_url diff --git a/.actrc b/.actrc index b0f4a64..c9c13ba 100644 --- a/.actrc +++ b/.actrc @@ -1,3 +1,5 @@ ---container-architecture linux/amd64 --W .gitea/workflows --P ubuntu-latest==ghcr.io/catthehacker/ubuntu:act-latest +--container-architecture linux/arm64 +--workflows .gitea/workflows/ +--platform ubuntu-latest +--secret-file .act.secrets +--env-file .env diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ef8ce16 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +GITEA_SERVER_URL=https://fake-gitea-url.example.com \ No newline at end of file diff --git a/.gitea/workflows/trigger-deploy.yaml b/.gitea/workflows/trigger-deploy.yaml new file mode 100644 index 0000000..3e72ee6 --- /dev/null +++ b/.gitea/workflows/trigger-deploy.yaml @@ -0,0 +1,31 @@ +name: Trigger Deploy Repo +run-name: Trigger deploy + +on: + push: + tags: + - '*' # Se déclenche sur n'importe quel tag + +jobs: + dispatch: + runs-on: ubuntu-latest + + env: + # ------- À ADAPTER ------- + CODE_REF: ${{ gitea.ref }} # branche/tag/sha à récupérer + GITEA_SERVER_URL: https://git.prod.k3s.technostrea.fr # URL du serveur Gitea + # -------------------------- + steps: + - name: Dispatch Signal to ttp-deploy + run: | + # Récupération de la version sans le "refs/tags/" + VERSION=${{ env.CODE_REF }} + + echo "Déclenchement du déploiement pour la version $VERSION sur l'instance ${{ env.GITEA_SERVER_URL }}" + + # Appel API Gitea. + # Notez la structure de l'URL : /api/v1/repos/{owner}/{repo}/dispatches + curl -X POST "${{ env.GITEA_SERVER_URL }}/api/v1/repos/technostrea/trouvetonprofile-deployment/dispatches" \ + -H "Authorization: token ${{ secrets.ROBOT_TOKEN }}" \ + -H "Content-Type: application/json" \ + -d "{\"event_type\": \"build-release\", \"client_payload\": {\"version\": \"$VERSION\"}}" diff --git a/.gitignore b/.gitignore index 62714e8..3f3a096 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,11 @@ logs/*.log pb/data/* src/environments/environment.development.ts + +*.iml +.idea +logs/ +*.log +*.secrets +.act.secrets +.env diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 1052520..ac13549 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -30,7 +30,7 @@ describe('AppComponent', () => { create: jest.fn(), list: jest.fn(), update: jest.fn(), - getByUserId: jest.fn(), + getById: jest.fn(), }; await TestBed.configureTestingModule({ diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 450dd6a..af96bb9 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -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', diff --git a/src/app/domain/profiles/profile.repository.ts b/src/app/domain/profiles/profile.repository.ts index 440bad0..86ddeeb 100644 --- a/src/app/domain/profiles/profile.repository.ts +++ b/src/app/domain/profiles/profile.repository.ts @@ -4,7 +4,7 @@ import { SearchFilters } from '@app/domain/search/search-filters'; export interface ProfileRepository { list(params?: SearchFilters): Observable; - getByUserId(userId: string): Observable; + getById(profileId: string): Observable; create(profile: Profile): Observable; update(profileId: string, profile: Partial): Observable; } diff --git a/src/app/domain/shareData/share-data.repository.ts b/src/app/domain/shareData/share-data.repository.ts new file mode 100644 index 0000000..0a8d058 --- /dev/null +++ b/src/app/domain/shareData/share-data.repository.ts @@ -0,0 +1,3 @@ +export abstract class ShareDataRepository { + abstract share(shareData: ShareData): void; +} diff --git a/src/app/domain/shareData/share-data.ts b/src/app/domain/shareData/share-data.ts new file mode 100644 index 0000000..27e7625 --- /dev/null +++ b/src/app/domain/shareData/share-data.ts @@ -0,0 +1,5 @@ +export interface ShareData { + title: string; + text: string; + url: string; +} diff --git a/src/app/infrastructure/profiles/pb-profile.repository.ts b/src/app/infrastructure/profiles/pb-profile.repository.ts index 925f89c..2e42615 100644 --- a/src/app/infrastructure/profiles/pb-profile.repository.ts +++ b/src/app/infrastructure/profiles/pb-profile.repository.ts @@ -29,9 +29,9 @@ export class PbProfileRepository implements ProfileRepository { ); } - getByUserId(userId: string): Observable { + getById(userId: string): Observable { return from( - this.pb.collection('profiles').getFirstListItem(`utilisateur="${userId}"`) + this.pb.collection('profiles').getOne(`${userId}`, { expand: 'utilisateur' }) ); } diff --git a/src/app/infrastructure/shareData/web-share.service.token.ts b/src/app/infrastructure/shareData/web-share.service.token.ts new file mode 100644 index 0000000..d6d5faf --- /dev/null +++ b/src/app/infrastructure/shareData/web-share.service.token.ts @@ -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' +); diff --git a/src/app/infrastructure/shareData/web-share.service.ts b/src/app/infrastructure/shareData/web-share.service.ts new file mode 100644 index 0000000..1cb9abf --- /dev/null +++ b/src/app/infrastructure/shareData/web-share.service.ts @@ -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, + }); + }); + } +} diff --git a/src/app/routes/home/home.component.spec.ts b/src/app/routes/home/home.component.spec.ts index 016aa95..9804a9b 100644 --- a/src/app/routes/home/home.component.spec.ts +++ b/src/app/routes/home/home.component.spec.ts @@ -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)), }; diff --git a/src/app/routes/my-profile/my-profile.component.spec.ts b/src/app/routes/my-profile/my-profile.component.spec.ts index 7851222..9834fcb 100644 --- a/src/app/routes/my-profile/my-profile.component.spec.ts +++ b/src/app/routes/my-profile/my-profile.component.spec.ts @@ -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 = { diff --git a/src/app/routes/profile/profile-detail/profile-detail.component.html b/src/app/routes/profile/profile-detail/profile-detail.component.html index 6336a68..51b64fa 100644 --- a/src/app/routes/profile/profile-detail/profile-detail.component.html +++ b/src/app/routes/profile/profile-detail/profile-detail.component.html @@ -9,11 +9,13 @@
- +
+ - - @if (profile().estVerifier) { -
+
+ + @if (profile()!.estVerifier) { +
+ + Profil vérifié + + + +
+ } + + +
- } + +
@@ -103,7 +131,7 @@ }

- {{ profile().profession | uppercase }} + {{ profile()!.profession | uppercase }}

@@ -135,9 +163,9 @@ Biographie - @if (profile().bio) { + @if (profile()!.bio) {

- {{ profile().bio }} + {{ profile()!.bio }}

} @else {

@@ -149,7 +177,7 @@ - @if (profile().secteur) { + @if (profile()!.secteur) {

@@ -168,12 +196,12 @@ Secteur - +
} - @if (profile().reseaux) { + @if (profile()!.reseaux) {
@@ -195,7 +223,7 @@ Réseaux - +
} @@ -203,7 +231,7 @@
- @if (profile().apropos) { + @if (profile()!.apropos) {
@@ -225,7 +253,7 @@ À propos

- {{ profile().apropos }} + {{ profile()!.apropos }}

} diff --git a/src/app/routes/profile/profile-detail/profile-detail.component.spec.ts b/src/app/routes/profile/profile-detail/profile-detail.component.spec.ts index 90469d3..f012de4 100644 --- a/src/app/routes/profile/profile-detail/profile-detail.component.spec.ts +++ b/src/app/routes/profile/profile-detail/profile-detail.component.spec.ts @@ -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; let mockProjectRepository: jest.Mocked; + let mockProfileRepository: jest.Mocked>; let mockSectorRepo: SectorRepository; + let mockToastr: Partial; + let mockUserRepo: jest.Mocked>; 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(); diff --git a/src/app/routes/profile/profile-detail/profile-detail.component.ts b/src/app/routes/profile/profile-detail/profile-detail.component.ts index b9ecebd..2401080 100644 --- a/src/app/routes/profile/profile-detail/profile-detail.component.ts +++ b/src/app/routes/profile/profile-detail/profile-detail.component.ts @@ -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, + }); + } } diff --git a/src/app/routes/profile/profile-list/profile-list.component.spec.ts b/src/app/routes/profile/profile-list/profile-list.component.spec.ts index cc2653d..ba44c89 100644 --- a/src/app/routes/profile/profile-list/profile-list.component.spec.ts +++ b/src/app/routes/profile/profile-list/profile-list.component.spec.ts @@ -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)), }; diff --git a/src/app/shared/components/my-profile-update-cv-form/my-profile-update-cv-form.component.spec.ts b/src/app/shared/components/my-profile-update-cv-form/my-profile-update-cv-form.component.spec.ts index 0da159f..aef5c37 100644 --- a/src/app/shared/components/my-profile-update-cv-form/my-profile-update-cv-form.component.spec.ts +++ b/src/app/shared/components/my-profile-update-cv-form/my-profile-update-cv-form.component.spec.ts @@ -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({ diff --git a/src/app/shared/components/my-profile-update-form/my-profile-update-form.component.spec.ts b/src/app/shared/components/my-profile-update-form/my-profile-update-form.component.spec.ts index d499b4b..e6d13fc 100644 --- a/src/app/shared/components/my-profile-update-form/my-profile-update-form.component.spec.ts +++ b/src/app/shared/components/my-profile-update-form/my-profile-update-form.component.spec.ts @@ -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 = { diff --git a/src/app/shared/components/my-profile-update-project-form/my-profile-update-project-form.component.spec.ts b/src/app/shared/components/my-profile-update-project-form/my-profile-update-project-form.component.spec.ts index 8a802ce..f63b5e4 100644 --- a/src/app/shared/components/my-profile-update-project-form/my-profile-update-project-form.component.spec.ts +++ b/src/app/shared/components/my-profile-update-project-form/my-profile-update-project-form.component.spec.ts @@ -51,7 +51,7 @@ describe('MyProfileUpdateProjectFormComponent', () => { create: jest.fn(), list: jest.fn(), update: jest.fn(), - getByUserId: jest.fn(), + getById: jest.fn(), }; await TestBed.configureTestingModule({ diff --git a/src/app/shared/components/nav-bar/nav-bar.component.html b/src/app/shared/components/nav-bar/nav-bar.component.html index 9671885..aca3b08 100644 --- a/src/app/shared/components/nav-bar/nav-bar.component.html +++ b/src/app/shared/components/nav-bar/nav-bar.component.html @@ -7,12 +7,12 @@ - TrouveTonProfile + TrouveTonProfil diff --git a/src/app/shared/components/nav-bar/nav-bar.component.spec.ts b/src/app/shared/components/nav-bar/nav-bar.component.spec.ts index 408ab20..377fb9c 100644 --- a/src/app/shared/components/nav-bar/nav-bar.component.spec.ts +++ b/src/app/shared/components/nav-bar/nav-bar.component.spec.ts @@ -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({ diff --git a/src/app/shared/components/project-list/project-list.component.ts b/src/app/shared/components/project-list/project-list.component.ts index 456d9c2..b0579c8 100644 --- a/src/app/shared/components/project-list/project-list.component.ts +++ b/src/app/shared/components/project-list/project-list.component.ts @@ -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); } } diff --git a/src/app/shared/components/vertical-profile-item/vertical-profile-item.component.html b/src/app/shared/components/vertical-profile-item/vertical-profile-item.component.html index d1cfdcb..5f513b5 100644 --- a/src/app/shared/components/vertical-profile-item/vertical-profile-item.component.html +++ b/src/app/shared/components/vertical-profile-item/vertical-profile-item.component.html @@ -1,5 +1,5 @@ @if (user() !== undefined) { - +
{ + 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); } diff --git a/src/app/shared/features/filter/filter.component.spec.ts b/src/app/shared/features/filter/filter.component.spec.ts index abdddc4..9b54010 100644 --- a/src/app/shared/features/filter/filter.component.spec.ts +++ b/src/app/shared/features/filter/filter.component.spec.ts @@ -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)), }; diff --git a/src/app/shared/features/login/login.component.spec.ts b/src/app/shared/features/login/login.component.spec.ts index 20f370d..6b563b9 100644 --- a/src/app/shared/features/login/login.component.spec.ts +++ b/src/app/shared/features/login/login.component.spec.ts @@ -42,7 +42,7 @@ describe('LoginComponent', () => { create: jest.fn(), list: jest.fn(), update: jest.fn(), - getByUserId: jest.fn(), + getById: jest.fn(), }; await TestBed.configureTestingModule({ diff --git a/src/app/shared/features/register/register.component.spec.ts b/src/app/shared/features/register/register.component.spec.ts index 9e207e6..a954c83 100644 --- a/src/app/shared/features/register/register.component.spec.ts +++ b/src/app/shared/features/register/register.component.spec.ts @@ -21,7 +21,7 @@ describe('RegisterComponent', () => { create: jest.fn(), list: jest.fn(), update: jest.fn(), - getByUserId: jest.fn(), + getById: jest.fn(), }; mockToastrService = { diff --git a/src/app/shared/features/search/search.component.spec.ts b/src/app/shared/features/search/search.component.spec.ts index bc1f1b8..c1aafac 100644 --- a/src/app/shared/features/search/search.component.spec.ts +++ b/src/app/shared/features/search/search.component.spec.ts @@ -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)), }; diff --git a/src/app/testing/domain/profiles/fake-profile.repository.ts b/src/app/testing/domain/profiles/fake-profile.repository.ts index c54291c..6652659 100644 --- a/src/app/testing/domain/profiles/fake-profile.repository.ts +++ b/src/app/testing/domain/profiles/fake-profile.repository.ts @@ -8,8 +8,8 @@ export class FakeProfileRepository implements ProfileRepository { return of(mockProfilePaginated); } - getByUserId(userId: string): Observable { - const profile = mockProfiles.find((p) => p.utilisateur === userId) ?? ({} as Profile); + getById(profileId: string): Observable { + const profile = mockProfiles.find((p) => p.utilisateur === profileId) ?? ({} as Profile); return of(profile); } diff --git a/src/app/testing/infrastructure/profiles/pb-profile.repository.spec.ts b/src/app/testing/infrastructure/profiles/pb-profile.repository.spec.ts index 87d4e8d..0c3de47 100644 --- a/src/app/testing/infrastructure/profiles/pb-profile.repository.spec.ts +++ b/src/app/testing/infrastructure/profiles/pb-profile.repository.spec.ts @@ -15,7 +15,7 @@ describe('PbProfileRepository', () => { // Création d’un 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]); }); }); diff --git a/src/app/ui/profiles/profile.facade.ts b/src/app/ui/profiles/profile.facade.ts index 9521a75..6e73070 100644 --- a/src/app/ui/profiles/profile.facade.ts +++ b/src/app/ui/profiles/profile.facade.ts @@ -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); diff --git a/src/app/ui/users/user.presenter.ts b/src/app/ui/users/user.presenter.ts index 1cb559c..88b3827 100644 --- a/src/app/ui/users/user.presenter.ts +++ b/src/app/ui/users/user.presenter.ts @@ -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 + } } diff --git a/src/app/usecase/profiles/get-profile.usecase.ts b/src/app/usecase/profiles/get-profile.usecase.ts index 7841dd2..fd01fc0 100644 --- a/src/app/usecase/profiles/get-profile.usecase.ts +++ b/src/app/usecase/profiles/get-profile.usecase.ts @@ -5,7 +5,7 @@ import { Profile } from '@app/domain/profiles/profile.model'; export class GetProfileUseCase { constructor(private readonly repo: ProfileRepository) {} - execute(userId: string): Observable { - return this.repo.getByUserId(userId); + execute(profileId: string): Observable { + return this.repo.getById(profileId); } } diff --git a/src/app/usecase/users/get-user.usecase.ts b/src/app/usecase/users/get-user.usecase.ts index 1839c26..b885023 100644 --- a/src/app/usecase/users/get-user.usecase.ts +++ b/src/app/usecase/users/get-user.usecase.ts @@ -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 { return this.repo.getUserById(userId); } }