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