Add basic result page

This commit is contained in:
Patrick Gebhardt 2020-06-14 20:34:51 +02:00
parent a9eab54674
commit 56818c4bff
17 changed files with 453 additions and 53 deletions

View File

@ -1,11 +1,13 @@
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
import {HomeComponent} from './containers/home/home.component';
import {NotfoundComponent} from './containers/notfound/notfound.component';
import {SearchComponent} from './containers/search/search.component';
const routes: Routes = [
{path: 'home', component: HomeComponent},
{path: 'search', component: SearchComponent},
{path: '', redirectTo: 'home', pathMatch: 'full'},
{path: '**', component: NotfoundComponent}
];

View File

@ -1,25 +1,30 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {MatToolbarModule} from '@angular/material/toolbar';
import {MatIconModule} from '@angular/material/icon';
import {MatButtonModule} from '@angular/material/button';
import {MatSidenavModule} from '@angular/material/sidenav';
import { HomeComponent } from './containers/home/home.component';
import {HomeComponent} from './containers/home/home.component';
import {MatCardModule} from '@angular/material/card';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import {MatSelectModule} from '@angular/material/select';
import { NotfoundComponent } from './containers/notfound/notfound.component';
import {NotfoundComponent} from './containers/notfound/notfound.component';
import {SearchComponent} from './containers/search/search.component';
import {SearchInputComponent} from './components/search-input/search-input.component';
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
@NgModule({
declarations: [
AppComponent,
HomeComponent,
NotfoundComponent
NotfoundComponent,
SearchComponent,
SearchInputComponent
],
imports: [
BrowserModule,
@ -32,7 +37,8 @@ import { NotfoundComponent } from './containers/notfound/notfound.component';
MatCardModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule
MatSelectModule,
MatProgressSpinnerModule
],
providers: [],
bootstrap: [AppComponent]

View File

@ -0,0 +1,28 @@
<mat-card class="search-container">
<mat-form-field appearance="outline">
<mat-label>Search</mat-label>
<input #textSearch matInput>
<button (click)="textSearch.value=''" *ngIf="textSearch.value.length>0" mat-icon-button matSuffix>
<mat-icon>clear</mat-icon>
</button>
</mat-form-field>
<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>
<mat-form-field>
<mat-label>Price</mat-label>
<mat-select value="Budget">
<mat-option value="Budget">Budget</mat-option>
<mat-option value="Mid-Range">Mid-Range</mat-option>
<mat-option value="Luxury">Luxury</mat-option>
</mat-select>
</mat-form-field>
<button (click)="onSearch()" color="primary" mat-flat-button>Search
<mat-icon matSuffix>search</mat-icon>
</button>
</mat-card>

View File

@ -0,0 +1,4 @@
.search-container {
display: flex;
flex-direction: column;
}

View File

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

View File

@ -0,0 +1,28 @@
import {Component, OnInit} from '@angular/core';
import {Router} from '@angular/router';
import {Query} from '../../interfaces/search-request.interface';
import {objToBase64} from '../../utils/base64conversion';
@Component({
selector: 'app-search-input',
templateUrl: './search-input.component.html',
styleUrls: ['./search-input.component.scss']
})
export class SearchInputComponent implements OnInit {
constructor(private router: Router) {
}
ngOnInit() {
}
async onSearch() {
const query: Query = {
from: Date.now(),
to: Date.now(),
};
await this.router.navigate(['/search'], {queryParams: {q: objToBase64(query)}});
}
}

View File

@ -1,28 +1 @@
<mat-card class="search-container">
<mat-form-field appearance="outline">
<mat-label>Search</mat-label>
<input #textSearch matInput>
<button *ngIf="textSearch.value.length>0" mat-icon-button matSuffix (click)="textSearch.value=''">
<mat-icon>clear</mat-icon>
</button>
</mat-form-field>
<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>
<mat-form-field>
<mat-label>Price</mat-label>
<mat-select value="Budget">
<mat-option value="Budget">Budget</mat-option>
<mat-option value="Mid-Range">Mid-Range</mat-option>
<mat-option value="Luxury">Luxury</mat-option>
</mat-select>
</mat-form-field>
<button mat-flat-button color="primary">Search
<mat-icon matSuffix>search</mat-icon>
</button>
</mat-card>
<app-search-input></app-search-input>

View File

@ -2,8 +2,3 @@
margin: 0;
box-sizing: border-box;
}
.search-container {
display: flex;
flex-direction: column;
}

View File

@ -0,0 +1,12 @@
<app-search-input></app-search-input>
<mat-card *ngIf="results">
<h2 matCardTitle>Suchergebnisse ({{results.length}}):</h2>
<ul>
<li *ngFor="let result of results">{{result.name}} ({{result.score}})</li>
</ul>
</mat-card>
<div *ngIf="!results" class="spinner">
<mat-spinner></mat-spinner>
</div>

View File

@ -0,0 +1,17 @@
:host {
display: flex;
flex-direction: column;
height: 100%;
> * {
margin-bottom: 1rem;
}
}
.spinner {
flex: 1 1 auto;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}

View File

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

View File

@ -0,0 +1,30 @@
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {Result} from '../../interfaces/result.interface';
import {MOCK_RESULT} from '../../mock/mock-data';
@Component({
selector: 'app-search',
templateUrl: './search.component.html',
styleUrls: ['./search.component.scss']
})
export class SearchComponent implements OnInit {
queryString: string;
results: Result[];
constructor(private route: ActivatedRoute) {
}
ngOnInit() {
this.route.queryParams.subscribe(params => {
this.queryString = params.q;
});
// Mock results
setTimeout(() => {
this.results = MOCK_RESULT;
}, 1000);
}
}

View File

@ -1,15 +1,14 @@
import {SearchParam} from './search-request.interface';
/** Represents the structure of one search result. */
export interface Result {
region_id: string;
region_name: string;
region_id: number;
name: string;
country_id: number;
score: number;
scores: Score[];
}
export interface Score {
type: SearchParam;
type: string;
value: number;
score: number;
}

View File

@ -10,7 +10,7 @@ export enum SearchParam {
FOOD = 'food'
}
export interface SearchParams {
export interface Query {
from: number;
to: number;
price?: number[];

View File

@ -0,0 +1,242 @@
import {Result} from '../interfaces/result.interface';
export const MOCK_RESULT: Result[] = [
{
region_id: 24,
country_id: 20,
name: 'Kuala Lumpur',
scores: [
{
type: 'temperature_mean',
value: 28.1,
score: 10
},
{
type: 'raindays',
value: 13.8,
score: 7.5
}
],
score: 8.75
},
{
region_id: 10,
country_id: 7,
name: 'Shanghai',
scores: [
{
type: 'temperature_mean',
value: 24.8,
score: 6.3
},
{
type: 'raindays',
value: 8.6,
score: 9.4
},
{
type: 'sunhours',
value: 151.2,
score: 2.2
}
],
score: 5.97
},
{
region_id: 3,
country_id: 2,
name: 'Sydney',
scores: [
{
type: 'temperature_mean',
value: 16.8,
score: 0
},
{
type: 'raindays',
value: 6.4,
score: 8.2
},
{
type: 'sunhours',
value: 245.8,
score: 9.5
}
],
score: 5.9
},
{
region_id: 9,
country_id: 7,
name: 'Peking',
scores: [
{
type: 'temperature_mean',
value: 21.1,
score: 4.2
},
{
type: 'raindays',
value: 5.4,
score: 7.1
},
{
type: 'sunhours',
value: 199,
score: 6
}
],
score: 5.77
},
{
region_id: 7,
country_id: 5,
name: 'Toronto',
scores: [
{
type: 'temperature_mean',
value: 16.8,
score: 1.2
},
{
type: 'raindays',
value: 8.4,
score: 9.3
},
{
type: 'sunhours',
value: 206.4,
score: 6.2
}
],
score: 5.57
},
{
region_id: 2,
country_id: 2,
name: 'Melbourne',
scores: [
{
type: 'temperature_mean',
value: 12.3,
score: 0
},
{
type: 'raindays',
value: 8.3,
score: 10
},
{
type: 'sunhours',
value: 187.4,
score: 5.1
}
],
score: 5.03
},
{
region_id: 12,
country_id: 9,
name: 'Kairo',
scores: [
{
type: 'temperature_mean',
value: 27.9,
score: 8.9
},
{
type: 'raindays',
value: 0,
score: 1.1
}
],
score: 5
},
{
region_id: 13,
country_id: 10,
name: 'London',
scores: [
{
type: 'temperature_mean',
value: 16,
score: 0
},
{
type: 'raindays',
value: 8.6,
score: 9.6
},
{
type: 'sunhours',
value: 142.4,
score: 1.9
}
],
score: 3.83
},
{
region_id: 6,
country_id: 4,
name: 'Sao Paolo',
scores: [
{
type: 'temperature_mean',
value: 19.6,
score: 0.8
},
{
type: 'raindays',
value: 4.6,
score: 6
}
],
score: 3.4
},
{
region_id: 11,
country_id: 8,
name: 'Bogota',
scores: [
{
type: 'temperature_mean',
value: 13.7,
score: 0
},
{
type: 'raindays',
value: 9.1,
score: 9.8
},
{
type: 'sunhours',
value: 121.2,
score: 0.1
}
],
score: 3.3
},
{
region_id: 18,
country_id: 15,
name: 'Reykjavik',
scores: [
{
type: 'temperature_mean',
value: 8.6,
score: 0
},
{
type: 'raindays',
value: 13.5,
score: 7.4
},
{
type: 'sunhours',
value: 135.1,
score: 1.7
}
],
score: 3.03
}
];

View File

@ -2,7 +2,6 @@ import {Injectable} from '@angular/core';
import {HttpClient, HttpParams} from '@angular/common/http';
import {Preset} from '../interfaces/preset.interface';
import {Result} from '../interfaces/result.interface';
import {SearchParams} from '../interfaces/search-request.interface';
import {Region} from '../interfaces/region.interface';
@Injectable({
@ -15,9 +14,9 @@ export class DataService {
constructor(private http: HttpClient) {
}
public searchRegions(searchParams: SearchParams): Promise<Result[]> {
public searchRegions(query: string): Promise<Result[]> {
const params = new HttpParams();
params.append('q', btoa(JSON.stringify(searchParams)));
params.append('q', query);
return this.http.get<Result[]>(this.API_URL + 'search', {params}).toPromise();
}

View File

@ -0,0 +1,15 @@
/**
* Encodes an object as base64 string.
* @param obj The object to encode
*/
export function objToBase64(obj: object): string {
return btoa(JSON.stringify(obj));
}
/**
* Decodes a base64 encoded object.
* @param base64 Encoded object
*/
export function base64ToObj(base64: string): object {
return JSON.parse(atob(base64));
}