Add details to search results

This commit is contained in:
Patrick Gebhardt 2020-06-19 13:25:32 +02:00
parent c8c0b7f900
commit afb4234ce4
16 changed files with 271 additions and 105 deletions

View File

@ -27,6 +27,7 @@ import {RegionComponent} from './components/region/region.component';
import {ResultComponent} from './components/result/result.component'; import {ResultComponent} from './components/result/result.component';
import {RegionDetailsComponent} from './containers/region-details/region-details.component'; import {RegionDetailsComponent} from './containers/region-details/region-details.component';
import {GraphComponent} from './components/graph/graph.component'; import {GraphComponent} from './components/graph/graph.component';
import {RegionStatsComponent} from './components/region-stats/region-stats.component';
@NgModule({ @NgModule({
@ -39,7 +40,8 @@ import {GraphComponent} from './components/graph/graph.component';
RegionComponent, RegionComponent,
ResultComponent, ResultComponent,
RegionDetailsComponent, RegionDetailsComponent,
GraphComponent GraphComponent,
RegionStatsComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -0,0 +1,20 @@
<table>
<tr *ngFor="let prop of shownKeys">
<td>
<div class="cell space">
<mat-icon>{{PROPERTY_VIS_DEF[prop].icon}}</mat-icon>
<span>{{prop|translate}}:</span>
</div>
</td>
<td>
<div class="cell right">
<span>{{region[prop] ? (region[prop]|number:'1.2-2') : 'N/A'}}</span>
</div>
</td>
<td>
<div class="cell">
<span>{{PROPERTY_VIS_DEF[prop].unit}}</span>
</div>
</td>
</tr>
</table>

View File

@ -0,0 +1,18 @@
.cell {
display: flex;
flex-direction: row;
align-items: center;
align-self: center;
&.right {
justify-content: flex-end;
}
&.space {
margin-right: 1rem;
}
> mat-icon {
margin-right: 0.5rem;
}
}

View File

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

View File

@ -0,0 +1,27 @@
import {Component, Input, OnInit} from '@angular/core';
import {Region} from '../../interfaces/region.interface';
import {SearchParameter} from '../../interfaces/search-request.interface';
import {REGION_PARAM_VIS} from '../../services/data.service';
@Component({
selector: 'app-region-stats',
templateUrl: './region-stats.component.html',
styleUrls: ['./region-stats.component.scss']
})
export class RegionStatsComponent implements OnInit {
@Input()
region: Region;
@Input()
shownKeys: SearchParameter[];
/** Contains the visual definitions */
readonly PROPERTY_VIS_DEF = REGION_PARAM_VIS;
constructor() {
}
ngOnInit() {
}
}

View File

@ -29,7 +29,7 @@
> .region-name { > .region-name {
font-weight: bold; font-weight: bold;
font-size: large; font-size: larger;
align-self: center; align-self: center;
margin-right: 0.25rem; margin-right: 0.25rem;
} }

View File

@ -1,7 +1,7 @@
<div class="result-mat-card"> <div class="result-mat-card">
<img class="result-img" src="https://travopti.de/api/v1/regions/{{result.region_id}}/image"> <img class="result-img" src="https://travopti.de/api/v1/regions/{{result.region_id}}/image">
<div class="result-footer"> <div class="result-title">
<div class="result-title"> <div class="result-name">
<span class="result-name">{{result.name}}</span> <span class="result-name">{{result.name}}</span>
<span class="result-country">| {{result.country}}</span> <span class="result-country">| {{result.country}}</span>
</div> </div>
@ -12,4 +12,26 @@
<mat-icon>share</mat-icon> <mat-icon>share</mat-icon>
</button> </button>
</div> </div>
<div class="result-details">
<table>
<tr *ngFor="let score of result.scores">
<td>
<div class="cell space">
<mat-icon>{{PROPERTY_VIS_DEF[score.type].icon}}</mat-icon>
<span>{{score.type|translate}}:</span>
</div>
</td>
<td>
<div class="cell right">
<span>{{score.value ? (score.value|number:'1.2-2') : 'N/A'}}</span>
</div>
</td>
<td>
<div class="cell">
<span>{{PROPERTY_VIS_DEF[score.type].unit}}</span>
</div>
</td>
</tr>
</table>
</div>
</div> </div>

View File

@ -10,12 +10,13 @@
object-fit: cover; object-fit: cover;
} }
> .result-footer { > .result-title {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
margin-bottom: 0.5rem;
> .result-title { > .result-name {
flex: 1 1 auto; flex: 1 1 auto;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -29,12 +30,32 @@
> .result-name { > .result-name {
font-weight: bold; font-weight: bold;
font-size: large; font-size: larger;
align-self: center; align-self: center;
margin-right: 0.25rem; margin-right: 0.25rem;
} }
} }
}
> .result-details {
}
}
.cell {
display: flex;
flex-direction: row;
align-items: center;
align-self: center;
&.right {
justify-content: flex-end;
}
&.space {
margin-right: 1rem;
}
> mat-icon {
margin-right: 0.5rem;
} }
} }

