* table filters
* image upload implemented now * auth token renewal
This commit is contained in:
parent
d614d1b020
commit
a909486396
@ -1,15 +1,32 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { AuthService } from "./auth/auth.service";
|
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({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrls: ['./app.component.css']
|
styleUrls: ['./app.component.css']
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
private authRenewTimer: Subscription;
|
||||||
|
|
||||||
constructor(private authService: AuthService) {}
|
constructor(private authService: AuthService) {}
|
||||||
|
|
||||||
get loggedIn(): boolean {
|
ngOnInit(): void {
|
||||||
return this.authService.isLoggedIn;
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,36 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
button + mat-divider {
|
mat-divider {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 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;
|
||||||
|
}
|
||||||
@ -2,10 +2,17 @@
|
|||||||
<mat-form-field class="full-width">
|
<mat-form-field class="full-width">
|
||||||
<input name="name" type="text" matInput placeholder="Display name" [(ngModel)]="awardee.name">
|
<input name="name" type="text" matInput placeholder="Display name" [(ngModel)]="awardee.name">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button type="button" mat-raised-button>
|
<div class="image-preview-zone">
|
||||||
<i class="far fa-image"></i>
|
<img [src]="profileImage" class="profile-image mat-elevation-z10" [class.tobe-replaced]="rawProfileImage"
|
||||||
Upload profile image
|
*ngIf="awardee.hasProfileImage">
|
||||||
</button>
|
<img [src]="rawProfileImage" class="profile-image mat-elevation-z10 clickable" *ngIf="rawProfileImage"
|
||||||
|
(click)="undoImageSelect(profileImageUpload)" matTooltip="Remove image">
|
||||||
|
</div>
|
||||||
|
<input #profileImageUpload id="profileImageUpload" type="file" accept="image/*" hidden (change)="profileImageSelectionChange()">
|
||||||
|
<label for="profileImageUpload" class="mat-raised-button">
|
||||||
|
<i class="far fa-image"></i> Upload profile image
|
||||||
|
</label>
|
||||||
|
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<mat-form-field class="full-width">
|
<mat-form-field class="full-width">
|
||||||
<mat-select name="year" placeholder="Year" [(ngModel)]="awardee.year">
|
<mat-select name="year" placeholder="Year" [(ngModel)]="awardee.year">
|
||||||
@ -16,9 +23,16 @@
|
|||||||
<mat-form-field class="full-width">
|
<mat-form-field class="full-width">
|
||||||
<textarea name="text" matInput placeholder="Article text" rows="10" [(ngModel)]="awardee.text"></textarea>
|
<textarea name="text" matInput placeholder="Article text" rows="10" [(ngModel)]="awardee.text"></textarea>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<button type="button" mat-raised-button>
|
<div class="image-preview-zone">
|
||||||
|
<img [src]="awardImage" class="award-image mat-elevation-z10" [class.tobe-replaced]="rawAwardImage"
|
||||||
|
*ngIf="awardee.hasAwardImage">
|
||||||
|
<img [src]="rawAwardImage" class="award-image mat-elevation-z10 clickable" *ngIf="rawAwardImage"
|
||||||
|
(click)="undoImageSelect(awardImageUpload)" matTooltip="Remove image">
|
||||||
|
</div>
|
||||||
|
<input #awardImageUpload id="awardImageUpload" type="file" accept="image/*" hidden (change)="awardImageSelectionChange()">
|
||||||
|
<label for="awardImageUpload" class="mat-raised-button">
|
||||||
<i class="far fa-image"></i> Upload article image
|
<i class="far fa-image"></i> Upload article image
|
||||||
</button>
|
</label>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
<mat-form-field class="full-width">
|
<mat-form-field class="full-width">
|
||||||
|
|||||||
@ -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 { Title } from "@angular/platform-browser";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
|
|
||||||
import { Awardee } from "../shared/awardee";
|
import { Awardee } from "../shared/awardee";
|
||||||
import { AwardeeService } from "../shared/awardee.service";
|
import { AwardeeService } from "../shared/awardee.service";
|
||||||
|
import { environment } from "../../environments/environment";
|
||||||
|
import { merge } from "rxjs";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-awardee-editor',
|
selector: 'app-awardee-editor',
|
||||||
@ -12,14 +14,21 @@ import { AwardeeService } from "../shared/awardee.service";
|
|||||||
})
|
})
|
||||||
export class AwardeeEditorComponent implements OnInit {
|
export class AwardeeEditorComponent implements OnInit {
|
||||||
|
|
||||||
|
private imageMatcher = /\.(jpe?g|png|gif)$/i;
|
||||||
|
|
||||||
public years: Array<number> = [];
|
public years: Array<number> = [];
|
||||||
public awardee: Awardee;
|
public awardee: Awardee;
|
||||||
|
public rawProfileImage: string = null;
|
||||||
|
public rawAwardImage: string = null;
|
||||||
|
|
||||||
|
@ViewChild('profileImageUpload') profileImageUpload: ElementRef<HTMLInputElement>;
|
||||||
|
@ViewChild('awardImageUpload') awardImageUpload: ElementRef<HTMLInputElement>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private titleService: Title,
|
private titleService: Title,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private awardeeService: AwardeeService,
|
private awardeeService: AwardeeService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
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() {
|
public saveAwardee() {
|
||||||
if (this.canSave) {
|
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 {
|
get canSave(): boolean {
|
||||||
return [
|
return [
|
||||||
this.awardee.name,
|
this.awardee.name,
|
||||||
this.awardee.text,
|
this.awardee.text,
|
||||||
this.awardee.imageLabel].every(textField => textField.length > 0)
|
this.awardee.imageLabel].every(textField => textField.length > 0)
|
||||||
&& this.awardee.year !== null;
|
&& 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}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { map } from 'rxjs/operators';
|
|||||||
import { Observable, of as observableOf, merge } from 'rxjs';
|
import { Observable, of as observableOf, merge } from 'rxjs';
|
||||||
import { AwardeeService } from "../shared/awardee.service";
|
import { AwardeeService } from "../shared/awardee.service";
|
||||||
import { Awardee } from "../shared/awardee";
|
import { Awardee } from "../shared/awardee";
|
||||||
|
import { EventEmitter } from "@angular/core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data source for the AwardeeListTable view. This class should
|
* Data source for the AwardeeListTable view. This class should
|
||||||
@ -12,6 +13,9 @@ import { Awardee } from "../shared/awardee";
|
|||||||
*/
|
*/
|
||||||
export class AwardeeListTableDataSource extends DataSource<Awardee> {
|
export class AwardeeListTableDataSource extends DataSource<Awardee> {
|
||||||
|
|
||||||
|
public filter: string = '';
|
||||||
|
public filterChange: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private paginator: MatPaginator,
|
private paginator: MatPaginator,
|
||||||
private sort: MatSort,
|
private sort: MatSort,
|
||||||
@ -30,6 +34,7 @@ export class AwardeeListTableDataSource extends DataSource<Awardee> {
|
|||||||
// stream for the data-table to consume.
|
// stream for the data-table to consume.
|
||||||
const dataMutations = [
|
const dataMutations = [
|
||||||
observableOf(this.awardeeService.awardees),
|
observableOf(this.awardeeService.awardees),
|
||||||
|
this.filterChange,
|
||||||
this.awardeeService.changed,
|
this.awardeeService.changed,
|
||||||
this.paginator.page,
|
this.paginator.page,
|
||||||
this.sort.sortChange
|
this.sort.sortChange
|
||||||
@ -39,7 +44,7 @@ export class AwardeeListTableDataSource extends DataSource<Awardee> {
|
|||||||
this.paginator.length = this.awardeeService.awardees.length;
|
this.paginator.length = this.awardeeService.awardees.length;
|
||||||
|
|
||||||
return merge(...dataMutations).pipe(map(() => {
|
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<Awardee> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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). */
|
/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
|
||||||
|
|||||||
@ -6,3 +6,13 @@
|
|||||||
.mat-column-buttons {
|
.mat-column-buttons {
|
||||||
flex: 0 0 100px;
|
flex: 0 0 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-header {
|
||||||
|
min-height: 64px;
|
||||||
|
padding: 8px 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-form-field {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
<div class="mat-elevation-z8">
|
<div class="mat-elevation-z8">
|
||||||
|
<div class="filter-header">
|
||||||
|
<mat-form-field>
|
||||||
|
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
|
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
|
||||||
<ng-container matColumnDef="buttons">
|
<ng-container matColumnDef="buttons">
|
||||||
<mat-header-cell *matHeaderCellDef class="controls"></mat-header-cell>
|
<mat-header-cell *matHeaderCellDef class="controls"></mat-header-cell>
|
||||||
|
|||||||
@ -27,6 +27,11 @@ export class AwardeeListTableComponent implements OnInit {
|
|||||||
this.dataSource = new AwardeeListTableDataSource(this.paginator, this.sort, this.awardeeService);
|
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<Awardee> {
|
get awardees(): Array<Awardee> {
|
||||||
return this.awardeeService.awardees;
|
return this.awardeeService.awardees;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,3 +29,27 @@ mat-divider {
|
|||||||
.mat-radio-button + .mat-radio-button {
|
.mat-radio-button + .mat-radio-button {
|
||||||
margin-left: 20px;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -9,10 +9,18 @@
|
|||||||
</mat-radio-group>
|
</mat-radio-group>
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
|
|
||||||
<button type="button" mat-raised-button>
|
<div class="image-preview-zone">
|
||||||
<i class="far fa-image"></i>
|
<img [src]="profileImage" class="profile-image mat-elevation-z10" [class.tobe-replaced]="rawProfileImage"
|
||||||
Upload profile image
|
*ngIf="judge.hasProfileImage">
|
||||||
</button>
|
<img [src]="rawProfileImage" class="profile-image mat-elevation-z10 clickable" *ngIf="rawProfileImage"
|
||||||
|
(click)="undoImageSelect(profileImageUpload)" matTooltip="Remove image">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input #profileImageUpload id="profileImageUpload" type="file" accept="image/*" hidden (change)="profileImageSelectionChange()">
|
||||||
|
<label for="profileImageUpload" class="mat-raised-button">
|
||||||
|
<i class="far fa-image"></i> Upload profile image
|
||||||
|
</label>
|
||||||
|
|
||||||
<mat-divider></mat-divider>
|
<mat-divider></mat-divider>
|
||||||
<div class="mat-elevation-z8">
|
<div class="mat-elevation-z8">
|
||||||
<mat-expansion-panel class="mat-elevation-z0">
|
<mat-expansion-panel class="mat-elevation-z0">
|
||||||
|
|||||||
@ -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 { Title } from "@angular/platform-browser";
|
||||||
import { MatTable } from "@angular/material";
|
import { MatTable } from "@angular/material";
|
||||||
import { ActivatedRoute, Router } from "@angular/router";
|
import { ActivatedRoute, Router } from "@angular/router";
|
||||||
import { Judge } from "../shared/judge";
|
import { Judge } from "../shared/judge";
|
||||||
import { JudgeService } from "../shared/judge.service";
|
import { JudgeService } from "../shared/judge.service";
|
||||||
import { JudgeTitle } from "../shared/judge-title";
|
import { JudgeTitle } from "../shared/judge-title";
|
||||||
|
import { environment } from "../../environments/environment";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-judge-editor',
|
selector: 'app-judge-editor',
|
||||||
@ -12,12 +13,17 @@ import { JudgeTitle } from "../shared/judge-title";
|
|||||||
styleUrls: ['./judge-editor.component.css']
|
styleUrls: ['./judge-editor.component.css']
|
||||||
})
|
})
|
||||||
export class JudgeEditorComponent implements OnInit {
|
export class JudgeEditorComponent implements OnInit {
|
||||||
|
|
||||||
|
private imageMatcher = /\.(jpe?g|png|gif)$/i;
|
||||||
|
|
||||||
@ViewChild(MatTable) private table: MatTable<Array<JudgeTitle>>;
|
@ViewChild(MatTable) private table: MatTable<Array<JudgeTitle>>;
|
||||||
|
@ViewChild('profileImageUpload') profileImageUpload: ElementRef<HTMLInputElement>;
|
||||||
|
|
||||||
public judgedYearInput: JudgeTitle = new JudgeTitle();
|
public judgedYearInput: JudgeTitle = new JudgeTitle();
|
||||||
public displayedColumns = ['buttons', 'year', 'title'];
|
public displayedColumns = ['buttons', 'year', 'title'];
|
||||||
|
|
||||||
public judge: Judge = new Judge();
|
public judge: Judge = new Judge();
|
||||||
|
public rawProfileImage: string = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private judgeService: JudgeService,
|
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 {
|
get canAdd(): boolean {
|
||||||
return this.judgedYearInput.year != null
|
return this.judgedYearInput.year != null
|
||||||
&& this.judgedYearInput.title.trim().length > 0;
|
&& this.judgedYearInput.title.trim().length > 0;
|
||||||
@ -60,10 +84,21 @@ export class JudgeEditorComponent implements OnInit {
|
|||||||
|
|
||||||
public saveJudge() {
|
public saveJudge() {
|
||||||
if (this.canSave) {
|
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() {
|
private sortByYear() {
|
||||||
this.judge.titles.sort((a,b) => a.year < b.year ? 1 : -1);
|
this.judge.titles.sort((a,b) => a.year < b.year ? 1 : -1);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { map } from 'rxjs/operators';
|
|||||||
import { Observable, of as observableOf, merge } from 'rxjs';
|
import { Observable, of as observableOf, merge } from 'rxjs';
|
||||||
import { JudgeService } from "../shared/judge.service";
|
import { JudgeService } from "../shared/judge.service";
|
||||||
import { Judge } from "../shared/judge";
|
import { Judge } from "../shared/judge";
|
||||||
|
import { EventEmitter } from "@angular/core";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data source for the JudgeListTable view. This class should
|
* Data source for the JudgeListTable view. This class should
|
||||||
@ -12,6 +13,9 @@ import { Judge } from "../shared/judge";
|
|||||||
*/
|
*/
|
||||||
export class JudgeListTableDataSource extends DataSource<Judge> {
|
export class JudgeListTableDataSource extends DataSource<Judge> {
|
||||||
|
|
||||||
|
public filter: string = '';
|
||||||
|
public filterChange: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private paginator: MatPaginator,
|
private paginator: MatPaginator,
|
||||||
private sort: MatSort,
|
private sort: MatSort,
|
||||||
@ -30,6 +34,7 @@ export class JudgeListTableDataSource extends DataSource<Judge> {
|
|||||||
// stream for the data-table to consume.
|
// stream for the data-table to consume.
|
||||||
const dataMutations = [
|
const dataMutations = [
|
||||||
observableOf(this.judgeService.judges),
|
observableOf(this.judgeService.judges),
|
||||||
|
this.filterChange,
|
||||||
this.judgeService.changed,
|
this.judgeService.changed,
|
||||||
this.paginator.page,
|
this.paginator.page,
|
||||||
this.sort.sortChange
|
this.sort.sortChange
|
||||||
@ -39,7 +44,7 @@ export class JudgeListTableDataSource extends DataSource<Judge> {
|
|||||||
this.paginator.length = this.judgeService.judges.length;
|
this.paginator.length = this.judgeService.judges.length;
|
||||||
|
|
||||||
return merge(...dataMutations).pipe(map(() => {
|
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<Judge> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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). */
|
/** Simple sort comparator for example ID/Name columns (for client-side sorting). */
|
||||||
|
|||||||
@ -6,3 +6,13 @@
|
|||||||
.mat-column-buttons {
|
.mat-column-buttons {
|
||||||
flex: 0 0 100px;
|
flex: 0 0 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-header {
|
||||||
|
min-height: 64px;
|
||||||
|
padding: 8px 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mat-form-field {
|
||||||
|
font-size: 14px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
<div class="mat-elevation-z8">
|
<div class="mat-elevation-z8">
|
||||||
|
<div class="filter-header">
|
||||||
|
<mat-form-field>
|
||||||
|
<input matInput (keyup)="applyFilter($event.target.value)" placeholder="Filter">
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
|
<mat-table #table [dataSource]="dataSource" matSort aria-label="Elements">
|
||||||
<ng-container matColumnDef="buttons">
|
<ng-container matColumnDef="buttons">
|
||||||
<mat-header-cell *matHeaderCellDef class="controls"></mat-header-cell>
|
<mat-header-cell *matHeaderCellDef class="controls"></mat-header-cell>
|
||||||
|
|||||||
@ -28,6 +28,11 @@ export class JudgeListTableComponent implements OnInit {
|
|||||||
this.dataSource = new JudgeListTableDataSource(this.paginator, this.sort, this.judgeService);
|
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<Judge> {
|
get judges(): Array<Judge> {
|
||||||
return this.judgeService.judges;
|
return this.judgeService.judges;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,11 +12,13 @@ import { Awardee } from "./awardee";
|
|||||||
export class AwardeeService implements Resolve<Array<Awardee>> {
|
export class AwardeeService implements Resolve<Array<Awardee>> {
|
||||||
|
|
||||||
private apiEndPoint = `${environment.apiUrl}/awardee`;
|
private apiEndPoint = `${environment.apiUrl}/awardee`;
|
||||||
|
private apiEndPointImage = `${environment.apiUrl}/awardee-image`;
|
||||||
private cachedAwardees: Array<Awardee> = [];
|
private cachedAwardees: Array<Awardee> = [];
|
||||||
|
|
||||||
public changed: EventEmitter<Array<Awardee>> = new EventEmitter<Array<Awardee>>();
|
public changed: EventEmitter<Array<Awardee>> = new EventEmitter<Array<Awardee>>();
|
||||||
|
|
||||||
constructor(private httpClient: HttpClient) {}
|
constructor(private httpClient: HttpClient) {
|
||||||
|
}
|
||||||
|
|
||||||
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Array<Awardee>> {
|
public resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Array<Awardee>> {
|
||||||
return this.getJudges().toPromise();
|
return this.getJudges().toPromise();
|
||||||
@ -39,10 +41,10 @@ export class AwardeeService implements Resolve<Array<Awardee>> {
|
|||||||
this.changed.emit(this.cachedAwardees);
|
this.changed.emit(this.cachedAwardees);
|
||||||
}
|
}
|
||||||
|
|
||||||
public persist(judge: Awardee): Observable<Awardee> {
|
public persist(awardee: Awardee): Observable<Awardee> {
|
||||||
return judge.id === null
|
return awardee.id === null
|
||||||
? this.create(judge)
|
? this.create(awardee)
|
||||||
: this.update(judge);
|
: this.update(awardee);
|
||||||
}
|
}
|
||||||
|
|
||||||
public create(awardee: Awardee): Observable<Awardee> {
|
public create(awardee: Awardee): Observable<Awardee> {
|
||||||
@ -53,6 +55,12 @@ export class AwardeeService implements Resolve<Array<Awardee>> {
|
|||||||
return this.httpClient.put<Awardee>(`${this.apiEndPoint}/${awardee.id}`, awardee);
|
return this.httpClient.put<Awardee>(`${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<Awardee>(`${this.apiEndPointImage}/${type}/${slug}`, form);
|
||||||
|
}
|
||||||
|
|
||||||
public delete(id: number): Observable<boolean> {
|
public delete(id: number): Observable<boolean> {
|
||||||
return this.httpClient.delete<boolean>(`${this.apiEndPoint}/${id}`);
|
return this.httpClient.delete<boolean>(`${this.apiEndPoint}/${id}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,4 +4,7 @@ export class Awardee {
|
|||||||
public name: string = '';
|
public name: string = '';
|
||||||
public text: string = '';
|
public text: string = '';
|
||||||
public imageLabel: string = '';
|
public imageLabel: string = '';
|
||||||
|
public slug: string = '';
|
||||||
|
public hasProfileImage: boolean = false;
|
||||||
|
public hasAwardImage: boolean = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { Observable } from "rxjs/internal/Observable";
|
|||||||
|
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { Judge } from "./judge";
|
import { Judge } from "./judge";
|
||||||
|
import { Awardee } from "./awardee";
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -12,6 +13,7 @@ import { Judge } from "./judge";
|
|||||||
export class JudgeService implements Resolve<Array<Judge>> {
|
export class JudgeService implements Resolve<Array<Judge>> {
|
||||||
|
|
||||||
private apiEndPoint = `${environment.apiUrl}/judge`;
|
private apiEndPoint = `${environment.apiUrl}/judge`;
|
||||||
|
private apiEndPointImage = `${environment.apiUrl}/judge-image`;
|
||||||
private cachedJudges: Array<Judge> = [];
|
private cachedJudges: Array<Judge> = [];
|
||||||
|
|
||||||
public changed: EventEmitter<Array<Judge>> = new EventEmitter<Array<Judge>>();
|
public changed: EventEmitter<Array<Judge>> = new EventEmitter<Array<Judge>>();
|
||||||
@ -53,6 +55,12 @@ export class JudgeService implements Resolve<Array<Judge>> {
|
|||||||
return this.httpClient.put<Judge>(`${this.apiEndPoint}/${judge.id}`, judge);
|
return this.httpClient.put<Judge>(`${this.apiEndPoint}/${judge.id}`, judge);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public saveImage(slug: string, image: File) {
|
||||||
|
let form = new FormData();
|
||||||
|
form.append('image', image);
|
||||||
|
return this.httpClient.post<Awardee>(`${this.apiEndPointImage}/${slug}`, form);
|
||||||
|
}
|
||||||
|
|
||||||
public delete(id: number): Observable<boolean> {
|
public delete(id: number): Observable<boolean> {
|
||||||
return this.httpClient.delete<boolean>(`${this.apiEndPoint}/${id}`);
|
return this.httpClient.delete<boolean>(`${this.apiEndPoint}/${id}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,4 +5,6 @@ export class Judge {
|
|||||||
public prefix: string = null;
|
public prefix: string = null;
|
||||||
public name: string = '';
|
public name: string = '';
|
||||||
public titles: Array<JudgeTitle> = [];
|
public titles: Array<JudgeTitle> = [];
|
||||||
|
public slug: string = '';
|
||||||
|
public hasProfileImage: boolean = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user