
125 changed files with 3086 additions and 1721 deletions
@ -1,19 +1,20 @@
|
||||
module.exports = { |
||||
parser: '@typescript-eslint/parser', |
||||
parserOptions: { |
||||
ecmaVersion: 2018, |
||||
sourceType: 'module', |
||||
project: './tsconfig.json', |
||||
tsconfigRootDir: __dirname, |
||||
}, |
||||
plugins: ['@typescript-eslint'], |
||||
extends: [ |
||||
'eslint:recommended', |
||||
'plugin:@typescript-eslint/eslint-recommended', |
||||
'plugin:@typescript-eslint/recommended', |
||||
'plugin:@typescript-eslint/recommended-requiring-type-checking', |
||||
'prettier/@typescript-eslint', |
||||
'plugin:prettier/recommended', |
||||
], |
||||
rules: {}, |
||||
parser: '@typescript-eslint/parser', |
||||
plugins: ['@typescript-eslint'], |
||||
extends: [ |
||||
'eslint:recommended', |
||||
'plugin:@typescript-eslint/eslint-recommended', |
||||
'plugin:@typescript-eslint/recommended', |
||||
'plugin:prettier/recommended', |
||||
'prettier/@typescript-eslint', |
||||
], |
||||
parserOptions: { |
||||
ecmaVersion: 2018, |
||||
sourceType: 'module', |
||||
project: './tsconfig.json', |
||||
tsconfigRootDir: __dirname, |
||||
}, |
||||
rules: { |
||||
'@typescript-eslint/unbound-method': ['error', { ignoreStatic: true }], |
||||
}, |
||||
}; |
||||
|
@ -0,0 +1,234 @@
|
||||
{ |
||||
"owners": [ |
||||
{ "id": 1, "email": "toto@gafam.com", "pseudo": "TOTO" }, |
||||
{ "id": 2, "email": "titi@gafam.com", "pseudo": "TITI" } |
||||
], |
||||
"voters": [{ "pseudo": "TOTO", "polls": [] }], |
||||
"polls": [ |
||||
{ |
||||
"id": 1, |
||||
"slug": "picnic", |
||||
"configuration": { |
||||
"isAboutDate": true, |
||||
"isProtectedByPassword": false, |
||||
"isOwnerNotifiedByEmailOnNewVote": false, |
||||
"isOwnerNotifiedByEmailOnNewComment": false, |
||||
"isMaybeAnswerAvailable": true, |
||||
"areResultsPublic": true, |
||||
"dateCreated": "2020-05-17", |
||||
"expires": "2020-06-17" |
||||
}, |
||||
"ownerId": 1, |
||||
"question": "Quelle date pour le picnic ?", |
||||
"description": "Gros badass picnic en plein air ! Come on !", |
||||
"answersByChoiceByParticipant": { |
||||
"TOTO": { "samedi": "YES", "dimanche": "NO" }, |
||||
"TATA": { "samedi": "NO", "dimanche": null }, |
||||
"TITI": { "samedi": "MAYBE", "dimanche": "NO" } |
||||
}, |
||||
"answers": [ |
||||
{ |
||||
"pseudo": "TOTO", |
||||
"token": "TOTO-TOKEN", |
||||
"responsesByChoices": [ |
||||
{ "choice": { "label": "samedi", "imagePath": null }, "responseType": "YES" }, |
||||
{ "choice": { "label": "dimanche", "imagePath": null }, "responseType": "NO" } |
||||
] |
||||
}, |
||||
{ |
||||
"pseudo": "TATA", |
||||
"token": "TATA-TOKEN", |
||||
"responsesByChoices": [ |
||||
{ "choice": { "label": "samedi", "imagePath": null }, "responseType": "NO" }, |
||||
{ "choice": { "label": "dimanche", "imagePath": null }, "responseType": "NO" } |
||||
] |
||||
}, |
||||
{ |
||||
"pseudo": "TITI", |
||||
"token": "TITI-TOKEN", |
||||
"responsesByChoices": [ |
||||
{ "choice": { "label": "samedi", "imagePath": null }, "responseType": "NO" }, |
||||
{ "choice": { "label": "dimanche", "imagePath": null }, "responseType": "YES" } |
||||
] |
||||
}, |
||||
{ |
||||
"pseudo": "TETE", |
||||
"token": "TETE-TOKEN", |
||||
"responsesByChoices": [ |
||||
{ "choice": { "label": "samedi", "imagePath": null }, "responseType": "YES" }, |
||||
{ "choice": { "label": "dimanche", "imagePath": null }, "responseType": "NO" } |
||||
] |
||||
} |
||||
] |
||||
}, |
||||
{ |
||||
"id": 2, |
||||
"slug": "vacances", |
||||
"configuration": { |
||||
"isAboutDate": true, |
||||
"isProtectedByPassword": false, |
||||
"isOwnerNotifiedByEmailOnNewVote": false, |
||||
"isOwnerNotifiedByEmailOnNewComment": false, |
||||
"isMaybeAnswerAvailable": true, |
||||
"areResultsPublic": true, |
||||
"dateCreated": "2020-05-17", |
||||
"expires": "2020-06-17" |
||||
}, |
||||
"ownerId": 2, |
||||
"question": "On fait quoi pendant les vacances ?", |
||||
"description": "Vacances en famille", |
||||
"answersByChoiceByParticipant": { |
||||
"TOTO": { "bateau": "YES", "montagne": "NO", "quad": "MAYBE" }, |
||||
"TATA": { "bateau": "NO", "montagne": null, "quad": "YES" }, |
||||
"TITI": { "bateau": "MAYBE", "montagne": "NO", "quad": null } |
||||
}, |
||||
"answers": [ |
||||
{ |
||||
"pseudo": "TOTO", |
||||
"token": "TOTO-TOKEN", |
||||
"responsesByChoices": [ |
||||
{ "choice": { "label": "bateau", "imagePath": null }, "responseType": "YES" }, |
||||
{ "choice": { "label": "montagne", "imagePath": null }, "responseType": "NO" }, |
||||
{ "choice": { "label": "quad", "imagePath": null }, "responseType": "NO" } |
||||
] |
||||
}, |
||||
{ |
||||
"pseudo": "TATA", |
||||
"token": "TATA-TOKEN", |
||||
"responsesByChoices": [ |
||||
{ "choice": { "label": "bateau", "imagePath": null }, "responseType": "NO" }, |
||||
{ "choice": { "label": "montagne", "imagePath": null }, "responseType": "NO" }, |
||||
{ "choice": { "label": "quad", "imagePath": null }, "responseType": "NO" } |
||||
] |
||||
}, |
||||
{ |
||||
"pseudo": "TITI", |
||||
"token": "TITI-TOKEN", |
||||
"responsesByChoices": [ |
||||
{ "choice": { "label": "bateau", "imagePath": null }, "responseType": "NO" }, |
||||
{ "choice": { "label": "montagne", "imagePath": null }, "responseType": "YES" }, |
||||
{ "choice": { "label": "quad", "imagePath": null }, "responseType": "NO" } |
||||
] |
||||
}, |
||||
{ |
||||
"pseudo": "TETE", |
||||
"token": "TETE-TOKEN", |
||||
"responsesByChoices": [ |
||||
{ "choice": { "label": "bateau", "imagePath": null }, "responseType": "YES" }, |
||||
{ "choice": { "label": "montagne", "imagePath": null }, "responseType": "NO" }, |
||||
{ "choice": { "label": "quad", "imagePath": null }, "responseType": "NO" } |
||||
] |
||||
} |
||||
] |
||||
} |
||||
], |
||||
"choices": [ |
||||
{ |
||||
"id": 1, |
||||
"pollId": 1, |
||||
"pollSlug": "picnic", |
||||
"label": "samedi", |
||||
"participants": [ |
||||
["YES", ["TOTO", "TITI"]], |
||||
["NO", ["TETE"]], |
||||
["MAYBE", ["TATA"]] |
||||
] |
||||
}, |
||||
{ |
||||
"id": 2, |
||||
"pollId": 1, |
||||
"pollSlug": "picnic", |
||||
"label": "dimanche", |
||||
"participants": [ |
||||
["YES", ["TOTO", "TITI"]], |
||||
["NO", ["TETE"]], |
||||
["MAYBE", ["TATA"]] |
||||
] |
||||
}, |
||||
{ |
||||
"id": 3, |
||||
"pollId": 2, |
||||
"pollSlug": "vacances", |
||||
"label": "bateau", |
||||
"participants": [ |
||||
["YES", ["TOTO", "TITI"]], |
||||
["NO", ["TETE"]], |
||||
["MAYBE", ["TATA"]] |
||||
] |
||||
}, |
||||
{ |
||||
"id": 4, |
||||
"pollId": 2, |
||||
"pollSlug": "vacances", |
||||
"label": "montagne", |
||||
"participants": [ |
||||
["YES", ["TOTO", "TITI"]], |
||||
["NO", ["TETE"]], |
||||
["MAYBE", ["TATA"]] |
||||
] |
||||
}, |
||||
{ |
||||
"id": 5, |
||||
"pollId": 2, |
||||
"pollSlug": "vacances", |
||||
"label": "quad", |
||||
"participants": [ |
||||
["YES", ["TOTO", "TITI"]], |
||||
["NO", ["TETE"]], |
||||
["MAYBE", ["TATA"]] |
||||
] |
||||
} |
||||
], |
||||
"answers": [ |
||||
{ "choiceId": 1, "pseudo": "TOTO", "response": "YES", "token": "TOTO-TOKEN" }, |
||||
{ "choiceId": 1, "pseudo": "TATA", "response": "NO", "token": "TATA-TOKEN" }, |
||||
{ "choiceId": 1, "pseudo": "TITI", "response": "MAYBE", "token": "TITI-TOKEN" }, |
||||
{ "choiceId": 2, "pseudo": "TOTO", "response": "YES", "token": "TOTO-TOKEN" }, |
||||
{ "choiceId": 2, "pseudo": "TATA", "response": "NO", "token": "TATA-TOKEN" }, |
||||
{ "choiceId": 2, "pseudo": "TITI", "response": "MAYBE", "token": "TITI-TOKEN" }, |
||||
{ "choiceId": 2, "pseudo": "EVA", "response": null, "token": "EVA-TOKEN" }, |
||||
{ "choiceId": 3, "pseudo": "TOTO", "response": "YES", "token": "TOTO-TOKEN" }, |
||||
{ "choiceId": 3, "pseudo": "TATA", "response": "NO", "token": "TATA-TOKEN" }, |
||||
{ "choiceId": 3, "pseudo": "TITI", "response": "MAYBE", "token": "TITI-TOKEN" }, |
||||
{ "choiceId": 4, "pseudo": "TOTO", "response": "YES", "token": "TOTO-TOKEN" }, |
||||
{ "choiceId": 4, "pseudo": "TATA", "response": "NO", "token": "TATA-TOKEN" }, |
||||
{ "choiceId": 4, "pseudo": "TITI", "response": "MAYBE", "token": "TITI-TOKEN" }, |
||||
{ "choiceId": 5, "pseudo": "TOTO", "response": "YES", "token": "TOTO-TOKEN" }, |
||||
{ "choiceId": 5, "pseudo": "TATA", "response": "NO", "token": "TATA-TOKEN" }, |
||||
{ "choiceId": 5, "pseudo": "TITI", "response": "MAYBE", "token": "TITI-TOKEN" } |
||||
], |
||||
"comments": [ |
||||
{ |
||||
"id": 1, |
||||
"pollId": 1, |
||||
"pollSlug": "picnic", |
||||
"content": "Les picnics, c’est trop bien, j’adore!", |
||||
"author": "TATA", |
||||
"dateCreated": 1589111111111 |
||||
}, |
||||
{ |
||||
"id": 2, |
||||
"pollId": 1, |
||||
"pollSlug": "picnic", |
||||
"content": "Oué, grave!", |
||||
"author": "TETE", |
||||
"dateCreated": 1589222222222 |
||||
}, |
||||
{ |
||||
"id": 3, |
||||
"pollId": 2, |
||||
"pollSlug": "vacances", |
||||
"content": "Désolé je pourrai pas être là, mais je penserai bien à vous. Mamie", |
||||
"author": "MAMIE", |
||||
"dateCreated": 1589333333333 |
||||
}, |
||||
{ |
||||
"id": 4, |
||||
"pollId": 2, |
||||
"pollSlug": "vacances", |
||||
"content": "Arf, trop dommage.", |
||||
"author": "Tom", |
||||
"dateCreated": 1589444444444 |
||||
} |
||||
] |
||||
} |
@ -0,0 +1,8 @@
|
||||
{ |
||||
"/api/v1/*": "/$1", |
||||
"/owners/:email/": "/owners?email=:email", |
||||
"/choices": "/choices?_embed=answers", |
||||
"/polls/:slug": "/polls?slug=:slug&_expand=owner&_embed=choices&_embed=comments", |
||||
"/polls/:slug/choices": "/choices?pollSlug=:slug&_embed=answers", |
||||
"/polls/:slug/comments": "/comments?pollSlug=:slug" |
||||
} |
@ -0,0 +1,11 @@
|
||||
{ |
||||
"/api/v1/*": { |
||||
"target": "http://localhost:8000", |
||||
"secure": false, |
||||
"pathRewrite": { |
||||
"^/api/v1": "" |
||||
}, |
||||
"changeOrigin": false, |
||||
"logLevel": "debug" |
||||
} |
||||
} |
@ -1,18 +1,12 @@
|
||||
import { Component, OnInit } from '@angular/core'; |
||||
|
||||
import { MockingService } from '../../../services/mocking.service'; |
||||
import { Poll } from '../../../models/poll.model'; |
||||
|
||||
@Component({ |
||||
selector: 'app-navigation', |
||||
templateUrl: './navigation.component.html', |
||||
styleUrls: ['./navigation.component.scss'], |
||||
}) |
||||
export class NavigationComponent implements OnInit { |
||||
public pollsDatabase: Poll[] = []; |
||||
constructor(private mockingService: MockingService) {} |
||||
constructor() {} |
||||
|
||||
ngOnInit(): void { |
||||
this.pollsDatabase = this.mockingService.pollsDatabase; |
||||
} |
||||
ngOnInit(): void {} |
||||
} |
||||
|
@ -1,4 +1,4 @@
|
||||
export enum Response { |
||||
export enum ResponseType { |
||||
YES = 'YES', |
||||
NO = 'NO', |
||||
MAYBE = 'MAYBE', |
@ -1,6 +0,0 @@
|
||||
import { Response } from '../enums/response.enum'; |
||||
import { Choice } from './choice.model'; |
||||
|
||||
export class Answer { |
||||
constructor(public author: string, public choice: Choice, public response: Response) {} |
||||
} |
@ -1,5 +1,38 @@
|
||||
import { Response } from '../enums/response.enum'; |
||||
import { ResponseType } from '../enums/response-type.enum'; |
||||
|
||||
export class Choice { |
||||
constructor(public label: string, public responses: Response[] = [Response.YES, Response.NO]) {} |
||||
constructor( |
||||
public label: string, |
||||
public imageUrl?: string, |
||||
public participants: Map<ResponseType, Set<string>> = new Map<ResponseType, Set<string>>([ |
||||
[ResponseType.YES, new Set<string>()], |
||||
[ResponseType.NO, new Set<string>()], |
||||
[ResponseType.MAYBE, new Set<string>()], |
||||
]), |
||||
public counts: Map<ResponseType, number> = new Map<ResponseType, number>([ |
||||
[ResponseType.YES, 0], |
||||
[ResponseType.NO, 0], |
||||
[ResponseType.MAYBE, 0], |
||||
]) |
||||
) {} |
||||
|
||||
public updateParticipation(pseudo: string, responseType: ResponseType): void { |
||||
this.removeParticipant(pseudo); |
||||
this.participants.get(responseType).add(pseudo); |
||||
this.updateCounts(); |
||||
} |
||||
|
||||
public removeParticipant(pseudo: string): void { |
||||
for (const responseType of Object.values(ResponseType)) { |
||||
if (this.participants.get(responseType).has(pseudo)) { |
||||
this.participants.get(responseType).delete(pseudo); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public updateCounts(): void { |
||||
for (const responseType of Object.values(ResponseType)) { |
||||
this.counts.set(responseType, this.participants.get(responseType).size); |
||||
} |
||||
} |
||||
} |
||||
|
@ -1,3 +1,13 @@
|
||||
export class Comment { |
||||
constructor(public author: string, public content: string) {} |
||||
constructor(public author: string, public content: string, public dateCreated: Date) {} |
||||
|
||||
public static sortChronologically(a: Comment, b: Comment): number { |
||||
if (a.dateCreated < b.dateCreated) { |
||||
return -1; |
||||
} |
||||
if (a.dateCreated > b.dateCreated) { |
||||
return 1; |
||||
} |
||||
return 0; |
||||
} |
||||
} |
||||
|
@ -1,26 +1,24 @@
|
||||
import { v4 as uuidv4 } from 'uuid'; |
||||
|
||||
import { environment } from '../../../environments/environment'; |
||||
import { DateUtilsService } from '../utils/date-utils.service'; |
||||
|
||||
export class Configuration { |
||||
constructor( |
||||
public isAboutDate: boolean = false, |
||||
public slug: string = uuidv4(), |
||||
public isProtectedByPassword: boolean = false, |
||||
public isOwnerNotifiedByEmail: { onNewVote: boolean; onNewComment: boolean } = { |
||||
onNewVote: false, |
||||
onNewComment: false, |
||||
}, |
||||
public isMaybeAnswerAvailable: boolean = false, |
||||
public creationDate: Date = new Date(Date.now()), |
||||
public expirationDate: Date = DateUtilsService.addDaysToDate( |
||||
public areResultsPublic: boolean = false, |
||||
public dateCreated: Date = new Date(Date.now()), |
||||
public expires: Date = DateUtilsService.addDaysToDate( |
||||
environment.poll.defaultConfig.expiracyInDays, |
||||
new Date(Date.now()) |
||||
) |
||||
) {} |
||||
|
||||
public getAdministrationUrl(): string { |
||||
return `${environment.api.baseHref}/administration/${environment.api.endpoints.polls}/${this.slug}`; |
||||
} |
||||
|
||||
public getParticipationUrl(): string { |
||||
return `${environment.api.baseHref}/participation/${environment.api.endpoints.polls}/${this.slug}`; |
||||
public static isArchived(configuration: Configuration): boolean { |
||||
return DateUtilsService.isDateInPast(configuration.expires); |
||||
} |
||||
} |
||||
|
@ -0,0 +1,6 @@
|
||||
import { ResponseType } from '../enums/response-type.enum'; |
||||
import { Choice } from './choice.model'; |
||||
|
||||
export class PollUserAnswers { |
||||
constructor(public pseudo: string, public token: string, public responsesByChoices: Map<Choice, ResponseType>) {} |
||||
} |
@ -1,17 +1,89 @@
|
||||
import { Answer } from './answer.model'; |
||||
import { environment } from 'src/environments/environment'; |
||||
import { v4 as uuidv4 } from 'uuid'; |
||||
|
||||
import { ResponseType } from '../enums/response-type.enum'; |
||||
import { PollUserAnswers } from './poll-user-answers.model'; |
||||
import { Choice } from './choice.model'; |
||||
import { Comment } from './comment.model'; |
||||
import { Configuration } from './configuration.model'; |
||||
import { Question } from './question.model'; |
||||
import { User } from './user.model'; |
||||
|
||||
export class Poll { |
||||
constructor( |
||||
public owner: User, |
||||
public question: Question, |
||||
public choices: Choice[], |
||||
public question: string, |
||||
public description?: string, |
||||
public slug: string = uuidv4(), |
||||
public configuration: Configuration = new Configuration(), |
||||
public answers: Answer[] = [], |
||||
public comments: Comment[] = [] |
||||
public comments: Comment[] = [], |
||||
public choices: Choice[] = [], |
||||
public answersByChoiceByParticipant: Map<string, Map<string, ResponseType>> = new Map< |
||||
string, |
||||
Map<string, ResponseType> |
||||
>(), |
||||
public answers: PollUserAnswers[] = [] |
||||
) {} |
||||
|
||||
public getAdministrationUrl(): string { |
||||
return `${environment.api.baseHref}/administration/polls/${this.slug}`; |
||||
} |
||||
|
||||
public getParticipationUrl(): string { |
||||
return `${environment.api.baseHref}/participation/polls/${this.slug}`; |
||||
} |
||||
|
||||
public static adaptFromLocalJsonServer( |
||||
item: Pick< |
||||
Poll, |
||||
| 'owner' |
||||
| 'question' |
||||
| 'description' |
||||
| 'slug' |
||||
| 'configuration' |
||||
| 'comments' |
||||
| 'choices' |
||||
| 'answersByChoiceByParticipant' |
||||
| 'answers' |
||||
> |
||||
): Poll { |
||||
const poll = new Poll( |
||||
new User(item.owner.pseudo, item.owner.email, undefined), |
||||
item.question, |
||||
item.description, |
||||
item.slug, |
||||
item.configuration, |
||||
item.comments |
||||
.map( |
||||
(c: Pick<Comment, 'author' | 'content' | 'dateCreated'>) => |
||||
new Comment(c.author, c.content, new Date(c.dateCreated)) |
||||
) |
||||
.sort(Comment.sortChronologically), |
||||
item.choices.map((c: Pick<Choice, 'label' | 'imageUrl' | 'participants' | 'imageUrl'>) => { |
||||
const choice = new Choice(c.label, c.imageUrl, new Map(c.participants)); |
||||
choice.participants.forEach((value, key) => { |
||||
choice.participants.set(key, new Set(value)); |
||||
}); |
||||
choice.updateCounts(); |
||||
return choice; |
||||
}) |
||||
); |
||||
|
||||
// handle answersByChoiceByParticipant
|
||||
for (const [pseudo, answersByChoice] of Object.entries(item.answersByChoiceByParticipant)) { |
||||
if (!poll.answersByChoiceByParticipant.has(pseudo)) { |
||||
poll.answersByChoiceByParticipant.set(pseudo, new Map<string, ResponseType>()); |
||||
} |
||||
for (const [choiceLabel, answer] of Object.entries(answersByChoice)) { |
||||
poll.answersByChoiceByParticipant.get(pseudo).set(choiceLabel, answer as ResponseType); |
||||
} |
||||
} |
||||
|
||||
// handle answers
|
||||
poll.answers = item.answers.map( |
||||
(pollUserAnswers: Pick<PollUserAnswers, 'pseudo' | 'token' | 'responsesByChoices'>) => |
||||
new PollUserAnswers(pollUserAnswers.pseudo, pollUserAnswers.token, pollUserAnswers.responsesByChoices) |
||||
); |
||||
|
||||
return poll; |
||||
} |
||||
} |
||||
|
@ -1,16 +0,0 @@
|
||||
import { TestBed } from '@angular/core/testing'; |
||||
|
||||
import { CommentService } from './comment.service'; |
||||
|
||||
describe('CommentService', () => { |
||||
let service: CommentService; |
||||
|
||||
beforeEach(() => { |
||||
TestBed.configureTestingModule({}); |
||||
service = TestBed.inject(CommentService); |
||||
}); |
||||
|
||||
it('should be created', () => { |
||||
expect(service).toBeTruthy(); |
||||
}); |
||||
}); |
@ -1,15 +0,0 @@
|
||||
import { Injectable } from '@angular/core'; |
||||
|
||||
import { Poll } from '../models/poll.model'; |
||||
import { ApiService } from './api.service'; |
||||
|
||||
@Injectable({ |
||||
providedIn: 'root', |
||||
}) |
||||
export class CommentService { |
||||
constructor(private apiService: ApiService) {} |
||||
|
||||
public saveComment(poll: Poll, comment: string): void { |
||||
this.apiService.saveComment(poll, comment); |
||||
} |
||||
} |