Travel list
This commit is contained in:
@@ -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 } : [],
|
||||
|
||||
10
src/app/entities/api-response.ts
Normal file
10
src/app/entities/api-response.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface ApiResponse<T> {
|
||||
success: boolean;
|
||||
data?: T;
|
||||
error?: ApiError;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
code: string;
|
||||
msg?: string;
|
||||
}
|
||||
15
src/app/entities/travel.ts
Normal file
15
src/app/entities/travel.ts
Normal 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
7
src/app/entities/user.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type UserId = string;
|
||||
|
||||
export interface User {
|
||||
id: UserId;
|
||||
matrixId?: string;
|
||||
name: string;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
16
src/app/services/api.service.spec.ts
Normal file
16
src/app/services/api.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
35
src/app/services/api.service.ts
Normal file
35
src/app/services/api.service.ts
Normal 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.');
|
||||
}
|
||||
}
|
||||
42
src/app/travel-list/travel-list.component.css
Normal file
42
src/app/travel-list/travel-list.component.css
Normal 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;
|
||||
}
|
||||
47
src/app/travel-list/travel-list.component.html
Normal file
47
src/app/travel-list/travel-list.component.html
Normal 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>
|
||||
80
src/app/travel-list/travel-list.component.ts
Normal file
80
src/app/travel-list/travel-list.component.ts
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
57
src/app/travel-list/travel-list.datasource.ts
Normal file
57
src/app/travel-list/travel-list.datasource.ts
Normal 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
BIN
src/assets/img/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/favicon.ico
BIN
src/favicon.ico
Binary file not shown.
|
Before Width: | Height: | Size: 5.3 KiB |
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user