Skip to content

Commit 61cfa47

Browse files
committed
support multiple decks
1 parent e7bc145 commit 61cfa47

13 files changed

+112
-78
lines changed

README.md

+3-5
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,9 @@
33
Open source implementation of planning poker game.
44

55
- The game has three roles: admin, players, observers.
6-
- Cards: 0, 0.5, 1, 2, 3, 5, 10, 20, ? (configurable via `config.json`)
6+
- Multiple card decks configurable via `config.json`
77
- Self-contained, can be run in closed intranet environments.
88

9-
The project is built on Angular 5. It uses [http-shared-storage](https://github.com/yarosla/httpstorage)
10-
backend to store session data.
11-
129
## Quick Start
1310

1411
1. Download release archive from [Releases](https://github.com/yarosla/poker/releases) page. Unpack it.
@@ -21,7 +18,8 @@ backend to store session data.
2118

2219
## Technologies
2320

24-
- Angular 5
21+
The project is built on Angular 5. It uses [http-shared-storage](https://github.com/yarosla/httpstorage)
22+
backend to store session data.
2523

2624
## Author
2725

package-lock.json

+37-37
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+13-13
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,34 @@
66
"ng": "ng",
77
"start": "ng serve",
88
"build": "ng build --prod",
9-
"release": "ng build --prod && rm -rf poker-release && mkdir poker-release && mv dist poker-release/poker && cp ../http-shared-storage/build/libs/http-shared-storage-1.0.jar poker-release && 7z a -tzip poker-release.zip poker-release",
9+
"release": "ng build --prod && rm -rf poker-release poker-release.zip && mkdir poker-release && mv dist poker-release/poker && cp ../http-shared-storage/build/libs/http-shared-storage-1.0.jar poker-release && 7z a -tzip poker-release.zip poker-release",
1010
"test": "ng test",
1111
"lint": "ng lint",
1212
"e2e": "ng e2e"
1313
},
1414
"private": true,
1515
"dependencies": {
16-
"@angular/animations": "^5.2.3",
17-
"@angular/common": "^5.2.3",
18-
"@angular/compiler": "^5.2.3",
19-
"@angular/core": "^5.2.3",
20-
"@angular/forms": "^5.2.3",
21-
"@angular/http": "^5.2.3",
22-
"@angular/platform-browser": "^5.2.3",
23-
"@angular/platform-browser-dynamic": "^5.2.3",
24-
"@angular/router": "^5.2.3",
16+
"@angular/animations": "^5.2.5",
17+
"@angular/common": "^5.2.5",
18+
"@angular/compiler": "^5.2.5",
19+
"@angular/core": "^5.2.5",
20+
"@angular/forms": "^5.2.5",
21+
"@angular/http": "^5.2.5",
22+
"@angular/platform-browser": "^5.2.5",
23+
"@angular/platform-browser-dynamic": "^5.2.5",
24+
"@angular/router": "^5.2.5",
2525
"core-js": "^2.4.1",
2626
"rxjs": "^5.5.2",
2727
"ts-mockito": "^2.2.9",
2828
"zone.js": "^0.8.20"
2929
},
3030
"devDependencies": {
3131
"@angular/cli": "1.6.3",
32-
"@angular/compiler-cli": "^5.2.3",
33-
"@angular/language-service": "^5.2.3",
32+
"@angular/compiler-cli": "^5.2.5",
33+
"@angular/language-service": "^5.2.5",
3434
"@types/jasmine": "~2.5.53",
3535
"@types/jasminewd2": "~2.0.2",
36-
"@types/node": "^6.0.96",
36+
"@types/node": "^6.0.101",
3737
"codelyzer": "^4.1.0",
3838
"jasmine-core": "~2.6.2",
3939
"jasmine-spec-reporter": "~4.1.0",

src/app/config.service.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@ import { HttpClient } from '@angular/common/http';
33
import { ReplaySubject } from 'rxjs/ReplaySubject';
44
import { Observable } from 'rxjs/Observable';
55

6+
export class Deck {
7+
name: string;
8+
cards: string[]
9+
}
10+
611
export class Config {
712
httpStoreUrl: string;
813
pollTimeout: number | string;
9-
deck?: string[]
14+
decks?: Deck[]
1015
}
1116

1217
@Injectable()

src/app/game/game.component.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ <h1>{{currentParticipant?.name || (isAdmin?'Admin':'Observer')}} @ {{session.nam
33
<a routerLink="/join/{{sessionId}}" title="Invite more people to join"><i class="ti-share"></i></a></h1>
44
<app-voting-pad *ngIf="votingStory && currentParticipant" [story]="votingStory"
55
[confirmedVote]="votingStory.votes[currentParticipant.id]"
6-
[deck]="deck"
6+
[deck]="session.deck"
77
(vote)="httpStorage.vote($event)"></app-voting-pad>
88
<table>
99
<tr>

src/app/game/game.component.spec.ts

-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { GameComponent } from './game.component';
88
import { HttpStorageService, Session } from '../http-storage.service';
99
import { ActivatedRouteStub, RouterLinkStubDirective, RouterStub } from '../../testing/router-stubs';
1010
import { VotingPadComponent } from '../voting-pad/voting-pad.component';
11-
import { Config, ConfigService } from '../config.service';
1211

1312
describe('GameComponent', () => {
1413
let component: GameComponent;
@@ -20,10 +19,6 @@ describe('GameComponent', () => {
2019
});
2120
when(httpStorageMock.sessionId).thenReturn('0abcd');
2221

23-
const configMock = mock(ConfigService);
24-
const configObservable = Observable.of<Config>({ httpStoreUrl: '', pollTimeout: 0, deck: ['1'] });
25-
when(configMock.getConfig()).thenReturn(configObservable);
26-
2722
beforeEach(async(() => {
2823
const activatedRoute = new ActivatedRouteStub({}, [{ path: 'play' }, { path: '0abcd' }, { path: 'qwe' }]);
2924
TestBed.configureTestingModule({
@@ -33,7 +28,6 @@ describe('GameComponent', () => {
3328
{ provide: HttpStorageService, useValue: instance(httpStorageMock) },
3429
{ provide: Router, useClass: RouterStub },
3530
{ provide: ActivatedRoute, useValue: activatedRoute },
36-
{ provide: ConfigService, useValue: instance(configMock) },
3731
]
3832
})
3933
.compileComponents();

src/app/game/game.component.ts

+1-5
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@ export class GameComponent implements OnInit {
2222
participants: Participant[];
2323
currentParticipant: Participant;
2424
votingStory: Story;
25-
deck: string[] = ['0', '0.5', '1', '2', '3', '5', '10', '20'];
2625

27-
constructor(private httpStorage: HttpStorageService, private route: ActivatedRoute, private config: ConfigService) {
26+
constructor(public httpStorage: HttpStorageService, private route: ActivatedRoute) {
2827
}
2928

3029
ngOnInit() {
@@ -41,9 +40,6 @@ export class GameComponent implements OnInit {
4140
this.currentParticipant = this.participants.find(p => p.id === this.httpStorage.participantId);
4241
this.votingStory = session.stories.find(s => s.votingInProgress);
4342
});
44-
this.config.getConfig().subscribe(conf => {
45-
if (conf.deck) this.deck = conf.deck;
46-
});
4743
}
4844

4945
hideVote(story: Story, participant: Participant): boolean {

src/app/http-storage.service.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,18 @@ export class Session {
6363
name: string;
6464
participants: Participant[] = [];
6565
stories: Story[] = [];
66+
deck: string[] = ['0', '0.5', '1', '2', '3', '5', '10', '20'];
6667

67-
constructor(name?: string) {
68+
constructor(name?: string, deck?: string[]) {
6869
this.name = name;
70+
if (deck) this.deck = deck;
6971
}
7072

7173
clone(): Session {
7274
const session = new Session(this.name);
7375
session.participants = this.participants.map(p => Participant.prototype.clone.call(p));
7476
session.stories = this.stories.map(s => Story.prototype.clone.call(s));
77+
session.deck = this.deck.slice();
7578
return session;
7679
}
7780
}
@@ -119,14 +122,14 @@ export class HttpStorageService {
119122
console.debug('got state', this.state);
120123
};
121124

122-
startSession(name: string): Promise<Session> {
125+
startSession(name: string, deck?: string[]): Promise<Session> {
123126
console.info('startSession', name);
124127
this.state = new State();
125128
return this.config.getConfig()
126129
.mergeMap(config => {
127130
const url = (config.httpStoreUrl || DEFAULT_URL);
128131
console.debug('sending POST', url);
129-
return this.http.post<Session>(url, new Session(name), { observe: 'response' })
132+
return this.http.post<Session>(url, new Session(name, deck), { observe: 'response' })
130133
.do(this.stateExtractor)
131134
.map((r: HttpResponse<Session>) => r.body);
132135
})

src/app/new-session/new-session.component.css

+7-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ form button {
55
cursor: pointer;
66
}
77

8-
form input[type="text"]{
8+
form input[type="text"] {
99
width: 300px;
1010
background-color: white;
1111
border: solid 1px #aaa;
1212
padding: 4px 6px;
1313
}
14+
15+
form select {
16+
background-color: white;
17+
border: solid 1px #aaa;
18+
padding: 3px 6px;
19+
}
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
<form #theForm="ngForm"><input type="text" name="name" required [(ngModel)]="newSessionName" placeholder="Session name...">
2-
<button type="submit" [disabled]="theForm.invalid || !newSessionName?.trim()" (click)="startSession(newSessionName.trim())">Start Session</button>
2+
<select name="deck" [(ngModel)]="selectedDeck" [compareWith]="compareDecks">
3+
<option *ngFor="let d of decks" [ngValue]="d">{{d.name}}</option>
4+
</select>
5+
<button type="submit" [disabled]="theForm.invalid || !newSessionName?.trim()" (click)="startSession()">Start Session</button>
36
</form>

src/app/new-session/new-session.component.spec.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
22
import { ActivatedRoute, Router } from '@angular/router';
33
import { FormsModule } from '@angular/forms';
4-
import { instance, mock } from 'ts-mockito';
4+
import { instance, mock, when } from 'ts-mockito';
55

66
import { NewSessionComponent } from './new-session.component';
77
import { HttpStorageService } from '../http-storage.service';
88
import { ActivatedRouteStub, RouterStub } from '../../testing/router-stubs';
9+
import { Config, ConfigService } from '../config.service';
10+
import { Observable } from 'rxjs/Observable';
911

1012
describe('NewSessionComponent', () => {
1113
let component: NewSessionComponent;
1214
let fixture: ComponentFixture<NewSessionComponent>;
1315
const httpStorageMock = mock(HttpStorageService);
1416

17+
const configMock = mock(ConfigService);
18+
const configObservable = Observable.of<Config>({
19+
httpStoreUrl: '',
20+
pollTimeout: 0,
21+
decks: [{ name: '~', cards: ['1'] }]
22+
});
23+
when(configMock.getConfig()).thenReturn(configObservable);
24+
1525
beforeEach(async(() => {
1626
const activatedRoute = new ActivatedRouteStub();
1727
TestBed.configureTestingModule({
@@ -21,6 +31,7 @@ describe('NewSessionComponent', () => {
2131
{ provide: HttpStorageService, useValue: instance(httpStorageMock) },
2232
{ provide: Router, useClass: RouterStub },
2333
{ provide: ActivatedRoute, useValue: activatedRoute },
34+
{ provide: ConfigService, useValue: instance(configMock) },
2435
]
2536
})
2637
.compileComponents();

0 commit comments

Comments
 (0)