Client - remove old client

This commit is contained in:
Sam 2021-10-31 18:47:38 +01:00
parent 21ba5270c1
commit c0b0bfdb17
111 changed files with 0 additions and 7130 deletions

View File

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

View File

@ -1,10 +0,0 @@
{
"tabWidth": 2,
"useTabs": false,
"semi": false,
"singleQuote": true,
"arrowParens": "avoid",
"trailingComma": "es5",
"bracketSpacing": true,
"jsxBracketSameLine": false
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1012 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -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>

View File

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

View File

@ -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}`)))
}

View File

@ -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)))
}

View File

@ -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}`)))

View File

@ -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}`)))

View File

@ -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}`)))

View File

@ -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)

View File

@ -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>
)
}

View File

@ -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)

View File

@ -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)
)

View File

@ -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)

View File

@ -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)
)

View File

@ -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)

View File

@ -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)
})

View File

@ -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">&times;</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>
)
}

View File

@ -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)
)

View File

@ -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>
)
}
}

View File

@ -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>
)
}
}

View File

@ -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>
)}
</>
)
}
}

View File

@ -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>
)
}
}

View File

@ -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>
)
}

View File

@ -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)

View File

@ -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>
)
}

View File

@ -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>
)
}
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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)
)

View File

@ -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>
)
}

View File

@ -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)

View File

@ -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)
)

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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)
)

View File

@ -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>
)
}

View File

@ -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)

View File

@ -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>
)
}

View File

@ -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)
)

View File

@ -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)
)

View File

@ -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)
)

View File

@ -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)
)

View File

@ -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)

View File

@ -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)
)

View File

@ -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]} />
)}
</>
)
}

View File

@ -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>
)
}

View File

@ -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)

View File

@ -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>
)
}

View File

@ -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)

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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>
)
}

View File

@ -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)
)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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>
)
}
}

View File

@ -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)
)

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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()

View File

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

View File

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

View File

@ -1,5 +0,0 @@
{
"Personal records": "Personal records",
"This month": "This month",
"Upload one !": "Upload one !"
}

View File

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

View File

@ -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,
}

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More