Add basic search ui
This commit is contained in:
parent
fd325fa005
commit
dc7a1d63dc
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -1,13 +1,36 @@
|
||||
<mat-card class="search-container">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Start</mat-label>
|
||||
<input matInput type="date">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>End</mat-label>
|
||||
<input matInput type="date">
|
||||
</mat-form-field>
|
||||
<button (click)="onSearch()" color="primary" mat-flat-button>Search
|
||||
<section class="group">
|
||||
<h2>When is your trip?</h2>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Start</mat-label>
|
||||
<input [(ngModel)]="from" matInput required type="date">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>End</mat-label>
|
||||
<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>
|
||||
</button>
|
||||
</mat-card>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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)}});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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);
|
||||
|
||||
}
|
||||
|
||||
@ -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[];
|
||||
}
|
||||
|
||||
@ -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
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
"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"
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user