Client - replace 'Activity' with 'Workout' - fix #58
This commit is contained in:
parent
3a80e01cc2
commit
5c04db6c08
@ -4,35 +4,35 @@ from selenium.webdriver.support.ui import Select, WebDriverWait
|
||||
from .utils import TEST_URL, register_valid_user
|
||||
|
||||
|
||||
class TestActivity:
|
||||
def test_user_can_add_activity_without_gpx(self, selenium):
|
||||
class TestWorkout:
|
||||
def test_user_can_add_workout_without_gpx(self, selenium):
|
||||
register_valid_user(selenium)
|
||||
nav_items = selenium.find_elements_by_class_name('nav-item')
|
||||
|
||||
nav_items[3].click()
|
||||
selenium.implicitly_wait(1)
|
||||
radio_buttons = selenium.find_elements_by_class_name(
|
||||
'add-activity-radio'
|
||||
'add-workout-radio'
|
||||
)
|
||||
radio_buttons[1].click()
|
||||
|
||||
selenium.find_element_by_name('title').send_keys('Activity title')
|
||||
selenium.find_element_by_name('title').send_keys('Workout title')
|
||||
select = Select(selenium.find_element_by_name('sport_id'))
|
||||
select.select_by_index(1)
|
||||
selenium.find_element_by_name('activity_date').send_keys('2018-12-20')
|
||||
selenium.find_element_by_name('activity_time').send_keys('14:05')
|
||||
selenium.find_element_by_name('workout_date').send_keys('2018-12-20')
|
||||
selenium.find_element_by_name('workout_time').send_keys('14:05')
|
||||
selenium.find_element_by_name('duration').send_keys('01:00:00')
|
||||
selenium.find_element_by_name('distance').send_keys('10')
|
||||
selenium.find_element_by_class_name('btn-primary').click()
|
||||
|
||||
WebDriverWait(selenium, 10).until(
|
||||
EC.url_changes(f"{TEST_URL}/activities/add")
|
||||
EC.url_changes(f"{TEST_URL}/workouts/add")
|
||||
)
|
||||
|
||||
activity_details = selenium.find_element_by_class_name(
|
||||
'activity-details'
|
||||
workout_details = selenium.find_element_by_class_name(
|
||||
'workout-details'
|
||||
).text
|
||||
assert 'Duration: 1:00:00' in activity_details
|
||||
assert 'Distance: 10 km' in activity_details
|
||||
assert 'Average speed: 10 km/h' in activity_details
|
||||
assert 'Max. speed: 10 km/h' in activity_details
|
||||
assert 'Duration: 1:00:00' in workout_details
|
||||
assert 'Distance: 10 km' in workout_details
|
||||
assert 'Average speed: 10 km/h' in workout_details
|
||||
assert 'Max. speed: 10 km/h' in workout_details
|
12
fittrackee/dist/asset-manifest.json
vendored
12
fittrackee/dist/asset-manifest.json
vendored
@ -1,14 +1,14 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.34182cc5.chunk.css",
|
||||
"main.js": "/static/js/main.385ae13c.chunk.js",
|
||||
"main.js.map": "/static/js/main.385ae13c.chunk.js.map",
|
||||
"main.css": "/static/css/main.06b7c846.chunk.css",
|
||||
"main.js": "/static/js/main.c96409fd.chunk.js",
|
||||
"main.js.map": "/static/js/main.c96409fd.chunk.js.map",
|
||||
"runtime-main.js": "/static/js/runtime-main.1240af94.js",
|
||||
"runtime-main.js.map": "/static/js/runtime-main.1240af94.js.map",
|
||||
"static/js/2.1b88ef3c.chunk.js": "/static/js/2.1b88ef3c.chunk.js",
|
||||
"static/js/2.1b88ef3c.chunk.js.map": "/static/js/2.1b88ef3c.chunk.js.map",
|
||||
"index.html": "/index.html",
|
||||
"static/css/main.34182cc5.chunk.css.map": "/static/css/main.34182cc5.chunk.css.map",
|
||||
"static/css/main.06b7c846.chunk.css.map": "/static/css/main.06b7c846.chunk.css.map",
|
||||
"static/js/2.1b88ef3c.chunk.js.LICENSE.txt": "/static/js/2.1b88ef3c.chunk.js.LICENSE.txt",
|
||||
"static/media/en.9e6dbfb0.svg": "/static/media/en.9e6dbfb0.svg",
|
||||
"static/media/fr.d0f9280c.svg": "/static/media/fr.d0f9280c.svg",
|
||||
@ -18,7 +18,7 @@
|
||||
"entrypoints": [
|
||||
"static/js/runtime-main.1240af94.js",
|
||||
"static/js/2.1b88ef3c.chunk.js",
|
||||
"static/css/main.34182cc5.chunk.css",
|
||||
"static/js/main.385ae13c.chunk.js"
|
||||
"static/css/main.06b7c846.chunk.css",
|
||||
"static/js/main.c96409fd.chunk.js"
|
||||
]
|
||||
}
|
2
fittrackee/dist/index.html
vendored
2
fittrackee/dist/index.html
vendored
@ -1 +1 @@
|
||||
<!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="/manifest.json"><link rel="shortcut icon" href="/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><link href="/static/css/main.34182cc5.chunk.css" rel="stylesheet"></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><script>!function(e){function t(t){for(var n,i,l=t[0],f=t[1],a=t[2],p=0,s=[];p<l.length;p++)i=l[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(t);s.length;)s.shift()();return u.push.apply(u,a||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,l=1;l<r.length;l++){var f=r[l];0!==o[f]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/";var l=this.webpackJsonpfittrackee_client=this.webpackJsonpfittrackee_client||[],f=l.push.bind(l);l.push=t,l=l.slice();for(var a=0;a<l.length;a++)t(l[a]);var c=f;r()}([])</script><script src="/static/js/2.1b88ef3c.chunk.js"></script><script src="/static/js/main.385ae13c.chunk.js"></script></body></html>
|
||||
<!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="/manifest.json"><link rel="shortcut icon" href="/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><link href="/static/css/main.06b7c846.chunk.css" rel="stylesheet"></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><script>!function(e){function t(t){for(var n,i,l=t[0],f=t[1],a=t[2],p=0,s=[];p<l.length;p++)i=l[p],Object.prototype.hasOwnProperty.call(o,i)&&o[i]&&s.push(o[i][0]),o[i]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(t);s.length;)s.shift()();return u.push.apply(u,a||[]),r()}function r(){for(var e,t=0;t<u.length;t++){for(var r=u[t],n=!0,l=1;l<r.length;l++){var f=r[l];0!==o[f]&&(n=!1)}n&&(u.splice(t--,1),e=i(i.s=r[0]))}return e}var n={},o={1:0},u=[];function i(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,i),r.l=!0,r.exports}i.m=e,i.c=n,i.d=function(e,t,r){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(i.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var n in e)i.d(r,n,function(t){return e[t]}.bind(null,n));return r},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="/";var l=this.webpackJsonpfittrackee_client=this.webpackJsonpfittrackee_client||[],f=l.push.bind(l);l.push=t,l=l.slice();for(var a=0;a<l.length;a++)t(l[a]);var c=f;r()}([])</script><script src="/static/js/2.1b88ef3c.chunk.js"></script><script src="/static/js/main.c96409fd.chunk.js"></script></body></html>
|
2
fittrackee/dist/static/css/main.06b7c846.chunk.css
vendored
Normal file
2
fittrackee/dist/static/css/main.06b7c846.chunk.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/css/main.06b7c846.chunk.css.map
vendored
Normal file
1
fittrackee/dist/static/css/main.06b7c846.chunk.css.map
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
fittrackee/dist/static/js/main.c96409fd.chunk.js
vendored
Normal file
2
fittrackee/dist/static/js/main.c96409fd.chunk.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
fittrackee/dist/static/js/main.c96409fd.chunk.js.map
vendored
Normal file
1
fittrackee/dist/static/js/main.c96409fd.chunk.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -1,192 +0,0 @@
|
||||
import FitTrackeeGenericApi from '../fitTrackeeApi'
|
||||
import { history } from '../index'
|
||||
import { formatChartData } from '../utils/activities'
|
||||
import { setError, setLoading } from './index'
|
||||
import { loadProfile } from './user'
|
||||
|
||||
export const pushActivities = activities => ({
|
||||
type: 'PUSH_ACTIVITIES',
|
||||
activities,
|
||||
})
|
||||
|
||||
export const removeActivity = activityId => ({
|
||||
type: 'REMOVE_ACTIVITY',
|
||||
activityId,
|
||||
})
|
||||
|
||||
export const updateCalendar = activities => ({
|
||||
type: 'UPDATE_CALENDAR',
|
||||
activities,
|
||||
})
|
||||
|
||||
export const setGpx = gpxContent => ({
|
||||
type: 'SET_GPX',
|
||||
gpxContent,
|
||||
})
|
||||
|
||||
export const setChartData = chartData => ({
|
||||
type: 'SET_CHART_DATA',
|
||||
chartData,
|
||||
})
|
||||
|
||||
export const addActivity = form => dispatch =>
|
||||
FitTrackeeGenericApi.addDataWithFile('activities', form)
|
||||
.then(ret => {
|
||||
if (ret.status === 'created') {
|
||||
if (ret.data.activities.length === 0) {
|
||||
dispatch(setError('activities|no correct file.'))
|
||||
} else if (ret.data.activities.length === 1) {
|
||||
dispatch(loadProfile())
|
||||
history.push(`/activities/${ret.data.activities[0].id}`)
|
||||
} else {
|
||||
// ret.data.activities.length > 1
|
||||
dispatch(loadProfile())
|
||||
history.push('/')
|
||||
}
|
||||
} else if (ret.status === 413) {
|
||||
dispatch(
|
||||
setError('activities|File size is greater than the allowed size')
|
||||
)
|
||||
} else {
|
||||
dispatch(setError(`activities|${ret.message}`))
|
||||
}
|
||||
dispatch(setLoading(false))
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(setLoading(false))
|
||||
dispatch(setError(`activities|${error}`))
|
||||
})
|
||||
|
||||
export const addActivityWithoutGpx = form => dispatch =>
|
||||
FitTrackeeGenericApi.addData('activities/no_gpx', form)
|
||||
.then(ret => {
|
||||
if (ret.status === 'created') {
|
||||
dispatch(loadProfile())
|
||||
history.push(`/activities/${ret.data.activities[0].id}`)
|
||||
} else {
|
||||
dispatch(setError(`activities|${ret.message}`))
|
||||
}
|
||||
})
|
||||
.catch(error => dispatch(setError(`activities|${error}`)))
|
||||
|
||||
export const getActivityGpx = activityId => dispatch => {
|
||||
if (activityId) {
|
||||
return FitTrackeeGenericApi.getData(`activities/${activityId}/gpx`)
|
||||
.then(ret => {
|
||||
if (ret.status === 'success') {
|
||||
dispatch(setGpx(ret.data.gpx))
|
||||
} else {
|
||||
dispatch(setError(`activities|${ret.message}`))
|
||||
}
|
||||
})
|
||||
.catch(error => dispatch(setError(`activities|${error}`)))
|
||||
}
|
||||
dispatch(setGpx(null))
|
||||
}
|
||||
|
||||
export const getSegmentGpx = (activityId, segmentId) => dispatch => {
|
||||
if (activityId) {
|
||||
return FitTrackeeGenericApi.getData(
|
||||
`activities/${activityId}/gpx/segment/${segmentId}`
|
||||
)
|
||||
.then(ret => {
|
||||
if (ret.status === 'success') {
|
||||
dispatch(setGpx(ret.data.gpx))
|
||||
} else {
|
||||
dispatch(setError(`activities|${ret.message}`))
|
||||
}
|
||||
})
|
||||
.catch(error => dispatch(setError(`activities|${error}`)))
|
||||
}
|
||||
dispatch(setGpx(null))
|
||||
}
|
||||
|
||||
export const getActivityChartData = activityId => dispatch => {
|
||||
if (activityId) {
|
||||
return FitTrackeeGenericApi.getData(`activities/${activityId}/chart_data`)
|
||||
.then(ret => {
|
||||
if (ret.status === 'success') {
|
||||
dispatch(setChartData(formatChartData(ret.data.chart_data)))
|
||||
} else {
|
||||
dispatch(setError(`activities|${ret.message}`))
|
||||
}
|
||||
})
|
||||
.catch(error => dispatch(setError(`activities|${error}`)))
|
||||
}
|
||||
dispatch(setChartData(null))
|
||||
}
|
||||
|
||||
export const getSegmentChartData = (activityId, segmentId) => dispatch => {
|
||||
if (activityId) {
|
||||
return FitTrackeeGenericApi.getData(
|
||||
`activities/${activityId}/chart_data/segment/${segmentId}`
|
||||
)
|
||||
.then(ret => {
|
||||
if (ret.status === 'success') {
|
||||
dispatch(setChartData(formatChartData(ret.data.chart_data)))
|
||||
} else {
|
||||
dispatch(setError(`activities|${ret.message}`))
|
||||
}
|
||||
})
|
||||
.catch(error => dispatch(setError(`activities|${error}`)))
|
||||
}
|
||||
dispatch(setChartData(null))
|
||||
}
|
||||
|
||||
export const deleteActivity = id => dispatch =>
|
||||
FitTrackeeGenericApi.deleteData('activities', id)
|
||||
.then(ret => {
|
||||
if (ret.status === 204) {
|
||||
Promise.resolve(dispatch(removeActivity(id)))
|
||||
.then(() => dispatch(loadProfile()))
|
||||
.then(() => history.push('/'))
|
||||
} else {
|
||||
dispatch(setError(`activities|${ret.status}`))
|
||||
}
|
||||
})
|
||||
.catch(error => dispatch(setError(`activities|${error}`)))
|
||||
|
||||
export const editActivity = form => dispatch =>
|
||||
FitTrackeeGenericApi.updateData('activities', form)
|
||||
.then(ret => {
|
||||
if (ret.status === 'success') {
|
||||
dispatch(loadProfile())
|
||||
history.push(`/activities/${ret.data.activities[0].id}`)
|
||||
} else {
|
||||
dispatch(setError(`activities|${ret.message}`))
|
||||
}
|
||||
dispatch(setLoading(false))
|
||||
})
|
||||
.catch(error => {
|
||||
dispatch(setLoading(false))
|
||||
dispatch(setError(`activities|${error}`))
|
||||
})
|
||||
|
||||
export const getMoreActivities = params => dispatch =>
|
||||
FitTrackeeGenericApi.getData('activities', params)
|
||||
.then(ret => {
|
||||
if (ret.status === 'success') {
|
||||
if (ret.data.activities.length > 0) {
|
||||
dispatch(pushActivities(ret.data.activities))
|
||||
}
|
||||
} else {
|
||||
dispatch(setError(`activities|${ret.message}`))
|
||||
}
|
||||
})
|
||||
.catch(error => dispatch(setError(`activities|${error}`)))
|
||||
|
||||
export const getMonthActivities = (from, to) => dispatch =>
|
||||
FitTrackeeGenericApi.getData('activities', {
|
||||
from,
|
||||
to,
|
||||
order: 'asc',
|
||||
per_page: 100,
|
||||
})
|
||||
.then(ret => {
|
||||
if (ret.status === 'success') {
|
||||
dispatch(updateCalendar(ret.data.activities))
|
||||
} else {
|
||||
dispatch(setError(`activities|${ret.message}`))
|
||||
}
|
||||
})
|
||||
.catch(error => dispatch(setError(`activities|${error}`)))
|
@ -47,7 +47,7 @@ export const getOrUpdateData = (
|
||||
canDispatch = true
|
||||
) => dispatch => {
|
||||
dispatch(setLoading(true))
|
||||
if (data && data.id && target !== 'activities' && isNaN(data.id)) {
|
||||
if (data && data.id && target !== 'workouts' && isNaN(data.id)) {
|
||||
dispatch(setLoading(false))
|
||||
return dispatch(setError(`${target}|Incorrect id`))
|
||||
}
|
||||
|
192
fittrackee_client/src/actions/workouts.js
Normal file
192
fittrackee_client/src/actions/workouts.js
Normal file
@ -0,0 +1,192 @@
|
||||
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: 'asc',
|
||||
per_page: 100,
|
||||
})
|
||||
.then(ret => {
|
||||
if (ret.status === 'success') {
|
||||
dispatch(updateCalendar(ret.data.workouts))
|
||||
} else {
|
||||
dispatch(setError(`workouts|${ret.message}`))
|
||||
}
|
||||
})
|
||||
.catch(error => dispatch(setError(`workouts|${error}`)))
|
@ -1,73 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import ActivityWeather from './ActivityWeather'
|
||||
|
||||
export default function ActivityDetails(props) {
|
||||
const { activity, t } = props
|
||||
const withPauses = activity.pauses !== '0:00:00' && activity.pauses !== null
|
||||
return (
|
||||
<div className="activity-details">
|
||||
<p>
|
||||
<i className="fa fa-clock-o custom-fa" aria-hidden="true" />
|
||||
{t('activities:Duration')}: {activity.moving}
|
||||
{activity.records &&
|
||||
activity.records.find(r => r.record_type === 'LD') && (
|
||||
<sup>
|
||||
<i className="fa fa-trophy custom-fa" aria-hidden="true" />
|
||||
</sup>
|
||||
)}
|
||||
{withPauses && (
|
||||
<span>
|
||||
<br />({t('activities:pauses')}: {activity.pauses},{' '}
|
||||
{t('activities:total duration')}: {activity.duration})
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<i className="fa fa-road custom-fa" aria-hidden="true" />
|
||||
{t('activities:Distance')}: {activity.distance} km
|
||||
{activity.records &&
|
||||
activity.records.find(r => r.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('activities:Average speed')}: {activity.ave_speed} km/h
|
||||
{activity.records &&
|
||||
activity.records.find(r => r.record_type === 'AS') && (
|
||||
<sup>
|
||||
<i className="fa fa-trophy custom-fa" aria-hidden="true" />
|
||||
</sup>
|
||||
)}
|
||||
<br />
|
||||
{t('activities:Max. speed')}: {activity.max_speed} km/h
|
||||
{activity.records &&
|
||||
activity.records.find(r => r.record_type === 'MS') && (
|
||||
<sup>
|
||||
<i className="fa fa-trophy custom-fa" aria-hidden="true" />
|
||||
</sup>
|
||||
)}
|
||||
</p>
|
||||
{activity.min_alt && activity.max_alt && (
|
||||
<p>
|
||||
<i className="fi-mountains custom-fa" />
|
||||
{t('activities:Min. altitude')}: {activity.min_alt}m
|
||||
<br />
|
||||
{t('activities:Max. altitude')}: {activity.max_alt}m
|
||||
</p>
|
||||
)}
|
||||
{activity.ascent && activity.descent && (
|
||||
<p>
|
||||
<i className="fa fa-location-arrow custom-fa" />
|
||||
{t('activities:Ascent')}: {activity.ascent}m
|
||||
<br />
|
||||
{t('activities:Descent')}: {activity.descent}m
|
||||
</p>
|
||||
)}
|
||||
<ActivityWeather activity={activity} t={t} />
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function ActivityNoMap(props) {
|
||||
const { t } = props
|
||||
return (
|
||||
<div className="activity-no-map text-center">{t('activities:No Map')}</div>
|
||||
)
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import ActivityAddOrEdit from './ActivityAddOrEdit'
|
||||
import { getOrUpdateData } from '../../actions'
|
||||
|
||||
class ActivityEdit extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.loadActivity(this.props.match.params.activityId)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { activities, message, sports } = this.props
|
||||
const [activity] = activities
|
||||
return (
|
||||
<div>
|
||||
{sports.length > 0 && (
|
||||
<ActivityAddOrEdit
|
||||
activity={activity}
|
||||
message={message}
|
||||
sports={sports}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
activities: state.activities.data,
|
||||
message: state.message,
|
||||
sports: state.sports.data,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadActivity: activityId => {
|
||||
dispatch(getOrUpdateData('getData', 'activities', { id: activityId }))
|
||||
},
|
||||
})
|
||||
)(ActivityEdit)
|
@ -1,42 +0,0 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Redirect, Route, Switch } from 'react-router-dom'
|
||||
|
||||
import ActivityAdd from './ActivityAdd'
|
||||
import ActivityDisplay from './ActivityDisplay'
|
||||
import ActivityEdit from './ActivityEdit'
|
||||
import NotFound from './../Others/NotFound'
|
||||
import { isLoggedIn } from '../../utils'
|
||||
|
||||
function Activity() {
|
||||
return (
|
||||
<div>
|
||||
{isLoggedIn() ? (
|
||||
<Switch>
|
||||
<Route exact path="/activities/add" component={ActivityAdd} />
|
||||
<Route
|
||||
exact
|
||||
path="/activities/:activityId"
|
||||
component={ActivityDisplay}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/activities/:activityId/edit"
|
||||
component={ActivityEdit}
|
||||
/>
|
||||
<Route
|
||||
path="/activities/:activityId/segment/:segmentId"
|
||||
component={ActivityDisplay}
|
||||
/>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
) : (
|
||||
<Redirect to="/login" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
user: state.user,
|
||||
}))(Activity)
|
@ -6,7 +6,7 @@ import AdminStats from './AdminStats'
|
||||
export default function AdminDashboard(props) {
|
||||
const { appConfig, t } = props
|
||||
return (
|
||||
<div className="card activity-card">
|
||||
<div className="card workout-card">
|
||||
<div className="card-header">
|
||||
<strong>{t('administration:Administration')}</strong>
|
||||
</div>
|
||||
|
@ -94,13 +94,13 @@ class AdminSports extends React.Component {
|
||||
updateSport(sport.id, !sport.is_active)
|
||||
}
|
||||
/>
|
||||
{sport.has_activities && (
|
||||
{sport.has_workouts && (
|
||||
<span className="admin-message">
|
||||
<i
|
||||
className="fa fa-warning custom-fa"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t('administration:activities exist')}
|
||||
{t('administration:workouts exist')}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
|
@ -16,7 +16,7 @@ class AdminStats extends React.Component {
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-lg-3 col-md-6 col-sm-6">
|
||||
<div className="card activity-card">
|
||||
<div className="card workout-card">
|
||||
<div className="card-body row">
|
||||
<div className="col-3">
|
||||
<i className="fa fa-users fa-3x fa-color" />
|
||||
@ -35,7 +35,7 @@ class AdminStats extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-3 col-md-6 col-sm-6">
|
||||
<div className="card activity-card">
|
||||
<div className="card workout-card">
|
||||
<div className="card-body row">
|
||||
<div className="col-3">
|
||||
<i className="fa fa-tags fa-3x fa-color" />
|
||||
@ -52,17 +52,17 @@ class AdminStats extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-3 col-md-6 col-sm-6">
|
||||
<div className="card activity-card">
|
||||
<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.activities ? appStats.activities : 0}
|
||||
{appStats.workouts ? appStats.workouts : 0}
|
||||
</div>
|
||||
<div>{`${
|
||||
appStats.activities === 1
|
||||
appStats.workouts === 1
|
||||
? t('common:workout')
|
||||
: t('common:workouts')
|
||||
}`}</div>
|
||||
@ -71,7 +71,7 @@ class AdminStats extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-3 col-md-6 col-sm-6">
|
||||
<div className="card activity-card">
|
||||
<div className="card workout-card">
|
||||
<div className="card-body row">
|
||||
<div className="col-3">
|
||||
<i className="fa fa-folder-open fa-3x fa-color" />
|
||||
|
@ -126,7 +126,7 @@ class AdminUsers extends React.Component {
|
||||
<th>{t('user:Username')}</th>
|
||||
<th>{t('user:Email')}</th>
|
||||
<th>{t('user:Registration Date')}</th>
|
||||
<th>{t('activities:Activities')}</th>
|
||||
<th>{t('workouts:Workouts')}</th>
|
||||
<th>{t('user:Admin')}</th>
|
||||
<th>{t('administration:Actions')}</th>
|
||||
</tr>
|
||||
@ -176,9 +176,9 @@ class AdminUsers extends React.Component {
|
||||
</td>
|
||||
<td>
|
||||
<span className="heading-span-absolute">
|
||||
{t('activities:Activities')}
|
||||
{t('workouts:Workouts')}
|
||||
</span>
|
||||
{user.nb_activities}
|
||||
{user.nb_workouts}
|
||||
</td>
|
||||
<td>
|
||||
<span className="heading-span-absolute">
|
||||
|
@ -64,106 +64,11 @@ label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.activities-result {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.activity-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.activity-details {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
.activity-date {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.activity-filter {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.activity-filter .col-2, .col-5{
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.activity-label {
|
||||
font-size: 0.8em;
|
||||
color: #666
|
||||
}
|
||||
|
||||
.activity-logo {
|
||||
margin: 0 5px;
|
||||
max-width: 20px;
|
||||
max-height: 20px;
|
||||
}
|
||||
|
||||
.activity-map {
|
||||
background-color: #eaeaea;
|
||||
height: 225px;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.activity-no-map {
|
||||
background-color: #eaeaea;
|
||||
color: #666666;
|
||||
font-style: italic;
|
||||
height: 400px;
|
||||
line-height: 400px;
|
||||
}
|
||||
|
||||
.activity-notes, .actvitiy-segments {
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
margin-top: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.activity-page {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.activity-segments-list {
|
||||
list-style: square;
|
||||
}
|
||||
|
||||
.activity-sport {
|
||||
margin-right: 1px;
|
||||
max-width: 18px;
|
||||
max-height: 18px;
|
||||
}
|
||||
|
||||
.activity-title img, .activity-title .map-attribution-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.activity-title img {
|
||||
border: 1px solid lightgrey;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
display: none;
|
||||
margin-left: 20px;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.activity-title .map-attribution-list {
|
||||
display: none;
|
||||
font-size: 11px;
|
||||
margin-left: 20px;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.activity-title:hover img, .activity-title:hover .map-attribution-list {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.add-activity {
|
||||
.add-workout {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.add-activity-radio {
|
||||
.add-workout-radio {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@ -199,7 +104,7 @@ label {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.chart-activities {
|
||||
.chart-workouts {
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
@ -238,7 +143,7 @@ label {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.col-activity-logo{
|
||||
.col-workout-logo{
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
@ -563,6 +468,101 @@ label {
|
||||
padding: 0.1em;
|
||||
}
|
||||
|
||||
.workouts-result {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.workout-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.workout-details {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
.workout-date {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.workout-filter {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.workout-filter .col-2, .col-5{
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.workout-label {
|
||||
font-size: 0.8em;
|
||||
color: #666
|
||||
}
|
||||
|
||||
.workout-logo {
|
||||
margin: 0 5px;
|
||||
max-width: 20px;
|
||||
max-height: 20px;
|
||||
}
|
||||
|
||||
.workout-map {
|
||||
background-color: #eaeaea;
|
||||
height: 225px;
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
.workout-no-map {
|
||||
background-color: #eaeaea;
|
||||
color: #666666;
|
||||
font-style: italic;
|
||||
height: 400px;
|
||||
line-height: 400px;
|
||||
}
|
||||
|
||||
.workout-notes, .actvitiy-segments {
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
margin-top: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.workout-page {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.workout-segments-list {
|
||||
list-style: square;
|
||||
}
|
||||
|
||||
.workout-sport {
|
||||
margin-right: 1px;
|
||||
max-width: 18px;
|
||||
max-height: 18px;
|
||||
}
|
||||
|
||||
.workout-title img, .workout-title .map-attribution-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.workout-title img {
|
||||
border: 1px solid lightgrey;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||
display: none;
|
||||
margin-left: 20px;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.workout-title .map-attribution-list {
|
||||
display: none;
|
||||
font-size: 11px;
|
||||
margin-left: 20px;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.workout-title:hover img, .workout-title:hover .map-attribution-list {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* responsive table */
|
||||
/* adapted from https://uglyduck.ca/making-tables-responsive-with-minimal-css/ */
|
||||
.heading-span,
|
||||
@ -745,7 +745,7 @@ label {
|
||||
background: #eff1f3;
|
||||
}
|
||||
|
||||
.calendar-activity,
|
||||
.calendar-workout,
|
||||
.calendar-more {
|
||||
display: none;
|
||||
}
|
||||
@ -767,15 +767,15 @@ label {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.calendar-activity-more {
|
||||
.calendar-workout-more {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
|
||||
.calendar-activity:nth-child(-n+2),
|
||||
.calendar-activity:nth-child(n+3) ~ .calendar-more,
|
||||
.calendar-activity-more:nth-child(n+3) {
|
||||
.calendar-workout:nth-child(-n+2),
|
||||
.calendar-workout:nth-child(n+3) ~ .calendar-more,
|
||||
.calendar-workout-more:nth-child(n+3) {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@ -783,9 +783,9 @@ label {
|
||||
|
||||
@media only screen and (min-width: 992px) and (max-width: 1200px) {
|
||||
|
||||
.calendar-activity:nth-child(-n+4),
|
||||
.calendar-activity:nth-child(n+5) ~ .calendar-more,
|
||||
.calendar-activity-more:nth-child(n+5) {
|
||||
.calendar-workout:nth-child(-n+4),
|
||||
.calendar-workout:nth-child(n+5) ~ .calendar-more,
|
||||
.calendar-workout-more:nth-child(n+5) {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@ -793,9 +793,9 @@ label {
|
||||
|
||||
@media only screen and (min-width: 1200px) {
|
||||
|
||||
.calendar-activity:nth-child(-n+6),
|
||||
.calendar-activity:nth-child(n+7) ~ .calendar-more,
|
||||
.calendar-activity-more:nth-child(n+7) {
|
||||
.calendar-workout:nth-child(-n+6),
|
||||
.calendar-workout:nth-child(n+7) ~ .calendar-more,
|
||||
.calendar-workout-more:nth-child(n+7) {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,8 @@ import { Route, Switch } from 'react-router-dom'
|
||||
|
||||
import './App.css'
|
||||
import Admin from './Admin'
|
||||
import Activity from './Activity'
|
||||
import Activities from './Activities'
|
||||
import Workout from './Workout'
|
||||
import Workouts from './Workouts'
|
||||
import CurrentUserProfile from './User/CurrentUserProfile'
|
||||
import Dashboard from './Dashboard'
|
||||
import Footer from './Footer'
|
||||
@ -68,10 +68,10 @@ class App extends React.Component {
|
||||
<Route exact path="/logout" component={Logout} />
|
||||
<Route exact path="/profile/edit" component={ProfileEdit} />
|
||||
<Route exact path="/profile" component={CurrentUserProfile} />
|
||||
<Route exact path="/activities/history" component={Activities} />
|
||||
<Route exact path="/activities/statistics" component={Statistics} />
|
||||
<Route exact path="/workouts/history" component={Workouts} />
|
||||
<Route exact path="/workouts/statistics" component={Statistics} />
|
||||
<Route exact path="/users/:userName" component={UserProfile} />
|
||||
<Route path="/activities" component={Activity} />
|
||||
<Route path="/workouts" component={Workout} />
|
||||
<Route path="/admin" component={Admin} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
|
@ -1,14 +1,14 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export default class NoActivities extends React.PureComponent {
|
||||
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: '/activities/add' }}>
|
||||
<Link to={{ pathname: '/workouts/add' }}>
|
||||
{t('dashboard:Upload one !')}
|
||||
</Link>
|
||||
</div>
|
@ -4,13 +4,13 @@ import { apiUrl } from '../../utils'
|
||||
|
||||
export default class StaticMap extends React.PureComponent {
|
||||
render() {
|
||||
const { activity, display } = this.props
|
||||
const { display, workout } = this.props
|
||||
|
||||
return (
|
||||
<div className={`activity-map${display === 'list' ? '-list' : ''}`}>
|
||||
<div className={`workout-map${display === 'list' ? '-list' : ''}`}>
|
||||
<img
|
||||
src={`${apiUrl}activities/map/${activity.map}?${Date.now()}`}
|
||||
alt="activity map"
|
||||
src={`${apiUrl}workouts/map/${workout.map}?${Date.now()}`}
|
||||
alt="workout map"
|
||||
/>
|
||||
<div className={`map-attribution${display === 'list' ? '-list' : ''}`}>
|
||||
<span className="map-attribution-text">©</span>
|
||||
|
@ -8,8 +8,8 @@ import {
|
||||
YAxis,
|
||||
} from 'recharts'
|
||||
|
||||
import { activityColors } from '../../../utils/activities'
|
||||
import { formatValue } from '../../../utils/stats'
|
||||
import { workoutColors } from '../../../utils/workouts'
|
||||
import CustomTooltip from './CustomTooltip'
|
||||
import CustomLabel from './CustomLabel'
|
||||
|
||||
@ -56,11 +56,11 @@ export default class StatsCharts extends React.PureComponent {
|
||||
<label className="radioLabel col">
|
||||
<input
|
||||
type="radio"
|
||||
name="activities"
|
||||
checked={displayedData === 'activities'}
|
||||
name="workouts"
|
||||
checked={displayedData === 'workouts'}
|
||||
onChange={e => this.handleRadioChange(e)}
|
||||
/>
|
||||
{t('statistics:activities')}
|
||||
{t('statistics:workouts')}
|
||||
</label>
|
||||
</div>
|
||||
<ResponsiveContainer height={300}>
|
||||
@ -81,7 +81,7 @@ export default class StatsCharts extends React.PureComponent {
|
||||
key={s.id}
|
||||
dataKey={s.label}
|
||||
stackId="a"
|
||||
fill={activityColors[i]}
|
||||
fill={workoutColors[i]}
|
||||
label={
|
||||
i === sports.length - 1 ? (
|
||||
<CustomLabel displayedData={displayedData} />
|
||||
|
@ -23,7 +23,7 @@ class Statistics extends React.PureComponent {
|
||||
|
||||
updateData() {
|
||||
if (this.props.user.username) {
|
||||
this.props.loadActivities(
|
||||
this.props.loadWorkouts(
|
||||
this.props.user.username,
|
||||
this.props.user.weekm,
|
||||
this.props.statsParams
|
||||
@ -62,7 +62,7 @@ export default connect(
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadActivities: (userName, weekm, data) => {
|
||||
loadWorkouts: (userName, weekm, data) => {
|
||||
const dateFormat = 'yyyy-MM-dd'
|
||||
// depends on user config (first day of week)
|
||||
const time =
|
||||
|
@ -17,8 +17,8 @@ import { enGB, fr } from 'date-fns/locale'
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import CalendarActivities from './CalendarActivities'
|
||||
import { getMonthActivities } from '../../actions/activities'
|
||||
import CalendarWorkouts from './CalendarWorkouts'
|
||||
import { getMonthWorkouts } from '../../actions/workouts'
|
||||
import { getDateWithTZ } from '../../utils'
|
||||
|
||||
const getStartAndEndMonth = (date, weekStartOnMonday) => {
|
||||
@ -44,7 +44,7 @@ class Calendar extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadMonthActivities(this.state.startDate, this.state.endDate)
|
||||
this.props.loadMonthWorkouts(this.state.startDate, this.state.endDate)
|
||||
}
|
||||
|
||||
renderHeader(localeOptions) {
|
||||
@ -81,11 +81,11 @@ class Calendar extends React.Component {
|
||||
return <div className="days row">{days}</div>
|
||||
}
|
||||
|
||||
filterActivities(day) {
|
||||
const { activities, user } = this.props
|
||||
if (activities) {
|
||||
return activities.filter(act =>
|
||||
isSameDay(getDateWithTZ(act.activity_date, user.timezone), day)
|
||||
filterWorkouts(day) {
|
||||
const { workouts, user } = this.props
|
||||
if (workouts) {
|
||||
return workouts.filter(act =>
|
||||
isSameDay(getDateWithTZ(act.workout_date, user.timezone), day)
|
||||
)
|
||||
}
|
||||
return []
|
||||
@ -105,7 +105,7 @@ class Calendar extends React.Component {
|
||||
while (day <= endDate) {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
formattedDate = format(day, dateFormat)
|
||||
const dayActivities = this.filterActivities(day)
|
||||
const dayWorkouts = this.filterWorkouts(day)
|
||||
const isDisabled = isSameMonth(day, currentMonth) ? '' : '-disabled'
|
||||
const isWeekEnd = weekStartOnMonday
|
||||
? [5, 6].includes(i)
|
||||
@ -119,8 +119,8 @@ class Calendar extends React.Component {
|
||||
>
|
||||
<div className={`img${isDisabled}`}>
|
||||
<span className="number">{formattedDate}</span>
|
||||
<CalendarActivities
|
||||
dayActivities={dayActivities}
|
||||
<CalendarWorkouts
|
||||
dayWorkouts={dayWorkouts}
|
||||
isDisabled={isDisabled}
|
||||
sports={sports}
|
||||
/>
|
||||
@ -149,7 +149,7 @@ class Calendar extends React.Component {
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
})
|
||||
this.props.loadMonthActivities(start, end)
|
||||
this.props.loadMonthWorkouts(start, end)
|
||||
}
|
||||
|
||||
handleNextMonth() {
|
||||
@ -167,7 +167,7 @@ class Calendar extends React.Component {
|
||||
locale: this.props.language === 'fr' ? fr : enGB,
|
||||
}
|
||||
return (
|
||||
<div className="card activity-card">
|
||||
<div className="card workout-card">
|
||||
<div className="calendar">
|
||||
{this.renderHeader(localeOptions)}
|
||||
{this.renderDays(localeOptions)}
|
||||
@ -180,16 +180,16 @@ class Calendar extends React.Component {
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
activities: state.calendarActivities.data,
|
||||
workouts: state.calendarWorkouts.data,
|
||||
language: state.language,
|
||||
sports: state.sports.data,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadMonthActivities: (start, end) => {
|
||||
loadMonthWorkouts: (start, end) => {
|
||||
const dateFormat = 'yyyy-MM-dd'
|
||||
dispatch(
|
||||
getMonthActivities(format(start, dateFormat), format(end, dateFormat))
|
||||
getMonthWorkouts(format(start, dateFormat), format(end, dateFormat))
|
||||
)
|
||||
},
|
||||
})
|
||||
|
@ -1,28 +1,28 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { recordsLabels } from '../../utils/activities'
|
||||
import { recordsLabels } from '../../utils/workouts'
|
||||
|
||||
export default function CalendarActivity(props) {
|
||||
const { activity, isDisabled, isMore, sportImg } = props
|
||||
export default function CalendarWorkout(props) {
|
||||
const { isDisabled, isMore, sportImg, workout } = props
|
||||
return (
|
||||
<Link
|
||||
className={`calendar-activity${isMore}`}
|
||||
to={`/activities/${activity.id}`}
|
||||
className={`calendar-workout${isMore}`}
|
||||
to={`/workouts/${workout.id}`}
|
||||
>
|
||||
<>
|
||||
<img
|
||||
alt="activity sport logo"
|
||||
className={`activity-sport ${isDisabled}`}
|
||||
alt="workout sport logo"
|
||||
className={`workout-sport ${isDisabled}`}
|
||||
src={sportImg}
|
||||
title={activity.title}
|
||||
title={workout.title}
|
||||
/>
|
||||
{activity.records.length > 0 && (
|
||||
{workout.records.length > 0 && (
|
||||
<sup>
|
||||
<i
|
||||
className="fa fa-trophy custom-fa-small"
|
||||
aria-hidden="true"
|
||||
title={activity.records.map(
|
||||
title={workout.records.map(
|
||||
rec =>
|
||||
` ${
|
||||
recordsLabels.filter(
|
@ -1,8 +1,8 @@
|
||||
import React from 'react'
|
||||
|
||||
import CalendarActivity from './CalendarActivity'
|
||||
import CalendarWorkout from './CalendarWorkout'
|
||||
|
||||
export default class CalendarActivities extends React.Component {
|
||||
export default class CalendarWorkouts extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
@ -17,33 +17,33 @@ export default class CalendarActivities extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dayActivities, isDisabled, sports } = this.props
|
||||
const { dayWorkouts, isDisabled, sports } = this.props
|
||||
const { isHidden } = this.state
|
||||
return (
|
||||
<div>
|
||||
{dayActivities.map(act => (
|
||||
<CalendarActivity
|
||||
{dayWorkouts.map(act => (
|
||||
<CalendarWorkout
|
||||
key={act.id}
|
||||
activity={act}
|
||||
workout={act}
|
||||
isDisabled={isDisabled}
|
||||
isMore=""
|
||||
sportImg={sports.filter(s => s.id === act.sport_id).map(s => s.img)}
|
||||
/>
|
||||
))}
|
||||
{dayActivities.length > 2 && (
|
||||
{dayWorkouts.length > 2 && (
|
||||
<i
|
||||
className={`fa fa-${isHidden ? 'plus' : 'times'} calendar-more`}
|
||||
aria-hidden="true"
|
||||
onClick={() => this.handleDisplayMore()}
|
||||
title="show more activities"
|
||||
title="show more workouts"
|
||||
/>
|
||||
)}
|
||||
{!isHidden && (
|
||||
<div className="calendar-display-more">
|
||||
{dayActivities.map(act => (
|
||||
<CalendarActivity
|
||||
{dayWorkouts.map(act => (
|
||||
<CalendarWorkout
|
||||
key={act.id}
|
||||
activity={act}
|
||||
workout={act}
|
||||
isDisabled={isDisabled}
|
||||
isMore="-more"
|
||||
sportImg={sports
|
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { formatRecord, translateSports } from '../../utils/activities'
|
||||
import { formatRecord, translateSports } from '../../utils/workouts'
|
||||
|
||||
export default function RecordsCard(props) {
|
||||
const { records, sports, t, user } = props
|
||||
@ -19,8 +19,8 @@ export default function RecordsCard(props) {
|
||||
}, {})
|
||||
|
||||
return (
|
||||
<div className="card activity-card">
|
||||
<div className="card-header">{t('activities:Personal records')}</div>
|
||||
<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.')
|
||||
@ -54,12 +54,12 @@ export default function RecordsCard(props) {
|
||||
{recordsBySport[sportLabel].records.map(rec => (
|
||||
<tr className="record-tr" key={rec.id}>
|
||||
<td className="record-td">
|
||||
{t(`activities:${rec.record_type}`)}
|
||||
{t(`workouts:${rec.record_type}`)}
|
||||
</td>
|
||||
<td className="record-td text-right">{rec.value}</td>
|
||||
<td className="record-td text-right">
|
||||
<Link to={`/activities/${rec.activity_id}`}>
|
||||
{rec.activity_date}
|
||||
<Link to={`/workouts/${rec.workout_id}`}>
|
||||
{rec.workout_date}
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -18,7 +18,7 @@ export default class Statistics extends React.Component {
|
||||
render() {
|
||||
const { t } = this.props
|
||||
return (
|
||||
<div className="card activity-card">
|
||||
<div className="card workout-card">
|
||||
<div className="card-header">{t('dashboard:This month')}</div>
|
||||
<div className="card-body">
|
||||
<Stats displayEmpty={false} statsParams={this.state} t={t} />
|
||||
|
@ -14,15 +14,15 @@ export default function UserStatistics(props) {
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-lg-3 col-md-6 col-sm-6">
|
||||
<div className="card activity-card">
|
||||
<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_activities}</div>
|
||||
<div className="huge">{user.nb_workouts}</div>
|
||||
<div>{`${
|
||||
user.nb_activities === 1
|
||||
user.nb_workouts === 1
|
||||
? t('common:workout')
|
||||
: t('common:workouts')
|
||||
}`}</div>
|
||||
@ -31,7 +31,7 @@ export default function UserStatistics(props) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-3 col-md-6 col-sm-6">
|
||||
<div className="card activity-card">
|
||||
<div className="card workout-card">
|
||||
<div className="card-body row">
|
||||
<div className="col-3">
|
||||
<i className="fa fa-road fa-3x fa-color" />
|
||||
@ -46,7 +46,7 @@ export default function UserStatistics(props) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-3 col-md-6 col-sm-6">
|
||||
<div className="card activity-card">
|
||||
<div className="card workout-card">
|
||||
<div className="card-body row">
|
||||
<div className="col-3">
|
||||
<i className="fa fa-clock-o fa-3x fa-color" />
|
||||
@ -59,7 +59,7 @@ export default function UserStatistics(props) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-lg-3 col-md-6 col-sm-6">
|
||||
<div className="card activity-card">
|
||||
<div className="card workout-card">
|
||||
<div className="card-body row">
|
||||
<div className="col-3">
|
||||
<i className="fa fa-tags fa-3x fa-color" />
|
||||
|
@ -5,35 +5,35 @@ import { Link } from 'react-router-dom'
|
||||
import StaticMap from '../Common/StaticMap'
|
||||
import { getDateWithTZ } from '../../utils'
|
||||
|
||||
export default function ActivityCard(props) {
|
||||
const { activity, sports, t, user } = props
|
||||
export default function WorkoutCard(props) {
|
||||
const { sports, t, user, workout } = props
|
||||
|
||||
return (
|
||||
<div className="card activity-card text-center">
|
||||
<div className="card workout-card text-center">
|
||||
<div className="card-header">
|
||||
<Link to={`/activities/${activity.id}`}>
|
||||
<Link to={`/workouts/${workout.id}`}>
|
||||
{sports
|
||||
.filter(sport => sport.id === activity.sport_id)
|
||||
.filter(sport => sport.id === workout.sport_id)
|
||||
.map(sport => t(`sports:${sport.label}`))}{' '}
|
||||
-{' '}
|
||||
{format(
|
||||
getDateWithTZ(activity.activity_date, user.timezone),
|
||||
getDateWithTZ(workout.workout_date, user.timezone),
|
||||
'dd/MM/yyyy HH:mm'
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
{activity.map && (
|
||||
{workout.map && (
|
||||
<div className="col">
|
||||
<StaticMap activity={activity} />
|
||||
<StaticMap workout={workout} />
|
||||
</div>
|
||||
)}
|
||||
<div className="col">
|
||||
<p>
|
||||
<i className="fa fa-clock-o" aria-hidden="true" />{' '}
|
||||
{t('activities:Duration')}: {activity.moving}
|
||||
{activity.map ? (
|
||||
{t('workouts:Duration')}: {workout.moving}
|
||||
{workout.map ? (
|
||||
<span>
|
||||
<br />
|
||||
<br />
|
||||
@ -42,7 +42,7 @@ export default function ActivityCard(props) {
|
||||
' - '
|
||||
)}
|
||||
<i className="fa fa-road" aria-hidden="true" />{' '}
|
||||
{t('activities:Distance')}: {activity.distance} km
|
||||
{t('workouts:Distance')}: {workout.distance} km
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
@ -3,15 +3,15 @@ import { Helmet } from 'react-helmet'
|
||||
import { withTranslation } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import ActivityCard from './ActivityCard'
|
||||
import Calendar from './Calendar'
|
||||
import Message from '../Common/Message'
|
||||
import NoActivities from '../Common/NoActivities'
|
||||
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 { getMoreActivities } from '../../actions/activities'
|
||||
import { getMoreWorkouts } from '../../actions/workouts'
|
||||
|
||||
class DashBoard extends React.Component {
|
||||
constructor(props, context) {
|
||||
@ -22,22 +22,22 @@ class DashBoard extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadActivities()
|
||||
this.props.loadWorkouts()
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
activities,
|
||||
loadMoreActivities,
|
||||
loadMoreWorkouts,
|
||||
message,
|
||||
records,
|
||||
sports,
|
||||
t,
|
||||
user,
|
||||
workouts,
|
||||
} = this.props
|
||||
const paginationEnd =
|
||||
activities.length > 0
|
||||
? activities[activities.length - 1].previous_activity === null
|
||||
workouts.length > 0
|
||||
? workouts[workouts.length - 1].previous_workout === null
|
||||
: true
|
||||
const { page } = this.state
|
||||
return (
|
||||
@ -48,7 +48,7 @@ class DashBoard extends React.Component {
|
||||
{message ? (
|
||||
<Message message={message} t={t} />
|
||||
) : (
|
||||
activities &&
|
||||
workouts &&
|
||||
user.total_duration &&
|
||||
sports.length > 0 && (
|
||||
<div className="container dashboard">
|
||||
@ -65,26 +65,26 @@ class DashBoard extends React.Component {
|
||||
</div>
|
||||
<div className="col-md-8">
|
||||
<Calendar weekm={user.weekm} />
|
||||
{activities.length > 0 ? (
|
||||
activities.map(activity => (
|
||||
<ActivityCard
|
||||
activity={activity}
|
||||
key={activity.id}
|
||||
{workouts.length > 0 ? (
|
||||
workouts.map(workout => (
|
||||
<WorkoutCard
|
||||
workout={workout}
|
||||
key={workout.id}
|
||||
sports={sports}
|
||||
t={t}
|
||||
user={user}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<NoActivities t={t} />
|
||||
<NoWorkouts t={t} />
|
||||
)}
|
||||
{!paginationEnd && (
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-default btn-md btn-block"
|
||||
value="Load more activities"
|
||||
value="Load more workouts"
|
||||
onClick={() => {
|
||||
loadMoreActivities(page + 1)
|
||||
loadMoreWorkouts(page + 1)
|
||||
this.setState({ page: page + 1 })
|
||||
}}
|
||||
/>
|
||||
@ -102,19 +102,19 @@ class DashBoard extends React.Component {
|
||||
export default withTranslation()(
|
||||
connect(
|
||||
state => ({
|
||||
activities: state.activities.data,
|
||||
workouts: state.workouts.data,
|
||||
message: state.message,
|
||||
records: state.records.data,
|
||||
sports: state.sports.data,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadActivities: () => {
|
||||
dispatch(getOrUpdateData('getData', 'activities', { page: 1 }))
|
||||
loadWorkouts: () => {
|
||||
dispatch(getOrUpdateData('getData', 'workouts', { page: 1 }))
|
||||
dispatch(getOrUpdateData('getData', 'records'))
|
||||
},
|
||||
loadMoreActivities: page => {
|
||||
dispatch(getMoreActivities({ page }))
|
||||
loadMoreWorkouts: page => {
|
||||
dispatch(getMoreWorkouts({ page }))
|
||||
},
|
||||
})
|
||||
)(DashBoard)
|
||||
|
@ -45,7 +45,7 @@ class NavBar extends React.PureComponent {
|
||||
<Link
|
||||
className="nav-link"
|
||||
to={{
|
||||
pathname: '/activities/history',
|
||||
pathname: '/workouts/history',
|
||||
}}
|
||||
>
|
||||
{t('Workouts')}
|
||||
@ -57,7 +57,7 @@ class NavBar extends React.PureComponent {
|
||||
<Link
|
||||
className="nav-link"
|
||||
to={{
|
||||
pathname: '/activities/statistics',
|
||||
pathname: '/workouts/statistics',
|
||||
}}
|
||||
>
|
||||
{t('common:Statistics')}
|
||||
@ -81,7 +81,7 @@ class NavBar extends React.PureComponent {
|
||||
<Link
|
||||
className="nav-link"
|
||||
to={{
|
||||
pathname: '/activities/add',
|
||||
pathname: '/workouts/add',
|
||||
}}
|
||||
>
|
||||
<strong>{t('common:Add workout')}</strong>
|
||||
|
@ -17,9 +17,9 @@ import { Helmet } from 'react-helmet'
|
||||
import { withTranslation } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import NoActivities from '../Common/NoActivities'
|
||||
import NoWorkouts from '../Common/NoWorkouts'
|
||||
import Stats from '../Common/Stats'
|
||||
import { activityColors, translateSports } from '../../utils/activities'
|
||||
import { workoutColors, translateSports } from '../../utils/workouts'
|
||||
|
||||
const durations = ['week', 'month', 'year']
|
||||
|
||||
@ -127,11 +127,11 @@ class Statistics extends React.Component {
|
||||
<title>FitTrackee - {t('statistics:Statistics')}</title>
|
||||
</Helmet>
|
||||
<div className="container dashboard">
|
||||
<div className="card activity-card">
|
||||
<div className="card workout-card">
|
||||
<div className="card-header">{t('statistics:Statistics')}</div>
|
||||
<div
|
||||
className={`card-body${
|
||||
user.nb_activities === 0 ? ' stats-disabled' : ''
|
||||
user.nb_workouts === 0 ? ' stats-disabled' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="chart-filters row">
|
||||
@ -176,16 +176,16 @@ class Statistics extends React.Component {
|
||||
statsParams={statsParams}
|
||||
t={t}
|
||||
/>
|
||||
<div className="row chart-activities">
|
||||
<div className="row chart-workouts">
|
||||
{translatedSports.map(sport => (
|
||||
<label className="col activity-label" key={sport.id}>
|
||||
<label className="col workout-label" key={sport.id}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={displayedSports.includes(sport.id)}
|
||||
name={sport.label}
|
||||
onChange={() => this.handleOnChangeSports(sport.id)}
|
||||
/>
|
||||
<span style={{ color: activityColors[sport.id - 1] }}>
|
||||
<span style={{ color: workoutColors[sport.id - 1] }}>
|
||||
{` ${sport.label}`}
|
||||
</span>
|
||||
</label>
|
||||
@ -193,7 +193,7 @@ class Statistics extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{user.nb_activities === 0 && <NoActivities t={t} />}
|
||||
{user.nb_workouts === 0 && <NoWorkouts t={t} />}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
@ -1,13 +1,13 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import ActivityAddOrEdit from './ActivityAddOrEdit'
|
||||
import WorkoutAddOrEdit from './WorkoutAddOrEdit'
|
||||
|
||||
function ActivityAdd(props) {
|
||||
function WorkoutAdd(props) {
|
||||
const { message, sports } = props
|
||||
return (
|
||||
<div>
|
||||
<ActivityAddOrEdit activity={null} message={message} sports={sports} />
|
||||
<WorkoutAddOrEdit workout={null} message={message} sports={sports} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -16,4 +16,4 @@ export default connect(state => ({
|
||||
message: state.message,
|
||||
sports: state.sports.data,
|
||||
user: state.user,
|
||||
}))(ActivityAdd)
|
||||
}))(WorkoutAdd)
|
@ -3,11 +3,11 @@ import { Helmet } from 'react-helmet'
|
||||
import { withTranslation } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import FormWithGpx from './ActivityForms/FormWithGpx'
|
||||
import FormWithoutGpx from './ActivityForms/FormWithoutGpx'
|
||||
import FormWithGpx from './WorkoutForms/FormWithGpx'
|
||||
import FormWithoutGpx from './WorkoutForms/FormWithoutGpx'
|
||||
import Message from '../Common/Message'
|
||||
|
||||
class ActivityAddEdit extends React.Component {
|
||||
class WorkoutAddEdit extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
@ -25,16 +25,16 @@ class ActivityAddEdit extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { activity, loading, message, sports, t } = this.props
|
||||
const { loading, message, sports, t, workout } = this.props
|
||||
const { withGpx } = this.state
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>
|
||||
FitTrackee -{' '}
|
||||
{activity
|
||||
? t('activities:Edit a workout')
|
||||
: t('activities:Add a workout')}
|
||||
{workout
|
||||
? t('workouts:Edit a workout')
|
||||
: t('workouts:Add a workout')}
|
||||
</title>
|
||||
</Helmet>
|
||||
<br />
|
||||
@ -44,22 +44,18 @@ class ActivityAddEdit extends React.Component {
|
||||
<div className="row">
|
||||
<div className="col-md-2" />
|
||||
<div className="col-md-8">
|
||||
<div className="card add-activity">
|
||||
<div className="card add-workout">
|
||||
<h2 className="card-header text-center">
|
||||
{activity
|
||||
? t('activities:Edit a workout')
|
||||
: t('activities:Add a workout')}
|
||||
{workout
|
||||
? t('workouts:Edit a workout')
|
||||
: t('workouts:Add a workout')}
|
||||
</h2>
|
||||
<div className="card-body">
|
||||
{activity ? (
|
||||
activity.with_gpx ? (
|
||||
<FormWithGpx activity={activity} sports={sports} t={t} />
|
||||
{workout ? (
|
||||
workout.with_gpx ? (
|
||||
<FormWithGpx workout={workout} sports={sports} t={t} />
|
||||
) : (
|
||||
<FormWithoutGpx
|
||||
activity={activity}
|
||||
sports={sports}
|
||||
t={t}
|
||||
/>
|
||||
<FormWithoutGpx workout={workout} sports={sports} t={t} />
|
||||
)
|
||||
) : (
|
||||
<div>
|
||||
@ -68,7 +64,7 @@ class ActivityAddEdit extends React.Component {
|
||||
<div className="col">
|
||||
<label className="radioLabel">
|
||||
<input
|
||||
className="add-activity-radio"
|
||||
className="add-workout-radio"
|
||||
type="radio"
|
||||
name="withGpx"
|
||||
disabled={loading}
|
||||
@ -77,13 +73,13 @@ class ActivityAddEdit extends React.Component {
|
||||
this.handleRadioChange(event)
|
||||
}
|
||||
/>
|
||||
{t('activities:with gpx file')}
|
||||
{t('workouts:with gpx file')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="col">
|
||||
<label className="radioLabel">
|
||||
<input
|
||||
className="add-activity-radio"
|
||||
className="add-workout-radio"
|
||||
type="radio"
|
||||
name="withoutGpx"
|
||||
disabled={loading}
|
||||
@ -92,7 +88,7 @@ class ActivityAddEdit extends React.Component {
|
||||
this.handleRadioChange(event)
|
||||
}
|
||||
/>
|
||||
{t('activities:without gpx file')}
|
||||
{t('workouts:without gpx file')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@ -118,5 +114,5 @@ class ActivityAddEdit extends React.Component {
|
||||
export default withTranslation()(
|
||||
connect(state => ({
|
||||
loading: state.loading,
|
||||
}))(ActivityAddEdit)
|
||||
}))(WorkoutAddEdit)
|
||||
)
|
@ -12,7 +12,7 @@ export default function Map({ bounds, coordinates, jsonData, mapAttribution }) {
|
||||
<TileLayer
|
||||
// eslint-disable-next-line max-len
|
||||
attribution={mapAttribution}
|
||||
url={`${apiUrl}activities/map_tile/{s}/{z}/{x}/{y}.png`}
|
||||
url={`${apiUrl}workouts/map_tile/{s}/{z}/{x}/{y}.png`}
|
||||
/>
|
||||
<GeoJSON
|
||||
// hash as a key to force re-rendering
|
@ -2,11 +2,10 @@ import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { getDateWithTZ } from '../../../utils'
|
||||
import { formatActivityDate } from '../../../utils/activities'
|
||||
import { formatWorkoutDate } from '../../../utils/workouts'
|
||||
|
||||
export default function ActivityCardHeader(props) {
|
||||
export default function WorkoutCardHeader(props) {
|
||||
const {
|
||||
activity,
|
||||
dataType,
|
||||
displayModal,
|
||||
segmentId,
|
||||
@ -14,22 +13,23 @@ export default function ActivityCardHeader(props) {
|
||||
t,
|
||||
title,
|
||||
user,
|
||||
workout,
|
||||
} = props
|
||||
const activityDate = activity
|
||||
? formatActivityDate(getDateWithTZ(activity.activity_date, user.timezone))
|
||||
const workoutDate = workout
|
||||
? formatWorkoutDate(getDateWithTZ(workout.workout_date, user.timezone))
|
||||
: null
|
||||
|
||||
const previousUrl =
|
||||
dataType === 'segment' && segmentId !== 1
|
||||
? `/activities/${activity.id}/segment/${segmentId - 1}`
|
||||
: dataType === 'activity' && activity.previous_activity
|
||||
? `/activities/${activity.previous_activity}`
|
||||
? `/workouts/${workout.id}/segment/${segmentId - 1}`
|
||||
: dataType === 'workout' && workout.previous_workout
|
||||
? `/workouts/${workout.previous_workout}`
|
||||
: null
|
||||
const nextUrl =
|
||||
dataType === 'segment' && segmentId < activity.segments.length
|
||||
? `/activities/${activity.id}/segment/${segmentId + 1}`
|
||||
: dataType === 'activity' && activity.next_activity
|
||||
? `/activities/${activity.next_activity}`
|
||||
dataType === 'segment' && segmentId < workout.segments.length
|
||||
? `/workouts/${workout.id}/segment/${segmentId + 1}`
|
||||
: dataType === 'workout' && workout.next_workout
|
||||
? `/workouts/${workout.next_workout}`
|
||||
: null
|
||||
|
||||
return (
|
||||
@ -41,53 +41,53 @@ export default function ActivityCardHeader(props) {
|
||||
<i
|
||||
className="fa fa-chevron-left"
|
||||
aria-hidden="true"
|
||||
title={t(`activities:See previous ${dataType}`)}
|
||||
title={t(`workouts:See previous ${dataType}`)}
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<i
|
||||
className="fa fa-chevron-left inactive-link"
|
||||
aria-hidden="true"
|
||||
title={t(`activities:No previous ${dataType}`)}
|
||||
title={t(`workouts:No previous ${dataType}`)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-auto col-activity-logo">
|
||||
<div className="col-auto col-workout-logo">
|
||||
<img className="sport-img-medium" src={sport.img} alt="sport logo" />
|
||||
</div>
|
||||
<div className="col">
|
||||
{dataType === 'activity' ? (
|
||||
{dataType === 'workout' ? (
|
||||
<>
|
||||
{title}{' '}
|
||||
<Link className="unlink" to={`/activities/${activity.id}/edit`}>
|
||||
<Link className="unlink" to={`/workouts/${workout.id}/edit`}>
|
||||
<i
|
||||
className="fa fa-edit custom-fa"
|
||||
aria-hidden="true"
|
||||
title={t('activities:Edit activity')}
|
||||
title={t('workouts:Edit workout')}
|
||||
/>
|
||||
</Link>
|
||||
<i
|
||||
className="fa fa-trash custom-fa"
|
||||
aria-hidden="true"
|
||||
onClick={() => displayModal(true)}
|
||||
title={t('activities:Delete activity')}
|
||||
title={t('workouts:Delete workout')}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* prettier-ignore */}
|
||||
<Link
|
||||
to={`/activities/${activity.id}`}
|
||||
to={`/workouts/${workout.id}`}
|
||||
>
|
||||
{title}
|
||||
</Link>{' '}
|
||||
- {t('activities:segment')} {segmentId}
|
||||
- {t('workouts:segment')} {segmentId}
|
||||
</>
|
||||
)}
|
||||
<br />
|
||||
{activityDate && (
|
||||
<span className="activity-date">
|
||||
{`${activityDate.activity_date} - ${activityDate.activity_time}`}
|
||||
{workoutDate && (
|
||||
<span className="workout-date">
|
||||
{`${workoutDate.workout_date} - ${workoutDate.workout_time}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@ -97,14 +97,14 @@ export default function ActivityCardHeader(props) {
|
||||
<i
|
||||
className="fa fa-chevron-right"
|
||||
aria-hidden="true"
|
||||
title={t(`activities:See next ${dataType}`)}
|
||||
title={t(`workouts:See next ${dataType}`)}
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<i
|
||||
className="fa fa-chevron-right inactive-link"
|
||||
aria-hidden="true"
|
||||
title={t(`activities:No next ${dataType}`)}
|
||||
title={t(`workouts:No next ${dataType}`)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
@ -12,11 +12,11 @@ import {
|
||||
} from 'recharts'
|
||||
|
||||
import {
|
||||
getActivityChartData,
|
||||
getSegmentChartData,
|
||||
} from '../../../actions/activities'
|
||||
getWorkoutChartData,
|
||||
} from '../../../actions/workouts'
|
||||
|
||||
class ActivityCharts extends React.Component {
|
||||
class WorkoutCharts extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
@ -26,31 +26,31 @@ class ActivityCharts extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.dataType === 'activity') {
|
||||
this.props.loadActivityData(this.props.activity.id)
|
||||
if (this.props.dataType === 'workout') {
|
||||
this.props.loadWorkoutData(this.props.workout.id)
|
||||
} else {
|
||||
this.props.loadSegmentData(this.props.activity.id, this.props.segmentId)
|
||||
this.props.loadSegmentData(this.props.workout.id, this.props.segmentId)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
(this.props.dataType === 'activity' &&
|
||||
prevProps.activity.id !== this.props.activity.id) ||
|
||||
(this.props.dataType === 'activity' && prevProps.dataType === 'segment')
|
||||
(this.props.dataType === 'workout' &&
|
||||
prevProps.workout.id !== this.props.workout.id) ||
|
||||
(this.props.dataType === 'workout' && prevProps.dataType === 'segment')
|
||||
) {
|
||||
this.props.loadActivityData(this.props.activity.id)
|
||||
this.props.loadWorkoutData(this.props.workout.id)
|
||||
}
|
||||
if (
|
||||
this.props.dataType === 'segment' &&
|
||||
prevProps.segmentId !== this.props.segmentId
|
||||
) {
|
||||
this.props.loadSegmentData(this.props.activity.id, this.props.segmentId)
|
||||
this.props.loadSegmentData(this.props.workout.id, this.props.segmentId)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.loadActivityData(null)
|
||||
this.props.loadWorkoutData(null)
|
||||
}
|
||||
|
||||
handleRadioChange(changeEvent) {
|
||||
@ -102,7 +102,7 @@ class ActivityCharts extends React.Component {
|
||||
checked={displayDistance}
|
||||
onChange={e => this.handleRadioChange(e)}
|
||||
/>
|
||||
{t('activities:distance')}
|
||||
{t('workouts:distance')}
|
||||
</label>
|
||||
<label className="radioLabel col-md-1">
|
||||
<input
|
||||
@ -111,7 +111,7 @@ class ActivityCharts extends React.Component {
|
||||
checked={!displayDistance}
|
||||
onChange={e => this.handleRadioChange(e)}
|
||||
/>
|
||||
{t('activities:duration')}
|
||||
{t('workouts:duration')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="row chart-radio">
|
||||
@ -123,7 +123,7 @@ class ActivityCharts extends React.Component {
|
||||
checked={this.displayData('speed')}
|
||||
onChange={e => this.handleLegendChange(e)}
|
||||
/>
|
||||
{t('activities:speed')}
|
||||
{t('workouts:speed')}
|
||||
</label>
|
||||
<label className="radioLabel col-md-1">
|
||||
<input
|
||||
@ -132,7 +132,7 @@ class ActivityCharts extends React.Component {
|
||||
checked={this.displayData('elevation')}
|
||||
onChange={e => this.handleLegendChange(e)}
|
||||
/>
|
||||
{t('activities:elevation')}
|
||||
{t('workouts:elevation')}
|
||||
</label>
|
||||
<div className="col-md-5" />
|
||||
</div>
|
||||
@ -148,7 +148,7 @@ class ActivityCharts extends React.Component {
|
||||
allowDecimals={false}
|
||||
dataKey={xDataKey}
|
||||
label={{
|
||||
value: t(`activities:${xDataKey}`),
|
||||
value: t(`workouts:${xDataKey}`),
|
||||
offset: 0,
|
||||
position: 'bottom',
|
||||
}}
|
||||
@ -161,7 +161,7 @@ class ActivityCharts extends React.Component {
|
||||
/>
|
||||
<YAxis
|
||||
label={{
|
||||
value: `${t('activities:speed')} (km/h)`,
|
||||
value: `${t('workouts:speed')} (km/h)`,
|
||||
angle: -90,
|
||||
position: 'left',
|
||||
}}
|
||||
@ -169,7 +169,7 @@ class ActivityCharts extends React.Component {
|
||||
/>
|
||||
<YAxis
|
||||
label={{
|
||||
value: `${t('activities:elevation')} (m)`,
|
||||
value: `${t('workouts:elevation')} (m)`,
|
||||
angle: -90,
|
||||
position: 'right',
|
||||
}}
|
||||
@ -181,7 +181,7 @@ class ActivityCharts extends React.Component {
|
||||
yAxisId="right"
|
||||
type="linear"
|
||||
dataKey="elevation"
|
||||
name={t('activities:elevation')}
|
||||
name={t('workouts:elevation')}
|
||||
fill="#e5e5e5"
|
||||
stroke="#cccccc"
|
||||
dot={false}
|
||||
@ -193,7 +193,7 @@ class ActivityCharts extends React.Component {
|
||||
yAxisId="left"
|
||||
type="linear"
|
||||
dataKey="speed"
|
||||
name={t('activities:speed')}
|
||||
name={t('workouts:speed')}
|
||||
stroke="#8884d8"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
@ -203,8 +203,8 @@ class ActivityCharts extends React.Component {
|
||||
<Tooltip
|
||||
labelFormatter={value =>
|
||||
displayDistance
|
||||
? `${t('activities:distance')}: ${value} km`
|
||||
: `${t('activities:duration')}: ${format(
|
||||
? `${t('workouts:distance')}: ${value} km`
|
||||
: `${t('workouts:duration')}: ${format(
|
||||
value,
|
||||
'HH:mm:ss'
|
||||
)}`
|
||||
@ -214,11 +214,11 @@ class ActivityCharts extends React.Component {
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="chart-info">
|
||||
{t('activities:data from gpx, without any cleaning')}
|
||||
{t('workouts:data from gpx, without any cleaning')}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
t('activities:No data to display')
|
||||
t('workouts:No data to display')
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@ -230,11 +230,11 @@ export default connect(
|
||||
chartData: state.chartData,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadActivityData: activityId => {
|
||||
dispatch(getActivityChartData(activityId))
|
||||
loadWorkoutData: workoutId => {
|
||||
dispatch(getWorkoutChartData(workoutId))
|
||||
},
|
||||
loadSegmentData: (activityId, segmentId) => {
|
||||
dispatch(getSegmentChartData(activityId, segmentId))
|
||||
loadSegmentData: (workoutId, segmentId) => {
|
||||
dispatch(getSegmentChartData(workoutId, segmentId))
|
||||
},
|
||||
})
|
||||
)(ActivityCharts)
|
||||
)(WorkoutCharts)
|
@ -0,0 +1,73 @@
|
||||
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>
|
||||
)
|
||||
}
|
@ -3,10 +3,10 @@ import { MapContainer } from 'react-leaflet'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import Map from './Map'
|
||||
import { getActivityGpx, getSegmentGpx } from '../../../actions/activities'
|
||||
import { getGeoJson } from '../../../utils/activities'
|
||||
import { getSegmentGpx, getWorkoutGpx } from '../../../actions/workouts'
|
||||
import { getGeoJson } from '../../../utils/workouts'
|
||||
|
||||
class ActivityMap extends React.Component {
|
||||
class WorkoutMap extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
@ -15,39 +15,39 @@ class ActivityMap extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.dataType === 'activity') {
|
||||
this.props.loadActivityGpx(this.props.activity.id)
|
||||
if (this.props.dataType === 'workout') {
|
||||
this.props.loadWorkoutGpx(this.props.workout.id)
|
||||
} else {
|
||||
this.props.loadSegmentGpx(this.props.activity.id, this.props.segmentId)
|
||||
this.props.loadSegmentGpx(this.props.workout.id, this.props.segmentId)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
(this.props.dataType === 'activity' &&
|
||||
prevProps.activity.id !== this.props.activity.id) ||
|
||||
(this.props.dataType === 'activity' && prevProps.dataType === 'segment')
|
||||
(this.props.dataType === 'workout' &&
|
||||
prevProps.workout.id !== this.props.workout.id) ||
|
||||
(this.props.dataType === 'workout' && prevProps.dataType === 'segment')
|
||||
) {
|
||||
this.props.loadActivityGpx(this.props.activity.id)
|
||||
this.props.loadWorkoutGpx(this.props.workout.id)
|
||||
}
|
||||
if (
|
||||
this.props.dataType === 'segment' &&
|
||||
prevProps.segmentId !== this.props.segmentId
|
||||
) {
|
||||
this.props.loadSegmentGpx(this.props.activity.id, this.props.segmentId)
|
||||
this.props.loadSegmentGpx(this.props.workout.id, this.props.segmentId)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.loadActivityGpx(null)
|
||||
this.props.loadWorkoutGpx(null)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { activity, coordinates, gpxContent, mapAttribution } = this.props
|
||||
const { coordinates, gpxContent, mapAttribution, workout } = this.props
|
||||
const { jsonData } = getGeoJson(gpxContent)
|
||||
const bounds = [
|
||||
[activity.bounds[0], activity.bounds[1]],
|
||||
[activity.bounds[2], activity.bounds[3]],
|
||||
[workout.bounds[0], workout.bounds[1]],
|
||||
[workout.bounds[2], workout.bounds[3]],
|
||||
]
|
||||
|
||||
return (
|
||||
@ -77,11 +77,11 @@ export default connect(
|
||||
mapAttribution: state.application.config.map_attribution,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadActivityGpx: activityId => {
|
||||
dispatch(getActivityGpx(activityId))
|
||||
loadWorkoutGpx: workoutId => {
|
||||
dispatch(getWorkoutGpx(workoutId))
|
||||
},
|
||||
loadSegmentGpx: (activityId, segmentId) => {
|
||||
dispatch(getSegmentGpx(activityId, segmentId))
|
||||
loadSegmentGpx: (workoutId, segmentId) => {
|
||||
dispatch(getSegmentGpx(workoutId, segmentId))
|
||||
},
|
||||
})
|
||||
)(ActivityMap)
|
||||
)(WorkoutMap)
|
@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function WorkoutNoMap(props) {
|
||||
const { t } = props
|
||||
return (
|
||||
<div className="workout-no-map text-center">{t('workouts:No Map')}</div>
|
||||
)
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function ActivityNotes(props) {
|
||||
export default function WorkoutNotes(props) {
|
||||
const { notes, t } = props
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<div className="card activity-card">
|
||||
<div className="card workout-card">
|
||||
<div className="card-body">
|
||||
Notes
|
||||
<div className="activity-notes">
|
||||
{notes ? notes : t('activities:No notes')}
|
||||
<div className="workout-notes">
|
||||
{notes ? notes : t('workouts:No notes')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1,31 +1,31 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export default function ActivitySegments(props) {
|
||||
export default function WorkoutSegments(props) {
|
||||
const { segments, t } = props
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<div className="card activity-card">
|
||||
<div className="card workout-card">
|
||||
<div className="card-body">
|
||||
{t('activities:Segments')}
|
||||
<div className="activity-segments">
|
||||
{t('workouts:Segments')}
|
||||
<div className="workout-segments">
|
||||
<ul>
|
||||
{segments.map((segment, index) => (
|
||||
<li
|
||||
className="activity-segments-list"
|
||||
className="workout-segments-list"
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`segment-${index}`}
|
||||
>
|
||||
<Link
|
||||
to={`/activities/${segment.activity_id}/segment/${
|
||||
to={`/workouts/${segment.workout_id}/segment/${
|
||||
index + 1
|
||||
}`}
|
||||
>
|
||||
{t('activities:segment')} {index + 1}
|
||||
{t('workouts:segment')} {index + 1}
|
||||
</Link>{' '}
|
||||
({t('activities:distance')}: {segment.distance} km,{' '}
|
||||
{t('activities:duration')}: {segment.duration})
|
||||
({t('workouts:distance')}: {segment.distance} km,{' '}
|
||||
{t('workouts:duration')}: {segment.duration})
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
@ -1,32 +1,32 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function ActivityWeather(props) {
|
||||
const { activity, t } = props
|
||||
export default function WorkoutWeather(props) {
|
||||
const { t, workout } = props
|
||||
return (
|
||||
<div className="container">
|
||||
{activity.weather_start && activity.weather_end && (
|
||||
{workout.weather_start && workout.weather_end && (
|
||||
<table className="table table-borderless weather-table text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>
|
||||
{t('activities:Start')}
|
||||
{t('workouts:Start')}
|
||||
<br />
|
||||
<img
|
||||
className="weather-img"
|
||||
src={`/img/weather/${activity.weather_start.icon}.png`}
|
||||
alt={`activity weather (${activity.weather_start.icon})`}
|
||||
title={activity.weather_start.summary}
|
||||
src={`/img/weather/${workout.weather_start.icon}.png`}
|
||||
alt={`workout weather (${workout.weather_start.icon})`}
|
||||
title={workout.weather_start.summary}
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
{t('activities:End')}
|
||||
{t('workouts:End')}
|
||||
<br />
|
||||
<img
|
||||
className="weather-img"
|
||||
src={`/img/weather/${activity.weather_end.icon}.png`}
|
||||
alt={`activity weather (${activity.weather_end.icon})`}
|
||||
title={activity.weather_end.summary}
|
||||
src={`/img/weather/${workout.weather_end.icon}.png`}
|
||||
alt={`workout weather (${workout.weather_end.icon})`}
|
||||
title={workout.weather_end.summary}
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
@ -40,8 +40,8 @@ export default function ActivityWeather(props) {
|
||||
alt="Temperatures"
|
||||
/>
|
||||
</td>
|
||||
<td>{Number(activity.weather_start.temperature).toFixed(1)}°C</td>
|
||||
<td>{Number(activity.weather_end.temperature).toFixed(1)}°C</td>
|
||||
<td>{Number(workout.weather_start.temperature).toFixed(1)}°C</td>
|
||||
<td>{Number(workout.weather_end.temperature).toFixed(1)}°C</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
@ -52,9 +52,9 @@ export default function ActivityWeather(props) {
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{Number(activity.weather_start.humidity * 100).toFixed(1)}%
|
||||
{Number(workout.weather_start.humidity * 100).toFixed(1)}%
|
||||
</td>
|
||||
<td>{Number(activity.weather_end.humidity * 100).toFixed(1)}%</td>
|
||||
<td>{Number(workout.weather_end.humidity * 100).toFixed(1)}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
@ -64,8 +64,8 @@ export default function ActivityWeather(props) {
|
||||
alt="Temperatures"
|
||||
/>
|
||||
</td>
|
||||
<td>{Number(activity.weather_start.wind).toFixed(1)}m/s</td>
|
||||
<td>{Number(activity.weather_end.wind).toFixed(1)}m/s</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>
|
@ -3,19 +3,19 @@ import { Helmet } from 'react-helmet'
|
||||
import { withTranslation } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import ActivityCardHeader from './ActivityCardHeader'
|
||||
import ActivityCharts from './ActivityCharts'
|
||||
import ActivityDetails from './ActivityDetails'
|
||||
import ActivityMap from './ActivityMap'
|
||||
import ActivityNoMap from './ActivityNoMap'
|
||||
import ActivityNotes from './ActivityNotes'
|
||||
import ActivitySegments from './ActivitySegments'
|
||||
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 { deleteActivity } from '../../../actions/activities'
|
||||
import { deleteWorkout } from '../../../actions/workouts'
|
||||
|
||||
class ActivityDisplay extends React.Component {
|
||||
class WorkoutDisplay extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
@ -28,14 +28,14 @@ class ActivityDisplay extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadActivity(this.props.match.params.activityId)
|
||||
this.props.loadWorkout(this.props.match.params.workoutId)
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.match.params.activityId !== this.props.match.params.activityId
|
||||
prevProps.match.params.workoutId !== this.props.match.params.workoutId
|
||||
) {
|
||||
this.props.loadActivity(this.props.match.params.activityId)
|
||||
this.props.loadWorkout(this.props.match.params.workoutId)
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,24 +64,15 @@ class ActivityDisplay extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
activities,
|
||||
message,
|
||||
onDeleteActivity,
|
||||
sports,
|
||||
t,
|
||||
user,
|
||||
} = this.props
|
||||
const { message, onDeleteWorkout, sports, t, user, workouts } = this.props
|
||||
const { coordinates, displayModal } = this.state
|
||||
const [activity] = activities
|
||||
const title = activity ? activity.title : t('activities:Activity')
|
||||
const [sport] = activity
|
||||
? sports.filter(s => s.id === activity.sport_id)
|
||||
: []
|
||||
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' : 'activity'
|
||||
const dataType = segmentId >= 0 ? 'segment' : 'workout'
|
||||
return (
|
||||
<div className="activity-page">
|
||||
<div className="workout-page">
|
||||
<Helmet>
|
||||
<title>FitTrackee - {title}</title>
|
||||
</Helmet>
|
||||
@ -93,23 +84,23 @@ class ActivityDisplay extends React.Component {
|
||||
<CustomModal
|
||||
title={t('common:Confirmation')}
|
||||
text={t(
|
||||
'activities:Are you sure you want to delete this activity?'
|
||||
'workouts:Are you sure you want to delete this workout?'
|
||||
)}
|
||||
confirm={() => {
|
||||
onDeleteActivity(activity.id)
|
||||
onDeleteWorkout(workout.id)
|
||||
this.displayModal(false)
|
||||
}}
|
||||
close={() => this.displayModal(false)}
|
||||
/>
|
||||
)}
|
||||
{activity && sport && activities.length === 1 && (
|
||||
{workout && sport && workouts.length === 1 && (
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<div className="card activity-card">
|
||||
<div className="card workout-card">
|
||||
<div className="card-header">
|
||||
<ActivityCardHeader
|
||||
activity={activity}
|
||||
<WorkoutCardHeader
|
||||
workout={workout}
|
||||
dataType={dataType}
|
||||
segmentId={segmentId}
|
||||
sport={sport}
|
||||
@ -122,23 +113,23 @@ class ActivityDisplay extends React.Component {
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
<div className="col-md-8">
|
||||
{activity.with_gpx ? (
|
||||
<ActivityMap
|
||||
activity={activity}
|
||||
{workout.with_gpx ? (
|
||||
<WorkoutMap
|
||||
workout={workout}
|
||||
coordinates={coordinates}
|
||||
dataType={dataType}
|
||||
segmentId={segmentId}
|
||||
/>
|
||||
) : (
|
||||
<ActivityNoMap t={t} />
|
||||
<WorkoutNoMap t={t} />
|
||||
)}
|
||||
</div>
|
||||
<div className="col">
|
||||
<ActivityDetails
|
||||
activity={
|
||||
dataType === 'activity'
|
||||
? activity
|
||||
: activity.segments[segmentId - 1]
|
||||
<WorkoutDetails
|
||||
workout={
|
||||
dataType === 'workout'
|
||||
? workout
|
||||
: workout.segments[segmentId - 1]
|
||||
}
|
||||
t={t}
|
||||
/>
|
||||
@ -148,18 +139,18 @@ class ActivityDisplay extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{activity.with_gpx && (
|
||||
{workout.with_gpx && (
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<div className="card activity-card">
|
||||
<div className="card workout-card">
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<div className="chart-title">
|
||||
{t('activities:Chart')}
|
||||
{t('workouts:Chart')}
|
||||
</div>
|
||||
<ActivityCharts
|
||||
activity={activity}
|
||||
<WorkoutCharts
|
||||
workout={workout}
|
||||
dataType={dataType}
|
||||
segmentId={segmentId}
|
||||
t={t}
|
||||
@ -174,11 +165,11 @@ class ActivityDisplay extends React.Component {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{dataType === 'activity' && (
|
||||
{dataType === 'workout' && (
|
||||
<>
|
||||
<ActivityNotes notes={activity.notes} t={t} />
|
||||
{activity.segments.length > 1 && (
|
||||
<ActivitySegments segments={activity.segments} t={t} />
|
||||
<WorkoutNotes notes={workout.notes} t={t} />
|
||||
{workout.segments.length > 1 && (
|
||||
<WorkoutSegments segments={workout.segments} t={t} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
@ -194,18 +185,18 @@ class ActivityDisplay extends React.Component {
|
||||
export default withTranslation()(
|
||||
connect(
|
||||
state => ({
|
||||
activities: state.activities.data,
|
||||
workouts: state.workouts.data,
|
||||
message: state.message,
|
||||
sports: state.sports.data,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadActivity: activityId => {
|
||||
dispatch(getOrUpdateData('getData', 'activities', { id: activityId }))
|
||||
loadWorkout: workoutId => {
|
||||
dispatch(getOrUpdateData('getData', 'workouts', { id: workoutId }))
|
||||
},
|
||||
onDeleteActivity: activityId => {
|
||||
dispatch(deleteActivity(activityId))
|
||||
onDeleteWorkout: workoutId => {
|
||||
dispatch(deleteWorkout(workoutId))
|
||||
},
|
||||
})
|
||||
)(ActivityDisplay)
|
||||
)(WorkoutDisplay)
|
||||
)
|
41
fittrackee_client/src/components/Workout/WorkoutEdit.jsx
Normal file
41
fittrackee_client/src/components/Workout/WorkoutEdit.jsx
Normal file
@ -0,0 +1,41 @@
|
||||
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)
|
@ -3,26 +3,26 @@ import { Trans } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { setLoading } from '../../../actions/index'
|
||||
import { addActivity, editActivity } from '../../../actions/activities'
|
||||
import { addWorkout, editWorkout } from '../../../actions/workouts'
|
||||
import { history } from '../../../index'
|
||||
import { getFileSize } from '../../../utils'
|
||||
import { translateSports } from '../../../utils/activities'
|
||||
import { translateSports } from '../../../utils/workouts'
|
||||
|
||||
function FormWithGpx(props) {
|
||||
const {
|
||||
activity,
|
||||
appConfig,
|
||||
loading,
|
||||
onAddActivity,
|
||||
onEditActivity,
|
||||
onAddWorkout,
|
||||
onEditWorkout,
|
||||
sports,
|
||||
t,
|
||||
workout,
|
||||
} = props
|
||||
const sportId = activity ? activity.sport_id : ''
|
||||
const sportId = workout ? workout.sport_id : ''
|
||||
const translatedSports = translateSports(sports, t, true)
|
||||
const zipTooltip = `${t('activities:no folder inside')}, ${
|
||||
const zipTooltip = `${t('workouts:no folder inside')}, ${
|
||||
appConfig.gpx_limit_import
|
||||
} ${t('activities:files max')}, ${t('activities:max size')}: ${getFileSize(
|
||||
} ${t('workouts:files max')}, ${t('workouts:max size')}: ${getFileSize(
|
||||
appConfig.max_zip_file_size
|
||||
)}`
|
||||
const fileSizeLimit = getFileSize(appConfig.max_single_file_size)
|
||||
@ -51,13 +51,13 @@ function FormWithGpx(props) {
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
{activity ? (
|
||||
{workout ? (
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('activities:Title')}:
|
||||
{t('workouts:Title')}:
|
||||
<input
|
||||
name="title"
|
||||
defaultValue={activity ? activity.title : ''}
|
||||
defaultValue={workout ? workout.title : ''}
|
||||
disabled={loading}
|
||||
className="form-control input-lg"
|
||||
/>
|
||||
@ -66,7 +66,7 @@ function FormWithGpx(props) {
|
||||
) : (
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans i18nKey="activities:gpxFile">
|
||||
<Trans i18nKey="workouts:gpxFile">
|
||||
<strong>gpx</strong> file
|
||||
</Trans>
|
||||
<sup>
|
||||
@ -74,10 +74,10 @@ function FormWithGpx(props) {
|
||||
className="fa fa-question-circle"
|
||||
aria-hidden="true"
|
||||
data-toggle="tooltip"
|
||||
title={`${t('activities:max size')}: ${fileSizeLimit}`}
|
||||
title={`${t('workouts:max size')}: ${fileSizeLimit}`}
|
||||
/>
|
||||
</sup>{' '}
|
||||
<Trans i18nKey="activities:zipFile">
|
||||
<Trans i18nKey="workouts:zipFile">
|
||||
or <strong> zip</strong> file containing <strong>gpx </strong>
|
||||
files
|
||||
</Trans>
|
||||
@ -104,10 +104,10 @@ function FormWithGpx(props) {
|
||||
)}
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('activities:Notes')}:
|
||||
{t('workouts:Notes')}:
|
||||
<textarea
|
||||
name="notes"
|
||||
defaultValue={activity ? activity.notes : ''}
|
||||
defaultValue={workout ? workout.notes : ''}
|
||||
disabled={loading}
|
||||
className="form-control input-lg"
|
||||
maxLength="500"
|
||||
@ -122,7 +122,7 @@ function FormWithGpx(props) {
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
onClick={event =>
|
||||
activity ? onEditActivity(event, activity) : onAddActivity(event)
|
||||
workout ? onEditWorkout(event, workout) : onAddWorkout(event)
|
||||
}
|
||||
value={t('common:Submit')}
|
||||
/>
|
||||
@ -144,7 +144,7 @@ export default connect(
|
||||
loading: state.loading,
|
||||
}),
|
||||
dispatch => ({
|
||||
onAddActivity: e => {
|
||||
onAddWorkout: e => {
|
||||
dispatch(setLoading(true))
|
||||
const form = new FormData()
|
||||
form.append('file', e.target.form.gpxFile.files[0])
|
||||
@ -154,12 +154,12 @@ export default connect(
|
||||
`{"sport_id": ${e.target.form.sport.value
|
||||
}, "notes": "${e.target.form.notes.value}"}`
|
||||
)
|
||||
dispatch(addActivity(form))
|
||||
dispatch(addWorkout(form))
|
||||
},
|
||||
onEditActivity: (e, activity) => {
|
||||
onEditWorkout: (e, workout) => {
|
||||
dispatch(
|
||||
editActivity({
|
||||
id: activity.id,
|
||||
editWorkout({
|
||||
id: workout.id,
|
||||
notes: e.target.form.notes.value,
|
||||
sport_id: +e.target.form.sport.value,
|
||||
title: e.target.form.title.value,
|
@ -1,38 +1,35 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import {
|
||||
addActivityWithoutGpx,
|
||||
editActivity,
|
||||
} from '../../../actions/activities'
|
||||
import { addWorkoutWithoutGpx, editWorkout } from '../../../actions/workouts'
|
||||
import { history } from '../../../index'
|
||||
import { getDateWithTZ } from '../../../utils'
|
||||
import { formatActivityDate, translateSports } from '../../../utils/activities'
|
||||
import { formatWorkoutDate, translateSports } from '../../../utils/workouts'
|
||||
|
||||
function FormWithoutGpx(props) {
|
||||
const { activity, onAddOrEdit, sports, t, user } = props
|
||||
const { onAddOrEdit, sports, t, user, workout } = props
|
||||
const translatedSports = translateSports(sports, t, true)
|
||||
let activityDate,
|
||||
activityTime,
|
||||
let workoutDate,
|
||||
workoutTime,
|
||||
sportId = ''
|
||||
if (activity) {
|
||||
const activityDateTime = formatActivityDate(
|
||||
getDateWithTZ(activity.activity_date, user.timezone),
|
||||
if (workout) {
|
||||
const workoutDateTime = formatWorkoutDate(
|
||||
getDateWithTZ(workout.workout_date, user.timezone),
|
||||
'yyyy-MM-dd'
|
||||
)
|
||||
activityDate = activityDateTime.activity_date
|
||||
activityTime = activityDateTime.activity_time
|
||||
sportId = activity.sport_id
|
||||
workoutDate = workoutDateTime.workout_date
|
||||
workoutTime = workoutDateTime.workout_time
|
||||
sportId = workout.sport_id
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={event => event.preventDefault()}>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('activities:Title')}:
|
||||
{t('workouts:Title')}:
|
||||
<input
|
||||
name="title"
|
||||
defaultValue={activity ? activity.title : ''}
|
||||
defaultValue={workout ? workout.title : ''}
|
||||
className="form-control input-lg"
|
||||
/>
|
||||
</label>
|
||||
@ -57,19 +54,19 @@ function FormWithoutGpx(props) {
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('activities:Activity Date')}:
|
||||
{t('workouts:Workout Date')}:
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<input
|
||||
name="activity_date"
|
||||
defaultValue={activityDate}
|
||||
name="workout_date"
|
||||
defaultValue={workoutDate}
|
||||
className="form-control col-md"
|
||||
required
|
||||
type="date"
|
||||
/>
|
||||
<input
|
||||
name="activity_time"
|
||||
defaultValue={activityTime}
|
||||
name="workout_time"
|
||||
defaultValue={workoutTime}
|
||||
className="form-control col-md"
|
||||
required
|
||||
type="time"
|
||||
@ -80,10 +77,10 @@ function FormWithoutGpx(props) {
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('activities:Duration')}:
|
||||
{t('workouts:Duration')}:
|
||||
<input
|
||||
name="duration"
|
||||
defaultValue={activity ? activity.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"
|
||||
@ -94,10 +91,10 @@ function FormWithoutGpx(props) {
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('activities:Distance')} (km):
|
||||
{t('workouts:Distance')} (km):
|
||||
<input
|
||||
name="distance"
|
||||
defaultValue={activity ? activity.distance : ''}
|
||||
defaultValue={workout ? workout.distance : ''}
|
||||
className="form-control input-lg"
|
||||
min={0}
|
||||
required
|
||||
@ -108,10 +105,10 @@ function FormWithoutGpx(props) {
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('activities:Notes')}:
|
||||
{t('workouts:Notes')}:
|
||||
<textarea
|
||||
name="notes"
|
||||
defaultValue={activity ? activity.notes : ''}
|
||||
defaultValue={workout ? workout.notes : ''}
|
||||
className="form-control input-lg"
|
||||
maxLength="500"
|
||||
/>
|
||||
@ -120,7 +117,7 @@ function FormWithoutGpx(props) {
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
onClick={event => onAddOrEdit(event, activity)}
|
||||
onClick={event => onAddOrEdit(event, workout)}
|
||||
value={t('common:Submit')}
|
||||
/>
|
||||
<input
|
||||
@ -138,27 +135,27 @@ export default connect(
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
onAddOrEdit: (e, activity) => {
|
||||
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 activityDate = `${e.target.form.activity_date.value
|
||||
} ${ e.target.form.activity_time.value}`
|
||||
const workoutDate = `${e.target.form.workout_date.value
|
||||
} ${ e.target.form.workout_time.value}`
|
||||
|
||||
const data = {
|
||||
activity_date: activityDate,
|
||||
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 (activity) {
|
||||
data.id = activity.id
|
||||
dispatch(editActivity(data))
|
||||
if (workout) {
|
||||
data.id = workout.id
|
||||
dispatch(editWorkout(data))
|
||||
} else {
|
||||
dispatch(addActivityWithoutGpx(data))
|
||||
dispatch(addWorkoutWithoutGpx(data))
|
||||
}
|
||||
},
|
||||
})
|
38
fittrackee_client/src/components/Workout/index.jsx
Normal file
38
fittrackee_client/src/components/Workout/index.jsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Redirect, Route, Switch } from 'react-router-dom'
|
||||
|
||||
import NotFound from './../Others/NotFound'
|
||||
import WorkoutAdd from './WorkoutAdd'
|
||||
import WorkoutDisplay from './WorkoutDisplay'
|
||||
import WorkoutEdit from './WorkoutEdit'
|
||||
import { isLoggedIn } from '../../utils'
|
||||
|
||||
function Workout() {
|
||||
return (
|
||||
<div>
|
||||
{isLoggedIn() ? (
|
||||
<Switch>
|
||||
<Route exact path="/workouts/add" component={WorkoutAdd} />
|
||||
<Route exact path="/workouts/:workoutId" component={WorkoutDisplay} />
|
||||
<Route
|
||||
exact
|
||||
path="/workouts/:workoutId/edit"
|
||||
component={WorkoutEdit}
|
||||
/>
|
||||
<Route
|
||||
path="/workouts/:workoutId/segment/:segmentId"
|
||||
component={WorkoutDisplay}
|
||||
/>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
) : (
|
||||
<Redirect to="/login" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
user: state.user,
|
||||
}))(Workout)
|
@ -1,18 +1,18 @@
|
||||
import React from 'react'
|
||||
|
||||
import { translateSports } from '../../utils/activities'
|
||||
import { translateSports } from '../../utils/workouts'
|
||||
|
||||
export default class ActivitiesFilter extends React.PureComponent {
|
||||
export default class WorkoutsFilter extends React.PureComponent {
|
||||
render() {
|
||||
const { loadActivities, sports, t, updateParams } = this.props
|
||||
const { loadWorkouts, sports, t, updateParams } = this.props
|
||||
const translatedSports = translateSports(sports, t)
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-body activity-filter">
|
||||
<div className="card-body workout-filter">
|
||||
<form onSubmit={event => event.preventDefault()}>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('activities:From')}:
|
||||
{t('workouts:From')}:
|
||||
<input
|
||||
className="form-control col-md"
|
||||
name="from"
|
||||
@ -21,7 +21,7 @@ export default class ActivitiesFilter extends React.PureComponent {
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
{t('activities:To')}:
|
||||
{t('workouts:To')}:
|
||||
<input
|
||||
className="form-control col-md"
|
||||
name="to"
|
||||
@ -49,7 +49,7 @@ export default class ActivitiesFilter extends React.PureComponent {
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('activities:Distance')} (km):
|
||||
{t('workouts:Distance')} (km):
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-5">
|
||||
@ -81,7 +81,7 @@ export default class ActivitiesFilter extends React.PureComponent {
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('activities:Duration')}:
|
||||
{t('workouts:Duration')}:
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-5">
|
||||
@ -113,7 +113,7 @@ export default class ActivitiesFilter extends React.PureComponent {
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('activities:Average speed')} (km/h):
|
||||
{t('workouts:Average speed')} (km/h):
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-5">
|
||||
@ -145,7 +145,7 @@ export default class ActivitiesFilter extends React.PureComponent {
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('activities:Max. speed')} (km/h):
|
||||
{t('workouts:Max. speed')} (km/h):
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-5">
|
||||
@ -177,9 +177,9 @@ export default class ActivitiesFilter extends React.PureComponent {
|
||||
</div>
|
||||
<input
|
||||
className="btn btn-primary btn-lg btn-block"
|
||||
onClick={() => loadActivities()}
|
||||
onClick={() => loadWorkouts()}
|
||||
type="submit"
|
||||
value={t('activities:Filter')}
|
||||
value={t('workouts:Filter')}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
@ -5,28 +5,28 @@ import { Link } from 'react-router-dom'
|
||||
import StaticMap from '../Common/StaticMap'
|
||||
import { getDateWithTZ } from '../../utils'
|
||||
|
||||
export default class ActivitiesList extends React.PureComponent {
|
||||
export default class WorkoutsList extends React.PureComponent {
|
||||
render() {
|
||||
const { activities, loading, sports, t, user } = this.props
|
||||
const { loading, sports, t, user, workouts } = this.props
|
||||
return (
|
||||
<div className="card activity-card">
|
||||
<div className="card workout-card">
|
||||
<div className="card-body">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" />
|
||||
<th scope="col">{t('common:Workout')}</th>
|
||||
<th scope="col">{t('activities:Date')}</th>
|
||||
<th scope="col">{t('activities:Distance')}</th>
|
||||
<th scope="col">{t('activities:Duration')}</th>
|
||||
<th scope="col">{t('activities:Ave. speed')}</th>
|
||||
<th scope="col">{t('activities:Max. speed')}</th>
|
||||
<th scope="col">{t('workouts:Date')}</th>
|
||||
<th scope="col">{t('workouts:Distance')}</th>
|
||||
<th scope="col">{t('workouts:Duration')}</th>
|
||||
<th scope="col">{t('workouts:Ave. speed')}</th>
|
||||
<th scope="col">{t('workouts:Max. speed')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!loading &&
|
||||
sports &&
|
||||
activities.map((activity, idx) => (
|
||||
workouts.map((workout, idx) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<tr key={idx}>
|
||||
<td>
|
||||
@ -34,56 +34,56 @@ export default class ActivitiesList extends React.PureComponent {
|
||||
{t('common:Sport')}
|
||||
</span>
|
||||
<img
|
||||
className="activity-sport"
|
||||
className="workout-sport"
|
||||
src={sports
|
||||
.filter(s => s.id === activity.sport_id)
|
||||
.filter(s => s.id === workout.sport_id)
|
||||
.map(s => s.img)}
|
||||
alt="activity sport logo"
|
||||
alt="workout sport logo"
|
||||
/>
|
||||
</td>
|
||||
<td className="activity-title">
|
||||
<td className="workout-title">
|
||||
<span className="heading-span-absolute">
|
||||
{t('common:Workout')}
|
||||
</span>
|
||||
<Link to={`/activities/${activity.id}`}>
|
||||
{activity.title}
|
||||
<Link to={`/workouts/${workout.id}`}>
|
||||
{workout.title}
|
||||
</Link>
|
||||
{activity.map && (
|
||||
<StaticMap activity={activity} display="list" />
|
||||
{workout.map && (
|
||||
<StaticMap workout={workout} display="list" />
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<span className="heading-span-absolute">
|
||||
{t('activities:Date')}
|
||||
{t('workouts:Date')}
|
||||
</span>
|
||||
{format(
|
||||
getDateWithTZ(activity.activity_date, user.timezone),
|
||||
getDateWithTZ(workout.workout_date, user.timezone),
|
||||
'dd/MM/yyyy HH:mm'
|
||||
)}
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="heading-span-absolute">
|
||||
{t('activities:Distance')}
|
||||
{t('workouts:Distance')}
|
||||
</span>
|
||||
{Number(activity.distance).toFixed(2)} km
|
||||
{Number(workout.distance).toFixed(2)} km
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="heading-span-absolute">
|
||||
{t('activities:Duration')}
|
||||
{t('workouts:Duration')}
|
||||
</span>
|
||||
{activity.moving}
|
||||
{workout.moving}
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="heading-span-absolute">
|
||||
{t('activities:Ave. speed')}
|
||||
{t('workouts:Ave. speed')}
|
||||
</span>
|
||||
{activity.ave_speed} km/h
|
||||
{workout.ave_speed} km/h
|
||||
</td>
|
||||
<td className="text-right">
|
||||
<span className="heading-span-absolute">
|
||||
{t('activities:Max. speed')}
|
||||
{t('workouts:Max. speed')}
|
||||
</span>
|
||||
{activity.max_speed} km/h
|
||||
{workout.max_speed} km/h
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
@ -3,14 +3,14 @@ import { Helmet } from 'react-helmet'
|
||||
import { withTranslation } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import ActivitiesFilter from './ActivitiesFilter'
|
||||
import ActivitiesList from './ActivitiesList'
|
||||
import Message from '../Common/Message'
|
||||
import NoActivities from '../Common/NoActivities'
|
||||
import NoWorkouts from '../Common/NoWorkouts'
|
||||
import WorkoutsFilter from './WorkoutsFilter'
|
||||
import WorkoutsList from './WorkoutsList'
|
||||
import { getOrUpdateData } from '../../actions'
|
||||
import { getMoreActivities } from '../../actions/activities'
|
||||
import { getMoreWorkouts } from '../../actions/workouts'
|
||||
|
||||
class Activities extends React.Component {
|
||||
class Workouts extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
@ -22,7 +22,7 @@ class Activities extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadActivities(this.state.params)
|
||||
this.props.loadWorkouts(this.state.params)
|
||||
}
|
||||
|
||||
setParams(e) {
|
||||
@ -37,19 +37,19 @@ class Activities extends React.Component {
|
||||
}
|
||||
render() {
|
||||
const {
|
||||
activities,
|
||||
loading,
|
||||
loadActivities,
|
||||
loadMoreActivities,
|
||||
loadWorkouts,
|
||||
loadMoreWorkouts,
|
||||
message,
|
||||
sports,
|
||||
t,
|
||||
user,
|
||||
workouts,
|
||||
} = this.props
|
||||
const { params } = this.state
|
||||
const paginationEnd =
|
||||
activities.length > 0
|
||||
? activities[activities.length - 1].previous_activity === null
|
||||
workouts.length > 0
|
||||
? workouts[workouts.length - 1].previous_workout === null
|
||||
: true
|
||||
return (
|
||||
<div>
|
||||
@ -62,16 +62,16 @@ class Activities extends React.Component {
|
||||
<div className="container history">
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<ActivitiesFilter
|
||||
<WorkoutsFilter
|
||||
sports={sports}
|
||||
loadActivities={() => loadActivities(params)}
|
||||
loadWorkouts={() => loadWorkouts(params)}
|
||||
t={t}
|
||||
updateParams={e => this.setParams(e)}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-9 activities-result">
|
||||
<ActivitiesList
|
||||
activities={activities}
|
||||
<div className="col-md-9 workouts-result">
|
||||
<WorkoutsList
|
||||
workouts={workouts}
|
||||
loading={loading}
|
||||
sports={sports}
|
||||
t={t}
|
||||
@ -81,15 +81,15 @@ class Activities extends React.Component {
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-default btn-md btn-block"
|
||||
value="Load more activities"
|
||||
value="Load more workouts"
|
||||
onClick={() => {
|
||||
params.page += 1
|
||||
loadMoreActivities(params)
|
||||
loadMoreWorkouts(params)
|
||||
this.setState(params)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{activities.length === 0 && <NoActivities t={t} />}
|
||||
{workouts.length === 0 && <NoWorkouts t={t} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -102,19 +102,19 @@ class Activities extends React.Component {
|
||||
export default withTranslation()(
|
||||
connect(
|
||||
state => ({
|
||||
activities: state.activities.data,
|
||||
workouts: state.workouts.data,
|
||||
loading: state.loading,
|
||||
message: state.message,
|
||||
sports: state.sports.data,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadActivities: params => {
|
||||
dispatch(getOrUpdateData('getData', 'activities', params))
|
||||
loadWorkouts: params => {
|
||||
dispatch(getOrUpdateData('getData', 'workouts', params))
|
||||
},
|
||||
loadMoreActivities: params => {
|
||||
dispatch(getMoreActivities(params))
|
||||
loadMoreWorkouts: params => {
|
||||
dispatch(getMoreWorkouts(params))
|
||||
},
|
||||
})
|
||||
)(Activities)
|
||||
)(Workouts)
|
||||
)
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"Actions": "Actions",
|
||||
"Active": "Active",
|
||||
"activities exist": "activities exist",
|
||||
"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",
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"activities count": "activities count",
|
||||
"workouts count": "workouts count",
|
||||
"Add workout": "Add workout",
|
||||
"admin rights": "admin rights",
|
||||
"ascending": "ascending",
|
||||
|
@ -2,7 +2,7 @@
|
||||
"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.",
|
||||
"activities": "activities",
|
||||
"workouts": "workouts",
|
||||
"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.",
|
||||
@ -18,13 +18,13 @@
|
||||
"No picture.": "No picture.",
|
||||
"No selected file.": "No selected file.",
|
||||
"no correct file.": "no correct file.",
|
||||
"no gpx file for this activity": "no gpx file for this activity",
|
||||
"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, activities exist." : "Sport can not be disabled, activities exist.",
|
||||
"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",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import EnActivitiesTranslations from './activities.json'
|
||||
import EnWorkoutsTranslations from './workouts.json'
|
||||
import EnAdministrationTranslations from './administration.json'
|
||||
import EnCommonTranslations from './common.json'
|
||||
import EnDashboardTranslations from './dashboard.json'
|
||||
@ -8,7 +8,7 @@ import EnStatisticsTranslations from './statistics.json'
|
||||
import EnUserTranslations from './user.json'
|
||||
|
||||
export const enResources = {
|
||||
activities: EnActivitiesTranslations,
|
||||
workouts: EnWorkoutsTranslations,
|
||||
administration: EnAdministrationTranslations,
|
||||
common: EnCommonTranslations,
|
||||
dashboard: EnDashboardTranslations,
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"activities": "activities",
|
||||
"workouts": "workouts",
|
||||
"distance": "distance",
|
||||
"duration": "duration",
|
||||
"month": "month",
|
||||
|
@ -1,23 +1,23 @@
|
||||
{
|
||||
"Activities": "Activities",
|
||||
"Activity": "Activity",
|
||||
"Activity Date": "Activity Date",
|
||||
"Workouts": "Workouts",
|
||||
"Workout": "Workout",
|
||||
"Workout Date": "Workout Date",
|
||||
"Add a workout": "Add a workout",
|
||||
"Are you sure you want to delete this activity?": "Are you sure you want to delete this activity?",
|
||||
"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 activity": "Delete activity",
|
||||
"Delete workout": "Delete workout",
|
||||
"Descent": "Descent",
|
||||
"Distance": "Distance",
|
||||
"distance": "distance",
|
||||
"Duration": "Duration",
|
||||
"duration": "duration",
|
||||
"Edit a workout": "Edit a workout",
|
||||
"Edit activity": "Edit activity",
|
||||
"Edit workout": "Edit workout",
|
||||
"elevation": "elevation",
|
||||
"End": "End",
|
||||
"Farest distance": "Farest distance",
|
||||
@ -33,17 +33,17 @@
|
||||
"max size": "max size",
|
||||
"No data to display": "No data to display",
|
||||
"No Map": "No Map",
|
||||
"No next activity": "No next activity",
|
||||
"No next workout": "No next workout",
|
||||
"No next segment": "No next segment",
|
||||
"No notes": "No notes",
|
||||
"No previous activity": "No previous activity",
|
||||
"No previous workout": "No previous workout",
|
||||
"No previous segment": "No previous segment",
|
||||
"Notes": "Notes",
|
||||
"pauses": "pauses",
|
||||
"Personal records": "Personal records",
|
||||
"See next activity": "See next activity",
|
||||
"See next workout": "See next workout",
|
||||
"See next segment": "See next segment",
|
||||
"See previous activity": "See previous activity",
|
||||
"See previous workout": "See previous workout",
|
||||
"See previous segment": "See previous segment",
|
||||
"segment": "segment",
|
||||
"Segments": "Segments",
|
@ -4,7 +4,7 @@
|
||||
"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",
|
||||
"activities exist": "des activités existent",
|
||||
"workouts exist": "des séances existent",
|
||||
"Application": "Application",
|
||||
"Application configuration": "Configuration de l'application",
|
||||
"Back": "Retour",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"activities count": "nombre d'activités",
|
||||
"Add workout": "Ajouter une activité",
|
||||
"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",
|
||||
@ -16,7 +16,7 @@
|
||||
"No": "Non",
|
||||
"no": "non",
|
||||
"No records.": "Pas de records.",
|
||||
"No workouts.": "Pas d'activités.",
|
||||
"No workouts.": "Pas d'séances.",
|
||||
"Page not found": "Page introuvable",
|
||||
"Previous": "Page précédente",
|
||||
"registration date": "date d'inscription",
|
||||
@ -30,10 +30,10 @@
|
||||
"Submit": "Valider",
|
||||
"to": "à",
|
||||
"user name": "utilisateur",
|
||||
"Workout": "Activité",
|
||||
"Workouts": "Activités",
|
||||
"workout": "activité",
|
||||
"workouts": "activités",
|
||||
"Workout": "Séance",
|
||||
"Workouts": "Séances",
|
||||
"workout": "séance",
|
||||
"workouts": "séances",
|
||||
"Yes": "Oui",
|
||||
"yes": "oui"
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"Personal records": "Mes records",
|
||||
"This month": "Ce mois",
|
||||
"Upload one !": "Ajoutez votre première activité !"
|
||||
"Upload one !": "Ajoutez votre première séance !"
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"3 to 12 characters required for username.": "3 à 12 caractères requis pour le nom.",
|
||||
"8 characters required for password.": "8 caractères minimum pour le mot de passe.",
|
||||
"An error occurred. Please contact the administrator.": "Une erreur s'est produite. Merci de contacter l'administrateur.",
|
||||
"activities": "activités",
|
||||
"workouts": "séances",
|
||||
"Error during picture deletion.": "Erreur lors de la suppression de l'image.",
|
||||
"Error during picture update.": "Erreur lors de la mise à jour de l'image.",
|
||||
"Error during picture update, file size exceeds max size.": "Erreur lors de la mise à jour de l'image, la taille du ficher dépasse la taille maximum autorisée",
|
||||
@ -18,13 +18,13 @@
|
||||
"No picture.": "Pas d'image.",
|
||||
"No selected file.": "Pas de fichier sélectionné.",
|
||||
"no correct file.": "fichier incorrect",
|
||||
"no gpx file for this activity": "pas de fichier gpx pour cette activité",
|
||||
"no gpx file for this workout": "pas de fichier gpx pour cette séance",
|
||||
"Password and password confirmation don't match.": "Les mots de passe saisis sont différents.",
|
||||
"Provide a valid auth token": "Merci de fournir un jeton valide",
|
||||
"records": "records",
|
||||
"Signature expired. Please log in again.": "Signature expirée. Merci de vous reconnecter.",
|
||||
"Sorry. That user already exists.": "Désolé. Cet utilisateur existe déjà.",
|
||||
"Sport can not be disabled, activities exist." : "Le sport ne peut être désactivé, des activitées existent",
|
||||
"Sport can not be disabled, workouts exist." : "Le sport ne peut être désactivé, des séancees existent",
|
||||
"Sport does not exist.": "Le sport n'existe pas.",
|
||||
"sports": "sports",
|
||||
"statistics": "statistics",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import FrActivitiesTranslations from './activities.json'
|
||||
import FrWorkoutsTranslations from './workouts.json'
|
||||
import FrAdministrationTranslations from './administration.json'
|
||||
import FrCommonTranslations from './common.json'
|
||||
import FrDashboardTranslations from './dashboard.json'
|
||||
@ -8,7 +8,7 @@ import FrStatisticsTranslations from './statistics.json'
|
||||
import FrUserTranslations from './user.json'
|
||||
|
||||
export const frResources = {
|
||||
activities: FrActivitiesTranslations,
|
||||
workouts: FrWorkoutsTranslations,
|
||||
administration: FrAdministrationTranslations,
|
||||
common: FrCommonTranslations,
|
||||
dashboard: FrDashboardTranslations,
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"activities": "activités",
|
||||
"workouts": "séances",
|
||||
"distance": "distance",
|
||||
"duration": "durée",
|
||||
"month": "mois",
|
||||
|
@ -1,23 +1,23 @@
|
||||
{
|
||||
"Activities": "Activités",
|
||||
"Activity": "Activité",
|
||||
"Activity Date": "Date de l'activité",
|
||||
"Add a workout": "Ajouter une activité",
|
||||
"Are you sure you want to delete this activity?": "Etes-vous sûr de vouloir supprimer cette activité ?",
|
||||
"Workouts": "Séances",
|
||||
"Workout": "Séance",
|
||||
"Workout Date": "Date de l'séance",
|
||||
"Add a workout": "Ajouter une séance",
|
||||
"Are you sure you want to delete this workout?": "Etes-vous sûr de vouloir supprimer cette séance ?",
|
||||
"Ave. speed": "Vitesse moyenne",
|
||||
"Ascent": "Dénivelé positif",
|
||||
"Average speed": "Vitesse moyenne",
|
||||
"Chart": "Analyse",
|
||||
"data from gpx, without any cleaning": "données issues du fichier gpx, sans correction",
|
||||
"Date": "Date",
|
||||
"Delete activity": "Supprimer l'activité",
|
||||
"Delete workout": "Supprimer l'séance",
|
||||
"Descent": "Dénivelé négatif",
|
||||
"Distance": "Distance",
|
||||
"distance": "distance",
|
||||
"Duration": "Durée",
|
||||
"duration": "durée",
|
||||
"Edit a workout": "Editer une activité",
|
||||
"Edit activity": "Editer une activity",
|
||||
"Edit a workout": "Editer une séance",
|
||||
"Edit workout": "Editer une workout",
|
||||
"elevation": "altitude",
|
||||
"End": "Arrivée",
|
||||
"Farest distance": "Distance la + longue",
|
||||
@ -33,17 +33,17 @@
|
||||
"max size": "taille max",
|
||||
"No data to display": "Pas de données à afficher",
|
||||
"No Map": "Pas de carte",
|
||||
"No next activity": "Pas d'activité suivante",
|
||||
"No next workout": "Pas d'séance suivante",
|
||||
"No next segment": "Pas de segment suivant",
|
||||
"No notes": "Pas de notes",
|
||||
"No previous activity": "Pas d'activité précédente",
|
||||
"No previous workout": "Pas d'séance précédente",
|
||||
"No previous segment": "Pas de segment précédent",
|
||||
"Notes": "Notes",
|
||||
"pauses": "pauses",
|
||||
"Personal records": "Records personnels",
|
||||
"See next activity": "Voir l'activité suivante",
|
||||
"See next workout": "Voir l'séance suivante",
|
||||
"See next segment": "Voir le segment suivant",
|
||||
"See previous activity": "Voir l'activité précédente",
|
||||
"See previous workout": "Voir l'séance précédente",
|
||||
"See previous segment": "Voir le segment précédent",
|
||||
"segment": "segment",
|
||||
"Segments": "Segments",
|
@ -23,22 +23,22 @@ const handleDataAndError = (state, type, action) => {
|
||||
return state
|
||||
}
|
||||
|
||||
const activities = (state = initial.activities, action) => {
|
||||
const workouts = (state = initial.workouts, action) => {
|
||||
switch (action.type) {
|
||||
case 'LOGOUT':
|
||||
return initial.activities
|
||||
case 'PUSH_ACTIVITIES':
|
||||
return initial.workouts
|
||||
case 'PUSH_WORKOUTS':
|
||||
return {
|
||||
...state,
|
||||
data: state.data.concat(action.activities),
|
||||
data: state.data.concat(action.workouts),
|
||||
}
|
||||
case 'REMOVE_ACTIVITY':
|
||||
case 'REMOVE_WORKOUT':
|
||||
return {
|
||||
...state,
|
||||
data: state.data.filter(activity => activity.id !== action.activityId),
|
||||
data: state.data.filter(workout => workout.id !== action.workoutId),
|
||||
}
|
||||
default:
|
||||
return handleDataAndError(state, 'activities', action)
|
||||
return handleDataAndError(state, 'workouts', action)
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,17 +58,17 @@ const application = (state = initial.application, action) => {
|
||||
return state
|
||||
}
|
||||
|
||||
const calendarActivities = (state = initial.calendarActivities, action) => {
|
||||
const calendarWorkouts = (state = initial.calendarWorkouts, action) => {
|
||||
switch (action.type) {
|
||||
case 'LOGOUT':
|
||||
return initial.calendarActivities
|
||||
return initial.calendarWorkouts
|
||||
case 'UPDATE_CALENDAR':
|
||||
return {
|
||||
...state,
|
||||
data: action.activities,
|
||||
data: action.workouts,
|
||||
}
|
||||
default:
|
||||
return handleDataAndError(state, 'calendarActivities', action)
|
||||
return handleDataAndError(state, 'calendarWorkouts', action)
|
||||
}
|
||||
}
|
||||
|
||||
@ -191,9 +191,9 @@ const statistics = (state = initial.statistics, action) => {
|
||||
|
||||
export default history =>
|
||||
combineReducers({
|
||||
activities,
|
||||
workouts,
|
||||
application,
|
||||
calendarActivities,
|
||||
calendarWorkouts,
|
||||
chartData,
|
||||
gpx,
|
||||
language,
|
||||
|
@ -9,7 +9,7 @@ export default {
|
||||
user: {
|
||||
isAuthenticated: false,
|
||||
},
|
||||
activities: {
|
||||
workouts: {
|
||||
...emptyData,
|
||||
},
|
||||
application: {
|
||||
@ -23,7 +23,7 @@ export default {
|
||||
registration: null,
|
||||
},
|
||||
},
|
||||
calendarActivities: {
|
||||
calendarWorkouts: {
|
||||
...emptyData,
|
||||
},
|
||||
chartData: [],
|
||||
|
@ -24,7 +24,7 @@ export const apiUrl =
|
||||
: `${process.env.REACT_APP_API_URL}/api/`
|
||||
|
||||
export const userFilters = [
|
||||
{ key: 'activities_count', label: 'activities count' },
|
||||
{ key: 'workouts_count', label: 'workouts count' },
|
||||
{ key: 'admin', label: 'admin rights' },
|
||||
{ key: 'created_at', label: 'registration date' },
|
||||
{ key: 'username', label: 'user name' },
|
||||
|
@ -66,7 +66,7 @@ const startDate = (duration, day, weekm) => {
|
||||
}
|
||||
|
||||
export const formatStats = (stats, sports, params, displayedSports, weekm) => {
|
||||
const nbActivitiesStats = []
|
||||
const nbWorkoutsStats = []
|
||||
const distanceStats = []
|
||||
const durationStats = []
|
||||
|
||||
@ -80,7 +80,7 @@ export const formatStats = (stats, sports, params, displayedSports, weekm) => {
|
||||
)
|
||||
const date = format(day, xAxisFormat.dateFormat)
|
||||
const xAxis = format(day, xAxisFormat.xAxis)
|
||||
const dataNbActivities = { date: xAxis }
|
||||
const dataNbWorkouts = { date: xAxis }
|
||||
const dataDistance = { date: xAxis }
|
||||
const dataDuration = { date: xAxis }
|
||||
|
||||
@ -91,19 +91,19 @@ export const formatStats = (stats, sports, params, displayedSports, weekm) => {
|
||||
)
|
||||
.map(sportId => {
|
||||
const sportLabel = sports.filter(s => s.id === +sportId)[0].label
|
||||
dataNbActivities[sportLabel] = stats[date][sportId].nb_activities
|
||||
dataNbWorkouts[sportLabel] = stats[date][sportId].nb_workouts
|
||||
dataDistance[sportLabel] = stats[date][sportId].total_distance
|
||||
dataDuration[sportLabel] = stats[date][sportId].total_duration
|
||||
return null
|
||||
})
|
||||
}
|
||||
nbActivitiesStats.push(dataNbActivities)
|
||||
nbWorkoutsStats.push(dataNbWorkouts)
|
||||
distanceStats.push(dataDistance)
|
||||
durationStats.push(dataDuration)
|
||||
}
|
||||
|
||||
return {
|
||||
activities: nbActivitiesStats,
|
||||
workouts: nbWorkoutsStats,
|
||||
distance: distanceStats,
|
||||
duration: durationStats,
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import togeojson from '@mapbox/togeojson'
|
||||
|
||||
import { getDateWithTZ } from './index'
|
||||
|
||||
export const activityColors = [
|
||||
export const workoutColors = [
|
||||
'#55a8a3',
|
||||
'#98C3A9',
|
||||
'#D0838A',
|
||||
@ -29,7 +29,7 @@ export const getGeoJson = gpxContent => {
|
||||
return { jsonData }
|
||||
}
|
||||
|
||||
export const formatActivityDate = (
|
||||
export const formatWorkoutDate = (
|
||||
dateTime,
|
||||
dateFormat = null,
|
||||
timeFormat = null
|
||||
@ -41,12 +41,12 @@ export const formatActivityDate = (
|
||||
timeFormat = 'HH:mm'
|
||||
}
|
||||
return {
|
||||
activity_date: dateTime ? format(dateTime, dateFormat) : null,
|
||||
activity_time: dateTime ? format(dateTime, timeFormat) : null,
|
||||
workout_date: dateTime ? format(dateTime, dateFormat) : null,
|
||||
workout_time: dateTime ? format(dateTime, timeFormat) : null,
|
||||
}
|
||||
}
|
||||
|
||||
export const formatActivityDuration = seconds => {
|
||||
export const formatWorkoutDuration = seconds => {
|
||||
let newDate = new Date(0)
|
||||
newDate = subHours(newDate.setSeconds(seconds), 1)
|
||||
return newDate.getTime()
|
||||
@ -55,7 +55,7 @@ export const formatActivityDuration = seconds => {
|
||||
export const formatChartData = chartData => {
|
||||
for (let i = 0; i < chartData.length; i++) {
|
||||
chartData[i].time = new Date(chartData[i].time).getTime()
|
||||
chartData[i].duration = formatActivityDuration(chartData[i].duration)
|
||||
chartData[i].duration = formatWorkoutDuration(chartData[i].duration)
|
||||
}
|
||||
return chartData
|
||||
}
|
||||
@ -78,9 +78,9 @@ export const formatRecord = (record, tz) => {
|
||||
r => r.record_type === record.record_type
|
||||
)
|
||||
return {
|
||||
activity_date: formatActivityDate(getDateWithTZ(record.activity_date, tz))
|
||||
.activity_date,
|
||||
activity_id: record.activity_id,
|
||||
workout_date: formatWorkoutDate(getDateWithTZ(record.workout_date, tz))
|
||||
.workout_date,
|
||||
workout_id: record.workout_id,
|
||||
id: record.id,
|
||||
record_type: recordType.label,
|
||||
value: value,
|
Loading…
Reference in New Issue
Block a user