* table filters

* image upload implemented now
* auth token renewal
This commit is contained in:
Danyi Dávid 2018-05-13 22:35:37 +02:00
parent d614d1b020
commit a909486396
19 changed files with 323 additions and 34 deletions

View File

@ -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();
}
}

View File

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

View File

@ -2,10 +2,17 @@
<mat-form-field class="full-width">
<input name="name" type="text" matInput placeholder="Display name" [(ngModel)]="awardee.name">
</mat-form-field>
<button type="button" mat-raised-button>
<i class="far fa-image"></i>
Upload profile image
</button>
<div class="image-preview-zone">
<img [src]="profileImage" class="profile-image mat-elevation-z10" [class.tobe-replaced]="rawProfileImage"
*ngIf="awardee.hasProfileImage">
<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-form-field class="full-width">
<mat-select name="year" placeholder="Year" [(ngModel)]="awardee.year">
@ -16,9 +23,16 @@
<mat-form-field class="full-width">
<textarea name="text" matInput placeholder="Article text" rows="10" [(ngModel)]="awardee.text"></textarea>
</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
</button>
</label>
<mat-divider></mat-divider>
<mat-form-field class="full-width">

View File

@ -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<number> = [];
public awardee: Awardee;
public rawProfileImage: string = null;
public rawAwardImage: string = null;
@ViewChild('profileImageUpload') profileImageUpload: ElementRef<HTMLInputElement>;
@ViewChild('awardImageUpload') awardImageUpload: ElementRef<HTMLInputElement>;
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}`
}
}

View File

@ -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<Awardee> {
public filter: string = '';
public filterChange: EventEmitter<boolean> = new EventEmitter<boolean>();
constructor(
private paginator: MatPaginator,
private sort: MatSort,
@ -30,6 +34,7 @@ export class AwardeeListTableDataSource extends DataSource<Awardee> {
// 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<Awardee> {
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<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). */

View File

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

View File

@ -1,4 +1,9 @@
<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">
<ng-container matColumnDef="buttons">
<mat-header-cell *matHeaderCellDef class="controls"></mat-header-cell>

View File

@ -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<Awardee> {
return this.awardeeService.awardees;
}

View File

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

View File

@ -9,10 +9,18 @@
</mat-radio-group>
<mat-divider></mat-divider>
<button type="button" mat-raised-button>
<i class="far fa-image"></i>
Upload profile image
</button>
<div class="image-preview-zone">
<img [src]="profileImage" class="profile-image mat-elevation-z10" [class.tobe-replaced]="rawProfileImage"
*ngIf="judge.hasProfileImage">
<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>
<div class="mat-elevation-z8">
<mat-expansion-panel class="mat-elevation-z0">

View File

@ -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<Array<JudgeTitle>>;
@ViewChild('profileImageUpload') profileImageUpload: ElementRef<HTMLInputElement>;
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);
}

View File

@ -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<Judge> {
public filter: string = '';
public filterChange: EventEmitter<boolean> = new EventEmitter<boolean>();
constructor(
private paginator: MatPaginator,
private sort: MatSort,
@ -30,6 +34,7 @@ export class JudgeListTableDataSource extends DataSource<Judge> {
// 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<Judge> {
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<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). */

View File

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

View File

@ -1,4 +1,9 @@
<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">
<ng-container matColumnDef="buttons">
<mat-header-cell *matHeaderCellDef class="controls"></mat-header-cell>

View File

@ -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<Judge> {
return this.judgeService.judges;
}

View File

@ -12,11 +12,13 @@ import { Awardee } from "./awardee";
export class AwardeeService implements Resolve<Array<Awardee>> {
private apiEndPoint = `${environment.apiUrl}/awardee`;
private apiEndPointImage = `${environment.apiUrl}/awardee-image`;
private cachedAwardees: 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>> {
return this.getJudges().toPromise();
@ -39,10 +41,10 @@ export class AwardeeService implements Resolve<Array<Awardee>> {
this.changed.emit(this.cachedAwardees);
}
public persist(judge: Awardee): Observable<Awardee> {
return judge.id === null
? this.create(judge)
: this.update(judge);
public persist(awardee: Awardee): Observable<Awardee> {
return awardee.id === null
? this.create(awardee)
: this.update(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);
}
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> {
return this.httpClient.delete<boolean>(`${this.apiEndPoint}/${id}`);
}

View File

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

View File

@ -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<Array<Judge>> {
private apiEndPoint = `${environment.apiUrl}/judge`;
private apiEndPointImage = `${environment.apiUrl}/judge-image`;
private cachedJudges: 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);
}
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> {
return this.httpClient.delete<boolean>(`${this.apiEndPoint}/${id}`);
}

View File

@ -5,4 +5,6 @@ export class Judge {
public prefix: string = null;
public name: string = '';
public titles: Array<JudgeTitle> = [];
public slug: string = '';
public hasProfileImage: boolean = false;
}