This commit is contained in:
styve Lioumba
2025-11-20 14:37:41 +01:00
parent dd77e3d023
commit 06979b79e3
21 changed files with 205 additions and 152 deletions

View File

@@ -10,14 +10,13 @@ describe('AppComponent', () => {
let component: AppComponent; let component: AppComponent;
let fixture: ComponentFixture<AppComponent>; let fixture: ComponentFixture<AppComponent>;
let mockAuthRepository: jest.Mocked<AuthRepository>; let mockAuthRepository: jest.Mocked<Partial<AuthRepository>>;
let mockProfileRepo: jest.Mocked<ProfileRepository>; let mockProfileRepo: jest.Mocked<Partial<ProfileRepository>>;
beforeEach(async () => { beforeEach(async () => {
mockAuthRepository = { mockAuthRepository = {
get: jest.fn(), get: jest.fn(),
login: jest.fn(), login: jest.fn(),
update: jest.fn(),
sendVerificationEmail: jest.fn(), sendVerificationEmail: jest.fn(),
logout: jest.fn(), logout: jest.fn(),
isAuthenticated: jest.fn(), isAuthenticated: jest.fn(),

View File

@@ -10,8 +10,8 @@ import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-r
describe('authGuard', () => { describe('authGuard', () => {
let mockRouter: Partial<Router>; let mockRouter: Partial<Router>;
let mockAuthRepository: jest.Mocked<AuthRepository>; let mockAuthRepository: jest.Mocked<Partial<AuthRepository>>;
let mockProfileRepo: jest.Mocked<ProfileRepository>; let mockProfileRepo: jest.Mocked<Partial<ProfileRepository>>;
const executeGuard: CanActivateFn = (...guardParameters) => const executeGuard: CanActivateFn = (...guardParameters) =>
TestBed.runInInjectionContext(() => authGuard(...guardParameters)); TestBed.runInInjectionContext(() => authGuard(...guardParameters));
@@ -24,7 +24,6 @@ describe('authGuard', () => {
mockAuthRepository = { mockAuthRepository = {
get: jest.fn(), get: jest.fn(),
login: jest.fn(), login: jest.fn(),
update: jest.fn(),
sendVerificationEmail: jest.fn(), sendVerificationEmail: jest.fn(),
logout: jest.fn(), logout: jest.fn(),
isAuthenticated: jest.fn(), isAuthenticated: jest.fn(),

View File

@@ -57,55 +57,61 @@
</div> </div>
<!-- Avatar et info principale --> <!-- Avatar et info principale -->
<div class="relative -mt-16 md:-mt-20 px-4 md:px-8 pb-6"> <div class="relative -mt-16 md:-mt-22 px-4 md:px-8 pb-6">
<div class="flex flex-col md:flex-row items-center md:items-end gap-4 md:gap-6"> <div class="flex flex-col md:flex-row items-center md:items-end gap-4 md:gap-6">
<!-- Avatar avec bouton edit --> <!-- Avatar avec bouton edit -->
<div class="relative group animate-slide-up"> @if (user() != undefined) {
<div <div class="relative group animate-slide-up">
class="w-28 h-28 md:w-36 md:h-36 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 p-1 shadow-2xl group-hover:scale-105 transition-transform duration-300" <div
> class="w-28 h-28 md:w-36 md:h-36 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 p-1 shadow-2xl group-hover:scale-105 transition-transform duration-300"
<div class="w-full h-full rounded-full overflow-hidden bg-white"> >
@if (user().avatar) { <div class="w-full h-full rounded-full overflow-hidden bg-white">
<img @if (user()!.avatar) {
alt="{{ user().username }}" <img
class="object-cover w-full h-full" alt="{{ user()!.username }}"
src="{{ environment.baseUrl }}/api/files/users/{{ user().id }}/{{ class="object-cover w-full h-full"
user().avatar src="{{ environment.baseUrl }}/api/files/users/{{ user()!.id }}/{{
}}" user()!.avatar
/> }}"
} @else { />
<img } @else {
alt="{{ user().username }}" <img
class="object-cover w-full h-full" alt="{{ user()!.username }}"
src="https://api.dicebear.com/9.x/adventurer/svg?seed={{ user().username }}" class="object-cover w-full h-full"
/> src="https://api.dicebear.com/9.x/adventurer/svg?seed={{
} user()!.username
}}"
/>
}
</div>
</div> </div>
</div> </div>
</div> }
<!-- Nom et titre --> <!-- Nom et titre -->
<div @if (user() != undefined) {
class="flex-1 text-center md:text-left mb-4 md:mb-0 animate-slide-up animation-delay-100" <div
> class="flex-1 text-center md:text-left mb-4 mt-4 md:mb-0 animate-slide-up animation-delay-100"
@if (user().name) { >
<h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1"> @if (user()!.name) {
{{ user().name }} <h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1">
</h1> {{ user()!.name }}
} @else if (user().username) { </h1>
<h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1"> } @else if (user()!.username) {
{{ user().username }} <h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1">
</h1> {{ user()!.username }}
} @else { </h1>
<h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1"> } @else {
{{ user().email }} <h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1">
</h1> {{ user()!.email }}
} </h1>
}
<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>
</div> </div>
</div> </div>
@@ -115,30 +121,32 @@
<!-- Sidebar - Informations --> <!-- Sidebar - Informations -->
<aside class="lg:col-span-1 space-y-6 animate-slide-up animation-delay-200"> <aside class="lg:col-span-1 space-y-6 animate-slide-up animation-delay-200">
<!-- Card Apropos --> <!-- Card Apropos -->
<div @if (profile().apropos) {
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow duration-300" <div
> class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow duration-300"
<h3
class="text-lg font-bold text-gray-900 dark:text-white mb-3 flex items-center gap-2"
> >
<svg <h3
xmlns="http://www.w3.org/2000/svg" class="text-lg font-bold text-gray-900 dark:text-white mb-3 flex items-center gap-2"
class="h-6 w-6 text-indigo-500"
viewBox="0 0 20 20"
fill="currentColor"
> >
<path <svg
fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" class="h-6 w-6 text-indigo-500"
clip-rule="evenodd" viewBox="0 0 20 20"
/> fill="currentColor"
</svg> >
À propos <path
</h3> fill-rule="evenodd"
<p class="text-gray-700 dark:text-gray-300 leading-relaxed text-base"> d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
{{ profile().apropos }} clip-rule="evenodd"
</p> />
</div> </svg>
À propos
</h3>
<p class="text-gray-700 dark:text-gray-300 leading-relaxed text-base">
{{ profile().apropos }}
</p>
</div>
}
<!-- Card Bio --> <!-- Card Bio -->
<div <div
@@ -318,7 +326,11 @@
<div class="tab-content"> <div class="tab-content">
@switch (menu().toLowerCase()) { @switch (menu().toLowerCase()) {
@case ('home') { @case ('home') {
<app-update-user [user]="user()" /> @if (!userLoading().isLoading) {
<app-update-user [user]="user()" />
} @else {
<app-loading message="Chargement encours..." />
}
} }
@case ('projects') { @case ('projects') {
<app-my-profile-project-list <app-my-profile-project-list
@@ -334,7 +346,11 @@
<app-pdf-viewer [profile]="profile()" /> <app-pdf-viewer [profile]="profile()" />
} }
@default { @default {
<app-update-user [user]="user()" /> @if (!userLoading().isLoading) {
<app-update-user [user]="user()" />
} @else {
<app-loading message="Chargement encours..." />
}
} }
} }
</div> </div>

View File

@@ -15,8 +15,8 @@ describe('MyProfileComponent', () => {
let fixture: ComponentFixture<MyProfileComponent>; let fixture: ComponentFixture<MyProfileComponent>;
let mockProfileRepo: ProfileRepository; let mockProfileRepo: ProfileRepository;
let mockUserRepo: UserRepository;
let mockToastrService: Partial<ToastrService>; let mockToastrService: Partial<ToastrService>;
let mockUserRepo: Partial<UserRepository>;
beforeEach(async () => { beforeEach(async () => {
mockProfileRepo = { mockProfileRepo = {

View File

@@ -1,4 +1,4 @@
import { Component, computed, inject, OnInit, signal } from '@angular/core'; import { Component, inject, OnInit, signal } from '@angular/core';
import { ActivatedRoute, RouterOutlet } from '@angular/router'; import { ActivatedRoute, RouterOutlet } from '@angular/router';
import { User } from '@app/domain/users/user.model'; import { User } from '@app/domain/users/user.model';
import { Location, UpperCasePipe } from '@angular/common'; import { Location, UpperCasePipe } from '@angular/common';
@@ -11,6 +11,8 @@ import { MyProfileProjectListComponent } from '@app/shared/components/my-profile
import { MyProfileUpdateFormComponent } from '@app/shared/components/my-profile-update-form/my-profile-update-form.component'; import { MyProfileUpdateFormComponent } from '@app/shared/components/my-profile-update-form/my-profile-update-form.component';
import { PdfViewerComponent } from '@app/shared/features/pdf-viewer/pdf-viewer.component'; import { PdfViewerComponent } from '@app/shared/features/pdf-viewer/pdf-viewer.component';
import { ProfileFacade } from '@app/ui/profiles/profile.facade'; import { ProfileFacade } from '@app/ui/profiles/profile.facade';
import { UserFacade } from '@app/ui/users/user.facade';
import { LoadingComponent } from '@app/shared/components/loading/loading.component';
@Component({ @Component({
selector: 'app-my-profile', selector: 'app-my-profile',
@@ -24,7 +26,9 @@ import { ProfileFacade } from '@app/ui/profiles/profile.facade';
MyProfileUpdateFormComponent, MyProfileUpdateFormComponent,
PdfViewerComponent, PdfViewerComponent,
UpperCasePipe, UpperCasePipe,
LoadingComponent,
], ],
providers: [UserFacade],
templateUrl: './my-profile.component.html', templateUrl: './my-profile.component.html',
styleUrl: './my-profile.component.scss', styleUrl: './my-profile.component.scss',
}) })
@@ -33,17 +37,14 @@ export class MyProfileComponent implements OnInit {
protected readonly environment = environment; protected readonly environment = environment;
protected menu = signal<string>('home'); protected menu = signal<string>('home');
protected myProfileQrCode = `${environment.production}`;
protected location = inject(Location); protected location = inject(Location);
protected readonly route = inject(ActivatedRoute); protected readonly route = inject(ActivatedRoute);
protected extraData: { user: User } = this.route.snapshot.data['user']; protected extraData: { user: User } = this.route.snapshot.data['user'];
protected user = computed(() => { private readonly userFacade = inject(UserFacade);
if (this.extraData != undefined) return this.extraData.user; protected user = this.userFacade.user;
return {} as User; protected readonly userLoading = this.userFacade.loading;
});
private readonly profileFacade = new ProfileFacade(); private readonly profileFacade = new ProfileFacade();
protected profile = this.profileFacade.profile; protected profile = this.profileFacade.profile;
@@ -51,7 +52,9 @@ export class MyProfileComponent implements OnInit {
protected readonly error = this.profileFacade.error; protected readonly error = this.profileFacade.error;
ngOnInit(): void { ngOnInit(): void {
this.myProfileQrCode = `${this.myProfileQrCode}/profiles/${this.user().id}`; if (this.extraData != undefined) {
this.profileFacade.loadOne(this.user().id); this.profileFacade.loadOne(this.extraData.user.id);
this.userFacade.loadOne(this.extraData.user.id);
}
} }
} }

View File

@@ -207,30 +207,32 @@
<!-- 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 -->
<div @if (profile().apropos) {
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 md:p-8 hover:shadow-xl transition-shadow duration-300" <div
> class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 md:p-8 hover:shadow-xl transition-shadow duration-300"
<h2
class="text-2xl font-bold text-gray-900 dark:text-white mb-4 flex items-center gap-2"
> >
<svg <h2
xmlns="http://www.w3.org/2000/svg" class="text-2xl font-bold text-gray-900 dark:text-white mb-4 flex items-center gap-2"
class="h-6 w-6 text-indigo-500"
viewBox="0 0 20 20"
fill="currentColor"
> >
<path <svg
fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" class="h-6 w-6 text-indigo-500"
clip-rule="evenodd" viewBox="0 0 20 20"
/> fill="currentColor"
</svg> >
À propos <path
</h2> fill-rule="evenodd"
<p class="text-gray-700 dark:text-gray-300 leading-relaxed text-base"> d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
{{ profile().apropos }} clip-rule="evenodd"
</p> />
</div> </svg>
À propos
</h2>
<p class="text-gray-700 dark:text-gray-300 leading-relaxed text-base">
{{ profile().apropos }}
</p>
</div>
}
<!-- Card Projets --> <!-- Card Projets -->
<div <div

View File

@@ -16,11 +16,11 @@ describe('MyProfileUpdateProjectFormComponent', () => {
let component: MyProfileUpdateProjectFormComponent; let component: MyProfileUpdateProjectFormComponent;
let fixture: ComponentFixture<MyProfileUpdateProjectFormComponent>; let fixture: ComponentFixture<MyProfileUpdateProjectFormComponent>;
let mockToastrService: Partial<ToastrService>; let mockToastrService: jest.Mocked<Partial<ToastrService>>;
let mockProjectRepository: jest.Mocked<ProjectRepository>; let mockProjectRepository: jest.Mocked<Partial<ProjectRepository>>;
let mockAuthRepository: jest.Mocked<AuthRepository>; let mockAuthRepository: jest.Mocked<Partial<AuthRepository>>;
let mockProfileRepo: jest.Mocked<ProfileRepository>; let mockProfileRepo: jest.Mocked<Partial<ProfileRepository>>;
beforeEach(async () => { beforeEach(async () => {
mockToastrService = { mockToastrService = {
@@ -38,7 +38,6 @@ describe('MyProfileUpdateProjectFormComponent', () => {
mockAuthRepository = { mockAuthRepository = {
get: jest.fn(), get: jest.fn(),
login: jest.fn(), login: jest.fn(),
update: jest.fn(),
sendVerificationEmail: jest.fn(), sendVerificationEmail: jest.fn(),
logout: jest.fn(), logout: jest.fn(),
isAuthenticated: jest.fn(), isAuthenticated: jest.fn(),

View File

@@ -14,10 +14,10 @@ import { ProfileRepository } from '@app/domain/profiles/profile.repository';
describe('NavBarComponent', () => { describe('NavBarComponent', () => {
let component: NavBarComponent; let component: NavBarComponent;
let fixture: ComponentFixture<NavBarComponent>; let fixture: ComponentFixture<NavBarComponent>;
let mockThemeService: Partial<ThemeService>; let mockThemeService: jest.Mocked<Partial<ThemeService>>;
let mockAuthRepository: jest.Mocked<AuthRepository>; let mockAuthRepository: jest.Mocked<Partial<AuthRepository>>;
let mockProfileRepo: jest.Mocked<ProfileRepository>; let mockProfileRepo: jest.Mocked<Partial<ProfileRepository>>;
const user: User = { const user: User = {
id: 'adbc123', id: 'adbc123',
@@ -41,7 +41,6 @@ describe('NavBarComponent', () => {
mockAuthRepository = { mockAuthRepository = {
get: jest.fn(), get: jest.fn(),
login: jest.fn(), login: jest.fn(),
update: jest.fn(),
sendVerificationEmail: jest.fn(), sendVerificationEmail: jest.fn(),
logout: jest.fn(), logout: jest.fn(),
isAuthenticated: jest.fn(), isAuthenticated: jest.fn(),

View File

@@ -1,12 +1,11 @@
import { Component, inject, Input } from '@angular/core'; import { Component, inject, Input } from '@angular/core';
import { JsonPipe } from '@angular/common';
import { ThemeService } from '@app/core/services/theme/theme.service'; import { ThemeService } from '@app/core/services/theme/theme.service';
import { UntilDestroy } from '@ngneat/until-destroy'; import { UntilDestroy } from '@ngneat/until-destroy';
@Component({ @Component({
selector: 'app-reseaux', selector: 'app-reseaux',
standalone: true, standalone: true,
imports: [JsonPipe], imports: [],
templateUrl: './reseaux.component.html', templateUrl: './reseaux.component.html',
styleUrl: './reseaux.component.scss', styleUrl: './reseaux.component.scss',
}) })

View File

@@ -5,7 +5,7 @@
} }
<ng-template #content> <ng-template #content>
@if (user) { @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"
> >

View File

@@ -1,4 +1,4 @@
import { Component, effect, inject, Input, output, signal } from '@angular/core'; import { Component, effect, inject, Input, signal } from '@angular/core';
import { User } from '@app/domain/users/user.model'; import { User } from '@app/domain/users/user.model';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { environment } from '@env/environment'; import { environment } from '@env/environment';
@@ -7,21 +7,24 @@ import { ToastrService } from 'ngx-toastr';
import { UserFacade } from '@app/ui/users/user.facade'; import { UserFacade } from '@app/ui/users/user.facade';
import { ActionType } from '@app/domain/action-type.util'; import { ActionType } from '@app/domain/action-type.util';
import { LoadingComponent } from '@app/shared/components/loading/loading.component'; import { LoadingComponent } from '@app/shared/components/loading/loading.component';
import { UntilDestroy } from '@ngneat/until-destroy';
import { UserViewModel } from '@app/ui/users/user.presenter.model';
@Component({ @Component({
selector: 'app-user-avatar-form', selector: 'app-user-avatar-form',
standalone: true, standalone: true,
providers: [UserFacade],
imports: [ReactiveFormsModule, NgClass, NgTemplateOutlet, LoadingComponent], imports: [ReactiveFormsModule, NgClass, NgTemplateOutlet, LoadingComponent],
templateUrl: './user-avatar-form.component.html', templateUrl: './user-avatar-form.component.html',
styleUrl: './user-avatar-form.component.scss', styleUrl: './user-avatar-form.component.scss',
}) })
@UntilDestroy()
export class UserAvatarFormComponent { export class UserAvatarFormComponent {
private readonly toastrService = inject(ToastrService); private readonly toastrService = inject(ToastrService);
protected readonly environment = environment; protected readonly environment = environment;
private readonly facade = inject(UserFacade); private readonly facade = inject(UserFacade);
@Input({ required: true }) user: User | undefined = undefined;
onFormSubmitted = output<any>(); @Input({ required: true }) user: UserViewModel | undefined = undefined;
file: File | null = null; // Variable to store file file: File | null = null; // Variable to store file
imagePreviewUrl: string | null = null; // URL for image preview imagePreviewUrl: string | null = null; // URL for image preview
@@ -34,13 +37,13 @@ export class UserAvatarFormComponent {
let message = ''; let message = '';
effect(() => { effect(() => {
if (!this.loading().isLoading && this.onSubmitted()) { if (!this.loading().isLoading) {
switch (this.loading().action) { switch (this.loading().action) {
case ActionType.UPDATE: case ActionType.UPDATE:
message = `Votre photo de profile a bien été modifier !`; message = `Votre photo de profile a bien été modifier !`;
this.customToast(ActionType.UPDATE, message); if (this.onSubmitted()) {
this.onSubmitted.set(false); this.customToast(ActionType.UPDATE, message);
this.onFormSubmitted.emit(''); }
break; break;
} }
} }
@@ -52,7 +55,7 @@ export class UserAvatarFormComponent {
const formData = new FormData(); const formData = new FormData();
formData.append('avatar', this.file); // "avatar" est le nom du champ dans PocketBase formData.append('avatar', this.file); // "avatar" est le nom du champ dans PocketBase
this.facade.update(this.user?.id!, formData as Partial<User>); this.facade.update(this.user!.id, formData as Partial<User>);
this.onSubmitted.set(true); this.onSubmitted.set(true);
} }
} }

View File

@@ -1,4 +1,4 @@
import { Component, effect, inject, Input, OnInit, output, signal } from '@angular/core'; import { Component, effect, inject, Input, OnInit, signal } from '@angular/core';
import { import {
FormBuilder, FormBuilder,
FormControl, FormControl,
@@ -18,6 +18,7 @@ import { NgTemplateOutlet } from '@angular/common';
selector: 'app-user-form', selector: 'app-user-form',
standalone: true, standalone: true,
imports: [ReactiveFormsModule, LoadingComponent, NgTemplateOutlet], imports: [ReactiveFormsModule, LoadingComponent, NgTemplateOutlet],
providers: [UserFacade],
templateUrl: './user-form.component.html', templateUrl: './user-form.component.html',
styleUrl: './user-form.component.scss', styleUrl: './user-form.component.scss',
}) })
@@ -27,12 +28,12 @@ export class UserFormComponent implements OnInit {
private readonly facade = inject(UserFacade); private readonly facade = inject(UserFacade);
protected readonly ActionType = ActionType; protected readonly ActionType = ActionType;
@Input({ required: true }) user: User | undefined = undefined; @Input({ required: true }) userId: string | undefined = undefined;
onFormSubmitted = output<any>();
private fb = inject(FormBuilder); private fb = inject(FormBuilder);
protected userForm!: FormGroup; protected userForm!: FormGroup;
protected readonly user = this.facade.user;
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 onSubmitted = signal<boolean>(false); protected onSubmitted = signal<boolean>(false);
@@ -41,23 +42,32 @@ export class UserFormComponent implements OnInit {
let message = ''; let message = '';
effect(() => { effect(() => {
if (!this.loading().isLoading && this.onSubmitted()) { if (!this.loading().isLoading) {
switch (this.loading().action) { switch (this.loading().action) {
case ActionType.UPDATE: case ActionType.UPDATE:
message = `Vos informations personnelles ont bien été modifier !`; message = `Vos informations personnelles ont bien été modifier !`;
this.customToast(ActionType.UPDATE, message); if (this.onSubmitted()) {
this.onSubmitted.set(false); this.customToast(ActionType.UPDATE, message);
}
break;
case ActionType.READ:
this.userForm.setValue({
firstname: this.user().name.split(' ').slice(0, -1).join(' ') ?? '',
name: this.user().name.split(' ').slice(-1)[0] ?? '',
});
break; break;
} }
} }
}); });
} }
ngOnInit(): void { ngOnInit(): void {
if (this.userId !== undefined) {
this.facade.loadOne(this.userId!);
}
this.userForm = this.fb.group({ this.userForm = this.fb.group({
firstname: new FormControl(this.user?.name?.split(' ').slice(0, -1).join(' ') ?? '', [ firstname: new FormControl('', [Validators.required]),
Validators.required, name: new FormControl('', [Validators.required]),
]),
name: new FormControl(this.user?.name?.split(' ').slice(-1)[0] ?? '', [Validators.required]),
}); });
} }
@@ -72,9 +82,7 @@ export class UserFormComponent implements OnInit {
name: this.userForm.getRawValue()!.firstname! + ' ' + this.userForm.getRawValue()!.name!, name: this.userForm.getRawValue()!.firstname! + ' ' + this.userForm.getRawValue()!.name!,
} as User; } as User;
this.facade.update(this.user?.id!, data); this.facade.update(this.userId!, data);
this.onFormSubmitted.emit(data);
this.onSubmitted.set(true); this.onSubmitted.set(true);
} }

View File

@@ -10,6 +10,7 @@ import { UserFacade } from '@app/ui/users/user.facade';
@Component({ @Component({
selector: 'app-vertical-profile-item', selector: 'app-vertical-profile-item',
standalone: true, standalone: true,
providers: [UserFacade],
imports: [ChipsComponent, ReseauxComponent, RouterLink], imports: [ChipsComponent, ReseauxComponent, RouterLink],
templateUrl: './vertical-profile-item.component.html', templateUrl: './vertical-profile-item.component.html',
styleUrl: './vertical-profile-item.component.scss', styleUrl: './vertical-profile-item.component.scss',

View File

@@ -15,8 +15,8 @@ describe('LoginComponent', () => {
// Mocks des services // Mocks des services
let mockToastrService: Partial<ToastrService>; let mockToastrService: Partial<ToastrService>;
let mockAuthRepository: jest.Mocked<AuthRepository>; let mockAuthRepository: jest.Mocked<Partial<AuthRepository>>;
let mockProfileRepo: jest.Mocked<ProfileRepository>; let mockProfileRepo: jest.Mocked<Partial<ProfileRepository>>;
beforeEach(async () => { beforeEach(async () => {
mockToastrService = { mockToastrService = {
@@ -29,7 +29,6 @@ describe('LoginComponent', () => {
mockAuthRepository = { mockAuthRepository = {
get: jest.fn(), get: jest.fn(),
login: jest.fn(), login: jest.fn(),
update: jest.fn(),
sendVerificationEmail: jest.fn(), sendVerificationEmail: jest.fn(),
logout: jest.fn(), logout: jest.fn(),
isAuthenticated: jest.fn(), isAuthenticated: jest.fn(),

View File

@@ -43,10 +43,20 @@ export class LoginComponent {
switch (this.loading().action) { switch (this.loading().action) {
case ActionType.READ: case ActionType.READ:
if (!this.error().hasError) { if (!this.error().hasError) {
if (!this.authResponse()!.isValid && !this.authResponse()?.record.verified) {
message = `Vous ne pouvez pas vous connecter sans valider la verification envoyé à cet adresse ${this.authResponse()?.record.email!}`;
this.toastrService.warning(`${message}`, `CONNEXION`, {
closeButton: true,
progressBar: true,
disableTimeOut: true,
});
return;
}
this.router this.router
.navigate(['/my-profile'], { state: { user: this.authResponse()!.record } }) .navigate(['/my-profile'], { state: { user: this.authResponse()!.record } })
.then(() => { .then(() => {
message = ` Bienvenue parmi nous!`; message = `Bienvenue parmi nous!`;
this.customToast(ActionType.READ, message); this.customToast(ActionType.READ, message);
}); });
} }

View File

@@ -12,9 +12,9 @@ describe('RegisterComponent', () => {
let component: RegisterComponent; let component: RegisterComponent;
let fixture: ComponentFixture<RegisterComponent>; let fixture: ComponentFixture<RegisterComponent>;
let mockToastrService: Partial<ToastrService>; let mockToastrService: jest.Mocked<Partial<ToastrService>>;
let mockProfileRepo: ProfileRepository; let mockProfileRepo: jest.Mocked<Partial<ProfileRepository>>;
let mockAuthRepository: jest.Mocked<AuthRepository>; let mockAuthRepository: jest.Mocked<Partial<AuthRepository>>;
beforeEach(async () => { beforeEach(async () => {
mockProfileRepo = { mockProfileRepo = {
@@ -33,7 +33,6 @@ describe('RegisterComponent', () => {
mockAuthRepository = { mockAuthRepository = {
get: jest.fn(), get: jest.fn(),
login: jest.fn(), login: jest.fn(),
update: jest.fn(),
sendVerificationEmail: jest.fn(), sendVerificationEmail: jest.fn(),
logout: jest.fn(), logout: jest.fn(),
isAuthenticated: jest.fn(), isAuthenticated: jest.fn(),

View File

@@ -28,7 +28,7 @@
<div <div
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 animate-fade-in animation-delay-100" class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 animate-fade-in animation-delay-100"
> >
<app-user-form [user]="user" /> <app-user-form [userId]="user!.id" />
</div> </div>
</div> </div>
} }

View File

@@ -1,14 +1,34 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UpdateUserComponent } from './update-user.component'; import { UpdateUserComponent } from './update-user.component';
import { provideRouter } from '@angular/router';
import { UserRepository } from '@app/domain/users/user.repository';
import { USER_REPOSITORY_TOKEN } from '@app/infrastructure/users/user-repository.token';
import { ToastrService } from 'ngx-toastr';
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 mockToastrService: jest.Mocked<Partial<ToastrService>>;
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: [
provideRouter([]),
{ provide: USER_REPOSITORY_TOKEN, useValue: mockUserRepo },
{ provide: ToastrService, useValue: mockToastrService },
],
}).compileComponents(); }).compileComponents();
fixture = TestBed.createComponent(UpdateUserComponent); fixture = TestBed.createComponent(UpdateUserComponent);

View File

@@ -1,7 +1,7 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { User } from '@app/domain/users/user.model';
import { UserFormComponent } from '@app/shared/components/user-form/user-form.component'; import { UserFormComponent } from '@app/shared/components/user-form/user-form.component';
import { UserAvatarFormComponent } from '@app/shared/components/user-avatar-form/user-avatar-form.component'; import { UserAvatarFormComponent } from '@app/shared/components/user-avatar-form/user-avatar-form.component';
import { UserViewModel } from '@app/ui/users/user.presenter.model';
@Component({ @Component({
selector: 'app-update-user', selector: 'app-update-user',
@@ -11,5 +11,5 @@ import { UserAvatarFormComponent } from '@app/shared/components/user-avatar-form
styleUrl: './update-user.component.scss', styleUrl: './update-user.component.scss',
}) })
export class UpdateUserComponent { export class UpdateUserComponent {
@Input({ required: true }) user: User | undefined = undefined; @Input({ required: true }) user: UserViewModel | undefined = undefined;
} }

View File

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

View File

@@ -8,9 +8,7 @@ import { ErrorResponse } from '@app/domain/error-response.util';
import { UserViewModel } from '@app/ui/users/user.presenter.model'; import { UserViewModel } from '@app/ui/users/user.presenter.model';
import { UserPresenter } from '@app/ui/users/user.presenter'; import { UserPresenter } from '@app/ui/users/user.presenter';
@Injectable({ @Injectable()
providedIn: 'root',
})
export class UserFacade { export class UserFacade {
private readonly userRepository = inject(USER_REPOSITORY_TOKEN); private readonly userRepository = inject(USER_REPOSITORY_TOKEN);