composant recherche et filtre disponible dans toute l'application

This commit is contained in:
styve Lioumba
2025-11-28 11:21:39 +01:00
parent a645919aa4
commit 4716e82628
9 changed files with 128 additions and 87 deletions

View File

@@ -0,0 +1,12 @@
import { SearchFilters } from '@app/domain/search/search-filters';
import { WritableSignal } from '@angular/core';
export interface SearchRepository {
search(query: string): void;
sortBy(value: string): void;
filterByProfileVerified(): void;
filterBySecteur(secteur: string): void;
filterByProfession(profession: string): void;
reset(): void;
getFilters(): WritableSignal<SearchFilters>;
}

View File

@@ -0,0 +1,57 @@
import { Injectable, signal, WritableSignal } from '@angular/core';
import { SearchRepository } from '@app/domain/search/search.repository';
import { SearchFilters } from '@app/domain/search/search-filters';
@Injectable({
providedIn: 'root',
})
export class SearchService implements SearchRepository {
private filters = signal<SearchFilters>({
search: '',
verified: false,
secteur: null,
profession: null,
sort: 'recent',
});
filterByProfession(profession: string | null) {
const filter = { ...this.filters(), profession };
this.filters.set(filter);
}
filterByProfileVerified() {
const filters = { ...this.filters(), verified: true };
this.filters.set(filters);
}
filterBySecteur(secteur: string | null) {
const filters = { ...this.filters(), secteur };
this.filters.set(filters);
}
getFilters(): WritableSignal<SearchFilters> {
return this.filters;
}
reset() {
const filters = {
...this.filters(),
verified: false,
sort: 'recent',
search: '',
profession: null,
secteur: null,
};
this.filters.set(filters);
}
search(query: string) {
const filters = { ...this.filters(), search: query };
this.filters.set(filters);
}
sortBy(value: string) {
const filters = { ...this.filters(), sort: value };
this.filters.set(filters);
}
}

View File

