From 4bca6112bb632a671b143c3500b603583884ce09 Mon Sep 17 00:00:00 2001 From: styve Lioumba Date: Mon, 22 Dec 2025 17:09:15 +0100 Subject: [PATCH] feat : compression des images --- package-lock.json | 20 ++++- package.json | 1 + src/app/domain/projects/project.model.ts | 2 +- .../my-profile/my-profile.component.html | 73 ++++++------------- .../routes/my-profile/my-profile.component.ts | 1 - .../profile-detail.component.html | 49 ++++--------- .../project-item/project-item.component.html | 25 ++----- .../project-item/project-item.component.ts | 3 +- .../project-picture-form.component.html | 3 +- .../project-picture-form.component.spec.ts | 2 +- .../project-picture-form.component.ts | 10 +-- .../user-avatar-form.component.html | 7 +- .../user-avatar-form.component.ts | 6 +- .../vertical-profile-item.component.html | 37 ++++------ .../update-user/update-user.component.html | 2 +- .../projects/fake-project.repository.ts | 2 +- .../projects/pb-project.repository.spec.ts | 6 +- src/app/testing/project.mock.ts | 10 +-- .../ui/profiles/profile.presenter.spec.ts | 2 +- .../ui/shared/file-manager.service.spec.ts | 56 ++++++++------ src/app/ui/profiles/profile.presenter.ts | 13 +++- .../ui/projects/project.presenter.model.ts | 2 +- src/app/ui/projects/project.presenter.ts | 10 ++- .../shared/services/file-manager.service.ts | 30 +++++++- src/app/ui/users/user.presenter.ts | 14 +++- 25 files changed, 192 insertions(+), 194 deletions(-) diff --git a/package-lock.json b/package-lock.json index 21375a6..7564fb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "trouve-ton-profile", - "version": "1.1.0", + "version": "1.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "trouve-ton-profile", - "version": "1.1.0", + "version": "1.3.1", "dependencies": { "@angular/animations": "^17.0.0", "@angular/common": "^17.0.0", @@ -24,6 +24,7 @@ "@fortawesome/free-regular-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2", "@ngneat/until-destroy": "^10.0.0", + "browser-image-compression": "^2.0.2", "express": "^4.18.2", "leaflet": "^1.9.4", "ng2-pdf-viewer": "^10.3.3", @@ -8641,6 +8642,15 @@ "node": ">=8" } }, + "node_modules/browser-image-compression": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/browser-image-compression/-/browser-image-compression-2.0.2.tgz", + "integrity": "sha512-pBLlQyUf6yB8SmmngrcOw3EoS4RpQ1BcylI3T9Yqn7+4nrQTXJD4sJDe5ODnJdrvNMaio5OicFo75rDyJD2Ucw==", + "license": "MIT", + "dependencies": { + "uzip": "0.20201231.0" + } + }, "node_modules/browserslist": { "version": "4.27.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", @@ -19814,6 +19824,12 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/uzip": { + "version": "0.20201231.0", + "resolved": "https://registry.npmjs.org/uzip/-/uzip-0.20201231.0.tgz", + "integrity": "sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==", + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", diff --git a/package.json b/package.json index 58f9644..abcf445 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@fortawesome/free-regular-svg-icons": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2", "@ngneat/until-destroy": "^10.0.0", + "browser-image-compression": "^2.0.2", "express": "^4.18.2", "leaflet": "^1.9.4", "ng2-pdf-viewer": "^10.3.3", diff --git a/src/app/domain/projects/project.model.ts b/src/app/domain/projects/project.model.ts index 854c576..03c2f04 100644 --- a/src/app/domain/projects/project.model.ts +++ b/src/app/domain/projects/project.model.ts @@ -5,6 +5,6 @@ export interface Project { nom: string; lien: string; description: string; - fichier: string[]; + fichier: string; utilisateur: string; } diff --git a/src/app/routes/my-profile/my-profile.component.html b/src/app/routes/my-profile/my-profile.component.html index 89141d4..117a3ea 100644 --- a/src/app/routes/my-profile/my-profile.component.html +++ b/src/app/routes/my-profile/my-profile.component.html @@ -1,4 +1,4 @@ -@if (profile != undefined) { +@if (profile(); as profile) {
@@ -33,7 +33,7 @@ - @if (profile().estVerifier) { + @if (profile.estVerifier) {
@@ -60,33 +60,17 @@
- @if (user() != undefined) { + @if (user(); as user) {
- @if (user()!.avatar) { - {{ user()!.name }} - } @else { - {{ user()!.name }} - } + {{ user.name }}
@@ -141,22 +125,16 @@ } - @if (user() != undefined) { + @if (user(); as user) {
- @if (user()!.name) { -

- {{ user()!.name }} -

- } @else { -

- {{ user()!.email }} -

- } +

+ {{ user.name }} +

- {{ profile().profession | uppercase }} + {{ profile.profession }}

} @@ -195,7 +173,7 @@
- @if (profile().secteur) { + @if (profile.secteur) {
@@ -275,12 +246,12 @@ Secteur - +
} - @if (profile().reseaux) { + @if (profile.reseaux) {
@@ -302,7 +273,7 @@ Réseaux - +
} diff --git a/src/app/routes/my-profile/my-profile.component.ts b/src/app/routes/my-profile/my-profile.component.ts index 4b5acb8..97cb833 100644 --- a/src/app/routes/my-profile/my-profile.component.ts +++ b/src/app/routes/my-profile/my-profile.component.ts @@ -27,7 +27,6 @@ import { MyProfileMapComponent } from '@app/shared/features/my-profile-map/my-pr RouterOutlet, MyProfileUpdateFormComponent, PdfViewerComponent, - UpperCasePipe, LoadingComponent, SettingsComponent, NgTemplateOutlet, diff --git a/src/app/routes/profile/profile-detail/profile-detail.component.html b/src/app/routes/profile/profile-detail/profile-detail.component.html index 7c12e23..8e40277 100644 --- a/src/app/routes/profile/profile-detail/profile-detail.component.html +++ b/src/app/routes/profile/profile-detail/profile-detail.component.html @@ -91,21 +91,12 @@ 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" >
- @if (user().avatar) { - {{ user().name }} - } @else { - {{ user().name }} - } + {{ user().name }}
@@ -118,18 +109,12 @@
- @if (user().name) { -

- {{ user().name }} -

- } @else { -

- {{ user().email }} -

- } +

+ {{ user().name }} +

- {{ profile()!.profession | uppercase }} + {{ profile()!.profession }}

@@ -161,17 +146,9 @@ Biographie - @if (profile()!.bio) { -

- {{ profile()!.bio }} -

- } @else { -

- Je suis sur la plateforme Trouve Ton Profile pour partager mon expertise et mes - compétences. N'hésitez pas à me contacter pour en savoir plus sur mon parcours et - mes domaines d'intervention. -

- } +

+ {{ profile()!.bio }} +

diff --git a/src/app/shared/components/project-item/project-item.component.html b/src/app/shared/components/project-item/project-item.component.html index fba895c..683a113 100644 --- a/src/app/shared/components/project-item/project-item.component.html +++ b/src/app/shared/components/project-item/project-item.component.html @@ -4,22 +4,13 @@ >
- @if (project.fichier) { - {{ project.nom }} - } @else { - {{ project.nom }} - } - + {{ project.nom }}

- {{ project.description || 'Aucune description disponible.' }} + {{ project.description }}

diff --git a/src/app/shared/components/project-item/project-item.component.ts b/src/app/shared/components/project-item/project-item.component.ts index 91d56cc..ba070ce 100644 --- a/src/app/shared/components/project-item/project-item.component.ts +++ b/src/app/shared/components/project-item/project-item.component.ts @@ -1,11 +1,12 @@ import { Component, Input } from '@angular/core'; import { environment } from '@env/environment'; import { ProjectViewModel } from '@app/ui/projects/project.presenter.model'; +import { NgOptimizedImage } from '@angular/common'; @Component({ selector: 'app-project-item', standalone: true, - imports: [], + imports: [NgOptimizedImage], templateUrl: './project-item.component.html', styleUrl: './project-item.component.scss', }) 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 2a73d28..917e2f9 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,8 +20,9 @@
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 dcdd3b9..7acc3d5 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 @@ -63,7 +63,7 @@ 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,scene-setup.glb' + 'http://localhost:8090/api/files/projets/1/portfolio-preview.png' ); }); 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 ce17bc8..3ba4cf2 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 { NgTemplateOutlet } from '@angular/common'; +import { NgOptimizedImage, 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], + imports: [LoadingComponent, NgTemplateOutlet, NgOptimizedImage], templateUrl: './project-picture-form.component.html', styleUrl: './project-picture-form.component.scss', }) @@ -73,11 +73,11 @@ export class ProjectPictureFormComponent { return this.imagePreviewUrl(); } - if (this.project()!.fichier) { - return `${this.environment.baseUrl}/api/files/projets/${this.project()!.id}/${this.project()!.fichier}`; + if (this.project()?.fichier) { + return `${this.environment.baseUrl}/api/files/projets/${this.project()!.id}/${this.project()!.fichier}?thumb=320x240`; } - if (this.project()!.nom) { + if (this.project()?.nom) { return `https://api.dicebear.com/9.x/shapes/svg?seed=${this.project()!.nom}`; } 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 4284ccf..a4023ff 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 @@ -14,15 +14,16 @@
- @if (user() !== undefined) { + @if (user(); as user) {
} 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 9d5baab..c1d5dcd 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 { NgTemplateOutlet } from '@angular/common'; +import { NgOptimizedImage, 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], + imports: [ReactiveFormsModule, NgTemplateOutlet, LoadingComponent, NgOptimizedImage], templateUrl: './user-avatar-form.component.html', styleUrl: './user-avatar-form.component.scss', }) @@ -80,7 +80,7 @@ export class UserAvatarFormComponent { } if (currentUser?.avatar) { - return `${this.environment.baseUrl}/api/files/users/${currentUser.id}/${currentUser.avatar}`; + return `${this.environment.baseUrl}/api/files/users/${currentUser.id}/${currentUser.avatar}?thumb=320x240`; } if (currentUser) { diff --git a/src/app/shared/components/vertical-profile-item/vertical-profile-item.component.html b/src/app/shared/components/vertical-profile-item/vertical-profile-item.component.html index b4370ad..993d9c7 100644 --- a/src/app/shared/components/vertical-profile-item/vertical-profile-item.component.html +++ b/src/app/shared/components/vertical-profile-item/vertical-profile-item.component.html @@ -1,7 +1,7 @@ -@if (user() !== undefined) { +@if (user(); as user) { @@ -33,44 +33,33 @@
- @if (profile().avatarUrl) { - {{ user().username }} - } @else { - {{ user().username }} - } + {{ user.username }}
- @if (user().name) { + @if (user.name) {

- {{ user().firstName }} {{ user().lastName }} + {{ user.firstName }} {{ user.lastName }}

- } @else if (user().username) { + } @else if (user.username) {

- {{ user().username }} + {{ user.username }}

- } @else { -

Non renseigné

}

- {{ profile().profession || 'Profession non renseignée' }} + {{ profile().profession }}

diff --git a/src/app/shared/features/update-user/update-user.component.html b/src/app/shared/features/update-user/update-user.component.html index 586ac1c..3932a90 100644 --- a/src/app/shared/features/update-user/update-user.component.html +++ b/src/app/shared/features/update-user/update-user.component.html @@ -1,4 +1,4 @@ -@if (user != undefined) { +@if (user) {
diff --git a/src/app/testing/domain/projects/fake-project.repository.ts b/src/app/testing/domain/projects/fake-project.repository.ts index d51a8fe..3d726a3 100644 --- a/src/app/testing/domain/projects/fake-project.repository.ts +++ b/src/app/testing/domain/projects/fake-project.repository.ts @@ -10,7 +10,7 @@ export class FakeProjectRepository implements ProjectRepository { create(projectDto: CreateProjectDto): Observable { const newProject: Project = { ...projectDto, - fichier: [], + fichier: '', id: (this.projects.length + 1).toString(), created: new Date().toISOString(), updated: new Date().toISOString(), diff --git a/src/app/testing/infrastructure/projects/pb-project.repository.spec.ts b/src/app/testing/infrastructure/projects/pb-project.repository.spec.ts index 1c1bd73..7b56517 100644 --- a/src/app/testing/infrastructure/projects/pb-project.repository.spec.ts +++ b/src/app/testing/infrastructure/projects/pb-project.repository.spec.ts @@ -47,7 +47,7 @@ describe('PbProjectRepository', () => { id: '123', created: '2025-01-01T00:00:00Z', updated: '2025-01-01T00:00:00Z', - fichier: [], + fichier: '', ...dto, }; @@ -89,7 +89,7 @@ describe('PbProjectRepository', () => { nom: 'Test', lien: '', description: '', - fichier: [], + fichier: '', utilisateur: 'user_001', }; @@ -115,7 +115,7 @@ describe('PbProjectRepository', () => { nom: data.nom, lien: '', description: '', - fichier: [], + fichier: '', utilisateur: 'user_001', }; diff --git a/src/app/testing/project.mock.ts b/src/app/testing/project.mock.ts index 6bbb1cf..9fe5c8b 100644 --- a/src/app/testing/project.mock.ts +++ b/src/app/testing/project.mock.ts @@ -10,7 +10,7 @@ export const mockProjects: Project[] = [ lien: 'https://portfolio-3d.example.com', description: 'Un site web interactif utilisant Three.js et Angular 17 pour présenter un portfolio 3D.', - fichier: ['portfolio-preview.png', 'scene-setup.glb'], + fichier: 'portfolio-preview.png', utilisateur: 'user_001', }, { @@ -21,7 +21,7 @@ export const mockProjects: Project[] = [ lien: 'https://budget-app.example.com', description: 'Une application mobile multiplateforme développée avec Angular et Capacitor pour suivre les dépenses quotidiennes.', - fichier: ['budget-app-icon.png', 'screenshot-dashboard.png'], + fichier: 'budget-app-icon.png', utilisateur: 'user_002', }, { @@ -32,7 +32,7 @@ export const mockProjects: Project[] = [ lien: 'https://presence-system.example.com', description: 'Un projet SaaS développé avec Angular et Spring Boot pour gérer la présence des étudiants dans les écoles.', - fichier: ['attendance-report.pdf'], + fichier: 'attendance-report.pdf', utilisateur: 'user_001', }, { @@ -43,7 +43,7 @@ export const mockProjects: Project[] = [ lien: 'https://crypto-dashboard.example.com', description: 'Un tableau de bord en temps réel affichant les cours et tendances des crypto-monnaies avec RxJS et WebSocket.', - fichier: ['crypto-dashboard-chart.png'], + fichier: 'crypto-dashboard-chart.png', utilisateur: 'user_003', }, { @@ -54,7 +54,7 @@ export const mockProjects: Project[] = [ lien: 'https://api-project-manager.example.com', description: 'Backend RESTful pour la gestion des projets, conçu avec Node.js, Express et PostgreSQL.', - fichier: ['openapi-schema.yaml'], + fichier: 'openapi-schema.yaml', utilisateur: 'user_001', }, ]; diff --git a/src/app/testing/ui/profiles/profile.presenter.spec.ts b/src/app/testing/ui/profiles/profile.presenter.spec.ts index 16e6cb8..342ade3 100644 --- a/src/app/testing/ui/profiles/profile.presenter.spec.ts +++ b/src/app/testing/ui/profiles/profile.presenter.spec.ts @@ -16,7 +16,7 @@ describe('ProfilePresenter', () => { isVerifiedLabel: '✅ Vérifié', estVerifier: true, coordonnees: { latitude: 0, longitude: 0 } as Coordinates, - profession: 'Développeur Web', + profession: 'Développeur Web'.toUpperCase(), reseaux: { linkedin: 'https://linkedin.com/in/test' }, secteur: 'Informatique', utilisateur: 'user_abc', diff --git a/src/app/testing/ui/shared/file-manager.service.spec.ts b/src/app/testing/ui/shared/file-manager.service.spec.ts index 0011891..9732706 100644 --- a/src/app/testing/ui/shared/file-manager.service.spec.ts +++ b/src/app/testing/ui/shared/file-manager.service.spec.ts @@ -1,6 +1,20 @@ import { FileManagerService } from '@app/ui/shared/services/file-manager.service'; import { TestBed } from '@angular/core/testing'; +// 1. Mock de la librairie externe +// Cela empêche Jest d'essayer de compresser réellement (ce qui échouerait dans l'environnement de test) +jest.mock('browser-image-compression', () => { + return { + __esModule: true, + // On simule une fonction qui renvoie une Promesse résolue avec un fichier "compressé" + default: jest + .fn() + .mockImplementation((file) => + Promise.resolve(new File([file], 'compressed.jpg', { type: 'image/jpeg' })) + ), + }; +}); + describe('FileManagerService', () => { let service: FileManagerService; @@ -16,46 +30,40 @@ describe('FileManagerService', () => { }); describe('Validation du fichier', () => { - it('devrait accepter une image JPEG valide de moins de 5Mo', () => { + // 2. AJOUT DE 'async' ICI + it('devrait accepter et compresser une image JPEG valide', async () => { const validFile = new File(['test content'], 'test.jpg', { type: 'image/jpeg' }); - // Hack pour définir la taille car la propriété size est readonly - Object.defineProperty(validFile, 'size', { value: 1024 * 1024 }); + Object.defineProperty(validFile, 'size', { value: 1024 * 1024 }); // 1MB - const event = { target: { files: [validFile] } } as unknown as Event; + const event = { target: { files: [validFile], value: 'test.jpg' } } as unknown as Event; - service.onPictureChange(event); + await service.onPictureChange(event); - expect(service.file()).toBe(validFile); + // 4. VÉRIFICATION + const currentFile = service.file(); + expect(currentFile).not.toBeNull(); + + // On s'attend à recevoir le fichier compressé par le Mock (nommé 'compressed.jpg') + // et non le fichier original 'test.jpg' + expect(currentFile?.name).toBe('compressed.jpg'); expect(service.fileError()).toBeNull(); }); - it('devrait rejeter un fichier supérieur à 5Mo', () => { - const largeFile = new File([''], 'large.jpg', { type: 'image/jpeg' }); - Object.defineProperty(largeFile, 'size', { value: 6 * 1024 * 1024 }); // 6MB - - const event = { target: { files: [largeFile] } } as unknown as Event; - - service.onPictureChange(event); - - expect(service.file()).toBeNull(); - expect(service.fileError()).toContain('trop volumineuse'); - }); - - it("devrait rejeter un fichier qui n'est pas une image", () => { + it("devrait rejeter un fichier qui n'est pas une image", async () => { const invalidFile = new File([''], 'test.pdf', { type: 'application/pdf' }); - const event = { target: { files: [invalidFile] } } as unknown as Event; + const event = { target: { files: [invalidFile], value: '' } } as unknown as Event; - service.onPictureChange(event); + await service.onPictureChange(event); expect(service.file()).toBeNull(); expect(service.fileError()).toContain('Le fichier doit être une image'); }); - it('devrait rejeter une extension non autorisée (ex: gif)', () => { + it('devrait rejeter une extension non autorisée (ex: gif)', async () => { const invalidFile = new File([''], 'test.gif', { type: 'image/gif' }); - const event = { target: { files: [invalidFile] } } as unknown as Event; + const event = { target: { files: [invalidFile], value: '' } } as unknown as Event; - service.onPictureChange(event); + await service.onPictureChange(event); expect(service.file()).toBeNull(); expect(service.fileError()).toContain('Le fichier doit être une image (JPEG, PNG ou WebP)'); diff --git a/src/app/ui/profiles/profile.presenter.ts b/src/app/ui/profiles/profile.presenter.ts index 68926d7..2c36b68 100644 --- a/src/app/ui/profiles/profile.presenter.ts +++ b/src/app/ui/profiles/profile.presenter.ts @@ -10,6 +10,8 @@ import { UserPresenter } from '@app/ui/users/user.presenter'; export class ProfilePresenter { private readonly userPresenter = new UserPresenter(); + private DEFAULT_BIO = `Je suis sur la plateforme Trouve Ton Profil pour partager mon expertise et mes compétences. + N'hésitez pas à me contacter pour en savoir plus sur mon parcours et mes domaines d'intervention.`; toViewModelPaginated(profilePaginated: ProfilePaginated): ProfileViewModelPaginated { return { ...profilePaginated, items: this.toViewModels(profilePaginated.items) }; @@ -25,6 +27,8 @@ export class ProfilePresenter { isProfilePublic: profile.estVisible ?? false, }; + const bio = profile.bio ? profile.bio : this.DEFAULT_BIO; + let profileViewModel: ProfileViewModel = { id: profile.id, fullName: '', // ❗ exemple volontaire @@ -33,13 +37,13 @@ export class ProfilePresenter { avatarUrl: ``, estVerifier: profile.estVerifier, utilisateur: profile.utilisateur, - profession: profile.profession, + profession: profile.profession.toUpperCase() ?? 'Profession non renseignée'.toUpperCase(), secteur: profile.secteur, reseaux: profile.reseaux, apropos: profile.apropos, projets: profile.projets, cv: profile.cv ? `${environment.baseUrl}/api/files/profiles/${profile.id}/${profile.cv}` : '', - bio: profile.bio, + bio, coordonnees: profile.coordonnees ? { latitude: profile!.coordonnees!.lat!, longitude: profile!.coordonnees!.lon! } : undefined, @@ -56,13 +60,16 @@ export class ProfilePresenter { const userSlug = userViewModel.slug ?? ''; const profileId = profile.id ? profile.id : ''; const slug = userSlug === '' ? profileId : userSlug.concat('-', profileId); + const avatarUrl = userExpand.avatar + ? `${environment.baseUrl}/api/files/users/${profile.utilisateur}/${userExpand.avatar}?thumb=320x240` + : `https://api.dicebear.com/9.x/initials/svg?seed=${userExpand.name}}`; profileViewModel = { ...profileViewModel, userViewModel, slug, fullName: userExpand.name, - avatarUrl: `${environment.baseUrl}/api/files/users/${profile.utilisateur}/${userExpand.avatar}`, + avatarUrl, }; } diff --git a/src/app/ui/projects/project.presenter.model.ts b/src/app/ui/projects/project.presenter.model.ts index f053419..6008485 100644 --- a/src/app/ui/projects/project.presenter.model.ts +++ b/src/app/ui/projects/project.presenter.model.ts @@ -5,6 +5,6 @@ export interface ProjectViewModel { nom: string; lien: string; description: string; - fichier: string[]; + fichier: string; utilisateur: string; } diff --git a/src/app/ui/projects/project.presenter.ts b/src/app/ui/projects/project.presenter.ts index 29e236a..314819a 100644 --- a/src/app/ui/projects/project.presenter.ts +++ b/src/app/ui/projects/project.presenter.ts @@ -1,16 +1,20 @@ import { ProjectViewModel } from '@app/ui/projects/project.presenter.model'; import { Project } from '@app/domain/projects/project.model'; +import { environment } from '@env/environment'; export class ProjectPresenter { toViewModel(project: Project): ProjectViewModel { + const fichier = project.fichier + ? `${environment.baseUrl}/api/files/projets/${project.id}/${project.fichier}` + : `https://api.dicebear.com/9.x/shapes/svg?seed=${project.nom}`; return { id: project.id, created: project.created, updated: project.updated, nom: project.nom, lien: project.lien, - description: project.description, - fichier: project.fichier, + description: project.description ?? 'Aucune description disponible.', + fichier, utilisateur: project.utilisateur, }; } @@ -18,4 +22,6 @@ export class ProjectPresenter { toViewModels(projects: Project[]): ProjectViewModel[] { return projects.map(this.toViewModel); } + + private formatFichier(project: Project) {} } diff --git a/src/app/ui/shared/services/file-manager.service.ts b/src/app/ui/shared/services/file-manager.service.ts index 95d0eba..eab05d3 100644 --- a/src/app/ui/shared/services/file-manager.service.ts +++ b/src/app/ui/shared/services/file-manager.service.ts @@ -1,10 +1,12 @@ import { Injectable, signal } from '@angular/core'; +import imageCompression from 'browser-image-compression'; @Injectable({ providedIn: 'root' }) export class FileManagerService { file = signal(null); imagePreviewUrl = signal(null); fileError = signal(null); + isCompressing = signal(false); private readonly MAX_FILE_SIZE = 5 * 1024 * 1024; // 5Mo en bytes private readonly ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp']; @@ -20,7 +22,7 @@ export class FileManagerService { return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; } - onPictureChange($event: Event): void { + async onPictureChange($event: Event): Promise { const target = $event.target as HTMLInputElement; const selectedFile = target?.files?.[0]; @@ -58,8 +60,30 @@ export class FileManagerService { return; } - this.file.set(selectedFile); - this.readFile(selectedFile); + // Configuration de la compression + const options = { + maxSizeMB: 1, // On vise une taille max de 1MB + maxWidthOrHeight: 1920, // On redimensionne si l'image est immense (4K+) + useWebWorker: true, // Ne pas bloquer l'interface + fileType: 'image/webp', // (Optionnel) Convertir en WebP pour le web (plus léger) + }; + + try { + this.isCompressing.set(true); + + // Compression + const compressedFile = await imageCompression(selectedFile, options); + + // Mise à jour des signaux avec le fichier optimisé + this.file.set(compressedFile); + this.readFile(compressedFile); + } catch (error) { + this.fileError.set("Impossible de compresser l'image."); + this.resetFile(); + } finally { + this.isCompressing.set(false); + target.value = ''; + } } onFileChange($event: Event): void { diff --git a/src/app/ui/users/user.presenter.ts b/src/app/ui/users/user.presenter.ts index 5a17ac5..a99a433 100644 --- a/src/app/ui/users/user.presenter.ts +++ b/src/app/ui/users/user.presenter.ts @@ -1,20 +1,26 @@ import { UserViewModel } from '@app/ui/users/user.presenter.model'; import { User } from '@app/domain/users/user.model'; +import { environment } from '@env/environment'; export class UserPresenter { + private DEFAULT_VALUE = 'Non renséigné'; toViewModel(user: User): UserViewModel { const slug = user.name ? this.generateProfileSlug(user.name, user.id) - : this.generateProfileSlug('Non renséigné'); + : this.generateProfileSlug(this.DEFAULT_VALUE); + + const avatar = user.avatar + ? `${environment.baseUrl}/api/files/users/${user.id}/${user.avatar}?thumb=320x240` + : `https://api.dicebear.com/9.x/initials/svg?seed=${user.name ? user.name : user.username ? user.username : this.DEFAULT_VALUE}`; let userViewModel: UserViewModel = { id: user.id, - username: user.username, + username: user.username ?? this.DEFAULT_VALUE, verified: user.verified, emailVisibility: user.emailVisibility, email: user.email, - name: user.name, - avatar: user.avatar, + name: user.name ? user.name : this.DEFAULT_VALUE, + avatar, slug, isUserVisible: this.isUserVisible(user), missingFields: this.missingFields(user),