feat : ajout d'un service de log

This commit is contained in:
styve Lioumba
2025-12-22 18:07:27 +01:00
parent 4bca6112bb
commit bbd555d61e
11 changed files with 104 additions and 43 deletions

View File

@@ -1,4 +1,4 @@
import { ApplicationConfig } from '@angular/core'; import { ApplicationConfig, ErrorHandler } from '@angular/core';
import { import {
PreloadAllModules, PreloadAllModules,
provideRouter, 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 { WebShareService } from '@app/infrastructure/shareData/web-share.service';
import { SETTING_REPOSITORY_TOKEN } from '@app/infrastructure/settings/setting-repository.token'; import { SETTING_REPOSITORY_TOKEN } from '@app/infrastructure/settings/setting-repository.token';
import { LocalSettingRepository } from '@app/infrastructure/settings/local-setting.repository'; import { LocalSettingRepository } from '@app/infrastructure/settings/local-setting.repository';
import { GlobalErrorHandler } from '@app/infrastructure/handlers/global-error-handler';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
@@ -48,6 +49,7 @@ export const appConfig: ApplicationConfig = {
{ provide: AUTH_REPOSITORY_TOKEN, useExisting: PbAuthRepository }, { provide: AUTH_REPOSITORY_TOKEN, useExisting: PbAuthRepository },
{ provide: WEB_SHARE_SERVICE_TOKEN, useExisting: WebShareService }, { provide: WEB_SHARE_SERVICE_TOKEN, useExisting: WebShareService },
{ provide: SETTING_REPOSITORY_TOKEN, useClass: LocalSettingRepository }, { provide: SETTING_REPOSITORY_TOKEN, useClass: LocalSettingRepository },
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
provideToastr({ provideToastr({
timeOut: 10000, timeOut: 10000,
positionClass: 'toast-top-right', positionClass: 'toast-top-right',

View File

@@ -0,0 +1,7 @@
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
OFF = 4,
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -187,7 +187,7 @@ export class MapComponent implements OnInit, AfterViewInit, OnDestroy {
// Image (Avatar) // Image (Avatar)
if (marker.profile!.avatarUrl) { if (marker.profile!.avatarUrl) {
const img = document.createElement('img'); 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'; img.className = 'w-16 h-16 rounded-full object-cover border-2 border-indigo-500 shadow-sm';
popupContainer.appendChild(img); popupContainer.appendChild(img);
} else { } else {

View File

@@ -20,9 +20,8 @@
<img <img
[alt]="project()?.nom || 'nouveau-projet'" [alt]="project()?.nom || 'nouveau-projet'"
class="object-cover object-center h-full w-full" class="object-cover object-center h-full w-full"
ngSrc="{{ currentImageUrl }}" [src]="currentImageUrl"
loading="lazy" loading="lazy"
fill
/> />
</div> </div>

View File

@@ -62,27 +62,25 @@ describe('ProjectPictureFormComponent', () => {
it("devrait afficher l'image du serveur si pas de preview", () => { it("devrait afficher l'image du serveur si pas de preview", () => {
// mockProject a 'image-serveur.jpg' // mockProject a 'image-serveur.jpg'
expect(component.currentImageUrl).toContain( expect(component.currentImageUrl).toContain('portfolio-preview.png');
'http://localhost:8090/api/files/projets/1/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 }; const projectNoImg = { ...mockProject, fichier: undefined };
fixture.componentRef.setInput('project', projectNoImg); fixture.componentRef.setInput('project', projectNoImg);
fixture.detectChanges(); fixture.detectChanges();
expect(component.currentImageUrl).toContain('api.dicebear.com'); expect(component.currentImageUrl).toContain('api.dicebear.com');
expect(component.currentImageUrl).toContain('Web 3D'); 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 }; const emptyProject = { ...mockProject, fichier: undefined, nom: undefined };
fixture.componentRef.setInput('project', emptyProject); fixture.componentRef.setInput('project', emptyProject);
fixture.detectChanges(); fixture.detectChanges();
expect(component.currentImageUrl).toContain('nouveau-projet'); expect(component.currentImageUrl).toContain(undefined);
}); });*/
}); });
describe('Validation (canSubmit)', () => { describe('Validation (canSubmit)', () => {

View File

@@ -1,5 +1,5 @@
import { Component, effect, inject, input, output, signal } from '@angular/core'; 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 { environment } from '@env/environment';
import { ProjectViewModel } from '@app/ui/projects/project.presenter.model'; import { ProjectViewModel } from '@app/ui/projects/project.presenter.model';
import { ProjectFacade } from '@app/ui/projects/project.facade'; import { ProjectFacade } from '@app/ui/projects/project.facade';
@@ -10,7 +10,7 @@ import { FileManagerService } from '@app/ui/shared/services/file-manager.service
@Component({ @Component({
selector: 'app-project-picture-form', selector: 'app-project-picture-form',
standalone: true, standalone: true,
imports: [LoadingComponent, NgTemplateOutlet, NgOptimizedImage], imports: [LoadingComponent, NgTemplateOutlet],
templateUrl: './project-picture-form.component.html', templateUrl: './project-picture-form.component.html',
styleUrl: './project-picture-form.component.scss', styleUrl: './project-picture-form.component.scss',
}) })
@@ -74,11 +74,7 @@ export class ProjectPictureFormComponent {
} }
if (this.project()?.fichier) { if (this.project()?.fichier) {
return `${this.environment.baseUrl}/api/files/projets/${this.project()!.id}/${this.project()!.fichier}?thumb=320x240`; return this.project()!.fichier;
}
if (this.project()?.nom) {
return `https://api.dicebear.com/9.x/shapes/svg?seed=${this.project()!.nom}`;
} }
return 'https://api.dicebear.com/9.x/shapes/svg?seed=nouveau-projet'; return 'https://api.dicebear.com/9.x/shapes/svg?seed=nouveau-projet';

View File

@@ -21,9 +21,8 @@
<img <img
[alt]="user.username || 'Avatar'" [alt]="user.username || 'Avatar'"
class="object-cover object-center h-full w-full" class="object-cover object-center h-full w-full"
ngSrc="{{ currentAvatarUrl }}" [src]="currentAvatarUrl"
loading="lazy" loading="lazy"
fill
/> />
</div> </div>
} }

View File

@@ -65,12 +65,6 @@ describe('UserAvatarFormComponent', () => {
expect(component.currentAvatarUrl).toBe('data:image/png;base64,preview'); 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", () => { it("devrait afficher un avatar Dicebear si l'utilisateur n'a pas d'avatar", () => {
const userNoAvatar = { ...mockUser, avatar: null }; const userNoAvatar = { ...mockUser, avatar: null };
fixture.componentRef.setInput('user', userNoAvatar); fixture.componentRef.setInput('user', userNoAvatar);
@@ -82,13 +76,6 @@ describe('UserAvatarFormComponent', () => {
'https://api.dicebear.com/9.x/initials/svg?seed=foo' '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)', () => { describe('Validation du formulaire (canSubmit)', () => {

View File

@@ -2,7 +2,7 @@ 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';
import { NgOptimizedImage, NgTemplateOutlet } from '@angular/common'; import { NgTemplateOutlet } from '@angular/common';
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';
@@ -13,7 +13,7 @@ import { FileManagerService } from '@app/ui/shared/services/file-manager.service
@Component({ @Component({
selector: 'app-user-avatar-form', selector: 'app-user-avatar-form',
standalone: true, standalone: true,
imports: [ReactiveFormsModule, NgTemplateOutlet, LoadingComponent, NgOptimizedImage], imports: [ReactiveFormsModule, 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',
}) })
@@ -80,14 +80,9 @@ export class UserAvatarFormComponent {
} }
if (currentUser?.avatar) { if (currentUser?.avatar) {
return `${this.environment.baseUrl}/api/files/users/${currentUser.id}/${currentUser.avatar}?thumb=320x240`; return currentUser.avatar;
} }
if (currentUser) { return `https://api.dicebear.com/9.x/initials/svg?seed=${currentUser!.name ?? 'trouveTonProfil'}`;
const seed = currentUser.name || currentUser.username || currentUser.email || 'user';
return `https://api.dicebear.com/9.x/initials/svg?seed=${seed}`;
}
return null;
} }
} }