diff --git a/angular.json b/angular.json index 20abea0c557f1c3d305f960ec8ef080bb1d2f53a..6e95360f6a90b5417e30d67c101ad0e1abe2b9be 100644 --- a/angular.json +++ b/angular.json @@ -70,8 +70,8 @@ }, { "type": "anyComponentStyle", - "maximumWarning": "6kb", - "maximumError": "10kb" + "maximumWarning": "10kb", + "maximumError": "15kb" } ] }, diff --git a/api/Dockerfile b/api/Dockerfile index 4a8add9b9783d01d9cbd2bb165b0f8180966eeee..8866e160eba9a870f6ae20d5e0e4cdf7f046fa8a 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -1,18 +1,15 @@ -# Stage 0, based on Node.js -FROM node:12.16-slim +# Based on Node.js +FROM node:12.16 WORKDIR /app # Copy the package.json file first in order to cache the modules COPY . . -RUN mkdir api -RUN cp routes.json ./api - # Install npm dependencies RUN npm install # expose port 3000 -EXPOSE 80 3000 +EXPOSE 3000 -CMD ["npm", "run", "api"] +CMD ["npm", "start"] diff --git a/api/package.json b/api/package.json index a74b88aa616f75ce8ef255ac7f4d73addd098495..997401923db7c8fd06c91601d172093be8b412ed 100644 --- a/api/package.json +++ b/api/package.json @@ -1,8 +1,8 @@ { "name": "pamn-mock-api", - "version": "0.0.1", + "version": "0.1.0", "scripts": { - "api": "json-server api/db.json --routes api/routes.json --no-cors=true" + "start": "node server.js" }, "private": true, "dependencies": { diff --git a/api/routes.json b/api/routes.json deleted file mode 100644 index 6a7af55fa20d537865c8442cde5b019802d92d08..0000000000000000000000000000000000000000 --- a/api/routes.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "/api/*": "/$1" -} diff --git a/api/server.js b/api/server.js new file mode 100644 index 0000000000000000000000000000000000000000..e85ef232a7d69299e0ce3c9ceffd20b3d44a868a --- /dev/null +++ b/api/server.js @@ -0,0 +1,71 @@ +const jsonServer = require('json-server'); +const server = jsonServer.create(); +const router = jsonServer.router('db.json'); +const routes = { + '/api/*': '/$1', +}; +const middlewares = [jsonServer.defaults(), jsonServer.rewriter(routes)]; + +// Set default middlewares (logger, static, cors and no-cache) +server.use(middlewares); + +// Add custom routes before JSON Server router +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 }); + // Accès aux droits + structureCountTab.push({ id: 176, count: 2 }); + 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 }); + // 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 }); + // 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 }); + // Culture et sécurité numérique + structureCountTab.push({ id: 264, count: 2 }); + structureCountTab.push({ id: 255, count: 2 }); + 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 }); + return res.status(200).jsonp(structureCountTab); +}); + +// Use default router +server.use(router); +server.listen(3000, () => { + console.log('JSON Server is running'); +}); diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 915a88e10cb935aa2184cb260b9ec7b483a5f3e4..803b77c89f8eee0d9008b9e636c17caa9fec3a71 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -10,7 +10,7 @@ } .app-body { - flex: 1 0 auto; + flex: 1 1 auto; overflow-y: hidden; overflow-x: hidden; position: relative; diff --git a/src/app/app.module.ts b/src/app/app.module.ts index f622516c4bc96f1b45bbef3658939231ccddd95d..a2af3088eb43c377246b6b92da09a971c95b70d0 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -12,9 +12,11 @@ import { FooterComponent } from './footer/footer.component'; import { HeaderComponent } from './header/header.component'; import { SharedModule } from './shared/shared.module'; import { MapModule } from './map/map.module'; -import { RechercheComponent } from './structure-list/components/recherche/recherche.component'; import { StructureListComponent } from './structure-list/structure-list.component'; import { CardComponent } from './structure-list/components/card/card.component'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { SearchComponent } from './structure-list/components/search/search.component'; +import { ModalFilterComponent } from './structure-list/components/modal-filter/modal-filter.component'; import { StructureDetailsComponent } from './structure-list/components/structure-details/structure-details.component'; import { StructureOpeningStatusComponent } from './structure-list/components/structure-opening-status/structure-opening-status.component'; @@ -26,11 +28,21 @@ import { StructureOpeningStatusComponent } from './structure-list/components/str HomeComponent, StructureListComponent, CardComponent, - RechercheComponent, + SearchComponent, + ModalFilterComponent, StructureDetailsComponent, StructureOpeningStatusComponent, ], - imports: [BrowserModule, HttpClientModule, AppRoutingModule, FlexLayoutModule, SharedModule, MapModule], + imports: [ + BrowserModule, + HttpClientModule, + AppRoutingModule, + FlexLayoutModule, + SharedModule, + MapModule, + FormsModule, + ReactiveFormsModule, + ], providers: [{ provide: LOCALE_ID, useValue: 'fr' }, CustomBreakPointsProvider], bootstrap: [AppComponent], }) diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index 1a0aedddd2a7cd83298c817c83bb20e97ed5b338..8d8ab36e948009cb1091d12e4aded7b874ef0591 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -1,5 +1,6 @@ <div fxLayout="row" class="content-container"> <app-structure-list + (searchEvent)="getStructures($event)" [structureList]="structures" [location]="currentLocation" (displayMapMarkerId)="setMapMarkerId($event)" diff --git a/src/app/home/home.component.scss b/src/app/home/home.component.scss index 4afaba3c202049ce01b6e2b8bbf43aaad13c07e4..e501173f8a4396e1be6c09814793874c8da26f6d 100644 --- a/src/app/home/home.component.scss +++ b/src/app/home/home.component.scss @@ -1,9 +1,12 @@ .left-pane { - padding: 0 25px; - width: 590px; - min-width: 590px; + width: 640px; + min-width: 640px; } .right-pane { - width: 100%; + width: 80%; + height: 80%; padding: 0 40px 40px 40px; } +.content-container { + height: 100%; +} diff --git a/src/app/home/home.component.spec.ts b/src/app/home/home.component.spec.ts index b952097115baec8b84f8b2346688caa8a883fb77..6aeea5f08a025669387b5c6e93fe67711ac7317d 100644 --- a/src/app/home/home.component.spec.ts +++ b/src/app/home/home.component.spec.ts @@ -36,7 +36,6 @@ describe('HomeComponent', () => { await new Promise((resolve) => { component.getCoord(21356).subscribe( (val) => { - console.log(val); expect(val.geometry.getLat()).toEqual(69800); expect(val.geometry.getLon).toEqual(69800); resolve(); diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index 6630be72c3c16ae587037871d72f3f2672128e8e..e5291b76321043b3645c1cfe9eb6ed6898c2dc1b 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -3,8 +3,10 @@ import { Observable } from 'rxjs'; import { mergeMap } from 'rxjs/operators'; const { DateTime } = require('luxon'); import * as _ from 'lodash'; + import { Structure } from '../models/structure.model'; import { StructureService } from '../services/structure-list.service'; +import { Filter } from '../structure-list/models/filter.model'; import { GeoJson } from '../map/models/geojson.model'; import { GeojsonService } from '../services/geojson.service'; @@ -25,11 +27,11 @@ export class HomeComponent implements OnInit { if (navigator.geolocation) { this.getLocation(); } - this.getStructures(); + this.getStructures(null); } - public getStructures(): void { - this.structureService.getStructures().subscribe((structures) => { + public getStructures(filters: Filter[]): void { + this.structureService.getStructures(filters).subscribe((structures) => { Promise.all( structures.map((structure) => { if (this.geolocation) { @@ -52,6 +54,7 @@ export class HomeComponent implements OnInit { private getStructurePosition(structure: Structure): Promise<Structure> { return new Promise((resolve, reject) => { this.getCoord(structure.voie).subscribe((coord: GeoJson) => { + structure.address = coord.properties.name + ' - ' + coord.properties.postcode + ' ' + coord.properties.city; structure.distance = this.geoJsonService.getDistance( coord.geometry.getLon(), coord.geometry.getLat(), @@ -69,7 +72,6 @@ export class HomeComponent implements OnInit { * @param idVoie Street reference */ public getCoord(idVoie: number): Observable<GeoJson> { - console.log('in'); return this.geoJsonService.getAddressByIdVoie(idVoie).pipe(mergeMap((res) => this.geoJsonService.getCoord(res))); } @@ -84,7 +86,9 @@ export class HomeComponent implements OnInit { private getAddress(longitude: number, latitude: number): void { this.geoJsonService.getAddressByCoord(longitude, latitude).subscribe( - (location) => (this.currentLocation = location), + (location) => { + this.currentLocation = location; + }, (err) => console.error(err) ); } diff --git a/src/app/map/components/map.component.html b/src/app/map/components/map.component.html index 4ed0d90d50bac4bf1baf0b64472986f93bc7d62a..4fc6326081f2e5319cdf04ebd9bf7cabbe78d282 100644 --- a/src/app/map/components/map.component.html +++ b/src/app/map/components/map.component.html @@ -1,2 +1,4 @@ -<div id="map" leaflet [leafletOptions]="mapOptions" (leafletMapReady)="onMapReady($event)"></div> -<leaflet-locate-control [map]="map" [options]="locateOptions"></leaflet-locate-control> +<div class="map-wrapper"> + <div id="map" class="body-wrap" leaflet [leafletOptions]="mapOptions" (leafletMapReady)="onMapReady($event)"></div> + <leaflet-locate-control [map]="map" [options]="locateOptions"></leaflet-locate-control> +</div> diff --git a/src/app/map/components/map.component.scss b/src/app/map/components/map.component.scss index b45b2ff1f76993a47e38c81d334abe691df46866..fa76e2cc47cfd4efb7de3a9252c13912580eaaf5 100644 --- a/src/app/map/components/map.component.scss +++ b/src/app/map/components/map.component.scss @@ -2,12 +2,18 @@ @import '../../../assets/scss/layout'; @import '../../../assets/scss/icons'; @import '../../../assets/scss/typography'; +@import '../../../assets/scss/shapes'; + +.map-wrapper { + border-radius: 6px; + @include background-hash; +} #map { height: calc(100vh - #{$header-height} - #{$footer-height} - 120px); - // height: 100%; width: 100%; - border: 10px solid white; + border: 10px solid $white; + border-radius: 6px; } ::ng-deep .fa-map-marker { diff --git a/src/app/map/components/map.component.ts b/src/app/map/components/map.component.ts index 08bacc687b6001449ab5f358b0261e3258461952..81b35c4554b4d6469b45cac011e72f7cb2e83241 100644 --- a/src/app/map/components/map.component.ts +++ b/src/app/map/components/map.component.ts @@ -74,7 +74,15 @@ export class MapComponent implements OnChanges { * @param structure Structure */ private buildToolTip(structure: Structure): string { - const cssAvailabilityClass = structure.isOpen ? 'available' : 'unavailable'; + let cssAvailabilityClass = structure.isOpen ? 'available' : null; + if (cssAvailabilityClass === null) { + if (structure.openedOn.day) { + cssAvailabilityClass = 'unavailable'; + } else { + cssAvailabilityClass = 'unknown'; + } + } + return ( '<h1>' + structure.nomDeVotreStructure + diff --git a/src/app/map/models/geoJsonProperties.model.ts b/src/app/map/models/geoJsonProperties.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..fc9abed8dfa0c6db0cbd2e0c3a1f93d651ec5195 --- /dev/null +++ b/src/app/map/models/geoJsonProperties.model.ts @@ -0,0 +1,7 @@ +export class GeoJsonProperties { + public city: string; + public country: string; + public name: string; + public postcode: string; + public state: string; +} diff --git a/src/app/map/models/geojson.model.ts b/src/app/map/models/geojson.model.ts index 3407c70da879424646eeff1ad4ad17893ac9ed7b..8ca263895f084b62e528a5dc49280ded859bdab9 100644 --- a/src/app/map/models/geojson.model.ts +++ b/src/app/map/models/geojson.model.ts @@ -1,9 +1,10 @@ import { AddressGeometry } from './addressGeometry.model'; +import { GeoJsonProperties } from './geoJsonProperties.model'; export class GeoJson { public geometry: AddressGeometry; public type: string; - public properties: object; + public properties: GeoJsonProperties; constructor(obj?: any) { Object.assign(this, obj, { diff --git a/src/app/models/structure.model.ts b/src/app/models/structure.model.ts index 16f2781e638a2c68288a9a21aefee746a03457ed..6cc2c585f5c142d79a99e779438b47f97ce96614 100644 --- a/src/app/models/structure.model.ts +++ b/src/app/models/structure.model.ts @@ -36,6 +36,7 @@ export class Structure { public isOpen: boolean; public openedOn: OpeningDay; public distance?: string; + public address?: string; constructor(obj?: any) { Object.assign(this, obj, { diff --git a/src/app/services/structure-list.service.spec.ts b/src/app/services/structure-list.service.spec.ts index 243602d45f0a89680d08ea8fe90ac5892935e764..230f2e331d15a4316a931d442b658b48a4d2a5f6 100644 --- a/src/app/services/structure-list.service.spec.ts +++ b/src/app/services/structure-list.service.spec.ts @@ -1,4 +1,4 @@ -import { HttpClientModule } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; import { inject, TestBed } from '@angular/core/testing'; import { Day } from '../models/day.model'; import { Structure } from '../models/structure.model'; @@ -7,15 +7,15 @@ import { StructureService } from './structure-list.service'; const { DateTime } = require('luxon'); describe('StructureService', () => { + let structureService: StructureService; + beforeEach(() => { TestBed.configureTestingModule({ - imports: [HttpClientModule], + imports: [HttpClientTestingModule], + providers: [StructureService], }); + structureService = TestBed.inject(StructureService); }); - let structureService: StructureService; - beforeEach(inject([StructureService], (s: StructureService) => { - structureService = s; - })); it('Mise à jour ouverture de la structure : should return true', () => { // Init structure avec aucun horaire diff --git a/src/app/services/structure-list.service.ts b/src/app/services/structure-list.service.ts index 82d06039ce5c73e24caa37fcadf2b20eb56560d7..9f2425166a388e66775f1c4727766c89bef5156e 100644 --- a/src/app/services/structure-list.service.ts +++ b/src/app/services/structure-list.service.ts @@ -9,6 +9,7 @@ import { Day } from '../models/day.model'; import { OpeningDay } from '../models/openingDay.model'; import { Weekday } from '../structure-list/enum/weekday.enum'; import { Time } from '../models/time.model'; +import { Filter } from '../structure-list/models/filter.model'; @Injectable({ providedIn: 'root', @@ -16,8 +17,35 @@ import { Time } from '../models/time.model'; export class StructureService { constructor(private http: HttpClient) {} - public getStructures(): Observable<Structure[]> { - return this.http.get('/api/Structures').pipe(map((data: any[]) => data.map((item) => new Structure(item)))); + 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; } /** 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 new file mode 100644 index 0000000000000000000000000000000000000000..ec2d69fb075c58894c9f0c3848264bd7f82f00fa --- /dev/null +++ b/src/app/structure-list/components/modal-filter/modal-filter.component.html @@ -0,0 +1,33 @@ +<div *ngIf="modalType" fxLayout="column" fxLayoutAlign="space-between" [ngClass]="['modal', 'modal' + modalType]"> + <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"> + <li class="checkbox"> + <div class="checkboxItem"> + <label> + <input + type="checkbox" + [checked]="getIndex(module.id, c.name) > -1" + [value]="module.id" + (change)="onCheckboxChange($event, c.name)" + /> + <span class="customCheck"></span> + <div class="label">{{ module.text }}</div> + </label> + </div> + </li> + <span class="nbResult">{{ module.count ? module.count : '0' }}</span> + </div> + </ul> + </div> + </div> + <div class="footer" fxLayout="row" fxLayoutAlign="end center" fxLayoutGap="3vw"> + <a (click)="clearFilters()">Effacer</a> + <button type="button" (click)="emitModules()">Appliquer</button> + </div> + </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 new file mode 100644 index 0000000000000000000000000000000000000000..5c3a3f64d3f904ab18c02ef3279a76b173ff90d3 --- /dev/null +++ b/src/app/structure-list/components/modal-filter/modal-filter.component.scss @@ -0,0 +1,101 @@ +@import '../../../../assets/scss/icons'; +@import '../../../../assets/scss/color'; +@import '../../../../assets/scss/typography'; +@import '../../../../assets/scss/breakpoint'; +@import '../../../../assets/scss/shapes'; +@import '../../../../assets/scss/buttons'; + +.modaltraining { + @media #{$desktop} { + margin-left: 0; + } + + margin-left: 196px; +} +.modalmoreFilters { + @media #{$desktop} { + margin-left: 0; + } + margin-left: 396px; +} +.modal { + max-height: 648px; + max-width: 754px; + width: 94%; + border-left: 6.5px solid transparent; + border-bottom: 6.5px solid transparent; + border-radius: 11px; + z-index: 401 !important; + position: absolute; + border: 1px solid $grey-6; + border-radius: 6px; + margin-top: 3.5px; + @include background-hash; + ::-webkit-scrollbar { + width: 10px; + } + ::-webkit-scrollbar-track { + background: $grey-6; + } + ::-webkit-scrollbar-thumb { + background: $grey; + border-radius: 6px; + } + .contentModal { + overflow-y: auto; + max-width: 1100px; + border-bottom: 1px solid $grey; + margin-bottom: 10px; + max-height: 500px; + .blockFiltre { + width: 100%; + padding: 32px 40px 10px 40px; + min-width: 450px; + } + .blockLigne { + padding-left: 0; + -moz-column-count: 2; + -moz-column-gap: 46px; + -webkit-column-count: 2; + -webkit-column-gap: 46px; + column-count: 2; + column-gap: 46px; + column-rule: dashed 1px $grey; + @media #{$large-phone} { + -moz-column-count: 1; + -webkit-column-count: 1; + column-count: 1; + } + } + .ligneFiltre { + padding: 5px 0 5px 0; + } + h4 { + @include cn-bold-14; + display: flex; + align-items: center; + margin-top: 0; + } + .nbResult { + @include cn-regular-14; + } + label { + @include cn-regular-14; + } + } + .footer { + margin: 0px 20px 16px 0; + a { + @include cn-bold-14; + display: flex; + align-items: center; + text-decoration: underline; + color: $secondary-color; + } + height: 32px; + button { + @include btn-search-filter; + @include cn-bold-14; + } + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..9c1ad3d140e147fb54201d06335a3fcd2f06e378 --- /dev/null +++ b/src/app/structure-list/components/modal-filter/modal-filter.component.spec.ts @@ -0,0 +1,91 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { Category } from '../../models/category.model'; +import { Filter } from '../../models/filter.model'; +import { Module } from '../../models/module.model'; + +import { ModalFilterComponent } from './modal-filter.component'; + +describe('ModalFilterComponent', () => { + let component: ModalFilterComponent; + let fixture: ComponentFixture<ModalFilterComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ModalFilterComponent], + imports: [ReactiveFormsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ModalFilterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should emit modules', () => { + const modules: Module[] = [ + { id: 176, text: 'training', count: 3 }, + { id: 173, text: 'training', count: 2 }, + { id: 172, text: 'training', count: 2 }, + ]; + component.checkedModules = modules; + spyOn(component.searchEvent, 'emit'); + component.emitModules(); + 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); + }); + it('should add a module to checkedModule array', () => { + 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 evt = { target: { checked: true, value: 175 } }; + component.onCheckboxChange(evt, 'training'); + expect(component.checkedModules.length).toEqual(4); + }); + it('should remove a module to checkedModule array', () => { + 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 evt = { target: { checked: false, value: 173 } }; + component.onCheckboxChange(evt, 'training'); + expect(component.checkedModules.length).toEqual(2); + }); + it('should remove all modules checked from same modal, here morefilters', () => { + const modules: Module[] = [ + { id: 176, text: 'morefilters', count: 0 }, + { id: 173, text: 'morefilters', count: 0 }, + { id: 172, text: 'morefilters', count: 0 }, + { id: 179, text: 'training', count: 0 }, + { id: 190, text: 'training', count: 0 }, + { id: 167, text: 'training', count: 0 }, + ]; + component.checkedModules = modules; + const category: Category = { name: 'morefilters', modules: [modules[0], modules[1], modules[2]] }; + component.categories = [category]; + component.clearFilters(); + expect(component.checkedModules.length).toEqual(3); + }); +}); 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 new file mode 100644 index 0000000000000000000000000000000000000000..2f46075fa9809c9adf2d9d261735956069d3819c --- /dev/null +++ b/src/app/structure-list/components/modal-filter/modal-filter.component.ts @@ -0,0 +1,64 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Category } from '../../models/category.model'; +import { Filter } from '../../models/filter.model'; +import { Module } from '../../models/module.model'; + +@Component({ + selector: 'app-modal-filter', + templateUrl: './modal-filter.component.html', + styleUrls: ['./modal-filter.component.scss'], +}) +export class ModalFilterComponent implements OnInit { + constructor(private fb: FormBuilder) { + this.searchForm = this.fb.group({ + searchTerm: '', + }); + } + @Input() public modalType: string; + @Input() public categories: Category[]; + @Input() public modules: Module[] = []; + @Output() searchEvent = new EventEmitter(); + // Checkbox variable + checkedModules: Module[]; + // Form search input + searchForm: FormGroup; + ngOnInit(): void { + // Manage checkbox + this.checkedModules = this.modules.slice(); + } + + // Return index of a specific module in array modules + public getIndex(id: number, 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): void { + const checkValue: number = parseInt(event.target.value, 10); + if (event.target.checked) { + 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); + } + } + } + + // Clear only filters in the current modal + public clearFilters(): void { + this.categories.forEach((categ: Category) => { + categ.modules.forEach((module: Module) => { + if (this.getIndex(module.id, categ.name) > -1) { + this.checkedModules.splice(this.getIndex(module.id, categ.name), 1); + } + }); + }); + } + + // Sends an array containing all modules + public emitModules(): void { + this.searchEvent.emit(this.checkedModules); + } +} diff --git a/src/app/structure-list/components/recherche/recherche.component.html b/src/app/structure-list/components/recherche/recherche.component.html deleted file mode 100644 index 63c7b1a72d082d00bfd6283b0c0aef923754e111..0000000000000000000000000000000000000000 --- a/src/app/structure-list/components/recherche/recherche.component.html +++ /dev/null @@ -1 +0,0 @@ -<p>recherche works!</p> diff --git a/src/app/structure-list/components/recherche/recherche.component.scss b/src/app/structure-list/components/recherche/recherche.component.scss deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/src/app/structure-list/components/recherche/recherche.component.spec.ts b/src/app/structure-list/components/recherche/recherche.component.spec.ts deleted file mode 100644 index e38e4d4e837b3cae1b8b7ac52cc98cb1ca9b97a3..0000000000000000000000000000000000000000 --- a/src/app/structure-list/components/recherche/recherche.component.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { RechercheComponent } from './recherche.component'; - -describe('RechercheComponent', () => { - let component: RechercheComponent; - let fixture: ComponentFixture<RechercheComponent>; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [ RechercheComponent ] - }) - .compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(RechercheComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/structure-list/components/recherche/recherche.component.ts b/src/app/structure-list/components/recherche/recherche.component.ts deleted file mode 100644 index 6ccca06c4ef0ec4a4a0343d76bec7e900352b6c5..0000000000000000000000000000000000000000 --- a/src/app/structure-list/components/recherche/recherche.component.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Component, OnInit, Input } from '@angular/core'; - -@Component({ - selector: 'app-recherche', - templateUrl: './recherche.component.html', - styleUrls: ['./recherche.component.scss'], -}) -export class RechercheComponent implements OnInit { - constructor() {} - - ngOnInit(): void {} -} diff --git a/src/app/structure-list/components/search/search.component.html b/src/app/structure-list/components/search/search.component.html new file mode 100644 index 0000000000000000000000000000000000000000..9de95de2431a81f9a7bc1da82ed11a7a77416087 --- /dev/null +++ b/src/app/structure-list/components/search/search.component.html @@ -0,0 +1,80 @@ +<div class="header"> + <span class="title">Acteurs de la médiation</span> +</div> +<div class="content" fxLayout="column"> + <div class="searchSection"> + <form + [formGroup]="searchForm" + fxLayout="row" + fxLayoutGap="1.5vw" + (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 grey"></div> + </div> + <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> + </div> + </form> + </div> + + <div class="btnSection" fxLayout="row" fxLayoutAlign="space-between center"> + <button + type="button" + [ngClass]="{ selected: modalTypeOpened === TypeModal[0] }" + (click)="openModal(TypeModal[0])" + fxLayout="row" + fxLayoutAlign="space-between center" + > + <span class="btnText">Accompagnement</span> + <div class="arrow"></div> + </button> + <button + type="button" + [ngClass]="{ selected: modalTypeOpened === TypeModal[1] }" + (click)="openModal(TypeModal[1])" + fxLayout="row" + fxLayoutAlign="space-between center" + > + <span class="btnText">Formations</span> + <div class="arrow"></div> + </button> + <button + type="button" + [ngClass]="{ selected: modalTypeOpened === TypeModal[2] }" + (click)="openModal(TypeModal[2])" + fxLayout="row" + fxLayoutAlign="space-between center" + > + <span class="btnText">Plus de filtres</span> + <div class="arrow"></div> + </button> + </div> + <div *ngIf="modalTypeOpened"> + <app-modal-filter + [modalType]="modalTypeOpened" + [categories]="categories" + [modules]="checkedModulesFilter" + (searchEvent)="fetchResults($event)" + ></app-modal-filter> + </div> +</div> +<div class="footerSearchSection" fxLayout="row" fxLayoutAlign="space-between center"> + <div class="checkbox"> + <div class="checkboxItem"> + <label> + <input type="checkbox" /> + <span class="customCheck"></span> + <div class="label">Pass numérique</div> + </label> + </div> + </div> + <a href="">Ajouter une structure</a> +</div> diff --git a/src/app/structure-list/components/search/search.component.scss b/src/app/structure-list/components/search/search.component.scss new file mode 100644 index 0000000000000000000000000000000000000000..6ee105f2f7cf869ff9eab0ae7731331913eb85b8 --- /dev/null +++ b/src/app/structure-list/components/search/search.component.scss @@ -0,0 +1,99 @@ +@import '../../../../assets/scss/icons'; +@import '../../../../assets/scss/color'; +@import '../../../../assets/scss/typography'; +@import '../../../../assets/scss/breakpoint'; +@import '../../../../assets/scss/shapes'; +@import '../../../../assets/scss/buttons'; +@import '../../../../assets/scss/inputs'; + +.header { + .title { + @include cn-bold-20; + padding: 16px 0 16px 0; + display: flex; + align-items: center; + text-transform: uppercase; + } +} +.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 { + .separator { + height: 100%; + width: 2px; + background-color: $grey-6; + margin: 0 5px 0 5px; + } + .inputSection { + padding: 6px 3px 6px 6px; + min-width: 463px; + border: 1px solid $grey-6; + background-color: $white; + height: 40px; + } + .searchButton { + border: 1px solid $grey-6; + border-radius: 6px; + @include background-hash; + padding: 0 0 4px 5px; + button { + border-radius: 6px; + @include btn-search; + @include cn-bold-14; + } + } + } + .btnSection { + padding: 16px 0 0px 0; + button { + @include btn-filter; + .btnText { + @include cn-regular-14; + } + } + .selected { + border: 1px solid $primary-color; + color: $primary-color; + .arrow { + background-color: transparent; + border-bottom: 1px solid $primary-color; + border-right: 1px solid $primary-color; + transform: translateY(25%) rotate(-135deg); + margin: 0 5px 0 10px; + height: 7px; + width: 7px; + } + } + .arrow { + background-color: transparent; + border-bottom: 1px solid $grey-2; + border-right: 1px solid $grey-2; + transform: translateY(-25%) rotate(45deg); + margin: 0 5px 0 10px; + height: 7px; + width: 7px; + } + } +} + +.footerSearchSection { + margin: 17px 0px 17px 0px; + + a { + @include cn-bold-14; + font-weight: bold; + text-decoration: underline; + } +} diff --git a/src/app/structure-list/components/search/search.component.spec.ts b/src/app/structure-list/components/search/search.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a5507ed760dbed84c2fd7f249061d9e59eac3156 --- /dev/null +++ b/src/app/structure-list/components/search/search.component.spec.ts @@ -0,0 +1,48 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { Category } from '../../models/category.model'; +import { Filter } from '../../models/filter.model'; +import { Module } from '../../models/module.model'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { SearchComponent } from './search.component'; +import { TypeModal } from '../../enum/typeModal.enum'; + +describe('SearchComponent', () => { + let component: SearchComponent; + let fixture: ComponentFixture<SearchComponent>; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [SearchComponent], + imports: [HttpClientTestingModule, ReactiveFormsModule], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SearchComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should emit filters', () => { + const filter: Filter[] = [new Filter('nomDeVotreStructure', 'valInput', false)]; + spyOn(component.searchEvent, 'emit'); + component.applyFilter('valInput'); + expect(component.searchEvent.emit).toHaveBeenCalled(); + expect(component.searchEvent.emit).toHaveBeenCalledWith(filter); + }); + + it('should update categories', () => { + let categories: Category[] = [new Category({ name: 'Accompagnement des démarches' })]; + categories[0].modules = []; + for (let i = 0; i < 7; i++) { + categories[0].modules.push(new Module(5 + i, 'CAF' + i)); + } + component.openModal(TypeModal[0]); + expect(component.categories).toEqual(categories); + }); +}); diff --git a/src/app/structure-list/components/search/search.component.ts b/src/app/structure-list/components/search/search.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..10f0393f2308c909fae300627cc278d50a2c9634 --- /dev/null +++ b/src/app/structure-list/components/search/search.component.ts @@ -0,0 +1,144 @@ +import { Component, EventEmitter, OnInit, Output } from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { forkJoin } from 'rxjs'; +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 { StructureCounter } from '../../models/structureCounter.model'; +import { SearchService } from '../../services/search.service'; + +@Component({ + selector: 'app-structure-list-search', + templateUrl: './search.component.html', + styleUrls: ['./search.component.scss'], +}) +export class SearchComponent implements OnInit { + constructor(private searchService: SearchService, private fb: FormBuilder) { + this.searchForm = this.fb.group({ + searchTerm: '', + }); + } + + @Output() searchEvent = new EventEmitter(); + + // Form search input + public searchForm: FormGroup; + // Modal variable + public categories: Category[]; + public modalTypeOpened: string; + // Checkbox variable + public checkedModulesFilter: Module[]; + + ngOnInit(): void { + // Will store the different categories + this.categories = []; + + this.checkedModulesFilter = new Array(); + } + + // Accessor to template angular. + public get TypeModal(): typeof TypeModal { + return TypeModal; + } + + // Clear input search + public clearInput(): void { + this.searchForm.reset(); + this.applyFilter(null); + } + + // Sends an array containing all filters + public applyFilter(term: string): void { + // Add search input filter + const filters: Filter[] = []; + if (term) { + filters.push(new Filter('nomDeVotreStructure', term, false)); + } + // Add checked box filter + this.checkedModulesFilter.forEach((cm) => { + filters.push(new Filter(this.fromStringToIdExcel(cm.text), this.mockApiNumber(cm.id), false)); + }); + // Send filters + this.searchEvent.emit(filters); + } + + // Delete when getting back-end + private mockApiNumber(nb: number): string { + return ('00' + nb).slice(-3); + } + + public fetchResults(checkedModules: Module[]): void { + const inputTerm = this.searchForm.get('searchTerm').value; + + // Store checked modules + this.checkedModulesFilter = checkedModules; + + // Close modal after receive filters from her. + this.openModal(this.modalTypeOpened); + inputTerm ? this.applyFilter(inputTerm) : this.applyFilter(null); + } + + // Open the modal and display the list according to the right filter button + public openModal(option: string): void { + this.categories = []; + if (this.modalTypeOpened !== option) { + this.modalTypeOpened = option; + this.fakeData(option); + } else { + this.modalTypeOpened = null; + } + } + + private fromStringToIdExcel(categ: string): string { + const splitStr = categ.toLowerCase().split(' '); + for (let i = 1; i < splitStr.length; i++) { + splitStr[i] = splitStr[i].charAt(0).toUpperCase() + splitStr[i].substring(1); + } + return splitStr + .join('') + .normalize('NFD') + .replace(/[\u0300-\u036f'’°()]/g, '') + .replace(/[\s-]/g, ' ') + .replace('?', ''); + } + + // Fake service api + private mockService(module: Category[], titre: string, categ: any, nbCateg: number): void { + const category = new Category({ name: titre, modules: [] }); + for (let i = 0; i < nbCateg; i++) { + category.modules.push(new Module(categ.id + i, categ.name + i)); + } + module.push(category); + } + + // Get the correct list of checkbox/modules depending on the type of modal. + private fakeData(option: string): void { + if (option === TypeModal[0]) { + this.mockService(this.categories, 'Accompagnement des démarches', { name: 'CAF', id: 5 }, 7); + } else if (option === TypeModal[1]) { + forkJoin([this.searchService.getCategoriesFormations(), this.searchService.getFakeCounterModule()]).subscribe( + (res) => { + const categories: Category[] = res[0]; + const structureCounter: StructureCounter[] = res[1]; + categories.forEach((category) => { + category = this.searchService.setCountModules(category, structureCounter); + this.categories.push(category); + }); + } + ); + } else if (option === TypeModal[2]) { + this.mockService( + this.categories, + 'Équipements', + { name: 'Accès à des revues ou livres infoirmatiques numériques', id: 1 }, + 8 + ); + this.mockService(this.categories, "Modalité d'accueil", { name: 'Matériel mis à dispostion', id: 2 }, 6); + this.mockService(this.categories, "Type d'acteurs", { name: 'Lieux de médiation (Pimms, assos...)', id: 3 }, 5); + this.mockService(this.categories, 'Publics', { name: 'Langues étrangères autres qu’anglais', id: 4 }, 12); + this.mockService(this.categories, 'Labelisation', { name: 'Prescripteur du Pass Numérique', id: 5 }, 6); + this.mockService(this.categories, 'Type de structure', { name: 'Espace de co-working', id: 6 }, 6); + } + } +} diff --git a/src/app/structure-list/components/structure-details/structure-details.component.html b/src/app/structure-list/components/structure-details/structure-details.component.html index 03cbfe8704900a3316bba2195f2312e9c939e520..ba303e734c3a332a6c018c34b0a1cfbaf1b1aaaf 100644 --- a/src/app/structure-list/components/structure-details/structure-details.component.html +++ b/src/app/structure-list/components/structure-details/structure-details.component.html @@ -12,7 +12,7 @@ <div fxLayout="column" fxFlex="60%"> <div *ngIf="structure.voie" fxLayout="row" fxLayoutAlign="none center"> <em class="ico-marker-pin-sm absolute"></em> - <p>{{ structure.voie }}</p> + <p>{{ structure.n }} {{ structure.address }}</p> </div> <div *ngIf="structure.siteWeb" fxLayout="row" fxLayoutAlign="none center"> <em class="ic-globe-alt"></em> diff --git a/src/app/structure-list/components/structure-details/structure-details.component.scss b/src/app/structure-list/components/structure-details/structure-details.component.scss index 7ad3492d412b2f7431e901d2b64018ec4d0d62a6..05793b4f6f9b6e1960dccdd2d8fe8772054f54d4 100644 --- a/src/app/structure-list/components/structure-details/structure-details.component.scss +++ b/src/app/structure-list/components/structure-details/structure-details.component.scss @@ -12,7 +12,7 @@ left: 0; max-width: 980px; width: 100%; - height: calc(100vh - #{$header-height} - #{$footer-height} - 36px); + height: calc(100vh - #{$header-height} - #{$footer-height}); padding: 18px 24px; overflow: auto; } diff --git a/src/app/structure-list/enum/typeModal.enum.ts b/src/app/structure-list/enum/typeModal.enum.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab391e2f6ccbeeb72f81aea06ff806c764a27d2e --- /dev/null +++ b/src/app/structure-list/enum/typeModal.enum.ts @@ -0,0 +1,5 @@ +export enum TypeModal { + accompaniment, + training, + moreFilters, +} diff --git a/src/app/structure-list/models/category.model.ts b/src/app/structure-list/models/category.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..0ffaec8dc7926d5db7fba0e622f189e82b49351b --- /dev/null +++ b/src/app/structure-list/models/category.model.ts @@ -0,0 +1,12 @@ +import { Module } from './module.model'; + +export class Category { + name: string; + modules: Module[]; + + constructor(obj?: any) { + Object.assign(this, obj, { + modules: obj && obj.modules ? obj.modules.map((module) => new Module(module.id, module.text)) : null, + }); + } +} diff --git a/src/app/structure-list/models/filter.model.ts b/src/app/structure-list/models/filter.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..826633f504a445e6ff4c829fec405255a291e9dd --- /dev/null +++ b/src/app/structure-list/models/filter.model.ts @@ -0,0 +1,11 @@ +export class Filter { + name: string; + value: string; + isStrict: boolean; + + constructor(name: string, value: any, isStrict: boolean) { + this.name = name; + this.value = value.toString(); + this.isStrict = isStrict; + } +} diff --git a/src/app/structure-list/models/module.model.ts b/src/app/structure-list/models/module.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..340c04092a730edcaa4a4a1f38d6778ee94064c1 --- /dev/null +++ b/src/app/structure-list/models/module.model.ts @@ -0,0 +1,10 @@ +export class Module { + id: number; + text: string; + count: number; + + constructor(id: number, text: string) { + this.id = id; + this.text = text; + } +} diff --git a/src/app/structure-list/models/structureCounter.model.ts b/src/app/structure-list/models/structureCounter.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..d208192f08299466b39fc4659fba3574804d04c4 --- /dev/null +++ b/src/app/structure-list/models/structureCounter.model.ts @@ -0,0 +1,8 @@ +export class StructureCounter { + id: number; + count: number; + + constructor(obj?: any) { + Object.assign(this, obj); + } +} diff --git a/src/app/structure-list/services/search.service.spec.ts b/src/app/structure-list/services/search.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..4da5c595424448853477d2530cbc4c68d0fc84ed --- /dev/null +++ b/src/app/structure-list/services/search.service.spec.ts @@ -0,0 +1,37 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { SearchService } from './search.service'; +import { StructureCounter } from '../models/structureCounter.model'; +import { Category } from '../models/category.model'; +import { Module } from '../models/module.model'; + +describe('SearchService', () => { + let service: SearchService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + service = TestBed.inject(SearchService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should return category with number of modules inside', () => { + const structureCount: StructureCounter[] = [ + { id: 176, count: 2 }, + { id: 172, count: 1 }, + { id: 173, count: 1 }, + ]; + const m1: Module = { id: 176, text: 'strm1', count: 0 }; + const m2: Module = { id: 173, text: 'strm2', count: 0 }; + const m3: Module = { id: 172, text: 'strm3', count: 0 }; + const category: Category = { name: 'strCateg', modules: [m1, m2, m3] }; + const result = service.setCountModules(category, structureCount); + expect(result.modules[0].count).toBe(2); + expect(result.modules[1].count).toBe(1); + expect(result.modules[2].count).toBe(1); + }); +}); diff --git a/src/app/structure-list/services/search.service.ts b/src/app/structure-list/services/search.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..1b6a33b59d159fd9f30a32e1b3051971819e67a2 --- /dev/null +++ b/src/app/structure-list/services/search.service.ts @@ -0,0 +1,36 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { Category } from '../models/category.model'; +import { Module } from '../models/module.model'; +import { StructureCounter } from '../models/structureCounter.model'; + +@Injectable({ + providedIn: 'root', +}) +export class SearchService { + constructor(private http: HttpClient) {} + + public getCategoriesFormations(): Observable<Category[]> { + return this.http + .get('/api/CategoriesFormations') + .pipe(map((data: any[]) => data.map((item) => new Category(item)))); + } + + public getFakeCounterModule(): Observable<StructureCounter[]> { + return this.http + .get('/api/structures/count') + .pipe(map((data: any[]) => data.map((item) => new StructureCounter(item)))); + } + public setCountModules(category: Category, structureCountTab: StructureCounter[]): Category { + category.modules.forEach((m: Module) => { + for (let i = 0; i < structureCountTab.length; i++) { + if (structureCountTab[i].id === m.id) { + m.count = structureCountTab[i].count; + } + } + }); + return category; + } +} diff --git a/src/app/structure-list/structure-list.component.html b/src/app/structure-list/structure-list.component.html index feb355d0a94e9cff4b067971827400f5e9e66df4..64ed26a4c875ee1b0f36b0386a084eb8e0ee585c 100644 --- a/src/app/structure-list/structure-list.component.html +++ b/src/app/structure-list/structure-list.component.html @@ -1,6 +1,9 @@ -<app-recherche></app-recherche> -<span class="nbStructuresLabel">{{ structureList.length }} structures</span> -<div (mouseout)="mouseOut()"> +<div class="topBlock"> + <app-structure-list-search (searchEvent)="fetchResults($event)"></app-structure-list-search> + <span class="nbStructuresLabel">{{ structureList.length }} structures</span> +</div> + +<div class="listCard" (mouseout)="mouseOut()"> <app-card *ngFor="let structure of structureList" [structure]="structure" @@ -8,6 +11,7 @@ (hover)="handleCardHover($event)" ></app-card> </div> + <app-structure-details *ngIf="showStructureDetails" [structure]="structure" diff --git a/src/app/structure-list/structure-list.scss b/src/app/structure-list/structure-list.component.scss similarity index 71% rename from src/app/structure-list/structure-list.scss rename to src/app/structure-list/structure-list.component.scss index 68c10dffe4b5a6c42bab3f90bd9513695bfdd215..ee31dcc056b43f50abb653733d9be53ee293cf92 100644 --- a/src/app/structure-list/structure-list.scss +++ b/src/app/structure-list/structure-list.component.scss @@ -1,5 +1,5 @@ -@import '../../assets/scss/icons'; @import '../../assets/scss/color'; +@import '../../assets/scss/icons'; @import '../../assets/scss/typography'; .nbStructuresLabel { @@ -8,3 +8,10 @@ display: flex; align-items: center; } +.listCard { + overflow-y: auto; + padding: 0 25px; +} +.topBlock { + padding: 0 25px; +} diff --git a/src/app/structure-list/structure-list.component.ts b/src/app/structure-list/structure-list.component.ts index 6c79653923eab6250e2ccb4783891cb7237bf7ca..1a849b45becdd23db725f0ba451244cae03749ea 100644 --- a/src/app/structure-list/structure-list.component.ts +++ b/src/app/structure-list/structure-list.component.ts @@ -1,23 +1,28 @@ import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Filter } from './models/filter.model'; import { Structure } from '../models/structure.model'; import { GeoJson } from '../map/models/geojson.model'; @Component({ selector: 'app-structure-list', templateUrl: './structure-list.component.html', - styleUrls: ['./structure-list.scss'], + styleUrls: ['./structure-list.component.scss'], }) export class StructureListComponent implements OnInit { @Input() public structureList: Structure[]; + @Output() searchEvent = new EventEmitter(); @Input() public location: GeoJson; @Output() public displayMapMarkerId: EventEmitter<Array<number>> = new EventEmitter<Array<number>>(); @Output() public hoverOut: EventEmitter<Array<number>> = new EventEmitter<Array<number>>(); @Output() public selectedMarkerId: EventEmitter<number> = new EventEmitter<number>(); public showStructureDetails = false; public structure: Structure; - constructor() {} + constructor() {} ngOnInit(): void {} + public fetchResults(filters: Filter[]): void { + this.searchEvent.emit(filters); + } public showDetails(event: Structure): void { this.showStructureDetails = true; this.structure = event; diff --git a/src/assets/scss/_buttons.scss b/src/assets/scss/_buttons.scss new file mode 100644 index 0000000000000000000000000000000000000000..4d203db525ff5ec4351c8e405f3ddf4b72db43a8 --- /dev/null +++ b/src/assets/scss/_buttons.scss @@ -0,0 +1,31 @@ +@import './color'; +@import './shapes'; + +@mixin btn-filter { + background: $white; + height: 40px; + width: 190px; + border: 1px solid $grey-6; + padding: 3px 16px 3px 16px; + outline: none; + border-radius: 4px; + cursor: pointer; +} +@mixin btn-search { + background: $white; + height: 34px; + border: none; + color: $primary-color; + padding: 3px 16px 3px 16px; + outline: none; + cursor: pointer; +} +@mixin btn-search-filter { + background: $secondary-color; + height: 40px; + border: none; + color: $white; + padding: 3px 16px 3px 16px; + outline: none; + cursor: pointer; +} diff --git a/src/assets/scss/_icons.scss b/src/assets/scss/_icons.scss index ea957e5c90e0d9a36db6ab1f04ffe53750dded52..f1334bbc885333ce0fb9a8226e644e3d5b4e6149 100644 --- a/src/assets/scss/_icons.scss +++ b/src/assets/scss/_icons.scss @@ -46,6 +46,26 @@ border-radius: 50%; } } +.ico-pin-search { + width: 18px; + height: 18px; + border-radius: 50% 50% 50% 0; + -webkit-transform: rotate(-45deg); + transform: rotate(-45deg); + &:before { + content: ''; + position: absolute; + left: 5px; + top: 6px; + width: 7px; + height: 7px; + border-radius: 4px; + background-color: white; + } + &.blue { + background-color: $secondary-color; + } +} .ico-dot-available { height: 12px; width: 12px; @@ -152,6 +172,26 @@ } } +.ico-close { + width: 15px; + height: 13px; + text-align: center; + &:before { + transform: rotate(45deg); + } + &:after { + transform: rotate(-45deg); + } +} +.ico-close:before, +.ico-close:after { + position: absolute; + content: ''; + height: 14px; + width: 2px; + background-color: $grey-6; +} + .ic-arrow-left { box-sizing: border-box; position: relative; diff --git a/src/assets/scss/_inputs.scss b/src/assets/scss/_inputs.scss new file mode 100644 index 0000000000000000000000000000000000000000..dac479e10cf68b8bfad2187cdfb5feff2f38e9da --- /dev/null +++ b/src/assets/scss/_inputs.scss @@ -0,0 +1,9 @@ +@mixin input-search { + width: 100%; + border: none; + padding-left: 10px; + text-overflow: ellipsis; + color: $grey-1; + outline: none; + font-style: italic; +} diff --git a/src/assets/scss/_shapes.scss b/src/assets/scss/_shapes.scss index 21c498f0589f3ee7ff3511ae8b11a825baeba1ef..fec384566eade89e0d916b65b1cf54f3845054fe 100644 --- a/src/assets/scss/_shapes.scss +++ b/src/assets/scss/_shapes.scss @@ -33,3 +33,23 @@ $mat-tab-shadow: 0px 2px 7px rgba(0, 0, 0, 0.25); background-repeat: no-repeat; background-position-y: 12px; } + +@mixin background-hash { + background: linear-gradient( + -45deg, + $grey-2 2.5%, + $white 2.5%, + $white 47.5%, + $grey-2 47.5%, + $grey-2 52.5%, + $white 52.5%, + $white 97.5%, + $grey-2 97.5% + ); + background-size: 5px 5px; + background-position: 25px 25px; + padding: 0 0 6px 6px; + .body-wrap { + background-color: $white; + } +} diff --git a/src/styles.scss b/src/styles.scss index c100a71c041a50d8288f0f65eb0ebc365033d001..e9f97287370120b4ff70c628cd068c872d1cb05f 100644 --- a/src/styles.scss +++ b/src/styles.scss @@ -74,6 +74,61 @@ a { } } +/** Checkboxes **/ + +.checkbox { + list-style-type: none; + input { + opacity: 0; + display: none; + &:checked ~ .customCheck { + background-color: $primary-color; + border-color: transparent; + } + &:checked ~ .customCheck:after { + display: block; + } + } + label { + align-items: center; + grid-template-columns: min-content auto; + display: inline-grid; + cursor: pointer; + } + .label { + padding-left: 8px; + @include cn-regular-14; + } + .customCheck { + display: inline-grid; + width: 18px; + height: 18px; + background-color: $white; + border: 1px solid $grey; + cursor: pointer; + position: relative; + + top: 0; + left: 0; + &:hover { + background-color: $grey-6; + } + &:after { + content: ''; + position: absolute; + display: none; + left: 7px; + top: 3px; + width: 4px; + height: 8px; + border: solid $white; + border-width: 0 2px 2px 0; + transform: rotate(45deg); + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + } + } +} // Layout .w-100 { width: 100%;