Merge branch 'feature/preset-driven-search' into 'develop'

Add basic search ui

See merge request tjohn/cc-data!9
This commit is contained in:
Patrick Gebhardt 2020-06-17 19:26:07 +02:00
commit 632bc801fb
14 changed files with 5064 additions and 32 deletions

View File

@ -1,10 +1,16 @@
import { Component } from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {PresetService} from './services/preset.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'] styleUrls: ['./app.component.scss']
}) })
export class AppComponent { export class AppComponent implements OnInit {
title = 'Travel optimizer'; constructor(private ps: PresetService) {
}
ngOnInit(): void {
this.ps.initialize().catch(e => console.log(e));
}
} }

View File

@ -20,6 +20,9 @@ import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {TranslateModule, TranslateService} from '@ngx-translate/core'; import {TranslateModule, TranslateService} from '@ngx-translate/core';
// @ts-ignore // @ts-ignore
import * as enLang from '../assets/i18n/en.json'; import * as enLang from '../assets/i18n/en.json';
import {HttpClientModule} from '@angular/common/http';
import {MatButtonToggleModule, MatCheckboxModule} from '@angular/material';
import {FormsModule} from '@angular/forms';
@NgModule({ @NgModule({
@ -43,7 +46,11 @@ import * as enLang from '../assets/i18n/en.json';
MatInputModule, MatInputModule,
MatSelectModule, MatSelectModule,
MatProgressSpinnerModule, MatProgressSpinnerModule,
TranslateModule.forRoot() TranslateModule.forRoot(),
HttpClientModule,
MatCheckboxModule,
FormsModule,
MatButtonToggleModule
], ],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]

View File

@ -1,13 +1,36 @@
<mat-card class="search-container"> <mat-card class="search-container">
<mat-form-field appearance="outline"> <section class="group">
<mat-label>Start</mat-label> <h2>When is your trip?</h2>
<input matInput type="date"> <mat-form-field appearance="outline">
</mat-form-field> <mat-label>Start</mat-label>
<mat-form-field appearance="outline"> <input [(ngModel)]="from" matInput required type="date">
<mat-label>End</mat-label> </mat-form-field>
<input matInput type="date"> <mat-form-field appearance="outline">
</mat-form-field> <mat-label>End</mat-label>
<button (click)="onSearch()" color="primary" mat-flat-button>Search <input [(ngModel)]="to" matInput required type="date">
</mat-form-field>
</section>
<section>
<h2>What would you prefer?</h2>
<div *ngFor="let key of multiPresetsKeys" class="sub-group">
<span class="lable">{{key|translate}}:</span><br>
<mat-button-toggle-group [(ngModel)]="multiPresetSelection[key]" [value]="undefined">
<mat-button-toggle *ngFor="let preset of multiPresets.get(key)"
[value]="preset.preset_id">{{preset.tag_label|translate}}</mat-button-toggle>
</mat-button-toggle-group>
</div>
</section>
<section>
<h2>Whats most important to you?</h2>
<div class="vertical">
<mat-checkbox *ngFor="let preset of singlePresets"
[(ngModel)]="singlePresetSelection[preset.preset_id]">{{preset.tag_label|translate}}</mat-checkbox>
</div>
</section>
<button (click)="onSearch()" class="search-btn" color="primary" mat-flat-button>Search
<mat-icon matSuffix>search</mat-icon> <mat-icon matSuffix>search</mat-icon>
</button> </button>
</mat-card> </mat-card>

View File

@ -1,4 +1,28 @@
.search-container { .search-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
> .group {
flex: 0 0 auto;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
> .search-btn {
margin-top: 1rem;
}
}
.sub-group {
margin-bottom: 0.5rem;
> .label {
font-size: large;
}
}
.vertical {
display: flex;
flex-direction: column;
} }

View File

@ -2,6 +2,9 @@ import {Component, OnInit} from '@angular/core';
import {Router} from '@angular/router'; import {Router} from '@angular/router';
import {Query} from '../../interfaces/search-request.interface'; import {Query} from '../../interfaces/search-request.interface';
import {objToBase64} from '../../utils/base64conversion'; import {objToBase64} from '../../utils/base64conversion';
import {PresetService} from '../../services/preset.service';
import {Preset} from '../../interfaces/preset.interface';
import {formatDate} from '@angular/common';
@Component({ @Component({
selector: 'app-search-input', selector: 'app-search-input',
@ -10,7 +13,32 @@ import {objToBase64} from '../../utils/base64conversion';
}) })
export class SearchInputComponent implements OnInit { export class SearchInputComponent implements OnInit {
constructor(private router: Router) { presets: Preset[];
singlePresets: Preset[];
multiPresets: Map<string, Preset[]>;
multiPresetsKeys: string[];
from: string;
to: string;
singlePresetSelection = {};
multiPresetSelection = {};
constructor(private router: Router, private ps: PresetService) {
this.presets = ps.presets;
this.singlePresets = ps.singlePresets;
this.multiPresets = ps.multiPresets;
this.multiPresetsKeys = [...ps.multiPresets.keys()];
const from = new Date();
const to = new Date();
to.setDate(from.getDate() + 7);
this.from = formatDate(from, 'yyyy-MM-dd', 'en-GB');
this.to = formatDate(to, 'yyyy-MM-dd', 'en-GB');
for (const preset of this.singlePresets) {
this.singlePresetSelection[preset.preset_id] = false;
}
} }
ngOnInit() { ngOnInit() {
@ -18,11 +46,22 @@ export class SearchInputComponent implements OnInit {
async onSearch() { async onSearch() {
const query: Query = { const query: Query = {
from: Date.now(), from: new Date(this.from).getTime(),
to: Date.now(), to: new Date(this.to).getTime(),
}; };
for (const preset of this.singlePresets) {
if (this.singlePresetSelection[preset.preset_id]) {
query[preset.parameter] = preset.value;
}
}
for (const key of this.multiPresetsKeys) {
if (this.multiPresetSelection[key]) {
query[key] = this.presets.find(preset => preset.preset_id === this.multiPresetSelection[key]).value;
}
}
await this.router.navigate(['/search'], {queryParams: {q: objToBase64(query)}}); await this.router.navigate(['/search'], {queryParams: {q: objToBase64(query)}});
} }
} }

View File

@ -1,5 +1,7 @@
<app-search-input></app-search-input> <app-search-input></app-search-input>
<span #result></span>
<mat-card *ngIf="results"> <mat-card *ngIf="results">
<h2 matCardTitle>Suchergebnisse ({{results.length}}):</h2> <h2 matCardTitle>Suchergebnisse ({{results.length}}):</h2>
<mat-card *ngFor="let result of results" class="resultCard"> <mat-card *ngFor="let result of results" class="resultCard">

View File

@ -1,4 +1,4 @@
import {Component, OnInit} from '@angular/core'; import {Component, ElementRef, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute} from '@angular/router'; import {ActivatedRoute} from '@angular/router';
import {Result} from '../../interfaces/result.interface'; import {Result} from '../../interfaces/result.interface';
import {MOCK_RESULT} from '../../mock/mock-data'; import {MOCK_RESULT} from '../../mock/mock-data';
@ -13,6 +13,9 @@ export class SearchComponent implements OnInit {
queryString: string; queryString: string;
results: Result[]; results: Result[];
@ViewChild('result', {static: false})
resultDiv: ElementRef;
constructor(private route: ActivatedRoute) { constructor(private route: ActivatedRoute) {
} }
@ -24,6 +27,7 @@ export class SearchComponent implements OnInit {
// Mock results // Mock results
setTimeout(() => { setTimeout(() => {
this.results = MOCK_RESULT; this.results = MOCK_RESULT;
this.resultDiv.nativeElement.scrollIntoView({behavior: 'smooth', block: 'start'});
}, 1000); }, 1000);
} }

View File

@ -1,7 +1,7 @@
/** Represents the structure of a search preset. */ /** Represents the structure of a search preset. */
export interface Preset { export interface Preset {
preset_id: number; preset_id: number;
preset_name: number; tag_label: string;
parameter: string; parameter: string;
value: number[]; value: number[];
} }

View File

@ -8,7 +8,7 @@ export interface Region {
country: string; country: string;
/** Short description of the region */ /** Short description of the region */
description: string; description: string;
/** Max temperature means per month */ /** Min temperature means per month */
temperature_mean_max: number[]; temperature_mean_max: number[];
/** Monthly precipitation */ /** Monthly precipitation */
precipitation: number[]; precipitation: number[];
@ -20,6 +20,14 @@ export interface Region {
food_costs: number; food_costs: number;
/** Average alcohol costs per day */ /** Average alcohol costs per day */
alcohol_costs: number; alcohol_costs: number;
/** Average water costs per day */
water_costs: number;
/** Average costs for local transportation per day */
local_transportation_costs: number;
/** Average entertainment costs per day */
entertainment_costs: number;
/** Average accommodation costs per day */ /** Average accommodation costs per day */
accommodation_costs: number; accommodation_costs: number;
/** Average costs per day */
average_per_day_costs: number;
} }

File diff suppressed because it is too large Load Diff

View File

@ -3,13 +3,14 @@ import {HttpClient, HttpParams} from '@angular/common/http';
import {Preset} from '../interfaces/preset.interface'; import {Preset} from '../interfaces/preset.interface';
import {Result} from '../interfaces/result.interface'; import {Result} from '../interfaces/result.interface';
import {Region} from '../interfaces/region.interface'; import {Region} from '../interfaces/region.interface';
import {MOCK_PRESETS, MOCK_REGIONS, MOCK_RESULT} from '../mock/mock-data';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DataService { export class DataService {
private readonly API_URL = 'https://example.com/api/v1/'; private readonly API_URL = 'https://example.com/api/v1';
constructor(private http: HttpClient) { constructor(private http: HttpClient) {
} }
@ -18,18 +19,27 @@ export class DataService {
const params = new HttpParams(); const params = new HttpParams();
params.append('q', query); params.append('q', query);
return this.http.get<Result[]>(this.API_URL + 'search', {params}).toPromise(); // return this.http.get<Result[]>(this.API_URL + '/search', {params}).toPromise();
return new Promise<Result[]>(resolve => {
resolve(MOCK_RESULT);
});
} }
public getAllPresets(): Promise<Preset[]> { public getAllPresets(): Promise<Preset[]> {
return this.http.get<Preset[]>(this.API_URL + 'preset').toPromise(); // return this.http.get<Preset[]>(this.API_URL + '/search/presets').toPromise();
return new Promise<Preset[]>(resolve => {
resolve(MOCK_PRESETS);
});
} }
public getAllRegions(): Promise<Region[]> { public getAllRegions(): Promise<Region[]> {
return this.http.get<Region[]>(this.API_URL + 'region').toPromise(); // return this.http.get<Region[]>(this.API_URL + '/regions').toPromise();
return new Promise<Region[]>(resolve => {
resolve(MOCK_REGIONS);
});
} }
public toMinMaxArray(min: number, max: number): number[] { public getRegion(id: number): Promise<Region> {
return [min, max]; return this.http.get<Region>(`${this.API_URL}/regions/${id}`).toPromise();
} }
} }

View File

@ -0,0 +1,12 @@
import {TestBed} from '@angular/core/testing';
import {PresetService} from './preset.service';
describe('PresetService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: PresetService = TestBed.get(PresetService);
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,37 @@
import {Injectable} from '@angular/core';
import {DataService} from './data.service';
import {Preset} from '../interfaces/preset.interface';
@Injectable({
providedIn: 'root'
})
export class PresetService {
presets: Preset[];
multiPresets: Map<string, Preset[]> = new Map<string, Preset[]>();
singlePresets: Preset[] = [];
constructor(private ds: DataService) {
}
async initialize() {
this.presets = await this.ds.getAllPresets();
const presetMap = new Map<string, Preset[]>();
for (const preset of this.presets) {
if (presetMap.has(preset.parameter)) {
presetMap.get(preset.parameter).push(preset);
} else {
presetMap.set(preset.parameter, [preset]);
}
}
for (const key of presetMap.keys()) {
if (presetMap.get(key).length > 1) {
this.multiPresets.set(key, presetMap.get(key));
} else {
this.singlePresets.push(presetMap.get(key)[0]);
}
}
}
}

View File

@ -1,10 +1,26 @@
{ {
"temperature_mean": "Temperature Average", "temperature_mean_max": "Temperature Average",
"temperature": "Temperature", "temperature": "Temperature",
"raindays": "Rainy days", "rain_days": "Rainy days",
"sunhours": "Sunny hours", "sun_hours": "Sunny hours",
"precipitation": "Precipitation", "precipitation": "Precipitation",
"humidity": "Humidity", "humidity": "Humidity",
"alcohol": "Alcohol coasts per day", "alcohol_costs": "Alcohol coasts per day",
"food": "Food costs per day" "food_costs": "Food costs per day",
"cheap_alcohol": "Cheap alcohol",
"cheap_food": "Cheap food",
"cheap_water": "Cheap water",
"cheap_transportations": "Cheap pubic transport",
"cheap_entertainment": "Cheap entertainment",
"warm": "warm",
"chilly": "chilly",
"mild:": "mild",
"cold": "cold",
"sunny": "sunny",
"dark": "dark",
"almost_no_rain": "almost none",
"little_rain": "little",
"floodlike_rain": "flooding",
"few_raindays": "few",
"many_raindays": "many"
} }