Client - replace 'Activity' with 'Workout' - fix #58

This commit is contained in:
Sam 2021-01-10 11:39:48 +01:00
parent 3a80e01cc2
commit 5c04db6c08
75 changed files with 986 additions and 1006 deletions

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
{
"activities count": "activities count",
"workouts count": "workouts count",
"Add workout": "Add workout",
"admin rights": "admin rights",
"ascending": "ascending",

View File

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

View File

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

View File

@ -1,5 +1,5 @@
{
"activities": "activities",
"workouts": "workouts",
"distance": "distance",
"duration": "duration",
"month": "month",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
{
"activities": "activités",
"workouts": "séances",
"distance": "distance",
"duration": "durée",
"month": "mois",

View File

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

View File

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

View File

@ -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: [],

View File

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

View File

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

View File

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