From bbd555d61e04ed40ce71f9b587f688f24f569571 Mon Sep 17 00:00:00 2001 From: styve Lioumba Date: Mon, 22 Dec 2025 18:07:27 +0100 Subject: [PATCH] feat : ajout d'un service de log --- src/app/app.config.ts | 4 +- src/app/domain/log-level.ts | 7 +++ .../handlers/global-error-handler.ts | 31 ++++++++++++ .../infrastructure/shared/logger.service.ts | 47 +++++++++++++++++++ .../shared/components/map/map.component.ts | 2 +- .../project-picture-form.component.html | 3 +- .../project-picture-form.component.spec.ts | 14 +++--- .../project-picture-form.component.ts | 10 ++-- .../user-avatar-form.component.html | 3 +- .../user-avatar-form.component.spec.ts | 13 ----- .../user-avatar-form.component.ts | 13 ++--- 11 files changed, 104 insertions(+), 43 deletions(-) create mode 100644 src/app/domain/log-level.ts create mode 100644 src/app/infrastructure/handlers/global-error-handler.ts create mode 100644 src/app/infrastructure/shared/logger.service.ts diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 42a0330..8fb66c3 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,4 +1,4 @@ -import { ApplicationConfig } from '@angular/core'; +import { ApplicationConfig, ErrorHandler } from '@angular/core'; import { PreloadAllModules, provideRouter, @@ -26,6 +26,7 @@ import { WEB_SHARE_SERVICE_TOKEN } from '@app/infrastructure/shareData/web-share import { WebShareService } from '@app/infrastructure/shareData/web-share.service'; import { SETTING_REPOSITORY_TOKEN } from '@app/infrastructure/settings/setting-repository.token'; import { LocalSettingRepository } from '@app/infrastructure/settings/local-setting.repository'; +import { GlobalErrorHandler } from '@app/infrastructure/handlers/global-error-handler'; export const appConfig: ApplicationConfig = { providers: [ @@ -48,6 +49,7 @@ export const appConfig: ApplicationConfig = { { provide: AUTH_REPOSITORY_TOKEN, useExisting: PbAuthRepository }, { provide: WEB_SHARE_SERVICE_TOKEN, useExisting: WebShareService }, { provide: SETTING_REPOSITORY_TOKEN, useClass: LocalSettingRepository }, + { provide: ErrorHandler, useClass: GlobalErrorHandler }, provideToastr({ timeOut: 10000, positionClass: 'toast-top-right', diff --git a/src/app/domain/log-level.ts b/src/app/domain/log-level.ts new file mode 100644 index 0000000..26b764a --- /dev/null +++ b/src/app/domain/log-level.ts @@ -0,0 +1,7 @@ +export enum LogLevel { + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3, + OFF = 4, +} diff --git a/src/app/infrastructure/handlers/global-error-handler.ts b/src/app/infrastructure/handlers/global-error-handler.ts new file mode 100644 index 0000000..37fb070 --- /dev/null +++ b/src/app/infrastructure/handlers/global-error-handler.ts @@ -0,0 +1,31 @@ +import { ErrorHandler, inject, Injectable } from '@angular/core'; +import { HttpErrorResponse } from '@angular/common/http'; +import { LoggerService } from '@app/infrastructure/shared/logger.service'; + +@Injectable() +export class GlobalErrorHandler implements ErrorHandler { + // On utilise inject() pour éviter les soucis de dépendance circulaire dans le constructeur + private logger = inject(LoggerService); + + handleError(error: any): void { + let message = ''; + let stackTrace = ''; + + if (error instanceof HttpErrorResponse) { + // Erreur serveur + message = `Erreur Serveur: ${error.status} ${error.message}`; + } else if (error instanceof Error) { + // Erreur Client (JavaScript) + message = `Erreur Client: ${error.message}`; + stackTrace = error.stack || ''; + } else { + message = error; + } + + // On loggue l'erreur via notre service + this.logger.error(message, stackTrace); + + // Optionnel : Relancer l'erreur pour qu'elle apparaisse quand même dans la console du navigateur en DEV + throw error; + } +} diff --git a/src/app/infrastructure/shared/logger.service.ts b/src/app/infrastructure/shared/logger.service.ts new file mode 100644 index 0000000..b3e7cd4 --- /dev/null +++ b/src/app/infrastructure/shared/logger.service.ts @@ -0,0 +1,47 @@ +import { environment } from '@env/environment'; +import { LogLevel } from '@app/domain/log-level'; +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root', +}) +export class LoggerService { + private level: LogLevel = environment.production ? LogLevel.ERROR : LogLevel.DEBUG; + + debug(msg: string, ...optionalParams: any[]) { + this.log(LogLevel.DEBUG, msg, optionalParams); + } + + info(msg: string, ...optionalParams: any[]) { + this.log(LogLevel.INFO, msg, optionalParams); + } + + warn(msg: string, ...optionalParams: any[]) { + this.log(LogLevel.WARN, msg, optionalParams); + } + + error(msg: string, ...optionalParams: any[]) { + this.log(LogLevel.ERROR, msg, optionalParams); + // TODO: Ici, vous pourriez envoyer l'erreur vers votre backend PocketBase ou Sentry + // this.sendToBackend(msg, optionalParams); + } + + private log(level: LogLevel, msg: string, params: any[]) { + if (level >= this.level) { + switch (level) { + case LogLevel.DEBUG: + console.debug('%c[DEBUG]:', 'color: blue', msg, ...params); + break; + case LogLevel.INFO: + console.info('%c[INFO]:', 'color: green', msg, ...params); + break; + case LogLevel.WARN: + console.warn('%c[WARN]:', 'color: orange', msg, ...params); + break; + case LogLevel.ERROR: + console.error('%c[ERROR]:', 'color: red', msg, ...params); + break; + } + } + } +} diff --git a/src/app/shared/components/map/map.component.ts b/src/app/shared/components/map/map.component.ts index 75bb72f..935f876 100644 --- a/src/app/shared/components/map/map.component.ts +++ b/src/app/shared/components/map/map.component.ts @@ -187,7 +187,7 @@ export class MapComponent implements OnInit, AfterViewInit, OnDestroy { // Image (Avatar) if (marker.profile!.avatarUrl) { const img = document.createElement('img'); - img.src = marker.profile!.avatarUrl; + img.src = marker.profile!.avatarUrl.replace('320x240', '100x100'); img.className = 'w-16 h-16 rounded-full object-cover border-2 border-indigo-500 shadow-sm'; popupContainer.appendChild(img); } else { diff --git a/src/app/shared/components/project-picture-form/project-picture-form.component.html b/src/app/shared/components/project-picture-form/project-picture-form.component.html index 917e2f9..2a73d28 100644 --- a/src/app/shared/components/project-picture-form/project-picture-form.component.html +++ b/src/app/shared/components/project-picture-form/project-picture-form.component.html @@ -20,9 +20,8 @@ diff --git a/src/app/shared/components/project-picture-form/project-picture-form.component.spec.ts b/src/app/shared/components/project-picture-form/project-picture-form.component.spec.ts index 7acc3d5..1389023 100644 --- a/src/app/shared/components/project-picture-form/project-picture-form.component.spec.ts +++ b/src/app/shared/components/project-picture-form/project-picture-form.component.spec.ts @@ -62,27 +62,25 @@ describe('ProjectPictureFormComponent', () => { it("devrait afficher l'image du serveur si pas de preview", () => { // mockProject a 'image-serveur.jpg' - expect(component.currentImageUrl).toContain( - 'http://localhost:8090/api/files/projets/1/portfolio-preview.png' - ); + expect(component.currentImageUrl).toContain('portfolio-preview.png'); }); - it('devrait générer une image Dicebear avec le nom du projet si aucune image', () => { + /*it('devrait générer une image Dicebear avec le nom du projet si aucune image', () => { const projectNoImg = { ...mockProject, fichier: undefined }; fixture.componentRef.setInput('project', projectNoImg); fixture.detectChanges(); expect(component.currentImageUrl).toContain('api.dicebear.com'); expect(component.currentImageUrl).toContain('Web 3D'); - }); + });*/ - it("devrait générer une image par défaut si le projet n'a ni image ni nom", () => { + /*it("devrait générer une image par défaut si le projet n'a ni image ni nom", () => { const emptyProject = { ...mockProject, fichier: undefined, nom: undefined }; fixture.componentRef.setInput('project', emptyProject); fixture.detectChanges(); - expect(component.currentImageUrl).toContain('nouveau-projet'); - }); + expect(component.currentImageUrl).toContain(undefined); + });*/ }); describe('Validation (canSubmit)', () => { diff --git a/src/app/shared/components/project-picture-form/project-picture-form.component.ts b/src/app/shared/components/project-picture-form/project-picture-form.component.ts index 3ba4cf2..aac189d 100644 --- a/src/app/shared/components/project-picture-form/project-picture-form.component.ts +++ b/src/app/shared/components/project-picture-form/project-picture-form.component.ts @@ -1,5 +1,5 @@ import { Component, effect, inject, input, output, signal } from '@angular/core'; -import { NgOptimizedImage, NgTemplateOutlet } from '@angular/common'; +import { NgTemplateOutlet } from '@angular/common'; import { environment } from '@env/environment'; import { ProjectViewModel } from '@app/ui/projects/project.presenter.model'; import { ProjectFacade } from '@app/ui/projects/project.facade'; @@ -10,7 +10,7 @@ import { FileManagerService } from '@app/ui/shared/services/file-manager.service @Component({ selector: 'app-project-picture-form', standalone: true, - imports: [LoadingComponent, NgTemplateOutlet, NgOptimizedImage], + imports: [LoadingComponent, NgTemplateOutlet], templateUrl: './project-picture-form.component.html', styleUrl: './project-picture-form.component.scss', }) @@ -74,11 +74,7 @@ export class ProjectPictureFormComponent { } if (this.project()?.fichier) { - return `${this.environment.baseUrl}/api/files/projets/${this.project()!.id}/${this.project()!.fichier}?thumb=320x240`; - } - - if (this.project()?.nom) { - return `https://api.dicebear.com/9.x/shapes/svg?seed=${this.project()!.nom}`; + return this.project()!.fichier; } return 'https://api.dicebear.com/9.x/shapes/svg?seed=nouveau-projet'; diff --git a/src/app/shared/components/user-avatar-form/user-avatar-form.component.html b/src/app/shared/components/user-avatar-form/user-avatar-form.component.html index a4023ff..4cd5cd4 100644 --- a/src/app/shared/components/user-avatar-form/user-avatar-form.component.html +++ b/src/app/shared/components/user-avatar-form/user-avatar-form.component.html @@ -21,9 +21,8 @@ } diff --git a/src/app/shared/components/user-avatar-form/user-avatar-form.component.spec.ts b/src/app/shared/components/user-avatar-form/user-avatar-form.component.spec.ts index e3cf544..77aaf53 100644 --- a/src/app/shared/components/user-avatar-form/user-avatar-form.component.spec.ts +++ b/src/app/shared/components/user-avatar-form/user-avatar-form.component.spec.ts @@ -65,12 +65,6 @@ describe('UserAvatarFormComponent', () => { expect(component.currentAvatarUrl).toBe('data:image/png;base64,preview'); }); - it("devrait afficher l'avatar de l'utilisateur si pas de prévisualisation", () => { - // Le mockUser a déjà 'avatar.jpg' défini - expect(component.currentAvatarUrl).toContain('foo.png'); - expect(component.currentAvatarUrl).toContain('user_001'); // Vérifie l'ID dans l'URL - }); - it("devrait afficher un avatar Dicebear si l'utilisateur n'a pas d'avatar", () => { const userNoAvatar = { ...mockUser, avatar: null }; fixture.componentRef.setInput('user', userNoAvatar); @@ -82,13 +76,6 @@ describe('UserAvatarFormComponent', () => { 'https://api.dicebear.com/9.x/initials/svg?seed=foo' ); }); - - it("devrait retourner null si aucun utilisateur n'est fourni", () => { - fixture.componentRef.setInput('user', undefined); - fixture.detectChanges(); - - expect(component.currentAvatarUrl).toBeNull(); - }); }); describe('Validation du formulaire (canSubmit)', () => { diff --git a/src/app/shared/components/user-avatar-form/user-avatar-form.component.ts b/src/app/shared/components/user-avatar-form/user-avatar-form.component.ts index c1d5dcd..53983a8 100644 --- a/src/app/shared/components/user-avatar-form/user-avatar-form.component.ts +++ b/src/app/shared/components/user-avatar-form/user-avatar-form.component.ts @@ -2,7 +2,7 @@ import { Component, effect, inject, input, signal } from '@angular/core'; import { User } from '@app/domain/users/user.model'; import { ReactiveFormsModule } from '@angular/forms'; import { environment } from '@env/environment'; -import { NgOptimizedImage, NgTemplateOutlet } from '@angular/common'; +import { NgTemplateOutlet } from '@angular/common'; import { UserFacade } from '@app/ui/users/user.facade'; import { ActionType } from '@app/domain/action-type.util'; import { LoadingComponent } from '@app/shared/components/loading/loading.component'; @@ -13,7 +13,7 @@ import { FileManagerService } from '@app/ui/shared/services/file-manager.service @Component({ selector: 'app-user-avatar-form', standalone: true, - imports: [ReactiveFormsModule, NgTemplateOutlet, LoadingComponent, NgOptimizedImage], + imports: [ReactiveFormsModule, NgTemplateOutlet, LoadingComponent], templateUrl: './user-avatar-form.component.html', styleUrl: './user-avatar-form.component.scss', }) @@ -80,14 +80,9 @@ export class UserAvatarFormComponent { } if (currentUser?.avatar) { - return `${this.environment.baseUrl}/api/files/users/${currentUser.id}/${currentUser.avatar}?thumb=320x240`; + return currentUser.avatar; } - if (currentUser) { - const seed = currentUser.name || currentUser.username || currentUser.email || 'user'; - return `https://api.dicebear.com/9.x/initials/svg?seed=${seed}`; - } - - return null; + return `https://api.dicebear.com/9.x/initials/svg?seed=${currentUser!.name ?? 'trouveTonProfil'}`; } }