Reviewed-on: #21 Reviewed-by: technostrea <contact@technostrea.fr>
This commit is contained in:
7
src/app/domain/search-filters.ts
Normal file
7
src/app/domain/search-filters.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface SearchFilters {
|
||||
search?: string;
|
||||
verified: boolean;
|
||||
secteur: string | null;
|
||||
profession: string | null;
|
||||
sort: string;
|
||||
}
|
||||
@@ -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<HomeComponent>;
|
||||
|
||||
let mockProfileRepo: jest.Mocked<Partial<ProfileRepository>>;
|
||||
let mockSectorRepo: jest.Mocked<Partial<SectorRepository>>;
|
||||
|
||||
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);
|
||||
|
||||
@@ -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 } });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,150 @@
|
||||
<p>not-found works!</p>
|
||||
<div class="page-container min-h-[calc(100vh-theme(spacing.32))] flex items-center justify-center">
|
||||
<div class="w-full max-w-2xl mx-auto text-center space-y-8 px-4 sm:px-6 py-12 sm:py-16">
|
||||
<!-- Illustration 404 animée -->
|
||||
<div class="relative">
|
||||
<!-- Cercles décoratifs en arrière-plan -->
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<div
|
||||
class="w-64 h-64 sm:w-80 sm:h-80 bg-purple-100 dark:bg-purple-900/20 rounded-full blur-3xl opacity-50 animate-pulse"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Nombre 404 -->
|
||||
<div class="relative">
|
||||
<h1
|
||||
class="text-8xl sm:text-9xl lg:text-[12rem] font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-purple-600 via-pink-600 to-purple-600 animate-gradient-x drop-shadow-lg"
|
||||
>
|
||||
404
|
||||
</h1>
|
||||
|
||||
<!-- Icône décoratives -->
|
||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-16 h-16 sm:w-24 sm:h-24 text-purple-600/20 dark:text-purple-400/20 animate-bounce"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003zM12 8.25a.75.75 0 01.75.75v3.75a.75.75 0 01-1.5 0V9a.75.75 0 01.75-.75zm0 8.25a.75.75 0 100-1.5.75.75 0 000 1.5z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message d'erreur -->
|
||||
<div class="space-y-3 sm:space-y-4">
|
||||
<h2 class="text-2xl sm:text-3xl lg:text-4xl font-bold text-gray-900 dark:text-white">
|
||||
Oups ! Page introuvable
|
||||
</h2>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-400 max-w-md mx-auto">
|
||||
La page que vous recherchez n'existe pas ou a été déplacée. Retournez à l'accueil ou
|
||||
explorez nos profils.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Suggestions et actions -->
|
||||
<div class="space-y-6">
|
||||
<!-- Boutons d'action -->
|
||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-3 sm:gap-4">
|
||||
<a
|
||||
[routerLink]="['/']"
|
||||
class="group w-full sm:w-auto inline-flex items-center justify-center gap-2 px-6 sm:px-8 py-3 sm:py-3.5 bg-purple-600 hover:bg-purple-700 text-white rounded-lg font-medium text-sm sm:text-base transition-all hover:scale-105 hover:shadow-lg"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5 group-hover:-translate-x-1 transition-transform"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9.293 2.293a1 1 0 011.414 0l7 7A1 1 0 0117 11h-1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-3a1 1 0 00-1-1H9a1 1 0 00-1 1v3a1 1 0 01-1 1H5a1 1 0 01-1-1v-6H3a1 1 0 01-.707-1.707l7-7z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Retour à l'accueil
|
||||
</a>
|
||||
|
||||
<a
|
||||
[routerLink]="['/profiles']"
|
||||
class="group w-full sm:w-auto inline-flex items-center justify-center gap-2 px-6 sm:px-8 py-3 sm:py-3.5 border-2 border-purple-600 dark:border-purple-500 text-purple-600 dark:text-purple-400 hover:bg-purple-50 dark:hover:bg-purple-900/20 rounded-lg font-medium text-sm sm:text-base transition-all hover:scale-105"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5 group-hover:rotate-12 transition-transform"
|
||||
>
|
||||
<path
|
||||
d="M10 9a3 3 0 100-6 3 3 0 000 6zM6 8a2 2 0 11-4 0 2 2 0 014 0zM1.49 15.326a.78.78 0 01-.358-.442 3 3 0 014.308-3.516 6.484 6.484 0 00-1.905 3.959c-.023.222-.014.442.025.654a4.97 4.97 0 01-2.07-.655zM16.44 15.98a4.97 4.97 0 002.07-.654.78.78 0 00.357-.442 3 3 0 00-4.308-3.517 6.484 6.484 0 011.907 3.96 2.32 2.32 0 01-.026.654zM18 8a2 2 0 11-4 0 2 2 0 014 0zM5.304 16.19a.844.844 0 01-.277-.71 5 5 0 019.947 0 .843.843 0 01-.277.71A6.975 6.975 0 0110 18a6.974 6.974 0 01-4.696-1.81z"
|
||||
/>
|
||||
</svg>
|
||||
Explorer les profils
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Liens rapides -->
|
||||
<div class="pt-8 border-t border-gray-200 dark:border-gray-800">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-500 mb-4">Liens rapides :</p>
|
||||
<div class="flex flex-wrap justify-center gap-4 sm:gap-6 text-sm">
|
||||
<a
|
||||
[routerLink]="['/profiles']"
|
||||
class="text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 hover:underline transition-colors"
|
||||
>
|
||||
Profils
|
||||
</a>
|
||||
<span class="text-gray-300 dark:text-gray-700">•</span>
|
||||
<a
|
||||
[routerLink]="['/auth']"
|
||||
class="text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 hover:underline transition-colors"
|
||||
>
|
||||
Se connecter
|
||||
</a>
|
||||
<span class="text-gray-300 dark:text-gray-700">•</span>
|
||||
<a
|
||||
href="mailto:contact@technostrea.fr"
|
||||
class="text-purple-600 dark:text-purple-400 hover:text-purple-700 dark:hover:text-purple-300 hover:underline transition-colors"
|
||||
>
|
||||
Support
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Message d'aide -->
|
||||
<div class="pt-6">
|
||||
<div
|
||||
class="inline-flex items-start gap-3 p-4 rounded-lg bg-gray-50 dark:bg-gray-800 text-left max-w-md"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5 text-purple-600 dark:text-purple-400 flex-shrink-0 mt-0.5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a.75.75 0 000 1.5h.253a.25.25 0 01.244.304l-.459 2.066A1.75 1.75 0 0010.747 15H11a.75.75 0 000-1.5h-.253a.25.25 0 01-.244-.304l.459-2.066A1.75 1.75 0 009.253 9H9z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white mb-1">Besoin d'aide ?</p>
|
||||
<p class="text-xs text-gray-600 dark:text-gray-400">
|
||||
Si vous pensez qu'il s'agit d'une erreur, contactez notre équipe support à
|
||||
<a
|
||||
href="mailto:contact@technostrea.fr"
|
||||
class="text-purple-600 dark:text-purple-400 hover:underline"
|
||||
>
|
||||
contact@technostrea.fr
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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<ProfileListComponent>;
|
||||
let mockProfileRepository: jest.Mocked<ProfileRepository>;
|
||||
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>>;
|
||||
let mockSectorRepository: jest.Mocked<Partial<SectorRepository>>;
|
||||
|
||||
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();
|
||||
|
||||
|
||||
@@ -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 } });
|
||||
}
|
||||
}
|
||||
|
||||
386
src/app/shared/features/filter/filter.component.html
Normal file
386
src/app/shared/features/filter/filter.component.html
Normal file
@@ -0,0 +1,386 @@
|
||||
<!-- Filtres -->
|
||||
<div class="w-full">
|
||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4">
|
||||
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">Filtrer les résultats</h3>
|
||||
|
||||
@if (hasActiveFilters()) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="clearAllFilters()"
|
||||
class="text-sm text-purple-600 hover:text-purple-700 dark:text-purple-400 dark:hover:text-purple-300 font-medium transition-colors flex items-center gap-1"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
Effacer les filtres
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
||||
<!-- Filtre: Profils vérifiés -->
|
||||
<button
|
||||
type="button"
|
||||
(click)="toggleVerifiedFilter()"
|
||||
[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"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8.603 3.799A4.49 4.49 0 0 1 12 2.25c1.357 0 2.573.6 3.397 1.549a4.49 4.49 0 0 1 3.498 1.307 4.491 4.491 0 0 1 1.307 3.497A4.49 4.49 0 0 1 21.75 12a4.49 4.49 0 0 1-1.549 3.397 4.491 4.491 0 0 1-1.307 3.497 4.491 4.491 0 0 1-3.497 1.307A4.49 4.49 0 0 1 12 21.75a4.49 4.49 0 0 1-3.397-1.549 4.49 4.49 0 0 1-3.498-1.306 4.491 4.491 0 0 1-1.307-3.498A4.49 4.49 0 0 1 2.25 12c0-1.357.6-2.573 1.549-3.397a4.49 4.49 0 0 1 1.307-3.497 4.49 4.49 0 0 1 3.497-1.307Zm7.007 6.387a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300"> Profils vérifiés </span>
|
||||
</div>
|
||||
|
||||
@if (filters.verified) {
|
||||
<div
|
||||
class="flex-shrink-0 w-5 h-5 rounded-full bg-purple-600 text-white flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-3 h-3"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
</button>
|
||||
|
||||
<!-- Filtre: Secteur -->
|
||||
<ng-container *ngTemplateOutlet="secteur"></ng-container>
|
||||
|
||||
<!-- Filtre: Profession -->
|
||||
<ng-container *ngTemplateOutlet="profession"></ng-container>
|
||||
|
||||
<!-- Filtre: Tri -->
|
||||
<ng-container *ngTemplateOutlet="tri"></ng-container>
|
||||
</div>
|
||||
|
||||
<!-- Tags des filtres actifs -->
|
||||
@if (hasActiveFilters()) {
|
||||
<div class="flex flex-wrap gap-2 mt-4">
|
||||
@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"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Vérifiés
|
||||
<button
|
||||
type="button"
|
||||
(click)="toggleVerifiedFilter()"
|
||||
class="hover:text-purple-900 dark:hover:text-purple-100"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-3.5 h-3.5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
@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 }}
|
||||
<button
|
||||
type="button"
|
||||
(click)="selectSector(null)"
|
||||
class="hover:text-purple-900 dark:hover:text-purple-100"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-3.5 h-3.5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
@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 }}
|
||||
<button
|
||||
type="button"
|
||||
(click)="selectProfession(null)"
|
||||
class="hover:text-purple-900 dark:hover:text-purple-100"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-3.5 h-3.5"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ng-template #secteur>
|
||||
<div class="relative">
|
||||
<button
|
||||
type="button"
|
||||
(click)="toggleSectorDropdown()"
|
||||
[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">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
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"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2 3.5A1.5 1.5 0 013.5 2h9A1.5 1.5 0 0114 3.5v11.75A2.75 2.75 0 0016.75 18h-12A2.75 2.75 0 012 15.25V3.5zm3.75 7a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-4.5zm0 3a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-4.5zM5 5.75A.75.75 0 015.75 5h4.5a.75.75 0 01.75.75v2.5a.75.75 0 01-.75.75h-4.5A.75.75 0 015 8.25v-2.5z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<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' }}
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5 text-gray-400 transition-transform"
|
||||
[class.rotate-180]="showSectorDropdown"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@if (showSectorDropdown) {
|
||||
<div
|
||||
class="absolute mt-2 w-full bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg shadow-lg max-h-60 overflow-y-auto"
|
||||
>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<span class="text-gray-700 dark:text-gray-300">Tous les secteurs</span>
|
||||
</button>
|
||||
@for (sector of sectors(); track sector.id) {
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ sector.nom }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #profession>
|
||||
<div class="relative">
|
||||
<button
|
||||
type="button"
|
||||
(click)="toggleProfessionDropdown()"
|
||||
[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">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
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"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M6 3.75A2.75 2.75 0 018.75 1h2.5A2.75 2.75 0 0114 3.75v.443c.572.055 1.14.122 1.706.2C17.053 4.582 18 5.75 18 7.07v3.469c0 1.126-.694 2.191-1.83 2.54-1.952.599-4.024.921-6.17.921s-4.219-.322-6.17-.921C2.694 12.73 2 11.665 2 10.539V7.07c0-1.321.947-2.489 2.294-2.676A41.047 41.047 0 016 4.193V3.75zm6.5 0v.325a41.622 41.622 0 00-5 0V3.75c0-.69.56-1.25 1.25-1.25h2.5c.69 0 1.25.56 1.25 1.25zM10 10a1 1 0 00-1 1v.01a1 1 0 001 1h.01a1 1 0 001-1V11a1 1 0 00-1-1H10z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="M3 15.055v-.684c.126.053.255.1.39.142 2.092.642 4.313.987 6.61.987 2.297 0 4.518-.345 6.61-.987.135-.041.264-.089.39-.142v.684c0 1.347-.985 2.53-2.363 2.686a41.454 41.454 0 01-9.274 0C3.985 17.585 3 16.402 3 15.055z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ filters.profession || 'Profession' }}
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5 text-gray-400 transition-transform"
|
||||
[class.rotate-180]="showProfessionDropdown"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@if (showProfessionDropdown) {
|
||||
<div
|
||||
class="absolute z-10 mt-2 w-full bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg shadow-lg max-h-60 overflow-y-auto"
|
||||
>
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<span class="text-gray-700 dark:text-gray-300">Toutes les professions</span>
|
||||
</button>
|
||||
@for (profile of profiles(); track profile.id) {
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ profile.profession }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #tri>
|
||||
<div class="relative">
|
||||
<button
|
||||
type="button"
|
||||
(click)="toggleSortDropdown()"
|
||||
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">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5 text-gray-400"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2.24 6.8a.75.75 0 001.06-.04l1.95-2.1v8.59a.75.75 0 001.5 0V4.66l1.95 2.1a.75.75 0 101.1-1.02l-3.25-3.5a.75.75 0 00-1.1 0L2.2 5.74a.75.75 0 00.04 1.06zm8 6.4a.75.75 0 00-.04 1.06l3.25 3.5a.75.75 0 001.1 0l3.25-3.5a.75.75 0 10-1.1-1.02l-1.95 2.1V6.75a.75.75 0 00-1.5 0v8.59l-1.95-2.1a.75.75 0 00-1.06-.04z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{{ sortLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5 text-gray-400 transition-transform"
|
||||
[class.rotate-180]="showSortDropdown"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
@if (showSortDropdown) {
|
||||
<div
|
||||
class="absolute z-10 mt-2 w-full bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg shadow-lg"
|
||||
>
|
||||
@for (sort of sortOptions; track sort.value) {
|
||||
<button
|
||||
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"
|
||||
>
|
||||
<span class="text-gray-700 dark:text-gray-300">{{ sort.label }}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
49
src/app/shared/features/filter/filter.component.spec.ts
Normal file
49
src/app/shared/features/filter/filter.component.spec.ts
Normal file
@@ -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<FilterComponent>;
|
||||
let mockProfileRepository: jest.Mocked<Partial<ProfileRepository>>;
|
||||
let mockSectorRepository: jest.Mocked<Partial<SectorRepository>>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
128
src/app/shared/features/filter/filter.component.ts
Normal file
128
src/app/shared/features/filter/filter.component.ts
Normal file
@@ -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<SearchFilters>();
|
||||
|
||||
// É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 });
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,38 @@
|
||||
<form class="w-full" [formGroup]="searchForm" (ngSubmit)="onSubmit()">
|
||||
<div class="flex w-full border rounded-full p-2 items-center">
|
||||
<div class="flex-1 flex flex-row items-center">
|
||||
<button class="inline-block w-8 h-8 p-1 text-gray-400">
|
||||
<div class="w-full space-y-4 sm:space-y-6 z-[800]">
|
||||
<!-- Filtres -->
|
||||
<div>
|
||||
<app-filter (filtersChanged)="onFiltersChanged($event)" />
|
||||
</div>
|
||||
|
||||
<!-- Barre de recherche -->
|
||||
<ng-container *ngTemplateOutlet="form"></ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #form>
|
||||
<form [formGroup]="searchForm" (ngSubmit)="onSubmit()" class="w-full">
|
||||
<div class="flex flex-col sm:flex-row gap-3 sm:gap-0 w-full">
|
||||
<div
|
||||
class="flex-1 flex items-center gap-2 sm:gap-3 px-4 py-3 sm:py-2.5 border border-gray-300 dark:border-gray-700 rounded-full sm:rounded-l-full sm:rounded-r-none bg-white dark:bg-gray-800"
|
||||
>
|
||||
<!-- Icône de recherche -->
|
||||
<button
|
||||
type="button"
|
||||
class="flex-shrink-0 w-5 h-5 sm:w-6 sm:h-6 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
aria-label="Icône de recherche"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-full h-full fill-current"
|
||||
class="w-full h-full"
|
||||
viewBox="0 0 512 512"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="M337.509 305.372h-17.501l-6.571-5.486c20.791-25.232 33.922-57.054 33.922-93.257C347.358 127.632 283.896 64 205.135 64 127.452 64 64 127.632 64 206.629s63.452 142.628 142.225 142.628c35.011 0 67.831-13.167 92.991-34.008l6.561 5.487v17.551L415.18 448 448 415.086 337.509 305.372zm-131.284 0c-54.702 0-98.463-43.887-98.463-98.743 0-54.858 43.761-98.742 98.463-98.742 54.7 0 98.462 43.884 98.462 98.742 0 54.856-43.762 98.743-98.462 98.743z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Input de recherche -->
|
||||
<input
|
||||
formControlName="search"
|
||||
type="search"
|
||||
@@ -20,24 +40,25 @@
|
||||
autocorrect="off"
|
||||
autocapitalize="none"
|
||||
spellcheck="false"
|
||||
required=""
|
||||
class="flex-1 focus:ring-0 focus:outline-none placeholder:text-gray-400 bg-transparent text-gray-800 dark:text-white"
|
||||
placeholder="Domaines, activités..."
|
||||
title="Search"
|
||||
role="searchbox"
|
||||
aria-label="Search"
|
||||
aria-controls="typeahead_results"
|
||||
aria-autocomplete="list"
|
||||
class="flex-1 focus:ring-0 focus:outline-none placeholder:text-gray-400 bg-transparent text-gray-800 dark:text-white text-sm sm:text-base"
|
||||
placeholder="Rechercher par nom, prénom..."
|
||||
aria-label="Rechercher un profil"
|
||||
aria-controls="search_results"
|
||||
/>
|
||||
</div>
|
||||
@if (searchForm.invalid) {
|
||||
<button class="w-32 h-12 text-xs rounded-full bg-purple-800 hover:bg-purple-900 text-gray-50">
|
||||
Voir tout
|
||||
|
||||
<!-- Bouton de recherche -->
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full sm:w-auto px-6 sm:px-8 py-3 sm:py-2.5 rounded-full sm:rounded-l-none sm:rounded-r-full bg-purple-600 hover:bg-purple-700 active:bg-purple-800 text-white font-medium text-sm sm:text-base transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<span class="hidden sm:inline">{{
|
||||
searchForm.value!.search! === '' ? 'Voir tout' : 'Rechercher'
|
||||
}}</span>
|
||||
<span class="sm:hidden">{{
|
||||
searchForm.value!.search! === '' ? 'Tout' : 'Rechercher'
|
||||
}}</span>
|
||||
</button>
|
||||
} @else {
|
||||
<button class="w-32 h-12 text-xs rounded-full bg-purple-800 hover:bg-purple-900 text-gray-50">
|
||||
Rechercher
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
</ng-template>
|
||||
|
||||
@@ -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<SearchComponent>;
|
||||
|
||||
let mockProfileRepo: jest.Mocked<Partial<ProfileRepository>>;
|
||||
let mockSectorRepo: jest.Mocked<Partial<SectorRepository>>;
|
||||
|
||||
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);
|
||||
|
||||
@@ -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<string>();
|
||||
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.setNewName(search);
|
||||
this.onSearchChange.emit({ ...this.filters, search });
|
||||
}
|
||||
|
||||
setNewName(newName: string) {
|
||||
this.onSearchChange.emit(newName);
|
||||
onFiltersChanged(event: SearchFilters) {
|
||||
this.filters = event;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user