Merge branch 'develop' of https://git.it.hs-heilbronn.de/tjohn/cc-data into feature/flights

This commit is contained in:
Timo John 2020-06-23 02:03:16 +02:00
commit 18463c3ca2
16 changed files with 411 additions and 94 deletions

View File

@ -5338,6 +5338,11 @@
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==", "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ==",
"dev": true "dev": true
}, },
"hammerjs": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/hammerjs/-/hammerjs-2.0.8.tgz",
"integrity": "sha1-BO93hiz/K7edMPdpIJWTAiK/YPE="
},
"handle-thing": { "handle-thing": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",

View File

@ -22,6 +22,7 @@
"@angular/platform-browser-dynamic": "~8.2.14", "@angular/platform-browser-dynamic": "~8.2.14",
"@angular/router": "~8.2.14", "@angular/router": "~8.2.14",
"@ngx-translate/core": "^12.1.2", "@ngx-translate/core": "^12.1.2",
"hammerjs": "^2.0.8",
"ngx-device-detector": "^1.4.5", "ngx-device-detector": "^1.4.5",
"rxjs": "~6.4.0", "rxjs": "~6.4.0",
"tslib": "^1.10.0", "tslib": "^1.10.0",

View File

@ -22,10 +22,15 @@ import {TranslateModule, TranslateService} from '@ngx-translate/core';
import * as enLang from '../assets/i18n/en.json'; import * as enLang from '../assets/i18n/en.json';
import {HttpClientModule} from '@angular/common/http'; import {HttpClientModule} from '@angular/common/http';
import { import {
MatBadgeModule,
MatButtonToggleModule, MatButtonToggleModule,
MatCheckboxModule, MatCheckboxModule,
MatDialogModule, MatDialogModule,
MatDividerModule, MatDividerModule,
MatRadioModule,
MatSliderModule,
MatSlideToggleModule,
MatStepperModule,
MatTabsModule, MatTabsModule,
MatTooltipModule MatTooltipModule
} from '@angular/material'; } from '@angular/material';
@ -41,6 +46,7 @@ import {ShareButtonComponent} from './components/share-button/share-button.compo
import {ShareDialogComponent} from './dialogs/share-dialog/share-dialog.component'; import {ShareDialogComponent} from './dialogs/share-dialog/share-dialog.component';
import {TeamComponent} from './containers/team/team.component'; import {TeamComponent} from './containers/team/team.component';
import {DeviceDetectorModule} from 'ngx-device-detector'; import {DeviceDetectorModule} from 'ngx-device-detector';
import {ToggleSliderComponent} from './components/toggle-slider/toggle-slider.component';
@NgModule({ @NgModule({
@ -59,7 +65,8 @@ import {DeviceDetectorModule} from 'ngx-device-detector';
BookmarkListComponent, BookmarkListComponent,
ShareButtonComponent, ShareButtonComponent,
ShareDialogComponent, ShareDialogComponent,
TeamComponent TeamComponent,
ToggleSliderComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -83,7 +90,12 @@ import {DeviceDetectorModule} from 'ngx-device-detector';
MatTooltipModule, MatTooltipModule,
MatDialogModule, MatDialogModule,
DeviceDetectorModule, DeviceDetectorModule,
MatTabsModule MatTabsModule,
MatBadgeModule,
MatStepperModule,
MatRadioModule,
MatSlideToggleModule,
MatSliderModule
], ],
providers: [], providers: [],
bootstrap: [AppComponent], bootstrap: [AppComponent],

View File

@ -14,7 +14,7 @@
<tr *ngFor="let score of result.scores" [ngClass]="{'undefined': score.value == undefined}"> <tr *ngFor="let score of result.scores" [ngClass]="{'undefined': score.value == undefined}">
<td> <td>
<div class="cell space"> <div class="cell space">
<mat-icon>{{PROPERTY_VIS_DEF[score.type].icon}}</mat-icon> <mat-icon>{{PROPERTY_VIS_DEF[score.type] ? PROPERTY_VIS_DEF[score.type].icon : 'bar_chart'}}</mat-icon>
<span>{{score.type|translate}}:</span> <span>{{score.type|translate}}:</span>
</div> </div>
</td> </td>
@ -25,7 +25,12 @@
</td> </td>
<td> <td>
<div class="cell"> <div class="cell">
<span>{{PROPERTY_VIS_DEF[score.type].unit}}</span> <span>{{PROPERTY_VIS_DEF[score.type] ? PROPERTY_VIS_DEF[score.type].unit : ''}}</span>
</div>
</td>
<td>
<div class="cell">
<span>({{score.score}})</span>
</div> </div>
</td> </td>
</tr> </tr>

View File

@ -1,42 +1,106 @@
<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-label>Start</mat-label>
<input (change)="checkDates()" [(ngModel)]="from" [min]="today" matInput required type="date">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>End</mat-label>
<input (change)="checkDates()" [(ngModel)]="to" [min]="from" matInput required type="date">
</mat-form-field>
</section>
<section> <span matCardTitle>Search</span>
<h2>What would you prefer?</h2>
<div *ngFor="let key of multiPresetsKeys" class="sub-group">
<span class="label">{{key|translate}}:</span><br>
<mat-button-toggle-group [ngModel]="multiPresetSelection[key]" [value]="undefined">
<mat-button-toggle
#btn
(click)="btn.checked = onMultiPresetSelect(preset)"
*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> <mat-tab-group #tabGroup [animationDuration]="'0'" [selectedIndex]="selectedTab">
<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()" [disabled]="!from || !to" class="search-btn" color="primary" mat-flat-button>Search <!-- Guided Search Tab -->
<mat-tab label="Guided">
<mat-vertical-stepper>
<mat-step>
<ng-template matStepLabel>When is your trip?</ng-template>
<div class="vertical-wrap">
<mat-form-field appearance="outline">
<mat-label>Start</mat-label>
<input (change)="checkDates()" [(ngModel)]="from" [min]="today" matInput required type="date">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>End</mat-label>
<input (change)="checkDates()" [(ngModel)]="to" [min]="from" matInput required type="date">
</mat-form-field>
</div>
</mat-step>
<mat-step>
<ng-template matStepLabel>Which climate would you prefer?</ng-template>
<div *ngFor="let key of multiPresetsKeys" class="sub-group">
<span class="label">{{key|translate}}:</span><br>
<mat-radio-group [ngModel]="multiPresetSelection[key]" [value]="undefined">
<mat-radio-button
#btn
(click)="btn.checked = onMultiPresetSelect(preset)"
*ngFor="let preset of multiPresets.get(key)"
[value]="preset.preset_id"
>{{preset.tag_label|translate}}</mat-radio-button>
</mat-radio-group>
</div>
</mat-step>
<mat-step>
<ng-template matStepLabel>What else is important to you?</ng-template>
<div class="vertical">
<mat-checkbox *ngFor="let preset of singlePresets"
[(ngModel)]="singlePresetSelection[preset.preset_id]">{{preset.tag_label|translate}}</mat-checkbox>
</div>
</mat-step>
</mat-vertical-stepper>
</mat-tab>
<!-- Advanced Search Tab -->
<mat-tab label="Advanced">
<!-- Date -->
<section class="group">
<span class="title">Date</span>
<div class=" content vertical-wrap">
<mat-form-field appearance="outline">
<mat-label>Start</mat-label>
<input (change)="checkDates()" [(ngModel)]="from" [min]="today" matInput required type="date">
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>End</mat-label>
<input (change)="checkDates()" [(ngModel)]="to" [min]="from" matInput required type="date">
</mat-form-field>
</div>
</section>
<!-- Text Filter -->
<section class="group">
<span class="title">Text</span>
<div class="content vertical-wrap">
<mat-form-field class="text-input">
<mat-label>Text search</mat-label>
<input [(ngModel)]="textFilter" matInput>
<button (click)="textFilter = ''" *ngIf="textFilter.length" mat-icon-button matSuffix>
<mat-icon>clear</mat-icon>
</button>
</mat-form-field>
<div class="horizontal space">
<span>Search in description </span>
<mat-slide-toggle [(ngModel)]="fullText"></mat-slide-toggle>
</div>
</div>
</section>
<!-- Climate Params -->
<section class="group">
<span class="title">Climate</span>
<app-toggle-slider [(model)]="temperatureMeanMax" [label]="'Max Temp'" [max]="45" [min]="0"></app-toggle-slider>
<app-toggle-slider [(model)]="precipitation" [label]="'Precipitation'" [max]="500"
[min]="0"></app-toggle-slider>
</section>
<!-- Financial -->
<section class="group">
<span class="title">Fincancial</span>
<app-toggle-slider [(model)]="accommodation" [label]="'Accommodation'" [max]="60" [min]="0"></app-toggle-slider>
</section>
</mat-tab>
</mat-tab-group>
<button (click)="onSearch(tabGroup.selectedIndex === 1)" [disabled]="!from || !to" 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

@ -2,27 +2,72 @@
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 { > .search-btn {
margin-top: 1rem; margin-top: 1rem;
} }
} }
.sub-group { .sub-group {
margin-bottom: 0.5rem; margin-bottom: 2rem;
display: flex;
flex-direction: column;
> .label { .label {
font-weight: bold; font-weight: bold;
} }
mat-radio-group {
display: flex;
flex-direction: column;
}
mat-radio-button {
margin: 0 1rem 0.5rem 0;
width: 40%;
}
}
.group {
display: flex;
flex-direction: column;
margin: 1rem 0;
> .title {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 0.5rem;
}
> .content {
margin: 0 2.5rem;
.text-input {
min-width: 14rem;
}
}
}
.horizontal {
display: flex;
flex-direction: row;
align-items: center;
&.space {
> * {
margin-right: 1rem;
}
}
} }
.vertical { .vertical {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.vertical-wrap {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-around;
}

View File

@ -14,6 +14,9 @@ import {SearchService} from '../../services/search.service';
}) })
export class SearchInputComponent implements OnInit { export class SearchInputComponent implements OnInit {
selectedTab = 0;
presets: Preset[]; presets: Preset[];
singlePresets: Preset[]; singlePresets: Preset[];
multiPresets: Map<string, Preset[]>; multiPresets: Map<string, Preset[]>;
@ -21,9 +24,18 @@ export class SearchInputComponent implements OnInit {
from: string; from: string;
to: string; to: string;
// Guided Search
singlePresetSelection = {}; singlePresetSelection = {};
multiPresetSelection = {}; multiPresetSelection = {};
// Advanced Search
textFilter = '';
fullText = false;
temperatureMeanMax: number;
precipitation: number;
accommodation: number;
readonly today = this.from = formatDate(new Date(), 'yyyy-MM-dd', 'en-GB'); readonly today = this.from = formatDate(new Date(), 'yyyy-MM-dd', 'en-GB');
constructor(private router: Router, private ps: PresetService, private ss: SearchService) { constructor(private router: Router, private ps: PresetService, private ss: SearchService) {
@ -43,41 +55,13 @@ export class SearchInputComponent implements OnInit {
this.multiPresets = this.ps.multiPresets; this.multiPresets = this.ps.multiPresets;
this.multiPresetsKeys = [...this.multiPresets.keys()]; this.multiPresetsKeys = [...this.multiPresets.keys()];
const prevInput = this.ss.loadSearchInput(); this.loadSearch();
if (prevInput) {
this.from = prevInput.from;
this.to = prevInput.to;
this.singlePresetSelection = prevInput.singlePresetSelection;
this.multiPresetSelection = prevInput.multiPresetSelection;
}
} }
async onSearch() { async onSearch(isAdvanced: boolean) {
const query: Query = { this.saveSearch(isAdvanced);
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;
}
}
this.ss.saveSearchInput({
from: this.from,
to: this.to,
singlePresetSelection: this.singlePresetSelection,
multiPresetSelection: this.multiPresetSelection
});
const query = isAdvanced ? this.getQueryFromAdvanced() : this.getQueryFromGuided();
await this.router.navigate(['/search'], {queryParams: {q: objToBase64(query)}}); await this.router.navigate(['/search'], {queryParams: {q: objToBase64(query)}});
} }
@ -107,4 +91,76 @@ export class SearchInputComponent implements OnInit {
this.to = formatDate(newToDate, 'yyyy-MM-dd', 'en-GB'); this.to = formatDate(newToDate, 'yyyy-MM-dd', 'en-GB');
} }
} }
private getQueryFromGuided(): Query {
const query: Query = {
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;
}
}
return query;
}
private getQueryFromAdvanced(): Query {
const query: Query = {
from: new Date(this.from).getTime(),
to: new Date(this.to).getTime(),
};
if (this.textFilter.length > 0) {
query.fulltext = this.fullText;
query.textfilter = this.textFilter;
}
query.temperature_mean_max = this.temperatureMeanMax ? [this.temperatureMeanMax, this.temperatureMeanMax] : undefined;
query.precipitation = this.precipitation ? [this.precipitation, this.precipitation] : undefined;
query.accommodation_costs = this.accommodation ? [this.accommodation, this.accommodation] : undefined;
return query;
}
private saveSearch(isAdvanced: boolean) {
this.ss.saveSearchInput({
wasAdvanced: isAdvanced,
from: this.from,
to: this.to,
singlePresetSelection: this.singlePresetSelection,
multiPresetSelection: this.multiPresetSelection,
fullText: this.fullText,
textFiler: this.textFilter,
tempMeanMax: this.temperatureMeanMax,
precipitation: this.precipitation,
accommodation: this.accommodation,
});
}
private loadSearch() {
const prevInput = this.ss.loadSearchInput();
if (prevInput) {
this.from = prevInput.from;
this.to = prevInput.to;
this.singlePresetSelection = prevInput.singlePresetSelection;
this.multiPresetSelection = prevInput.multiPresetSelection;
this.textFilter = prevInput.textFiler;
this.fullText = prevInput.fullText;
this.selectedTab = prevInput.wasAdvanced ? 1 : 0;
this.temperatureMeanMax = prevInput.tempMeanMax;
this.precipitation = prevInput.precipitation;
this.accommodation = prevInput.accommodation;
}
}
} }

