From 74dc3a7c4f6e5c2280f3864f6b73e42564347683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20BRISON?= <ext.sopra.jbrison@grandlyon.com> Date: Tue, 10 Nov 2020 15:41:51 +0100 Subject: [PATCH] (fix): Topic search --- api/server.js | 124 ++++++++++++------ src/app/home/home.component.ts | 25 ++++ src/app/services/structure-list.service.ts | 29 +--- .../modal-filter/modal-filter.component.html | 10 +- .../modal-filter/modal-filter.component.scss | 51 +++++-- .../modal-filter.component.spec.ts | 46 ++++--- .../modal-filter/modal-filter.component.ts | 20 ++- .../components/search/search.component.html | 24 ++-- .../components/search/search.component.scss | 100 ++++++++++---- .../search/search.component.spec.ts | 100 +++++++++++++- .../components/search/search.component.ts | 48 ++++++- src/app/structure-list/models/filter.model.ts | 4 +- .../structure-list/services/search.service.ts | 4 + .../structure-list.component.html | 2 +- .../structure-list.component.scss | 11 +- src/assets/scss/_buttons.scss | 18 ++- src/assets/scss/_color.scss | 2 + src/assets/scss/_icons.scss | 6 +- src/assets/scss/_typography.scss | 19 +++ src/styles.scss | 5 +- 20 files changed, 478 insertions(+), 170 deletions(-) diff --git a/api/server.js b/api/server.js index 4cd6c1318..89878e701 100644 --- a/api/server.js +++ b/api/server.js @@ -13,54 +13,94 @@ server.use(middlewares); server.get('/structures/count', (req, res) => { let structureCountTab = []; // Compétences de base - structureCountTab.push({ id: '260', count: 3 }); - structureCountTab.push({ id: '260', count: 3 }); - structureCountTab.push({ id: '259', count: 3 }); - structureCountTab.push({ id: '261', count: 3 }); - structureCountTab.push({ id: '249', count: 3 }); - structureCountTab.push({ id: '222', count: 2 }); - structureCountTab.push({ id: '212', count: 3 }); - structureCountTab.push({ id: '186', count: 2 }); - structureCountTab.push({ id: '183', count: 2 }); + structureCountTab.push({ id: '260', count: 12 }); + structureCountTab.push({ id: '259', count: 10 }); + structureCountTab.push({ id: '261', count: 10 }); + structureCountTab.push({ id: '249', count: 9 }); + structureCountTab.push({ id: '222', count: 9 }); + structureCountTab.push({ id: '212', count: 8 }); + structureCountTab.push({ id: '186', count: 7 }); + structureCountTab.push({ id: '183', count: 6 }); // Accès aux droits - structureCountTab.push({ id: '176', count: 2 }); + structureCountTab.push({ id: '176', count: 6 }); structureCountTab.push({ id: '175', count: 1 }); - structureCountTab.push({ id: '174', count: 1 }); - structureCountTab.push({ id: '173', count: 1 }); - structureCountTab.push({ id: '172', count: 1 }); - structureCountTab.push({ id: '171', count: 1 }); - structureCountTab.push({ id: '167', count: 1 }); - structureCountTab.push({ id: '165', count: 1 }); + structureCountTab.push({ id: '174', count: 2 }); + structureCountTab.push({ id: '173', count: 2 }); + structureCountTab.push({ id: '172', count: 2 }); + structureCountTab.push({ id: '171', count: 4 }); + structureCountTab.push({ id: '167', count: 3 }); + structureCountTab.push({ id: '165', count: 2 }); // Insertion sociale et professionnelle - structureCountTab.push({ id: '254', count: 2 }); - structureCountTab.push({ id: '240', count: 2 }); - structureCountTab.push({ id: '194', count: 3 }); - structureCountTab.push({ id: '193', count: 3 }); - structureCountTab.push({ id: '192', count: 3 }); - structureCountTab.push({ id: '191', count: 3 }); - structureCountTab.push({ id: '262', count: 3 }); - structureCountTab.push({ id: '263', count: 2 }); - structureCountTab.push({ id: '3', count: 2 }); + structureCountTab.push({ id: '254', count: 5 }); + structureCountTab.push({ id: '240', count: 4 }); + structureCountTab.push({ id: '194', count: 7 }); + structureCountTab.push({ id: '193', count: 7 }); + structureCountTab.push({ id: '192', count: 5 }); + structureCountTab.push({ id: '191', count: 7 }); + structureCountTab.push({ id: '262', count: 5 }); + structureCountTab.push({ id: '263', count: 3 }); + structureCountTab.push({ id: '3', count: 3 }); // Aide à la parentalité - structureCountTab.push({ id: '257', count: 2 }); - structureCountTab.push({ id: '238', count: 2 }); - structureCountTab.push({ id: '178', count: 1 }); - structureCountTab.push({ id: '166', count: 1 }); + structureCountTab.push({ id: '257', count: 4 }); + structureCountTab.push({ id: '238', count: 1 }); + structureCountTab.push({ id: '178', count: 4 }); + structureCountTab.push({ id: '166', count: 2 }); // Culture et sécurité numérique - structureCountTab.push({ id: '264', count: 2 }); - structureCountTab.push({ id: '255', count: 2 }); + structureCountTab.push({ id: '264', count: 5 }); + structureCountTab.push({ id: '255', count: 7 }); structureCountTab.push({ id: '265', count: 2 }); - structureCountTab.push({ id: '232', count: 2 }); - structureCountTab.push({ id: '225', count: 2 }); - structureCountTab.push({ id: '221', count: 2 }); - structureCountTab.push({ id: '218', count: 1 }); - structureCountTab.push({ id: '209', count: 1 }); - structureCountTab.push({ id: '208', count: 1 }); - structureCountTab.push({ id: '206', count: 2 }); - structureCountTab.push({ id: '195', count: 1 }); - structureCountTab.push({ id: '164', count: 1 }); - structureCountTab.push({ id: '163', count: 1 }); - structureCountTab.push({ id: '162', count: 2 }); + structureCountTab.push({ id: '232', count: 4 }); + structureCountTab.push({ id: '225', count: 5 }); + structureCountTab.push({ id: '221', count: 3 }); + structureCountTab.push({ id: '218', count: 2 }); + structureCountTab.push({ id: '209', count: 3 }); + structureCountTab.push({ id: '208', count: 4 }); + structureCountTab.push({ id: '206', count: 5 }); + structureCountTab.push({ id: '195', count: 5 }); + structureCountTab.push({ id: '164', count: 4 }); + structureCountTab.push({ id: '163', count: 2 }); + structureCountTab.push({ id: '162', count: 3 }); + // Accompagnement des démarches + structureCountTab.push({ id: 'Accompagnant CAF', count: 7 }); + structureCountTab.push({ id: 'Pôle Emploi', count: 9 }); + structureCountTab.push({ id: 'CPAM', count: 7 }); + structureCountTab.push({ id: 'Impôts', count: 6 }); + structureCountTab.push({ id: 'Logement', count: 5 }); + structureCountTab.push({ id: 'CARSAT', count: 5 }); + structureCountTab.push({ id: 'Autres', count: 2 }); + // Publics acceptés + structureCountTab.push({ id: 'Tout public', count: 7 }); + structureCountTab.push({ id: 'Moins de 16 ans', count: 4 }); + structureCountTab.push({ id: 'Jeunes (16-25 ans)', count: 6 }); + structureCountTab.push({ id: 'Adultes', count: 9 }); + structureCountTab.push({ id: 'Séniors (+ de 65 ans)', count: 1 }); + // Labels et qualifications + structureCountTab.push({ id: 'Aidants Connect', count: 0 }); + structureCountTab.push({ id: 'Espace public numérique (EPN)', count: 2 }); + structureCountTab.push({ id: 'Fabrique de territoire', count: 3 }); + structureCountTab.push({ id: 'Maison France Service', count: 0 }); + structureCountTab.push({ id: 'Pass numérique', count: 4 }); + // Modalités d'accès + structureCountTab.push({ id: 'Uniquement sur RDV', count: 13 }); + structureCountTab.push({ id: 'Accès libre', count: 6 }); + structureCountTab.push({ id: 'Téléphone / Visio', count: 6 }); + // Accompagnement des publics + structureCountTab.push({ id: "Personnes en situation d'illetrisme", count: 0 }); + structureCountTab.push({ id: 'Langue étrangère (anglais)', count: 0 }); + structureCountTab.push({ id: 'Langues étrangères (autres)', count: 0 }); + structureCountTab.push({ id: 'Surdité', count: 0 }); + structureCountTab.push({ id: 'Déficience visuelle', count: 0 }); + structureCountTab.push({ id: 'Handicap moteur', count: 0 }); + // Équipements et services proposés + structureCountTab.push({ id: 'Wifi en accès libre', count: 6 }); + structureCountTab.push({ id: 'Ordinateurs', count: 5 }); + structureCountTab.push({ id: 'Tablettes', count: 1 }); + structureCountTab.push({ id: 'Bornes numériques', count: 1 }); + structureCountTab.push({ id: 'Imprimantes', count: 5 }); + structureCountTab.push({ id: 'Prêt / don de matériels', count: 0 }); + structureCountTab.push({ id: 'Reconditionnements de matériel', count: 0 }); + structureCountTab.push({ id: 'Accès à des revues ou livres informatiques et numériques', count: 0 }); + return res.status(200).jsonp(structureCountTab); }); diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 54325dc33..2a1d85c1d 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -32,6 +32,9 @@ export class HomeComponent implements OnInit { public getStructures(filters: Filter[]): void { this.structureService.getStructures(filters).subscribe((structures) => { + console.log(filters); + filters ? (structures = this.applyFilters(structures, filters)) : structures; + Promise.all( structures.map((structure) => { if (this.geolocation) { @@ -48,6 +51,28 @@ export class HomeComponent implements OnInit { }); } + /** + * Delete when we have back-end + * Fix a bug with Json-server request + */ + private applyFilters(structures, filters): Structure[] { + let structuresFiltered = []; + structures.forEach((s: Structure) => { + let count = 0; + filters.forEach((filter: Filter) => { + let properties: string[] = []; + properties = s[filter.name]; + if (properties && properties.includes(filter.value)) { + count++; + } + }); + if (count === filters.length) { + structuresFiltered.push(s); + } + }); + return structuresFiltered; + } + /** * Get structures positions and add marker corresponding to those positons on the map */ diff --git a/src/app/services/structure-list.service.ts b/src/app/services/structure-list.service.ts index 9f2425166..853256af6 100644 --- a/src/app/services/structure-list.service.ts +++ b/src/app/services/structure-list.service.ts @@ -18,34 +18,7 @@ export class StructureService { constructor(private http: HttpClient) {} public getStructures(filters: Filter[]): Observable<Structure[]> { - return this.http - .get('/api/Structures?' + this.constructSearchRequest(filters)) - .pipe(map((data: any[]) => data.map((item) => new Structure(item)))); - } - - private constructSearchRequest(filters: Filter[]): string { - let requestParam = ''; - if (filters) { - filters.forEach((filter) => { - if (requestParam) { - requestParam = requestParam + '&'; - } - if (filter.isStrict) { - if (requestParam.includes(filter.name)) { - requestParam = requestParam + '=' + filter.value; - } else { - requestParam = requestParam + filter.name + '=' + filter.value; - } - } else { - if (requestParam.includes(filter.name)) { - requestParam = requestParam + filter.value; - } else { - requestParam = requestParam + filter.name + '_like=' + filter.value; - } - } - }); - } - return requestParam; + return this.http.get('/api/Structures').pipe(map((data: any[]) => data.map((item) => new Structure(item)))); } /** diff --git a/src/app/structure-list/components/modal-filter/modal-filter.component.html b/src/app/structure-list/components/modal-filter/modal-filter.component.html index 3acb10b38..e09cabe48 100644 --- a/src/app/structure-list/components/modal-filter/modal-filter.component.html +++ b/src/app/structure-list/components/modal-filter/modal-filter.component.html @@ -1,11 +1,11 @@ -<div fxLayout="column" fxLayoutAlign="space-between" [ngClass]="['modal', 'modal' + getModalType()]"> +<div *ngIf="modalType" fxLayout="column" fxLayoutAlign="space-between" [ngClass]="['modal', 'modal' + getModalType()]"> <div class="body-wrap"> <div class="contentModal" fxLayout="row wrap" fxLayoutAlign="flex-start" *ngIf="categories.length > 0"> <div class="blockFiltre" *ngFor="let c of categories"> <h4>{{ c.name }}</h4> <ul class="blockLigne"> - <div fxLayout="row" class="ligneFiltre" fxLayoutAlign="space-between center" *ngFor="let module of c.modules"> + <div fxLayout="row" class="ligneFiltre" fxLayoutAlign="center" *ngFor="let module of c.modules"> <li class="checkbox"> <div class="checkboxItem"> <label> @@ -13,8 +13,8 @@ type="checkbox" [checked]=" c.name !== 'Équipements et services proposés' - ? getIndex(module.id, c.name) > -1 - : getIndex('True', module.id) > -1 + ? searchService.getIndex(checkedModules, module.id, c.name) > -1 + : searchService.getIndex(checkedModules, 'True', module.id) > -1 " [value]="module.id" (change)=" @@ -34,7 +34,7 @@ </div> </div> <div class="footer" fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="3vw"> - <a (click)="clearFilters()">Effacer</a> + <a tabindex="0" (click)="clearFilters()">Effacer</a> <button type="button" (click)="emitModules(checkedModules)">Appliquer</button> </div> </div> diff --git a/src/app/structure-list/components/modal-filter/modal-filter.component.scss b/src/app/structure-list/components/modal-filter/modal-filter.component.scss index 9f12d9b37..7be709e3a 100644 --- a/src/app/structure-list/components/modal-filter/modal-filter.component.scss +++ b/src/app/structure-list/components/modal-filter/modal-filter.component.scss @@ -29,7 +29,7 @@ margin-top: 3.5px; @include background-hash; ::-webkit-scrollbar { - width: 10px; + width: 16px; } ::-webkit-scrollbar-track { background: $grey-6; @@ -42,14 +42,18 @@ overflow-y: auto; max-width: 1100px; border-bottom: 1px solid $grey; - margin-bottom: 10px; + margin-bottom: 16px; max-height: 438px; .blockFiltre { width: 100%; - padding: 32px 40px 0px 40px; + margin: 0 32px; + padding: 40px 0; min-width: 450px; + border-bottom: 1px dashed $grey-4; + &:last-child { padding-bottom: 32px; + border-bottom: none; } } .blockLigne { @@ -60,7 +64,6 @@ -webkit-column-gap: 46px; column-count: 2; column-gap: 46px; - column-rule: dashed 1px $grey; margin: 0px; @media #{$large-phone} { -moz-column-count: 1; @@ -69,10 +72,13 @@ } } .ligneFiltre { - padding: 5px 0 5px 0; + padding: 0 5px; } h4 { @include cn-bold-14; + line-height: 17px; + text-transform: uppercase; + color: $grey-3; display: flex; align-items: center; margin-top: 0; @@ -80,6 +86,8 @@ } .nbResult { @include cn-regular-14; + line-height: 16px; + color: $grey-3; } label { @include cn-regular-14; @@ -88,16 +96,37 @@ .footer { margin: 0px 20px 16px 0; a { - @include cn-bold-14; - display: flex; - align-items: center; - text-decoration: underline; - color: $secondary-color; + @include btn-search-addStructure; + border: 1px solid transparent; + padding: 8px 8px 6px 8px; + + &:hover { + color: $blue-hover; + } + &:focus { + color: $secondary-color; + border-color: $secondary-color; + border-radius: 4px; + } + &:active { + border: none; + color: $blue-active; + } } height: 32px; button { @include btn-search-filter; - @include cn-bold-14; + &:hover { + background-color: $blue-hover; + } + &:focus { + background-color: $white; + border-color: $secondary-color; + color: $secondary-color; + } + &:active { + background-color: $blue-active; + } } } } diff --git a/src/app/structure-list/components/modal-filter/modal-filter.component.spec.ts b/src/app/structure-list/components/modal-filter/modal-filter.component.spec.ts index cac3dc7a3..113f340a6 100644 --- a/src/app/structure-list/components/modal-filter/modal-filter.component.spec.ts +++ b/src/app/structure-list/components/modal-filter/modal-filter.component.spec.ts @@ -1,7 +1,8 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ReactiveFormsModule } from '@angular/forms'; +import { TypeModal } from '../../enum/typeModal.enum'; import { Category } from '../../models/category.model'; -import { Filter } from '../../models/filter.model'; import { Module } from '../../models/module.model'; import { ModalFilterComponent } from './modal-filter.component'; @@ -13,7 +14,7 @@ describe('ModalFilterComponent', () => { beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [ModalFilterComponent], - imports: [ReactiveFormsModule], + imports: [HttpClientTestingModule, ReactiveFormsModule], }).compileComponents(); }); @@ -27,6 +28,7 @@ describe('ModalFilterComponent', () => { expect(component).toBeTruthy(); }); + // emitModules function it('should emit modules', () => { const modules: Module[] = [ { id: '176', text: 'training', count: 3 }, @@ -38,18 +40,8 @@ describe('ModalFilterComponent', () => { expect(component.searchEvent.emit).toHaveBeenCalled(); expect(component.searchEvent.emit).toHaveBeenCalledWith(modules); }); - it('should return an index or -1', () => { - const modules: Module[] = [ - { id: '176', text: 'training', count: 0 }, - { id: '173', text: 'training', count: 0 }, - { id: '172', text: 'training', count: 0 }, - ]; - component.checkedModules = modules; - const foundItem = component.getIndex('173', 'training'); - const notFoundItem = component.getIndex('189', 'training'); - expect(foundItem).toEqual(1); - expect(notFoundItem).toEqual(-1); - }); + + // onCheckboxChange function it('should add a module to checkedModule array', () => { const modules: Module[] = [ { id: '176', text: 'training', count: 0 }, @@ -58,7 +50,7 @@ describe('ModalFilterComponent', () => { ]; component.checkedModules = modules; const evt = { target: { checked: true, value: '175' } }; - component.onCheckboxChange(evt, 'training'); + component.onCheckboxChange(evt, 'training', false); expect(component.checkedModules.length).toEqual(4); }); it('should remove a module to checkedModule array', () => { @@ -69,9 +61,11 @@ describe('ModalFilterComponent', () => { ]; component.checkedModules = modules; const evt = { target: { checked: false, value: '173' } }; - component.onCheckboxChange(evt, 'training'); + component.onCheckboxChange(evt, 'training', false); expect(component.checkedModules.length).toEqual(2); }); + + // clearFilters function it('should remove all modules checked from same modal, here morefilters', () => { const modules: Module[] = [ { id: '176', text: 'morefilters', count: 0 }, @@ -87,4 +81,24 @@ describe('ModalFilterComponent', () => { component.clearFilters(); expect(component.checkedModules.length).toEqual(3); }); + + // getModalType function + it('should return string of type about current enum', () => { + component.modalType = TypeModal.training; + const resultTraining = component.getModalType(); + component.modalType = TypeModal.accompaniment; + const resultAccopaniment = component.getModalType(); + component.modalType = TypeModal.moreFilters; + const resultMoreFilters = component.getModalType(); + expect(resultTraining).toEqual('training'); + expect(resultMoreFilters).toEqual('moreFilters'); + expect(resultAccopaniment).toEqual(''); + }); + + // closeModal function + it('should emit modules', () => { + spyOn(component.closeEvent, 'emit'); + component.closeModal(); + expect(component.closeEvent.emit).toHaveBeenCalled(); + }); }); diff --git a/src/app/structure-list/components/modal-filter/modal-filter.component.ts b/src/app/structure-list/components/modal-filter/modal-filter.component.ts index 5bab3d8d2..e71cbd329 100644 --- a/src/app/structure-list/components/modal-filter/modal-filter.component.ts +++ b/src/app/structure-list/components/modal-filter/modal-filter.component.ts @@ -3,6 +3,7 @@ import { FormBuilder, FormGroup } from '@angular/forms'; import { TypeModal } from '../../enum/typeModal.enum'; import { Category } from '../../models/category.model'; import { Module } from '../../models/module.model'; +import { SearchService } from '../../services/search.service'; @Component({ selector: 'app-modal-filter', @@ -10,7 +11,7 @@ import { Module } from '../../models/module.model'; styleUrls: ['./modal-filter.component.scss'], }) export class ModalFilterComponent implements OnInit { - constructor(private fb: FormBuilder) { + constructor(private fb: FormBuilder, public searchService: SearchService) { this.searchForm = this.fb.group({ searchTerm: '', }); @@ -21,19 +22,14 @@ export class ModalFilterComponent implements OnInit { @Output() searchEvent = new EventEmitter(); @Output() closeEvent = new EventEmitter(); // Checkbox variable - checkedModules: Module[]; + public checkedModules: Module[] = []; // Form search input - searchForm: FormGroup; + private searchForm: FormGroup; ngOnInit(): void { // Manage checkbox this.checkedModules = this.modules.slice(); } - // Return index of a specific module in array modules - public getIndex(id: string, categ: string): number { - return this.checkedModules.findIndex((m: Module) => m.id === id && m.text === categ); - } - // Management of the checkbox event (Check / Uncheck) public onCheckboxChange(event, categ: string, isSpecial: boolean): void { const checkValue: string = isSpecial ? 'True' : event.target.value; @@ -41,8 +37,8 @@ export class ModalFilterComponent implements OnInit { this.checkedModules.push(new Module(checkValue, categ)); } else { // Check if the unchecked module is present in the list and remove it - if (this.getIndex(checkValue, categ) > -1) { - this.checkedModules.splice(this.getIndex(checkValue, categ), 1); + if (this.searchService.getIndex(this.checkedModules, checkValue, categ) > -1) { + this.checkedModules.splice(this.searchService.getIndex(this.checkedModules, checkValue, categ), 1); } } } @@ -50,8 +46,8 @@ export class ModalFilterComponent implements OnInit { public clearFilters(): void { this.categories.forEach((categ: Category) => { categ.modules.forEach((module: Module) => { - const index = this.getIndex(module.id, categ.name); - const indexSpecial = this.getIndex('True', module.id); + const index = this.searchService.getIndex(this.checkedModules, module.id, categ.name); + const indexSpecial = this.searchService.getIndex(this.checkedModules, 'True', module.id); if (index > -1) { this.checkedModules.splice(index, 1); } else if (indexSpecial > -1) { diff --git a/src/app/structure-list/components/search/search.component.html b/src/app/structure-list/components/search/search.component.html index ee31cd2f1..1a8539fc3 100644 --- a/src/app/structure-list/components/search/search.component.html +++ b/src/app/structure-list/components/search/search.component.html @@ -7,21 +7,20 @@ [formGroup]="searchForm" fxLayout="row" fxLayoutGap="1.5vw" + fxLayoutAlign=" center" (ngSubmit)="applyFilter(searchForm.value.searchTerm)" > <div class="inputSection" fxLayout="row" fxLayoutAlign="space-between center"> <input type="text" formControlName="searchTerm" placeholder="Rechercher une adresse, une association..." /> - <div class="icon close" (click)="clearInput()"> - <div class="ico-close-search"></div> - </div> + <button type="button" (click)="clearInput()" class="icon close"><div class="ico-close-search"></div></button> + <span class="separator"></span> - <div class="icon pin"> - <div class="ico-pin-search blue"></div> - </div> - </div> - <div class="searchButton"> - <button type="submit">Rechercher</button> + <button class="icon pin" type="button" (click)="locateMe()"><div class="ico-pin-search"></div></button> </div> + + <button class="btnSearch" type="submit"> + <div class="searchButton">Rechercher</div> + </button> </form> </div> <div (clickOutside)="closeModal()"> @@ -81,7 +80,12 @@ <div class="checkbox"> <div class="checkboxItem"> <label> - <input type="checkbox" /> + <input + type="checkbox" + value="Pass numérique" + [checked]="searchService.getIndex(checkedModulesFilter, 'Pass numérique', 'Labels et qualifications') > -1" + (change)="numericPassCheck($event, 'Labels et qualifications')" + /> <span class="customCheck"></span> <div class="label">Pass numérique</div> </label> diff --git a/src/app/structure-list/components/search/search.component.scss b/src/app/structure-list/components/search/search.component.scss index 9d93e9bf7..4f2d6c8e5 100644 --- a/src/app/structure-list/components/search/search.component.scss +++ b/src/app/structure-list/components/search/search.component.scss @@ -17,19 +17,47 @@ } .content { margin: 10px 0 0px 0; - .icon { - padding: 0 6px 0 6px; - border-radius: 4px; - cursor: pointer; - &.pin { - margin-bottom: 4px; - } - } + input { @include cn-regular-14; @include input-search; } .searchSection { + .icon { + background-color: transparent; + border: 1px solid transparent; + outline: none; + cursor: pointer; + &.pin { + padding: 4px 6px 8px 6px; + &:hover { + .ico-pin-search { + background-color: $blue-hover; + } + } + &:focus { + border-color: $secondary-color; + .ico-pin-search { + background-color: $secondary-color; + } + } + &:active { + border-color: transparent; + .ico-pin-search { + background-color: $blue-active; + } + } + } + &.close { + padding: 4px 7px 10px 5px; + &:focus { + border-color: $secondary-color; + } + &:active { + border-color: transparent; + } + } + } .separator { height: 100%; width: 2px; @@ -38,20 +66,33 @@ } .inputSection { padding: 6px 3px 6px 6px; - min-width: 463px; + width: 100%; border: 1px solid $grey-4; background-color: $white; height: 40px; } - .searchButton { - border: 1px solid $grey-4; - border-radius: 6px; + .btnSearch { @include background-hash; + border: 1px solid $grey-4; + border-radius: 4px; padding: 0 0 4px 5px; - button { - border-radius: 6px; + outline: none; + &:hover { + border-color: $grey-6; + .searchButton { + color: $grey-3; + } + } + &:focus { + border-color: $blue-hover; + } + &:active { + background: none; + border-color: $grey-6; + } + .searchButton { + border-radius: 4px; @include btn-search; - @include cn-bold-14; } } } @@ -59,15 +100,15 @@ padding: 16px 0 0px 0; button { @include btn-filter; - .btnText { - @include cn-regular-14; + &:focus { + border-color: $secondary-color; } } .containCheckedFilters { border-color: $secondary-color; } .selected { - border-color: $primary-color; + border-color: $primary-color !important; color: inherit; .arrow { background-color: transparent; @@ -92,11 +133,24 @@ } .footerSearchSection { - margin: 17px 0px 17px 0px; - + margin: 8px 0px 8px 0px; + height: 40px; a { - @include cn-bold-14; - font-weight: bold; - text-decoration: underline; + @include btn-search-addStructure; + border: 1px solid transparent; + padding: 8px 8px 6px 8px; + + &:hover { + color: $blue-hover; + } + &:focus { + color: $secondary-color; + border-color: $secondary-color; + border-radius: 4px; + } + &:active { + border: none; + color: $blue-active; + } } } diff --git a/src/app/structure-list/components/search/search.component.spec.ts b/src/app/structure-list/components/search/search.component.spec.ts index a4cdfb399..3b89eff5b 100644 --- a/src/app/structure-list/components/search/search.component.spec.ts +++ b/src/app/structure-list/components/search/search.component.spec.ts @@ -3,11 +3,16 @@ import { ReactiveFormsModule } from '@angular/forms'; import { Filter } from '../../models/filter.model'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { SearchComponent } from './search.component'; +import { Module } from '../../models/module.model'; +import { TypeModal } from '../../enum/typeModal.enum'; +import { GeojsonService } from '../../../services/geojson.service'; +import { GeoJson } from '../../../map/models/geojson.model'; +import { of } from 'rxjs'; describe('SearchComponent', () => { let component: SearchComponent; let fixture: ComponentFixture<SearchComponent>; - + let geoService: GeojsonService; beforeEach(async () => { await TestBed.configureTestingModule({ declarations: [SearchComponent], @@ -19,12 +24,14 @@ describe('SearchComponent', () => { fixture = TestBed.createComponent(SearchComponent); component = fixture.componentInstance; fixture.detectChanges(); + geoService = TestBed.inject(GeojsonService); }); it('should create', () => { expect(component).toBeTruthy(); }); + // applyFilter function it('should emit filters', () => { const filter: Filter[] = [new Filter('nomDeVotreStructure', 'valInput', false)]; spyOn(component.searchEvent, 'emit'); @@ -32,4 +39,95 @@ describe('SearchComponent', () => { expect(component.searchEvent.emit).toHaveBeenCalled(); expect(component.searchEvent.emit).toHaveBeenCalledWith(filter); }); + + // countCheckFiltersOnModules function + it('should return a number of checked elements in an array', () => { + const checkedModules: Module[] = [ + { id: '176', text: 'training', count: 0 }, + { id: '173', text: 'training', count: 0 }, + { id: '172', text: 'training', count: 0 }, + ]; + + const nbCheckedElements: number = component.countCheckFiltersOnModules(checkedModules, 2); + expect(nbCheckedElements).toEqual(1); + }); + it('should return 0 of checked elements in an array', () => { + const checkedModules: Module[] = [ + { id: '176', text: 'training', count: 0 }, + { id: '173', text: 'training', count: 0 }, + { id: '172', text: 'training', count: 0 }, + ]; + + const nbCheckedElements: number = component.countCheckFiltersOnModules(checkedModules, 3); + expect(nbCheckedElements).toEqual(0); + }); + + // fetchResults function + it('should update number of checked elements in current filter', () => { + const checkedModules: Module[] = [ + { id: '176', text: 'training', count: 0 }, + { id: '173', text: 'accompaniment', count: 0 }, + { id: '172', text: 'accompaniment', count: 0 }, + { id: '180', text: 'moreFilters', count: 0 }, + { id: '130', text: 'moreFilters', count: 0 }, + { id: '219', text: 'moreFilters', count: 0 }, + ]; + component.modalTypeOpened = TypeModal.training; + component.numberAccompanimentChecked = 2; + component.numberMoreFiltersChecked = 3; + component.fetchResults(checkedModules); + expect(component.numberTrainingChecked).toEqual(1); + }); + // openModal function + it('should open modal', () => { + component.openModal(TypeModal.training); + expect(component.modalTypeOpened).toEqual(TypeModal.training); + }); + // closeModal function + it('should close modal', () => { + component.modalTypeOpened = TypeModal.training; + component.closeModal(); + expect(component.modalTypeOpened).toBeUndefined(); + }); + // numericPassCheck function + it('should add numericPass filter to array of current filters and increment by one number of moreFilters element', () => { + const evt = { target: { checked: true, value: 'Pass numérique' } }; + const categ = 'Labels et qualifications'; + component.numericPassCheck(evt, categ); + const expectArray: Module[] = [new Module(evt.target.value, categ)]; + expect(component.checkedModulesFilter).toEqual(expectArray); + expect(component.numberMoreFiltersChecked).toEqual(1); + }); + it('should remove numericPass filter to array of current filters and increment by one number of moreFilters element', () => { + const evt = { target: { checked: false, value: 'Pass numérique' } }; + const categ = 'Labels et qualifications'; + const checkedModules: Module[] = [{ id: evt.target.value, text: categ, count: 0 }]; + component.checkedModulesFilter = checkedModules; + component.numericPassCheck(evt, categ); + const expectArray: Module[] = [new Module(evt.target.value, categ)]; + expect(component.checkedModulesFilter.length).toEqual(0); + expect(component.numberMoreFiltersChecked).toEqual(0); + }); + // learInput function + it('should reset form', () => { + component.searchForm.setValue({ searchTerm: 'someSearchTerm' }); + component.clearInput(); + expect(component.searchForm.get('searchTerm').value).toBeNull(); + }); + // locateMe function + it('should update form with the correct address ', () => { + let fakeGeo: GeoJson = new GeoJson({ properties: { name: 'Rue du lac' } }); + + spyOn(navigator.geolocation, 'getCurrentPosition').and.callFake(function () { + var position = { coords: { latitude: 45.7585243, longitude: 4.85442 } }; + arguments[0](position); + }); + spyOn(geoService, 'getAddressByCoord').and.callFake(function () { + return of(fakeGeo); + }); + component.locateMe(); + expect(navigator.geolocation.getCurrentPosition).toHaveBeenCalled(); + expect(geoService.getAddressByCoord).toHaveBeenCalled(); + expect(component.searchForm.get('searchTerm').value).toBe('Rue du lac'); + }); }); diff --git a/src/app/structure-list/components/search/search.component.ts b/src/app/structure-list/components/search/search.component.ts index 5e8e47689..414dc2df4 100644 --- a/src/app/structure-list/components/search/search.component.ts +++ b/src/app/structure-list/components/search/search.component.ts @@ -1,6 +1,8 @@ import { Component, EventEmitter, OnInit, Output, Type } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { forkJoin } from 'rxjs'; +import { GeoJson } from '../../../map/models/geojson.model'; +import { GeojsonService } from '../../../services/geojson.service'; import { TypeModal } from '../../enum/typeModal.enum'; import { Category } from '../../models/category.model'; import { Filter } from '../../models/filter.model'; @@ -14,7 +16,7 @@ import { SearchService } from '../../services/search.service'; styleUrls: ['./search.component.scss'], }) export class SearchComponent implements OnInit { - constructor(private searchService: SearchService, private fb: FormBuilder) { + constructor(public searchService: SearchService, private fb: FormBuilder, private geoJsonService: GeojsonService) { this.searchForm = this.fb.group({ searchTerm: '', }); @@ -57,11 +59,11 @@ export class SearchComponent implements OnInit { // Add search input filter const filters: Filter[] = []; if (term) { - filters.push(new Filter('nomDeVotreStructure', term, false)); + filters.push(new Filter('nomDeVotreStructure', term)); } // Add checked box filter this.checkedModulesFilter.forEach((cm) => { - filters.push(new Filter(this.fromStringToIdExcel(cm.text), this.mockApiNumber(cm.id), false)); + filters.push(new Filter(this.fromStringToIdExcel(cm.text), this.mockApiNumber(cm.id))); }); // Send filters this.searchEvent.emit(filters); @@ -69,7 +71,7 @@ export class SearchComponent implements OnInit { // Delete when getting back-end private mockApiNumber(nb: string): string { - return ('00' + nb).slice(-3); + return nb.length < 3 ? ('00' + nb).slice(-3) : nb; } public fetchResults(checkedModules: Module[]): void { @@ -105,10 +107,10 @@ export class SearchComponent implements OnInit { // Close modal after receive filters from her. this.closeModal(); - inputTerm ? this.applyFilter(inputTerm) : this.applyFilter(null); + this.applyFilter(inputTerm); } - // Check if some modules is checked on first filter and store number of modules checked + // Check if some modules is checked on filter and store number of modules checked public countCheckFiltersOnModules(checkedModules: Module[], value: number): number { if (checkedModules.length && value !== checkedModules.length) { return checkedModules.length - value; @@ -121,7 +123,7 @@ export class SearchComponent implements OnInit { this.categories = []; // if modal already opened, reset type if (this.modalTypeOpened === modalType) { - this.modalTypeOpened = undefined; + this.closeModal(); } else if (this.modalTypeOpened !== modalType) { this.modalTypeOpened = modalType; this.fakeData(modalType); @@ -144,6 +146,38 @@ export class SearchComponent implements OnInit { .replace(/[\s-]/g, ' ') .replace('?', ''); } + // Get adress and put it in input + public locateMe(): void { + navigator.geolocation.getCurrentPosition((position) => { + const longitude = position.coords.longitude; + const latitude = position.coords.latitude; + this.geoJsonService.getAddressByCoord(longitude, latitude).subscribe((geoPosition: GeoJson) => { + const adress = geoPosition.properties.name; + this.searchForm.setValue({ searchTerm: adress }); + this.applyFilter(adress); + }); + }); + } + // Management of the checkbox event (Check / Uncheck) + public numericPassCheck(event, categ): void { + const checkValue: string = event.target.value; + const inputTerm = this.searchForm.get('searchTerm').value; + if (event.target.checked) { + this.checkedModulesFilter.push(new Module(checkValue, categ)); + this.numberMoreFiltersChecked++; + } else { + // Check if the unchecked module is present in the list and remove it + const index = this.checkedModulesFilter.findIndex((m: Module) => m.id === checkValue && m.text === categ); + if (index > -1) { + this.checkedModulesFilter.splice(index, 1); + this.numberMoreFiltersChecked = this.countCheckFiltersOnModules( + this.checkedModulesFilter, + this.numberAccompanimentChecked + this.numberTrainingChecked + ); + } + } + this.applyFilter(inputTerm); + } // Get the correct list of checkbox/modules depending on the type of modal. private fakeData(option: TypeModal): void { diff --git a/src/app/structure-list/models/filter.model.ts b/src/app/structure-list/models/filter.model.ts index 826633f50..6cf52358a 100644 --- a/src/app/structure-list/models/filter.model.ts +++ b/src/app/structure-list/models/filter.model.ts @@ -1,11 +1,9 @@ export class Filter { name: string; value: string; - isStrict: boolean; - constructor(name: string, value: any, isStrict: boolean) { + constructor(name: string, value: any) { this.name = name; this.value = value.toString(); - this.isStrict = isStrict; } } diff --git a/src/app/structure-list/services/search.service.ts b/src/app/structure-list/services/search.service.ts index b7d825016..de2252c1f 100644 --- a/src/app/structure-list/services/search.service.ts +++ b/src/app/structure-list/services/search.service.ts @@ -45,4 +45,8 @@ export class SearchService { }); return category; } + + public getIndex(array: Module[], id: string, categ: string): number { + return array.findIndex((m: Module) => m.id === id && m.text === categ); + } } diff --git a/src/app/structure-list/structure-list.component.html b/src/app/structure-list/structure-list.component.html index 64ed26a4c..cf88f4479 100644 --- a/src/app/structure-list/structure-list.component.html +++ b/src/app/structure-list/structure-list.component.html @@ -1,7 +1,7 @@ <div class="topBlock"> <app-structure-list-search (searchEvent)="fetchResults($event)"></app-structure-list-search> - <span class="nbStructuresLabel">{{ structureList.length }} structures</span> </div> +<div class="nbStructuresLabel">{{ structureList.length }} structure{{ structureList.length > 1 ? 's' : '' }}</div> <div class="listCard" (mouseout)="mouseOut()"> <app-card diff --git a/src/app/structure-list/structure-list.component.scss b/src/app/structure-list/structure-list.component.scss index ee31dcc05..b619da552 100644 --- a/src/app/structure-list/structure-list.component.scss +++ b/src/app/structure-list/structure-list.component.scss @@ -3,13 +3,18 @@ @import '../../assets/scss/typography'; .nbStructuresLabel { - color: $grey; + color: $white; @include cn-regular-16; - display: flex; + display: grid; align-items: center; + height: 32px; + background-color: $secondary-color; + padding-left: 9px; + margin: 0 16px; } .listCard { - overflow-y: auto; + overflow-y: scroll; + overflow-y: overlay; padding: 0 25px; } .topBlock { diff --git a/src/assets/scss/_buttons.scss b/src/assets/scss/_buttons.scss index a6e87f83d..491a797dc 100644 --- a/src/assets/scss/_buttons.scss +++ b/src/assets/scss/_buttons.scss @@ -1,5 +1,6 @@ @import './color'; @import './shapes'; +@import './typography'; @mixin btn-filter { background: $white; @@ -10,22 +11,35 @@ outline: none; border-radius: 4px; cursor: pointer; + @include btn-normal; } @mixin btn-search { background: $white; - height: 34px; + height: 31px; border: none; color: $primary-color; padding: 3px 16px 3px 16px; outline: none; cursor: pointer; + display: table-cell; + vertical-align: middle; + @include btn-bold; } @mixin btn-search-filter { background: $secondary-color; height: 40px; border: none; color: $white; - padding: 3px 16px 3px 16px; + padding: 4px 37px 4px 37px; + border-radius: 4px; outline: none; + border: 1px solid transparent; cursor: pointer; + @include cn-bold-16; + line-break: 18px; +} +@mixin btn-search-addStructure { + @include btn-bold-sousligne; + color: $secondary-color; + outline: none; } diff --git a/src/assets/scss/_color.scss b/src/assets/scss/_color.scss index 2efedea70..4d9911334 100644 --- a/src/assets/scss/_color.scss +++ b/src/assets/scss/_color.scss @@ -13,6 +13,8 @@ $green: #41c29c; $red: #f98181; /* OTHERS */ $blue: #348899; +$blue-hover: #117083; +$blue-active: #8cb6be; $grey-1: #594d59; $red-metro: #d50000; /* APP COLORS */ diff --git a/src/assets/scss/_icons.scss b/src/assets/scss/_icons.scss index 62278f53f..052eb5e85 100644 --- a/src/assets/scss/_icons.scss +++ b/src/assets/scss/_icons.scss @@ -52,6 +52,7 @@ border-radius: 50% 50% 50% 0; -webkit-transform: rotate(-45deg); transform: rotate(-45deg); + background-color: $secondary-color; &:before { content: ''; position: absolute; @@ -62,9 +63,6 @@ border-radius: 4px; background-color: white; } - &.blue { - background-color: $secondary-color; - } } .ico-dot-available { height: 12px; @@ -174,7 +172,7 @@ .ico-close-search { width: 15px; - height: 13px; + height: 7px; text-align: center; &:before { transform: rotate(45deg); diff --git a/src/assets/scss/_typography.scss b/src/assets/scss/_typography.scss index b8a467c30..e7469954d 100644 --- a/src/assets/scss/_typography.scss +++ b/src/assets/scss/_typography.scss @@ -35,6 +35,25 @@ h6, font-family: $title-font; } +@mixin btn-bold { + @include cn-bold-16; + line-height: 18px; +} + +@mixin btn-bold-sousligne { + @include btn-bold; + text-decoration: underline; +} +@mixin btn-normal { + @include cn-regular-16; + line-height: 19px; +} + +@mixin btn-pass { + @include cn-regular-18; + line-height: 21px; +} + @mixin arial-regular-16 { font-family: $footer-text-font; font-style: normal; diff --git a/src/styles.scss b/src/styles.scss index f5c771f59..e0ff61ab2 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -94,8 +94,9 @@ a { cursor: pointer; } .label { - padding-left: 8px; - @include cn-regular-14; + padding: 0 16px; + @include btn-pass; + width: 232px; } .customCheck { display: inline-grid; -- GitLab