forked from tykayn/funky-framadate-front
Merge branch 'develop' into 'master'
Develop to master See merge request framasoft/framadate/funky-framadate-front!47
This commit is contained in:
commit
fd82a3b6b2
13
.editorconfig
Normal file
13
.editorconfig
Normal file
@ -0,0 +1,13 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = tab
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
1
.eslintcache
Normal file
1
.eslintcache
Normal file
File diff suppressed because one or more lines are too long
20
.eslintrc.js
Normal file
20
.eslintrc.js
Normal file
@ -0,0 +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,
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/unbound-method': ['error', { ignoreStatic: true }],
|
||||
},
|
||||
};
|
54
.gitignore
vendored
Normal file
54
.gitignore
vendored
Normal file
@ -0,0 +1,54 @@
|
||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
|
||||
|
||||
# compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc/
|
||||
documentation
|
||||
# Only exists if Bazel was run
|
||||
/bazel-out
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# profiling files
|
||||
chrome-profiler-events*.json
|
||||
speed-measure-plugin*.json
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
.idea
|
||||
|
||||
# misc
|
||||
/.sass-cache
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System Files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Editor-specific configuration
|
||||
.vscode/
|
||||
|
||||
# linter
|
||||
.eslintcache
|
65
.gitlab-ci.yml
Normal file
65
.gitlab-ci.yml
Normal file
@ -0,0 +1,65 @@
|
||||
# image: weboaks/node-karma-protractor-chrome
|
||||
image: node:latest
|
||||
|
||||
stages:
|
||||
- build
|
||||
# - pages
|
||||
- test
|
||||
# - e2e
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/
|
||||
|
||||
#pages:
|
||||
# stage: pages
|
||||
# script:
|
||||
# - yarn install --pure-lockfile
|
||||
# - yarn build:prod:gitlabpage
|
||||
# - mv dist/framadate/ public/
|
||||
# artifacts:
|
||||
# paths:
|
||||
# - public
|
||||
# expire_in: '2 hours'
|
||||
# only:
|
||||
# - develop
|
||||
|
||||
#test:
|
||||
# stage: test
|
||||
# script:
|
||||
# - npm i
|
||||
# - pkill Xvfb
|
||||
# - npm run test:ci
|
||||
# artifacts:
|
||||
# paths:
|
||||
# - coverage/
|
||||
# cache:
|
||||
# policy: pull
|
||||
|
||||
build:
|
||||
stage: build
|
||||
script:
|
||||
- yarn install --pure-lockfile
|
||||
- npx ng build --prod
|
||||
cache:
|
||||
policy: pull
|
||||
|
||||
#e2e:
|
||||
# stage: e2e
|
||||
# script:
|
||||
# - npm i
|
||||
# - pkill Xvfb
|
||||
# - npm run e2e
|
||||
|
||||
#pages:
|
||||
# stage: .post
|
||||
# dependencies:
|
||||
# - test
|
||||
# script:
|
||||
# - mv coverage/ public/
|
||||
# artifacts:
|
||||
# paths:
|
||||
# - public
|
||||
# expire_in: 30 days
|
||||
# only:
|
||||
# - master
|
9
.prettierignore
Normal file
9
.prettierignore
Normal file
@ -0,0 +1,9 @@
|
||||
package-lock.json
|
||||
yarn-lock.json
|
||||
yarn.lock
|
||||
node_modules
|
||||
build
|
||||
dist
|
||||
res
|
||||
coverage
|
||||
.vscode/*
|
3
.prettierrc.yaml
Normal file
3
.prettierrc.yaml
Normal file
@ -0,0 +1,3 @@
|
||||
printWidth: 120
|
||||
singleQuote: true
|
||||
tabWidth: 4
|
0
LICENSE.md
Normal file
0
LICENSE.md
Normal file
92
README.md
Normal file
92
README.md
Normal file
@ -0,0 +1,92 @@
|
||||
# Framadate - funky version
|
||||
FR: Un logiciel libre de sondage fait par les contributeurs de l'association Framasoft, avec une API backend.
|
||||
EN: A libre polling software made by contributors around the French association Framasoft.
|
||||
This version uses a brand new backend API.
|
||||
|
||||
## Pour débuter - getting started
|
||||
[lire la doc pour débuter votre Funky Framadate](doc/GETTING_STARTED.md)
|
||||
|
||||
## Documentation
|
||||
FR: Toute la documentation est disponible [dans le dossier "doc"](/doc), principalement en Français.
|
||||
EN: All documentation is available in the "doc" folder, mainly in French because reasons.
|
||||
* Meeting notes
|
||||
* Getting Started (yarn start / npm start)
|
||||
* How to contribute
|
||||
* Architecture
|
||||
* Translation i18n
|
||||
* Accesibility
|
||||
* Licence GNU affero V3
|
||||
|
||||
# Version funky framadate
|
||||
|
||||
* [Spécifications](docs/cadrage/specifications-fonctionnelles)
|
||||
* maquettes par @maiwann : https://scene.zeplin.io/project/5d4d83d68866d6522ff2ff10
|
||||
* vidéo de démo des maquettes par @maiwann : https://nuage.maiwann.net/s/JRRHTR9D2akMAa7
|
||||
* discussions sur framateam canal général : https://framateam.org/ux-framatrucs/channaels/framadate
|
||||
* discussions techniques côté développeurs : https://framateam.org/ux-framatrucs/channels/framadate-dev
|
||||
* [notes de réunion](notes-de-reunion)
|
||||
* [traductions](traductions)
|
||||
|
||||
|
||||
# Documentations sur Angular
|
||||
|
||||
* `{- sur sass -}` (on va utiliser CSS, si angular permet d'avoir des variables CSS, @newick)
|
||||
|
||||
|
||||
# Exemple de maquette de la nouvelle version
|
||||
|
||||
![funky_framadate_maquette](uploads/535da7c3a5ce5fae67b2b497bdc4631d/funky_framadate_maquette.png)
|
||||
|
||||
## LIBRARIES USED
|
||||
|
||||
| status | lib choice_label | 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.
|
||||
# Framadate
|
||||
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
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
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
|
||||
|
||||
## Further help
|
||||
|
||||
Before using ng for the first time in this project, use `npm i` to install needed npm modules.
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
|
121
angular.json
Normal file
121
angular.json
Normal file
@ -0,0 +1,121 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"framadate": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/framadate",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": [
|
||||
"node_modules/fork-awesome/css/fork-awesome.min.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": [
|
||||
"node_modules/chart.js/dist/Chart.min.js",
|
||||
"node_modules/marked/lib/marked.js",
|
||||
"node_modules/prismjs/prism.js",
|
||||
"node_modules/prismjs/components/prism-css.min.js"
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"crossOrigin": "anonymous",
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "2mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb",
|
||||
"maximumError": "10kb"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "framadate:build",
|
||||
"proxyConfig": "src/proxy.conf.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "framadate:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "framadate:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-builders/jest:run",
|
||||
"options": {}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": ["tsconfig.app.json", "tsconfig.spec.json", "e2e/tsconfig.json"],
|
||||
"exclude": ["**/node_modules/**"]
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "framadate:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "framadate:serve:production"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "framadate",
|
||||
"cli": {
|
||||
"analytics": "0ba9c0a9-850f-4c5f-8124-cbe6f4c79ef1"
|
||||
}
|
||||
}
|
3
babel.config.json
Normal file
3
babel.config.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["@babel/preset-env"]
|
||||
}
|
12
browserslist
Normal file
12
browserslist
Normal file
@ -0,0 +1,12 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
> 0.5%
|
||||
last 2 versions
|
||||
Firefox ESR
|
||||
not dead
|
||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
51
doc/CONTRIBUTE.md
Normal file
51
doc/CONTRIBUTE.md
Normal file
@ -0,0 +1,51 @@
|
||||
# Comment contribuer à Framadate funky ?
|
||||
Il existe des tas de façons de contribuer à un logiciel libre comme Framadate. Vous pouvez __discuter__ avec d'autres personnes de ce que vous souhaiteriez voir naître dans le logiciel, __essayer__ de l'utiliser dans sa version expérimentale, __mettre en place une démo__ ou un service publiquement utilisable, __écrire__ des modifications de code, proposer de l'aide de toute sorte, [traduire les textes](cadrage/i18n.md), vérifier l'accessibilité, lire la [documentation d'architecture](../doc/cadrage/architecture.md) etc...
|
||||
|
||||
* Avoir un compte Framateam, et Framagit si vous souhaitez contribuer au code
|
||||
* Examiner les tickets
|
||||
Discuter avec les autres membres participant, sur un ticket en particulier et aussi dans les canaux de framateam
|
||||
* Nous avons créé deux canaux: un pour les discussions générales, et un autre pour les discussions techniques de dev. Prenez soin de bien cibler le canal dans lequel vous communiquez afin de ne pas faire peur aux gens qui ne codent pas et qui souhaitent contribuer de toutes les autres façons.
|
||||
* Une fois d'accord avec les autres, mettre à jour votre dépot de travail local. Voir [la doc d'installation / getting started](GETTING_STARTED.md) à ce sujet.
|
||||
* Utiliser la branche `develop`
|
||||
```
|
||||
git checkout develop
|
||||
git fetch
|
||||
```
|
||||
* choisir un ticket gitlab consacré à votre branche, si il n'en existe pas, le créer. Un ticket doit avoir un sujet suffisament petit pour pouvoir le réaliser dans un temps raisonnable. Faites des sous-tickets reliés pour les longues fonctionnalités. Le but des tickets n'étant pas de rester ad vitam dans la backlog, mais bien de montrer un avancement détaillé, avec un titre évocateur. "exit donc les tickets du genre 'ça ne marche pas' ou 'finir framadate'"
|
||||
* Faire une branche dédiée à vos modifications en lui mettant un nom éloquent.
|
||||
```
|
||||
git checkout -b ma-description-de-fonctionnalite
|
||||
```
|
||||
* lisez les logs des commits les plus récents pour comprendre ce qui se passe.
|
||||
* Faites des petits commits, avec un titre désignant précisément ce sur quoi vous progressez.
|
||||
* Faire une merge request sur framagit qui sera soumise à la revue de code par les pairs du projet.
|
||||
* Continuer à interagir avec les autres membres pour utiliser au mieux les savoir-faire de chacun et ne pas se marcher sur les pieds.
|
||||
* Mettez toujours en avant la politesse et l'empathie, la collaboration n'en sera que meilleure.
|
||||
* N'hésitez pas à contacter en direct les personnes avec lesquelles vous souhaitez avancer.
|
||||
|
||||
# Qui veut faire quoi ?
|
||||
|
||||
* maiwann : maquettes, UX
|
||||
* tykayn : développeur front end, JS & styles
|
||||
* newick : intégrateur dans la vraie vie
|
||||
* llaq : plutôt HTML / css et un peu de développement en php
|
||||
* talone : plutôt coté JS
|
||||
* tcit : dev tout qui connait bien le backend de Framadate actuel
|
||||
* pouhiou : soutien moral
|
||||
* come_744 : git, découverte d'angular
|
||||
* arnaldo : php, découverte du libre
|
||||
* elbuffeto : l'intégration HTML/CSS, accessibilité
|
||||
* l4pin : un peu de front JS, beaucoup de back
|
||||
* wadouk : dev compilé (elm, haskell, scala)
|
||||
* cbossard : dev (plutôt backend), java/javascript, avec un peu de temps en ce moment
|
||||
* seraf : dev fullstack (plutôt front), JS (Angular, VueJS, Svelte), PHP (Symfony, ApiPlatform), Java (Spring)
|
||||
|
||||
|
||||
# Liens utiles:
|
||||
* Discussion : https://framateam.org/ux-framatrucs/channels/framadate
|
||||
* Repo front/dev : https://framagit.org/framasoft/framadate/funky-framadate-front/tree/dev
|
||||
* Repo back : https://framagit.org/framasoft/framadate/framadate
|
||||
* Maquettes Zeplin : demander l'accès à maiwann
|
||||
* La démo : https://framadate-api.cipherbliss.com/
|
||||
* Vidéo de présentation au lancement de la refonte : https://nuage.maiwann.net/s/JRRHTR9D2akMAa7
|
||||
|
20
doc/GETTING_STARTED.md
Normal file
20
doc/GETTING_STARTED.md
Normal file
@ -0,0 +1,20 @@
|
||||
# Pour débuter - getting started
|
||||
Clonez le dépot, installez les dépendances, et lancez le serveur local qui se recharge lorsque vous sauvegardez des fichiers dans le dépot. Les fois suivantes vous n'aurez qu'a lancer yarn start.
|
||||
```bash
|
||||
# clone the repo
|
||||
git clone https://framagit.org/framasoft/framadate/funky-framadate-front.git
|
||||
cd funky-framadate-front
|
||||
# install yarn
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
sudo apt-get update && sudo apt-get install yarn
|
||||
|
||||
yarn install --pure-lockfile
|
||||
yarn start
|
||||
# and now check your browser http://localhost:4200
|
||||
```
|
||||
|
||||
Pour interagir avec la base de données, il vous faudra également faire démarrer l'API Symfony de backend dans l'autre dépot, qui se lance par défaut sur le port 8000.
|
||||
|
||||
# Pas de config docker
|
||||
Nous n'avons pas actuellement de config docker qui solutionnerait tout ça, les merge request sont les bienvenues! :)
|
21
doc/cadrage/accessibility.md
Normal file
21
doc/cadrage/accessibility.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Accessibilité
|
||||
Vérifiez toujours que ce que vous développez est Accessible pour des personnes aux handicaps divers tels que le préconise le WECAG.
|
||||
https://www.w3.org/Translations/WCAG20-fr/
|
||||
|
||||
Quelques lignes à vérifier:
|
||||
* Couleurs contrastées
|
||||
* ordre des liens cohérent
|
||||
* textes des liens éloquents
|
||||
* textes alternatifs aux images les décrivant
|
||||
* possibilité de changer la taille des textes et le thème visuel
|
||||
* couleurs lisibles pour les personnes daltoniennes
|
||||
* raccourcis claviers. alt+t pour passer a un autre thème visuel par exemple.
|
||||
* attributs aria pour les lecteurs d'écran
|
||||
* performance de chargement pour les petits débits
|
||||
* server side rendering
|
||||
* pas d'éléments perdus en dehors de l'écran, faites du mobile first
|
||||
* parcimonie: si un élément n'est pas prévu pour être montré sur petit écran, ne le mettez pas sur grand écran sous prétexte qu'il y a plus de place.
|
||||
* less is more
|
||||
* système de recherche basique
|
||||
|
||||
N'ayez pas peur de demander de l'aide
|
13
doc/cadrage/architecture.md
Normal file
13
doc/cadrage/architecture.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Architecture de Framadate
|
||||
* Documentation de référence dans le dossier "doc" de ce dépot.
|
||||
* Frontend généré avec Angular CLI
|
||||
* chaque commit sur ce dépot passe les contrôles de format de Husky avant de pouvoir être envoyé.
|
||||
* Doc sur l'api du nouveau backend à utiliser pour les appels REST du front funky : https://framagit.org/tykayn/date-poll-api/wikis/home
|
||||
toujours se référer à la sortie de la commande de symfony `bin/console debug:router` du dépot back.
|
||||
|
||||
ancien backend en PHP sans framework ici pour mémoire : https://framagit.org/framasoft/framadate/framadate
|
||||
|
||||
# Schéma de base de données du backend
|
||||
https://framagit.org/framasoft/framadate/funky-framadate-front/-/wikis/uploads/9d9a82cc6d7c6efea073b64d3667dc9b/framadate-api.png
|
||||
|
||||
png créé avec sqlfairy https://www.cipherbliss.com/exporter-une-visualisation-de-son-schma-sql/
|
56
doc/cadrage/backend-api-endpoints-doc.txt
Normal file
56
doc/cadrage/backend-api-endpoints-doc.txt
Normal file
@ -0,0 +1,56 @@
|
||||
// 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}
|
||||
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)
|
||||
* -------------------------- -------- -------- ------ ------------------------------------------------
|
||||
Name Method Scheme Host Path
|
||||
-------------------------- -------- -------- ------ ------------------------------------------------
|
||||
api_get_poll_comment GET ANY ANY /polls/{id}/comment
|
||||
api_delete_poll_comments DELETE ANY ANY /polls/{id}/comment
|
||||
api_user_polls_send_by_email GET ANY ANY /users/{email}/polls/send-by-email
|
||||
api_get_user_polls GET ANY ANY /users/{email}/polls
|
||||
api_get_poll_slug GET ANY ANY /polls/slug/{id}/{token}
|
||||
api_clean_expired_polls GET ANY ANY /admin/clean-polls/{token}
|
||||
|
||||
api_test-mail-polls GET ANY ANY /polls/mail/test-mail-polls/{emailChoice}
|
||||
api_update_vote_stack PATCH ANY ANY /polls/{slug}/answers/{pseudo}/token/{modifierToken}
|
||||
|
||||
-------------------------- -------- -------- ------ ------------------------------------------------
|
||||
*/
|
13
doc/cadrage/i18n.md
Normal file
13
doc/cadrage/i18n.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Internationalisation - i18n
|
||||
Toutes les chaînes de texte doivent être disponible en minimum deux langues: Français et Anglais.
|
||||
|
||||
La documentation a été pensée pour être compréhensible en premier lieu par des personnes francophones, le projet étant issu de Framasoft et de personnes uniquement Francophones, nous avons jugé que c'était le moyen le plus efficace pour le faire grandir.
|
||||
|
||||
Voir les fichiers src/assets/i18n [EN.json](/src/assets/i18n/EN.json) et [FR.json](/src/assets/i18n/FR.json)
|
||||
La traduction se base sur un système de clés-valeur.
|
||||
Les clés sont entrées dans les templates html, et c'est la config d'Angular qui les traduit selon la langue demandée par le visiteur du site.
|
||||
|
||||
Chaque fichier de traduction est déclaré dans le [AppModule](../../src/app/app.module.ts) avec le module @ngx-translate. Examinez l'exemple pour rajouter votre propre traduction.
|
||||
|
||||
Utilisez des sous groupes dans vos traductions afin de mieux segmenter les chaines de caractère par page et selon le sens qu'elles sous tendent.
|
||||
|
109
doc/cadrage/specifications-fonctionnelles.md
Normal file
109
doc/cadrage/specifications-fonctionnelles.md
Normal file
@ -0,0 +1,109 @@
|
||||
# Spécifications fonctionnelles de Framadate
|
||||
|
||||
[[_TOC_]]
|
||||
|
||||
|
||||
![Diagramme des cas d'utilisations décrits dans la suite du document](uploads/uml/diagramme_cas_utilisation.png)
|
||||
|
||||
|
||||
# Glossaire
|
||||
|
||||
| Terme | Signification |
|
||||
| :--- | :--- |
|
||||
| Créateur | Personne ayant créé le sondage |
|
||||
| Participant | Personne invitée à participer ou ayant participé |
|
||||
| Archivage | Empêcher votes et commentaires mais conserver les résultats |
|
||||
| _Slug_ | Partie le l'URL identifiant un sondage de manière unique |
|
||||
|
||||
|
||||
# Fonctionnalités actuellement dans framadate legacy et à conserver
|
||||
|
||||
## Généralités
|
||||
|
||||
Il existe deux types de sondage. Le type « dates » est adapté à la proposition de dates (voir [§plages horaires](#plages-horaires)) et le type « texte », plus généraliste, ne contient que du texte. Ce choix est l'un des premiers choix effectués lors de la création d'un nouveau sondage.
|
||||
|
||||
Deux types d'acteurs sont distingués : la personne ayant créé le sondage et les personnes qui répondent au sondage. Ces deux types d'acteurs n'ont pas les mêmes droits sur le sondage : la personne ayant créé le sondage est la seule ayant des droits d'administration sur celui-ci, en plus des droits des participants. En particulier, les droits d'administration permettent d'ajuster les droits des participants.
|
||||
|
||||
|
||||
## Création d'un sondage
|
||||
|
||||
Il est nécessaire de fournir un email lors de la création du sondage. Cela permet aux créateurs de sondages de pouvoir obtenir via email la liste des sondages qu'ils ont créés.
|
||||
|
||||
Il est possible de choisir le _slug_ dans l'URL du sondage, à condition que ce _slug_ soit disponible.
|
||||
|
||||
Le sondage reste modifiable après sa création.
|
||||
|
||||
|
||||
### Plages horaires
|
||||
|
||||
Les sondages de type « dates » permettent de proposer des jours et des plages horaires pour chaque journée.
|
||||
|
||||
Pour faciliter la saisie des dates proposées, il est demandé si les plages horaires sont les mêmes chaque jour ou si elles sont différentes selon les jours. Dans le premier cas, les plages horaires sont demandées uniquement pour le premier jour et sont automatiquement reproduites sur les autres jours.
|
||||
|
||||
Toujours pour faciliter la saisie des propositions, il est possible d'ajouter plusieurs jours consécutifs en sélectionnant le premier et le dernier jour plutôt qu'en sélectionnant chaque jour un à un.
|
||||
|
||||
|
||||
## Participation à un sondage
|
||||
|
||||
### Votes
|
||||
|
||||
Les participants n'ont pas besoin de créer un compte ou de fournir leur email pour participer à un sondage.
|
||||
|
||||
Le créateur du sondage peut protéger le sondage par un mot de passe. Sans ce mot de passe, il est impossible de voir le sondage, donc impossible d'y voter. Attention cependant, le sondage est stocké en clair dans la base de données et ne bénéficie donc d'aucun chiffrement. *# Le mot de passe, lui, est-il chiffré ?*
|
||||
|
||||
Le créateur du sondage choisit de permettre ou non les modifications des votes sur le sondage. Il a le choix entre les trois formules suivantes :
|
||||
|
||||
* ne pas permettre de modifier de réponse ;
|
||||
* permettre de modifier uniquement sa propre réponse a un sondage (modalités d'identification encore non déterminées) ;
|
||||
* permettre de modifier toute réponse à un sondage (y compris celles des autres).
|
||||
|
||||
|
||||
### Commentaires
|
||||
|
||||
Les participants au sondage ont la possibilité de créer des commentaires sur le sondage. Seule la personne ayant les droits d'administration sur le sondage peut modifier les commentaires.
|
||||
|
||||
|
||||
## Résultats
|
||||
|
||||
Selon la configuration du sondage, les résultats peuvent n'être accessibles qu'à la personne ayant un accès d'administration ou bien être publics. Les personnes pouvant accéder aux résultats du sondage peuvent exporter ces résultats au format CSV.
|
||||
|
||||
|
||||
## Emails
|
||||
|
||||
Lorsqu'une personne crée un sondage, un email reprenant les informations du sondage ainsi que les URL uniques servant à le modifier (lien d'administration) et à y participer (lien à transmettre aux participants) lui est envoyé. De plus, elle peut choisir de recevoir des emails lors d'un nouveau vote ou commentaire sur le sondage.
|
||||
|
||||
Une personne ayant créé au moins un sondage peut demander à recevoir par email la liste des sondages qu'elle a créés en utilisant cette adresse email.
|
||||
|
||||
|
||||
## Stockage et export de données
|
||||
|
||||
Les données sont stockées en clair sur le serveur. L'archivage (resp. suppression) automatique des sondages est effectué via cronjob 90 (resp. 120) jours après sa création. Il est possible pour l'administrateur d'un sondage de modifier la durée avant archivage et la suppression automatique suit toujours de 30 jours la date d'archivage.
|
||||
|
||||
L'export d'un sondage et des résultats d'un sondage est possible au format CSV.
|
||||
|
||||
|
||||
# Nouveautés principales
|
||||
|
||||
* Accessibilité renforcée.
|
||||
* Traduction dynamique de toutes les phrases en choisissant la langue dans le menu.
|
||||
* Adapté aussi bien sur mobile que grands écrans à haute ou faible densité de pixels.
|
||||
* Anti-spam de commentaires.
|
||||
* Anti-spam de vote.
|
||||
* Tests unitaires et end-to-end.
|
||||
* Couverture de test.
|
||||
|
||||
|
||||
# Nouveautés secondaires
|
||||
|
||||
* Choix de réponses possibles. Proposer de ne répondre que « oui » ou rien, ou aller dans la nuance en proposant « oui », « peut-être », « non », « ? ». *# Redondance ou le choix de réponses possibles de la première phrase concerne un autre choix ?*
|
||||
* Insertion d'images dans le sondage de type texte, avec des URL uniquement. Une seule image par title possible ou rien.
|
||||
* Thème sombre.
|
||||
* Boutons pour copier dans le presse-papier les liens publics et privés / admin des sondages.
|
||||
* Limiter le nombre de participants maximum
|
||||
|
||||
|
||||
# Idées pour de futures améliorations (pertinence à vérifier)
|
||||
|
||||
* Gagner en vie privée en chiffrant certaines informations ?
|
||||
* À réfléchir : permettre à Framadate de faire entrer à des gens plusieurs plages de temps de disponibilité et le service déduit quelles sont les plages de temps favorables (calcul d'intersection sur des lignes discontinues). Cela pourrait être avec divers niveaux de détail. Comme https://omnipointment.com/ (qui est un logiciel privateur)
|
||||
* SSO du fédiverse ?
|
14
doc/customisation.md
Normal file
14
doc/customisation.md
Normal file
@ -0,0 +1,14 @@
|
||||
# Personnalisation
|
||||
Vous pouvez modifier les variables d'environnement afin de modifier le logo et le titre de votre installation.
|
||||
Ce logiciel étant libre et sous la licence GNU Affero V3 vous pouvez bien entendu le modifier à volonté. Voici donc un guide concernant le titre et le logo.
|
||||
|
||||
Modifiez le fichier `src/environment.prod.ts`, et remplacez le contenu des variables `appTitle` (un texte, pas forcément un seul mot) et `appLogo` (une URL absolue ou relative d'image)
|
||||
Et voilà!
|
||||
|
||||
Quand vous builderez votre app, vous verrez ces valeurs dans la barre titre, incluses par le composant HeaderComponent automatiquement.
|
||||
|
||||
Vous pouvez modifier les valeurs dans le fichier `src/environment.ts` pour voir ce que cela donne en environnement de développement en lancant l'app avec `npm start` ou `yarn start`.
|
||||
|
||||
#Customization
|
||||
Change the content of the file `src/environment.prod.ts`, and the content of the vars `appTitle` (any text) and `appLogo` (any picture URL, relative or absolute) .
|
||||
And voilà!
|
39
doc/reunions/0_blank_notes.md
Normal file
39
doc/reunions/0_blank_notes.md
Normal file
@ -0,0 +1,39 @@
|
||||
Framadate suivi - (__date de réunion__)
|
||||
========================================
|
||||
###### tags: #framadate, #suivi
|
||||
|
||||
> Participants à la réunion:
|
||||
*
|
||||
*
|
||||
*
|
||||
|
||||
## État des lieux
|
||||
|
||||
### > Où on en est
|
||||
https://framagit.org/framasoft/framadate/funky-framadate-front/-/boards
|
||||
> Quel est le projet ? Quels sont les enjeux ?
|
||||
> Quelles méthodes de travail ?
|
||||
> Quel niveau de qualité ? standards et de bonnes pratiques ?
|
||||
> *
|
||||
> Quelles priorités ?
|
||||
> *
|
||||
> Qui veut faire quoi ?
|
||||
> *
|
||||
|
||||
## Notes de réunion
|
||||
-
|
||||
|
||||
## Trucs à faire
|
||||
-
|
||||
|
||||
## Décisions prises
|
||||
-
|
||||
|
||||
## Ressources
|
||||
|
||||
* Discussion : https://framateam.org/ux-framatrucs/channels/framadate
|
||||
* Repo front/dev : https://framagit.org/framasoft/framadate/funky-framadate-front/tree/dev
|
||||
* Repo back : https://framagit.org/framasoft/framadate/framadate
|
||||
* Maquettes Zeplin : demander l'accès à maiwann
|
||||
* La démo : https://framadate-api.cipherbliss.com/
|
||||
* Vidéo de présentation : https://nuage.maiwann.net/s/JRRHTR9D2akMAa7
|
129
doc/reunions/2019_08_09.md
Normal file
129
doc/reunions/2019_08_09.md
Normal file
@ -0,0 +1,129 @@
|
||||
# notes du 9/8/2019 avec Mumble
|
||||
|
||||
À discuter :
|
||||
|
||||
discussion via framatalk ? plutôt mumble !
|
||||
|
||||
Se connecter à mumble.tcit.fr. Il n'a pas de mot de passe et il y a un salon Framadate ouvert.
|
||||
|
||||
|
||||
qui peut/veut faire quoi ?
|
||||
|
||||
tykayn: dev front end, JS & styles
|
||||
|
||||
newick : intégrateur dans la vraie vie
|
||||
|
||||
llaq : plutôt HTML
|
||||
|
||||
talone : plutôt coté JS
|
||||
|
||||
|
||||
on ne s'occupe toujours que du front ?
|
||||
|
||||
[newick] Est-ce que tout le monde est ok sur le fait de faire que le front et pas tout ?
|
||||
|
||||
llaq : pas de souci avec le back, donc pourquoi le changer ?
|
||||
|
||||
Ok on y touche pas
|
||||
|
||||
|
||||
repartir de 0 ?
|
||||
|
||||
Je parle de funky framadate
|
||||
|
||||
llaq : Pourquoi pas recommencer car c'est bien différent des maquettes
|
||||
|
||||
newick : On est toujours en mode "MVP", on fait ça pour tester les maquettes et ensuite seulement on le connecte au back-end ?
|
||||
|
||||
maiwann : Ouip tant que ça reste cohérent niveau expérience de l'utilisateur
|
||||
|
||||
newick : C'est plutot coté JS qu'il faut voir… talone ?
|
||||
|
||||
Talone : on peut faire des choses, je maitrise pas php
|
||||
|
||||
llaq : moi non plus
|
||||
|
||||
talone : il y a des contributeurices de framadate ici ?
|
||||
|
||||
maiwann : nope :x
|
||||
|
||||
newick : on peut avancer comme ça, surtout sur le front et ensuite on verra le back ?
|
||||
|
||||
talone & llaq : oui on peut faire ça
|
||||
|
||||
|
||||
[tentative de tykayn de nous rejoindre]
|
||||
|
||||
pourquoi y'a des pages php dans le dossier front end ?
|
||||
|
||||
newick : C'est dans le dossier principal, pas dans funky
|
||||
|
||||
maiwann : donc a priori rien d'obligatoire ?
|
||||
|
||||
|
||||
API de backend existe t elle?
|
||||
|
||||
llaq : nope ^_^
|
||||
|
||||
|
||||
|
||||
|
||||
Sass, css, autre ?
|
||||
|
||||
https://css2sass.herokuapp.com/ pour convertir le css actuel vers du sass
|
||||
|
||||
|
||||
|
||||
|
||||
intérêt de Bootstrap / frameworks ?
|
||||
|
||||
Angular cli pour se concentrer sur des composants front end
|
||||
|
||||
Boostrap a priori c'est pas forcément nécessaire, newick assez à l'aise pour ne pas en avoir besoin dans HTML / CSS
|
||||
|
||||
Composants <3 Atomic design <3
|
||||
|
||||
tykayn : propose de faire une démo
|
||||
|
||||
(Beaucoup de discussion sur les framework front, j'ai pas tout compris ce qu'il se passait donc mes notes sont très limitées !)
|
||||
|
||||
En gros, on fait du Angular et chacun intègre son truc indépendamment
|
||||
|
||||
|
||||
|
||||
Invitation sur funkyframadate, on repart de 0 !
|
||||
La suite
|
||||
remplissons le wiki et les board d'issues
|
||||
on zieute la démo et on voit ce qu'on fait ensuite
|
||||
|
||||
|
||||
--------
|
||||
# Questions notées par Come.
|
||||
Suite au visionnage de la vidéo de présentation de maiwann (Framadate_Presentation_maquettes.flv), des questions ont été levées :
|
||||
|
||||
|
||||
* Quel est l'intérêt de différentier les dates limites de modification et vote ? Pourquoi appelle-t-on cela archivage ?
|
||||
* Peut-on écrire plus explicitement que le nom du créateur, s'il est renseigné, est affiché à ceux qui répondent ?
|
||||
* Lors de la validation du vote, peut-on mettre davantage en avant l'importance de l'URL d'édition afin d'éviter la fermeture machinale (par réflexe) du popup ?
|
||||
* Pour la vue des réponses, pourquoi ne pas mettre pour tous les utilisateurs les petites icônes actuellement réservées aux daltoniens ? On économiserait alors un champ.
|
||||
* Quitte à faire la chasse aux clics, pour les menus déroulants à deux options (du type « je veux » / « je ne veux pas ») qui nécessitent deux clics pour commuter, pourquoi pas une commutation en un clic ; comme une check-box mais avec du texte à la place du check ? Bien-sûr, ceci à condition que les champs conditionnels ne soient pas vidés lorsqu'ils disparaissent.
|
||||
|
||||
|
||||
* Y a-t-il des champs obligatoires ? Si oui, comment les matérialiser ?
|
||||
* En cas d'erreur à la validation du formulaire (pour cause de champ vide ou format incorrect par exemple), on affiche systématiquement le message d'erreur en orange (cf. écran sondage_date_intervalle) en dessous du champ concerné ?
|
||||
* Quel est l'état de focus sur les champs ? Celui par défaut du navigateur ?
|
||||
* Étant donné qu'il y a plusieurs étapes dans la création du sondage, ne faudrait-il pas les indiquer au début de la page ? Ex : 1/3, 2/3, 3/3 (je tire ça des bonnes pratiques opquast)
|
||||
* Dans le récapitulatif à la fin, pourquoi ne rappelle-t-on pas également le titre et la description du sondage créé ?
|
||||
|
||||
Les boutons :
|
||||
|
||||
* Quel est l'état de focus sur les boutons ?
|
||||
|
||||
* Sur l'écran « Mot de passe », le bouton « Voir » est-il immédiatement actif ou on attend d'avoir tapé la première lettre ? Une fois qu'on a cliqué sur « Voir », est-ce que le contenu du bouton devient « Masquer » ?
|
||||
|
||||
* Dans ta vidéo tu précises que le popup pour ajouter l'intervalle de dates se situe en dessous du calendrier pour qu'on puisse voir les dates préalablement sélectionnées, mais je pense que sur mobile ça va être compliqué : ça signifierait que le popup se situe très bas dans l'écran, il n'y aurait que le titre visible sur les téléphones de petite taille.
|
||||
|
||||
* Dans les écrans de réponse au sondage date, quelle est la règle pour l'abbréviation des dates ? Des fois c'est le jour qui est abrégé, des fois le mois.
|
||||
|
||||
* On part sur une version desktop à partir de 768px ? Je demande, car tes maquettes desktop font 602 px de large et je ne sais pas si c'est délibéré ou non.
|
||||
|
68
doc/reunions/2020_08_09.md
Normal file
68
doc/reunions/2020_08_09.md
Normal file
@ -0,0 +1,68 @@
|
||||
Framadate suivi - Avril 2020
|
||||
==
|
||||
###### tags: `framadate` `suivi`
|
||||
|
||||
> Participants à la réunion: Maiwann / Tykain / Côme / Seraf
|
||||
|
||||
## État des lieux
|
||||
|
||||
> Où on en est sur la version funky ?
|
||||
|
||||
https://framagit.org/framasoft/framadate/funky-framadate-front/-/boards
|
||||
|
||||
> Quel est le projet ? Quels sont les enjeux ?
|
||||
- Avoir un truc utilisable (sur téléphone !)
|
||||
- Graphiquement plus élégant
|
||||
|
||||
> Quelles méthodes de travail ?
|
||||
- Une refonte ergonomique avant de faire des tests utilisateurs pour vérifier que la nouvelle interface est chouette
|
||||
|
||||
> Quel niveau de qualité ? standards et de bonnes pratiques ?
|
||||
- Une refonte graphique, utilisable sur mobile
|
||||
- De l'internationalisation
|
||||
- Un logiciel accessible
|
||||
|
||||
> Quelles priorités ?
|
||||
- Cycle de vote à finir
|
||||
|
||||
> Qui veut faire quoi ?
|
||||
|
||||
|
||||
## Notes de réunion
|
||||
|
||||
> Le nom semble peu approprié depuis l'apparition des nouvelles fonctionnalités.
|
||||
|
||||
Suggestions :
|
||||
- Framapool
|
||||
- FramEnquête
|
||||
- Framasondage
|
||||
|
||||
> Idée de nouvelle fonctionnalité (pour plus tard hein :D )
|
||||
|
||||
- Chaque utilisateur indique ses disponibilités et le service renvoie les plages pour lesquelles le plus de personnes sont disponibles.
|
||||
|
||||
## Trucs à faire
|
||||
|
||||
- Gitlab CI : exécuter les tests automatiquement
|
||||
- envoi de mail au créateur du sondage
|
||||
- cafouillage entre front & back quand édition d'un sondage (à préciser par tykain)
|
||||
- vérifier l'accesibilité du formulaire
|
||||
- prompt de modale pour accès par mot de passe a un sondage privé
|
||||
|
||||
## Remarques
|
||||
|
||||
Il est difficile d'entrer dans le projet en absence de spécifications écrites. (seraf)
|
||||
|
||||
## Décisions prises
|
||||
|
||||
- Vérifier que chacun.e arrive à faire tourner le projet
|
||||
- Chacune choisit des issues / tickets adaptés à ce qu'il a envie de faire
|
||||
|
||||
## Ressources
|
||||
|
||||
* Discussion : https://framateam.org/ux-framatrucs/channels/framadate
|
||||
* Repo front/dev : https://framagit.org/framasoft/framadate/funky-framadate-front/tree/dev
|
||||
* Repo back : https://framagit.org/framasoft/framadate/framadate
|
||||
* Maquettes Zeplin : https://scene.zeplin.io/project/5d4d83d68866d6522ff2ff10
|
||||
* La démo : https://framadate-api.cipherbliss.com/
|
||||
* Vidéo de présentation : https://nuage.maiwann.net/s/JRRHTR9D2akMAa7
|
32
e2e/protractor.conf.js
Normal file
32
e2e/protractor.conf.js
Normal file
@ -0,0 +1,32 @@
|
||||
// @ts-check
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
|
||||
|
||||
const { SpecReporter } = require('jasmine-spec-reporter');
|
||||
|
||||
/**
|
||||
* @type { import("protractor").Config }
|
||||
*/
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
capabilities: {
|
||||
'browserName': 'chrome'
|
||||
},
|
||||
directConnect: true,
|
||||
baseUrl: 'http://localhost:4200/',
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
print: function() {}
|
||||
},
|
||||
onPrepare() {
|
||||
require('ts-node').register({
|
||||
project: require('path').join(__dirname, './tsconfig.json')
|
||||
});
|
||||
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
|
||||
}
|
||||
};
|
23
e2e/src/app.e2e-spec.ts
Normal file
23
e2e/src/app.e2e-spec.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { AppPage } from './app.po';
|
||||
import { browser, logging } from 'protractor';
|
||||
|
||||
describe('workspace-project App', () => {
|
||||
let page: AppPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new AppPage();
|
||||
});
|
||||
|
||||
it('should display welcome message', () => {
|
||||
page.navigateTo();
|
||||
expect(page.getTitleText()).toEqual('Welcome to framadate!');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Assert that there are no errors emitted from the browser
|
||||
const logs = await browser.manage().logs().get(logging.Type.BROWSER);
|
||||
expect(logs).not.toContain(jasmine.objectContaining({
|
||||
level: logging.Level.SEVERE,
|
||||
} as logging.Entry));
|
||||
});
|
||||
});
|
11
e2e/src/app.po.ts
Normal file
11
e2e/src/app.po.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { browser, by, element } from 'protractor';
|
||||
|
||||
export class AppPage {
|
||||
navigateTo() {
|
||||
return browser.get(browser.baseUrl) as Promise<any>;
|
||||
}
|
||||
|
||||
getTitleText() {
|
||||
return element(by.css('app-root h1')).getText() as Promise<string>;
|
||||
}
|
||||
}
|
9
e2e/tsconfig.json
Normal file
9
e2e/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../out-tsc/e2e",
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"types": ["jasmine", "jasminewd2", "node"]
|
||||
}
|
||||
}
|
5
index.lokalize
Normal file
5
index.lokalize
Normal file
@ -0,0 +1,5 @@
|
||||
[General]
|
||||
LangCode=fr_FR
|
||||
PotBaseDir=src/assets/i18n
|
||||
ProjectID=funky-framadate-front
|
||||
TargetLangCode=fr_FR
|
14
lokalize-scripts/scripts.rc
Normal file
14
lokalize-scripts/scripts.rc
Normal file
@ -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
Normal file
4
main.lqa
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<qa version="1.0">
|
||||
<category name="default"/>
|
||||
</qa>
|
165
mocks/db.json
Normal file
165
mocks/db.json
Normal file
@ -0,0 +1,165 @@
|
||||
{
|
||||
"owners": [
|
||||
{ "id": 1, "email": "titi@gafam.com", "pseudo": "TITI", "role": "REGISTERED" },
|
||||
{ "id": 2, "email": "toto@gafam.com", "pseudo": "TOTO", "role": "REGISTERED" }
|
||||
],
|
||||
"polls": [
|
||||
{
|
||||
"id": 1,
|
||||
"slug": "picnic",
|
||||
"configuration": {
|
||||
"id": 1,
|
||||
"isAboutDate": true,
|
||||
"isProtectedByPassword": false,
|
||||
"isOwnerNotifiedByEmailOnNewVote": false,
|
||||
"isOwnerNotifiedByEmailOnNewComment": false,
|
||||
"isMaybeAnswerAvailable": true,
|
||||
"areResultsPublic": true,
|
||||
"dateCreated": "2020-05-17",
|
||||
"expires": "2020-12-31"
|
||||
},
|
||||
"ownerId": 1,
|
||||
"title": "Quelle date pour le picnic ?",
|
||||
"description": "Gros badass picnic en plein air ! Come on !"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"slug": "vacances",
|
||||
"configuration": {
|
||||
"id": 2,
|
||||
"isAboutDate": true,
|
||||
"isProtectedByPassword": false,
|
||||
"isOwnerNotifiedByEmailOnNewVote": false,
|
||||
"isOwnerNotifiedByEmailOnNewComment": false,
|
||||
"isMaybeAnswerAvailable": true,
|
||||
"areResultsPublic": true,
|
||||
"dateCreated": "2020-05-17",
|
||||
"expires": "2020-11-30"
|
||||
},
|
||||
"ownerId": 2,
|
||||
"title": "On fait quoi pendant les vacances ?",
|
||||
"description": "Vacances en famille"
|
||||
}
|
||||
],
|
||||
"choices": [
|
||||
{
|
||||
"id": 1,
|
||||
"pollId": 1,
|
||||
"pollSlug": "picnic",
|
||||
"label": "samedi",
|
||||
"participants": [
|
||||
[
|
||||
"YES",
|
||||
[
|
||||
{ "pseudo": "TOTO", "token": "TOTO-TOKEN" },
|
||||
{ "pseudo": "TITI", "token": "TITI-TOKEN" }
|
||||
]
|
||||
],
|
||||
["NO", []],
|
||||
["MAYBE", [{ "pseudo": "TATA", "token": "TATA-TOKEN" }]]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"pollId": 1,
|
||||
"pollSlug": "picnic",
|
||||
"label": "dimanche",
|
||||
"participants": [
|
||||
[
|
||||
"YES",
|
||||
[
|
||||
{ "pseudo": "TATA", "token": "TATA-TOKEN" },
|
||||
{ "pseudo": "TETE", "token": "TETE-TOKEN" }
|
||||
]
|
||||
],
|
||||
["NO", [{ "pseudo": "TOTO", "token": "TOTO-TOKEN" }]],
|
||||
["MAYBE", [{ "pseudo": "TITI", "token": "TITI-TOKEN" }]]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"pollId": 2,
|
||||
"pollSlug": "vacances",
|
||||
"label": "bateau",
|
||||
"participants": [
|
||||
[
|
||||
"YES",
|
||||
[
|
||||
{ "pseudo": "TOTO", "token": "TOTO-TOKEN" },
|
||||
{ "pseudo": "TITI", "token": "TITI-TOKEN" }
|
||||
]
|
||||
],
|
||||
["NO", [{ "pseudo": "TETE", "token": "TETE-TOKEN" }]],
|
||||
["MAYBE", [{ "pseudo": "TATA", "token": "TATA-TOKEN" }]]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"pollId": 2,
|
||||
"pollSlug": "vacances",
|
||||
"label": "montagne",
|
||||
"participants": [
|
||||
[
|
||||
"YES",
|
||||
[
|
||||
{ "pseudo": "TOTO", "token": "TOTO-TOKEN" },
|
||||
{ "pseudo": "TITI", "token": "TITI-TOKEN" }
|
||||
]
|
||||
],
|
||||
["NO", [{ "pseudo": "TETE", "token": "TETE-TOKEN" }]],
|
||||
["MAYBE", [{ "pseudo": "TATA", "token": "TATA-TOKEN" }]]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"pollId": 2,
|
||||
"pollSlug": "vacances",
|
||||
"label": "quad",
|
||||
"participants": [
|
||||
[
|
||||
"YES",
|
||||
[
|
||||
{ "pseudo": "TOTO", "token": "TOTO-TOKEN" },
|
||||
{ "pseudo": "TITI", "token": "TITI-TOKEN" }
|
||||
]
|
||||
],
|
||||
["NO", [{ "pseudo": "TETE", "token": "TETE-TOKEN" }]],
|
||||
["MAYBE", [{ "pseudo": "TATA", "token": "TATA-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
|
||||
}
|
||||
]
|
||||
}
|
8
mocks/routes.json
Normal file
8
mocks/routes.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"/api/v1/*": "/$1",
|
||||
"/owners/:email/": "/owners?email=:email",
|
||||
"/polls": "/polls?_expand=owner&_embed=choices&_embed=comments",
|
||||
"/polls/:slug": "/polls?slug=:slug&_expand=owner&_embed=choices&_embed=comments",
|
||||
"/polls/:slug/choices": "/choices?pollSlug=:slug",
|
||||
"/polls/:slug/comments": "/comments?pollSlug=:slug"
|
||||
}
|
20900
package-lock.json
generated
Normal file
20900
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
121
package.json
Normal file
121
package.json
Normal file
@ -0,0 +1,121 @@
|
||||
{
|
||||
"name": "framadate-funky-frontend",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build:prod": "ng build --prod",
|
||||
"build:prod:stats": "ng build --prod --stats-json",
|
||||
"build:prod:gitlabpage": "ng build --prod --baseHref=/framadate/funky-framadate-front/",
|
||||
"build:prod:demobliss": "ng build --prod --baseHref=https://framadate-api.cipherbliss.com",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:ci": "jest --ci",
|
||||
"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",
|
||||
"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\"",
|
||||
"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",
|
||||
"@angular/cdk": "^9.2.2",
|
||||
"@angular/common": "^9.0.7",
|
||||
"@angular/compiler": "^9.0.7",
|
||||
"@angular/core": "^9.0.7",
|
||||
"@angular/forms": "^9.0.7",
|
||||
"@angular/localize": "^9.1.1",
|
||||
"@angular/material": "^9.2.4",
|
||||
"@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",
|
||||
"angular-date-value-accessor": "^1.0.2",
|
||||
"axios": "^0.19.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",
|
||||
"primeng": "^9.0.6",
|
||||
"quill": "^1.3.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"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/jest": "^9.0.1",
|
||||
"@angular-devkit/build-angular": "^0.901.2",
|
||||
"@angular/cli": "^9.1.2",
|
||||
"@angular/compiler-cli": "^9.1.1",
|
||||
"@angular/language-service": "^9.0.7",
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/preset-env": "^7.9.5",
|
||||
"@babel/preset-typescript": "^7.9.0",
|
||||
"@compodoc/compodoc": "^1.1.11",
|
||||
"@types/jest": "^26.0.0",
|
||||
"@types/node": "^14.0.1",
|
||||
"@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": "^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": "~7.0.0",
|
||||
"ts-jest": "^26.0.0",
|
||||
"ts-mockito": "^2.5.0",
|
||||
"ts-node": "^8.10.1",
|
||||
"typescript": "<3.9.0"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{js,jsx,ts,tsx,md,html,css,scss}": [
|
||||
"prettier --write",
|
||||
"git add"
|
||||
],
|
||||
"*.js": [
|
||||
"prettier --write"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-preset-angular",
|
||||
"setupFilesAfterEnv": [
|
||||
"<rootDir>/src/jest.setup.ts"
|
||||
],
|
||||
"testEnvironment": "jest-environment-jsdom-sixteen",
|
||||
"transform": {
|
||||
"^.+\\.(ts|html)$": "ts-jest",
|
||||
"^.+\\.jsx?$": "babel-jest"
|
||||
}
|
||||
}
|
||||
}
|
11
proxy.conf.json
Normal file
11
proxy.conf.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"/api/v1/*": {
|
||||
"target": "http://localhost:8000",
|
||||
"secure": false,
|
||||
"pathRewrite": {
|
||||
"^/api/v1": ""
|
||||
},
|
||||
"changeOrigin": false,
|
||||
"logLevel": "debug"
|
||||
}
|
||||
}
|
14
src/app/app-routing.module.ts
Normal file
14
src/app/app-routing.module.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import {routes} from "./routes-framadate";
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(routes, {
|
||||
// enableTracing: true, // <-- debugging purposes only
|
||||
}),
|
||||
],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AppRoutingModule {}
|
23
src/app/app.component.html
Normal file
23
src/app/app.component.html
Normal file
@ -0,0 +1,23 @@
|
||||
<mat-sidenav-container (backdropClick)="sidenav.toggle()">
|
||||
<mat-sidenav #sidenav mode="side">
|
||||
<app-navigation></app-navigation>
|
||||
</mat-sidenav>
|
||||
|
||||
<mat-sidenav-content>
|
||||
<div id="big_container" [class]="themeClass">
|
||||
<div class="container">
|
||||
<app-header [appTitle]="appTitle" [appLogo]="appLogo"></app-header>
|
||||
<main>
|
||||
<router-outlet></router-outlet>
|
||||
<div *ngIf="devModeEnabled">
|
||||
<br />
|
||||
<mat-slide-toggle (change)="sidenav.toggle()" label="dev menu"> </mat-slide-toggle> menu
|
||||
développeur
|
||||
</div>
|
||||
</main>
|
||||
<app-footer></app-footer>
|
||||
<app-feedback></app-feedback>
|
||||
</div>
|
||||
</div>
|
||||
</mat-sidenav-content>
|
||||
</mat-sidenav-container>
|
0
src/app/app.component.scss
Normal file
0
src/app/app.component.scss
Normal file
31
src/app/app.component.spec.ts
Normal file
31
src/app/app.component.spec.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { TestBed, async } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterTestingModule],
|
||||
declarations: [AppComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
it('should create the app', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'framadate'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app.title).toEqual('framadate');
|
||||
});
|
||||
|
||||
it('should render title in a h1 tag', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
const compiled = fixture.debugElement.nativeElement;
|
||||
expect(compiled.querySelector('h1').textContent).toContain('Welcome to framadate!');
|
||||
});
|
||||
});
|
62
src/app/app.component.ts
Normal file
62
src/app/app.component.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
import { environment } from '../environments/environment';
|
||||
import { Theme } from './core/enums/theme.enum';
|
||||
import { LanguageService } from './core/services/language.service';
|
||||
import { ThemeService } from './core/services/theme.service';
|
||||
import { MockingService } from './core/services/mocking.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
})
|
||||
export class AppComponent implements OnInit, OnDestroy {
|
||||
public appTitle: string = environment.appTitle;
|
||||
public appLogo: string = environment.appLogo;
|
||||
public themeClass: string;
|
||||
public isSidebarOpened = false;
|
||||
public devModeEnabled = !environment.production;
|
||||
private themeSubscription: Subscription;
|
||||
|
||||
constructor(
|
||||
private titleService: Title,
|
||||
private themeService: ThemeService,
|
||||
private languageService: LanguageService // private mockingService: MockingService
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (!environment.production) {
|
||||
this.appTitle += ' [DEV]';
|
||||
}
|
||||
this.titleService.setTitle(this.appTitle);
|
||||
this.languageService.configureAndInitTranslations();
|
||||
this.themeSubscription = this.themeService.theme.subscribe((theme: Theme) => {
|
||||
switch (theme) {
|
||||
case Theme.DARK:
|
||||
this.themeClass = 'theme-dark-crystal';
|
||||
break;
|
||||
case Theme.CONTRAST:
|
||||
this.themeClass = 'theme-hot-covid';
|
||||
break;
|
||||
case Theme.RED:
|
||||
this.themeClass = 'theme-hot-covid';
|
||||
break;
|
||||
default:
|
||||
this.themeClass = 'theme-light-watermelon';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.themeSubscription) {
|
||||
this.themeSubscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
public toggleSidebar(status: boolean): void {
|
||||
this.isSidebarOpened = status === true;
|
||||
}
|
||||
}
|
71
src/app/app.module.ts
Normal file
71
src/app/app.module.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { 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';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import {
|
||||
MissingTranslationHandler,
|
||||
MissingTranslationHandlerParams,
|
||||
TranslateLoader,
|
||||
TranslateModule,
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
import { ClipboardModule } from 'ngx-clipboard';
|
||||
import { MarkdownModule } from 'ngx-markdown';
|
||||
import { NgxWebstorageModule } from 'ngx-webstorage';
|
||||
|
||||
import { environment } from '../environments/environment';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
import { CoreModule } from './core/core.module';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
|
||||
registerLocaleData(localeEn, 'en-EN');
|
||||
registerLocaleData(localeFr, 'fr-FR');
|
||||
|
||||
export class MyMissingTranslationHandler implements MissingTranslationHandler {
|
||||
public handle(params: MissingTranslationHandlerParams): string {
|
||||
return `MISSING TRANSLATION FOR [${params.key}]`;
|
||||
}
|
||||
}
|
||||
|
||||
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
|
||||
return new TranslateHttpLoader(http);
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
imports: [
|
||||
AppRoutingModule,
|
||||
BrowserAnimationsModule,
|
||||
BrowserModule,
|
||||
ClipboardModule,
|
||||
CommonModule,
|
||||
CoreModule,
|
||||
FormsModule,
|
||||
HttpClientModule,
|
||||
MarkdownModule.forRoot(),
|
||||
NgxWebstorageModule.forRoot({ prefix: environment.localStorage.key }),
|
||||
SharedModule,
|
||||
TranslateModule.forRoot({
|
||||
defaultLanguage: 'FR',
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: HttpLoaderFactory,
|
||||
deps: [HttpClient],
|
||||
},
|
||||
missingTranslationHandler: {
|
||||
provide: MissingTranslationHandler,
|
||||
useClass: MyMissingTranslationHandler,
|
||||
},
|
||||
useDefaultLang: true,
|
||||
}),
|
||||
],
|
||||
providers: [Title, TranslateService],
|
||||
bootstrap: [AppComponent],
|
||||
})
|
||||
export class AppModule {}
|
30
src/app/core/components/footer/footer.component.html
Normal file
30
src/app/core/components/footer/footer.component.html
Normal file
@ -0,0 +1,30 @@
|
||||
<footer class="footer">
|
||||
<div class="content has-text-centered">
|
||||
<p>
|
||||
<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
|
||||
>
|
||||
|
|
||||
<a href="https://framagit.org/framasoft/framadate/funky-framadate-front/-/wikis/home">
|
||||
<i class="fa fa-book"></i>
|
||||
Documentation
|
||||
</a>
|
||||
|
|
||||
<a href="https://framateam.org/ux-framatrucs/channels/framadate">
|
||||
<i class="fa fa-comment"></i>
|
||||
canal de discussion Framateam
|
||||
</a>
|
||||
|
|
||||
<a href="https://riot.im/app/#/room/#framadate:matrix.org">
|
||||
<i class="fa fa-matrix-org"></i>
|
||||
canal Matrix
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
4
src/app/core/components/footer/footer.component.scss
Normal file
4
src/app/core/components/footer/footer.component.scss
Normal file
@ -0,0 +1,4 @@
|
||||
.app-logo {
|
||||
max-width: 5em;
|
||||
max-height: 5em;
|
||||
}
|
24
src/app/core/components/footer/footer.component.spec.ts
Normal file
24
src/app/core/components/footer/footer.component.spec.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { FooterComponent } from './footer.component';
|
||||
|
||||
describe('FooterComponent', () => {
|
||||
let component: FooterComponent;
|
||||
let fixture: ComponentFixture<FooterComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [FooterComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(FooterComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
14
src/app/core/components/footer/footer.component.ts
Normal file
14
src/app/core/components/footer/footer.component.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrls: ['./footer.component.scss'],
|
||||
})
|
||||
export class FooterComponent implements OnInit {
|
||||
public env = environment;
|
||||
constructor() {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
}
|
49
src/app/core/components/header/header.component.html
Normal file
49
src/app/core/components/header/header.component.html
Normal file
@ -0,0 +1,49 @@
|
||||
<header class="big-header">
|
||||
<nav class="navbar" role="navigation" aria-label="main navigation">
|
||||
<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 is-2">
|
||||
{{ appTitle }}
|
||||
</span>
|
||||
<span class="dev-env button has-background-success" *ngIf="!env.production">
|
||||
<i>
|
||||
(dev)
|
||||
</i>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a
|
||||
role="button"
|
||||
class="navbar-burger burger"
|
||||
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>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<span>
|
||||
{{ 'config.title' | translate }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<app-language-selector></app-language-selector>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mobile-menu" *ngIf="mobileMenuVisible">
|
||||
menu mobile
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
17
src/app/core/components/header/header.component.scss
Normal file
17
src/app/core/components/header/header.component.scss
Normal file
@ -0,0 +1,17 @@
|
||||
:host {
|
||||
header {
|
||||
nav {
|
||||
padding-right: 1em;
|
||||
.fa {
|
||||
margin-right: 1ch;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
.app-logo {
|
||||
max-width: 5em;
|
||||
max-height: 5em;
|
||||
}
|
||||
}
|
24
src/app/core/components/header/header.component.spec.ts
Normal file
24
src/app/core/components/header/header.component.spec.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HeaderComponent } from './header.component';
|
||||
|
||||
describe('HeaderComponent', () => {
|
||||
let component: HeaderComponent;
|
||||
let fixture: ComponentFixture<HeaderComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [HeaderComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(HeaderComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
25
src/app/core/components/header/header.component.ts
Normal file
25
src/app/core/components/header/header.component.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { SettingsComponent } from '../../../shared/components/settings/settings.component';
|
||||
import { User } from '../../models/user.model';
|
||||
import { ModalService } from '../../services/modal.service';
|
||||
import { UserService } from '../../services/user.service';
|
||||
import { environment } from '../../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
templateUrl: './header.component.html',
|
||||
styleUrls: ['./header.component.scss'],
|
||||
})
|
||||
export class HeaderComponent implements OnInit {
|
||||
public _user: Observable<User> = this.userService.user;
|
||||
public env = environment;
|
||||
@Input() public appTitle: string = 'FramaDate Funky';
|
||||
@Input() public appLogo: string;
|
||||
mobileMenuVisible = false;
|
||||
|
||||
constructor(private userService: UserService) {}
|
||||
|
||||
public ngOnInit(): void {}
|
||||
}
|
69
src/app/core/components/home/home.component.html
Normal file
69
src/app/core/components/home/home.component.html
Normal file
@ -0,0 +1,69 @@
|
||||
<section class="hero">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">
|
||||
{{ 'home.title' | translate }}
|
||||
{{ env.appTitle }}
|
||||
</h1>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h2 class="subtitle">
|
||||
{{ 'home.subtitle' | translate }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="column">
|
||||
<h2 class="subtitle">
|
||||
{{ '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">
|
||||
<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
Normal file
6
src/app/core/components/home/home.component.scss
Normal file
@ -0,0 +1,6 @@
|
||||
:host {
|
||||
text-align: center;
|
||||
a .fa {
|
||||
margin-right: 1ch;
|
||||
}
|
||||
}
|
24
src/app/core/components/home/home.component.spec.ts
Normal file
24
src/app/core/components/home/home.component.spec.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { HomeComponent } from './home.component';
|
||||
|
||||
describe('HomeComponent', () => {
|
||||
let component: HomeComponent;
|
||||
let fixture: ComponentFixture<HomeComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [HomeComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(HomeComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
11
src/app/core/components/home/home.component.ts
Normal file
11
src/app/core/components/home/home.component.ts
Normal file
@ -0,0 +1,11 @@
|
||||
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 {
|
||||
public env = environment;
|
||||
}
|
13
src/app/core/components/logo/logo.component.html
Normal file
13
src/app/core/components/logo/logo.component.html
Normal file
@ -0,0 +1,13 @@
|
||||
<div class="home_link">
|
||||
<a class="button" routerLink="/" aria-roledescription="home">
|
||||
<h1>
|
||||
<span class="logo_first">Frama</span>
|
||||
<span class="logo_second">Sondage</span>
|
||||
</h1>
|
||||
</a>
|
||||
<a class="button legend" href="https://framasoft.org/" target="_blank">
|
||||
proposé par
|
||||
<span class="legend_first">Frama</span>
|
||||
<span class="legend_second">soft</span>
|
||||
</a>
|
||||
</div>
|
0
src/app/core/components/logo/logo.component.scss
Normal file
0
src/app/core/components/logo/logo.component.scss
Normal file
24
src/app/core/components/logo/logo.component.spec.ts
Normal file
24
src/app/core/components/logo/logo.component.spec.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LogoComponent } from './logo.component';
|
||||
|
||||
describe('LogoComponent', () => {
|
||||
let component: LogoComponent;
|
||||
let fixture: ComponentFixture<LogoComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [LogoComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(LogoComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
8
src/app/core/components/logo/logo.component.ts
Normal file
8
src/app/core/components/logo/logo.component.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-logo',
|
||||
templateUrl: './logo.component.html',
|
||||
styleUrls: ['./logo.component.scss'],
|
||||
})
|
||||
export class LogoComponent {}
|
@ -0,0 +1,62 @@
|
||||
<nav class="has-background-light">
|
||||
<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 poll of _pollsAvailables | async"
|
||||
routerLink="{{ '/poll/' + poll.slug + '/consultation' }}"
|
||||
routerLinkActive="is-active"
|
||||
>
|
||||
« {{ poll.slug }} »
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<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">
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
<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/home" routerLinkActive="active">
|
||||
<i class="fa fa-home" aria-hidden="true"></i> Accueil
|
||||
</a>
|
||||
</nav>
|
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { NavigationComponent } from './navigation.component';
|
||||
|
||||
describe('NavigationComponent', () => {
|
||||
let component: NavigationComponent;
|
||||
let fixture: ComponentFixture<NavigationComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [NavigationComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(NavigationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
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',
|
||||
templateUrl: './navigation.component.html',
|
||||
styleUrls: ['./navigation.component.scss'],
|
||||
})
|
||||
export class NavigationComponent implements OnInit {
|
||||
public _pollsAvailables: Observable<Poll[]> = this.mockingService.pollsAvailables;
|
||||
public devModeEnabled = !environment.production;
|
||||
|
||||
constructor(private mockingService: MockingService) {}
|
||||
|
||||
public ngOnInit(): void {}
|
||||
}
|
24
src/app/core/core.module.ts
Normal file
24
src/app/core/core.module.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule, Optional, SkipSelf } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { RouterModule } from '@angular/router';
|
||||
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 { 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, SharedModule],
|
||||
exports: [HeaderComponent, FooterComponent, NavigationComponent, LogoComponent],
|
||||
})
|
||||
export class CoreModule {
|
||||
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
|
||||
throwIfAlreadyLoaded(parentModule, 'CoreModule');
|
||||
}
|
||||
}
|
5
src/app/core/enums/answer.enum.ts
Normal file
5
src/app/core/enums/answer.enum.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum Answer {
|
||||
YES = 'YES',
|
||||
NO = 'NO',
|
||||
MAYBE = 'MAYBE',
|
||||
}
|
15
src/app/core/enums/language.enum.ts
Normal file
15
src/app/core/enums/language.enum.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export enum Language {
|
||||
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/enums/theme.enum.ts
Normal file
6
src/app/core/enums/theme.enum.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export enum Theme {
|
||||
LIGHT = 'LIGHT',
|
||||
DARK = 'DARK',
|
||||
CONTRAST = 'CONTRAST',
|
||||
RED = 'RED',
|
||||
}
|
5
src/app/core/enums/user-role.enum.ts
Normal file
5
src/app/core/enums/user-role.enum.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum UserRole {
|
||||
ANONYMOUS = 'ANONYMOUS',
|
||||
REGISTERED = 'REGISTERED',
|
||||
ADMIN = 'ADMIN',
|
||||
}
|
5
src/app/core/guards/module-import.guard.ts
Normal file
5
src/app/core/guards/module-import.guard.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export function throwIfAlreadyLoaded(parentModule: any, moduleName: string): void {
|
||||
if (parentModule) {
|
||||
throw new Error(`${moduleName} has already been loaded. Import ${moduleName} in the AppModule only.`);
|
||||
}
|
||||
}
|
39
src/app/core/models/choice.model.ts
Normal file
39
src/app/core/models/choice.model.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { Answer } from '../enums/answer.enum';
|
||||
import { User } from './user.model';
|
||||
|
||||
export class Choice {
|
||||
constructor(
|
||||
public label: string,
|
||||
public imageUrl?: string,
|
||||
public participants: Map<Answer, Set<User>> = new Map<Answer, Set<User>>([
|
||||
[Answer.YES, new Set<User>()],
|
||||
[Answer.NO, new Set<User>()],
|
||||
[Answer.MAYBE, new Set<User>()],
|
||||
]),
|
||||
public counts: Map<Answer, number> = new Map<Answer, number>([
|
||||
[Answer.YES, 0],
|
||||
[Answer.NO, 0],
|
||||
[Answer.MAYBE, 0],
|
||||
])
|
||||
) {}
|
||||
|
||||
public updateParticipation(user: User, responseType: Answer): void {
|
||||
this.removeParticipant(user);
|
||||
this.participants.get(responseType).add(user);
|
||||
this.updateCounts();
|
||||
}
|
||||
|
||||
public removeParticipant(user: User): void {
|
||||
for (const responseType of Object.values(Answer)) {
|
||||
if (this.participants.get(responseType).has(user)) {
|
||||
this.participants.get(responseType).delete(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public updateCounts(): void {
|
||||
for (const responseType of Object.values(Answer)) {
|
||||
this.counts.set(responseType, this.participants.get(responseType).size);
|
||||
}
|
||||
}
|
||||
}
|
13
src/app/core/models/comment.model.ts
Normal file
13
src/app/core/models/comment.model.ts
Normal file
@ -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;
|
||||
}
|
||||
}
|
28
src/app/core/models/configuration.model.ts
Normal file
28
src/app/core/models/configuration.model.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { DateService } from '../services/date.service';
|
||||
|
||||
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())
|
||||
)
|
||||
) {}
|
||||
|
||||
public static isArchived(configuration: PollConfiguration): boolean {
|
||||
return configuration.expires ? DateService.isDateInPast(configuration.expires) : undefined;
|
||||
}
|
||||
}
|
54
src/app/core/models/poll.model.ts
Normal file
54
src/app/core/models/poll.model.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { environment } from 'src/environments/environment';
|
||||
|
||||
import { Choice } from './choice.model';
|
||||
import { Comment } from './comment.model';
|
||||
import { PollConfiguration } from './configuration.model';
|
||||
import { User } from './user.model';
|
||||
|
||||
export class Poll {
|
||||
constructor(
|
||||
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 dateChoices: Choice[] = [],
|
||||
public timeChoices: Choice[] = []
|
||||
) {}
|
||||
|
||||
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' | 'title' | 'description' | 'slug' | 'configuration' | 'comments' | 'choices'>
|
||||
): Poll {
|
||||
return new Poll(
|
||||
new User(item.owner.pseudo, item.owner.email, undefined),
|
||||
item.slug,
|
||||
item.title,
|
||||
item.description,
|
||||
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' | 'counts'>) => {
|
||||
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;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
12
src/app/core/models/user.model.ts
Normal file
12
src/app/core/models/user.model.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { UserRole } from '../enums/user-role.enum';
|
||||
import { Poll } from './poll.model';
|
||||
|
||||
export class User {
|
||||
constructor(
|
||||
public pseudo: string = 'pseudo',
|
||||
public email: string = 'example@example.com',
|
||||
public polls: Poll[] = [],
|
||||
public role?: UserRole,
|
||||
public token?: string
|
||||
) {}
|
||||
}
|
16
src/app/core/services/api.service.spec.ts
Normal file
16
src/app/core/services/api.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ApiService } from './api.service';
|
||||
|
||||
describe('ApiService', () => {
|
||||
let service: ApiService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(ApiService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
287
src/app/core/services/api.service.ts
Normal file
287
src/app/core/services/api.service.ts
Normal file
@ -0,0 +1,287 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import axios, { AxiosInstance, AxiosResponse } from 'axios';
|
||||
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 = 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(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<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 {
|
||||
console.log('currentApiRoutes', currentApiRoutes);
|
||||
return await this.axiosInstance.post(`${apiBaseHref}${currentApiRoutes['api_new_poll']}`, {
|
||||
data: poll,
|
||||
});
|
||||
} catch (error) {
|
||||
ApiService.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async createParticipation(
|
||||
pollId: string,
|
||||
choiceLabel: string,
|
||||
pseudo: string,
|
||||
response: Answer
|
||||
): Promise<string> {
|
||||
try {
|
||||
return await this.axiosInstance.post(`${this.pollsEndpoint}/${pollId}${this.answersEndpoint}`, {
|
||||
choiceLabel,
|
||||
pseudo,
|
||||
response,
|
||||
});
|
||||
} catch (error) {
|
||||
ApiService.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async createComment(slug: string, comment: string): Promise<string> {
|
||||
try {
|
||||
return await this.axiosInstance.post(`${this.pollsEndpoint}/${slug}${this.commentsEndpoint}`, comment);
|
||||
} catch (error) {
|
||||
ApiService.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
//////////
|
||||
// READ //
|
||||
//////////
|
||||
public async getAllAvailablePolls(): Promise<Poll[]> {
|
||||
// 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;
|
||||
} catch (error) {
|
||||
ApiService.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): 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;
|
||||
}
|
||||
);
|
||||
|
||||
const response: AxiosResponse<Poll> = await this.axiosInstance.get<Poll>(`${this.pollsEndpoint}/${slug}`);
|
||||
console.log('fetch API : asking for poll with slug=' + slug, { response });
|
||||
|
||||
axios.interceptors.request.eject(adapterInterceptor);
|
||||
|
||||
return response && response.data && !Array.isArray(response.data) ? response.data : undefined;
|
||||
} catch (error) {
|
||||
if (error.response?.status === 404) {
|
||||
return undefined;
|
||||
} else {
|
||||
ApiService.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(
|
||||
`${this.pollsEndpoint}${this.slugsEndpoint}/${slug}`
|
||||
);
|
||||
if (response?.status !== 404) {
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.response?.status === 404) {
|
||||
return true;
|
||||
} else {
|
||||
ApiService.handleError(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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}/${email}${this.usersPollsEndpoint}${this.usersPollsSendEmailEndpoint}`
|
||||
);
|
||||
} catch (error) {
|
||||
ApiService.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
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}/${email}${this.usersPollsEndpoint}`
|
||||
);
|
||||
return response?.data;
|
||||
} catch (error) {
|
||||
ApiService.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
////////////
|
||||
// UPDATE //
|
||||
////////////
|
||||
public async updateAnswer(slug: string, choiceLabel: string, pseudo: string, answer: Answer): Promise<string> {
|
||||
try {
|
||||
return await this.axiosInstance.patch(`${this.pollsEndpoint}/${slug}${this.answersEndpoint}`, {
|
||||
choiceLabel,
|
||||
pseudo,
|
||||
answer,
|
||||
});
|
||||
} catch (error) {
|
||||
ApiService.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
////////////
|
||||
// DELETE //
|
||||
////////////
|
||||
public async deletePoll(slug: string): Promise<boolean> {
|
||||
try {
|
||||
const response: AxiosResponse = await this.axiosInstance.delete(`${this.pollsEndpoint}/${slug}`);
|
||||
return response?.status === 204;
|
||||
} catch (error) {
|
||||
ApiService.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async deletePollAnswers(slug: string): Promise<boolean> {
|
||||
try {
|
||||
const response: AxiosResponse = await this.axiosInstance.delete(
|
||||
`${this.pollsEndpoint}/${slug}${this.answersEndpoint}`
|
||||
);
|
||||
return response?.status === 204;
|
||||
} catch (error) {
|
||||
ApiService.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
public async deletePollComments(slug: string): Promise<boolean> {
|
||||
try {
|
||||
const response: AxiosResponse = await this.axiosInstance.delete(
|
||||
`${this.pollsEndpoint}/${slug}${this.commentsEndpoint}`
|
||||
);
|
||||
return response?.status === 204;
|
||||
} catch (error) {
|
||||
ApiService.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////
|
||||
// PRIVATE METHODS //
|
||||
/////////////////////
|
||||
/**
|
||||
* 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
|
||||
console.log(error.response.data);
|
||||
console.log(error.response.status);
|
||||
console.log(error.response.headers);
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
console.log(error.request);
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log('Error', error.message);
|
||||
}
|
||||
console.log(error.config);
|
||||
}
|
||||
}
|
16
src/app/core/services/date.service.spec.ts
Normal file
16
src/app/core/services/date.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DateService } from './date.service';
|
||||
|
||||
describe('DateUtilsService', () => {
|
||||
let service: DateService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(DateService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
27
src/app/core/services/date.service.ts
Normal file
27
src/app/core/services/date.service.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import * as moment from 'moment';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DateService {
|
||||
public static addDaysToDate(days: number, date: Date): Date {
|
||||
return moment(date).add(days, 'days').toDate();
|
||||
}
|
||||
|
||||
public static diffInDays(dateLeft: Date, dateRight: Date): number {
|
||||
return moment(dateLeft).diff(moment(dateRight));
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
16
src/app/core/services/language.service.spec.ts
Normal file
16
src/app/core/services/language.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LanguageService } from './language.service';
|
||||
|
||||
describe('LanguageService', () => {
|
||||
let service: LanguageService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(LanguageService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
68
src/app/core/services/language.service.ts
Normal file
68
src/app/core/services/language.service.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { TranslateService, LangChangeEvent } from '@ngx-translate/core';
|
||||
|
||||
import { Language } from '../enums/language.enum';
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LanguageService {
|
||||
constructor(private translate: TranslateService, private storageService: StorageService) {}
|
||||
|
||||
public getLangage(): Language {
|
||||
return this.translate.currentLang as Language;
|
||||
}
|
||||
|
||||
public setLanguage(language: Language): void {
|
||||
this.translate.use(language.toString());
|
||||
}
|
||||
|
||||
public getAvailableLanguages(): string[] {
|
||||
return this.translate.getLangs();
|
||||
}
|
||||
|
||||
public configureAndInitTranslations(): void {
|
||||
// always save in storage the currentLang used
|
||||
this.translate.onLangChange.subscribe((event: LangChangeEvent) => {
|
||||
this.storageService.language = event.lang as Language;
|
||||
});
|
||||
|
||||
// set all languages available
|
||||
this.translate.addLangs(Object.keys(Language));
|
||||
|
||||
// set language
|
||||
this.setLanguageOnInit();
|
||||
}
|
||||
|
||||
private setLanguageOnInit(): void {
|
||||
// set language from storage
|
||||
if (!this.translate.currentLang) {
|
||||
this.setLanguageFromStorage();
|
||||
}
|
||||
|
||||
// or set language from browser
|
||||
if (!this.translate.currentLang) {
|
||||
this.setLanguageFromBrowser();
|
||||
}
|
||||
|
||||
// set default language
|
||||
if (!this.translate.currentLang) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
16
src/app/core/services/loader.service.spec.ts
Normal file
16
src/app/core/services/loader.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { LoaderService } from './loader.service';
|
||||
|
||||
describe('LoaderService', () => {
|
||||
let service: LoaderService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(LoaderService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
14
src/app/core/services/loader.service.ts
Normal file
14
src/app/core/services/loader.service.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LoaderService {
|
||||
private _loadingStatus: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||
public readonly isLoading: Observable<boolean> = this._loadingStatus.asObservable();
|
||||
|
||||
public setStatus(status: boolean): void {
|
||||
this._loadingStatus.next(status);
|
||||
}
|
||||
}
|
16
src/app/core/services/mocking.service.spec.ts
Normal file
16
src/app/core/services/mocking.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MockingService } from './mocking.service';
|
||||
|
||||
describe('MockingService', () => {
|
||||
let service: MockingService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(MockingService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
55
src/app/core/services/mocking.service.ts
Normal file
55
src/app/core/services/mocking.service.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
import { UserRole } from '../enums/user-role.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 { UserService } from './user.service';
|
||||
import { UuidService } from './uuid.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MockingService {
|
||||
private _pollsAvailables: BehaviorSubject<Poll[]> = new BehaviorSubject<Poll[]>([]);
|
||||
public readonly pollsAvailables: Observable<Poll[]> = this._pollsAvailables.asObservable();
|
||||
|
||||
constructor(private apiService: ApiService, private userService: UserService, private uuidService: UuidService) {}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
const pollsAvailable = await this.apiService.getAllAvailablePolls();
|
||||
this._pollsAvailables.next(pollsAvailable);
|
||||
|
||||
if (this._pollsAvailables.getValue() && this._pollsAvailables.getValue().length > 0) {
|
||||
// arbitrary choose first owner available
|
||||
const currentUser = this._pollsAvailables.getValue()[0].owner;
|
||||
currentUser.polls = [this._pollsAvailables.getValue()[0]];
|
||||
this.userService.updateUser(currentUser);
|
||||
} else {
|
||||
this.loadMock();
|
||||
}
|
||||
}
|
||||
|
||||
public loadMock(): void {
|
||||
const owner = new User('TOTO', 'toto@gafam.com', [], UserRole.REGISTERED);
|
||||
|
||||
const poll1: Poll = new Poll(owner, this.uuidService.getUUID(), 'Quand le picnic ?', 'Pour faire la teuf');
|
||||
const poll2: Poll = new Poll(
|
||||
owner,
|
||||
this.uuidService.getUUID(),
|
||||
'On fait quoi à la soirée ?',
|
||||
'Balancez vos idées'
|
||||
);
|
||||
|
||||
poll1.choices = [new Choice('mardi prochain'), new Choice('mercredi')];
|
||||
poll2.choices = [new Choice('jeux'), new Choice('danser'), new Choice('discuter en picolant')];
|
||||
|
||||
this._pollsAvailables.next([poll1, poll2]);
|
||||
owner.polls = [poll1, poll2];
|
||||
this.userService.updateUser(owner);
|
||||
|
||||
console.info('MOCKING user', { user: owner });
|
||||
}
|
||||
}
|
16
src/app/core/services/modal.service.spec.ts
Normal file
16
src/app/core/services/modal.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ModalService } from './modal.service';
|
||||
|
||||
describe('ModalService', () => {
|
||||
let service: ModalService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(ModalService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
20
src/app/core/services/modal.service.ts
Normal file
20
src/app/core/services/modal.service.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { ComponentType } from '@angular/cdk/portal';
|
||||
import { Injectable, TemplateRef } from '@angular/core';
|
||||
import { MatDialog, MatDialogConfig } from '@angular/material/dialog';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ModalService {
|
||||
constructor(public dialog: MatDialog) {}
|
||||
|
||||
public openModal_OLD<T, K>(componentOrTemplateRef: ComponentType<T> | TemplateRef<T>, data?: K): void {
|
||||
this.dialog.open(componentOrTemplateRef, { data: data });
|
||||
}
|
||||
public openModal<T, D = any>(
|
||||
componentOrTemplateRef: ComponentType<T> | TemplateRef<T>,
|
||||
config?: MatDialogConfig<D>
|
||||
): void {
|
||||
this.dialog.open<T, D>(componentOrTemplateRef, config);
|
||||
}
|
||||
}
|
16
src/app/core/services/poll.service.spec.ts
Normal file
16
src/app/core/services/poll.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { PollService } from './poll.service';
|
||||
|
||||
describe('PollService', () => {
|
||||
let service: PollService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(PollService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
243
src/app/core/services/poll.service.ts
Normal file
243
src/app/core/services/poll.service.ts
Normal file
@ -0,0 +1,243 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Resolve, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
|
||||
import { Answer } from '../enums/answer.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 { 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',
|
||||
})
|
||||
export class PollService implements Resolve<Poll> {
|
||||
private _poll: BehaviorSubject<Poll | undefined> = new BehaviorSubject<Poll | undefined>(undefined);
|
||||
public readonly poll: Observable<Poll | undefined> = this._poll.asObservable();
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private router: Router,
|
||||
private apiService: ApiService,
|
||||
private userService: UserService,
|
||||
private uuidService: UuidService,
|
||||
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] : '';
|
||||
if (!wantedSlug && state.url.includes('administration')) {
|
||||
// creation of new poll
|
||||
const poll = new Poll(this.userService.getCurrentUser(), this.uuidService.getUUID(), '');
|
||||
this._poll.next(poll);
|
||||
this.router.navigate(['poll/' + poll.slug + '/administration']);
|
||||
}
|
||||
if (!this._poll.getValue() || !this._poll.getValue().slug || this._poll.getValue().slug !== wantedSlug) {
|
||||
await this.loadPollBySlug(wantedSlug);
|
||||
}
|
||||
if (this._poll.getValue()) {
|
||||
return this._poll.getValue();
|
||||
} else {
|
||||
this.router.navigate(['page-not-found']);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
const poll: Poll | undefined = await this.apiService.getPollBySlug(slug);
|
||||
console.log({ loadPollBySlugResponse: poll });
|
||||
this.updateCurrentPoll(poll);
|
||||
}
|
||||
}
|
||||
|
||||
public updateCurrentPoll(poll: Poll): void {
|
||||
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: 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.');
|
||||
}
|
||||
}
|
||||
|
||||
public saveParticipation(choice: Choice, user: User, response: Answer): void {
|
||||
const currentPoll = this._poll.getValue();
|
||||
currentPoll.choices.find((c) => c.label === choice.label)?.updateParticipation(user, response);
|
||||
this.updateCurrentPoll(currentPoll);
|
||||
this.apiService.createParticipation(currentPoll.slug, choice.label, user.pseudo, response);
|
||||
this.toastService.display('Votre participation au sondage a été enregistrée.');
|
||||
}
|
||||
|
||||
public async deleteAllAnswers(): Promise<void> {
|
||||
await this.apiService.deletePollAnswers(this._poll.getValue().slug);
|
||||
this.toastService.display('Les participations des votants à ce sondage ont été supprimées.');
|
||||
}
|
||||
|
||||
public async addComment(comment: string): Promise<void> {
|
||||
await this.apiService.createComment(this._poll.getValue().slug, comment);
|
||||
this.toastService.display('Votre commentaire a été enregistré.');
|
||||
}
|
||||
|
||||
public async deleteComments(): Promise<void> {
|
||||
await this.apiService.deletePollComments(this._poll.getValue().slug);
|
||||
this.toastService.display('Les commentaires de ce sondage ont été supprimés.');
|
||||
}
|
||||
|
||||
public buildAnswersByChoiceLabelByPseudo(poll: Poll): Map<string, Map<string, Answer>> {
|
||||
const pseudos: Set<string> = new Set();
|
||||
poll.choices.forEach((choice: Choice) => {
|
||||
choice.participants.forEach((users: Set<User>) => {
|
||||
users.forEach((user: User) => {
|
||||
pseudos.add(user.pseudo);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const list = new Map<string, Map<string, Answer>>();
|
||||
pseudos.forEach((pseudo: string) => {
|
||||
list.set(
|
||||
pseudo,
|
||||
new Map<string, Answer>(
|
||||
poll.choices.map((choice: Choice) => {
|
||||
return [choice.label, undefined];
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
poll.choices.forEach((choice: Choice) => {
|
||||
choice.participants.forEach((users: Set<User>, answer: Answer) => {
|
||||
users.forEach((user: User) => {
|
||||
list.get(user.pseudo).set(choice.label, answer);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
16
src/app/core/services/storage.service.spec.ts
Normal file
16
src/app/core/services/storage.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { StorageService } from './storage.service';
|
||||
|
||||
describe('StorageService', () => {
|
||||
let service: StorageService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(StorageService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
22
src/app/core/services/storage.service.ts
Normal file
22
src/app/core/services/storage.service.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { LocalStorage } from 'ngx-webstorage';
|
||||
|
||||
import { Language } from '../enums/language.enum';
|
||||
import { Theme } from '../enums/theme.enum';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class StorageService {
|
||||
@LocalStorage()
|
||||
public theme: Theme;
|
||||
|
||||
@LocalStorage()
|
||||
public language: Language;
|
||||
|
||||
@LocalStorage()
|
||||
public userPollsIds: string[];
|
||||
|
||||
@LocalStorage()
|
||||
public pseudo: string;
|
||||
}
|
16
src/app/core/services/theme.service.spec.ts
Normal file
16
src/app/core/services/theme.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ThemeService } from './theme.service';
|
||||
|
||||
describe('ThemeService', () => {
|
||||
let service: ThemeService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(ThemeService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
15
src/app/core/services/theme.service.ts
Normal file
15
src/app/core/services/theme.service.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
import { Theme } from '../enums/theme.enum';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ThemeService {
|
||||
private _theme: BehaviorSubject<Theme> = new BehaviorSubject<Theme>(Theme.LIGHT);
|
||||
public readonly theme: Observable<Theme> = this._theme.asObservable();
|
||||
|
||||
public selectTheme(theme: Theme): void {
|
||||
this._theme.next(theme);
|
||||
}
|
||||
}
|
16
src/app/core/services/toast.service.spec.ts
Normal file
16
src/app/core/services/toast.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ToastService } from './toast.service';
|
||||
|
||||
describe('MessageService', () => {
|
||||
let service: ToastService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(ToastService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
14
src/app/core/services/toast.service.ts
Normal file
14
src/app/core/services/toast.service.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ToastService {
|
||||
constructor(private _snackBar: MatSnackBar) {}
|
||||
|
||||
public display(message: string, action?: string): void {
|
||||
const config: MatSnackBarConfig = { duration: 2000 };
|
||||
this._snackBar.open(message, action, config);
|
||||
}
|
||||
}
|
16
src/app/core/services/user.service.spec.ts
Normal file
16
src/app/core/services/user.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UserService } from './user.service';
|
||||
|
||||
describe('UserService', () => {
|
||||
let service: UserService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(UserService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
40
src/app/core/services/user.service.ts
Normal file
40
src/app/core/services/user.service.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
import { UserRole } from '../enums/user-role.enum';
|
||||
import { User } from '../models/user.model';
|
||||
import { ApiService } from './api.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UserService {
|
||||
public anonymous: User = new User('', '', [], UserRole.ANONYMOUS);
|
||||
|
||||
private _user: BehaviorSubject<User> = new BehaviorSubject<User>(this.anonymous);
|
||||
public readonly user: Observable<User> = this._user.asObservable();
|
||||
|
||||
constructor(private apiService: ApiService) {}
|
||||
|
||||
public updateUser(user: User): void {
|
||||
this._user.next(user);
|
||||
}
|
||||
|
||||
public getCurrentUser(): User {
|
||||
return this._user.getValue();
|
||||
}
|
||||
|
||||
public isCurrentUserIdentifiable(): boolean {
|
||||
return this._user.getValue()?.pseudo ? true : false;
|
||||
}
|
||||
|
||||
public async getUserPolls(): Promise<void> {
|
||||
const currentUser: User = this._user.getValue();
|
||||
currentUser.polls = await this.apiService.getPollsUrlsByUserEmail(currentUser.email);
|
||||
this.updateUser(currentUser);
|
||||
}
|
||||
|
||||
public async sendEmailToUserTheListOfItsPolls(): Promise<void> {
|
||||
await this.apiService.sendEmailToUserOfItsPollsList(this._user.getValue().email);
|
||||
}
|
||||
}
|
16
src/app/core/services/uuid.service.spec.ts
Normal file
16
src/app/core/services/uuid.service.spec.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { UuidService } from './uuid.service';
|
||||
|
||||
describe('UuidService', () => {
|
||||
let service: UuidService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(UuidService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
23
src/app/core/services/uuid.service.ts
Normal file
23
src/app/core/services/uuid.service.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import ShortUniqueId from 'short-unique-id';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UuidService {
|
||||
private uid = new ShortUniqueId();
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { AdministrationComponent } from './administration.component';
|
||||
import { NamingComponent } from './naming/naming.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: AdministrationComponent },
|
||||
{ path: 'naming', component: NamingComponent },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class AdministrationRoutingModule {}
|
@ -0,0 +1,5 @@
|
||||
<div class="columns administration">
|
||||
<div class="column">
|
||||
<app-admin-form [poll]="poll"></app-admin-form>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AdministrationComponent } from './administration.component';
|
||||
|
||||
describe('AdministrationComponent', () => {
|
||||
let component: AdministrationComponent;
|
||||
let fixture: ComponentFixture<AdministrationComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AdministrationComponent],
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AdministrationComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user