add events page to customer frontend

This commit is contained in:
Roland Schneider 2019-07-11 16:32:36 +02:00 committed by Roland Schneider
parent b60bf5a377
commit e5797f41a8
11 changed files with 480 additions and 90 deletions

View File

@ -498,6 +498,35 @@
} }
} }
}, },
"@fortawesome/angular-fontawesome": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@fortawesome/angular-fontawesome/-/angular-fontawesome-0.4.0.tgz",
"integrity": "sha512-DYVXdCzwQo6d0CxVMRK+10LpBAvYN9xigWeQW4wKYq/Czd5es46nPMKixB5rHfNViECwwlM2gTM61K4DpxlJxg==",
"requires": {
"tslib": "^1.9.0"
}
},
"@fortawesome/fontawesome-common-types": {
"version": "0.2.19",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.19.tgz",
"integrity": "sha512-nd2Ul/CUs8U9sjofQYAALzOGpgkVJQgEhIJnOHaoyVR/LeC3x2mVg4eB910a4kS6WgLPebAY0M2fApEI497raQ=="
},
"@fortawesome/fontawesome-svg-core": {
"version": "1.2.19",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.19.tgz",
"integrity": "sha512-D4ICXg9oU08eF9o7Or392gPpjmwwgJu8ecCFusthbID95CLVXOgIyd4mOKD9Nud5Ckz+Ty59pqkNtThDKR0erA==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.19"
}
},
"@fortawesome/free-solid-svg-icons": {
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.9.0.tgz",
"integrity": "sha512-U8YXPfWcSozsCW0psCtlRGKjjRs5+Am5JJwLOUmVHFZbIEWzaz4YbP84EoPwUsVmSAKrisu3QeNcVOtmGml0Xw==",
"requires": {
"@fortawesome/fontawesome-common-types": "^0.2.19"
}
},
"@ngtools/webpack": { "@ngtools/webpack": {
"version": "8.0.6", "version": "8.0.6",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-8.0.6.tgz", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-8.0.6.tgz",

View File

@ -19,6 +19,9 @@
"@angular/platform-browser": "~8.0.3", "@angular/platform-browser": "~8.0.3",
"@angular/platform-browser-dynamic": "~8.0.3", "@angular/platform-browser-dynamic": "~8.0.3",
"@angular/router": "~8.0.3", "@angular/router": "~8.0.3",
"@fortawesome/angular-fontawesome": "^0.4.0",
"@fortawesome/fontawesome-svg-core": "^1.2.19",
"@fortawesome/free-solid-svg-icons": "^5.9.0",
"bootstrap": "^4.3.1", "bootstrap": "^4.3.1",
"ngx-bootstrap": "^5.0.0", "ngx-bootstrap": "^5.0.0",
"rxjs": "~6.4.0", "rxjs": "~6.4.0",

View File

@ -1,5 +1,6 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router"; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";
import {AuthenticationService} from "../services/authentication.service";
/* /*
The auth guard is used to prevent unauthenticated users from accessing restricted routes. The auth guard is used to prevent unauthenticated users from accessing restricted routes.
@ -13,10 +14,10 @@ There can be other conditions too, like role based authentication
@Injectable({providedIn: 'root'}) @Injectable({providedIn: 'root'})
export class AuthGuard implements CanActivate{ export class AuthGuard implements CanActivate{
constructor(private router: Router){} constructor(private router: Router, private authenticationService: AuthenticationService){}
canActivate(router: ActivatedRouteSnapshot, state: RouterStateSnapshot){ canActivate(router: ActivatedRouteSnapshot, state: RouterStateSnapshot){
// check if the user is logged in // check if the user is logged in
if(localStorage.getItem('currentUser')){ if(this.authenticationService.isLoggedIn()){
return true; return true;
} }

View File

@ -1,7 +1,17 @@
import { Injectable } from "@angular/core"; import {Injectable} from "@angular/core";
import { HttpRequest, HttpResponse, HttpHandler, HttpEvent, HttpInterceptor, HTTP_INTERCEPTORS } from '@angular/common/http'; import {
import { Observable, of, throwError } from 'rxjs'; HttpRequest,
import { delay, mergeMap, materialize, dematerialize } from 'rxjs/operators';; HttpResponse,
HttpHandler,
HttpEvent,
HttpInterceptor,
HTTP_INTERCEPTORS
} from '@angular/common/http';
import {Observable, of, throwError} from 'rxjs';
import {delay, mergeMap, materialize, dematerialize} from 'rxjs/operators';
import {Event, EventType, Trainer} from "../services/event.service";
;
/* /*
FAKE BACKEND FAKE BACKEND
@ -19,92 +29,180 @@ API
@Injectable() @Injectable()
export class FakeBackendInterceptor implements HttpInterceptor{ export class FakeBackendInterceptor implements HttpInterceptor {
constructor(){} constructor() {
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>>{ intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
// test user - (one of the users detais in database) // test user - (one of the users detais in database)
let testUser = { let testUser = {
id: 1, id: 1,
username: 'test', username: 'test',
password: 'test', password: 'test',
firstName: 'Test', firstName: 'Test',
lastName: 'User' lastName: 'User'
};
let eventTypes: EventType[] = [
{
id: 1,
name: 'Típus 1'
},
{
id: 2,
name: 'Típus 2'
}
];
let trainer1: Trainer = {
id: 1,
name: "Fitness Trainer"
};
let trainer2: Trainer = {
id: 1,
name: "weight lift Trainer"
};
let events: Event[] = [
];
let id = 1000;
for (let dayIndex = 0; dayIndex < 28; dayIndex++) {
for (let hourIndex = 8; hourIndex < 22; hourIndex++) {
for (let itemIndex = 0; itemIndex < 3; itemIndex++) {
id = id + 1;
let reservedAt = id % 2 ? this.createEventDate(0,1) : null;
events.push(
{
id: id,
name: 'esemény ' + id,
start: this.createEventDate(dayIndex, hourIndex),
end: this.createEventDate(dayIndex, hourIndex + 1),
trainer: [trainer1, trainer2][id % 2],
eventType: eventTypes[id % eventTypes.length],
reservedAt: reservedAt,
reservationCount: id% 11,
seatCount: 10,
}
)
}
}
}
console.info('events.size', events.length);
// wrapping the API's in delayed observable to simulate the Server API Calls
return of(null).pipe(
mergeMap(() => {
// API 1: Authenticate - Will be hit by Authentication Service - STARTS
if (request.url.endsWith('/users/authenticate') && request.method === 'POST') {
//check the credentials entered by the user with the data in database
if (request.body.username === testUser.username && request.body.password === testUser.password) {
// if login details are valid, return status 200 with a Fake JWT Token
let body = {
id: testUser.id,
username: testUser.username,
firstName: testUser.firstName,
lastName: testUser.lastName,
token: '0000-fake-jwt-token-0000'
}
return of(new HttpResponse({status: 200, body}))
} else {
// if the credentials by user doesn't match the data in the db, return Status 400 - Bad Request
return throwError({
error: {
message: 'Username or Password is incorrect.'
}
})
}
}
// API 1: Authenticate - Will be hit by Authentication Service - ENDS
// SECURE API END POINT - will check for valid JWT Token in Request
// API 2: Get all users data (we now have only 1 user - testUser) - STARTS
if (request.url.endsWith('/users') && request.method === 'GET') {
// check for a fake jwt token. If valid JWT token found, return the list of users, else throw error
if (request.headers.get('Authorization') === 'Bearer 0000-fake-jwt-token-0000') {
return of(new HttpResponse({status: 200, body: [testUser]}));
} else {
// invalid JWT token found in request header
return throwError({
error: {
message: 'Unauthorized'
}
});
}
}
// API 2: Get all users data (we now have only 1 user - testUser) - ENDS
if (request.url.endsWith('/event-type') && request.method === 'GET') {
// check for a fake jwt token. If valid JWT token found, return the list of users, else throw error
if (request.headers.get('Authorization') === 'Bearer 0000-fake-jwt-token-0000') {
return of(new HttpResponse({status: 200, body: eventTypes}));
} else {
// invalid JWT token found in request header
return throwError({
error: {
message: 'Unauthorized'
}
});
}
} }
// wrapping the API's in delayed observable to simulate the Server API Calls
return of(null).pipe(
mergeMap(() => {
// API 1: Authenticate - Will be hit by Authentication Service - STARTS if (request.url.indexOf('/events&id_event_type') && request.method === 'GET') {
if(request.url.endsWith('/users/authenticate') && request.method === 'POST'){
// check for a fake jwt token. If valid JWT token found, return the list of users, else throw error
if (request.headers.get('Authorization') === 'Bearer 0000-fake-jwt-token-0000') {
return of(new HttpResponse({status: 200, body: events}));
} else {
// invalid JWT token found in request header
return throwError({
error: {
message: 'Unauthorized'
}
});
}
}
//check the credentials entered by the user with the data in database // Pass any other requests left (unhandled
if(request.body.username === testUser.username && request.body.password === testUser.password){ return next.handle(request);
})
)
// call materialize and dematerialize to ensure delay even if an error is thrown
.pipe(materialize())
.pipe(delay(500))
.pipe(dematerialize());
}
// if login details are valid, return status 200 with a Fake JWT Token createEventDate(plusDays: number, plusHours: number) {
let body = { let date = new Date();
id: testUser.id, date = new Date(date.getTime() + (plusDays * 1000 * 60 * 60 * 24));
username: testUser.username, date.setHours(plusHours);
firstName: testUser.firstName, return date.getTime();
lastName: testUser.lastName, }
token: '0000-fake-jwt-token-0000'
}
return of(new HttpResponse({status: 200, body}))
}
else {
// if the credentials by user doesn't match the data in the db, return Status 400 - Bad Request
return throwError({
error: {
message: 'Username or Password is incorrect.'
}
})
}
}
// API 1: Authenticate - Will be hit by Authentication Service - ENDS
// SECURE API END POINT - will check for valid JWT Token in Request
// API 2: Get all users data (we now have only 1 user - testUser) - STARTS
if(request.url.endsWith('/users') && request.method === 'GET'){
// check for a fake jwt token. If valid JWT token found, return the list of users, else throw error
if(request.headers.get('Authorization') === 'Bearer 0000-fake-jwt-token-0000'){
return of(new HttpResponse({status: 200, body: [testUser]}));
}
else{
// invalid JWT token found in request header
return throwError({
error: {
message: 'Unauthorized'
}
});
}
}
// API 2: Get all users data (we now have only 1 user - testUser) - ENDS
// Pass any other requests left (unhandled
return next.handle(request);
})
)
// call materialize and dematerialize to ensure delay even if an error is thrown
.pipe(materialize())
.pipe(delay(500))
.pipe(dematerialize());
}
} }
// creating a PROVIDER // creating a PROVIDER
export let fakeBackendProvider = { export let fakeBackendProvider = {
// use fake backend in place of Http service for backend-less development // use fake backend in place of Http service for backend-less development
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
useClass: FakeBackendInterceptor, useClass: FakeBackendInterceptor,
multi: true multi: true
}; };

View File

@ -1,12 +1,12 @@
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core'; import {LOCALE_ID, NgModule} from '@angular/core';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { FitNavigationComponent } from './components/fit-navigation/fit-navigation.component'; import { FitNavigationComponent } from './components/fit-navigation/fit-navigation.component';
import { FitSlidesComponent } from './components/fit-slides/fit-slides.component'; import { FitSlidesComponent } from './components/fit-slides/fit-slides.component';
import {BrowserAnimationsModule} from "@angular/platform-browser/animations"; import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {CollapseModule} from "ngx-bootstrap"; import {CollapseModule, TypeaheadModule} from "ngx-bootstrap";
import { HomeComponent } from './pages/home/home.component'; import { HomeComponent } from './pages/home/home.component';
import { LoginComponent } from './pages/login/login.component'; import { LoginComponent } from './pages/login/login.component';
import { ProfileComponent } from './pages/profile/profile.component'; import { ProfileComponent } from './pages/profile/profile.component';
@ -15,6 +15,19 @@ import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { fakeBackendProvider } from './_helpers'; import { fakeBackendProvider } from './_helpers';
import {ReactiveFormsModule} from "@angular/forms"; import {ReactiveFormsModule} from "@angular/forms";
import { EventsComponent } from './pages/events/events.component'; import { EventsComponent } from './pages/events/events.component';
import { FitEventTypesComponent } from './components/fit-event-types/fit-event-types.component';
import { registerLocaleData } from '@angular/common';
import localeHu from '@angular/common/locales/hu';
import {FontAwesomeModule} from "@fortawesome/angular-fontawesome";
import { library } from '@fortawesome/fontawesome-svg-core';
import { faUserPlus , faUserMinus } from '@fortawesome/free-solid-svg-icons';
// the second parameter 'fr' is optional
registerLocaleData(localeHu, 'hu');
@NgModule({ @NgModule({
@ -26,7 +39,7 @@ import { EventsComponent } from './pages/events/events.component';
LoginComponent, LoginComponent,
ProfileComponent, ProfileComponent,
EventsComponent, EventsComponent,
FitEventTypesComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
@ -34,9 +47,12 @@ import { EventsComponent } from './pages/events/events.component';
ReactiveFormsModule, ReactiveFormsModule,
BrowserAnimationsModule, BrowserAnimationsModule,
CollapseModule.forRoot(), CollapseModule.forRoot(),
HttpClientModule HttpClientModule,
TypeaheadModule.forRoot(),
FontAwesomeModule,
], ],
providers: [ providers: [
{ provide: LOCALE_ID, useValue: "hu-hu" },
{ provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: JwtInterceptor, multi: true },
{ provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true }, { provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true },
@ -45,4 +61,10 @@ import { EventsComponent } from './pages/events/events.component';
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
export class AppModule { } export class AppModule {
constructor(){
library.add(faUserPlus);
library.add(faUserMinus);
}
}

View File

@ -1 +1,48 @@
<p>events works!</p> <div class="container">
<div>
<form [formGroup]="eventTypeForm">
<app-fit-event-types formControlName="eventType"></app-fit-event-types>
</form>
</div>
<ng-container *ngFor="let eventDay of eventDays">
<div class="row">
<div class="col-12"><h1>{{eventDay.date | date }}</h1></div>
</div>
<ng-container *ngFor="let eventHour of eventDay.hours">
<div class="row">
<div class="col-12 col-sm-12">
<h2>{{eventHour.hour | date:'HH:mm'}}</h2>
</div>
</div>
<div class="event-items ">
<ng-container *ngFor="let event of eventHour.events">
<div class="event-item ">
<div class="row ">
<div class="col-lg-3 col-sm-12 " >
<span class="pl-2">
{{event.start | date:'HH:mm' }} - {{event.end | date:'HH:mm' }}
</span>
</div>
<div class="col-lg-3 col-sm-12">
<span class="pl-2">
{{event.eventType.name }} ( {{event.reservationCount}}/{{event.seatCount}}
</span>
</div>
<div class="col-lg-3 col-sm-12">
<span class="pl-2">
{{event.trainer.name}}
</span>
</div>
<div class="col-lg-3 col-sm-12 text-sm-left text-lg-right">
<a *ngIf="mayRegister(event)" class="btn btn-primary pull-left"> <fa-icon [icon]="['fas', 'user-plus']"></fa-icon><span class="pl-2">Jelentkezem</span></a>
<a *ngIf="mayCancel(event)" class="btn btn-primary pull-left"> <fa-icon [icon]="['fas', 'user-minus']"></fa-icon><span class="pl-2">Leiratkozom</span></a>
<span *ngIf="noFreeSeat(event)" > <span class="pl-2">Nincs szabad hely</span></span>
</div>
</div>
</div>
</ng-container>
</div>
</ng-container>
</ng-container>
</div>

View File

@ -0,0 +1,22 @@
.event-item{padding: 2px;
.btn-primary {
color: gray;
&:hover{
background-color: black;
color: #e5ce48;
border: 1px solid #e5ce48;
}
}
}
.event-item:nth-child(even) {background: lightgrey;}
.event-item:nth-child(odd) {
background: gray;
color: #ffffff;
}
.event-item:hover { background: orange; cursor: pointer; }

View File

@ -1,4 +1,9 @@
import { Component, OnInit } from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {Event, EventService, EventType} from "../../services/event.service";
import {FormBuilder, FormGroup, Validators} from "@angular/forms";
import {of, pipe} from "rxjs";
import {flatMap} from "rxjs/operators";
import {findAll} from "@angular/compiler-cli/ngcc/src/utils";
@Component({ @Component({
selector: 'app-events', selector: 'app-events',
@ -7,9 +12,148 @@ import { Component, OnInit } from '@angular/core';
}) })
export class EventsComponent implements OnInit { export class EventsComponent implements OnInit {
constructor() { } types: EventType[];
eventTypeForm: FormGroup;
originalEvents: Event[] = [];
events: Event[] = [];
eventDays: EventDate[] = [];
selectedEventType: number = null;
constructor(private eventService: EventService,
private formBuilder: FormBuilder,) {
this.eventTypeForm = this.formBuilder.group({
eventType: ['', [Validators.required]],
});
this.eventTypeForm.get('eventType').valueChanges.subscribe((params) => {
console.info('event type value change', params);
this.selectedEventType = params;
this.filterEvents();
});
}
filterEvents() {
console.info(this.events);
this.eventTypeForm.get('eventType');
this.events = this.originalEvents;
if (this.selectedEventType && this.selectedEventType > 0) {
this.events = this.originalEvents.filter(
value => value.eventType.id == this.selectedEventType
);
}
this.prepareEventDates(this.events);
}
ngOnInit() { ngOnInit() {
of(null).pipe(
flatMap(() => this.eventService.findAllEventTypes()),
flatMap(value => {
this.types = value;
return of(null)
}),
flatMap(() => this.eventService.findEvents()),
flatMap(value => {
this.originalEvents = value;
return of(null)
}),
flatMap(() => {
this.filterEvents();
return of(null)
})
).subscribe();
}
mayRegister(event: Event) {
return event.reservedAt == null && event.reservationCount < event.seatCount;
}
mayCancel(event: Event) {
return event.reservedAt;
}
noFreeSeat(event: Event) {
return event.reservedAt == null && event.reservationCount >= event.seatCount;
}
prepareEventDates(events: Event[]) {
this.eventDays = [];
let date = new Date();
date.setSeconds(0);
date.setMilliseconds(0);
// we will display events for the next 28 days
for (let dayIndex = 0; dayIndex < 14; dayIndex++) {
// the day with the current time
let day = new Date(date.getTime() + (dayIndex * 1000 * 60 * 60 * 24));
let dayAt = day;
// the day with start of day
let dayStartAt = new Date(dayAt.getTime());
dayStartAt.setHours(0, 0, 0, 0);
let eventDate = {
date: day.getTime(),
hours: []
};
let hourIndex = 0;
let iterate = true;
// on the current date we will display events from this timestamp.
// We need this, because we display on the first day from the current timestampe
// but on the other days, from the start of the day
let startDisplayAt = dayIndex == 0 ? dayAt.getTime() : dayStartAt.getTime();
// for each day add all hours in the present and future
while (iterate) {
let hour = new Date(startDisplayAt + (hourIndex * (1000 * 60 * 60)));
hour.setHours(hour.getHours(), 0, 0, 0);
// we have increased the hour of the day
// if there was no dayswitch because of the hour increase, we can add this hour slot to the current hour
iterate = hour.getMonth() == dayAt.getMonth() && hour.getDate() == dayAt.getDate();
hourIndex = hourIndex + 1;
// add the hour
if (iterate) {
let eventHour = {
hour: hour.getTime(),
events: []
};
// iterate over all events, and put each event to it's hour slot
for (let j = 0; j < events.length; j++) {
let currentEvent = events[j];
let hourStart = hour;
let hourEnd = new Date(hour.getTime() + (1000 * 60 * 60));
// if event is in this day
if (currentEvent.start >= hourStart.getTime() && currentEvent.start < hourEnd.getTime()) {
eventHour.events.push(currentEvent);
}
}
if (eventHour.events.length) {
eventDate.hours.push(eventHour);
}
}
}
if (eventDate.hours.length) {
this.eventDays.push(eventDate);
}
}
} }
} }
export interface EventDate {
date: number;
hours: EventHour[];
}
export interface EventHour {
hour: number;
events: Event[];
}

View File

@ -9,7 +9,12 @@ import {BehaviorSubject} from "rxjs";
}) })
export class AuthenticationService { export class AuthenticationService {
private _user: BehaviorSubject<any> = new BehaviorSubject(null); private _user: BehaviorSubject<any> = new BehaviorSubject(null);
constructor(private http: HttpClient){} constructor(private http: HttpClient){
let user = localStorage.getItem('currentUser' );
if ( user ){
this.user.next( JSON.stringify(user));
}
}
// login // login
login(username: string, password:string){ login(username: string, password:string){
@ -34,6 +39,10 @@ export class AuthenticationService {
return this._user; return this._user;
} }
public isLoggedIn(){
return this.user.value;
}
// logout // logout
logout(){ logout(){
// remove user from local storage // remove user from local storage

View File

@ -1,3 +1,6 @@
import {Observable} from "rxjs";
import {EventType} from "./event.service";
export class Endpoints { export class Endpoints {
private static contextPath = "/api"; private static contextPath = "/api";
private static baseUrl: string = Endpoints.contextPath + "/rest"; private static baseUrl: string = Endpoints.contextPath + "/rest";
@ -6,4 +9,12 @@ export class Endpoints {
return `${this.baseUrl}/users/authenticate`; return `${this.baseUrl}/users/authenticate`;
} }
public static GET_EVENTS(eventType: number){
return `${this.baseUrl}/events&id_event_type=${eventType}`;
}
public static GET_EVENT_TYPES(){
return `${this.baseUrl}/event-type`;
}
} }

View File

@ -0,0 +1,4 @@
export interface Reservation {
idEvent: number;
}