diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 00000000..cbcf329a
Binary files /dev/null and b/.DS_Store differ
diff --git a/.angular-cli.json b/.angular-cli.json
deleted file mode 100644
index efb433bb..00000000
--- a/.angular-cli.json
+++ /dev/null
@@ -1,65 +0,0 @@
-{
- "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
- "project": {
- "name": "portfolio",
- "ejected": false
- },
- "apps": [
- {
- "root": "src",
- "outDir": "dist",
- "assets": [
- "assets"
- ],
- "index": "index.html",
- "main": "main.ts",
- "polyfills": "polyfills.ts",
- "test": "test.ts",
- "tsconfig": "tsconfig.app.json",
- "testTsconfig": "tsconfig.spec.json",
- "prefix": "app",
- "styles": [
- "../node_modules/normalize.css/normalize.css",
- "../node_modules/primeng/resources/primeng.min.css",
- "../node_modules/primeng/resources/themes/omega/theme.css",
- "../node_modules/font-awesome/css/font-awesome.min.css",
- "assets/style/styles.less"
- ],
- "scripts": [],
- "environmentSource": "environments/environment.ts",
- "environments": {
- "dev": "environments/environment.ts",
- "prod": "environments/environment.prod.ts"
- }
- }
- ],
- "e2e": {
- "protractor": {
- "config": "./protractor.conf.js"
- }
- },
- "lint": [
- {
- "project": "src/tsconfig.app.json"
- },
- {
- "project": "src/tsconfig.spec.json"
- },
- {
- "project": "e2e/tsconfig.e2e.json"
- }
- ],
- "test": {
- "karma": {
- "config": "./karma.conf.js"
- }
- },
- "defaults": {
- "styleExt": "less",
- "serve": {
- "port": 4200
- },
- "component": {
- }
- }
-}
diff --git a/.editorconfig b/.editorconfig
index 6e87a003..8ffc628f 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -5,6 +5,7 @@ root = true
charset = utf-8
indent_style = space
indent_size = 2
+end_of_line = crlf
insert_final_newline = true
trim_trailing_whitespace = true
diff --git a/.gitignore b/.gitignore
index 54bfd200..05300181 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,42 +1,38 @@
-# See http://help.github.com/ignore-files/ for more about ignoring files.
-
-# compiled output
-/dist
-/tmp
-/out-tsc
-
-# dependencies
-/node_modules
-
-# 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
-
-# misc
-/.sass-cache
-/connect.lock
-/coverage
-/libpeerconnection.log
-npm-debug.log
-testem.log
-/typings
-
-# e2e
-/e2e/*.js
-/e2e/*.map
-
-# System Files
+# Server files
+/server/node_modules
+
+# Client files
+/client/.DS_Store
+/client/node_modules/
+/legacy-client/node_modules/
+/client/dist/
+/client/test/unit/reports/
+/client/test/e2e/reports/
+/client/test/e2e/features/undefined-steps.js
+/client/selenium-debug.log
+
+/client/src/components/projects/.DS_Store
+/client/src/components/root/.DS_Store
+/client/src/shared/.DS_Store
+/client/src/store/.DS_Store
+/client/src/store/modules/.DS_Store
+/src/app/.DS_Store
+/src/.DS_Store
+/client/src/.DS_Store
+/client/src/components/root/.DS_Store
+/.DS_Store
+
+# Editor directories and files
+.idea
+.vscode
+
+#misc
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+
+# Ignore Mac DS_Store files
.DS_Store
-Thumbs.db
+
+
diff --git a/.sequelizerc b/.sequelizerc
deleted file mode 100644
index 7e32c6ba..00000000
--- a/.sequelizerc
+++ /dev/null
@@ -1,8 +0,0 @@
-var path = require('path');
-
-module.exports = {
- 'config': path.resolve('./server', 'config/config.json'),
- 'migrations-path': path.resolve('./server', 'migrations'),
- 'models-path': path.resolve('./server', 'models'),
- 'seeders-path': path.resolve('./server', 'seeders')
-}
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 00000000..421aa737
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,53 @@
+dist: trusty
+
+services:
+ - postgresql
+
+env:
+ # Use test credentials for sequelize
+ - NODE_ENV=test
+
+sudo: required
+before_install:
+ # Repo for Yarn
+ - sudo apt-key adv --fetch-keys http://dl.yarnpkg.com/debian/pubkey.gpg
+ - echo "deb http://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
+ - sudo apt-get update -qq
+ - sudo apt-get install -y -qq yarn=1.5.1-1
+addons:
+ chrome: stable
+
+language: node_js
+node_js:
+ - "9"
+
+cache:
+ yarn: true
+ directories:
+ - ./client/node_modules
+ - ./server/node_modules
+
+install:
+ - yarn run client-server:install
+
+script:
+ # Use Chromium instead of Chrome.
+ # - export CHROME_BIN=chromium-browser
+ # Initialize database: create, migrate, seed
+ - xvfb-run -a yarn run db
+ # Run backend
+ - xvfb-run -a yarn run server &
+ # Run linter
+ - xvfb-run -a yarn run client:lint
+ # Run unit tests
+ - xvfb-run -a yarn run client:unit
+ # Run e2e tests
+ - xvfb-run -a yarn run client:e2e
+# - sleep 3
+
+after_script:
+ # Stop backend
+ - pkill node
+
+notifications:
+ email: false
diff --git a/.vs/Portfolio/v15/.suo b/.vs/Portfolio/v15/.suo
deleted file mode 100644
index 90733dcb..00000000
Binary files a/.vs/Portfolio/v15/.suo and /dev/null differ
diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json
deleted file mode 100644
index 6b611411..00000000
--- a/.vs/VSWorkspaceState.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "ExpandedNodes": [
- ""
- ],
- "PreviewInSolutionExplorer": false
-}
\ No newline at end of file
diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite
deleted file mode 100644
index 2104a4ed..00000000
Binary files a/.vs/slnx.sqlite and /dev/null differ
diff --git a/.vscode/launch.json b/.vscode/launch.json
deleted file mode 100644
index 00b48a4f..00000000
--- a/.vscode/launch.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- // Use IntelliSense to learn about possible attributes.
- // Hover to view descriptions of existing attributes.
- // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
- "version": "0.2.0",
- "configurations": [
- {
- "type": "node",
- "request": "launch",
- "name": "Launch Program",
- "program": "${workspaceFolder}\\proxy.conf.json",
- "preLaunchTask": "tsc: build - tsconfig.json",
- "outFiles": [
- "${workspaceFolder}/dist/out-tsc/**/*.js"
- ]
- }
- ]
-}
\ No newline at end of file
diff --git a/README.md b/README.md
index e2559ef7..96b39115 100644
--- a/README.md
+++ b/README.md
@@ -1,39 +1,31 @@
-# Portfolio
+# T-Portfolio
-## How to run
-
-1. Get MySQL server locally or via docker:
-`docker run -p 3306:3306 --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:latest`
-(where `my-secret-pw` is your `root` user password)
-
-2. Run `npm install` to install all dependencies
+[](https://travis-ci.org/T-Systems-RUS/Portfolio)
-3. Run `npm run db:init` to initialize database (tweak DB connection settings via`server/config/config.json`)
-
-4. Run `npm run client-server` to run both client and server in development watch mode.
+## How to run
+**PREREQUISITE**: Get PostgreSQL server locally or via docker:
+`docker run --name some-postgres -p 5432:5432 -e POSTGRES_PASSWORD=123 -d postgres` (where `123` is your `postgres` user password)
-## Development server
+There are to possible scenarios to start the app:
-Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
+### One process for client and server (easy to start with)
-## Code scaffolding
+1. Run `yarn run client-server:install` to install both server and client dependencies
-Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|module`.
+3. Run `yarn run db` to initialize database (tweak DB connection settings via`server/config/config.json`)
-## Build
+4. Run `yarn run client-server` to run both client and server in development watch mode.
-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
+### Separate processes for client and server (easier to manage)
-Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
+1. Go to `server` folder and run `yarn` to install server dependencies
-## Running end-to-end tests
+3. Run `yarn run db` to initialize database (tweak DB connection settings via`server/config/config.json`)
-Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
-Before running the tests make sure you are serving the app via `ng serve`.
+4. Run `yarn run server:watch` to run server in development watch mode
-## Further help
+5. Go to `client` folder and run `yarn` to install client dependencies
-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).
+6. Run `yarn run dev` to run client in development watch mode.
diff --git a/client/.babelrc b/client/.babelrc
new file mode 100644
index 00000000..9390d164
--- /dev/null
+++ b/client/.babelrc
@@ -0,0 +1,18 @@
+{
+ "presets": [
+ ["env", {
+ "modules": false,
+ "targets": {
+ "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
+ }
+ }],
+ "stage-2"
+ ],
+ "plugins": ["transform-vue-jsx", "transform-runtime"],
+ "env": {
+ "test": {
+ "presets": ["env", "stage-2"],
+ "plugins": ["transform-vue-jsx", "transform-es2015-modules-commonjs", "dynamic-import-node"]
+ }
+ }
+}
diff --git a/client/.editorconfig b/client/.editorconfig
new file mode 100644
index 00000000..9d08a1a8
--- /dev/null
+++ b/client/.editorconfig
@@ -0,0 +1,9 @@
+root = true
+
+[*]
+charset = utf-8
+indent_style = space
+indent_size = 2
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
diff --git a/client/.eslintignore b/client/.eslintignore
new file mode 100644
index 00000000..51ebc9d7
--- /dev/null
+++ b/client/.eslintignore
@@ -0,0 +1,5 @@
+/build/
+/config/
+/dist/
+/*.js
+/test/unit/reports/
diff --git a/client/.eslintrc.js b/client/.eslintrc.js
new file mode 100644
index 00000000..5e0369de
--- /dev/null
+++ b/client/.eslintrc.js
@@ -0,0 +1,94 @@
+// https://eslint.org/docs/user-guide/configuring
+
+module.exports = {
+ root: true,
+ parserOptions: {
+ parser: 'typescript-eslint-parser'
+ },
+ env: {
+ browser: true,
+ },
+ // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
+ extends: ['plugin:vue/recommended', 'airbnb-base'],
+ // required to lint *.vue files
+ plugins: [
+ 'vue',
+ 'typescript'
+ ],
+ // check if imports actually resolve
+ settings: {
+ 'import/resolver': {
+ webpack: {
+ config: 'build/webpack.base.conf.js'
+ }
+ }
+ },
+ // add your custom rules here
+ rules: {
+ // don't require .vue extension when importing
+ 'import/extensions': ['error', 'always', {
+ js: 'never',
+ ts: 'never',
+ //vue: 'never'
+ }],
+ // disallow reassignment of function parameters
+ // disallow parameter object manipulation except for specific exclusions
+ 'no-param-reassign': 'off',
+ // TODO investigate a.b = !a.b assignments
+ // 'no-param-reassign': ['error', {
+ // props: true,
+ // ignorePropertyModificationsFor: [
+ // 'state', // for vuex state
+ // 'acc', // for reduce accumulators
+ // 'e' // for e.returnvalue
+ // ]
+ // }],
+ // allow optionalDependencies
+ 'import/no-extraneous-dependencies': ['error', {
+ optionalDependencies: ['test/unit/index.js']
+ }],
+ // allow debugger during development
+ 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
+ 'indent': ['error', 2],
+ 'object-curly-spacing': ['error', 'never'],
+ 'comma-dangle': ['error', 'never'],
+ 'arrow-parens': ['error', 'as-needed'],
+ 'max-len': ['error', { "code": 120 }],
+ // Broken
+ 'space-infix-ops': 'off',
+ // TypeScript checks this via compiler options - see tsconfig
+ 'no-undef': 'off',
+ 'no-unused-vars': 'off',
+ // TODO fix after parser fixed https://github.com/eslint/typescript-eslint-parser#known-issues
+ 'import/prefer-default-export': 'off',
+ // TODO enable later if really needed
+ 'vue/require-v-for-key': 'off',
+
+ // Typescript Plugin
+ 'typescript/adjacent-overload-signatures': ['error'],
+ 'typescript/class-name-casing': ['error'],
+ 'typescript/interface-name-prefix': ['error', 'always'],
+ 'typescript/member-ordering': ['error'],
+ 'typescript/no-angle-bracket-type-assertion': ['error'],
+ 'typescript/no-array-constructor': ['error'],
+ 'typescript/no-empty-interface': ['error'],
+ 'typescript/no-explicit-any': ['error'],
+ 'typescript/no-namespace': ['error'],
+ 'typescript/no-triple-slash-reference': ['error'],
+ // Broken
+ // 'typescript/type-annotation-spacing': ['error'],
+ },
+ overrides: [
+ {
+ files: ["*.vue"],
+ rules: {
+ 'indent': 'off',
+ 'vue/script-indent': [
+ 'error',
+ 2,
+ {'baseIndent': 1}
+ ],
+ }
+ }
+ ]
+}
diff --git a/client/.postcssrc.js b/client/.postcssrc.js
new file mode 100644
index 00000000..eee3e92d
--- /dev/null
+++ b/client/.postcssrc.js
@@ -0,0 +1,10 @@
+// https://github.com/michael-ciniawsky/postcss-load-config
+
+module.exports = {
+ "plugins": {
+ "postcss-import": {},
+ "postcss-url": {},
+ // to edit target browsers: use "browserslist" field in package.json
+ "autoprefixer": {}
+ }
+}
diff --git a/client/README.md b/client/README.md
new file mode 100644
index 00000000..2b450660
--- /dev/null
+++ b/client/README.md
@@ -0,0 +1,89 @@
+# t-portfolio client
+
+> A Vue.js project
+
+It's highly recommended to use [Yarn](https://yarnpkg.com/en/) for this project
+
+Unit tests are done with Jest.
+e2e tests are based on Nightwatch and Cucumber.
+
+## How to run
+``` bash
+# Install dependencies
+yarn
+OR
+npm install
+
+# Serve with hot reload at localhost:8080
+yarn run dev
+
+# Build for production with minification
+yarn run build
+
+# Run unit tests
+yarn run unit
+# Report will be located at: test/unit/reports/test-report.html
+# Coverage report will be located at: test/unit/reports/coverage/index.html
+
+# Run e2e tests with output to console
+yarn run e2e
+
+# Generate HTML report (after running e2e)
+yarn run e2e-html-report
+# Report will be located at: test/e2e/reports/html/cucumber_report.html
+
+# Perform ESLint code check
+yarn run lint
+```
+
+## Other commands
+```
+# build for production and view the bundle analyzer report
+yarn run build --report
+
+# Run unit tests in watch mode for development
+yarn run unit-watch
+
+# Run e2e and generate HTML report together
+# (!) Report will not be generated in case of error in e2e tests
+yarn run e2e-html-report
+```
+
+## WebStorm / Intellij IDEA configuration
+
+Enable ESlint plugin, Vue plugin and TypeScript integration.
+All of these should work by default.
+
+### Unit testing via Jest
+
+1. Click Run in the main toolbar
+2. Edit Configurations
+3. On the top left of the Run/Debug Configurations dialog, click the + sign.
+4. Choose Jest
+5. Name the new configuration "Jest"
+6. Under "Configuration file" enter `{YOUR_PATH}\test\unit\jest.conf.js` (be sure to change {YOUR_PATH})
+7. Click Apply
+
+You can now both run Unit tests and debug them inside the IDE.
+
+### e2e testing via Nightwatch
+
+1. Click Run in the main toolbar
+2. Edit Configurations
+3. On the top left of the Run/Debug Configurations dialog, click the + sign.
+4. Choose Node.js
+5. Name the new configuration "Nightwatch"
+6. Under "JavaScript file" enter `node_modules\nightwatch\bin\runner.js`
+7. Under "Application parameters" enter `--config test/e2e/nightwatch.conf.js --env chrome`
+8. Click Apply
+
+You can now both run e2e tests and debug them inside the IDE.
+
+**Note that this is different from running via console** `yarn run e2e`
+**which also starts the http server and checks if port is in use.**
+
+**If your app is not served at default port (8080), change devServerURL in** `nightwatch.conf.js`
+
+**For this to work you will need a running SPA in background, e.g. run** `yarn run dev` **and start e2e in IDE afterwards.**
+
+For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
diff --git a/client/build/build.js b/client/build/build.js
new file mode 100644
index 00000000..8f2ad8ad
--- /dev/null
+++ b/client/build/build.js
@@ -0,0 +1,41 @@
+'use strict'
+require('./check-versions')()
+
+process.env.NODE_ENV = 'production'
+
+const ora = require('ora')
+const rm = require('rimraf')
+const path = require('path')
+const chalk = require('chalk')
+const webpack = require('webpack')
+const config = require('../config')
+const webpackConfig = require('./webpack.prod.conf')
+
+const spinner = ora('building for production...')
+spinner.start()
+
+rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
+ if (err) throw err
+ webpack(webpackConfig, (err, stats) => {
+ spinner.stop()
+ if (err) throw err
+ process.stdout.write(stats.toString({
+ colors: true,
+ modules: false,
+ children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
+ chunks: false,
+ chunkModules: false
+ }) + '\n\n')
+
+ if (stats.hasErrors()) {
+ console.log(chalk.red(' Build failed with errors.\n'))
+ process.exit(1)
+ }
+
+ console.log(chalk.cyan(' Build complete.\n'))
+ console.log(chalk.yellow(
+ ' Tip: built files are meant to be served over an HTTP server.\n' +
+ ' Opening index.html over file:// won\'t work.\n'
+ ))
+ })
+})
diff --git a/client/build/check-versions.js b/client/build/check-versions.js
new file mode 100644
index 00000000..3ef972a0
--- /dev/null
+++ b/client/build/check-versions.js
@@ -0,0 +1,54 @@
+'use strict'
+const chalk = require('chalk')
+const semver = require('semver')
+const packageConfig = require('../package.json')
+const shell = require('shelljs')
+
+function exec (cmd) {
+ return require('child_process').execSync(cmd).toString().trim()
+}
+
+const versionRequirements = [
+ {
+ name: 'node',
+ currentVersion: semver.clean(process.version),
+ versionRequirement: packageConfig.engines.node
+ }
+]
+
+if (shell.which('npm')) {
+ versionRequirements.push({
+ name: 'npm',
+ currentVersion: exec('npm --version'),
+ versionRequirement: packageConfig.engines.npm
+ })
+}
+
+module.exports = function () {
+ const warnings = []
+
+ for (let i = 0; i < versionRequirements.length; i++) {
+ const mod = versionRequirements[i]
+
+ if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
+ warnings.push(mod.name + ': ' +
+ chalk.red(mod.currentVersion) + ' should be ' +
+ chalk.green(mod.versionRequirement)
+ )
+ }
+ }
+
+ if (warnings.length) {
+ console.log('')
+ console.log(chalk.yellow('To use this template, you must update following to modules:'))
+ console.log()
+
+ for (let i = 0; i < warnings.length; i++) {
+ const warning = warnings[i]
+ console.log(' ' + warning)
+ }
+
+ console.log()
+ process.exit(1)
+ }
+}
diff --git a/client/build/logo.png b/client/build/logo.png
new file mode 100644
index 00000000..f3d2503f
Binary files /dev/null and b/client/build/logo.png differ
diff --git a/client/build/utils.js b/client/build/utils.js
new file mode 100644
index 00000000..5e3d20a9
--- /dev/null
+++ b/client/build/utils.js
@@ -0,0 +1,101 @@
+'use strict'
+const path = require('path')
+const config = require('../config')
+const ExtractTextPlugin = require('extract-text-webpack-plugin')
+const packageConfig = require('../package.json')
+
+exports.assetsPath = function (_path) {
+ const assetsSubDirectory = process.env.NODE_ENV === 'production'
+ ? config.build.assetsSubDirectory
+ : config.dev.assetsSubDirectory
+
+ return path.posix.join(assetsSubDirectory, _path)
+}
+
+exports.cssLoaders = function (options) {
+ options = options || {}
+
+ const cssLoader = {
+ loader: 'css-loader',
+ options: {
+ sourceMap: options.sourceMap
+ }
+ }
+
+ const postcssLoader = {
+ loader: 'postcss-loader',
+ options: {
+ sourceMap: options.sourceMap
+ }
+ }
+
+ // generate loader string to be used with extract text plugin
+ function generateLoaders (loader, loaderOptions) {
+ const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
+
+ if (loader) {
+ loaders.push({
+ loader: loader + '-loader',
+ options: Object.assign({}, loaderOptions, {
+ sourceMap: options.sourceMap
+ })
+ })
+ }
+
+ // Extract CSS when that option is specified
+ // (which is the case during production build)
+ if (options.extract) {
+ return ExtractTextPlugin.extract({
+ use: loaders,
+ fallback: 'vue-style-loader'
+ })
+ } else {
+ return ['vue-style-loader'].concat(loaders)
+ }
+ }
+
+ // https://vue-loader.vuejs.org/en/configurations/extract-css.html
+ return {
+ css: generateLoaders(),
+ postcss: generateLoaders(),
+ less: generateLoaders('less'),
+ sass: generateLoaders('sass', { indentedSyntax: true }),
+ scss: generateLoaders('sass'),
+ stylus: generateLoaders('stylus'),
+ styl: generateLoaders('stylus')
+ }
+}
+
+// Generate loaders for standalone style files (outside of .vue)
+exports.styleLoaders = function (options) {
+ const output = []
+ const loaders = exports.cssLoaders(options)
+
+ for (const extension in loaders) {
+ const loader = loaders[extension]
+ output.push({
+ test: new RegExp('\\.' + extension + '$'),
+ use: loader
+ })
+ }
+
+ return output
+}
+
+exports.createNotifierCallback = () => {
+ const notifier = require('node-notifier')
+
+ return (severity, errors) => {
+ if (severity !== 'error') return
+
+ const error = errors[0]
+ const filename = error.file && error.file.split('!').pop()
+
+ notifier.notify({
+ title: 'Error',
+ message: error.file,
+ subtitle: filename || '',
+ icon: path.join(__dirname, 'logo.png')
+ })
+ }
+}
diff --git a/client/build/vue-loader.conf.js b/client/build/vue-loader.conf.js
new file mode 100644
index 00000000..33ed58bc
--- /dev/null
+++ b/client/build/vue-loader.conf.js
@@ -0,0 +1,22 @@
+'use strict'
+const utils = require('./utils')
+const config = require('../config')
+const isProduction = process.env.NODE_ENV === 'production'
+const sourceMapEnabled = isProduction
+ ? config.build.productionSourceMap
+ : config.dev.cssSourceMap
+
+module.exports = {
+ loaders: utils.cssLoaders({
+ sourceMap: sourceMapEnabled,
+ extract: isProduction
+ }),
+ cssSourceMap: sourceMapEnabled,
+ cacheBusting: config.dev.cacheBusting,
+ transformToRequire: {
+ video: ['src', 'poster'],
+ source: 'src',
+ img: 'src',
+ image: 'xlink:href'
+ }
+}
diff --git a/client/build/webpack.base.conf.js b/client/build/webpack.base.conf.js
new file mode 100644
index 00000000..09db511a
--- /dev/null
+++ b/client/build/webpack.base.conf.js
@@ -0,0 +1,105 @@
+'use strict'
+const path = require('path')
+const utils = require('./utils')
+const config = require('../config')
+const vueLoaderConfig = require('./vue-loader.conf')
+
+function resolve (dir) {
+ return path.join(__dirname, '..', dir)
+}
+
+// i18n with YAML format support
+vueLoaderConfig.loaders.i18n = '@kazupon/vue-i18n-loader';
+vueLoaderConfig.preLoaders = {
+ i18n: 'yaml-loader'
+};
+
+const createLintingRule = () => ({
+ test: /\.(js|vue)$/,
+ loader: 'eslint-loader',
+ enforce: 'pre',
+ include: [resolve('src'), resolve('test')],
+ options: {
+ formatter: require('eslint-friendly-formatter'),
+ emitWarning: !config.dev.showEslintErrorsInOverlay
+ }
+})
+
+module.exports = {
+ context: path.resolve(__dirname, '../'),
+ entry: {
+ app: './src/main.ts'
+ },
+ output: {
+ path: config.build.assetsRoot,
+ filename: '[name].js',
+ publicPath: process.env.NODE_ENV === 'production'
+ ? config.build.assetsPublicPath
+ : config.dev.assetsPublicPath
+ },
+ resolve: {
+ extensions: ['.ts', '.js', '.vue', '.json'],
+ alias: {
+ 'vue$': 'vue/dist/vue.esm.js',
+ '@': resolve('src'),
+ }
+ },
+ module: {
+ rules: [
+ ...(config.dev.useEslint ? [createLintingRule()] : []),
+ {
+ test: /\.vue$/,
+ loader: 'vue-loader',
+ options: vueLoaderConfig
+ },
+ {
+ test: /\.js$/,
+ loader: 'babel-loader',
+ include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
+ },
+ {
+ test: /\.ts$/,
+ loader: "ts-loader",
+ options: {
+ appendTsSuffixTo: [/\.vue$/]
+ }
+ },
+ {
+ test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
+ loader: 'url-loader',
+ options: {
+ limit: 10000,
+ name: utils.assetsPath('img/[name].[hash:7].[ext]')
+ }
+ },
+ {
+ test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
+ loader: 'url-loader',
+ options: {
+ limit: 10000,
+ name: utils.assetsPath('media/[name].[hash:7].[ext]')
+ }
+ },
+ {
+ test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
+ loader: 'url-loader',
+ options: {
+ limit: 10000,
+ name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
+ }
+ }
+ ]
+ },
+ node: {
+ // prevent webpack from injecting useless setImmediate polyfill because Vue
+ // source contains it (although only uses it if it's native).
+ setImmediate: false,
+ // prevent webpack from injecting mocks to Node native modules
+ // that does not make sense for the client
+ dgram: 'empty',
+ fs: 'empty',
+ net: 'empty',
+ tls: 'empty',
+ child_process: 'empty'
+ }
+}
diff --git a/client/build/webpack.dev.conf.js b/client/build/webpack.dev.conf.js
new file mode 100644
index 00000000..070ae221
--- /dev/null
+++ b/client/build/webpack.dev.conf.js
@@ -0,0 +1,95 @@
+'use strict'
+const utils = require('./utils')
+const webpack = require('webpack')
+const config = require('../config')
+const merge = require('webpack-merge')
+const path = require('path')
+const baseWebpackConfig = require('./webpack.base.conf')
+const CopyWebpackPlugin = require('copy-webpack-plugin')
+const HtmlWebpackPlugin = require('html-webpack-plugin')
+const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
+const portfinder = require('portfinder')
+
+const HOST = process.env.HOST
+const PORT = process.env.PORT && Number(process.env.PORT)
+
+const devWebpackConfig = merge(baseWebpackConfig, {
+ module: {
+ rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
+ },
+ // cheap-module-eval-source-map is faster for development
+ devtool: config.dev.devtool,
+
+ // these devServer options should be customized in /config/index.js
+ devServer: {
+ clientLogLevel: 'warning',
+ historyApiFallback: {
+ rewrites: [
+ { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
+ ],
+ },
+ hot: true,
+ contentBase: false, // since we use CopyWebpackPlugin.
+ compress: true,
+ host: HOST || config.dev.host,
+ port: PORT || config.dev.port,
+ open: config.dev.autoOpenBrowser,
+ overlay: config.dev.errorOverlay
+ ? { warnings: false, errors: true }
+ : false,
+ publicPath: config.dev.assetsPublicPath,
+ proxy: config.dev.proxyTable,
+ quiet: true, // necessary for FriendlyErrorsPlugin
+ watchOptions: {
+ poll: config.dev.poll,
+ }
+ },
+ plugins: [
+ new webpack.DefinePlugin({
+ 'process.env': require('../config/dev.env')
+ }),
+ new webpack.HotModuleReplacementPlugin(),
+ new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
+ new webpack.NoEmitOnErrorsPlugin(),
+ // https://github.com/ampedandwired/html-webpack-plugin
+ new HtmlWebpackPlugin({
+ filename: 'index.html',
+ template: 'index.html',
+ inject: true
+ }),
+ // copy custom static assets
+ new CopyWebpackPlugin([
+ {
+ from: path.resolve(__dirname, '../static'),
+ to: config.dev.assetsSubDirectory,
+ ignore: ['.*']
+ }
+ ])
+ ]
+})
+
+module.exports = new Promise((resolve, reject) => {
+ portfinder.basePort = process.env.PORT || config.dev.port
+ portfinder.getPort((err, port) => {
+ if (err) {
+ reject(err)
+ } else {
+ // publish the new Port, necessary for e2e tests
+ process.env.PORT = port
+ // add port to devServer config
+ devWebpackConfig.devServer.port = port
+
+ // Add FriendlyErrorsPlugin
+ devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
+ compilationSuccessInfo: {
+ messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
+ },
+ onErrors: config.dev.notifyOnErrors
+ ? utils.createNotifierCallback()
+ : undefined
+ }))
+
+ resolve(devWebpackConfig)
+ }
+ })
+})
diff --git a/client/build/webpack.prod.conf.js b/client/build/webpack.prod.conf.js
new file mode 100644
index 00000000..2f172596
--- /dev/null
+++ b/client/build/webpack.prod.conf.js
@@ -0,0 +1,149 @@
+'use strict'
+const path = require('path')
+const utils = require('./utils')
+const webpack = require('webpack')
+const config = require('../config')
+const merge = require('webpack-merge')
+const baseWebpackConfig = require('./webpack.base.conf')
+const CopyWebpackPlugin = require('copy-webpack-plugin')
+const HtmlWebpackPlugin = require('html-webpack-plugin')
+const ExtractTextPlugin = require('extract-text-webpack-plugin')
+const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
+const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
+
+const env = process.env.NODE_ENV === 'testing'
+ ? require('../config/test.env')
+ : require('../config/prod.env')
+
+const webpackConfig = merge(baseWebpackConfig, {
+ module: {
+ rules: utils.styleLoaders({
+ sourceMap: config.build.productionSourceMap,
+ extract: true,
+ usePostCSS: true
+ })
+ },
+ devtool: config.build.productionSourceMap ? config.build.devtool : false,
+ output: {
+ path: config.build.assetsRoot,
+ filename: utils.assetsPath('js/[name].[chunkhash].js'),
+ chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
+ },
+ plugins: [
+ // http://vuejs.github.io/vue-loader/en/workflow/production.html
+ new webpack.DefinePlugin({
+ 'process.env': env
+ }),
+ new UglifyJsPlugin({
+ uglifyOptions: {
+ compress: {
+ warnings: false
+ }
+ },
+ sourceMap: config.build.productionSourceMap,
+ parallel: true
+ }),
+ // extract css into its own file
+ new ExtractTextPlugin({
+ filename: utils.assetsPath('css/[name].[contenthash].css'),
+ // Setting the following option to `false` will not extract CSS from codesplit chunks.
+ // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
+ // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
+ // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
+ allChunks: true,
+ }),
+ // Compress extracted CSS. We are using this plugin so that possible
+ // duplicated CSS from different components can be deduped.
+ new OptimizeCSSPlugin({
+ cssProcessorOptions: config.build.productionSourceMap
+ ? { safe: true, map: { inline: false } }
+ : { safe: true }
+ }),
+ // generate dist index.html with correct asset hash for caching.
+ // you can customize output by editing /index.html
+ // see https://github.com/ampedandwired/html-webpack-plugin
+ new HtmlWebpackPlugin({
+ filename: process.env.NODE_ENV === 'testing'
+ ? 'index.html'
+ : config.build.index,
+ template: 'index.html',
+ inject: true,
+ minify: {
+ removeComments: true,
+ collapseWhitespace: true,
+ removeAttributeQuotes: true
+ // more options:
+ // https://github.com/kangax/html-minifier#options-quick-reference
+ },
+ // necessary to consistently work with multiple chunks via CommonsChunkPlugin
+ chunksSortMode: 'dependency'
+ }),
+ // keep module.id stable when vendor modules does not change
+ new webpack.HashedModuleIdsPlugin(),
+ // enable scope hoisting
+ new webpack.optimize.ModuleConcatenationPlugin(),
+ // split vendor js into its own file
+ new webpack.optimize.CommonsChunkPlugin({
+ name: 'vendor',
+ minChunks (module) {
+ // any required modules inside node_modules are extracted to vendor
+ return (
+ module.resource &&
+ /\.js$/.test(module.resource) &&
+ module.resource.indexOf(
+ path.join(__dirname, '../node_modules')
+ ) === 0
+ )
+ }
+ }),
+ // extract webpack runtime and module manifest to its own file in order to
+ // prevent vendor hash from being updated whenever app bundle is updated
+ new webpack.optimize.CommonsChunkPlugin({
+ name: 'manifest',
+ minChunks: Infinity
+ }),
+ // This instance extracts shared chunks from code splitted chunks and bundles them
+ // in a separate chunk, similar to the vendor chunk
+ // see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
+ new webpack.optimize.CommonsChunkPlugin({
+ name: 'app',
+ async: 'vendor-async',
+ children: true,
+ minChunks: 3
+ }),
+
+ // copy custom static assets
+ new CopyWebpackPlugin([
+ {
+ from: path.resolve(__dirname, '../static'),
+ to: config.build.assetsSubDirectory,
+ ignore: ['.*']
+ }
+ ])
+ ]
+})
+
+if (config.build.productionGzip) {
+ const CompressionWebpackPlugin = require('compression-webpack-plugin')
+
+ webpackConfig.plugins.push(
+ new CompressionWebpackPlugin({
+ asset: '[path].gz[query]',
+ algorithm: 'gzip',
+ test: new RegExp(
+ '\\.(' +
+ config.build.productionGzipExtensions.join('|') +
+ ')$'
+ ),
+ threshold: 10240,
+ minRatio: 0.8
+ })
+ )
+}
+
+if (config.build.bundleAnalyzerReport) {
+ const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
+ webpackConfig.plugins.push(new BundleAnalyzerPlugin())
+}
+
+module.exports = webpackConfig
diff --git a/client/config/dev.env.js b/client/config/dev.env.js
new file mode 100644
index 00000000..1e22973a
--- /dev/null
+++ b/client/config/dev.env.js
@@ -0,0 +1,7 @@
+'use strict'
+const merge = require('webpack-merge')
+const prodEnv = require('./prod.env')
+
+module.exports = merge(prodEnv, {
+ NODE_ENV: '"development"'
+})
diff --git a/client/config/index.js b/client/config/index.js
new file mode 100644
index 00000000..44ee94ec
--- /dev/null
+++ b/client/config/index.js
@@ -0,0 +1,79 @@
+'use strict'
+// Template version: 1.3.1
+// see http://vuejs-templates.github.io/webpack for documentation.
+
+const path = require('path')
+
+module.exports = {
+ dev: {
+
+ // Paths
+ assetsSubDirectory: 'static',
+ assetsPublicPath: '/',
+ proxyTable: {
+ '/api': 'http://localhost:3000',
+ '/server/images': 'http://localhost:3000'
+ },
+
+ // Various Dev Server settings
+ host: 'localhost', // can be overwritten by process.env.HOST
+ port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
+ autoOpenBrowser: false,
+ errorOverlay: true,
+ notifyOnErrors: true,
+ poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
+
+ // Use Eslint Loader?
+ // If true, your code will be linted during bundling and
+ // linting errors and warnings will be shown in the console.
+ useEslint: true,
+ // If true, eslint errors and warnings will also be shown in the error overlay
+ // in the browser.
+ showEslintErrorsInOverlay: false,
+
+ /**
+ * Source Maps
+ */
+
+ // https://webpack.js.org/configuration/devtool/#development
+ devtool: 'cheap-module-eval-source-map',
+
+ // If you have problems debugging vue-files in devtools,
+ // set this to false - it *may* help
+ // https://vue-loader.vuejs.org/en/options.html#cachebusting
+ cacheBusting: true,
+
+ cssSourceMap: true
+ },
+
+ build: {
+ // Template for index.html
+ index: path.resolve(__dirname, '../dist/index.html'),
+
+ // Paths
+ assetsRoot: path.resolve(__dirname, '../dist'),
+ assetsSubDirectory: 'static',
+ assetsPublicPath: '/',
+
+ /**
+ * Source Maps
+ */
+
+ productionSourceMap: true,
+ // https://webpack.js.org/configuration/devtool/#production
+ devtool: '#source-map',
+
+ // Gzip off by default as many popular static hosts such as
+ // Surge or Netlify already gzip all static assets for you.
+ // Before setting to `true`, make sure to:
+ // npm install --save-dev compression-webpack-plugin
+ productionGzip: false,
+ productionGzipExtensions: ['js', 'css'],
+
+ // Run the build command with an extra argument to
+ // View the bundle analyzer report after build finishes:
+ // `npm run build --report`
+ // Set to `true` or `false` to always turn it on or off
+ bundleAnalyzerReport: process.env.npm_config_report
+ }
+}
diff --git a/client/config/prod.env.js b/client/config/prod.env.js
new file mode 100644
index 00000000..a6f99761
--- /dev/null
+++ b/client/config/prod.env.js
@@ -0,0 +1,4 @@
+'use strict'
+module.exports = {
+ NODE_ENV: '"production"'
+}
diff --git a/client/config/test.env.js b/client/config/test.env.js
new file mode 100644
index 00000000..c2824a30
--- /dev/null
+++ b/client/config/test.env.js
@@ -0,0 +1,7 @@
+'use strict'
+const merge = require('webpack-merge')
+const devEnv = require('./dev.env')
+
+module.exports = merge(devEnv, {
+ NODE_ENV: '"testing"'
+})
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 00000000..7c79f670
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ T-Portfolio
+
+
+
+
+
+
diff --git a/client/jesthtmlreporter.config.json b/client/jesthtmlreporter.config.json
new file mode 100644
index 00000000..f54cb443
--- /dev/null
+++ b/client/jesthtmlreporter.config.json
@@ -0,0 +1,4 @@
+{
+ "outputPath": "./test/unit/reports/test-report.html",
+ "includeFailureMsg": true
+}
diff --git a/client/package.json b/client/package.json
new file mode 100644
index 00000000..9d9937fc
--- /dev/null
+++ b/client/package.json
@@ -0,0 +1,112 @@
+{
+ "name": "t-portfolio-client",
+ "version": "1.0.0",
+ "description": "A Vue.js project",
+ "author": "Igogrek ",
+ "private": true,
+ "scripts": {
+ "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
+ "start": "npm run dev",
+ "unit": "jest --config test/unit/jest.conf.js --coverage",
+ "unit-watch": "jest --config test/unit/jest.conf.js --watch",
+ "e2e": "node test/e2e/runner.js",
+ "e2e-html-report": "node test/e2e/html-report.js",
+ "e2e-report": "npm run e2e && npm run e2e-html-report",
+ "test": "npm run unit && npm run e2e",
+ "lint": "eslint --ext .js,.vue src test/unit test/e2e/specs",
+ "build": "node build/build.js"
+ },
+ "dependencies": {
+ "axios": "^0.18.0",
+ "buefy": "^0.6.6",
+ "bulma": "^0.6.2",
+ "eslint-plugin-typescript": "^0.11.0",
+ "pptxgenjs": "^2.1.0",
+ "vue": "^2.5.2",
+ "vue-i18n": "^7.8.0",
+ "vue-router": "^3.0.1",
+ "vuelidate": "^0.7.4",
+ "vuex": "^3.0.1"
+ },
+ "devDependencies": {
+ "@kazupon/vue-i18n-loader": "^0.3.0",
+ "@types/jest": "^22.1.3",
+ "@vue/test-utils": "^1.0.0-beta.12",
+ "autoprefixer": "^7.1.2",
+ "babel-core": "^6.22.1",
+ "babel-eslint": "^8.2.1",
+ "babel-helper-vue-jsx-merge-props": "^2.0.3",
+ "babel-jest": "^21.0.2",
+ "babel-loader": "^7.1.1",
+ "babel-plugin-dynamic-import-node": "^1.2.0",
+ "babel-plugin-syntax-jsx": "^6.18.0",
+ "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
+ "babel-plugin-transform-runtime": "^6.22.0",
+ "babel-plugin-transform-vue-jsx": "^3.5.0",
+ "babel-preset-env": "^1.3.2",
+ "babel-preset-stage-2": "^6.22.0",
+ "babel-register": "^6.22.0",
+ "chalk": "^2.0.1",
+ "chromedriver": "^2.27.2",
+ "copy-webpack-plugin": "^4.0.1",
+ "cross-spawn": "^5.0.1",
+ "css-loader": "^0.28.0",
+ "cucumber": "^4.0.0",
+ "cucumber-html-reporter": "^4.0.1",
+ "cucumber-pretty": "^1.4.0",
+ "eslint": "^4.15.0",
+ "eslint-config-airbnb-base": "^11.3.0",
+ "eslint-friendly-formatter": "^3.0.0",
+ "eslint-import-resolver-webpack": "^0.8.3",
+ "eslint-loader": "^1.7.1",
+ "eslint-plugin-import": "^2.7.0",
+ "eslint-plugin-vue": "^4.0.0",
+ "extract-text-webpack-plugin": "^3.0.0",
+ "file-loader": "^1.1.4",
+ "friendly-errors-webpack-plugin": "^1.6.1",
+ "html-webpack-plugin": "^2.30.1",
+ "jest": "^22.0.4",
+ "jest-html-reporter": "^1.2.0",
+ "jest-serializer-vue": "^0.3.0",
+ "nightwatch": "^0.9.19",
+ "nightwatch-cucumber": "^9.1.0",
+ "node-notifier": "^5.1.2",
+ "node-sass": "^4.7.2",
+ "optimize-css-assets-webpack-plugin": "^3.2.0",
+ "ora": "^1.2.0",
+ "portfinder": "^1.0.13",
+ "postcss-import": "^11.0.0",
+ "postcss-loader": "^2.0.8",
+ "postcss-url": "^7.2.1",
+ "rimraf": "^2.6.0",
+ "sass-loader": "^6.0.6",
+ "selenium-server": "^3.0.1",
+ "semver": "^5.3.0",
+ "shelljs": "^0.7.6",
+ "style-loader": "^0.20.2",
+ "ts-jest": "^22.0.4",
+ "ts-loader": "^3.5.0",
+ "typescript": "^2.7.2",
+ "typescript-eslint-parser": "^13.0.0",
+ "uglifyjs-webpack-plugin": "^1.1.1",
+ "url-loader": "^0.5.8",
+ "vue-jest": "^1.0.2",
+ "vue-loader": "^13.3.0",
+ "vue-style-loader": "^3.0.1",
+ "vue-template-compiler": "^2.5.2",
+ "webpack": "^3.6.0",
+ "webpack-bundle-analyzer": "^2.9.0",
+ "webpack-dev-server": "^2.9.1",
+ "webpack-merge": "^4.1.0",
+ "yaml-loader": "^0.5.0"
+ },
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ },
+ "browserslist": [
+ "> 1%",
+ "last 2 versions",
+ "not ie <= 8"
+ ]
+}
diff --git a/client/src/.DS_Store b/client/src/.DS_Store
new file mode 100644
index 00000000..c4261e3a
Binary files /dev/null and b/client/src/.DS_Store differ
diff --git a/client/src/App.vue b/client/src/App.vue
new file mode 100644
index 00000000..0c359055
--- /dev/null
+++ b/client/src/App.vue
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
diff --git a/client/src/assets/fonts/TeleGroteskNext-Bold.woff b/client/src/assets/fonts/TeleGroteskNext-Bold.woff
new file mode 100644
index 00000000..487172fd
Binary files /dev/null and b/client/src/assets/fonts/TeleGroteskNext-Bold.woff differ
diff --git a/client/src/assets/fonts/TeleGroteskNext-Bold.woff2 b/client/src/assets/fonts/TeleGroteskNext-Bold.woff2
new file mode 100644
index 00000000..fbcc3918
Binary files /dev/null and b/client/src/assets/fonts/TeleGroteskNext-Bold.woff2 differ
diff --git a/client/src/assets/fonts/TeleGroteskNext-BoldItalic.woff b/client/src/assets/fonts/TeleGroteskNext-BoldItalic.woff
new file mode 100644
index 00000000..7aaab08b
Binary files /dev/null and b/client/src/assets/fonts/TeleGroteskNext-BoldItalic.woff differ
diff --git a/client/src/assets/fonts/TeleGroteskNext-BoldItalic.woff2 b/client/src/assets/fonts/TeleGroteskNext-BoldItalic.woff2
new file mode 100644
index 00000000..6e94b5f5
Binary files /dev/null and b/client/src/assets/fonts/TeleGroteskNext-BoldItalic.woff2 differ
diff --git a/client/src/assets/fonts/TeleGroteskNext-Medium.woff b/client/src/assets/fonts/TeleGroteskNext-Medium.woff
new file mode 100644
index 00000000..ec95ac9a
Binary files /dev/null and b/client/src/assets/fonts/TeleGroteskNext-Medium.woff differ
diff --git a/client/src/assets/fonts/TeleGroteskNext-Medium.woff2 b/client/src/assets/fonts/TeleGroteskNext-Medium.woff2
new file mode 100644
index 00000000..7f5fbf4c
Binary files /dev/null and b/client/src/assets/fonts/TeleGroteskNext-Medium.woff2 differ
diff --git a/client/src/assets/fonts/TeleGroteskNext-MediumItalic.woff b/client/src/assets/fonts/TeleGroteskNext-MediumItalic.woff
new file mode 100644
index 00000000..8d1ea064
Binary files /dev/null and b/client/src/assets/fonts/TeleGroteskNext-MediumItalic.woff differ
diff --git a/client/src/assets/fonts/TeleGroteskNext-MediumItalic.woff2 b/client/src/assets/fonts/TeleGroteskNext-MediumItalic.woff2
new file mode 100644
index 00000000..c9950a0a
Binary files /dev/null and b/client/src/assets/fonts/TeleGroteskNext-MediumItalic.woff2 differ
diff --git a/client/src/assets/fonts/TeleGroteskNext-Regular.woff b/client/src/assets/fonts/TeleGroteskNext-Regular.woff
new file mode 100644
index 00000000..3a8dd415
Binary files /dev/null and b/client/src/assets/fonts/TeleGroteskNext-Regular.woff differ
diff --git a/client/src/assets/fonts/TeleGroteskNext-Regular.woff2 b/client/src/assets/fonts/TeleGroteskNext-Regular.woff2
new file mode 100644
index 00000000..54e5524b
Binary files /dev/null and b/client/src/assets/fonts/TeleGroteskNext-Regular.woff2 differ
diff --git a/client/src/assets/fonts/TeleGroteskNext-RegularItalic.woff b/client/src/assets/fonts/TeleGroteskNext-RegularItalic.woff
new file mode 100644
index 00000000..c171f9b4
Binary files /dev/null and b/client/src/assets/fonts/TeleGroteskNext-RegularItalic.woff differ
diff --git a/client/src/assets/fonts/TeleGroteskNext-RegularItalic.woff2 b/client/src/assets/fonts/TeleGroteskNext-RegularItalic.woff2
new file mode 100644
index 00000000..ef407dc1
Binary files /dev/null and b/client/src/assets/fonts/TeleGroteskNext-RegularItalic.woff2 differ
diff --git a/client/src/assets/fonts/TeleGroteskNext-Thin.woff b/client/src/assets/fonts/TeleGroteskNext-Thin.woff
new file mode 100644
index 00000000..08f70429
Binary files /dev/null and b/client/src/assets/fonts/TeleGroteskNext-Thin.woff differ
diff --git a/client/src/assets/fonts/TeleGroteskNext-Thin.woff2 b/client/src/assets/fonts/TeleGroteskNext-Thin.woff2
new file mode 100644
index 00000000..b4b77361
Binary files /dev/null and b/client/src/assets/fonts/TeleGroteskNext-Thin.woff2 differ
diff --git a/client/src/assets/fonts/TeleGroteskNext-Ultra.woff b/client/src/assets/fonts/TeleGroteskNext-Ultra.woff
new file mode 100644
index 00000000..3297178c
Binary files /dev/null and b/client/src/assets/fonts/TeleGroteskNext-Ultra.woff differ
diff --git a/client/src/assets/fonts/TeleGroteskNext-Ultra.woff2 b/client/src/assets/fonts/TeleGroteskNext-Ultra.woff2
new file mode 100644
index 00000000..76d889f4
Binary files /dev/null and b/client/src/assets/fonts/TeleGroteskNext-Ultra.woff2 differ
diff --git a/client/src/assets/images/close.svg b/client/src/assets/images/close.svg
new file mode 100644
index 00000000..cefc2345
--- /dev/null
+++ b/client/src/assets/images/close.svg
@@ -0,0 +1,12 @@
+
diff --git a/client/src/assets/images/close_fill.svg b/client/src/assets/images/close_fill.svg
new file mode 100644
index 00000000..ac5f0d4a
--- /dev/null
+++ b/client/src/assets/images/close_fill.svg
@@ -0,0 +1,12 @@
+
diff --git a/client/src/components/.DS_Store b/client/src/components/.DS_Store
new file mode 100644
index 00000000..80a2caab
Binary files /dev/null and b/client/src/components/.DS_Store differ
diff --git a/client/src/components/common/.DS_Store b/client/src/components/common/.DS_Store
new file mode 100644
index 00000000..34404070
Binary files /dev/null and b/client/src/components/common/.DS_Store differ
diff --git a/client/src/components/common/Accordion/Accordion.test.ts b/client/src/components/common/Accordion/Accordion.test.ts
new file mode 100644
index 00000000..6dad62ca
--- /dev/null
+++ b/client/src/components/common/Accordion/Accordion.test.ts
@@ -0,0 +1,14 @@
+import {mount} from '@vue/test-utils';
+import Accordion from './Accordion.vue';
+
+describe('Accordion', () => {
+
+ const wrapper = mount(Accordion);
+
+ it('change value on close button click', () => {
+ wrapper.vm.toggleOpened();
+
+ expect(wrapper.emitted()['update:opened']).toEqual([[false]]);
+ });
+
+});
diff --git a/client/src/components/common/Accordion/Accordion.vue b/client/src/components/common/Accordion/Accordion.vue
new file mode 100644
index 00000000..e2530525
--- /dev/null
+++ b/client/src/components/common/Accordion/Accordion.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/common/Autocomplete/Autocomplete.vue b/client/src/components/common/Autocomplete/Autocomplete.vue
new file mode 100644
index 00000000..f5d3a10c
--- /dev/null
+++ b/client/src/components/common/Autocomplete/Autocomplete.vue
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/common/Checkbox/Checkbox.test.ts b/client/src/components/common/Checkbox/Checkbox.test.ts
new file mode 100644
index 00000000..026a12d0
--- /dev/null
+++ b/client/src/components/common/Checkbox/Checkbox.test.ts
@@ -0,0 +1,18 @@
+import {mount} from '@vue/test-utils'
+import Checkbox from './Checkbox.vue'
+
+describe('Checkbox', () => {
+
+ const wrapper = mount(Checkbox);
+
+ it('unchecked by default', () => {
+ expect(wrapper.vm.checked).toEqual(false);
+ });
+
+ it('change value on check', () => {
+ wrapper.vm.toggleCheck();
+
+ expect(wrapper.emitted()['update:checked']).toEqual([[true]]);
+ });
+
+});
diff --git a/client/src/components/common/Checkbox/Checkbox.vue b/client/src/components/common/Checkbox/Checkbox.vue
new file mode 100644
index 00000000..8323c174
--- /dev/null
+++ b/client/src/components/common/Checkbox/Checkbox.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/common/Checkbox/check.svg b/client/src/components/common/Checkbox/check.svg
new file mode 100644
index 00000000..58a76a95
--- /dev/null
+++ b/client/src/components/common/Checkbox/check.svg
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/client/src/components/common/Chip/Chip.test.ts b/client/src/components/common/Chip/Chip.test.ts
new file mode 100644
index 00000000..3b5edf3f
--- /dev/null
+++ b/client/src/components/common/Chip/Chip.test.ts
@@ -0,0 +1,29 @@
+import {mount} from '@vue/test-utils';
+import Chip from './Chip.vue';
+import store from '../../../store/index';
+
+const name = 'Agile';
+
+describe('Chips', () => {
+
+ const wrapper = mount(Chip, {
+ store,
+ propsData: {
+ name
+ },
+ filters: {
+ date(value) {
+ return ""
+ }
+ }
+ });
+
+ it('not active by default', () => {
+ expect(wrapper.vm.isSelected).toEqual(false);
+ });
+
+
+ it('should render correct name property', () => {
+ expect(wrapper.props().name).toBe(name);
+ });
+});
diff --git a/client/src/components/common/Chip/Chip.vue b/client/src/components/common/Chip/Chip.vue
new file mode 100644
index 00000000..06e2d8ca
--- /dev/null
+++ b/client/src/components/common/Chip/Chip.vue
@@ -0,0 +1,137 @@
+
+
+
+
![]()
+
{{ name }}
+
+
+
+
+
+
+
+
diff --git a/client/src/components/common/CommonModal/CommonModal.vue b/client/src/components/common/CommonModal/CommonModal.vue
new file mode 100644
index 00000000..fc2cd3a1
--- /dev/null
+++ b/client/src/components/common/CommonModal/CommonModal.vue
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/common/ConfirmModal/ConfirmModal.vue b/client/src/components/common/ConfirmModal/ConfirmModal.vue
new file mode 100755
index 00000000..a718705b
--- /dev/null
+++ b/client/src/components/common/ConfirmModal/ConfirmModal.vue
@@ -0,0 +1,145 @@
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/common/ConfirmModal/cancel_outline.svg b/client/src/components/common/ConfirmModal/cancel_outline.svg
new file mode 100755
index 00000000..6bc01a2e
--- /dev/null
+++ b/client/src/components/common/ConfirmModal/cancel_outline.svg
@@ -0,0 +1,12 @@
+
diff --git a/client/src/components/common/ErrorMessage/ErrorMessage.vue b/client/src/components/common/ErrorMessage/ErrorMessage.vue
new file mode 100755
index 00000000..9f01cfb2
--- /dev/null
+++ b/client/src/components/common/ErrorMessage/ErrorMessage.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
diff --git a/client/src/components/common/ErrorMessage/ErrorMessage.yml b/client/src/components/common/ErrorMessage/ErrorMessage.yml
new file mode 100755
index 00000000..e0c32991
--- /dev/null
+++ b/client/src/components/common/ErrorMessage/ErrorMessage.yml
@@ -0,0 +1,3 @@
+en:
+
+
diff --git a/client/src/components/common/FileUploader/FileList/FileList.vue b/client/src/components/common/FileUploader/FileList/FileList.vue
new file mode 100644
index 00000000..d332f67f
--- /dev/null
+++ b/client/src/components/common/FileUploader/FileList/FileList.vue
@@ -0,0 +1,225 @@
+
+
+
+
+
+
+
+
+
+
+
{{ file.file.name }}
+
+
+
+
+
+
+
+
+
{{ imageUrl.name }}
+
+
+
+
+
+ {{ $t('deleteFileQuestion') }}
+
+ {{ $t('reallyDelete') }} {{ fileToDelete.file ? fileToDelete.file.name : imageUrl.name }}?
+ {{ $t('actionUndone') }}
+
+ {{ $t('deleteFileBtn') }}
+
+
+
+
+
+
+
diff --git a/client/src/components/common/FileUploader/FileList/FileList.yml b/client/src/components/common/FileUploader/FileList/FileList.yml
new file mode 100644
index 00000000..d7c4efbf
--- /dev/null
+++ b/client/src/components/common/FileUploader/FileList/FileList.yml
@@ -0,0 +1,13 @@
+en:
+ delete: "Delete"
+ deleteFileQuestion: "Delete File?"
+ reallyDelete: "Do you really want to delete the file"
+ actionUndone: "This action can not be undone."
+ deleteFileBtn: "Delete File"
+
+de:
+ delete: "Löschen"
+ deleteFileQuestion: "Datei löschen"
+ reallyDelete: "Wollen Sie die folgende Datei wirklich löschen:"
+ actionUndone: "Diese Aktion kann nicht rückgängig gemacht werden."
+ deleteFileBtn: "Datei löschen"
diff --git a/client/src/components/common/FileUploader/FileUploader.vue b/client/src/components/common/FileUploader/FileUploader.vue
new file mode 100644
index 00000000..51dbb9cb
--- /dev/null
+++ b/client/src/components/common/FileUploader/FileUploader.vue
@@ -0,0 +1,139 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/common/FileUploader/FileUploader.yml b/client/src/components/common/FileUploader/FileUploader.yml
new file mode 100644
index 00000000..43ffbdb7
--- /dev/null
+++ b/client/src/components/common/FileUploader/FileUploader.yml
@@ -0,0 +1,5 @@
+en:
+ addFiles: "Add Files"
+
+de:
+ addFiles: "Dateien hinzufügen"
diff --git a/client/src/components/common/FileUploader/FileUploaderModal/FileUploaderModal.vue b/client/src/components/common/FileUploader/FileUploaderModal/FileUploaderModal.vue
new file mode 100644
index 00000000..bb0e2c4a
--- /dev/null
+++ b/client/src/components/common/FileUploader/FileUploaderModal/FileUploaderModal.vue
@@ -0,0 +1,395 @@
+
+
+
+
+ {{ $t('uploadFiles') }}
+
+
+ {{ $t('defaultOptional') }}
+
+
+
+
+ {{ getInvalidFileNames(fileSizeError.fileNames) }}:
+ {{ $t('fileSizeError') }}
+
+
+ {{ getInvalidFileNames(fileExtensionError.fileNames) }}:
+ {{ $t('fileExtensionError') }}
+
+
+ {{ getInvalidFileNames(imageExtensionError.fileNames) }}:
+ {{ $t('imageExtensionError') }}
+
+
+
+
+ {{ $t('uploadImage') }}
+
+ {{ $t('addUrl') }}
+
+
+
+
+
+ {{ $t('addFiles') }}
+ {{ $t('filesInfo') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('replaceImageQuestion') }}
+
+
+ {{ $t('reallyReplace') }} {{ files[0] ? files[0].file.name : imageUrl.name }}? {{ $t('actionUndone') }}
+
+ {{ $t('replaceImageBtn') }}
+
+
+
+
+
+
+
+
diff --git a/client/src/components/common/FileUploader/FileUploaderModal/FileUploaderModal.yml b/client/src/components/common/FileUploader/FileUploaderModal/FileUploaderModal.yml
new file mode 100644
index 00000000..5a052741
--- /dev/null
+++ b/client/src/components/common/FileUploader/FileUploaderModal/FileUploaderModal.yml
@@ -0,0 +1,39 @@
+en:
+ uploadFiles: "Upload Files"
+ defaultOptional: "Upload an image from your computer or an URL?"
+ addFiles: "Add Files"
+ filesInfo: "Optional files may be added to this new dataset (Maximum file size: 10 MB)"
+ dragFilesHere: "Drag and drop or"
+ selectFiles: "select files from computer"
+ addFilesBtn: "Add Files"
+ skipThisStep: "Skip this step"
+ replaceImageQuestion: "Replace Image"
+ reallyReplace: "Do you really want to replace the image"
+ actionUndone: "This action can not be undone."
+ replaceImageBtn: "Replace Image"
+ uploadImage: "Add From Computer"
+ addUrl: "Add URL"
+ imageUrl: "Image URL"
+ exampleUrl: "http://example.com/image.jpg"
+ fileNotUploadedError: "The following files cannot be uploaded."
+ fileSizeError: "The size is bigger"
+ fileExtensionError: "The file with this extension is not allowed"
+ imageExtensionError: "The file format is not supported. Supported formats: .jpg, .jpeg, .png"
+
+de:
+ uploadFiles: "Dateien hochladen"
+ defaultOptional: "Möchten Sie ein Bild von Ihrem Computer oder von einer URL hochladen?"
+ addFiles: "Dateien hinzufügen"
+ filesInfo: "Zusätzliche Dateien werden dem neuen Datensatz hinzugefügt (Maximale Dateigröße: 10 MB)"
+ dragFilesHere: "Drag & Drop oder"
+ selectFiles: "Dateien vom Computer auswählen"
+ addFilesBtn: "Dateien hinzufügen"
+ skipThisStep: "Schritt überspringen"
+ replaceImageQuestion: "Bild ersetzen"
+ reallyReplace: "Wollen Sie das Bild wirklich ersetzen"
+ actionUndone: "Diese Aktion kann nicht rückgängig gemacht werden."
+ replaceImageBtn: "Bild ersetzen"
+ uploadImage: "Vom Computer"
+ addUrl: "Von URL"
+ imageUrl: "Bild-URL"
+ exampleUrl: "http://beispiel.de/bild.jpg"
diff --git a/client/src/components/common/FileUploader/FileUploaderService.ts b/client/src/components/common/FileUploader/FileUploaderService.ts
new file mode 100644
index 00000000..b9dc5d67
--- /dev/null
+++ b/client/src/components/common/FileUploader/FileUploaderService.ts
@@ -0,0 +1,66 @@
+import {FileUploadStatus, IFileUpload, IImageUrl} from './IFileUploadList';
+import axios from 'axios';
+import Guid from '../../../shared/classes/Guid';
+
+const harmfulFileExtensionRegex = new RegExp('(.|/)(bat|exe|cmd|sh|php([0-9])?|pl|cgi|' +
+ '386|dll|com|torrent|js|app|jar|pif|vb|vbscript|wsf|asp|cer|csr|jsp|drv|sys|ade|adp|bas|chm' +
+ '|cpl|crt|csh|fxp|hlp|hta|inf|ins|isp|jse|htaccess|htpasswd|ksh|lnk|mdb|mde|mdt|mdw' +
+ '|msc|msi|msp|mst|ops|pcd|prg|reg|scr|sct|shb|shs|url|vbe|vbs|wsc|wsf|wsh)$');
+const imageFileExtesionRegex = /\.(jpg|jpeg|png)$/;
+
+export class FileUploaderService {
+ public static fileListToFileUploadList(fileList: FileList): IFileUpload[] {
+ return Array.prototype.map.call(fileList, (item: File) => FileUploaderService.fileToFileUpload(item));
+ }
+
+ public static fileToFileUpload(file: File): IFileUpload {
+ return {
+ file,
+ loadingStatus: FileUploadStatus.NULL,
+ imageDataUrl: ''
+ };
+ }
+
+ public static isFileInUploadList(file: File, fileUploadList: IFileUpload[]): boolean {
+ return fileUploadList.some((item: IFileUpload) => item.file.name === file.name);
+ }
+
+ public static getFileNameFromUrl(url: string): string {
+ return url.split('/').slice(-1)[0];
+ }
+
+ public static urlToImageUrl(url: string): IImageUrl {
+ return {
+ url,
+ name: FileUploaderService.getFileNameFromUrl(url),
+ loadingStatus: FileUploadStatus.NULL
+ };
+ }
+
+ // TODO: should be removed when API is working
+ public static fakeUploadFiles(): Promise {
+ return new Promise(resolve => setTimeout(resolve, Math.random() * 2000));
+ }
+
+ public static uploadFiles(file: IFileUpload) {
+ const fileExtension = file.file.type.replace(/^[^/]*[/]/, '');
+
+ return axios.post('/api/images/add/', {
+ 'data': file.imageDataUrl,
+ 'name': `${Guid.newGuid()}.${fileExtension}`,
+ 'type': fileExtension
+ });
+ }
+
+ public static validateFileSize(file: File, maxSize: number): boolean {
+ return file.size <= maxSize;
+ }
+
+ public static validateFileExtension(fileName: string): boolean {
+ return !harmfulFileExtensionRegex.test(fileName);
+ }
+
+ public static validateImageExtension(fileName: string): boolean {
+ return imageFileExtesionRegex.test(fileName);
+ }
+}
diff --git a/client/src/components/common/FileUploader/IFileUploadList.ts b/client/src/components/common/FileUploader/IFileUploadList.ts
new file mode 100644
index 00000000..54b50234
--- /dev/null
+++ b/client/src/components/common/FileUploader/IFileUploadList.ts
@@ -0,0 +1,18 @@
+export enum FileUploadStatus {
+ NULL = 'NULL',
+ LOADING = 'LOADING',
+ COMPLETED = 'COMPLETED'
+}
+
+export interface IFileUpload {
+ file: File,
+ loadingStatus: FileUploadStatus,
+ imageDataUrl?: string
+}
+
+export interface IImageUrl {
+ url: string;
+ name: string;
+ loadingStatus: FileUploadStatus;
+}
+
diff --git a/client/src/components/common/FileUploader/assets/check.svg b/client/src/components/common/FileUploader/assets/check.svg
new file mode 100644
index 00000000..58a76a95
--- /dev/null
+++ b/client/src/components/common/FileUploader/assets/check.svg
@@ -0,0 +1,9 @@
+
+
+
+
diff --git a/client/src/components/common/FileUploader/assets/download-outline.svg b/client/src/components/common/FileUploader/assets/download-outline.svg
new file mode 100644
index 00000000..827d394d
--- /dev/null
+++ b/client/src/components/common/FileUploader/assets/download-outline.svg
@@ -0,0 +1,12 @@
+
diff --git a/client/src/components/common/FileUploader/assets/drag-and-drop-outline.svg b/client/src/components/common/FileUploader/assets/drag-and-drop-outline.svg
new file mode 100644
index 00000000..033d7da3
--- /dev/null
+++ b/client/src/components/common/FileUploader/assets/drag-and-drop-outline.svg
@@ -0,0 +1,14 @@
+
+
+
diff --git a/client/src/components/common/FileUploader/assets/file-solid.svg b/client/src/components/common/FileUploader/assets/file-solid.svg
new file mode 100644
index 00000000..ef54992e
--- /dev/null
+++ b/client/src/components/common/FileUploader/assets/file-solid.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/client/src/components/common/FileUploader/assets/loading.svg b/client/src/components/common/FileUploader/assets/loading.svg
new file mode 100644
index 00000000..66c6ada8
--- /dev/null
+++ b/client/src/components/common/FileUploader/assets/loading.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/client/src/components/common/FileUploader/fileUploadStore/action-types.ts b/client/src/components/common/FileUploader/fileUploadStore/action-types.ts
new file mode 100644
index 00000000..9d7cca08
--- /dev/null
+++ b/client/src/components/common/FileUploader/fileUploadStore/action-types.ts
@@ -0,0 +1,13 @@
+export const ADD_TEMP_TO_FILES = 'ADD_TEMP_TO_FILES';
+export const DELETE_FROM_ALL_LISTS = 'DELETE_FROM_ALL_LISTS';
+export const UPLOAD_FILES = 'UPLOAD_FILES';
+export const SET_PREVIEW_IMAGES = 'SET_PREVIEW_IMAGES';
+export const UPDATE_TEMP_FILES = 'UPDATE_TEMP_FILES';
+export const HANDLE_FILES_CHANGE = 'HANDLE_FILES_CHANGE';
+export const VALIDATE_FILES = 'VALIDATE_FILES';
+export const HANDLE_MULTIPLE_FILE_CHANGE = 'HANDLE_MULTIPLE_FILE_CHANGE';
+export const HANDLE_SINGLE_FILE_CHANGE = 'HANDLE_SINGLE_FILE_CHANGE';
+export const HANDLE_REPLACE_FILES = 'HANDLE_REPLACE_FILES';
+export const HANDLE_URL_CHANGE = 'HANDLE_URL_CHANGE';
+export const UPDATE_REPLACEMENT_FILES = 'UPDATE_REPLACEMENT_FILES';
+export const UPDATE_REPLACEMENT_URL = 'UPDATE_REPLACEMENT_URL';
diff --git a/client/src/components/common/FileUploader/fileUploadStore/actions.ts b/client/src/components/common/FileUploader/fileUploadStore/actions.ts
new file mode 100644
index 00000000..78fbc76d
--- /dev/null
+++ b/client/src/components/common/FileUploader/fileUploadStore/actions.ts
@@ -0,0 +1,146 @@
+import {ActionTree} from 'vuex';
+import {FILE_MAX_SIZE, FileUploadErrorType, IFileUploadState, IMAGE_URL_INITIAL} from './index';
+import {
+ ADD_FILES,
+ DELETE_FILE,
+ DELETE_TEMP_FILE,
+ SET_FILE_LOADING,
+ SET_FILE_COMPLETED,
+ SET_FILE_DATA_URL,
+ SET_IMAGE_STATUS,
+ SET_FILE_UPLOAD_ERROR,
+ ADD_TEMP_FILES,
+ SET_TEMP_FILES,
+ RESET_UPLOAD_ERRORS,
+ SET_REPLACEMENT_FILES, RESET_TEMP_FILES, SET_IMAGE_URL, RESET_FILES, SET_REPLACEMENT_URL, RESET_UPLOAD_REPLACEMENTS
+} from './mutation-types';
+import {
+ ADD_TEMP_TO_FILES,
+ DELETE_FROM_ALL_LISTS,
+ HANDLE_FILES_CHANGE,
+ HANDLE_MULTIPLE_FILE_CHANGE,
+ HANDLE_REPLACE_FILES,
+ HANDLE_SINGLE_FILE_CHANGE, HANDLE_URL_CHANGE,
+ SET_PREVIEW_IMAGES, UPDATE_REPLACEMENT_FILES, UPDATE_REPLACEMENT_URL,
+ UPDATE_TEMP_FILES,
+ UPLOAD_FILES,
+ VALIDATE_FILES
+} from './action-types';
+import {FileUploaderService} from '../FileUploaderService';
+import {FileUploadStatus, IFileUpload} from '../IFileUploadList';
+import {ALL_UPLOAD_FILES} from './getter-types';
+import {SET_PROJECT_IMAGE} from '../../../../store/modules/projects/mutation-types';
+
+export const actions: ActionTree = {
+ [ADD_TEMP_TO_FILES]({state, commit, dispatch}) {
+ commit(ADD_FILES, state.tempFiles);
+ commit(RESET_TEMP_FILES);
+ dispatch(UPLOAD_FILES);
+ },
+ [UPDATE_TEMP_FILES]({state, dispatch, commit}, payload) {
+ dispatch(VALIDATE_FILES, payload)
+ .then(fileUploadList => {
+ if (fileUploadList.length) {
+ commit(state.isMultiple ? ADD_TEMP_FILES : SET_TEMP_FILES, fileUploadList);
+ }
+ });
+ },
+ [VALIDATE_FILES]({state, commit}, payload) {
+ return payload.filter((item: IFileUpload) => {
+ let isValid = true;
+ if (!FileUploaderService.validateFileSize(item.file, FILE_MAX_SIZE)) {
+ commit(SET_FILE_UPLOAD_ERROR, {errorType: FileUploadErrorType.FILE_SIZE, fileName: item.file.name});
+ isValid = false;
+ }
+ if (state.isImageUpload && !FileUploaderService.validateImageExtension(item.file.name)) {
+ commit(SET_FILE_UPLOAD_ERROR, {errorType: FileUploadErrorType.IMAGE_EXTENSION, fileName: item.file.name});
+ isValid = false;
+ } else if (!FileUploaderService.validateFileExtension(item.file.name)) {
+ commit(SET_FILE_UPLOAD_ERROR, {errorType: FileUploadErrorType.FILE_EXTENSION, fileName: item.file.name});
+ isValid = false;
+ }
+ return isValid;
+ });
+ },
+ [DELETE_FROM_ALL_LISTS]({commit, dispatch}, payload) {
+ commit(DELETE_FILE, payload);
+ commit(DELETE_TEMP_FILE, payload);
+ },
+ [UPLOAD_FILES]({commit, state}) {
+ return Promise.all(state.files.map((file: IFileUpload) => {
+ commit(SET_FILE_LOADING, file);
+ return FileUploaderService.uploadFiles(file)
+ .then((res) => {
+ commit(SET_PROJECT_IMAGE, res.data.filename);
+ return commit(SET_FILE_COMPLETED, file)
+ });
+ }));
+ },
+ [SET_PREVIEW_IMAGES]({state, commit, getters}) {
+ if (state.isImageUpload) {
+ getters[ALL_UPLOAD_FILES].forEach((fileUpload: IFileUpload) => {
+ if (!fileUpload.imageDataUrl) {
+ const reader = new FileReader();
+ reader.addEventListener('load', () => {
+ commit(SET_FILE_DATA_URL, {file: fileUpload, dataUrl: reader.result});
+ }, false);
+ reader.readAsDataURL(fileUpload.file);
+ }
+ });
+ }
+ },
+ [HANDLE_FILES_CHANGE]({state, commit, dispatch}, payload) {
+ commit(RESET_UPLOAD_ERRORS);
+ dispatch(
+ state.isMultiple ? HANDLE_MULTIPLE_FILE_CHANGE : HANDLE_SINGLE_FILE_CHANGE,
+ FileUploaderService.fileListToFileUploadList(payload)
+ );
+ },
+ [HANDLE_MULTIPLE_FILE_CHANGE]({dispatch}, payload) {
+ dispatch(UPDATE_TEMP_FILES, payload)
+ .then(() => dispatch(SET_PREVIEW_IMAGES));
+ },
+ [HANDLE_SINGLE_FILE_CHANGE]({state, dispatch, getters}, payload) {
+ if (getters[ALL_UPLOAD_FILES].length || state.imageUrl.url) {
+ dispatch(UPDATE_REPLACEMENT_FILES, payload);
+ } else {
+ dispatch(HANDLE_MULTIPLE_FILE_CHANGE, payload);
+ }
+ },
+ [HANDLE_REPLACE_FILES]({state, commit, dispatch}) {
+ commit(RESET_FILES);
+ if (state.replacementFiles.length) {
+ commit(SET_TEMP_FILES, state.replacementFiles);
+ commit(SET_IMAGE_URL, IMAGE_URL_INITIAL);
+ dispatch(SET_PREVIEW_IMAGES);
+ } else if (state.replacementUrl) {
+ commit(RESET_TEMP_FILES);
+ commit(SET_IMAGE_URL, FileUploaderService.urlToImageUrl(state.replacementUrl));
+ }
+ commit(RESET_UPLOAD_REPLACEMENTS);
+ },
+ [UPDATE_REPLACEMENT_FILES]({commit, dispatch}, payload) {
+ dispatch(VALIDATE_FILES, payload)
+ .then(fileUploadList => {
+ commit(SET_REPLACEMENT_FILES, fileUploadList);
+ });
+ },
+ [UPDATE_REPLACEMENT_URL]({commit}, payload) {
+ const urlFileName = FileUploaderService.getFileNameFromUrl(payload);
+ if (!FileUploaderService.validateImageExtension(urlFileName)) {
+ commit(SET_FILE_UPLOAD_ERROR, {errorType: FileUploadErrorType.IMAGE_EXTENSION, fileName: urlFileName});
+ } else {
+ commit(SET_REPLACEMENT_URL, payload);
+ }
+ },
+ [HANDLE_URL_CHANGE]({state, commit, getters, dispatch}, payload) {
+ commit(RESET_UPLOAD_ERRORS);
+ dispatch(UPDATE_REPLACEMENT_URL, payload)
+ .then(() => {
+ if (!getters[ALL_UPLOAD_FILES].length && !state.imageUrl.url) {
+ commit(SET_IMAGE_URL, FileUploaderService.urlToImageUrl(state.replacementUrl));
+ commit(SET_REPLACEMENT_URL, '');
+ }
+ });
+ }
+};
diff --git a/client/src/components/common/FileUploader/fileUploadStore/getter-types.ts b/client/src/components/common/FileUploader/fileUploadStore/getter-types.ts
new file mode 100644
index 00000000..f8190858
--- /dev/null
+++ b/client/src/components/common/FileUploader/fileUploadStore/getter-types.ts
@@ -0,0 +1,14 @@
+export const ALL_UPLOAD_FILES = 'ALL_UPLOAD_FILES';
+export const UPLOAD_IMAGE_URL = 'UPLOAD_IMAGE_URL';
+export const IS_IMAGE_URL = 'IS_IMAGE_URL';
+export const IS_IMAGE_UPLOAD = 'IS_IMAGE_UPLOAD';
+export const IS_UPLOAD_MULTIPLE = 'IS_UPLOAD_MULTIPLE';
+export const IS_UPLOAD_MODAL_OPEN = 'IS_UPLOAD_MODAL_OPEN';
+export const FILE_UPLOAD_ERRORS = 'FILE_UPLOAD_ERRORS';
+export const FILE_EXTENSION_ERROR = 'FILE_EXTENSION_ERROR';
+export const FILE_SIZE_ERROR = 'FILE_SIZE_ERROR';
+export const IMAGE_EXTENSION_ERROR = 'IMAGE_EXTENSION_ERROR';
+export const HAS_UPLOAD_ERRORS = 'HAS_UPLOAD_ERRORS';
+export const REPLACEMENT_FILES = 'REPLACEMENT_FILES';
+export const REPLACEMENT_URL = 'REPLACEMENT_URL';
+export const HAS_UPLOAD_REPLACEMENTS = 'HAS_UPLOAD_REPLACEMENTS';
diff --git a/client/src/components/common/FileUploader/fileUploadStore/index.ts b/client/src/components/common/FileUploader/fileUploadStore/index.ts
new file mode 100644
index 00000000..d831d20c
--- /dev/null
+++ b/client/src/components/common/FileUploader/fileUploadStore/index.ts
@@ -0,0 +1,104 @@
+import {Module} from 'vuex';
+import {FileUploadStatus, IFileUpload, IImageUrl} from '../IFileUploadList';
+import {mutations} from './mutations';
+import {actions} from './actions';
+import {
+ ALL_UPLOAD_FILES,
+ UPLOAD_IMAGE_URL,
+ IS_IMAGE_UPLOAD,
+ IS_IMAGE_URL,
+ IS_UPLOAD_MULTIPLE,
+ FILE_UPLOAD_ERRORS,
+ HAS_UPLOAD_ERRORS,
+ FILE_EXTENSION_ERROR,
+ FILE_SIZE_ERROR,
+ IMAGE_EXTENSION_ERROR,
+ REPLACEMENT_URL,
+ REPLACEMENT_FILES,
+ HAS_UPLOAD_REPLACEMENTS, IS_UPLOAD_MODAL_OPEN
+} from './getter-types';
+
+export enum FileUploadErrorType {
+ FILE_SIZE = 'FILE_SIZE',
+ FILE_EXTENSION = 'FILE_EXTENSION',
+ IMAGE_EXTENSION = 'IMAGE_EXTENSION',
+}
+
+export interface IFileUploadError {
+ hasError: boolean;
+ fileNames: string[];
+}
+
+export type IFileUploadErrors = {
+ [k in FileUploadErrorType]: IFileUploadError
+};
+
+export interface IFileUploadState {
+ isImageUpload: boolean,
+ isMultiple: boolean,
+ isUploadModalOpen: boolean,
+ files: IFileUpload[],
+ tempFiles: IFileUpload[],
+ isImageUrl: boolean,
+ imageUrl: IImageUrl,
+ replacementFiles: IFileUpload[],
+ replacementUrl: string,
+ errors: IFileUploadErrors
+}
+
+export const IMAGE_URL_INITIAL = {
+ url: '',
+ name: '',
+ loadingStatus: FileUploadStatus.NULL
+};
+
+export const FILE_MAX_SIZE = 10485760; // 10Mb
+
+const fileUploadState: Module = {
+ state: {
+ files: [],
+ tempFiles: [],
+ isImageUpload: false,
+ isMultiple: false,
+ isUploadModalOpen: false,
+ isImageUrl: false,
+ imageUrl: IMAGE_URL_INITIAL,
+ replacementFiles: [],
+ replacementUrl: '',
+ errors: {
+ [FileUploadErrorType.FILE_SIZE]: {
+ hasError: false,
+ fileNames: []
+ },
+ [FileUploadErrorType.FILE_EXTENSION]: {
+ hasError: false,
+ fileNames: []
+ },
+ [FileUploadErrorType.IMAGE_EXTENSION]: {
+ hasError: false,
+ fileNames: []
+ }
+ }
+ },
+ mutations,
+ actions,
+ getters: {
+ [ALL_UPLOAD_FILES]: state => [...state.files, ...state.tempFiles],
+ [UPLOAD_IMAGE_URL]: state => state.imageUrl,
+ [IS_IMAGE_URL]: state => state.isImageUrl,
+ [IS_IMAGE_UPLOAD]: state => state.isImageUpload,
+ [IS_UPLOAD_MULTIPLE]: state => state.isMultiple,
+ [IS_UPLOAD_MODAL_OPEN]: state => state.isUploadModalOpen,
+ [FILE_UPLOAD_ERRORS]: state => state.errors,
+ [FILE_EXTENSION_ERROR]: state => state.errors[FileUploadErrorType.FILE_EXTENSION],
+ [FILE_SIZE_ERROR]: state => state.errors[FileUploadErrorType.FILE_SIZE],
+ [IMAGE_EXTENSION_ERROR]: state => state.errors[FileUploadErrorType.IMAGE_EXTENSION],
+ [HAS_UPLOAD_ERRORS]: state => Object.keys(state.errors)
+ .some(key => state.errors[key as FileUploadErrorType].hasError),
+ [REPLACEMENT_FILES]: state => state.replacementFiles,
+ [REPLACEMENT_URL]: state => state.replacementUrl,
+ [HAS_UPLOAD_REPLACEMENTS]: state => state.replacementFiles.length || state.replacementUrl
+ }
+};
+
+export default fileUploadState;
diff --git a/client/src/components/common/FileUploader/fileUploadStore/mutation-types.ts b/client/src/components/common/FileUploader/fileUploadStore/mutation-types.ts
new file mode 100644
index 00000000..0c4a7055
--- /dev/null
+++ b/client/src/components/common/FileUploader/fileUploadStore/mutation-types.ts
@@ -0,0 +1,22 @@
+export const ADD_FILES = 'ADD_FILES';
+export const ADD_TEMP_FILES = 'ADD_TEMP_FILES';
+export const DELETE_FILE = 'DELETE_FILE';
+export const DELETE_TEMP_FILE = 'DELETE_TEMP_FILE';
+export const RESET_TEMP_FILES = 'RESET_TEMP_FILES';
+export const RESET_FILES = 'RESET_FILES';
+export const SET_FILE_LOADING = 'SET_FILE_STATUS';
+export const SET_FILE_COMPLETED = 'SET_FILE_COMPLETED';
+export const SET_FILE_DATA_URL = 'SET_FILE_DATA_URL';
+export const SET_FILES = 'SET_FILES';
+export const SET_TEMP_FILES = 'SET_TEMP_FILES';
+export const SET_IS_IMAGE_URL = 'SET_IS_IMAGE_URL';
+export const SET_IMAGE_URL = 'SET_IMAGE_URL';
+export const SET_IS_IMAGE_UPLOAD = 'SET_IS_IMAGE_UPLOAD';
+export const SET_IS_UPLOAD_MULTIPLE = 'SET_IS_UPLOAD_MULTIPLE';
+export const SET_IS_UPLOAD_MODAL_OPEN = 'SET_IS_UPLOAD_MODAL_OPEN';
+export const SET_IMAGE_STATUS = 'SET_IMAGE_STATUS';
+export const RESET_UPLOAD_ERRORS = 'RESET_UPLOAD_ERRORS';
+export const SET_FILE_UPLOAD_ERROR = 'SET_FILE_UPLOAD_ERROR';
+export const SET_REPLACEMENT_FILES = 'SET_REPLACEMENT_FILES';
+export const SET_REPLACEMENT_URL = 'SET_REPLACEMENT_URL';
+export const RESET_UPLOAD_REPLACEMENTS = 'RESET_UPLOAD_REPLACEMENTS';
diff --git a/client/src/components/common/FileUploader/fileUploadStore/mutations.ts b/client/src/components/common/FileUploader/fileUploadStore/mutations.ts
new file mode 100644
index 00000000..7a2d95d4
--- /dev/null
+++ b/client/src/components/common/FileUploader/fileUploadStore/mutations.ts
@@ -0,0 +1,121 @@
+import {MutationTree} from 'vuex';
+import {FileUploadErrorType, IFileUploadState} from './index';
+import {
+ ADD_FILES,
+ SET_FILE_LOADING,
+ SET_FILE_COMPLETED,
+ ADD_TEMP_FILES,
+ DELETE_FILE,
+ DELETE_TEMP_FILE,
+ RESET_FILES,
+ RESET_TEMP_FILES,
+ SET_FILE_DATA_URL,
+ SET_FILES,
+ SET_TEMP_FILES,
+ SET_IMAGE_URL,
+ SET_IS_IMAGE_URL,
+ SET_IMAGE_STATUS,
+ RESET_UPLOAD_ERRORS,
+ SET_FILE_UPLOAD_ERROR,
+ SET_IS_IMAGE_UPLOAD,
+ SET_IS_UPLOAD_MULTIPLE,
+ SET_REPLACEMENT_FILES,
+ SET_REPLACEMENT_URL, RESET_UPLOAD_REPLACEMENTS, SET_IS_UPLOAD_MODAL_OPEN
+} from './mutation-types';
+import {FileUploadStatus, IFileUpload} from '../IFileUploadList';
+import {FileUploaderService} from '../FileUploaderService';
+
+export const mutations: MutationTree = {
+ [ADD_FILES](state, filesUploadList: IFileUpload[]) {
+ filesUploadList.forEach((fileUpload: IFileUpload) => {
+ if (!FileUploaderService.isFileInUploadList(fileUpload.file, state.files)) {
+ state.files.push(fileUpload);
+ }
+ });
+ },
+ [ADD_TEMP_FILES](state, filesUploadList: IFileUpload[]) {
+ filesUploadList.forEach((fileUpload: IFileUpload) => {
+ if (!FileUploaderService.isFileInUploadList(fileUpload.file, state.tempFiles) &&
+ !FileUploaderService.isFileInUploadList(fileUpload.file, state.files)) {
+ state.tempFiles.push(fileUpload);
+ }
+ });
+ },
+ [SET_FILES](state, payload) {
+ state.files = payload;
+ },
+ [SET_TEMP_FILES](state, payload) {
+ state.tempFiles = payload;
+ },
+ [RESET_FILES](state) {
+ state.files = [];
+ },
+ [RESET_TEMP_FILES](state) {
+ state.tempFiles = [];
+ },
+ [DELETE_FILE](state, payload) {
+ const index = state.files.indexOf(payload);
+ if (index > -1) {
+ state.files.splice(index, 1);
+ }
+ },
+ [DELETE_TEMP_FILE](state, payload) {
+ const index = state.tempFiles.indexOf(payload);
+ if (index > -1) {
+ state.tempFiles.splice(index, 1);
+ }
+ },
+ [SET_FILE_LOADING](state, payload) {
+ state.files[state.files.indexOf(payload)].loadingStatus = FileUploadStatus.LOADING;
+ },
+ [SET_FILE_COMPLETED](state, payload) {
+ state.files[state.files.indexOf(payload)].loadingStatus = FileUploadStatus.COMPLETED;
+ },
+ [SET_FILE_DATA_URL](state, {file, dataUrl}) {
+ if (state.files.indexOf(file) > -1) {
+ state.files[state.files.indexOf(file)].imageDataUrl = dataUrl;
+ }
+ if (state.tempFiles.indexOf(file) > -1) {
+ state.tempFiles[state.tempFiles.indexOf(file)].imageDataUrl = dataUrl;
+ }
+ },
+ [SET_IS_IMAGE_URL](state, payload) {
+ state.isImageUrl = payload;
+ },
+ [SET_IS_IMAGE_UPLOAD](state, payload) {
+ state.isImageUpload = payload;
+ },
+ [SET_IS_UPLOAD_MULTIPLE](state, payload) {
+ state.isMultiple = payload;
+ },
+ [SET_IMAGE_URL](state, payload) {
+ state.imageUrl = payload;
+ },
+ [SET_IMAGE_STATUS](state, payload) {
+ state.imageUrl.loadingStatus = payload;
+ },
+ [RESET_UPLOAD_ERRORS](state) {
+ (Object.keys(state.errors) as FileUploadErrorType[])
+ .forEach(key => {
+ state.errors[key].hasError = false;
+ state.errors[key].fileNames = [];
+ });
+ },
+ [SET_FILE_UPLOAD_ERROR](state, {errorType, fileName}: {errorType: FileUploadErrorType; fileName: string;}) {
+ state.errors[errorType].hasError = true;
+ state.errors[errorType].fileNames.push(fileName);
+ },
+ [SET_REPLACEMENT_FILES](state, payload) {
+ state.replacementFiles = payload;
+ },
+ [SET_REPLACEMENT_URL](state, payload) {
+ state.replacementUrl = payload;
+ },
+ [RESET_UPLOAD_REPLACEMENTS](state) {
+ state.replacementFiles = [];
+ state.replacementUrl = '';
+ },
+ [SET_IS_UPLOAD_MODAL_OPEN](state, payload: boolean) {
+ state.isUploadModalOpen = payload;
+ }
+};
diff --git a/client/src/components/common/RadioButton/RadioButton.vue b/client/src/components/common/RadioButton/RadioButton.vue
new file mode 100644
index 00000000..6f96bfad
--- /dev/null
+++ b/client/src/components/common/RadioButton/RadioButton.vue
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/common/Ribbon/Ribbon.vue b/client/src/components/common/Ribbon/Ribbon.vue
new file mode 100644
index 00000000..1217c2c6
--- /dev/null
+++ b/client/src/components/common/Ribbon/Ribbon.vue
@@ -0,0 +1,48 @@
+
+
+ {{ value }}
+
+
+
+
+
+
diff --git a/client/src/components/common/Stepper/Stepper.vue b/client/src/components/common/Stepper/Stepper.vue
new file mode 100644
index 00000000..9c612dc3
--- /dev/null
+++ b/client/src/components/common/Stepper/Stepper.vue
@@ -0,0 +1,64 @@
+
+
+
+ {{ step }}
+
+
+ {{ name }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/common/assets/arrowDown.svg b/client/src/components/common/assets/arrowDown.svg
new file mode 100644
index 00000000..bb2288f3
--- /dev/null
+++ b/client/src/components/common/assets/arrowDown.svg
@@ -0,0 +1,20 @@
+
+
\ No newline at end of file
diff --git a/client/src/components/common/assets/download.svg b/client/src/components/common/assets/download.svg
new file mode 100644
index 00000000..16897e6c
--- /dev/null
+++ b/client/src/components/common/assets/download.svg
@@ -0,0 +1,17 @@
+
+
\ No newline at end of file
diff --git a/client/src/components/common/assets/sort.svg b/client/src/components/common/assets/sort.svg
new file mode 100644
index 00000000..0ebf326f
--- /dev/null
+++ b/client/src/components/common/assets/sort.svg
@@ -0,0 +1,17 @@
+
+
\ No newline at end of file
diff --git a/client/src/components/common/index.ts b/client/src/components/common/index.ts
new file mode 100644
index 00000000..6dbfa9da
--- /dev/null
+++ b/client/src/components/common/index.ts
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import Checkbox from './Checkbox/Checkbox.vue';
+import CommonModal from './CommonModal/CommonModal.vue';
+import ConfirmModal from './ConfirmModal/ConfirmModal.vue';
+import FileUploader from './FileUploader/FileUploader.vue';
+import RadioButton from './RadioButton/RadioButton.vue';
+import Autocomplete from './Autocomplete/Autocomplete.vue';
+import Ribbon from './Ribbon/Ribbon.vue';
+import Accordion from './Accordion/Accordion.vue';
+import ErrorMessage from './ErrorMessage/ErrorMessage.vue';
+
+const components: { [key: string]: {} } = {
+ Checkbox,
+ ConfirmModal,
+ CommonModal,
+ FileUploader,
+ RadioButton,
+ Autocomplete,
+ Ribbon,
+ Accordion,
+ ErrorMessage
+};
+
+Object.keys(components)
+ .forEach(componentName => Vue.component(componentName, components[componentName]));
diff --git a/client/src/components/employees/.DS_Store b/client/src/components/employees/.DS_Store
new file mode 100644
index 00000000..f8aaa68e
Binary files /dev/null and b/client/src/components/employees/.DS_Store differ
diff --git a/client/src/components/employees/EmployeeItem/EmployeeItem.vue b/client/src/components/employees/EmployeeItem/EmployeeItem.vue
new file mode 100644
index 00000000..10b18bcf
--- /dev/null
+++ b/client/src/components/employees/EmployeeItem/EmployeeItem.vue
@@ -0,0 +1,242 @@
+
+
+

+
+
+
+
+ {{ schedule.employee.firstname }} {{ schedule.employee.lastname }}
+
+
+
+
+
+
+
+
+
+ Participation(%):
+
+
+
+
+
+
+
+
+
+
+
+
Start date is required
+
End date must be higher than a start date
+
+
+
Participation is required
+
Participation must be decimal
+
Participation must be between 0 and 100
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/employees/ScheduleItem/ScheduleItem.vue b/client/src/components/employees/ScheduleItem/ScheduleItem.vue
new file mode 100644
index 00000000..8c9f6df6
--- /dev/null
+++ b/client/src/components/employees/ScheduleItem/ScheduleItem.vue
@@ -0,0 +1,52 @@
+
+
+

+
+
+ {{ schedule.employee.firstname }} {{ schedule.employee.lastname }}
+
+ {{ schedule.role.name }}
+
+
+
{{ schedule.participation }}%
+
+
+
+
+
+
+
diff --git a/client/src/components/employees/assets/person.svg b/client/src/components/employees/assets/person.svg
new file mode 100644
index 00000000..60b29490
--- /dev/null
+++ b/client/src/components/employees/assets/person.svg
@@ -0,0 +1,23 @@
+
+
\ No newline at end of file
diff --git a/client/src/components/projects/AppliedFilters/AppliedFilters.vue b/client/src/components/projects/AppliedFilters/AppliedFilters.vue
new file mode 100644
index 00000000..b7ae618d
--- /dev/null
+++ b/client/src/components/projects/AppliedFilters/AppliedFilters.vue
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Sorted by:
+
+ {{ mapName(sort) }}
+
+
+
+
+
+
+ Completion:
+
+ {{ mapName(completion) }}
+
+
+
+
+
+
+ Reset All Filters
+
+
+
+
+
+
+ Search:
+
+ {{ search }}
+
+
+
+
+
+
+ {{ filterKey }}:
+
+ {{ filterValue(filterKey, filter) }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/projects/AppliedFilters/assets/export_outline.svg b/client/src/components/projects/AppliedFilters/assets/export_outline.svg
new file mode 100644
index 00000000..eec5cb3c
--- /dev/null
+++ b/client/src/components/projects/AppliedFilters/assets/export_outline.svg
@@ -0,0 +1,12 @@
+
diff --git a/client/src/components/projects/AppliedFilters/assets/plus_outline.svg b/client/src/components/projects/AppliedFilters/assets/plus_outline.svg
new file mode 100644
index 00000000..04eebc2b
--- /dev/null
+++ b/client/src/components/projects/AppliedFilters/assets/plus_outline.svg
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/client/src/components/projects/AppliedFilters/assets/presentation-file_outline.svg b/client/src/components/projects/AppliedFilters/assets/presentation-file_outline.svg
new file mode 100644
index 00000000..c431d81e
--- /dev/null
+++ b/client/src/components/projects/AppliedFilters/assets/presentation-file_outline.svg
@@ -0,0 +1,12 @@
+
diff --git a/client/src/components/projects/Project/Project.vue b/client/src/components/projects/Project/Project.vue
new file mode 100644
index 00000000..2be2dc13
--- /dev/null
+++ b/client/src/components/projects/Project/Project.vue
@@ -0,0 +1,240 @@
+
+
+
+
+ Delete project?
+
+
+
+ Would you like to delete project
+ {{ project.name }}?
+
+
+ Delete project
+
+
+
+ {{ project.updatedAt | date }} {{ project.program.line.name }}
+
+
+
+
+
+
+
+
+
+
+ {{ project.description }}
+
+
+
+
![]()
+
+
+
+
+
+
+ Team
+ {{ project.schedules.length }} FTE
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/projects/Project/ProjectFooter.vue b/client/src/components/projects/Project/ProjectFooter.vue
new file mode 100644
index 00000000..3293d0a8
--- /dev/null
+++ b/client/src/components/projects/Project/ProjectFooter.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
diff --git a/client/src/components/projects/Project/assets/copy.svg b/client/src/components/projects/Project/assets/copy.svg
new file mode 100644
index 00000000..d852b486
--- /dev/null
+++ b/client/src/components/projects/Project/assets/copy.svg
@@ -0,0 +1,20 @@
+
+
\ No newline at end of file
diff --git a/client/src/components/projects/Project/assets/edit.svg b/client/src/components/projects/Project/assets/edit.svg
new file mode 100644
index 00000000..eef71454
--- /dev/null
+++ b/client/src/components/projects/Project/assets/edit.svg
@@ -0,0 +1,18 @@
+
+
\ No newline at end of file
diff --git a/client/src/components/projects/Project/assets/history.svg b/client/src/components/projects/Project/assets/history.svg
new file mode 100644
index 00000000..4de6ed6c
--- /dev/null
+++ b/client/src/components/projects/Project/assets/history.svg
@@ -0,0 +1,20 @@
+
+
\ No newline at end of file
diff --git a/client/src/components/projects/Project/assets/presentation.svg b/client/src/components/projects/Project/assets/presentation.svg
new file mode 100644
index 00000000..c8cec2c8
--- /dev/null
+++ b/client/src/components/projects/Project/assets/presentation.svg
@@ -0,0 +1,12 @@
+
diff --git a/client/src/components/projects/Project/assets/trash.svg b/client/src/components/projects/Project/assets/trash.svg
new file mode 100644
index 00000000..c2a10634
--- /dev/null
+++ b/client/src/components/projects/Project/assets/trash.svg
@@ -0,0 +1,20 @@
+
+
\ No newline at end of file
diff --git a/client/src/components/projects/ProjectCard/ProjectCard.test.ts b/client/src/components/projects/ProjectCard/ProjectCard.test.ts
new file mode 100644
index 00000000..b92367fa
--- /dev/null
+++ b/client/src/components/projects/ProjectCard/ProjectCard.test.ts
@@ -0,0 +1,34 @@
+import ProjectCard from './ProjectCard.vue';
+import {IProject} from '../../../shared/interfaces/IProject';
+import {ISchedule} from '../../../shared/interfaces/ISchedule';
+import {mount} from '@vue/test-utils';
+import {IProgram} from '../../../shared/interfaces/IProgram';
+import {IType} from '../../../shared/interfaces/IType';
+import {IDomain} from '../../../shared/interfaces/IDomain';
+import {ICustomer} from '../../../shared/interfaces/ICustomer';
+import {ILine} from '../../../shared/interfaces/ILine';
+import {ITechnology} from '../../../shared/interfaces/ITechnology';
+import {TestMocks} from '../../../shared/classes/TestMocks';
+
+describe('ProjectChange Card', () => {
+
+ it('should render correct contents', () => {
+
+
+ const project = TestMocks.TestProject();
+
+ const wrapper = mount(ProjectCard, {
+ propsData: {
+ project: project
+ },
+ filters: {
+ date(value) {
+ return ""
+ }
+ }
+ });
+
+ expect(wrapper.props().project).toMatchObject(project);
+
+ });
+});
diff --git a/client/src/components/projects/ProjectCard/ProjectCard.vue b/client/src/components/projects/ProjectCard/ProjectCard.vue
new file mode 100644
index 00000000..cd2fccf7
--- /dev/null
+++ b/client/src/components/projects/ProjectCard/ProjectCard.vue
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+ {{ project.name }}
+
+
+ {{ project.domain.name }}
+
+
+ {{ project.description }}
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/projects/ProjectChange/ProjectChange.vue b/client/src/components/projects/ProjectChange/ProjectChange.vue
new file mode 100644
index 00000000..6cd70c46
--- /dev/null
+++ b/client/src/components/projects/ProjectChange/ProjectChange.vue
@@ -0,0 +1,605 @@
+
+
+ {{ id ? 'Edit' : 'Create' }} project
+
+ Please {{ id ? 'edit' : 'create' }} project information here
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/projects/ProjectFilter/CompletedProjects.vue b/client/src/components/projects/ProjectFilter/CompletedProjects.vue
new file mode 100644
index 00000000..3b494265
--- /dev/null
+++ b/client/src/components/projects/ProjectFilter/CompletedProjects.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/projects/ProjectFilter/ProjectFilter.test.ts b/client/src/components/projects/ProjectFilter/ProjectFilter.test.ts
new file mode 100644
index 00000000..ce00ec1f
--- /dev/null
+++ b/client/src/components/projects/ProjectFilter/ProjectFilter.test.ts
@@ -0,0 +1,102 @@
+import ProjectFilter from './ProjectFilter.vue';
+import {mount} from '@vue/test-utils';
+import store from '../../../store/index';
+import {TestMocks} from '../../../shared/classes/TestMocks';
+import {SET_FILTER, SET_PROJECTS} from '../../../store/modules/projects/mutation-types';
+import {PROJECTS} from '../../../store/modules/projects/getter-types';
+import router from '../../../router';
+
+describe('ProjectChange Filter', () => {
+
+ const wrapper = mount(ProjectFilter, {
+ store: store,
+ router
+ });
+
+ const line = TestMocks.TestLine('Automotive');
+ const domain = TestMocks.TestDomain('Health');
+ const program = TestMocks.TestProgram('Sales and Autosales');
+ const customer = TestMocks.TestCustomer('1','BMW AG');
+ const customer2 = TestMocks.TestCustomer('2','BMW AG');
+ const technology = TestMocks.TestTechnology('1','React');
+ const technology2 = TestMocks.TestTechnology('2','React');
+ const project = TestMocks.TestProject();
+
+ it('correctly filter by all properties', () => {
+
+ store.commit(SET_PROJECTS, [project]);
+ store.commit(SET_FILTER, { key: 'line', value: line.id});
+ store.commit(SET_FILTER, { key: 'program', value: program.id});
+ store.commit(SET_FILTER, { key: 'domain', value: domain.id})
+ store.commit(SET_FILTER, { key: 'customers', value: customer.id});
+ store.commit(SET_FILTER, { key: 'customers', value: customer2.id});
+ store.commit(SET_FILTER, { key: 'technologies', value: technology.id});
+ store.commit(SET_FILTER, { key: 'technologies', value: technology2.id});
+
+ const filtered = store.getters[PROJECTS];
+
+ expect(filtered.length).toBe(1);
+
+ });
+
+ it('should correctly filter by line', () => {
+
+ store.commit(SET_PROJECTS, [project]);
+ store.commit(SET_FILTER, { key: 'line', value: line.id});
+
+
+ const filtered = store.getters[PROJECTS];
+
+ expect(filtered[0].program.lineId).toBe(line.id);
+
+ });
+
+ it('should correctly filter by program', () => {
+
+ store.commit(SET_PROJECTS, [project]);
+ store.commit(SET_FILTER, { key: 'program', value: program.id});
+
+
+ const filtered = store.getters[PROJECTS];
+
+ expect(filtered[0].program.id).toBe(program.id);
+
+ });
+
+ it('should correctly filter by domain', () => {
+
+ store.commit(SET_PROJECTS, [project]);
+ store.commit(SET_FILTER, { key: 'domain', value: domain.id});
+
+
+ const filtered = store.getters[PROJECTS];
+
+ expect(filtered[0].domain.id).toBe(domain.id);
+
+ });
+
+ it('should correctly filter by customer', () => {
+
+ store.commit(SET_PROJECTS, [project]);
+ store.commit(SET_FILTER, { key: 'customers', value: customer.id});
+
+
+ const filtered = store.getters[PROJECTS];
+
+ expect(filtered.length).toBe(1);
+
+ });
+
+ it('should correctly filter by technology', () => {
+ const technology3 = TestMocks.TestTechnology('3','React');
+
+ store.commit(SET_PROJECTS, [project]);
+ store.commit(SET_FILTER, { key: 'technologies', value: technology.id});
+ store.commit(SET_FILTER, { key: 'technologies', value: technology3.id});
+
+ const filtered = store.getters[PROJECTS];
+
+ expect(filtered.length).toBe(0);
+
+ });
+});
diff --git a/client/src/components/projects/ProjectFilter/ProjectFilter.vue b/client/src/components/projects/ProjectFilter/ProjectFilter.vue
new file mode 100644
index 00000000..cb0aa3b5
--- /dev/null
+++ b/client/src/components/projects/ProjectFilter/ProjectFilter.vue
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+ {{ item.value }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/projects/ProjectFilter/Sorting.vue b/client/src/components/projects/ProjectFilter/Sorting.vue
new file mode 100644
index 00000000..576a083b
--- /dev/null
+++ b/client/src/components/projects/ProjectFilter/Sorting.vue
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Reverse
+
+
+
+
+
+
+
diff --git a/client/src/components/projects/Projects.vue b/client/src/components/projects/Projects.vue
new file mode 100644
index 00000000..651f39e5
--- /dev/null
+++ b/client/src/components/projects/Projects.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
No projects found
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/root/.DS_Store b/client/src/components/root/.DS_Store
new file mode 100644
index 00000000..d76a7d50
Binary files /dev/null and b/client/src/components/root/.DS_Store differ
diff --git a/client/src/components/root/Footer/TelekomFooter.vue b/client/src/components/root/Footer/TelekomFooter.vue
new file mode 100644
index 00000000..15b02cab
--- /dev/null
+++ b/client/src/components/root/Footer/TelekomFooter.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
diff --git a/client/src/components/root/Header/Header.vue b/client/src/components/root/Header/Header.vue
new file mode 100644
index 00000000..e57bdce9
--- /dev/null
+++ b/client/src/components/root/Header/Header.vue
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/root/Header/Search.vue b/client/src/components/root/Header/Search.vue
new file mode 100644
index 00000000..14ff1ac5
--- /dev/null
+++ b/client/src/components/root/Header/Search.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
diff --git a/client/src/components/root/assets/close.svg b/client/src/components/root/assets/close.svg
new file mode 100644
index 00000000..cefc2345
--- /dev/null
+++ b/client/src/components/root/assets/close.svg
@@ -0,0 +1,12 @@
+
diff --git a/client/src/components/root/assets/deutsche-telekom-logo.svg b/client/src/components/root/assets/deutsche-telekom-logo.svg
new file mode 100644
index 00000000..03d5bf05
--- /dev/null
+++ b/client/src/components/root/assets/deutsche-telekom-logo.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/client/src/components/root/assets/logo-tportfolio.svg b/client/src/components/root/assets/logo-tportfolio.svg
new file mode 100644
index 00000000..9caa770b
--- /dev/null
+++ b/client/src/components/root/assets/logo-tportfolio.svg
@@ -0,0 +1,35 @@
+
+
diff --git a/client/src/components/root/assets/search.svg b/client/src/components/root/assets/search.svg
new file mode 100644
index 00000000..8bbd9ae9
--- /dev/null
+++ b/client/src/components/root/assets/search.svg
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/client/src/components/root/assets/team.svg b/client/src/components/root/assets/team.svg
new file mode 100644
index 00000000..1c81b134
--- /dev/null
+++ b/client/src/components/root/assets/team.svg
@@ -0,0 +1,25 @@
+
+
\ No newline at end of file
diff --git a/client/src/components/root/assets/user.svg b/client/src/components/root/assets/user.svg
new file mode 100644
index 00000000..f8d6e054
--- /dev/null
+++ b/client/src/components/root/assets/user.svg
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/client/src/components/technologies/TechnologyPanel/TechnologyPanel.vue b/client/src/components/technologies/TechnologyPanel/TechnologyPanel.vue
new file mode 100644
index 00000000..797548bc
--- /dev/null
+++ b/client/src/components/technologies/TechnologyPanel/TechnologyPanel.vue
@@ -0,0 +1,50 @@
+
+
+
+ {{ domain | capitalize }}
+
+
+
+
+
+
+
+
diff --git a/client/src/components/technologies/TechnologyPicker/TechnologyPicker.test.ts b/client/src/components/technologies/TechnologyPicker/TechnologyPicker.test.ts
new file mode 100644
index 00000000..b172c4a8
--- /dev/null
+++ b/client/src/components/technologies/TechnologyPicker/TechnologyPicker.test.ts
@@ -0,0 +1,23 @@
+import {mount} from '@vue/test-utils';
+import store from '../../../store/index';
+import TechnologyPicker from './TechnologyPicker.vue';
+import {FETCH_TECHNOLOGIES} from '../../../store/modules/technologies/action-types';
+
+describe('TechnologyPicker', () => {
+
+ const wrapper = mount(TechnologyPicker, {
+ store: store,
+ filters: {
+ date(value) {
+ return ""
+ }
+ }
+ });
+
+ it('should render correct contents', () => {
+ const result = store.dispatch(FETCH_TECHNOLOGIES);
+ result.then((response) => {
+ expect(wrapper.vm.technologies.length).toBe(1);
+ })
+ });
+});
diff --git a/client/src/components/technologies/TechnologyPicker/TechnologyPicker.vue b/client/src/components/technologies/TechnologyPicker/TechnologyPicker.vue
new file mode 100644
index 00000000..b0666e7f
--- /dev/null
+++ b/client/src/components/technologies/TechnologyPicker/TechnologyPicker.vue
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/technologies/assets/search.svg b/client/src/components/technologies/assets/search.svg
new file mode 100644
index 00000000..f0f96bbf
--- /dev/null
+++ b/client/src/components/technologies/assets/search.svg
@@ -0,0 +1,20 @@
+
+
\ No newline at end of file
diff --git a/client/src/i18n.ts b/client/src/i18n.ts
new file mode 100644
index 00000000..8de4de79
--- /dev/null
+++ b/client/src/i18n.ts
@@ -0,0 +1,9 @@
+import Vue from 'vue';
+import VueI18n from 'vue-i18n';
+
+Vue.use(VueI18n);
+
+export const i18n = new VueI18n({
+ locale: 'en',
+ fallbackLocale: 'en'
+});
diff --git a/client/src/main.ts b/client/src/main.ts
new file mode 100644
index 00000000..fbe8b947
--- /dev/null
+++ b/client/src/main.ts
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import Buefy from 'buefy';
+import Vuelidate from 'vuelidate';
+import App from './App.vue';
+import router from './router/index';
+import store from './store/index';
+import {i18n} from './i18n';
+import './shared/http/interceptors/loading';
+
+// Global styles
+import './styles/styles.scss';
+
+import './components/common';
+import date from './shared/filters/Date';
+import capitalize from './shared/filters/Capitalize';
+
+
+Vue.filter('date', date);
+Vue.filter('capitalize', capitalize);
+
+Vue.use(Buefy);
+Vue.use(Vuelidate);
+
+Vue.config.productionTip = false;
+
+new Vue({
+ el: '#app',
+ store,
+ router,
+ i18n,
+ components: {App},
+ template: ''
+});
diff --git a/client/src/router/index.ts b/client/src/router/index.ts
new file mode 100644
index 00000000..94445b97
--- /dev/null
+++ b/client/src/router/index.ts
@@ -0,0 +1,45 @@
+import Vue from 'vue';
+import Router from 'vue-router';
+import Projects from '../components/projects/Projects.vue';
+import Project from '../components/projects/Project/Project.vue';
+import ProjectChange from '../components/projects/ProjectChange/ProjectChange.vue';
+import store from '../store';
+import {SYNC_PARAMS} from '../store/modules/projects/action-types';
+
+Vue.use(Router);
+
+export enum Routes {
+ Projects = 'Projects',
+ Project = 'Project'
+}
+
+const router = new Router({
+ routes: [
+ {
+ path: '/',
+ component: Projects,
+ name: Routes.Projects,
+ beforeEnter(to, _from, next) {
+ store.dispatch(SYNC_PARAMS, to.query);
+ next();
+ }
+ },
+ {
+ path: '/projects/create',
+ component: ProjectChange
+ },
+ {
+ path: '/projects/:id',
+ component: Project,
+ props: true,
+ name: Routes.Project
+ },
+ {
+ path: '/projects/:id/edit',
+ component: ProjectChange,
+ props: true
+ }
+ ]
+});
+
+export default router;
diff --git a/client/src/shared/classes/Extension.ts b/client/src/shared/classes/Extension.ts
new file mode 100644
index 00000000..f59b0405
--- /dev/null
+++ b/client/src/shared/classes/Extension.ts
@@ -0,0 +1,98 @@
+import {IModel} from '../interfaces/IModel';
+import {ISchedule} from '../interfaces/ISchedule';
+import {IRole} from '../interfaces/IRole';
+
+export class Extension {
+
+ /**
+ * Get specific property from each object in Array
+ * and returns list of unique values
+ * @static
+ * @param {Object[]} source array of objects to extract from
+ * @param {string} key Property name
+ * @returns array of strings
+ * @memberof Extension
+ */
+
+ public static getUniqueValues(source: IModel[] ,key: string):string[] {
+ const uniqueSet=new Set(source.map(item=> item[key]).filter(i => i));
+
+ return Array.from(uniqueSet).sort();
+ }
+
+ /**
+ * Checks if array has element
+ * if has removes it if not adds it
+ * @param {string[]} source array to be checked
+ * @param {string} value to be checked if add or remove from array
+ * @returns {(any | string)[] | string[]}
+ */
+ public static toggleArray(source: string[]=[], value: string) {
+ const index = source.indexOf(value);
+ return index === -1
+ ? [...source, value]
+ : source.filter(item => item !== value);
+ }
+
+ /**
+ * group array by key
+ * @param {{}[]} xs source array
+ * @param {string} key property of all objects in array
+ * @returns {{}}
+ */
+ public static groupBy(xs: {}[], key: string) {
+ return xs.reduce((rv:any, x:any) => {
+ (rv[x[key]] = rv[x[key]] || []).push(x);
+ return rv;
+ }, {});
+ }
+
+ /**
+ * Set start/end date for schedule
+ * @param {ISchedule[]} schedules
+ * @param {string} property startdate or enddate
+ * @param {string} targetId id of employee of schedule
+ * @param {Date} value
+ * @returns {ISchedule[]}
+ */
+ public static setScheduleDate(schedules: ISchedule[], property:string, targetId:string, value: Date) {
+ schedules.forEach(schedule => {
+ if(schedule.employee.id === targetId) schedule[property] = value;
+ });
+
+ return schedules;
+ }
+
+ /**
+ * Set role of selected schedule
+ * @param {ISchedule[]} schedules
+ * @param {string} targetId id of employee of schedule
+ * @param {IRole} role to be set
+ * @returns {ISchedule[]}
+ */
+ public static setScheduleRole(schedules: ISchedule[], targetId:string, role:IRole) {
+ schedules.forEach(schedule => {
+ if(schedule.employee.id===targetId) {
+ schedule.roleId = role.id;
+ schedule.role = role;
+ }
+ });
+
+ return schedules;
+ }
+
+ /**
+ * Set participation of schedule
+ * @param {ISchedule[]} schedules
+ * @param {string} targetId id of employee of schedule
+ * @param {number} value
+ * @returns {ISchedule[]}
+ */
+ public static setScheduleParticipation(schedules: ISchedule[], targetId:string, value: number) {
+ schedules.forEach(schedule => {
+ if(schedule.employee.id===targetId) schedule.participation=value;
+ });
+
+ return schedules;
+ }
+ }
diff --git a/client/src/shared/classes/Guid.ts b/client/src/shared/classes/Guid.ts
new file mode 100644
index 00000000..47de44eb
--- /dev/null
+++ b/client/src/shared/classes/Guid.ts
@@ -0,0 +1,13 @@
+export default class Guid {
+ /**
+ * Generates a new guid.
+ * @returns The newly generated guid.
+ */
+ static newGuid() {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (character: string) => {
+ const random = Math.random() * 16 | 0;
+ const value = character === 'x' ? random : (random & 0x3 | 0x8);
+ return value.toString(16);
+ });
+ }
+}
diff --git a/client/src/shared/classes/ModelFactory.ts b/client/src/shared/classes/ModelFactory.ts
new file mode 100644
index 00000000..43c005a3
--- /dev/null
+++ b/client/src/shared/classes/ModelFactory.ts
@@ -0,0 +1,51 @@
+import {IEmployee} from '../interfaces/IEmployee';
+import {IRole} from '../interfaces/IRole';
+import {ISchedule} from '../interfaces/ISchedule';
+import {IProject} from '../interfaces/IProject';
+import {ITechnology} from '../interfaces/ITechnology';
+import {IType} from '../interfaces/IType';
+import {IProgram} from '../interfaces/IProgram';
+import {IDomain} from '../interfaces/IDomain';
+import {ICustomer} from '../interfaces/ICustomer';
+import Guid from './Guid';
+
+export class ModelFactory {
+ static createSchedule(employee: IEmployee, projectId?: string, role? :IRole): ISchedule {
+ return {
+ id:'',
+ active: false,
+ employee,
+ employeeId: employee ? employee.id : '',
+ role: role || {} as IRole,
+ roleId: role ? role.id : '',
+ projectId: projectId || '',
+ startdate: new Date(),
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ participation: 100.00
+ } as ISchedule
+ }
+
+ static createProject() : IProject {
+ return {
+ id:'',
+ active: false,
+ name: '',
+ description: '',
+ completed: false,
+ domain: {} as IDomain,
+ program: {} as IProgram,
+ type: {} as IType,
+ image: '',
+ schedules: [] as ISchedule[],
+ customers: [] as ICustomer[],
+ technologies: [] as ITechnology[],
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ uniqueId: Guid.newGuid(),
+ pss: 0,
+ startdate: '',
+ enddate: ''
+ } as IProject
+ }
+}
diff --git a/client/src/shared/classes/TestMocks.ts b/client/src/shared/classes/TestMocks.ts
new file mode 100644
index 00000000..b30cc305
--- /dev/null
+++ b/client/src/shared/classes/TestMocks.ts
@@ -0,0 +1,153 @@
+import {IProject} from '../interfaces/IProject';
+import {ICustomer} from '../interfaces/ICustomer';
+import {IDomain} from '../interfaces/IDomain';
+import {IType} from '../interfaces/IType';
+import {IProgram} from '../interfaces/IProgram';
+import {ILine} from '../interfaces/ILine';
+import {ISchedule} from '../interfaces/ISchedule';
+import {ITechnology} from '../interfaces/ITechnology';
+import {IEmployee} from '../interfaces/IEmployee';
+import Guid from './Guid';
+
+export class TestMocks {
+ static TestDomain(domainName: string = 'Health') {
+ const domain: IDomain = {
+ id: '1',
+ name: domainName,
+ active: false,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ projects: [] as IProject[],
+ customers: [] as ICustomer[]
+ }
+
+ return domain;
+ }
+
+ static TestTechnology(techId: string = '1', techName: string = 'React') {
+ const technology: ITechnology = {
+ id: techId,
+ name: techName,
+ domain: 'backend',
+ active: false,
+ image: '',
+ version: '1',
+ projects: [],
+ createdAt: new Date(),
+ updatedAt: new Date()
+ };
+
+ return technology;
+ }
+
+ static TestType(typeName: string = 'ProjectChange') {
+ const type: IType = {
+ id: '1',
+ name: typeName,
+ active: false,
+ createdAt: new Date(),
+ updatedAt: new Date()
+ }
+
+ return type;
+ }
+
+ static TestLine(lineName: string = 'Automotive') {
+ const line: ILine = {
+ id: '1',
+ name: lineName,
+ active: false,
+ programs: [] as IProgram[],
+ createdAt: new Date(),
+ updatedAt: new Date()
+ };
+
+ return line;
+ }
+
+ static TestProgram(programName: string = 'Sales and Autosales') {
+ const line = this.TestLine();
+
+ const program: IProgram = {
+ id: '1',
+ name: programName,
+ active: false,
+ line,
+ lineId: '1',
+ projects: [] as IProject[],
+ createdAt: new Date(),
+ updatedAt: new Date()
+ };
+
+ return program;
+ }
+
+ static TestCustomer(customerId: string = '1', customerName: string = 'BMW AG') {
+ const domain = this.TestDomain();
+
+ const customer: ICustomer = {
+ id: customerId,
+ name: customerName,
+ image: '',
+ active: false,
+ domain,
+ projects: [],
+ createdAt: new Date(),
+ updatedAt: new Date()
+ };
+
+ return customer;
+ }
+
+ static TestEmployee(employeeId: string = '1', employeeLastName: string = 'Fedorov') {
+ const technology = this.TestTechnology();
+
+ const employee: IEmployee = {
+ id: employeeId,
+ lastname: employeeLastName,
+ active: false,
+ firstname: 'Artur',
+ schedules: [] as ISchedule[],
+ technologies: [technology],
+ createdAt: new Date(),
+ updatedAt: new Date()
+ };
+
+ return employee;
+ }
+
+ static TestProject(projectName: string = 'PPA') {
+ const domain = this.TestDomain();
+ const program = this.TestProgram();
+ const type = this.TestType();
+ const customer1 = this.TestCustomer();
+ const customer2 = this.TestCustomer('2', 'Daimler');
+ const technology1 = this.TestTechnology();
+ const technology2 = this.TestTechnology('2', 'Angular 6');
+
+
+ const project: IProject = {
+ id: '1',
+ active: false,
+ name: projectName,
+ description: '123',
+ domain,
+ program,
+ image: '',
+ completed: false,
+ type,
+ schedules: [] as ISchedule[],
+ customers: [customer1, customer2],
+ technologies: [technology1, technology2],
+ startdate: new Date().toLocaleDateString(),
+ enddate: new Date().toLocaleDateString(),
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ version: 1,
+ uniqueId: Guid.newGuid(),
+ pss: 0
+ };
+
+ return project;
+ }
+}
diff --git a/client/src/shared/classes/Util.ts b/client/src/shared/classes/Util.ts
new file mode 100644
index 00000000..b9f6f6fe
--- /dev/null
+++ b/client/src/shared/classes/Util.ts
@@ -0,0 +1,139 @@
+import Vue from 'vue';
+import {IModel} from '../interfaces/IModel';
+import {IProject} from '../interfaces/IProject';
+import {Types} from '../../store/modules/projects/constant-types';
+import {CompleteTypes} from '../enums/CompleteTypes';
+
+export class Util {
+ public static getApiUrl(url: string) {
+ return `/api/${url}`;
+ }
+
+ /**
+ * maps name of filter header to property name of project opject
+ * like Production line -> line, Customers -> customers
+ * @param {string} name
+ * @returns {string}
+ */
+ public static mapNameToProperty(name: string) {
+ const words = name.trim().split(/\s+/);
+ // if only one word in filter header -> only lowercase
+ // if 2 words takes last word maps to project property
+ return words.length > 1 ? words[1].toLowerCase() : words[0].toLowerCase();
+ }
+
+ public static checkFIltersInProjects(key: string, itemsToCheck: IModel[], projects: IProject[]) {
+
+ if (key === this.mapNameToProperty(Types.CUSTOMER)) {
+ const newArray = [].concat.apply([], projects.map(project => project[key]));
+
+ itemsToCheck.forEach(item => item.active =
+ newArray.map((model: IModel) => model.id).indexOf(item.id) > -1);
+ } else if (key === this.mapNameToProperty(Types.PRODUCTION_LINE)) {
+ itemsToCheck.forEach(item => item.active =
+ projects.map(project => project['program'].lineId).indexOf(item.id) > -1);
+ } else {
+ itemsToCheck.forEach(item => item.active =
+ projects.map(project => project[key].id).indexOf(item.id) > -1);
+ }
+
+
+ return itemsToCheck;
+ }
+
+ /**
+ * Create two way mapper Getter <-> Mutation for computed property
+ * @param {string} getter
+ * @param {string} mutation
+ * @returns {any} Mapper for computed property
+ */
+ static mapTwoWay(getter: string, mutation: string) {
+ return {
+ get(this: Vue): T {
+ return this.$store.getters[getter];
+ },
+ set(this: Vue, value: T) {
+ this.$store.commit(mutation, value);
+ }
+ };
+ }
+
+ /**
+ * Checks if strings contains a substring ignoring casing of both
+ * @param {string} text
+ * @param {string} part
+ * @returns {boolean}
+ */
+ static containsIgnoreCase(text: string, part: string) {
+ return text.toLowerCase().indexOf(part.toLowerCase()) > -1;
+ }
+
+ /**
+ * Sort by field function
+ * @param {string} field Field to sort for
+ * @param {boolean} reverse Reverse sorting direction
+ * @param {boolean} ignoreCase case for strings
+ * @returns {(a: {[p: string]: {}}, b: {[p: string]: {}}) => (number | number)} Sort function
+ */
+ static sortByField(field: keyof T, reverse = false, ignoreCase = false) {
+ return (a: T, b: T) => {
+ const aValue = Util.getNestedProperty(a as { [key: string]: {} }, field);
+ const bValue = Util.getNestedProperty(b as { [key: string]: {} }, field);
+
+ const aField = ignoreCase ? aValue.toString().toLowerCase() : aValue;
+ const bField = ignoreCase ? bValue.toString().toLowerCase() : bValue;
+
+ return reverse ?
+ Util.compareValues(aField, bField) :
+ Util.compareValues(bField, aField);
+ };
+ }
+
+ /**
+ * Filters opened or completed or all projects
+ * @param {IProject[]} projects
+ * @param {string} completion
+ * @returns {IProject[]}
+ */
+ static filterCompletedProjects(projects: IProject[], completion: string) {
+ switch (completion) {
+ case CompleteTypes.OPENED:
+ return projects.filter(project => !Boolean(project.enddate));
+ case CompleteTypes.COMPLETED:
+ return projects.filter(project => this.projectCompleted(project));
+ default:
+ return projects;
+ }
+ }
+
+ /**
+ * Checks if project if completed
+ * has to have enddate and date has to be in past
+ * @param {IProject} project
+ * @returns {boolean}
+ */
+ static projectCompleted(project: IProject) {
+ return Boolean(project.enddate) && (new Date(project.enddate) <= new Date());
+ }
+
+ private static getNestedProperty(object: { [key: string]: {} }, property: string): {} {
+ if (typeof object === 'undefined') {
+ return false;
+ }
+ const index = property.indexOf('.');
+ if (index > -1) {
+ return Util.getNestedProperty(object[property.substring(0, index)], property.substr(index + 1));
+ }
+ return object[property];
+ }
+
+ private static compareValues(a: {}, b: {}): number {
+ if (a > b) {
+ return 1;
+ }
+ if (a < b) {
+ return -1;
+ }
+ return 0;
+ }
+}
diff --git a/client/src/shared/enums/CompleteTypes.ts b/client/src/shared/enums/CompleteTypes.ts
new file mode 100644
index 00000000..4473b16c
--- /dev/null
+++ b/client/src/shared/enums/CompleteTypes.ts
@@ -0,0 +1,5 @@
+export enum CompleteTypes {
+ ALL = 'all',
+ OPENED = 'opened',
+ COMPLETED = 'completed'
+}
diff --git a/client/src/shared/enums/ProjectsQueryKey.ts b/client/src/shared/enums/ProjectsQueryKey.ts
new file mode 100644
index 00000000..b2dcfcc9
--- /dev/null
+++ b/client/src/shared/enums/ProjectsQueryKey.ts
@@ -0,0 +1,6 @@
+export enum ProjectQueryKey {
+ SEARCH = 'search',
+ COMPLETION = 'completion',
+ SORT = 'sort',
+ SORT_REVERSE = 'reverse'
+}
diff --git a/client/src/shared/filters/Capitalize.ts b/client/src/shared/filters/Capitalize.ts
new file mode 100644
index 00000000..33c9084c
--- /dev/null
+++ b/client/src/shared/filters/Capitalize.ts
@@ -0,0 +1,4 @@
+export default function (value:string) {
+ if (!value) return ''
+ return value.charAt(0).toUpperCase() + value.slice(1);
+}
diff --git a/client/src/shared/filters/Date.ts b/client/src/shared/filters/Date.ts
new file mode 100644
index 00000000..2794b7fe
--- /dev/null
+++ b/client/src/shared/filters/Date.ts
@@ -0,0 +1,5 @@
+export default function (value:any) {
+ if (!value) return ''
+ value = new Date(value);
+ return value.toLocaleDateString();
+}
\ No newline at end of file
diff --git a/client/src/shared/http/interceptors/loading.ts b/client/src/shared/http/interceptors/loading.ts
new file mode 100644
index 00000000..cc29aa6a
--- /dev/null
+++ b/client/src/shared/http/interceptors/loading.ts
@@ -0,0 +1,16 @@
+import axios from 'axios';
+import store from '../../../store';
+import {DECREMENT_LOADING_STATE, INCREMENT_LOADING_STATE} from '../../../store/modules/loading/mutation-types';
+
+axios.interceptors.request.use(config => {
+ store.commit(INCREMENT_LOADING_STATE, Date.now());
+ return config;
+}, error => Promise.reject(error));
+
+axios.interceptors.response.use(response => {
+ store.commit(DECREMENT_LOADING_STATE);
+ return response;
+}, error => {
+ store.commit(DECREMENT_LOADING_STATE);
+ return Promise.reject(error);
+});
diff --git a/client/src/shared/interfaces/ICustomer.ts b/client/src/shared/interfaces/ICustomer.ts
new file mode 100644
index 00000000..2b9e27a5
--- /dev/null
+++ b/client/src/shared/interfaces/ICustomer.ts
@@ -0,0 +1,10 @@
+import {IModel} from './IModel';
+import {IProject} from './IProject';
+import {IDomain} from './IDomain';
+
+export interface ICustomer extends IModel{
+ name: string;
+ domain: IDomain;
+ image: string;
+ projects: IProject[];
+}
diff --git a/client/src/shared/interfaces/IDomain.ts b/client/src/shared/interfaces/IDomain.ts
new file mode 100644
index 00000000..21e112e0
--- /dev/null
+++ b/client/src/shared/interfaces/IDomain.ts
@@ -0,0 +1,9 @@
+import {IModel} from './IModel';
+import {IProject} from './IProject';
+import {ICustomer} from './ICustomer';
+
+export interface IDomain extends IModel{
+ name: string;
+ projects: IProject[];
+ customers: ICustomer[];
+}
diff --git a/client/src/shared/interfaces/IEmployee.ts b/client/src/shared/interfaces/IEmployee.ts
new file mode 100644
index 00000000..7216d7c4
--- /dev/null
+++ b/client/src/shared/interfaces/IEmployee.ts
@@ -0,0 +1,11 @@
+import {IModel} from './IModel';
+import {ITechnology} from './ITechnology';
+import {ISchedule} from './ISchedule';
+
+
+export interface IEmployee extends IModel{
+ firstname: string;
+ lastname: string;
+ technologies: ITechnology[];
+ schedules: ISchedule[];
+}
diff --git a/client/src/shared/interfaces/IFilter.ts b/client/src/shared/interfaces/IFilter.ts
new file mode 100644
index 00000000..7e5e6e60
--- /dev/null
+++ b/client/src/shared/interfaces/IFilter.ts
@@ -0,0 +1,3 @@
+export interface IFilter {
+ [key: string]: string[];
+}
diff --git a/client/src/shared/interfaces/ILine.ts b/client/src/shared/interfaces/ILine.ts
new file mode 100644
index 00000000..2531ed6f
--- /dev/null
+++ b/client/src/shared/interfaces/ILine.ts
@@ -0,0 +1,7 @@
+import {IModel} from './IModel';
+import {IProgram} from './IProgram';
+
+export interface ILine extends IModel{
+ name: string;
+ programs: IProgram[];
+}
diff --git a/client/src/shared/interfaces/IModel.ts b/client/src/shared/interfaces/IModel.ts
new file mode 100644
index 00000000..373b9397
--- /dev/null
+++ b/client/src/shared/interfaces/IModel.ts
@@ -0,0 +1,11 @@
+/**
+ * Base interface for generic methods
+ * avoid usage of any
+ */
+export interface IModel {
+ [key: string]: any;
+ active: boolean;
+ id:string;
+ createdAt: Date;
+ updatedAt: Date;
+}
diff --git a/client/src/shared/interfaces/IProgram.ts b/client/src/shared/interfaces/IProgram.ts
new file mode 100644
index 00000000..7c603e43
--- /dev/null
+++ b/client/src/shared/interfaces/IProgram.ts
@@ -0,0 +1,9 @@
+import {IModel} from './IModel';
+import {ILine} from './ILine';
+import {IProject} from './IProject';
+
+export interface IProgram extends IModel{
+ name: string;
+ line: ILine;
+ projects: IProject[];
+}
diff --git a/client/src/shared/interfaces/IProject.ts b/client/src/shared/interfaces/IProject.ts
new file mode 100644
index 00000000..27c9433d
--- /dev/null
+++ b/client/src/shared/interfaces/IProject.ts
@@ -0,0 +1,28 @@
+import {IModel} from './IModel';
+import {ISchedule} from './ISchedule';
+import {IDomain} from './IDomain';
+import {IProgram} from './IProgram';
+import {ICustomer} from './ICustomer';
+import {IType} from './IType';
+import {ITechnology} from './ITechnology';
+
+
+export interface IProject extends IModel {
+ name: string;
+ description: string;
+ completed: boolean,
+ domain: IDomain;
+ program: IProgram;
+ type: IType;
+ image: string;
+ schedules: ISchedule[];
+ customers: ICustomer[];
+ technologies: ITechnology[];
+ startdate: string;
+ enddate: string;
+ createdAt: Date;
+ updatedAt: Date;
+ uniqueId: string;
+ pss: number;
+ version: number;
+}
diff --git a/client/src/shared/interfaces/IRole.ts b/client/src/shared/interfaces/IRole.ts
new file mode 100644
index 00000000..3fcb4edf
--- /dev/null
+++ b/client/src/shared/interfaces/IRole.ts
@@ -0,0 +1,10 @@
+import {IModel} from './IModel';
+import {ISchedule} from './ISchedule';
+
+
+export interface IRole extends IModel{
+ name: string;
+ domain: string;
+ leadrole: boolean;
+ schedules: ISchedule[];
+}
diff --git a/client/src/shared/interfaces/ISchedule.ts b/client/src/shared/interfaces/ISchedule.ts
new file mode 100644
index 00000000..201b7597
--- /dev/null
+++ b/client/src/shared/interfaces/ISchedule.ts
@@ -0,0 +1,14 @@
+import {IModel} from './IModel';
+import {IEmployee} from './IEmployee';
+import {IRole} from './IRole';
+
+export interface ISchedule extends IModel{
+ employeeId: string,
+ employee: IEmployee;
+ projectId: string,
+ participation: number,
+ roleId: string,
+ role: IRole,
+ startdate: Date,
+ enddate?: Date
+}
diff --git a/client/src/shared/interfaces/ITechnology.ts b/client/src/shared/interfaces/ITechnology.ts
new file mode 100644
index 00000000..63de00f2
--- /dev/null
+++ b/client/src/shared/interfaces/ITechnology.ts
@@ -0,0 +1,16 @@
+import {IModel} from './IModel';
+import {IProject} from './IProject';
+
+export interface ITechnology extends IModel {
+ name: string;
+ domain: string;
+ active: boolean;
+ image: string;
+ version: string;
+ projects: IProject[];
+
+ // TODO
+ // employees: IEmployee[];
+}
+
+
diff --git a/client/src/shared/interfaces/IType.ts b/client/src/shared/interfaces/IType.ts
new file mode 100644
index 00000000..31aa9209
--- /dev/null
+++ b/client/src/shared/interfaces/IType.ts
@@ -0,0 +1,5 @@
+import {IModel} from './IModel';
+
+export interface IType extends IModel{
+ name: string
+}
diff --git a/client/src/shared/interfaces/shared/IProjectFilter.ts b/client/src/shared/interfaces/shared/IProjectFilter.ts
new file mode 100644
index 00000000..ad06957f
--- /dev/null
+++ b/client/src/shared/interfaces/shared/IProjectFilter.ts
@@ -0,0 +1,12 @@
+export interface IProjectFilter {
+ name: String,
+ opened: Boolean,
+ items: IProjectFilterCheck[]
+}
+
+export interface IProjectFilterCheck {
+ active: boolean,
+ checked: boolean,
+ value: String,
+ id: String
+}
diff --git a/client/src/shared/interfaces/ui/IAccordion.ts b/client/src/shared/interfaces/ui/IAccordion.ts
new file mode 100644
index 00000000..49d04bbd
--- /dev/null
+++ b/client/src/shared/interfaces/ui/IAccordion.ts
@@ -0,0 +1,5 @@
+
+export interface IAccordion {
+ [key: string]: boolean;
+
+}
diff --git a/client/src/store/http/routes.ts b/client/src/store/http/routes.ts
new file mode 100644
index 00000000..faf6df47
--- /dev/null
+++ b/client/src/store/http/routes.ts
@@ -0,0 +1,15 @@
+export enum routes {
+ DOES_PROJECT_EXIST = '/api/projects/exists/',
+ DOES_PROJECT_EXIST_WITH_ID = '/api/projects/update/exists/',
+ GET_PROJECTS = '/api/projects',
+ GET_PROJECT = '/api/projects/',
+ GET_PROJECT_ADDONS = '/api/projects/addons',
+ GET_TECHNOLOGIES = '/api/technologies',
+ GET_ROLES = '/api/roles',
+ GET_EMPLOYEES = '/api/employees',
+ CREATE_PROJECT = '/api/projects/create',
+ EDIT_PROJECT = '/api/projects/update',
+ DELETE_PROJECT = '/api/projects/delete/',
+ REMOVE_PROJECT_IMAGE = '/api/images/remove',
+ UPDATE_PROJECT_IMAGE = '/api/projects/update/image'
+}
diff --git a/client/src/store/index.ts b/client/src/store/index.ts
new file mode 100644
index 00000000..81ce9885
--- /dev/null
+++ b/client/src/store/index.ts
@@ -0,0 +1,22 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+
+import employees from './modules/employees/index';
+import projects from './modules/projects/index';
+import technologies from './modules/technologies/index';
+import loading from './modules/loading';
+import fileUpload from '../components/common/FileUploader/fileUploadStore';
+
+Vue.use(Vuex);
+
+const store = new Vuex.Store({
+ modules: {
+ employees,
+ projects,
+ technologies,
+ fileUpload,
+ loading
+ }
+});
+
+export default store;
diff --git a/client/src/store/modules/employees/EmployeeService.ts b/client/src/store/modules/employees/EmployeeService.ts
new file mode 100644
index 00000000..49487ba4
--- /dev/null
+++ b/client/src/store/modules/employees/EmployeeService.ts
@@ -0,0 +1,15 @@
+import axios from 'axios';
+import {routes} from '../../http/routes';
+import {IEmployee} from '../../../shared/interfaces/IEmployee';
+import {IRole} from '../../../shared/interfaces/IRole';
+
+export class EmployeeService {
+
+ static getEmployees() {
+ return axios.get(routes.GET_EMPLOYEES);
+ }
+
+ static getRoles() {
+ return axios.get(routes.GET_ROLES);
+ }
+}
diff --git a/client/src/store/modules/employees/action-types.ts b/client/src/store/modules/employees/action-types.ts
new file mode 100644
index 00000000..cb1726aa
--- /dev/null
+++ b/client/src/store/modules/employees/action-types.ts
@@ -0,0 +1,2 @@
+export const FETCH_EMPLOYEES = 'employees/FETCH_EMPLOYEES';
+export const FETCH_ROLES = 'employees/FETCH_ROLES';
diff --git a/client/src/store/modules/employees/actions.ts b/client/src/store/modules/employees/actions.ts
new file mode 100644
index 00000000..0c1d05ed
--- /dev/null
+++ b/client/src/store/modules/employees/actions.ts
@@ -0,0 +1,24 @@
+import {ActionTree} from 'vuex';
+import {FETCH_EMPLOYEES} from './action-types';
+import {EmployeeService} from './EmployeeService';
+import {IEmployeeState} from './index';
+import {SET_EMPLOYEES} from './mutation-types';
+import {SET_ROLES} from './mutation-types';
+import {FETCH_ROLES} from './action-types';
+
+
+export const actions: ActionTree = {
+ [FETCH_EMPLOYEES]({commit}) {
+ return EmployeeService.getEmployees()
+ .then((response) => {
+ commit(SET_EMPLOYEES, response.data);
+ });
+ },
+
+ [FETCH_ROLES]({commit}) {
+ return EmployeeService.getRoles()
+ .then(response => {
+ commit(SET_ROLES, response.data);
+ });
+ },
+}
diff --git a/client/src/store/modules/employees/getter-types.ts b/client/src/store/modules/employees/getter-types.ts
new file mode 100644
index 00000000..d424e54f
--- /dev/null
+++ b/client/src/store/modules/employees/getter-types.ts
@@ -0,0 +1,2 @@
+export const EMPLOYEES = 'EMPLOYEES';
+export const ROLES = 'ROLES';
diff --git a/client/src/store/modules/employees/getters.ts b/client/src/store/modules/employees/getters.ts
new file mode 100644
index 00000000..ff32b056
--- /dev/null
+++ b/client/src/store/modules/employees/getters.ts
@@ -0,0 +1,11 @@
+import {GetterTree} from 'vuex';
+import {IEmployeeState} from './index';
+import {EMPLOYEES} from './getter-types';
+import {ROLES} from './getter-types';
+
+
+
+export const getters: GetterTree = {
+ [EMPLOYEES]: state => state.employees,
+ [ROLES]: state => state.roles
+};
diff --git a/client/src/store/modules/employees/index.ts b/client/src/store/modules/employees/index.ts
new file mode 100644
index 00000000..930393f1
--- /dev/null
+++ b/client/src/store/modules/employees/index.ts
@@ -0,0 +1,30 @@
+import {Module} from 'vuex';
+import {actions} from './actions';
+import {getters} from './getters';
+import {mutations} from './mutations';
+import {IEmployee} from '../../../shared/interfaces/IEmployee';
+import {IRole} from '../../../shared/interfaces/IRole';
+
+
+export interface IEmployeeState {
+ employees: IEmployee[];
+ roles: IRole[],
+ selected: string[];
+ loading: boolean;
+ filter: string;
+}
+
+const employeeModule: Module = {
+ state: {
+ employees: [],
+ roles: [],
+ selected:[],
+ loading: true,
+ filter: ''
+ },
+ mutations,
+ actions,
+ getters
+};
+
+export default employeeModule;
diff --git a/client/src/store/modules/employees/mutation-types.ts b/client/src/store/modules/employees/mutation-types.ts
new file mode 100644
index 00000000..464b9449
--- /dev/null
+++ b/client/src/store/modules/employees/mutation-types.ts
@@ -0,0 +1,2 @@
+export const SET_EMPLOYEES = 'employees/SET_EMPLOYEES';
+export const SET_ROLES = 'employees/SET_ROLES';
diff --git a/client/src/store/modules/employees/mutations.ts b/client/src/store/modules/employees/mutations.ts
new file mode 100644
index 00000000..4ff42855
--- /dev/null
+++ b/client/src/store/modules/employees/mutations.ts
@@ -0,0 +1,17 @@
+import {MutationTree} from 'vuex';
+import {IEmployeeState} from './index';
+import {SET_EMPLOYEES} from './mutation-types';
+import {IEmployee} from '../../../shared/interfaces/IEmployee';
+import {SET_ROLES} from './mutation-types';
+import {IRole} from '../../../shared/interfaces/IRole';
+
+export const mutations: MutationTree = {
+ [SET_EMPLOYEES](state, payload: IEmployee[]) {
+ state.employees = payload;
+ state.loading = false;
+ },
+ [SET_ROLES](state, payload: IRole[]) {
+ state.roles = payload;
+ state.loading = false;
+ },
+};
diff --git a/client/src/store/modules/loading/getter-types.ts b/client/src/store/modules/loading/getter-types.ts
new file mode 100644
index 00000000..fd27944e
--- /dev/null
+++ b/client/src/store/modules/loading/getter-types.ts
@@ -0,0 +1 @@
+export const GET_LOADING_STATE = 'GET_LOADING_STATE';
diff --git a/client/src/store/modules/loading/index.ts b/client/src/store/modules/loading/index.ts
new file mode 100644
index 00000000..c922cb8b
--- /dev/null
+++ b/client/src/store/modules/loading/index.ts
@@ -0,0 +1,26 @@
+import {Module} from 'vuex';
+import {GET_LOADING_STATE} from './getter-types';
+import {DECREMENT_LOADING_STATE, INCREMENT_LOADING_STATE} from './mutation-types';
+
+interface ILoadingState {
+ loading: string[];
+}
+
+const loadingState: Module = {
+ state: {
+ loading: []
+ },
+ mutations: {
+ [INCREMENT_LOADING_STATE](state, loading: string) {
+ state.loading.push(loading);
+ },
+ [DECREMENT_LOADING_STATE](state) {
+ state.loading.pop();
+ }
+ },
+ getters: {
+ [GET_LOADING_STATE]: state => Boolean(state.loading.length)
+ }
+};
+
+export default loadingState;
diff --git a/client/src/store/modules/loading/mutation-types.ts b/client/src/store/modules/loading/mutation-types.ts
new file mode 100644
index 00000000..94ebb2cd
--- /dev/null
+++ b/client/src/store/modules/loading/mutation-types.ts
@@ -0,0 +1,2 @@
+export const INCREMENT_LOADING_STATE = 'INCREMENT_LOADING_STATE';
+export const DECREMENT_LOADING_STATE = 'DECREMENT_LOADING_STATE';
diff --git a/client/src/store/modules/projects/PowerPointService.ts b/client/src/store/modules/projects/PowerPointService.ts
new file mode 100644
index 00000000..189f53a3
--- /dev/null
+++ b/client/src/store/modules/projects/PowerPointService.ts
@@ -0,0 +1,214 @@
+import axios from 'axios';
+import PptxGenJS from 'pptxgenjs';
+import {IProject} from '../../../shared/interfaces/IProject';
+import {ITechnology} from '../../../shared/interfaces/ITechnology';
+
+interface ISlide {
+ addText(text: string, params: {}): void
+
+ addImage(params: {}): void
+
+ addShape(type: string, shape: {}): void
+}
+
+interface IPptx {
+
+ shapes: { RECTANGLE: string }
+
+ setBrowser(param: boolean): void
+
+ setLayout(param: string): void
+
+ save(param: string): void
+
+ addNewSlide(): ISlide
+}
+
+interface IPresentationResponse {
+ header: string;
+ header2: string;
+ domain: string;
+ image: string;
+ technologies: ITechnology[];
+}
+
+// Constants
+const HEADLINE_FONT = 'TELEGROTESK HEADLINE ULTRA';
+const NORMAL_FONT = 'Tele-GroteskNor';
+
+const MAGENTA = 'e20074';
+const WHITE = 'ffffff';
+const GRAY = '7f7f7f';
+const LIGHT_GRAY = 'a4a4a4';
+const BLACK = '000000';
+// default padding from left
+const X = 0.39;
+
+let pptx: IPptx | null = null;
+
+export class PowerPointService {
+ static createSingleProjectPresentation(project: IProject) {
+ this.newPresentation();
+
+ this.addProjectSlide(project)
+ .then(() => this.generatePresentation(project.name));
+ }
+
+ static createProjectsPresentation(projects: IProject[]) {
+ this.newPresentation();
+
+ const promises: Promise[] = [];
+ projects.forEach(project => {
+ promises.push(this.addProjectSlide(project));
+ });
+
+ Promise.all(promises)
+ .then(() => this.generatePresentation());
+ }
+
+ private static newPresentation() {
+ pptx = new PptxGenJS();
+
+ pptx!.setBrowser(true);
+ pptx!.setLayout('LAYOUT_4x3');
+ }
+
+ private static generatePresentation(name = '') {
+ pptx!.save(`POP Russia Portfolio ${name}`);
+ pptx = null;
+ }
+
+ private static addProjectSlide(project: IProject) {
+ return axios.get(`/api/presentation/images/${project.id}`)
+ .then(response => {
+ const header = response.data.header;
+ const header2 = response.data.header2;
+ const domain = response.data.domain;
+ const image = response.data.image;
+ const technologies = response.data.technologies;
+
+ const slide = pptx!.addNewSlide();
+
+ this.addImage(slide, header, 0, 0, '100%', 0.5);
+ this.addImage(slide, header2, 0, 0, '100%', 0.5);
+ if (domain) {
+ this.addImage(slide, domain, '92%', 0.05, 0.45, 0.45);
+ }
+
+ this.addText(slide, project.name, X, 0, HEADLINE_FONT, 28, GRAY);
+ this.addText(slide, 'Description of Project', X, 0.6, HEADLINE_FONT, 18, MAGENTA);
+ this.addText(slide, project.description, X, 1.0, NORMAL_FONT, 14, BLACK, false, 'top');
+
+ if (image) {
+ this.addImage(slide, image, '60%', 1.1, 3.2, 1.9);
+ }
+
+ slide.addShape(pptx!.shapes.RECTANGLE, {x: 0.0, y: 5.5, w: '50%', h: 2.0, fill: MAGENTA});
+ slide.addShape(pptx!.shapes.RECTANGLE, {x: '50%', y: 5.5, w: '50%', h: 2.0, fill: LIGHT_GRAY});
+
+ this.addText(slide, 'Details', X, 5.45, HEADLINE_FONT, 18);
+
+ const start = 5.75;
+ const lineheight = 0.25;
+
+ this.addText(slide, 'Project duration:', X, start, NORMAL_FONT, 18);
+ this.addText(slide,
+ this.getDate(project.startdate) + (project.enddate ? ` - ${this.getDate(project.enddate)}` : ''),
+ 1.9, start, NORMAL_FONT, 18, WHITE, true);
+
+ const programY = start + lineheight;
+ this.addText(slide, 'Program:', X, programY, NORMAL_FONT, 18);
+ this.addText(slide, project.program.name, 1.3, programY, NORMAL_FONT, 18, WHITE, true);
+
+ const domainY = start + (lineheight * 2);
+ this.addText(slide, 'Domain:', X, domainY, NORMAL_FONT, 18);
+ this.addText(slide, project.domain.name, 1.3, domainY, NORMAL_FONT, 18, WHITE, true);
+
+ let interval = 0;
+ const language = project.technologies.filter(tech => tech.domain === 'language');
+ if (language.length) {
+ interval += 0.25;
+ const text = language.map(item => item.name).join(' ');
+
+ const languageY = start + (lineheight * 3);
+ this.addText(slide, 'Language:', X, languageY, NORMAL_FONT, 18);
+ this.addText(slide, text, 1.4, languageY, NORMAL_FONT, 18, WHITE, true);
+ }
+
+ const methodology = project.technologies.filter(tech => tech.domain === 'methodology');
+ if (methodology.length) {
+ const text = methodology.map(item => item.name).join(' ');
+
+ const methodologyY = start + (lineheight * 3) + interval;
+ this.addText(slide, 'Methodology:', X, methodologyY, NORMAL_FONT, 18);
+ this.addText(slide, text, 1.7, methodologyY, NORMAL_FONT, 18, WHITE, true);
+ }
+
+ const backend = technologies.filter(item => item.domain === 'backend');
+ const frontend = technologies.filter(item => item.domain === 'frontend');
+
+ let headerY = 5.45;
+ let iconY = 5.9;
+ let textY = 6.1;
+
+ if (backend.length) {
+ this.addText(slide, 'Back-end', 5.1, headerY, HEADLINE_FONT, 18);
+
+ let bside = 5.2;
+ let bsidetext = 5.1;
+ backend.forEach(item => {
+ this.addImage(slide, item.image, bside, iconY, 0.4, 0.3);
+ this.addText(slide, item.name, bsidetext, textY);
+
+ bside += 0.7;
+ bsidetext += 0.7;
+ });
+
+ headerY += 0.95;
+ iconY += 0.95;
+ textY += 0.95;
+ }
+
+ if (frontend.length) {
+ this.addText(slide, 'Front-end', 5.1, headerY, HEADLINE_FONT, 18);
+
+ let bside = 5.2;
+ let bsidetext = 5.1;
+
+ frontend.forEach(item => {
+ this.addImage(slide, item.image, bside, iconY, 0.4, 0.3);
+ this.addText(slide, item.name, bsidetext, textY);
+
+ bside += 0.7;
+ bsidetext += 0.7;
+ });
+ }
+ });
+ }
+
+ private static addText(slide: ISlide, text: string, x: number, y: number, fontFace = NORMAL_FONT, fontSize = 14,
+ color = WHITE, underline = false, valign = 'middle') {
+ slide.addText(text, {
+ x,
+ y,
+ w: '50%',
+ h: 0.5,
+ align: 'l',
+ valign,
+ fontSize,
+ fontFace,
+ color,
+ underline
+ });
+ }
+
+ private static addImage(slide: ISlide, imageBase64: string, x: number | string, y: number, w: number | string,
+ h: number) {
+ slide.addImage({data: `image/png;base64,${imageBase64}`, x, y, w, h});
+ }
+
+ private static getDate(date: string) {
+ const newDate = new Date(date);
+ return `${newDate.getDate()}.${newDate.getMonth()}.${newDate.getFullYear()}`;
+ }
+}
diff --git a/client/src/store/modules/projects/action-types.ts b/client/src/store/modules/projects/action-types.ts
new file mode 100644
index 00000000..ea26d264
--- /dev/null
+++ b/client/src/store/modules/projects/action-types.ts
@@ -0,0 +1,23 @@
+export const FETCH_ADDONS = 'projects/FETCH_ADDONS';
+export const FETCH_PROJECTS = 'projects/FETCH_PROJECTS';
+
+export const FETCH_PROJECT = 'projects/FETCH_PROJECT';
+export const FETCH_PROJECT_WITH_IMAGE = 'FETCH_PROJECT_WITH_IMAGE';
+export const CHECK_PROJECT_EXISTENCE = 'projects/CHECK_PROJECT_EXISTENCE';
+export const CHECK_PROJECT_EXISTENCE_UPDATE = 'projects/CHECK_PROJECT_EXISTENCE_UPDATE';
+
+export const CREATE_PROJECT = 'projects/CREATE_PROJECT';
+export const EDIT_PROJECT = 'projects/SAVE_PROJECT';
+
+export const DELETE_PROJECT = 'projects/DELETE_PROJECT';
+
+export const GENERATE_PRESENTATION = 'GENERATE_PRESENTATION';
+export const GENERATE_PRESENTATION_SINGLE = 'GENERATE_PRESENTATION_SINGLE';
+
+
+export const REMOVE_PROJECT_IMAGE = 'projects/REMOVE_PROJECT_IMAGE';
+export const UPDATE_PROJECT_IMAGE = 'projects/UPDATE_PROJECT_IMAGE';
+
+export const SYNC_PARAMS = 'SYNC_PARAMS';
+export const RESET_FILTERS_TECHNOLOGIES = 'RESET_FILTERS_TECHNOLOGIES';
+
diff --git a/client/src/store/modules/projects/actions.ts b/client/src/store/modules/projects/actions.ts
new file mode 100644
index 00000000..204000ba
--- /dev/null
+++ b/client/src/store/modules/projects/actions.ts
@@ -0,0 +1,157 @@
+import {ActionTree} from 'vuex';
+import {ProjectService} from './project.service';
+import {IProjectState} from './index';
+import {
+ CHECK_PROJECT_EXISTENCE, CHECK_PROJECT_EXISTENCE_UPDATE,
+ CREATE_PROJECT,
+ DELETE_PROJECT, EDIT_PROJECT,
+ FETCH_ADDONS,
+ FETCH_PROJECT,
+ FETCH_PROJECT_WITH_IMAGE,
+ FETCH_PROJECTS,
+
+ GENERATE_PRESENTATION, GENERATE_PRESENTATION_SINGLE, REMOVE_PROJECT_IMAGE, UPDATE_PROJECT_IMAGE,
+ RESET_FILTERS_TECHNOLOGIES, SYNC_PARAMS
+} from './action-types';
+
+import {
+ FINISH_LOADING, RESET_FILTERS, SET_COMPLETION_VALUE,
+ SET_CUSTOMERS,
+ SET_DOMAINS,
+ SET_FILTER_VALUE,
+ SET_LINES,
+ SET_PROGRAMS,
+ SET_PROJECT, SET_PROJECT_IMAGE,
+ SET_PROJECTS,
+ SET_SEARCH_VALUE, SET_SORT_REVERSE_VALUE, SET_SORT_VALUE,
+
+ SET_TYPES
+} from './mutation-types';
+import {PowerPointService} from './PowerPointService';
+import {PROJECT, PROJECTS} from './getter-types';
+import {SET_IMAGE_URL} from '../../../components/common/FileUploader/fileUploadStore/mutation-types';
+import {FileUploadStatus} from '../../../components/common/FileUploader/IFileUploadList';
+import {FilterTypes} from './filter-types';
+import {RESET_TECHNOLOGIES, TOGGLE_TECHNOLOGY} from '../technologies/mutation-types';
+import {default as router, Routes} from '../../../router';
+import {ProjectQueryKey} from '../../../shared/enums/ProjectsQueryKey';
+
+export const actions: ActionTree = {
+
+ [FETCH_PROJECTS]({commit}) {
+ return ProjectService.getProjects()
+ .then(response => {
+ commit(SET_PROJECTS, response.data);
+ });
+ },
+
+
+ [FETCH_PROJECT]({commit}, id:string) {
+ return ProjectService.getProject(id)
+ .then(response => {
+ commit(SET_PROJECT, response.data);
+ return response.data;
+ });
+ },
+ [FETCH_PROJECT_WITH_IMAGE]({commit, dispatch}, id: string) {
+ return dispatch(FETCH_PROJECT, id)
+ .then(project => {
+ if (project.image) {
+ // Set image for file uploader
+ commit(SET_IMAGE_URL, {
+ url: `./server/images/${project.image}`,
+ name: project.image,
+ loadingStatus: FileUploadStatus.NULL
+ });
+ }
+ });
+ },
+ [FETCH_ADDONS]({commit}) {
+ return ProjectService.getProjectAddons()
+ .then(response => {
+ commit(SET_LINES, response.data.lines);
+ commit(SET_PROGRAMS, response.data.programs);
+ commit(SET_DOMAINS, response.data.domains);
+ commit(SET_TYPES, response.data.types);
+ commit(SET_CUSTOMERS, response.data.customers);
+ commit(FINISH_LOADING);
+ });
+ },
+
+
+ [CHECK_PROJECT_EXISTENCE]({commit}, name:string) {
+ return ProjectService.doesProjectExist(name)
+ .then(response => response.data);
+ },
+
+ [CHECK_PROJECT_EXISTENCE_UPDATE]({commit}, payload: {name:string, id:string}) {
+ return ProjectService.doesProjectWithIdExist(payload.name, payload.id)
+ .then(response => response.data);
+ },
+
+ [CREATE_PROJECT]({state}) {
+ return ProjectService.createProject(state.project)
+ .then(() => router.push({name: Routes.Projects}));
+ },
+
+ [EDIT_PROJECT]({state}) {
+ return ProjectService.editProject(state.project)
+ .then((response) => router.push({name: Routes.Project, params: { id: String(response.data.id) }}));
+ },
+
+ [DELETE_PROJECT]({commit}, id:string) {
+ return ProjectService.deleteProject(id)
+ .then(() => router.push({name: Routes.Projects}));
+ },
+ [GENERATE_PRESENTATION]({getters}) {
+ return PowerPointService.createProjectsPresentation(getters[PROJECTS]);
+ },
+ [GENERATE_PRESENTATION_SINGLE]({getters}) {
+ return PowerPointService.createSingleProjectPresentation(getters[PROJECT]);
+ },
+
+
+ [REMOVE_PROJECT_IMAGE]({commit}, image: string) {
+ return ProjectService.removeImage