project refactoring en clean archi

This commit is contained in:
styve Lioumba
2025-10-23 14:10:53 +02:00
parent ef02c6a537
commit 02637235e3
52 changed files with 3873 additions and 875 deletions

View File

@@ -1,8 +1,8 @@
@if (project) {
@if (project()) {
<div class="bg-white rounded-2xl border p-6 max-w-sm">
<div class="">
<h3 class="text-lg font-bold text-gray-800 mb-3">{{ project.nom }}</h3>
<p class="text-gray-800 text-sm">{{ project.description }}</p>
<h3 class="text-lg font-bold text-gray-800 mb-3">{{ project().nom }}</h3>
<p class="text-gray-800 text-sm">{{ project().description }}</p>
<div class="mt-6">
<a
[routerLink]="[]"

View File

@@ -1,9 +1,8 @@
import { Component, inject, Input, OnInit } from '@angular/core';
import { AuthService } from '@app/core/services/authentication/auth.service';
import { ProjectService } from '@app/core/services/project/project.service';
import { Project } from '@app/shared/models/project';
import { environment } from '@env/environment';
import { RouterLink } from '@angular/router';
import { ProjectFacade } from '@app/ui/projects/project.facade';
@Component({
selector: 'app-my-profile-project-item',
@@ -17,11 +16,10 @@ export class MyProfileProjectItemComponent implements OnInit {
@Input({ required: true }) projectId = '';
protected authService = inject(AuthService);
protected projectService = inject(ProjectService);
protected project: Project | undefined = undefined;
private readonly projectFacade = new ProjectFacade();
protected project = this.projectFacade.project;
ngOnInit(): void {
this.projectService.getProjectById(this.projectId).subscribe((value) => (this.project = value));
this.projectFacade.loadOne(this.projectId);
}
}

View File

@@ -2,7 +2,7 @@
<div class="max-w-4xl max-lg:max-w-2xl max-sm:max-w-sm mx-auto">
<h2 class="text-2xl font-bold text-gray-800 mb-8">Mes projets</h2>
@if (projects) {
@if (projects()) {
<div class="relative flex items-center">
<select
[(ngModel)]="projectIdSelected"
@@ -12,7 +12,7 @@
<option [value]="'add'.toLowerCase()">Ajouter un nouveau projet</option>
@for (project of projects; track project.id) {
@for (project of projects(); track project.id) {
<option [value]="project.id">
{{ project.nom }}
</option>

View File

@@ -1,24 +1,14 @@
import { Component, inject, Input, OnInit, signal } from '@angular/core';
import { MyProfileProjectItemComponent } from '@app/shared/components/my-profile-project-item/my-profile-project-item.component';
import { Component, Input, OnInit, signal } from '@angular/core';
import { PaginatorModule } from 'primeng/paginator';
import { ReactiveFormsModule } from '@angular/forms';
import { ProjectService } from '@app/core/services/project/project.service';
import { AsyncPipe, JsonPipe } from '@angular/common';
import { Project } from '@app/shared/models/project';
import { UntilDestroy } from '@ngneat/until-destroy';
import { MyProfileUpdateProjectFormComponent } from '@app/shared/components/my-profile-update-project-form/my-profile-update-project-form.component';
import { ProjectFacade } from '@app/ui/projects/project.facade';
@Component({
selector: 'app-my-profile-project-list',
standalone: true,
imports: [
MyProfileProjectItemComponent,
PaginatorModule,
ReactiveFormsModule,
AsyncPipe,
JsonPipe,
MyProfileUpdateProjectFormComponent,
],
imports: [PaginatorModule, ReactiveFormsModule, MyProfileUpdateProjectFormComponent],
templateUrl: './my-profile-project-list.component.html',
styleUrl: './my-profile-project-list.component.scss',
})
@@ -26,15 +16,14 @@ import { MyProfileUpdateProjectFormComponent } from '@app/shared/components/my-p
export class MyProfileProjectListComponent implements OnInit {
@Input({ required: true }) projectIds: string[] = [];
@Input({ required: true }) userId = '';
protected projectService = inject(ProjectService);
protected projectIdSelected = signal<string | null>(null);
protected projects: Project[] = [];
private readonly projectFacade = new ProjectFacade();
protected projects = this.projectFacade.projects;
ngOnInit(): void {
this.projectService
.getProjectByUserId(this.userId)
.subscribe((value) => (this.projects = value));
this.projectFacade.load(this.userId);
}
onProjectFormSubmitted($event: string | null) {

View File

@@ -2,88 +2,97 @@
@if (projectId == 'add'.toLowerCase()) {
<app-project-picture-form [project]="undefined" />
} @else {
<app-project-picture-form [project]="project" />
<app-project-picture-form [project]="project()" />
}
<h3 class="font-ubuntu w-full text-start font-bold text-xl uppercase dark:text-white my-5">
Information du projet
</h3>
<form class="mx-8" [formGroup]="projectForm" (ngSubmit)="onSubmit()">
<label class="mb-2 text-sm text-black block dark:text-white">Nom</label>
<div class="relative flex items-center">
<input
type="text"
placeholder="nom du projet"
formControlName="nom"
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"
/>
@if (loading().isLoading) {
@switch (loading().action) {
@case (ActionType.NONE || ActionType.CREATE || ActionType.DELETE) {}
@default {
<p>Chargement...</p>
}
}
} @else {
<form class="mx-8" [formGroup]="projectForm" (ngSubmit)="onSubmit()">
<label class="mb-2 text-sm text-black block dark:text-white">Nom</label>
<div class="relative flex items-center">
<input
type="text"
placeholder="nom du projet"
formControlName="nom"
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"
/>
<div class="absolute left-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="22px"
height="22px"
fill="#bbb"
viewBox="0 0 512 512"
>
<path
d="M437.02 74.981C388.667 26.629 324.38 0 256 0S123.333 26.629 74.98 74.981C26.629 123.333 0 187.62 0 256s26.629 132.667 74.98 181.019C123.333 485.371 187.62 512 256 512s132.667-26.629 181.02-74.981C485.371 388.667 512 324.38 512 256s-26.629-132.667-74.98-181.019zM256 482c-66.869 0-127.037-29.202-168.452-75.511C113.223 338.422 178.948 290 256 290c-49.706 0-90-40.294-90-90s40.294-90 90-90 90 40.294 90 90-40.294 90-90 90c77.052 0 142.777 48.422 168.452 116.489C383.037 452.798 322.869 482 256 482z"
data-original="#000000"
></path>
</svg>
<div class="absolute left-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="22px"
height="22px"
fill="#bbb"
viewBox="0 0 512 512"
>
<path
d="M437.02 74.981C388.667 26.629 324.38 0 256 0S123.333 26.629 74.98 74.981C26.629 123.333 0 187.62 0 256s26.629 132.667 74.98 181.019C123.333 485.371 187.62 512 256 512s132.667-26.629 181.02-74.981C485.371 388.667 512 324.38 512 256s-26.629-132.667-74.98-181.019zM256 482c-66.869 0-127.037-29.202-168.452-75.511C113.223 338.422 178.948 290 256 290c-49.706 0-90-40.294-90-90s40.294-90 90-90 90 40.294 90 90-40.294 90-90 90c77.052 0 142.777 48.422 168.452 116.489C383.037 452.798 322.869 482 256 482z"
data-original="#000000"
></path>
</svg>
</div>
</div>
</div>
<label class="mb-2 text-sm text-black block dark:text-white">Lien</label>
<div class="relative flex items-center">
<input
type="text"
placeholder="lien vers votre projet ex : http://monprojet"
formControlName="lien"
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"
/>
<label class="mb-2 text-sm text-black block dark:text-white">Lien</label>
<div class="relative flex items-center">
<input
type="text"
placeholder="lien vers votre projet ex : http://monprojet"
formControlName="lien"
class="pr-4 pl-14 py-3 text-sm text-black rounded bg-white border border-gray-400 w-full outline-[#333]"
/>
<div class="absolute left-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="22px"
height="22px"
fill="#bbb"
viewBox="0 0 512 512"
>
<path
d="M437.02 74.981C388.667 26.629 324.38 0 256 0S123.333 26.629 74.98 74.981C26.629 123.333 0 187.62 0 256s26.629 132.667 74.98 181.019C123.333 485.371 187.62 512 256 512s132.667-26.629 181.02-74.981C485.371 388.667 512 324.38 512 256s-26.629-132.667-74.98-181.019zM256 482c-66.869 0-127.037-29.202-168.452-75.511C113.223 338.422 178.948 290 256 290c-49.706 0-90-40.294-90-90s40.294-90 90-90 90 40.294 90 90-40.294 90-90 90c77.052 0 142.777 48.422 168.452 116.489C383.037 452.798 322.869 482 256 482z"
data-original="#000000"
></path>
</svg>
<div class="absolute left-4">
<svg
xmlns="http://www.w3.org/2000/svg"
width="22px"
height="22px"
fill="#bbb"
viewBox="0 0 512 512"
>
<path
d="M437.02 74.981C388.667 26.629 324.38 0 256 0S123.333 26.629 74.98 74.981C26.629 123.333 0 187.62 0 256s26.629 132.667 74.98 181.019C123.333 485.371 187.62 512 256 512s132.667-26.629 181.02-74.981C485.371 388.667 512 324.38 512 256s-26.629-132.667-74.98-181.019zM256 482c-66.869 0-127.037-29.202-168.452-75.511C113.223 338.422 178.948 290 256 290c-49.706 0-90-40.294-90-90s40.294-90 90-90 90 40.294 90 90-40.294 90-90 90c77.052 0 142.777 48.422 168.452 116.489C383.037 452.798 322.869 482 256 482z"
data-original="#000000"
></path>
</svg>
</div>
</div>
</div>
<label class="mb-2 text-sm text-black block dark:text-white">description</label>
<div class="relative flex items-center">
<textarea
placeholder="Type Message"
formControlName="description"
class="p-4 bg-white w-full block text-sm border border-gray-300 outline-[#007bff] rounded"
rows="4"
></textarea>
</div>
<label class="mb-2 text-sm text-black block dark:text-white">description</label>
<div class="relative flex items-center">
<textarea
placeholder="Type Message"
formControlName="description"
class="p-4 bg-white w-full block text-sm border border-gray-300 outline-[#007bff] rounded"
rows="4"
></textarea>
</div>
<button
type="submit"
[ngClass]="{ 'bg-purple-600': projectForm.valid }"
class="!mt-8 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block"
>
Sauvegarder
</button>
<button
type="submit"
[ngClass]="{ 'bg-purple-600': projectForm.valid }"
class="!mt-8 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block"
>
Sauvegarder
</button>
<button
type="button"
(click)="formIsUpdated.emit(null)"
class="!mt-8 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block"
>
Annuler
</button>
</form>
<button
type="button"
(click)="formIsUpdated.emit(null)"
class="!mt-8 px-6 py-2 w-full bg-[#333] hover:bg-[#444] text-sm text-white mx-auto block"
>
Annuler
</button>
</form>
}
}

View File

@@ -3,7 +3,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyProfileUpdateProjectFormComponent } from './my-profile-update-project-form.component';
import { AuthService } from '@app/core/services/authentication/auth.service';
import { ToastrService } from 'ngx-toastr';
import { ProjectService } from '@app/core/services/project/project.service';
import { provideRouter } from '@angular/router';
import { signal } from '@angular/core';
import { Auth } from '@app/shared/models/auth';
@@ -12,7 +11,6 @@ describe('MyProfileUpdateProjectFormComponent', () => {
let component: MyProfileUpdateProjectFormComponent;
let fixture: ComponentFixture<MyProfileUpdateProjectFormComponent>;
let mockProjectService: Partial<ProjectService>;
let mockAuthService: Partial<AuthService>;
let mockToastrService: Partial<ToastrService>;
@@ -27,15 +25,12 @@ describe('MyProfileUpdateProjectFormComponent', () => {
user: signal<Auth | undefined>(undefined),
};
mockProjectService = {};
await TestBed.configureTestingModule({
imports: [MyProfileUpdateProjectFormComponent],
providers: [
provideRouter([]),
{ provide: AuthService, useValue: mockAuthService },
{ provide: ToastrService, useValue: mockToastrService },
{ provide: ProjectService, useValue: mockProjectService },
],
}).compileComponents();

View File

@@ -1,13 +1,22 @@
import { Component, inject, Input, OnChanges, OnInit, output, SimpleChanges } from '@angular/core';
import {
Component,
effect,
inject,
Input,
OnChanges,
OnInit,
output,
SimpleChanges,
} from '@angular/core';
import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from '@angular/forms';
import { ProjectService } from '@app/core/services/project/project.service';
import { Project } from '@app/shared/models/project';
import { NgClass } from '@angular/common';
import { PaginatorModule } from 'primeng/paginator';
import { ProjectPictureFormComponent } from '@app/shared/components/project-picture-form/project-picture-form.component';
import { ToastrService } from 'ngx-toastr';
import { AuthService } from '@app/core/services/authentication/auth.service';
import { ProjectDto } from '@app/shared/models/project-dto';
import { ProjectFacade } from '@app/ui/projects/project.facade';
import { CreateProjectDto } from '@app/domain/projects/dto/create-project.dto';
import { ActionType } from '@app/domain/action-type.util';
@Component({
selector: 'app-my-profile-update-project-form',
@@ -19,11 +28,15 @@ import { ProjectDto } from '@app/shared/models/project-dto';
export class MyProfileUpdateProjectFormComponent implements OnInit, OnChanges {
@Input({ required: true }) projectId: string | null = null;
protected project: Project | undefined = undefined;
private readonly toastrService = inject(ToastrService);
private readonly authService = inject(AuthService);
protected readonly projectService = inject(ProjectService);
private readonly projectFacade = new ProjectFacade();
protected readonly ActionType = ActionType;
protected readonly project = this.projectFacade.project;
protected readonly loading = this.projectFacade.loading;
protected readonly error = this.projectFacade.error;
private readonly formBuilder = inject(FormBuilder);
protected projectForm = this.formBuilder.group({
@@ -34,6 +47,32 @@ export class MyProfileUpdateProjectFormComponent implements OnInit, OnChanges {
formIsUpdated = output<string | null>();
constructor() {
let message = '';
effect(() => {
if (!this.loading().isLoading) {
switch (this.loading().action) {
case ActionType.CREATE:
message = `Le projet ${this.projectForm.getRawValue().nom} a bien été créer !`;
this.customToast(ActionType.CREATE, message);
break;
case ActionType.UPDATE:
message = `Les informations du projet ${this.projectForm.getRawValue().nom} ont bien été modifier !`;
this.customToast(ActionType.UPDATE, message);
break;
}
if (this.project() !== undefined) {
this.projectForm.setValue({
nom: this.project()!.nom,
description: this.project()!.description,
lien: this.project()!.lien,
});
}
}
});
}
ngOnInit(): void {
if (this.projectId == 'add'.toLowerCase()) {
this.projectForm.setValue({
@@ -44,14 +83,7 @@ export class MyProfileUpdateProjectFormComponent implements OnInit, OnChanges {
}
if (this.projectId != null && this.projectId != 'add'.toLowerCase()) {
this.projectService.getProjectById(this.projectId).subscribe((value) => {
this.project = value;
this.projectForm.setValue({
nom: value.nom,
description: value.description,
lien: value.lien,
});
});
this.projectFacade.loadOne(this.projectId);
}
}
@@ -62,39 +94,15 @@ export class MyProfileUpdateProjectFormComponent implements OnInit, OnChanges {
if (this.projectId != null && this.projectId != 'add'.toLowerCase()) {
// Update
this.projectService
.updateProject(this.project!.id, this.projectForm.getRawValue())
.subscribe((value) => {
this.formIsUpdated.emit(value.id);
this.toastrService.success(
`Les informations du projet ${value.nom} ont bien été modifier !`,
`Mise à jour`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
}
);
});
this.projectFacade.update(this.project()!.id, this.projectForm.getRawValue());
} else {
// Create
const projectDto: ProjectDto = {
const projectDto: CreateProjectDto = {
...this.projectForm.getRawValue(),
utilisateur: this.authService.user()!.record!.id,
} as ProjectDto;
} as CreateProjectDto;
this.projectService.createProject(projectDto).subscribe((value) => {
this.formIsUpdated.emit(value.id);
this.toastrService.success(`Le projet ${value.nom} a bien été créer !`, `Nouveau projet`, {
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
});
});
this.projectFacade.create(projectDto);
}
}
@@ -102,4 +110,30 @@ export class MyProfileUpdateProjectFormComponent implements OnInit, OnChanges {
this.projectId = changes['projectId'].currentValue;
this.ngOnInit();
}
private customToast(action: ActionType, message: string): void {
if (this.error().hasError) {
this.toastrService.error(
`Une erreur s'est produite, veuillez réessayer ulterieurement`,
`Erreur`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
}
);
return;
}
this.formIsUpdated.emit(this.project()!.id);
this.toastrService.success(
`${message}`,
`${action === ActionType.UPDATE ? 'Mise à jour' : 'Nouveau projet'}`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
}
);
}
}

View File

@@ -1,18 +1,16 @@
import { Component, Input } from '@angular/core';
import { Project } from '@app/shared/models/project';
import { JsonPipe } from '@angular/common';
import { environment } from '@env/environment';
import { RouterLink } from '@angular/router';
import { ProjectViewModel } from '@app/ui/projects/project.presenter.model';
@Component({
selector: 'app-project-item',
standalone: true,
imports: [JsonPipe, RouterLink],
imports: [],
templateUrl: './project-item.component.html',
styleUrl: './project-item.component.scss',
})
export class ProjectItemComponent {
protected readonly environment = environment;
@Input({ required: true }) project: Project | undefined = undefined;
@Input({ required: true }) project: ProjectViewModel | undefined = undefined;
}

View File

@@ -2,7 +2,7 @@
<div class="max-w-4xl max-lg:max-w-2xl max-sm:max-w-sm mx-auto">
<h2 class="text-2xl font-bold text-gray-800 mb-8">Explorer les projets</h2>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
@for (project of projects; track project) {
@for (project of projects(); track project) {
<app-project-item [project]="project" />
} @empty {
<p>Aucun projet</p>

View File

@@ -1,14 +1,12 @@
import { Component, inject, Input, OnInit } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { UntilDestroy } from '@ngneat/until-destroy';
import { ProjectItemComponent } from '@app/shared/components/project-item/project-item.component';
import { JsonPipe } from '@angular/common';
import { ProjectService } from '@app/core/services/project/project.service';
import { Project } from '@app/shared/models/project';
import { ProjectFacade } from '@app/ui/projects/project.facade';
@Component({
selector: 'app-project-list',
standalone: true,
imports: [ProjectItemComponent, JsonPipe],
imports: [ProjectItemComponent],
templateUrl: './project-list.component.html',
styleUrl: './project-list.component.scss',
})
@@ -16,13 +14,11 @@ import { Project } from '@app/shared/models/project';
export class ProjectListComponent implements OnInit {
@Input({ required: true }) userProjectId = '';
protected readonly projectService = inject(ProjectService);
private readonly projectFacade = new ProjectFacade();
protected projects: Project[] = [];
protected projects = this.projectFacade.projects;
ngOnInit(): void {
this.projectService
.getProjectByUserId(this.userProjectId)
.subscribe((value) => (this.projects = value));
this.projectFacade.load(this.userProjectId);
}
}

View File

@@ -2,7 +2,6 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ProjectPictureFormComponent } from './project-picture-form.component';
import { provideRouter } from '@angular/router';
import { ProjectService } from '@app/core/services/project/project.service';
import { ToastrService } from 'ngx-toastr';
import { AuthService } from '@app/core/services/authentication/auth.service';
@@ -10,16 +9,10 @@ describe('ProjectPictureFormComponent', () => {
let component: ProjectPictureFormComponent;
let fixture: ComponentFixture<ProjectPictureFormComponent>;
let mockProjectService: Partial<ProjectService>;
let mockToastrService: Partial<ToastrService>;
let mockAuthService: Partial<AuthService>;
beforeEach(async () => {
mockProjectService = {
updateProject: jest.fn().mockReturnValue({
subscribe: jest.fn(),
}),
};
mockToastrService = {
success: jest.fn(),
error: jest.fn(),
@@ -33,7 +26,6 @@ describe('ProjectPictureFormComponent', () => {
imports: [ProjectPictureFormComponent],
providers: [
provideRouter([]),
{ provide: ProjectService, useValue: mockProjectService },
{ provide: ToastrService, useValue: mockToastrService },
{ provide: AuthService, useValue: mockAuthService },
],

View File

@@ -1,10 +1,11 @@
import { Component, inject, Input, output } from '@angular/core';
import { Component, effect, inject, Input, output } from '@angular/core';
import { AuthService } from '@app/core/services/authentication/auth.service';
import { Project } from '@app/shared/models/project';
import { ProjectService } from '@app/core/services/project/project.service';
import { NgClass } from '@angular/common';
import { environment } from '@env/environment';
import { ToastrService } from 'ngx-toastr';
import { ProjectViewModel } from '@app/ui/projects/project.presenter.model';
import { ProjectFacade } from '@app/ui/projects/project.facade';
import { ActionType } from '@app/domain/action-type.util';
@Component({
selector: 'app-project-picture-form',
@@ -14,37 +15,50 @@ import { ToastrService } from 'ngx-toastr';
styleUrl: './project-picture-form.component.scss',
})
export class ProjectPictureFormComponent {
@Input({ required: true }) project: Project | undefined = undefined;
@Input({ required: true }) project: ProjectViewModel | undefined = undefined;
onFormSubmitted = output<any>();
private readonly projectService = inject(ProjectService);
private readonly toastrService = inject(ToastrService);
private readonly projectFacade = new ProjectFacade();
protected readonly loading = this.projectFacade.loading;
protected readonly error = this.projectFacade.error;
private readonly authService = inject(AuthService);
file: File | null = null; // Variable to store file
imagePreviewUrl: string | null = null; // URL for image preview
constructor() {
effect(() => {
if (!this.loading().isLoading) {
switch (this.loading().action) {
case ActionType.UPDATE:
this.authService.updateUser();
this.toastrService.success(
`L'aperçu du projet ${this.project!.nom} ont bien été modifier !`,
`Mise à jour`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
}
);
this.onFormSubmitted.emit('');
break;
}
}
});
}
onSubmit() {
if (this.file != null) {
const formData = new FormData();
formData.append('fichier', this.file); // "fichier" est le nom du champ dans PocketBase
this.projectService.updateProject(this.project?.id!, formData).subscribe((value) => {
this.authService.updateUser();
this.toastrService.success(
`L'aperçu du projet ${value.nom} ont bien été modifier !`,
`Mise à jour`,
{
closeButton: true,
progressAnimation: 'decreasing',
progressBar: true,
}
);
});
this.onFormSubmitted.emit('');
this.projectFacade.update(this.project?.id!, formData);
}
}