Merge branch 'feature/user_settings' into 'develop'

create services, add user settings button & modal, and other things

See merge request framasoft/framadate/funky-framadate-front!37
archived-develop
seraph_ino 3 years ago
commit 789a1e7535

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

@ -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:

@ -1,27 +1,29 @@
## 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) |
| | [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 |
| | [husky](https://www.npmjs.com/package/husky) | Hook actions on commit |
| | [jest](https://jestjs.io/) | test engine |
| removed | [locale-enum](https://www.npmjs.com/package/locale-enum) | enum of all locales |
| | [momentJS](https://momentjs.com/) | manipulate dates. (chartJSโ€™s dependency) |
| to be installed | [ng2-charts](https://valor-software.com/ng2-charts/) | Manipulate graphs along with chart.js |
| | [ngx-clipboard](https://www.npmjs.com/package/ngx-clipboard) | Handle clipboard |
| | [ngx-markdown](https://www.npmjs.com/package/ngx-markdown) | markdown parser |
| | [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. |
| 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 |
| status | lib name | usage |
| :-------------: | -------------------------------------------------------------- | --------------------------------------------------------- |
| | [axios](https://github.com/axios/axios) | http client |
| | [bulma](https://bulma.io/) | CSS framework |
| | [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) | 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. (chartJSโ€™s dependency) |
| to be installed | [ng2-charts](https://valor-software.com/ng2-charts/) | Manipulate graphs along with chart.js |
| | [ngx-clipboard](https://www.npmjs.com/package/ngx-clipboard) | Handle clipboard |
| | [ngx-markdown](https://www.npmjs.com/package/ngx-markdown) | markdown parser |
| | [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. |
| 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.

@ -25,9 +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/bulma-switch/dist/css/bulma-switch.min.css",
"src/styles.scss"
],
"scripts": [

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

@ -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,19 +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",
"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"
},
@ -63,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",
"babel-jest": "^25.4.0",
"eslint": "^6.8.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",
"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": "^25.5.1",
"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"
@ -89,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",

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

@ -2,17 +2,19 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './core/components/home/home.component';
import { LoginComponent } from './core/components/login/login.component';
import { PageNotFoundComponent } from './shared/components/page-not-found/page-not-found.component';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'login', component: LoginComponent },
{
path: 'administration',
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),

@ -5,6 +5,8 @@ import { Subscription } from 'rxjs';
import { environment } from '../environments/environment';
import { Theme } from './core/enums/theme.enum';
import { UserRole } from './core/enums/user-role.enum';
import { User } from './core/models/user.model';
import { LanguageService } from './core/services/language.service';
import { MockingService } from './core/services/mocking.service';
import { ThemeService } from './core/services/theme.service';
@ -22,15 +24,18 @@ export class AppComponent implements OnInit, OnDestroy {
constructor(
private titleService: Title,
private themeService: ThemeService,
private languageService: LanguageService,
private mockingService: MockingService
) {}
ngOnInit(): void {
if (!environment.production) {
this.appTitle += ' [DEV]';
this.mockingService.loadUser(UserRole.REGISTERED);
// TODO: to be removed
this.mockingService.loadUser(new User('TOTO', 'toto@gafam.com', UserRole.REGISTERED));
}
this.titleService.setTitle(this.appTitle);
this.languageService.configureAndInitTranslations();
this.themeSubscription = this.themeService.theme.subscribe((theme: Theme) => {
switch (theme) {
case Theme.DARK:

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

@ -37,35 +37,43 @@
<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>
<a class="navbar-item" routerLink="oldstuff" routerLinkActive="is-active">
Old stuff
</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"
*ngFor="let slug of slugsAvailables"
routerLink="{{ '/consultation/poll/' + slug }}"
routerLinkActive="is-active"
>
ยซ {{ slug }} ยป
</a>
</div>
</div>
</div>
<div class="navbar-item">
<app-theme-selector></app-theme-selector>
</div>
<div class="navbar-item">
<app-language-selector></app-language-selector>
</div>
<div class="navbar-end">
<div class="navbar-item">
<div class="buttons">
<a class="button is-primary">
<strong>Sign up</strong>
</a>
<a class="button is-light">
Log in
</a>
<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" 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" aria-hidden="true"></i></button>
</div>
</div>
</div>

@ -1,14 +1,38 @@
import { Component, EventEmitter, Output, Input } 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';
@Component({
selector: 'app-header',
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;
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();
}
public toggleSidebarOpening(): void {
this.isSidebarOpened = !this.isSidebarOpened;
this.toggleSidebarEE.emit(this.isSidebarOpened);

@ -1,12 +0,0 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
})
export class LoginComponent implements OnInit {
constructor() {}
ngOnInit(): void {}
}

@ -1,10 +0,0 @@
<div class="control has-icons-left">
<select class="select is-small" [(ngModel)]="currentLang">
<ng-container *ngFor="let language of languagesAvailable">
<option value="{{ language }}">{{ 'LANGUAGES.' + language | translate }}</option>
</ng-container>
</select>
<div class="icon is-left">
<i class="fa fa-globe"></i>
</div>
</div>

@ -1,38 +0,0 @@
import { Component, DoCheck, OnInit } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Language } from '../../../enums/language.enum';
import { StorageService } from '../../../services/storage.service';
@Component({
selector: 'app-language-selector',
templateUrl: './language-selector.component.html',
styleUrls: ['./language-selector.component.scss'],
})
export class LanguageSelectorComponent implements OnInit, DoCheck {
public currentLang: Language;
public languagesAvailable: string[] = Object.values(Language);
constructor(private translate: TranslateService, private storageService: StorageService) {}
ngOnInit(): void {
const currentBrowserLanguage: Language = this.translate.getBrowserLang().toUpperCase() as Language;
if (this.storageService.language && Object.keys(Language).includes(this.storageService.language)) {
this.currentLang = this.storageService.language;
} else if (Object.keys(Language).includes(currentBrowserLanguage)) {
this.currentLang = currentBrowserLanguage;
} else {
this.currentLang = Language.EN;
}
this.updateLanguage();
}
ngDoCheck(): void {
this.updateLanguage();
}
public updateLanguage(): void {
this.translate.use(this.currentLang.toString().toUpperCase());
this.storageService.language = this.currentLang;
}
}

@ -1,24 +0,0 @@
<div class="buttons has-addons is-centered">
<button class="button is-small is-static">Theme</button>
<button
class="button is-small"
[ngClass]="{ 'is-active': (currentTheme | async) === themeEnum.LIGHT }"
(click)="selectTheme('LIGHT')"
>
<i class="fa fa-sun-o"></i>
</button>
<button
class="button is-small"
[ngClass]="{ 'is-active': (currentTheme | async) === themeEnum.DARK }"
(click)="selectTheme('DARK')"
>
<i class="fa fa-moon"></i>
</button>
<button
class="button is-small"
[ngClass]="{ 'is-active': (currentTheme | async) === themeEnum.RED }"
(click)="selectTheme('RED')"
>
<i class="fa fa-adjust"></i>
</button>
</div>

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

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

@ -7,26 +7,14 @@ import { TranslateModule } from '@ngx-translate/core';
import { FooterComponent } from './components/footer/footer.component';
import { HeaderComponent } from './components/header/header.component';
import { HomeComponent } from './components/home/home.component';
import { LoginComponent } from './components/login/login.component';
import { LogoComponent } from './components/logo/logo.component';
import { LanguageSelectorComponent } from './components/selectors/language-selector/language-selector.component';
import { ThemeSelectorComponent } from './components/selectors/theme-selector/theme-selector.component';
import { NavigationComponent } from './components/sibebar/navigation/navigation.component';
import { throwIfAlreadyLoaded } from './guards/module-import.guard';
@NgModule({
declarations: [
FooterComponent,
HeaderComponent,
HomeComponent,
LanguageSelectorComponent,
LoginComponent,
LogoComponent,
NavigationComponent,
ThemeSelectorComponent,
],
declarations: [FooterComponent, HeaderComponent, HomeComponent, LogoComponent, NavigationComponent],
imports: [CommonModule, FormsModule, RouterModule, TranslateModule],
exports: [HeaderComponent, FooterComponent, NavigationComponent, LoginComponent, LogoComponent],
exports: [HeaderComponent, FooterComponent, NavigationComponent, LogoComponent],
})
export class CoreModule {
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {

@ -1,4 +0,0 @@
export enum AnswerGranularity {
BASIC = 'BASIC',
COMPLEX = 'COMPLEX',
}

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

@ -1,5 +1,5 @@
export enum UserRole {
ANONYMOUS = 'ANONYMOUS',
REGISTERED = 'ADMIN',
REGISTERED = 'REGISTERED',
ADMIN = 'ADMIN',
}

@ -1,5 +1,5 @@
export enum WorkflowStep {
DESCRIPTION = 'DESCRIPTION',
OPTIONS = 'OPTIONS',
CHOICES = 'CHOICES',
CONFIGURATION = 'CONFIGURATION',
}

@ -1,5 +0,0 @@
export interface DateOption {
timeList: any;
literal: string;
date_object?: object;
}

@ -1,6 +0,0 @@
import { AnswerType } from '../enums/answer-type.enum';
import { PollOption } from './poll-options.model';
export class Answer {
constructor(public pollOption: PollOption, public type: AnswerType, public userPseudo: string) {}
}

@ -0,0 +1,38 @@
import { ResponseType } from '../enums/response-type.enum';
export class Choice {
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);
}
}
}

@ -0,0 +1,13 @@
export class Comment {
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;
}
}

@ -0,0 +1,24 @@
import { environment } from '../../../environments/environment';
import { DateUtilsService } from '../utils/date-utils.service';
export class Configuration {
constructor(
public isAboutDate: boolean = false,
public isProtectedByPassword: boolean = false,
public isOwnerNotifiedByEmail: { onNewVote: boolean; onNewComment: boolean } = {
onNewVote: false,
onNewComment: false,
},
public isMaybeAnswerAvailable: boolean = false,
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 static isArchived(configuration: Configuration): boolean {
return DateUtilsService.isDateInPast(configuration.expires);
}
}

@ -1,13 +0,0 @@
import { AnswerGranularity } from '../enums/answer-granularity.enum';
export class PollConfig {
constructor(
public allowSeveralHours = true,
public isVisibleToAnyoneWithTheLink: boolean = true,
public answerType: AnswerGranularity = AnswerGranularity.BASIC,
public creationDate: Date = new Date(),
public expirationDate?: Date,
public canVotersModifyTheirAnswers = true,
public isProtectedByPassword: boolean = false
) {}
}

@ -1,9 +0,0 @@
import * as moment from 'moment';
export class PollOption {
constructor(public label: string, public url?: string, public subOptions?: PollOption[]) {}
public isDatePoll(): boolean {
return moment(this.label).isValid();
}
}

@ -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,23 +1,89 @@
import { Answer } from './answer.model';
import { PollConfig } from './poll-config.model';
import { PollOption } from './poll-options.model';
import { User } from './user.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 { User } from './user.model';
export class Poll {
constructor(
public isDateType: boolean,
public title: string,
public description: string,
public slug: string,
public id: string,
public owner?: User,
public config?: PollConfig,
public options: PollOption[] = [],
public answers: Answer[] = []
public owner: User,
public question: string,
public description?: string,
public slug: string = uuidv4(),
public configuration: Configuration = new Configuration(),
public comments: Comment[] = [],
public choices: Choice[] = [],
public answersByChoiceByParticipant: Map<string, Map<string, ResponseType>> = new Map<
string,
Map<string, ResponseType>
>(),
public answers: PollUserAnswers[] = []
) {}
public getUrl(): string {
return `${environment.api.baseHref}/${this.slug}`;
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;
}
}

@ -0,0 +1,3 @@
export class Question {
constructor(public label: string, public description: string) {}
}

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

@ -2,55 +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.voteStack.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}`, { params: { config: poll.config } });
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.id}${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.id}${this.commentsEndpoint}`, {
params: { comment },
});
return await this.axiosInstance.post(`${this.pollsEndpoint}/${slug}${this.commentsEndpoint}`, comment);
} catch (error) {
this.handleError(error);
}
@ -59,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(
@ -77,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) {
@ -101,41 +148,20 @@ export class ApiService {
}
}
public async getPollByIdentifier(identifier: string): Promise<Poll | undefined> {
// TODO: identifier should be decided according to backend : Id || Slug ?
try {
const response: AxiosResponse<Poll> = await this.axiosInstance.get<Poll>(
`${this.pollsEndpoint}/${identifier}`
);
return response?.data;
} catch (error) {
if (error.response?.status === 404) {
return undefined;
} else {
this.handleError(error);
}
}
}
////////////
// UPDATE //
////////////
public async updatePoll(poll: Poll): Promise<void> {
try {
// TODO: implement the params when entities are finalized.
await this.axiosInstance.put(`${this.pollsEndpoint}/${poll.id}`, {
params: { voteStack: {}, token: '' },
});
} catch (error) {
this.handleError(error);
}
}
public async updateVote(voteStack: any): 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.patch(`${this.votesStacksEndpoint}/${voteStack.id}`, {
params: { voteStack: {}, token: '' },
return await this.axiosInstance.patch(`${this.pollsEndpoint}/${slug}${this.answersEndpoint}`, {
choiceLabel,