@@ -1,7 +1,7 @@
import { Component, inject } from '@angular/core';
import { SearchComponent } from '@app/shared/features/search/search.component';
import { Router } from '@angular/router';
import { SearchFilters } from '@app/domain/search-filters';
import { SearchFilters } from '@app/domain/search/search-filters';
@Component({
selector: 'app-home',

View File

@@ -5,7 +5,8 @@ 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';
import { SearchFilters } from '@app/domain/search/search-filters';
import { SearchService } from '@app/infrastructure/search/search.service';
@Component({
selector: 'app-profile-list',
@@ -16,6 +17,7 @@ import { SearchFilters } from '@app/domain/search-filters';
})
@UntilDestroy()
export class ProfileListComponent implements OnInit {
private readonly searchService = inject(SearchService);
private readonly facade = inject(ProfileFacade);
private readonly router = inject(Router);
@@ -23,12 +25,13 @@ export class ProfileListComponent implements OnInit {
protected readonly loading = this.facade.loading;
protected readonly error = this.facade.error;
protected readonly searchFilters = this.searchService.getFilters();
ngOnInit() {
this.facade.load();
}
showNewQuery(filters: SearchFilters) {
console.log(filters);
this.router.navigate(['/profiles'], { queryParams: { search: filters.search } });
}
}

View File

@@ -29,24 +29,24 @@
<button
type="button"
(click)="toggleVerifiedFilter()"
[class.ring-2]="filters.verified"
[class.ring-purple-500]="filters.verified"
[class.ring-2]="filters().verified"
[class.ring-purple-500]="filters().verified"
class="group relative flex items-center justify-between gap-3 px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-700 hover:border-purple-500 dark:hover:border-purple-400 transition-all bg-white dark:bg-gray-800"
>
<div class="flex items-center gap-2">
<div
class="flex-shrink-0 w-5 h-5 rounded-full flex items-center justify-center"
[class.bg-purple-100]="filters.verified"
[class.dark:bg-purple-900]="filters.verified"
[class.bg-purple-100]="filters().verified"
[class.dark:bg-purple-900]="filters().verified"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-4 h-4"
[class.text-purple-600]="filters.verified"
[class.dark:text-purple-400]="filters.verified"
[class.text-gray-400]="!filters.verified"
[class.text-purple-600]="filters().verified"
[class.dark:text-purple-400]="filters().verified"
[class.text-gray-400]="!filters().verified"
>
<path
fill-rule="evenodd"
@@ -58,7 +58,7 @@
<span class="text-sm font-medium text-gray-700 dark:text-gray-300"> Profils vérifiés </span>
</div>
@if (filters.verified) {
@if (filters().verified) {
<div
class="flex-shrink-0 w-5 h-5 rounded-full bg-purple-600 text-white flex items-center justify-center"
>
@@ -91,7 +91,7 @@
<!-- Tags des filtres actifs -->
@if (hasActiveFilters()) {
<div class="flex flex-wrap gap-2 mt-4">
@if (filters.verified) {
@if (filters().verified) {
<span
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 text-xs font-medium"
>
@@ -126,11 +126,11 @@
</button>
</span>
}
@if (filters.secteur) {
@if (filters().secteur) {
<span
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 text-xs font-medium"
>
{{ filters.secteur }}
{{ filters().secteur }}
<button
type="button"
(click)="selectSector(null)"
@@ -149,11 +149,11 @@
</button>
</span>
}
@if (filters.profession) {
@if (filters().profession) {
<span
class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 text-xs font-medium"
>
{{ filters.profession }}
{{ filters().profession }}
<button
type="button"
(click)="selectProfession(null)"
@@ -181,8 +181,8 @@
<button
type="button"
(click)="toggleSectorDropdown()"
[class.ring-2]="filters.secteur"
[class.ring-purple-500]="filters.secteur"
[class.ring-2]="filters().secteur"
[class.ring-purple-500]="filters().secteur"
class="w-full flex items-center justify-between gap-3 px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-700 hover:border-purple-500 dark:hover:border-purple-400 transition-all bg-white dark:bg-gray-800"
>
<div class="flex items-center gap-2">
@@ -191,9 +191,9 @@
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
[class.text-purple-600]="filters.secteur"
[class.dark:text-purple-400]="filters.secteur"
[class.text-gray-400]="!filters.secteur"
[class.text-purple-600]="filters().secteur"
[class.dark:text-purple-400]="filters().secteur"
[class.text-gray-400]="!filters().secteur"
>
<path
fill-rule="evenodd"
@@ -203,7 +203,7 @@
<path d="M16.5 6.5h-1v8.75a1.25 1.25 0 102.5 0V8a1.5 1.5 0 00-1.5-1.5z" />
</svg>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ filters.secteur || 'Secteur' }}
{{ filters().secteur || 'Secteur' }}
</span>
</div>
<svg
@@ -229,8 +229,8 @@
type="button"
(click)="selectSector(null)"
class="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
[class.bg-purple-50]="!filters.secteur"
[class.dark:bg-purple-900]="!filters.secteur"
[class.bg-purple-50]="!filters().secteur"
[class.dark:bg-purple-900]="!filters().secteur"
>
<span class="text-gray-700 dark:text-gray-300">Tous les secteurs</span>
</button>
@@ -239,8 +239,8 @@
type="button"
(click)="selectSector(sector.nom)"
class="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
[class.bg-purple-50]="filters.secteur === sector.nom"
[class.dark:bg-purple-900]="filters.secteur === sector.nom"
[class.bg-purple-50]="filters().secteur === sector.nom"
[class.dark:bg-purple-900]="filters().secteur === sector.nom"
>
<span class="text-gray-700 dark:text-gray-300">{{ sector.nom }}</span>
</button>
@@ -255,8 +255,8 @@
<button
type="button"
(click)="toggleProfessionDropdown()"
[class.ring-2]="filters.profession"
[class.ring-purple-500]="filters.profession"
[class.ring-2]="filters().profession"
[class.ring-purple-500]="filters().profession"
class="w-full flex items-center justify-between gap-3 px-4 py-3 rounded-lg border border-gray-300 dark:border-gray-700 hover:border-purple-500 dark:hover:border-purple-400 transition-all bg-white dark:bg-gray-800"
>
<div class="flex items-center gap-2">
@@ -265,9 +265,9 @@
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
[class.text-purple-600]="filters.profession"
[class.dark:text-purple-400]="filters.profession"
[class.text-gray-400]="!filters.profession"
[class.text-purple-600]="filters().profession"
[class.dark:text-purple-400]="filters().profession"
[class.text-gray-400]="!filters().profession"
>
<path
fill-rule="evenodd"
@@ -279,7 +279,7 @@
/>
</svg>
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
{{ filters.profession || 'Profession' }}
{{ filters().profession || 'Profession' }}
</span>
</div>
<svg
@@ -305,8 +305,8 @@
type="button"
(click)="selectProfession(null)"
class="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
[class.bg-purple-50]="!filters.profession"
[class.dark:bg-purple-900]="!filters.profession"
[class.bg-purple-50]="!filters().profession"
[class.dark:bg-purple-900]="!filters().profession"
>
<span class="text-gray-700 dark:text-gray-300">Toutes les professions</span>
</button>
@@ -315,8 +315,8 @@
type="button"
(click)="selectProfession(profile.profession)"
class="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
[class.bg-purple-50]="filters.profession === profile.profession"
[class.dark:bg-purple-900]="filters.profession === profile.profession"
[class.bg-purple-50]="filters().profession === profile.profession"
[class.dark:bg-purple-900]="filters().profession === profile.profession"
>
<span class="text-gray-700 dark:text-gray-300">{{ profile.profession }}</span>
</button>
@@ -374,8 +374,8 @@
type="button"
(click)="selectSort(sort)"
class="w-full px-4 py-2.5 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
[class.bg-purple-50]="filters.sort === sort.value"
[class.dark:bg-purple-900]="filters.sort === sort.value"
[class.bg-purple-50]="filters().sort === sort.value"
[class.dark:bg-purple-900]="filters().sort === sort.value"
>
<span class="text-gray-700 dark:text-gray-300">{{ sort.label }}</span>
</button>

View File

@@ -1,8 +1,8 @@
import { Component, inject, OnInit, output } from '@angular/core';
import { SearchFilters } from '@app/domain/search-filters';
import { Component, inject, OnInit } from '@angular/core';
import { SectorFacade } from '@app/ui/sectors/sector.facade';
import { ProfileFacade } from '@app/ui/profiles/profile.facade';
import { NgTemplateOutlet } from '@angular/common';
import { SearchService } from '@app/infrastructure/search/search.service';
interface SortOption {
value: string;
@@ -18,7 +18,7 @@ interface SortOption {
styleUrl: './filter.component.scss',
})
export class FilterComponent implements OnInit {
filtersChanged = output<SearchFilters>();
private readonly searchService = inject(SearchService);
// État des dropdowns
showSectorDropdown = false;
@@ -26,13 +26,7 @@ export class FilterComponent implements OnInit {
showSortDropdown = false;
// Filtres
filters: SearchFilters = {
search: '',
verified: false,
secteur: null,
profession: null,
sort: 'recent',
};
filters = this.searchService.getFilters();
protected readonly sectorFacade = inject(SectorFacade);
protected readonly sectors = this.sectorFacade.sectors;
@@ -57,13 +51,12 @@ export class FilterComponent implements OnInit {
];
get sortLabel(): string {
return this.sortOptions.find((s) => s.value === this.filters.sort)?.label || 'Trier par';
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();
this.filters().verified = !this.filters().verified;
}
// Gestion des dropdowns
@@ -87,42 +80,27 @@ export class FilterComponent implements OnInit {
// Sélection des filtres
selectSector(sector: string | null): void {
this.filters.secteur = sector;
this.searchService.filterBySecteur(sector);
this.showSectorDropdown = false;
this.emitFilters();
}
selectProfession(profession: string | null): void {
this.filters.profession = profession;
this.searchService.filterByProfession(profession);
this.showProfessionDropdown = false;
this.emitFilters();
}
selectSort(sort: SortOption): void {
this.filters.sort = sort.value;
this.searchService.sortBy(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;
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 });
this.searchService.reset();
}
}

View File

@@ -1,7 +1,7 @@
<div class="w-full space-y-4 sm:space-y-6 z-[800]">
<!-- Filtres -->
<div>
<app-filter (filtersChanged)="onFiltersChanged($event)" />
<app-filter />
</div>
<!-- Barre de recherche -->

View File

@@ -1,8 +1,9 @@
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 { SearchFilters } from '@app/domain/search/search-filters';
import { NgTemplateOutlet } from '@angular/common';
import { SearchService } from '@app/infrastructure/search/search.service';
@Component({
selector: 'app-search',
@@ -12,28 +13,18 @@ import { NgTemplateOutlet } from '@angular/common';
styleUrl: './search.component.scss',
})
export class SearchComponent {
private readonly searchService = inject(SearchService);
onSearchChange = output<SearchFilters>();
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.onSearchChange.emit({ ...this.filters, search });
}
onFiltersChanged(event: SearchFilters) {
this.filters = event;
this.searchService.search(search);
const filters = this.searchService.getFilters();
this.onSearchChange.emit({ ...filters(), search });
}
}