add json-server + refacto

This commit is contained in:
seraph 2020-05-12 19:16:23 +02:00
parent 355fed53f3
commit ae29749670
125 changed files with 3065 additions and 1700 deletions

View File

@ -1,19 +1,20 @@
module.exports = {
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,
},
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: {},
rules: {
'@typescript-eslint/unbound-method': ['error', { ignoreStatic: true }],
},
};

View File

@ -13,7 +13,7 @@ pages:
stage: pages
script:
- yarn install --pure-lockfile
- npx ng build --base-href=/framadate/funky-framadate-front/
- yarn build
- mv dist/framadate/ public/
artifacts:
paths:

View File

@ -1,16 +1,17 @@
## LIBRARIES USED
| status | lib name | usage |
| --------------- | -------------------------------------------------------------- | ---------------------------------------- |
| :-------------: | -------------------------------------------------------------- | --------------------------------------------------------- |
| | [axios](https://github.com/axios/axios) | http client |
| | [bulma](https://bulma.io/) | CSS framework |
| | [chart.js](https://www.chartjs.org/) | Display graphs. (Comes with MomentJS) |
| | [chart.js](https://www.chartjs.org/) | PrimeNG solution for graphs. (Chart.js installs MomentJS) |
| | [compodoc](https://compodoc.app/) | Generate technic documentation |
| | ESlint, Prettier, Lint-staged | Format & lint code |
| | [fork-awesome](https://forkaweso.me) | Icons collection |
| | [fullcalendar](https://fullcalendar.io/docs/initialize-es6) | Manage & display calendars |
| | [fullcalendar](https://fullcalendar.io/docs/initialize-es6) | PrimeNG solution to manage & display calendars |
| | [husky](https://www.npmjs.com/package/husky) | Hook actions on commit |
| | [jest](https://jestjs.io/) | test engine |
| | [json-server](https://www.npmjs.com/package/json-server) | local server for mocking data backend |
| removed | [locale-enum](https://www.npmjs.com/package/locale-enum) | enum of all locales |
| | [momentJS](https://momentjs.com/) | manipulate dates. (chartJSs dependency) |
| to be installed | [ng2-charts](https://valor-software.com/ng2-charts/) | Manipulate graphs along with chart.js |
@ -19,9 +20,10 @@
| | [ngx-webstorage](https://www.npmjs.com/package/ngx-webstorage) | handle localStorage & webStorage |
| | [primeNG](https://www.primefaces.org/primeng/) | UI components collection |
| | [quill](https://www.npmjs.com/package/quill) | powerful rich text editor. WYSIWYG. |
| to be installed | [short-uuid](https://www.npmjs.com/package/short-uuid) | generate uuid |
| removed | [storybook](https://storybook.js.org/) | StyleGuide UI |
| | [ts-mockito](https://www.npmjs.com/package/ts-mockito) | Mocks for testing. |
| | [uuid](https://www.npmjs.com/package/uuid) | handle client-side generation of uuids |
| to be removed | [uuid](https://www.npmjs.com/package/uuid) | generate uuid |
---
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.2.1.

View File

@ -25,10 +25,9 @@
"assets": ["src/favicon.ico", "src/assets"],
"styles": [
"node_modules/fork-awesome/css/fork-awesome.min.css",
"node_modules/primeicons/primeicons.css",
"node_modules/primeng/resources/themes/nova-light/theme.css",
"node_modules/primeng/resources/primeng.min.css",
"node_modules/primeflex/primeflex.css",
"node_modules/bulma-switch/dist/css/bulma-switch.min.css",
"src/styles.scss"
],
"scripts": [

234
mocks/db.json Normal file
View File

@ -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, cest trop bien, jadore!",
"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
}
]
}

8
mocks/routes.json Normal file
View File

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

View File

@ -12,12 +12,15 @@
"test": "jest",
"test:watch": "jest --watch",
"test:ci": "jest --runInBand",
"lint": "ng lint",
"lint": "prettier --write \"src/**/*.{js,jsx,ts,tsx,md,html,css,scss}\"",
"e2e": "ng e2e",
"format:check": "prettier --list-different \"src/{app,environments,assets}/**/*{.ts,.js,.json,.css,.scss}\"",
"format:all": "prettier --write \"src/**/*.{js,jsx,ts,tsx,md,html,css,scss}\"",
"trans": "ng xi18n --output-path=src/locale --i18n-locale=fr",
"compodoc": "compodoc -p tsconfig.app.json"
"compodoc": "compodoc -p tsconfig.app.json",
"mock:server": "json-server --port 8000 --watch ./mocks/db.json --routes ./mocks/routes.json",
"start:proxy": "ng serve --proxy-config proxy.conf.json",
"start:proxymock": "concurrently --kill-others \"yarn mock:server\" \"yarn start:proxy\""
},
"private": false,
"dependencies": {
@ -36,20 +39,19 @@
"@ngx-translate/http-loader": "^4.0.0",
"angular-date-value-accessor": "^1.0.2",
"axios": "^0.19.2",
"bulma": "^0.8.2",
"bulma": "^0.9.0",
"bulma-switch": "^2.0.0",
"chart.js": "^2.9.3",
"fork-awesome": "^1.1.7",
"ng2-charts": "^2.3.0",
"ngx-clipboard": "^13.0.0",
"ngx-markdown": "^9.0.0",
"ngx-webstorage": "^5.0.0",
"primeflex": "^1.0.0",
"primeicons": "^2.0.0",
"primeng": "^9.0.6",
"quill": "^1.3.7",
"rxjs": "^6.5.5",
"rxjs-compat": "^6.5.5",
"tslib": "^1.11.1",
"tslib": "<2.0.0",
"uuid": "^8.0.0",
"zone.js": "^0.10.3"
},
@ -64,22 +66,24 @@
"@babel/preset-typescript": "^7.9.0",
"@compodoc/compodoc": "^1.1.11",
"@types/jest": "^25.2.1",
"@types/node": "^13.13.2",
"@types/uuid": "^7.0.2",
"@typescript-eslint/eslint-plugin": "^2.27.0",
"@typescript-eslint/parser": "^2.27.0",
"@types/node": "^14.0.1",
"@types/uuid": "^8.0.0",
"@typescript-eslint/eslint-plugin": "^3.0.0",
"@typescript-eslint/parser": "^3.0.0",
"babel-jest": "^26.0.0",
"eslint": "^6.8.0",
"concurrently": "^5.2.0",
"eslint": "^7.0.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3",
"husky": "^4.2.5",
"jest": "^26.0.0",
"jest-environment-jsdom-sixteen": "^1.0.3",
"jest-preset-angular": "^8.1.3",
"json-server": "^0.16.1",
"lint-staged": "^10.1.7",
"prettier": "^2.0.5",
"protractor": "~5.4.3",
"ts-jest": "^25.4.0",
"protractor": "~7.0.0",
"ts-jest": "^26.0.0",
"ts-mockito": "^2.5.0",
"ts-node": "^8.10.1",
"typescript": "~3.8.3"
@ -90,11 +94,13 @@
}
},
"lint-staged": {
"src/{app,environments,assets}/**/*.{js,jsx,ts,tsx,md,html,css,scss}": [
"src/**/*.{js,jsx,ts,tsx,md,html,css,scss}": [
"prettier --write",
"git add"
],
"*.js": "eslint --cache --fix"
"*.js": [
"prettier --write"
]
},
"jest": {
"preset": "jest-preset-angular",

11
proxy.conf.json Normal file
View File

@ -0,0 +1,11 @@
{
"/api/v1/*": {
"target": "http://localhost:8000",
"secure": false,
"pathRewrite": {
"^/api/v1": ""
},
"changeOrigin": false,
"logLevel": "debug"
}
}

View File

@ -11,6 +11,10 @@ const routes: Routes = [
loadChildren: () =>
import('./features/administration/administration.module').then((m) => m.AdministrationModule),
},
{
path: 'consultation',
loadChildren: () => import('./features/consultation/consultation.module').then((m) => m.ConsultationModule),
},
{
path: 'participation',
loadChildren: () => import('./features/participation/participation.module').then((m) => m.ParticipationModule),

View File

@ -32,7 +32,7 @@ export class AppComponent implements OnInit, OnDestroy {
if (!environment.production) {
this.appTitle += ' [DEV]';
// TODO: to be removed
this.mockingService.loadUser(new User('TOTO', 'toto@gafam.com', UserRole.REGISTERED, false));
this.mockingService.loadUser(new User('TOTO', 'toto@gafam.com', UserRole.REGISTERED));
}
this.titleService.setTitle(this.appTitle);
this.languageService.configureAndInitTranslations();

View File

@ -1,5 +1,7 @@
import { CommonModule } from '@angular/common';
import { APP_BASE_HREF, CommonModule, registerLocaleData } from '@angular/common';
import { HttpClient, HttpClientModule } from '@angular/common/http';
import localeEn from '@angular/common/locales/en';
import localeFr from '@angular/common/locales/fr';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrowserModule, Title } from '@angular/platform-browser';
@ -21,8 +23,9 @@ import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { CoreModule } from './core/core.module';
import { SharedModule } from './shared/shared.module';
import { ParticipationModule } from './features/participation/participation.module';
import { AdministrationModule } from './features/administration/administration.module';
registerLocaleData(localeEn, 'en-EN');
registerLocaleData(localeFr, 'fr-FR');
export class MyMissingTranslationHandler implements MissingTranslationHandler {
public handle(params: MissingTranslationHandlerParams): string {
@ -38,7 +41,6 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
declarations: [AppComponent],
imports: [
AppRoutingModule,
AdministrationModule,
BrowserAnimationsModule,
BrowserModule,
ClipboardModule,
@ -61,9 +63,8 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
},
useDefaultLang: false,
}),
ParticipationModule,
],
providers: [Title, TranslateService],
providers: [{ provide: APP_BASE_HREF, useValue: environment.baseHref }, Title, TranslateService],
bootstrap: [AppComponent],
})
export class AppModule {}

View File

@ -37,18 +37,30 @@
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link"> Modules </a>
<div class="navbar-dropdown">
<a class="navbar-item" routerLink="oldstuff" routerLinkActive="is-active">
Old stuff
</a>
<a class="navbar-item" routerLink="administration" routerLinkActive="is-active">
Administration
</a>
<a class="navbar-item" routerLink="consultation" routerLinkActive="is-active">
Consultation
</a>
<a class="navbar-item" routerLink="participation" routerLinkActive="is-active">
Participation
</a>
</div>
</div>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link"> Tous les sondages </a>
<div class="navbar-dropdown">
<a
class="navbar-item"
routerLink="participation/poll/citron_ou_orange"
*ngFor="let slug of slugsAvailables"
routerLink="{{ '/consultation/poll/' + slug }}"
routerLinkActive="is-active"
>
Participation à citron_ou_orange
</a>
<a class="navbar-item" routerLink="oldstuff" routerLinkActive="is-active">
Old stuff
« {{ slug }} »
</a>
</div>
</div>
@ -57,11 +69,11 @@
<div class="navbar-end">
<div class="navbar-item" #container>
<div class="buttons has-addons is-centered clickable" (click)="openDialog()">
<button class="button is-static"><i class="fa fa-user-circle"></i></button>
<button class="button is-static" *ngIf="_user | async as user">
{{ user.pseudo || 'anonyme' }}
<button class="button is-static"><i class="fa fa-user-circle" aria-hidden="true"></i></button>
<button class="button is-static" *ngIf="_user | async">
{{ (_user | async)?.pseudo || 'anonyme' }}
</button>
<button class="button is-static"><i class="fa fa-cogs"></i></button>
<button class="button is-static"><i class="fa fa-cogs" aria-hidden="true"></i></button>
</div>
</div>
</div>

View File

@ -1,7 +1,8 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Observable } from 'rxjs';
import { User } from '../../models/user.model';
import { ApiService } from '../../services/api.service';
import { ModalService } from '../../services/modal.service';
import { UserService } from '../../services/user.service';
@ -10,13 +11,23 @@ import { UserService } from '../../services/user.service';
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
})
export class HeaderComponent {
export class HeaderComponent implements OnInit {
@Input() isSidebarOpened: boolean;
@Output() toggleSidebarEE = new EventEmitter<boolean>();
public _user: Observable<User> = this.userService.user;
constructor(private userService: UserService, private modalService: ModalService) {}
public slugsAvailables: string[] = [];
constructor(private userService: UserService, private modalService: ModalService, private apiService: ApiService) {}
public ngOnInit(): void {
this.getSlugs();
}
public async getSlugs(): Promise<void> {
this.slugsAvailables = await this.apiService.getAllPollsSlugs();
}
public openDialog(): void {
this.modalService.openSettingsComponent();

View File

@ -1,11 +1,7 @@
<nav>
<a class="button" routerLink="administration" routerLinkActive="active"> AdministrationModule</a>
<a class="button" routerLink="participation/poll/SuperCustomSlug" routerLinkActive="active">
Participate to poll/SuperCustomSlug
<a class="button" routerLink="oldstuff/home" routerLinkActive="active">
<i class="fa fa-home" aria-hidden="true"></i> Accueil
</a>
<hr />
<a class="button" routerLink="oldstuff/home" routerLinkActive="active"> <i class="fa fa-home"></i> Accueil </a>
<a class="button" routerLink="oldstuff/step/creation" routerLinkActive="active"> Création </a>
<a class="button" routerLink="oldstuff/step/date" routerLinkActive="active"> Les Dates </a>
<a class="button" routerLink="oldstuff/step/answers" routerLinkActive="active"> Réponses </a>

View File

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

View File

@ -1,4 +1,4 @@
export enum Response {
export enum ResponseType {
YES = 'YES',
NO = 'NO',
MAYBE = 'MAYBE',

View File

@ -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) {}
}

View File

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

View File

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

View File

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

View File

@ -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>) {}
}

View File

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

View File

@ -2,11 +2,5 @@ import { UserRole } from '../enums/user-role.enum';
import { Poll } from './poll.model';
export class User {
constructor(
public pseudo: string,
public email: string,
public role: UserRole = UserRole.ANONYMOUS,
public isOwner: boolean = false,
public polls?: Poll[]
) {}
constructor(public pseudo: string, public email: string, public role?: UserRole, public polls?: Poll[]) {}
}

View File

@ -2,56 +2,60 @@ import { Injectable } from '@angular/core';
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import { environment } from 'src/environments/environment';
import { ResponseType } from '../enums/response-type.enum';
import { Poll } from '../models/poll.model';
import { User } from '../models/user.model';
@Injectable({
providedIn: 'root',
})
export class ApiService {
private axiosInstance: AxiosInstance;
private readonly pollsEndpoint = environment.api.endpoints.polls.name;
private readonly answersEndpoint = environment.api.endpoints.polls.answers.name;
private readonly commentsEndpoint = environment.api.endpoints.polls.comments.name;
private readonly votesEndpoint = environment.api.endpoints.polls.votes.name;
private readonly slugsEndpoint = environment.api.endpoints.polls.slugs.name;
private readonly votesStacksEndpoint = environment.api.endpoints.polls.votesStacks.name;
private readonly usersEndpoint = environment.api.endpoints.users.name;
private readonly usersPollsEndpoint = environment.api.endpoints.users.polls.name;
private readonly usersPollsSendEmailEndpoint = environment.api.endpoints.users.polls.sendEmail.name;
constructor() {
this.axiosInstance = axios.create({ baseURL: environment.api.baseHref });
this.axiosInstance.defaults.timeout = 2500;
this.axiosInstance.defaults.headers.post['Content-Type'] = 'application/json';
}
////////////
// CREATE //
////////////
public async savePoll(poll: Poll): Promise<void> {
//////////////////////
// CREATE OR UPDATE //
//////////////////////
public async createPoll(poll: Poll): Promise<string> {
try {
await this.axiosInstance.post(`${this.pollsEndpoint}`, poll);
return await this.axiosInstance.post(`${this.pollsEndpoint}`, poll);
} catch (error) {
this.handleError(error);
}
}
public async saveVote(poll: Poll): Promise<void> {
public async createParticipation(
pollId: string,
choiceLabel: string,
pseudo: string,
response: ResponseType
): Promise<string> {
try {
// TODO: add the votestack in the params
await this.axiosInstance.post(`${this.pollsEndpoint}/${poll.configuration.slug}${this.votesEndpoint}`, {
params: { voteStack: {} },
return await this.axiosInstance.post(`${this.pollsEndpoint}/${pollId}${this.answersEndpoint}`, {
choiceLabel,
pseudo,
response,
});
} catch (error) {
this.handleError(error);
}
}
public async saveComment(poll: Poll, comment: string): Promise<void> {
public async createComment(slug: string, comment: string): Promise<string> {
try {
// TODO: add the comment in the params
await this.axiosInstance.post(
`${this.pollsEndpoint}/${poll.configuration.slug}${this.commentsEndpoint}`,
comment
);
return await this.axiosInstance.post(`${this.pollsEndpoint}/${slug}${this.commentsEndpoint}`, comment);
} catch (error) {
this.handleError(error);
}
@ -60,7 +64,48 @@ export class ApiService {
//////////
// READ //
//////////
public async isSlugAvailable(slug: string): Promise<boolean> {
public async getAllPollsSlugs(): Promise<string[]> {
// TODO: used for facilities in DEV, should be removed in production
try {
const response: AxiosResponse<Poll[]> = await this.axiosInstance.get<Poll[]>(`${this.pollsEndpoint}`);
return response?.data.map((poll: Poll) => poll.slug);
} catch (error) {
this.handleError(error);
}
}
public async getPollBySlug(slug: string): Promise<Poll | undefined> {
// TODO: identifier should be decided according to backend : Id || Slug ?
try {
// TODO: this interceptor should be avoided if backends returns the good object
const adapterInterceptor: number = this.axiosInstance.interceptors.response.use(
(response): AxiosResponse => {
if (response.data['poll']) {
// response from cipherbliss backend, actually used by oldstuffModule
response.data = response.data['poll'];
} else if (response.data[0] && response.data[0]['slug'] && response.data[0]['question']) {
// response from local json-server
response.data = Poll.adaptFromLocalJsonServer(response.data[0]);
}
return response;
}
);
console.log('fetch API : asking for poll with slug=' + slug);
const response: AxiosResponse<Poll> = await this.axiosInstance.get<Poll>(`${this.pollsEndpoint}/${slug}`);
axios.interceptors.request.eject(adapterInterceptor);
return response?.data;
} catch (error) {
if (error.response?.status === 404) {
return undefined;
} else {
this.handleError(error);
}
}
}
public async getSlug(slug: string): Promise<boolean> {
try {
// TODO: scenario should be : if we can get this slug, it exists. if not, it doesn't. It's just a GET.
const response: AxiosResponse = await this.axiosInstance.get(
@ -78,23 +123,24 @@ export class ApiService {
}
}
public async sendEmailToUserOfItsPollsList(user: User): Promise<void> {
public async sendEmailToUserOfItsPollsList(email: string): Promise<void> {
// If user is not authenticated: the list of polls is send to user's email by the backend.
try {
await this.axiosInstance.get<Poll[]>(
`${this.usersEndpoint}/${user.email}${this.usersPollsEndpoint}${this.usersPollsSendEmailEndpoint}`
`${this.usersEndpoint}/${email}${this.usersPollsEndpoint}${this.usersPollsSendEmailEndpoint}`
);
} catch (error) {
this.handleError(error);
}
}
public async getPollsByUserEmail(user: User): Promise<Poll[]> {
public async getPollsUrlsByUserEmail(email: string): Promise<Poll[]> {
// If user is authenticated : retrieve polls & display directly in frontend.
// TODO: Backend should handle this case. Actually the endpoint doesn't exist in backend.
// Here, only the list of slugs is usefull. Maybe just handle the list of slugs.
try {
const response: AxiosResponse<Poll[]> = await this.axiosInstance.get<Poll[]>(
`${this.usersEndpoint}/${user.email}${this.usersPollsEndpoint}`
`${this.usersEndpoint}/${email}${this.usersPollsEndpoint}`
);
return response?.data;
} catch (error) {
@ -102,49 +148,20 @@ export class ApiService {
}
}
public async getPollByIdentifier(identifier: string): Promise<Poll | undefined> {
// TODO: identifier should be decided according to backend : Id || Slug ?
try {
// TODO: this interceptor should not be existing, backend should return the good object poll
const adapterInterceptor: number = this.axiosInstance.interceptors.response.use(
(response): AxiosResponse => {
response.data = response.data['poll'];
return response;
}
);
const response: AxiosResponse<Poll> = await this.axiosInstance.get<Poll>(
`${this.pollsEndpoint}/${identifier}`
);
axios.interceptors.request.eject(adapterInterceptor);
return response?.data;
} catch (error) {
if (error.response?.status === 404) {
return undefined;
} else {
this.handleError(error);
}
}
}
////////////
// UPDATE //
////////////
public async updatePoll(poll: Poll): Promise<void> {
public async updateAnswer(
slug: string,
choiceLabel: string,
pseudo: string,
response: ResponseType
): Promise<string> {
try {
// TODO: implement the params when entities are finalized.
await this.axiosInstance.put(`${this.pollsEndpoint}/${poll.configuration.slug}`, {
params: { voteStack: {}, token: '' },
});
} catch (error) {
this.handleError(error);
}
}
public async updateVote(voteStack: any): Promise<void> {
try {
// TODO: implement the params when entities are finalized.
await this.axiosInstance.patch(`${this.votesStacksEndpoint}/${voteStack.id}`, {
params: { voteStack: {}, token: '' },
return await this.axiosInstance.patch(`${this.pollsEndpoint}/${slug}${this.answersEndpoint}`, {
choiceLabel,
pseudo,
response,
});
} catch (error) {
this.handleError(error);
@ -154,27 +171,32 @@ export class ApiService {
////////////
// DELETE //
////////////
public async deletePoll(poll: Poll): Promise<void> {
public async deletePoll(slug: string): Promise<boolean> {
try {
await this.axiosInstance.delete(`${this.pollsEndpoint}${poll.configuration.slug}`, {});
const response: AxiosResponse = await this.axiosInstance.delete(`${this.pollsEndpoint}/${slug}`);
return response?.status === 204;
} catch (error) {
this.handleError(error);
}
}
public async deletePollVotes(poll: Poll): Promise<void> {
public async deletePollAnswers(slug: string): Promise<boolean> {
try {
// TODO: update endpoint in Backend
await this.axiosInstance.delete(`${this.pollsEndpoint}${poll.configuration.slug}${this.votesEndpoint}`);
const response: AxiosResponse = await this.axiosInstance.delete(
`${this.pollsEndpoint}/${slug}${this.answersEndpoint}`
);
return response?.status === 204;
} catch (error) {
this.handleError(error);
}
}
public async deletePollComments(poll: Poll): Promise<void> {
public async deletePollComments(slug: string): Promise<boolean> {
try {
// TODO: modify endpoint in Backend
await this.axiosInstance.delete(`${this.pollsEndpoint}${poll.configuration.slug}${this.commentsEndpoint}`);
const response: AxiosResponse = await this.axiosInstance.delete(
`${this.pollsEndpoint}/${slug}${this.commentsEndpoint}`
);
return response?.status === 204;
} catch (error) {
this.handleError(error);
}
@ -200,6 +222,5 @@ export class ApiService {
console.log('Error', error.message);
}
console.log(error.config);
throw error;
}
}

View File

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

View File

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

View File

@ -1,6 +1,5 @@
import { Injectable } from '@angular/core';
import { UserRole } from '../enums/user-role.enum';
import { Choice } from '../models/choice.model';
import { Poll } from '../models/poll.model';
import { Question } from '../models/question.model';
@ -15,17 +14,13 @@ export class MockingService {
private user: User;
public pollsDatabase: Poll[] = [];
private poll1: Poll = new Poll(this.user, new Question('Quand le picnic ?', 'Pour faire la teuf'), [
new Choice('mardi prochain'),
new Choice('mercredi'),
]);
private poll2: Poll = new Poll(this.user, new Question('On fait quoi à la soirée ?', 'Balancez vos idées'), [
new Choice('jeux'),
new Choice('danser'),
new Choice('discuter en picolant'),
]);
private poll1: Poll = new Poll(this.user, 'Quand le picnic ?', 'Pour faire la teuf');
private poll2: Poll = new Poll(this.user, 'On fait quoi à la soirée ?', 'Balancez vos idées');
constructor(private userService: UserService, private pollService: PollService) {
this.poll1.choices = [new Choice('mardi prochain'), new Choice('mercredi')];
this.poll2.choices = [new Choice('jeux'), new Choice('danser'), new Choice('discuter en picolant')];
this.pollsDatabase = [this.poll1, this.poll2];
}
@ -37,7 +32,7 @@ export class MockingService {
}
public loadPoll(slug: string): void {
this.poll1.configuration.slug = slug;
this.poll1.slug = slug;
console.info('MOCKING poll', { poll: this.poll1 });
this.pollService.updateCurrentPoll(this.poll1);
}

View File

@ -1,7 +1,9 @@
import { Injectable } from '@angular/core';
import { DialogService } from 'primeng';
import { ChoiceDetailsComponent } from '../../shared/components/choice-details/choice-details.component';
import { SettingsComponent } from '../../shared/components/settings/settings.component';
import { Choice } from '../models/choice.model';
@Injectable({
providedIn: 'root',
@ -12,4 +14,13 @@ export class ModalService {
public openSettingsComponent(): void {
this.dialogService.open(SettingsComponent, { header: 'Paramètres', dismissableMask: true });
}
public openChoiceDetailsComponent(choice: Choice): void {
this.dialogService.open(ChoiceDetailsComponent, {
header: 'Détails des votes',
dismissableMask: true,
data: choice,
width: '70%',
});
}
}

View File

@ -2,7 +2,10 @@ import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { MessageSeverity } from '../enums/message-severity.enum';
import { ResponseType } from '../enums/response-type.enum';
import { Choice } from '../models/choice.model';
import { Poll } from '../models/poll.model';
import { User } from '../models/user.model';
import { ApiService } from './api.service';
import { MessageDisplayerService } from './message-displayer.service';
@ -13,47 +16,59 @@ export class PollService {
private _poll: BehaviorSubject<Poll | undefined> = new BehaviorSubject<Poll | undefined>(undefined);
public readonly poll: Observable<Poll | undefined> = this._poll.asObservable();
constructor(private apiService: ApiService, private messageService: MessageDisplayerService) {}
constructor(private apiService: ApiService, private messageDisplayerService: MessageDisplayerService) {}
public updateCurrentPoll(poll: Poll): void {
this._poll.next(poll);
}
// SAVE
public async savePoll(poll: Poll): Promise<void> {
await this.apiService.savePoll(poll);
this.messageService.display(MessageSeverity.SUCCESS, 'Le sondage a été créé.');
}
public async saveVote(poll: Poll): Promise<void> {
await this.apiService.saveVote(poll);
this.messageService.display(MessageSeverity.SUCCESS, 'Votre participation au sondage a été enregistrée.');
}
public async saveComment(poll: Poll, comment: string): Promise<void> {
await this.apiService.saveComment(poll, comment);
this.messageService.display(MessageSeverity.SUCCESS, 'Votre commentaire a été enregistré.');
}
// GET
public async getPollByIdentifier(slug: string): Promise<void> {
const poll: Poll | undefined = await this.apiService.getPollByIdentifier(slug);
if (poll) {
public async getPollBySlug(slug: string): Promise<void> {
const poll: Poll | undefined = await this.apiService.getPollBySlug(slug);
this.updateCurrentPoll(poll);
}
public async saveCurrentPoll(): Promise<void> {
const pollUrl: string = await this.apiService.createPoll(this._poll.getValue());
// TODO: Maybe handle the url to update currentPoll according to backend response
if (pollUrl) {
this.messageDisplayerService.display(MessageSeverity.SUCCESS, 'Le sondage a été enregistré.');
} else {
this.messageDisplayerService.display(
MessageSeverity.ERROR,
'Le sondage na été correctement enregistré, veuillez ré-essayer.'
);
}
}
// DELETE
public async deletePollVotes(poll: Poll): Promise<void> {
await this.apiService.deletePollVotes(poll);
this.messageService.display(
public saveParticipation(choice: Choice, user: User, response: ResponseType): void {
const currentPoll = this._poll.getValue();
currentPoll.choices.find((c) => c.label === choice.label)?.updateParticipation(user.pseudo, response);
this.updateCurrentPoll(currentPoll);
this.apiService.createParticipation(currentPoll.slug, choice.label, user.pseudo, response);
this.messageDisplayerService.display(
MessageSeverity.SUCCESS,
'Votre participation au sondage a été enregistrée.'
);
}
public async deleteAllAnswers(): Promise<void> {
await this.apiService.deletePollAnswers(this._poll.getValue().slug);
this.messageDisplayerService.display(
MessageSeverity.SUCCESS,
'Les participations des votants à ce sondage ont été supprimées.'
);
}
public async deletePollComments(poll: Poll): Promise<void> {
await this.apiService.deletePollComments(poll);
this.messageService.display(MessageSeverity.SUCCESS, 'Les commentaires de ce sondage ont été supprimés.');
public async addComment(comment: string): Promise<void> {
await this.apiService.createComment(this._poll.getValue().slug, comment);
this.messageDisplayerService.display(MessageSeverity.SUCCESS, 'Votre commentaire a été enregistré.');
}
public async deleteComments(): Promise<void> {
await this.apiService.deletePollComments(this._poll.getValue().slug);
this.messageDisplayerService.display(
MessageSeverity.SUCCESS,
'Les commentaires de ce sondage ont été supprimés.'
);
}
}

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { ActivatedRoute, UrlSegment } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { LoaderService } from './loader.service';
import { PollService } from './poll.service';
@ -15,9 +15,12 @@ export class UrlService {
) {}
public async loadPollFromUrl(): Promise<void> {
// TODO: behavior improvement needed.
// check if pollService currentPolls slug match the url : if yes, dont fetch again.
this.loaderService.setStatus(true);
const wantedSlug: string = this.route.snapshot.firstChild.firstChild.url[1].path;
await this.pollService.getPollByIdentifier(wantedSlug);
await this.pollService.getPollBySlug(wantedSlug);
this.loaderService.setStatus(false);
}
}

View File

@ -10,7 +10,7 @@ import { ApiService } from './api.service';
providedIn: 'root',
})
export class UserService {
public anonymous: User = new User('', '', UserRole.ANONYMOUS, false);
public anonymous: User = new User('', '', UserRole.ANONYMOUS);
private _user: BehaviorSubject<User> = new BehaviorSubject<User>(this.anonymous);
public readonly user: Observable<User> = this._user.asObservable();
@ -25,15 +25,13 @@ export class UserService {
return this._user.getValue().pseudo ? true : false;
}
// GET
public async getUserPolls(): Promise<void> {
const currentUser: User = this._user.getValue();
currentUser.polls = await this.apiService.getPollsByUserEmail(currentUser);
currentUser.polls = await this.apiService.getPollsUrlsByUserEmail(currentUser.email);
this.updateUser(currentUser);
}
// POST
public async sendEmailToUserTheListOfItsPolls(): Promise<void> {
await this.apiService.sendEmailToUserOfItsPollsList(this._user.getValue());
await this.apiService.sendEmailToUserOfItsPollsList(this._user.getValue().email);
}
}

View File

@ -1,16 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { VoteService } from './vote.service';
describe('VoteService', () => {
let service: VoteService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(VoteService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -1,17 +0,0 @@
import { Injectable } from '@angular/core';
import { ApiService } from './api.service';
@Injectable({
providedIn: 'root',
})
export class VoteService {
constructor(private apiService: ApiService) {}
public saveVote(vote: any): void {
this.apiService.saveVote(vote);
}
public updateVote(vote: any): void {
this.apiService.updateVote(vote);
}
}

View File

@ -13,7 +13,15 @@ export class DateUtilsService {
return moment(dateLeft).diff(moment(dateRight));
}
public static formatDate(date): string {
public static isDateInFuture(date: Date): boolean {
return this.diffInDays(date, new Date()) > 0;
}
public static isDateInPast(date: Date): boolean {
return this.diffInDays(date, new Date()) < 0;
}
public static formatDate(date: Date): string {
return moment(date).format('yyyy-MM-dd');
}
}

View File

@ -19,9 +19,9 @@ export class EditDescriptionComponent implements OnInit {
ngOnInit(): void {
this.pollForm = this.fb.group({
type: [this.poll ? this.poll.configuration.isAboutDate : false, [Validators.required]],
title: [this.poll ? this.poll.question.label : '', [Validators.required]],
description: [this.poll ? this.poll.question.description : ''],
slug: [this.poll ? this.poll.configuration.slug : this.generateRandomSlug(), [Validators.required]],
title: [this.poll ? this.poll.question : '', [Validators.required]],
description: [this.poll ? this.poll.description : ''],
slug: [this.poll ? this.poll.slug : this.generateRandomSlug(), [Validators.required]],
address: this.fb.group({
street: [''],
city: [''],

View File

@ -19,9 +19,9 @@ export class PollEditComponent implements OnInit {
ngOnInit(): void {
this.pollForm = this.fb.group({
type: [this.poll ? this.poll.configuration.isAboutDate : false, [Validators.required]],
title: [this.poll ? this.poll.question.label : '', [Validators.required]],
description: [this.poll ? this.poll.question.description : ''],
slug: [this.poll ? this.poll.configuration.slug : this.generateRandomSlug(), [Validators.required]],
title: [this.poll ? this.poll.question : '', [Validators.required]],
description: [this.poll ? this.poll.description : ''],
slug: [this.poll ? this.poll.slug : this.generateRandomSlug(), [Validators.required]],
address: this.fb.group({
street: [''],
city: [''],

View File

@ -11,10 +11,10 @@
<thead></thead>
<tbody>
<tr *ngFor="let poll of (_user | async)?.polls">
<th>{{ poll.question.label }}</th>
<th>{{ poll.question }}</th>
<td>
<a routerLink="{{ '/administration/edit/description/' + poll.configuration.slug }}">
{{ poll.configuration.getAdministrationUrl() }}
<a routerLink="{{ '/administration/edit/description/' + poll.slug }}">
{{ poll.slug }}
</a>
</td>
</tr>

View File

@ -0,0 +1,17 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ConsultationComponent } from './consultation.component';
const routes: Routes = [
{ path: '', redirectTo: 'poll', pathMatch: 'full' },
{ path: 'poll', component: ConsultationComponent },
{ path: ':slug', redirectTo: 'poll/:slug', pathMatch: 'full' },
{ path: 'poll/:slug', component: ConsultationComponent },
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class ConsultationRoutingModule {}

View File

@ -0,0 +1,62 @@
<div class="columns">
<div class="column has-text-centered">
<h1>Consultation</h1>
</div>
</div>
<div *ngIf="_isLoading | async" class="columns p-justify-center">
<div class="column has-text-centered">
<p-progressSpinner></p-progressSpinner>
</div>
</div>
<ng-container *ngIf="!(_isLoading | async)">
<ng-container *ngIf="!(_poll | async)">
<app-page-not-found [message]="'PAGE_NOT_FOUND.POLL'"></app-page-not-found>
</ng-container>
<ng-container *ngIf="_poll | async as poll">
<div class="columns">
<div class="column">
<div class="card">
<header class="card-header">
<p class="card-header-title">{{ poll.question }}</p>
<p class="card-header-icon">author : {{ poll.owner.pseudo }}</p>
</header>
<div class="card-content">
<div class="content">
<p>{{ poll.description }}</p>
<div class="buttons has-addons is-small is-right">
<button class="button" [class.is-active]="isCompactMode" (click)="isCompactMode = true">
Compact
</button>
<button
class="button"
[class.is-active]="!isCompactMode"
(click)="isCompactMode = false"
>
Detailed
</button>
</div>
<app-poll-results-compact *ngIf="isCompactMode" [poll]="poll"></app-poll-results-compact>
<app-poll-results-detailed *ngIf="!isCompactMode" [poll]="poll"></app-poll-results-detailed>
</div>
</div>
<footer class="card-footer" *ngIf="!isArchived(poll)">
<a routerLink="{{ '../../../participation/poll/' + poll.slug }}" class="card-footer-item">
Participer
</a>
<a routerLink="{{ '../../../administration/poll/' + poll.slug }}" class="card-footer-item">
Administrer
</a>
</footer>
</div>
</div>
</div>
<div class="columns">
<div class="column">
<app-comments></app-comments>
</div>
</div>
</ng-container>
</ng-container>

View File

@ -0,0 +1,24 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ConsultationComponent } from './consultation.component';
describe('ConsultationComponent', () => {
let component: ConsultationComponent;
let fixture: ComponentFixture<ConsultationComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ConsultationComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ConsultationComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,40 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Configuration } from '../../core/models/configuration.model';
import { Poll } from '../../core/models/poll.model';
import { LoaderService } from '../../core/services/loader.service';
import { ModalService } from '../../core/services/modal.service';
import { PollService } from '../../core/services/poll.service';
import { UrlService } from '../../core/services/url.service';
import { UserService } from '../../core/services/user.service';
@Component({
selector: 'app-consultation',
templateUrl: './consultation.component.html',
styleUrls: ['./consultation.component.scss'],
})
export class ConsultationComponent implements OnInit {
public _isLoading: Observable<boolean> = this.loaderService.isLoading;
public _poll: Observable<Poll> = this.pollService.poll;
public isCompactMode = true;
constructor(
private urlService: UrlService,
private loaderService: LoaderService,
private pollService: PollService,
private userService: UserService,
private modalService: ModalService
) {}
ngOnInit(): void {
if (!this.userService.isCurrentUserIdentifiable()) {
this.modalService.openSettingsComponent();
}
this.urlService.loadPollFromUrl();
}
public isArchived(poll: Poll): boolean {
return Configuration.isArchived(poll.configuration);
}
}

View File

@ -0,0 +1,15 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { SharedModule } from '../../shared/shared.module';
import { ConsultationRoutingModule } from './consultation-routing.module';
import { ConsultationComponent } from './consultation.component';
import { PollResultsCompactComponent } from './poll-results-compact/poll-results-compact.component';
import { PollResultsDetailedComponent } from './poll-results-detailed/poll-results-detailed.component';
@NgModule({
declarations: [ConsultationComponent, PollResultsCompactComponent, PollResultsDetailedComponent],
imports: [CommonModule, ConsultationRoutingModule, SharedModule, TranslateModule.forChild({ extend: true })],
})
export class ConsultationModule {}

View File

@ -0,0 +1,19 @@
<div class="box" *ngFor="let choice of poll.choices">
<div class="columns is-vcentered is-mobile">
<div class="column">
<label class="label">{{ choice.label }}</label>
</div>
<div class="column is-narrow">
<div class="buttons has-addons is-right" (click)="openModal(choice)">
<button class="button is-white">
<img class="image is-24x24" src="../../../assets/img/icon_voter_YES.svg" />
{{ choice.counts.get(responseTypeEnum.YES) }}
</button>
<button class="button is-white" *ngIf="poll.configuration.isMaybeAnswerAvailable">
<img class="image is-24x24" src="../../../assets/img/icon_voter_MAYBE.svg" />
{{ choice.counts.get(responseTypeEnum.MAYBE) }}
</button>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,24 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PollResultsCompactComponent } from './poll-results-compact.component';
describe('PollResultsCompactComponent', () => {
let component: PollResultsCompactComponent;
let fixture: ComponentFixture<PollResultsCompactComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [PollResultsCompactComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PollResultsCompactComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,28 @@
import { Component, Input, OnInit } from '@angular/core';
import { ResponseType } from '../../../core/enums/response-type.enum';
import { Choice } from '../../../core/models/choice.model';
import { Poll } from '../../../core/models/poll.model';
import { ModalService } from '../../../core/services/modal.service';
@Component({
selector: 'app-poll-results-compact',
templateUrl: './poll-results-compact.component.html',
styleUrls: ['./poll-results-compact.component.scss'],
})
export class PollResultsCompactComponent implements OnInit {
@Input() public poll: Poll;
public isModalOpened = false;
public choiceInModal: Choice;
public responseTypeEnum = ResponseType;
constructor(private modalService: ModalService) {}
ngOnInit(): void {}
public openModal(choice: Choice): void {
this.modalService.openChoiceDetailsComponent(choice);
this.choiceInModal = choice;
this.isModalOpened = true;
}
}

View File

@ -0,0 +1,35 @@
<table>
<thead>
<tr>
<th></th>
<th *ngFor="let choice of poll.choices">{{ choice.label }}</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let item of poll.answersByChoiceByParticipant | keyvalue">
<tr>
<td>{{ item.key }}</td>
<td *ngFor="let subItem of item.value | keyvalue">{{ subItem.value }}</td>
</tr>
</ng-container>
</tbody>
</table>
<hr />
<table>
<thead>
<tr>
<th></th>
<th *ngFor="let choice of poll.choices">{{ choice.label }}</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let answer of poll.answers">
<tr>
<td>{{ answer.pseudo }}</td>
<td *ngFor="let response of answer.responsesByChoices | keyvalue">
{{ response.value }}
</td>
</tr>
</ng-container>
</tbody>
</table>

View File

@ -0,0 +1,24 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PollResultsDetailedComponent } from './poll-results-detailed.component';
describe('PollResultsDetailedComponent', () => {
let component: PollResultsDetailedComponent;
let fixture: ComponentFixture<PollResultsDetailedComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [PollResultsDetailedComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PollResultsDetailedComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,16 @@
import { Component, Input, OnInit } from '@angular/core';
import { Poll } from '../../../core/models/poll.model';
@Component({
selector: 'app-poll-results-detailed',
templateUrl: './poll-results-detailed.component.html',
styleUrls: ['./poll-results-detailed.component.scss'],
})
export class PollResultsDetailedComponent implements OnInit {
@Input() public poll: Poll;
constructor() {}
ngOnInit(): void {}
}

View File

@ -10,7 +10,7 @@
<ol>
<li #answers *ngFor="let answer of config.answers; index as i; trackBy: trackFunction" class="answer-item">
<button class="btn btn--default" title="ajouter une image" (click)="showModalForPictureOfAnswer(answer)">
<i class="fa fa-image"></i>
<i class="fa fa-image" aria-hidden="true"></i>
</button>
<label for="answer_{{ answer.id }}_url" (click)="showModalForPictureOfAnswer(answer)">
<img class="img-thumbnail" src="{{ answer.url }}" alt="image {{ answer.url }}" />
@ -24,7 +24,7 @@
<label for="answer_{{ answer.id }}_url">
Choisissez une URL pour illustrer le choix de réponse
</label>
<i class="fa fa-image"></i>
<i class="fa fa-image" aria-hidden="true"></i>
<br />
<input
class="input is-block"
@ -56,7 +56,7 @@
[ngClass]="{ 'btn--primary': allAnswersAreValid }"
i18n
>
<i class="fa fa-plus"></i>
<i class="fa fa-plus" aria-hidden="true"></i>
Ajouter une proposition
</button>
<br />

View File

@ -22,7 +22,7 @@
</h1>
<form (ngSubmit)="findMyPollsByEmail(config.myEmail)">
<label class="description" for="sendemail" i18n>
<i class="fa fa-envelope"></i>
<i class="fa fa-envelope" aria-hidden="true"></i>
{{ 'config.find_helper' | translate }} :
</label>
<input
@ -51,9 +51,9 @@
<ul class="poll-list" *ngFor="let poll of config.myPolls; index as i; trackBy: trackFunction">
<li>
<a href="{{ poll.url }}">
{{ poll.question.label }}
{{ poll.question }}
<sub>
{{ poll.question.description }}
{{ poll.description }}
</sub>
</a>
</li>
@ -64,7 +64,7 @@
</div>
</section>
<div class="loading" *ngIf="config.loading">
<i class="fa fa-refresh fa-spin"></i>
<i class="fa fa-refresh fa-spin" aria-hidden="true"></i>
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
<h1 class="title is-1"><i class="fa fa-calendar"></i> {{ 'dates.title' | translate }}</h1>
<h1 class="title is-1"><i class="fa fa-calendar" aria-hidden="true"></i> {{ 'dates.title' | translate }}</h1>
<div>
<label for="multi_hours">
@ -16,7 +16,7 @@
</div>
<button (click)="addDate()" class="btn btn--primary" id="add_date_button">
<i class="fa fa-plus"></i>
<i class="fa fa-plus" aria-hidden="true"></i>
{{ 'dates.add' | translate }}
</button>
<button
@ -25,12 +25,12 @@
class="btn btn--primary"
id="toggle_interval_button"
>
<i class="fa fa-clock-o"></i>
<i class="fa fa-clock-o" aria-hidden="true"></i>
{{ 'dates.add_interval' | translate }}
</button>
<button (click)="emptyAll()" class="btn btn--warning" id="empty_button">
<i class="fa fa-trash"></i>
<i class="fa fa-trash" aria-hidden="true"></i>
{{ 'dates.empty' | translate }}
</button>
<section *ngIf="showDateInterval" class="date-interval">
@ -45,7 +45,7 @@
<br />
</p>
<button (click)="addIntervalOfDates()" class="btn btn-block btn--primary">
<i class="fa fa-plus"></i>
<i class="fa fa-plus" aria-hidden="true"></i>
{{ 'dates.interval_button' | translate }}
{{ intervalDays }}
{{ 'dates.interval_button_dates' | translate }}
@ -71,7 +71,7 @@
class="btn btn--primary"
id="add_time_button"
>
<i class="fa fa-plus"></i>
<i class="fa fa-plus" aria-hidden="true"></i>
{{ 'dates.add_time' | translate }}
</button>
<button
@ -80,7 +80,7 @@
class="btn btn--warning"
id="remove_time_button"
>
<i class="fa fa-trash"></i>
<i class="fa fa-trash" aria-hidden="true"></i>
Aucune plage horaire
</button>
<button
@ -89,7 +89,7 @@
class="btn btn--warning"
id="reset_time_button"
>
<i class="fa fa-refresh"></i>
<i class="fa fa-refresh" aria-hidden="true"></i>
réinitialiser
</button>
</div>
@ -97,7 +97,7 @@
<div *ngIf="'false' === config.allowSeveralHours" class="identical-dates">
<div *ngFor="let time of config.timeList; index as id" class="time-choice">
<label for="timeChoices_{{ id }}">
<i class="fa fa-clock-o"></i>
<i class="fa fa-clock-o" aria-hidden="true"></i>
</label>
<input
[(ngModel)]="time.literal"
@ -106,7 +106,7 @@
id="timeChoices_{{ id }}"
/>
<button (click)="time.timeList.splice(id, 1)" class="btn btn-warning">
<i class="fa fa-times"></i>
<i class="fa fa-times" aria-hidden="true"></i>
</button>
</div>
</div>
@ -130,7 +130,7 @@
type="date"
/>
<button (click)="config.dateList.splice(id, 1)" class="btn btn-warning">
<i class="fa fa-times"></i>
<i class="fa fa-times" aria-hidden="true"></i>
</button>
<button
(click)="addTimeToDate(choice, id)"
@ -148,7 +148,7 @@
type="text"
/>
<button (click)="choice.timeList.splice(idTime, 1)" class="btn btn-warning">
<i class="fa fa-times"></i>
<i class="fa fa-times" aria-hidden="true"></i>
</button>
</div>
</div>

View File

@ -47,7 +47,7 @@
</p>
<button class="btn btn--primary" (click)="sendToEmail()">
<i class="fa fa-paper-plane"></i>
<i class="fa fa-paper-plane" aria-hidden="true"></i>
Envoyer les liens du sondage
</button>
<a href="{{ config.urlPublic }}">

View File

@ -79,11 +79,11 @@
<button routerLink="../answers" class="btn btn--primary btn--full" *ngIf="config.pollType == 'classic'" i18n>
Continuer
<i class="fa fa-file-text"></i>
<i class="fa fa-file-text" aria-hidden="true"></i>
</button>
<button routerLink="../date" class="btn btn--primary btn--full" *ngIf="config.pollType == 'dates'" i18n>
Continuer
<i class="fa fa-calendar-check-o"></i>
<i class="fa fa-calendar-check-o" aria-hidden="true"></i>
</button>
<a routerLink="../creation" class="prev" i18n>
Retour

View File

@ -3,12 +3,12 @@
launch admin action execStuff !
</button>
<button class="btn btn--primary" (click)="config.exportJson()" *ngIf="config.isAdmin">
<i class="fa fa-file-archive-o"></i>
<i class="fa fa-file-archive-o" aria-hidden="true"></i>
export CSV
</button>
<div class="loading" *ngIf="config.loading">
<i class="fa fa-refresh fa-spin"></i>
<i class="fa fa-refresh fa-spin" aria-hidden="true"></i>
</div>
<div class="loaded-poll" *ngIf="!config.loading && config.currentPoll">
<div id="choices">
@ -29,7 +29,7 @@
<h3 class="margin-top-x8">
Partager le sondage
<i class="fa fa-share"></i>
<i class="fa fa-share" aria-hidden="true"></i>
</h3>
<p class="nobold text-14" for="copyLink">
Pour partager le sondage, vous pouvez diffuser ce lien :

View File

@ -1,10 +1,10 @@
import { Component, OnInit } from '@angular/core';
import { BaseComponent } from '../../example/base-page/base.component';
import { ConfigService } from '../../../services/config.service';
import { mockComments } from '../../../../../mocks/mock-comments';
import { mockComments } from '../../../mocks/mock-comments';
import { ActivatedRoute, Router } from '@angular/router';
import { environment } from '../../../../../../environments/environment';
import { mockPoll3 } from '../../../../../mocks/mock-poll3';
import { mockPoll3 } from '../../../mocks/mock-poll3';
@Component({
selector: 'app-poll-display',

View File

@ -1,9 +1,9 @@
import { Component, Inject, OnInit } from '@angular/core';
import { Chart } from 'chart.js';
import { DOCUMENT } from '@angular/common';
import { mockGraphConfig } from '../../../../../mocks/mock-graph';
import { mockGraphConfig } from '../../../mocks/mock-graph';
import { ConfigService } from '../../../services/config.service';
import { mockPoll3 } from '../../../../../mocks/mock-poll3';
import { mockPoll3 } from '../../../mocks/mock-poll3';
@Component({
selector: 'app-poll-graphic',

View File

@ -23,7 +23,7 @@
</ul>
</section>
<section class="type-date" *ngIf="config.pollType !== 'classic'">
<i class="fa fa-clock-o"></i>
<i class="fa fa-clock-o" aria-hidden="true"></i>
<span class="well" *ngIf="'true' === config.allowSeveralHours">
{{ 'dates.multiple.different' | translate }}
</span>
@ -32,7 +32,7 @@
</span>
<div *ngFor="let choice of config.dateList; index as id" class="date-choice">
<div class="only-one-slice" *ngIf="!choice.timeList.length">
<i class="fa fa-square-o"></i>
<i class="fa fa-square-o" aria-hidden="true"></i>
</div>
{{ choice.literal }}
@ -40,14 +40,14 @@
<div *ngIf="'true' === config.allowSeveralHours" class="several-times">
<div *ngFor="let time of choice.timeList; index as idTime" class="time-choice">
{{ idTime }})
<i class="fa fa-square-o"></i>
<i class="fa fa-square-o" aria-hidden="true"></i>
{{ time.literal }}
</div>
</div>
<!-- CASE all dates having the same slices of the day-->
<div *ngIf="'false' === config.allowSeveralHours" class="same-times">
<div *ngFor="let time of config.timeList" class="time-choice">
<i class="fa fa-square-o"></i>
<i class="fa fa-square-o" aria-hidden="true"></i>
{{ time.literal }}
</div>
</div>

View File

@ -142,7 +142,7 @@
[disabled]="!config.password"
class="btn btn--default"
>
<i class="fa fa-eye"></i>
<i class="fa fa-eye" aria-hidden="true"></i>
{{ 'visibility.see_pass' | translate }}
</button>
</div>
@ -151,10 +151,10 @@
<button (click)="submitCreationAndGoToEnd()" class="btn btn--primary btn--full" i18n="@@confirm">
{{ 'visibility.validate_btn' | translate }}
<ng-container *ngIf="!config.loading">
<i class="fa fa-paper-plane"></i>
<i class="fa fa-paper-plane" aria-hidden="true"></i>
</ng-container>
<span class="loading" *ngIf="config.loading">
<i class="fa fa-refresh fa-spin fa-fw"></i>
<i class="fa fa-refresh fa-spin fa-fw" aria-hidden="true"></i>
</span>
</button>

View File

@ -17,13 +17,13 @@
[ngClass]="{ 'btn--primary': config.myTempVoteStack }"
*ngIf="!config.myVoteStack || !config.myVoteStack.id"
>
<i class="fa fa-paper-plane"></i> Envoyer
<i class="fa fa-paper-plane" aria-hidden="true"></i> Envoyer
</button>
<button
class="btn btn--primary btn-block submit-votestack update"
(click)="config.updateVote(config.myVoteStack)"
*ngIf="config.myVoteStack && config.myVoteStack.id"
>
<i class="fa fa-edit"></i> Mettre à jour
<i class="fa fa-edit" aria-hidden="true"></i> Mettre à jour
</button>
</div>

View File

@ -1,11 +1,11 @@
<section class="name">
<label for="name">
<i class="fa fa-user"></i>
<i class="fa fa-user" aria-hidden="true"></i>
Votre nom :</label
>
<input type="text" name="name" id="name" [(ngModel)]="config.myName" />
<input type="text" name="name" id="email" [(ngModel)]="config.myEmail" />
<i class="fa fa-envelope"></i>
<i class="fa fa-envelope" aria-hidden="true"></i>
</section>
<div class="comments" id="comments">
<h2 class="margin-top-x7">Laisser un commentaire</h2>
@ -13,7 +13,7 @@
<input type="text" class="margin-btm-x3" name="crname" [(ngModel)]="config.myName" id="crname" />
<input type="text" name="cremail" id="email_comment" [(ngModel)]="config.myEmail" />
<label for="email_comment">
<i class="fa fa-envelope"></i>
<i class="fa fa-envelope" aria-hidden="true"></i>
</label>
<div>
<label for="comment">Votre commentaire :</label>

View File

@ -5,7 +5,7 @@
class="btn btn--primary manage"
(click)="choice.simpleAnswer = !choice.simpleAnswer"
>
<i class="fa fa-cogs"></i>
<i class="fa fa-cogs" aria-hidden="true"></i>
</button>
<div class="choicebox__subject">
@ -84,16 +84,21 @@
>
<div class="choicebox__vote">
{{ poll.choices_count.counts[choice.id].yes.count }}
<img width="20px" height="21px" src="../../../assets/img/votant-sur.svg" alt="" />
<img width="20px" height="21px" src="../../../assets/img/icon_voter_YES.svg" alt="" />
</div>
<div class="choicebox__vote">
{{ poll.choices_count.counts[choice.id].maybe.count }}
<img width="22px" height="24px" src="../../../assets/img/votant-pas-sur.svg" alt="" />
<img width="22px" height="24px" src="../../../assets/img/icon_voter_MAYBE.svg" alt="" />
</div>
<div class="choicebox__tooltip" id="choicebox-tooltip">
<div class="choicebox__tooltiplist">
<div class="choicebox__tooltipttl">
<img width="20px" height="21px" src="../../../assets/img/votant-sur.svg" alt="" />
<img
width="20px"
height="21px"
src="../../../assets/img/icon_voter_YES.svg"
alt=""
/>
{{ poll.choices_count.counts[choice.id].yes.count }} "Oui"
</div>
<!-- liste des gens qui ont répondu oui-->
@ -108,7 +113,7 @@
<img
width="22px"
height="24px"
src="../../../assets/img/votant-pas-sur.svg"
src="../../../assets/img/icon_voter_MAYBE.svg"
alt=""
/>
{{ poll.choices_count.counts[choice.id].maybe.count }} "Peut-être"
@ -121,7 +126,7 @@
</div>
<div class="choicebox__tooltiplist" *ngIf="!simpleAnswer">
<div class="choicebox__tooltipttl">
<i class="fa fa-times"></i>
<i class="fa fa-times" aria-hidden="true"></i>
{{ poll.choices_count.counts[choice.id].no.count }} "Non"
</div>
<ul>

View File

@ -7,8 +7,8 @@ import { ConfirmationService, MessageService } from 'primeng';
import { Router } from '@angular/router';
import { ConfigService } from '../../../services/config.service';
import { VotingChoiceComponent } from './voting-choice.component';
import { mockChoice } from '../../../../../mocks/choice';
import { mockPoll3 } from '../../../../../mocks/mock-poll3';
import { mockChoice } from '../../../mocks/choice';
import { mockPoll3 } from '../../../mocks/mock-poll3';
const routerSpy = jest.fn({ navigateByUrl: jest.fn() });

View File

@ -1,5 +1,5 @@
import { Component, Input, OnInit } from '@angular/core';
import { mockComments } from '../../../../../mocks/mock-comments';
import { mockComments } from '../../../mocks/mock-comments';
@Component({
selector: 'app-voting-comment',

View File

@ -3,7 +3,7 @@
<ul>
<li>
<a href="title">
{{ config.currentPoll.poll.question.label }}
{{ config.currentPoll.poll.question }}
</a>
</li>
<li>
@ -19,7 +19,7 @@
</li>
<li>
<a href="comments">
<i class="fa fa-comments"></i>
<i class="fa fa-comments" aria-hidden="true"></i>
<span *ngIf="config.currentPoll && config.currentPoll.comments" class="comments-count">
{{ config.currentPoll.comments.length }}
</span>

View File

@ -1,18 +1,18 @@
<h2>Résumé</h2>
<div class="heading">
<div class="col-xs-6">
<h1 id="title">{{ config.currentPoll.poll.question.label }}</h1>
<p>{{ config.currentPoll.poll.question.description }}</p>
<h1 id="title">{{ config.currentPoll.poll.question }}</h1>
<p>{{ config.currentPoll.poll.description }}</p>
<span class="creationDate"> Créé le {{ config.currentPoll.poll.creationDate.date }} </span>
<span class="expiracyDate"> Expire le {{ config.currentPoll.poll.expiracyDate.date }} </span>
<div class="votants">
<i class="fa fa-users"></i>
<i class="fa fa-users" aria-hidden="true"></i>
{{ config.currentPoll.stacks.length }} votants, {{ config.currentPoll.choices.length }} choix,
</div>
</div>
</div>
<div class="preferred">
<i class="fa fa-star"></i>
<i class="fa fa-star" aria-hidden="true"></i>
Pour l'instant,
<span *ngIf="severalPreferred">
les
@ -47,7 +47,7 @@
</thead>
<tbody>
<tr title='somme des points, dont un demi point pour les "peut être"'>
<td><i class="fa fa-plus-circle"></i> points</td>
<td><i class="fa fa-plus-circle" aria-hidden="true"></i> points</td>
<!-- <td-->
<!-- *ngFor='let choice of config.currentPoll.choices'-->
<!-- [ngClass]='{"has-max-score" : config.currentPoll.choices_count.maxScore === config.currentPoll.choices_count.counts[choice.id].score}' >-->
@ -59,7 +59,7 @@
</tr>
<tr class="details">
<td>
<i class="fa fa-eye"></i>
<i class="fa fa-eye" aria-hidden="true"></i>
</td>
<td *ngFor="let choice of config.currentPoll.choices">
id: {{ choice.id }}
@ -85,7 +85,7 @@
*ngIf="config.currentPoll.poll.modificationPolicy === 'everybody'"
class="btn btn--primary pull-left btn--small"
>
<i class="fa fa-edit"></i>
<i class="fa fa-edit" aria-hidden="true"></i>
</button>
{{ voteStack.pseudo }}
@ -94,12 +94,12 @@
<span *ngIf="voteStack.votes[v].value">
<img
*ngIf="voteStack.votes[v].value == 'yes'"
src="../../../../assets/img/votant-sur.svg"
src="../../../../assets/img/icon_voter_YES.svg"
alt="yes"
/>
<img
*ngIf="voteStack.votes[v].value == 'maybe'"
src="../../../../assets/img/votant-pas-sur.svg"
src="../../../../assets/img/icon_voter_MAYBE.svg"
alt="yes"
/>
</span>
@ -110,15 +110,9 @@
</table>
</div>
<button
*ngIf="config.isAdmin"
type="button"
(click)="toggleModalDialogVisibility()"
pButton
icon="pi pi-external-link"
label="Show"
>
<button *ngIf="config.isAdmin" type="button" (click)="toggleModalDialogVisibility()" pButton label="Show">
show admin confirmation modal
<i class="fa fa-external-link" aria-hidden="true"></i>
</button>
<p-dialog
[visible]="displayConfirmVoteModalAdmin"

View File

@ -1,6 +1,6 @@
import { Component, Input, OnInit } from '@angular/core';
import { ConfigService } from '../../../services/config.service';
import { mockPoll3 } from '../../../../../mocks/mock-poll3';
import { mockPoll3 } from '../../../mocks/mock-poll3';
@Component({
selector: 'app-voting-summary',

View File

@ -6,9 +6,9 @@ import { ConfirmationService, MessageService } from 'primeng/api';
import { environment } from '../../../../environments/environment';
import { PollConfig } from '../config/PollConfig';
import { PollUtilities } from '../config/PollUtilities';
import { mockPoll3 } from '../../../mocks/mock-poll3';
import { mockSuccessVote } from '../../../mocks/mock-success-vote';
import { mockMyPolls } from '../../../mocks/mockmypolls';
import { mockPoll3 } from '../mocks/mock-poll3';
import { mockSuccessVote } from '../mocks/mock-success-vote';
import { mockMyPolls } from '../mocks/mockmypolls';
/**
* le service transverse à chaque page qui permet de syncroniser la configuration de sondage souhaitée

View File

@ -1,5 +1,5 @@
<button (click)="copy()" class="btn btn--primary btn--outline" id="copyLink">
<i class="fa fa-copy"></i>
<i class="fa fa-copy" aria-hidden="true"></i>
{{ 'admin.copy_link' | translate }}
<span *ngIf="displayContentToCopy"> " {{ textToCopy }}" </span>
</button>

View File

@ -1,3 +1,3 @@
<button class="erase btn btn--warning" *ngIf="inputModel.length" (click)="eraseInput()">
<i class="fa fa-times"></i>
<i class="fa fa-times" aria-hidden="true"></i>
</button>

View File

@ -0,0 +1,41 @@
<div class="box">
<div class="columns is-vcentered">
<div class="column">
<label class="label">{{ choice.label }}</label>
</div>
<div class="column">
<div class="buttons has-addons is-centered">
<button
class="button"
[ngClass]="{ 'is-selected is-success': response == 'YES' }"
(click)="vote('YES')"
>
YES
</button>
<button
class="button"
[ngClass]="{ 'is-selected is-warning': response == 'MAYBE' }"
(click)="vote('MAYBE')"
*ngIf="poll.configuration.isMaybeAnswerAvailable"
>
MAYBE
</button>
<button class="button" [ngClass]="{ 'is-selected is-danger': response == 'NO' }" (click)="vote('NO')">
NO
</button>
</div>
</div>
<div class="column">
<div class="buttons has-addons is-right">
<button class="button is-white">
{{ choice.counts.get(responseTypeEnum.YES) }}
<img class="image is-24x24" src="../../../assets/img/icon_voter_YES.svg" />
</button>
<button class="button is-white" *ngIf="poll.configuration.isMaybeAnswerAvailable">
{{ choice.counts.get(responseTypeEnum.MAYBE) }}
<img class="image is-24x24" src="../../../assets/img/icon_voter_MAYBE.svg" />
</button>
</div>
</div>
</div>
</div>

View File

@ -1,19 +1,19 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PollPageComponent } from './poll-page.component';
import { AddAnswerComponent } from './add-answer.component';
describe('PollPageComponent', () => {
let component: PollPageComponent;
let fixture: ComponentFixture<PollPageComponent>;
describe('AddVoteComponent', () => {
let component: AddAnswerComponent;
let fixture: ComponentFixture<AddAnswerComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [PollPageComponent],
declarations: [AddAnswerComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PollPageComponent);
fixture = TestBed.createComponent(AddAnswerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -0,0 +1,30 @@
import { Component, Input, OnInit } from '@angular/core';
import { ResponseType } from 'src/app/core/enums/response-type.enum';
import { Choice } from '../../../core/models/choice.model';
import { Poll } from '../../../core/models/poll.model';
import { User } from '../../../core/models/user.model';
import { PollService } from '../../../core/services/poll.service';
@Component({
selector: 'app-add-answer',
templateUrl: './add-answer.component.html',
styleUrls: ['./add-answer.component.scss'],
})
export class AddAnswerComponent implements OnInit {
@Input() user: User;
@Input() poll: Poll;
@Input() choice: Choice;
public responseTypeEnum = ResponseType;
public response: ResponseType;
constructor(private pollService: PollService) {}
ngOnInit(): void {}
public vote(response: string): void {
this.response = response as ResponseType;
console.log(this.response);
this.pollService.saveParticipation(this.choice, this.user, this.response);
}
}

View File

@ -0,0 +1,26 @@
<article class="message is-primary is-small">
<div class="message-header">
<p>{{ pseudo }}</p>
</div>
<div class="message-body">
<form>
<div class="field">
<div class="control">
<textarea
class="textarea is-primary is-fullwidth"
name="comment"
placeholder="Je commente"
[(ngModel)]="newComment"
></textarea>
</div>
</div>
<div class="field">
<div class="control">
<button type="submit" class="button is-primary is-fullwidth" (click)="saveComment()">
Enregistrer
</button>
</div>
</div>
</form>
</div>
</article>

View File

@ -1,19 +1,19 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PollCommentComponent } from './poll-comment.component';
import { AddCommentComponent } from './add-comment.component';
describe('PollCommentComponent', () => {
let component: PollCommentComponent;
let fixture: ComponentFixture<PollCommentComponent>;
describe('AddCommentComponent', () => {
let component: AddCommentComponent;
let fixture: ComponentFixture<AddCommentComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [PollCommentComponent],
declarations: [AddCommentComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PollCommentComponent);
fixture = TestBed.createComponent(AddCommentComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -0,0 +1,21 @@
import { Component, Input, OnInit } from '@angular/core';
import { PollService } from '../../../core/services/poll.service';
@Component({
selector: 'app-add-comment',
templateUrl: './add-comment.component.html',
styleUrls: ['./add-comment.component.scss'],
})
export class AddCommentComponent implements OnInit {
@Input() pseudo: string;
public newComment: string;
constructor(private pollService: PollService) {}
ngOnInit(): void {}
public saveComment(): void {
this.pollService.addComment(this.newComment);
}
}

View File

@ -0,0 +1 @@
<app-add-answer *ngFor="let choice of poll.choices" [user]="user" [poll]="poll" [choice]="choice"></app-add-answer>

View File

@ -1,19 +1,19 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PollComponent } from './poll.component';
import { AnswersComponent } from './answers.component';
describe('PollComponent', () => {
let component: PollComponent;
let fixture: ComponentFixture<PollComponent>;
describe('AddVotesComponent', () => {
let component: AnswersComponent;
let fixture: ComponentFixture<AnswersComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [PollComponent],
declarations: [AnswersComponent],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PollComponent);
fixture = TestBed.createComponent(AnswersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -0,0 +1,18 @@
import { Component, Input, OnInit } from '@angular/core';
import { Poll } from '../../../core/models/poll.model';
import { User } from '../../../core/models/user.model';
@Component({
selector: 'app-answers',
templateUrl: './answers.component.html',
styleUrls: ['./answers.component.scss'],
})
export class AnswersComponent implements OnInit {
@Input() poll: Poll;
@Input() user: User;
constructor() {}
ngOnInit(): void {}
}

View File

@ -4,8 +4,6 @@ import { RouterModule, Routes } from '@angular/router';
import { ParticipationComponent } from './participation.component';
const routes: Routes = [
{ path: '', redirectTo: 'poll', pathMatch: 'full' },
{ path: 'poll', component: ParticipationComponent },
{ path: ':slug', redirectTo: 'poll/:slug', pathMatch: 'full' },
{ path: 'poll/:slug', component: ParticipationComponent },
];

View File

@ -1,11 +1,11 @@
<div class="p-grid">
<div class="p-col has-text-centered">
<div class="columns">
<div class="column has-text-centered">
<h1>Participation</h1>
</div>
</div>
<div *ngIf="_isLoading | async" class="p-grid p-justify-center">
<div class="p-col has-text-centered">
<div *ngIf="_isLoading | async" class="columns p-justify-center">
<div class="column has-text-centered">
<p-progressSpinner></p-progressSpinner>
</div>
</div>
@ -16,14 +16,19 @@
</ng-container>
<ng-container *ngIf="_poll | async">
<div class="p-grid">
<div class="p-col">
<app-poll></app-poll>
<div class="columns">
<div class="column">
<app-answers [poll]="_poll | async" [user]="_user | async"></app-answers>
</div>
</div>
<div class="p-grid">
<div class="p-col">
<app-poll-comment></app-poll-comment>
<div class="columns">
<div class="column">
<app-add-comment [pseudo]="(_user | async)?.pseudo"></app-add-comment>
</div>
</div>
<div class="columns">
<div class="column">
<app-comments></app-comments>
</div>
</div>
</ng-container>

View File

@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Poll } from '../../core/models/poll.model';
import { User } from '../../core/models/user.model';
import { LoaderService } from '../../core/services/loader.service';
import { ModalService } from '../../core/services/modal.service';
import { PollService } from '../../core/services/poll.service';
@ -16,6 +17,7 @@ import { UserService } from '../../core/services/user.service';
export class ParticipationComponent implements OnInit {
public _isLoading: Observable<boolean> = this.loaderService.isLoading;
public _poll: Observable<Poll> = this.pollService.poll;
public _user: Observable<User> = this.userService.user;
constructor(
private urlService: UrlService,

View File

@ -3,13 +3,14 @@ import { NgModule } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { SharedModule } from '../../shared/shared.module';
import { AddAnswerComponent } from './add-answer/add-answer.component';
import { AddCommentComponent } from './add-comment/add-comment.component';
import { AnswersComponent } from './answers/answers.component';
import { ParticipationRoutingModule } from './participation-routing.module';
import { ParticipationComponent } from './participation.component';
import { PollComponent } from './poll/poll.component';
import { PollCommentComponent } from './poll-comment/poll-comment.component';
@NgModule({
declarations: [ParticipationComponent, PollComponent, PollCommentComponent],
declarations: [ParticipationComponent, AddCommentComponent, AnswersComponent, AddAnswerComponent],
imports: [CommonModule, ParticipationRoutingModule, SharedModule, TranslateModule.forChild({ extend: true })],
exports: [],
})

View File

@ -1 +0,0 @@
<p>poll-comment works!</p>

Some files were not shown because too many files have changed in this diff Show More