Merge branch 'feature/preset-driven-search' into 'develop'
Add basic search ui See merge request tjohn/cc-data!9
This commit is contained in:
commit
632bc801fb
@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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]
|
||||||
|
|||||||
@ -1,13 +1,36 @@
|
|||||||
<mat-card class="search-container">
|
<mat-card class="search-container">
|
||||||
|
<section class="group">
|
||||||
|
<h2>When is your trip?</h2>
|
||||||
<mat-form-field appearance="outline">
|
<mat-form-field appearance="outline">
|
||||||
<mat-label>Start</mat-label>
|
<mat-label>Start</mat-label>
|
||||||
<input matInput type="date">
|
<input [(ngModel)]="from" matInput required type="date">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<mat-form-field appearance="outline">
|
<mat-form-field appearance="outline">
|
||||||
<mat-label>End</mat-label>
|
<mat-label>End</mat-label>
|
||||||
<input matInput type="date">
|
<input [(ngModel)]="to" matInput required type="date">
|
||||||
</mat-form-field>
|
</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>
|
<mat-icon matSuffix>search</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)}});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
frontend/src/app/services/preset.service.spec.ts
Normal file
12
frontend/src/app/services/preset.service.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
37
frontend/src/app/services/preset.service.ts
Normal file
37
frontend/src/app/services/preset.service.ts
Normal 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user