Travel list

This commit is contained in:
Eneko Nieto
2021-01-22 02:34:32 +01:00
parent d133876325
commit 6bb528e5fc
16 changed files with 349 additions and 30 deletions

View File

@@ -14,6 +14,12 @@ import { RouterModule, ExtraOptions } from '@angular/router';
import { useHash } from '../flags';
import { HeaderComponent } from './header/header.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatTableModule } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { TravelListComponent } from './travel-list/travel-list.component';
const ROUTING_OPTIONS: ExtraOptions = {
// preloadingStrategy: CustomPreloadingStrategy,
@@ -36,12 +42,18 @@ const ROUTING_OPTIONS: ExtraOptions = {
},
}),
BrowserAnimationsModule,
MatInputModule,
MatTableModule,
MatPaginatorModule,
MatSortModule,
MatProgressSpinnerModule
],
declarations: [
AppComponent,
HomeComponent,
FlightHistoryComponent,
HeaderComponent
HeaderComponent,
TravelListComponent
],
providers: [
// (useHash) ? { provide: LocationStrategy, useClass: HashLocationStrategy } : [],

View File

@@ -0,0 +1,10 @@
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: ApiError;
}
export interface ApiError {
code: string;
msg?: string;
}

View File

@@ -0,0 +1,15 @@
import { User } from './user';
export type TravelId = number;
export interface Travel {
id: Travel;
driver: User;
travelers: User[];
departureDate: string;
origin: string;
destination: string;
availablePlaces: number;
description?: string;
matrixRoomId: string;
}

7
src/app/entities/user.ts Normal file
View File

@@ -0,0 +1,7 @@
export type UserId = string;
export interface User {
id: UserId;
matrixId?: string;
name: string;
}

View File

