composant recherche et filtre disponible dans toute l'application
This commit is contained in:
12
src/app/domain/search/search.repository.ts
Normal file
12
src/app/domain/search/search.repository.ts
Normal 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>;
|
||||
}
|
||||
57
src/app/infrastructure/search/search.service.ts
Normal file
57
src/app/infrastructure/search/search.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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 } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user