From 218603911a86943c557992e713626d1c7b6f9860 Mon Sep 17 00:00:00 2001 From: styve Lioumba Date: Tue, 25 Nov 2025 18:34:38 +0100 Subject: [PATCH] feat : page not found --- src/app/domain/search-filters.ts | 7 + src/app/routes/home/home.component.spec.ts | 28 +- src/app/routes/home/home.component.ts | 8 +- .../routes/not-found/not-found.component.html | 151 ++++++- .../not-found/not-found.component.spec.ts | 2 + .../routes/not-found/not-found.component.ts | 3 +- .../profile-list.component.spec.ts | 12 +- .../profile-list/profile-list.component.ts | 6 +- .../features/filter/filter.component.html | 386 ++++++++++++++++++ .../features/filter/filter.component.scss | 0 .../features/filter/filter.component.spec.ts | 49 +++ .../features/filter/filter.component.ts | 128 ++++++ .../features/search/search.component.html | 105 +++-- .../features/search/search.component.spec.ts | 27 ++ .../features/search/search.component.ts | 22 +- 15 files changed, 877 insertions(+), 57 deletions(-) create mode 100644 src/app/domain/search-filters.ts create mode 100644 src/app/shared/features/filter/filter.component.html create mode 100644 src/app/shared/features/filter/filter.component.scss create mode 100644 src/app/shared/features/filter/filter.component.spec.ts create mode 100644 src/app/shared/features/filter/filter.component.ts diff --git a/src/app/domain/search-filters.ts b/src/app/domain/search-filters.ts new file mode 100644 index 0000000..c1d5224 --- /dev/null +++ b/src/app/domain/search-filters.ts @@ -0,0 +1,7 @@ +export interface SearchFilters { + search?: string; + verified: boolean; + secteur: string | null; + profession: string | null; + sort: string; +} diff --git a/src/app/routes/home/home.component.spec.ts b/src/app/routes/home/home.component.spec.ts index d6c16ef..016aa95 100644 --- a/src/app/routes/home/home.component.spec.ts +++ b/src/app/routes/home/home.component.spec.ts @@ -2,15 +2,41 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { HomeComponent } from './home.component'; import { provideRouter } from '@angular/router'; +import { ProfileRepository } from '@app/domain/profiles/profile.repository'; +import { SectorRepository } from '@app/domain/sectors/sector.repository'; +import { of } from 'rxjs'; +import { Profile } from '@app/domain/profiles/profile.model'; +import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token'; +import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token'; +import { Sector } from '@app/domain/sectors/sector.model'; describe('HomeComponent', () => { let component: HomeComponent; let fixture: ComponentFixture; + let mockProfileRepo: jest.Mocked>; + let mockSectorRepo: jest.Mocked>; + beforeEach(async () => { + mockProfileRepo = { + create: jest.fn().mockReturnValue(of({} as Profile)), + list: jest.fn().mockReturnValue(of([])), + getByUserId: jest.fn().mockReturnValue(of({} as Profile)), + update: jest.fn().mockReturnValue(of({} as Profile)), + }; + + mockSectorRepo = { + list: jest.fn().mockReturnValue(of([])), + getOne: jest.fn().mockReturnValue(of({} as Sector)), + }; + await TestBed.configureTestingModule({ imports: [HomeComponent], - providers: [provideRouter([])], + providers: [ + provideRouter([]), + { provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo }, + { provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepo }, + ], }).compileComponents(); fixture = TestBed.createComponent(HomeComponent); diff --git a/src/app/routes/home/home.component.ts b/src/app/routes/home/home.component.ts index 64ec1e6..a927182 100644 --- a/src/app/routes/home/home.component.ts +++ b/src/app/routes/home/home.component.ts @@ -1,19 +1,19 @@ import { Component, inject } from '@angular/core'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { SearchComponent } from '@app/shared/features/search/search.component'; import { Router } from '@angular/router'; +import { SearchFilters } from '@app/domain/search-filters'; @Component({ selector: 'app-home', standalone: true, - imports: [FaIconComponent, SearchComponent], + imports: [SearchComponent], templateUrl: './home.component.html', styleUrl: './home.component.scss', }) export class HomeComponent { private readonly router = inject(Router); - showNewQuery(newQuery: string) { - this.router.navigate(['/profiles'], { queryParams: { search: newQuery } }); + showNewQuery(filters: SearchFilters) { + this.router.navigate(['/profiles'], { queryParams: { search: filters.search } }); } } diff --git a/src/app/routes/not-found/not-found.component.html b/src/app/routes/not-found/not-found.component.html index 8071020..482f8d7 100644 --- a/src/app/routes/not-found/not-found.component.html +++ b/src/app/routes/not-found/not-found.component.html @@ -1 +1,150 @@ -

not-found works!

+
+
+ +
+ +
+
+
+ + +
+

+ 404 +

+ + +
+ + + +
+
+
+ + +
+

+ Oups ! Page introuvable +

+

+ La page que vous recherchez n'existe pas ou a été déplacée. Retournez à l'accueil ou + explorez nos profils. +

+
+ + + + + +
+
+ + + +
+

Besoin d'aide ?

+

+ Si vous pensez qu'il s'agit d'une erreur, contactez notre équipe support à + + contact@technostrea.fr + +

+
+
+
+
+
diff --git a/src/app/routes/not-found/not-found.component.spec.ts b/src/app/routes/not-found/not-found.component.spec.ts index 989cd30..289e5e3 100644 --- a/src/app/routes/not-found/not-found.component.spec.ts +++ b/src/app/routes/not-found/not-found.component.spec.ts @@ -1,6 +1,7 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { NotFoundComponent } from './not-found.component'; +import { provideRouter } from '@angular/router'; describe('NotFoundComponent', () => { let component: NotFoundComponent; @@ -9,6 +10,7 @@ describe('NotFoundComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ imports: [NotFoundComponent], + providers: [provideRouter([])], }).compileComponents(); fixture = TestBed.createComponent(NotFoundComponent); diff --git a/src/app/routes/not-found/not-found.component.ts b/src/app/routes/not-found/not-found.component.ts index eb1a82a..11997cc 100644 --- a/src/app/routes/not-found/not-found.component.ts +++ b/src/app/routes/not-found/not-found.component.ts @@ -1,9 +1,10 @@ import { Component } from '@angular/core'; +import { RouterLink } from '@angular/router'; @Component({ selector: 'app-not-found', standalone: true, - imports: [], + imports: [RouterLink], templateUrl: './not-found.component.html', styleUrl: './not-found.component.scss', }) diff --git a/src/app/routes/profile/profile-list/profile-list.component.spec.ts b/src/app/routes/profile/profile-list/profile-list.component.spec.ts index fbb7dc8..cc2653d 100644 --- a/src/app/routes/profile/profile-list/profile-list.component.spec.ts +++ b/src/app/routes/profile/profile-list/profile-list.component.spec.ts @@ -6,11 +6,15 @@ import { of } from 'rxjs'; import { ProfileRepository } from '@app/domain/profiles/profile.repository'; import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token'; import { Profile } from '@app/domain/profiles/profile.model'; +import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token'; +import { SectorRepository } from '@app/domain/sectors/sector.repository'; +import { Sector } from '@app/domain/sectors/sector.model'; describe('ProfileListComponent', () => { let component: ProfileListComponent; let fixture: ComponentFixture; - let mockProfileRepository: jest.Mocked; + let mockProfileRepository: jest.Mocked>; + let mockSectorRepository: jest.Mocked>; beforeEach(async () => { mockProfileRepository = { @@ -20,11 +24,17 @@ describe('ProfileListComponent', () => { update: jest.fn().mockReturnValue(of({} as Profile)), }; + mockSectorRepository = { + list: jest.fn().mockReturnValue(of([])), + getOne: jest.fn().mockReturnValue(of({} as Sector)), + }; + await TestBed.configureTestingModule({ imports: [ProfileListComponent], providers: [ provideRouter([]), { provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository }, + { provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepository }, ], }).compileComponents(); diff --git a/src/app/routes/profile/profile-list/profile-list.component.ts b/src/app/routes/profile/profile-list/profile-list.component.ts index e203e01..c0618ec 100644 --- a/src/app/routes/profile/profile-list/profile-list.component.ts +++ b/src/app/routes/profile/profile-list/profile-list.component.ts @@ -5,6 +5,7 @@ import { UntilDestroy } from '@ngneat/until-destroy'; import { ProfileFacade } from '@app/ui/profiles/profile.facade'; import { LoadingComponent } from '@app/shared/components/loading/loading.component'; import { Router } from '@angular/router'; +import { SearchFilters } from '@app/domain/search-filters'; @Component({ selector: 'app-profile-list', @@ -26,7 +27,8 @@ export class ProfileListComponent implements OnInit { this.facade.load(); } - showNewQuery(newQuery: string) { - this.router.navigate(['/profiles'], { queryParams: { search: newQuery } }); + showNewQuery(filters: SearchFilters) { + console.log(filters); + this.router.navigate(['/profiles'], { queryParams: { search: filters.search } }); } } diff --git a/src/app/shared/features/filter/filter.component.html b/src/app/shared/features/filter/filter.component.html new file mode 100644 index 0000000..6d9c35d --- /dev/null +++ b/src/app/shared/features/filter/filter.component.html @@ -0,0 +1,386 @@ + +
+
+

Filtrer les résultats

+ + @if (hasActiveFilters()) { + + } +
+ +
+ + + + + + + + + + + +
+ + + @if (hasActiveFilters()) { +
+ @if (filters.verified) { + + + + + Vérifiés + + + } + @if (filters.secteur) { + + {{ filters.secteur }} + + + } + @if (filters.profession) { + + {{ filters.profession }} + + + } +
+ } +
+ + +
+ + + @if (showSectorDropdown) { +
+ + @for (sector of sectors(); track sector.id) { + + } +
+ } +
+
+ + +
+ + + @if (showProfessionDropdown) { +
+ + @for (profile of profiles(); track profile.id) { + + } +
+ } +
+
+ + +
+ + + @if (showSortDropdown) { +
+ @for (sort of sortOptions; track sort.value) { + + } +
+ } +
+
diff --git a/src/app/shared/features/filter/filter.component.scss b/src/app/shared/features/filter/filter.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/features/filter/filter.component.spec.ts b/src/app/shared/features/filter/filter.component.spec.ts new file mode 100644 index 0000000..abdddc4 --- /dev/null +++ b/src/app/shared/features/filter/filter.component.spec.ts @@ -0,0 +1,49 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FilterComponent } from './filter.component'; +import { SectorRepository } from '@app/domain/sectors/sector.repository'; +import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token'; +import { provideRouter } from '@angular/router'; +import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token'; +import { of } from 'rxjs'; +import { Profile } from '@app/domain/profiles/profile.model'; +import { ProfileRepository } from '@app/domain/profiles/profile.repository'; +import { Sector } from '@app/domain/sectors/sector.model'; + +describe('FilterComponent', () => { + let component: FilterComponent; + let fixture: ComponentFixture; + let mockProfileRepository: jest.Mocked>; + let mockSectorRepository: jest.Mocked>; + + beforeEach(async () => { + mockProfileRepository = { + create: jest.fn().mockReturnValue(of({} as Profile)), + list: jest.fn().mockReturnValue(of([])), + getByUserId: jest.fn().mockReturnValue(of({} as Profile)), + update: jest.fn().mockReturnValue(of({} as Profile)), + }; + + mockSectorRepository = { + list: jest.fn().mockReturnValue(of([])), + getOne: jest.fn().mockReturnValue(of({} as Sector)), + }; + + await TestBed.configureTestingModule({ + imports: [FilterComponent], + providers: [ + provideRouter([]), + { provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepository }, + { provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepository }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(FilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/features/filter/filter.component.ts b/src/app/shared/features/filter/filter.component.ts new file mode 100644 index 0000000..2abe652 --- /dev/null +++ b/src/app/shared/features/filter/filter.component.ts @@ -0,0 +1,128 @@ +import { Component, inject, OnInit, output } from '@angular/core'; +import { SearchFilters } from '@app/domain/search-filters'; +import { SectorFacade } from '@app/ui/sectors/sector.facade'; +import { ProfileFacade } from '@app/ui/profiles/profile.facade'; +import { NgTemplateOutlet } from '@angular/common'; + +interface SortOption { + value: string; + label: string; +} + +@Component({ + selector: 'app-filter', + standalone: true, + imports: [NgTemplateOutlet], + providers: [SectorFacade], + templateUrl: './filter.component.html', + styleUrl: './filter.component.scss', +}) +export class FilterComponent implements OnInit { + filtersChanged = output(); + + // État des dropdowns + showSectorDropdown = false; + showProfessionDropdown = false; + showSortDropdown = false; + + // Filtres + filters: SearchFilters = { + search: '', + verified: false, + secteur: null, + profession: null, + sort: 'recent', + }; + + protected readonly sectorFacade = inject(SectorFacade); + protected readonly sectors = this.sectorFacade.sectors; + protected readonly sectorLoading = this.sectorFacade.loading; + protected readonly sectorError = this.sectorFacade.error; + + protected readonly ProfileFacade = inject(ProfileFacade); + protected readonly profiles = this.ProfileFacade.profiles; + protected readonly profileLoading = this.ProfileFacade.loading; + protected readonly profileError = this.ProfileFacade.error; + + ngOnInit() { + this.sectorFacade.load(); + this.ProfileFacade.load(); + } + + sortOptions: SortOption[] = [ + { value: 'recent', label: 'Récents' }, + { value: 'name-asc', label: 'Nom (A-Z)' }, + { value: 'name-desc', label: 'Nom (Z-A)' }, + { value: 'verified', label: 'Vérifiés' }, + ]; + + get sortLabel(): string { + return this.sortOptions.find((s) => s.value === this.filters.sort)?.label || 'Trier par'; + } + + // Gestion du filtre "Vérifiés" + toggleVerifiedFilter(): void { + this.filters.verified = !this.filters.verified; + this.emitFilters(); + } + + // Gestion des dropdowns + toggleSectorDropdown(): void { + this.showSectorDropdown = !this.showSectorDropdown; + this.showProfessionDropdown = false; + this.showSortDropdown = false; + } + + toggleProfessionDropdown(): void { + this.showProfessionDropdown = !this.showProfessionDropdown; + this.showSectorDropdown = false; + this.showSortDropdown = false; + } + + toggleSortDropdown(): void { + this.showSortDropdown = !this.showSortDropdown; + this.showSectorDropdown = false; + this.showProfessionDropdown = false; + } + + // Sélection des filtres + selectSector(sector: string | null): void { + this.filters.secteur = sector; + this.showSectorDropdown = false; + this.emitFilters(); + } + + selectProfession(profession: string | null): void { + this.filters.profession = profession; + this.showProfessionDropdown = false; + this.emitFilters(); + } + + selectSort(sort: SortOption): void { + this.filters.sort = sort.value; + this.showSortDropdown = false; + this.emitFilters(); + } + + // Vérifier si des filtres sont actifs + hasActiveFilters(): boolean { + return this.filters.verified || !!this.filters.secteur || !!this.filters.profession; + } + + // Effacer tous les filtres + clearAllFilters(): void { + this.filters = { + search: '', + verified: false, + secteur: null, + profession: null, + sort: 'recent', + }; + this.emitFilters(); + } + + // Émettre les changements de filtres + private emitFilters(): void { + this.filtersChanged.emit({ ...this.filters }); + } +} diff --git a/src/app/shared/features/search/search.component.html b/src/app/shared/features/search/search.component.html index 832f1f0..5f57c84 100644 --- a/src/app/shared/features/search/search.component.html +++ b/src/app/shared/features/search/search.component.html @@ -1,43 +1,64 @@ -
-
-
- - -
- @if (searchForm.invalid) { - - } @else { - - } +
+ +
+
- + + + +
+ + +
+
+
+ + + + + +
+ + + +
+
+
diff --git a/src/app/shared/features/search/search.component.spec.ts b/src/app/shared/features/search/search.component.spec.ts index 29557fc..bc1f1b8 100644 --- a/src/app/shared/features/search/search.component.spec.ts +++ b/src/app/shared/features/search/search.component.spec.ts @@ -1,14 +1,41 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { SearchComponent } from './search.component'; +import { provideRouter } from '@angular/router'; +import { PROFILE_REPOSITORY_TOKEN } from '@app/infrastructure/profiles/profile-repository.token'; +import { SECTOR_REPOSITORY_TOKEN } from '@app/infrastructure/sectors/sector-repository.token'; +import { of } from 'rxjs'; +import { Profile } from '@app/domain/profiles/profile.model'; +import { ProfileRepository } from '@app/domain/profiles/profile.repository'; +import { SectorRepository } from '@app/domain/sectors/sector.repository'; +import { Sector } from '@app/domain/sectors/sector.model'; describe('SearchComponent', () => { let component: SearchComponent; let fixture: ComponentFixture; + let mockProfileRepo: jest.Mocked>; + let mockSectorRepo: jest.Mocked>; + beforeEach(async () => { + mockProfileRepo = { + create: jest.fn().mockReturnValue(of({} as Profile)), + list: jest.fn().mockReturnValue(of([])), + getByUserId: jest.fn().mockReturnValue(of({} as Profile)), + update: jest.fn().mockReturnValue(of({} as Profile)), + }; + + mockSectorRepo = { + list: jest.fn().mockReturnValue(of([])), + getOne: jest.fn().mockReturnValue(of({} as Sector)), + }; await TestBed.configureTestingModule({ imports: [SearchComponent], + providers: [ + provideRouter([]), + { provide: PROFILE_REPOSITORY_TOKEN, useValue: mockProfileRepo }, + { provide: SECTOR_REPOSITORY_TOKEN, useValue: mockSectorRepo }, + ], }).compileComponents(); fixture = TestBed.createComponent(SearchComponent); diff --git a/src/app/shared/features/search/search.component.ts b/src/app/shared/features/search/search.component.ts index fa4085f..2cf2141 100644 --- a/src/app/shared/features/search/search.component.ts +++ b/src/app/shared/features/search/search.component.ts @@ -1,27 +1,39 @@ import { Component, inject, output } from '@angular/core'; import { FormBuilder, FormControl, ReactiveFormsModule, Validators } from '@angular/forms'; +import { FilterComponent } from '@app/shared/features/filter/filter.component'; +import { SearchFilters } from '@app/domain/search-filters'; +import { NgTemplateOutlet } from '@angular/common'; @Component({ selector: 'app-search', standalone: true, - imports: [ReactiveFormsModule], + imports: [ReactiveFormsModule, FilterComponent, NgTemplateOutlet], templateUrl: './search.component.html', styleUrl: './search.component.scss', }) export class SearchComponent { - onSearchChange = output(); + onSearchChange = output(); private formBuilder: FormBuilder = inject(FormBuilder); + // Filtres + filters: SearchFilters = { + search: '', + verified: false, + secteur: null, + profession: null, + sort: 'recent', + }; + searchForm = this.formBuilder.group({ search: new FormControl('', Validators.required), }); onSubmit() { const search = this.searchForm.value.search?.toLowerCase()!; - this.setNewName(search); + this.onSearchChange.emit({ ...this.filters, search }); } - setNewName(newName: string) { - this.onSearchChange.emit(newName); + onFiltersChanged(event: SearchFilters) { + this.filters = event; } }