Browse Source

Merge branch 'creation-form' into 'develop'

Creation form

See merge request framasoft/framadate/funky-framadate-front!46
develop
ty kayn 2 years ago
parent
commit
949a945bbc
  1. 4
      README.md
  2. 3
      angular.json
  3. 63
      doc/cadrage/backend-api-endpoints-doc.txt
  4. 5
      index.lokalize
  5. 14
      lokalize-scripts/scripts.rc
  6. 4
      main.lqa
  7. 9
      package.json
  8. 3
      src/app/app.module.ts
  9. 8
      src/app/core/components/footer/footer.component.html
  10. 4
      src/app/core/components/footer/footer.component.scss
  11. 2
      src/app/core/components/footer/footer.component.ts
  12. 18
      src/app/core/components/header/header.component.html
  13. 3
      src/app/core/components/header/header.component.scss
  14. 3
      src/app/core/components/header/header.component.ts
  15. 56
      src/app/core/components/home/home.component.html
  16. 6
      src/app/core/components/home/home.component.scss
  17. 9
      src/app/core/components/home/home.component.ts
  18. 47
      src/app/core/components/sibebar/navigation/navigation.component.html
  19. 2
      src/app/core/components/sibebar/navigation/navigation.component.ts
  20. 3
      src/app/core/core.module.ts
  21. 15
      src/app/core/enums/language.enum.ts
  22. 6
      src/app/core/models/configuration.model.ts
  23. 14
      src/app/core/models/poll.model.ts
  24. 4
      src/app/core/models/user.model.ts
  25. 113
      src/app/core/services/api.service.ts
  26. 6
      src/app/core/services/language.service.ts
  27. 120
      src/app/core/services/poll.service.ts
  28. 10
      src/app/core/services/uuid.service.ts
  29. 9
      src/app/features/administration/administration.component.html
  30. 4
      src/app/features/administration/administration.component.ts
  31. 2
      src/app/features/administration/administration.module.ts
  32. 390
      src/app/features/administration/form/form.component.html
  33. 79
      src/app/features/administration/form/form.component.scss
  34. 358
      src/app/features/administration/form/form.component.ts
  35. 22
      src/app/features/old-stuff/config/DateUtilities.ts
  36. 2
      src/app/features/old-stuff/custom-lib/date-value-accessor/date-value-accessor.ts
  37. 4
      src/app/features/old-stuff/pages/dates/dates.component.ts
  38. 6
      src/app/features/old-stuff/pages/example/kind/kind.component.html
  39. 2
      src/app/features/old-stuff/pages/home/home.component.html
  40. 2
      src/app/features/old-stuff/pages/voting/voting-choice/voting-choice.component.ts
  41. 2
      src/app/features/old-stuff/services/config.service.ts
  42. 5
      src/app/features/user-profile/user-polls/user-polls.component.html
  43. 21
      src/app/shared/components/selectors/language-selector/language-selector.component.html
  44. 4
      src/app/shared/components/selectors/language-selector/language-selector.component.ts
  45. 3
      src/app/shared/components/selectors/theme-selector/theme-selector.component.html
  46. 5
      src/app/shared/components/ui/copy-text/copy-text.component.html
  47. 0
      src/app/shared/components/ui/copy-text/copy-text.component.scss
  48. 24
      src/app/shared/components/ui/copy-text/copy-text.component.spec.ts
  49. 23
      src/app/shared/components/ui/copy-text/copy-text.component.ts
  50. 3
      src/app/shared/components/ui/erasable-input/erasable-input.component.html
  51. 0
      src/app/shared/components/ui/erasable-input/erasable-input.component.scss
  52. 24
      src/app/shared/components/ui/erasable-input/erasable-input.component.spec.ts
  53. 22
      src/app/shared/components/ui/erasable-input/erasable-input.component.ts
  54. 4
      src/app/shared/shared.module.ts
  55. 454
      src/assets/i18n/EN.json
  56. 453
      src/assets/i18n/FR.json
  57. 1930
      src/assets/i18n/po/br.po
  58. 1951
      src/assets/i18n/po/ca.po
  59. 427
      src/assets/i18n/po/converted/br.json
  60. 427
      src/assets/i18n/po/converted/ca.json
  61. 427
      src/assets/i18n/po/converted/de.json
  62. 427
      src/assets/i18n/po/converted/el.json
  63. 428
      src/assets/i18n/po/converted/en.json
  64. 427
      src/assets/i18n/po/converted/es.json
  65. 428
      src/assets/i18n/po/converted/fr.json
  66. 427
      src/assets/i18n/po/converted/gl.json
  67. 427
      src/assets/i18n/po/converted/hu.json
  68. 427
      src/assets/i18n/po/converted/it.json
  69. 427
      src/assets/i18n/po/converted/nl.json
  70. 427
      src/assets/i18n/po/converted/oc.json
  71. 427
      src/assets/i18n/po/converted/sv.json
  72. 1971
      src/assets/i18n/po/de.po
  73. 1931
      src/assets/i18n/po/el.po
  74. 1932
      src/assets/i18n/po/en.po
  75. 1967
      src/assets/i18n/po/es.po
  76. 1981
      src/assets/i18n/po/fr.po
  77. 1804
      src/assets/i18n/po/framadate.pot
  78. 1907
      src/assets/i18n/po/gl.po
  79. 1934
      src/assets/i18n/po/hu.po
  80. 1958
      src/assets/i18n/po/it.po
  81. 1922
      src/assets/i18n/po/nl.po
  82. 1958
      src/assets/i18n/po/oc.po
  83. 1896
      src/assets/i18n/po/sv.po
  84. BIN
      src/assets/img/kind/classic.jpeg
  85. BIN
      src/assets/img/kind/date.jpeg
  86. 1
      src/assets/img/undraw_Chatting_re_j55r.svg
  87. 1
      src/assets/img/undraw_Moving_twwf.svg
  88. 1
      src/assets/img/undraw_group_selfie_ijc6.svg
  89. 1
      src/assets/img/undraw_having_fun_iais.svg
  90. 1
      src/assets/img/undraw_prototyping_process_rswj.svg
  91. 15
      src/environments/environment.ts
  92. 7
      src/proxy.conf.json
  93. 4
      src/styles/dev-utilities/_helpers.scss
  94. 10
      src/styles/partials/_links.scss
  95. 6
      src/styles/partials/_navigation.scss
  96. 5
      src/styles/variables.scss
  97. 159
      yarn.lock

