Skip to content

Commit

Permalink
feat(nickname.component): nickname component introduced and feature t…
Browse files Browse the repository at this point in the history
…oggle for voting event flow

It is possible to enable the voting event flow setting the property "enableVotingEventFlow" to true
(boolean) in the default configuration  defined in the mongo db
  • Loading branch information
EnricoPicci committed Jul 3, 2019
1 parent d54f942 commit f939ede
Show file tree
Hide file tree
Showing 12 changed files with 312 additions and 31 deletions.
5 changes: 5 additions & 0 deletions src/app/app-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { LoginComponent } from './modules/login/login.component';
import { ErrorComponent } from './components/error/error.component';
import { VotingEventSelectComponent } from './components/voting-event-select/voting-event-select.component';
import { LoginVotingEventComponent } from './modules/login/login-voting-event/login-voting-event.component';
import { NicknameComponent } from './modules/login/nickname/nickname.component';

export const appRoutes: Routes = [
{
Expand All @@ -26,6 +27,10 @@ export const appRoutes: Routes = [
path: 'login-voting-event',
component: LoginVotingEventComponent
},
{
path: 'nickname',
component: NicknameComponent
},
{
path: 'vote',
loadChildren: './modules/vote/vote.module#VoteModule'
Expand Down
9 changes: 9 additions & 0 deletions src/app/app-session.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { VotingEvent } from 'src/app/models/voting-event';
import { Technology } from './models/technology';
import { Credentials } from './models/credentials';

@Injectable({
providedIn: 'root'
Expand All @@ -9,6 +10,7 @@ export class AppSessionService {
private votingEvents: VotingEvent[];
private selectedVotingEvent: VotingEvent;
private selectedTechnology: Technology;
private credentials: Credentials;

constructor() {}

Expand All @@ -32,4 +34,11 @@ export class AppSessionService {
setSelectedTechnology(technology: Technology) {
this.selectedTechnology = technology;
}

getCredentials() {
return this.credentials;
}
setCredentials(credentials: Credentials) {
this.credentials = credentials;
}
}
51 changes: 32 additions & 19 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { BackendService } from './services/backend.service';
import { ErrorService } from './services/error.service';
import { AppSessionService } from './app-session.service';
import { getIdentificationRoute } from './utils/voting-event-flow.util';
import { map } from 'rxjs/operators';
import { map, tap } from 'rxjs/operators';
import { ConfigurationService } from './services/configuration.service';

@Component({
selector: 'byor-root',
Expand All @@ -19,27 +20,39 @@ export class AppComponent implements OnInit {
private router: Router,
private backend: BackendService,
public errorService: ErrorService,
private appSession: AppSessionService
private appSession: AppSessionService,
private configurationService: ConfigurationService
) {}

ngOnInit() {
this.backend
.getVotingEvents()
.pipe(map((votingEvents) => votingEvents.filter((ve) => ve.status === 'open')))
.subscribe((votingEvents) => {
if (!votingEvents || votingEvents.length === 0) {
this.errorService.setError(new Error('There are no Voting Events open'));
this.router.navigate(['error']);
} else if (votingEvents.length === 1) {
const votingEvent = votingEvents[0];
this.appSession.setSelectedVotingEvent(votingEvent);
const route = getIdentificationRoute(votingEvent);
this.router.navigate([route]);
} else {
this.appSession.setVotingEvents(votingEvents);
this.router.navigate(['selectVotingEvent']);
}
});
this.configurationService
.defaultConfiguration()
.pipe(
tap((config) => {
if (config.enableVotingEventFlow) {
this.backend
.getVotingEvents()
.pipe(map((votingEvents) => votingEvents.filter((ve) => ve.status === 'open')))
.subscribe((votingEvents) => {
if (!votingEvents || votingEvents.length === 0) {
this.errorService.setError(new Error('There are no Voting Events open'));
this.router.navigate(['error']);
} else if (votingEvents.length === 1) {
const votingEvent = votingEvents[0];
this.appSession.setSelectedVotingEvent(votingEvent);
const route = getIdentificationRoute(votingEvent);
this.router.navigate([route]);
} else {
this.appSession.setVotingEvents(votingEvents);
this.router.navigate(['selectVotingEvent']);
}
});
} else {
this.router.navigate(['vote']);
}
})
)
.subscribe();
}

goToAdminPage() {
Expand Down
4 changes: 4 additions & 0 deletions src/app/models/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Credentials {
userId?: string;
nickname?: string;
}
3 changes: 2 additions & 1 deletion src/app/modules/login/login.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { AuthGuard } from './auth.guard';
import { AuthService } from './auth.service';
import { AppMaterialModule } from '../../app-material.module';
import { LoginVotingEventComponent } from './login-voting-event/login-voting-event.component';
import { NicknameComponent } from './nickname/nickname.component';

@NgModule({
declarations: [LoginComponent, LoginVotingEventComponent],
declarations: [LoginComponent, LoginVotingEventComponent, NicknameComponent],
providers: [AuthGuard, AuthService],
imports: [CommonModule, AppMaterialModule],
exports: [LoginComponent]
Expand Down
26 changes: 26 additions & 0 deletions src/app/modules/login/nickname/nickname.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<div class="nickname-section">
<div>
<span class="event-label">Voting for event: <strong>{{appSession.getSelectedVotingEvent().name}}</strong></span>
</div>
</div>

<div class="nickname-section">
<div class="nickname">
<label>Nickname</label>
<input #nickname type="text" class="nickname-input" id="nickname">
</div>

<div class="form-actions">
<button #startButton mat-flat-button color="accent" [class.button-disabled]="!(isValidInputData$ | async)"
[class.button-enabled]="(isValidInputData$ | async)" [disabled]="!(isValidInputData$ | async)">
<span class="button-text"> Start Session </span>
</button>
</div>

<div *ngIf="message$ | async as message" class="message" [innerHTML]="message"></div>

</div>
<div class="banner-section">
<a [href]="(configuration$ | async)?.bannerTargetUrl" target="_blank"><img
[src]="(configuration$ | async)?.bannerImageUrl"></a>
</div>
53 changes: 53 additions & 0 deletions src/app/modules/login/nickname/nickname.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
@import "../../../styles/abstracts/mixins";

$login-sections-max-width: 480px;

.nickname-section {
margin: 1.3rem auto;
padding: 0 1.3rem;
max-width: $login-sections-max-width;

.event-selection {
display: block;
}

mat-form-field {
width: 100%;
}

.nickname {
margin-bottom: 2rem;

.nickname-input {
width: 100%;
@include byor-input;
}

label {
display: block;
margin-bottom: .3rem;
}
}

.button-disabled {
border: none;
}

.button-text {
color: #ffffff;
}
}

.banner-section {
margin: 0 auto;
padding: 6rem 1.3rem;
max-width: $login-sections-max-width;
text-align: center;

a {
border-bottom: none;
img {
max-width: 100%;
}
}
}
44 changes: 44 additions & 0 deletions src/app/modules/login/nickname/nickname.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { NicknameComponent } from './nickname.component';
import { RouterTestingModule } from '@angular/router/testing';
import { AppMaterialModule } from 'src/app/app-material.module';
import { HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { VotingEvent } from 'src/app/models/voting-event';
import { AppSessionService } from 'src/app/app-session.service';

class MockAppSessionService {
private votingEvent: VotingEvent;

constructor() {
this.votingEvent = { _id: '123', name: 'an event', status: 'open', creationTS: 'abc' };
}

getSelectedVotingEvent() {
return this.votingEvent;
}
}

describe('NicknameComponent', () => {
let component: NicknameComponent;
let fixture: ComponentFixture<NicknameComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [NicknameComponent],
imports: [RouterTestingModule, AppMaterialModule, HttpClientModule, BrowserAnimationsModule],
providers: [{ provide: AppSessionService, useClass: MockAppSessionService }]
}).compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(NicknameComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
108 changes: 108 additions & 0 deletions src/app/modules/login/nickname/nickname.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Component, OnInit, AfterViewInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { Router } from '@angular/router';

import { Observable, Subject, fromEvent, Subscription, NEVER } from 'rxjs';
import { shareReplay, map, share, switchMap, tap, filter } from 'rxjs/operators';

import { AppSessionService } from 'src/app/app-session.service';
import { ConfigurationService } from 'src/app/services/configuration.service';
import { BackendService } from 'src/app/services/backend.service';
import { VoteCredentials } from 'src/app/models/vote-credentials';
import { ErrorService } from 'src/app/services/error.service';
import { logError } from 'src/app/utils/utils';

@Component({
selector: 'byor-nickname',
templateUrl: './nickname.component.html',
styleUrls: ['./nickname.component.scss']
})
export class NicknameComponent implements AfterViewInit, OnDestroy, OnInit {
constructor(
private router: Router,
private errorService: ErrorService,
public appSession: AppSessionService,
private configurationService: ConfigurationService,
private backend: BackendService
) {}

isValidInputData$: Observable<boolean>;
message$ = new Subject<string>();
configuration$: Observable<any>;

goToVoteSubscription: Subscription;

@ViewChild('nickname') voterFirstName: ElementRef;
@ViewChild('startButton', { read: ElementRef }) startButtonRef: ElementRef;

ngOnInit() {
this.configuration$ = this.configurationService.defaultConfiguration().pipe(shareReplay(1));
}

ngAfterViewInit() {
// notify when nickname is changed
const nickname$ = fromEvent(this.voterFirstName.nativeElement, 'keyup').pipe(map(() => this.voterFirstName.nativeElement.value));

const startButtonClick$ = fromEvent(this.startButtonRef.nativeElement, 'click');

// the main subscription
this.goToVoteSubscription = this.goToVote$(nickname$, startButtonClick$).subscribe(
(nickname) => {
this.appSession.setCredentials({ nickname });
this.router.navigate(['vote/start']);
},
(error) => {
logError(error);
let _errMsg = error;
if (error.message) {
_errMsg = error.message;
}
this.errorService.setError(_errMsg);
this.errorService.setErrorMessage(error);
this.router.navigate(['error']);
}
);
}

ngOnDestroy() {
if (this.goToVoteSubscription) {
this.goToVoteSubscription.unsubscribe();
}
}

// this method takes in input all Observable created out of DOM events which are relevant for this Component
// in this way we can easily test this logic with marble tests
goToVote$(nickname$: Observable<string>, startButtonClick$: Observable<any>) {
// notifies when the input data provided changes - the value notified is true of false
// depending on the fact that the input data is valid or not
this.isValidInputData$ = nickname$.pipe(
map((nickname) => this.isNicknameValid(nickname)),
share() // share() is used since this Observable is used also on the Html template
);

const clickOnVote$ = nickname$.pipe(
switchMap((nickname) => (this.isNicknameValid(nickname) ? startButtonClick$.pipe(map(() => nickname)) : NEVER))
);

// notifies when the user has clicked to go to voting session and he has not voted yet
return clickOnVote$.pipe(
switchMap((nickname) => {
const votingEvent = this.appSession.getSelectedVotingEvent();
// to remove when we refactor hasAlreadyVoted
const oldCredentials: VoteCredentials = { voterId: { firstName: nickname, lastName: '' }, votingEvent };
return this.backend.hasAlreadyVoted(oldCredentials).pipe(
tap((hasAlreadyVoted) => {
if (hasAlreadyVoted) {
this.message$.next(`You have already voted for <strong>${votingEvent.name}</strong>`);
}
}),
filter((hasAlreadyVoted) => !hasAlreadyVoted),
map(() => nickname)
);
})
);
}

isNicknameValid(nickname: string) {
return nickname && nickname.trim().length > 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { VoteService } from '../services/vote.service';
import { ErrorService } from 'src/app/services/error.service';
import { ConfigurationService } from 'src/app/services/configuration.service';
import { logError } from 'src/app/utils/utils';
import { AppSessionService } from 'src/app/app-session.service';

@Component({
selector: 'byor-start-voting-session',
Expand All @@ -34,7 +35,8 @@ export class StartVotingSessionComponent implements AfterViewInit, OnDestroy, On
private router: Router,
private voteService: VoteService,
private errorService: ErrorService,
private configurationService: ConfigurationService
private configurationService: ConfigurationService,
private appSession: AppSessionService
) {}
openVotingEvents$: Observable<Array<VotingEvent>>;

Expand Down Expand Up @@ -83,6 +85,7 @@ export class StartVotingSessionComponent implements AfterViewInit, OnDestroy, On
.subscribe(
(credentials) => {
this.voteService.credentials = credentials;
this.appSession.setSelectedVotingEvent(credentials.votingEvent);
this.router.navigate(['vote/start']);
},
(error) => {
Expand Down
Loading

0 comments on commit f939ede

Please sign in to comment.