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;
}