diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 1d5b3cc..337c3b1 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -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 } : [], diff --git a/src/app/entities/api-response.ts b/src/app/entities/api-response.ts new file mode 100644 index 0000000..1ce0166 --- /dev/null +++ b/src/app/entities/api-response.ts @@ -0,0 +1,10 @@ +export interface ApiResponse { + success: boolean; + data?: T; + error?: ApiError; +} + +export interface ApiError { + code: string; + msg?: string; +} diff --git a/src/app/entities/travel.ts b/src/app/entities/travel.ts new file mode 100644 index 0000000..3a00b50 --- /dev/null +++ b/src/app/entities/travel.ts @@ -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; +} diff --git a/src/app/entities/user.ts b/src/app/entities/user.ts new file mode 100644 index 0000000..5ec140a --- /dev/null +++ b/src/app/entities/user.ts @@ -0,0 +1,7 @@ +export type UserId = string; + +export interface User { + id: UserId; + matrixId?: string; + name: string; +} diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts index 3b3d50d..eeef071 100644 --- a/src/app/header/header.component.ts +++ b/src/app/header/header.component.ts @@ -13,11 +13,8 @@ export class HeaderComponent implements OnInit { ngOnInit(): void {} async login(): Promise { - // 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(); diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html index f2f502b..4521526 100644 --- a/src/app/home/home.component.html +++ b/src/app/home/home.component.html @@ -1,3 +1,5 @@ + +

access_token_expiration: {{ access_token_expiration }}

diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts index aa4fe97..0b8f749 100644 --- a/src/app/home/home.component.ts +++ b/src/app/home/home.component.ts @@ -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) => { + 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)); } } diff --git a/src/app/services/api.service.spec.ts b/src/app/services/api.service.spec.ts new file mode 100644 index 0000000..c0310ae --- /dev/null +++ b/src/app/services/api.service.spec.ts @@ -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(); + }); +}); diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts new file mode 100644 index 0000000..26c0eb1 --- /dev/null +++ b/src/app/services/api.service.ts @@ -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(relativeUrl: string, parameters?: any[]): Observable> { + return this.http.get>(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 { + 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.'); + } +} diff --git a/src/app/travel-list/travel-list.component.css b/src/app/travel-list/travel-list.component.css new file mode 100644 index 0000000..511addb --- /dev/null +++ b/src/app/travel-list/travel-list.component.css @@ -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; +} \ No newline at end of file diff --git a/src/app/travel-list/travel-list.component.html b/src/app/travel-list/travel-list.component.html new file mode 100644 index 0000000..8bbad7d --- /dev/null +++ b/src/app/travel-list/travel-list.component.html @@ -0,0 +1,47 @@ +
+ + + + +
+ +
+ + + + # + {{ travel.driverId }} + + + + Departure date + {{ + travel.departureDate + }} + + + + Description + {{ + travel.description + }} + + + + + + + + +
diff --git a/src/app/travel-list/travel-list.component.ts b/src/app/travel-list/travel-list.component.ts new file mode 100644 index 0000000..baf68c0 --- /dev/null +++ b/src/app/travel-list/travel-list.component.ts @@ -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 + ); + } +} diff --git a/src/app/travel-list/travel-list.datasource.ts b/src/app/travel-list/travel-list.datasource.ts new file mode 100644 index 0000000..19b9f5d --- /dev/null +++ b/src/app/travel-list/travel-list.datasource.ts @@ -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 { + numTravels = 0; + private travelsSubject = new BehaviorSubject([]); + + private loadingSubject = new BehaviorSubject(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/list') + .pipe( + catchError(() => of([])), + finalize(() => this.loadingSubject.next(false)) + ) + .subscribe((res) => { + const data = (res as ApiResponse).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 { + console.log('Connecting data source'); + return this.travelsSubject.asObservable(); + } + + disconnect(collectionViewer: CollectionViewer): void { + this.travelsSubject.complete(); + this.loadingSubject.complete(); + } +} diff --git a/src/assets/img/favicon.png b/src/assets/img/favicon.png new file mode 100644 index 0000000..5eeb6f7 Binary files /dev/null and b/src/assets/img/favicon.png differ diff --git a/src/favicon.ico b/src/favicon.ico deleted file mode 100644 index 8081c7c..0000000 Binary files a/src/favicon.ico and /dev/null differ diff --git a/src/index.html b/src/index.html index 63736d2..23aae5d 100644 --- a/src/index.html +++ b/src/index.html @@ -6,7 +6,7 @@ - +