View File

@ -1,5 +1,6 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {Result} from '../../interfaces/result.interface'; import {Result} from '../../interfaces/result.interface';
import {REGION_PARAM_VIS} from '../../services/data.service';
@Component({ @Component({
selector: 'app-result', selector: 'app-result',
@ -11,6 +12,9 @@ export class ResultComponent implements OnInit {
@Input() @Input()
result: Result; result: Result;
/** Contains the visual definitions */
readonly PROPERTY_VIS_DEF = REGION_PARAM_VIS;
constructor() { constructor() {
} }

View File

@ -19,35 +19,13 @@
<span *ngIf="isDescExtended">{{region.description.substr(DESC_CUT_POINT)}}</span> <span *ngIf="isDescExtended">{{region.description.substr(DESC_CUT_POINT)}}</span>
</p> </p>
<div class="region-stats-group"> <div class="region-stats-group">
<div> <app-region-stats [region]="region" [shownKeys]="SHOWN_PROPS"></app-region-stats>
<table>
<tr *ngFor="let prop of SHOWN_PROPS">
<td>
<div class="cell">
<mat-icon>{{prop.icon}}</mat-icon>
<span>{{prop.property|translate}}:</span>
</div>
</td>
<td>
<div class="cell right">
<span>{{region[prop.property].toFixed(2)}}</span>
</div>
</td>
<td>
<div class="cell">
<span>{{prop.unit}}</span>
</div>
</td>
</tr>
</table>
</div>
</div> </div>
<div class="region-stats-group"> <div *ngIf="region.temperature_mean_max && region.temperature_mean_max[0]" class="region-stats-group">
<span class="group-title">Max Temperatures [°C]</span> <span class="group-title">Max Temperatures [°C]</span>
<app-graph [monthlyData]="region.temperature_mean_max" class="graph"></app-graph> <app-graph [monthlyData]="region.temperature_mean_max" class="graph"></app-graph>
</div> </div>
<div class="region-stats-group"> <div *ngIf="region.precipitation && region.precipitation[0]" class="region-stats-group">
<span class="group-title">Precipitation [mm]</span> <span class="group-title">Precipitation [mm]</span>
<app-graph [monthlyData]="region.precipitation" class="graph"></app-graph> <app-graph [monthlyData]="region.precipitation" class="graph"></app-graph>
</div> </div>

View File

@ -59,21 +59,6 @@
} }
} }
.cell {
display: flex;
flex-direction: row;
align-items: center;
align-self: center;
&.right {
justify-content: flex-end;
}
> mat-icon {
margin-right: 0.5rem;
}
}
.spinner { .spinner {
flex: 1 1 auto; flex: 1 1 auto;
display: flex; display: flex;

View File

@ -3,12 +3,8 @@ import {Region} from '../../interfaces/region.interface';
import {ActivatedRoute, ParamMap} from '@angular/router'; import {ActivatedRoute, ParamMap} from '@angular/router';
import {DataService} from '../../services/data.service'; import {DataService} from '../../services/data.service';
import {switchMap} from 'rxjs/operators'; import {switchMap} from 'rxjs/operators';
import {SearchParameter} from '../../interfaces/search-request.interface';
interface VisualRegionPropDef {
property: string;
icon: string;
unit: string;
}
@Component({ @Component({
selector: 'app-region-details', selector: 'app-region-details',
@ -20,32 +16,14 @@ export class RegionDetailsComponent implements OnInit {
/** Cut descriptions after x chars */ /** Cut descriptions after x chars */
readonly DESC_CUT_POINT = 300; readonly DESC_CUT_POINT = 300;
/** Region property to show in view */ /** Region property to show in view */
readonly SHOWN_PROPS: VisualRegionPropDef[] = [ readonly SHOWN_PROPS: SearchParameter[] = [
{ SearchParameter.AVERAGE_PER_DAY_COSTS,
property: 'average_per_day_costs', SearchParameter.ACCOMMODATION_COSTS,
icon: 'euro', SearchParameter.FOOD_COSTS,
unit: '€/day', SearchParameter.WATER_COSTS,
}, SearchParameter.ALCOHOL_COSTS,
{ SearchParameter.LOCAL_TRANSPORTATION_COSTS,
property: 'food_costs', SearchParameter.ENTERTAINMENT_COSTS,
icon: 'local_dining',
unit: '€/day',
},
{
property: 'alcohol_costs',
icon: 'local_bar',
unit: '€/day',
},
{
property: 'local_transportation_costs',
icon: 'commute',
unit: '€/day',
},
{
property: 'entertainment_costs',
icon: 'local_activity',
unit: '€/day',
}
]; ];
/** Current region */ /** Current region */

View File

@ -13,7 +13,7 @@
flex-direction: column; flex-direction: column;
> app-result { > app-result {
margin-bottom: 2rem; margin-bottom: 3rem;
} }
} }

View File

@ -1,25 +1,32 @@
export enum SearchParam {
FROM = 'from',
TO = 'to',
TEMPERATURE = 'temperature',
PRECIPITATION = 'precipitation',
RAINDAYS = 'raindays',
HUMIDITY = 'humidity',
SUNHOURS = 'sunhours',
ALCOHOL = 'alcohol',
FOOD = 'food'
}
export interface Query { export interface Query {
from: number; from: number;
to: number; to: number;
price?: number[]; temperature_mean_max?: number[];
exclude_region_ids?: number[];
temperature?: number[];
precipitation?: number[]; precipitation?: number[];
raindays?: number[]; rain_days?: number[];
humidity?: number[]; humidity?: number[];
sunhours?: number[]; sun_hours?: number[];
alcohol?: number[]; alcohol_costs?: number[];
food?: number[]; food_costs?: number[];
local_transportation_costs: number[];
entertainment_costs: number[];
accommodation_costs: number[];
average_per_day_costs: number[];
}
export enum SearchParameter {
TEMP_MEAN = 'temperature_mean',
TEMP_MEAN_MIN = 'temperature_mean_min',
TEMP_MEAN_MAX = 'temperature_mean_max',
PRECIPITATION = 'precipitation',
HUMIDITY = 'humidity',
SUN_HOURS = 'sun_hours',
RAIN_DAYS = 'rain_days',
FOOD_COSTS = 'food_costs',
ALCOHOL_COSTS = 'alcohol_costs',
WATER_COSTS = 'water_costs',
LOCAL_TRANSPORTATION_COSTS = 'local_transportation_costs',
ENTERTAINMENT_COSTS = 'entertainment_costs',
ACCOMMODATION_COSTS = 'accommodation_costs',
AVERAGE_PER_DAY_COSTS = 'average_per_day_costs'
} }

View File

@ -17,29 +17,106 @@ export class DataService {
public searchRegions(query: string): Promise<Result[]> { public searchRegions(query: string): Promise<Result[]> {
const params = new HttpParams().set('q', query); const params = new HttpParams().set('q', query);
console.log(params);
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 => { // return new Promise<Result[]>(resolve => {
// resolve(MOCK_RESULT); // setTimeout(() => {
// resolve(MOCK_RESULT);
// }, 100);
// }); // });
} }
public getAllPresets(): Promise<Preset[]> { public getAllPresets(): Promise<Preset[]> {
return this.http.get<Preset[]>(this.API_URL + '/search/presets').toPromise(); return this.http.get<Preset[]>(this.API_URL + '/search/presets').toPromise();
// return new Promise<Preset[]>(resolve => { // return new Promise<Preset[]>(resolve => {
// resolve(MOCK_PRESETS); // setTimeout(() => {
// resolve(MOCK_PRESETS);
// }, 100);
// }); // });
} }
public getAllRegions(): Promise<Region[]> { public getAllRegions(): Promise<Region[]> {
return this.http.get<Region[]>(this.API_URL + '/regions').toPromise(); return this.http.get<Region[]>(this.API_URL + '/regions').toPromise();
// return new Promise<Region[]>(resolve => { // return new Promise<Region[]>(resolve => {
// resolve(MOCK_REGIONS); // setTimeout(() => {
// resolve(MOCK_REGIONS);
// }, 100);
// }); // });
} }
public getRegion(id: number): Promise<Region> { public getRegion(id: number): Promise<Region> {
return this.http.get<Region>(`${this.API_URL}/regions/${id}`).toPromise(); return this.http.get<Region>(`${this.API_URL}/regions/${id}`).toPromise();
// return new Promise<Region>(resolve => {
// setTimeout(() => {
// resolve(MOCK_REGIONS.find(region => region.region_id === id));
// }, 100);
// });
} }
} }
export const REGION_PARAM_VIS: RegionParamVisLookup = {
temperature_mean: {
icon: 'wb_sunny',
unit: '°C'
},
temperature_mean_min: {
icon: 'wb_sunny',
unit: '°C'
},
temperature_mean_max: {
icon: 'wb_sunny',
unit: '°C'
},
precipitation: {
icon: 'opacity',
unit: 'mm'
},
humidity: {
icon: 'grain',
unit: '%'
},
sun_hours: {
icon: 'flare',
unit: 'h'
},
rain_days: {
icon: 'date_range',
unit: ''
},
food_costs: {
icon: 'local_dining',
unit: '€/day'
},
alcohol_costs: {
icon: 'local_bar',
unit: '€/day'
},
water_costs: {
icon: 'local_cafe',
unit: '€/day'
},
local_transportation_costs: {
icon: 'commute',
unit: '€/day'
},
entertainment_costs: {
icon: 'local_activity',
unit: '€/day'
},
accommodation_costs: {
icon: 'hotel',
unit: '€/day'
},
average_per_day_costs: {
icon: 'euro',
unit: '€/day'
}
};
export interface RegionParamVisLookup {
[key: string]: RegionParamVis;
}
export interface RegionParamVis {
icon: string;
unit: string;
}

View File

@ -1,5 +1,5 @@
{ {
"temperature_mean_max": "Temperature Average", "temperature_mean_max": "Max Temperature Average",
"temperature": "Temperature", "temperature": "Temperature",
"rain_days": "Rainy days", "rain_days": "Rainy days",
"sun_hours": "Sunny hours", "sun_hours": "Sunny hours",
@ -7,10 +7,12 @@
"humidity": "Humidity", "humidity": "Humidity",
"alcohol_costs": "Alcohol costs", "alcohol_costs": "Alcohol costs",
"food_costs": "Food costs", "food_costs": "Food costs",
"water_costs": "Water costs",
"cheap_alcohol": "Cheap alcohol", "cheap_alcohol": "Cheap alcohol",
"local_transportation_costs": "Public transport", "local_transportation_costs": "Public transport",
"average_per_day_costs": "Average total costs", "average_per_day_costs": "Average total costs",
"entertainment_costs": "Entertainment costs", "entertainment_costs": "Entertainment costs",
"accommodation_costs": "Accommodation costs",
"cheap_food": "Cheap food", "cheap_food": "Cheap food",
"cheap_water": "Cheap water", "cheap_water": "Cheap water",
"cheap_transportations": "Cheap public transport", "cheap_transportations": "Cheap public transport",