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({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'Travel optimizer';
export class AppComponent implements OnInit {
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';
// @ts-ignore
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({
@ -43,7 +46,11 @@ import * as enLang from '../assets/i18n/en.json';
MatInputModule,
MatSelectModule,
MatProgressSpinnerModule,
TranslateModule.forRoot()
TranslateModule.forRoot(),
HttpClientModule,
MatCheckboxModule,
FormsModule,
MatButtonToggleModule
],
providers: [],
bootstrap: [AppComponent]

View File

@ -1,13 +1,36 @@
<mat-card class="search-container">
<section class="group">
<h2>When is your trip?</h2>
<mat-form-field appearance="outline">
<mat-label>Start</mat-label>
<input matInput type="date">
<input [(ngModel)]="from" matInput required type="date">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>End</mat-label>
<input matInput type="date">
<input [(ngModel)]="to" matInput required type="date">
</mat-form-field>
<button (click)="onSearch()" color="primary" mat-flat-button>Search
</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>
</button>
</mat-card>

View File

@ -1,4 +1,28 @@
.search-container {
display: flex;
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 {Query} from '../../interfaces/search-request.interface';
import {objToBase64} from '../../utils/base64conversion';
import {PresetService} from '../../services/preset.service';
import {Preset} from '../../interfaces/preset.interface';
import {formatDate} from '@angular/common';
@Component({
selector: 'app-search-input',
@ -10,7 +13,32 @@ import {objToBase64} from '../../utils/base64conversion';
})
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() {
@ -18,11 +46,22 @@ export class SearchInputComponent implements OnInit {
async onSearch() {
const query: Query = {
from: Date.now(),
to: Date.now(),
from: new Date(this.from).getTime(),
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)}});
}
}

View File

@ -1,5 +1,7 @@
<app-search-input></app-search-input>
<span #result></span>
<mat-card *ngIf="results">
<h2 matCardTitle>Suchergebnisse ({{results.length}}):</h2>
<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 {Result} from '../../interfaces/result.interface';
import {MOCK_RESULT} from '../../mock/mock-data';
@ -13,6 +13,9 @@ export class SearchComponent implements OnInit {
queryString: string;
results: Result[];
@ViewChild('result', {static: false})
resultDiv: ElementRef;
constructor(private route: ActivatedRoute) {
}
@ -24,6 +27,7 @@ export class SearchComponent implements OnInit {
// Mock results
setTimeout(() => {
this.results = MOCK_RESULT;
this.resultDiv.nativeElement.scrollIntoView({behavior: 'smooth', block: 'start'});
}, 1000);
}

View File

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

View File

@ -8,7 +8,7 @@ export interface Region {
country: string;
/** Short description of the region */
description: string;
/** Max temperature means per month */
/** Min temperature means per month */
temperature_mean_max: number[];
/** Monthly precipitation */
precipitation: number[];
@ -20,6 +20,14 @@ export interface Region {
food_costs: number;
/** Average alcohol costs per day */
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 */
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 {Result} from '../interfaces/result.interface';
import {Region} from '../interfaces/region.interface';
import {MOCK_PRESETS, MOCK_REGIONS, MOCK_RESULT} from '../mock/mock-data';
@Injectable({
providedIn: 'root'
})
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) {
}
@ -18,18 +19,27 @@ export class DataService {
const params = new HttpParams();
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[]> {
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[]> {
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[] {
return [min, max];
public getRegion(id: number): Promise<Region> {
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",
"raindays": "Rainy days",
"sunhours": "Sunny hours",
"rain_days": "Rainy days",
"sun_hours": "Sunny hours",
"precipitation": "Precipitation",
"humidity": "Humidity",
"alcohol": "Alcohol coasts per day",
"food": "Food costs per day"
"alcohol_costs": "Alcohol coasts 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"
}