4
README.md

@ -39,7 +39,7 @@ EN: All documentation is available in the "doc" folder, mainly in French because
## LIBRARIES USED
| status | lib name | usage |
| status | lib choice_label | usage |
| :-------------: | -------------------------------------------------------------- | --------------------------------------------------------- |
| | [axios](https://github.com/axios/axios) | http client |
| | [bulma](https://bulma.io/) | CSS framework |
@ -71,7 +71,7 @@ This project was generated with [Angular CLI](https://github.com/angular/angular
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
Run `ng generate component component-choice_label` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
## Build

3
angular.json

@ -73,7 +73,8 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "framadate:build"
"browserTarget": "framadate:build",
"proxyConfig": "src/proxy.conf.json"
},
"configurations": {
"production": {

63
doc/cadrage/backend-api-endpoints-doc.txt

@ -1,32 +1,41 @@
// TODO: File to be deleted : just temporary documentation of backend API endpoints
/**
*
* -------------------------- -------- -------- ------ ------------------------------------------------
Name Method Scheme Host Path
-------------------------- -------- -------- ------ ------------------------------------------------
_twig_error_test ANY ANY ANY /_error/{code}.{_format}
admin_homepage_get_default GET ANY ANY /admin/
admin_homepage_clean_expired_polls GET ANY ANY /admin/polls/clean/{token}
api_get_poll_comment GET ANY ANY /polls/{id}/comments
api_new_comment POST ANY ANY /polls/{id}/comments
api_poll_comments_delete DELETE ANY ANY /polls/{id}/comments
user_homepageget_default GET ANY ANY /users/
user_homepage_polls_send_by_email GET ANY ANY /users/{email}/polls/send-by-email
api_get_all_polls GET ANY ANY /polls/
api_get_poll GET ANY ANY /polls/{id}
api_update_poll PUT ANY ANY /polls/{id}/{token}
api_new_poll POST ANY ANY /polls/
api_test-mail-polls GET ANY ANY /polls/mail/test-mail-polls/{emailChoice}
api_poll_delete DELETE ANY ANY /polls/{id}
api_check_slug_is_unique GET ANY ANY /polls/slugs/{slug}
api_get_admin_config GET ANY ANY /polls/admin/{token}
api_new_vote_stack POST ANY ANY /polls/{id}/votes
api_update_vote_stack PATCH ANY ANY /votes-stacks/{id}/token/{modifierToken}
api_poll_votes_delete DELETE ANY ANY /polls/{id}/votes/{accessToken}
app.swagger GET ANY ANY /api/doc.json
-------------------------- -------- -------- ------ ------------------------------------------------
*/
------------------------------------------ ---------- -------- ------ ------------------------------------------------
Name Method Scheme Host Path
------------------------------------------ ---------- -------- ------ ------------------------------------------------
_twig_error_test ANY ANY ANY /_error/{code}.{_format}
admin_homepage_get_default GET ANY ANY /admin/
admin_homepage_clean_expired_polls GET ANY ANY /admin/polls/clean/{token}
admin_homepage_migrate_framadate GET ANY ANY /admin/polls/migrate
api_get_poll_comment GET ANY ANY /api/v1/poll/{id}/comments
api_new_comment POST ANY ANY /api/v1/poll/{id}/comment
api_poll_comments_delete DELETE ANY ANY /api/v1/poll/{id}/comments
api_page_home GET ANY ANY /page/
user_homepageget_default GET ANY ANY /user/
user_homepage_polls_send_by_email GET ANY ANY /user/{email}/polls/send-by-email
poll_index GET ANY ANY /poll/
poll_new GET|POST ANY ANY /poll/new
poll_show GET ANY ANY /poll/{id}
poll_edit GET|POST ANY ANY /poll/{id}/edit
poll_delete DELETE ANY ANY /poll/{id}
api_new_vote_stack POST ANY ANY /api/v1/poll/{id}/answer
api_update_vote_stack PATCH ANY ANY /api/v1/vote-stack/{id}/token/{modifierToken}
api_poll_votes_delete DELETE ANY ANY /api/v1/poll/{id}/votes/{accessToken}
api_get_all_polls GET ANY ANY /api/v1/poll/
api_get_poll GET ANY ANY /api/v1/poll/{id}
api_update_poll PUT ANY ANY /api/v1/poll/{id}/{token}
api_new_poll POST ANY ANY /api/v1/poll/
api_test-mail-poll GET ANY ANY /api/v1/poll/mail/test-mail-poll/{emailChoice}
api_poll_delete DELETE ANY ANY /api/v1/poll/{id}
api_check_slug_is_unique GET ANY ANY /api/v1/poll/slug/{slug}
api_get_admin_config GET ANY ANY /api/v1/poll/admin/{token}
overblog_graphql_endpoint ANY ANY ANY /api/graphql/
overblog_graphql_batch_endpoint ANY ANY ANY /api/graphql/batch
overblog_graphql_multiple_endpoint ANY ANY ANY /api/graphql/graphql/{schemaName}
overblog_graphql_batch_multiple_endpoint ANY ANY ANY /api/graphql/graphql/{schemaName}/batch
app.swagger GET ANY ANY /api/doc.json
------------------------------------------ ---------- -------- ------ ------------------------------------------------
/**
* WANTED CHANGES (seraf)

5
index.lokalize

@ -0,0 +1,5 @@
[General]
LangCode=fr_FR
PotBaseDir=src/assets/i18n
ProjectID=funky-framadate-front
TargetLangCode=fr_FR

14
lokalize-scripts/scripts.rc

@ -0,0 +1,14 @@
<!--
Collection name attribute represents the name of the menu, e.g., to use menu "File" use "file" or "Help" use "help". You can add new menus.
If you type a relative script file beware that this script is located in $XDG_DATA_HOME/applicationname/
The following example adds an action with the text "Export..." into the "File" menu
<KrossScripting>
<collection name="file" text="File" comment="File menu">
<script name="export" text="Export..." comment="Export content" file="export.py" />
</collection>
</KrossScripting>
-->

4
main.lqa

@ -0,0 +1,4 @@
<?xml version='1.0' encoding='UTF-8'?>
<qa version="1.0">
<category name="default"/>
</qa>

9
package.json

@ -20,8 +20,10 @@
"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\""
},
"start:proxymock": "concurrently --kill-others \"yarn mock:server\" \"yarn start:proxy\"",
"i18n:init": "ngx-translate-extract --input ./src --output ./src/assets/i18n/template.json --key-as-default-value --replace --format json",
"i18n:extract": "ngx-translate-extract --input ./src --output ./src/assets/i18n/{en,da,de,fi,nb,nl,sv}.json --clean --format json"
},
"private": false,
"dependencies": {
"@angular/animations": "^9.1.1",
@ -35,6 +37,8 @@
"@angular/platform-browser": "^9.0.7",
"@angular/platform-browser-dynamic": "^9.0.7",
"@angular/router": "^9.0.7",
"@biesbjerg/ngx-translate-extract": "^7.0.3",
"@biesbjerg/ngx-translate-po-http-loader": "^3.1.0",
"@fullcalendar/core": "^4.4.0",
"@ngx-translate/core": "^12.1.2",
"@ngx-translate/http-loader": "^5.0.0",
@ -53,6 +57,7 @@
"rxjs": "^6.5.5",
"rxjs-compat": "^6.5.5",
"short-unique-id": "^3.0.3",
"stream": "^0.0.2",
"tslib": "<2.0.0",
"zone.js": "^0.10.3"
},

3
src/app/app.module.ts

@ -52,6 +52,7 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
NgxWebstorageModule.forRoot({ prefix: environment.localStorage.key }),
SharedModule,
TranslateModule.forRoot({
defaultLanguage: 'FR',
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
@ -61,7 +62,7 @@ export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
provide: MissingTranslationHandler,
useClass: MyMissingTranslationHandler,
},
useDefaultLang: false,
useDefaultLang: true,
}),
],
providers: [Title, TranslateService],

8
src/app/core/components/footer/footer.component.html

@ -1,8 +1,12 @@
<footer class="footer">
<div class="content has-text-centered">
<p>
Framadate - libérez vos sondages.
<i class="fa fa-copyleft"></i> Logiciel libre sous licence AGPL v3.
<img class="app-logo logo" *ngIf="env.appLogo" src="{{ env.appLogo }}" alt="{{ env.appTitle }}" />
{{ env.appTitle }} - {{ env.appVersion }} - libérez vos sondages. <i class="fa fa-copyleft"></i> Logiciel
libre sous licence AGPL v3.
<app-theme-selector></app-theme-selector>
<a href="https://framagit.org/framasoft/framadate/funky-framadate-front">
<i class="fa fa-gitlab"></i> Sources</a
>

4
src/app/core/components/footer/footer.component.scss

@ -0,0 +1,4 @@
.app-logo {
max-width: 5em;
max-height: 5em;
}

2
src/app/core/components/footer/footer.component.ts

@ -1,4 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { environment } from '../../../../environments/environment';
@Component({
selector: 'app-footer',
@ -6,6 +7,7 @@ import { Component, OnInit } from '@angular/core';
styleUrls: ['./footer.component.scss'],
})
export class FooterComponent implements OnInit {
public env = environment;
constructor() {}
ngOnInit(): void {}

18
src/app/core/components/header/header.component.html

@ -3,10 +3,10 @@
<div class="navbar-brand">
<a class="navbar-item" routerLink="/">
<img class="app-logo logo" *ngIf="appLogo" src="{{ appLogo }}" alt="{{ appTitle }}" />
<span class="app-title title">
<span class="app-title title is-2">
{{ appTitle }}
</span>
<span class="dev-env" *ngIf="!env.production">
<span class="dev-env button has-background-success" *ngIf="!env.production">
<i>
(dev)
</i>
@ -19,6 +19,8 @@
aria-label="menu"
aria-expanded="false"
data-target="navbarBasicExample"
[ngClass]="{ 'has-background-primary': mobileMenuVisible }"
(click)="mobileMenuVisible = !mobileMenuVisible"
>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
@ -29,15 +31,19 @@
<div id="navbarBasicExample" class="navbar-menu">
<div class="navbar-start">
<a class="navbar-item btn btn--primary" routerLink="administration" routerLinkActive="is-active">
<i class="fa fa-plus-circle"></i> {{ 'config.title' | translate }}
<i class="fa fa-plus-circle"></i>
<span>
{{ 'config.title' | translate }}
</span>
</a>
</div>
<div class="navbar-end">
<a class="navbar-item btn btn-primary" routerLink="user/polls" routerLinkActive="is-active">
<i class="fa fa-user"></i> {{ 'config.find_my_polls' | translate }}
</a>
<app-language-selector></app-language-selector>
</div>
</div>
<div class="mobile-menu" *ngIf="mobileMenuVisible">
menu mobile
</div>
</nav>
</header>

3
src/app/core/components/header/header.component.scss

@ -2,6 +2,9 @@
header {
nav {
padding-right: 1em;
.fa {
margin-right: 1ch;
}
}
.container {
padding: 0;

3
src/app/core/components/header/header.component.ts

@ -17,8 +17,9 @@ export class HeaderComponent implements OnInit {
public env = environment;
@Input() public appTitle: string = 'FramaDate Funky';
@Input() public appLogo: string;
mobileMenuVisible = false;
constructor(private userService: UserService, private modalService: ModalService) {}
constructor(private userService: UserService) {}
public ngOnInit(): void {}
}

56
src/app/core/components/home/home.component.html

@ -2,26 +2,68 @@
<div class="hero-body">
<div class="container">
<h1 class="title">
Bienvenue sur Framasondage
{{ 'home.title' | translate }}
{{ env.appTitle }}
</h1>
<div class="columns">
<div class="column">
<h2 class="subtitle">
Se consulter simplement pour s’organiser collectivement.
{{ 'home.subtitle' | translate }}
</h2>
<a role="button" class="button is-fullwidth is-primary" routerLink="administration">
Créer un nouveau sondage
</a>
</div>
<div class="column">
<h2 class="subtitle">
Où sont mes sondages?
{{ 'home.search_title' | translate }}
</h2>
</div>
</div>
<div class="columns">
<div class="column">
<a role="button" class="button is-fullwidth is-primary" routerLink="administration">
<i class="fa fa-plus-circle"></i>
{{ 'home.create_button' | translate }}
</a>
<img src="assets/img/kind/date.jpeg" alt="sondage date" />
</div>
<div class="column">
<a role="button" class="button is-fullwidth is-primary" routerLink="user/polls">
Mes sondages
<i class="fa fa-search"></i>
{{ 'home.search_button' | translate }}
</a>
<img src="assets/img/kind/classic.jpeg" alt="sondage date" />
</div>
</div>
<div class="column">
<img src="assets/img/undraw_group_selfie_ijc6.svg" alt="image WIP" />
<p>
{{
'SENTENCES.framadate-is-an-online-service-for-planning-an-appointment-or-making-a-decision-quickly-and-easily'
| translate
}}
{{ 'SENTENCES.here-is-how-it-works' | translate }}
{{ 'SENTENCES.send-the-poll-link-to-your-friends-or-colleagues' | translate }}
</p>
<p>
{{ 'SENTENCES.what-is-framadate' | translate }}
{{ 'SENTENCES.view-an-example' | translate }}
{{ 'SENTENCES.framadate-is-licensed-under-the' | translate }}
<span class="licence">
GNU Affero v3 Licence
</span>
</p>
<p>
{{ 'SENTENCES.grow-your-own' | translate }}
{{
'SENTENCES.if-you-want-to-install-the-software-for-your-own-use-and-thus-increase-your-independence-we-can-help'
| translate
}}
{{
'SENTENCES.to-participate-in-the-software-development-suggest-improvements-or-simply-download-it-please-visit'
| translate
}}
{{ 'SENTENCES.the-development-site' | translate }}
</p>
</div>
</div>
</div>
</section>

6
src/app/core/components/home/home.component.scss

@ -0,0 +1,6 @@
:host {
text-align: center;
a .fa {
margin-right: 1ch;
}
}

9
src/app/core/components/home/home.component.ts

@ -1,12 +1,11 @@
import { Component, OnInit } from '@angular/core';
import { Component } from '@angular/core';
import { environment } from '../../../../environments/environment';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss'],
})
export class HomeComponent implements OnInit {
constructor() {}
ngOnInit(): void {}
export class HomeComponent {
public env = environment;
}

47
src/app/core/components/sibebar/navigation/navigation.component.html

@ -2,9 +2,6 @@
<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="/poll/inexistentPoll/consultation" routerLinkActive="is-active">
« inexistentPoll »
</a>
<a
class="navbar-item"
*ngFor="let poll of _pollsAvailables | async"
@ -18,7 +15,7 @@
<hr />
<div class="navbar-item has-dropdown is-hoverable">
<div class="navbar-item has-dropdown is-hoverable is-warning" *ngIf="devModeEnabled">
<a class="navbar-link"> Modules </a>
<div class="navbar-dropdown">
<a class="navbar-item" routerLink="oldstuff" routerLinkActive="is-active">
@ -34,28 +31,32 @@
Participation
</a>
</div>
</div>
<hr />
<hr />
<a class="navbar-item" routerLink="/poll/inexistentPoll/consultation" routerLinkActive="is-active">
« inexistentPoll »
</a>
<hr />
<a class="button is-block" routerLink="oldstuff/step/creation" routerLinkActive="active"> Création </a>
<a class="button is-block" routerLink="oldstuff/step/date" routerLinkActive="active"> Les Dates </a>
<a class="button is-block" routerLink="oldstuff/step/answers" routerLinkActive="active"> Réponses </a>
<a class="button is-block" routerLink="oldstuff/step/visibility" routerLinkActive="active"> Visibilité </a>
<a class="button is-block" routerLink="oldstuff/step/resume" routerLinkActive="active"> Résumé </a>
<a class="button is-block" routerLink="oldstuff/step/end" routerLinkActive="active"> Confirmation </a>
<a class="button is-block" routerLink="oldstuff/step/admin"> Administration </a>
<a class="button is-block" routerLink="oldstuff/step/home" routerLinkActive="active">
<i class="fa fa-home" aria-hidden="true"></i> Accueil
</a>
<a class="button is-block" routerLink="oldstuff/step/creation" routerLinkActive="active"> Création </a>
<a class="button is-block" routerLink="oldstuff/step/date" routerLinkActive="active"> Les Dates </a>
<a class="button is-block" routerLink="oldstuff/step/answers" routerLinkActive="active"> Réponses </a>
<a class="button is-block" routerLink="oldstuff/step/visibility" routerLinkActive="active"> Visibilité </a>
<a class="button is-block" routerLink="oldstuff/step/resume" routerLinkActive="active"> Résumé </a>
<a class="button is-block" routerLink="oldstuff/step/end" routerLinkActive="active"> Confirmation </a>
<a class="button is-block" routerLink="oldstuff/step/admin"> Administration </a>
<hr />
<hr />
<a class="button is-block" routerLink="oldstuff/step/kind" routerLinkActive="active"> Page démo </a>
<a class="button is-block" routerLink="oldstuff/vote/poll/id/1" routerLinkActive="active"> Sondage 1 </a>
<a class="button is-block" routerLink="oldstuff/vote/poll/id/2" routerLinkActive="active"> Sondage 2 </a>
<a class="button is-block" routerLink="oldstuff/vote/poll/id/3" routerLinkActive="active">
Sondage 3 (dessins animés)
</a>
<a class="button is-block" routerLink="oldstuff/graphic/toto" routerLinkActive="active"> Graphique </a>
</div>
<a class="button is-block" routerLink="oldstuff/step/kind" routerLinkActive="active"> Page démo </a>
<a class="button is-block" routerLink="oldstuff/vote/poll/id/1" routerLinkActive="active"> Sondage 1 </a>
<a class="button is-block" routerLink="oldstuff/vote/poll/id/2" routerLinkActive="active"> Sondage 2 </a>
<a class="button is-block" routerLink="oldstuff/vote/poll/id/3" routerLinkActive="active">
Sondage 3 (dessins animés)
<a class="button is-block" routerLink="oldstuff/step/home" routerLinkActive="active">
<i class="fa fa-home" aria-hidden="true"></i> Accueil
</a>
<a class="button is-block" routerLink="oldstuff/graphic/toto" routerLinkActive="active"> Graphique </a>
</nav>

2
src/app/core/components/sibebar/navigation/navigation.component.ts

@ -3,6 +3,7 @@ import { Observable } from 'rxjs';
import { Poll } from '../../../models/poll.model';
import { MockingService } from '../../../services/mocking.service';
import { environment } from '../../../../../environments/environment';
@Component({
selector: 'app-navigation',
@ -11,6 +12,7 @@ import { MockingService } from '../../../services/mocking.service';
})
export class NavigationComponent implements OnInit {
public _pollsAvailables: Observable<Poll[]> = this.mockingService.pollsAvailables;
public devModeEnabled = !environment.production;
constructor(private mockingService: MockingService) {}

3
src/app/core/core.module.ts

@ -10,10 +10,11 @@ import { HomeComponent } from './components/home/home.component';
import { LogoComponent } from './components/logo/logo.component';
import { NavigationComponent } from './components/sibebar/navigation/navigation.component';
import { throwIfAlreadyLoaded } from './guards/module-import.guard';
import { SharedModule } from '../shared/shared.module';
@NgModule({
declarations: [FooterComponent, HeaderComponent, HomeComponent, LogoComponent, NavigationComponent],
imports: [CommonModule, FormsModule, RouterModule, TranslateModule],
imports: [CommonModule, FormsModule, RouterModule, TranslateModule, SharedModule],
exports: [HeaderComponent, FooterComponent, NavigationComponent, LogoComponent],
})
export class CoreModule {

15
src/app/core/enums/language.enum.ts

@ -1,4 +1,15 @@
export enum Language {
FR = 'FR',
EN = 'EN',
FR = 'fr',
// BR = 'br',
// CA = 'ca',
// DE = 'de',
// EL = 'el',
EN = 'en',
// ES = 'es',
// GL = 'gl',
// HU = 'hu',
// IT = 'it',
// NL = 'nl',
// OC = 'oc',
// SV = 'sv',
}

6
src/app/core/models/configuration.model.ts

@ -5,11 +5,17 @@ export class PollConfiguration {
constructor(
public isAboutDate: boolean = false,
public isProtectedByPassword: boolean = false,
public password: string = '',
public isOwnerNotifiedByEmailOnNewVote: boolean = false,
public isOwnerNotifiedByEmailOnNewComment: boolean = false,
public isMaybeAnswerAvailable: boolean = false,
public areResultsPublic: boolean = true,
public isAllowingtoChangeOwnAnswers: boolean = true,
public whoCanChangeAnswers: string = 'everybody',
public dateCreated: Date = new Date(Date.now()),
public expiresDaysDelay: number = environment.poll.defaultConfig.expiracyInDays,
public expiracyAfterLastModificationInDays: number = environment.poll.defaultConfig
.expiracyAfterLastModificationInDays,
public expires: Date = DateService.addDaysToDate(
environment.poll.defaultConfig.expiracyInDays,
new Date(Date.now())

14
src/app/core/models/poll.model.ts

@ -7,13 +7,15 @@ import { User } from './user.model';
export class Poll {
constructor(
public owner: User,
public slug: string,
public title: string,
public owner: User = new User(),
public slug: string = 'default-slug',
public title: string = 'default title',
public description?: string,
public configuration: PollConfiguration = new PollConfiguration(),
public comments: Comment[] = [],
public choices: Choice[] = []
public choices: Choice[] = [],
public dateChoices: Choice[] = [],
public timeChoices: Choice[] = []
) {}
public getAdministrationUrl(): string {
@ -27,7 +29,7 @@ export class Poll {
public static adaptFromLocalJsonServer(
item: Pick<Poll, 'owner' | 'title' | 'description' | 'slug' | 'configuration' | 'comments' | 'choices'>
): Poll {
const poll = new Poll(
return new Poll(
new User(item.owner.pseudo, item.owner.email, undefined),
item.slug,
item.title,
@ -48,7 +50,5 @@ export class Poll {
return choice;
})
);
return poll;
}
}

4
src/app/core/models/user.model.ts

@ -3,8 +3,8 @@ import { Poll } from './poll.model';
export class User {
constructor(
public pseudo: string,
public email: string,
public pseudo: string = 'pseudo',
public email: string = 'example@example.com',
public polls: Poll[] = [],
public role?: UserRole,
public token?: string

113
src/app/core/services/api.service.ts

@ -4,35 +4,77 @@ import { environment } from 'src/environments/environment';
import { Answer } from '../enums/answer.enum';
import { Poll } from '../models/poll.model';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Subscription } from 'rxjs';
import { ToastService } from './toast.service';
import { LoaderService } from './loader.service';
const apiVersion = environment.api.versionToUse;
const currentApiRoutes = environment.api.version[apiVersion];
const apiBaseHref = environment.api.version[apiVersion].baseHref;
const apiEndpoints = environment.api.endpoints;
@Injectable({
providedIn: 'root',
})
export class ApiService {
private useDevLocalServer = true;
private devLocalServerBaseHref = 'http://localhost:8000/';
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 slugsEndpoint = environment.api.endpoints.polls.slugs.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;
private readonly pollsEndpoint = apiEndpoints.polls.name;
private readonly answersEndpoint = apiEndpoints.polls.answers.name;
private readonly commentsEndpoint = apiEndpoints.polls.comments.name;
private readonly slugsEndpoint = apiEndpoints.polls.slugs.name;
private readonly usersEndpoint = apiEndpoints.users.name;
private readonly usersPollsEndpoint = apiEndpoints.users.polls.name;
private readonly usersPollsSendEmailEndpoint = apiEndpoints.users.polls.sendEmail.name;
private static loader: LoaderService;
constructor() {
this.axiosInstance = axios.create({ baseURL: environment.api.baseHref });
constructor(private http: HttpClient, private loader: LoaderService, private toastService: ToastService) {
this.axiosInstance = axios.create({ baseURL: apiBaseHref });
this.axiosInstance.defaults.timeout = 2500;
this.axiosInstance.defaults.headers.post['Content-Type'] = 'application/json';
this.axiosInstance.defaults.headers.post['Accept'] = 'application/json';
this.axiosInstance.defaults.headers.post['Charset'] = 'UTF-8';
this.axiosInstance.defaults.headers.post['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS';
this.axiosInstance.defaults.headers.post['Access-Control-Allow-Origin'] = '*';
console.log('this.axiosInstance.defaults.headers', this.axiosInstance.defaults.headers);
}
//////////////////////
// CREATE OR UPDATE //
//////////////////////
public async createPoll(poll: Poll): Promise<string> {
public async createPoll(poll: Poll): Promise<Subscription> {
// this.loader.setStatus(true);
console.log('config', poll);
// const baseHref = this.useDevLocalServer ? 'http://localhost:8000' : apiBaseHref;
// return this.http
// .post(`${baseHref}${currentApiRoutes['api_new_poll']}`, poll, ApiService.makeHeaders())
// .subscribe(
// (res: Observable<any>) => {
// // redirect to the page to administrate the new poll
// this.toastService.display('Sondage Créé');
//
// console.log('res', res);
// // this.updateCurrentPollFromResponse(res);
//
// this.loader.setStatus(false);
// },
// (e) => {
// ApiService.handleError(e);
// }
// );
try {
return await this.axiosInstance.post(`${this.pollsEndpoint}`, poll);
console.log('currentApiRoutes', currentApiRoutes);
return await this.axiosInstance.post(`${apiBaseHref}${currentApiRoutes['api_new_poll']}`, {
data: poll,
});
} catch (error) {
this.handleError(error);
ApiService.handleError(error);
}
}
@ -49,7 +91,7 @@ export class ApiService {
response,
});
} catch (error) {
this.handleError(error);
ApiService.handleError(error);
}
}
@ -57,7 +99,7 @@ export class ApiService {
try {
return await this.axiosInstance.post(`${this.pollsEndpoint}/${slug}${this.commentsEndpoint}`, comment);
} catch (error) {
this.handleError(error);
ApiService.handleError(error);
}
}
@ -70,7 +112,7 @@ export class ApiService {
const response: AxiosResponse<Poll[]> = await this.axiosInstance.get<Poll[]>(`${this.pollsEndpoint}`);
return response?.data;
} catch (error) {
this.handleError(error);
ApiService.handleError(error);
}
}
@ -101,7 +143,7 @@ export class ApiService {
if (error.response?.status === 404) {
return undefined;
} else {
this.handleError(error);
ApiService.handleError(error);
}
}
}
@ -119,7 +161,7 @@ export class ApiService {
if (error.response?.status === 404) {
return true;
} else {
this.handleError(error);
ApiService.handleError(error);
}
}
}
@ -131,7 +173,7 @@ export class ApiService {
`${this.usersEndpoint}/${email}${this.usersPollsEndpoint}${this.usersPollsSendEmailEndpoint}`
);
} catch (error) {
this.handleError(error);
ApiService.handleError(error);
}
}
@ -145,7 +187,7 @@ export class ApiService {
);
return response?.data;
} catch (error) {
this.handleError(error);
ApiService.handleError(error);
}
}
@ -160,7 +202,7 @@ export class ApiService {
answer,
});
} catch (error) {
this.handleError(error);
ApiService.handleError(error);
}
}
@ -172,7 +214,7 @@ export class ApiService {
const response: AxiosResponse = await this.axiosInstance.delete(`${this.pollsEndpoint}/${slug}`);
return response?.status === 204;
} catch (error) {
this.handleError(error);
ApiService.handleError(error);
}
}
@ -183,7 +225,7 @@ export class ApiService {
);
return response?.status === 204;
} catch (error) {
this.handleError(error);
ApiService.handleError(error);
}
}
@ -194,14 +236,37 @@ export class ApiService {
);
return response?.status === 204;
} catch (error) {
this.handleError(error);
ApiService.handleError(error);
}
}
/////////////////////
// PRIVATE METHODS //
/////////////////////
private handleError(error): void {
/**
* prepare headers like the charset and json type for any call to the backend
* @param bodyContent?
*/
static makeHeaders(bodyContent?: any) {
const headerDict = {
Charset: 'UTF-8',
'Content-Type': 'application/json',
Accept: 'application/json',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Accept,Accept-Language,Content-Language,Content-Type',
'Access-Control-Allow-Origin': '*',
};
const requestOptions = {
headers: new HttpHeaders(headerDict),
body: bodyContent,
};
return requestOptions;
}
private static handleError(error): void {
// this.loader.setStatus(true);
if (error.response) {
// The request was made and the server responded with a status code
// that falls out of the range of 2xx

6
src/app/core/services/language.service.ts

@ -15,7 +15,7 @@ export class LanguageService {
}
public setLanguage(language: Language): void {
this.translate.use(language.toString().toUpperCase());
this.translate.use(language.toString());
}
public getAvailableLanguages(): string[] {
@ -48,17 +48,19 @@ export class LanguageService {
// set default language
if (!this.translate.currentLang) {
this.setLanguage(Language.EN);
this.setLanguage(Language.FR);
}
}
private setLanguageFromStorage(): void {
console.log('this.storageService.language', this.storageService.language);
if (this.storageService.language && this.translate.getLangs().includes(this.storageService.language)) {
this.setLanguage(this.storageService.language);
}
}
private setLanguageFromBrowser(): void {
const currentBrowserLanguage: Language = this.translate.getBrowserLang().toUpperCase() as Language;
console.log('currentBrowserLanguage', currentBrowserLanguage);
if (this.translate.getLangs().includes(currentBrowserLanguage)) {
this.setLanguage(currentBrowserLanguage);
}

120
src/app/core/services/poll.service.ts

@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
import { BehaviorSubject, Observable } from 'rxjs';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { Answer } from '../enums/answer.enum';
import { Choice } from '../models/choice.model';
@ -10,6 +10,8 @@ import { ApiService } from './api.service';
import { ToastService } from './toast.service';
import { UserService } from './user.service';
import { UuidService } from './uuid.service';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../../environments/environment';
@Injectable({
providedIn: 'root',
@ -19,6 +21,7 @@ export class PollService implements Resolve<Poll> {
public readonly poll: Observable<Poll | undefined> = this._poll.asObservable();
constructor(
private http: HttpClient,
private router: Router,
private apiService: ApiService,
private userService: UserService,
@ -26,6 +29,11 @@ export class PollService implements Resolve<Poll> {
private toastService: ToastService
) {}
/**
* auto fetch a poll when route is looking for one in the administration pattern
* @param route
* @param state
*/
public async resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<Poll> {
const segments: string[] = state.url.split('/');
const wantedSlug: string = segments.includes('poll') ? segments[segments.indexOf('poll') + 1] : '';
@ -46,6 +54,20 @@ export class PollService implements Resolve<Poll> {
}
}
getAllAvailablePolls() {
const baseHref = environment.api.version.apiV1.baseHref;
console.log('getAllAvailablePolls baseHref', baseHref);
const headers = ApiService.makeHeaders();
console.log('getAllAvailablePolls headers', headers);
try {
this.http.get(`${baseHref}/poll`, headers).subscribe((res: Observable<any>) => {
console.log('getAllAvailablePolls res', res);
});
} catch (e) {
console.log('getAllAvailablePolls e', e);
}
}
public async loadPollBySlug(slug: string): Promise<void> {
console.log('slug', slug);
if (slug) {
@ -59,10 +81,45 @@ export class PollService implements Resolve<Poll> {
this._poll.next(poll);
}
/**
* make a uniq slug for the current poll creation
* @param config
*/
makeSlug(config: Poll): string {
let str = '';
str =
config.configuration.dateCreated.getFullYear() +
'_' +
(config.configuration.dateCreated.getMonth() + 1) +
'_' +
config.configuration.dateCreated.getDate() +
'_' +
config.owner.pseudo +
'_' +
config.title;
str = str.replace(/^\s+|\s+$/g, ''); // trim
str = str.toLowerCase();
// remove accents, swap ñ for n, etc
const from = 'àáäâèéëêìíïîòóöôùúüûñç·/_,:;';
const to = 'aaaaeeeeiiiioooouuuunc------';
for (let i = 0, l = from.length; i < l; i++) {
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i));
}
str = str
.replace(/[^a-z0-9 -]/g, '') // remove invalid chars
.replace(/\s+/g, '-') // collapse whitespace and replace by -
.replace(/-+/g, '-'); // collapse dashes
return str + '-' + this.uuidService.getUUID();
}
public async saveCurrentPoll(): Promise<void> {
const pollUrl: string = await this.apiService.createPoll(this._poll.getValue());
const pollUrl: Subscription = await this.apiService.createPoll(this._poll.getValue());
// TODO: Maybe handle the url to update currentPoll according to backend response
if (pollUrl) {
console.log('pollUrl', pollUrl);
this.toastService.display('Le sondage a été enregistré.');
} else {
this.toastService.display('Le sondage n’a été correctement enregistré, veuillez ré-essayer.');
@ -124,4 +181,63 @@ export class PollService implements Resolve<Poll> {
return list;
}
newPollFromForm(form: any): any {
const newpoll = new Poll(
this.userService.getCurrentUser(),
this.uuidService.getUUID(),
form.controls.title.value
);
/**
* convert to API version 1 config poll
*/
const apiV1Poll = {
menuVisible: true,
expiracyDateDefaultInDays: newpoll.configuration.expiresDaysDelay,
deletionDateAfterLastModification: newpoll.configuration.expiracyAfterLastModificationInDays,
pollType: newpoll.configuration.isAboutDate ? 'dates' : 'classic', // classic or dates
title: newpoll.title,
description: newpoll.description,
myName: newpoll.owner.pseudo,
myComment: '',
isAdmin: true, // when we create a poll, we are admin on it
myVoteStack: {},
myTempVoteStack: 0,
myEmail: newpoll.owner.email,
myPolls: [], // list of retrieved polls from the backend api
/*
date specific poll, we have the choice to setup different hours (timeList) for all possible dates (dateList), or use the same hours for all dates
*/
allowSeveralHours: 'true',
// access
visibility: newpoll.configuration.areResultsPublic, // visible to one with the link:
voteChoices: newpoll.configuration.isMaybeAnswerAvailable ? 'yes, maybe, no' : 'yes', // possible answers to a vote choice: only "yes", "yes, maybe, no"
creationDate: new Date(),
expirationDate: '', // expiracy date
voteStackId: null, // id of the vote stack to update
pollId: null, // id of the current poll when created. data given by the backend api
pollSlug: null, // id of the current poll when created. data given by the backend api
currentPoll: null, // current poll selected with createPoll or getPoll of ConfigService
passwordAccess: newpoll.configuration.isProtectedByPassword,
password: newpoll.configuration.password,
customUrl: newpoll.slug, // custom slug in the url, must be unique
customUrlIsUnique: null, // given by the backend
urlSlugPublic: null,
urlPublic: null,
urlAdmin: null,
adminKey: '', // key to change config of the poll
owner_modifier_token: '', // key to change a vote stack
canModifyAnswers: newpoll.configuration.isAllowingtoChangeOwnAnswers, // bool for the frontend selector
whoModifiesAnswers: newpoll.configuration.whoCanChangeAnswers, // everybody, self, nobody (: just admin)
whoCanChangeAnswers: newpoll.configuration.whoCanChangeAnswers, // everybody, self, nobody (: just admin)
dateList: newpoll.dateChoices, // sets of days as strings, config to set identical time for days in a special days poll
timeList: newpoll.timeChoices, // ranges of time expressed as strings
answers: newpoll.choices,
// modals
displayConfirmVoteModalAdmin: false,
};
console.log('apiV1Poll', apiV1Poll);
return apiV1Poll;
}
}

10
src/app/core/services/uuid.service.ts

@ -10,4 +10,14 @@ export class UuidService {
public getUUID(): string {
return this.uid();
}
/**
* generate unique id to have a default url for future poll
*/
getLongUUID(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0,
v = c == 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
}

9
src/app/features/administration/administration.component.html

@ -1,12 +1,5 @@
<div class="columns">
<div class="column has-text-centered">
<h1>Administration</h1>
</div>
</div>
<div class="columns">
<div class="columns administration">
<div class="column">
<app-admin-form [poll]="poll"></app-admin-form>
<h1 class="title is-1"><i class="fa fa-calendar" aria-hidden="true"></i> {{ 'dates.title' | translate }}</h1>
</div>
</div>

4
src/app/features/administration/administration.component.ts

@ -5,7 +5,6 @@ import { Subscription } from 'rxjs';
import { Poll } from '../../core/models/poll.model';
import { ModalService } from '../../core/services/modal.service';
import { UserService } from '../../core/services/user.service';
import { SettingsComponent } from '../../shared/components/settings/settings.component';
@Component({
selector: 'app-administration',
@ -13,8 +12,7 @@ import { SettingsComponent } from '../../shared/components/settings/settings.com
styleUrls: ['./administration.component.scss'],
})
export class AdministrationComponent implements OnInit, OnDestroy {
public poll: Poll;
public form: Object;
public poll: Poll = new Poll();
private routeSubscription: Subscription;
constructor(private route: ActivatedRoute, private userService: UserService, private modalService: ModalService) {}

2
src/app/features/administration/administration.module.ts

@ -9,6 +9,7 @@ import { AdministrationComponent } from './administration.component';
import { StepperComponent } from './stepper/stepper.component';
import { NamingComponent } from './naming/naming.component';
import { FormComponent } from './form/form.component';
import { DateValueAccessorModule } from 'angular-date-value-accessor';
@NgModule({
declarations: [AdministrationComponent, StepperComponent, NamingComponent, FormComponent],
@ -18,6 +19,7 @@ import { FormComponent } from './form/form.component';
ReactiveFormsModule,
SharedModule,
TranslateModule.forChild({ extend: true }),
DateValueAccessorModule,
],
})
export class AdministrationModule {}

390
src/app/features/administration/form/form.component.html

@ -1,21 +1,169 @@
<div class="admin-form">
<h1 i18n>
<h1>
{{ 'creation.title' | translate }}
</h1>
<span class="pre-selector" i18n>
{{ 'creation.want' | translate }}
</span>
<button class="btn btn--warning">
Reset all
<img src="assets/img/undraw_Moving_twwf.svg" alt="image WIP" />
<button class="btn btn--warning" (click)="askInitFormDefault()">
<i class="fa fa-refresh"></i>
Tout réinitialiser
</button>
<button class="btn is-success" (click)="createPoll()">
<i class="fa fa-save"></i>
Enregistrer le sondage
</button>
<button class="btn is-default" (click)="automaticSlug()">
<i class="fa fa-save"></i>
Slug automatique
</button>
<div class="simple">
<form [formGroup]="pollFormGroup">
<label for="title">Titre</label>
<div class="well">
{{ poll.slug }}
</div>
<div class="has-background-danger" *ngIf="!form.valid">
le formulaire est invalide
<pre>
{{ form.errors | json }}
</pre
>
</div>
<div class="dates-list">
<div class="title">
<span class="count-dates">
{{ timeList.length }}
</span>
<span class="count-dates-txt">
{{ 'dates.count_time' | translate }}
(pour chaque jour)
</span>
</div>
<div class="actions">
<button
(click)="addTime()"
*ngIf="'false' === allowSeveralHours"
class="btn btn--primary"
id="add_time_button"
>
<i class="fa fa-plus" aria-hidden="true"></i>
{{ 'dates.add_time' | translate }}
</button>
<button
(click)="removeAllTimes()"
*ngIf="'false' === allowSeveralHours"
class="btn btn--warning"
id="remove_time_button"
>
<i class="fa fa-trash" aria-hidden="true"></i>
Aucune plage horaire
</button>
<button
(click)="resetTimes()"
*ngIf="'false' === allowSeveralHours"
class="btn btn--warning"
id="reset_time_button"
>
<i class="fa fa-refresh" aria-hidden="true"></i>
réinitialiser
</button>
</div>
<div *ngIf="'false' === allowSeveralHours" class="identical-dates">
<div cdkDropList class="example-list" (cdkDropListDropped)="drop($event)">
<div *ngFor="let time of timeList; index as id" class="time-choice" cdkDrag>
<label for="timeChoices_{{ id }}">
<i class="fa fa-clock-o" aria-hidden="true"></i>
</label>
<input
[(ngModel)]="time.literal"
name="timeChoices_{{ id }}"
type="text"
id="timeChoices_{{ id }}"
/>
<button (click)="time.timeList.splice(id, 1)" class="btn btn-warning">
<i class="fa fa-times" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<hr />
<span class="count-dates title">
{{ dateList.length }}
</span>
<span>
{{ 'dates.count_dates' | translate }}
</span>
<button class="btn btn--primary" (click)="addChoice()">
{{ 'dates.add' | translate }}
</button>
<div *ngFor="let choice of dateList; index as id" class="date-choice">
{{ id }})
<input
[(ngModel)]="choice.date_object"
name="dateChoices_{{ id }}"
id="dateChoices_{{ id }}"
useValueAsDate
type="date"
/>
<button (click)="dateList.splice(id, 1)" class="btn btn-warning">
<i class="fa fa-times" aria-hidden="true"></i>