Client - remove old client
@@ -1,433 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "root": true,
 | 
			
		||||
  "parser": "babel-eslint",
 | 
			
		||||
  "parserOptions": {
 | 
			
		||||
    "ecmaVersion": 2017,
 | 
			
		||||
    "sourceType": "module",
 | 
			
		||||
    "ecmaFeatures": {
 | 
			
		||||
      "jsx": true,
 | 
			
		||||
      "module": true,
 | 
			
		||||
      "experimentalObjectRestSpread": true
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "env": {
 | 
			
		||||
    "browser": true,
 | 
			
		||||
    "node": true,
 | 
			
		||||
    "es6": true,
 | 
			
		||||
    "jest": true
 | 
			
		||||
  },
 | 
			
		||||
  "extends": ["plugin:prettier/recommended"],
 | 
			
		||||
  "rules": {
 | 
			
		||||
    "no-alert": "warn",
 | 
			
		||||
    "no-array-constructor": "off",
 | 
			
		||||
    "no-await-in-loop": "off",
 | 
			
		||||
    "no-bitwise": "off",
 | 
			
		||||
    "no-caller": "warn",
 | 
			
		||||
    "no-case-declarations": "error",
 | 
			
		||||
    "no-catch-shadow": "off",
 | 
			
		||||
    "no-class-assign": "error",
 | 
			
		||||
    "no-compare-neg-zero": "warn",
 | 
			
		||||
    "no-cond-assign": "error",
 | 
			
		||||
    "no-confusing-arrow": "off",
 | 
			
		||||
    "no-console": [
 | 
			
		||||
      "error",
 | 
			
		||||
      {
 | 
			
		||||
        "allow": ["warn", "error", "info"]
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "no-const-assign": "error",
 | 
			
		||||
    "no-constant-condition": "error",
 | 
			
		||||
    "no-continue": "off",
 | 
			
		||||
    "no-control-regex": "error",
 | 
			
		||||
    "no-debugger": "off",
 | 
			
		||||
    "no-delete-var": "error",
 | 
			
		||||
    "no-div-regex": "off",
 | 
			
		||||
    "no-dupe-args": "error",
 | 
			
		||||
    "no-dupe-class-members": "error",
 | 
			
		||||
    "no-dupe-keys": "error",
 | 
			
		||||
    "no-duplicate-case": "error",
 | 
			
		||||
    "no-duplicate-imports": "warn",
 | 
			
		||||
    "no-else-return": "warn",
 | 
			
		||||
    "no-empty": "error",
 | 
			
		||||
    "no-empty-character-class": "error",
 | 
			
		||||
    "no-empty-function": "off",
 | 
			
		||||
    "no-empty-pattern": "error",
 | 
			
		||||
    "no-eq-null": "warn",
 | 
			
		||||
    "no-eval": "error",
 | 
			
		||||
    "no-ex-assign": "error",
 | 
			
		||||
    "no-extend-native": "error",
 | 
			
		||||
    "no-extra-bind": "warn",
 | 
			
		||||
    "no-extra-boolean-cast": "error",
 | 
			
		||||
    "no-extra-label": "warn",
 | 
			
		||||
    "no-extra-parens": "off",
 | 
			
		||||
    "no-extra-semi": "off",
 | 
			
		||||
    "no-fallthrough": "error",
 | 
			
		||||
    "no-floating-decimal": "off",
 | 
			
		||||
    "no-func-assign": "error",
 | 
			
		||||
    "no-global-assign": "error",
 | 
			
		||||
    "no-implicit-coercion": "off",
 | 
			
		||||
    "no-implicit-globals": "off",
 | 
			
		||||
    "no-implied-eval": "error",
 | 
			
		||||
    "no-inline-comments": "off",
 | 
			
		||||
    "no-inner-declarations": "error",
 | 
			
		||||
    "no-invalid-regexp": "error",
 | 
			
		||||
    "no-invalid-this": "error",
 | 
			
		||||
    "no-irregular-whitespace": [
 | 
			
		||||
      "warn",
 | 
			
		||||
      {
 | 
			
		||||
        "skipStrings": true,
 | 
			
		||||
        "skipTemplates": true
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "no-iterator": "off",
 | 
			
		||||
    "no-label-var": "off",
 | 
			
		||||
    "no-labels": "off",
 | 
			
		||||
    "no-lone-blocks": "warn",
 | 
			
		||||
    "no-lonely-if": "warn",
 | 
			
		||||
    "no-loop-func": "warn",
 | 
			
		||||
    "no-magic-numbers": "off",
 | 
			
		||||
    "no-mixed-operators": "off",
 | 
			
		||||
    "no-mixed-requires": "off",
 | 
			
		||||
    "no-mixed-spaces-and-tabs": "error",
 | 
			
		||||
    "no-multi-assign": "off",
 | 
			
		||||
    "no-multi-spaces": "error",
 | 
			
		||||
    "no-multi-str": "off",
 | 
			
		||||
    "no-multiple-empty-lines": "warn",
 | 
			
		||||
    "no-native-reassign": "off",
 | 
			
		||||
    "no-negated-condition": "warn",
 | 
			
		||||
    "no-negated-in-lhs": "off",
 | 
			
		||||
    "no-nested-ternary": "off",
 | 
			
		||||
    "no-new": "off",
 | 
			
		||||
    "no-new-func": "off",
 | 
			
		||||
    "no-new-object": "off",
 | 
			
		||||
    "no-new-require": "off",
 | 
			
		||||
    "no-new-symbol": "error",
 | 
			
		||||
    "no-new-wrappers": "off",
 | 
			
		||||
    "no-obj-calls": "error",
 | 
			
		||||
    "no-octal": "error",
 | 
			
		||||
    "no-octal-escape": "off",
 | 
			
		||||
    "no-param-reassign": "off",
 | 
			
		||||
    "no-path-concat": "warn",
 | 
			
		||||
    "no-plusplus": "off",
 | 
			
		||||
    "no-process-env": "off",
 | 
			
		||||
    "no-process-exit": "off",
 | 
			
		||||
    "no-proto": "off",
 | 
			
		||||
    "no-prototype-builtins": "off",
 | 
			
		||||
    "no-redeclare": "error",
 | 
			
		||||
    "no-regex-spaces": "error",
 | 
			
		||||
    "no-restricted-globals": "off",
 | 
			
		||||
    "no-restricted-imports": "off",
 | 
			
		||||
    "no-restricted-modules": "off",
 | 
			
		||||
    "no-restricted-properties": "off",
 | 
			
		||||
    "no-restricted-syntax": "off",
 | 
			
		||||
    "no-return-assign": "off",
 | 
			
		||||
    "no-return-await": "warn",
 | 
			
		||||
    "no-script-url": "error",
 | 
			
		||||
    "no-self-assign": "error",
 | 
			
		||||
    "no-self-compare": "error",
 | 
			
		||||
    "no-sequences": "warn",
 | 
			
		||||
    "no-shadow": "warn",
 | 
			
		||||
    "no-shadow-restricted-names": "warn",
 | 
			
		||||
    "no-whitespace-before-property": "error",
 | 
			
		||||
    "no-spaced-func": "off",
 | 
			
		||||
    "no-sparse-arrays": "error",
 | 
			
		||||
    "no-sync": "off",
 | 
			
		||||
    "no-tabs": "error",
 | 
			
		||||
    "no-ternary": "off",
 | 
			
		||||
    "no-trailing-spaces": "error",
 | 
			
		||||
    "no-this-before-super": "error",
 | 
			
		||||
    "no-throw-literal": "warn",
 | 
			
		||||
    "no-undef": "error",
 | 
			
		||||
    "no-undef-init": "warn",
 | 
			
		||||
    "no-undefined": "error",
 | 
			
		||||
    "no-unexpected-multiline": "error",
 | 
			
		||||
    "no-underscore-dangle": "off",
 | 
			
		||||
    "no-unmodified-loop-condition": "off",
 | 
			
		||||
    "no-unneeded-ternary": "error",
 | 
			
		||||
    "no-unreachable": "error",
 | 
			
		||||
    "no-unsafe-finally": "error",
 | 
			
		||||
    "no-unsafe-negation": "error",
 | 
			
		||||
    "no-unused-expressions": [
 | 
			
		||||
      "error",
 | 
			
		||||
      {
 | 
			
		||||
        "allowShortCircuit": true,
 | 
			
		||||
        "allowTernary": true
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "no-unused-labels": "error",
 | 
			
		||||
    "no-unused-vars": "error",
 | 
			
		||||
    "no-use-before-define": "off",
 | 
			
		||||
    "no-useless-call": "error",
 | 
			
		||||
    "no-useless-computed-key": "off",
 | 
			
		||||
    "no-useless-concat": "off",
 | 
			
		||||
    "no-useless-constructor": "error",
 | 
			
		||||
    "no-useless-escape": "warn",
 | 
			
		||||
    "no-useless-rename": "error",
 | 
			
		||||
    "no-useless-return": "error",
 | 
			
		||||
    "no-void": "off",
 | 
			
		||||
    "no-var": "off",
 | 
			
		||||
    "no-warning-comments": [
 | 
			
		||||
      "warn",
 | 
			
		||||
      {
 | 
			
		||||
        "terms": ["todo", "fixme", "xxx"],
 | 
			
		||||
        "location": "start"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "no-with": "off",
 | 
			
		||||
    "array-bracket-spacing": ["error", "never"],
 | 
			
		||||
    "array-callback-return": "off",
 | 
			
		||||
    "arrow-body-style": ["error", "as-needed"],
 | 
			
		||||
    "arrow-parens": ["error", "as-needed"],
 | 
			
		||||
    "arrow-spacing": "error",
 | 
			
		||||
    "accessor-pairs": "off",
 | 
			
		||||
    "block-scoped-var": "warn",
 | 
			
		||||
    "block-spacing": "error",
 | 
			
		||||
    "brace-style": ["error", "1tbs"],
 | 
			
		||||
    "callback-return": "off",
 | 
			
		||||
    "camelcase": [
 | 
			
		||||
      "warn",
 | 
			
		||||
      {
 | 
			
		||||
        "properties": "never"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "capitalized-comments": "off",
 | 
			
		||||
    "class-methods-use-this": "off",
 | 
			
		||||
    "comma-dangle": "off",
 | 
			
		||||
    "comma-spacing": "error",
 | 
			
		||||
    "comma-style": ["error", "last"],
 | 
			
		||||
    "complexity": "off",
 | 
			
		||||
    "computed-property-spacing": ["error", "never"],
 | 
			
		||||
    "consistent-return": "off",
 | 
			
		||||
    "consistent-this": "off",
 | 
			
		||||
    "constructor-super": "error",
 | 
			
		||||
    "curly": "error",
 | 
			
		||||
    "default-case": "off",
 | 
			
		||||
    "dot-location": ["warn", "property"],
 | 
			
		||||
    "dot-notation": "error",
 | 
			
		||||
    "eol-last": "error",
 | 
			
		||||
    "eqeqeq": ["error", "smart"],
 | 
			
		||||
    "func-call-spacing": "error",
 | 
			
		||||
    "func-names": "off",
 | 
			
		||||
    "func-name-matching": "off",
 | 
			
		||||
    "func-style": "off",
 | 
			
		||||
    "generator-star-spacing": "error",
 | 
			
		||||
    "global-require": "off",
 | 
			
		||||
    "guard-for-in": "warn",
 | 
			
		||||
    "handle-callback-err": "off",
 | 
			
		||||
    "id-blacklist": "off",
 | 
			
		||||
    "id-length": "off",
 | 
			
		||||
    "id-match": "off",
 | 
			
		||||
    "indent": "off",
 | 
			
		||||
    "init-declarations": "off",
 | 
			
		||||
    "jsx-quotes": "error",
 | 
			
		||||
    "key-spacing": "error",
 | 
			
		||||
    "keyword-spacing": "error",
 | 
			
		||||
    "linebreak-style": "error",
 | 
			
		||||
    "line-comment-position": "off",
 | 
			
		||||
    "lines-around-comment": "off",
 | 
			
		||||
    "lines-around-directive": "off",
 | 
			
		||||
    "max-depth": "warn",
 | 
			
		||||
    "max-len": "warn",
 | 
			
		||||
    "max-lines": ["warn", 500],
 | 
			
		||||
    "max-nested-callbacks": "warn",
 | 
			
		||||
    "max-params": "off",
 | 
			
		||||
    "max-statements": "off",
 | 
			
		||||
    "max-statements-per-line": "error",
 | 
			
		||||
    "multiline-ternary": "off",
 | 
			
		||||
    "new-cap": "off",
 | 
			
		||||
    "new-parens": "error",
 | 
			
		||||
    "newline-after-var": "off",
 | 
			
		||||
    "newline-before-return": "off",
 | 
			
		||||
    "newline-per-chained-call": "off",
 | 
			
		||||
    "object-curly-newline": "off",
 | 
			
		||||
    "object-curly-spacing": ["error", "always"],
 | 
			
		||||
    "object-property-newline": [
 | 
			
		||||
      "error",
 | 
			
		||||
      {
 | 
			
		||||
        "allowMultiplePropertiesPerLine": true
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "object-shorthand": "off",
 | 
			
		||||
    "one-var": "off",
 | 
			
		||||
    "one-var-declaration-per-line": "off",
 | 
			
		||||
    "operator-assignment": "warn",
 | 
			
		||||
    "operator-linebreak": "error",
 | 
			
		||||
    "padded-blocks": "off",
 | 
			
		||||
    "prefer-arrow-callback": "off",
 | 
			
		||||
    "prefer-const": "warn",
 | 
			
		||||
    "prefer-destructuring": "warn",
 | 
			
		||||
    "prefer-numeric-literals": "warn",
 | 
			
		||||
    "prefer-promise-reject-errors": "warn",
 | 
			
		||||
    "prefer-reflect": "off",
 | 
			
		||||
    "prefer-rest-params": "error",
 | 
			
		||||
    "prefer-spread": "error",
 | 
			
		||||
    "prefer-template": "warn",
 | 
			
		||||
    "quote-props": ["error", "as-needed"],
 | 
			
		||||
    "quotes": [
 | 
			
		||||
      "warn",
 | 
			
		||||
      "single",
 | 
			
		||||
      {
 | 
			
		||||
        "avoidEscape": true
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "radix": "off",
 | 
			
		||||
    "require-await": "error",
 | 
			
		||||
    "require-jsdoc": "off",
 | 
			
		||||
    "require-yield": "error",
 | 
			
		||||
    "rest-spread-spacing": "error",
 | 
			
		||||
    "semi": ["error", "never"],
 | 
			
		||||
    "semi-spacing": "error",
 | 
			
		||||
    "sort-keys": "off",
 | 
			
		||||
    "sort-imports": "off",
 | 
			
		||||
    "sort-vars": "off",
 | 
			
		||||
    "space-before-blocks": "error",
 | 
			
		||||
    "space-before-function-paren": "off",
 | 
			
		||||
    "space-in-parens": "error",
 | 
			
		||||
    "space-infix-ops": "error",
 | 
			
		||||
    "space-unary-ops": "error",
 | 
			
		||||
    "spaced-comment": "error",
 | 
			
		||||
    "strict": "off",
 | 
			
		||||
    "symbol-description": "off",
 | 
			
		||||
    "template-curly-spacing": "off",
 | 
			
		||||
    "template-tag-spacing": "off",
 | 
			
		||||
    "unicode-bom": "error",
 | 
			
		||||
    "use-isnan": "error",
 | 
			
		||||
    "valid-jsdoc": "off",
 | 
			
		||||
    "valid-typeof": "error",
 | 
			
		||||
    "vars-on-top": "warn",
 | 
			
		||||
    "wrap-iife": "off",
 | 
			
		||||
    "wrap-regex": "off",
 | 
			
		||||
    "no-template-curly-in-string": "warn",
 | 
			
		||||
    "yield-star-spacing": "error",
 | 
			
		||||
    "yoda": "off",
 | 
			
		||||
    "react/display-name": "error",
 | 
			
		||||
    "react/forbid-component-props": "off",
 | 
			
		||||
    "react/forbid-prop-types": "off",
 | 
			
		||||
    "react/no-array-index-key": "error",
 | 
			
		||||
    "react/no-children-prop": "error",
 | 
			
		||||
    "react/no-danger": "warn",
 | 
			
		||||
    "react/no-danger-with-children": "error",
 | 
			
		||||
    "react/no-deprecated": "error",
 | 
			
		||||
    "react/no-did-mount-set-state": "error",
 | 
			
		||||
    "react/no-did-update-set-state": "error",
 | 
			
		||||
    "react/no-direct-mutation-state": "error",
 | 
			
		||||
    "react/no-find-dom-node": "error",
 | 
			
		||||
    "react/no-is-mounted": "error",
 | 
			
		||||
    "react/no-multi-comp": "error",
 | 
			
		||||
    "react/no-render-return-value": "error",
 | 
			
		||||
    "react/no-set-state": "off",
 | 
			
		||||
    "react/no-string-refs": "error",
 | 
			
		||||
    "react/no-unescaped-entities": "error",
 | 
			
		||||
    "react/no-unknown-property": "error",
 | 
			
		||||
    "react/no-unused-prop-types": "off",
 | 
			
		||||
    "react/prefer-es6-class": "error",
 | 
			
		||||
    "react/prefer-stateless-function": [1, { "ignorePureComponents": true }],
 | 
			
		||||
    "react/prop-types": "off",
 | 
			
		||||
    "react/react-in-jsx-scope": "error",
 | 
			
		||||
    "react/react-default-props": "off",
 | 
			
		||||
    "react/require-optimization": "off",
 | 
			
		||||
    "react/require-render-return": "error",
 | 
			
		||||
    "react/self-closing-comp": "error",
 | 
			
		||||
    "react/sort-comp": "error",
 | 
			
		||||
    "react/sort-prop-types": "error",
 | 
			
		||||
    "react/style-prop-object": "off",
 | 
			
		||||
    "react/jsx-boolean-value": ["error", "never"],
 | 
			
		||||
    "react/jsx-closing-bracket-location": "error",
 | 
			
		||||
    "react/jsx-curly-spacing": ["error", "never"],
 | 
			
		||||
    "react/jsx-equals-spacing": "error",
 | 
			
		||||
    "react/jsx-filename-extension": "error",
 | 
			
		||||
    "react/jsx-first-prop-new-line": ["error", "multiline-multiprop"],
 | 
			
		||||
    "react/jsx-handler-names": "error",
 | 
			
		||||
    "react/jsx-indent": "off",
 | 
			
		||||
    "react/jsx-indent-props": "off",
 | 
			
		||||
    "react/jsx-key": "error",
 | 
			
		||||
    "react/jsx-max-props-per-line": "off",
 | 
			
		||||
    "react/jsx-no-bind": "off",
 | 
			
		||||
    "react/jsx-no-comment-textnodes": "error",
 | 
			
		||||
    "react/jsx-no-duplicate-props": "error",
 | 
			
		||||
    "react/jsx-no-literals": "off",
 | 
			
		||||
    "react/jsx-no-target-blank": "error",
 | 
			
		||||
    "react/jsx-no-undef": "error",
 | 
			
		||||
    "react/jsx-pascal-case": [
 | 
			
		||||
      "error",
 | 
			
		||||
      {
 | 
			
		||||
        "ignore": ["_"]
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "react/jsx-sort-props": "off",
 | 
			
		||||
    "react/jsx-tag-spacing": [
 | 
			
		||||
      "error",
 | 
			
		||||
      {
 | 
			
		||||
        "beforeSelfClosing": "always"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "react/jsx-uses-react": "error",
 | 
			
		||||
    "react/jsx-uses-vars": "error",
 | 
			
		||||
    "react/jsx-wrap-multilines": "error",
 | 
			
		||||
    "import/no-unresolved": "error",
 | 
			
		||||
    "import/named": "error",
 | 
			
		||||
    "import/default": "error",
 | 
			
		||||
    "import/namespace": "error",
 | 
			
		||||
    "import/no-restricted-paths": "off",
 | 
			
		||||
    "import/no-absolute-path": "error",
 | 
			
		||||
    "import/no-dynamic-require": "off",
 | 
			
		||||
    "import/no-internal-modules": "off",
 | 
			
		||||
    "import/no-webpack-loader-syntax": "error",
 | 
			
		||||
    "import/export": "error",
 | 
			
		||||
    "import/no-named-as-default": "warn",
 | 
			
		||||
    "import/no-named-as-default-member": "warn",
 | 
			
		||||
    "import/no-deprecated": "warn",
 | 
			
		||||
    "import/no-extraneous-dependencies": [
 | 
			
		||||
      "warn",
 | 
			
		||||
      {
 | 
			
		||||
        "devDependencies": true,
 | 
			
		||||
        "packageDir": "."
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "import/no-mutable-exports": "warn",
 | 
			
		||||
    "import/unambiguous": "off",
 | 
			
		||||
    "import/no-commonjs": "off",
 | 
			
		||||
    "import/no-amd": "error",
 | 
			
		||||
    "import/no-nodejs-modules": "off",
 | 
			
		||||
    "import/first": "warn",
 | 
			
		||||
    "import/no-duplicates": "error",
 | 
			
		||||
    "import/no-namespace": "off",
 | 
			
		||||
    "import/extensions": "warn",
 | 
			
		||||
    "import/order": [
 | 
			
		||||
      "error",
 | 
			
		||||
      {
 | 
			
		||||
        "newlines-between": "always",
 | 
			
		||||
        "groups": ["builtin", "external", ["parent", "sibling", "index"]]
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "import/newline-after-import": [
 | 
			
		||||
      "error",
 | 
			
		||||
      {
 | 
			
		||||
        "count": 1
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "import/prefer-default-export": "off",
 | 
			
		||||
    "import/max-dependencies": "off",
 | 
			
		||||
    "import/no-unassigned-import": "off",
 | 
			
		||||
    "import/no-named-default": "warn",
 | 
			
		||||
    "import/no-anonymous-default-export": "off"
 | 
			
		||||
  },
 | 
			
		||||
  "plugins": ["react", "import", "prettier"],
 | 
			
		||||
  "settings": {
 | 
			
		||||
    "import/resolver": {
 | 
			
		||||
      "node": {
 | 
			
		||||
        "extensions": [".js", ".jsx"]
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "react": {
 | 
			
		||||
      "createClass": "createReactClass",
 | 
			
		||||
      "pragma": "React",
 | 
			
		||||
      "version": "16.13"
 | 
			
		||||
    },
 | 
			
		||||
    "propWrapperFunctions": [
 | 
			
		||||
        "forbidExtraProps",
 | 
			
		||||
        {"property": "freeze", "object": "Object"},
 | 
			
		||||
        {"property": "myFavoriteWrapper"}
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "tabWidth": 2,
 | 
			
		||||
  "useTabs": false,
 | 
			
		||||
  "semi": false,
 | 
			
		||||
  "singleQuote": true,
 | 
			
		||||
  "arrowParens": "avoid",
 | 
			
		||||
  "trailingComma": "es5",
 | 
			
		||||
  "bracketSpacing": true,
 | 
			
		||||
  "jsxBracketSameLine": false
 | 
			
		||||
}
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 318 B  | 
| 
		 Before Width: | Height: | Size: 2.9 KiB  | 
| 
		 Before Width: | Height: | Size: 5.4 KiB  | 
| 
		 Before Width: | Height: | Size: 5.5 KiB  | 
| 
		 Before Width: | Height: | Size: 3.0 KiB  | 
| 
		 Before Width: | Height: | Size: 4.3 KiB  | 
| 
		 Before Width: | Height: | Size: 3.0 KiB  | 
| 
		 Before Width: | Height: | Size: 3.5 KiB  | 
| 
		 Before Width: | Height: | Size: 2.1 KiB  | 
| 
		 Before Width: | Height: | Size: 1.9 KiB  | 
| 
		 Before Width: | Height: | Size: 2.5 KiB  | 
| 
		 Before Width: | Height: | Size: 2.0 KiB  | 
| 
		 Before Width: | Height: | Size: 1012 B  | 
| 
		 Before Width: | Height: | Size: 2.6 KiB  | 
| 
		 Before Width: | Height: | Size: 2.4 KiB  | 
| 
		 Before Width: | Height: | Size: 2.9 KiB  | 
| 
		 Before Width: | Height: | Size: 2.7 KiB  | 
| 
		 Before Width: | Height: | Size: 2.7 KiB  | 
| 
		 Before Width: | Height: | Size: 2.7 KiB  | 
| 
		 Before Width: | Height: | Size: 1.6 KiB  | 
| 
		 Before Width: | Height: | Size: 2.5 KiB  | 
@@ -1,62 +0,0 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="utf-8">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
 | 
			
		||||
    <meta name="theme-color" content="#000000">
 | 
			
		||||
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
 | 
			
		||||
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
 | 
			
		||||
    <link
 | 
			
		||||
      rel="stylesheet"
 | 
			
		||||
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
 | 
			
		||||
      integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
 | 
			
		||||
      crossorigin="anonymous"
 | 
			
		||||
    >
 | 
			
		||||
    <link
 | 
			
		||||
      rel="stylesheet"
 | 
			
		||||
      href="https://cdn.jsdelivr.net/npm/fork-awesome@1.1.7/css/fork-awesome.min.css"
 | 
			
		||||
      integrity="sha256-gsmEoJAws/Kd3CjuOQzLie5Q3yshhvmo7YNtBG7aaEY="
 | 
			
		||||
      crossorigin="anonymous"
 | 
			
		||||
    >
 | 
			
		||||
    <link
 | 
			
		||||
      rel="stylesheet"
 | 
			
		||||
      href="https://cdn.jsdelivr.net/foundation-icons/3.0/foundation-icons.min.css"
 | 
			
		||||
    >
 | 
			
		||||
    <link
 | 
			
		||||
      rel="stylesheet"
 | 
			
		||||
      href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"
 | 
			
		||||
      integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
 | 
			
		||||
      crossorigin=""
 | 
			
		||||
    >
 | 
			
		||||
    <title>FitTrackee</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <noscript>
 | 
			
		||||
      You need to enable JavaScript to run this app.
 | 
			
		||||
    </noscript>
 | 
			
		||||
    <div id="root"></div>
 | 
			
		||||
    <script
 | 
			
		||||
      src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
 | 
			
		||||
      integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"
 | 
			
		||||
      crossorigin="anonymous"
 | 
			
		||||
    ></script>
 | 
			
		||||
    <script
 | 
			
		||||
      src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
 | 
			
		||||
      integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
 | 
			
		||||
      crossorigin="anonymous"
 | 
			
		||||
    ></script>
 | 
			
		||||
    <script
 | 
			
		||||
      src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
 | 
			
		||||
      integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
 | 
			
		||||
      crossorigin="anonymous"
 | 
			
		||||
    ></script>
 | 
			
		||||
    <script type="text/javascript">
 | 
			
		||||
      $( document ).ready(function() {
 | 
			
		||||
          $("li.nav-item").click(function(){
 | 
			
		||||
              $("button.navbar-toggler").toggleClass("collapsed");
 | 
			
		||||
              $("#navbarSupportedContent").toggleClass("show");
 | 
			
		||||
          });
 | 
			
		||||
      });
 | 
			
		||||
    </script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
@@ -1,16 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "short_name": "FitTrackee",
 | 
			
		||||
  "name": "Self hosted workout/activity tracker",
 | 
			
		||||
  "icons": [
 | 
			
		||||
    {
 | 
			
		||||
      "src": "favicon.ico",
 | 
			
		||||
      "sizes": "64x64 32x32 24x24 16x16",
 | 
			
		||||
      "type": "image/x-icon"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "start_url": "./index.html",
 | 
			
		||||
  "display": "standalone",
 | 
			
		||||
  "theme_color": "#000000",
 | 
			
		||||
  "background_color": "#ffffff",
 | 
			
		||||
  "version": "0.2.0-beta"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,47 +0,0 @@
 | 
			
		||||
import FitTrackeeGenericApi from '../fitTrackeeApi'
 | 
			
		||||
import { history } from '../index'
 | 
			
		||||
import { generateIds } from '../utils'
 | 
			
		||||
import { emptyMessages, setError } from './index'
 | 
			
		||||
 | 
			
		||||
export const setAppConfig = data => ({
 | 
			
		||||
  type: 'SET_APP_CONFIG',
 | 
			
		||||
  data,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const setAppStats = data => ({
 | 
			
		||||
  type: 'SET_APP_STATS',
 | 
			
		||||
  data,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const SetAppErrors = messages => ({ type: 'APP_ERRORS', messages })
 | 
			
		||||
 | 
			
		||||
export const getAppData = target => dispatch =>
 | 
			
		||||
  FitTrackeeGenericApi.getData(target)
 | 
			
		||||
    .then(ret => {
 | 
			
		||||
      if (ret.status === 'success') {
 | 
			
		||||
        if (target === 'config') {
 | 
			
		||||
          dispatch(setAppConfig(ret.data))
 | 
			
		||||
        } else if (target === 'stats/all') {
 | 
			
		||||
          dispatch(setAppStats(ret.data))
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        dispatch(setError(`application|${ret.message}`))
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => dispatch(setError(`application|${error}`)))
 | 
			
		||||
 | 
			
		||||
export const updateAppConfig = formData => dispatch => {
 | 
			
		||||
  dispatch(emptyMessages())
 | 
			
		||||
  FitTrackeeGenericApi.updateData('config', formData)
 | 
			
		||||
    .then(ret => {
 | 
			
		||||
      if (ret.status === 'success') {
 | 
			
		||||
        dispatch(setAppConfig(ret.data))
 | 
			
		||||
        history.push('/admin/application')
 | 
			
		||||
      } else if (Array.isArray(ret.message)) {
 | 
			
		||||
        dispatch(SetAppErrors(generateIds(ret.message)))
 | 
			
		||||
      } else {
 | 
			
		||||
        dispatch(setError(ret.message))
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => dispatch(setError(`application|${error}`)))
 | 
			
		||||
}
 | 
			
		||||
@@ -1,111 +0,0 @@
 | 
			
		||||
import i18next from 'i18next'
 | 
			
		||||
 | 
			
		||||
import FitTrackeeApi from '../fitTrackeeApi/index'
 | 
			
		||||
import { history } from '../index'
 | 
			
		||||
 | 
			
		||||
export const emptyMessages = () => ({
 | 
			
		||||
  type: 'CLEAN_ALL_MESSAGES',
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const setData = (target, data) => ({
 | 
			
		||||
  type: 'SET_DATA',
 | 
			
		||||
  data,
 | 
			
		||||
  target,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const setPaginatedData = (target, data, pagination) => ({
 | 
			
		||||
  type: 'SET_PAGINATED_DATA',
 | 
			
		||||
  data,
 | 
			
		||||
  pagination,
 | 
			
		||||
  target,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const setError = message => ({
 | 
			
		||||
  type: 'SET_ERROR',
 | 
			
		||||
  message,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const setLanguage = language => ({
 | 
			
		||||
  type: 'SET_LANGUAGE',
 | 
			
		||||
  language,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const setLoading = loading => ({
 | 
			
		||||
  type: 'SET_LOADING',
 | 
			
		||||
  loading,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const updateSportsData = data => ({
 | 
			
		||||
  type: 'UPDATE_SPORT_DATA',
 | 
			
		||||
  data,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const updateUsersData = data => ({
 | 
			
		||||
  type: 'UPDATE_USER_DATA',
 | 
			
		||||
  data,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const getOrUpdateData =
 | 
			
		||||
  (action, target, data, canDispatch = true) =>
 | 
			
		||||
  dispatch => {
 | 
			
		||||
    dispatch(setLoading(true))
 | 
			
		||||
    if (data && data.id && target !== 'workouts' && isNaN(data.id)) {
 | 
			
		||||
      dispatch(setLoading(false))
 | 
			
		||||
      return dispatch(setError(`${target}|Incorrect id`))
 | 
			
		||||
    }
 | 
			
		||||
    dispatch(emptyMessages())
 | 
			
		||||
    return FitTrackeeApi[action](target, data)
 | 
			
		||||
      .then(ret => {
 | 
			
		||||
        if (ret.status === 'success') {
 | 
			
		||||
          if (canDispatch) {
 | 
			
		||||
            if (target === 'users' && action === 'getData') {
 | 
			
		||||
              return dispatch(
 | 
			
		||||
                setPaginatedData(target, ret.data, ret.pagination)
 | 
			
		||||
              )
 | 
			
		||||
            }
 | 
			
		||||
            dispatch(setData(target, ret.data))
 | 
			
		||||
          } else if (action === 'updateData' && target === 'sports') {
 | 
			
		||||
            dispatch(updateSportsData(ret.data.sports[0]))
 | 
			
		||||
          } else if (action === 'updateData' && target === 'users') {
 | 
			
		||||
            dispatch(updateUsersData(ret.data.users[0]))
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          dispatch(setError(`${target}|${ret.message || ret.status}`))
 | 
			
		||||
        }
 | 
			
		||||
        dispatch(setLoading(false))
 | 
			
		||||
      })
 | 
			
		||||
      .catch(error => {
 | 
			
		||||
        dispatch(setLoading(false))
 | 
			
		||||
        dispatch(setError(`${target}|${error}`))
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
export const addData = (target, data) => dispatch =>
 | 
			
		||||
  FitTrackeeApi.addData(target, data)
 | 
			
		||||
    .then(ret => {
 | 
			
		||||
      if (ret.status === 'created') {
 | 
			
		||||
        history.push(`/admin/${target}`)
 | 
			
		||||
      } else {
 | 
			
		||||
        dispatch(setError(`${target}|${ret.status}`))
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => dispatch(setError(`${target}|${error}`)))
 | 
			
		||||
 | 
			
		||||
export const deleteData = (target, id) => dispatch => {
 | 
			
		||||
  if (isNaN(id)) {
 | 
			
		||||
    return dispatch(setError(target, `${target}|Incorrect id`))
 | 
			
		||||
  }
 | 
			
		||||
  return FitTrackeeApi.deleteData(target, id)
 | 
			
		||||
    .then(ret => {
 | 
			
		||||
      if (ret.status === 204) {
 | 
			
		||||
        history.push(`/admin/${target}`)
 | 
			
		||||
      } else {
 | 
			
		||||
        dispatch(setError(`${target}|${ret.message || ret.status}`))
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => dispatch(setError(`${target}|${error}`)))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const updateLanguage = language => dispatch => {
 | 
			
		||||
  i18next.changeLanguage(language).then(dispatch(setLanguage(language)))
 | 
			
		||||
}
 | 
			
		||||
@@ -1,13 +0,0 @@
 | 
			
		||||
import FitTrackeeGenericApi from '../fitTrackeeApi'
 | 
			
		||||
import { setData, setError } from './index'
 | 
			
		||||
 | 
			
		||||
export const getStats = (userName, type, data) => dispatch =>
 | 
			
		||||
  FitTrackeeGenericApi.getData(`stats/${userName}/${type}`, data)
 | 
			
		||||
    .then(ret => {
 | 
			
		||||
      if (ret.status === 'success') {
 | 
			
		||||
        dispatch(setData('statistics', ret.data))
 | 
			
		||||
      } else {
 | 
			
		||||
        dispatch(setError(`statistics|${ret.message}`))
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => dispatch(setError(`statistics|${error}`)))
 | 
			
		||||
@@ -1,174 +0,0 @@
 | 
			
		||||
import FitTrackeeGenericApi from '../fitTrackeeApi'
 | 
			
		||||
import FitTrackeeApi from '../fitTrackeeApi/auth'
 | 
			
		||||
import { history } from '../index'
 | 
			
		||||
import { generateIds } from '../utils'
 | 
			
		||||
import { getOrUpdateData, setError, updateLanguage } from './index'
 | 
			
		||||
import { getAppData } from './application'
 | 
			
		||||
 | 
			
		||||
const AuthError = message => ({ type: 'AUTH_ERROR', message })
 | 
			
		||||
 | 
			
		||||
const AuthErrors = messages => ({ type: 'AUTH_ERRORS', messages })
 | 
			
		||||
 | 
			
		||||
const PictureError = message => ({ type: 'PICTURE_ERROR', message })
 | 
			
		||||
 | 
			
		||||
const ProfileSuccess = profil => ({ type: 'PROFILE_SUCCESS', profil })
 | 
			
		||||
 | 
			
		||||
const ProfileError = message => ({ type: 'PROFILE_ERROR', message })
 | 
			
		||||
 | 
			
		||||
const ProfileUpdateError = message => ({
 | 
			
		||||
  type: 'PROFILE_UPDATE_ERROR',
 | 
			
		||||
  message,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const logout = () => ({ type: 'LOGOUT' })
 | 
			
		||||
 | 
			
		||||
export const loadProfile = () => dispatch => {
 | 
			
		||||
  if (window.localStorage.getItem('authToken')) {
 | 
			
		||||
    return dispatch(getProfile())
 | 
			
		||||
  }
 | 
			
		||||
  return { type: 'LOGOUT' }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getProfile = () => dispatch =>
 | 
			
		||||
  FitTrackeeGenericApi.getData('auth/profile')
 | 
			
		||||
    .then(ret => {
 | 
			
		||||
      if (ret.status === 'success') {
 | 
			
		||||
        dispatch(getOrUpdateData('getData', 'sports'))
 | 
			
		||||
        ret.data.isAuthenticated = true
 | 
			
		||||
        if (ret.data.language) {
 | 
			
		||||
          dispatch(updateLanguage(ret.data.language))
 | 
			
		||||
        }
 | 
			
		||||
        return dispatch(ProfileSuccess(ret.data))
 | 
			
		||||
      }
 | 
			
		||||
      return dispatch(ProfileError(ret.message))
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => {
 | 
			
		||||
      throw error
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
export const loginOrRegisterOrPasswordReset = (target, formData) => dispatch =>
 | 
			
		||||
  FitTrackeeApi.loginOrRegisterOrPasswordReset(target, formData)
 | 
			
		||||
    .then(ret => {
 | 
			
		||||
      if (ret.status === 'success') {
 | 
			
		||||
        if (target === 'password/reset-request') {
 | 
			
		||||
          return history.push({
 | 
			
		||||
            pathname: '/password-reset/sent',
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
        if (target === 'password/update') {
 | 
			
		||||
          return history.push({
 | 
			
		||||
            pathname: '/updated-password',
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
        if (target === 'login' || target === 'register') {
 | 
			
		||||
          window.localStorage.setItem('authToken', ret.auth_token)
 | 
			
		||||
          if (target === 'register') {
 | 
			
		||||
            dispatch(getAppData('config'))
 | 
			
		||||
          }
 | 
			
		||||
          return dispatch(getProfile())
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return dispatch(AuthError(ret.message))
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => {
 | 
			
		||||
      throw error
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
const RegisterFormControl = (formData, onlyPasswords = false) => {
 | 
			
		||||
  const errMsg = []
 | 
			
		||||
  if (
 | 
			
		||||
    !onlyPasswords &&
 | 
			
		||||
    (formData.username.length < 3 || formData.username.length > 12)
 | 
			
		||||
  ) {
 | 
			
		||||
    errMsg.push('3 to 12 characters required for username.')
 | 
			
		||||
  }
 | 
			
		||||
  if (formData.password !== formData.password_conf) {
 | 
			
		||||
    errMsg.push("Password and password confirmation don't match.")
 | 
			
		||||
  }
 | 
			
		||||
  if (formData.password.length < 8) {
 | 
			
		||||
    errMsg.push('8 characters required for password.')
 | 
			
		||||
  }
 | 
			
		||||
  return errMsg
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const handleUserFormSubmit = (formData, formType) => dispatch => {
 | 
			
		||||
  if (formType === 'register' || formType === 'password/update') {
 | 
			
		||||
    const ret = RegisterFormControl(formData, formType === 'password/update')
 | 
			
		||||
    if (ret.length > 0) {
 | 
			
		||||
      return dispatch(AuthErrors(generateIds(ret)))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return dispatch(loginOrRegisterOrPasswordReset(formType, formData))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const handleProfileFormSubmit = formData => dispatch => {
 | 
			
		||||
  if (!formData.password === formData.password_conf) {
 | 
			
		||||
    return dispatch(
 | 
			
		||||
      ProfileUpdateError("Password and password confirmation don't match.")
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  delete formData.id
 | 
			
		||||
  return FitTrackeeGenericApi.postData('auth/profile/edit', formData)
 | 
			
		||||
    .then(ret => {
 | 
			
		||||
      if (ret.status === 'success') {
 | 
			
		||||
        dispatch(getProfile())
 | 
			
		||||
        return history.push('/profile')
 | 
			
		||||
      }
 | 
			
		||||
      dispatch(ProfileUpdateError(ret.message))
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => {
 | 
			
		||||
      throw error
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const uploadPicture = event => dispatch => {
 | 
			
		||||
  event.preventDefault()
 | 
			
		||||
  const form = new FormData()
 | 
			
		||||
  form.append('file', event.target.picture.files[0])
 | 
			
		||||
  event.target.reset()
 | 
			
		||||
  return FitTrackeeGenericApi.addDataWithFile('auth/picture', form)
 | 
			
		||||
    .then(ret => {
 | 
			
		||||
      if (ret.status === 'success') {
 | 
			
		||||
        return dispatch(getProfile())
 | 
			
		||||
      }
 | 
			
		||||
      const msg =
 | 
			
		||||
        ret.status === 413
 | 
			
		||||
          ? 'Error during picture update, file size exceeds max size.'
 | 
			
		||||
          : ret.message
 | 
			
		||||
      return dispatch(PictureError(msg))
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => {
 | 
			
		||||
      throw error
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const deletePicture = () => dispatch =>
 | 
			
		||||
  FitTrackeeApi.deletePicture()
 | 
			
		||||
    .then(ret => {
 | 
			
		||||
      if (ret.status === 204) {
 | 
			
		||||
        return dispatch(getProfile())
 | 
			
		||||
      }
 | 
			
		||||
      return dispatch(PictureError(ret.message))
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => {
 | 
			
		||||
      throw error
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
export const deleteUser =
 | 
			
		||||
  (username, isAdmin = false) =>
 | 
			
		||||
  dispatch =>
 | 
			
		||||
    FitTrackeeGenericApi.deleteData('users', username)
 | 
			
		||||
      .then(ret => {
 | 
			
		||||
        if (ret.status === 204) {
 | 
			
		||||
          dispatch(getAppData('config'))
 | 
			
		||||
          if (isAdmin) {
 | 
			
		||||
            history.push('/admin/users')
 | 
			
		||||
          } else {
 | 
			
		||||
            dispatch(logout())
 | 
			
		||||
            history.push('/')
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          ret.json().then(r => dispatch(setError(`${r.message}`)))
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch(error => dispatch(setError(`user|${error}`)))
 | 
			
		||||
@@ -1,192 +0,0 @@
 | 
			
		||||
import FitTrackeeGenericApi from '../fitTrackeeApi'
 | 
			
		||||
import { history } from '../index'
 | 
			
		||||
import { formatChartData } from '../utils/workouts'
 | 
			
		||||
import { setError, setLoading } from './index'
 | 
			
		||||
import { loadProfile } from './user'
 | 
			
		||||
 | 
			
		||||
export const pushWorkouts = workouts => ({
 | 
			
		||||
  type: 'PUSH_WORKOUTS',
 | 
			
		||||
  workouts,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const removeWorkout = workoutId => ({
 | 
			
		||||
  type: 'REMOVE_WORKOUT',
 | 
			
		||||
  workoutId,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const updateCalendar = workouts => ({
 | 
			
		||||
  type: 'UPDATE_CALENDAR',
 | 
			
		||||
  workouts,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const setGpx = gpxContent => ({
 | 
			
		||||
  type: 'SET_GPX',
 | 
			
		||||
  gpxContent,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const setChartData = chartData => ({
 | 
			
		||||
  type: 'SET_CHART_DATA',
 | 
			
		||||
  chartData,
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const addWorkout = form => dispatch =>
 | 
			
		||||
  FitTrackeeGenericApi.addDataWithFile('workouts', form)
 | 
			
		||||
    .then(ret => {
 | 
			
		||||
      if (ret.status === 'created') {
 | 
			
		||||
        if (ret.data.workouts.length === 0) {
 | 
			
		||||
          dispatch(setError('workouts|no correct file.'))
 | 
			
		||||
        } else if (ret.data.workouts.length === 1) {
 | 
			
		||||
          dispatch(loadProfile())
 | 
			
		||||
          history.push(`/workouts/${ret.data.workouts[0].id}`)
 | 
			
		||||
        } else {
 | 
			
		||||
          // ret.data.workouts.length > 1
 | 
			
		||||
          dispatch(loadProfile())
 | 
			
		||||
          history.push('/')
 | 
			
		||||
        }
 | 
			
		||||
      } else if (ret.status === 413) {
 | 
			
		||||
        dispatch(
 | 
			
		||||
          setError('workouts|File size is greater than the allowed size')
 | 
			
		||||
        )
 | 
			
		||||
      } else {
 | 
			
		||||
        dispatch(setError(`workouts|${ret.message}`))
 | 
			
		||||
      }
 | 
			
		||||
      dispatch(setLoading(false))
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => {
 | 
			
		||||
      dispatch(setLoading(false))
 | 
			
		||||
      dispatch(setError(`workouts|${error}`))
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
export const addWorkoutWithoutGpx = form => dispatch =>
 | 
			
		||||
  FitTrackeeGenericApi.addData('workouts/no_gpx', form)
 | 
			
		||||
    .then(ret => {
 | 
			
		||||
      if (ret.status === 'created') {
 | 
			
		||||
        dispatch(loadProfile())
 | 
			
		||||
        history.push(`/workouts/${ret.data.workouts[0].id}`)
 | 
			
		||||
      } else {
 | 
			
		||||
        dispatch(setError(`workouts|${ret.message}`))
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => dispatch(setError(`workouts|${error}`)))
 | 
			
		||||
 | 
			
		||||
export const getWorkoutGpx = workoutId => dispatch => {
 | 
			
		||||
  if (workoutId) {
 | 
			
		||||
    return FitTrackeeGenericApi.getData(`workouts/${workoutId}/gpx`)
 | 
			
		||||
      .then(ret => {
 | 
			
		||||
        if (ret.status === 'success') {
 | 
			
		||||
          dispatch(setGpx(ret.data.gpx))
 | 
			
		||||
        } else {
 | 
			
		||||
          dispatch(setError(`workouts|${ret.message}`))
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch(error => dispatch(setError(`workouts|${error}`)))
 | 
			
		||||
  }
 | 
			
		||||
  dispatch(setGpx(null))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getSegmentGpx = (workoutId, segmentId) => dispatch => {
 | 
			
		||||
  if (workoutId) {
 | 
			
		||||
    return FitTrackeeGenericApi.getData(
 | 
			
		||||
      `workouts/${workoutId}/gpx/segment/${segmentId}`
 | 
			
		||||
    )
 | 
			
		||||
      .then(ret => {
 | 
			
		||||
        if (ret.status === 'success') {
 | 
			
		||||
          dispatch(setGpx(ret.data.gpx))
 | 
			
		||||
        } else {
 | 
			
		||||
          dispatch(setError(`workouts|${ret.message}`))
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch(error => dispatch(setError(`workouts|${error}`)))
 | 
			
		||||
  }
 | 
			
		||||
  dispatch(setGpx(null))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getWorkoutChartData = workoutId => dispatch => {
 | 
			
		||||
  if (workoutId) {
 | 
			
		||||
    return FitTrackeeGenericApi.getData(`workouts/${workoutId}/chart_data`)
 | 
			
		||||
      .then(ret => {
 | 
			
		||||
        if (ret.status === 'success') {
 | 
			
		||||
          dispatch(setChartData(formatChartData(ret.data.chart_data)))
 | 
			
		||||
        } else {
 | 
			
		||||
          dispatch(setError(`workouts|${ret.message}`))
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch(error => dispatch(setError(`workouts|${error}`)))
 | 
			
		||||
  }
 | 
			
		||||
  dispatch(setChartData(null))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getSegmentChartData = (workoutId, segmentId) => dispatch => {
 | 
			
		||||
  if (workoutId) {
 | 
			
		||||
    return FitTrackeeGenericApi.getData(
 | 
			
		||||
      `workouts/${workoutId}/chart_data/segment/${segmentId}`
 | 
			
		||||
    )
 | 
			
		||||
      .then(ret => {
 | 
			
		||||
        if (ret.status === 'success') {
 | 
			
		||||
          dispatch(setChartData(formatChartData(ret.data.chart_data)))
 | 
			
		||||
        } else {
 | 
			
		||||
          dispatch(setError(`workouts|${ret.message}`))
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .catch(error => dispatch(setError(`workouts|${error}`)))
 | 
			
		||||
  }
 | 
			
		||||
  dispatch(setChartData(null))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const deleteWorkout = id => dispatch =>
 | 
			
		||||
  FitTrackeeGenericApi.deleteData('workouts', id)
 | 
			
		||||
    .then(ret => {
 | 
			
		||||
      if (ret.status === 204) {
 | 
			
		||||
        Promise.resolve(dispatch(removeWorkout(id)))
 | 
			
		||||
          .then(() => dispatch(loadProfile()))
 | 
			
		||||
          .then(() => history.push('/'))
 | 
			
		||||
      } else {
 | 
			
		||||
        dispatch(setError(`workouts|${ret.status}`))
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => dispatch(setError(`workouts|${error}`)))
 | 
			
		||||
 | 
			
		||||
export const editWorkout = form => dispatch =>
 | 
			
		||||
  FitTrackeeGenericApi.updateData('workouts', form)
 | 
			
		||||
    .then(ret => {
 | 
			
		||||
      if (ret.status === 'success') {
 | 
			
		||||
        dispatch(loadProfile())
 | 
			
		||||
        history.push(`/workouts/${ret.data.workouts[0].id}`)
 | 
			
		||||
      } else {
 | 
			
		||||
        dispatch(setError(`workouts|${ret.message}`))
 | 
			
		||||
      }
 | 
			
		||||
      dispatch(setLoading(false))
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => {
 | 
			
		||||
      dispatch(setLoading(false))
 | 
			
		||||
      dispatch(setError(`workouts|${error}`))
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
export const getMoreWorkouts = params => dispatch =>
 | 
			
		||||
  FitTrackeeGenericApi.getData('workouts', params)
 | 
			
		||||
    .then(ret => {
 | 
			
		||||
      if (ret.status === 'success') {
 | 
			
		||||
        if (ret.data.workouts.length > 0) {
 | 
			
		||||
          dispatch(pushWorkouts(ret.data.workouts))
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        dispatch(setError(`workouts|${ret.message}`))
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => dispatch(setError(`workouts|${error}`)))
 | 
			
		||||
 | 
			
		||||
export const getMonthWorkouts = (from, to) => dispatch =>
 | 
			
		||||
  FitTrackeeGenericApi.getData('workouts', {
 | 
			
		||||
    from,
 | 
			
		||||
    to,
 | 
			
		||||
    order: 'desc',
 | 
			
		||||
    per_page: 100,
 | 
			
		||||
  })
 | 
			
		||||
    .then(ret => {
 | 
			
		||||
      if (ret.status === 'success') {
 | 
			
		||||
        dispatch(updateCalendar(ret.data.workouts))
 | 
			
		||||
      } else {
 | 
			
		||||
        dispatch(setError(`workouts|${ret.message}`))
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    .catch(error => dispatch(setError(`workouts|${error}`)))
 | 
			
		||||
@@ -1,228 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import Message from '../Common/Message'
 | 
			
		||||
import { getAppData, updateAppConfig } from '../../actions/application'
 | 
			
		||||
import { history } from '../../index'
 | 
			
		||||
import { getFileSizeInMB } from '../../utils'
 | 
			
		||||
 | 
			
		||||
class AdminApplication extends React.Component {
 | 
			
		||||
  constructor(props, context) {
 | 
			
		||||
    super(props, context)
 | 
			
		||||
    this.state = {
 | 
			
		||||
      formData: {},
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.initForm()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate(prevProps) {
 | 
			
		||||
    if (this.props.appConfig !== prevProps.appConfig) {
 | 
			
		||||
      this.initForm()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  initForm() {
 | 
			
		||||
    const { appConfig } = this.props
 | 
			
		||||
    const formData = {}
 | 
			
		||||
    Object.keys(appConfig).map(k =>
 | 
			
		||||
      appConfig[k] === null
 | 
			
		||||
        ? (formData[k] = '')
 | 
			
		||||
        : ['max_single_file_size', 'max_zip_file_size'].includes(k)
 | 
			
		||||
        ? (formData[k] = getFileSizeInMB(appConfig[k]))
 | 
			
		||||
        : (formData[k] = appConfig[k])
 | 
			
		||||
    )
 | 
			
		||||
    this.setState({ formData })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleFormChange(e) {
 | 
			
		||||
    const { formData } = this.state
 | 
			
		||||
    formData[e.target.name] = +e.target.value
 | 
			
		||||
    this.setState(formData)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const {
 | 
			
		||||
      isInEdition,
 | 
			
		||||
      loadAppConfig,
 | 
			
		||||
      message,
 | 
			
		||||
      messages,
 | 
			
		||||
      onHandleConfigFormSubmit,
 | 
			
		||||
      t,
 | 
			
		||||
    } = this.props
 | 
			
		||||
    const { formData } = this.state
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        {(message || messages) && (
 | 
			
		||||
          <Message message={message} messages={messages} t={t} />
 | 
			
		||||
        )}
 | 
			
		||||
        {Object.keys(formData).length > 0 && (
 | 
			
		||||
          <div className="row">
 | 
			
		||||
            <div className="col-md-12">
 | 
			
		||||
              <div className="card">
 | 
			
		||||
                <div className="card-header">
 | 
			
		||||
                  <strong>
 | 
			
		||||
                    {t('administration:Application configuration')}
 | 
			
		||||
                  </strong>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="card-body">
 | 
			
		||||
                  <form
 | 
			
		||||
                    className={`app-config-form ${
 | 
			
		||||
                      isInEdition ? '' : 'form-disabled'
 | 
			
		||||
                    }`}
 | 
			
		||||
                    onSubmit={e => {
 | 
			
		||||
                      e.preventDefault()
 | 
			
		||||
                      onHandleConfigFormSubmit(formData)
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <div className="form-group row">
 | 
			
		||||
                      <label
 | 
			
		||||
                        className="col-sm-6 col-form-label"
 | 
			
		||||
                        htmlFor="max_users"
 | 
			
		||||
                      >
 | 
			
		||||
                        {t(
 | 
			
		||||
                          // eslint-disable-next-line max-len
 | 
			
		||||
                          'administration:Max. number of active users'
 | 
			
		||||
                        )}
 | 
			
		||||
                        <sup>
 | 
			
		||||
                          <i
 | 
			
		||||
                            className="fa fa-question-circle"
 | 
			
		||||
                            aria-hidden="true"
 | 
			
		||||
                            title={t('administration:if 0, no limitation')}
 | 
			
		||||
                          />
 | 
			
		||||
                        </sup>
 | 
			
		||||
                        :
 | 
			
		||||
                      </label>
 | 
			
		||||
                      <input
 | 
			
		||||
                        className="col-sm-5"
 | 
			
		||||
                        id="max_users"
 | 
			
		||||
                        name="max_users"
 | 
			
		||||
                        type="number"
 | 
			
		||||
                        min="0"
 | 
			
		||||
                        value={formData.max_users}
 | 
			
		||||
                        onChange={e => this.handleFormChange(e)}
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="form-group row">
 | 
			
		||||
                      <label
 | 
			
		||||
                        className="col-sm-6 col-form-label"
 | 
			
		||||
                        htmlFor="max_single_file_size"
 | 
			
		||||
                      >
 | 
			
		||||
                        {t(
 | 
			
		||||
                          'administration:Max. size of uploaded files (in Mb)'
 | 
			
		||||
                        )}
 | 
			
		||||
                        :
 | 
			
		||||
                      </label>
 | 
			
		||||
                      <input
 | 
			
		||||
                        className="col-sm-5"
 | 
			
		||||
                        id="max_single_file_size"
 | 
			
		||||
                        name="max_single_file_size"
 | 
			
		||||
                        type="number"
 | 
			
		||||
                        step="0.1"
 | 
			
		||||
                        min="0"
 | 
			
		||||
                        value={formData.max_single_file_size}
 | 
			
		||||
                        onChange={e => this.handleFormChange(e)}
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="form-group row">
 | 
			
		||||
                      <label
 | 
			
		||||
                        className="col-sm-6 col-form-label"
 | 
			
		||||
                        htmlFor="max_zip_file_size"
 | 
			
		||||
                      >
 | 
			
		||||
                        {t('administration:Max. size of zip archive (in Mb)')}:
 | 
			
		||||
                      </label>
 | 
			
		||||
                      <input
 | 
			
		||||
                        className="col-sm-5"
 | 
			
		||||
                        id="max_zip_file_size"
 | 
			
		||||
                        name="max_zip_file_size"
 | 
			
		||||
                        type="number"
 | 
			
		||||
                        step="0.1"
 | 
			
		||||
                        min="0"
 | 
			
		||||
                        value={formData.max_zip_file_size}
 | 
			
		||||
                        onChange={e => this.handleFormChange(e)}
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="form-group row">
 | 
			
		||||
                      <label
 | 
			
		||||
                        className="col-sm-6 col-form-label"
 | 
			
		||||
                        htmlFor="gpx_limit_import"
 | 
			
		||||
                      >
 | 
			
		||||
                        {t('administration:Max. files of zip archive')}
 | 
			
		||||
                      </label>
 | 
			
		||||
                      <input
 | 
			
		||||
                        className="col-sm-5"
 | 
			
		||||
                        id="gpx_limit_import"
 | 
			
		||||
                        name="gpx_limit_import"
 | 
			
		||||
                        type="number"
 | 
			
		||||
                        min="0"
 | 
			
		||||
                        value={formData.gpx_limit_import}
 | 
			
		||||
                        onChange={e => this.handleFormChange(e)}
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    {isInEdition ? (
 | 
			
		||||
                      <>
 | 
			
		||||
                        <input
 | 
			
		||||
                          type="submit"
 | 
			
		||||
                          className="btn btn-primary"
 | 
			
		||||
                          value={t('common:Submit')}
 | 
			
		||||
                        />
 | 
			
		||||
                        <input
 | 
			
		||||
                          type="submit"
 | 
			
		||||
                          className="btn btn-secondary"
 | 
			
		||||
                          onClick={e => {
 | 
			
		||||
                            e.preventDefault()
 | 
			
		||||
                            loadAppConfig()
 | 
			
		||||
                            history.push('/admin/application')
 | 
			
		||||
                          }}
 | 
			
		||||
                          value={t('common:Cancel')}
 | 
			
		||||
                        />
 | 
			
		||||
                      </>
 | 
			
		||||
                    ) : (
 | 
			
		||||
                      <>
 | 
			
		||||
                        <input
 | 
			
		||||
                          type="submit"
 | 
			
		||||
                          className="btn btn-primary"
 | 
			
		||||
                          onClick={e => {
 | 
			
		||||
                            e.preventDefault()
 | 
			
		||||
                            history.push('/admin/application/edit')
 | 
			
		||||
                          }}
 | 
			
		||||
                          value={t('common:Edit')}
 | 
			
		||||
                        />
 | 
			
		||||
                        <input
 | 
			
		||||
                          type="submit"
 | 
			
		||||
                          className="btn btn-secondary"
 | 
			
		||||
                          onClick={() => history.push('/admin')}
 | 
			
		||||
                          value={t('common:Back')}
 | 
			
		||||
                        />
 | 
			
		||||
                      </>
 | 
			
		||||
                    )}
 | 
			
		||||
                  </form>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(
 | 
			
		||||
  state => ({
 | 
			
		||||
    message: state.message,
 | 
			
		||||
    messages: state.messages,
 | 
			
		||||
  }),
 | 
			
		||||
  dispatch => ({
 | 
			
		||||
    loadAppConfig: () => {
 | 
			
		||||
      dispatch(getAppData('config'))
 | 
			
		||||
    },
 | 
			
		||||
    onHandleConfigFormSubmit: formData => {
 | 
			
		||||
      const data = Object.assign({}, formData)
 | 
			
		||||
      data.max_single_file_size *= 1048576
 | 
			
		||||
      data.max_zip_file_size *= 1048576
 | 
			
		||||
      dispatch(updateAppConfig(data))
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
)(AdminApplication)
 | 
			
		||||
@@ -1,71 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Link } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
import AdminStats from './AdminStats'
 | 
			
		||||
 | 
			
		||||
export default function AdminDashboard(props) {
 | 
			
		||||
  const { appConfig, t } = props
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="card workout-card">
 | 
			
		||||
      <div className="card-header">
 | 
			
		||||
        <strong>{t('administration:Administration')}</strong>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="card-body">
 | 
			
		||||
        <AdminStats />
 | 
			
		||||
        <br />
 | 
			
		||||
        <dl className="admin-items">
 | 
			
		||||
          <dt>
 | 
			
		||||
            <Link
 | 
			
		||||
              to={{
 | 
			
		||||
                pathname: '/admin/application',
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {t('administration:Application')}
 | 
			
		||||
            </Link>
 | 
			
		||||
          </dt>
 | 
			
		||||
          <dd>
 | 
			
		||||
            {t(
 | 
			
		||||
              'administration:Update application configuration ' +
 | 
			
		||||
                '(maximum number of registered users, maximum files size).'
 | 
			
		||||
            )}
 | 
			
		||||
            <br />
 | 
			
		||||
            <strong>
 | 
			
		||||
              {t(
 | 
			
		||||
                `administration:Registration is currently ${
 | 
			
		||||
                  appConfig.is_registration_enabled ? 'enabled' : 'disabled'
 | 
			
		||||
                }.`
 | 
			
		||||
              )}
 | 
			
		||||
            </strong>
 | 
			
		||||
          </dd>
 | 
			
		||||
          <br />
 | 
			
		||||
          <dt>
 | 
			
		||||
            <Link
 | 
			
		||||
              to={{
 | 
			
		||||
                pathname: '/admin/sports',
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {t('administration:Sports')}
 | 
			
		||||
            </Link>
 | 
			
		||||
          </dt>
 | 
			
		||||
          <dd>{t('administration:Enable/disable sports.')}</dd>
 | 
			
		||||
          <br />
 | 
			
		||||
          <dt>
 | 
			
		||||
            <Link
 | 
			
		||||
              to={{
 | 
			
		||||
                pathname: '/admin/users',
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {t('administration:Users')}
 | 
			
		||||
            </Link>
 | 
			
		||||
          </dt>
 | 
			
		||||
          <dd>
 | 
			
		||||
            {t(
 | 
			
		||||
              'administration:Add/remove admin rights, ' +
 | 
			
		||||
                'delete user account.'
 | 
			
		||||
            )}
 | 
			
		||||
          </dd>
 | 
			
		||||
        </dl>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,142 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import Message from '../Common/Message'
 | 
			
		||||
import { getOrUpdateData } from '../../actions'
 | 
			
		||||
import { history } from '../../index'
 | 
			
		||||
 | 
			
		||||
class AdminSports extends React.Component {
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.loadSports()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { message, sports, t, updateSport } = this.props
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        {message && <Message message={message} t={t} />}
 | 
			
		||||
        <div className="row">
 | 
			
		||||
          <div className="col">
 | 
			
		||||
            <div className="card">
 | 
			
		||||
              <div className="card-header">
 | 
			
		||||
                <strong>{t('administration:Sports')}</strong>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="card-body">
 | 
			
		||||
                {sports.length > 0 && (
 | 
			
		||||
                  <table className="table">
 | 
			
		||||
                    <thead>
 | 
			
		||||
                      <tr>
 | 
			
		||||
                        <th>{t('administration:id')}</th>
 | 
			
		||||
                        <th>{t('administration:Image')}</th>
 | 
			
		||||
                        <th>{t('administration:Label')}</th>
 | 
			
		||||
                        <th>{t('administration:Active')}</th>
 | 
			
		||||
                        <th>{t('administration:Actions')}</th>
 | 
			
		||||
                      </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
                    <tbody>
 | 
			
		||||
                      {sports.map(sport => (
 | 
			
		||||
                        <tr key={sport.id}>
 | 
			
		||||
                          <td>
 | 
			
		||||
                            <span className="heading-span-absolute">
 | 
			
		||||
                              {t('administration:id')}
 | 
			
		||||
                            </span>
 | 
			
		||||
                            {sport.id}
 | 
			
		||||
                          </td>
 | 
			
		||||
                          <td>
 | 
			
		||||
                            <span className="heading-span-absolute">
 | 
			
		||||
                              {t('administration:Image')}
 | 
			
		||||
                            </span>
 | 
			
		||||
                            <img
 | 
			
		||||
                              className="admin-img"
 | 
			
		||||
                              src={sport.img ? sport.img : '/img/photo.png'}
 | 
			
		||||
                              alt="sport logo"
 | 
			
		||||
                            />
 | 
			
		||||
                          </td>
 | 
			
		||||
                          <td>
 | 
			
		||||
                            <span className="heading-span-absolute">
 | 
			
		||||
                              {t('administration:Label')}
 | 
			
		||||
                            </span>
 | 
			
		||||
                            {t(`sports:${sport.label}`)}
 | 
			
		||||
                          </td>
 | 
			
		||||
                          <td>
 | 
			
		||||
                            <span className="heading-span-absolute">
 | 
			
		||||
                              {t('administration:Active')}
 | 
			
		||||
                            </span>
 | 
			
		||||
                            {sport.is_active ? (
 | 
			
		||||
                              <i
 | 
			
		||||
                                className="fa fa-check-square-o custom-fa"
 | 
			
		||||
                                aria-hidden="true"
 | 
			
		||||
                                data-toggle="tooltip"
 | 
			
		||||
                              />
 | 
			
		||||
                            ) : (
 | 
			
		||||
                              <i
 | 
			
		||||
                                className="fa fa-square-o custom-fa"
 | 
			
		||||
                                aria-hidden="true"
 | 
			
		||||
                                data-toggle="tooltip"
 | 
			
		||||
                              />
 | 
			
		||||
                            )}
 | 
			
		||||
                          </td>
 | 
			
		||||
                          <td>
 | 
			
		||||
                            <span className="heading-span-absolute">
 | 
			
		||||
                              {t('administration:Actions')}
 | 
			
		||||
                            </span>
 | 
			
		||||
                            <input
 | 
			
		||||
                              type="submit"
 | 
			
		||||
                              className={`btn btn-${
 | 
			
		||||
                                sport.is_active ? 'dark' : 'primary'
 | 
			
		||||
                              } btn-sm`}
 | 
			
		||||
                              value={
 | 
			
		||||
                                sport.is_active
 | 
			
		||||
                                  ? t('administration:Disable')
 | 
			
		||||
                                  : t('administration:Enable')
 | 
			
		||||
                              }
 | 
			
		||||
                              onClick={() =>
 | 
			
		||||
                                updateSport(sport.id, !sport.is_active)
 | 
			
		||||
                              }
 | 
			
		||||
                            />
 | 
			
		||||
                            {sport.has_workouts && (
 | 
			
		||||
                              <span className="admin-message">
 | 
			
		||||
                                <i
 | 
			
		||||
                                  className="fa fa-warning custom-fa"
 | 
			
		||||
                                  aria-hidden="true"
 | 
			
		||||
                                />
 | 
			
		||||
                                {t('administration:workouts exist')}
 | 
			
		||||
                              </span>
 | 
			
		||||
                            )}
 | 
			
		||||
                          </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                      ))}
 | 
			
		||||
                    </tbody>
 | 
			
		||||
                  </table>
 | 
			
		||||
                )}
 | 
			
		||||
                <input
 | 
			
		||||
                  type="submit"
 | 
			
		||||
                  className="btn btn-secondary"
 | 
			
		||||
                  onClick={() => history.push('/admin/')}
 | 
			
		||||
                  value={t('common:Back')}
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(
 | 
			
		||||
  state => ({
 | 
			
		||||
    message: state.message,
 | 
			
		||||
    sports: state.sports.data,
 | 
			
		||||
    user: state.user,
 | 
			
		||||
  }),
 | 
			
		||||
  dispatch => ({
 | 
			
		||||
    loadSports: () => {
 | 
			
		||||
      dispatch(getOrUpdateData('getData', 'sports'))
 | 
			
		||||
    },
 | 
			
		||||
    updateSport: (sportId, isActive) => {
 | 
			
		||||
      const data = { id: sportId, is_active: isActive }
 | 
			
		||||
      dispatch(getOrUpdateData('updateData', 'sports', data, false))
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
)(AdminSports)
 | 
			
		||||
@@ -1,104 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { withTranslation } from 'react-i18next'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import { getAppData } from '../../actions/application'
 | 
			
		||||
import { getFileSize } from '../../utils'
 | 
			
		||||
 | 
			
		||||
class AdminStats extends React.Component {
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.loadAppStats()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { appStats, t } = this.props
 | 
			
		||||
    const uploadDirSize = getFileSize(appStats.uploads_dir_size, false)
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="row">
 | 
			
		||||
        <div className="col-lg-3 col-md-6 col-sm-6">
 | 
			
		||||
          <div className="card workout-card">
 | 
			
		||||
            <div className="card-body row">
 | 
			
		||||
              <div className="col-3">
 | 
			
		||||
                <i className="fa fa-users fa-3x fa-color" />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="col-9 text-right">
 | 
			
		||||
                <div className="huge">
 | 
			
		||||
                  {appStats.users ? appStats.users : 0}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div>{`${
 | 
			
		||||
                  appStats.users === 1
 | 
			
		||||
                    ? t('administration:user')
 | 
			
		||||
                    : t('administration:users')
 | 
			
		||||
                }`}</div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="col-lg-3 col-md-6 col-sm-6">
 | 
			
		||||
          <div className="card workout-card">
 | 
			
		||||
            <div className="card-body row">
 | 
			
		||||
              <div className="col-3">
 | 
			
		||||
                <i className="fa fa-tags fa-3x fa-color" />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="col-9 text-right">
 | 
			
		||||
                <div className="huge">
 | 
			
		||||
                  {appStats.sports ? appStats.sports : 0}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div>{`${
 | 
			
		||||
                  appStats.sports === 1 ? t('common:sport') : t('common:sports')
 | 
			
		||||
                }`}</div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="col-lg-3 col-md-6 col-sm-6">
 | 
			
		||||
          <div className="card workout-card">
 | 
			
		||||
            <div className="card-body row">
 | 
			
		||||
              <div className="col-3">
 | 
			
		||||
                <i className="fa fa-calendar fa-3x fa-color" />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="col-9 text-right">
 | 
			
		||||
                <div className="huge">
 | 
			
		||||
                  {appStats.workouts ? appStats.workouts : 0}
 | 
			
		||||
                </div>
 | 
			
		||||
                <div>{`${
 | 
			
		||||
                  appStats.workouts === 1
 | 
			
		||||
                    ? t('common:workout')
 | 
			
		||||
                    : t('common:workouts')
 | 
			
		||||
                }`}</div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="col-lg-3 col-md-6 col-sm-6">
 | 
			
		||||
          <div className="card workout-card">
 | 
			
		||||
            <div className="card-body row">
 | 
			
		||||
              <div className="col-3">
 | 
			
		||||
                <i className="fa fa-folder-open fa-3x fa-color" />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="col-9 text-right">
 | 
			
		||||
                <div className="huge">{uploadDirSize.size}</div>
 | 
			
		||||
                <div>
 | 
			
		||||
                  {uploadDirSize.suffix} ({t('administration:uploads')})
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withTranslation()(
 | 
			
		||||
  connect(
 | 
			
		||||
    state => ({
 | 
			
		||||
      appStats: state.application.statistics,
 | 
			
		||||
    }),
 | 
			
		||||
    dispatch => ({
 | 
			
		||||
      loadAppStats: () => {
 | 
			
		||||
        dispatch(getAppData('stats/all'))
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  )(AdminStats)
 | 
			
		||||
)
 | 
			
		||||
@@ -1,257 +0,0 @@
 | 
			
		||||
import { format } from 'date-fns'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
import { Link } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
import Message from '../Common/Message'
 | 
			
		||||
import Pagination from '../Common/Pagination'
 | 
			
		||||
import { history } from '../../index'
 | 
			
		||||
import { getOrUpdateData } from '../../actions'
 | 
			
		||||
import {
 | 
			
		||||
  apiUrl,
 | 
			
		||||
  formatUrl,
 | 
			
		||||
  sortOrders,
 | 
			
		||||
  translateValues,
 | 
			
		||||
  userFilters,
 | 
			
		||||
} from '../../utils'
 | 
			
		||||
 | 
			
		||||
class AdminUsers extends React.Component {
 | 
			
		||||
  constructor(props, context) {
 | 
			
		||||
    super(props, context)
 | 
			
		||||
    this.state = {
 | 
			
		||||
      page: null,
 | 
			
		||||
      per_page: null,
 | 
			
		||||
      order_by: 'created_at',
 | 
			
		||||
      order: 'asc',
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.loadUsers(this.initState())
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate(prevProps) {
 | 
			
		||||
    if (prevProps.location.query !== this.props.location.query) {
 | 
			
		||||
      this.props.loadUsers(this.props.location.query)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  initState() {
 | 
			
		||||
    const { query } = this.props.location
 | 
			
		||||
    const newQuery = {
 | 
			
		||||
      page: query.page,
 | 
			
		||||
      per_page: query.per_page,
 | 
			
		||||
      order_by: query.order_by ? query.order_by : 'created_at',
 | 
			
		||||
      order: query.order ? query.order : 'asc',
 | 
			
		||||
    }
 | 
			
		||||
    this.setState(newQuery)
 | 
			
		||||
    return newQuery
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updatePage(key, value) {
 | 
			
		||||
    const query = Object.assign({}, this.state)
 | 
			
		||||
    query[key] = value
 | 
			
		||||
    this.setState(query)
 | 
			
		||||
    const url = formatUrl(this.props.location.pathname, query)
 | 
			
		||||
    history.push(url)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { authUser, location, message, t, pagination, updateUser, users } =
 | 
			
		||||
      this.props
 | 
			
		||||
    const translatedFilters = translateValues(t, userFilters)
 | 
			
		||||
    const translatedSortOrders = translateValues(t, sortOrders)
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        {message && <Message message={message} t={t} />}
 | 
			
		||||
        <div className="container">
 | 
			
		||||
          <div className="row">
 | 
			
		||||
            <div className="col">
 | 
			
		||||
              <div className="card">
 | 
			
		||||
                <div className="card-header">
 | 
			
		||||
                  <strong>{t('administration:Users')}</strong>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="card-body">
 | 
			
		||||
                  <div className="row user-filters">
 | 
			
		||||
                    <div className="col-lg-4 col-md-6 col-sm-12">
 | 
			
		||||
                      <label htmlFor="order_by">
 | 
			
		||||
                        {t('common:Sort by')}:{' '}
 | 
			
		||||
                        <select
 | 
			
		||||
                          id="order_by"
 | 
			
		||||
                          name="order_by"
 | 
			
		||||
                          value={this.state.order_by}
 | 
			
		||||
                          onChange={e =>
 | 
			
		||||
                            this.updatePage('order_by', e.target.value)
 | 
			
		||||
                          }
 | 
			
		||||
                        >
 | 
			
		||||
                          {translatedFilters.map(filter => (
 | 
			
		||||
                            <option key={filter.key} value={filter.key}>
 | 
			
		||||
                              {filter.label}
 | 
			
		||||
                            </option>
 | 
			
		||||
                          ))}
 | 
			
		||||
                        </select>{' '}
 | 
			
		||||
                      </label>
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="col-lg-4 col-md-6 col-sm-12">
 | 
			
		||||
                      <label htmlFor="sort">
 | 
			
		||||
                        {t('common:Sort')}:{' '}
 | 
			
		||||
                        <select
 | 
			
		||||
                          id="sort"
 | 
			
		||||
                          name="sort"
 | 
			
		||||
                          value={this.state.order}
 | 
			
		||||
                          onChange={e =>
 | 
			
		||||
                            this.updatePage('order', e.target.value)
 | 
			
		||||
                          }
 | 
			
		||||
                        >
 | 
			
		||||
                          {translatedSortOrders.map(sort => (
 | 
			
		||||
                            <option key={sort.key} value={sort.key}>
 | 
			
		||||
                              {sort.label}
 | 
			
		||||
                            </option>
 | 
			
		||||
                          ))}
 | 
			
		||||
                        </select>{' '}
 | 
			
		||||
                      </label>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <table className="table">
 | 
			
		||||
                    <thead>
 | 
			
		||||
                      <tr>
 | 
			
		||||
                        <th>#</th>
 | 
			
		||||
                        <th>{t('user:Username')}</th>
 | 
			
		||||
                        <th>{t('user:Email')}</th>
 | 
			
		||||
                        <th>{t('user:Registration Date')}</th>
 | 
			
		||||
                        <th>{t('workouts:Workouts')}</th>
 | 
			
		||||
                        <th>{t('user:Admin')}</th>
 | 
			
		||||
                        <th>{t('administration:Actions')}</th>
 | 
			
		||||
                      </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
                    <tbody>
 | 
			
		||||
                      {users.map(user => (
 | 
			
		||||
                        <tr key={user.username}>
 | 
			
		||||
                          <td>
 | 
			
		||||
                            <span className="heading-span-absolute">#</span>
 | 
			
		||||
                            {user.picture === true ? (
 | 
			
		||||
                              <img
 | 
			
		||||
                                alt="Avatar"
 | 
			
		||||
                                src={`${apiUrl}users/${
 | 
			
		||||
                                  user.username
 | 
			
		||||
                                }/picture?${Date.now()}`}
 | 
			
		||||
                                className="img-fluid App-nav-profile-img"
 | 
			
		||||
                              />
 | 
			
		||||
                            ) : (
 | 
			
		||||
                              <i
 | 
			
		||||
                                className="fa fa-user-circle-o fa-2x no-picture"
 | 
			
		||||
                                aria-hidden="true"
 | 
			
		||||
                              />
 | 
			
		||||
                            )}
 | 
			
		||||
                          </td>
 | 
			
		||||
                          <td>
 | 
			
		||||
                            <span className="heading-span-absolute">
 | 
			
		||||
                              {t('user:Username')}
 | 
			
		||||
                            </span>
 | 
			
		||||
                            <Link to={`/users/${user.username}`}>
 | 
			
		||||
                              {user.username}
 | 
			
		||||
                            </Link>
 | 
			
		||||
                          </td>
 | 
			
		||||
                          <td>
 | 
			
		||||
                            <span className="heading-span-absolute">
 | 
			
		||||
                              {t('user:Email')}
 | 
			
		||||
                            </span>
 | 
			
		||||
                            {user.email}
 | 
			
		||||
                          </td>
 | 
			
		||||
                          <td>
 | 
			
		||||
                            <span className="heading-span-absolute">
 | 
			
		||||
                              {t('user:Registration Date')}
 | 
			
		||||
                            </span>
 | 
			
		||||
                            {format(
 | 
			
		||||
                              new Date(user.created_at),
 | 
			
		||||
                              'dd/MM/yyyy HH:mm'
 | 
			
		||||
                            )}
 | 
			
		||||
                          </td>
 | 
			
		||||
                          <td>
 | 
			
		||||
                            <span className="heading-span-absolute">
 | 
			
		||||
                              {t('workouts:Workouts')}
 | 
			
		||||
                            </span>
 | 
			
		||||
                            {user.nb_workouts}
 | 
			
		||||
                          </td>
 | 
			
		||||
                          <td>
 | 
			
		||||
                            <span className="heading-span-absolute">
 | 
			
		||||
                              {t('user:Admin')}
 | 
			
		||||
                            </span>
 | 
			
		||||
                            {user.admin ? (
 | 
			
		||||
                              <i
 | 
			
		||||
                                className="fa fa-check-square-o custom-fa"
 | 
			
		||||
                                aria-hidden="true"
 | 
			
		||||
                                data-toggle="tooltip"
 | 
			
		||||
                              />
 | 
			
		||||
                            ) : (
 | 
			
		||||
                              <i
 | 
			
		||||
                                className="fa fa-square-o custom-fa"
 | 
			
		||||
                                aria-hidden="true"
 | 
			
		||||
                                data-toggle="tooltip"
 | 
			
		||||
                              />
 | 
			
		||||
                            )}
 | 
			
		||||
                          </td>
 | 
			
		||||
                          <td>
 | 
			
		||||
                            <span className="heading-span-absolute">
 | 
			
		||||
                              {t('administration:Actions')}
 | 
			
		||||
                            </span>
 | 
			
		||||
                            <input
 | 
			
		||||
                              type="submit"
 | 
			
		||||
                              className={`btn btn-${
 | 
			
		||||
                                user.admin ? 'dark' : 'primary'
 | 
			
		||||
                              } btn-sm`}
 | 
			
		||||
                              disabled={user.username === authUser.username}
 | 
			
		||||
                              value={
 | 
			
		||||
                                user.admin
 | 
			
		||||
                                  ? t('administration:Remove admin rights')
 | 
			
		||||
                                  : t('administration:Add admin rights')
 | 
			
		||||
                              }
 | 
			
		||||
                              onClick={() =>
 | 
			
		||||
                                updateUser(user.username, !user.admin)
 | 
			
		||||
                              }
 | 
			
		||||
                            />
 | 
			
		||||
                          </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                      ))}
 | 
			
		||||
                    </tbody>
 | 
			
		||||
                  </table>
 | 
			
		||||
                  <Pagination
 | 
			
		||||
                    pagination={pagination}
 | 
			
		||||
                    pathname={location.pathname}
 | 
			
		||||
                    query={this.state}
 | 
			
		||||
                    t={t}
 | 
			
		||||
                  />
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="submit"
 | 
			
		||||
                    className="btn btn-secondary"
 | 
			
		||||
                    onClick={() => history.push('/admin/')}
 | 
			
		||||
                    value={t('common:Back')}
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(
 | 
			
		||||
  state => ({
 | 
			
		||||
    authUser: state.user,
 | 
			
		||||
    location: state.router.location,
 | 
			
		||||
    message: state.message,
 | 
			
		||||
    pagination: state.users.pagination,
 | 
			
		||||
    users: state.users.data,
 | 
			
		||||
  }),
 | 
			
		||||
  dispatch => ({
 | 
			
		||||
    loadUsers: query => {
 | 
			
		||||
      dispatch(getOrUpdateData('getData', 'users', query))
 | 
			
		||||
    },
 | 
			
		||||
    updateUser: (userName, isAdmin) => {
 | 
			
		||||
      const data = { username: userName, admin: isAdmin }
 | 
			
		||||
      dispatch(getOrUpdateData('updateData', 'users', data, false))
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
)(AdminUsers)
 | 
			
		||||
@@ -1,71 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Helmet } from 'react-helmet'
 | 
			
		||||
import { withTranslation } from 'react-i18next'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
import { Route, Switch } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
import AdminApplication from './AdminApplication'
 | 
			
		||||
import AdminDashboard from './AdminDashboard'
 | 
			
		||||
import AdminSports from './AdminSports'
 | 
			
		||||
import AdminUsers from './AdminUsers'
 | 
			
		||||
import NotFound from './../Others/NotFound'
 | 
			
		||||
 | 
			
		||||
function Admin(props) {
 | 
			
		||||
  const { appConfig, t, user } = props
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Helmet>
 | 
			
		||||
        <title>FitTrackee - {t('administration:Administration')}</title>
 | 
			
		||||
      </Helmet>
 | 
			
		||||
      <div className="container dashboard">
 | 
			
		||||
        {user.admin ? (
 | 
			
		||||
          <Switch>
 | 
			
		||||
            <Route
 | 
			
		||||
              exact
 | 
			
		||||
              path="/admin"
 | 
			
		||||
              render={() => <AdminDashboard appConfig={appConfig} t={t} />}
 | 
			
		||||
            />
 | 
			
		||||
            <Route
 | 
			
		||||
              exact
 | 
			
		||||
              path="/admin/application"
 | 
			
		||||
              render={() => (
 | 
			
		||||
                <AdminApplication
 | 
			
		||||
                  appConfig={appConfig}
 | 
			
		||||
                  t={t}
 | 
			
		||||
                  isInEdition={false}
 | 
			
		||||
                />
 | 
			
		||||
              )}
 | 
			
		||||
            />
 | 
			
		||||
            <Route
 | 
			
		||||
              exact
 | 
			
		||||
              path="/admin/application/edit"
 | 
			
		||||
              render={() => (
 | 
			
		||||
                <AdminApplication appConfig={appConfig} t={t} isInEdition />
 | 
			
		||||
              )}
 | 
			
		||||
            />
 | 
			
		||||
            <Route
 | 
			
		||||
              exact
 | 
			
		||||
              path="/admin/sports"
 | 
			
		||||
              render={() => <AdminSports t={t} />}
 | 
			
		||||
            />
 | 
			
		||||
            <Route
 | 
			
		||||
              exact
 | 
			
		||||
              path="/admin/users"
 | 
			
		||||
              render={() => <AdminUsers t={t} />}
 | 
			
		||||
            />
 | 
			
		||||
            <Route component={NotFound} />
 | 
			
		||||
          </Switch>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <NotFound />
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withTranslation()(
 | 
			
		||||
  connect(state => ({
 | 
			
		||||
    appConfig: state.application.config,
 | 
			
		||||
    user: state.user,
 | 
			
		||||
  }))(Admin)
 | 
			
		||||
)
 | 
			
		||||
@@ -1,90 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
import { Route, Switch } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
import './App.css'
 | 
			
		||||
import Admin from './Admin'
 | 
			
		||||
import Workout from './Workout'
 | 
			
		||||
import Workouts from './Workouts'
 | 
			
		||||
import CurrentUserProfile from './User/CurrentUserProfile'
 | 
			
		||||
import Dashboard from './Dashboard'
 | 
			
		||||
import Footer from './Footer'
 | 
			
		||||
import Logout from './User/Logout'
 | 
			
		||||
import NavBar from './NavBar'
 | 
			
		||||
import NotFound from './Others/NotFound'
 | 
			
		||||
import PasswordReset from './User/PasswordReset'
 | 
			
		||||
import ProfileEdit from './User/ProfileEdit'
 | 
			
		||||
import Statistics from './Statistics'
 | 
			
		||||
import UserForm from './User/UserForm'
 | 
			
		||||
import UserProfile from './User/UserProfile'
 | 
			
		||||
import { getAppData } from '../actions/application'
 | 
			
		||||
 | 
			
		||||
class App extends React.Component {
 | 
			
		||||
  constructor(props) {
 | 
			
		||||
    super(props)
 | 
			
		||||
    this.props = props
 | 
			
		||||
  }
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.loadAppConfig()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="App">
 | 
			
		||||
        <NavBar />
 | 
			
		||||
        <Switch>
 | 
			
		||||
          <Route exact path="/" component={Dashboard} />
 | 
			
		||||
          <Route
 | 
			
		||||
            exact
 | 
			
		||||
            path="/register"
 | 
			
		||||
            render={() => <UserForm formType={'register'} />}
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            exact
 | 
			
		||||
            path="/login"
 | 
			
		||||
            render={() => <UserForm formType={'login'} />}
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            exact
 | 
			
		||||
            path="/password-reset"
 | 
			
		||||
            render={() => <UserForm formType={'password reset'} />}
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            exact
 | 
			
		||||
            path="/password-reset/request"
 | 
			
		||||
            render={() => <UserForm formType={'reset your password'} />}
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            exact
 | 
			
		||||
            path="/password-reset/sent"
 | 
			
		||||
            render={() => <PasswordReset action={'sent'} />}
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            exact
 | 
			
		||||
            path="/updated-password"
 | 
			
		||||
            render={() => <PasswordReset action={'updated'} />}
 | 
			
		||||
          />
 | 
			
		||||
          <Route exact path="/password-reset/sent" component={PasswordReset} />
 | 
			
		||||
          <Route exact path="/logout" component={Logout} />
 | 
			
		||||
          <Route exact path="/profile/edit" component={ProfileEdit} />
 | 
			
		||||
          <Route exact path="/profile" component={CurrentUserProfile} />
 | 
			
		||||
          <Route exact path="/workouts/history" component={Workouts} />
 | 
			
		||||
          <Route exact path="/workouts/statistics" component={Statistics} />
 | 
			
		||||
          <Route exact path="/users/:userName" component={UserProfile} />
 | 
			
		||||
          <Route path="/workouts" component={Workout} />
 | 
			
		||||
          <Route path="/admin" component={Admin} />
 | 
			
		||||
          <Route component={NotFound} />
 | 
			
		||||
        </Switch>
 | 
			
		||||
        <Footer />
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
export default connect(
 | 
			
		||||
  () => ({}),
 | 
			
		||||
  dispatch => ({
 | 
			
		||||
    loadAppConfig: () => {
 | 
			
		||||
      dispatch(getAppData('config'))
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
)(App)
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import ReactDOM from 'react-dom'
 | 
			
		||||
 | 
			
		||||
import App from './App'
 | 
			
		||||
 | 
			
		||||
it('renders without crashing', () => {
 | 
			
		||||
  const div = document.createElement('div')
 | 
			
		||||
  ReactDOM.render(<App />, div)
 | 
			
		||||
})
 | 
			
		||||
@@ -1,44 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
 | 
			
		||||
export default function CustomModal(props) {
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="custom-modal-backdrop">
 | 
			
		||||
      <div className="custom-modal">
 | 
			
		||||
        <div className="modal-content">
 | 
			
		||||
          <div className="modal-header">
 | 
			
		||||
            <h5 className="modal-title">{props.title}</h5>
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              className="close"
 | 
			
		||||
              aria-label="Close"
 | 
			
		||||
              onClick={() => props.close()}
 | 
			
		||||
            >
 | 
			
		||||
              <span aria-hidden="true">×</span>
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="modal-body">
 | 
			
		||||
            <p>{props.text}</p>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="modal-footer">
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              className="btn btn-primary"
 | 
			
		||||
              onClick={() => props.confirm()}
 | 
			
		||||
            >
 | 
			
		||||
              {t('common:Yes')}
 | 
			
		||||
            </button>
 | 
			
		||||
            <button
 | 
			
		||||
              type="button"
 | 
			
		||||
              className="btn btn-secondary"
 | 
			
		||||
              onClick={() => props.close()}
 | 
			
		||||
            >
 | 
			
		||||
              {t('common:No')}
 | 
			
		||||
            </button>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,45 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { withTranslation } from 'react-i18next'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
class CustomTextArea extends React.Component {
 | 
			
		||||
  constructor(props, context) {
 | 
			
		||||
    super(props, context)
 | 
			
		||||
    this.state = {
 | 
			
		||||
      text: props.defaultValue ? props.defaultValue : '',
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleOnChange(changeEvent) {
 | 
			
		||||
    this.setState({ text: changeEvent.target.value })
 | 
			
		||||
    if (this.props.onTextChange) {
 | 
			
		||||
      this.props.onTextChange(changeEvent)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { charLimit, loading, name, t } = this.props
 | 
			
		||||
    const { text } = this.state
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        <textarea
 | 
			
		||||
          name={name}
 | 
			
		||||
          defaultValue={text}
 | 
			
		||||
          disabled={loading ? loading : false}
 | 
			
		||||
          className="form-control input-lg"
 | 
			
		||||
          maxLength={charLimit}
 | 
			
		||||
          onChange={event => this.handleOnChange(event)}
 | 
			
		||||
        />
 | 
			
		||||
        <div className="remaining-chars">
 | 
			
		||||
          {t('common:remaining characters')}: {text.length}/{charLimit}
 | 
			
		||||
        </div>
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withTranslation()(
 | 
			
		||||
  connect(state => ({
 | 
			
		||||
    loading: state.loading,
 | 
			
		||||
  }))(CustomTextArea)
 | 
			
		||||
)
 | 
			
		||||
@@ -1,33 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
export default class Message extends React.PureComponent {
 | 
			
		||||
  render() {
 | 
			
		||||
    const { message, messages, t } = this.props
 | 
			
		||||
    const singleMessage =
 | 
			
		||||
      message === '' || !message
 | 
			
		||||
        ? ''
 | 
			
		||||
        : message.split('|').length > 1
 | 
			
		||||
        ? `${t(`messages:${message.split('|')[0]}`)}: ${t(
 | 
			
		||||
            `messages:${message.split('|')[1]}`
 | 
			
		||||
          )}`
 | 
			
		||||
        : t(`messages:${message}`)
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="error-message">
 | 
			
		||||
        {singleMessage !== '' && <code>{singleMessage}</code>}
 | 
			
		||||
        {messages &&
 | 
			
		||||
          messages.length > 0 &&
 | 
			
		||||
          (messages.length === 1 ? (
 | 
			
		||||
            <code>{messages[0].value}</code>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <code>
 | 
			
		||||
              <ul>
 | 
			
		||||
                {messages.map(msg => (
 | 
			
		||||
                  <li key={msg.id}>{t(`messages:${msg.value}`)}</li>
 | 
			
		||||
                ))}
 | 
			
		||||
              </ul>
 | 
			
		||||
            </code>
 | 
			
		||||
          ))}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Link } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
export default class NoWorkouts extends React.PureComponent {
 | 
			
		||||
  render() {
 | 
			
		||||
    const { t } = this.props
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="card text-center">
 | 
			
		||||
        <div className="card-body">
 | 
			
		||||
          {t('common:No workouts.')}{' '}
 | 
			
		||||
          <Link to={{ pathname: '/workouts/add' }}>
 | 
			
		||||
            {t('dashboard:Upload one !')}
 | 
			
		||||
          </Link>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,72 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Link } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
import { formatUrl, rangePagination } from '../../utils'
 | 
			
		||||
 | 
			
		||||
export default class Pagination extends React.PureComponent {
 | 
			
		||||
  getUrl(value) {
 | 
			
		||||
    const { query, pathname } = this.props
 | 
			
		||||
    const newQuery = Object.assign({}, query)
 | 
			
		||||
    let page = query.page ? +query.page : 1
 | 
			
		||||
    switch (value) {
 | 
			
		||||
      case 'prev':
 | 
			
		||||
        page -= 1
 | 
			
		||||
        break
 | 
			
		||||
      case 'next':
 | 
			
		||||
        page += 1
 | 
			
		||||
        break
 | 
			
		||||
      default:
 | 
			
		||||
        page = +value
 | 
			
		||||
    }
 | 
			
		||||
    newQuery.page = page
 | 
			
		||||
    return formatUrl(pathname, newQuery)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { pagination, t } = this.props
 | 
			
		||||
    return (
 | 
			
		||||
      <>
 | 
			
		||||
        {pagination && Object.keys(pagination).length > 0 && (
 | 
			
		||||
          <nav aria-label="Page navigation example">
 | 
			
		||||
            <ul className="pagination justify-content-center">
 | 
			
		||||
              <li
 | 
			
		||||
                className={`page-item ${pagination.has_prev ? '' : 'disabled'}`}
 | 
			
		||||
              >
 | 
			
		||||
                <Link
 | 
			
		||||
                  className="page-link"
 | 
			
		||||
                  to={this.getUrl('prev')}
 | 
			
		||||
                  aria-disabled={!pagination.has_prev}
 | 
			
		||||
                >
 | 
			
		||||
                  {t('common:Previous')}
 | 
			
		||||
                </Link>
 | 
			
		||||
              </li>
 | 
			
		||||
              {rangePagination(pagination.pages).map(page => (
 | 
			
		||||
                <li
 | 
			
		||||
                  key={page}
 | 
			
		||||
                  className={`page-item ${
 | 
			
		||||
                    page === pagination.page ? 'active' : ''
 | 
			
		||||
                  }`}
 | 
			
		||||
                >
 | 
			
		||||
                  <Link className="page-link" to={this.getUrl(page)}>
 | 
			
		||||
                    {page}
 | 
			
		||||
                  </Link>
 | 
			
		||||
                </li>
 | 
			
		||||
              ))}
 | 
			
		||||
              <li
 | 
			
		||||
                className={`page-item ${pagination.has_next ? '' : 'disabled'}`}
 | 
			
		||||
              >
 | 
			
		||||
                <Link
 | 
			
		||||
                  className="page-link"
 | 
			
		||||
                  to={this.getUrl('next')}
 | 
			
		||||
                  aria-disabled={!pagination.has_next}
 | 
			
		||||
                >
 | 
			
		||||
                  {t('common:Next')}
 | 
			
		||||
                </Link>
 | 
			
		||||
              </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
          </nav>
 | 
			
		||||
        )}
 | 
			
		||||
      </>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,29 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
import { apiUrl } from '../../utils'
 | 
			
		||||
 | 
			
		||||
export default class StaticMap extends React.PureComponent {
 | 
			
		||||
  render() {
 | 
			
		||||
    const { display, workout } = this.props
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div className={`workout-map${display === 'list' ? '-list' : ''}`}>
 | 
			
		||||
        <img
 | 
			
		||||
          src={`${apiUrl}workouts/map/${workout.map}?${Date.now()}`}
 | 
			
		||||
          alt="workout map"
 | 
			
		||||
        />
 | 
			
		||||
        <div className={`map-attribution${display === 'list' ? '-list' : ''}`}>
 | 
			
		||||
          <span className="map-attribution-text">©</span>
 | 
			
		||||
          <a
 | 
			
		||||
            className="map-attribution-text"
 | 
			
		||||
            href="http://www.openstreetmap.org/copyright"
 | 
			
		||||
            target="_blank"
 | 
			
		||||
            rel="noopener noreferrer"
 | 
			
		||||
          >
 | 
			
		||||
            OpenStreetMap
 | 
			
		||||
          </a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,30 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
import { formatValue } from '../../../utils/stats'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @return {null}
 | 
			
		||||
 */
 | 
			
		||||
export default function CustomLabel(props) {
 | 
			
		||||
  const { displayedData, x, y, width, value } = props
 | 
			
		||||
  if (!value) {
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
  const radius = 10
 | 
			
		||||
  const formattedValue = formatValue(displayedData, value)
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <g>
 | 
			
		||||
      <text
 | 
			
		||||
        x={x + width / 2}
 | 
			
		||||
        y={y - radius}
 | 
			
		||||
        fill="#666"
 | 
			
		||||
        fontSize="11"
 | 
			
		||||
        textAnchor="middle"
 | 
			
		||||
        dominantBaseline="middle"
 | 
			
		||||
      >
 | 
			
		||||
        {formattedValue}
 | 
			
		||||
      </text>
 | 
			
		||||
    </g>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,198 +0,0 @@
 | 
			
		||||
// eslint-disable-next-line max-len
 | 
			
		||||
// source: https://blog.flowandform.agency/create-a-custom-calendar-in-react-3df1bfd0b728
 | 
			
		||||
import {
 | 
			
		||||
  addDays,
 | 
			
		||||
  addMonths,
 | 
			
		||||
  endOfMonth,
 | 
			
		||||
  endOfWeek,
 | 
			
		||||
  format,
 | 
			
		||||
  isSameDay,
 | 
			
		||||
  isSameMonth,
 | 
			
		||||
  isToday,
 | 
			
		||||
  startOfMonth,
 | 
			
		||||
  startOfWeek,
 | 
			
		||||
  subMonths,
 | 
			
		||||
} from 'date-fns'
 | 
			
		||||
import { enGB, fr } from 'date-fns/locale'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import CalendarWorkouts from './CalendarWorkouts'
 | 
			
		||||
import { getMonthWorkouts } from '../../actions/workouts'
 | 
			
		||||
import { getDateWithTZ } from '../../utils'
 | 
			
		||||
 | 
			
		||||
const getStartAndEndMonth = (date, weekStartOnMonday) => {
 | 
			
		||||
  const monthStart = startOfMonth(date)
 | 
			
		||||
  const monthEnd = endOfMonth(date)
 | 
			
		||||
  const weekStartsOn = weekStartOnMonday ? 1 : 0
 | 
			
		||||
  return {
 | 
			
		||||
    start: startOfWeek(monthStart, { weekStartsOn }),
 | 
			
		||||
    end: endOfWeek(monthEnd),
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class Calendar extends React.Component {
 | 
			
		||||
  constructor(props, context) {
 | 
			
		||||
    super(props, context)
 | 
			
		||||
    const calendarDate = new Date()
 | 
			
		||||
    this.state = {
 | 
			
		||||
      currentMonth: calendarDate,
 | 
			
		||||
      startDate: getStartAndEndMonth(calendarDate, props.weekm).start,
 | 
			
		||||
      endDate: getStartAndEndMonth(calendarDate, props.weekm).end,
 | 
			
		||||
      weekStartOnMonday: props.weekm,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.loadMonthWorkouts(this.state.startDate, this.state.endDate)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  renderHeader(localeOptions) {
 | 
			
		||||
    const dateFormat = 'MMM yyyy'
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="header row flex-middle">
 | 
			
		||||
        <div className="col col-start" onClick={() => this.handlePrevMonth()}>
 | 
			
		||||
          <i className="fa fa-chevron-left" aria-hidden="true" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="col col-center">
 | 
			
		||||
          <span>
 | 
			
		||||
            {format(this.state.currentMonth, dateFormat, localeOptions)}
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="col col-end" onClick={() => this.handleNextMonth()}>
 | 
			
		||||
          <i className="fa fa-chevron-right" aria-hidden="true" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  renderDays(localeOptions) {
 | 
			
		||||
    const dateFormat = 'EEE'
 | 
			
		||||
    const days = []
 | 
			
		||||
    const { startDate } = this.state
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < 7; i++) {
 | 
			
		||||
      days.push(
 | 
			
		||||
        <div className="col col-center" key={i}>
 | 
			
		||||
          {format(addDays(startDate, i), dateFormat, localeOptions)}
 | 
			
		||||
        </div>
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
    return <div className="days row">{days}</div>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  filterWorkouts(day) {
 | 
			
		||||
    const { workouts, user } = this.props
 | 
			
		||||
    if (workouts) {
 | 
			
		||||
      return workouts
 | 
			
		||||
        .filter(act =>
 | 
			
		||||
          isSameDay(getDateWithTZ(act.workout_date, user.timezone), day)
 | 
			
		||||
        )
 | 
			
		||||
        .reverse()
 | 
			
		||||
    }
 | 
			
		||||
    return []
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  renderCells() {
 | 
			
		||||
    const { currentMonth, startDate, endDate, weekStartOnMonday } = this.state
 | 
			
		||||
    const { sports } = this.props
 | 
			
		||||
 | 
			
		||||
    const dateFormat = 'd'
 | 
			
		||||
    const rows = []
 | 
			
		||||
 | 
			
		||||
    let days = []
 | 
			
		||||
    let day = startDate
 | 
			
		||||
    let formattedDate = ''
 | 
			
		||||
 | 
			
		||||
    while (day <= endDate) {
 | 
			
		||||
      for (let i = 0; i < 7; i++) {
 | 
			
		||||
        formattedDate = format(day, dateFormat)
 | 
			
		||||
        const dayWorkouts = this.filterWorkouts(day)
 | 
			
		||||
        const isDisabled = isSameMonth(day, currentMonth) ? '' : '-disabled'
 | 
			
		||||
        const isWeekEnd = weekStartOnMonday
 | 
			
		||||
          ? [5, 6].includes(i)
 | 
			
		||||
          : [0, 6].includes(i)
 | 
			
		||||
        days.push(
 | 
			
		||||
          <div
 | 
			
		||||
            className={`col cell ${isWeekEnd ? ' weekend' : ''}${
 | 
			
		||||
              isToday(day) ? ' today' : ''
 | 
			
		||||
            }`}
 | 
			
		||||
            key={day}
 | 
			
		||||
          >
 | 
			
		||||
            <div className={`img${isDisabled}`}>
 | 
			
		||||
              <span className="number">{formattedDate}</span>
 | 
			
		||||
              <CalendarWorkouts
 | 
			
		||||
                dayWorkouts={dayWorkouts}
 | 
			
		||||
                isDisabled={isDisabled}
 | 
			
		||||
                sports={sports}
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        )
 | 
			
		||||
        day = addDays(day, 1)
 | 
			
		||||
      }
 | 
			
		||||
      rows.push(
 | 
			
		||||
        <div className="row" key={day}>
 | 
			
		||||
          {days}
 | 
			
		||||
        </div>
 | 
			
		||||
      )
 | 
			
		||||
      days = []
 | 
			
		||||
    }
 | 
			
		||||
    return <div className="body">{rows}</div>
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateStateDate(calendarDate) {
 | 
			
		||||
    const { start, end } = getStartAndEndMonth(
 | 
			
		||||
      calendarDate,
 | 
			
		||||
      this.state.weekStartOnMonday
 | 
			
		||||
    )
 | 
			
		||||
    this.setState({
 | 
			
		||||
      currentMonth: calendarDate,
 | 
			
		||||
      startDate: start,
 | 
			
		||||
      endDate: end,
 | 
			
		||||
    })
 | 
			
		||||
    this.props.loadMonthWorkouts(start, end)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleNextMonth() {
 | 
			
		||||
    const calendarDate = addMonths(this.state.currentMonth, 1)
 | 
			
		||||
    this.updateStateDate(calendarDate)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handlePrevMonth() {
 | 
			
		||||
    const calendarDate = subMonths(this.state.currentMonth, 1)
 | 
			
		||||
    this.updateStateDate(calendarDate)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const localeOptions = {
 | 
			
		||||
      locale: this.props.language === 'fr' ? fr : enGB,
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="card workout-card">
 | 
			
		||||
        <div className="calendar">
 | 
			
		||||
          {this.renderHeader(localeOptions)}
 | 
			
		||||
          {this.renderDays(localeOptions)}
 | 
			
		||||
          {this.renderCells()}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(
 | 
			
		||||
  state => ({
 | 
			
		||||
    workouts: state.calendarWorkouts.data,
 | 
			
		||||
    language: state.language,
 | 
			
		||||
    sports: state.sports.data,
 | 
			
		||||
    user: state.user,
 | 
			
		||||
  }),
 | 
			
		||||
  dispatch => ({
 | 
			
		||||
    loadMonthWorkouts: (start, end) => {
 | 
			
		||||
      const dateFormat = 'yyyy-MM-dd'
 | 
			
		||||
      dispatch(
 | 
			
		||||
        getMonthWorkouts(format(start, dateFormat), format(end, dateFormat))
 | 
			
		||||
      )
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
)(Calendar)
 | 
			
		||||
@@ -1,39 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Link } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
import { recordsLabels } from '../../utils/workouts'
 | 
			
		||||
 | 
			
		||||
export default function CalendarWorkout(props) {
 | 
			
		||||
  const { isDisabled, isMore, sportImg, workout } = props
 | 
			
		||||
  return (
 | 
			
		||||
    <Link
 | 
			
		||||
      className={`calendar-workout${isMore}`}
 | 
			
		||||
      to={`/workouts/${workout.id}`}
 | 
			
		||||
    >
 | 
			
		||||
      <>
 | 
			
		||||
        <img
 | 
			
		||||
          alt="workout sport logo"
 | 
			
		||||
          className={`workout-sport ${isDisabled}`}
 | 
			
		||||
          src={sportImg}
 | 
			
		||||
          title={workout.title}
 | 
			
		||||
        />
 | 
			
		||||
        {workout.records.length > 0 && (
 | 
			
		||||
          <sup>
 | 
			
		||||
            <i
 | 
			
		||||
              className="fa fa-trophy custom-fa-small"
 | 
			
		||||
              aria-hidden="true"
 | 
			
		||||
              title={workout.records.map(
 | 
			
		||||
                rec =>
 | 
			
		||||
                  ` ${
 | 
			
		||||
                    recordsLabels.filter(
 | 
			
		||||
                      r => r.record_type === rec.record_type
 | 
			
		||||
                    )[0].label
 | 
			
		||||
                  }`
 | 
			
		||||
              )}
 | 
			
		||||
            />
 | 
			
		||||
          </sup>
 | 
			
		||||
        )}
 | 
			
		||||
      </>
 | 
			
		||||
    </Link>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,59 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
import CalendarWorkout from './CalendarWorkout'
 | 
			
		||||
 | 
			
		||||
export default class CalendarWorkouts extends React.Component {
 | 
			
		||||
  constructor(props, context) {
 | 
			
		||||
    super(props, context)
 | 
			
		||||
    this.state = {
 | 
			
		||||
      isHidden: true,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleDisplayMore() {
 | 
			
		||||
    this.setState({
 | 
			
		||||
      isHidden: !this.state.isHidden,
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { dayWorkouts, isDisabled, sports } = this.props
 | 
			
		||||
    const { isHidden } = this.state
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        {dayWorkouts.map(act => (
 | 
			
		||||
          <CalendarWorkout
 | 
			
		||||
            key={act.id}
 | 
			
		||||
            workout={act}
 | 
			
		||||
            isDisabled={isDisabled}
 | 
			
		||||
            isMore=""
 | 
			
		||||
            sportImg={sports.filter(s => s.id === act.sport_id).map(s => s.img)}
 | 
			
		||||
          />
 | 
			
		||||
        ))}
 | 
			
		||||
        {dayWorkouts.length > 2 && (
 | 
			
		||||
          <i
 | 
			
		||||
            className={`fa fa-${isHidden ? 'plus' : 'times'} calendar-more`}
 | 
			
		||||
            aria-hidden="true"
 | 
			
		||||
            onClick={() => this.handleDisplayMore()}
 | 
			
		||||
            title="show more workouts"
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        {!isHidden && (
 | 
			
		||||
          <div className="calendar-display-more">
 | 
			
		||||
            {dayWorkouts.map(act => (
 | 
			
		||||
              <CalendarWorkout
 | 
			
		||||
                key={act.id}
 | 
			
		||||
                workout={act}
 | 
			
		||||
                isDisabled={isDisabled}
 | 
			
		||||
                isMore="-more"
 | 
			
		||||
                sportImg={sports
 | 
			
		||||
                  .filter(s => s.id === act.sport_id)
 | 
			
		||||
                  .map(s => s.img)}
 | 
			
		||||
              />
 | 
			
		||||
            ))}
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,74 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Link } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
import { formatRecord, translateSports } from '../../utils/workouts'
 | 
			
		||||
 | 
			
		||||
export default function RecordsCard(props) {
 | 
			
		||||
  const { records, sports, t, user } = props
 | 
			
		||||
  const translatedSports = translateSports(sports, t)
 | 
			
		||||
  const recordsBySport = records.reduce((sportList, record) => {
 | 
			
		||||
    const sport = translatedSports.find(s => s.id === record.sport_id)
 | 
			
		||||
    if (sportList[sport.label] === void 0) {
 | 
			
		||||
      sportList[sport.label] = {
 | 
			
		||||
        img: sport.img,
 | 
			
		||||
        records: [],
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    sportList[sport.label].records.push(formatRecord(record, user.timezone))
 | 
			
		||||
    return sportList
 | 
			
		||||
  }, {})
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="card workout-card">
 | 
			
		||||
      <div className="card-header">{t('workouts:Personal records')}</div>
 | 
			
		||||
      <div className="card-body">
 | 
			
		||||
        {Object.keys(recordsBySport).length === 0
 | 
			
		||||
          ? t('common:No records.')
 | 
			
		||||
          : Object.keys(recordsBySport)
 | 
			
		||||
              .sort()
 | 
			
		||||
              .map(sportLabel => (
 | 
			
		||||
                <div key={sportLabel}>
 | 
			
		||||
                  <span className="heading-span">
 | 
			
		||||
                    <img
 | 
			
		||||
                      alt={`${sportLabel} logo`}
 | 
			
		||||
                      className="record-logo"
 | 
			
		||||
                      src={recordsBySport[sportLabel].img}
 | 
			
		||||
                    />
 | 
			
		||||
                    {sportLabel}
 | 
			
		||||
                  </span>
 | 
			
		||||
                  {/* eslint-disable-next-line max-len */}
 | 
			
		||||
                  <table className="table table-borderless table-sm record-table">
 | 
			
		||||
                    <thead>
 | 
			
		||||
                      <tr>
 | 
			
		||||
                        <th colSpan="3">
 | 
			
		||||
                          <img
 | 
			
		||||
                            alt={`${sportLabel} logo`}
 | 
			
		||||
                            className="record-logo"
 | 
			
		||||
                            src={recordsBySport[sportLabel].img}
 | 
			
		||||
                          />
 | 
			
		||||
                          {sportLabel}
 | 
			
		||||
                        </th>
 | 
			
		||||
                      </tr>
 | 
			
		||||
                    </thead>
 | 
			
		||||
                    <tbody>
 | 
			
		||||
                      {recordsBySport[sportLabel].records.map(rec => (
 | 
			
		||||
                        <tr className="record-tr" key={rec.id}>
 | 
			
		||||
                          <td className="record-td">
 | 
			
		||||
                            {t(`workouts:${rec.record_type}`)}
 | 
			
		||||
                          </td>
 | 
			
		||||
                          <td className="record-td text-right">{rec.value}</td>
 | 
			
		||||
                          <td className="record-td text-right">
 | 
			
		||||
                            <Link to={`/workouts/${rec.workout_id}`}>
 | 
			
		||||
                              {rec.workout_date}
 | 
			
		||||
                            </Link>
 | 
			
		||||
                          </td>
 | 
			
		||||
                        </tr>
 | 
			
		||||
                      ))}
 | 
			
		||||
                    </tbody>
 | 
			
		||||
                  </table>
 | 
			
		||||
                </div>
 | 
			
		||||
              ))}
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,78 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
export default function UserStatistics(props) {
 | 
			
		||||
  const { t, user } = props
 | 
			
		||||
  const days = user.total_duration.match(/day/g)
 | 
			
		||||
    ? `${user.total_duration.split(' ')[0]} ${
 | 
			
		||||
        user.total_duration.match(/days/g) ? t('common:days') : t('common:day')
 | 
			
		||||
      }`
 | 
			
		||||
    : `0 ${t('common:days')},`
 | 
			
		||||
  let duration = user.total_duration.match(/day/g)
 | 
			
		||||
    ? user.total_duration.split(', ')[1]
 | 
			
		||||
    : user.total_duration
 | 
			
		||||
  duration = `${duration.split(':')[0]}h ${duration.split(':')[1]}min`
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="row">
 | 
			
		||||
      <div className="col-lg-3 col-md-6 col-sm-6">
 | 
			
		||||
        <div className="card workout-card">
 | 
			
		||||
          <div className="card-body row">
 | 
			
		||||
            <div className="col-3">
 | 
			
		||||
              <i className="fa fa-calendar fa-3x fa-color" />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="col-9 text-right">
 | 
			
		||||
              <div className="huge">{user.nb_workouts}</div>
 | 
			
		||||
              <div>{`${
 | 
			
		||||
                user.nb_workouts === 1
 | 
			
		||||
                  ? t('common:workout')
 | 
			
		||||
                  : t('common:workouts')
 | 
			
		||||
              }`}</div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="col-lg-3 col-md-6 col-sm-6">
 | 
			
		||||
        <div className="card workout-card">
 | 
			
		||||
          <div className="card-body row">
 | 
			
		||||
            <div className="col-3">
 | 
			
		||||
              <i className="fa fa-road fa-3x fa-color" />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="col-9 text-right">
 | 
			
		||||
              <div className="huge">
 | 
			
		||||
                {Number(user.total_distance).toFixed(2)}
 | 
			
		||||
              </div>
 | 
			
		||||
              <div>km</div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="col-lg-3 col-md-6 col-sm-6">
 | 
			
		||||
        <div className="card workout-card">
 | 
			
		||||
          <div className="card-body row">
 | 
			
		||||
            <div className="col-3">
 | 
			
		||||
              <i className="fa fa-clock-o fa-3x fa-color" />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="col-9 text-right">
 | 
			
		||||
              <div className="huge">{days}</div>
 | 
			
		||||
              <div>{duration}</div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="col-lg-3 col-md-6 col-sm-6">
 | 
			
		||||
        <div className="card workout-card">
 | 
			
		||||
          <div className="card-body row">
 | 
			
		||||
            <div className="col-3">
 | 
			
		||||
              <i className="fa fa-tags fa-3x fa-color" />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="col-9 text-right">
 | 
			
		||||
              <div className="huge">{user.nb_sports}</div>
 | 
			
		||||
              <div>{`${
 | 
			
		||||
                user.nb_sports === 1 ? t('common:sport') : t('common:sports')
 | 
			
		||||
              }`}</div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,114 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Helmet } from 'react-helmet'
 | 
			
		||||
import { withTranslation } from 'react-i18next'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import Calendar from './Calendar'
 | 
			
		||||
import Message from '../Common/Message'
 | 
			
		||||
import NoWorkouts from '../Common/NoWorkouts'
 | 
			
		||||
import Records from './Records'
 | 
			
		||||
import Statistics from './Statistics'
 | 
			
		||||
import UserStatistics from './UserStatistics'
 | 
			
		||||
import WorkoutCard from './WorkoutCard'
 | 
			
		||||
import { getOrUpdateData } from '../../actions'
 | 
			
		||||
import { getMoreWorkouts } from '../../actions/workouts'
 | 
			
		||||
 | 
			
		||||
class DashBoard extends React.Component {
 | 
			
		||||
  constructor(props, context) {
 | 
			
		||||
    super(props, context)
 | 
			
		||||
    this.state = {
 | 
			
		||||
      page: 1,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.loadWorkouts()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { loadMoreWorkouts, message, records, sports, t, user, workouts } =
 | 
			
		||||
      this.props
 | 
			
		||||
    const paginationEnd =
 | 
			
		||||
      workouts.length > 0
 | 
			
		||||
        ? workouts[workouts.length - 1].previous_workout === null
 | 
			
		||||
        : true
 | 
			
		||||
    const { page } = this.state
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        <Helmet>
 | 
			
		||||
          <title>FitTrackee - {t('common:Dashboard')}</title>
 | 
			
		||||
        </Helmet>
 | 
			
		||||
        {message ? (
 | 
			
		||||
          <Message message={message} t={t} />
 | 
			
		||||
        ) : (
 | 
			
		||||
          workouts &&
 | 
			
		||||
          user.total_duration &&
 | 
			
		||||
          sports.length > 0 && (
 | 
			
		||||
            <div className="container dashboard">
 | 
			
		||||
              <UserStatistics user={user} t={t} />
 | 
			
		||||
              <div className="row">
 | 
			
		||||
                <div className="col-md-4">
 | 
			
		||||
                  <Statistics t={t} />
 | 
			
		||||
                  <Records
 | 
			
		||||
                    t={t}
 | 
			
		||||
                    records={records}
 | 
			
		||||
                    sports={sports}
 | 
			
		||||
                    user={user}
 | 
			
		||||
                  />
 | 
			
		||||
                </div>
 | 
			
		||||
                <div className="col-md-8">
 | 
			
		||||
                  <Calendar weekm={user.weekm} />
 | 
			
		||||
                  {workouts.length > 0 ? (
 | 
			
		||||
                    workouts.map(workout => (
 | 
			
		||||
                      <WorkoutCard
 | 
			
		||||
                        workout={workout}
 | 
			
		||||
                        key={workout.id}
 | 
			
		||||
                        sports={sports}
 | 
			
		||||
                        t={t}
 | 
			
		||||
                        user={user}
 | 
			
		||||
                      />
 | 
			
		||||
                    ))
 | 
			
		||||
                  ) : (
 | 
			
		||||
                    <NoWorkouts t={t} />
 | 
			
		||||
                  )}
 | 
			
		||||
                  {!paginationEnd && (
 | 
			
		||||
                    <input
 | 
			
		||||
                      type="submit"
 | 
			
		||||
                      className="btn btn-default btn-md btn-block"
 | 
			
		||||
                      value="Load more workouts"
 | 
			
		||||
                      onClick={() => {
 | 
			
		||||
                        loadMoreWorkouts(page + 1)
 | 
			
		||||
                        this.setState({ page: page + 1 })
 | 
			
		||||
                      }}
 | 
			
		||||
                    />
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          )
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withTranslation()(
 | 
			
		||||
  connect(
 | 
			
		||||
    state => ({
 | 
			
		||||
      workouts: state.workouts.data,
 | 
			
		||||
      message: state.message,
 | 
			
		||||
      records: state.records.data,
 | 
			
		||||
      sports: state.sports.data,
 | 
			
		||||
      user: state.user,
 | 
			
		||||
    }),
 | 
			
		||||
    dispatch => ({
 | 
			
		||||
      loadWorkouts: () => {
 | 
			
		||||
        dispatch(getOrUpdateData('getData', 'workouts', { page: 1 }))
 | 
			
		||||
        dispatch(getOrUpdateData('getData', 'records'))
 | 
			
		||||
      },
 | 
			
		||||
      loadMoreWorkouts: page => {
 | 
			
		||||
        dispatch(getMoreWorkouts({ page }))
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  )(DashBoard)
 | 
			
		||||
)
 | 
			
		||||
@@ -1,36 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
import { version } from './../../utils'
 | 
			
		||||
 | 
			
		||||
export default function Footer() {
 | 
			
		||||
  return (
 | 
			
		||||
    <footer className="footer">
 | 
			
		||||
      <div className="container">
 | 
			
		||||
        <strong>FitTrackee</strong> v{version} -{' '}
 | 
			
		||||
        <a
 | 
			
		||||
          href="https://github.com/SamR1/FitTrackee"
 | 
			
		||||
          target="_blank"
 | 
			
		||||
          rel="noopener noreferrer"
 | 
			
		||||
        >
 | 
			
		||||
          source code
 | 
			
		||||
        </a>{' '}
 | 
			
		||||
        under{' '}
 | 
			
		||||
        <a
 | 
			
		||||
          href="https://choosealicense.com/licenses/gpl-3.0/"
 | 
			
		||||
          target="_blank"
 | 
			
		||||
          rel="noopener noreferrer"
 | 
			
		||||
        >
 | 
			
		||||
          GPLv3
 | 
			
		||||
        </a>{' '}
 | 
			
		||||
        license -{' '}
 | 
			
		||||
        <a
 | 
			
		||||
          href="https://samr1.github.io/FitTrackee/"
 | 
			
		||||
          target="_blank"
 | 
			
		||||
          rel="noopener noreferrer"
 | 
			
		||||
        >
 | 
			
		||||
          documentation
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
    </footer>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,75 +0,0 @@
 | 
			
		||||
import React, { Component } from 'react'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import { ReactComponent as EnFlag } from '../../images/flags/en.svg'
 | 
			
		||||
import { ReactComponent as FrFlag } from '../../images/flags/fr.svg'
 | 
			
		||||
import { updateLanguage } from '../../actions/index'
 | 
			
		||||
 | 
			
		||||
export const languages = [
 | 
			
		||||
  {
 | 
			
		||||
    name: 'en',
 | 
			
		||||
    selected: true,
 | 
			
		||||
    flag: <EnFlag />,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: 'fr',
 | 
			
		||||
    selected: false,
 | 
			
		||||
    flag: <FrFlag />,
 | 
			
		||||
  },
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
class Dropdown extends Component {
 | 
			
		||||
  constructor(props) {
 | 
			
		||||
    super(props)
 | 
			
		||||
    this.state = {
 | 
			
		||||
      isOpen: false,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toggleDropdown() {
 | 
			
		||||
    this.setState(prevState => ({
 | 
			
		||||
      isOpen: !prevState.isOpen,
 | 
			
		||||
    }))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { isOpen } = this.state
 | 
			
		||||
    const { language: selected, onUpdateLanguage } = this.props
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="dropdown-wrapper" onClick={() => this.toggleDropdown()}>
 | 
			
		||||
        <ul className="dropdown-list i18n-flag">
 | 
			
		||||
          {languages
 | 
			
		||||
            .filter(language =>
 | 
			
		||||
              isOpen ? language : language.name === selected
 | 
			
		||||
            )
 | 
			
		||||
            .map(language => (
 | 
			
		||||
              <li
 | 
			
		||||
                className={`dropdown-item${
 | 
			
		||||
                  language.name === selected && isOpen
 | 
			
		||||
                    ? ' dropdown-item-selected'
 | 
			
		||||
                    : ''
 | 
			
		||||
                }`}
 | 
			
		||||
                key={language.name}
 | 
			
		||||
                onClick={() => onUpdateLanguage(language.name, selected)}
 | 
			
		||||
              >
 | 
			
		||||
                {language.flag} {language.name}
 | 
			
		||||
              </li>
 | 
			
		||||
            ))}
 | 
			
		||||
        </ul>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(
 | 
			
		||||
  state => ({
 | 
			
		||||
    language: state.language,
 | 
			
		||||
  }),
 | 
			
		||||
  dispatch => ({
 | 
			
		||||
    onUpdateLanguage: (lang, selected) => {
 | 
			
		||||
      if (lang !== selected) {
 | 
			
		||||
        dispatch(updateLanguage(lang))
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
)(Dropdown)
 | 
			
		||||
@@ -1,173 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
import { withTranslation } from 'react-i18next'
 | 
			
		||||
import { Link } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
import LanguageDropdown from './LanguageDropdown'
 | 
			
		||||
import { apiUrl } from '../../utils'
 | 
			
		||||
 | 
			
		||||
class NavBar extends React.PureComponent {
 | 
			
		||||
  render() {
 | 
			
		||||
    const { admin, isAuthenticated, picture, t, username } = this.props
 | 
			
		||||
    return (
 | 
			
		||||
      <header>
 | 
			
		||||
        <nav className="navbar navbar-expand-lg navbar-light bg-light">
 | 
			
		||||
          <div className="container">
 | 
			
		||||
            <span className="navbar-brand">FitTrackee</span>
 | 
			
		||||
            <button
 | 
			
		||||
              className="navbar-toggler"
 | 
			
		||||
              type="button"
 | 
			
		||||
              data-toggle="collapse"
 | 
			
		||||
              data-target="#navbarSupportedContent"
 | 
			
		||||
              aria-controls="navbarSupportedContent"
 | 
			
		||||
              aria-expanded="false"
 | 
			
		||||
              aria-label="Toggle navigation"
 | 
			
		||||
            >
 | 
			
		||||
              <span className="navbar-toggler-icon" />
 | 
			
		||||
            </button>
 | 
			
		||||
            <div
 | 
			
		||||
              className="collapse navbar-collapse"
 | 
			
		||||
              id="navbarSupportedContent"
 | 
			
		||||
            >
 | 
			
		||||
              <ul className="navbar-nav mr-auto">
 | 
			
		||||
                <li className="nav-item">
 | 
			
		||||
                  <Link
 | 
			
		||||
                    className="nav-link"
 | 
			
		||||
                    to={{
 | 
			
		||||
                      pathname: '/',
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    {t('common:Dashboard')}
 | 
			
		||||
                  </Link>
 | 
			
		||||
                </li>
 | 
			
		||||
                {isAuthenticated && (
 | 
			
		||||
                  <li className="nav-item">
 | 
			
		||||
                    <Link
 | 
			
		||||
                      className="nav-link"
 | 
			
		||||
                      to={{
 | 
			
		||||
                        pathname: '/workouts/history',
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      {t('Workouts')}
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  </li>
 | 
			
		||||
                )}
 | 
			
		||||
                {isAuthenticated && (
 | 
			
		||||
                  <li className="nav-item">
 | 
			
		||||
                    <Link
 | 
			
		||||
                      className="nav-link"
 | 
			
		||||
                      to={{
 | 
			
		||||
                        pathname: '/workouts/statistics',
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      {t('common:Statistics')}
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  </li>
 | 
			
		||||
                )}
 | 
			
		||||
                {admin && (
 | 
			
		||||
                  <li className="nav-item">
 | 
			
		||||
                    <Link
 | 
			
		||||
                      className="nav-link"
 | 
			
		||||
                      to={{
 | 
			
		||||
                        pathname: '/admin',
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      Admin
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  </li>
 | 
			
		||||
                )}
 | 
			
		||||
                {isAuthenticated && (
 | 
			
		||||
                  <li className="nav-item">
 | 
			
		||||
                    <Link
 | 
			
		||||
                      className="nav-link"
 | 
			
		||||
                      to={{
 | 
			
		||||
                        pathname: '/workouts/add',
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      <strong>{t('common:Add workout')}</strong>
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  </li>
 | 
			
		||||
                )}
 | 
			
		||||
              </ul>
 | 
			
		||||
              {/* prettier-ignore */}
 | 
			
		||||
              <ul
 | 
			
		||||
                className="navbar-nav flex-row ml-md-auto d-none d-md-flex"
 | 
			
		||||
              >
 | 
			
		||||
                {!isAuthenticated && (
 | 
			
		||||
                  <li className="nav-item">
 | 
			
		||||
                    <Link
 | 
			
		||||
                      className="nav-link"
 | 
			
		||||
                      to={{
 | 
			
		||||
                        pathname: '/register',
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      {t('user:Register')}
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  </li>
 | 
			
		||||
                )}
 | 
			
		||||
                {!isAuthenticated && (
 | 
			
		||||
                  <li className="nav-item">
 | 
			
		||||
                    <Link
 | 
			
		||||
                      className="nav-link"
 | 
			
		||||
                      to={{
 | 
			
		||||
                        pathname: '/login',
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      {t('user:Login')}
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  </li>
 | 
			
		||||
                )}
 | 
			
		||||
                {isAuthenticated && (
 | 
			
		||||
                <>
 | 
			
		||||
                  {picture === true ? (
 | 
			
		||||
                    <img
 | 
			
		||||
                      alt="Avatar"
 | 
			
		||||
                      src={`${apiUrl}users/${username}/picture?${Date.now()}`}
 | 
			
		||||
                      className="img-fluid App-nav-profile-img"
 | 
			
		||||
                    />
 | 
			
		||||
                  ) : (
 | 
			
		||||
                    <i
 | 
			
		||||
                      className="fa fa-user-circle-o fa-2x no-picture"
 | 
			
		||||
                      aria-hidden="true"
 | 
			
		||||
                    />
 | 
			
		||||
                  )}
 | 
			
		||||
                  <li className="nav-item">
 | 
			
		||||
                    <Link
 | 
			
		||||
                      className="nav-link"
 | 
			
		||||
                      to={{
 | 
			
		||||
                        pathname: '/profile',
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      {username}
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  </li>
 | 
			
		||||
                  <li className="nav-item">
 | 
			
		||||
                    <Link
 | 
			
		||||
                      className="nav-link"
 | 
			
		||||
                      to={{
 | 
			
		||||
                        pathname: '/logout',
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      {t('user:Logout')}
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  </li>
 | 
			
		||||
                </>
 | 
			
		||||
                )}
 | 
			
		||||
                <li><LanguageDropdown /></li>
 | 
			
		||||
              </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </nav>
 | 
			
		||||
      </header>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withTranslation()(
 | 
			
		||||
  connect(({ user }) => ({
 | 
			
		||||
    admin: user.admin,
 | 
			
		||||
    isAuthenticated: user.isAuthenticated,
 | 
			
		||||
    picture: user.picture,
 | 
			
		||||
    username: user.username,
 | 
			
		||||
  }))(NavBar)
 | 
			
		||||
)
 | 
			
		||||
@@ -1,24 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Helmet } from 'react-helmet'
 | 
			
		||||
 | 
			
		||||
export default function AccessDenied(props) {
 | 
			
		||||
  const { t } = props
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <Helmet>
 | 
			
		||||
        <title>FitTrackee - {t('Access denied')}</title>
 | 
			
		||||
      </Helmet>
 | 
			
		||||
      <div className="row">
 | 
			
		||||
        <div className="col-2" />
 | 
			
		||||
        <div className="card col-8">
 | 
			
		||||
          <div className="card-body">
 | 
			
		||||
            <div className="text-center">
 | 
			
		||||
              {t("You don't have permissions to access this page.")}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="col-2" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,15 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Helmet } from 'react-helmet'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
 | 
			
		||||
export default function NotFound() {
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <Helmet>
 | 
			
		||||
        <title>fittrackee - 404</title>
 | 
			
		||||
      </Helmet>
 | 
			
		||||
      <h1 className="page-title">{t('Page not found')}</h1>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Provider } from 'react-redux'
 | 
			
		||||
import { ConnectedRouter } from 'connected-react-router'
 | 
			
		||||
 | 
			
		||||
export default function Root({ store, history, children }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <Provider store={store}>
 | 
			
		||||
      <ConnectedRouter history={history}>{children}</ConnectedRouter>
 | 
			
		||||
    </Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { withTranslation } from 'react-i18next'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import ProfileDetail from './ProfileDetail'
 | 
			
		||||
 | 
			
		||||
function CurrentUserProfile({ t, user }) {
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <ProfileDetail editable t={t} user={user} />
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withTranslation()(
 | 
			
		||||
  connect(state => ({
 | 
			
		||||
    user: state.user,
 | 
			
		||||
  }))(CurrentUserProfile)
 | 
			
		||||
)
 | 
			
		||||
@@ -1,125 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { useTranslation } from 'react-i18next'
 | 
			
		||||
import { Helmet } from 'react-helmet'
 | 
			
		||||
import { Link } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
import { history } from '../../index'
 | 
			
		||||
 | 
			
		||||
export default function Form(props) {
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  const pageTitle = `user:${props.formType
 | 
			
		||||
    .charAt(0)
 | 
			
		||||
    .toUpperCase()}${props.formType.slice(1)}`
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <Helmet>
 | 
			
		||||
        <title>FitTrackee - {t(`user:${props.formType}`)}</title>
 | 
			
		||||
      </Helmet>
 | 
			
		||||
      <h1 className="page-title">{t(pageTitle)}</h1>
 | 
			
		||||
      <div className="container">
 | 
			
		||||
        <div className="row">
 | 
			
		||||
          <div className="col-md-3" />
 | 
			
		||||
          <div className="col-md-6">
 | 
			
		||||
            <br />
 | 
			
		||||
            {props.formType === 'register' && !props.isRegistrationAllowed ? (
 | 
			
		||||
              <div className="card">
 | 
			
		||||
                <div className="card-body">Registration is disabled.</div>
 | 
			
		||||
                <div className="card-body">
 | 
			
		||||
                  <button
 | 
			
		||||
                    type="submit"
 | 
			
		||||
                    className="btn btn-secondary btn-lg btn-block"
 | 
			
		||||
                    onClick={() => history.go(-1)}
 | 
			
		||||
                  >
 | 
			
		||||
                    Back
 | 
			
		||||
                  </button>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            ) : (
 | 
			
		||||
              <>
 | 
			
		||||
                <form
 | 
			
		||||
                  onSubmit={event =>
 | 
			
		||||
                    props.handleUserFormSubmit(event, props.formType)
 | 
			
		||||
                  }
 | 
			
		||||
                >
 | 
			
		||||
                  {props.formType === 'register' && (
 | 
			
		||||
                    <div className="form-group">
 | 
			
		||||
                      <input
 | 
			
		||||
                        className="form-control input-lg"
 | 
			
		||||
                        name="username"
 | 
			
		||||
                        placeholder={t('user:Enter a username')}
 | 
			
		||||
                        required
 | 
			
		||||
                        type="text"
 | 
			
		||||
                        value={props.userForm.username}
 | 
			
		||||
                        onChange={props.onHandleFormChange}
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                  )}
 | 
			
		||||
                  {props.formType !== 'password reset' && (
 | 
			
		||||
                    <div className="form-group">
 | 
			
		||||
                      <input
 | 
			
		||||
                        className="form-control input-lg"
 | 
			
		||||
                        name="email"
 | 
			
		||||
                        placeholder={t('user:Enter an email address')}
 | 
			
		||||
                        required
 | 
			
		||||
                        type="email"
 | 
			
		||||
                        value={props.userForm.email}
 | 
			
		||||
                        onChange={props.onHandleFormChange}
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                  )}
 | 
			
		||||
                  {props.formType !== 'reset your password' && (
 | 
			
		||||
                    <>
 | 
			
		||||
                      <div className="form-group">
 | 
			
		||||
                        <input
 | 
			
		||||
                          className="form-control input-lg"
 | 
			
		||||
                          name="password"
 | 
			
		||||
                          placeholder={t('user:Enter a password')}
 | 
			
		||||
                          required
 | 
			
		||||
                          type="password"
 | 
			
		||||
                          value={props.userForm.password}
 | 
			
		||||
                          onChange={props.onHandleFormChange}
 | 
			
		||||
                        />
 | 
			
		||||
                      </div>
 | 
			
		||||
                      {props.formType !== 'login' && (
 | 
			
		||||
                        <div className="form-group">
 | 
			
		||||
                          <input
 | 
			
		||||
                            className="form-control input-lg"
 | 
			
		||||
                            name="password_conf"
 | 
			
		||||
                            placeholder={t(
 | 
			
		||||
                              'user:Enter the password confirmation'
 | 
			
		||||
                            )}
 | 
			
		||||
                            required
 | 
			
		||||
                            type="password"
 | 
			
		||||
                            value={props.userForm.password_conf}
 | 
			
		||||
                            onChange={props.onHandleFormChange}
 | 
			
		||||
                          />
 | 
			
		||||
                        </div>
 | 
			
		||||
                      )}
 | 
			
		||||
                    </>
 | 
			
		||||
                  )}
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="submit"
 | 
			
		||||
                    className="btn btn-primary btn-lg btn-block"
 | 
			
		||||
                    value={t('Submit')}
 | 
			
		||||
                  />
 | 
			
		||||
                </form>
 | 
			
		||||
                <p className="password-forget">
 | 
			
		||||
                  {props.formType === 'login' && (
 | 
			
		||||
                    <Link
 | 
			
		||||
                      to={{
 | 
			
		||||
                        pathname: '/password-reset/request',
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      {t('user:Forgot password?')}
 | 
			
		||||
                    </Link>
 | 
			
		||||
                  )}
 | 
			
		||||
                </p>
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="col-md-3" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,43 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Trans } from 'react-i18next'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
import { Link } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
import { logout } from '../../actions/user'
 | 
			
		||||
 | 
			
		||||
class Logout extends React.Component {
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.UserLogout()
 | 
			
		||||
  }
 | 
			
		||||
  render() {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="container dashboard">
 | 
			
		||||
        <div className="row">
 | 
			
		||||
          <div className="col-2" />
 | 
			
		||||
          <div className="card col-8">
 | 
			
		||||
            <div className="card-body">
 | 
			
		||||
              <div className="text-center">
 | 
			
		||||
                <Trans i18nKey="user:loggedOut">
 | 
			
		||||
                  You are now logged out. Click <Link to="/login">here</Link> to
 | 
			
		||||
                  log back in.
 | 
			
		||||
                </Trans>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div className="col-2" />
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(
 | 
			
		||||
  state => ({
 | 
			
		||||
    user: state.user,
 | 
			
		||||
  }),
 | 
			
		||||
  dispatch => ({
 | 
			
		||||
    UserLogout: () => {
 | 
			
		||||
      dispatch(logout())
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
)(Logout)
 | 
			
		||||
@@ -1,48 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Trans, useTranslation } from 'react-i18next'
 | 
			
		||||
import { Link } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
import { ReactComponent as Password } from '../../images/password.svg'
 | 
			
		||||
import { ReactComponent as MailSend } from '../../images/mail-send.svg'
 | 
			
		||||
 | 
			
		||||
export default function PasswordReset(props) {
 | 
			
		||||
  const { t } = useTranslation()
 | 
			
		||||
  const { action } = props
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="container dashboard">
 | 
			
		||||
      <div className="row">
 | 
			
		||||
        <div className="col-2" />
 | 
			
		||||
        <div className="card col-8">
 | 
			
		||||
          <div className="card-body">
 | 
			
		||||
            <div className="text-center ">
 | 
			
		||||
              {action === 'sent' && (
 | 
			
		||||
                <>
 | 
			
		||||
                  <div className="svg-icon">
 | 
			
		||||
                    <MailSend />
 | 
			
		||||
                  </div>
 | 
			
		||||
                  {t(
 | 
			
		||||
                    // eslint-disable-next-line max-len
 | 
			
		||||
                    "user:Check your email. If your address is in our database, you'll received an email with a link to reset your password."
 | 
			
		||||
                  )}
 | 
			
		||||
                </>
 | 
			
		||||
              )}
 | 
			
		||||
              {action === 'updated' && (
 | 
			
		||||
                <>
 | 
			
		||||
                  <div className="svg-icon">
 | 
			
		||||
                    <Password />
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <Trans i18nKey="user:updatedPasswordText">
 | 
			
		||||
                    {/* prettier-ignore */}
 | 
			
		||||
                    Your password have been updated. Click
 | 
			
		||||
                    <Link to="/login">here</Link> to log in.
 | 
			
		||||
                  </Trans>
 | 
			
		||||
                </>
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="col-2" />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,199 +0,0 @@
 | 
			
		||||
import { format } from 'date-fns'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Helmet } from 'react-helmet'
 | 
			
		||||
import { withTranslation } from 'react-i18next'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import Message from '../Common/Message'
 | 
			
		||||
import { deletePicture, uploadPicture } from '../../actions/user'
 | 
			
		||||
import { apiUrl, getFileSize } from '../../utils'
 | 
			
		||||
import { history } from '../../index'
 | 
			
		||||
 | 
			
		||||
function ProfileDetail({
 | 
			
		||||
  appConfig,
 | 
			
		||||
  displayModal,
 | 
			
		||||
  editable,
 | 
			
		||||
  isDeletable,
 | 
			
		||||
  message,
 | 
			
		||||
  onDeletePicture,
 | 
			
		||||
  onUploadPicture,
 | 
			
		||||
  pathname,
 | 
			
		||||
  t,
 | 
			
		||||
  user,
 | 
			
		||||
}) {
 | 
			
		||||
  const createdAt = user.created_at
 | 
			
		||||
    ? format(new Date(user.created_at), 'dd/MM/yyyy HH:mm')
 | 
			
		||||
    : ''
 | 
			
		||||
  const birthDate = user.birth_date
 | 
			
		||||
    ? format(new Date(user.birth_date), 'dd/MM/yyyy')
 | 
			
		||||
    : ''
 | 
			
		||||
  const fileSizeLimit = getFileSize(appConfig.max_single_file_size)
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <Helmet>
 | 
			
		||||
        <title>FitTrackee - {t('user:Profile')}</title>
 | 
			
		||||
      </Helmet>
 | 
			
		||||
      <Message message={message} t={t} />
 | 
			
		||||
      <div className="container">
 | 
			
		||||
        <h1 className="page-title">{t('user:Profile')}</h1>
 | 
			
		||||
        <div className="row">
 | 
			
		||||
          <div className="col-md-12">
 | 
			
		||||
            <div className="card">
 | 
			
		||||
              <div className="card-header userName">
 | 
			
		||||
                <strong>{user.username}</strong>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="card-body">
 | 
			
		||||
                <div className="row">
 | 
			
		||||
                  <div className="col-md-8">
 | 
			
		||||
                    <p>
 | 
			
		||||
                      {/* eslint-disable-next-line max-len */}
 | 
			
		||||
                      <span className="user-label">
 | 
			
		||||
                        {t('user:Email')}
 | 
			
		||||
                      </span>: {user.email}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                      <span className="user-label">
 | 
			
		||||
                        {t('user:Registration Date')}
 | 
			
		||||
                      </span>
 | 
			
		||||
                      : {createdAt}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                      <span className="user-label">{t('user:First Name')}</span>
 | 
			
		||||
                      : {user.first_name}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                      {/* eslint-disable-next-line max-len */}
 | 
			
		||||
                      <span className="user-label">
 | 
			
		||||
                        {t('user:Last Name')}
 | 
			
		||||
                      </span>: {user.last_name}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                      <span className="user-label">{t('user:Birth Date')}</span>
 | 
			
		||||
                      : {birthDate}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                      {/* eslint-disable-next-line max-len */}
 | 
			
		||||
                      <span className="user-label">
 | 
			
		||||
                        {t('user:Location')}
 | 
			
		||||
                      </span>: {user.location}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                      <span className="user-label">{t('user:Bio')}</span>:{' '}
 | 
			
		||||
                      <span className="user-bio">{user.bio}</span>
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                      {/* eslint-disable-next-line max-len */}
 | 
			
		||||
                      <span className="user-label">
 | 
			
		||||
                        {t('user:Language')}
 | 
			
		||||
                      </span>: {user.language}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                      {/* eslint-disable-next-line max-len */}
 | 
			
		||||
                      <span className="user-label">
 | 
			
		||||
                        {t('user:Timezone')}
 | 
			
		||||
                      </span>: {user.timezone}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                      <span className="user-label">
 | 
			
		||||
                        {t('user:First day of week')}
 | 
			
		||||
                      </span>
 | 
			
		||||
                      : {user.weekm ? t('user:Monday') : t('user:Sunday')}
 | 
			
		||||
                    </p>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div className="col-md-4">
 | 
			
		||||
                    {user.picture === true && (
 | 
			
		||||
                      <div>
 | 
			
		||||
                        <img
 | 
			
		||||
                          alt="Profile"
 | 
			
		||||
                          src={
 | 
			
		||||
                            `${apiUrl}users/${user.username}/picture` +
 | 
			
		||||
                            `?${Date.now()}`
 | 
			
		||||
                          }
 | 
			
		||||
                          className="img-fluid App-profile-img-small"
 | 
			
		||||
                        />
 | 
			
		||||
                        {editable && (
 | 
			
		||||
                          <>
 | 
			
		||||
                            <br />
 | 
			
		||||
                            <button
 | 
			
		||||
                              type="submit"
 | 
			
		||||
                              onClick={() => onDeletePicture()}
 | 
			
		||||
                            >
 | 
			
		||||
                              {t('user:Delete picture')}
 | 
			
		||||
                            </button>
 | 
			
		||||
                            <br />
 | 
			
		||||
                            <br />
 | 
			
		||||
                          </>
 | 
			
		||||
                        )}
 | 
			
		||||
                      </div>
 | 
			
		||||
                    )}
 | 
			
		||||
                    {editable && (
 | 
			
		||||
                      <form
 | 
			
		||||
                        encType="multipart/form-data"
 | 
			
		||||
                        onSubmit={event => onUploadPicture(event)}
 | 
			
		||||
                      >
 | 
			
		||||
                        <input
 | 
			
		||||
                          type="file"
 | 
			
		||||
                          name="picture"
 | 
			
		||||
                          accept=".png,.jpg,.gif"
 | 
			
		||||
                        />
 | 
			
		||||
                        <br />
 | 
			
		||||
                        <button type="submit">{t('user:Send')}</button>
 | 
			
		||||
                        {` (max. size: ${fileSizeLimit})`}
 | 
			
		||||
                      </form>
 | 
			
		||||
                    )}{' '}
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                {editable && (
 | 
			
		||||
                  <button
 | 
			
		||||
                    className="btn btn-primary"
 | 
			
		||||
                    onClick={() => history.push('/profile/edit')}
 | 
			
		||||
                  >
 | 
			
		||||
                    {t('common:Edit')}
 | 
			
		||||
                  </button>
 | 
			
		||||
                )}
 | 
			
		||||
                {isDeletable && (
 | 
			
		||||
                  <button
 | 
			
		||||
                    className="btn btn-danger"
 | 
			
		||||
                    onClick={() => displayModal(true)}
 | 
			
		||||
                  >
 | 
			
		||||
                    {t('user:Delete user account')}
 | 
			
		||||
                  </button>
 | 
			
		||||
                )}
 | 
			
		||||
                <button
 | 
			
		||||
                  className="btn btn-secondary"
 | 
			
		||||
                  onClick={() =>
 | 
			
		||||
                    pathname === '/profile' ? history.push('/') : history.go(-1)
 | 
			
		||||
                  }
 | 
			
		||||
                >
 | 
			
		||||
                  {t(
 | 
			
		||||
                    pathname === '/profile'
 | 
			
		||||
                      ? 'common:Back to home'
 | 
			
		||||
                      : 'common:Back'
 | 
			
		||||
                  )}
 | 
			
		||||
                </button>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withTranslation()(
 | 
			
		||||
  connect(
 | 
			
		||||
    state => ({
 | 
			
		||||
      appConfig: state.application.config,
 | 
			
		||||
      pathname: state.router.location.pathname,
 | 
			
		||||
      message: state.message,
 | 
			
		||||
    }),
 | 
			
		||||
    dispatch => ({
 | 
			
		||||
      onDeletePicture: () => {
 | 
			
		||||
        dispatch(deletePicture())
 | 
			
		||||
      },
 | 
			
		||||
      onUploadPicture: event => {
 | 
			
		||||
        dispatch(uploadPicture(event))
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  )(ProfileDetail)
 | 
			
		||||
)
 | 
			
		||||
@@ -1,314 +0,0 @@
 | 
			
		||||
import { format } from 'date-fns'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Helmet } from 'react-helmet'
 | 
			
		||||
import { withTranslation } from 'react-i18next'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
import TimezonePicker from 'react-timezone'
 | 
			
		||||
 | 
			
		||||
import Message from '../Common/Message'
 | 
			
		||||
import { deleteUser, handleProfileFormSubmit } from '../../actions/user'
 | 
			
		||||
import { history } from '../../index'
 | 
			
		||||
import { languages } from '../NavBar/LanguageDropdown'
 | 
			
		||||
import CustomModal from '../Common/CustomModal'
 | 
			
		||||
import CustomTextArea from '../Common/CustomTextArea'
 | 
			
		||||
 | 
			
		||||
class ProfileEdit extends React.Component {
 | 
			
		||||
  constructor(props, context) {
 | 
			
		||||
    super(props, context)
 | 
			
		||||
    this.state = {
 | 
			
		||||
      formData: {},
 | 
			
		||||
      displayModal: false,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.initForm()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate(prevProps) {
 | 
			
		||||
    if (prevProps.user !== this.props.user) {
 | 
			
		||||
      this.initForm()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  initForm() {
 | 
			
		||||
    const { user } = this.props
 | 
			
		||||
    const formData = {}
 | 
			
		||||
    Object.keys(user).map(k =>
 | 
			
		||||
      user[k] === null
 | 
			
		||||
        ? (formData[k] = '')
 | 
			
		||||
        : k === 'birth_date'
 | 
			
		||||
        ? (formData[k] = format(new Date(user[k]), 'yyyy-MM-dd'))
 | 
			
		||||
        : (formData[k] = user[k])
 | 
			
		||||
    )
 | 
			
		||||
    this.setState({ formData })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleFormChange(e) {
 | 
			
		||||
    const { formData } = this.state
 | 
			
		||||
    if (e.target.name === 'weekm') {
 | 
			
		||||
      formData.weekm = e.target.value === 'Monday'
 | 
			
		||||
    } else {
 | 
			
		||||
      formData[e.target.name] = e.target.value
 | 
			
		||||
    }
 | 
			
		||||
    this.setState(formData)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  displayModal(value) {
 | 
			
		||||
    this.setState(prevState => ({
 | 
			
		||||
      ...prevState,
 | 
			
		||||
      displayModal: value,
 | 
			
		||||
    }))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { message, onDeleteUser, onHandleProfileFormSubmit, t, user } =
 | 
			
		||||
      this.props
 | 
			
		||||
    const { displayModal, formData } = this.state
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        <Helmet>
 | 
			
		||||
          <title>FitTrackee - {t('user:Profile Edition')}</title>
 | 
			
		||||
        </Helmet>
 | 
			
		||||
        {formData.isAuthenticated && (
 | 
			
		||||
          <div className="container">
 | 
			
		||||
            {displayModal && (
 | 
			
		||||
              <CustomModal
 | 
			
		||||
                title={t('common:Confirmation')}
 | 
			
		||||
                text={t(
 | 
			
		||||
                  'user:Are you sure you want to delete your account? ' +
 | 
			
		||||
                    'All data will be deleted, this cannot be undone.'
 | 
			
		||||
                )}
 | 
			
		||||
                confirm={() => {
 | 
			
		||||
                  onDeleteUser(user.username)
 | 
			
		||||
                  this.displayModal(false)
 | 
			
		||||
                }}
 | 
			
		||||
                close={() => this.displayModal(false)}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            <h1 className="page-title">{t('user:Profile Edition')}</h1>
 | 
			
		||||
            <div className="row">
 | 
			
		||||
              <div className="col-md-2" />
 | 
			
		||||
              <div className="col-md-8">
 | 
			
		||||
                <div className="card">
 | 
			
		||||
                  <div className="card-header">{user.username}</div>
 | 
			
		||||
                  <div className="card-body">
 | 
			
		||||
                    <div className="row">
 | 
			
		||||
                      <div className="col-md-12">
 | 
			
		||||
                        <form
 | 
			
		||||
                          onSubmit={event => {
 | 
			
		||||
                            event.preventDefault()
 | 
			
		||||
                            onHandleProfileFormSubmit(formData)
 | 
			
		||||
                          }}
 | 
			
		||||
                        >
 | 
			
		||||
                          <div className="form-group">
 | 
			
		||||
                            <label>
 | 
			
		||||
                              {t('user:Email')}:
 | 
			
		||||
                              <input
 | 
			
		||||
                                name="email"
 | 
			
		||||
                                className="form-control input-lg"
 | 
			
		||||
                                type="text"
 | 
			
		||||
                                value={formData.email}
 | 
			
		||||
                                readOnly
 | 
			
		||||
                              />
 | 
			
		||||
                            </label>
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <div className="form-group">
 | 
			
		||||
                            <label>
 | 
			
		||||
                              {t('user:Registration Date')}:
 | 
			
		||||
                              <input
 | 
			
		||||
                                name="createdAt"
 | 
			
		||||
                                className="form-control input-lg"
 | 
			
		||||
                                type="text"
 | 
			
		||||
                                value={formData.created_at}
 | 
			
		||||
                                disabled
 | 
			
		||||
                              />
 | 
			
		||||
                            </label>
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <div className="form-group">
 | 
			
		||||
                            <label>
 | 
			
		||||
                              {t('user:Password')}:
 | 
			
		||||
                              <input
 | 
			
		||||
                                name="password"
 | 
			
		||||
                                className="form-control input-lg"
 | 
			
		||||
                                type="password"
 | 
			
		||||
                                onChange={e => this.handleFormChange(e)}
 | 
			
		||||
                              />
 | 
			
		||||
                            </label>
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <div className="form-group">
 | 
			
		||||
                            <label>
 | 
			
		||||
                              {t('user:Password Confirmation')}:
 | 
			
		||||
                              <input
 | 
			
		||||
                                name="password_conf"
 | 
			
		||||
                                className="form-control input-lg"
 | 
			
		||||
                                type="password"
 | 
			
		||||
                                onChange={e => this.handleFormChange(e)}
 | 
			
		||||
                              />
 | 
			
		||||
                            </label>
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <hr />
 | 
			
		||||
                          <div className="form-group">
 | 
			
		||||
                            <label>
 | 
			
		||||
                              {t('user:First Name')}:
 | 
			
		||||
                              <input
 | 
			
		||||
                                name="first_name"
 | 
			
		||||
                                className="form-control input-lg"
 | 
			
		||||
                                type="text"
 | 
			
		||||
                                value={formData.first_name}
 | 
			
		||||
                                onChange={e => this.handleFormChange(e)}
 | 
			
		||||
                              />
 | 
			
		||||
                            </label>
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <div className="form-group">
 | 
			
		||||
                            <label>
 | 
			
		||||
                              {t('user:Last Name')}:
 | 
			
		||||
                              <input
 | 
			
		||||
                                name="last_name"
 | 
			
		||||
                                className="form-control input-lg"
 | 
			
		||||
                                type="text"
 | 
			
		||||
                                value={formData.last_name}
 | 
			
		||||
                                onChange={e => this.handleFormChange(e)}
 | 
			
		||||
                              />
 | 
			
		||||
                            </label>
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <div className="form-group">
 | 
			
		||||
                            <label>
 | 
			
		||||
                              {t('user:Birth Date')}
 | 
			
		||||
                              <input
 | 
			
		||||
                                name="birth_date"
 | 
			
		||||
                                className="form-control input-lg"
 | 
			
		||||
                                type="date"
 | 
			
		||||
                                value={formData.birth_date}
 | 
			
		||||
                                onChange={e => this.handleFormChange(e)}
 | 
			
		||||
                              />
 | 
			
		||||
                            </label>
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <div className="form-group">
 | 
			
		||||
                            <label>
 | 
			
		||||
                              {t('user:Location')}:
 | 
			
		||||
                              <input
 | 
			
		||||
                                name="location"
 | 
			
		||||
                                className="form-control input-lg"
 | 
			
		||||
                                type="text"
 | 
			
		||||
                                value={formData.location}
 | 
			
		||||
                                onChange={e => this.handleFormChange(e)}
 | 
			
		||||
                              />
 | 
			
		||||
                            </label>
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <div className="form-group">
 | 
			
		||||
                            <label>
 | 
			
		||||
                              {t('user:Bio')}:
 | 
			
		||||
                              <CustomTextArea
 | 
			
		||||
                                charLimit={200}
 | 
			
		||||
                                name="bio"
 | 
			
		||||
                                defaultValue={formData.bio}
 | 
			
		||||
                                onTextChange={e => this.handleFormChange(e)}
 | 
			
		||||
                              />
 | 
			
		||||
                            </label>
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <div className="form-group">
 | 
			
		||||
                            <label>
 | 
			
		||||
                              {t('user:Language')}:
 | 
			
		||||
                              <select
 | 
			
		||||
                                name="language"
 | 
			
		||||
                                className="form-control input-lg"
 | 
			
		||||
                                value={formData.language}
 | 
			
		||||
                                onChange={e => this.handleFormChange(e)}
 | 
			
		||||
                              >
 | 
			
		||||
                                {languages.map(lang => (
 | 
			
		||||
                                  <option value={lang.name} key={lang.name}>
 | 
			
		||||
                                    {lang.name}
 | 
			
		||||
                                  </option>
 | 
			
		||||
                                ))}
 | 
			
		||||
                              </select>
 | 
			
		||||
                            </label>
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <div className="form-group">
 | 
			
		||||
                            <label>
 | 
			
		||||
                              {t('user:Timezone')}:
 | 
			
		||||
                              <TimezonePicker
 | 
			
		||||
                                className="form-control timezone-custom"
 | 
			
		||||
                                onChange={tz => {
 | 
			
		||||
                                  const e = {
 | 
			
		||||
                                    target: {
 | 
			
		||||
                                      name: 'timezone',
 | 
			
		||||
                                      value: tz ? tz : 'Europe/Paris',
 | 
			
		||||
                                    },
 | 
			
		||||
                                  }
 | 
			
		||||
                                  this.handleFormChange(e)
 | 
			
		||||
                                }}
 | 
			
		||||
                                value={formData.timezone}
 | 
			
		||||
                              />
 | 
			
		||||
                            </label>
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <div className="form-group">
 | 
			
		||||
                            <label>
 | 
			
		||||
                              {t('user:First day of week')}:
 | 
			
		||||
                              <select
 | 
			
		||||
                                name="weekm"
 | 
			
		||||
                                className="form-control input-lg"
 | 
			
		||||
                                value={formData.weekm ? 'Monday' : 'Sunday'}
 | 
			
		||||
                                onChange={e => this.handleFormChange(e)}
 | 
			
		||||
                              >
 | 
			
		||||
                                <option value="Sunday">
 | 
			
		||||
                                  {t('user:Sunday')}
 | 
			
		||||
                                </option>
 | 
			
		||||
                                <option value="Monday">
 | 
			
		||||
                                  {t('user:Monday')}
 | 
			
		||||
                                </option>
 | 
			
		||||
                              </select>
 | 
			
		||||
                            </label>
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <button type="submit" className="btn btn-primary">
 | 
			
		||||
                            {t('common:Submit')}
 | 
			
		||||
                          </button>
 | 
			
		||||
                          <button
 | 
			
		||||
                            className="btn btn-danger"
 | 
			
		||||
                            onClick={event => {
 | 
			
		||||
                              event.preventDefault()
 | 
			
		||||
                              this.displayModal(true)
 | 
			
		||||
                            }}
 | 
			
		||||
                          >
 | 
			
		||||
                            {t('user:Delete my account')}
 | 
			
		||||
                          </button>
 | 
			
		||||
                          <button
 | 
			
		||||
                            type="submit"
 | 
			
		||||
                            className="btn btn-secondary"
 | 
			
		||||
                            onClick={() => history.push('/profile')}
 | 
			
		||||
                          >
 | 
			
		||||
                            {t('common:Cancel')}
 | 
			
		||||
                          </button>
 | 
			
		||||
                        </form>
 | 
			
		||||
                        <Message message={message} t={t} />
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="col-md-2" />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withTranslation()(
 | 
			
		||||
  connect(
 | 
			
		||||
    state => ({
 | 
			
		||||
      location: state.router.location,
 | 
			
		||||
      message: state.message,
 | 
			
		||||
      user: state.user,
 | 
			
		||||
    }),
 | 
			
		||||
    dispatch => ({
 | 
			
		||||
      onDeleteUser: username => {
 | 
			
		||||
        dispatch(deleteUser(username))
 | 
			
		||||
      },
 | 
			
		||||
      onHandleProfileFormSubmit: formData => {
 | 
			
		||||
        dispatch(handleProfileFormSubmit(formData))
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  )(ProfileEdit)
 | 
			
		||||
)
 | 
			
		||||
@@ -1,99 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { withTranslation } from 'react-i18next'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
import { Redirect } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
import Form from './Form'
 | 
			
		||||
import Message from '../Common/Message'
 | 
			
		||||
import { handleUserFormSubmit } from '../../actions/user'
 | 
			
		||||
import { isLoggedIn } from '../../utils'
 | 
			
		||||
 | 
			
		||||
class UserForm extends React.Component {
 | 
			
		||||
  constructor(props, context) {
 | 
			
		||||
    super(props, context)
 | 
			
		||||
    this.state = {
 | 
			
		||||
      formData: {
 | 
			
		||||
        username: '',
 | 
			
		||||
        email: '',
 | 
			
		||||
        password: '',
 | 
			
		||||
        password_conf: '',
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate(prevProps) {
 | 
			
		||||
    if (prevProps.location.pathname !== this.props.location.pathname) {
 | 
			
		||||
      this.emptyForm()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  emptyForm() {
 | 
			
		||||
    const { formData } = this.state
 | 
			
		||||
    Object.keys(formData).map(k => (formData[k] = ''))
 | 
			
		||||
    this.setState(formData)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onHandleFormChange(e) {
 | 
			
		||||
    const { formData } = this.state
 | 
			
		||||
    formData[e.target.name] = e.target.value
 | 
			
		||||
    this.setState(formData)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const {
 | 
			
		||||
      formType,
 | 
			
		||||
      isRegistrationAllowed,
 | 
			
		||||
      message,
 | 
			
		||||
      messages,
 | 
			
		||||
      onHandleUserFormSubmit,
 | 
			
		||||
      t,
 | 
			
		||||
    } = this.props
 | 
			
		||||
    const { formData } = this.state
 | 
			
		||||
    const { token } = this.props.location.query
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        {isLoggedIn() || (formType === 'password reset' && !token) ? (
 | 
			
		||||
          <Redirect to="/" />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <div>
 | 
			
		||||
            <Message message={message} messages={messages} t={t} />
 | 
			
		||||
            <Form
 | 
			
		||||
              isRegistrationAllowed={isRegistrationAllowed}
 | 
			
		||||
              formType={formType}
 | 
			
		||||
              userForm={formData}
 | 
			
		||||
              onHandleFormChange={event => this.onHandleFormChange(event)}
 | 
			
		||||
              handleUserFormSubmit={event => {
 | 
			
		||||
                event.preventDefault()
 | 
			
		||||
                if (formType === 'password reset') {
 | 
			
		||||
                  formData.token = token
 | 
			
		||||
                }
 | 
			
		||||
                onHandleUserFormSubmit(formData, formType)
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
export default withTranslation()(
 | 
			
		||||
  connect(
 | 
			
		||||
    state => ({
 | 
			
		||||
      isRegistrationAllowed: state.application.config.is_registration_enabled,
 | 
			
		||||
      location: state.router.location,
 | 
			
		||||
      message: state.message,
 | 
			
		||||
      messages: state.messages,
 | 
			
		||||
    }),
 | 
			
		||||
    dispatch => ({
 | 
			
		||||
      onHandleUserFormSubmit: (formData, formType) => {
 | 
			
		||||
        formType =
 | 
			
		||||
          formType === 'password reset'
 | 
			
		||||
            ? 'password/update'
 | 
			
		||||
            : formType === 'reset your password'
 | 
			
		||||
            ? 'password/reset-request'
 | 
			
		||||
            : formType
 | 
			
		||||
        dispatch(handleUserFormSubmit(formData, formType))
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  )(UserForm)
 | 
			
		||||
)
 | 
			
		||||
@@ -1,86 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { withTranslation } from 'react-i18next'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import CustomModal from '../Common/CustomModal'
 | 
			
		||||
import ProfileDetail from './ProfileDetail'
 | 
			
		||||
import { getOrUpdateData } from '../../actions'
 | 
			
		||||
import { deleteUser } from '../../actions/user'
 | 
			
		||||
 | 
			
		||||
class UserProfile extends React.Component {
 | 
			
		||||
  constructor(props, context) {
 | 
			
		||||
    super(props, context)
 | 
			
		||||
    this.state = {
 | 
			
		||||
      displayModal: false,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.loadUser(this.props.match.params.userName)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate(prevProps) {
 | 
			
		||||
    if (prevProps.match.params.userName !== this.props.match.params.userName) {
 | 
			
		||||
      this.props.loadUser(this.props.match.params.userName)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  displayModal(value) {
 | 
			
		||||
    this.setState(prevState => ({
 | 
			
		||||
      ...prevState,
 | 
			
		||||
      displayModal: value,
 | 
			
		||||
    }))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { t, currentUser, onDeleteUser, users } = this.props
 | 
			
		||||
    const { displayModal } = this.state
 | 
			
		||||
    const [user] = users
 | 
			
		||||
    const editable = user ? currentUser.username === user.username : false
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        {displayModal && (
 | 
			
		||||
          <CustomModal
 | 
			
		||||
            title={t('common:Confirmation')}
 | 
			
		||||
            text={t(
 | 
			
		||||
              'user:Are you sure you want to delete this account? ' +
 | 
			
		||||
                'All data will be deleted, this cannot be undone.'
 | 
			
		||||
            )}
 | 
			
		||||
            confirm={() => {
 | 
			
		||||
              onDeleteUser(user.username)
 | 
			
		||||
              this.displayModal(false)
 | 
			
		||||
            }}
 | 
			
		||||
            close={() => this.displayModal(false)}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        {user && (
 | 
			
		||||
          <ProfileDetail
 | 
			
		||||
            editable={editable}
 | 
			
		||||
            isDeletable={currentUser.admin && !editable}
 | 
			
		||||
            onDeleteUser={onDeleteUser}
 | 
			
		||||
            displayModal={e => this.displayModal(e)}
 | 
			
		||||
            t={t}
 | 
			
		||||
            user={user}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withTranslation()(
 | 
			
		||||
  connect(
 | 
			
		||||
    state => ({
 | 
			
		||||
      currentUser: state.user,
 | 
			
		||||
      users: state.users.data,
 | 
			
		||||
    }),
 | 
			
		||||
    dispatch => ({
 | 
			
		||||
      onDeleteUser: username => {
 | 
			
		||||
        dispatch(deleteUser(username, true))
 | 
			
		||||
      },
 | 
			
		||||
      loadUser: userName => {
 | 
			
		||||
        dispatch(getOrUpdateData('getData', 'users', { username: userName }))
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  )(UserProfile)
 | 
			
		||||
)
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import WorkoutAddOrEdit from './WorkoutAddOrEdit'
 | 
			
		||||
 | 
			
		||||
function WorkoutAdd(props) {
 | 
			
		||||
  const { message, sports } = props
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <WorkoutAddOrEdit workout={null} message={message} sports={sports} />
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(state => ({
 | 
			
		||||
  message: state.message,
 | 
			
		||||
  sports: state.sports.data,
 | 
			
		||||
  user: state.user,
 | 
			
		||||
}))(WorkoutAdd)
 | 
			
		||||
@@ -1,118 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Helmet } from 'react-helmet'
 | 
			
		||||
import { withTranslation } from 'react-i18next'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import FormWithGpx from './WorkoutForms/FormWithGpx'
 | 
			
		||||
import FormWithoutGpx from './WorkoutForms/FormWithoutGpx'
 | 
			
		||||
import Message from '../Common/Message'
 | 
			
		||||
 | 
			
		||||
class WorkoutAddEdit extends React.Component {
 | 
			
		||||
  constructor(props, context) {
 | 
			
		||||
    super(props, context)
 | 
			
		||||
    this.state = {
 | 
			
		||||
      withGpx: true,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleRadioChange(changeEvent) {
 | 
			
		||||
    this.setState({
 | 
			
		||||
      withGpx:
 | 
			
		||||
        changeEvent.target.name === 'withGpx'
 | 
			
		||||
          ? changeEvent.target.value
 | 
			
		||||
          : !changeEvent.target.value,
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { loading, message, sports, t, workout } = this.props
 | 
			
		||||
    const { withGpx } = this.state
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        <Helmet>
 | 
			
		||||
          <title>
 | 
			
		||||
            FitTrackee -{' '}
 | 
			
		||||
            {workout
 | 
			
		||||
              ? t('workouts:Edit a workout')
 | 
			
		||||
              : t('workouts:Add a workout')}
 | 
			
		||||
          </title>
 | 
			
		||||
        </Helmet>
 | 
			
		||||
        <br />
 | 
			
		||||
        <br />
 | 
			
		||||
        <Message message={message} t={t} />
 | 
			
		||||
        <div className="container">
 | 
			
		||||
          <div className="row">
 | 
			
		||||
            <div className="col-md-2" />
 | 
			
		||||
            <div className="col-md-8">
 | 
			
		||||
              <div className="card add-workout">
 | 
			
		||||
                <h2 className="card-header text-center">
 | 
			
		||||
                  {workout
 | 
			
		||||
                    ? t('workouts:Edit a workout')
 | 
			
		||||
                    : t('workouts:Add a workout')}
 | 
			
		||||
                </h2>
 | 
			
		||||
                <div className="card-body">
 | 
			
		||||
                  {workout ? (
 | 
			
		||||
                    workout.with_gpx ? (
 | 
			
		||||
                      <FormWithGpx workout={workout} sports={sports} t={t} />
 | 
			
		||||
                    ) : (
 | 
			
		||||
                      <FormWithoutGpx workout={workout} sports={sports} t={t} />
 | 
			
		||||
                    )
 | 
			
		||||
                  ) : (
 | 
			
		||||
                    <div>
 | 
			
		||||
                      <form>
 | 
			
		||||
                        <div className="form-group row">
 | 
			
		||||
                          <div className="col">
 | 
			
		||||
                            <label className="radioLabel">
 | 
			
		||||
                              <input
 | 
			
		||||
                                className="add-workout-radio"
 | 
			
		||||
                                type="radio"
 | 
			
		||||
                                name="withGpx"
 | 
			
		||||
                                disabled={loading}
 | 
			
		||||
                                checked={withGpx}
 | 
			
		||||
                                onChange={event =>
 | 
			
		||||
                                  this.handleRadioChange(event)
 | 
			
		||||
                                }
 | 
			
		||||
                              />
 | 
			
		||||
                              {t('workouts:with gpx file')}
 | 
			
		||||
                            </label>
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <div className="col">
 | 
			
		||||
                            <label className="radioLabel">
 | 
			
		||||
                              <input
 | 
			
		||||
                                className="add-workout-radio"
 | 
			
		||||
                                type="radio"
 | 
			
		||||
                                name="withoutGpx"
 | 
			
		||||
                                disabled={loading}
 | 
			
		||||
                                checked={!withGpx}
 | 
			
		||||
                                onChange={event =>
 | 
			
		||||
                                  this.handleRadioChange(event)
 | 
			
		||||
                                }
 | 
			
		||||
                              />
 | 
			
		||||
                              {t('workouts:without gpx file')}
 | 
			
		||||
                            </label>
 | 
			
		||||
                          </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                      </form>
 | 
			
		||||
                      {withGpx ? (
 | 
			
		||||
                        <FormWithGpx sports={sports} t={t} />
 | 
			
		||||
                      ) : (
 | 
			
		||||
                        <FormWithoutGpx sports={sports} t={t} />
 | 
			
		||||
                      )}
 | 
			
		||||
                    </div>
 | 
			
		||||
                  )}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="col-md-2" />
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withTranslation()(
 | 
			
		||||
  connect(state => ({
 | 
			
		||||
    loading: state.loading,
 | 
			
		||||
  }))(WorkoutAddEdit)
 | 
			
		||||
)
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { GeoJSON, Marker, TileLayer, useMap } from 'react-leaflet'
 | 
			
		||||
import hash from 'object-hash'
 | 
			
		||||
 | 
			
		||||
import { apiUrl } from '../../../utils'
 | 
			
		||||
 | 
			
		||||
export default function Map({ bounds, coordinates, jsonData, mapAttribution }) {
 | 
			
		||||
  const map = useMap()
 | 
			
		||||
  map.fitBounds(bounds)
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <TileLayer
 | 
			
		||||
        // eslint-disable-next-line max-len
 | 
			
		||||
        attribution={mapAttribution}
 | 
			
		||||
        url={`${apiUrl}workouts/map_tile/{s}/{z}/{x}/{y}.png`}
 | 
			
		||||
      />
 | 
			
		||||
      <GeoJSON
 | 
			
		||||
        // hash as a key to force re-rendering
 | 
			
		||||
        key={hash(jsonData)}
 | 
			
		||||
        data={jsonData}
 | 
			
		||||
      />
 | 
			
		||||
      {coordinates.latitude && (
 | 
			
		||||
        <Marker position={[coordinates.latitude, coordinates.longitude]} />
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,106 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Link } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
import { getDateWithTZ } from '../../../utils'
 | 
			
		||||
import { formatWorkoutDate } from '../../../utils/workouts'
 | 
			
		||||
 | 
			
		||||
export default function WorkoutCardHeader(props) {
 | 
			
		||||
  const { dataType, displayModal, segmentId, sport, t, title, user, workout } =
 | 
			
		||||
    props
 | 
			
		||||
  const workoutDate = workout
 | 
			
		||||
    ? formatWorkoutDate(getDateWithTZ(workout.workout_date, user.timezone))
 | 
			
		||||
    : null
 | 
			
		||||
 | 
			
		||||
  const previousUrl =
 | 
			
		||||
    dataType === 'segment' && segmentId !== 1
 | 
			
		||||
      ? `/workouts/${workout.id}/segment/${segmentId - 1}`
 | 
			
		||||
      : dataType === 'workout' && workout.previous_workout
 | 
			
		||||
      ? `/workouts/${workout.previous_workout}`
 | 
			
		||||
      : null
 | 
			
		||||
  const nextUrl =
 | 
			
		||||
    dataType === 'segment' && segmentId < workout.segments.length
 | 
			
		||||
      ? `/workouts/${workout.id}/segment/${segmentId + 1}`
 | 
			
		||||
      : dataType === 'workout' && workout.next_workout
 | 
			
		||||
      ? `/workouts/${workout.next_workout}`
 | 
			
		||||
      : null
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="container">
 | 
			
		||||
      <div className="row">
 | 
			
		||||
        <div className="col-auto">
 | 
			
		||||
          {previousUrl ? (
 | 
			
		||||
            <Link className="unlink" to={previousUrl}>
 | 
			
		||||
              <i
 | 
			
		||||
                className="fa fa-chevron-left"
 | 
			
		||||
                aria-hidden="true"
 | 
			
		||||
                title={t(`workouts:See previous ${dataType}`)}
 | 
			
		||||
              />
 | 
			
		||||
            </Link>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <i
 | 
			
		||||
              className="fa fa-chevron-left inactive-link"
 | 
			
		||||
              aria-hidden="true"
 | 
			
		||||
              title={t(`workouts:No previous ${dataType}`)}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="col-auto col-workout-logo">
 | 
			
		||||
          <img className="sport-img-medium" src={sport.img} alt="sport logo" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="col">
 | 
			
		||||
          {dataType === 'workout' ? (
 | 
			
		||||
            <>
 | 
			
		||||
              {title}{' '}
 | 
			
		||||
              <Link className="unlink" to={`/workouts/${workout.id}/edit`}>
 | 
			
		||||
                <i
 | 
			
		||||
                  className="fa fa-edit custom-fa"
 | 
			
		||||
                  aria-hidden="true"
 | 
			
		||||
                  title={t('workouts:Edit workout')}
 | 
			
		||||
                />
 | 
			
		||||
              </Link>
 | 
			
		||||
              <i
 | 
			
		||||
                className="fa fa-trash custom-fa"
 | 
			
		||||
                aria-hidden="true"
 | 
			
		||||
                onClick={() => displayModal(true)}
 | 
			
		||||
                title={t('workouts:Delete workout')}
 | 
			
		||||
              />
 | 
			
		||||
            </>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <>
 | 
			
		||||
              {/* prettier-ignore */}
 | 
			
		||||
              <Link
 | 
			
		||||
                to={`/workouts/${workout.id}`}
 | 
			
		||||
              >
 | 
			
		||||
                {title}
 | 
			
		||||
              </Link>{' '}
 | 
			
		||||
              - {t('workouts:segment')} {segmentId}
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
          <br />
 | 
			
		||||
          {workoutDate && (
 | 
			
		||||
            <span className="workout-date">
 | 
			
		||||
              {`${workoutDate.workout_date} - ${workoutDate.workout_time}`}
 | 
			
		||||
            </span>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
        <div className="col-auto">
 | 
			
		||||
          {nextUrl ? (
 | 
			
		||||
            <Link className="unlink" to={nextUrl}>
 | 
			
		||||
              <i
 | 
			
		||||
                className="fa fa-chevron-right"
 | 
			
		||||
                aria-hidden="true"
 | 
			
		||||
                title={t(`workouts:See next ${dataType}`)}
 | 
			
		||||
              />
 | 
			
		||||
            </Link>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <i
 | 
			
		||||
              className="fa fa-chevron-right inactive-link"
 | 
			
		||||
              aria-hidden="true"
 | 
			
		||||
              title={t(`workouts:No next ${dataType}`)}
 | 
			
		||||
            />
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,240 +0,0 @@
 | 
			
		||||
import { format } from 'date-fns'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
import {
 | 
			
		||||
  Area,
 | 
			
		||||
  ComposedChart,
 | 
			
		||||
  Line,
 | 
			
		||||
  ResponsiveContainer,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  XAxis,
 | 
			
		||||
  YAxis,
 | 
			
		||||
} from 'recharts'
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
  getSegmentChartData,
 | 
			
		||||
  getWorkoutChartData,
 | 
			
		||||
} from '../../../actions/workouts'
 | 
			
		||||
 | 
			
		||||
class WorkoutCharts extends React.Component {
 | 
			
		||||
  constructor(props, context) {
 | 
			
		||||
    super(props, context)
 | 
			
		||||
    this.state = {
 | 
			
		||||
      displayDistance: true,
 | 
			
		||||
      dataToHide: [],
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    if (this.props.dataType === 'workout') {
 | 
			
		||||
      this.props.loadWorkoutData(this.props.workout.id)
 | 
			
		||||
    } else {
 | 
			
		||||
      this.props.loadSegmentData(this.props.workout.id, this.props.segmentId)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate(prevProps) {
 | 
			
		||||
    if (
 | 
			
		||||
      (this.props.dataType === 'workout' &&
 | 
			
		||||
        prevProps.workout.id !== this.props.workout.id) ||
 | 
			
		||||
      (this.props.dataType === 'workout' && prevProps.dataType === 'segment')
 | 
			
		||||
    ) {
 | 
			
		||||
      this.props.loadWorkoutData(this.props.workout.id)
 | 
			
		||||
    }
 | 
			
		||||
    if (
 | 
			
		||||
      this.props.dataType === 'segment' &&
 | 
			
		||||
      prevProps.segmentId !== this.props.segmentId
 | 
			
		||||
    ) {
 | 
			
		||||
      this.props.loadSegmentData(this.props.workout.id, this.props.segmentId)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount() {
 | 
			
		||||
    this.props.loadWorkoutData(null)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleRadioChange(changeEvent) {
 | 
			
		||||
    this.setState({
 | 
			
		||||
      displayDistance:
 | 
			
		||||
        changeEvent.target.name === 'distance'
 | 
			
		||||
          ? changeEvent.target.value
 | 
			
		||||
          : !changeEvent.target.value,
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleLegendChange(e) {
 | 
			
		||||
    const { dataToHide } = this.state
 | 
			
		||||
    const name = e.target.name // eslint-disable-line prefer-destructuring
 | 
			
		||||
    if (dataToHide.find(d => d === name)) {
 | 
			
		||||
      dataToHide.splice(dataToHide.indexOf(name), 1)
 | 
			
		||||
    } else {
 | 
			
		||||
      dataToHide.push(name)
 | 
			
		||||
    }
 | 
			
		||||
    this.setState({ dataToHide })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  displayData(name) {
 | 
			
		||||
    const { dataToHide } = this.state
 | 
			
		||||
    return !dataToHide.find(d => d === name)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { chartData, t, updateCoordinates } = this.props
 | 
			
		||||
    const { displayDistance } = this.state
 | 
			
		||||
    const xInterval = chartData ? parseInt(chartData.length / 10, 10) : 0
 | 
			
		||||
    let xDataKey, xScale
 | 
			
		||||
    if (displayDistance) {
 | 
			
		||||
      xDataKey = 'distance'
 | 
			
		||||
      xScale = 'linear'
 | 
			
		||||
    } else {
 | 
			
		||||
      xDataKey = 'duration'
 | 
			
		||||
      xScale = 'time'
 | 
			
		||||
    }
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="container">
 | 
			
		||||
        {chartData && chartData.length > 0 ? (
 | 
			
		||||
          <div>
 | 
			
		||||
            <div className="row chart-radio">
 | 
			
		||||
              <label className="radioLabel col-md-1">
 | 
			
		||||
                <input
 | 
			
		||||
                  type="radio"
 | 
			
		||||
                  name="distance"
 | 
			
		||||
                  checked={displayDistance}
 | 
			
		||||
                  onChange={e => this.handleRadioChange(e)}
 | 
			
		||||
                />
 | 
			
		||||
                {t('workouts:distance')}
 | 
			
		||||
              </label>
 | 
			
		||||
              <label className="radioLabel col-md-1">
 | 
			
		||||
                <input
 | 
			
		||||
                  type="radio"
 | 
			
		||||
                  name="duration"
 | 
			
		||||
                  checked={!displayDistance}
 | 
			
		||||
                  onChange={e => this.handleRadioChange(e)}
 | 
			
		||||
                />
 | 
			
		||||
                {t('workouts:duration')}
 | 
			
		||||
              </label>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="row chart-radio">
 | 
			
		||||
              <div className="col-md-5" />
 | 
			
		||||
              <label className="radioLabel col-md-1">
 | 
			
		||||
                <input
 | 
			
		||||
                  type="checkbox"
 | 
			
		||||
                  name="speed"
 | 
			
		||||
                  checked={this.displayData('speed')}
 | 
			
		||||
                  onChange={e => this.handleLegendChange(e)}
 | 
			
		||||
                />
 | 
			
		||||
                {t('workouts:speed')}
 | 
			
		||||
              </label>
 | 
			
		||||
              <label className="radioLabel col-md-1">
 | 
			
		||||
                <input
 | 
			
		||||
                  type="checkbox"
 | 
			
		||||
                  name="elevation"
 | 
			
		||||
                  checked={this.displayData('elevation')}
 | 
			
		||||
                  onChange={e => this.handleLegendChange(e)}
 | 
			
		||||
                />
 | 
			
		||||
                {t('workouts:elevation')}
 | 
			
		||||
              </label>
 | 
			
		||||
              <div className="col-md-5" />
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="row chart">
 | 
			
		||||
              <ResponsiveContainer height={300}>
 | 
			
		||||
                <ComposedChart
 | 
			
		||||
                  data={chartData}
 | 
			
		||||
                  margin={{ top: 15, right: 30, left: 20, bottom: 15 }}
 | 
			
		||||
                  onMouseMove={e => updateCoordinates(e.activePayload)}
 | 
			
		||||
                  onMouseLeave={() => updateCoordinates(null)}
 | 
			
		||||
                >
 | 
			
		||||
                  <XAxis
 | 
			
		||||
                    allowDecimals={false}
 | 
			
		||||
                    dataKey={xDataKey}
 | 
			
		||||
                    label={{
 | 
			
		||||
                      value: t(`workouts:${xDataKey}`),
 | 
			
		||||
                      offset: 0,
 | 
			
		||||
                      position: 'bottom',
 | 
			
		||||
                    }}
 | 
			
		||||
                    scale={xScale}
 | 
			
		||||
                    interval={xInterval}
 | 
			
		||||
                    tickFormatter={value =>
 | 
			
		||||
                      displayDistance ? value : format(value, 'HH:mm:ss')
 | 
			
		||||
                    }
 | 
			
		||||
                    type="number"
 | 
			
		||||
                  />
 | 
			
		||||
                  <YAxis
 | 
			
		||||
                    label={{
 | 
			
		||||
                      value: `${t('workouts:speed')} (km/h)`,
 | 
			
		||||
                      angle: -90,
 | 
			
		||||
                      position: 'left',
 | 
			
		||||
                    }}
 | 
			
		||||
                    yAxisId="left"
 | 
			
		||||
                  />
 | 
			
		||||
                  <YAxis
 | 
			
		||||
                    label={{
 | 
			
		||||
                      value: `${t('workouts:elevation')} (m)`,
 | 
			
		||||
                      angle: -90,
 | 
			
		||||
                      position: 'right',
 | 
			
		||||
                    }}
 | 
			
		||||
                    yAxisId="right"
 | 
			
		||||
                    orientation="right"
 | 
			
		||||
                  />
 | 
			
		||||
                  {this.displayData('elevation') && (
 | 
			
		||||
                    <Area
 | 
			
		||||
                      yAxisId="right"
 | 
			
		||||
                      type="linear"
 | 
			
		||||
                      dataKey="elevation"
 | 
			
		||||
                      name={t('workouts:elevation')}
 | 
			
		||||
                      fill="#e5e5e5"
 | 
			
		||||
                      stroke="#cccccc"
 | 
			
		||||
                      dot={false}
 | 
			
		||||
                      unit=" m"
 | 
			
		||||
                    />
 | 
			
		||||
                  )}
 | 
			
		||||
                  {this.displayData('speed') && (
 | 
			
		||||
                    <Line
 | 
			
		||||
                      yAxisId="left"
 | 
			
		||||
                      type="linear"
 | 
			
		||||
                      dataKey="speed"
 | 
			
		||||
                      name={t('workouts:speed')}
 | 
			
		||||
                      stroke="#8884d8"
 | 
			
		||||
                      strokeWidth={2}
 | 
			
		||||
                      dot={false}
 | 
			
		||||
                      unit=" km/h"
 | 
			
		||||
                    />
 | 
			
		||||
                  )}
 | 
			
		||||
                  <Tooltip
 | 
			
		||||
                    labelFormatter={value =>
 | 
			
		||||
                      displayDistance
 | 
			
		||||
                        ? `${t('workouts:distance')}: ${value} km`
 | 
			
		||||
                        : `${t('workouts:duration')}: ${format(
 | 
			
		||||
                            value,
 | 
			
		||||
                            'HH:mm:ss'
 | 
			
		||||
                          )}`
 | 
			
		||||
                    }
 | 
			
		||||
                  />
 | 
			
		||||
                </ComposedChart>
 | 
			
		||||
              </ResponsiveContainer>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="chart-info">
 | 
			
		||||
              {t('workouts:data from gpx, without any cleaning')}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        ) : (
 | 
			
		||||
          t('workouts:No data to display')
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(
 | 
			
		||||
  state => ({
 | 
			
		||||
    chartData: state.chartData,
 | 
			
		||||
  }),
 | 
			
		||||
  dispatch => ({
 | 
			
		||||
    loadWorkoutData: workoutId => {
 | 
			
		||||
      dispatch(getWorkoutChartData(workoutId))
 | 
			
		||||
    },
 | 
			
		||||
    loadSegmentData: (workoutId, segmentId) => {
 | 
			
		||||
      dispatch(getSegmentChartData(workoutId, segmentId))
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
)(WorkoutCharts)
 | 
			
		||||
@@ -1,73 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
import WorkoutWeather from './WorkoutWeather'
 | 
			
		||||
 | 
			
		||||
export default function WorkoutDetails(props) {
 | 
			
		||||
  const { t, workout } = props
 | 
			
		||||
  const withPauses = workout.pauses !== '0:00:00' && workout.pauses !== null
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="workout-details">
 | 
			
		||||
      <p>
 | 
			
		||||
        <i className="fa fa-clock-o custom-fa" aria-hidden="true" />
 | 
			
		||||
        {t('workouts:Duration')}: {workout.moving}
 | 
			
		||||
        {workout.records &&
 | 
			
		||||
          workout.records.find(record => record.record_type === 'LD') && (
 | 
			
		||||
            <sup>
 | 
			
		||||
              <i className="fa fa-trophy custom-fa" aria-hidden="true" />
 | 
			
		||||
            </sup>
 | 
			
		||||
          )}
 | 
			
		||||
        {withPauses && (
 | 
			
		||||
          <span>
 | 
			
		||||
            <br />({t('workouts:pauses')}: {workout.pauses},{' '}
 | 
			
		||||
            {t('workouts:total duration')}: {workout.duration})
 | 
			
		||||
          </span>
 | 
			
		||||
        )}
 | 
			
		||||
      </p>
 | 
			
		||||
      <p>
 | 
			
		||||
        <i className="fa fa-road custom-fa" aria-hidden="true" />
 | 
			
		||||
        {t('workouts:Distance')}: {workout.distance} km
 | 
			
		||||
        {workout.records &&
 | 
			
		||||
          workout.records.find(record => record.record_type === 'FD') && (
 | 
			
		||||
            <sup>
 | 
			
		||||
              <i className="fa fa-trophy custom-fa" aria-hidden="true" />
 | 
			
		||||
            </sup>
 | 
			
		||||
          )}
 | 
			
		||||
      </p>
 | 
			
		||||
      <p>
 | 
			
		||||
        <i className="fa fa-tachometer custom-fa" aria-hidden="true" />
 | 
			
		||||
        {t('workouts:Average speed')}: {workout.ave_speed} km/h
 | 
			
		||||
        {workout.records &&
 | 
			
		||||
          workout.records.find(record => record.record_type === 'AS') && (
 | 
			
		||||
            <sup>
 | 
			
		||||
              <i className="fa fa-trophy custom-fa" aria-hidden="true" />
 | 
			
		||||
            </sup>
 | 
			
		||||
          )}
 | 
			
		||||
        <br />
 | 
			
		||||
        {t('workouts:Max. speed')}: {workout.max_speed} km/h
 | 
			
		||||
        {workout.records &&
 | 
			
		||||
          workout.records.find(record => record.record_type === 'MS') && (
 | 
			
		||||
            <sup>
 | 
			
		||||
              <i className="fa fa-trophy custom-fa" aria-hidden="true" />
 | 
			
		||||
            </sup>
 | 
			
		||||
          )}
 | 
			
		||||
      </p>
 | 
			
		||||
      {workout.min_alt && workout.max_alt && (
 | 
			
		||||
        <p>
 | 
			
		||||
          <i className="fi-mountains custom-fa" />
 | 
			
		||||
          {t('workouts:Min. altitude')}: {workout.min_alt}m
 | 
			
		||||
          <br />
 | 
			
		||||
          {t('workouts:Max. altitude')}: {workout.max_alt}m
 | 
			
		||||
        </p>
 | 
			
		||||
      )}
 | 
			
		||||
      {workout.ascent && workout.descent && (
 | 
			
		||||
        <p>
 | 
			
		||||
          <i className="fa fa-location-arrow custom-fa" />
 | 
			
		||||
          {t('workouts:Ascent')}: {workout.ascent}m
 | 
			
		||||
          <br />
 | 
			
		||||
          {t('workouts:Descent')}: {workout.descent}m
 | 
			
		||||
        </p>
 | 
			
		||||
      )}
 | 
			
		||||
      <WorkoutWeather workout={workout} t={t} />
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,87 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { MapContainer } from 'react-leaflet'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import Map from './Map'
 | 
			
		||||
import { getSegmentGpx, getWorkoutGpx } from '../../../actions/workouts'
 | 
			
		||||
import { getGeoJson } from '../../../utils/workouts'
 | 
			
		||||
 | 
			
		||||
class WorkoutMap extends React.Component {
 | 
			
		||||
  constructor(props, context) {
 | 
			
		||||
    super(props, context)
 | 
			
		||||
    this.state = {
 | 
			
		||||
      zoom: 13,
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    if (this.props.dataType === 'workout') {
 | 
			
		||||
      this.props.loadWorkoutGpx(this.props.workout.id)
 | 
			
		||||
    } else {
 | 
			
		||||
      this.props.loadSegmentGpx(this.props.workout.id, this.props.segmentId)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate(prevProps) {
 | 
			
		||||
    if (
 | 
			
		||||
      (this.props.dataType === 'workout' &&
 | 
			
		||||
        prevProps.workout.id !== this.props.workout.id) ||
 | 
			
		||||
      (this.props.dataType === 'workout' && prevProps.dataType === 'segment')
 | 
			
		||||
    ) {
 | 
			
		||||
      this.props.loadWorkoutGpx(this.props.workout.id)
 | 
			
		||||
    }
 | 
			
		||||
    if (
 | 
			
		||||
      this.props.dataType === 'segment' &&
 | 
			
		||||
      prevProps.segmentId !== this.props.segmentId
 | 
			
		||||
    ) {
 | 
			
		||||
      this.props.loadSegmentGpx(this.props.workout.id, this.props.segmentId)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentWillUnmount() {
 | 
			
		||||
    this.props.loadWorkoutGpx(null)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { coordinates, gpxContent, mapAttribution, workout } = this.props
 | 
			
		||||
    const { jsonData } = getGeoJson(gpxContent)
 | 
			
		||||
    const bounds = [
 | 
			
		||||
      [workout.bounds[0], workout.bounds[1]],
 | 
			
		||||
      [workout.bounds[2], workout.bounds[3]],
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        {jsonData && (
 | 
			
		||||
          <MapContainer
 | 
			
		||||
            zoom={this.state.zoom}
 | 
			
		||||
            bounds={bounds}
 | 
			
		||||
            boundsOptions={{ padding: [10, 10] }}
 | 
			
		||||
          >
 | 
			
		||||
            <Map
 | 
			
		||||
              bounds={bounds}
 | 
			
		||||
              coordinates={coordinates}
 | 
			
		||||
              jsonData={jsonData}
 | 
			
		||||
              mapAttribution={mapAttribution}
 | 
			
		||||
            />
 | 
			
		||||
          </MapContainer>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(
 | 
			
		||||
  state => ({
 | 
			
		||||
    gpxContent: state.gpx,
 | 
			
		||||
    mapAttribution: state.application.config.map_attribution,
 | 
			
		||||
  }),
 | 
			
		||||
  dispatch => ({
 | 
			
		||||
    loadWorkoutGpx: workoutId => {
 | 
			
		||||
      dispatch(getWorkoutGpx(workoutId))
 | 
			
		||||
    },
 | 
			
		||||
    loadSegmentGpx: (workoutId, segmentId) => {
 | 
			
		||||
      dispatch(getSegmentGpx(workoutId, segmentId))
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
)(WorkoutMap)
 | 
			
		||||
@@ -1,8 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
export default function WorkoutNoMap(props) {
 | 
			
		||||
  const { t } = props
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="workout-no-map text-center">{t('workouts:No Map')}</div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
export default function WorkoutNotes(props) {
 | 
			
		||||
  const { notes, t } = props
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="row">
 | 
			
		||||
      <div className="col">
 | 
			
		||||
        <div className="card workout-card">
 | 
			
		||||
          <div className="card-body">
 | 
			
		||||
            Notes
 | 
			
		||||
            <div className="workout-notes">
 | 
			
		||||
              {notes && notes !== '' ? notes : t('workouts:No notes')}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,38 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Link } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
export default function WorkoutSegments(props) {
 | 
			
		||||
  const { segments, t } = props
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="row">
 | 
			
		||||
      <div className="col">
 | 
			
		||||
        <div className="card workout-card">
 | 
			
		||||
          <div className="card-body">
 | 
			
		||||
            {t('workouts:Segments')}
 | 
			
		||||
            <div className="workout-segments">
 | 
			
		||||
              <ul>
 | 
			
		||||
                {segments.map((segment, index) => (
 | 
			
		||||
                  <li
 | 
			
		||||
                    className="workout-segments-list"
 | 
			
		||||
                    // eslint-disable-next-line react/no-array-index-key
 | 
			
		||||
                    key={`segment-${index}`}
 | 
			
		||||
                  >
 | 
			
		||||
                    <Link
 | 
			
		||||
                      to={`/workouts/${segment.workout_id}/segment/${
 | 
			
		||||
                        index + 1
 | 
			
		||||
                      }`}
 | 
			
		||||
                    >
 | 
			
		||||
                      {t('workouts:segment')} {index + 1}
 | 
			
		||||
                    </Link>{' '}
 | 
			
		||||
                    ({t('workouts:distance')}: {segment.distance} km,{' '}
 | 
			
		||||
                    {t('workouts:duration')}: {segment.duration})
 | 
			
		||||
                  </li>
 | 
			
		||||
                ))}
 | 
			
		||||
              </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,75 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
export default function WorkoutWeather(props) {
 | 
			
		||||
  const { t, workout } = props
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="container">
 | 
			
		||||
      {workout.weather_start && workout.weather_end && (
 | 
			
		||||
        <table className="table table-borderless weather-table text-center">
 | 
			
		||||
          <thead>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <th />
 | 
			
		||||
              <th>
 | 
			
		||||
                {t('workouts:Start')}
 | 
			
		||||
                <br />
 | 
			
		||||
                <img
 | 
			
		||||
                  className="weather-img"
 | 
			
		||||
                  src={`/img/weather/${workout.weather_start.icon}.png`}
 | 
			
		||||
                  alt={`workout weather (${workout.weather_start.icon})`}
 | 
			
		||||
                  title={workout.weather_start.summary}
 | 
			
		||||
                />
 | 
			
		||||
              </th>
 | 
			
		||||
              <th>
 | 
			
		||||
                {t('workouts:End')}
 | 
			
		||||
                <br />
 | 
			
		||||
                <img
 | 
			
		||||
                  className="weather-img"
 | 
			
		||||
                  src={`/img/weather/${workout.weather_end.icon}.png`}
 | 
			
		||||
                  alt={`workout weather (${workout.weather_end.icon})`}
 | 
			
		||||
                  title={workout.weather_end.summary}
 | 
			
		||||
                />
 | 
			
		||||
              </th>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </thead>
 | 
			
		||||
          <tbody>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td>
 | 
			
		||||
                <img
 | 
			
		||||
                  className="weather-img-small"
 | 
			
		||||
                  src="/img/weather/temperature.png"
 | 
			
		||||
                  alt="Temperatures"
 | 
			
		||||
                />
 | 
			
		||||
              </td>
 | 
			
		||||
              <td>{Number(workout.weather_start.temperature).toFixed(1)}°C</td>
 | 
			
		||||
              <td>{Number(workout.weather_end.temperature).toFixed(1)}°C</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td>
 | 
			
		||||
                <img
 | 
			
		||||
                  className="weather-img-small"
 | 
			
		||||
                  src="/img/weather/pour-rain.png"
 | 
			
		||||
                  alt="Temperatures"
 | 
			
		||||
                />
 | 
			
		||||
              </td>
 | 
			
		||||
              <td>
 | 
			
		||||
                {Number(workout.weather_start.humidity * 100).toFixed(1)}%
 | 
			
		||||
              </td>
 | 
			
		||||
              <td>{Number(workout.weather_end.humidity * 100).toFixed(1)}%</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
            <tr>
 | 
			
		||||
              <td>
 | 
			
		||||
                <img
 | 
			
		||||
                  className="weather-img-small"
 | 
			
		||||
                  src="/img/weather/breeze.png"
 | 
			
		||||
                  alt="Temperatures"
 | 
			
		||||
                />
 | 
			
		||||
              </td>
 | 
			
		||||
              <td>{Number(workout.weather_start.wind).toFixed(1)}m/s</td>
 | 
			
		||||
              <td>{Number(workout.weather_end.wind).toFixed(1)}m/s</td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@@ -1,202 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Helmet } from 'react-helmet'
 | 
			
		||||
import { withTranslation } from 'react-i18next'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import CustomModal from '../../Common/CustomModal'
 | 
			
		||||
import Message from '../../Common/Message'
 | 
			
		||||
import WorkoutCardHeader from './WorkoutCardHeader'
 | 
			
		||||
import WorkoutCharts from './WorkoutCharts'
 | 
			
		||||
import WorkoutDetails from './WorkoutDetails'
 | 
			
		||||
import WorkoutMap from './WorkoutMap'
 | 
			
		||||
import WorkoutNoMap from './WorkoutNoMap'
 | 
			
		||||
import WorkoutNotes from './WorkoutNotes'
 | 
			
		||||
import WorkoutSegments from './WorkoutSegments'
 | 
			
		||||
import { getOrUpdateData } from '../../../actions'
 | 
			
		||||
import { deleteWorkout } from '../../../actions/workouts'
 | 
			
		||||
 | 
			
		||||
class WorkoutDisplay extends React.Component {
 | 
			
		||||
  constructor(props, context) {
 | 
			
		||||
    super(props, context)
 | 
			
		||||
    this.state = {
 | 
			
		||||
      displayModal: false,
 | 
			
		||||
      coordinates: {
 | 
			
		||||
        latitude: null,
 | 
			
		||||
        longitude: null,
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.loadWorkout(this.props.match.params.workoutId)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidUpdate(prevProps) {
 | 
			
		||||
    if (
 | 
			
		||||
      prevProps.match.params.workoutId !== this.props.match.params.workoutId
 | 
			
		||||
    ) {
 | 
			
		||||
      this.props.loadWorkout(this.props.match.params.workoutId)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  displayModal(value) {
 | 
			
		||||
    this.setState(prevState => ({
 | 
			
		||||
      ...prevState,
 | 
			
		||||
      displayModal: value,
 | 
			
		||||
    }))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateCoordinates(activePayload) {
 | 
			
		||||
    const coordinates =
 | 
			
		||||
      activePayload && activePayload.length > 0
 | 
			
		||||
        ? {
 | 
			
		||||
            latitude: activePayload[0].payload.latitude,
 | 
			
		||||
            longitude: activePayload[0].payload.longitude,
 | 
			
		||||
          }
 | 
			
		||||
        : {
 | 
			
		||||
            latitude: null,
 | 
			
		||||
            longitude: null,
 | 
			
		||||
          }
 | 
			
		||||
    this.setState(prevState => ({
 | 
			
		||||
      ...prevState,
 | 
			
		||||
      coordinates,
 | 
			
		||||
    }))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { message, onDeleteWorkout, sports, t, user, workouts } = this.props
 | 
			
		||||
    const { coordinates, displayModal } = this.state
 | 
			
		||||
    const [workout] = workouts
 | 
			
		||||
    const title = workout ? workout.title : t('workouts:Workout')
 | 
			
		||||
    const [sport] = workout ? sports.filter(s => s.id === workout.sport_id) : []
 | 
			
		||||
    const segmentId = parseInt(this.props.match.params.segmentId)
 | 
			
		||||
    const dataType = segmentId >= 0 ? 'segment' : 'workout'
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="workout-page">
 | 
			
		||||
        <Helmet>
 | 
			
		||||
          <title>FitTrackee - {title}</title>
 | 
			
		||||
        </Helmet>
 | 
			
		||||
        {message ? (
 | 
			
		||||
          <Message message={message} t={t} />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <div className="container">
 | 
			
		||||
            {displayModal && (
 | 
			
		||||
              <CustomModal
 | 
			
		||||
                title={t('common:Confirmation')}
 | 
			
		||||
                text={t(
 | 
			
		||||
                  'workouts:Are you sure you want to delete this workout?'
 | 
			
		||||
                )}
 | 
			
		||||
                confirm={() => {
 | 
			
		||||
                  onDeleteWorkout(workout.id)
 | 
			
		||||
                  this.displayModal(false)
 | 
			
		||||
                }}
 | 
			
		||||
                close={() => this.displayModal(false)}
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            {workout && sport && workouts.length === 1 && (
 | 
			
		||||
              <div>
 | 
			
		||||
                <div className="row">
 | 
			
		||||
                  <div className="col">
 | 
			
		||||
                    <div className="card workout-card">
 | 
			
		||||
                      <div className="card-header">
 | 
			
		||||
                        <WorkoutCardHeader
 | 
			
		||||
                          workout={workout}
 | 
			
		||||
                          dataType={dataType}
 | 
			
		||||
                          segmentId={segmentId}
 | 
			
		||||
                          sport={sport}
 | 
			
		||||
                          t={t}
 | 
			
		||||
                          title={title}
 | 
			
		||||
                          user={user}
 | 
			
		||||
                          displayModal={() => this.displayModal(true)}
 | 
			
		||||
                        />
 | 
			
		||||
                      </div>
 | 
			
		||||
                      <div className="card-body">
 | 
			
		||||
                        <div className="row">
 | 
			
		||||
                          <div className="col-md-8">
 | 
			
		||||
                            {workout.with_gpx ? (
 | 
			
		||||
                              <WorkoutMap
 | 
			
		||||
                                workout={workout}
 | 
			
		||||
                                coordinates={coordinates}
 | 
			
		||||
                                dataType={dataType}
 | 
			
		||||
                                segmentId={segmentId}
 | 
			
		||||
                              />
 | 
			
		||||
                            ) : (
 | 
			
		||||
                              <WorkoutNoMap t={t} />
 | 
			
		||||
                            )}
 | 
			
		||||
                          </div>
 | 
			
		||||
                          <div className="col">
 | 
			
		||||
                            <WorkoutDetails
 | 
			
		||||
                              workout={
 | 
			
		||||
                                dataType === 'workout'
 | 
			
		||||
                                  ? workout
 | 
			
		||||
                                  : workout.segments[segmentId - 1]
 | 
			
		||||
                              }
 | 
			
		||||
                              t={t}
 | 
			
		||||
                            />
 | 
			
		||||
                          </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
                {workout.with_gpx && (
 | 
			
		||||
                  <div className="row">
 | 
			
		||||
                    <div className="col">
 | 
			
		||||
                      <div className="card workout-card">
 | 
			
		||||
                        <div className="card-body">
 | 
			
		||||
                          <div className="row">
 | 
			
		||||
                            <div className="col">
 | 
			
		||||
                              <div className="chart-title">
 | 
			
		||||
                                {t('workouts:Chart')}
 | 
			
		||||
                              </div>
 | 
			
		||||
                              <WorkoutCharts
 | 
			
		||||
                                workout={workout}
 | 
			
		||||
                                dataType={dataType}
 | 
			
		||||
                                segmentId={segmentId}
 | 
			
		||||
                                t={t}
 | 
			
		||||
                                updateCoordinates={e =>
 | 
			
		||||
                                  this.updateCoordinates(e)
 | 
			
		||||
                                }
 | 
			
		||||
                              />
 | 
			
		||||
                            </div>
 | 
			
		||||
                          </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                )}
 | 
			
		||||
                {dataType === 'workout' && (
 | 
			
		||||
                  <>
 | 
			
		||||
                    <WorkoutNotes notes={workout.notes} t={t} />
 | 
			
		||||
                    {workout.segments.length > 1 && (
 | 
			
		||||
                      <WorkoutSegments segments={workout.segments} t={t} />
 | 
			
		||||
                    )}
 | 
			
		||||
                  </>
 | 
			
		||||
                )}
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withTranslation()(
 | 
			
		||||
  connect(
 | 
			
		||||
    state => ({
 | 
			
		||||
      workouts: state.workouts.data,
 | 
			
		||||
      message: state.message,
 | 
			
		||||
      sports: state.sports.data,
 | 
			
		||||
      user: state.user,
 | 
			
		||||
    }),
 | 
			
		||||
    dispatch => ({
 | 
			
		||||
      loadWorkout: workoutId => {
 | 
			
		||||
        dispatch(getOrUpdateData('getData', 'workouts', { id: workoutId }))
 | 
			
		||||
      },
 | 
			
		||||
      onDeleteWorkout: workoutId => {
 | 
			
		||||
        dispatch(deleteWorkout(workoutId))
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  )(WorkoutDisplay)
 | 
			
		||||
)
 | 
			
		||||
@@ -1,41 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import WorkoutAddOrEdit from './WorkoutAddOrEdit'
 | 
			
		||||
import { getOrUpdateData } from '../../actions'
 | 
			
		||||
 | 
			
		||||
class WorkoutEdit extends React.Component {
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.loadWorkout(this.props.match.params.workoutId)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const { message, sports, workouts } = this.props
 | 
			
		||||
    const [workout] = workouts
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        {sports.length > 0 && (
 | 
			
		||||
          <WorkoutAddOrEdit
 | 
			
		||||
            workout={workout}
 | 
			
		||||
            message={message}
 | 
			
		||||
            sports={sports}
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(
 | 
			
		||||
  state => ({
 | 
			
		||||
    workouts: state.workouts.data,
 | 
			
		||||
    message: state.message,
 | 
			
		||||
    sports: state.sports.data,
 | 
			
		||||
    user: state.user,
 | 
			
		||||
  }),
 | 
			
		||||
  dispatch => ({
 | 
			
		||||
    loadWorkout: workoutId => {
 | 
			
		||||
      dispatch(getOrUpdateData('getData', 'workouts', { id: workoutId }))
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
)(WorkoutEdit)
 | 
			
		||||
@@ -1,170 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Trans } from 'react-i18next'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import { setLoading } from '../../../actions/index'
 | 
			
		||||
import { addWorkout, editWorkout } from '../../../actions/workouts'
 | 
			
		||||
import { history } from '../../../index'
 | 
			
		||||
import { getFileSize } from '../../../utils'
 | 
			
		||||
import { translateSports } from '../../../utils/workouts'
 | 
			
		||||
import CustomTextArea from '../../Common/CustomTextArea'
 | 
			
		||||
 | 
			
		||||
function FormWithGpx(props) {
 | 
			
		||||
  const {
 | 
			
		||||
    appConfig,
 | 
			
		||||
    loading,
 | 
			
		||||
    onAddWorkout,
 | 
			
		||||
    onEditWorkout,
 | 
			
		||||
    sports,
 | 
			
		||||
    t,
 | 
			
		||||
    workout,
 | 
			
		||||
  } = props
 | 
			
		||||
  const sportId = workout ? workout.sport_id : ''
 | 
			
		||||
  const translatedSports = translateSports(sports, t, true)
 | 
			
		||||
  const zipTooltip = `${t('workouts:no folder inside')}, ${
 | 
			
		||||
    appConfig.gpx_limit_import
 | 
			
		||||
  } ${t('workouts:files max')}, ${t('workouts:max size')}: ${getFileSize(
 | 
			
		||||
    appConfig.max_zip_file_size
 | 
			
		||||
  )}`
 | 
			
		||||
  const fileSizeLimit = getFileSize(appConfig.max_single_file_size)
 | 
			
		||||
  return (
 | 
			
		||||
    <form
 | 
			
		||||
      encType="multipart/form-data"
 | 
			
		||||
      method="post"
 | 
			
		||||
      onSubmit={event => event.preventDefault()}
 | 
			
		||||
    >
 | 
			
		||||
      <div className="form-group">
 | 
			
		||||
        <label>
 | 
			
		||||
          {t('common:Sport')}:
 | 
			
		||||
          <select
 | 
			
		||||
            className="form-control input-lg"
 | 
			
		||||
            defaultValue={sportId}
 | 
			
		||||
            disabled={loading}
 | 
			
		||||
            name="sport"
 | 
			
		||||
            required
 | 
			
		||||
          >
 | 
			
		||||
            <option value="" />
 | 
			
		||||
            {translatedSports.map(sport => (
 | 
			
		||||
              <option key={sport.id} value={sport.id}>
 | 
			
		||||
                {sport.label}
 | 
			
		||||
              </option>
 | 
			
		||||
            ))}
 | 
			
		||||
          </select>
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
      {workout ? (
 | 
			
		||||
        <div className="form-group">
 | 
			
		||||
          <label>
 | 
			
		||||
            {t('workouts:Title')}:
 | 
			
		||||
            <input
 | 
			
		||||
              name="title"
 | 
			
		||||
              defaultValue={workout ? workout.title : ''}
 | 
			
		||||
              disabled={loading}
 | 
			
		||||
              className="form-control input-lg"
 | 
			
		||||
            />
 | 
			
		||||
          </label>
 | 
			
		||||
        </div>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <div className="form-group">
 | 
			
		||||
          <label>
 | 
			
		||||
            <Trans i18nKey="workouts:gpxFile">
 | 
			
		||||
              <strong>gpx</strong> file
 | 
			
		||||
            </Trans>
 | 
			
		||||
            <sup>
 | 
			
		||||
              <i
 | 
			
		||||
                className="fa fa-question-circle"
 | 
			
		||||
                aria-hidden="true"
 | 
			
		||||
                data-toggle="tooltip"
 | 
			
		||||
                title={`${t('workouts:max size')}: ${fileSizeLimit}`}
 | 
			
		||||
              />
 | 
			
		||||
            </sup>{' '}
 | 
			
		||||
            <Trans i18nKey="workouts:zipFile">
 | 
			
		||||
              or <strong> zip</strong> file containing <strong>gpx </strong>
 | 
			
		||||
              files
 | 
			
		||||
            </Trans>
 | 
			
		||||
            <sup>
 | 
			
		||||
              <i
 | 
			
		||||
                className="fa fa-question-circle"
 | 
			
		||||
                aria-hidden="true"
 | 
			
		||||
                data-toggle="tooltip"
 | 
			
		||||
                data-placement="top"
 | 
			
		||||
                title={zipTooltip}
 | 
			
		||||
              />
 | 
			
		||||
            </sup>{' '}
 | 
			
		||||
            :
 | 
			
		||||
            <input
 | 
			
		||||
              accept=".gpx, .zip"
 | 
			
		||||
              className="form-control form-control-file gpx-file"
 | 
			
		||||
              disabled={loading}
 | 
			
		||||
              name="gpxFile"
 | 
			
		||||
              required
 | 
			
		||||
              type="file"
 | 
			
		||||
            />
 | 
			
		||||
          </label>
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      <div className="form-group">
 | 
			
		||||
        <label>
 | 
			
		||||
          {t('workouts:Notes')}:
 | 
			
		||||
          <CustomTextArea
 | 
			
		||||
            charLimit={500}
 | 
			
		||||
            defaultValue={workout ? workout.notes : ''}
 | 
			
		||||
            loading={loading}
 | 
			
		||||
            name="notes"
 | 
			
		||||
          />
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
      {loading ? (
 | 
			
		||||
        <div className="loader" />
 | 
			
		||||
      ) : (
 | 
			
		||||
        <div>
 | 
			
		||||
          <input
 | 
			
		||||
            type="submit"
 | 
			
		||||
            className="btn btn-primary"
 | 
			
		||||
            onClick={event =>
 | 
			
		||||
              workout ? onEditWorkout(event, workout) : onAddWorkout(event)
 | 
			
		||||
            }
 | 
			
		||||
            value={t('common:Submit')}
 | 
			
		||||
          />
 | 
			
		||||
          <input
 | 
			
		||||
            type="submit"
 | 
			
		||||
            className="btn btn-secondary"
 | 
			
		||||
            onClick={() => history.push('/')}
 | 
			
		||||
            value={t('common:Cancel')}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
    </form>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(
 | 
			
		||||
  state => ({
 | 
			
		||||
    appConfig: state.application.config,
 | 
			
		||||
    loading: state.loading,
 | 
			
		||||
  }),
 | 
			
		||||
  dispatch => ({
 | 
			
		||||
    onAddWorkout: e => {
 | 
			
		||||
      dispatch(setLoading(true))
 | 
			
		||||
      const form = new FormData()
 | 
			
		||||
      form.append('file', e.target.form.gpxFile.files[0])
 | 
			
		||||
      /* prettier-ignore */
 | 
			
		||||
      form.append(
 | 
			
		||||
        'data',
 | 
			
		||||
        `{"sport_id": ${e.target.form.sport.value
 | 
			
		||||
        }, "notes": "${e.target.form.notes.value}"}`
 | 
			
		||||
      )
 | 
			
		||||
      dispatch(addWorkout(form))
 | 
			
		||||
    },
 | 
			
		||||
    onEditWorkout: (e, workout) => {
 | 
			
		||||
      dispatch(
 | 
			
		||||
        editWorkout({
 | 
			
		||||
          id: workout.id,
 | 
			
		||||
          notes: e.target.form.notes.value,
 | 
			
		||||
          sport_id: +e.target.form.sport.value,
 | 
			
		||||
          title: e.target.form.title.value,
 | 
			
		||||
        })
 | 
			
		||||
      )
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
)(FormWithGpx)
 | 
			
		||||
@@ -1,162 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import { addWorkoutWithoutGpx, editWorkout } from '../../../actions/workouts'
 | 
			
		||||
import { history } from '../../../index'
 | 
			
		||||
import { getDateWithTZ } from '../../../utils'
 | 
			
		||||
import { formatWorkoutDate, translateSports } from '../../../utils/workouts'
 | 
			
		||||
import CustomTextArea from '../../Common/CustomTextArea'
 | 
			
		||||
 | 
			
		||||
function FormWithoutGpx(props) {
 | 
			
		||||
  const { onAddOrEdit, sports, t, user, workout } = props
 | 
			
		||||
  const translatedSports = translateSports(sports, t, true)
 | 
			
		||||
  let workoutDate,
 | 
			
		||||
    workoutTime,
 | 
			
		||||
    sportId = ''
 | 
			
		||||
  if (workout) {
 | 
			
		||||
    const workoutDateTime = formatWorkoutDate(
 | 
			
		||||
      getDateWithTZ(workout.workout_date, user.timezone),
 | 
			
		||||
      'yyyy-MM-dd'
 | 
			
		||||
    )
 | 
			
		||||
    workoutDate = workoutDateTime.workout_date
 | 
			
		||||
    workoutTime = workoutDateTime.workout_time
 | 
			
		||||
    sportId = workout.sport_id
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <form onSubmit={event => event.preventDefault()}>
 | 
			
		||||
      <div className="form-group">
 | 
			
		||||
        <label>
 | 
			
		||||
          {t('workouts:Title')}:
 | 
			
		||||
          <input
 | 
			
		||||
            name="title"
 | 
			
		||||
            defaultValue={workout ? workout.title : ''}
 | 
			
		||||
            className="form-control input-lg"
 | 
			
		||||
          />
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="form-group">
 | 
			
		||||
        <label>
 | 
			
		||||
          {t('common:Sport')}:
 | 
			
		||||
          <select
 | 
			
		||||
            className="form-control input-lg"
 | 
			
		||||
            defaultValue={sportId}
 | 
			
		||||
            name="sport_id"
 | 
			
		||||
            required
 | 
			
		||||
          >
 | 
			
		||||
            <option value="" />
 | 
			
		||||
            {translatedSports.map(sport => (
 | 
			
		||||
              <option key={sport.id} value={sport.id}>
 | 
			
		||||
                {sport.label}
 | 
			
		||||
              </option>
 | 
			
		||||
            ))}
 | 
			
		||||
          </select>
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="form-group">
 | 
			
		||||
        <label>
 | 
			
		||||
          {t('workouts:Workout Date')}:
 | 
			
		||||
          <div className="container">
 | 
			
		||||
            <div className="row">
 | 
			
		||||
              <input
 | 
			
		||||
                name="workout_date"
 | 
			
		||||
                defaultValue={workoutDate}
 | 
			
		||||
                className="form-control col-md"
 | 
			
		||||
                required
 | 
			
		||||
                type="date"
 | 
			
		||||
              />
 | 
			
		||||
              <input
 | 
			
		||||
                name="workout_time"
 | 
			
		||||
                defaultValue={workoutTime}
 | 
			
		||||
                className="form-control col-md"
 | 
			
		||||
                required
 | 
			
		||||
                type="time"
 | 
			
		||||
              />
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="form-group">
 | 
			
		||||
        <label>
 | 
			
		||||
          {t('workouts:Duration')}:
 | 
			
		||||
          <input
 | 
			
		||||
            name="duration"
 | 
			
		||||
            defaultValue={workout ? workout.duration : ''}
 | 
			
		||||
            className="form-control col-xs-4"
 | 
			
		||||
            pattern="^([0-9]*[0-9]):([0-5][0-9]):([0-5][0-9])$"
 | 
			
		||||
            placeholder="hh:mm:ss"
 | 
			
		||||
            required
 | 
			
		||||
            type="text"
 | 
			
		||||
          />
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="form-group">
 | 
			
		||||
        <label>
 | 
			
		||||
          {t('workouts:Distance')} (km):
 | 
			
		||||
          <input
 | 
			
		||||
            name="distance"
 | 
			
		||||
            defaultValue={workout ? workout.distance : ''}
 | 
			
		||||
            className="form-control input-lg"
 | 
			
		||||
            min={0}
 | 
			
		||||
            required
 | 
			
		||||
            step="0.001"
 | 
			
		||||
            type="number"
 | 
			
		||||
          />
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div className="form-group">
 | 
			
		||||
        <label>
 | 
			
		||||
          {t('workouts:Notes')}:
 | 
			
		||||
          <CustomTextArea
 | 
			
		||||
            charLimit={500}
 | 
			
		||||
            defaultValue={workout ? workout.notes : ''}
 | 
			
		||||
            name="notes"
 | 
			
		||||
          />
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
      <input
 | 
			
		||||
        type="submit"
 | 
			
		||||
        className="btn btn-primary"
 | 
			
		||||
        onClick={event => onAddOrEdit(event, workout)}
 | 
			
		||||
        value={t('common:Submit')}
 | 
			
		||||
      />
 | 
			
		||||
      <input
 | 
			
		||||
        type="submit"
 | 
			
		||||
        className="btn btn-secondary"
 | 
			
		||||
        onClick={() => history.push('/')}
 | 
			
		||||
        value={t('common:Cancel')}
 | 
			
		||||
      />
 | 
			
		||||
    </form>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(
 | 
			
		||||
  state => ({
 | 
			
		||||
    user: state.user,
 | 
			
		||||
  }),
 | 
			
		||||
  dispatch => ({
 | 
			
		||||
    onAddOrEdit: (e, workout) => {
 | 
			
		||||
      const d = e.target.form.duration.value.split(':')
 | 
			
		||||
      const duration = +d[0] * 60 * 60 + +d[1] * 60 + +d[2]
 | 
			
		||||
 | 
			
		||||
      /* prettier-ignore */
 | 
			
		||||
      const workoutDate = `${e.target.form.workout_date.value
 | 
			
		||||
        } ${ e.target.form.workout_time.value}`
 | 
			
		||||
 | 
			
		||||
      const data = {
 | 
			
		||||
        workout_date: workoutDate,
 | 
			
		||||
        distance: +e.target.form.distance.value,
 | 
			
		||||
        duration,
 | 
			
		||||
        notes: e.target.form.notes.value,
 | 
			
		||||
        sport_id: +e.target.form.sport_id.value,
 | 
			
		||||
        title: e.target.form.title.value,
 | 
			
		||||
      }
 | 
			
		||||
      if (workout) {
 | 
			
		||||
        data.id = workout.id
 | 
			
		||||
        dispatch(editWorkout(data))
 | 
			
		||||
      } else {
 | 
			
		||||
        dispatch(addWorkoutWithoutGpx(data))
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
)(FormWithoutGpx)
 | 
			
		||||
@@ -1,38 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
import { Redirect, Route, Switch } from 'react-router-dom'
 | 
			
		||||
 | 
			
		||||
import NotFound from './../Others/NotFound'
 | 
			
		||||
import WorkoutAdd from './WorkoutAdd'
 | 
			
		||||
import WorkoutDisplay from './WorkoutDisplay'
 | 
			
		||||
import WorkoutEdit from './WorkoutEdit'
 | 
			
		||||
import { isLoggedIn } from '../../utils'
 | 
			
		||||
 | 
			
		||||
function Workout() {
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      {isLoggedIn() ? (
 | 
			
		||||
        <Switch>
 | 
			
		||||
          <Route exact path="/workouts/add" component={WorkoutAdd} />
 | 
			
		||||
          <Route exact path="/workouts/:workoutId" component={WorkoutDisplay} />
 | 
			
		||||
          <Route
 | 
			
		||||
            exact
 | 
			
		||||
            path="/workouts/:workoutId/edit"
 | 
			
		||||
            component={WorkoutEdit}
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/workouts/:workoutId/segment/:segmentId"
 | 
			
		||||
            component={WorkoutDisplay}
 | 
			
		||||
          />
 | 
			
		||||
          <Route component={NotFound} />
 | 
			
		||||
        </Switch>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <Redirect to="/login" />
 | 
			
		||||
      )}
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default connect(state => ({
 | 
			
		||||
  user: state.user,
 | 
			
		||||
}))(Workout)
 | 
			
		||||
@@ -1,189 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
 | 
			
		||||
import { translateSports } from '../../utils/workouts'
 | 
			
		||||
 | 
			
		||||
export default class WorkoutsFilter extends React.PureComponent {
 | 
			
		||||
  render() {
 | 
			
		||||
    const { loadWorkouts, sports, t, updateParams } = this.props
 | 
			
		||||
    const translatedSports = translateSports(sports, t)
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="card">
 | 
			
		||||
        <div className="card-body workout-filter">
 | 
			
		||||
          <form onSubmit={event => event.preventDefault()}>
 | 
			
		||||
            <div className="form-group">
 | 
			
		||||
              <label>
 | 
			
		||||
                {t('workouts:From')}:
 | 
			
		||||
                <input
 | 
			
		||||
                  className="form-control col-md"
 | 
			
		||||
                  name="from"
 | 
			
		||||
                  onChange={e => updateParams(e)}
 | 
			
		||||
                  type="date"
 | 
			
		||||
                />
 | 
			
		||||
              </label>
 | 
			
		||||
              <label>
 | 
			
		||||
                {t('workouts:To')}:
 | 
			
		||||
                <input
 | 
			
		||||
                  className="form-control col-md"
 | 
			
		||||
                  name="to"
 | 
			
		||||
                  onChange={e => updateParams(e)}
 | 
			
		||||
                  type="date"
 | 
			
		||||
                />
 | 
			
		||||
              </label>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="form-group">
 | 
			
		||||
              <label>
 | 
			
		||||
                {t('common:Sport')}:
 | 
			
		||||
                <select
 | 
			
		||||
                  className="form-control input-lg"
 | 
			
		||||
                  name="sport_id"
 | 
			
		||||
                  onChange={e => updateParams(e)}
 | 
			
		||||
                >
 | 
			
		||||
                  <option value="" />
 | 
			
		||||
                  {translatedSports.map(sport => (
 | 
			
		||||
                    <option key={sport.id} value={sport.id}>
 | 
			
		||||
                      {sport.label}
 | 
			
		||||
                    </option>
 | 
			
		||||
                  ))}
 | 
			
		||||
                </select>
 | 
			
		||||
              </label>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="form-group">
 | 
			
		||||
              <label>
 | 
			
		||||
                {t('workouts:Distance')} (km):
 | 
			
		||||
                <div className="container">
 | 
			
		||||
                  <div className="row">
 | 
			
		||||
                    <div className="col-5">
 | 
			
		||||
                      <input
 | 
			
		||||
                        className="form-control"
 | 
			
		||||
                        min={0}
 | 
			
		||||
                        name="distance_from"
 | 
			
		||||
                        onChange={e => updateParams(e)}
 | 
			
		||||
                        step="1"
 | 
			
		||||
                        type="number"
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="col-2 align-middle text-center">
 | 
			
		||||
                      {t('common:to')}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="col-5">
 | 
			
		||||
                      <input
 | 
			
		||||
                        className="form-control"
 | 
			
		||||
                        min={0}
 | 
			
		||||
                        name="distance_to"
 | 
			
		||||
                        onChange={e => updateParams(e)}
 | 
			
		||||
                        step="1"
 | 
			
		||||
                        type="number"
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </label>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="form-group">
 | 
			
		||||
              <label>
 | 
			
		||||
                {t('workouts:Duration')}:
 | 
			
		||||
                <div className="container">
 | 
			
		||||
                  <div className="row">
 | 
			
		||||
                    <div className="col-5">
 | 
			
		||||
                      <input
 | 
			
		||||
                        className="form-control"
 | 
			
		||||
                        name="duration_from"
 | 
			
		||||
                        onChange={e => updateParams(e)}
 | 
			
		||||
                        pattern="^([0-9]*[0-9]):([0-5][0-9])$"
 | 
			
		||||
                        placeholder="hh:mm"
 | 
			
		||||
                        type="text"
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="col-2 align-middle text-center">
 | 
			
		||||
                      {t('common:to')}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="col-5">
 | 
			
		||||
                      <input
 | 
			
		||||
                        className="form-control"
 | 
			
		||||
                        name="duration_to"
 | 
			
		||||
                        onChange={e => updateParams(e)}
 | 
			
		||||
                        pattern="^([0-9]*[0-9]):([0-5][0-9])$"
 | 
			
		||||
                        placeholder="hh:mm"
 | 
			
		||||
                        type="text"
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </label>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="form-group">
 | 
			
		||||
              <label>
 | 
			
		||||
                {t('workouts:Average speed')} (km/h):
 | 
			
		||||
                <div className="container">
 | 
			
		||||
                  <div className="row">
 | 
			
		||||
                    <div className="col-5">
 | 
			
		||||
                      <input
 | 
			
		||||
                        className="form-control"
 | 
			
		||||
                        min={0}
 | 
			
		||||
                        name="ave_speed_from"
 | 
			
		||||
                        onChange={e => updateParams(e)}
 | 
			
		||||
                        step="1"
 | 
			
		||||
                        type="number"
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="col-2 align-middle text-center">
 | 
			
		||||
                      {t('common:to')}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="col-5">
 | 
			
		||||
                      <input
 | 
			
		||||
                        className="form-control"
 | 
			
		||||
                        min={0}
 | 
			
		||||
                        name="ave_speed_to"
 | 
			
		||||
                        onChange={e => updateParams(e)}
 | 
			
		||||
                        step="1"
 | 
			
		||||
                        type="number"
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </label>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div className="form-group">
 | 
			
		||||
              <label>
 | 
			
		||||
                {t('workouts:Max. speed')} (km/h):
 | 
			
		||||
                <div className="container">
 | 
			
		||||
                  <div className="row">
 | 
			
		||||
                    <div className="col-5">
 | 
			
		||||
                      <input
 | 
			
		||||
                        className="form-control"
 | 
			
		||||
                        min={0}
 | 
			
		||||
                        name="max_speed_from"
 | 
			
		||||
                        onChange={e => updateParams(e)}
 | 
			
		||||
                        step="1"
 | 
			
		||||
                        type="number"
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="col-2 align-middle text-center">
 | 
			
		||||
                      {t('common:to')}
 | 
			
		||||
                    </div>
 | 
			
		||||
                    <div className="col-5">
 | 
			
		||||
                      <input
 | 
			
		||||
                        className="form-control"
 | 
			
		||||
                        min={0}
 | 
			
		||||
                        name="max_speed_to"
 | 
			
		||||
                        onChange={e => updateParams(e)}
 | 
			
		||||
                        step="1"
 | 
			
		||||
                        type="number"
 | 
			
		||||
                      />
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </label>
 | 
			
		||||
            </div>
 | 
			
		||||
            <input
 | 
			
		||||
              className="btn btn-primary btn-lg btn-block"
 | 
			
		||||
              onClick={() => loadWorkouts()}
 | 
			
		||||
              type="submit"
 | 
			
		||||
              value={t('workouts:Filter')}
 | 
			
		||||
            />
 | 
			
		||||
          </form>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,120 +0,0 @@
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { Helmet } from 'react-helmet'
 | 
			
		||||
import { withTranslation } from 'react-i18next'
 | 
			
		||||
import { connect } from 'react-redux'
 | 
			
		||||
 | 
			
		||||
import Message from '../Common/Message'
 | 
			
		||||
import NoWorkouts from '../Common/NoWorkouts'
 | 
			
		||||
import WorkoutsFilter from './WorkoutsFilter'
 | 
			
		||||
import WorkoutsList from './WorkoutsList'
 | 
			
		||||
import { getOrUpdateData } from '../../actions'
 | 
			
		||||
import { getMoreWorkouts } from '../../actions/workouts'
 | 
			
		||||
 | 
			
		||||
class Workouts extends React.Component {
 | 
			
		||||
  constructor(props, context) {
 | 
			
		||||
    super(props, context)
 | 
			
		||||
    this.state = {
 | 
			
		||||
      params: {
 | 
			
		||||
        page: 1,
 | 
			
		||||
        per_page: 10,
 | 
			
		||||
      },
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  componentDidMount() {
 | 
			
		||||
    this.props.loadWorkouts(this.state.params)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setParams(e) {
 | 
			
		||||
    const { params } = this.state
 | 
			
		||||
    if (e.target.value === '') {
 | 
			
		||||
      delete params[e.target.name]
 | 
			
		||||
    } else {
 | 
			
		||||
      params[e.target.name] = e.target.value
 | 
			
		||||
    }
 | 
			
		||||
    params.page = 1
 | 
			
		||||
    this.setState(params)
 | 
			
		||||
  }
 | 
			
		||||
  render() {
 | 
			
		||||
    const {
 | 
			
		||||
      loading,
 | 
			
		||||
      loadWorkouts,
 | 
			
		||||
      loadMoreWorkouts,
 | 
			
		||||
      message,
 | 
			
		||||
      sports,
 | 
			
		||||
      t,
 | 
			
		||||
      user,
 | 
			
		||||
      workouts,
 | 
			
		||||
    } = this.props
 | 
			
		||||
    const { params } = this.state
 | 
			
		||||
    const paginationEnd =
 | 
			
		||||
      workouts.length > 0
 | 
			
		||||
        ? workouts[workouts.length - 1].previous_workout === null
 | 
			
		||||
        : true
 | 
			
		||||
    return (
 | 
			
		||||
      <div>
 | 
			
		||||
        <Helmet>
 | 
			
		||||
          <title>FitTrackee - {t('common:Workouts')}</title>
 | 
			
		||||
        </Helmet>
 | 
			
		||||
        {message ? (
 | 
			
		||||
          <Message message={message} t={t} />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <div className="container history">
 | 
			
		||||
            <div className="row">
 | 
			
		||||
              <div className="col-md-3">
 | 
			
		||||
                <WorkoutsFilter
 | 
			
		||||
                  sports={sports}
 | 
			
		||||
                  loadWorkouts={() => loadWorkouts(params)}
 | 
			
		||||
                  t={t}
 | 
			
		||||
                  updateParams={e => this.setParams(e)}
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div className="col-md-9 workouts-result">
 | 
			
		||||
                <WorkoutsList
 | 
			
		||||
                  workouts={workouts}
 | 
			
		||||
                  loading={loading}
 | 
			
		||||
                  sports={sports}
 | 
			
		||||
                  t={t}
 | 
			
		||||
                  user={user}
 | 
			
		||||
                />
 | 
			
		||||
                {!paginationEnd && (
 | 
			
		||||
                  <input
 | 
			
		||||
                    type="submit"
 | 
			
		||||
                    className="btn btn-default btn-md btn-block"
 | 
			
		||||
                    value="Load more workouts"
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      params.page += 1
 | 
			
		||||
                      loadMoreWorkouts(params)
 | 
			
		||||
                      this.setState(params)
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                )}
 | 
			
		||||
                {workouts.length === 0 && <NoWorkouts t={t} />}
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        )}
 | 
			
		||||
      </div>
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default withTranslation()(
 | 
			
		||||
  connect(
 | 
			
		||||
    state => ({
 | 
			
		||||
      workouts: state.workouts.data,
 | 
			
		||||
      loading: state.loading,
 | 
			
		||||
      message: state.message,
 | 
			
		||||
      sports: state.sports.data,
 | 
			
		||||
      user: state.user,
 | 
			
		||||
    }),
 | 
			
		||||
    dispatch => ({
 | 
			
		||||
      loadWorkouts: params => {
 | 
			
		||||
        dispatch(getOrUpdateData('getData', 'workouts', params))
 | 
			
		||||
      },
 | 
			
		||||
      loadMoreWorkouts: params => {
 | 
			
		||||
        dispatch(getMoreWorkouts(params))
 | 
			
		||||
      },
 | 
			
		||||
    })
 | 
			
		||||
  )(Workouts)
 | 
			
		||||
)
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
import { createApiRequest } from '../utils'
 | 
			
		||||
 | 
			
		||||
export default class FitTrackeeApi {
 | 
			
		||||
  static loginOrRegisterOrPasswordReset(target, data) {
 | 
			
		||||
    const params = {
 | 
			
		||||
      url: `auth/${target}`,
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      noAuthorization: true,
 | 
			
		||||
      body: data,
 | 
			
		||||
      type: 'application/json',
 | 
			
		||||
    }
 | 
			
		||||
    return createApiRequest(params)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static deletePicture() {
 | 
			
		||||
    const params = {
 | 
			
		||||
      url: 'auth/picture',
 | 
			
		||||
      method: 'DELETE',
 | 
			
		||||
    }
 | 
			
		||||
    return createApiRequest(params)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,63 +0,0 @@
 | 
			
		||||
import { createApiRequest, formatUrl } from '../utils'
 | 
			
		||||
 | 
			
		||||
export default class FitTrackeeApi {
 | 
			
		||||
  static getData(target, data = {}) {
 | 
			
		||||
    const url = formatUrl(target, data)
 | 
			
		||||
    const params = {
 | 
			
		||||
      url: url,
 | 
			
		||||
      method: 'GET',
 | 
			
		||||
      type: 'application/json',
 | 
			
		||||
    }
 | 
			
		||||
    return createApiRequest(params)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static addData(target, data) {
 | 
			
		||||
    const params = {
 | 
			
		||||
      url: target,
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      body: data,
 | 
			
		||||
      type: 'application/json',
 | 
			
		||||
    }
 | 
			
		||||
    return createApiRequest(params)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static addDataWithFile(target, data) {
 | 
			
		||||
    const params = {
 | 
			
		||||
      url: target,
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      body: data,
 | 
			
		||||
    }
 | 
			
		||||
    return createApiRequest(params)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static postData(target, data) {
 | 
			
		||||
    const params = {
 | 
			
		||||
      url: `${target}${data.id ? `/${data.id}` : ''}`,
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      body: data,
 | 
			
		||||
      type: 'application/json',
 | 
			
		||||
    }
 | 
			
		||||
    return createApiRequest(params)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static updateData(target, data) {
 | 
			
		||||
    const params = {
 | 
			
		||||
      url: `${target}${
 | 
			
		||||
        data.id ? `/${data.id}` : data.username ? `/${data.username}` : ''
 | 
			
		||||
      }`,
 | 
			
		||||
      method: 'PATCH',
 | 
			
		||||
      body: data,
 | 
			
		||||
      type: 'application/json',
 | 
			
		||||
    }
 | 
			
		||||
    return createApiRequest(params)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static deleteData(target, id) {
 | 
			
		||||
    const params = {
 | 
			
		||||
      url: `${target}/${id}`,
 | 
			
		||||
      method: 'DELETE',
 | 
			
		||||
      type: 'application/json',
 | 
			
		||||
    }
 | 
			
		||||
    return createApiRequest(params)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,23 +0,0 @@
 | 
			
		||||
import i18n from 'i18next'
 | 
			
		||||
import LanguageDetector from 'i18next-browser-languagedetector'
 | 
			
		||||
import XHR from 'i18next-xhr-backend'
 | 
			
		||||
 | 
			
		||||
import { resources } from './locales'
 | 
			
		||||
 | 
			
		||||
i18n
 | 
			
		||||
  .use(XHR)
 | 
			
		||||
  .use(LanguageDetector)
 | 
			
		||||
  .init({
 | 
			
		||||
    debug: process.env.NODE_ENV === 'development',
 | 
			
		||||
    lng: 'en',
 | 
			
		||||
    fallbackLng: 'en',
 | 
			
		||||
    keySeparator: false,
 | 
			
		||||
    interpolation: {
 | 
			
		||||
      escapeValue: false,
 | 
			
		||||
    },
 | 
			
		||||
    resources,
 | 
			
		||||
    ns: ['common'],
 | 
			
		||||
    defaultNS: 'common',
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
export default i18n
 | 
			
		||||
@@ -1,27 +0,0 @@
 | 
			
		||||
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512"
 | 
			
		||||
     viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg">
 | 
			
		||||
    <path d="m466.916 27.803h-421.832c-24.859 0-45.084 20.225-45.084 45.084v366.226c0 24.859 20.225 45.084 45.084 45.084h421.832c24.859 0 45.084-20.225 45.084-45.084v-366.226c0-24.859-20.225-45.084-45.084-45.084z"
 | 
			
		||||
          fill="#f0f9ff"/>
 | 
			
		||||
    <path d="m198.58 188.334-181.344-150.862c-7.75 6.107-13.456 14.691-15.905 24.554l164.142 136.551h33.102z"
 | 
			
		||||
          fill="#f40055"/>
 | 
			
		||||
    <path d="m313.425 198.576h33.93l163.447-135.973c-2.325-9.923-7.93-18.592-15.613-24.796l-181.764 151.211z"
 | 
			
		||||
          fill="#c20044"/>
 | 
			
		||||
    <path d="m165.472 313.425-164.141 136.549c2.449 9.863 8.155 18.447 15.905 24.553l181.344-150.861-.005-10.241z"
 | 
			
		||||
          fill="#f40055"/>
 | 
			
		||||
    <path d="m313.425 313.425v9.557l181.765 151.211c7.683-6.204 13.288-14.874 15.613-24.796l-163.446-135.971z"
 | 
			
		||||
          fill="#c20044"/>
 | 
			
		||||
    <path d="m53.273 27.803 145.302 120.879v-120.879z" fill="#406bd4"/>
 | 
			
		||||
    <path d="m313.425 150.571v-122.768h148.082z" fill="#3257b0"/>
 | 
			
		||||
    <path d="m394.732 198.575 117.268-97.556v97.556z" fill="#3257b0"/>
 | 
			
		||||
    <g fill="#406bd4">
 | 
			
		||||
        <path d="m0 99.317v99.258h119.313z"/>
 | 
			
		||||
        <path d="m0 313.425v97.699l117.44-97.699z"/>
 | 
			
		||||
        <path d="m50.49 484.197 148.085-122.676v122.676z"/>
 | 
			
		||||
    </g>
 | 
			
		||||
    <path d="m313.425 484.197v-124.139l149.221 124.139z" fill="#3257b0"/>
 | 
			
		||||
    <path d="m512 409.423-115.395-95.998h115.395z" fill="#3257b0"/>
 | 
			
		||||
    <path d="m512 222.142h-222.142v-194.339h-67.716v194.339h-222.142v67.716h222.142v194.339h67.716v-194.339h222.142z"
 | 
			
		||||
          fill="#f40055"/>
 | 
			
		||||
    <path d="m289.858 222.142v-194.339h-33.858v456.394h33.858v-194.339h222.142v-67.716z"
 | 
			
		||||
          fill="#c20044"/>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 1.7 KiB  | 
@@ -1 +0,0 @@
 | 
			
		||||
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m173.899 31.804h-8.707l-4.397-4h-115.711c-24.859-.001-45.084 20.224-45.084 45.083v366.226c0 24.859 20.225 45.084 45.084 45.084h115.711l6.348-4h6.755v-448.393z" fill="#406bd4"/><path d="m466.916 27.803h-115.711l-4.523 4h-5.141v448.393h4.141l5.523 4h115.711c24.859 0 45.084-20.225 45.084-45.084v-366.225c0-24.859-20.225-45.084-45.084-45.084z" fill="#c20044"/><path d="m160.795 27.803h190.409v456.394h-190.409z" fill="#f0f9ff"/><path d="m256 27.803h95.205v456.394h-95.205z" fill="#cee5f5"/></svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 637 B  | 
@@ -1,42 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="iso-8859-1"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
 | 
			
		||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 | 
			
		||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
 | 
			
		||||
	 viewBox="0 0 345.834 345.834" style="enable-background:new 0 0 345.834 345.834;" xml:space="preserve">
 | 
			
		||||
<g>
 | 
			
		||||
	<path d="M339.798,260.429c0.13-0.026,0.257-0.061,0.385-0.094c0.109-0.028,0.219-0.051,0.326-0.084
 | 
			
		||||
		c0.125-0.038,0.247-0.085,0.369-0.129c0.108-0.039,0.217-0.074,0.324-0.119c0.115-0.048,0.226-0.104,0.338-0.157
 | 
			
		||||
		c0.109-0.052,0.22-0.1,0.327-0.158c0.107-0.057,0.208-0.122,0.312-0.184c0.107-0.064,0.215-0.124,0.319-0.194
 | 
			
		||||
		c0.111-0.074,0.214-0.156,0.321-0.236c0.09-0.067,0.182-0.13,0.27-0.202c0.162-0.133,0.316-0.275,0.466-0.421
 | 
			
		||||
		c0.027-0.026,0.056-0.048,0.083-0.075c0.028-0.028,0.052-0.059,0.079-0.088c0.144-0.148,0.284-0.3,0.416-0.46
 | 
			
		||||
		c0.077-0.094,0.144-0.192,0.216-0.289c0.074-0.1,0.152-0.197,0.221-0.301c0.074-0.111,0.139-0.226,0.207-0.34
 | 
			
		||||
		c0.057-0.096,0.118-0.19,0.171-0.289c0.062-0.115,0.114-0.234,0.169-0.351c0.049-0.104,0.101-0.207,0.146-0.314
 | 
			
		||||
		c0.048-0.115,0.086-0.232,0.128-0.349c0.041-0.114,0.085-0.227,0.12-0.343c0.036-0.118,0.062-0.238,0.092-0.358
 | 
			
		||||
		c0.029-0.118,0.063-0.234,0.086-0.353c0.028-0.141,0.045-0.283,0.065-0.425c0.014-0.1,0.033-0.199,0.043-0.3
 | 
			
		||||
		c0.025-0.249,0.038-0.498,0.038-0.748V92.76c0-4.143-3.357-7.5-7.5-7.5h-236.25c-0.066,0-0.13,0.008-0.196,0.01
 | 
			
		||||
		c-0.143,0.004-0.285,0.01-0.427,0.022c-0.113,0.009-0.225,0.022-0.337,0.037c-0.128,0.016-0.255,0.035-0.382,0.058
 | 
			
		||||
		c-0.119,0.021-0.237,0.046-0.354,0.073c-0.119,0.028-0.238,0.058-0.356,0.092c-0.117,0.033-0.232,0.069-0.346,0.107
 | 
			
		||||
		c-0.117,0.04-0.234,0.082-0.349,0.128c-0.109,0.043-0.216,0.087-0.322,0.135c-0.118,0.053-0.235,0.11-0.351,0.169
 | 
			
		||||
		c-0.099,0.051-0.196,0.103-0.292,0.158c-0.116,0.066-0.23,0.136-0.343,0.208c-0.093,0.06-0.184,0.122-0.274,0.185
 | 
			
		||||
		c-0.106,0.075-0.211,0.153-0.314,0.235c-0.094,0.075-0.186,0.152-0.277,0.231c-0.09,0.079-0.179,0.158-0.266,0.242
 | 
			
		||||
		c-0.099,0.095-0.194,0.194-0.288,0.294c-0.047,0.05-0.097,0.094-0.142,0.145c-0.027,0.03-0.048,0.063-0.074,0.093
 | 
			
		||||
		c-0.094,0.109-0.182,0.223-0.27,0.338c-0.064,0.084-0.13,0.168-0.19,0.254c-0.078,0.112-0.15,0.227-0.222,0.343
 | 
			
		||||
		c-0.059,0.095-0.12,0.189-0.174,0.286c-0.063,0.112-0.118,0.227-0.175,0.342c-0.052,0.105-0.106,0.21-0.153,0.317
 | 
			
		||||
		c-0.049,0.113-0.092,0.23-0.135,0.345c-0.043,0.113-0.087,0.225-0.124,0.339c-0.037,0.115-0.067,0.232-0.099,0.349
 | 
			
		||||
		c-0.032,0.12-0.066,0.239-0.093,0.36c-0.025,0.113-0.042,0.228-0.062,0.342c-0.022,0.13-0.044,0.26-0.06,0.39
 | 
			
		||||
		c-0.013,0.108-0.019,0.218-0.027,0.328c-0.01,0.14-0.019,0.28-0.021,0.421c-0.001,0.041-0.006,0.081-0.006,0.122v46.252
 | 
			
		||||
		c0,4.143,3.357,7.5,7.5,7.5s7.5-3.357,7.5-7.5v-29.595l66.681,59.037c-0.348,0.245-0.683,0.516-0.995,0.827l-65.687,65.687v-49.288
 | 
			
		||||
		c0-4.143-3.357-7.5-7.5-7.5s-7.5,3.357-7.5,7.5v9.164h-38.75c-4.143,0-7.5,3.357-7.5,7.5s3.357,7.5,7.5,7.5h38.75v43.231
 | 
			
		||||
		c0,4.143,3.357,7.5,7.5,7.5h236.25c0.247,0,0.494-0.013,0.74-0.037c0.115-0.011,0.226-0.033,0.339-0.049
 | 
			
		||||
		C339.542,260.469,339.67,260.454,339.798,260.429z M330.834,234.967l-65.688-65.687c-0.042-0.042-0.087-0.077-0.13-0.117
 | 
			
		||||
		l49.383-41.897c3.158-2.68,3.546-7.412,0.866-10.571c-2.678-3.157-7.41-3.547-10.571-0.866l-84.381,71.59l-98.444-87.158h208.965
 | 
			
		||||
		V234.967z M185.878,179.888c0.535-0.535,0.969-1.131,1.308-1.765l28.051,24.835c1.418,1.255,3.194,1.885,4.972,1.885
 | 
			
		||||
		c1.726,0,3.451-0.593,4.853-1.781l28.587-24.254c0.26,0.38,0.553,0.743,0.89,1.08l65.687,65.687H120.191L185.878,179.888z"/>
 | 
			
		||||
	<path d="M7.5,170.676h126.667c4.143,0,7.5-3.357,7.5-7.5s-3.357-7.5-7.5-7.5H7.5c-4.143,0-7.5,3.357-7.5,7.5
 | 
			
		||||
		S3.357,170.676,7.5,170.676z"/>
 | 
			
		||||
	<path d="M20.625,129.345H77.5c4.143,0,7.5-3.357,7.5-7.5s-3.357-7.5-7.5-7.5H20.625c-4.143,0-7.5,3.357-7.5,7.5
 | 
			
		||||
		S16.482,129.345,20.625,129.345z"/>
 | 
			
		||||
	<path d="M62.5,226.51h-55c-4.143,0-7.5,3.357-7.5,7.5s3.357,7.5,7.5,7.5h55c4.143,0,7.5-3.357,7.5-7.5S66.643,226.51,62.5,226.51z"
 | 
			
		||||
		/>
 | 
			
		||||
</g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 4.0 KiB  | 
@@ -1,65 +0,0 @@
 | 
			
		||||
<?xml version="1.0" encoding="iso-8859-1"?>
 | 
			
		||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
 | 
			
		||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
 | 
			
		||||
	 viewBox="0 0 512.001 512.001" style="enable-background:new 0 0 512.001 512.001;" xml:space="preserve">
 | 
			
		||||
<g>
 | 
			
		||||
	<g>
 | 
			
		||||
		<path d="M468.683,287.265h-69.07c-4.147,0-7.508,3.361-7.508,7.508c0,4.147,3.361,7.508,7.508,7.508h69.07
 | 
			
		||||
			c4.147,0,7.508-3.361,7.508-7.508C476.191,290.626,472.83,287.265,468.683,287.265z"/>
 | 
			
		||||
	</g>
 | 
			
		||||
</g>
 | 
			
		||||
<g>
 | 
			
		||||
	<g>
 | 
			
		||||
		<path d="M105.012,268.377L85.781,256l19.231-12.376c3.487-2.243,4.495-6.888,2.251-10.376c-2.244-3.486-6.888-4.497-10.376-2.25
 | 
			
		||||
			l-17.471,11.243v-20.776c0-4.147-3.361-7.508-7.508-7.508c-4.147,0-7.508,3.361-7.508,7.508v20.775l-17.47-11.243
 | 
			
		||||
			c-3.486-2.245-8.132-1.238-10.376,2.25c-2.245,3.487-1.237,8.133,2.25,10.376L58.034,256l-19.231,12.376
 | 
			
		||||
			c-3.487,2.244-4.495,6.889-2.25,10.376c1.435,2.23,3.852,3.446,6.32,3.446c1.391,0,2.799-0.386,4.056-1.196l17.47-11.243v20.775
 | 
			
		||||
			c0,4.147,3.361,7.508,7.508,7.508c4.147,0,7.508-3.361,7.508-7.508V269.76l17.471,11.243c1.257,0.809,2.664,1.196,4.056,1.196
 | 
			
		||||
			c2.467,0,4.885-1.216,6.32-3.446C109.507,275.266,108.499,270.62,105.012,268.377z"/>
 | 
			
		||||
	</g>
 | 
			
		||||
</g>
 | 
			
		||||
<g>
 | 
			
		||||
	<g>
 | 
			
		||||
		<path d="M194.441,268.377L175.21,256l19.231-12.376c3.487-2.244,4.495-6.889,2.25-10.376c-2.245-3.486-6.888-4.497-10.376-2.25
 | 
			
		||||
			l-17.47,11.243v-20.775c0-4.147-3.361-7.508-7.508-7.508c-4.147,0-7.508,3.361-7.508,7.508v20.776l-17.471-11.243
 | 
			
		||||
			c-3.487-2.245-8.133-1.238-10.376,2.25c-2.245,3.487-1.237,8.133,2.25,10.376L147.463,256l-19.231,12.376
 | 
			
		||||
			c-3.487,2.244-4.495,6.889-2.25,10.376c1.435,2.23,3.852,3.446,6.32,3.446c1.391,0,2.799-0.386,4.056-1.196l17.471-11.243v20.776
 | 
			
		||||
			c0,4.147,3.361,7.508,7.508,7.508c4.147,0,7.508-3.361,7.508-7.508V269.76l17.47,11.243c1.257,0.809,2.664,1.196,4.056,1.196
 | 
			
		||||
			c2.467,0,4.885-1.216,6.32-3.446C198.936,275.266,197.928,270.62,194.441,268.377z"/>
 | 
			
		||||
	</g>
 | 
			
		||||
</g>
 | 
			
		||||
<g>
 | 
			
		||||
	<g>
 | 
			
		||||
		<path d="M283.871,268.377L264.64,256l19.231-12.376c3.487-2.243,4.495-6.888,2.251-10.376c-2.245-3.486-6.888-4.497-10.376-2.25
 | 
			
		||||
			l-17.471,11.243v-20.775c0-4.147-3.361-7.508-7.508-7.508c-4.147,0-7.508,3.361-7.508,7.508v20.775l-17.471-11.243
 | 
			
		||||
			c-3.486-2.245-8.134-1.238-10.376,2.25c-2.245,3.487-1.237,8.133,2.25,10.376L236.892,256l-19.231,12.376
 | 
			
		||||
			c-3.487,2.244-4.495,6.889-2.25,10.376c1.435,2.23,3.852,3.446,6.32,3.446c1.391,0,2.799-0.386,4.056-1.196l17.471-11.243v20.775
 | 
			
		||||
			c0,4.147,3.361,7.508,7.508,7.508c4.147,0,7.508-3.361,7.508-7.508V269.76l17.471,11.243c1.257,0.809,2.664,1.196,4.056,1.196
 | 
			
		||||
			c2.467,0,4.886-1.216,6.32-3.446C288.366,275.266,287.358,270.62,283.871,268.377z"/>
 | 
			
		||||
	</g>
 | 
			
		||||
</g>
 | 
			
		||||
<g>
 | 
			
		||||
	<g>
 | 
			
		||||
		<path d="M373.3,268.377L354.069,256l19.231-12.376c3.487-2.244,4.495-6.889,2.25-10.376c-2.244-3.486-6.888-4.497-10.376-2.25
 | 
			
		||||
			l-17.471,11.243v-20.776c0-4.147-3.361-7.508-7.508-7.508c-4.147,0-7.508,3.361-7.508,7.508v20.775l-17.47-11.243
 | 
			
		||||
			c-3.486-2.245-8.132-1.238-10.376,2.25c-2.245,3.487-1.237,8.133,2.25,10.376L326.322,256l-19.231,12.376
 | 
			
		||||
			c-3.487,2.244-4.495,6.889-2.25,10.376c1.435,2.23,3.852,3.446,6.32,3.446c1.391,0,2.799-0.386,4.056-1.196l17.47-11.243v20.776
 | 
			
		||||
			c0,4.147,3.361,7.508,7.508,7.508c4.147,0,7.508-3.361,7.508-7.508V269.76l17.471,11.243c1.257,0.809,2.664,1.196,4.056,1.196
 | 
			
		||||
			c2.467,0,4.885-1.216,6.32-3.446C377.795,275.266,376.787,270.62,373.3,268.377z"/>
 | 
			
		||||
	</g>
 | 
			
		||||
</g>
 | 
			
		||||
<g>
 | 
			
		||||
	<g>
 | 
			
		||||
		<path d="M271.792,330.359H15.016V181.642h93.1c4.147,0,7.508-3.361,7.508-7.508c0-4.147-3.361-7.508-7.508-7.508H12.513
 | 
			
		||||
			C5.613,166.626,0,172.24,0,179.14v153.722c0,6.9,5.613,12.513,12.513,12.513h259.278c4.147,0,7.508-3.361,7.508-7.508
 | 
			
		||||
			C279.299,333.72,275.939,330.359,271.792,330.359z"/>
 | 
			
		||||
	</g>
 | 
			
		||||
</g>
 | 
			
		||||
<g>
 | 
			
		||||
	<g>
 | 
			
		||||
		<path d="M499.487,166.626H162.174c-4.147,0-7.508,3.361-7.508,7.508c0,4.147,3.361,7.508,7.508,7.508h334.811v148.716H323.848
 | 
			
		||||
			c-4.147,0-7.508,3.361-7.508,7.508c0,4.147,3.361,7.508,7.508,7.508h175.64c6.9,0,12.513-5.613,12.513-12.513V179.14
 | 
			
		||||
			C512.001,172.24,506.387,166.626,499.487,166.626z"/>
 | 
			
		||||
	</g>
 | 
			
		||||
</g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 4.0 KiB  | 
@@ -1,46 +0,0 @@
 | 
			
		||||
/* eslint-disable react/jsx-filename-extension */
 | 
			
		||||
import { createBrowserHistory } from 'history'
 | 
			
		||||
import React from 'react'
 | 
			
		||||
import { I18nextProvider } from 'react-i18next'
 | 
			
		||||
import ReactDOM from 'react-dom'
 | 
			
		||||
import { routerMiddleware } from 'connected-react-router'
 | 
			
		||||
import { applyMiddleware, compose, createStore } from 'redux'
 | 
			
		||||
import thunk from 'redux-thunk'
 | 
			
		||||
 | 
			
		||||
import i18n from './i18n'
 | 
			
		||||
import App from './components/App'
 | 
			
		||||
import Root from './components/Root'
 | 
			
		||||
import registerServiceWorker from './registerServiceWorker'
 | 
			
		||||
import createRootReducer from './reducers'
 | 
			
		||||
import { loadProfile } from './actions/user'
 | 
			
		||||
import { historyEnhancer } from './utils/history'
 | 
			
		||||
 | 
			
		||||
export const history = historyEnhancer(createBrowserHistory())
 | 
			
		||||
 | 
			
		||||
history.listen(() => {
 | 
			
		||||
  window.scrollTo(0, 0)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export const rootNode = document.getElementById('root')
 | 
			
		||||
 | 
			
		||||
export const store = createStore(
 | 
			
		||||
  createRootReducer(history),
 | 
			
		||||
  window.__STATE__, // Server state
 | 
			
		||||
  (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose)(
 | 
			
		||||
    applyMiddleware(routerMiddleware(history), thunk)
 | 
			
		||||
  )
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
if (window.localStorage.authToken !== null) {
 | 
			
		||||
  store.dispatch(loadProfile())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ReactDOM.render(
 | 
			
		||||
  <Root store={store} history={history}>
 | 
			
		||||
    <I18nextProvider i18n={i18n}>
 | 
			
		||||
      <App />
 | 
			
		||||
    </I18nextProvider>
 | 
			
		||||
  </Root>,
 | 
			
		||||
  rootNode
 | 
			
		||||
)
 | 
			
		||||
registerServiceWorker()
 | 
			
		||||
@@ -1,34 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "Actions": "Actions",
 | 
			
		||||
  "Active": "Active",
 | 
			
		||||
  "workouts exist": "workouts exist",
 | 
			
		||||
  "Add admin rights": "Add admin rights",
 | 
			
		||||
  "Add/remove admin rights, delete user account.": "Add/remove admin rights, delete user account.",
 | 
			
		||||
  "Administration": "Administration",
 | 
			
		||||
  "Application": "Application",
 | 
			
		||||
  "Application configuration": "Application configuration",
 | 
			
		||||
  "Back": "Back",
 | 
			
		||||
  "Disable": "Disable",
 | 
			
		||||
  "Enable": "Enable",
 | 
			
		||||
  "Enable/disable sports.": "Enable/disable sports.",
 | 
			
		||||
  "FitTrackee administration": "FitTrackee administration",
 | 
			
		||||
  "id": "id",
 | 
			
		||||
  "if 0, no limitation": "if 0, no limitation",
 | 
			
		||||
  "Image": "Image",
 | 
			
		||||
  "Label": "Label",
 | 
			
		||||
  "Max. number of active users": "Max. number of active users",
 | 
			
		||||
  "Max. files of zip archive": "Max. files of zip archive",
 | 
			
		||||
  "Max. size of uploaded files": "Max. size of uploaded files",
 | 
			
		||||
  "Max. size of uploaded files (in Mb)": "Max. size of uploaded files (in Mb)",
 | 
			
		||||
  "Max. size of zip archive": "Max. size of zip archive",
 | 
			
		||||
  "Max. size of zip archive (in Mb)": "Max. size of zip archive (in Mb)",
 | 
			
		||||
  "Registration is currently disabled.": "Registration is currently disabled.",
 | 
			
		||||
  "Registration is currently enabled.": "Registration is currently enabled.",
 | 
			
		||||
  "Remove admin rights": "Remove admin rights",
 | 
			
		||||
  "Sports": "Sports",
 | 
			
		||||
  "Update application configuration (maximum number of registered users, maximum files size).": "Update application configuration (maximum number of registered users, maximum files size).",
 | 
			
		||||
  "uploads": "uploads",
 | 
			
		||||
  "user": "user",
 | 
			
		||||
  "Users": "Users",
 | 
			
		||||
  "users": "users"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "workouts count": "workouts count",
 | 
			
		||||
  "Add workout": "Add workout",
 | 
			
		||||
  "admin rights": "admin rights",
 | 
			
		||||
  "ascending": "ascending",
 | 
			
		||||
  "Back": "Back",
 | 
			
		||||
  "Back to home": "Back to home",
 | 
			
		||||
  "Cancel": "Cancel",
 | 
			
		||||
  "Confirmation": "Confirmation",
 | 
			
		||||
  "Dashboard": "Dashboard",
 | 
			
		||||
  "descending": "descending",
 | 
			
		||||
  "Edit": "Edit",
 | 
			
		||||
  "day": "day",
 | 
			
		||||
  "days": "days",
 | 
			
		||||
  "Next": "Next",
 | 
			
		||||
  "No": "No",
 | 
			
		||||
  "no": "no",
 | 
			
		||||
  "No records.": "No records.",
 | 
			
		||||
  "No workouts.": "No workouts.",
 | 
			
		||||
  "Page not found": "Page not found",
 | 
			
		||||
  "Previous": "Prev",
 | 
			
		||||
  "registration date": "registration date",
 | 
			
		||||
  "remaining characters": "remaining characters",
 | 
			
		||||
  "Sort": "Sort",
 | 
			
		||||
  "Sort by": "Sort by",
 | 
			
		||||
  "Sport": "Sport",
 | 
			
		||||
  "sport": "sport",
 | 
			
		||||
  "Sports": "Sports",
 | 
			
		||||
  "sports": "sports",
 | 
			
		||||
  "Statistics": "Statistics",
 | 
			
		||||
  "Submit": "Submit",
 | 
			
		||||
  "to": "to",
 | 
			
		||||
  "user name": "user name",
 | 
			
		||||
  "Workout": "Workout",
 | 
			
		||||
  "Workouts": "Workouts",
 | 
			
		||||
  "workout": "workout",
 | 
			
		||||
  "workouts": "workouts",
 | 
			
		||||
  "Yes": "Yes",
 | 
			
		||||
  "yes": "yes"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "Personal records": "Personal records",
 | 
			
		||||
  "This month": "This month",
 | 
			
		||||
  "Upload one !": "Upload one !"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "3 to 12 characters required for username.": "3 to 12 characters required for username.",
 | 
			
		||||
  "8 characters required for password.": "8 characters required for password.",
 | 
			
		||||
  "An error occurred. Please contact the administrator.": "An error occurred. Please contact the administrator.",
 | 
			
		||||
  "application": "application",
 | 
			
		||||
  "Error during picture deletion.": "Error during picture deletion.",
 | 
			
		||||
  "Error during picture update.":  "Error during picture update.",
 | 
			
		||||
  "Error during picture update, file size exceeds max size.": "Error during picture update, file size exceeds max size.",
 | 
			
		||||
  "Error. Registration is disabled.": "Error. Registration is disabled.",
 | 
			
		||||
  "Error. Please try again or contact the administrator.": "Error. Please try again or contact the administrator.",
 | 
			
		||||
  "File extension not allowed.": "File extension not allowed.",
 | 
			
		||||
  "File size is greater than the allowed size": "File size is greater than the allowed size",
 | 
			
		||||
  "Incorrect id": "Incorrect id",
 | 
			
		||||
  "Invalid credentials.": "Invalid credentials.",
 | 
			
		||||
  "Invalid payload.": "Invalid payload.",
 | 
			
		||||
  "Invalid token. Please log in again.": "Invalid token. Please log in again.",
 | 
			
		||||
  "Max. files in a zip archive must be greater than 0": "Max. files in a zip archive must be greater than 0",
 | 
			
		||||
  "Max. size of uploaded files must be greater than 0": "Max. size of uploaded files must be greater than 0",
 | 
			
		||||
  "Max. size of zip archive must be equal or greater than max. size of uploaded files": "Max. size of zip archive must be equal or greater than max. size of uploaded files",
 | 
			
		||||
  "Max. size of zip archive must be greater than 0": "Max. size of zip archive must be greater than 0",
 | 
			
		||||
  "No file part.": "No file part.",
 | 
			
		||||
  "No picture.": "No picture.",
 | 
			
		||||
  "No selected file.": "No selected file.",
 | 
			
		||||
  "no correct file.": "no correct file.",
 | 
			
		||||
  "no gpx file for this workout": "no gpx file for this workout",
 | 
			
		||||
  "Password and password confirmation don't match.": "Password and password confirmation don't match.",
 | 
			
		||||
  "Provide a valid auth token": "Provide a valid auth token",
 | 
			
		||||
  "records": "records",
 | 
			
		||||
  "Signature expired. Please log in again.": "Signature expired. Please log in again.",
 | 
			
		||||
  "Sorry. That user already exists.": "Sorry. That user already exists.",
 | 
			
		||||
  "Sport can not be disabled, workouts exist." : "Sport can not be disabled, workouts exist.",
 | 
			
		||||
  "Sport does not exist.": "Sport does not exist.",
 | 
			
		||||
  "sports": "sports",
 | 
			
		||||
  "statistics": "statistiques",
 | 
			
		||||
  "User does not exist.": "User does not exist.",
 | 
			
		||||
  "Valid email must be provided.\n": "Valid email must be provided.",
 | 
			
		||||
  "workouts": "workouts",
 | 
			
		||||
  "You can not delete your account, no other user has admin rights.": "You can not delete your account, no other user has admin rights.",
 | 
			
		||||
  "You do not have permissions.": "You do not have permissions."
 | 
			
		||||
}
 | 
			
		||||
@@ -1,19 +0,0 @@
 | 
			
		||||
import EnWorkoutsTranslations from './workouts.json'
 | 
			
		||||
import EnAdministrationTranslations from './administration.json'
 | 
			
		||||
import EnCommonTranslations from './common.json'
 | 
			
		||||
import EnDashboardTranslations from './dashboard.json'
 | 
			
		||||
import EnMessagesTranslations from './messages.json'
 | 
			
		||||
import EnSportsTranslations from './sports.json'
 | 
			
		||||
import EnStatisticsTranslations from './statistics.json'
 | 
			
		||||
import EnUserTranslations from './user.json'
 | 
			
		||||
 | 
			
		||||
export const enResources = {
 | 
			
		||||
  workouts: EnWorkoutsTranslations,
 | 
			
		||||
  administration: EnAdministrationTranslations,
 | 
			
		||||
  common: EnCommonTranslations,
 | 
			
		||||
  dashboard: EnDashboardTranslations,
 | 
			
		||||
  messages: EnMessagesTranslations,
 | 
			
		||||
  sports: EnSportsTranslations,
 | 
			
		||||
  statistics: EnStatisticsTranslations,
 | 
			
		||||
  user: EnUserTranslations,
 | 
			
		||||
}
 | 
			
		||||
@@ -1,45 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "Admin": "Admin",
 | 
			
		||||
  "Are you sure you want to delete this account? All data will be deleted, this cannot be undone.": "Are you sure you want to delete this account? All data will be deleted, this cannot be undone.",
 | 
			
		||||
  "Are you sure you want to delete your account? All data will be deleted, this cannot be undone.": "Are you sure you want to delete your account? All data will be deleted, this cannot be undone.",
 | 
			
		||||
  "Bio": "Bio",
 | 
			
		||||
  "Birth Date": "Birth Date",
 | 
			
		||||
  "Check your email. If your address is in our database, you'll received an email with a link to reset your password.": "Check your email. If your address is in our database, you'll received an email with a link to reset your password.",
 | 
			
		||||
  "Delete my account": "Delete my account",
 | 
			
		||||
  "Delete picture": "Delete picture",
 | 
			
		||||
  "Delete user account": "Delete user account",
 | 
			
		||||
  "Edit Profile": "Edit Profile",
 | 
			
		||||
  "Email": "Email",
 | 
			
		||||
  "Enter a username": "Enter a username",
 | 
			
		||||
  "Enter an email address": "Enter an email address",
 | 
			
		||||
  "Enter a password": "Enter a password",
 | 
			
		||||
  "Enter the password confirmation": "Enter the password confirmation",
 | 
			
		||||
  "First day of week": "First day of week",
 | 
			
		||||
  "First Name": "First Name",
 | 
			
		||||
  "Forgot password?": "Forgot password?",
 | 
			
		||||
  "Invalid token. Please request a new token.": "Invalid token. Please request a new token.",
 | 
			
		||||
  "Language": "Language",
 | 
			
		||||
  "Last Name": "Last Name",
 | 
			
		||||
  "Location": "Location",
 | 
			
		||||
  "loggedOut": "You are now logged out. Click <1>here</1> to log back in.",
 | 
			
		||||
  "Login": "Login",
 | 
			
		||||
  "login": "login",
 | 
			
		||||
  "Logout": "Logout",
 | 
			
		||||
  "Monday": "Monday",
 | 
			
		||||
  "Password": "Password",
 | 
			
		||||
  "Password Confirmation": "Password Confirmation",
 | 
			
		||||
  "Password reset": "Password reset",
 | 
			
		||||
  "password reset": "password reset",
 | 
			
		||||
  "Profile": "Profile",
 | 
			
		||||
  "Profile Edition": "Profile Edition",
 | 
			
		||||
  "Register": "Register",
 | 
			
		||||
  "register": "register",
 | 
			
		||||
  "Registration Date": "Registration Date",
 | 
			
		||||
  "Reset your password": "Reset your password",
 | 
			
		||||
  "reset your password": "reset your password",
 | 
			
		||||
  "Send": "Send",
 | 
			
		||||
  "Sunday": "Sunday",
 | 
			
		||||
  "Timezone": "Timezone",
 | 
			
		||||
  "updatedPasswordText": "Your password have been updated. Click <1>here</1> to log in." ,
 | 
			
		||||
  "Username": "Username"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,58 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "Workouts": "Workouts",
 | 
			
		||||
  "Workout": "Workout",
 | 
			
		||||
  "Workout Date": "Workout Date",
 | 
			
		||||
  "Add a workout": "Add a workout",
 | 
			
		||||
  "Are you sure you want to delete this workout?": "Are you sure you want to delete this workout?",
 | 
			
		||||
  "Ave. speed": "Ave. speed",
 | 
			
		||||
  "Ascent": "Ascent",
 | 
			
		||||
  "Average speed": "Average speed",
 | 
			
		||||
  "Chart": "Chart",
 | 
			
		||||
  "data from gpx, without any cleaning": "data from gpx, without any cleaning",
 | 
			
		||||
  "Date": "Date",
 | 
			
		||||
  "Delete workout": "Delete workout",
 | 
			
		||||
  "Descent": "Descent",
 | 
			
		||||
  "Distance": "Distance",
 | 
			
		||||
  "distance": "distance",
 | 
			
		||||
  "Duration": "Duration",
 | 
			
		||||
  "duration": "duration",
 | 
			
		||||
  "Edit a workout": "Edit a workout",
 | 
			
		||||
  "Edit workout": "Edit workout",
 | 
			
		||||
  "elevation": "elevation",
 | 
			
		||||
  "End": "End",
 | 
			
		||||
  "Farest distance": "Farest distance",
 | 
			
		||||
  "Filter": "Filter",
 | 
			
		||||
  "From": "From",
 | 
			
		||||
  "gpxFile": "<strong>gpx</strong> file",
 | 
			
		||||
  "Longest duration": "Longest duration",
 | 
			
		||||
  "Max. altitude" : "Max. altitude",
 | 
			
		||||
  "Max. speed": "Max. speed",
 | 
			
		||||
  "Min. altitude": "Min. altitude",
 | 
			
		||||
  "no folder inside": "no folder inside",
 | 
			
		||||
  "files max": "files max",
 | 
			
		||||
  "max size": "max size",
 | 
			
		||||
  "No data to display": "No data to display",
 | 
			
		||||
  "No Map": "No Map",
 | 
			
		||||
  "No next workout": "No next workout",
 | 
			
		||||
  "No next segment": "No next segment",
 | 
			
		||||
  "No notes": "No notes",
 | 
			
		||||
  "No previous workout": "No previous workout",
 | 
			
		||||
  "No previous segment": "No previous segment",
 | 
			
		||||
  "Notes": "Notes",
 | 
			
		||||
  "pauses": "pauses",
 | 
			
		||||
  "Personal records": "Personal records",
 | 
			
		||||
  "See next workout": "See next workout",
 | 
			
		||||
  "See next segment": "See next segment",
 | 
			
		||||
  "See previous workout": "See previous workout",
 | 
			
		||||
  "See previous segment": "See previous segment",
 | 
			
		||||
  "segment": "segment",
 | 
			
		||||
  "Segments": "Segments",
 | 
			
		||||
  "speed": "speed",
 | 
			
		||||
  "Start": "Start",
 | 
			
		||||
  "Title": "Title",
 | 
			
		||||
  "To": "To",
 | 
			
		||||
  "total duration": "total duration",
 | 
			
		||||
  "with gpx file": "with gpx file",
 | 
			
		||||
  "without gpx file": "without gpx file",
 | 
			
		||||
  "zipFile": "or <strong> zip</strong> file containing <strong>gpx </strong> files"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,34 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "Actions": "Actions",
 | 
			
		||||
  "Active": "Active",
 | 
			
		||||
  "Add admin rights": "Ajouter des droits d'admin",
 | 
			
		||||
  "Add/remove admin rights, delete user account.": "Ajouter/retirer des droits d'adminsitration, supprimer des comptes utilisateurs.",
 | 
			
		||||
  "Administration": "Administration",
 | 
			
		||||
  "workouts exist": "des séances existent",
 | 
			
		||||
  "Application": "Application",
 | 
			
		||||
  "Application configuration": "Configuration de l'application",
 | 
			
		||||
  "Back": "Retour",
 | 
			
		||||
  "Disable": "désactiver",
 | 
			
		||||
  "Enable": "activer",
 | 
			
		||||
  "Enable/disable sports.": "Activer/désactiver des sports.",
 | 
			
		||||
  "FitTrackee administration": "Administration de FitTrackee",
 | 
			
		||||
  "id": "id",
 | 
			
		||||
  "if 0, no limitation": "si égal à 0, pas limite d'inscription",
 | 
			
		||||
  "Image": "Image",
 | 
			
		||||
  "Label": "Label",
 | 
			
		||||
  "Max. number of active users": "Nombre maximum d'utilisateurs actifs",
 | 
			
		||||
  "Max. files of zip archive": "Nombre max. de fichiers dans une archive zip",
 | 
			
		||||
  "Max. size of uploaded files": "Taille max. des fichiers",
 | 
			
		||||
  "Max. size of uploaded files (in Mb)": "Taille max. des fichiers (en Mo)",
 | 
			
		||||
  "Max. size of zip archive": "Taille max. des archives zip",
 | 
			
		||||
  "Max. size of zip archive (in Mb)": "Taille max. des archives zip (en Mo)",
 | 
			
		||||
  "Registration is currently disabled.": "Les inscriptions sont actuellement désactivées.",
 | 
			
		||||
  "Registration is currently enabled.": "Les inscriptions sont actuellement activées.",
 | 
			
		||||
  "Remove admin rights": "Retirer des droits d'admin",
 | 
			
		||||
  "Sports": "Sports",
 | 
			
		||||
  "Update application configuration (maximum number of registered users, maximum files size).": "Configurer l'application (nombre maximum d'utilisateurs inscrits, taille maximale des fichers).",
 | 
			
		||||
  "uploads": "fichiers",
 | 
			
		||||
  "user": "user",
 | 
			
		||||
  "Users": "Utilisateurs",
 | 
			
		||||
  "users": "utilisateurs"
 | 
			
		||||
}
 | 
			
		||||
@@ -1,40 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "workouts count": "nombre d'séances",
 | 
			
		||||
  "Add workout": "Ajouter une séance",
 | 
			
		||||
  "admin rights": "droits d'admin",
 | 
			
		||||
  "ascending": "ascendant",
 | 
			
		||||
  "Back": "Revenir à la page précédente",
 | 
			
		||||
  "Back to home": "Retour à l'accueil",
 | 
			
		||||
  "Cancel": "Annuler",
 | 
			
		||||
  "Confirmation": "Confirmation",
 | 
			
		||||
  "Dashboard": "Tableau de Bord",
 | 
			
		||||
  "descending": "descendant",
 | 
			
		||||
  "Edit": "Modifier",
 | 
			
		||||
  "day": "jour",
 | 
			
		||||
  "days": "jours",
 | 
			
		||||
  "Next": "Page suivante",
 | 
			
		||||
  "No": "Non",
 | 
			
		||||
  "no": "non",
 | 
			
		||||
  "No records.": "Pas de records.",
 | 
			
		||||
  "No workouts.": "Pas de séances.",
 | 
			
		||||
  "Page not found": "Page introuvable",
 | 
			
		||||
  "Previous": "Page précédente",
 | 
			
		||||
  "remaining characters": "nombre de caractères restants ",
 | 
			
		||||
  "registration date": "date d'inscription",
 | 
			
		||||
  "Sort": "Tri",
 | 
			
		||||
  "Sort by": "Trier par",
 | 
			
		||||
  "Sport": "Sport",
 | 
			
		||||
  "sport": "sport",
 | 
			
		||||
  "Sports": "Sports",
 | 
			
		||||
  "sports": "sports",
 | 
			
		||||
  "Statistics": "Statistiques",
 | 
			
		||||
  "Submit": "Valider",
 | 
			
		||||
  "to": "à",
 | 
			
		||||
  "user name": "utilisateur",
 | 
			
		||||
  "Workout": "Séance",
 | 
			
		||||
  "Workouts": "Séances",
 | 
			
		||||
  "workout": "séance",
 | 
			
		||||
  "workouts": "séances",
 | 
			
		||||
  "Yes": "Oui",
 | 
			
		||||
  "yes": "oui"
 | 
			
		||||
}
 | 
			
		||||