@@ -13,11 +13,8 @@ export class HeaderComponent implements OnInit {
ngOnInit(): void {}
async login(): Promise<void> {
// Tweak config for code flow
this.oauthService.configure(authConfig);
console.log('login pre');
await this.oauthService.loadDiscoveryDocument();
console.log('login post');
sessionStorage.setItem('flow', 'code');
this.oauthService.initLoginFlow();

View File

@@ -1,3 +1,5 @@
<app-travel-list></app-travel-list>
<div class="panel panel-default">
<div class="panel-body">
<p><b>access_token_expiration:</b> {{ access_token_expiration }}</p>

View File

@@ -1,26 +1,42 @@
import { Component, OnInit } from '@angular/core';
import { OAuthService } from 'angular-oauth2-oidc';
import { ActivatedRoute } from '@angular/router';
import { ApiService } from '../services/api.service';
import { Travel } from '../entities/travel';
import { ApiResponse } from '../entities/api-response';
@Component({
templateUrl: './home.component.html'
templateUrl: './home.component.html',
})
export class HomeComponent implements OnInit {
loginFailed = false;
userProfile: object;
usePopup: boolean;
login: false;
userTravels: Travel[];
constructor(
private route: ActivatedRoute,
private oauthService: OAuthService
private oauthService: OAuthService,
private apiService: ApiService
) {}
ngOnInit(): void {
this.route.params.subscribe(p => {
this.route.params.subscribe((p) => {
this.login = p['login'];
});
this.apiService
.call('/travel/list')
.subscribe((res: ApiResponse<Travel[]>) => {
if (res.success) {
this.userTravels = res.data;
console.log(res.data);
} else {
console.error(res.error.code + ' ' + res.error.msg);
}
});
// This would directly (w/o user interaction) redirect the user to the
// login page if they are not already logged in.
/*
@@ -32,25 +48,8 @@ export class HomeComponent implements OnInit {
*/
}
logout(): void {
// this.oauthService.logOut();
this.oauthService.revokeTokenAndLogout();
}
loadUserProfile(): void {
this.oauthService.loadUserProfile().then(up => (this.userProfile = up));
}
get givenName(): any {
const claims = this.oauthService.getIdentityClaims();
if (!claims) { return null; }
return claims['given_name'];
}
get familyName(): any {
const claims = this.oauthService.getIdentityClaims();
if (!claims) { return null; }
return claims['family_name'];
this.oauthService.loadUserProfile().then((up) => (this.userProfile = up));
}
refresh(): void {
@@ -62,13 +61,13 @@ export class HomeComponent implements OnInit {
) {
this.oauthService
.refreshToken()
.then(info => console.log('refresh ok', info))
.catch(err => console.error('refresh error', err));
.then((info) => console.log('refresh ok', info))
.catch((err) => console.error('refresh error', err));
} else {
this.oauthService
.silentRefresh()
.then(info => console.log('silent refresh ok', info))
.catch(err => console.error('silent refresh error', err));
.then((info) => console.log('silent refresh ok', info))
.catch((err) => console.error('silent refresh error', err));
}
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { ApiService } from './api.service';
describe('ApiService', () => {
let service: ApiService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ApiService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
import { API_URL } from '../app.config';
import { ApiResponse } from '../entities/api-response';
@Injectable({
providedIn: 'root',
})
export class ApiService {
constructor(private http: HttpClient) {}
call<T>(relativeUrl: string, parameters?: any[]): Observable<ApiResponse<T>> {
return this.http.get<ApiResponse<T>>(API_URL + relativeUrl).pipe(
retry(3), // retry a failed request up to 3 times
catchError(this.handleError) // then handle the error
);
}
private handleError(error: HttpErrorResponse): Observable<never> {
if (error.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly.
console.error('An error occurred:', error.error.message);
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong.
console.error(
`API returned code ${error.status}, ` + `body was: ${error.error}`
);
}
// Return an observable with a user-facing error message.
return throwError('Something bad happened; please try again later.');
}
}

View File

@@ -0,0 +1,42 @@
.course {
text-align: center;
max-width: 390px;
margin: 0 auto;
}
.course-thumbnail {
width: 150px;
margin: 20px auto 0 auto;
display: block;
}
.description-cell {
text-align: left;
margin: 10px auto;
}
.duration-cell {
text-align: center;
}
.duration-cell mat-icon {
display: inline-block;
vertical-align: middle;
font-size: 20px;
}
.spinner-container {
height: 360px;
width: 390px;
position: fixed;
}
.lessons-table {
min-height: 360px;
margin-top: 10px;
}
.spinner-container mat-spinner {
margin: 130px auto 0 auto;
}

View File

@@ -0,0 +1,47 @@
<div class="course">
<mat-form-field>
<input matInput placeholder="Search lessons" #input />
</mat-form-field>
<div class="spinner-container" *ngIf="dataSource.loading$ | async">
<mat-spinner></mat-spinner>
</div>
<mat-table
class="lessons-table mat-elevation-z8"
[dataSource]="dataSource"
matSort
matSortActive="driver"
matSortDirection="asc"
matSortDisableClear
>
<ng-container matColumnDef="driver">
<mat-header-cell *matHeaderCellDef mat-sort-header>#</mat-header-cell>
<mat-cell *matCellDef="let travel">{{ travel.driverId }}</mat-cell>
</ng-container>
<ng-container matColumnDef="departureDate">
<mat-header-cell *matHeaderCellDef mat-sort-header>Departure date</mat-header-cell>
<mat-cell *matCellDef="let travel">{{
travel.departureDate
}}</mat-cell>
</ng-container>
<ng-container matColumnDef="description">
<mat-header-cell *matHeaderCellDef>Description</mat-header-cell>
<mat-cell class="description-cell" *matCellDef="let travel">{{
travel.description
}}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>
<mat-paginator
[length]="10"
[pageSize]="3"
[pageSizeOptions]="[3, 5, 10]"
></mat-paginator>
</div>

View File

@@ -0,0 +1,80 @@
import {
AfterViewInit,
Component,
ElementRef,
OnInit,
ViewChild,
} from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import {
debounceTime,
distinctUntilChanged,
startWith,
tap,
delay,
} from 'rxjs/operators';
import { merge, fromEvent } from 'rxjs';
import { Travel } from '../entities/travel';
import { ApiService } from '../services/api.service';
import { TravelsDataSource } from './travel-list.datasource';
@Component({
selector: 'app-travel-list',
templateUrl: './travel-list.component.html',
styleUrls: ['./travel-list.component.css'],
})
export class TravelListComponent implements OnInit, AfterViewInit {
dataSource: TravelsDataSource;
displayedColumns = ['driver', 'departureDate', 'description'];
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
@ViewChild(MatSort, { static: true }) sort: MatSort;
@ViewChild('input', { static: true }) input: ElementRef;
constructor(
private route: ActivatedRoute,
private apiService: ApiService
) {}
ngOnInit(): void {
this.dataSource = new TravelsDataSource(this.apiService);
this.dataSource.loadLessons(1, '', 'asc', 0, 3);
}
ngAfterViewInit(): void {
this.sort.sortChange.subscribe(() => (this.paginator.pageIndex = 0));
fromEvent(this.input.nativeElement, 'keyup')
.pipe(
debounceTime(150),
distinctUntilChanged(),
tap(() => {
this.paginator.pageIndex = 0;
this.loadTravels();
})
)
.subscribe();
merge(this.sort.sortChange, this.paginator.page)
.pipe(tap(() => this.loadTravels()))
.subscribe();
}
loadTravels(): void {
this.dataSource.loadLessons(
1,
this.input.nativeElement.value,
this.sort.direction,
this.paginator.pageIndex,
this.paginator.pageSize
);
}
}

View File

@@ -0,0 +1,57 @@
import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { Observable, BehaviorSubject, of } from 'rxjs';
import { catchError, finalize } from 'rxjs/operators';
import { ApiResponse } from '../entities/api-response';
import { Travel } from '../entities/travel';
import { ApiService } from '../services/api.service';
export class TravelsDataSource implements DataSource<Travel> {
numTravels = 0;
private travelsSubject = new BehaviorSubject<Travel[]>([]);
private loadingSubject = new BehaviorSubject<boolean>(false);
public loading$ = this.loadingSubject.asObservable();
constructor(private apiService: ApiService) {}
loadLessons(
courseId: number,
filter: string,
sortDirection: string,
pageIndex: number,
pageSize: number
): void {
this.loadingSubject.next(true);
this.apiService
.call<Travel[]>('/travel/list')
.pipe(
catchError(() => of([])),
finalize(() => this.loadingSubject.next(false))
)
.subscribe((res) => {
const data = (res as ApiResponse<Travel[]>).data;
this.numTravels = data.length;
this.travelsSubject.next(data);
});
// this.apiService
// .findLessons(courseId, filter, sortDirection, pageIndex, pageSize)
// .pipe(
// catchError(() => of([])),
// finalize(() => this.loadingSubject.next(false))
// )
// .subscribe((lessons) => this.lessonsSubject.next(lessons));
}
connect(collectionViewer: CollectionViewer): Observable<Travel[]> {
console.log('Connecting data source');
return this.travelsSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.travelsSubject.complete();
this.loadingSubject.complete();
}
}

BIN
src/assets/img/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -6,7 +6,7 @@
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="icon" type="image/x-icon" href="assets/img/favicon.png" />
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>