From a909486396cb074d8d1cfeab26082d99b80edab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danyi=20D=C3=A1vid?= Date: Sun, 13 May 2018 22:35:37 +0200 Subject: [PATCH] * table filters * image upload implemented now * auth token renewal --- src/app/app.component.ts | 25 ++++- .../awardee-editor.component.css | 31 +++++- .../awardee-editor.component.html | 26 +++-- .../awardee-editor.component.ts | 94 +++++++++++++++++-- .../awardee-list-table-datasource.ts | 14 ++- .../awardee-list-table.component.css | 10 ++ .../awardee-list-table.component.html | 5 + .../awardee-list-table.component.ts | 5 + .../judge-editor/judge-editor.component.css | 24 +++++ .../judge-editor/judge-editor.component.html | 16 +++- .../judge-editor/judge-editor.component.ts | 39 +++++++- .../judge-list-table-datasource.ts | 17 +++- .../judge-list-table.component.css | 10 ++ .../judge-list-table.component.html | 5 + .../judge-list-table.component.ts | 5 + src/app/shared/awardee.service.ts | 18 +++- src/app/shared/awardee.ts | 3 + src/app/shared/judge.service.ts | 8 ++ src/app/shared/judge.ts | 2 + 19 files changed, 323 insertions(+), 34 deletions(-) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index b066d27..f859ce3 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,15 +1,32 @@ -import { Component } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { AuthService } from "./auth/auth.service"; +import { Subscription } from "rxjs/internal/Subscription"; +import { timer } from "rxjs/internal/observable/timer"; + +const RENEW_TIMER_INITIAL = 300000; // 5min +const RENEW_TIMER_PERIOD = 300000; // 5min @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) -export class AppComponent { +export class AppComponent implements OnInit, OnDestroy { + + private authRenewTimer: Subscription; + constructor(private authService: AuthService) {} - get loggedIn(): boolean { - return this.authService.isLoggedIn; + ngOnInit(): void { + let authRenewObservable = timer(RENEW_TIMER_INITIAL, RENEW_TIMER_PERIOD); + this.authRenewTimer = authRenewObservable.subscribe(() => { + if (this.authService.isLoggedIn) { + this.authService.renew(); + } + }); + } + + ngOnDestroy(): void { + this.authRenewTimer.unsubscribe(); } } diff --git a/src/app/awardee-editor/awardee-editor.component.css b/src/app/awardee-editor/awardee-editor.component.css index 86d0c3f..3680615 100644 --- a/src/app/awardee-editor/awardee-editor.component.css +++ b/src/app/awardee-editor/awardee-editor.component.css @@ -13,7 +13,36 @@ width: 100%; } -button + mat-divider { +mat-divider { margin-top: 10px; margin-bottom: 10px; } + +.profile-image { + width: 180px; + height: 180px; + border-radius: 90px; +} + +.award-image { + width: 45%; + height: 45%; +} + +.tobe-replaced { + opacity: .75; + filter: grayscale(100%); + /*background: black;*/ +} + +.image-preview-zone { + margin-bottom: 20px; +} + +.clickable { + cursor: pointer; +} + +.image-preview-zone > img { + margin-right: 10px; +} \ No newline at end of file diff --git a/src/app/awardee-editor/awardee-editor.component.html b/src/app/awardee-editor/awardee-editor.component.html index 1cc0ff6..ab5e675 100644 --- a/src/app/awardee-editor/awardee-editor.component.html +++ b/src/app/awardee-editor/awardee-editor.component.html @@ -2,10 +2,17 @@ - +
+ + +
+ + + @@ -16,9 +23,16 @@ - + diff --git a/src/app/awardee-editor/awardee-editor.component.ts b/src/app/awardee-editor/awardee-editor.component.ts index 5d463a9..55aed25 100644 --- a/src/app/awardee-editor/awardee-editor.component.ts +++ b/src/app/awardee-editor/awardee-editor.component.ts @@ -1,9 +1,11 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { Title } from "@angular/platform-browser"; import { ActivatedRoute, Router } from "@angular/router"; import { Awardee } from "../shared/awardee"; import { AwardeeService } from "../shared/awardee.service"; +import { environment } from "../../environments/environment"; +import { merge } from "rxjs"; @Component({ selector: 'app-awardee-editor', @@ -12,14 +14,21 @@ import { AwardeeService } from "../shared/awardee.service"; }) export class AwardeeEditorComponent implements OnInit { + private imageMatcher = /\.(jpe?g|png|gif)$/i; + public years: Array = []; public awardee: Awardee; + public rawProfileImage: string = null; + public rawAwardImage: string = null; + + @ViewChild('profileImageUpload') profileImageUpload: ElementRef; + @ViewChild('awardImageUpload') awardImageUpload: ElementRef; constructor( - private titleService: Title, - private route: ActivatedRoute, - private router: Router, - private awardeeService: AwardeeService, + private titleService: Title, + private route: ActivatedRoute, + private router: Router, + private awardeeService: AwardeeService, ) {} ngOnInit() { @@ -33,17 +42,82 @@ export class AwardeeEditorComponent implements OnInit { }); } + public profileImageSelectionChange() { + if (this.profileImageUpload.nativeElement.files.length === 0) { + this.rawProfileImage = null; + return; + } + if (!this.imageMatcher.test(this.profileImageUpload.nativeElement.files[0].name)) { + return; + } + let reader = new FileReader(); + reader.addEventListener("load", () => this.rawProfileImage = reader.result); + reader.readAsDataURL(this.profileImageUpload.nativeElement.files[0]); + } + + public awardImageSelectionChange() { + if (this.awardImageUpload.nativeElement.files.length === 0) { + this.rawAwardImage = null; + return; + } + if (!this.imageMatcher.test(this.awardImageUpload.nativeElement.files[0].name)) { + return; + } + let reader = new FileReader(); + reader.addEventListener("load", () => this.rawAwardImage = reader.result); + reader.readAsDataURL(this.awardImageUpload.nativeElement.files[0]); + } + + public undoImageSelect(field: HTMLInputElement) { + field.value = null; + field.dispatchEvent(new Event('change')); + } + public saveAwardee() { if (this.canSave) { - this.awardeeService.persist(this.awardee).subscribe(() => this.router.navigate(['/awardees'])); + this.awardeeService.persist( + this.awardee + ).subscribe(savedAwardee => { + let observables = []; + if (this.rawProfileImage) { + observables.push( + this.awardeeService.saveImage( + savedAwardee.slug, + this.profileImageUpload.nativeElement.files[0], + 'profile' + ) + ); + } + if (this.rawAwardImage) { + observables.push( + this.awardeeService.saveImage( + savedAwardee.slug, + this.awardImageUpload.nativeElement.files[0], + 'award' + ) + ); + } + + observables.length + ? merge(...observables).subscribe(() => this.router.navigate(['/awardees'])) + : this.router.navigate(['/awardees']); + }); } } get canSave(): boolean { return [ - this.awardee.name, - this.awardee.text, - this.awardee.imageLabel].every(textField => textField.length > 0) - && this.awardee.year !== null; + this.awardee.name, + this.awardee.text, + this.awardee.imageLabel].every(textField => textField.length > 0) + && this.awardee.year !== null; + } + + get profileImage(): string { + return `${environment.apiUrl}/awardee-image/profile/${this.awardee.slug}` + } + + get awardImage(): string { + return `${environment.apiUrl}/awardee-image/award/${this.awardee.slug}` } } diff --git a/src/app/awardee-list-table/awardee-list-table-datasource.ts b/src/app/awardee-list-table/awardee-list-table-datasource.ts index 82bcadb..cd15457 100644 --- a/src/app/awardee-list-table/awardee-list-table-datasource.ts +++ b/src/app/awardee-list-table/awardee-list-table-datasource.ts @@ -4,6 +4,7 @@ import { map } from 'rxjs/operators'; import { Observable, of as observableOf, merge } from 'rxjs'; import { AwardeeService } from "../shared/awardee.service"; import { Awardee } from "../shared/awardee"; +import { EventEmitter } from "@angular/core"; /** * Data source for the AwardeeListTable view. This class should @@ -12,6 +13,9 @@ import { Awardee } from "../shared/awardee"; */ export class AwardeeListTableDataSource extends DataSource { + public filter: string = ''; + public filterChange: EventEmitter = new EventEmitter(); + constructor( private paginator: MatPaginator, private sort: MatSort, @@ -30,6 +34,7 @@ export class AwardeeListTableDataSource extends DataSource { // stream for the data-table to consume. const dataMutations = [ observableOf(this.awardeeService.awardees), + this.filterChange, this.awardeeService.changed, this.paginator.page, this.sort.sortChange @@ -39,7 +44,7 @@ export class AwardeeListTableDataSource extends DataSource { this.paginator.length = this.awardeeService.awardees.length; return merge(...dataMutations).pipe(map(() => { - return this.getPagedData(this.getSortedData([...this.awardeeService.awardees])); + return this.getPagedData(this.getSortedData(this.getFilteredData([...this.awardeeService.awardees]))); })); } @@ -75,6 +80,13 @@ export class AwardeeListTableDataSource extends DataSource { } }); } + + private getFilteredData(data: Awardee[]) { + return this.filter + ? data.filter( + row => row.name.toLocaleLowerCase().indexOf(this.filter) > -1 || row.year.toString().startsWith(this.filter)) + : data; + } } /** Simple sort comparator for example ID/Name columns (for client-side sorting). */ diff --git a/src/app/awardee-list-table/awardee-list-table.component.css b/src/app/awardee-list-table/awardee-list-table.component.css index d3f73aa..fdc77f0 100644 --- a/src/app/awardee-list-table/awardee-list-table.component.css +++ b/src/app/awardee-list-table/awardee-list-table.component.css @@ -6,3 +6,13 @@ .mat-column-buttons { flex: 0 0 100px; } + +.filter-header { + min-height: 64px; + padding: 8px 24px 0; +} + +.mat-form-field { + font-size: 14px; + width: 100%; +} diff --git a/src/app/awardee-list-table/awardee-list-table.component.html b/src/app/awardee-list-table/awardee-list-table.component.html index 0d25953..80c47c5 100644 --- a/src/app/awardee-list-table/awardee-list-table.component.html +++ b/src/app/awardee-list-table/awardee-list-table.component.html @@ -1,4 +1,9 @@
+
+ + + +
diff --git a/src/app/awardee-list-table/awardee-list-table.component.ts b/src/app/awardee-list-table/awardee-list-table.component.ts index 239d48d..5b9dcfd 100644 --- a/src/app/awardee-list-table/awardee-list-table.component.ts +++ b/src/app/awardee-list-table/awardee-list-table.component.ts @@ -27,6 +27,11 @@ export class AwardeeListTableComponent implements OnInit { this.dataSource = new AwardeeListTableDataSource(this.paginator, this.sort, this.awardeeService); } + applyFilter(filterValue: string) { + this.dataSource.filter = filterValue.trim().toLowerCase(); + this.dataSource.filterChange.emit(true); + } + get awardees(): Array { return this.awardeeService.awardees; } diff --git a/src/app/judge-editor/judge-editor.component.css b/src/app/judge-editor/judge-editor.component.css index 936f0c2..b1042be 100644 --- a/src/app/judge-editor/judge-editor.component.css +++ b/src/app/judge-editor/judge-editor.component.css @@ -29,3 +29,27 @@ mat-divider { .mat-radio-button + .mat-radio-button { margin-left: 20px; } + +.profile-image { + width: 80px; + height: 80px; + border-radius: 40px; +} + +.tobe-replaced { + opacity: .75; + filter: grayscale(100%); + /*background: black;*/ +} + +.image-preview-zone { + margin-bottom: 10px; +} + +.clickable { + cursor: pointer; +} + +.image-preview-zone > img { + margin-right: 10px; +} diff --git a/src/app/judge-editor/judge-editor.component.html b/src/app/judge-editor/judge-editor.component.html index 83fbf0c..f0af3ef 100644 --- a/src/app/judge-editor/judge-editor.component.html +++ b/src/app/judge-editor/judge-editor.component.html @@ -9,10 +9,18 @@ - +
+ + +
+ + + +
diff --git a/src/app/judge-editor/judge-editor.component.ts b/src/app/judge-editor/judge-editor.component.ts index 5f52ed7..94d7711 100644 --- a/src/app/judge-editor/judge-editor.component.ts +++ b/src/app/judge-editor/judge-editor.component.ts @@ -1,10 +1,11 @@ -import { Component, OnInit, ViewChild } from '@angular/core'; +import { Component, ElementRef, OnInit, ViewChild } from '@angular/core'; import { Title } from "@angular/platform-browser"; import { MatTable } from "@angular/material"; import { ActivatedRoute, Router } from "@angular/router"; import { Judge } from "../shared/judge"; import { JudgeService } from "../shared/judge.service"; import { JudgeTitle } from "../shared/judge-title"; +import { environment } from "../../environments/environment"; @Component({ selector: 'app-judge-editor', @@ -12,12 +13,17 @@ import { JudgeTitle } from "../shared/judge-title"; styleUrls: ['./judge-editor.component.css'] }) export class JudgeEditorComponent implements OnInit { + + private imageMatcher = /\.(jpe?g|png|gif)$/i; + @ViewChild(MatTable) private table: MatTable>; + @ViewChild('profileImageUpload') profileImageUpload: ElementRef; public judgedYearInput: JudgeTitle = new JudgeTitle(); public displayedColumns = ['buttons', 'year', 'title']; public judge: Judge = new Judge(); + public rawProfileImage: string = null; constructor( private judgeService: JudgeService, @@ -36,6 +42,24 @@ export class JudgeEditorComponent implements OnInit { }); } + public profileImageSelectionChange() { + if (this.profileImageUpload.nativeElement.files.length === 0) { + this.rawProfileImage = null; + return; + } + if (!this.imageMatcher.test(this.profileImageUpload.nativeElement.files[0].name)) { + return; + } + let reader = new FileReader(); + reader.addEventListener("load", () => this.rawProfileImage = reader.result); + reader.readAsDataURL(this.profileImageUpload.nativeElement.files[0]); + } + + public undoImageSelect(field: HTMLInputElement) { + field.value = null; + field.dispatchEvent(new Event('change')); + } + get canAdd(): boolean { return this.judgedYearInput.year != null && this.judgedYearInput.title.trim().length > 0; @@ -60,10 +84,21 @@ export class JudgeEditorComponent implements OnInit { public saveJudge() { if (this.canSave) { - this.judgeService.persist(this.judge).subscribe(() => this.router.navigate(['/judges'])); + this.judgeService.persist(this.judge).subscribe(savedJudge => { + this.rawProfileImage + ? this.judgeService.saveImage( + savedJudge.slug, + this.profileImageUpload.nativeElement.files[0] + ).subscribe(() => this.router.navigate(['/judges'])) + : this.router.navigate(['/judges']); + }); } } + get profileImage(): string { + return `${environment.apiUrl}/judge-image/${this.judge.slug}` + } + private sortByYear() { this.judge.titles.sort((a,b) => a.year < b.year ? 1 : -1); } diff --git a/src/app/judge-list-table/judge-list-table-datasource.ts b/src/app/judge-list-table/judge-list-table-datasource.ts index fb0950d..fca928d 100644 --- a/src/app/judge-list-table/judge-list-table-datasource.ts +++ b/src/app/judge-list-table/judge-list-table-datasource.ts @@ -4,6 +4,7 @@ import { map } from 'rxjs/operators'; import { Observable, of as observableOf, merge } from 'rxjs'; import { JudgeService } from "../shared/judge.service"; import { Judge } from "../shared/judge"; +import { EventEmitter } from "@angular/core"; /** * Data source for the JudgeListTable view. This class should @@ -12,6 +13,9 @@ import { Judge } from "../shared/judge"; */ export class JudgeListTableDataSource extends DataSource { + public filter: string = ''; + public filterChange: EventEmitter = new EventEmitter(); + constructor( private paginator: MatPaginator, private sort: MatSort, @@ -30,6 +34,7 @@ export class JudgeListTableDataSource extends DataSource { // stream for the data-table to consume. const dataMutations = [ observableOf(this.judgeService.judges), + this.filterChange, this.judgeService.changed, this.paginator.page, this.sort.sortChange @@ -39,7 +44,7 @@ export class JudgeListTableDataSource extends DataSource { this.paginator.length = this.judgeService.judges.length; return merge(...dataMutations).pipe(map(() => { - return this.getPagedData(this.getSortedData([...this.judgeService.judges])); + return this.getPagedData(this.getSortedData(this.getFilteredData([...this.judgeService.judges]))); })); } @@ -75,6 +80,16 @@ export class JudgeListTableDataSource extends DataSource { } }); } + + private getFilteredData(data: Judge[]) { + return this.filter + ? data.filter( + row => row.name.toLocaleLowerCase().indexOf(this.filter) > -1 + || row.titles.some( + title => title.year.toString().startsWith(this.filter) + )) + : data; + } } /** Simple sort comparator for example ID/Name columns (for client-side sorting). */ diff --git a/src/app/judge-list-table/judge-list-table.component.css b/src/app/judge-list-table/judge-list-table.component.css index d3f73aa..846c0b5 100644 --- a/src/app/judge-list-table/judge-list-table.component.css +++ b/src/app/judge-list-table/judge-list-table.component.css @@ -6,3 +6,13 @@ .mat-column-buttons { flex: 0 0 100px; } + +.filter-header { + min-height: 64px; + padding: 8px 24px 0; +} + +.mat-form-field { + font-size: 14px; + width: 100%; +} diff --git a/src/app/judge-list-table/judge-list-table.component.html b/src/app/judge-list-table/judge-list-table.component.html index 8c93e66..ef04fe6 100644 --- a/src/app/judge-list-table/judge-list-table.component.html +++ b/src/app/judge-list-table/judge-list-table.component.html @@ -1,4 +1,9 @@
+
+ + + +
diff --git a/src/app/judge-list-table/judge-list-table.component.ts b/src/app/judge-list-table/judge-list-table.component.ts index 91fa9ed..404b174 100644 --- a/src/app/judge-list-table/judge-list-table.component.ts +++ b/src/app/judge-list-table/judge-list-table.component.ts @@ -28,6 +28,11 @@ export class JudgeListTableComponent implements OnInit { this.dataSource = new JudgeListTableDataSource(this.paginator, this.sort, this.judgeService); } + applyFilter(filterValue: string) { + this.dataSource.filter = filterValue.trim().toLowerCase(); + this.dataSource.filterChange.emit(true); + } + get judges(): Array { return this.judgeService.judges; } diff --git a/src/app/shared/awardee.service.ts b/src/app/shared/awardee.service.ts index 8efde7c..6ee812a 100644 --- a/src/app/shared/awardee.service.ts +++ b/src/app/shared/awardee.service.ts @@ -12,11 +12,13 @@ import { Awardee } from "./awardee"; export class AwardeeService implements Resolve> { private apiEndPoint = `${environment.apiUrl}/awardee`; + private apiEndPointImage = `${environment.apiUrl}/awardee-image`; private cachedAwardees: Array = []; public changed: EventEmitter> = new EventEmitter>(); - constructor(private httpClient: HttpClient) {} + constructor(private httpClient: HttpClient) { + } public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise> { return this.getJudges().toPromise(); @@ -39,10 +41,10 @@ export class AwardeeService implements Resolve> { this.changed.emit(this.cachedAwardees); } - public persist(judge: Awardee): Observable { - return judge.id === null - ? this.create(judge) - : this.update(judge); + public persist(awardee: Awardee): Observable { + return awardee.id === null + ? this.create(awardee) + : this.update(awardee); } public create(awardee: Awardee): Observable { @@ -53,6 +55,12 @@ export class AwardeeService implements Resolve> { return this.httpClient.put(`${this.apiEndPoint}/${awardee.id}`, awardee); } + public saveImage(slug: string, image: File, type: string) { + let form = new FormData(); + form.append('image', image); + return this.httpClient.post(`${this.apiEndPointImage}/${type}/${slug}`, form); + } + public delete(id: number): Observable { return this.httpClient.delete(`${this.apiEndPoint}/${id}`); } diff --git a/src/app/shared/awardee.ts b/src/app/shared/awardee.ts index 5f9cb7b..1c4743c 100644 --- a/src/app/shared/awardee.ts +++ b/src/app/shared/awardee.ts @@ -4,4 +4,7 @@ export class Awardee { public name: string = ''; public text: string = ''; public imageLabel: string = ''; + public slug: string = ''; + public hasProfileImage: boolean = false; + public hasAwardImage: boolean = false; } diff --git a/src/app/shared/judge.service.ts b/src/app/shared/judge.service.ts index 1dc0dcf..8a77c76 100644 --- a/src/app/shared/judge.service.ts +++ b/src/app/shared/judge.service.ts @@ -5,6 +5,7 @@ import { Observable } from "rxjs/internal/Observable"; import { environment } from '../../environments/environment'; import { Judge } from "./judge"; +import { Awardee } from "./awardee"; @Injectable({ providedIn: 'root' @@ -12,6 +13,7 @@ import { Judge } from "./judge"; export class JudgeService implements Resolve> { private apiEndPoint = `${environment.apiUrl}/judge`; + private apiEndPointImage = `${environment.apiUrl}/judge-image`; private cachedJudges: Array = []; public changed: EventEmitter> = new EventEmitter>(); @@ -53,6 +55,12 @@ export class JudgeService implements Resolve> { return this.httpClient.put(`${this.apiEndPoint}/${judge.id}`, judge); } + public saveImage(slug: string, image: File) { + let form = new FormData(); + form.append('image', image); + return this.httpClient.post(`${this.apiEndPointImage}/${slug}`, form); + } + public delete(id: number): Observable { return this.httpClient.delete(`${this.apiEndPoint}/${id}`); } diff --git a/src/app/shared/judge.ts b/src/app/shared/judge.ts index 58eb113..bc09713 100644 --- a/src/app/shared/judge.ts +++ b/src/app/shared/judge.ts @@ -5,4 +5,6 @@ export class Judge { public prefix: string = null; public name: string = ''; public titles: Array = []; + public slug: string = ''; + public hasProfileImage: boolean = false; }