View File

@ -0,0 +1,11 @@
<span>{{label}}</span>
<mat-slide-toggle (change)="onSlideToggleChange($event)" [(ngModel)]="enabled"></mat-slide-toggle>
<mat-slider
(click)="enabled=true"
[(value)]="value"
[disabled]="!enabled"
[max]="max"
[min]="min"
[step]="step"
[thumbLabel]="true"
></mat-slider>

View File

@ -0,0 +1,24 @@
:host {
display: flex;
flex-direction: row;
align-items: center;
}
span {
width: 33%;
min-width: 8rem;
margin-right: 0.5rem;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
mat-slide-toggle {
flex: 0 1 auto;
margin-right: 0.5rem;
}
mat-slider {
flex: 1 1 auto;
}

View File

@ -0,0 +1,25 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {ToggleSliderComponent} from './toggle-slider.component';
describe('ToggleSliderComponent', () => {
let component: ToggleSliderComponent;
let fixture: ComponentFixture<ToggleSliderComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ToggleSliderComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ToggleSliderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,42 @@
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
import {MatSlideToggleChange} from '@angular/material';
@Component({
selector: 'app-toggle-slider',
templateUrl: './toggle-slider.component.html',
styleUrls: ['./toggle-slider.component.scss']
})
export class ToggleSliderComponent implements OnInit {
enabled = false;
rawValue: number;
@Output() modelChange: EventEmitter<number> = new EventEmitter<number>();
@Input() min = 0;
@Input() max = 100;
@Input() step = 1;
@Input() label: string;
get value(): number {
return this.rawValue;
}
set value(value: number) {
this.rawValue = value;
this.modelChange.emit(value);
}
@Input()
set model(value: number) {
this.rawValue = value;
this.enabled = value !== undefined;
}
ngOnInit() {
}
onSlideToggleChange(event: MatSlideToggleChange) {
if (event.checked === false) {
this.modelChange.emit(undefined);
}
}
}

View File

@ -9,6 +9,10 @@
</div> </div>
</div> </div>
<div *ngIf="results && results.length === 0" class="note">
<mat-icon>error</mat-icon>
<span>No match found!</span>
</div>
<div *ngIf="!results" class="spinner"> <div *ngIf="!results" class="spinner">
<mat-spinner></mat-spinner> <mat-spinner></mat-spinner>
</div> </div>

View File

@ -15,6 +15,21 @@
> app-result { > app-result {
margin-bottom: 3rem; margin-bottom: 3rem;
} }
}
.note {
flex: 1 1 auto;
align-self: center;
font-size: 1.2rem;
margin: 1.5rem;
display: flex;
flex-direction: row;
align-items: center;
mat-icon {
margin-right: 0.5rem;
}
} }
.spinner { .spinner {

View File

@ -3,10 +3,16 @@ import {Result} from '../interfaces/result.interface';
import {DataService} from './data.service'; import {DataService} from './data.service';
export interface SearchInput { export interface SearchInput {
wasAdvanced: boolean;
from: string; from: string;
to: string; to: string;
singlePresetSelection: object; singlePresetSelection: object;
multiPresetSelection: object; multiPresetSelection: object;
textFiler: string;
fullText: boolean;
tempMeanMax: number;
precipitation: number;
accommodation: number;
} }
@Injectable({ @Injectable({

View File

@ -1,8 +1,10 @@
import { enableProdMode } from '@angular/core'; import {enableProdMode} from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module'; import {AppModule} from './app/app.module';
import { environment } from './environments/environment'; import {environment} from './environments/environment';
import 'hammerjs';
if (environment.production) { if (environment.production) {
enableProdMode(); enableProdMode();

View File

@ -55,20 +55,20 @@ $travopti-green: (
A400: #006400, A400: #006400,
A700: #004d00, A700: #004d00,
contrast: ( contrast: (
50: $dark-primary-text, 50: $light-primary-text,
100: $dark-primary-text, 100: $light-primary-text,
200: $dark-primary-text, 200: $light-primary-text,
300: $dark-primary-text, 300: $light-primary-text,
400: $dark-primary-text, 400: $light-primary-text,
500: $dark-primary-text, 500: $light-primary-text,
600: $light-primary-text, 600: $light-primary-text,
700: $light-primary-text, 700: $light-primary-text,
800: $light-primary-text, 800: $light-primary-text,
900: $light-primary-text, 900: $light-primary-text,
A100: $dark-primary-text, A100: $light-primary-text,
A200: $dark-primary-text, A200: $light-primary-text,
A400: $dark-primary-text, A400: $light-primary-text,
A700: $dark-primary-text, A700: $light-primary-text,
) )
); );