From 6bb528e5fce99e4b51dde5b7ef57a1c95233d3d7 Mon Sep 17 00:00:00 2001 From: Eneko Nieto Date: Fri, 22 Jan 2021 02:34:32 +0100 Subject: [PATCH] Travel list --- src/app/app.module.ts | 14 ++- src/app/entities/api-response.ts | 10 +++ src/app/entities/travel.ts | 15 ++++ src/app/entities/user.ts | 7 ++ src/app/header/header.component.ts | 3 - src/app/home/home.component.html | 2 + src/app/home/home.component.ts | 49 ++++++----- src/app/services/api.service.spec.ts | 16 ++++ src/app/services/api.service.ts | 35 ++++++++ src/app/travel-list/travel-list.component.css | 42 +++++++++ .../travel-list/travel-list.component.html | 47 ++++++++++ src/app/travel-list/travel-list.component.ts | 80 ++++++++++++++++++ src/app/travel-list/travel-list.datasource.ts | 57 +++++++++++++ src/assets/img/favicon.png | Bin 0 -> 1269 bytes src/favicon.ico | Bin 5430 -> 0 bytes src/index.html | 2 +- 16 files changed, 349 insertions(+), 30 deletions(-) create mode 100644 src/app/entities/api-response.ts create mode 100644 src/app/entities/travel.ts create mode 100644 src/app/entities/user.ts create mode 100644 src/app/services/api.service.spec.ts create mode 100644 src/app/services/api.service.ts create mode 100644 src/app/travel-list/travel-list.component.css create mode 100644 src/app/travel-list/travel-list.component.html create mode 100644 src/app/travel-list/travel-list.component.ts create mode 100644 src/app/travel-list/travel-list.datasource.ts create mode 100644 src/assets/img/favicon.png delete mode 100644 src/favicon.ico 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 0000000000000000000000000000000000000000..5eeb6f7e14e0900102a7908a59a45ba3aa1055f5 GIT binary patch literal 1269 zcmVDU5|qp+VTS4q(n#wJGQ<8L zo1582L^C27btH2c35YB>!A!>=TS@RAwqEH4i&#cU`(BY{TLc#q7PAaDBSu_w z8s^WSYp;K_k361z134E`r@26!3Byq&}n@G6NajihG&E%3ZaJ}2-D&;@+zbUNG8k!J!QrXrDL zS>)#CikzGrAgsALD=TM8 zN=iPr*=*P2@p#7U?Cgq5mo7aPiA0tU4-XTI#k7@s1t@H8Zr(C6F;Nu?g`9eVwQJX2 zD=RBIk&0Xb{Q8RAZnyCJ{THLr=(B@^ga6v&fq?;gG#afvdi3a*E|<%=8Y39ajooe+ zhYlTTFe#o|eek+<>taJgL#-Vh9kLlfvp^t_5ex?RdA;7brPh0Xe!h?-NvvPL{_6Pn zxHVM{D{xH%tX{o377B$-m6};sSJyknv*Go6#p%%F>u&_{U-MaN}-Pl*DlIfS?ULa5Va5x+vrHEfaq8q-L z{C*qQ2MhzRX+dk&tik8=9g`%fPKUi8xXYv{03OxU%Cao>?%i8pm@g&KrM0a=hmEPXoivmUti# zSYaw)U|_)RbUO7XxCJ<=ufXMU3BTVzsH$qNs;bNS`}_Oq>+9pEPM!K7Rfh!L(y%L6 zuADI?*Q~6p>@ywVEj{_hjT>(TgTZ}5h*3Fce9mP3*x(bm@1xFDjasOWXWIS|dw&6BFCx|T>@4BXUmc)i}6 z;c)n#bO0GRfbPE5`DLJJ3FIZfl!njD%oII6J)P+QxxmMI($dmWqlh0!yo^%bU`bmB zG@+|pmSwSf_wLJ~P$)Z{fFTWFu~?T`wc}=iqnzP($b<@tybZ1IE2sVyQ1y4q9{+dwzmFkv)LB=7M$~}tSr&l z+4;SqD37Nlhs5>h^ZBm!_4R#n?%cUN{hX?*4<0{$e7vfv%Ixho&n*^<*sx)P2m}J7 zkx0bY%2S#IaN)v*oT;g)-)m}WV#a_#Adum3I9`~Zo^Bl*8++*d`Sa4`yBx&xCNjx4Omt{FvQBm>r fQu5ugI|lF%85JN6l-{#g00000NkvXXu0mjf1lw+l literal 0 HcmV?d00001 diff --git a/src/favicon.ico b/src/favicon.ico deleted file mode 100644 index 8081c7ceaf2be08bf59010158c586170d9d2d517..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5430 zcmc(je{54#6vvCoAI3i*G5%$U7!sA3wtMZ$fH6V9C`=eXGJb@R1%(I_{vnZtpD{6n z5Pl{DmxzBDbrB>}`90e12m8T*36WoeDLA&SD_hw{H^wM!cl_RWcVA!I+x87ee975; z@4kD^=bYPn&pmG@(+JZ`rqQEKxW<}RzhW}I!|ulN=fmjVi@x{p$cC`)5$a!)X&U+blKNvN5tg=uLvuLnuqRM;Yc*swiexsoh#XPNu{9F#c`G zQLe{yWA(Y6(;>y|-efAy11k<09(@Oo1B2@0`PtZSkqK&${ zgEY}`W@t{%?9u5rF?}Y7OL{338l*JY#P!%MVQY@oqnItpZ}?s z!r?*kwuR{A@jg2Chlf0^{q*>8n5Ir~YWf*wmsh7B5&EpHfd5@xVaj&gqsdui^spyL zB|kUoblGoO7G(MuKTfa9?pGH0@QP^b#!lM1yHWLh*2iq#`C1TdrnO-d#?Oh@XV2HK zKA{`eo{--^K&MW66Lgsktfvn#cCAc*(}qsfhrvOjMGLE?`dHVipu1J3Kgr%g?cNa8 z)pkmC8DGH~fG+dlrp(5^-QBeEvkOvv#q7MBVLtm2oD^$lJZx--_=K&Ttd=-krx(Bb zcEoKJda@S!%%@`P-##$>*u%T*mh+QjV@)Qa=Mk1?#zLk+M4tIt%}wagT{5J%!tXAE;r{@=bb%nNVxvI+C+$t?!VJ@0d@HIyMJTI{vEw0Ul ze(ha!e&qANbTL1ZneNl45t=#Ot??C0MHjjgY8%*mGisN|S6%g3;Hlx#fMNcL<87MW zZ>6moo1YD?P!fJ#Jb(4)_cc50X5n0KoDYfdPoL^iV`k&o{LPyaoqMqk92wVM#_O0l z09$(A-D+gVIlq4TA&{1T@BsUH`Bm=r#l$Z51J-U&F32+hfUP-iLo=jg7Xmy+WLq6_tWv&`wDlz#`&)Jp~iQf zZP)tu>}pIIJKuw+$&t}GQuqMd%Z>0?t%&BM&Wo^4P^Y z)c6h^f2R>X8*}q|bblAF?@;%?2>$y+cMQbN{X$)^R>vtNq_5AB|0N5U*d^T?X9{xQnJYeU{ zoZL#obI;~Pp95f1`%X3D$Mh*4^?O?IT~7HqlWguezmg?Ybq|7>qQ(@pPHbE9V?f|( z+0xo!#m@Np9PljsyxBY-UA*{U*la#8Wz2sO|48_-5t8%_!n?S$zlGe+NA%?vmxjS- zHE5O3ZarU=X}$7>;Okp(UWXJxI%G_J-@IH;%5#Rt$(WUX?6*Ux!IRd$dLP6+SmPn= z8zjm4jGjN772R{FGkXwcNv8GBcZI#@Y2m{RNF_w8(Z%^A*!bS*!}s6sh*NnURytky humW;*g7R+&|Ledvc- - +