feat : compression des images
This commit is contained in:
20
package-lock.json
generated
20
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "trouve-ton-profile",
|
"name": "trouve-ton-profile",
|
||||||
"version": "1.1.0",
|
"version": "1.3.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "trouve-ton-profile",
|
"name": "trouve-ton-profile",
|
||||||
"version": "1.1.0",
|
"version": "1.3.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^17.0.0",
|
"@angular/animations": "^17.0.0",
|
||||||
"@angular/common": "^17.0.0",
|
"@angular/common": "^17.0.0",
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^6.4.2",
|
"@fortawesome/free-regular-svg-icons": "^6.4.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
"@ngneat/until-destroy": "^10.0.0",
|
"@ngneat/until-destroy": "^10.0.0",
|
||||||
|
"browser-image-compression": "^2.0.2",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"ng2-pdf-viewer": "^10.3.3",
|
"ng2-pdf-viewer": "^10.3.3",
|
||||||
@@ -8641,6 +8642,15 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.27.0",
|
"version": "4.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
|
||||||
@@ -19814,6 +19824,12 @@
|
|||||||
"uuid": "dist/bin/uuid"
|
"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": {
|
"node_modules/v8-to-istanbul": {
|
||||||
"version": "9.3.0",
|
"version": "9.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz",
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"@fortawesome/free-regular-svg-icons": "^6.4.2",
|
"@fortawesome/free-regular-svg-icons": "^6.4.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
"@fortawesome/free-solid-svg-icons": "^6.4.2",
|
||||||
"@ngneat/until-destroy": "^10.0.0",
|
"@ngneat/until-destroy": "^10.0.0",
|
||||||
|
"browser-image-compression": "^2.0.2",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"ng2-pdf-viewer": "^10.3.3",
|
"ng2-pdf-viewer": "^10.3.3",
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ export interface Project {
|
|||||||
nom: string;
|
nom: string;
|
||||||
lien: string;
|
lien: string;
|
||||||
description: string;
|
description: string;
|
||||||
fichier: string[];
|
fichier: string;
|
||||||
utilisateur: string;
|
utilisateur: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@if (profile != undefined) {
|
@if (profile(); as profile) {
|
||||||
<section
|
<section
|
||||||
class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800"
|
class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800"
|
||||||
>
|
>
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Badge vérifié -->
|
<!-- Badge vérifié -->
|
||||||
@if (profile().estVerifier) {
|
@if (profile.estVerifier) {
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 bg-purple-500/20 backdrop-blur-md px-3 py-2 rounded-full animate-pulse-slow"
|
class="flex items-center gap-2 bg-purple-500/20 backdrop-blur-md px-3 py-2 rounded-full animate-pulse-slow"
|
||||||
>
|
>
|
||||||
@@ -60,33 +60,17 @@
|
|||||||
<div class="relative -mt-16 md:-mt-22 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 indicateur de visibilité -->
|
<!-- Avatar avec indicateur de visibilité -->
|
||||||
@if (user() != undefined) {
|
@if (user(); as user) {
|
||||||
<div class="relative group animate-slide-up">
|
<div class="relative group animate-slide-up">
|
||||||
<div
|
<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"
|
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">
|
<div class="w-full h-full rounded-full overflow-hidden bg-white">
|
||||||
@if (user()!.avatar) {
|
|
||||||
<img
|
<img
|
||||||
alt="{{ user()!.name }}"
|
alt="{{ user.name }}"
|
||||||
class="object-cover w-full h-full"
|
class="object-cover w-full h-full"
|
||||||
src="{{ environment.baseUrl }}/api/files/users/{{ user()!.id }}/{{
|
src="{{ user.avatar }}"
|
||||||
user()!.avatar
|
|
||||||
}}"
|
|
||||||
/>
|
/>
|
||||||
} @else {
|
|
||||||
<img
|
|
||||||
alt="{{ user()!.name }}"
|
|
||||||
class="object-cover w-full h-full"
|
|
||||||
src="https://api.dicebear.com/9.x/initials/svg?seed={{
|
|
||||||
user().name
|
|
||||||
? user().name
|
|
||||||
: user().username
|
|
||||||
? user().username
|
|
||||||
: user().email
|
|
||||||
}}"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -141,22 +125,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<!-- Nom et titre -->
|
<!-- Nom et titre -->
|
||||||
@if (user() != undefined) {
|
@if (user(); as user) {
|
||||||
<div
|
<div
|
||||||
class="flex-1 text-center md:text-left mb-4 mt-8 md:mt-4 md:mb-0 animate-slide-up animation-delay-100"
|
class="flex-1 text-center md:text-left mb-4 mt-8 md: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">
|
<h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1">
|
||||||
{{ user()!.name }}
|
{{ user.name }}
|
||||||
</h1>
|
</h1>
|
||||||
} @else {
|
|
||||||
<h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1">
|
|
||||||
{{ 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 }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -195,7 +173,7 @@
|
|||||||
<!-- 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 -->
|
||||||
@if (profile().apropos) {
|
@if (profile.apropos) {
|
||||||
<div
|
<div
|
||||||
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow duration-300"
|
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow duration-300"
|
||||||
>
|
>
|
||||||
@@ -217,7 +195,7 @@
|
|||||||
À propos
|
À propos
|
||||||
</h3>
|
</h3>
|
||||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed text-base">
|
<p class="text-gray-700 dark:text-gray-300 leading-relaxed text-base">
|
||||||
{{ profile().apropos }}
|
{{ profile.apropos }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -243,20 +221,13 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Biographie
|
Biographie
|
||||||
</h3>
|
</h3>
|
||||||
@if (profile().bio) {
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed">
|
<p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed">
|
||||||
{{ profile().bio }}
|
{{ profile.bio }}
|
||||||
</p>
|
</p>
|
||||||
} @else {
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed">
|
|
||||||
Je suis sur la plateforme Trouve Ton Profile pour partager mon expertise et mes
|
|
||||||
compétences.
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card Secteur -->
|
<!-- Card Secteur -->
|
||||||
@if (profile().secteur) {
|
@if (profile.secteur) {
|
||||||
<div
|
<div
|
||||||
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow duration-300"
|
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow duration-300"
|
||||||
>
|
>
|
||||||
@@ -275,12 +246,12 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Secteur
|
Secteur
|
||||||
</h3>
|
</h3>
|
||||||
<app-chips [sectorId]="profile().secteur" />
|
<app-chips [sectorId]="profile.secteur" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Card Réseaux -->
|
<!-- Card Réseaux -->
|
||||||
@if (profile().reseaux) {
|
@if (profile.reseaux) {
|
||||||
<div
|
<div
|
||||||
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow duration-300"
|
class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl transition-shadow duration-300"
|
||||||
>
|
>
|
||||||
@@ -302,7 +273,7 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Réseaux
|
Réseaux
|
||||||
</h3>
|
</h3>
|
||||||
<app-reseaux [reseaux]="profile().reseaux" />
|
<app-reseaux [reseaux]="profile.reseaux" />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import { MyProfileMapComponent } from '@app/shared/features/my-profile-map/my-pr
|
|||||||
RouterOutlet,
|
RouterOutlet,
|
||||||
MyProfileUpdateFormComponent,
|
MyProfileUpdateFormComponent,
|
||||||
PdfViewerComponent,
|
PdfViewerComponent,
|
||||||
UpperCasePipe,
|
|
||||||
LoadingComponent,
|
LoadingComponent,
|
||||||
SettingsComponent,
|
SettingsComponent,
|
||||||
NgTemplateOutlet,
|
NgTemplateOutlet,
|
||||||
|
|||||||
@@ -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"
|
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">
|
<div class="w-full h-full rounded-full overflow-hidden bg-white">
|
||||||
@if (user().avatar) {
|
|
||||||
<img
|
<img
|
||||||
alt="{{ user().name }}"
|
alt="{{ user().name }}"
|
||||||
class="object-cover w-full h-full"
|
class="object-cover w-full h-full"
|
||||||
src="{{ profile().avatarUrl }}"
|
src="{{ profile().avatarUrl }}"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
} @else {
|
|
||||||
<img
|
|
||||||
alt="{{ user().name }}"
|
|
||||||
class="object-cover w-full h-full"
|
|
||||||
src="https://api.dicebear.com/9.x/initials/svg?seed={{ user().name }}"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Indicateur en ligne (optionnel) -->
|
<!-- Indicateur en ligne (optionnel) -->
|
||||||
@@ -118,18 +109,12 @@
|
|||||||
<div
|
<div
|
||||||
class="flex-1 text-center md:text-left mb-4 md:mb-0 animate-slide-up animation-delay-100"
|
class="flex-1 text-center md:text-left mb-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">
|
<h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1">
|
||||||
{{ user().name }}
|
{{ user().name }}
|
||||||
</h1>
|
</h1>
|
||||||
} @else {
|
|
||||||
<h1 class="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-1">
|
|
||||||
{{ 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 }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,17 +146,9 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Biographie
|
Biographie
|
||||||
</h3>
|
</h3>
|
||||||
@if (profile()!.bio) {
|
|
||||||
<p class="text-gray-600 dark:text-gray-300 text-sm leading-relaxed">
|
<p class="text-gray-600 dark:text-gray-300 text-sm leading-relaxed">
|
||||||
{{ profile()!.bio }}
|
{{ profile()!.bio }}
|
||||||
</p>
|
</p>
|
||||||
} @else {
|
|
||||||
<p class="text-gray-600 dark:text-gray-300 text-sm leading-relaxed">
|
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card Secteur -->
|
<!-- Card Secteur -->
|
||||||
|
|||||||
@@ -4,22 +4,13 @@
|
|||||||
>
|
>
|
||||||
<!-- Image du projet -->
|
<!-- Image du projet -->
|
||||||
<div class="relative h-48 overflow-hidden bg-gray-100 dark:bg-gray-700">
|
<div class="relative h-48 overflow-hidden bg-gray-100 dark:bg-gray-700">
|
||||||
@if (project.fichier) {
|
|
||||||
<img
|
<img
|
||||||
alt="{{ project.nom }}"
|
alt="{{ project.nom }}"
|
||||||
class="w-full h-full object-cover object-center transition-transform duration-500 group-hover:scale-110"
|
class="w-full h-full object-cover object-center transition-transform duration-500 group-hover:scale-110"
|
||||||
src="{{ environment.baseUrl }}/api/files/projets/{{ project.id }}/{{ project.fichier }}"
|
ngSrc="{{ project.fichier }}"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
fill
|
||||||
/>
|
/>
|
||||||
} @else {
|
|
||||||
<img
|
|
||||||
alt="{{ project.nom }}"
|
|
||||||
class="w-full h-full object-cover object-center transition-transform duration-500 group-hover:scale-110 opacity-60"
|
|
||||||
src="https://api.dicebear.com/9.x/shapes/svg?seed={{ project.nom }}"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Overlay gradient au hover -->
|
<!-- Overlay gradient au hover -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
||||||
@@ -37,7 +28,7 @@
|
|||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-300 line-clamp-3">
|
<p class="text-sm text-gray-600 dark:text-gray-300 line-clamp-3">
|
||||||
{{ project.description || 'Aucune description disponible.' }}
|
{{ project.description }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Lien vers le projet -->
|
<!-- Lien vers le projet -->
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
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 { NgOptimizedImage } from '@angular/common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-project-item',
|
selector: 'app-project-item',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [],
|
imports: [NgOptimizedImage],
|
||||||
templateUrl: './project-item.component.html',
|
templateUrl: './project-item.component.html',
|
||||||
styleUrl: './project-item.component.scss',
|
styleUrl: './project-item.component.scss',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,8 +20,9 @@
|
|||||||
<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"
|
||||||
[src]="currentImageUrl"
|
ngSrc="{{ currentImageUrl }}"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
fill
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ 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(
|
||||||
'http://localhost:8090/api/files/projets/1/portfolio-preview.png,scene-setup.glb'
|
'http://localhost:8090/api/files/projets/1/portfolio-preview.png'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 { NgTemplateOutlet } from '@angular/common';
|
import { NgOptimizedImage, 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],
|
imports: [LoadingComponent, NgTemplateOutlet, NgOptimizedImage],
|
||||||
templateUrl: './project-picture-form.component.html',
|
templateUrl: './project-picture-form.component.html',
|
||||||
styleUrl: './project-picture-form.component.scss',
|
styleUrl: './project-picture-form.component.scss',
|
||||||
})
|
})
|
||||||
@@ -73,11 +73,11 @@ export class ProjectPictureFormComponent {
|
|||||||
return this.imagePreviewUrl();
|
return this.imagePreviewUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.project()!.fichier) {
|
if (this.project()?.fichier) {
|
||||||
return `${this.environment.baseUrl}/api/files/projets/${this.project()!.id}/${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}`;
|
return `https://api.dicebear.com/9.x/shapes/svg?seed=${this.project()!.nom}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,15 +14,16 @@
|
|||||||
<!-- Avatar avec preview -->
|
<!-- Avatar avec preview -->
|
||||||
<div class="flex flex-col items-center space-y-4">
|
<div class="flex flex-col items-center space-y-4">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
@if (user() !== undefined) {
|
@if (user(); as user) {
|
||||||
<div
|
<div
|
||||||
class="w-40 h-40 rounded-full overflow-hidden bg-gray-200 dark:bg-gray-700 flex items-center justify-center shadow-lg ring-4 ring-gray-100 dark:ring-gray-600"
|
class="w-40 h-40 rounded-full overflow-hidden bg-gray-200 dark:bg-gray-700 flex items-center justify-center shadow-lg ring-4 ring-gray-100 dark:ring-gray-600"
|
||||||
>
|
>
|
||||||
<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"
|
||||||
[src]="currentAvatarUrl"
|
ngSrc="{{ currentAvatarUrl }}"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
fill
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { NgTemplateOutlet } from '@angular/common';
|
import { NgOptimizedImage, 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],
|
imports: [ReactiveFormsModule, NgTemplateOutlet, LoadingComponent, NgOptimizedImage],
|
||||||
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,7 +80,7 @@ export class UserAvatarFormComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentUser?.avatar) {
|
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) {
|
if (currentUser) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@if (user() !== undefined) {
|
@if (user(); as user) {
|
||||||
<a
|
<a
|
||||||
[routerLink]="[profile()!.slug!]"
|
[routerLink]="[profile()!.slug!]"
|
||||||
[state]="{ user: user(), profile: profile() }"
|
[state]="{ user: user, profile: profile() }"
|
||||||
class="block group"
|
class="block group"
|
||||||
>
|
>
|
||||||
<!-- Card du profil -->
|
<!-- Card du profil -->
|
||||||
@@ -33,44 +33,33 @@
|
|||||||
<!-- Avatar avec bordure gradient -->
|
<!-- Avatar avec bordure gradient -->
|
||||||
<div class="relative inline-block mb-4">
|
<div class="relative inline-block mb-4">
|
||||||
<div class="w-32 h-32 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 p-1">
|
<div class="w-32 h-32 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 p-1">
|
||||||
@if (profile().avatarUrl) {
|
|
||||||
<img
|
<img
|
||||||
class="w-full h-full rounded-full object-cover grayscale group-hover:grayscale-0 transition-all duration-500 group-hover:scale-105"
|
class="w-full h-full rounded-full object-cover grayscale group-hover:grayscale-0 transition-all duration-500 group-hover:scale-105"
|
||||||
src="{{ profile().avatarUrl }}"
|
src="{{ profile().avatarUrl }}"
|
||||||
alt="{{ user().username }}"
|
alt="{{ user.username }}"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
} @else {
|
|
||||||
<img
|
|
||||||
class="w-full h-full rounded-full object-cover grayscale group-hover:grayscale-0 transition-all duration-500 group-hover:scale-105"
|
|
||||||
src="https://api.dicebear.com/9.x/initials/svg?seed={{ user().name }}"
|
|
||||||
alt="{{ user().username }}"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Nom -->
|
<!-- Nom -->
|
||||||
@if (user().name) {
|
@if (user.name) {
|
||||||
<h3
|
<h3
|
||||||
class="text-lg font-bold text-gray-900 dark:text-white mb-2 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors"
|
class="text-lg font-bold text-gray-900 dark:text-white mb-2 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors"
|
||||||
>
|
>
|
||||||
{{ user().firstName }} {{ user().lastName }}
|
{{ user.firstName }} {{ user.lastName }}
|
||||||
</h3>
|
</h3>
|
||||||
} @else if (user().username) {
|
} @else if (user.username) {
|
||||||
<h3
|
<h3
|
||||||
class="text-lg font-bold text-gray-900 dark:text-white mb-2 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors"
|
class="text-lg font-bold text-gray-900 dark:text-white mb-2 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors"
|
||||||
>
|
>
|
||||||
{{ user().username }}
|
{{ user.username }}
|
||||||
</h3>
|
</h3>
|
||||||
} @else {
|
|
||||||
<h3 class="text-lg font-bold text-gray-500 dark:text-gray-400 mb-2">Non renseigné</h3>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Profession -->
|
<!-- Profession -->
|
||||||
<p class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">
|
<p class="text-sm font-semibold text-gray-600 dark:text-gray-300 mb-2">
|
||||||
{{ profile().profession || 'Profession non renseignée' }}
|
{{ profile().profession }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
@if (user != undefined) {
|
@if (user) {
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<!-- Section Avatar -->
|
<!-- Section Avatar -->
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 animate-fade-in">
|
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 animate-fade-in">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export class FakeProjectRepository implements ProjectRepository {
|
|||||||
create(projectDto: CreateProjectDto): Observable<Project> {
|
create(projectDto: CreateProjectDto): Observable<Project> {
|
||||||
const newProject: Project = {
|
const newProject: Project = {
|
||||||
...projectDto,
|
...projectDto,
|
||||||
fichier: [],
|
fichier: '',
|
||||||
id: (this.projects.length + 1).toString(),
|
id: (this.projects.length + 1).toString(),
|
||||||
created: new Date().toISOString(),
|
created: new Date().toISOString(),
|
||||||
updated: new Date().toISOString(),
|
updated: new Date().toISOString(),
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ describe('PbProjectRepository', () => {
|
|||||||
id: '123',
|
id: '123',
|
||||||
created: '2025-01-01T00:00:00Z',
|
created: '2025-01-01T00:00:00Z',
|
||||||
updated: '2025-01-01T00:00:00Z',
|
updated: '2025-01-01T00:00:00Z',
|
||||||
fichier: [],
|
fichier: '',
|
||||||
...dto,
|
...dto,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ describe('PbProjectRepository', () => {
|
|||||||
nom: 'Test',
|
nom: 'Test',
|
||||||
lien: '',
|
lien: '',
|
||||||
description: '',
|
description: '',
|
||||||
fichier: [],
|
fichier: '',
|
||||||
utilisateur: 'user_001',
|
utilisateur: 'user_001',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ describe('PbProjectRepository', () => {
|
|||||||
nom: data.nom,
|
nom: data.nom,
|
||||||
lien: '',
|
lien: '',
|
||||||
description: '',
|
description: '',
|
||||||
fichier: [],
|
fichier: '',
|
||||||
utilisateur: 'user_001',
|
utilisateur: 'user_001',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const mockProjects: Project[] = [
|
|||||||
lien: 'https://portfolio-3d.example.com',
|
lien: 'https://portfolio-3d.example.com',
|
||||||
description:
|
description:
|
||||||
'Un site web interactif utilisant Three.js et Angular 17 pour présenter un portfolio 3D.',
|
'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',
|
utilisateur: 'user_001',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -21,7 +21,7 @@ export const mockProjects: Project[] = [
|
|||||||
lien: 'https://budget-app.example.com',
|
lien: 'https://budget-app.example.com',
|
||||||
description:
|
description:
|
||||||
'Une application mobile multiplateforme développée avec Angular et Capacitor pour suivre les dépenses quotidiennes.',
|
'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',
|
utilisateur: 'user_002',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -32,7 +32,7 @@ export const mockProjects: Project[] = [
|
|||||||
lien: 'https://presence-system.example.com',
|
lien: 'https://presence-system.example.com',
|
||||||
description:
|
description:
|
||||||
'Un projet SaaS développé avec Angular et Spring Boot pour gérer la présence des étudiants dans les écoles.',
|
'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',
|
utilisateur: 'user_001',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -43,7 +43,7 @@ export const mockProjects: Project[] = [
|
|||||||
lien: 'https://crypto-dashboard.example.com',
|
lien: 'https://crypto-dashboard.example.com',
|
||||||
description:
|
description:
|
||||||
'Un tableau de bord en temps réel affichant les cours et tendances des crypto-monnaies avec RxJS et WebSocket.',
|
'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',
|
utilisateur: 'user_003',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -54,7 +54,7 @@ export const mockProjects: Project[] = [
|
|||||||
lien: 'https://api-project-manager.example.com',
|
lien: 'https://api-project-manager.example.com',
|
||||||
description:
|
description:
|
||||||
'Backend RESTful pour la gestion des projets, conçu avec Node.js, Express et PostgreSQL.',
|
'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',
|
utilisateur: 'user_001',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ describe('ProfilePresenter', () => {
|
|||||||
isVerifiedLabel: '✅ Vérifié',
|
isVerifiedLabel: '✅ Vérifié',
|
||||||
estVerifier: true,
|
estVerifier: true,
|
||||||
coordonnees: { latitude: 0, longitude: 0 } as Coordinates,
|
coordonnees: { latitude: 0, longitude: 0 } as Coordinates,
|
||||||
profession: 'Développeur Web',
|
profession: 'Développeur Web'.toUpperCase(),
|
||||||
reseaux: { linkedin: 'https://linkedin.com/in/test' },
|
reseaux: { linkedin: 'https://linkedin.com/in/test' },
|
||||||
secteur: 'Informatique',
|
secteur: 'Informatique',
|
||||||
utilisateur: 'user_abc',
|
utilisateur: 'user_abc',
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
import { FileManagerService } from '@app/ui/shared/services/file-manager.service';
|
import { FileManagerService } from '@app/ui/shared/services/file-manager.service';
|
||||||
import { TestBed } from '@angular/core/testing';
|
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', () => {
|
describe('FileManagerService', () => {
|
||||||
let service: FileManagerService;
|
let service: FileManagerService;
|
||||||
|
|
||||||
@@ -16,46 +30,40 @@ describe('FileManagerService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Validation du fichier', () => {
|
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' });
|
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 }); // 1MB
|
||||||
Object.defineProperty(validFile, 'size', { value: 1024 * 1024 });
|
|
||||||
|
|
||||||
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();
|
expect(service.fileError()).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('devrait rejeter un fichier supérieur à 5Mo', () => {
|
it("devrait rejeter un fichier qui n'est pas une image", async () => {
|
||||||
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", () => {
|
|
||||||
const invalidFile = new File([''], 'test.pdf', { type: 'application/pdf' });
|
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.file()).toBeNull();
|
||||||
expect(service.fileError()).toContain('Le fichier doit être une image');
|
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 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.file()).toBeNull();
|
||||||
expect(service.fileError()).toContain('Le fichier doit être une image (JPEG, PNG ou WebP)');
|
expect(service.fileError()).toContain('Le fichier doit être une image (JPEG, PNG ou WebP)');
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import { UserPresenter } from '@app/ui/users/user.presenter';
|
|||||||
|
|
||||||
export class ProfilePresenter {
|
export class ProfilePresenter {
|
||||||
private readonly userPresenter = new UserPresenter();
|
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 {
|
toViewModelPaginated(profilePaginated: ProfilePaginated): ProfileViewModelPaginated {
|
||||||
return { ...profilePaginated, items: this.toViewModels(profilePaginated.items) };
|
return { ...profilePaginated, items: this.toViewModels(profilePaginated.items) };
|
||||||
@@ -25,6 +27,8 @@ export class ProfilePresenter {
|
|||||||
isProfilePublic: profile.estVisible ?? false,
|
isProfilePublic: profile.estVisible ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bio = profile.bio ? profile.bio : this.DEFAULT_BIO;
|
||||||
|
|
||||||
let profileViewModel: ProfileViewModel = {
|
let profileViewModel: ProfileViewModel = {
|
||||||
id: profile.id,
|
id: profile.id,
|
||||||
fullName: '', // ❗ exemple volontaire
|
fullName: '', // ❗ exemple volontaire
|
||||||
@@ -33,13 +37,13 @@ export class ProfilePresenter {
|
|||||||
avatarUrl: ``,
|
avatarUrl: ``,
|
||||||
estVerifier: profile.estVerifier,
|
estVerifier: profile.estVerifier,
|
||||||
utilisateur: profile.utilisateur,
|
utilisateur: profile.utilisateur,
|
||||||
profession: profile.profession,
|
profession: profile.profession.toUpperCase() ?? 'Profession non renseignée'.toUpperCase(),
|
||||||
secteur: profile.secteur,
|
secteur: profile.secteur,
|
||||||
reseaux: profile.reseaux,
|
reseaux: profile.reseaux,
|
||||||
apropos: profile.apropos,
|
apropos: profile.apropos,
|
||||||
projets: profile.projets,
|
projets: profile.projets,
|
||||||
cv: profile.cv ? `${environment.baseUrl}/api/files/profiles/${profile.id}/${profile.cv}` : '',
|
cv: profile.cv ? `${environment.baseUrl}/api/files/profiles/${profile.id}/${profile.cv}` : '',
|
||||||
bio: profile.bio,
|
bio,
|
||||||
coordonnees: profile.coordonnees
|
coordonnees: profile.coordonnees
|
||||||
? { latitude: profile!.coordonnees!.lat!, longitude: profile!.coordonnees!.lon! }
|
? { latitude: profile!.coordonnees!.lat!, longitude: profile!.coordonnees!.lon! }
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -56,13 +60,16 @@ export class ProfilePresenter {
|
|||||||
const userSlug = userViewModel.slug ?? '';
|
const userSlug = userViewModel.slug ?? '';
|
||||||
const profileId = profile.id ? profile.id : '';
|
const profileId = profile.id ? profile.id : '';
|
||||||
const slug = userSlug === '' ? profileId : userSlug.concat('-', profileId);
|
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 = {
|
||||||
...profileViewModel,
|
...profileViewModel,
|
||||||
userViewModel,
|
userViewModel,
|
||||||
slug,
|
slug,
|
||||||
fullName: userExpand.name,
|
fullName: userExpand.name,
|
||||||
avatarUrl: `${environment.baseUrl}/api/files/users/${profile.utilisateur}/${userExpand.avatar}`,
|
avatarUrl,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ export interface ProjectViewModel {
|
|||||||
nom: string;
|
nom: string;
|
||||||
lien: string;
|
lien: string;
|
||||||
description: string;
|
description: string;
|
||||||
fichier: string[];
|
fichier: string;
|
||||||
utilisateur: string;
|
utilisateur: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { ProjectViewModel } from '@app/ui/projects/project.presenter.model';
|
import { ProjectViewModel } from '@app/ui/projects/project.presenter.model';
|
||||||
import { Project } from '@app/domain/projects/project.model';
|
import { Project } from '@app/domain/projects/project.model';
|
||||||
|
import { environment } from '@env/environment';
|
||||||
|
|
||||||
export class ProjectPresenter {
|
export class ProjectPresenter {
|
||||||
toViewModel(project: Project): ProjectViewModel {
|
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 {
|
return {
|
||||||
id: project.id,
|
id: project.id,
|
||||||
created: project.created,
|
created: project.created,
|
||||||
updated: project.updated,
|
updated: project.updated,
|
||||||
nom: project.nom,
|
nom: project.nom,
|
||||||
lien: project.lien,
|
lien: project.lien,
|
||||||
description: project.description,
|
description: project.description ?? 'Aucune description disponible.',
|
||||||
fichier: project.fichier,
|
fichier,
|
||||||
utilisateur: project.utilisateur,
|
utilisateur: project.utilisateur,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -18,4 +22,6 @@ export class ProjectPresenter {
|
|||||||
toViewModels(projects: Project[]): ProjectViewModel[] {
|
toViewModels(projects: Project[]): ProjectViewModel[] {
|
||||||
return projects.map(this.toViewModel);
|
return projects.map(this.toViewModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatFichier(project: Project) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Injectable, signal } from '@angular/core';
|
import { Injectable, signal } from '@angular/core';
|
||||||
|
import imageCompression from 'browser-image-compression';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class FileManagerService {
|
export class FileManagerService {
|
||||||
file = signal<File | null>(null);
|
file = signal<File | null>(null);
|
||||||
imagePreviewUrl = signal<string | null>(null);
|
imagePreviewUrl = signal<string | null>(null);
|
||||||
fileError = signal<string | null>(null);
|
fileError = signal<string | null>(null);
|
||||||
|
isCompressing = signal<boolean>(false);
|
||||||
|
|
||||||
private readonly MAX_FILE_SIZE = 5 * 1024 * 1024; // 5Mo en bytes
|
private readonly MAX_FILE_SIZE = 5 * 1024 * 1024; // 5Mo en bytes
|
||||||
private readonly ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
|
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];
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
onPictureChange($event: Event): void {
|
async onPictureChange($event: Event): Promise<void> {
|
||||||
const target = $event.target as HTMLInputElement;
|
const target = $event.target as HTMLInputElement;
|
||||||
const selectedFile = target?.files?.[0];
|
const selectedFile = target?.files?.[0];
|
||||||
|
|
||||||
@@ -58,8 +60,30 @@ export class FileManagerService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.file.set(selectedFile);
|
// Configuration de la compression
|
||||||
this.readFile(selectedFile);
|
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 {
|
onFileChange($event: Event): void {
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
import { UserViewModel } from '@app/ui/users/user.presenter.model';
|
import { UserViewModel } from '@app/ui/users/user.presenter.model';
|
||||||
import { User } from '@app/domain/users/user.model';
|
import { User } from '@app/domain/users/user.model';
|
||||||
|
import { environment } from '@env/environment';
|
||||||
|
|
||||||
export class UserPresenter {
|
export class UserPresenter {
|
||||||
|
private DEFAULT_VALUE = 'Non renséigné';
|
||||||
toViewModel(user: User): UserViewModel {
|
toViewModel(user: User): UserViewModel {
|
||||||
const slug = user.name
|
const slug = user.name
|
||||||
? this.generateProfileSlug(user.name, user.id)
|
? 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 = {
|
let userViewModel: UserViewModel = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
username: user.username,
|
username: user.username ?? this.DEFAULT_VALUE,
|
||||||
verified: user.verified,
|
verified: user.verified,
|
||||||
emailVisibility: user.emailVisibility,
|
emailVisibility: user.emailVisibility,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
name: user.name,
|
name: user.name ? user.name : this.DEFAULT_VALUE,
|
||||||
avatar: user.avatar,
|
avatar,
|
||||||
slug,
|
slug,
|
||||||
isUserVisible: this.isUserVisible(user),
|
isUserVisible: this.isUserVisible(user),
|
||||||
missingFields: this.missingFields(user),
|
missingFields: this.missingFields(user),
|
||||||
|
|||||||
Reference in New Issue
Block a user