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 {
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',

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)
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 {

View File

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

View File

@@ -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)', () => {

View File

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

View File

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

View File

@@ -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)', () => {

View File

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