Compare commits
12 Commits
a645919aa4
...
e5b1831d9e
| Author | SHA1 | Date | |
|---|---|---|---|
| e5b1831d9e | |||
|
|
7fb776a6f0 | ||
|
|
65ecc516c4 | ||
| 6afb13b2e9 | |||
|
|
5dc8185237 | ||
|
|
735afc1d4a | ||
|
|
45b4dd1dad | ||
|
|
0c768296d1 | ||
|
|
2a9eb55e1b | ||
| d426b75b64 | |||
|
|
4ed6d812db | ||
|
|
4716e82628 |
2
.act.secrets.example
Normal file
2
.act.secrets.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ROBOT_TOKEN=fake_gitea_token
|
||||||
|
ENV_URL=fake_env_url
|
||||||
8
.actrc
8
.actrc
@@ -1,3 +1,5 @@
|
|||||||
--container-architecture linux/amd64
|
--container-architecture linux/arm64
|
||||||
-W .gitea/workflows
|
--workflows .gitea/workflows/
|
||||||
-P ubuntu-latest==ghcr.io/catthehacker/ubuntu:act-latest
|
--platform ubuntu-latest
|
||||||
|
--secret-file .act.secrets
|
||||||
|
--env-file .env
|
||||||
|
|||||||
1
.env.example
Normal file
1
.env.example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
GITEA_SERVER_URL=https://fake-gitea-url.example.com
|
||||||
31
.gitea/workflows/trigger-deploy.yaml
Normal file
31
.gitea/workflows/trigger-deploy.yaml
Normal file
@@ -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\"}}"
|
||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -127,3 +127,11 @@ logs/*.log
|
|||||||
|
|
||||||
pb/data/*
|
pb/data/*
|
||||||
src/environments/environment.development.ts
|
src/environments/environment.development.ts
|
||||||
|
|
||||||
|
*.iml
|
||||||
|
.idea
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
*.secrets
|
||||||
|
.act.secrets
|
||||||
|
.env
|
||||||
|
|||||||
69
package-lock.json
generated
69
package-lock.json
generated
@@ -60,6 +60,7 @@
|
|||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"tailwindcss": "^3.4.12",
|
"tailwindcss": "^3.4.12",
|
||||||
"ts-jest": "^29.4.5",
|
"ts-jest": "^29.4.5",
|
||||||
|
"ts-unused-exports": "^11.0.1",
|
||||||
"typescript": "~5.2.2",
|
"typescript": "~5.2.2",
|
||||||
"typescript-eslint": "8.46.0"
|
"typescript-eslint": "8.46.0"
|
||||||
}
|
}
|
||||||
@@ -6467,6 +6468,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/json5": {
|
||||||
|
"version": "0.0.29",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||||
|
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/mime": {
|
"node_modules/@types/mime": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
|
||||||
@@ -19249,6 +19257,67 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-unused-exports": {
|
||||||
|
"version": "11.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-unused-exports/-/ts-unused-exports-11.0.1.tgz",
|
||||||
|
"integrity": "sha512-b1uIe0B8YfNZjeb+bx62LrB6qaO4CHT8SqMVBkwbwLj7Nh0xQ4J8uV0dS9E6AABId0U4LQ+3yB/HXZBMslGn2A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^4.0.0",
|
||||||
|
"tsconfig-paths": "^3.9.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ts-unused-exports": "bin/ts-unused-exports"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/pzavolinsky/ts-unused-exports?sponsor=1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": ">=3.8.3"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"typescript": {
|
||||||
|
"optional": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsconfig-paths": {
|
||||||
|
"version": "3.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
||||||
|
"integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/json5": "^0.0.29",
|
||||||
|
"json5": "^1.0.2",
|
||||||
|
"minimist": "^1.2.6",
|
||||||
|
"strip-bom": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsconfig-paths/node_modules/json5": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": "^1.2.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"json5": "lib/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tsconfig-paths/node_modules/strip-bom": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tslib": {
|
"node_modules/tslib": {
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "trouve-ton-profile",
|
"name": "trouve-ton-profile",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "bash replace-prod-env.sh src/environments/environment.development.ts $ENV_URL && ng serve",
|
"start": "bash replace-prod-env.sh src/environments/environment.development.ts $ENV_URL && ng serve",
|
||||||
@@ -14,8 +14,7 @@
|
|||||||
"format": "bash replace-prod-env.sh src/environments/environment.development.ts http://localhost:8090 && npm run prettier && npm run lint:fix",
|
"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": "ng lint",
|
||||||
"lint:fix": "ng lint --fix",
|
"lint:fix": "ng lint --fix",
|
||||||
"clean:imports": "ts-unused-exports tsconfig.json --excludePathsFromReport=\"src/main.ts;src/environments\" && npm run 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",
|
||||||
"fix:all": "npm run format && npm run tsc",
|
|
||||||
"check:all": "npm run format && npm run tsc && npm run lint && npm run test",
|
"check:all": "npm run format && npm run tsc && npm run lint && npm run test",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
@@ -77,6 +76,7 @@
|
|||||||
"prettier": "^3.6.2",
|
"prettier": "^3.6.2",
|
||||||
"tailwindcss": "^3.4.12",
|
"tailwindcss": "^3.4.12",
|
||||||
"ts-jest": "^29.4.5",
|
"ts-jest": "^29.4.5",
|
||||||
|
"ts-unused-exports": "^11.0.1",
|
||||||
"typescript": "~5.2.2",
|
"typescript": "~5.2.2",
|
||||||
"typescript-eslint": "8.46.0"
|
"typescript-eslint": "8.46.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ describe('AppComponent', () => {
|
|||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
list: jest.fn(),
|
list: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
getByUserId: jest.fn(),
|
getById: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
@@ -55,9 +55,9 @@ describe('AppComponent', () => {
|
|||||||
expect(app).toBeTruthy();
|
expect(app).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should have the 'TrouveTonProfile' title`, () => {
|
it(`should have the 'TrouveTonProfil' title`, () => {
|
||||||
const fixture = TestBed.createComponent(AppComponent);
|
const fixture = TestBed.createComponent(AppComponent);
|
||||||
const app = fixture.componentInstance;
|
const app = fixture.componentInstance;
|
||||||
expect(app.title).toEqual('TrouveTonProfile');
|
expect(app.title).toEqual('TrouveTonProfil');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ import { ThemeService } from '@app/core/services/theme/theme.service';
|
|||||||
styleUrl: './app.component.scss',
|
styleUrl: './app.component.scss',
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
title = 'TrouveTonProfile';
|
title = 'TrouveTonProfil';
|
||||||
themeService = inject(ThemeService);
|
themeService = inject(ThemeService);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ApplicationConfig } from '@angular/core';
|
|||||||
import {
|
import {
|
||||||
PreloadAllModules,
|
PreloadAllModules,
|
||||||
provideRouter,
|
provideRouter,
|
||||||
|
withComponentInputBinding,
|
||||||
withInMemoryScrolling,
|
withInMemoryScrolling,
|
||||||
withPreloading,
|
withPreloading,
|
||||||
withViewTransitions,
|
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 { PbUserRepository } from '@app/infrastructure/users/pb-user.repository';
|
||||||
import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token';
|
import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token';
|
||||||
import { PbAuthRepository } from '@app/infrastructure/authentification/pb-auth.repository';
|
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 = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
@@ -31,7 +34,8 @@ export const appConfig: ApplicationConfig = {
|
|||||||
withInMemoryScrolling({
|
withInMemoryScrolling({
|
||||||
scrollPositionRestoration: 'enabled',
|
scrollPositionRestoration: 'enabled',
|
||||||
anchorScrolling: 'enabled',
|
anchorScrolling: 'enabled',
|
||||||
})
|
}),
|
||||||
|
withComponentInputBinding()
|
||||||
),
|
),
|
||||||
provideAnimations(),
|
provideAnimations(),
|
||||||
provideHttpClient(withFetch()),
|
provideHttpClient(withFetch()),
|
||||||
@@ -40,6 +44,7 @@ export const appConfig: ApplicationConfig = {
|
|||||||
{ provide: SECTOR_REPOSITORY_TOKEN, useExisting: PbSectorRepository },
|
{ provide: SECTOR_REPOSITORY_TOKEN, useExisting: PbSectorRepository },
|
||||||
{ provide: USER_REPOSITORY_TOKEN, useExisting: PbUserRepository },
|
{ provide: USER_REPOSITORY_TOKEN, useExisting: PbUserRepository },
|
||||||
{ provide: AUTH_REPOSITORY_TOKEN, useExisting: PbAuthRepository },
|
{ provide: AUTH_REPOSITORY_TOKEN, useExisting: PbAuthRepository },
|
||||||
|
{ provide: WEB_SHARE_SERVICE_TOKEN, useExisting: WebShareService },
|
||||||
provideToastr({
|
provideToastr({
|
||||||
timeOut: 10000,
|
timeOut: 10000,
|
||||||
positionClass: 'toast-top-right',
|
positionClass: 'toast-top-right',
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ import { TestBed } from '@angular/core/testing';
|
|||||||
|
|
||||||
import { authGuard } from './auth.guard';
|
import { authGuard } from './auth.guard';
|
||||||
import { CanActivateFn, Router, UrlTree } from '@angular/router';
|
import { CanActivateFn, Router, UrlTree } from '@angular/router';
|
||||||
import { AuthRepository } from '@app/domain/authentification/auth.repository';
|
|
||||||
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
|
||||||
import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token';
|
|
||||||
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
|
||||||
import { AuthFacade } from '@app/ui/authentification/auth.facade';
|
import { AuthFacade } from '@app/ui/authentification/auth.facade';
|
||||||
|
|
||||||
describe('authGuard', () => {
|
describe('authGuard', () => {
|
||||||
|
|||||||
@@ -12,3 +12,11 @@ export interface Profile {
|
|||||||
projets: string[];
|
projets: string[];
|
||||||
apropos: string;
|
apropos: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProfilePaginated {
|
||||||
|
page: number;
|
||||||
|
perPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
totalItems: number;
|
||||||
|
items: any[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Profile } from '@app/domain/profiles/profile.model';
|
import { Profile, ProfilePaginated } from '@app/domain/profiles/profile.model';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
import { SearchFilters } from '@app/domain/search/search-filters';
|
||||||
|
|
||||||
export interface ProfileRepository {
|
export interface ProfileRepository {
|
||||||
list(params?: { search?: string; page?: number; pageSize?: number }): Observable<Profile[]>;
|
list(params?: SearchFilters): Observable<ProfilePaginated>;
|
||||||
getByUserId(userId: string): Observable<Profile>;
|
getById(profileId: string): Observable<Profile>;
|
||||||
create(profile: Profile): Observable<Profile>;
|
create(profile: Profile): Observable<Profile>;
|
||||||
update(profileId: string, profile: Partial<Profile>): Observable<Profile>;
|
update(profileId: string, profile: Partial<Profile>): Observable<Profile>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,8 @@ export interface SearchFilters {
|
|||||||
secteur: string | null;
|
secteur: string | null;
|
||||||
profession: string | null;
|
profession: string | null;
|
||||||
sort: string;
|
sort: string;
|
||||||
|
page?: number;
|
||||||
|
perPage?: number;
|
||||||
|
totalPages?: number;
|
||||||
|
totalItems?: number;
|
||||||
}
|
}
|
||||||
13
src/app/domain/search/search.repository.ts
Normal file
13
src/app/domain/search/search.repository.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { SearchFilters } from '@app/domain/search/search-filters';
|
||||||
|
import { WritableSignal } from '@angular/core';
|
||||||
|
|
||||||
|
export interface SearchRepository {
|
||||||
|
search(query: string): void;
|
||||||
|
sortBy(value: string): void;
|
||||||
|
filterByProfileVerified(): void;
|
||||||
|
filterBySecteur(secteur: string): void;
|
||||||
|
filterByProfession(profession: string): void;
|
||||||
|
reset(): void;
|
||||||
|
getFilters(): WritableSignal<SearchFilters>;
|
||||||
|
setFilters(filters: SearchFilters): void;
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export interface CreateSectorDto {
|
|
||||||
id: string;
|
|
||||||
created: string;
|
|
||||||
updated: string;
|
|
||||||
nom: string;
|
|
||||||
}
|
|
||||||
3
src/app/domain/shareData/share-data.repository.ts
Normal file
3
src/app/domain/shareData/share-data.repository.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export abstract class ShareDataRepository {
|
||||||
|
abstract share(shareData: ShareData): void;
|
||||||
|
}
|
||||||
5
src/app/domain/shareData/share-data.ts
Normal file
5
src/app/domain/shareData/share-data.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface ShareData {
|
||||||
|
title: string;
|
||||||
|
text: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
@@ -1,28 +1,37 @@
|
|||||||
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
||||||
import { from, Observable } from 'rxjs';
|
import { from, Observable } from 'rxjs';
|
||||||
import { Profile } from '@app/domain/profiles/profile.model';
|
import { Profile, ProfilePaginated } from '@app/domain/profiles/profile.model';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import PocketBase from 'pocketbase';
|
import PocketBase from 'pocketbase';
|
||||||
import { environment } from '@env/environment';
|
import { environment } from '@env/environment';
|
||||||
import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
|
import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
|
||||||
|
import { SearchFilters } from '@app/domain/search/search-filters';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class PbProfileRepository implements ProfileRepository {
|
export class PbProfileRepository implements ProfileRepository {
|
||||||
private pb = new PocketBase(environment.baseUrl);
|
private pb = new PocketBase(environment.baseUrl);
|
||||||
|
|
||||||
list(): Observable<Profile[]> {
|
private defaultOptions = {
|
||||||
const options = {
|
expand: 'utilisateur,secteur',
|
||||||
sort: 'profession',
|
};
|
||||||
expand: 'utilisateur',
|
|
||||||
filter:
|
list(params?: SearchFilters): Observable<ProfilePaginated> {
|
||||||
"utilisateur.verified=true && utilisateur.name !='' && profession!='Profession non renseignée' && secteur!='' ",
|
const requestOptions = {
|
||||||
|
...this.defaultOptions,
|
||||||
|
sort: this.onSortSetting(params),
|
||||||
|
filter: this.onFilterSetting(params).join(' && '),
|
||||||
};
|
};
|
||||||
return from(this.pb.collection('profiles').getFullList<Profile>(options));
|
|
||||||
|
return from(
|
||||||
|
this.pb
|
||||||
|
.collection('profiles')
|
||||||
|
.getList<ProfilePaginated>(params?.page, params?.perPage, requestOptions)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getByUserId(userId: string): Observable<Profile> {
|
getById(userId: string): Observable<Profile> {
|
||||||
return from(
|
return from(
|
||||||
this.pb.collection('profiles').getFirstListItem<Profile>(`utilisateur="${userId}"`)
|
this.pb.collection('profiles').getOne<Profile>(`${userId}`, { expand: 'utilisateur' })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,4 +42,57 @@ export class PbProfileRepository implements ProfileRepository {
|
|||||||
update(id: string, data: Partial<Profile>): Observable<Profile> {
|
update(id: string, data: Partial<Profile>): Observable<Profile> {
|
||||||
return from(this.pb.collection('profiles').update<Profile>(id, data));
|
return from(this.pb.collection('profiles').update<Profile>(id, data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onFilterSetting(params?: SearchFilters): string[] {
|
||||||
|
const filters: string[] = [
|
||||||
|
'utilisateur.verified = true',
|
||||||
|
"utilisateur.name != ''",
|
||||||
|
"profession != 'Profession non renseignée'",
|
||||||
|
"secteur != ''",
|
||||||
|
];
|
||||||
|
|
||||||
|
if (params) {
|
||||||
|
if (params.secteur) {
|
||||||
|
filters.push(`secteur.nom ~ '${params.secteur}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.profession) {
|
||||||
|
filters.push(`profession ~ '${params.profession}'`);
|
||||||
|
}
|
||||||
|
if (params.search) {
|
||||||
|
filters.push(`utilisateur.name ~ '${params.search}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.verified) {
|
||||||
|
filters.push('estVerifier = true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
private onSortSetting(params?: SearchFilters): string {
|
||||||
|
let sortSetting = '-created';
|
||||||
|
|
||||||
|
if (params?.sort) {
|
||||||
|
switch (params.sort) {
|
||||||
|
case 'recent':
|
||||||
|
sortSetting = '-created'; // Du plus récent au plus vieux
|
||||||
|
break;
|
||||||
|
case 'name-asc':
|
||||||
|
sortSetting = '+utilisateur.name'; // Alphabétique A-Z
|
||||||
|
break;
|
||||||
|
case 'name-desc':
|
||||||
|
sortSetting = '-utilisateur.name'; // Alphabétique Z-A
|
||||||
|
break;
|
||||||
|
case 'verified':
|
||||||
|
sortSetting = '-estVerifier,-created';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
sortSetting = '-created';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortSetting;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
src/app/infrastructure/search/search.service.ts
Normal file
64
src/app/infrastructure/search/search.service.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { Injectable, signal, WritableSignal } from '@angular/core';
|
||||||
|
import { SearchRepository } from '@app/domain/search/search.repository';
|
||||||
|
import { SearchFilters } from '@app/domain/search/search-filters';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class SearchService implements SearchRepository {
|
||||||
|
private filters = signal<SearchFilters>({
|
||||||
|
search: '',
|
||||||
|
verified: false,
|
||||||
|
secteur: null,
|
||||||
|
profession: null,
|
||||||
|
sort: 'recent',
|
||||||
|
page: 1,
|
||||||
|
perPage: 25,
|
||||||
|
});
|
||||||
|
|
||||||
|
setFilters(filters: SearchFilters) {
|
||||||
|
const filtersToSet = { ...this.filters(), ...filters };
|
||||||
|
this.filters.set(filtersToSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
filterByProfession(profession: string | null) {
|
||||||
|
const filter = { ...this.filters(), profession };
|
||||||
|
this.filters.set(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
filterByProfileVerified() {
|
||||||
|
const filters = { ...this.filters(), verified: true };
|
||||||
|
this.filters.set(filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
filterBySecteur(secteur: string | null) {
|
||||||
|
const filters = { ...this.filters(), secteur };
|
||||||
|
this.filters.set(filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFilters(): WritableSignal<SearchFilters> {
|
||||||
|
return this.filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
const filters = {
|
||||||
|
...this.filters(),
|
||||||
|
verified: false,
|
||||||
|
sort: 'recent',
|
||||||
|
search: '',
|
||||||
|
profession: null,
|
||||||
|
secteur: null,
|
||||||
|
};
|
||||||
|
this.filters.set(filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
search(query: string) {
|
||||||
|
const filters = { ...this.filters(), search: query };
|
||||||
|
this.filters.set(filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
sortBy(value: string) {
|
||||||
|
const filters = { ...this.filters(), sort: value };
|
||||||
|
this.filters.set(filters);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
);
|
||||||
36
src/app/infrastructure/shareData/web-share.service.ts
Normal file
36
src/app/infrastructure/shareData/web-share.service.ts
Normal 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,38 +4,25 @@ import { HomeComponent } from './home.component';
|
|||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
||||||
import { SectorRepository } from '@app/domain/sectors/sector.repository';
|
import { SectorRepository } from '@app/domain/sectors/sector.repository';
|
||||||
import { of } from 'rxjs';
|
|
||||||
import { Profile } from '@app/domain/profiles/profile.model';
|
|
||||||
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
||||||
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
|
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
|
||||||
import { Sector } from '@app/domain/sectors/sector.model';
|
import { mockSectorRepo } from '@app/testing/sector.mock';
|
||||||
|
import { mockProfileRepo } from '@app/testing/profile.mock';
|
||||||
|
|
||||||
describe('HomeComponent', () => {
|
describe('HomeComponent', () => {
|
||||||
let component: HomeComponent;
|
let component: HomeComponent;
|
||||||
let fixture: ComponentFixture<HomeComponent>;
|
let fixture: ComponentFixture<HomeComponent>;
|
||||||
|
|
||||||
let mockProfileRepo: jest.Mocked<Partial<ProfileRepository>>;
|
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>> = mockProfileRepo;
|
||||||
let mockSectorRepo: jest.Mocked<Partial<SectorRepository>>;
|
let mockSectorRepository: jest.Mocked<Partial<SectorRepository>> = mockSectorRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockProfileRepo = {
|
|
||||||
create: jest.fn().mockReturnValue(of({} as Profile)),
|
|
||||||
list: jest.fn().mockReturnValue(of([])),
|
|
||||||
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
|
|
||||||
update: jest.fn().mockReturnValue(of({} as Profile)),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockSectorRepo = {
|
|
||||||
list: jest.fn().mockReturnValue(of([])),
|
|
||||||
getOne: jest.fn().mockReturnValue(of({} as Sector)),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [HomeComponent],
|
imports: [HomeComponent],
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
|
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository },
|
||||||
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepo },
|
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepository },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Component, inject } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
import { SearchComponent } from '@app/shared/features/search/search.component';
|
import { SearchComponent } from '@app/shared/features/search/search.component';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { SearchFilters } from '@app/domain/search-filters';
|
import { SearchFilters } from '@app/domain/search/search-filters';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-home',
|
selector: 'app-home',
|
||||||
|
|||||||
@@ -78,8 +78,12 @@
|
|||||||
<img
|
<img
|
||||||
alt="{{ user()!.username }}"
|
alt="{{ user()!.username }}"
|
||||||
class="object-cover w-full h-full"
|
class="object-cover w-full h-full"
|
||||||
src="https://api.dicebear.com/9.x/adventurer/svg?seed={{
|
src="https://api.dicebear.com/9.x/initials/svg?seed={{
|
||||||
user()!.username
|
user().name
|
||||||
|
? user().name
|
||||||
|
: user().username
|
||||||
|
? user().username
|
||||||
|
: user().email
|
||||||
}}"
|
}}"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,62 +4,33 @@ import { MyProfileComponent } from './my-profile.component';
|
|||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
||||||
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
||||||
import { of } from 'rxjs';
|
|
||||||
import { Profile } from '@app/domain/profiles/profile.model';
|
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository.token';
|
import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository.token';
|
||||||
import { UserRepository } from '@app/domain/users/user.repository';
|
import { UserRepository } from '@app/domain/users/user.repository';
|
||||||
import { AuthRepository } from '@app/domain/authentification/auth.repository';
|
import { AuthRepository } from '@app/domain/authentification/auth.repository';
|
||||||
import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token';
|
import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token';
|
||||||
|
import { mockProfileRepo } from '@app/testing/profile.mock';
|
||||||
|
import { mockToastR } from '@app/testing/toastr.mock';
|
||||||
|
import { mockUserRepo } from '@app/testing/user.mock';
|
||||||
|
import { mockAuthRepo } from '@app/testing/auth.mock';
|
||||||
|
|
||||||
describe('MyProfileComponent', () => {
|
describe('MyProfileComponent', () => {
|
||||||
let component: MyProfileComponent;
|
let component: MyProfileComponent;
|
||||||
let fixture: ComponentFixture<MyProfileComponent>;
|
let fixture: ComponentFixture<MyProfileComponent>;
|
||||||
|
|
||||||
let mockProfileRepo: ProfileRepository;
|
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>> = mockProfileRepo;
|
||||||
let mockToastrService: Partial<ToastrService>;
|
let mockToastrService: jest.Mocked<Partial<ToastrService>> = mockToastR;
|
||||||
let mockUserRepo: Partial<UserRepository>;
|
let mockUserRepository: jest.Mocked<Partial<UserRepository>> = mockUserRepo;
|
||||||
let mockAuthRepository: jest.Mocked<Partial<AuthRepository>>;
|
let mockAuthRepository: jest.Mocked<Partial<AuthRepository>> = mockAuthRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockProfileRepo = {
|
|
||||||
create: jest.fn(),
|
|
||||||
list: jest.fn(),
|
|
||||||
update: jest.fn(),
|
|
||||||
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockUserRepo = {
|
|
||||||
update: jest.fn(),
|
|
||||||
getUserById: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockToastrService = {
|
|
||||||
warning: jest.fn(),
|
|
||||||
success: jest.fn(),
|
|
||||||
info: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockAuthRepository = {
|
|
||||||
get: jest.fn(),
|
|
||||||
login: jest.fn(),
|
|
||||||
sendVerificationEmail: jest.fn(),
|
|
||||||
logout: jest.fn(),
|
|
||||||
isAuthenticated: jest.fn(),
|
|
||||||
isEmailVerified: jest.fn(),
|
|
||||||
register: jest.fn(),
|
|
||||||
sendRequestPasswordReset: jest.fn(),
|
|
||||||
confirmPasswordReset: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [MyProfileComponent],
|
imports: [MyProfileComponent],
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
|
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository },
|
||||||
{ provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository },
|
{ provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository },
|
||||||
{ provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepo },
|
{ provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepository },
|
||||||
{ provide: ToastrService, useValue: mockToastrService },
|
{ provide: ToastrService, useValue: mockToastrService },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|||||||
@@ -9,11 +9,13 @@
|
|||||||
<!-- Overlay gradient -->
|
<!-- Overlay gradient -->
|
||||||
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-black/30"></div>
|
<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">
|
<div class="relative z-10 p-4 flex justify-between items-start">
|
||||||
|
<!-- Bouton retour -->
|
||||||
<a
|
<a
|
||||||
[routerLink]="['/profiles']"
|
[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"
|
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
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -30,27 +32,53 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Badge vérifié -->
|
<!-- Conteneur pour badge vérifié et bouton partage -->
|
||||||
@if (profile().estVerifier) {
|
<div class="flex items-center gap-2">
|
||||||
<div
|
<!-- Badge vérifié -->
|
||||||
class="flex items-center gap-2 bg-purple-500/20 backdrop-blur-md px-3 py-2 rounded-full animate-pulse-slow"
|
@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
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="currentColor"
|
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
|
<path
|
||||||
fill-rule="evenodd"
|
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"
|
clip-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="text-white text-sm font-medium hidden md:inline">Vérifié</span>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -65,7 +93,7 @@
|
|||||||
<div class="w-full h-full rounded-full overflow-hidden bg-white">
|
<div class="w-full h-full rounded-full overflow-hidden bg-white">
|
||||||
@if (user().avatar) {
|
@if (user().avatar) {
|
||||||
<img
|
<img
|
||||||
alt="{{ user().username }}"
|
alt="{{ user().name }}"
|
||||||
class="object-cover w-full h-full"
|
class="object-cover w-full h-full"
|
||||||
src="{{ environment.baseUrl }}/api/files/users/{{ user().id }}/{{
|
src="{{ environment.baseUrl }}/api/files/users/{{ user().id }}/{{
|
||||||
user().avatar
|
user().avatar
|
||||||
@@ -74,9 +102,9 @@
|
|||||||
/>
|
/>
|
||||||
} @else {
|
} @else {
|
||||||
<img
|
<img
|
||||||
alt="{{ user().username }}"
|
alt="{{ user().name }}"
|
||||||
class="object-cover w-full h-full"
|
class="object-cover w-full h-full"
|
||||||
src="https://api.dicebear.com/9.x/adventurer/svg?seed={{ user().username }}"
|
src="https://api.dicebear.com/9.x/initials/svg?seed={{ user().name }}"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -96,10 +124,6 @@
|
|||||||
<h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1">
|
<h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1">
|
||||||
{{ user().name }}
|
{{ user().name }}
|
||||||
</h1>
|
</h1>
|
||||||
} @else if (user().username) {
|
|
||||||
<h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1">
|
|
||||||
{{ user().username }}
|
|
||||||
</h1>
|
|
||||||
} @else {
|
} @else {
|
||||||
<h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1">
|
<h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1">
|
||||||
{{ user().email }}
|
{{ user().email }}
|
||||||
@@ -107,7 +131,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<p class="text-lg md:text-xl text-indigo-600 dark:text-indigo-400 font-semibold">
|
<p class="text-lg md:text-xl text-indigo-600 dark:text-indigo-400 font-semibold">
|
||||||
{{ profile().profession | uppercase }}
|
{{ profile()!.profession | uppercase }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,9 +163,9 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Biographie
|
Biographie
|
||||||
</h3>
|
</h3>
|
||||||
@if (profile().bio) {
|
@if (profile()!.bio) {
|
||||||
<p class="text-gray-600 dark:text-gray-300 text-sm leading-relaxed">
|
<p class="text-gray-600 dark:text-gray-300 text-sm leading-relaxed">
|
||||||
{{ profile().bio }}
|
{{ profile()!.bio }}
|
||||||
</p>
|
</p>
|
||||||
} @else {
|
} @else {
|
||||||
<p class="text-gray-600 dark:text-gray-300 text-sm leading-relaxed">
|
<p class="text-gray-600 dark:text-gray-300 text-sm leading-relaxed">
|
||||||
@@ -153,7 +177,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card Secteur -->
|
<!-- Card Secteur -->
|
||||||
@if (profile().secteur) {
|
@if (profile()!.secteur) {
|
||||||
<div
|
<div
|
||||||
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow duration-300"
|
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow duration-300"
|
||||||
>
|
>
|
||||||
@@ -172,12 +196,12 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Secteur
|
Secteur
|
||||||
</h3>
|
</h3>
|
||||||
<app-chips [sectorId]="profile().secteur" />
|
<app-chips [sectorId]="profile()!.secteur" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Card Réseaux -->
|
<!-- Card Réseaux -->
|
||||||
@if (profile().reseaux) {
|
@if (profile()!.reseaux) {
|
||||||
<div
|
<div
|
||||||
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow duration-300"
|
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow duration-300"
|
||||||
>
|
>
|
||||||
@@ -199,7 +223,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Réseaux
|
Réseaux
|
||||||
</h3>
|
</h3>
|
||||||
<app-reseaux [reseaux]="profile().reseaux" />
|
<app-reseaux [reseaux]="profile()!.reseaux" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -207,7 +231,7 @@
|
|||||||
<!-- Colonne droite - À propos et Projets -->
|
<!-- Colonne droite - À propos et Projets -->
|
||||||
<div class="lg:col-span-2 space-y-6 animate-slide-up animation-delay-300">
|
<div class="lg:col-span-2 space-y-6 animate-slide-up animation-delay-300">
|
||||||
<!-- Card À propos -->
|
<!-- Card À propos -->
|
||||||
@if (profile().apropos) {
|
@if (profile()!.apropos) {
|
||||||
<div
|
<div
|
||||||
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 md:p-8 hover:shadow-xl transition-shadow duration-300"
|
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 md:p-8 hover:shadow-xl transition-shadow duration-300"
|
||||||
>
|
>
|
||||||
@@ -229,7 +253,7 @@
|
|||||||
À propos
|
À propos
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed text-base">
|
<p class="text-gray-700 dark:text-gray-300 leading-relaxed text-base">
|
||||||
{{ profile().apropos }}
|
{{ profile()!.apropos }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,37 +4,39 @@ import { ProfileDetailComponent } from './profile-detail.component';
|
|||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
|
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
|
||||||
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
||||||
import { of } from 'rxjs';
|
|
||||||
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 { SectorRepository } from '@app/domain/sectors/sector.repository';
|
||||||
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
|
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 { 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 { mockProfileRepo } from '@app/testing/profile.mock';
|
||||||
|
import { mockSectorRepo } from '@app/testing/sector.mock';
|
||||||
|
import { mockToastR } from '@app/testing/toastr.mock';
|
||||||
|
import { mockUserRepo } from '@app/testing/user.mock';
|
||||||
|
import { mockProjectRepo } from '@app/testing/project.mock';
|
||||||
|
|
||||||
describe('ProfileDetailComponent', () => {
|
describe('ProfileDetailComponent', () => {
|
||||||
let component: ProfileDetailComponent;
|
let component: ProfileDetailComponent;
|
||||||
let fixture: ComponentFixture<ProfileDetailComponent>;
|
let fixture: ComponentFixture<ProfileDetailComponent>;
|
||||||
let mockProjectRepository: jest.Mocked<ProjectRepository>;
|
|
||||||
let mockSectorRepo: SectorRepository;
|
let mockProjectRepository: jest.Mocked<Partial<ProjectRepository>> = mockProjectRepo;
|
||||||
|
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>> = mockProfileRepo;
|
||||||
|
let mockSectorRepository: jest.Mocked<Partial<SectorRepository>> = mockSectorRepo;
|
||||||
|
let mockToastr: jest.Mocked<Partial<ToastrService>> = mockToastR;
|
||||||
|
let mockUserRepository: jest.Mocked<Partial<UserRepository>> = mockUserRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockProjectRepository = {
|
|
||||||
create: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
list: jest.fn().mockReturnValue(of([])),
|
|
||||||
get: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
update: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockSectorRepo = {
|
|
||||||
list: jest.fn(),
|
|
||||||
getOne: jest.fn().mockReturnValue(of({} as Sector)),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ProfileDetailComponent],
|
imports: [ProfileDetailComponent],
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{ provide: PROJECT_REPOSITORY_TOKEN, useValue: mockProjectRepository },
|
{ provide: PROJECT_REPOSITORY_TOKEN, useValue: mockProjectRepository },
|
||||||
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepo },
|
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository },
|
||||||
|
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepository },
|
||||||
|
{ provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepository },
|
||||||
|
{ provide: ToastrService, useValue: mockToastr },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
|
|||||||
@@ -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 { ActivatedRoute, RouterLink } from '@angular/router';
|
||||||
import { UpperCasePipe } from '@angular/common';
|
import { UpperCasePipe } from '@angular/common';
|
||||||
import { User } from '@app/domain/users/user.model';
|
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 { UntilDestroy } from '@ngneat/until-destroy';
|
||||||
import { ProjectListComponent } from '@app/shared/components/project-list/project-list.component';
|
import { ProjectListComponent } from '@app/shared/components/project-list/project-list.component';
|
||||||
import { environment } from '@env/environment';
|
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({
|
@Component({
|
||||||
selector: 'app-profile-detail',
|
selector: 'app-profile-detail',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ChipsComponent, ReseauxComponent, RouterLink, UpperCasePipe, ProjectListComponent],
|
imports: [ChipsComponent, ReseauxComponent, RouterLink, UpperCasePipe, ProjectListComponent],
|
||||||
|
providers: [UserFacade],
|
||||||
templateUrl: './profile-detail.component.html',
|
templateUrl: './profile-detail.component.html',
|
||||||
styleUrl: './profile-detail.component.scss',
|
styleUrl: './profile-detail.component.scss',
|
||||||
})
|
})
|
||||||
@UntilDestroy()
|
@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 environment = environment;
|
||||||
|
protected readonly ActionType = ActionType;
|
||||||
private readonly route = inject(ActivatedRoute);
|
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(() => {
|
slug = computed(() => this.route.snapshot.params['name'] ?? '');
|
||||||
if (this.extraData != undefined) return this.extraData.user;
|
|
||||||
return {} as User;
|
|
||||||
});
|
|
||||||
|
|
||||||
protected profile = computed(() => {
|
protected user = this.userFacade.user;
|
||||||
if (this.extraData != undefined) return this.extraData.profile;
|
protected readonly userLoading = this.userFacade.loading;
|
||||||
return {} as Profile;
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,9 +37,15 @@
|
|||||||
<!-- Barre de recherche améliorée -->
|
<!-- Barre de recherche améliorée -->
|
||||||
<div class="max-w-3xl mx-auto animate-slide-up animation-delay-200">
|
<div class="max-w-3xl mx-auto animate-slide-up animation-delay-200">
|
||||||
<div
|
<div
|
||||||
class="bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-4 sm:p-6 hover:shadow-2xl transition-shadow duration-300"
|
class="flex flex-col space-y-8 bg-white dark:bg-gray-800 rounded-2xl shadow-xl p-4 sm:p-6 hover:shadow-2xl transition-shadow duration-300"
|
||||||
>
|
>
|
||||||
<app-search (onSearchChange)="showNewQuery($event)" />
|
<div>
|
||||||
|
<app-search (onSearchChange)="showNewQuery($event)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<app-filter (onFilterChange)="onFilterChange($event)" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -64,13 +70,17 @@
|
|||||||
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"
|
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>
|
</svg>
|
||||||
<span>{{ profiles().length }} profil(s)</span>
|
<span>{{ profilePaginated().items.length }} profil(s)</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Liste des profils avec animation d'apparition -->
|
<!-- Liste des profils avec animation d'apparition -->
|
||||||
<div class="animate-slide-up animation-delay-100">
|
<div class="animate-slide-up animation-delay-100">
|
||||||
<app-vertical-profile-list [profiles]="profiles()" />
|
<app-vertical-profile-list [profiles]="profilePaginated().items" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="animate-slide-up animation-delay-100">
|
||||||
|
<app-pagination [filters]="searchFilters()" (onPageChange)="onPageChange($event)" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,33 +2,20 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
|
|
||||||
import { ProfileListComponent } from './profile-list.component';
|
import { ProfileListComponent } from './profile-list.component';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { of } from 'rxjs';
|
|
||||||
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
||||||
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
||||||
import { Profile } from '@app/domain/profiles/profile.model';
|
|
||||||
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
|
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
|
||||||
import { SectorRepository } from '@app/domain/sectors/sector.repository';
|
import { SectorRepository } from '@app/domain/sectors/sector.repository';
|
||||||
import { Sector } from '@app/domain/sectors/sector.model';
|
import { mockProfileRepo } from '@app/testing/profile.mock';
|
||||||
|
import { mockSectorRepo } from '@app/testing/sector.mock';
|
||||||
|
|
||||||
describe('ProfileListComponent', () => {
|
describe('ProfileListComponent', () => {
|
||||||
let component: ProfileListComponent;
|
let component: ProfileListComponent;
|
||||||
let fixture: ComponentFixture<ProfileListComponent>;
|
let fixture: ComponentFixture<ProfileListComponent>;
|
||||||
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>>;
|
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>> = mockProfileRepo;
|
||||||
let mockSectorRepository: jest.Mocked<Partial<SectorRepository>>;
|
let mockSectorRepository: jest.Mocked<Partial<SectorRepository>> = mockSectorRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockProfileRepository = {
|
|
||||||
create: jest.fn().mockReturnValue(of({} as Profile)),
|
|
||||||
list: jest.fn().mockReturnValue(of([])),
|
|
||||||
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
|
|
||||||
update: jest.fn().mockReturnValue(of({} as Profile)),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockSectorRepository = {
|
|
||||||
list: jest.fn().mockReturnValue(of([])),
|
|
||||||
getOne: jest.fn().mockReturnValue(of({} as Sector)),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ProfileListComponent],
|
imports: [ProfileListComponent],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -1,34 +1,47 @@
|
|||||||
import { Component, inject, OnInit } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
import { SearchComponent } from '@app/shared/features/search/search.component';
|
import { SearchComponent } from '@app/shared/features/search/search.component';
|
||||||
import { VerticalProfileListComponent } from '@app/shared/components/vertical-profile-list/vertical-profile-list.component';
|
import { VerticalProfileListComponent } from '@app/shared/components/vertical-profile-list/vertical-profile-list.component';
|
||||||
import { UntilDestroy } from '@ngneat/until-destroy';
|
import { UntilDestroy } from '@ngneat/until-destroy';
|
||||||
import { ProfileFacade } from '@app/ui/profiles/profile.facade';
|
import { ProfileFacade } from '@app/ui/profiles/profile.facade';
|
||||||
import { LoadingComponent } from '@app/shared/components/loading/loading.component';
|
import { LoadingComponent } from '@app/shared/components/loading/loading.component';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { SearchFilters } from '@app/domain/search-filters';
|
import { SearchFilters } from '@app/domain/search/search-filters';
|
||||||
|
import { FilterComponent } from '@app/shared/features/filter/filter.component';
|
||||||
|
import { PaginationComponent } from '@app/shared/features/pagination/pagination.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-profile-list',
|
selector: 'app-profile-list',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [SearchComponent, VerticalProfileListComponent, LoadingComponent],
|
imports: [
|
||||||
|
SearchComponent,
|
||||||
|
VerticalProfileListComponent,
|
||||||
|
LoadingComponent,
|
||||||
|
FilterComponent,
|
||||||
|
PaginationComponent,
|
||||||
|
],
|
||||||
templateUrl: './profile-list.component.html',
|
templateUrl: './profile-list.component.html',
|
||||||
styleUrl: './profile-list.component.scss',
|
styleUrl: './profile-list.component.scss',
|
||||||
})
|
})
|
||||||
@UntilDestroy()
|
@UntilDestroy()
|
||||||
export class ProfileListComponent implements OnInit {
|
export class ProfileListComponent {
|
||||||
private readonly facade = inject(ProfileFacade);
|
private readonly facade = inject(ProfileFacade);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
protected readonly profiles = this.facade.profiles;
|
protected readonly searchFilters = this.facade.searchFilters;
|
||||||
|
protected readonly profilePaginated = this.facade.profilePaginated;
|
||||||
protected readonly loading = this.facade.loading;
|
protected readonly loading = this.facade.loading;
|
||||||
protected readonly error = this.facade.error;
|
protected readonly error = this.facade.error;
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.facade.load();
|
|
||||||
}
|
|
||||||
|
|
||||||
showNewQuery(filters: SearchFilters) {
|
showNewQuery(filters: SearchFilters) {
|
||||||
console.log(filters);
|
this.facade.load(filters);
|
||||||
this.router.navigate(['/profiles'], { queryParams: { search: filters.search } });
|
this.router.navigate(['/profiles'], { queryParams: { search: filters.search } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onFilterChange(filters: SearchFilters) {
|
||||||
|
this.facade.load(filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
onPageChange(filters: SearchFilters) {
|
||||||
|
this.facade.load(filters);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,27 +3,21 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
import { ChipsComponent } from './chips.component';
|
import { ChipsComponent } from './chips.component';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
|
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
|
||||||
import { of } from 'rxjs';
|
|
||||||
import { Sector } from '@app/domain/sectors/sector.model';
|
|
||||||
import { SectorRepository } from '@app/domain/sectors/sector.repository';
|
import { SectorRepository } from '@app/domain/sectors/sector.repository';
|
||||||
|
import { mockSectorRepo } from '@app/testing/sector.mock';
|
||||||
|
|
||||||
describe('ChipsComponent', () => {
|
describe('ChipsComponent', () => {
|
||||||
let component: ChipsComponent;
|
let component: ChipsComponent;
|
||||||
let fixture: ComponentFixture<ChipsComponent>;
|
let fixture: ComponentFixture<ChipsComponent>;
|
||||||
|
|
||||||
let mockSectorRepo: SectorRepository;
|
let mockSectorRepository: jest.Mocked<Partial<SectorRepository>> = mockSectorRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockSectorRepo = {
|
|
||||||
list: jest.fn(),
|
|
||||||
getOne: jest.fn().mockReturnValue(of({} as Sector)),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ChipsComponent],
|
imports: [ChipsComponent],
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepo },
|
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepository },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<span
|
<span
|
||||||
class="text-xl sm:text-2xl font-semibold text-gray-900 dark:text-white group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors"
|
class="text-xl sm:text-2xl font-semibold text-gray-900 dark:text-white group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors"
|
||||||
>
|
>
|
||||||
TrouveTonProfile
|
TrouveTonProfil
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
<!-- Copyright (optionnel) -->
|
<!-- Copyright (optionnel) -->
|
||||||
<div class="pt-6 border-t border-gray-200 dark:border-gray-800">
|
<div class="pt-6 border-t border-gray-200 dark:border-gray-800">
|
||||||
<p class="text-center text-xs sm:text-sm text-gray-500 dark:text-gray-500">
|
<p class="text-center text-xs sm:text-sm text-gray-500 dark:text-gray-500">
|
||||||
© {{ currentYear }} TrouveTonProfile. Tous droits réservés.
|
© {{ currentYear }} TrouveTonProfil. Tous droits réservés.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,21 +6,15 @@ import { Project } from '@app/domain/projects/project.model';
|
|||||||
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
|
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
|
||||||
|
import { mockProjectRepo } from '@app/testing/project.mock';
|
||||||
|
|
||||||
describe('MyProfileProjectItemComponent', () => {
|
describe('MyProfileProjectItemComponent', () => {
|
||||||
let component: MyProfileProjectItemComponent;
|
let component: MyProfileProjectItemComponent;
|
||||||
let fixture: ComponentFixture<MyProfileProjectItemComponent>;
|
let fixture: ComponentFixture<MyProfileProjectItemComponent>;
|
||||||
|
|
||||||
let mockProjectRepository: jest.Mocked<ProjectRepository>;
|
let mockProjectRepository: jest.Mocked<Partial<ProjectRepository>> = mockProjectRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockProjectRepository = {
|
|
||||||
create: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
list: jest.fn().mockReturnValue(of([])),
|
|
||||||
get: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
update: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [MyProfileProjectItemComponent],
|
imports: [MyProfileProjectItemComponent],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -2,24 +2,16 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
|
|
||||||
import { MyProfileProjectListComponent } from './my-profile-project-list.component';
|
import { MyProfileProjectListComponent } from './my-profile-project-list.component';
|
||||||
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
||||||
import { of } from 'rxjs';
|
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
|
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
|
||||||
import { Project } from '@app/domain/projects/project.model';
|
import { mockProfileRepo } from '@app/testing/profile.mock';
|
||||||
|
|
||||||
describe('MyProfileProjectListComponent', () => {
|
describe('MyProfileProjectListComponent', () => {
|
||||||
let component: MyProfileProjectListComponent;
|
let component: MyProfileProjectListComponent;
|
||||||
let fixture: ComponentFixture<MyProfileProjectListComponent>;
|
let fixture: ComponentFixture<MyProfileProjectListComponent>;
|
||||||
let mockProjectRepository: jest.Mocked<ProjectRepository>;
|
let mockProjectRepository: jest.Mocked<Partial<ProjectRepository>> = mockProfileRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockProjectRepository = {
|
|
||||||
create: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
list: jest.fn().mockReturnValue(of([])),
|
|
||||||
get: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
update: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [MyProfileProjectListComponent],
|
imports: [MyProfileProjectListComponent],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -3,37 +3,24 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
import { MyProfileUpdateCvFormComponent } from './my-profile-update-cv-form.component';
|
import { MyProfileUpdateCvFormComponent } from './my-profile-update-cv-form.component';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
|
||||||
import { of } from 'rxjs';
|
|
||||||
import { Profile } from '@app/domain/profiles/profile.model';
|
|
||||||
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
||||||
|
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
||||||
|
import { mockProfileRepo } from '@app/testing/profile.mock';
|
||||||
|
import { mockToastR } from '@app/testing/toastr.mock';
|
||||||
|
|
||||||
describe('MyProfileUpdateCvFormComponent', () => {
|
describe('MyProfileUpdateCvFormComponent', () => {
|
||||||
let component: MyProfileUpdateCvFormComponent;
|
let component: MyProfileUpdateCvFormComponent;
|
||||||
let fixture: ComponentFixture<MyProfileUpdateCvFormComponent>;
|
let fixture: ComponentFixture<MyProfileUpdateCvFormComponent>;
|
||||||
|
|
||||||
let mockToastrService: Partial<ToastrService>;
|
let mockToastrService: jest.Mocked<Partial<ToastrService>> = mockToastR;
|
||||||
let mockProfileRepo: ProfileRepository;
|
let mockProjectRepository: jest.Mocked<Partial<ProjectRepository>> = mockProfileRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockToastrService = {
|
|
||||||
success: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warning: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockProfileRepo = {
|
|
||||||
create: jest.fn(),
|
|
||||||
list: jest.fn(),
|
|
||||||
update: jest.fn().mockReturnValue(of({} as Profile)),
|
|
||||||
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [MyProfileUpdateCvFormComponent],
|
imports: [MyProfileUpdateCvFormComponent],
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
|
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProjectRepository },
|
||||||
{ provide: ToastrService, useValue: mockToastrService },
|
{ provide: ToastrService, useValue: mockToastrService },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|||||||
@@ -134,7 +134,7 @@
|
|||||||
for="profession"
|
for="profession"
|
||||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||||
>
|
>
|
||||||
Profession
|
Profession<small class="text-sm font-medium text-red-500"> * </small>
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
@@ -170,7 +170,7 @@
|
|||||||
for="secteur"
|
for="secteur"
|
||||||
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
|
||||||
>
|
>
|
||||||
Secteur d'activité
|
Secteur d'activité<small class="text-sm font-medium text-red-500"> * </small>
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
|
|||||||
@@ -11,14 +11,18 @@ import { Profile } from '@app/domain/profiles/profile.model';
|
|||||||
import { SectorRepository } from '@app/domain/sectors/sector.repository';
|
import { SectorRepository } from '@app/domain/sectors/sector.repository';
|
||||||
import { Sector } from '@app/domain/sectors/sector.model';
|
import { Sector } from '@app/domain/sectors/sector.model';
|
||||||
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
|
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
|
||||||
|
import { mockToastR } from '@app/testing/toastr.mock';
|
||||||
|
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
||||||
|
import { mockProfileRepo } from '@app/testing/profile.mock';
|
||||||
|
import { mockSectorRepo } from '@app/testing/sector.mock';
|
||||||
|
|
||||||
describe('MyProfileUpdateFormComponent', () => {
|
describe('MyProfileUpdateFormComponent', () => {
|
||||||
let component: MyProfileUpdateFormComponent;
|
let component: MyProfileUpdateFormComponent;
|
||||||
let fixture: ComponentFixture<MyProfileUpdateFormComponent>;
|
let fixture: ComponentFixture<MyProfileUpdateFormComponent>;
|
||||||
|
|
||||||
let mockToastrService: Partial<ToastrService>;
|
let mockToastrService: jest.Mocked<Partial<ToastrService>> = mockToastR;
|
||||||
let mockProfileRepo: ProfileRepository;
|
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>> = mockProfileRepo;
|
||||||
let mockSectorRepo: SectorRepository;
|
let mockSectorRepository: jest.Mocked<Partial<SectorRepository>> = mockSectorRepo;
|
||||||
|
|
||||||
const mockProfileData = {
|
const mockProfileData = {
|
||||||
profession: '',
|
profession: '',
|
||||||
@@ -29,32 +33,14 @@ describe('MyProfileUpdateFormComponent', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockToastrService = {
|
|
||||||
warning: jest.fn(),
|
|
||||||
success: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockProfileRepo = {
|
|
||||||
create: jest.fn(),
|
|
||||||
list: jest.fn(),
|
|
||||||
update: jest.fn().mockReturnValue(of({} as Profile)),
|
|
||||||
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockSectorRepo = {
|
|
||||||
list: jest.fn().mockReturnValue(of([])),
|
|
||||||
getOne: jest.fn().mockReturnValue(of({} as Sector)),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [MyProfileUpdateFormComponent],
|
imports: [MyProfileUpdateFormComponent],
|
||||||
providers: [
|
providers: [
|
||||||
FormBuilder,
|
FormBuilder,
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{ provide: ToastrService, useValue: mockToastrService },
|
{ provide: ToastrService, useValue: mockToastrService },
|
||||||
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
|
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository },
|
||||||
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepo },
|
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepository },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Image du projet</h3>
|
<h3 class="text-xl font-bold text-gray-900 dark:text-white">Image du projet</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (projectId == 'add'.toLowerCase()) {
|
@if (projectId === 'add'.toLowerCase()) {
|
||||||
<app-project-picture-form [project]="undefined" />
|
<app-project-picture-form [project]="undefined" />
|
||||||
} @else {
|
} @else {
|
||||||
<app-project-picture-form [project]="project()" />
|
<app-project-picture-form [project]="project()" />
|
||||||
|
|||||||
@@ -11,49 +11,22 @@ import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth
|
|||||||
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
||||||
import { AuthRepository } from '@app/domain/authentification/auth.repository';
|
import { AuthRepository } from '@app/domain/authentification/auth.repository';
|
||||||
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
||||||
|
import { mockAuthRepo } from '@app/testing/auth.mock';
|
||||||
|
import { mockProfileRepo } from '@app/testing/profile.mock';
|
||||||
|
import { mockProjectRepo } from '@app/testing/project.mock';
|
||||||
|
import { mockToastR } from '@app/testing/toastr.mock';
|
||||||
|
|
||||||
describe('MyProfileUpdateProjectFormComponent', () => {
|
describe('MyProfileUpdateProjectFormComponent', () => {
|
||||||
let component: MyProfileUpdateProjectFormComponent;
|
let component: MyProfileUpdateProjectFormComponent;
|
||||||
let fixture: ComponentFixture<MyProfileUpdateProjectFormComponent>;
|
let fixture: ComponentFixture<MyProfileUpdateProjectFormComponent>;
|
||||||
|
|
||||||
let mockToastrService: jest.Mocked<Partial<ToastrService>>;
|
let mockToastrService: jest.Mocked<Partial<ToastrService>> = mockToastR;
|
||||||
let mockProjectRepository: jest.Mocked<Partial<ProjectRepository>>;
|
let mockProjectRepository: jest.Mocked<Partial<ProjectRepository>> = mockProjectRepo;
|
||||||
|
|
||||||
let mockAuthRepository: jest.Mocked<Partial<AuthRepository>>;
|
let mockAuthRepository: jest.Mocked<Partial<AuthRepository>> = mockAuthRepo;
|
||||||
let mockProfileRepo: jest.Mocked<Partial<ProfileRepository>>;
|
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>> = mockProfileRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockToastrService = {
|
|
||||||
success: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warning: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockProjectRepository = {
|
|
||||||
create: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
list: jest.fn().mockReturnValue(of([])),
|
|
||||||
get: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
update: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
};
|
|
||||||
mockAuthRepository = {
|
|
||||||
get: jest.fn(),
|
|
||||||
login: jest.fn(),
|
|
||||||
sendVerificationEmail: jest.fn(),
|
|
||||||
logout: jest.fn(),
|
|
||||||
isAuthenticated: jest.fn(),
|
|
||||||
isEmailVerified: jest.fn(),
|
|
||||||
register: jest.fn(),
|
|
||||||
sendRequestPasswordReset: jest.fn(),
|
|
||||||
confirmPasswordReset: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockProfileRepo = {
|
|
||||||
create: jest.fn(),
|
|
||||||
list: jest.fn(),
|
|
||||||
update: jest.fn(),
|
|
||||||
getByUserId: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [MyProfileUpdateProjectFormComponent],
|
imports: [MyProfileUpdateProjectFormComponent],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -61,7 +34,7 @@ describe('MyProfileUpdateProjectFormComponent', () => {
|
|||||||
{ provide: ToastrService, useValue: mockToastrService },
|
{ provide: ToastrService, useValue: mockToastrService },
|
||||||
{ provide: PROJECT_REPOSITORY_TOKEN, useValue: mockProjectRepository },
|
{ provide: PROJECT_REPOSITORY_TOKEN, useValue: mockProjectRepository },
|
||||||
{ provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository },
|
{ provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository },
|
||||||
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
|
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
|
|||||||
@@ -7,43 +7,17 @@
|
|||||||
<a
|
<a
|
||||||
[routerLink]="['/']"
|
[routerLink]="['/']"
|
||||||
class="flex items-center space-x-2 group"
|
class="flex items-center space-x-2 group"
|
||||||
aria-label="Accueil TrouveTonProfile"
|
aria-label="Accueil TrouveTonProfil"
|
||||||
>
|
>
|
||||||
<span
|
<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"
|
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>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<!-- Actions utilisateur -->
|
<!-- Actions utilisateur -->
|
||||||
<div class="flex items-center gap-2 sm:gap-3">
|
<div class="flex items-center gap-2 sm:gap-4">
|
||||||
<!-- Avatar utilisateur (si connecté) -->
|
|
||||||
@if (isAuthenticated() && isEmailVerified() && user(); as user) {
|
|
||||||
<a
|
|
||||||
[routerLink]="['my-profile']"
|
|
||||||
[state]="{ user }"
|
|
||||||
class="w-9 h-9 sm:w-10 sm:h-10 rounded-full overflow-hidden bg-gray-200 border-2 border-transparent hover:border-indigo-500 dark:hover:border-indigo-400 transition-all ring-2 ring-transparent hover:ring-2 hover:ring-indigo-200 dark:hover:ring-indigo-900"
|
|
||||||
aria-label="Mon profil"
|
|
||||||
>
|
|
||||||
@if (user.avatar) {
|
|
||||||
<img
|
|
||||||
[alt]="user.username"
|
|
||||||
class="object-cover w-full h-full"
|
|
||||||
[src]="environment.baseUrl + '/api/files/users/' + user.id + '/' + user.avatar"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
} @else {
|
|
||||||
<img
|
|
||||||
[alt]="user.username"
|
|
||||||
class="object-cover w-full h-full"
|
|
||||||
[src]="'https://api.dicebear.com/9.x/adventurer/svg?seed=' + user.username"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</a>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Toggle thème -->
|
<!-- Toggle thème -->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -84,51 +58,128 @@
|
|||||||
}
|
}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Séparateur -->
|
|
||||||
<div class="h-6 w-px bg-gray-300 dark:bg-gray-700"></div>
|
|
||||||
|
|
||||||
<!-- Bouton de connexion/déconnexion -->
|
|
||||||
@if (isAuthenticated() && isEmailVerified()) {
|
@if (isAuthenticated() && isEmailVerified()) {
|
||||||
<a
|
<!-- Menu utilisateur connecté -->
|
||||||
[routerLink]="['/auth']"
|
<div class="flex items-center gap-2">
|
||||||
(click)="authFacade.logout()"
|
<!-- Lien Mon Profil - visible sur desktop -->
|
||||||
class="flex items-center gap-2 px-3 py-2 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
<a
|
||||||
aria-label="Se déconnecter"
|
[routerLink]="['my-profile']"
|
||||||
>
|
[state]="{ user: user() }"
|
||||||
<svg
|
class="hidden sm:flex items-center gap-2 px-4 py-2 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors font-medium"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
aria-label="Mon profil"
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
stroke-linecap="round"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
stroke-linejoin="round"
|
fill="none"
|
||||||
d="M15.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15M12 9l-3 3m0 0 3 3m-3-3h12.75"
|
viewBox="0 0 24 24"
|
||||||
/>
|
stroke-width="1.5"
|
||||||
</svg>
|
stroke="currentColor"
|
||||||
<span class="hidden sm:inline text-sm font-medium">Déconnexion</span>
|
class="w-5 h-5"
|
||||||
</a>
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm">Mon Profil</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Icône Mon Profil - visible sur mobile uniquement -->
|
||||||
|
<a
|
||||||
|
[routerLink]="['my-profile']"
|
||||||
|
[state]="{ user: user() }"
|
||||||
|
class="sm:hidden p-2 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
||||||
|
aria-label="Mon profil"
|
||||||
|
>
|
||||||
|
<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.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Séparateur (desktop seulement) -->
|
||||||
|
<div class="hidden sm:block h-6 w-px bg-gray-300 dark:bg-gray-700"></div>
|
||||||
|
|
||||||
|
<!-- Bouton Déconnexion - version desktop -->
|
||||||
|
<a
|
||||||
|
[routerLink]="['/auth']"
|
||||||
|
(click)="authFacade.logout()"
|
||||||
|
class="hidden sm:flex items-center gap-2 px-4 py-2 rounded-lg bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors font-medium border border-red-200 dark:border-red-800"
|
||||||
|
aria-label="Se déconnecter"
|
||||||
|
>
|
||||||
|
<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.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15M12 9l-3 3m0 0 3 3m-3-3h12.75"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm">Déconnexion</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Icône Déconnexion - version mobile -->
|
||||||
|
<a
|
||||||
|
[routerLink]="['/auth']"
|
||||||
|
(click)="authFacade.logout()"
|
||||||
|
class="sm:hidden p-2 rounded-lg bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors border border-red-200 dark:border-red-800"
|
||||||
|
aria-label="Se déconnecter"
|
||||||
|
>
|
||||||
|
<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.75 9V5.25A2.25 2.25 0 0 0 13.5 3h-6a2.25 2.25 0 0 0-2.25 2.25v13.5A2.25 2.25 0 0 0 7.5 21h6a2.25 2.25 0 0 0 2.25-2.25V15M12 9l-3 3m0 0 3 3m-3-3h12.75"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
} @else {
|
} @else {
|
||||||
<a
|
<!-- Boutons pour utilisateur non connecté -->
|
||||||
[routerLink]="['/auth']"
|
<div class="flex items-center gap-2">
|
||||||
class="flex items-center gap-2 px-3 py-2 rounded-lg text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
<!-- Bouton Connexion mis en valeur -->
|
||||||
aria-label="Se connecter"
|
<a
|
||||||
>
|
[routerLink]="['/auth']"
|
||||||
<svg
|
class="flex items-center gap-2 px-4 py-2 rounded-lg bg-indigo-600 dark:bg-indigo-500 text-white hover:bg-indigo-700 dark:hover:bg-indigo-600 transition-colors font-medium shadow-md hover:shadow-lg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
aria-label="Se connecter"
|
||||||
viewBox="0 -960 960 960"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
>
|
>
|
||||||
<path
|
<svg
|
||||||
d="M234-276q51-39 114-61.5T480-360q69 0 132 22.5T726-276q35-41 54.5-93T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 59 19.5 111t54.5 93Zm246-164q-59 0-99.5-40.5T340-580q0-59 40.5-99.5T480-720q59 0 99.5 40.5T620-580q0 59-40.5 99.5T480-440Zm0 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q53 0 100-15.5t86-44.5q-39-29-86-44.5T480-280q-53 0-100 15.5T294-220q39 29 86 44.5T480-160Zm0-360q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm0-60Zm0 360Z"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
/>
|
viewBox="0 -960 960 960"
|
||||||
</svg>
|
fill="currentColor"
|
||||||
<span class="hidden sm:inline text-sm font-medium">Connexion</span>
|
class="w-5 h-5"
|
||||||
</a>
|
>
|
||||||
|
<path
|
||||||
|
d="M234-276q51-39 114-61.5T480-360q69 0 132 22.5T726-276q35-41 54.5-93T800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 59 19.5 111t54.5 93Zm246-164q-59 0-99.5-40.5T340-580q0-59 40.5-99.5T480-720q59 0 99.5 40.5T620-580q0 59-40.5 99.5T480-440Zm0 360q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q53 0 100-15.5t86-44.5q-39-29-86-44.5T480-280q-53 0-100 15.5T294-220q39 29 86 44.5T480-160Zm0-360q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm0-60Zm0 360Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="text-sm">Connexion</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,21 +3,22 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
import { NavBarComponent } from './nav-bar.component';
|
import { NavBarComponent } from './nav-bar.component';
|
||||||
import { ThemeService } from '@app/core/services/theme/theme.service';
|
import { ThemeService } from '@app/core/services/theme/theme.service';
|
||||||
import { provideRouter } from '@angular/router';
|
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 { User } from '@app/domain/users/user.model';
|
||||||
import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token';
|
import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token';
|
||||||
import { AuthRepository } from '@app/domain/authentification/auth.repository';
|
import { AuthRepository } from '@app/domain/authentification/auth.repository';
|
||||||
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
||||||
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
||||||
|
import { mockProfileRepo } from '@app/testing/profile.mock';
|
||||||
|
import { mockAuthRepo } from '@app/testing/auth.mock';
|
||||||
|
import { mockThemeService } from '@app/testing/theme.mock';
|
||||||
|
|
||||||
describe('NavBarComponent', () => {
|
describe('NavBarComponent', () => {
|
||||||
let component: NavBarComponent;
|
let component: NavBarComponent;
|
||||||
let fixture: ComponentFixture<NavBarComponent>;
|
let fixture: ComponentFixture<NavBarComponent>;
|
||||||
let mockThemeService: jest.Mocked<Partial<ThemeService>>;
|
let mockTheme: jest.Mocked<Partial<ThemeService>> = mockThemeService;
|
||||||
|
|
||||||
let mockAuthRepository: jest.Mocked<Partial<AuthRepository>>;
|
let mockAuthRepository: jest.Mocked<Partial<AuthRepository>> = mockAuthRepo;
|
||||||
let mockProfileRepo: jest.Mocked<Partial<ProfileRepository>>;
|
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>> = mockProfileRepo;
|
||||||
|
|
||||||
const user: User = {
|
const user: User = {
|
||||||
id: 'adbc123',
|
id: 'adbc123',
|
||||||
@@ -30,40 +31,15 @@ describe('NavBarComponent', () => {
|
|||||||
name: 'john doe',
|
name: 'john doe',
|
||||||
avatar: '',
|
avatar: '',
|
||||||
};
|
};
|
||||||
const mockUser: AuthModel = { isValid: false, record: user, token: 'mockToken123' } as AuthModel;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockThemeService = {
|
|
||||||
darkModeSignal: signal<string>('null'),
|
|
||||||
updateDarkMode: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockAuthRepository = {
|
|
||||||
get: jest.fn(),
|
|
||||||
login: jest.fn(),
|
|
||||||
sendVerificationEmail: jest.fn(),
|
|
||||||
logout: jest.fn(),
|
|
||||||
isAuthenticated: jest.fn(),
|
|
||||||
isEmailVerified: jest.fn(),
|
|
||||||
register: jest.fn(),
|
|
||||||
sendRequestPasswordReset: jest.fn(),
|
|
||||||
confirmPasswordReset: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockProfileRepo = {
|
|
||||||
create: jest.fn(),
|
|
||||||
list: jest.fn(),
|
|
||||||
update: jest.fn(),
|
|
||||||
getByUserId: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [NavBarComponent],
|
imports: [NavBarComponent],
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{ provide: ThemeService, useValue: mockThemeService },
|
{ provide: ThemeService, useValue: mockTheme },
|
||||||
{ provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository },
|
{ provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository },
|
||||||
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
|
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
<!-- Pagination responsive -->
|
|
||||||
<nav class="flex justify-center mt-8 sm:mt-12" aria-label="Pagination">
|
|
||||||
<ul class="flex flex-wrap gap-2 items-center justify-center">
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
||||||
>
|
|
||||||
Précédent
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button class="px-4 py-2 rounded-lg bg-indigo-600 text-white font-medium">1</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
class="px-4 py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
||||||
>
|
|
||||||
2
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
class="px-4 py-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
||||||
>
|
|
||||||
3
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
|
|
||||||
>
|
|
||||||
Suivant
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-pagination',
|
|
||||||
standalone: true,
|
|
||||||
imports: [],
|
|
||||||
templateUrl: './pagination.component.html',
|
|
||||||
styleUrl: './pagination.component.scss',
|
|
||||||
})
|
|
||||||
export class PaginationComponent {}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<!-- Grid des projets -->
|
<!-- Grid des projets -->
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 gap-6">
|
||||||
@for (project of projects(); track project.id) {
|
@for (project of projects(); track project.id) {
|
||||||
<app-project-item [project]="project" />
|
<app-project-item [project]="project" />
|
||||||
} @empty {
|
} @empty {
|
||||||
|
|||||||
@@ -2,25 +2,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
|
|
||||||
import { ProjectListComponent } from './project-list.component';
|
import { ProjectListComponent } from './project-list.component';
|
||||||
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
||||||
import { of } from 'rxjs';
|
|
||||||
import { Project } from '@app/domain/projects/project.model';
|
|
||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
|
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
|
||||||
|
import { mockProjectRepo } from '@app/testing/project.mock';
|
||||||
|
|
||||||
describe('ProjectListComponent', () => {
|
describe('ProjectListComponent', () => {
|
||||||
let component: ProjectListComponent;
|
let component: ProjectListComponent;
|
||||||
let fixture: ComponentFixture<ProjectListComponent>;
|
let fixture: ComponentFixture<ProjectListComponent>;
|
||||||
|
|
||||||
let mockProjectRepository: jest.Mocked<ProjectRepository>;
|
let mockProjectRepository: jest.Mocked<ProjectRepository> = mockProjectRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockProjectRepository = {
|
|
||||||
create: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
list: jest.fn().mockReturnValue(of([])),
|
|
||||||
get: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
update: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ProjectListComponent],
|
imports: [ProjectListComponent],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -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 { UntilDestroy } from '@ngneat/until-destroy';
|
||||||
import { ProjectItemComponent } from '@app/shared/components/project-item/project-item.component';
|
import { ProjectItemComponent } from '@app/shared/components/project-item/project-item.component';
|
||||||
import { ProjectFacade } from '@app/ui/projects/project.facade';
|
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',
|
styleUrl: './project-list.component.scss',
|
||||||
})
|
})
|
||||||
@UntilDestroy()
|
@UntilDestroy()
|
||||||
export class ProjectListComponent implements OnInit {
|
export class ProjectListComponent implements OnChanges {
|
||||||
@Input({ required: true }) userProjectId = '';
|
@Input({ required: true }) userProjectId = '';
|
||||||
|
|
||||||
private readonly projectFacade = new ProjectFacade();
|
private readonly projectFacade = new ProjectFacade();
|
||||||
|
|
||||||
protected projects = this.projectFacade.projects;
|
protected projects = this.projectFacade.projects;
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnChanges(changes: SimpleChanges) {
|
||||||
this.projectFacade.load(this.userProjectId);
|
this.projectFacade.load(this.userProjectId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,14 +13,14 @@
|
|||||||
<div
|
<div
|
||||||
class="w-40 h-40 rounded-full inline-flex items-center justify-center bg-gray-200 text-gray-400"
|
class="w-40 h-40 rounded-full inline-flex items-center justify-center bg-gray-200 text-gray-400"
|
||||||
>
|
>
|
||||||
@if (imagePreviewUrl != null && project != undefined) {
|
@if (imagePreviewUrl !== null && project !== undefined) {
|
||||||
<img
|
<img
|
||||||
alt="nouveau-projet"
|
alt="nouveau-projet"
|
||||||
class="object-cover object-center h-full w-full"
|
class="object-cover object-center h-full w-full"
|
||||||
[src]="imagePreviewUrl"
|
[src]="imagePreviewUrl"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
} @else if (project != undefined) {
|
} @else if (project !== undefined) {
|
||||||
@if (project.fichier) {
|
@if (project.fichier) {
|
||||||
<img
|
<img
|
||||||
alt="{{ project!.nom }}"
|
alt="{{ project!.nom }}"
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (project == undefined) {
|
@if (project === undefined) {
|
||||||
<img
|
<img
|
||||||
alt="nouveau-projet"
|
alt="nouveau-projet"
|
||||||
class="object-cover object-center h-full w-full"
|
class="object-cover object-center h-full w-full"
|
||||||
@@ -77,10 +77,10 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@if (file != null || imagePreviewUrl != null) {
|
@if (file !== null || imagePreviewUrl !== null) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
[ngClass]="{ 'bg-purple-600': file != null || imagePreviewUrl != null }"
|
[ngClass]="{ 'bg-purple-600': file !== null || imagePreviewUrl !== null }"
|
||||||
class="!mt-2 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block"
|
class="!mt-2 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block"
|
||||||
(click)="onSubmit()"
|
(click)="onSubmit()"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,31 +4,18 @@ import { ProjectPictureFormComponent } from './project-picture-form.component';
|
|||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
|
import { PROJECT_REPOSITORY_TOKEN } from '@app/infrastructure/projects/project-repository.token';
|
||||||
import { of } from 'rxjs';
|
|
||||||
import { Project } from '@app/domain/projects/project.model';
|
|
||||||
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
import { ProjectRepository } from '@app/domain/projects/project.repository';
|
||||||
|
import { mockToastR } from '@app/testing/toastr.mock';
|
||||||
|
import { mockProjectRepo } from '@app/testing/project.mock';
|
||||||
|
|
||||||
describe('ProjectPictureFormComponent', () => {
|
describe('ProjectPictureFormComponent', () => {
|
||||||
let component: ProjectPictureFormComponent;
|
let component: ProjectPictureFormComponent;
|
||||||
let fixture: ComponentFixture<ProjectPictureFormComponent>;
|
let fixture: ComponentFixture<ProjectPictureFormComponent>;
|
||||||
|
|
||||||
let mockToastrService: Partial<ToastrService>;
|
let mockToastrService: jest.Mocked<Partial<ToastrService>> = mockToastR;
|
||||||
let mockProjectRepository: jest.Mocked<ProjectRepository>;
|
let mockProjectRepository: jest.Mocked<Partial<ProjectRepository>> = mockProjectRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockToastrService = {
|
|
||||||
success: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warning: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockProjectRepository = {
|
|
||||||
create: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
list: jest.fn().mockReturnValue(of([])),
|
|
||||||
get: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
update: jest.fn().mockReturnValue(of({} as Project)),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ProjectPictureFormComponent],
|
imports: [ProjectPictureFormComponent],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@if (reseaux != undefined) {
|
@if (reseaux !== undefined) {
|
||||||
<ul class="flex flex-wrap justify-center mt-4 space-x-2">
|
<ul class="flex flex-wrap justify-center mt-4 space-x-2">
|
||||||
@for (rx of Object.keys(reseaux); track rx) {
|
@for (rx of Object.keys(reseaux); track rx) {
|
||||||
@if (reseaux[rx] !== '') {
|
@if (reseaux[rx] !== '') {
|
||||||
|
|||||||
@@ -5,11 +5,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<ng-template #content>
|
<ng-template #content>
|
||||||
@if (user != undefined) {
|
@if (user !== undefined) {
|
||||||
<div
|
<div
|
||||||
class="w-40 h-40 rounded-full inline-flex items-center justify-center bg-gray-200 text-gray-400"
|
class="w-40 h-40 rounded-full inline-flex items-center justify-center bg-gray-200 text-gray-400"
|
||||||
>
|
>
|
||||||
@if (imagePreviewUrl != null) {
|
@if (imagePreviewUrl !== null) {
|
||||||
<img
|
<img
|
||||||
alt="{{ user!.username }}"
|
alt="{{ user!.username }}"
|
||||||
class="object-cover object-center h-full w-full rounded-full"
|
class="object-cover object-center h-full w-full rounded-full"
|
||||||
@@ -27,7 +27,9 @@
|
|||||||
<img
|
<img
|
||||||
alt="{{ user!.username }}"
|
alt="{{ user!.username }}"
|
||||||
class="object-cover object-center h-full w-full rounded-full"
|
class="object-cover object-center h-full w-full rounded-full"
|
||||||
src="https://api.dicebear.com/9.x/adventurer/svg?seed={{ user.username }}"
|
src="https://api.dicebear.com/9.x/initials/svg?seed={{
|
||||||
|
user.name ? user.name : user.username ? user.username : user.email
|
||||||
|
}}"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -62,10 +64,10 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@if (file != null || imagePreviewUrl != null) {
|
@if (file !== null || imagePreviewUrl !== null) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
[ngClass]="{ 'bg-purple-600': file != null || imagePreviewUrl != null }"
|
[ngClass]="{ 'bg-purple-600': file !== null || imagePreviewUrl !== null }"
|
||||||
class="!mt-2 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block"
|
class="!mt-2 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block"
|
||||||
(click)="onUserAvatarFormSubmit()"
|
(click)="onUserAvatarFormSubmit()"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -5,31 +5,22 @@ import { provideRouter } from '@angular/router';
|
|||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { UserRepository } from '@app/domain/users/user.repository';
|
import { UserRepository } from '@app/domain/users/user.repository';
|
||||||
import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository.token';
|
import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository.token';
|
||||||
|
import { mockToastR } from '@app/testing/toastr.mock';
|
||||||
|
import { mockUserRepo } from '@app/testing/user.mock';
|
||||||
|
|
||||||
describe('UserAvatarFormComponent', () => {
|
describe('UserAvatarFormComponent', () => {
|
||||||
let component: UserAvatarFormComponent;
|
let component: UserAvatarFormComponent;
|
||||||
let fixture: ComponentFixture<UserAvatarFormComponent>;
|
let fixture: ComponentFixture<UserAvatarFormComponent>;
|
||||||
|
|
||||||
let mockToastrService: Partial<ToastrService>;
|
let mockToastrService: jest.Mocked<Partial<ToastrService>> = mockToastR;
|
||||||
let mockUserRepo: UserRepository;
|
let mockUserRepository: jest.Mocked<Partial<UserRepository>> = mockUserRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockToastrService = {
|
|
||||||
warning: jest.fn(),
|
|
||||||
success: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockUserRepo = {
|
|
||||||
update: jest.fn(),
|
|
||||||
getUserById: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [UserAvatarFormComponent],
|
imports: [UserAvatarFormComponent],
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{ provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepo },
|
{ provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepository },
|
||||||
{ provide: ToastrService, useValue: mockToastrService },
|
{ provide: ToastrService, useValue: mockToastrService },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
<!-- Champ Nom -->
|
<!-- Champ Nom -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Nom
|
Nom(s)<small class="text-sm font-medium text-red-500"> * </small>
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
<!-- Champ Prénom -->
|
<!-- Champ Prénom -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label for="firstname" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label for="firstname" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
Prénom
|
Prénom(s)<small class="text-sm font-medium text-red-500"> * </small>
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||||
@@ -108,7 +108,11 @@
|
|||||||
d="M7.707 10.293a1 1 0 10-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 11.586V6h5a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V8a2 2 0 012-2h5v5.586l-1.293-1.293zM9 4a1 1 0 012 0v2H9V4z"
|
d="M7.707 10.293a1 1 0 10-1.414 1.414l3 3a1 1 0 001.414 0l3-3a1 1 0 00-1.414-1.414L11 11.586V6h5a2 2 0 012 2v7a2 2 0 01-2 2H4a2 2 0 01-2-2V8a2 2 0 012-2h5v5.586l-1.293-1.293zM9 4a1 1 0 012 0v2H9V4z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Modifier mon identité
|
@if (userForm.valid) {
|
||||||
|
Sauvegarder les modifications
|
||||||
|
} @else {
|
||||||
|
Modifier mon identité
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -6,32 +6,23 @@ import { provideRouter } from '@angular/router';
|
|||||||
import { FormBuilder } from '@angular/forms';
|
import { FormBuilder } from '@angular/forms';
|
||||||
import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository.token';
|
import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository.token';
|
||||||
import { UserRepository } from '@app/domain/users/user.repository';
|
import { UserRepository } from '@app/domain/users/user.repository';
|
||||||
|
import { mockToastR } from '@app/testing/toastr.mock';
|
||||||
|
import { mockUserRepo } from '@app/testing/user.mock';
|
||||||
|
|
||||||
describe('UserFormComponent', () => {
|
describe('UserFormComponent', () => {
|
||||||
let component: UserFormComponent;
|
let component: UserFormComponent;
|
||||||
let fixture: ComponentFixture<UserFormComponent>;
|
let fixture: ComponentFixture<UserFormComponent>;
|
||||||
|
|
||||||
let mockToastrService: Partial<ToastrService>;
|
let mockToastrService: jest.Mocked<Partial<ToastrService>> = mockToastR;
|
||||||
let mockUserRepo: UserRepository;
|
let mockUserRepository: jest.Mocked<Partial<UserRepository>> = mockUserRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockToastrService = {
|
|
||||||
warning: jest.fn(),
|
|
||||||
success: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockUserRepo = {
|
|
||||||
update: jest.fn(),
|
|
||||||
getUserById: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [UserFormComponent],
|
imports: [UserFormComponent],
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
FormBuilder,
|
FormBuilder,
|
||||||
{ provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepo },
|
{ provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepository },
|
||||||
{ provide: ToastrService, useValue: mockToastrService },
|
{ provide: ToastrService, useValue: mockToastrService },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|||||||
@@ -6,34 +6,23 @@ import { AuthFacade } from '@app/ui/authentification/auth.facade';
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { ActionType } from '@app/domain/action-type.util';
|
import { ActionType } from '@app/domain/action-type.util';
|
||||||
import { signal, WritableSignal } from '@angular/core';
|
import { signal, WritableSignal } from '@angular/core';
|
||||||
|
import { mockToastR } from '@app/testing/toastr.mock';
|
||||||
|
import { mockAuthenticationFacade } from '@app/testing/ui/authentification/auth.facade.mock';
|
||||||
|
|
||||||
describe('UserPasswordFormComponent', () => {
|
describe('UserPasswordFormComponent', () => {
|
||||||
let component: UserPasswordFormComponent;
|
let component: UserPasswordFormComponent;
|
||||||
let fixture: ComponentFixture<UserPasswordFormComponent>;
|
let fixture: ComponentFixture<UserPasswordFormComponent>;
|
||||||
|
|
||||||
let mockToastrService: Partial<ToastrService>;
|
let mockToastrService: jest.Mocked<Partial<ToastrService>> = mockToastR;
|
||||||
|
|
||||||
// On mocke la Facade car c'est ce que le composant utilise directement
|
// On mocke la Facade car c'est ce que le composant utilise directement
|
||||||
let mockAuthFacade: {
|
let mockAuthFacade: {
|
||||||
sendRequestPasswordReset: jest.Mock;
|
sendRequestPasswordReset: jest.Mock;
|
||||||
loading: WritableSignal<{ isLoading: boolean; action: ActionType }>;
|
loading: WritableSignal<{ isLoading: boolean; action: ActionType }>;
|
||||||
error: WritableSignal<{ hasError: boolean }>;
|
error: WritableSignal<{ hasError: boolean }>;
|
||||||
};
|
} = mockAuthenticationFacade;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockToastrService = {
|
|
||||||
warning: jest.fn(),
|
|
||||||
success: jest.fn(),
|
|
||||||
info: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockAuthFacade = {
|
|
||||||
sendRequestPasswordReset: jest.fn(),
|
|
||||||
loading: signal({ isLoading: false, action: ActionType.NONE }),
|
|
||||||
error: signal({ hasError: false }),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [UserPasswordFormComponent],
|
imports: [UserPasswordFormComponent],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
@if (user() != undefined) {
|
@if (user() !== undefined) {
|
||||||
<a
|
<a [routerLink]="[slug()]" [state]="{ user: user(), profile }" class="block group">
|
||||||
[routerLink]="[user().username ? user().username : user().id]"
|
|
||||||
[state]="{ user: user(), profile }"
|
|
||||||
class="block group"
|
|
||||||
>
|
|
||||||
<!-- Card du profil -->
|
<!-- Card du profil -->
|
||||||
<div
|
<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"
|
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"
|
||||||
@@ -43,7 +39,7 @@
|
|||||||
} @else {
|
} @else {
|
||||||
<img
|
<img
|
||||||
class="w-full h-full rounded-full object-cover grayscale group-hover:grayscale-0 transition-all duration-500 group-hover:scale-105"
|
class="w-full h-full rounded-full object-cover grayscale group-hover:grayscale-0 transition-all duration-500 group-hover:scale-105"
|
||||||
src="https://api.dicebear.com/9.x/adventurer/svg?seed={{ user().username }}"
|
src="https://api.dicebear.com/9.x/initials/svg?seed={{ user().name }}"
|
||||||
alt="{{ user().username }}"
|
alt="{{ user().username }}"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
@@ -56,7 +52,7 @@
|
|||||||
<h3
|
<h3
|
||||||
class="text-lg font-bold text-gray-900 dark:text-white mb-2 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors"
|
class="text-lg font-bold text-gray-900 dark:text-white mb-2 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors"
|
||||||
>
|
>
|
||||||
{{ user().name }}
|
{{ user().firstName }} {{ user().lastName }}
|
||||||
</h3>
|
</h3>
|
||||||
} @else if (user().username) {
|
} @else if (user().username) {
|
||||||
<h3
|
<h3
|
||||||
|
|||||||
@@ -9,31 +9,23 @@ import { of } from 'rxjs';
|
|||||||
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
|
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
|
||||||
import { SectorRepository } from '@app/domain/sectors/sector.repository';
|
import { SectorRepository } from '@app/domain/sectors/sector.repository';
|
||||||
import { Sector } from '@app/domain/sectors/sector.model';
|
import { Sector } from '@app/domain/sectors/sector.model';
|
||||||
|
import { mockUserRepo } from '@app/testing/user.mock';
|
||||||
|
import { mockSectorRepo } from '@app/testing/sector.mock';
|
||||||
|
|
||||||
describe('VerticalProfileItemComponent', () => {
|
describe('VerticalProfileItemComponent', () => {
|
||||||
let component: VerticalProfileItemComponent;
|
let component: VerticalProfileItemComponent;
|
||||||
let fixture: ComponentFixture<VerticalProfileItemComponent>;
|
let fixture: ComponentFixture<VerticalProfileItemComponent>;
|
||||||
|
|
||||||
let mockUserRepo: UserRepository;
|
let mockUserRepository: jest.Mocked<Partial<UserRepository>> = mockUserRepo;
|
||||||
let mockSectorRepo: SectorRepository;
|
let mockSectorRepository: jest.Mocked<Partial<SectorRepository>> = mockSectorRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockUserRepo = {
|
|
||||||
update: jest.fn().mockReturnValue(of({} as User)),
|
|
||||||
getUserById: jest.fn().mockReturnValue(of({} as User)),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockSectorRepo = {
|
|
||||||
list: jest.fn().mockReturnValue(of({} as Sector)),
|
|
||||||
getOne: jest.fn().mockReturnValue(of({} as Sector)),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [VerticalProfileItemComponent],
|
imports: [VerticalProfileItemComponent],
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{ provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepo },
|
{ provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepository },
|
||||||
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepo },
|
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepository },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
|
|||||||
@@ -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 { Router, RouterLink } from '@angular/router';
|
||||||
import { UntilDestroy } from '@ngneat/until-destroy';
|
import { UntilDestroy } from '@ngneat/until-destroy';
|
||||||
import { environment } from '@env/environment';
|
import { environment } from '@env/environment';
|
||||||
@@ -23,6 +23,12 @@ export class VerticalProfileItemComponent implements OnInit {
|
|||||||
protected readonly loading = this.facade.loading;
|
protected readonly loading = this.facade.loading;
|
||||||
protected readonly error = this.facade.error;
|
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 {
|
ngOnInit(): void {
|
||||||
this.facade.loadOne(this.profile.utilisateur);
|
this.facade.loadOne(this.profile.utilisateur);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,28 @@
|
|||||||
<!-- Filtres -->
|
<div
|
||||||
<div class="w-full">
|
class="w-full bg-white dark:bg-gray-800 p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm"
|
||||||
|
>
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
|
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
|
||||||
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Filtrer les résultats</h3>
|
<h3 class="text-lg font-bold text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-5 w-5 text-indigo-500"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Filtres & Tri
|
||||||
|
</h3>
|
||||||
|
|
||||||
@if (hasActiveFilters()) {
|
@if (hasActiveFilters()) {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
(click)="clearAllFilters()"
|
(click)="clearAllFilters()"
|
||||||
class="text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300 font-medium transition-colors flex items-center gap-1"
|
class="text-sm text-red-600 hover:text-red-700 dark:text-red-400 font-medium transition-colors flex items-center gap-1"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -16,371 +31,98 @@
|
|||||||
class="w-4 h-4"
|
class="w-4 h-4"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
fill-rule="evenodd"
|
||||||
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
|
clip-rule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Effacer les filtres
|
Réinitialiser
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
<form
|
||||||
<!-- Filtre: Profils vérifiés -->
|
[formGroup]="filterForm"
|
||||||
<button
|
(ngSubmit)="onSubmit()"
|
||||||
type="button"
|
class="w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 items-end"
|
||||||
(click)="toggleVerifiedFilter()"
|
>
|
||||||
[class.ring-2]="filters.verified"
|
<div
|
||||||
[class.ring-purple-500]="filters.verified"
|
class="flex items-center h-[42px] px-3 rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-700/50"
|
||||||
class="group relative flex items-center justify-between gap-3 px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-700 hover:border-purple-500 dark:hover:border-purple-400 transition-all bg-white dark:bg-gray-800"
|
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2">
|
<label class="flex items-center gap-3 w-full cursor-pointer">
|
||||||
<div
|
<input
|
||||||
class="flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center"
|
type="checkbox"
|
||||||
[class.bg-purple-100]="filters.verified"
|
formControlName="verified"
|
||||||
[class.dark:bg-purple-900]="filters.verified"
|
class="flex-1 focus:ring-0 focus:outline-none placeholder:text-gray-400 bg-transparent text-gray-800 dark:text-white text-sm sm:text-base"
|
||||||
|
/>
|
||||||
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300"
|
||||||
|
>Profils vérifiés uniquement</span
|
||||||
>
|
>
|
||||||
<svg
|
</label>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
[class.text-purple-600]="filters.verified"
|
|
||||||
[class.dark:text-purple-400]="filters.verified"
|
|
||||||
[class.text-gray-400]="!filters.verified"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300"> Profils vérifiés </span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (filters.verified) {
|
|
||||||
<div
|
|
||||||
class="flex-shrink-0 w-5 h-5 rounded-full bg-purple-600 text-white flex items-center justify-center"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-3 h-3"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Filtre: Secteur -->
|
|
||||||
<ng-container *ngTemplateOutlet="secteur"></ng-container>
|
|
||||||
|
|
||||||
<!-- Filtre: Profession -->
|
|
||||||
<ng-container *ngTemplateOutlet="profession"></ng-container>
|
|
||||||
|
|
||||||
<!-- Filtre: Tri -->
|
|
||||||
<ng-container *ngTemplateOutlet="tri"></ng-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tags des filtres actifs -->
|
|
||||||
@if (hasActiveFilters()) {
|
|
||||||
<div class="flex flex-wrap gap-2 mt-4">
|
|
||||||
@if (filters.verified) {
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 text-xs font-medium"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-4 h-4"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Vérifiés
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
(click)="toggleVerifiedFilter()"
|
|
||||||
class="hover:text-purple-900 dark:hover:text-purple-100"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-3.5 h-3.5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
@if (filters.secteur) {
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 text-xs font-medium"
|
|
||||||
>
|
|
||||||
{{ filters.secteur }}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
(click)="selectSector(null)"
|
|
||||||
class="hover:text-purple-900 dark:hover:text-purple-100"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-3.5 h-3.5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
@if (filters.profession) {
|
|
||||||
<span
|
|
||||||
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 text-xs font-medium"
|
|
||||||
>
|
|
||||||
{{ filters.profession }}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
(click)="selectProfession(null)"
|
|
||||||
class="hover:text-purple-900 dark:hover:text-purple-100"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-3.5 h-3.5"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-template #secteur>
|
<div class="space-y-1">
|
||||||
<div class="relative">
|
<label
|
||||||
<button
|
for="sector-select"
|
||||||
type="button"
|
class="block text-xs font-medium text-gray-500 dark:text-gray-400 uppercase"
|
||||||
(click)="toggleSectorDropdown()"
|
>Secteur</label
|
||||||
[class.ring-2]="filters.secteur"
|
|
||||||
[class.ring-purple-500]="filters.secteur"
|
|
||||||
class="w-full flex items-center justify-between gap-3 px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-700 hover:border-purple-500 dark:hover:border-purple-400 transition-all bg-white dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
[class.text-purple-600]="filters.secteur"
|
|
||||||
[class.dark:text-purple-400]="filters.secteur"
|
|
||||||
[class.text-gray-400]="!filters.secteur"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M2 3.5A1.5 1.5 0 013.5 2h9A1.5 1.5 0 0114 3.5v11.75A2.75 2.75 0 0016.75 18h-12A2.75 2.75 0 012 15.25V3.5zm3.75 7a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-4.5zm0 3a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-4.5zM5 5.75A.75.75 0 015.75 5h4.5a.75.75 0 01.75.75v2.5a.75.75 0 01-.75.75h-4.5A.75.75 0 015 8.25v-2.5z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
<path d="M16.5 6.5h-1v8.75a1.25 1.25 0 102.5 0V8a1.5 1.5 0 00-1.5-1.5z" />
|
|
||||||
</svg>
|
|
||||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
{{ filters.secteur || 'Secteur' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5 text-gray-400 transition-transform"
|
|
||||||
[class.rotate-180]="showSectorDropdown"
|
|
||||||
>
|
>
|
||||||
<path
|
<select
|
||||||
fill-rule="evenodd"
|
id="sector-select"
|
||||||
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
|
formControlName="secteur"
|
||||||
clip-rule="evenodd"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@if (showSectorDropdown) {
|
|
||||||
<div
|
|
||||||
class="absolute mt-2 w-full bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg shadow-lg max-h-60 overflow-y-auto"
|
|
||||||
>
|
>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
(click)="selectSector(null)"
|
|
||||||
class="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
||||||
[class.bg-purple-50]="!filters.secteur"
|
|
||||||
[class.dark:bg-purple-900]="!filters.secteur"
|
|
||||||
>
|
>
|
||||||
<span class="text-gray-700 dark:text-gray-300">Tous les secteurs</span>
|
<option [ngValue]="null">Tous les secteurs</option>
|
||||||
</button>
|
|
||||||
@for (sector of sectors(); track sector.id) {
|
@for (sector of sectors(); track sector.id) {
|
||||||
<button
|
<option [value]="sector.nom">{{ sector.nom }}</option>
|
||||||
type="button"
|
|
||||||
(click)="selectSector(sector.nom)"
|
|
||||||
class="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
||||||
[class.bg-purple-50]="filters.secteur === sector.nom"
|
|
||||||
[class.dark:bg-purple-900]="filters.secteur === sector.nom"
|
|
||||||
>
|
|
||||||
<span class="text-gray-700 dark:text-gray-300">{{ sector.nom }}</span>
|
|
||||||
</button>
|
|
||||||
}
|
}
|
||||||
</div>
|
</select>
|
||||||
}
|
</div>
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template #profession>
|
<div class="space-y-1">
|
||||||
<div class="relative">
|
<label
|
||||||
<button
|
for="job-select"
|
||||||
type="button"
|
class="block text-xs font-medium text-gray-500 dark:text-gray-400 uppercase"
|
||||||
(click)="toggleProfessionDropdown()"
|
>Profession</label
|
||||||
[class.ring-2]="filters.profession"
|
|
||||||
[class.ring-purple-500]="filters.profession"
|
|
||||||
class="w-full flex items-center justify-between gap-3 px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-700 hover:border-purple-500 dark:hover:border-purple-400 transition-all bg-white dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5"
|
|
||||||
[class.text-purple-600]="filters.profession"
|
|
||||||
[class.dark:text-purple-400]="filters.profession"
|
|
||||||
[class.text-gray-400]="!filters.profession"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M6 3.75A2.75 2.75 0 018.75 1h2.5A2.75 2.75 0 0114 3.75v.443c.572.055 1.14.122 1.706.2C17.053 4.582 18 5.75 18 7.07v3.469c0 1.126-.694 2.191-1.83 2.54-1.952.599-4.024.921-6.17.921s-4.219-.322-6.17-.921C2.694 12.73 2 11.665 2 10.539V7.07c0-1.321.947-2.489 2.294-2.676A41.047 41.047 0 016 4.193V3.75zm6.5 0v.325a41.622 41.622 0 00-5 0V3.75c0-.69.56-1.25 1.25-1.25h2.5c.69 0 1.25.56 1.25 1.25zM10 10a1 1 0 00-1 1v.01a1 1 0 001 1h.01a1 1 0 001-1V11a1 1 0 00-1-1H10z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M3 15.055v-.684c.126.053.255.1.39.142 2.092.642 4.313.987 6.61.987 2.297 0 4.518-.345 6.61-.987.135-.041.264-.089.39-.142v.684c0 1.347-.985 2.53-2.363 2.686a41.454 41.454 0 01-9.274 0C3.985 17.585 3 16.402 3 15.055z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
{{ filters.profession || 'Profession' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5 text-gray-400 transition-transform"
|
|
||||||
[class.rotate-180]="showProfessionDropdown"
|
|
||||||
>
|
>
|
||||||
<path
|
<select
|
||||||
fill-rule="evenodd"
|
id="job-select"
|
||||||
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
|
formControlName="profession"
|
||||||
clip-rule="evenodd"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@if (showProfessionDropdown) {
|
|
||||||
<div
|
|
||||||
class="absolute z-10 mt-2 w-full bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg shadow-lg max-h-60 overflow-y-auto"
|
|
||||||
>
|
>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
(click)="selectProfession(null)"
|
|
||||||
class="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
||||||
[class.bg-purple-50]="!filters.profession"
|
|
||||||
[class.dark:bg-purple-900]="!filters.profession"
|
|
||||||
>
|
>
|
||||||
<span class="text-gray-700 dark:text-gray-300">Toutes les professions</span>
|
<option [ngValue]="null">Toutes les professions</option>
|
||||||
</button>
|
|
||||||
@for (profile of profiles(); track profile.id) {
|
@for (profile of profiles(); track profile.id) {
|
||||||
<button
|
<option [value]="profile.profession">{{ profile.profession }}</option>
|
||||||
type="button"
|
|
||||||
(click)="selectProfession(profile.profession)"
|
|
||||||
class="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
||||||
[class.bg-purple-50]="filters.profession === profile.profession"
|
|
||||||
[class.dark:bg-purple-900]="filters.profession === profile.profession"
|
|
||||||
>
|
|
||||||
<span class="text-gray-700 dark:text-gray-300">{{ profile.profession }}</span>
|
|
||||||
</button>
|
|
||||||
}
|
}
|
||||||
</div>
|
</select>
|
||||||
}
|
</div>
|
||||||
</div>
|
|
||||||
</ng-template>
|
|
||||||
|
|
||||||
<ng-template #tri>
|
<div class="space-y-1">
|
||||||
<div class="relative">
|
<label
|
||||||
<button
|
for="sort-select"
|
||||||
type="button"
|
class="block text-xs font-medium text-gray-500 dark:text-gray-400 uppercase"
|
||||||
(click)="toggleSortDropdown()"
|
>Trier par</label
|
||||||
class="w-full flex items-center justify-between gap-3 px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-700 hover:border-purple-500 dark:hover:border-purple-400 transition-all bg-white dark:bg-gray-800"
|
>
|
||||||
>
|
<select
|
||||||
<div class="flex items-center gap-2">
|
id="sort-select"
|
||||||
<svg
|
formControlName="sort"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
||||||
viewBox="0 0 20 20"
|
>
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5 text-gray-400"
|
|
||||||
>
|
>
|
||||||
<path
|
@for (opt of sortOptions; track opt.value) {
|
||||||
fill-rule="evenodd"
|
<option [value]="opt.value">{{ opt.label }}</option>
|
||||||
d="M2.24 6.8a.75.75 0 001.06-.04l1.95-2.1v8.59a.75.75 0 001.5 0V4.66l1.95 2.1a.75.75 0 101.1-1.02l-3.25-3.5a.75.75 0 00-1.1 0L2.2 5.74a.75.75 0 00.04 1.06zm8 6.4a.75.75 0 00-.04 1.06l3.25 3.5a.75.75 0 001.1 0l3.25-3.5a.75.75 0 10-1.1-1.02l-1.95 2.1V6.75a.75.75 0 00-1.5 0v8.59l-1.95-2.1a.75.75 0 00-1.06-.04z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
||||||
{{ sortLabel }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
fill="currentColor"
|
|
||||||
class="w-5 h-5 text-gray-400 transition-transform"
|
|
||||||
[class.rotate-180]="showSortDropdown"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@if (showSortDropdown) {
|
|
||||||
<div
|
|
||||||
class="absolute z-10 mt-2 w-full bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg shadow-lg"
|
|
||||||
>
|
|
||||||
@for (sort of sortOptions; track sort.value) {
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
(click)="selectSort(sort)"
|
|
||||||
class="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
|
|
||||||
[class.bg-purple-50]="filters.sort === sort.value"
|
|
||||||
[class.dark:bg-purple-900]="filters.sort === sort.value"
|
|
||||||
>
|
|
||||||
<span class="text-gray-700 dark:text-gray-300">{{ sort.label }}</span>
|
|
||||||
</button>
|
|
||||||
}
|
}
|
||||||
</div>
|
</select>
|
||||||
}
|
</div>
|
||||||
</div>
|
<div class="w-full">
|
||||||
</ng-template>
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full py-3 px-4 bg-gradient-to-r from-indigo-600 to-purple-600 hover:from-indigo-700 hover:to-purple-700 text-white font-medium rounded-lg shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
|
||||||
|
>
|
||||||
|
<small> Appliquer les filtres</small>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -9,26 +9,16 @@ import { of } from 'rxjs';
|
|||||||
import { Profile } from '@app/domain/profiles/profile.model';
|
import { Profile } from '@app/domain/profiles/profile.model';
|
||||||
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
||||||
import { Sector } from '@app/domain/sectors/sector.model';
|
import { Sector } from '@app/domain/sectors/sector.model';
|
||||||
|
import { mockProfileRepo } from '@app/testing/profile.mock';
|
||||||
|
import { mockSectorRepo } from '@app/testing/sector.mock';
|
||||||
|
|
||||||
describe('FilterComponent', () => {
|
describe('FilterComponent', () => {
|
||||||
let component: FilterComponent;
|
let component: FilterComponent;
|
||||||
let fixture: ComponentFixture<FilterComponent>;
|
let fixture: ComponentFixture<FilterComponent>;
|
||||||
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>>;
|
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>> = mockProfileRepo;
|
||||||
let mockSectorRepository: jest.Mocked<Partial<SectorRepository>>;
|
let mockSectorRepository: jest.Mocked<Partial<SectorRepository>> = mockSectorRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockProfileRepository = {
|
|
||||||
create: jest.fn().mockReturnValue(of({} as Profile)),
|
|
||||||
list: jest.fn().mockReturnValue(of([])),
|
|
||||||
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
|
|
||||||
update: jest.fn().mockReturnValue(of({} as Profile)),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockSectorRepository = {
|
|
||||||
list: jest.fn().mockReturnValue(of([])),
|
|
||||||
getOne: jest.fn().mockReturnValue(of({} as Sector)),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [FilterComponent],
|
imports: [FilterComponent],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Component, inject, OnInit, output } from '@angular/core';
|
import { Component, inject, OnInit, output } from '@angular/core';
|
||||||
import { SearchFilters } from '@app/domain/search-filters';
|
|
||||||
import { SectorFacade } from '@app/ui/sectors/sector.facade';
|
import { SectorFacade } from '@app/ui/sectors/sector.facade';
|
||||||
import { ProfileFacade } from '@app/ui/profiles/profile.facade';
|
import { ProfileFacade } from '@app/ui/profiles/profile.facade';
|
||||||
import { NgTemplateOutlet } from '@angular/common';
|
import { SearchService } from '@app/infrastructure/search/search.service';
|
||||||
|
import { FormBuilder, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { SearchFilters } from '@app/domain/search/search-filters';
|
||||||
|
|
||||||
interface SortOption {
|
interface SortOption {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -12,41 +13,36 @@ interface SortOption {
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'app-filter',
|
selector: 'app-filter',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [NgTemplateOutlet],
|
imports: [FormsModule, ReactiveFormsModule],
|
||||||
providers: [SectorFacade],
|
providers: [SectorFacade],
|
||||||
templateUrl: './filter.component.html',
|
templateUrl: './filter.component.html',
|
||||||
styleUrl: './filter.component.scss',
|
styleUrl: './filter.component.scss',
|
||||||
})
|
})
|
||||||
export class FilterComponent implements OnInit {
|
export class FilterComponent implements OnInit {
|
||||||
filtersChanged = output<SearchFilters>();
|
private readonly searchService = inject(SearchService);
|
||||||
|
|
||||||
// État des dropdowns
|
|
||||||
showSectorDropdown = false;
|
|
||||||
showProfessionDropdown = false;
|
|
||||||
showSortDropdown = false;
|
|
||||||
|
|
||||||
// Filtres
|
|
||||||
filters: SearchFilters = {
|
|
||||||
search: '',
|
|
||||||
verified: false,
|
|
||||||
secteur: null,
|
|
||||||
profession: null,
|
|
||||||
sort: 'recent',
|
|
||||||
};
|
|
||||||
|
|
||||||
protected readonly sectorFacade = inject(SectorFacade);
|
protected readonly sectorFacade = inject(SectorFacade);
|
||||||
protected readonly sectors = this.sectorFacade.sectors;
|
protected readonly sectors = this.sectorFacade.sectors;
|
||||||
protected readonly sectorLoading = this.sectorFacade.loading;
|
protected readonly profileFacade = inject(ProfileFacade);
|
||||||
protected readonly sectorError = this.sectorFacade.error;
|
protected readonly profiles = this.profileFacade.profiles;
|
||||||
|
|
||||||
protected readonly ProfileFacade = inject(ProfileFacade);
|
readonly activeFilters = this.searchService.getFilters();
|
||||||
protected readonly profiles = this.ProfileFacade.profiles;
|
|
||||||
protected readonly profileLoading = this.ProfileFacade.loading;
|
|
||||||
protected readonly profileError = this.ProfileFacade.error;
|
|
||||||
|
|
||||||
ngOnInit() {
|
onFilterChange = output<SearchFilters>();
|
||||||
this.sectorFacade.load();
|
|
||||||
this.ProfileFacade.load();
|
private fb = inject(FormBuilder);
|
||||||
|
|
||||||
|
filterForm = this.fb.group({
|
||||||
|
verified: [this.activeFilters().verified],
|
||||||
|
secteur: [this.activeFilters().secteur],
|
||||||
|
profession: [this.activeFilters().profession],
|
||||||
|
sort: [this.activeFilters().sort || 'recent'],
|
||||||
|
});
|
||||||
|
|
||||||
|
onSubmit() {
|
||||||
|
const filters = this.filterForm.getRawValue() as SearchFilters;
|
||||||
|
this.searchService.setFilters(filters);
|
||||||
|
this.onFilterChange.emit(filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
sortOptions: SortOption[] = [
|
sortOptions: SortOption[] = [
|
||||||
@@ -56,73 +52,33 @@ export class FilterComponent implements OnInit {
|
|||||||
{ value: 'verified', label: 'Vérifiés' },
|
{ value: 'verified', label: 'Vérifiés' },
|
||||||
];
|
];
|
||||||
|
|
||||||
get sortLabel(): string {
|
ngOnInit() {
|
||||||
return this.sortOptions.find((s) => s.value === this.filters.sort)?.label || 'Trier par';
|
this.sectorFacade.load();
|
||||||
|
this.profileFacade.load();
|
||||||
|
this.initFormValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gestion du filtre "Vérifiés"
|
// Vérifier si des filtres sont actifs pour afficher le bouton "Effacer"
|
||||||
toggleVerifiedFilter(): void {
|
|
||||||
this.filters.verified = !this.filters.verified;
|
|
||||||
this.emitFilters();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gestion des dropdowns
|
|
||||||
toggleSectorDropdown(): void {
|
|
||||||
this.showSectorDropdown = !this.showSectorDropdown;
|
|
||||||
this.showProfessionDropdown = false;
|
|
||||||
this.showSortDropdown = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleProfessionDropdown(): void {
|
|
||||||
this.showProfessionDropdown = !this.showProfessionDropdown;
|
|
||||||
this.showSectorDropdown = false;
|
|
||||||
this.showSortDropdown = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleSortDropdown(): void {
|
|
||||||
this.showSortDropdown = !this.showSortDropdown;
|
|
||||||
this.showSectorDropdown = false;
|
|
||||||
this.showProfessionDropdown = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sélection des filtres
|
|
||||||
selectSector(sector: string | null): void {
|
|
||||||
this.filters.secteur = sector;
|
|
||||||
this.showSectorDropdown = false;
|
|
||||||
this.emitFilters();
|
|
||||||
}
|
|
||||||
|
|
||||||
selectProfession(profession: string | null): void {
|
|
||||||
this.filters.profession = profession;
|
|
||||||
this.showProfessionDropdown = false;
|
|
||||||
this.emitFilters();
|
|
||||||
}
|
|
||||||
|
|
||||||
selectSort(sort: SortOption): void {
|
|
||||||
this.filters.sort = sort.value;
|
|
||||||
this.showSortDropdown = false;
|
|
||||||
this.emitFilters();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier si des filtres sont actifs
|
|
||||||
hasActiveFilters(): boolean {
|
hasActiveFilters(): boolean {
|
||||||
return this.filters.verified || !!this.filters.secteur || !!this.filters.profession;
|
const f = this.activeFilters();
|
||||||
|
return f.verified || !!f.secteur || !!f.profession;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Effacer tous les filtres
|
|
||||||
clearAllFilters(): void {
|
clearAllFilters(): void {
|
||||||
this.filters = {
|
this.searchService.reset();
|
||||||
search: '',
|
this.onFilterChange.emit(this.activeFilters());
|
||||||
verified: false,
|
this.initFormValues();
|
||||||
secteur: null,
|
|
||||||
profession: null,
|
|
||||||
sort: 'recent',
|
|
||||||
};
|
|
||||||
this.emitFilters();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Émettre les changements de filtres
|
private initFormValues() {
|
||||||
private emitFilters(): void {
|
const filters = this.searchService.getFilters();
|
||||||
this.filtersChanged.emit({ ...this.filters });
|
this.activeFilters.set(filters());
|
||||||
|
|
||||||
|
this.filterForm.setValue({
|
||||||
|
verified: this.activeFilters().verified,
|
||||||
|
secteur: this.activeFilters().secteur,
|
||||||
|
profession: this.activeFilters().profession,
|
||||||
|
sort: this.activeFilters().sort || 'recent',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,43 +8,20 @@ import { AuthRepository } from '@app/domain/authentification/auth.repository';
|
|||||||
import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token';
|
import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token';
|
||||||
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
||||||
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
||||||
|
import { mockAuthRepo } from '@app/testing/auth.mock';
|
||||||
|
import { mockProfileRepo } from '@app/testing/profile.mock';
|
||||||
|
import { mockToastR } from '@app/testing/toastr.mock';
|
||||||
|
|
||||||
describe('LoginComponent', () => {
|
describe('LoginComponent', () => {
|
||||||
let component: LoginComponent;
|
let component: LoginComponent;
|
||||||
let fixture: ComponentFixture<LoginComponent>;
|
let fixture: ComponentFixture<LoginComponent>;
|
||||||
|
|
||||||
// Mocks des services
|
// Mocks des services
|
||||||
let mockToastrService: Partial<ToastrService>;
|
let mockToastrService: Partial<ToastrService> = mockToastR;
|
||||||
let mockAuthRepository: jest.Mocked<Partial<AuthRepository>>;
|
let mockAuthRepository: jest.Mocked<Partial<AuthRepository>> = mockAuthRepo;
|
||||||
let mockProfileRepo: jest.Mocked<Partial<ProfileRepository>>;
|
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>> = mockProfileRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockToastrService = {
|
|
||||||
warning: jest.fn(),
|
|
||||||
success: jest.fn(),
|
|
||||||
info: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockAuthRepository = {
|
|
||||||
get: jest.fn(),
|
|
||||||
login: jest.fn(),
|
|
||||||
sendVerificationEmail: jest.fn(),
|
|
||||||
logout: jest.fn(),
|
|
||||||
isAuthenticated: jest.fn(),
|
|
||||||
isEmailVerified: jest.fn(),
|
|
||||||
register: jest.fn(),
|
|
||||||
sendRequestPasswordReset: jest.fn(),
|
|
||||||
confirmPasswordReset: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockProfileRepo = {
|
|
||||||
create: jest.fn(),
|
|
||||||
list: jest.fn(),
|
|
||||||
update: jest.fn(),
|
|
||||||
getByUserId: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [LoginComponent],
|
imports: [LoginComponent],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -52,7 +29,7 @@ describe('LoginComponent', () => {
|
|||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{ provide: ToastrService, useValue: mockToastrService },
|
{ provide: ToastrService, useValue: mockToastrService },
|
||||||
{ provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository },
|
{ provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository },
|
||||||
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
|
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
|
|||||||
38
src/app/shared/features/pagination/pagination.component.html
Normal file
38
src/app/shared/features/pagination/pagination.component.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<!-- Pagination responsive -->
|
||||||
|
<section class="flex justify-center mt-8 sm:mt-12" aria-label="Pagination">
|
||||||
|
<ul class="flex flex-wrap gap-2 items-center justify-center">
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 transition-colors"
|
||||||
|
[class.opacity-50]="currentPage === 1"
|
||||||
|
[class.pointer-events-none]="currentPage === 1"
|
||||||
|
[class.hover:bg-gray-100]="currentPage > 1"
|
||||||
|
(click)="goToPreviousPage()"
|
||||||
|
type="button"
|
||||||
|
[disabled]="currentPage === 1"
|
||||||
|
>
|
||||||
|
Précédent
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<span class="px-4 py-2 rounded-lg bg-indigo-600 text-white font-medium">
|
||||||
|
{{ currentPage }} / {{ filters.totalPages! }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
class="px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-700 transition-colors"
|
||||||
|
[class.opacity-50]="currentPage >= filters.totalPages!"
|
||||||
|
[class.pointer-events-none]="currentPage >= filters.totalPages!"
|
||||||
|
[class.hover:bg-gray-100]="currentPage < filters.totalPages!"
|
||||||
|
(click)="goToNextPage()"
|
||||||
|
type="button"
|
||||||
|
[disabled]="currentPage >= filters.totalPages!"
|
||||||
|
>
|
||||||
|
Suivant
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
34
src/app/shared/features/pagination/pagination.component.ts
Normal file
34
src/app/shared/features/pagination/pagination.component.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { Component, Input, OnInit, output } from '@angular/core';
|
||||||
|
import { SearchFilters } from '@app/domain/search/search-filters';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-pagination',
|
||||||
|
standalone: true,
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './pagination.component.html',
|
||||||
|
styleUrl: './pagination.component.scss',
|
||||||
|
})
|
||||||
|
export class PaginationComponent implements OnInit {
|
||||||
|
@Input({ required: true }) filters: SearchFilters = {} as SearchFilters;
|
||||||
|
onPageChange = output<SearchFilters>();
|
||||||
|
currentPage = 1;
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.currentPage = this.filters.page!;
|
||||||
|
}
|
||||||
|
|
||||||
|
goToPreviousPage() {
|
||||||
|
this.currentPage = this.currentPage - 1;
|
||||||
|
this.emitPageChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
goToNextPage() {
|
||||||
|
this.currentPage = this.currentPage + 1;
|
||||||
|
this.emitPageChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
private emitPageChange() {
|
||||||
|
const filters = { ...this.filters, page: this.currentPage };
|
||||||
|
this.onPageChange.emit(filters);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,47 +7,25 @@ import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
|||||||
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
||||||
import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token';
|
import { AUTH_REPOSITORY_TOKEN } from '@app/infrastructure/authentification/auth-repository.token';
|
||||||
import { AuthRepository } from '@app/domain/authentification/auth.repository';
|
import { AuthRepository } from '@app/domain/authentification/auth.repository';
|
||||||
|
import { mockToastR } from '@app/testing/toastr.mock';
|
||||||
|
import { mockProfileRepo } from '@app/testing/profile.mock';
|
||||||
|
import { mockAuthRepo } from '@app/testing/auth.mock';
|
||||||
|
|
||||||
describe('RegisterComponent', () => {
|
describe('RegisterComponent', () => {
|
||||||
let component: RegisterComponent;
|
let component: RegisterComponent;
|
||||||
let fixture: ComponentFixture<RegisterComponent>;
|
let fixture: ComponentFixture<RegisterComponent>;
|
||||||
|
|
||||||
let mockToastrService: jest.Mocked<Partial<ToastrService>>;
|
let mockToastrService: jest.Mocked<Partial<ToastrService>> = mockToastR;
|
||||||
let mockProfileRepo: jest.Mocked<Partial<ProfileRepository>>;
|
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>> = mockProfileRepo;
|
||||||
let mockAuthRepository: jest.Mocked<Partial<AuthRepository>>;
|
let mockAuthRepository: jest.Mocked<Partial<AuthRepository>> = mockAuthRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockProfileRepo = {
|
|
||||||
create: jest.fn(),
|
|
||||||
list: jest.fn(),
|
|
||||||
update: jest.fn(),
|
|
||||||
getByUserId: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockToastrService = {
|
|
||||||
success: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warning: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockAuthRepository = {
|
|
||||||
get: jest.fn(),
|
|
||||||
login: jest.fn(),
|
|
||||||
sendVerificationEmail: jest.fn(),
|
|
||||||
logout: jest.fn(),
|
|
||||||
isAuthenticated: jest.fn(),
|
|
||||||
isEmailVerified: jest.fn(),
|
|
||||||
register: jest.fn(),
|
|
||||||
sendRequestPasswordReset: jest.fn(),
|
|
||||||
confirmPasswordReset: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [RegisterComponent],
|
imports: [RegisterComponent],
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{ provide: ToastrService, useValue: mockToastrService },
|
{ provide: ToastrService, useValue: mockToastrService },
|
||||||
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
|
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository },
|
||||||
{ provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository },
|
{ provide: AUTH_REPOSITORY_TOKEN, useValue: mockAuthRepository },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|||||||
@@ -1,14 +1,5 @@
|
|||||||
<div class="w-full space-y-4 sm:space-y-6 z-[800]">
|
<div class="w-full space-y-4 sm:space-y-6 z-[800]">
|
||||||
<!-- Filtres -->
|
|
||||||
<div>
|
|
||||||
<app-filter (filtersChanged)="onFiltersChanged($event)" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Barre de recherche -->
|
<!-- Barre de recherche -->
|
||||||
<ng-container *ngTemplateOutlet="form"></ng-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ng-template #form>
|
|
||||||
<form [formGroup]="searchForm" (ngSubmit)="onSubmit()" class="w-full">
|
<form [formGroup]="searchForm" (ngSubmit)="onSubmit()" class="w-full">
|
||||||
<div class="flex flex-col sm:flex-row gap-3 sm:gap-0 w-full">
|
<div class="flex flex-col sm:flex-row gap-3 sm:gap-0 w-full">
|
||||||
<div
|
<div
|
||||||
@@ -53,12 +44,12 @@
|
|||||||
class="w-full sm:w-auto px-6 sm:px-8 py-3 sm:py-2.5 rounded-full sm:rounded-l-none sm:rounded-r-full bg-purple-600 hover:bg-purple-700 active:bg-purple-800 text-white font-medium text-sm sm:text-base transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
class="w-full sm:w-auto px-6 sm:px-8 py-3 sm:py-2.5 rounded-full sm:rounded-l-none sm:rounded-r-full bg-purple-600 hover:bg-purple-700 active:bg-purple-800 text-white font-medium text-sm sm:text-base transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
<span class="hidden sm:inline">{{
|
<span class="hidden sm:inline">{{
|
||||||
searchForm.value!.search! === '' ? 'Voir tout' : 'Rechercher'
|
searchForm.value!.search! === '' ? 'Explorer' : 'Rechercher'
|
||||||
}}</span>
|
}}</span>
|
||||||
<span class="sm:hidden">{{
|
<span class="sm:hidden">{{
|
||||||
searchForm.value!.search! === '' ? 'Tout' : 'Rechercher'
|
searchForm.value!.search! === '' ? 'Explorer' : 'Rechercher'
|
||||||
}}</span>
|
}}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</ng-template>
|
</div>
|
||||||
|
|||||||
@@ -4,37 +4,25 @@ import { SearchComponent } from './search.component';
|
|||||||
import { provideRouter } from '@angular/router';
|
import { provideRouter } from '@angular/router';
|
||||||
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
||||||
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
|
import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token';
|
||||||
import { of } from 'rxjs';
|
|
||||||
import { Profile } from '@app/domain/profiles/profile.model';
|
|
||||||
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
||||||
import { SectorRepository } from '@app/domain/sectors/sector.repository';
|
import { SectorRepository } from '@app/domain/sectors/sector.repository';
|
||||||
import { Sector } from '@app/domain/sectors/sector.model';
|
import { mockProfileRepo } from '@app/testing/profile.mock';
|
||||||
|
import { mockSectorRepo } from '@app/testing/sector.mock';
|
||||||
|
|
||||||
describe('SearchComponent', () => {
|
describe('SearchComponent', () => {
|
||||||
let component: SearchComponent;
|
let component: SearchComponent;
|
||||||
let fixture: ComponentFixture<SearchComponent>;
|
let fixture: ComponentFixture<SearchComponent>;
|
||||||
|
|
||||||
let mockProfileRepo: jest.Mocked<Partial<ProfileRepository>>;
|
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>> = mockProfileRepo;
|
||||||
let mockSectorRepo: jest.Mocked<Partial<SectorRepository>>;
|
let mockSectorRepository: jest.Mocked<Partial<SectorRepository>> = mockSectorRepo;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockProfileRepo = {
|
|
||||||
create: jest.fn().mockReturnValue(of({} as Profile)),
|
|
||||||
list: jest.fn().mockReturnValue(of([])),
|
|
||||||
getByUserId: jest.fn().mockReturnValue(of({} as Profile)),
|
|
||||||
update: jest.fn().mockReturnValue(of({} as Profile)),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockSectorRepo = {
|
|
||||||
list: jest.fn().mockReturnValue(of([])),
|
|
||||||
getOne: jest.fn().mockReturnValue(of({} as Sector)),
|
|
||||||
};
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [SearchComponent],
|
imports: [SearchComponent],
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo },
|
{ provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository },
|
||||||
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepo },
|
{ provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepository },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,28 @@
|
|||||||
import { Component, inject, output } from '@angular/core';
|
import { Component, inject, output } from '@angular/core';
|
||||||
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { FilterComponent } from '@app/shared/features/filter/filter.component';
|
import { SearchFilters } from '@app/domain/search/search-filters';
|
||||||
import { SearchFilters } from '@app/domain/search-filters';
|
import { SearchService } from '@app/infrastructure/search/search.service';
|
||||||
import { NgTemplateOutlet } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-search',
|
selector: 'app-search',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ReactiveFormsModule, FilterComponent, NgTemplateOutlet],
|
imports: [ReactiveFormsModule],
|
||||||
templateUrl: './search.component.html',
|
templateUrl: './search.component.html',
|
||||||
styleUrl: './search.component.scss',
|
styleUrl: './search.component.scss',
|
||||||
})
|
})
|
||||||
export class SearchComponent {
|
export class SearchComponent {
|
||||||
|
private readonly searchService = inject(SearchService);
|
||||||
onSearchChange = output<SearchFilters>();
|
onSearchChange = output<SearchFilters>();
|
||||||
private formBuilder: FormBuilder = inject(FormBuilder);
|
private formBuilder: FormBuilder = inject(FormBuilder);
|
||||||
|
|
||||||
// Filtres
|
|
||||||
filters: SearchFilters = {
|
|
||||||
search: '',
|
|
||||||
verified: false,
|
|
||||||
secteur: null,
|
|
||||||
profession: null,
|
|
||||||
sort: 'recent',
|
|
||||||
};
|
|
||||||
|
|
||||||
searchForm = this.formBuilder.group({
|
searchForm = this.formBuilder.group({
|
||||||
search: new FormControl('', Validators.required),
|
search: new FormControl('', Validators.required),
|
||||||
});
|
});
|
||||||
|
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
const search = this.searchForm.value.search?.toLowerCase()!;
|
const search = this.searchForm.value.search?.toLowerCase()!;
|
||||||
this.onSearchChange.emit({ ...this.filters, search });
|
this.searchService.search(search);
|
||||||
}
|
const filters = this.searchService.getFilters();
|
||||||
|
this.onSearchChange.emit({ ...filters(), search });
|
||||||
onFiltersChanged(event: SearchFilters) {
|
|
||||||
this.filters = event;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,28 +5,22 @@ import { provideRouter } from '@angular/router';
|
|||||||
import { UserRepository } from '@app/domain/users/user.repository';
|
import { UserRepository } from '@app/domain/users/user.repository';
|
||||||
import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository.token';
|
import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository.token';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
|
import { mockUserRepo } from '@app/testing/user.mock';
|
||||||
|
import { mockToastR } from '@app/testing/toastr.mock';
|
||||||
|
|
||||||
describe('UpdateUserComponent', () => {
|
describe('UpdateUserComponent', () => {
|
||||||
let component: UpdateUserComponent;
|
let component: UpdateUserComponent;
|
||||||
let fixture: ComponentFixture<UpdateUserComponent>;
|
let fixture: ComponentFixture<UpdateUserComponent>;
|
||||||
|
|
||||||
let mockUserRepo: jest.Mocked<Partial<UserRepository>>;
|
let mockUserRepository: jest.Mocked<Partial<UserRepository>> = mockUserRepo;
|
||||||
let mockToastrService: jest.Mocked<Partial<ToastrService>>;
|
let mockToastrService: jest.Mocked<Partial<ToastrService>> = mockToastR;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockUserRepo = {
|
|
||||||
getUserById: jest.fn(),
|
|
||||||
};
|
|
||||||
mockToastrService = {
|
|
||||||
warning: jest.fn(),
|
|
||||||
success: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
};
|
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [UpdateUserComponent],
|
imports: [UpdateUserComponent],
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{ provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepo },
|
{ provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepository },
|
||||||
{ provide: ToastrService, useValue: mockToastrService },
|
{ provide: ToastrService, useValue: mockToastrService },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|||||||
11
src/app/testing/auth.mock.ts
Normal file
11
src/app/testing/auth.mock.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export const mockAuthRepo = {
|
||||||
|
get: jest.fn(),
|
||||||
|
login: jest.fn(),
|
||||||
|
sendVerificationEmail: jest.fn(),
|
||||||
|
logout: jest.fn(),
|
||||||
|
isAuthenticated: jest.fn(),
|
||||||
|
isEmailVerified: jest.fn(),
|
||||||
|
register: jest.fn(),
|
||||||
|
sendRequestPasswordReset: jest.fn(),
|
||||||
|
confirmPasswordReset: jest.fn(),
|
||||||
|
};
|
||||||
@@ -11,7 +11,9 @@ export class FakeAuthRepository implements AuthRepository {
|
|||||||
newPassword: string,
|
newPassword: string,
|
||||||
confirmPassword: string
|
confirmPassword: string
|
||||||
): Observable<boolean> {
|
): Observable<boolean> {
|
||||||
return of(true);
|
return of(
|
||||||
|
resetToken === 'fakeToken' && newPassword === confirmPassword && fakeUsers[0].verified
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(): User | undefined {
|
get(): User | undefined {
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { Profile } from '@app/domain/profiles/profile.model';
|
import { Profile, ProfilePaginated } from '@app/domain/profiles/profile.model';
|
||||||
import { mockProfiles } from '@app/testing/profile.mock';
|
import { mockProfilePaginated, mockProfiles } from '@app/testing/profile.mock';
|
||||||
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
||||||
import { Observable, of } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
|
|
||||||
export class FakeProfileRepository implements ProfileRepository {
|
export class FakeProfileRepository implements ProfileRepository {
|
||||||
list(): Observable<Profile[]> {
|
list(): Observable<ProfilePaginated> {
|
||||||
return of(mockProfiles);
|
return of(mockProfilePaginated);
|
||||||
}
|
}
|
||||||
|
|
||||||
getByUserId(userId: string): Observable<Profile> {
|
getById(profileId: string): Observable<Profile> {
|
||||||
const profile = mockProfiles.find((p) => p.utilisateur === userId) ?? ({} as Profile);
|
const profile = mockProfiles.find((p) => p.utilisateur === profileId) ?? ({} as Profile);
|
||||||
return of(profile);
|
return of(profile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { PbProfileRepository } from '@app/infrastructure/profiles/pb-profile.rep
|
|||||||
import { Profile } from '@app/domain/profiles/profile.model';
|
import { Profile } from '@app/domain/profiles/profile.model';
|
||||||
import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
|
import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
|
||||||
import PocketBase from 'pocketbase';
|
import PocketBase from 'pocketbase';
|
||||||
import { mockProfiles } from '@app/testing/profile.mock';
|
import { mockProfilePaginated, mockProfiles } from '@app/testing/profile.mock';
|
||||||
|
|
||||||
jest.mock('pocketbase'); // on mock le module PocketBase
|
jest.mock('pocketbase'); // on mock le module PocketBase
|
||||||
|
|
||||||
@@ -14,8 +14,8 @@ describe('PbProfileRepository', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Création d’un faux client PocketBase avec les méthodes dont on a besoin
|
// Création d’un faux client PocketBase avec les méthodes dont on a besoin
|
||||||
mockCollection = {
|
mockCollection = {
|
||||||
getFullList: jest.fn(),
|
getList: jest.fn(),
|
||||||
getFirstListItem: jest.fn(),
|
getOne: jest.fn(),
|
||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
};
|
};
|
||||||
@@ -24,7 +24,6 @@ describe('PbProfileRepository', () => {
|
|||||||
collection: jest.fn().mockReturnValue(mockCollection),
|
collection: jest.fn().mockReturnValue(mockCollection),
|
||||||
};
|
};
|
||||||
|
|
||||||
// @ts-ignore : on remplace l’instance réelle de PocketBase par notre mock
|
|
||||||
(PocketBase as jest.Mock).mockImplementation(() => mockPocketBase);
|
(PocketBase as jest.Mock).mockImplementation(() => mockPocketBase);
|
||||||
|
|
||||||
// on récupère une "collection" simulée
|
// on récupère une "collection" simulée
|
||||||
@@ -36,18 +35,18 @@ describe('PbProfileRepository', () => {
|
|||||||
// ------------------------------------------
|
// ------------------------------------------
|
||||||
// 🔹 TEST : list()
|
// 🔹 TEST : list()
|
||||||
// ------------------------------------------
|
// ------------------------------------------
|
||||||
it('devrait appeler pb.collection("profiles").getFullList() avec un tri par profession', (done) => {
|
it('devrait appeler pb.collection("profiles").getList() avec un tri par profession', (done) => {
|
||||||
mockCollection.getFullList.mockResolvedValue(mockProfiles);
|
mockCollection.getList.mockResolvedValue(mockProfilePaginated);
|
||||||
const options = {
|
const options = {
|
||||||
sort: 'profession',
|
expand: 'utilisateur,secteur',
|
||||||
expand: 'utilisateur',
|
|
||||||
filter:
|
filter:
|
||||||
"utilisateur.verified=true && utilisateur.name !='' && profession!='Profession non renseignée' && secteur!='' ",
|
"utilisateur.verified = true && utilisateur.name != '' && profession != 'Profession non renseignée' && secteur != ''",
|
||||||
|
sort: '-created',
|
||||||
};
|
};
|
||||||
repo.list().subscribe((result) => {
|
repo.list().subscribe((result) => {
|
||||||
expect(mockPocketBase.collection).toHaveBeenCalledWith('profiles');
|
expect(mockPocketBase.collection).toHaveBeenCalledWith('profiles');
|
||||||
expect(mockCollection.getFullList).toHaveBeenCalledWith(options);
|
expect(mockCollection.getList).toHaveBeenCalledWith(undefined, undefined, options);
|
||||||
expect(result).toEqual(mockProfiles);
|
expect(result).toEqual(mockProfilePaginated);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -55,13 +54,13 @@ describe('PbProfileRepository', () => {
|
|||||||
// ------------------------------------------
|
// ------------------------------------------
|
||||||
// 🔹 TEST : getByUserId()
|
// 🔹 TEST : getByUserId()
|
||||||
// ------------------------------------------
|
// ------------------------------------------
|
||||||
it('devrait appeler pb.collection("profiles").getFirstListItem() avec le bon filtre utilisateur', () => {
|
it('devrait appeler pb.collection("profiles").getOne() avec le bon filtre utilisateur', () => {
|
||||||
const userId = '1';
|
const profileId = '1';
|
||||||
|
|
||||||
mockCollection.getFirstListItem.mockResolvedValue(mockProfiles);
|
mockCollection.getOne.mockResolvedValue(mockProfiles);
|
||||||
|
|
||||||
repo.getByUserId(userId).subscribe((result) => {
|
repo.getById(profileId).subscribe((result) => {
|
||||||
expect(mockCollection.getFirstListItem).toHaveBeenCalledWith(`utilisateur="${userId}"`);
|
expect(mockCollection.getOne).toHaveBeenCalledWith(`${profileId}`, { expand: 'utilisateur' });
|
||||||
expect(result).toEqual(mockProfiles[0]);
|
expect(result).toEqual(mockProfiles[0]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Profile } from '@app/domain/profiles/profile.model';
|
import { Profile, ProfilePaginated } from '@app/domain/profiles/profile.model';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
export const mockProfiles: Profile[] = [
|
export const mockProfiles: Profile[] = [
|
||||||
{
|
{
|
||||||
@@ -30,3 +31,18 @@ export const mockProfiles: Profile[] = [
|
|||||||
apropos: 'Designer Freelance',
|
apropos: 'Designer Freelance',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const mockProfilePaginated: ProfilePaginated = {
|
||||||
|
page: 1,
|
||||||
|
perPage: 10,
|
||||||
|
totalPages: 1,
|
||||||
|
totalItems: 1,
|
||||||
|
items: mockProfiles,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mockProfileRepo = {
|
||||||
|
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)),
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Project } from '@app/domain/projects/project.model';
|
import { Project } from '@app/domain/projects/project.model';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
export const fakeProjects: Project[] = [
|
export const fakeProjects: Project[] = [
|
||||||
{
|
{
|
||||||
@@ -57,3 +58,10 @@ export const fakeProjects: Project[] = [
|
|||||||
utilisateur: 'user_001',
|
utilisateur: 'user_001',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const mockProjectRepo = {
|
||||||
|
create: jest.fn().mockReturnValue(of({} as Project)),
|
||||||
|
list: jest.fn().mockReturnValue(of([])),
|
||||||
|
get: jest.fn().mockReturnValue(of({} as Project)),
|
||||||
|
update: jest.fn().mockReturnValue(of({} as Project)),
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Sector } from '@app/domain/sectors/sector.model';
|
import { Sector } from '@app/domain/sectors/sector.model';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
export const fakeSectors: Sector[] = [
|
export const fakeSectors: Sector[] = [
|
||||||
{
|
{
|
||||||
@@ -32,3 +33,8 @@ export const fakeSectors: Sector[] = [
|
|||||||
nom: 'Ressources humaines',
|
nom: 'Ressources humaines',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const mockSectorRepo = {
|
||||||
|
list: jest.fn().mockReturnValue(of([])),
|
||||||
|
getOne: jest.fn().mockReturnValue(of({} as Sector)),
|
||||||
|
};
|
||||||
|
|||||||
6
src/app/testing/theme.mock.ts
Normal file
6
src/app/testing/theme.mock.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { signal } from '@angular/core';
|
||||||
|
|
||||||
|
export const mockThemeService = {
|
||||||
|
darkModeSignal: signal<string>('null'),
|
||||||
|
updateDarkMode: jest.fn(),
|
||||||
|
};
|
||||||
6
src/app/testing/toastr.mock.ts
Normal file
6
src/app/testing/toastr.mock.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export const mockToastR = {
|
||||||
|
warning: jest.fn(),
|
||||||
|
success: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
};
|
||||||
8
src/app/testing/ui/authentification/auth.facade.mock.ts
Normal file
8
src/app/testing/ui/authentification/auth.facade.mock.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { signal } from '@angular/core';
|
||||||
|
import { ActionType } from '@app/domain/action-type.util';
|
||||||
|
|
||||||
|
export const mockAuthenticationFacade = {
|
||||||
|
sendRequestPasswordReset: jest.fn(),
|
||||||
|
loading: signal({ isLoading: false, action: ActionType.NONE }),
|
||||||
|
error: signal({ hasError: false }),
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ListProfilesUseCase } from '@app/usecase/profiles/list-profiles.usecase';
|
import { ListProfilesUseCase } from '@app/usecase/profiles/list-profiles.usecase';
|
||||||
import { FakeProfileRepository } from '@app/testing/domain/profiles/fake-profile.repository';
|
import { FakeProfileRepository } from '@app/testing/domain/profiles/fake-profile.repository';
|
||||||
import { mockProfiles } from '@app/testing/profile.mock';
|
import { mockProfilePaginated, mockProfiles } from '@app/testing/profile.mock';
|
||||||
|
|
||||||
describe('ListProfilesUseCase', () => {
|
describe('ListProfilesUseCase', () => {
|
||||||
it('doit retourner la liste des profils', () => {
|
it('doit retourner la liste des profils', () => {
|
||||||
@@ -9,8 +9,8 @@ describe('ListProfilesUseCase', () => {
|
|||||||
|
|
||||||
useCase.execute().subscribe({
|
useCase.execute().subscribe({
|
||||||
next: (profiles) => {
|
next: (profiles) => {
|
||||||
expect(profiles.length).toBe(2);
|
expect(profiles.items.length).toBe(2);
|
||||||
expect(profiles).toEqual(mockProfiles);
|
expect(profiles).toEqual(mockProfilePaginated);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { User } from '@app/domain/users/user.model';
|
import { User } from '@app/domain/users/user.model';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
|
||||||
export const fakeUsers: User[] = [
|
export const fakeUsers: User[] = [
|
||||||
{
|
{
|
||||||
@@ -13,3 +14,8 @@ export const fakeUsers: User[] = [
|
|||||||
verified: false,
|
verified: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const mockUserRepo = {
|
||||||
|
getUserById: jest.fn().mockReturnValue(of({} as User)),
|
||||||
|
update: jest.fn(),
|
||||||
|
};
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export class AuthFacade {
|
|||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
this.logoutUseCase.execute();
|
this.logoutUseCase.execute();
|
||||||
|
this.isAuthenticated.set(false);
|
||||||
this.getCurrentUser();
|
this.getCurrentUser();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ListProfilesUseCase } from '@app/usecase/profiles/list-profiles.usecase';
|
import { ListProfilesUseCase } from '@app/usecase/profiles/list-profiles.usecase';
|
||||||
import { inject, Injectable, signal } from '@angular/core';
|
import { inject, Injectable, signal } from '@angular/core';
|
||||||
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token';
|
||||||
import { Profile } from '@app/domain/profiles/profile.model';
|
import { Profile, ProfilePaginated } from '@app/domain/profiles/profile.model';
|
||||||
import { ProfilePresenter } from '@app/ui/profiles/profile.presenter';
|
import { ProfilePresenter } from '@app/ui/profiles/profile.presenter';
|
||||||
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
|
import { ProfileViewModel } from '@app/ui/profiles/profile.presenter.model';
|
||||||
import { LoaderAction } from '@app/domain/loader-action.util';
|
import { LoaderAction } from '@app/domain/loader-action.util';
|
||||||
@@ -11,19 +11,24 @@ import { CreateProfileUseCase } from '@app/usecase/profiles/create-profile.useca
|
|||||||
import { UpdateProfileUseCase } from '@app/usecase/profiles/update-profile.usecase';
|
import { UpdateProfileUseCase } from '@app/usecase/profiles/update-profile.usecase';
|
||||||
import { GetProfileUseCase } from '@app/usecase/profiles/get-profile.usecase';
|
import { GetProfileUseCase } from '@app/usecase/profiles/get-profile.usecase';
|
||||||
import { ProfileDTO } from '@app/domain/profiles/dto/create-profile.dto';
|
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';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class ProfileFacade {
|
export class ProfileFacade {
|
||||||
private profileRepository = inject(PROFILE_REPOSITORY_TOKEN);
|
private profileRepository = inject(PROFILE_REPOSITORY_TOKEN);
|
||||||
|
private readonly searchService = inject(SearchService);
|
||||||
|
|
||||||
private listUseCase = new ListProfilesUseCase(this.profileRepository);
|
private listUseCase = new ListProfilesUseCase(this.profileRepository);
|
||||||
private createUseCase = new CreateProfileUseCase(this.profileRepository);
|
private createUseCase = new CreateProfileUseCase(this.profileRepository);
|
||||||
private updateUseCase = new UpdateProfileUseCase(this.profileRepository);
|
private updateUseCase = new UpdateProfileUseCase(this.profileRepository);
|
||||||
private getUseCase = new GetProfileUseCase(this.profileRepository);
|
private getUseCase = new GetProfileUseCase(this.profileRepository);
|
||||||
|
|
||||||
|
readonly searchFilters = this.searchService.getFilters();
|
||||||
readonly profiles = signal<ProfileViewModel[]>([]);
|
readonly profiles = signal<ProfileViewModel[]>([]);
|
||||||
|
readonly profilePaginated = signal<ProfilePaginated>({} as ProfilePaginated);
|
||||||
readonly profile = signal<ProfileViewModel>({} as ProfileViewModel);
|
readonly profile = signal<ProfileViewModel>({} as ProfileViewModel);
|
||||||
readonly loading = signal<LoaderAction>({ isLoading: false, action: ActionType.NONE });
|
readonly loading = signal<LoaderAction>({ isLoading: false, action: ActionType.NONE });
|
||||||
readonly error = signal<ErrorResponse>({
|
readonly error = signal<ErrorResponse>({
|
||||||
@@ -32,12 +37,28 @@ export class ProfileFacade {
|
|||||||
message: null,
|
message: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
load(search?: string) {
|
load(search?: SearchFilters) {
|
||||||
this.handleError(ActionType.READ, false, null, true);
|
this.handleError(ActionType.READ, false, null, true);
|
||||||
|
|
||||||
this.listUseCase.execute({ search }).subscribe({
|
if (search === undefined || search === null) {
|
||||||
next: (profiles) => {
|
search = this.searchFilters();
|
||||||
this.profiles.set(ProfilePresenter.toViewModels(profiles));
|
}
|
||||||
|
|
||||||
|
this.listUseCase.execute(search).subscribe({
|
||||||
|
next: (profilePaginated: ProfilePaginated) => {
|
||||||
|
const filters = {
|
||||||
|
...this.searchFilters(),
|
||||||
|
page: profilePaginated.page,
|
||||||
|
perPage: profilePaginated.perPage,
|
||||||
|
totalItems: profilePaginated.totalItems,
|
||||||
|
totalPages: profilePaginated.totalPages,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.searchService.setFilters(filters);
|
||||||
|
this.searchFilters.set(filters);
|
||||||
|
|
||||||
|
this.profilePaginated.set(profilePaginated);
|
||||||
|
this.profiles.set(ProfilePresenter.toViewModels(profilePaginated.items as Profile[]));
|
||||||
this.handleError(ActionType.READ, false, null, false);
|
this.handleError(ActionType.READ, false, null, false);
|
||||||
},
|
},
|
||||||
error: (err) => {
|
error: (err) => {
|
||||||
@@ -46,9 +67,9 @@ export class ProfileFacade {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadOne(userId: string) {
|
loadOne(profileId: string) {
|
||||||
this.handleError(ActionType.READ, false, null, true);
|
this.handleError(ActionType.READ, false, null, true);
|
||||||
this.getUseCase.execute(userId).subscribe({
|
this.getUseCase.execute(profileId).subscribe({
|
||||||
next: (profile: Profile) => {
|
next: (profile: Profile) => {
|
||||||
this.profile.set(ProfilePresenter.toViewModel(profile));
|
this.profile.set(ProfilePresenter.toViewModel(profile));
|
||||||
this.handleError(ActionType.READ, false, null, false);
|
this.handleError(ActionType.READ, false, null, false);
|
||||||
|
|||||||
@@ -45,7 +45,6 @@ export class ProjectFacade {
|
|||||||
error: (err) => {
|
error: (err) => {
|
||||||
this.handleError(ActionType.READ, false, err, false);
|
this.handleError(ActionType.READ, false, err, false);
|
||||||
},
|
},
|
||||||
complete: () => {},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import { ProjectViewModel } from '@app/ui/projects/project.presenter.model';
|
|||||||
import { Project } from '@app/domain/projects/project.model';
|
import { Project } from '@app/domain/projects/project.model';
|
||||||
|
|
||||||
export class ProjectPresenter {
|
export class ProjectPresenter {
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
toViewModel(project: Project): ProjectViewModel {
|
toViewModel(project: Project): ProjectViewModel {
|
||||||
return {
|
return {
|
||||||
id: project.id,
|
id: project.id,
|
||||||
|
|||||||
@@ -6,4 +6,7 @@ export interface UserViewModel {
|
|||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
slug?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { UserViewModel } from '@app/ui/users/user.presenter.model';
|
|||||||
import { User } from '@app/domain/users/user.model';
|
import { User } from '@app/domain/users/user.model';
|
||||||
|
|
||||||
export class UserPresenter {
|
export class UserPresenter {
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
toViewModel(user: User): UserViewModel {
|
toViewModel(user: User): UserViewModel {
|
||||||
return {
|
const slug = user.name
|
||||||
|
? this.generateProfileSlug(user.name, user.id)
|
||||||
|
: this.generateProfileSlug('Non renséigné');
|
||||||
|
|
||||||
|
let userViewModel: UserViewModel = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
verified: user.verified,
|
verified: user.verified,
|
||||||
@@ -13,10 +15,31 @@ export class UserPresenter {
|
|||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
avatar: user.avatar,
|
avatar: user.avatar,
|
||||||
|
slug,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (user.name) {
|
||||||
|
const firstName = user.name.split(' ').slice(0, -1).join(' ').toLowerCase().trim() ?? '';
|
||||||
|
const lastName = user.name.split(' ').slice(-1)[0].toUpperCase().trim() ?? '';
|
||||||
|
userViewModel = { ...userViewModel, firstName, lastName };
|
||||||
|
}
|
||||||
|
|
||||||
|
return userViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
toViewModels(users: User[]): UserViewModel[] {
|
toViewModels(users: User[]): UserViewModel[] {
|
||||||
return users.map(this.toViewModel);
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { AuthRepository } from '@app/domain/authentification/auth.repository';
|
|
||||||
|
|
||||||
export class ConfirmPasswordResetUsecase {
|
|
||||||
constructor(private readonly authRepo: AuthRepository) {}
|
|
||||||
|
|
||||||
execute(resetToken: string, newPassword: string, confirmPassword: string) {
|
|
||||||
return this.authRepo.confirmPasswordReset(resetToken, newPassword, confirmPassword);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@ import { Profile } from '@app/domain/profiles/profile.model';
|
|||||||
export class GetProfileUseCase {
|
export class GetProfileUseCase {
|
||||||
constructor(private readonly repo: ProfileRepository) {}
|
constructor(private readonly repo: ProfileRepository) {}
|
||||||
|
|
||||||
execute(userId: string): Observable<Profile> {
|
execute(profileId: string): Observable<Profile> {
|
||||||
return this.repo.getByUserId(userId);
|
return this.repo.getById(profileId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
import { ProfileRepository } from '@app/domain/profiles/profile.repository';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Profile } from '@app/domain/profiles/profile.model';
|
import { ProfilePaginated } from '@app/domain/profiles/profile.model';
|
||||||
|
import { SearchFilters } from '@app/domain/search/search-filters';
|
||||||
|
|
||||||
export class ListProfilesUseCase {
|
export class ListProfilesUseCase {
|
||||||
constructor(private readonly repo: ProfileRepository) {}
|
constructor(private readonly repo: ProfileRepository) {}
|
||||||
|
|
||||||
execute(params?: { search?: string; page?: number; pageSize?: number }): Observable<Profile[]> {
|
execute(params?: SearchFilters): Observable<ProfilePaginated> {
|
||||||
return this.repo.list(params);
|
return this.repo.list(params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { UserRepository } from '@app/domain/users/user.repository';
|
import { UserRepository } from '@app/domain/users/user.repository';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { User } from '@app/domain/users/user.model';
|
||||||
|
|
||||||
export class GetUserUseCase {
|
export class GetUserUseCase {
|
||||||
constructor(private readonly repo: UserRepository) {}
|
constructor(private readonly repo: UserRepository) {}
|
||||||
execute(userId: string) {
|
execute(userId: string): Observable<User> {
|
||||||
return this.repo.getUserById(userId);
|
return this.repo.getUserById(userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
{
|
{
|
||||||
"compileOnSave": false,
|
"compileOnSave": false,
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"skipLibCheck": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|||||||
Reference in New Issue
Block a user