Client - reformat js files w/ prettier

This commit is contained in:
Sam 2019-08-28 15:35:22 +02:00
parent c8ea44eecc
commit 2a52b9081d
57 changed files with 1148 additions and 1252 deletions

View File

@ -29,155 +29,154 @@ export const setChartData = chartData => ({
chartData, chartData,
}) })
export const addActivity = form => dispatch => FitTrackeeGenericApi export const addActivity = form => dispatch =>
.addDataWithFile('activities', form) FitTrackeeGenericApi.addDataWithFile('activities', form)
.then(ret => { .then(ret => {
if (ret.status === 'created') { if (ret.status === 'created') {
if (ret.data.activities.length === 0) { if (ret.data.activities.length === 0) {
dispatch(setError('activities: no correct file')) dispatch(setError('activities: no correct file'))
} else if (ret.data.activities.length === 1) { } else if (ret.data.activities.length === 1) {
dispatch(loadProfile()) dispatch(loadProfile())
history.push(`/activities/${ret.data.activities[0].id}`) history.push(`/activities/${ret.data.activities[0].id}`)
} else { // ret.data.activities.length > 1 } else {
// ret.data.activities.length > 1
dispatch(loadProfile()) dispatch(loadProfile())
history.push('/') history.push('/')
}
} else {
dispatch(setError(`activities: ${ret.message}`))
} }
} else { dispatch(setLoading(false))
dispatch(setError(`activities: ${ret.message}`)) })
} .catch(error => dispatch(setError(`activities: ${error}`)))
dispatch(setLoading(false))
})
.catch(error => dispatch(setError(`activities: ${error}`)))
export const addActivityWithoutGpx = form => dispatch =>
export const addActivityWithoutGpx = form => dispatch => FitTrackeeGenericApi FitTrackeeGenericApi.addData('activities/no_gpx', form)
.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 => { .then(ret => {
if (ret.status === 'success') { if (ret.status === 'created') {
dispatch(setGpx(ret.data.gpx)) dispatch(loadProfile())
history.push(`/activities/${ret.data.activities[0].id}`)
} else { } else {
dispatch(setError(`activities: ${ret.message}`)) dispatch(setError(`activities: ${ret.message}`))
} }
}) })
.catch(error => dispatch(setError(`activities: ${error}`))) .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)) dispatch(setGpx(null))
} }
export const getSegmentGpx = (activityId, segmentId) => dispatch => { export const getSegmentGpx = (activityId, segmentId) => dispatch => {
if (activityId) { if (activityId) {
return FitTrackeeGenericApi return FitTrackeeGenericApi.getData(
.getData(`activities/${activityId}/gpx/segment/${segmentId}`) `activities/${activityId}/gpx/segment/${segmentId}`
.then(ret => { )
if (ret.status === 'success') { .then(ret => {
dispatch(setGpx(ret.data.gpx)) if (ret.status === 'success') {
} else { dispatch(setGpx(ret.data.gpx))
dispatch(setError(`activities: ${ret.message}`)) } else {
} dispatch(setError(`activities: ${ret.message}`))
}) }
.catch(error => dispatch(setError(`activities: ${error}`))) })
.catch(error => dispatch(setError(`activities: ${error}`)))
} }
dispatch(setGpx(null)) dispatch(setGpx(null))
} }
export const getActivityChartData = activityId => dispatch => { export const getActivityChartData = activityId => dispatch => {
if (activityId) { if (activityId) {
return FitTrackeeGenericApi return FitTrackeeGenericApi.getData(`activities/${activityId}/chart_data`)
.getData(`activities/${activityId}/chart_data`) .then(ret => {
.then(ret => { if (ret.status === 'success') {
if (ret.status === 'success') { dispatch(setChartData(formatChartData(ret.data.chart_data)))
dispatch(setChartData(formatChartData(ret.data.chart_data))) } else {
} else { dispatch(setError(`activities: ${ret.message}`))
dispatch(setError(`activities: ${ret.message}`)) }
} })
}) .catch(error => dispatch(setError(`activities: ${error}`)))
.catch(error => dispatch(setError(`activities: ${error}`)))
} }
dispatch(setChartData(null)) dispatch(setChartData(null))
} }
export const getSegmentChartData = (activityId, segmentId) => dispatch => { export const getSegmentChartData = (activityId, segmentId) => dispatch => {
if (activityId) { if (activityId) {
return FitTrackeeGenericApi return FitTrackeeGenericApi.getData(
.getData(`activities/${activityId}/chart_data/segment/${segmentId}`) `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 => { .then(ret => {
if (ret.status === 'success') { if (ret.status === 'success') {
dispatch(setChartData(formatChartData(ret.data.chart_data))) dispatch(loadProfile())
history.push(`/activities/${ret.data.activities[0].id}`)
} else {
dispatch(setError(`activities: ${ret.message}`))
}
dispatch(setLoading(false))
})
.catch(error => 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 { } else {
dispatch(setError(`activities: ${ret.message}`)) dispatch(setError(`activities: ${ret.message}`))
} }
}) })
.catch(error => dispatch(setError(`activities: ${error}`))) .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(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 => export const getMonthActivities = (from, to) => dispatch =>
FitTrackeeGenericApi FitTrackeeGenericApi.getData('activities', {
.getData('activities', { from, to, order: 'asc', per_page: 100 }) from,
.then(ret => { to,
if (ret.status === 'success') { order: 'asc',
dispatch(updateCalendar(ret.data.activities)) per_page: 100,
} else {
dispatch(setError(`activities: ${ret.message}`))
}
}) })
.catch(error => dispatch(setError(`activities: ${error}`))) .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

@ -1,7 +1,6 @@
import FitTrackeeApi from '../fitTrackeeApi/index' import FitTrackeeApi from '../fitTrackeeApi/index'
import { history } from '../index' import { history } from '../index'
export const setData = (target, data) => ({ export const setData = (target, data) => ({
type: 'SET_DATA', type: 'SET_DATA',
data, data,
@ -15,7 +14,7 @@ export const setError = message => ({
export const setLoading = loading => ({ export const setLoading = loading => ({
type: 'SET_LOADING', type: 'SET_LOADING',
loading loading,
}) })
export const getOrUpdateData = (action, target, data) => dispatch => { export const getOrUpdateData = (action, target, data) => dispatch => {
@ -23,39 +22,38 @@ export const getOrUpdateData = (action, target, data) => dispatch => {
return dispatch(setError(target, `${target}: Incorrect id`)) return dispatch(setError(target, `${target}: Incorrect id`))
} }
return FitTrackeeApi[action](target, data) return FitTrackeeApi[action](target, data)
.then(ret => { .then(ret => {
if (ret.status === 'success') { if (ret.status === 'success') {
dispatch(setData(target, ret.data)) dispatch(setData(target, ret.data))
} else { } else {
dispatch(setError(`${target}: ${ret.message || ret.status}`)) dispatch(setError(`${target}: ${ret.message || ret.status}`))
} }
}) })
.catch(error => dispatch(setError(`${target}: ${error}`))) .catch(error => dispatch(setError(`${target}: ${error}`)))
} }
export const addData = (target, data) => dispatch => FitTrackeeApi export const addData = (target, data) => dispatch =>
.addData(target, data) FitTrackeeApi.addData(target, data)
.then(ret => { .then(ret => {
if (ret.status === 'created') { if (ret.status === 'created') {
history.push(`/admin/${target}`) history.push(`/admin/${target}`)
} else { } else {
dispatch(setError(`${target}: ${ret.status}`)) dispatch(setError(`${target}: ${ret.status}`))
} }
}) })
.catch(error => dispatch(setError(`${target}: ${error}`))) .catch(error => dispatch(setError(`${target}: ${error}`)))
export const deleteData = (target, id) => dispatch => { export const deleteData = (target, id) => dispatch => {
if (isNaN(id)) { if (isNaN(id)) {
return dispatch(setError(target, `${target}: Incorrect id`)) return dispatch(setError(target, `${target}: Incorrect id`))
} }
return FitTrackeeApi return FitTrackeeApi.deleteData(target, id)
.deleteData(target, id) .then(ret => {
.then(ret => { if (ret.status === 204) {
if (ret.status === 204) { history.push(`/admin/${target}`)
history.push(`/admin/${target}`) } else {
} else { dispatch(setError(`${target}: ${ret.message || ret.status}`))
dispatch(setError(`${target}: ${ret.message || ret.status}`)) }
} })
}) .catch(error => dispatch(setError(`${target}: ${error}`)))
.catch(error => dispatch(setError(`${target}: ${error}`)))
} }

View File

@ -1,11 +1,11 @@
import FitTrackeeGenericApi from '../fitTrackeeApi' import FitTrackeeGenericApi from '../fitTrackeeApi'
import { setData, setError } from './index' import { setData, setError } from './index'
export const getStats = (userId, type, data) => dispatch => FitTrackeeGenericApi export const getStats = (userId, type, data) => dispatch =>
.getData(`stats/${userId}/${type}`, data) FitTrackeeGenericApi.getData(`stats/${userId}/${type}`, data)
.then(ret => { .then(ret => {
if (ret.status === 'success') { if (ret.status === 'success') {
dispatch(setData('statistics', ret.data)) dispatch(setData('statistics', ret.data))
} else { } else {
dispatch(setError(`statistics: ${ret.message}`)) dispatch(setError(`statistics: ${ret.message}`))
} }

View File

@ -4,7 +4,6 @@ import { history } from '../index'
import { generateIds } from '../utils' import { generateIds } from '../utils'
import { getOrUpdateData } from './index' import { getOrUpdateData } from './index'
const AuthError = message => ({ type: 'AUTH_ERROR', message }) const AuthError = message => ({ type: 'AUTH_ERROR', message })
const AuthErrors = messages => ({ type: 'AUTH_ERRORS', messages }) const AuthErrors = messages => ({ type: 'AUTH_ERRORS', messages })
@ -16,7 +15,8 @@ const ProfileSuccess = profil => ({ type: 'PROFILE_SUCCESS', profil })
const ProfileError = message => ({ type: 'PROFILE_ERROR', message }) const ProfileError = message => ({ type: 'PROFILE_ERROR', message })
const ProfileUpdateError = message => ({ const ProfileUpdateError = message => ({
type: 'PROFILE_UPDATE_ERROR', message type: 'PROFILE_UPDATE_ERROR',
message,
}) })
export const logout = () => ({ type: 'LOGOUT' }) export const logout = () => ({ type: 'LOGOUT' })
@ -28,32 +28,32 @@ export const loadProfile = () => dispatch => {
return { type: 'LOGOUT' } return { type: 'LOGOUT' }
} }
export const getProfile = () => dispatch => FitTrackeeGenericApi export const getProfile = () => dispatch =>
.getData('auth/profile') FitTrackeeGenericApi.getData('auth/profile')
.then(ret => { .then(ret => {
if (ret.status === 'success') { if (ret.status === 'success') {
dispatch(getOrUpdateData('getData', 'sports')) dispatch(getOrUpdateData('getData', 'sports'))
ret.data.isAuthenticated = true ret.data.isAuthenticated = true
return dispatch(ProfileSuccess(ret.data)) return dispatch(ProfileSuccess(ret.data))
} }
return dispatch(ProfileError(ret.message)) return dispatch(ProfileError(ret.message))
}) })
.catch(error => { .catch(error => {
throw error throw error
}) })
export const loginOrRegister = (target, formData) => dispatch => FitTrackeeApi export const loginOrRegister = (target, formData) => dispatch =>
.loginOrRegister(target, formData) FitTrackeeApi.loginOrRegister(target, formData)
.then(ret => { .then(ret => {
if (ret.status === 'success') { if (ret.status === 'success') {
window.localStorage.setItem('authToken', ret.auth_token) window.localStorage.setItem('authToken', ret.auth_token)
return dispatch(getProfile()) return dispatch(getProfile())
} }
return dispatch(AuthError(ret.message)) return dispatch(AuthError(ret.message))
}) })
.catch(error => { .catch(error => {
throw error throw error
}) })
const RegisterFormControl = formData => { const RegisterFormControl = formData => {
const errMsg = [] const errMsg = []
@ -61,7 +61,7 @@ const RegisterFormControl = formData => {
errMsg.push('Username: 3 to 12 characters required.') errMsg.push('Username: 3 to 12 characters required.')
} }
if (formData.password !== formData.password_conf) { if (formData.password !== formData.password_conf) {
errMsg.push('Password and password confirmation don\'t match.') errMsg.push("Password and password confirmation don't match.")
} }
if (formData.password.length < 8) { if (formData.password.length < 8) {
errMsg.push('Password: 8 characters required.') errMsg.push('Password: 8 characters required.')
@ -81,13 +81,12 @@ export const handleUserFormSubmit = (formData, formType) => dispatch => {
export const handleProfileFormSubmit = formData => dispatch => { export const handleProfileFormSubmit = formData => dispatch => {
if (!formData.password === formData.password_conf) { if (!formData.password === formData.password_conf) {
return dispatch(ProfileUpdateError( return dispatch(
'Password and password confirmation don\'t match.' ProfileUpdateError("Password and password confirmation don't match.")
)) )
} }
delete formData.id delete formData.id
return FitTrackeeGenericApi return FitTrackeeGenericApi.postData('auth/profile/edit', formData)
.postData('auth/profile/edit', formData)
.then(ret => { .then(ret => {
if (ret.status === 'success') { if (ret.status === 'success') {
dispatch(getProfile()) dispatch(getProfile())
@ -105,8 +104,7 @@ export const uploadPicture = event => dispatch => {
const form = new FormData() const form = new FormData()
form.append('file', event.target.picture.files[0]) form.append('file', event.target.picture.files[0])
event.target.reset() event.target.reset()
return FitTrackeeGenericApi return FitTrackeeGenericApi.addDataWithFile('auth/picture', form)
.addDataWithFile('auth/picture', form)
.then(ret => { .then(ret => {
if (ret.status === 'success') { if (ret.status === 'success') {
return dispatch(getProfile()) return dispatch(getProfile())
@ -118,14 +116,14 @@ export const uploadPicture = event => dispatch => {
}) })
} }
export const deletePicture = () => dispatch => FitTrackeeApi export const deletePicture = () => dispatch =>
.deletePicture() FitTrackeeApi.deletePicture()
.then(ret => { .then(ret => {
if (ret.status === 204) { if (ret.status === 204) {
return dispatch(getProfile()) return dispatch(getProfile())
} }
return dispatch(PictureError(ret.message)) return dispatch(PictureError(ret.message))
}) })
.catch(error => { .catch(error => {
throw error throw error
}) })

View File

@ -12,48 +12,49 @@ export default class ActivitiesList extends React.PureComponent {
<div className="card-body"> <div className="card-body">
<table className="table"> <table className="table">
<thead> <thead>
<tr> <tr>
<th scope="col" /> <th scope="col" />
<th scope="col">Workout</th> <th scope="col">Workout</th>
<th scope="col">Date</th> <th scope="col">Date</th>
<th scope="col">Distance</th> <th scope="col">Distance</th>
<th scope="col">Duration</th> <th scope="col">Duration</th>
<th scope="col">Ave. speed</th> <th scope="col">Ave. speed</th>
<th scope="col">Max. speed</th> <th scope="col">Max. speed</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{sports && activities.map((activity, idx) => ( {sports &&
// eslint-disable-next-line react/no-array-index-key activities.map((activity, idx) => (
<tr key={idx}> // eslint-disable-next-line react/no-array-index-key
<td> <tr key={idx}>
<img <td>
className="activity-sport" <img
src={sports className="activity-sport"
.filter(s => s.id === activity.sport_id) src={sports
.map(s => s.img)} .filter(s => s.id === activity.sport_id)
alt="activity sport logo" .map(s => s.img)}
/> alt="activity sport logo"
</td> />
<td> </td>
<Link to={`/activities/${activity.id}`}> <td>
{activity.title} <Link to={`/activities/${activity.id}`}>
</Link> {activity.title}
</td> </Link>
<td> </td>
{format( <td>
getDateWithTZ(activity.activity_date, user.timezone), {format(
'dd/MM/yyyy HH:mm' getDateWithTZ(activity.activity_date, user.timezone),
)} 'dd/MM/yyyy HH:mm'
</td> )}
<td className="text-right"> </td>
{Number(activity.distance).toFixed(2)} km <td className="text-right">
</td> {Number(activity.distance).toFixed(2)} km
<td className="text-right">{activity.moving}</td> </td>
<td className="text-right">{activity.ave_speed} km/h</td> <td className="text-right">{activity.moving}</td>
<td className="text-right">{activity.max_speed} km/h</td> <td className="text-right">{activity.ave_speed} km/h</td>
</tr> <td className="text-right">{activity.max_speed} km/h</td>
))} </tr>
))}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -8,7 +8,6 @@ import ActivitiesList from './ActivitiesList'
import { getOrUpdateData } from '../../actions' import { getOrUpdateData } from '../../actions'
import { getMoreActivities } from '../../actions/activities' import { getMoreActivities } from '../../actions/activities'
class Activities extends React.Component { class Activities extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context) super(props, context)
@ -36,12 +35,18 @@ class Activities extends React.Component {
} }
render() { render() {
const { const {
activities, loadActivities, loadMoreActivities, message, sports, user activities,
loadActivities,
loadMoreActivities,
message,
sports,
user,
} = this.props } = this.props
const { params } = this.state const { params } = this.state
const paginationEnd = activities.length > 0 const paginationEnd =
? activities[activities.length - 1].previous_activity === null activities.length > 0
: true ? activities[activities.length - 1].previous_activity === null
: true
return ( return (
<div> <div>
<Helmet> <Helmet>
@ -65,7 +70,7 @@ class Activities extends React.Component {
sports={sports} sports={sports}
user={user} user={user}
/> />
{!paginationEnd && {!paginationEnd && (
<input <input
type="submit" type="submit"
className="btn btn-default btn-md btn-block" className="btn btn-default btn-md btn-block"
@ -76,11 +81,11 @@ class Activities extends React.Component {
this.setState(params) this.setState(params)
}} }}
/> />
} )}
{activities.length === 0 && ( {activities.length === 0 && (
<div className="card text-center"> <div className="card text-center">
<div className="card-body"> <div className="card-body">
No workouts. {' '} No workouts.{' '}
<Link to={{ pathname: '/activities/add' }}> <Link to={{ pathname: '/activities/add' }}>
Upload one ! Upload one !
</Link> </Link>

View File

@ -3,24 +3,17 @@ import { connect } from 'react-redux'
import ActivityAddOrEdit from './ActivityAddOrEdit' import ActivityAddOrEdit from './ActivityAddOrEdit'
function ActivityAdd(props) {
function ActivityAdd (props) {
const { message, sports } = props const { message, sports } = props
return ( return (
<div> <div>
<ActivityAddOrEdit <ActivityAddOrEdit activity={null} message={message} sports={sports} />
activity={null}
message={message}
sports={sports}
/>
</div> </div>
) )
} }
export default connect( export default connect(state => ({
state => ({ message: state.message,
message: state.message, sports: state.sports.data,
sports: state.sports.data, user: state.user,
user: state.user, }))(ActivityAdd)
}),
)(ActivityAdd)

View File

@ -13,11 +13,12 @@ class ActivityAddEdit extends React.Component {
} }
} }
handleRadioChange (changeEvent) { handleRadioChange(changeEvent) {
this.setState({ this.setState({
withGpx: withGpx:
changeEvent.target.name === 'withGpx' changeEvent.target.name === 'withGpx'
? changeEvent.target.value : !changeEvent.target.value ? changeEvent.target.value
: !changeEvent.target.value,
}) })
} }
@ -27,15 +28,13 @@ class ActivityAddEdit extends React.Component {
return ( return (
<div> <div>
<Helmet> <Helmet>
<title>FitTrackee - {activity <title>
? 'Edit a workout' FitTrackee - {activity ? 'Edit a workout' : 'Add a workout'}
: 'Add a workout'} </title>
</title>
</Helmet> </Helmet>
<br /><br /> <br />
{message && ( <br />
<code>{message}</code> {message && <code>{message}</code>}
)}
<div className="container"> <div className="container">
<div className="row"> <div className="row">
<div className="col-md-2" /> <div className="col-md-2" />
@ -57,27 +56,31 @@ class ActivityAddEdit extends React.Component {
<div className="form-group row"> <div className="form-group row">
<div className="col"> <div className="col">
<label className="radioLabel"> <label className="radioLabel">
<input <input
className="add-activity-radio" className="add-activity-radio"
type="radio" type="radio"
name="withGpx" name="withGpx"
disabled={loading} disabled={loading}
checked={withGpx} checked={withGpx}
onChange={event => this.handleRadioChange(event)} onChange={event =>
/> this.handleRadioChange(event)
}
/>
with gpx file with gpx file
</label> </label>
</div> </div>
<div className="col"> <div className="col">
<label className="radioLabel"> <label className="radioLabel">
<input <input
className="add-activity-radio" className="add-activity-radio"
type="radio" type="radio"
name="withoutGpx" name="withoutGpx"
disabled={loading} disabled={loading}
checked={!withGpx} checked={!withGpx}
onChange={event => this.handleRadioChange(event)} onChange={event =>
/> this.handleRadioChange(event)
}
/>
without gpx file without gpx file
</label> </label>
</div> </div>
@ -101,8 +104,6 @@ class ActivityAddEdit extends React.Component {
} }
} }
export default connect( export default connect(state => ({
state => ({ loading: state.loading,
loading: state.loading }))(ActivityAddEdit)
}),
)(ActivityAddEdit)

View File

@ -4,26 +4,30 @@ import { Link } from 'react-router-dom'
import { getDateWithTZ } from '../../../utils' import { getDateWithTZ } from '../../../utils'
import { formatActivityDate } from '../../../utils/activities' import { formatActivityDate } from '../../../utils/activities'
export default function ActivityCardHeader(props) { export default function ActivityCardHeader(props) {
const { const {
activity, dataType, displayModal, segmentId, sport, title, user activity,
dataType,
displayModal,
segmentId,
sport,
title,
user,
} = props } = props
const activityDate = activity const activityDate = activity
? formatActivityDate( ? formatActivityDate(getDateWithTZ(activity.activity_date, user.timezone))
getDateWithTZ(activity.activity_date, user.timezone)
)
: null : null
const previousUrl = dataType === 'segment' && segmentId !== 1 const previousUrl =
? `/activities/${activity.id}/segment/${segmentId - 1}` dataType === 'segment' && segmentId !== 1
: dataType === 'activity' && activity.previous_activity ? `/activities/${activity.id}/segment/${segmentId - 1}`
: dataType === 'activity' && activity.previous_activity
? `/activities/${activity.previous_activity}` ? `/activities/${activity.previous_activity}`
: null : null
const nextUrl = const nextUrl =
dataType === 'segment' && segmentId < activity.segments.length dataType === 'segment' && segmentId < activity.segments.length
? `/activities/${activity.id}/segment/${segmentId + 1}` ? `/activities/${activity.id}/segment/${segmentId + 1}`
: dataType === 'activity' && activity.next_activity : dataType === 'activity' && activity.next_activity
? `/activities/${activity.next_activity}` ? `/activities/${activity.next_activity}`
: null : null
@ -32,10 +36,7 @@ export default function ActivityCardHeader(props) {
<div className="row"> <div className="row">
<div className="col-auto"> <div className="col-auto">
{previousUrl ? ( {previousUrl ? (
<Link <Link className="unlink" to={previousUrl}>
className="unlink"
to={previousUrl}
>
<i <i
className="fa fa-chevron-left" className="fa fa-chevron-left"
aria-hidden="true" aria-hidden="true"
@ -51,35 +52,29 @@ export default function ActivityCardHeader(props) {
)} )}
</div> </div>
<div className="col-auto col-activity-logo"> <div className="col-auto col-activity-logo">
<img <img className="sport-img-medium" src={sport.img} alt="sport logo" />
className="sport-img-medium"
src={sport.img}
alt="sport logo"
/>
</div> </div>
<div className="col"> <div className="col">
{dataType === 'activity' ? ( {dataType === 'activity' ? (
<> <>
{title}{' '} {title}{' '}
<Link <Link className="unlink" to={`/activities/${activity.id}/edit`}>
className="unlink" <i
to={`/activities/${activity.id}/edit`} className="fa fa-edit custom-fa"
> aria-hidden="true"
title="Edit activity"
/>
</Link>
<i <i
className="fa fa-edit custom-fa" className="fa fa-trash custom-fa"
aria-hidden="true" aria-hidden="true"
title="Edit activity" onClick={() => displayModal(true)}
title="Delete activity"
/> />
</Link> </>
<i
className="fa fa-trash custom-fa"
aria-hidden="true"
onClick={() => displayModal(true)}
title="Delete activity"
/>
</>
) : ( ) : (
<> <>
{/* prettier-ignore */}
<Link <Link
to={`/activities/${activity.id}`} to={`/activities/${activity.id}`}
> >
@ -91,16 +86,13 @@ export default function ActivityCardHeader(props) {
<br /> <br />
{activityDate && ( {activityDate && (
<span className="activity-date"> <span className="activity-date">
{`${activityDate.activity_date} - ${activityDate.activity_time}`} {`${activityDate.activity_date} - ${activityDate.activity_time}`}
</span> </span>
)} )}
</div> </div>
<div className="col-auto"> <div className="col-auto">
{nextUrl ? ( {nextUrl ? (
<Link <Link className="unlink" to={nextUrl}>
className="unlink"
to={nextUrl}
>
<i <i
className="fa fa-chevron-right" className="fa fa-chevron-right"
aria-hidden="true" aria-hidden="true"

View File

@ -2,20 +2,26 @@ import { format } from 'date-fns'
import React from 'react' import React from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { import {
Area, ComposedChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis Area,
ComposedChart,
Line,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts' } from 'recharts'
import { import {
getActivityChartData, getSegmentChartData getActivityChartData,
getSegmentChartData,
} from '../../../actions/activities' } from '../../../actions/activities'
class ActivityCharts extends React.Component { class ActivityCharts extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context) super(props, context)
this.state = { this.state = {
displayDistance: true, displayDistance: true,
dataToHide: [] dataToHide: [],
} }
} }
@ -28,13 +34,15 @@ class ActivityCharts extends React.Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.dataType === 'activity' && ( if (
prevProps.activity.id !== this.props.activity.id) this.props.dataType === 'activity' &&
prevProps.activity.id !== this.props.activity.id
) { ) {
this.props.loadActivityData(this.props.activity.id) this.props.loadActivityData(this.props.activity.id)
} }
if (this.props.dataType === 'segment' && ( if (
prevProps.segmentId !== this.props.segmentId) this.props.dataType === 'segment' &&
prevProps.segmentId !== this.props.segmentId
) { ) {
this.props.loadSegmentData(this.props.activity.id, this.props.segmentId) this.props.loadSegmentData(this.props.activity.id, this.props.segmentId)
} }
@ -44,16 +52,16 @@ class ActivityCharts extends React.Component {
this.props.loadActivityData(null) this.props.loadActivityData(null)
} }
handleRadioChange (changeEvent) { handleRadioChange(changeEvent) {
this.setState({ this.setState({
displayDistance: displayDistance:
changeEvent.target.name === 'distance' changeEvent.target.name === 'distance'
? changeEvent.target.value ? changeEvent.target.value
: !changeEvent.target.value : !changeEvent.target.value,
}) })
} }
handleLegendChange (e) { handleLegendChange(e) {
const { dataToHide } = this.state const { dataToHide } = this.state
const name = e.target.name // eslint-disable-line prefer-destructuring const name = e.target.name // eslint-disable-line prefer-destructuring
if (dataToHide.find(d => d === name)) { if (dataToHide.find(d => d === name)) {
@ -64,7 +72,7 @@ class ActivityCharts extends React.Component {
this.setState({ dataToHide }) this.setState({ dataToHide })
} }
displayData (name) { displayData(name) {
const { dataToHide } = this.state const { dataToHide } = this.state
return !dataToHide.find(d => d === name) return !dataToHide.find(d => d === name)
} }
@ -141,22 +149,27 @@ class ActivityCharts extends React.Component {
label={{ value: xDataKey, offset: 0, position: 'bottom' }} label={{ value: xDataKey, offset: 0, position: 'bottom' }}
scale={xScale} scale={xScale}
interval={xInterval} interval={xInterval}
tickFormatter={value => displayDistance tickFormatter={value =>
? value displayDistance ? value : format(value, 'HH:mm:ss')
: format(value, 'HH:mm:ss')} }
type="number" type="number"
/> />
<YAxis <YAxis
label={{ label={{
value: 'speed (km/h)', angle: -90, position: 'left' value: 'speed (km/h)',
angle: -90,
position: 'left',
}} }}
yAxisId="left" yAxisId="left"
/> />
<YAxis <YAxis
label={{ label={{
value: 'altitude (m)', angle: -90, position: 'right' value: 'altitude (m)',
angle: -90,
position: 'right',
}} }}
yAxisId="right" orientation="right" yAxisId="right"
orientation="right"
/> />
{this.displayData('elevation') && ( {this.displayData('elevation') && (
<Area <Area
@ -181,9 +194,11 @@ class ActivityCharts extends React.Component {
/> />
)} )}
<Tooltip <Tooltip
labelFormatter={value => displayDistance labelFormatter={value =>
? `distance: ${value} km` displayDistance
: `duration: ${format(value, 'HH:mm:ss')}`} ? `distance: ${value} km`
: `duration: ${format(value, 'HH:mm:ss')}`
}
/> />
</ComposedChart> </ComposedChart>
</ResponsiveContainer> </ResponsiveContainer>
@ -202,7 +217,7 @@ class ActivityCharts extends React.Component {
export default connect( export default connect(
state => ({ state => ({
chartData: state.chartData chartData: state.chartData,
}), }),
dispatch => ({ dispatch => ({
loadActivityData: activityId => { loadActivityData: activityId => {

View File

@ -8,20 +8,14 @@ export default function ActivityDetails(props) {
return ( return (
<div className="activity-details"> <div className="activity-details">
<p> <p>
<i <i className="fa fa-clock-o custom-fa" aria-hidden="true" />
className="fa fa-clock-o custom-fa"
aria-hidden="true"
/>
Duration: {activity.moving} Duration: {activity.moving}
{activity.records && activity.records.find(r => r.record_type === 'LD' {activity.records &&
) && ( activity.records.find(r => r.record_type === 'LD') && (
<sup> <sup>
<i <i className="fa fa-trophy custom-fa" aria-hidden="true" />
className="fa fa-trophy custom-fa" </sup>
aria-hidden="true" )}
/>
</sup>
)}
{withPauses && ( {withPauses && (
<span> <span>
<br /> <br />
@ -30,47 +24,32 @@ export default function ActivityDetails(props) {
)} )}
</p> </p>
<p> <p>
<i <i className="fa fa-road custom-fa" aria-hidden="true" />
className="fa fa-road custom-fa"
aria-hidden="true"
/>
Distance: {activity.distance} km Distance: {activity.distance} km
{activity.records && activity.records.find(r => r.record_type === 'FD' {activity.records &&
) && ( activity.records.find(r => r.record_type === 'FD') && (
<sup> <sup>
<i <i className="fa fa-trophy custom-fa" aria-hidden="true" />
className="fa fa-trophy custom-fa" </sup>
aria-hidden="true" )}
/>
</sup>
)}
</p> </p>
<p> <p>
<i <i className="fa fa-tachometer custom-fa" aria-hidden="true" />
className="fa fa-tachometer custom-fa"
aria-hidden="true"
/>
Average speed: {activity.ave_speed} km/h Average speed: {activity.ave_speed} km/h
{activity.records && activity.records.find(r => r.record_type === 'AS' {activity.records &&
) && ( activity.records.find(r => r.record_type === 'AS') && (
<sup> <sup>
<i <i className="fa fa-trophy custom-fa" aria-hidden="true" />
className="fa fa-trophy custom-fa" </sup>
aria-hidden="true" )}
/>
</sup>
)}
<br /> <br />
Max speed : {activity.max_speed} km/h Max speed : {activity.max_speed} km/h
{activity.records && activity.records.find(r => r.record_type === 'MS' {activity.records &&
) && ( activity.records.find(r => r.record_type === 'MS') && (
<sup> <sup>
<i <i className="fa fa-trophy custom-fa" aria-hidden="true" />
className="fa fa-trophy custom-fa" </sup>
aria-hidden="true" )}
/>
</sup>
)}
</p> </p>
{activity.min_alt && activity.max_alt && ( {activity.min_alt && activity.max_alt && (
<p> <p>

View File

@ -8,7 +8,6 @@ import { thunderforestApiKey } from '../../../utils'
import { getGeoJson } from '../../../utils/activities' import { getGeoJson } from '../../../utils/activities'
class ActivityMap extends React.Component { class ActivityMap extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context) super(props, context)
this.state = { this.state = {
@ -25,13 +24,15 @@ class ActivityMap extends React.Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (this.props.dataType === 'activity' && ( if (
prevProps.activity.id !== this.props.activity.id) this.props.dataType === 'activity' &&
prevProps.activity.id !== this.props.activity.id
) { ) {
this.props.loadActivityGpx(this.props.activity.id) this.props.loadActivityGpx(this.props.activity.id)
} }
if (this.props.dataType === 'segment' && ( if (
prevProps.segmentId !== this.props.segmentId) this.props.dataType === 'segment' &&
prevProps.segmentId !== this.props.segmentId
) { ) {
this.props.loadSegmentGpx(this.props.activity.id, this.props.segmentId) this.props.loadSegmentGpx(this.props.activity.id, this.props.segmentId)
} }
@ -42,13 +43,11 @@ class ActivityMap extends React.Component {
} }
render() { render() {
const { const { activity, coordinates, gpxContent } = this.props
activity, coordinates, gpxContent
} = this.props
const { jsonData } = getGeoJson(gpxContent) const { jsonData } = getGeoJson(gpxContent)
const bounds = [ const bounds = [
[activity.bounds[0], activity.bounds[1]], [activity.bounds[0], activity.bounds[1]],
[activity.bounds[2], activity.bounds[3]] [activity.bounds[2], activity.bounds[3]],
] ]
return ( return (
@ -78,14 +77,13 @@ class ActivityMap extends React.Component {
</Map> </Map>
)} )}
</div> </div>
) )
} }
} }
export default connect( export default connect(
state => ({ state => ({
gpxContent: state.gpx gpxContent: state.gpx,
}), }),
dispatch => ({ dispatch => ({
loadActivityGpx: activityId => { loadActivityGpx: activityId => {

View File

@ -1,9 +1,5 @@
import React from 'react' import React from 'react'
export default function ActivityNoMap() { export default function ActivityNoMap() {
return ( return <div className="activity-no-map text-center">No Map</div>
<div className="activity-no-map text-center">
No Map
</div>
)
} }

View File

@ -8,9 +8,7 @@ export default function ActivityNotes(props) {
<div className="card activity-card"> <div className="card activity-card">
<div className="card-body"> <div className="card-body">
Notes Notes
<div className="activity-notes"> <div className="activity-notes">{notes ? notes : 'No notes'}</div>
{notes ? notes : 'No notes'}
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -18,8 +18,8 @@ export default function ActivitySegments(props) {
key={`segment-${index}`} key={`segment-${index}`}
> >
<Link <Link
to={`/activities/${ to={`/activities/${segment.activity_id}/segment/${index +
segment.activity_id}/segment/${index + 1}`} 1}`}
> >
segment {index + 1} segment {index + 1}
</Link>{' '} </Link>{' '}

View File

@ -5,9 +5,7 @@ export default function ActivityWeather(props) {
return ( return (
<div className="container"> <div className="container">
{activity.weather_start && activity.weather_end && ( {activity.weather_start && activity.weather_end && (
<table <table className="table table-borderless weather-table text-center">
className="table table-borderless weather-table text-center"
>
<thead> <thead>
<tr> <tr>
<th /> <th />
@ -42,12 +40,8 @@ export default function ActivityWeather(props) {
alt="Temperatures" alt="Temperatures"
/> />
</td> </td>
<td> <td>{Number(activity.weather_start.temperature).toFixed(1)}°C</td>
{Number(activity.weather_start.temperature).toFixed(1)}°C <td>{Number(activity.weather_end.temperature).toFixed(1)}°C</td>
</td>
<td>
{Number(activity.weather_end.temperature).toFixed(1)}°C
</td>
</tr> </tr>
<tr> <tr>
<td> <td>
@ -60,9 +54,7 @@ export default function ActivityWeather(props) {
<td> <td>
{Number(activity.weather_start.humidity * 100).toFixed(1)}% {Number(activity.weather_start.humidity * 100).toFixed(1)}%
</td> </td>
<td> <td>{Number(activity.weather_end.humidity * 100).toFixed(1)}%</td>
{Number(activity.weather_end.humidity * 100).toFixed(1)}%
</td>
</tr> </tr>
<tr> <tr>
<td> <td>
@ -72,12 +64,8 @@ export default function ActivityWeather(props) {
alt="Temperatures" alt="Temperatures"
/> />
</td> </td>
<td> <td>{Number(activity.weather_start.wind).toFixed(1)}m/s</td>
{Number(activity.weather_start.wind).toFixed(1)}m/s <td>{Number(activity.weather_end.wind).toFixed(1)}m/s</td>
</td>
<td>
{Number(activity.weather_end.wind).toFixed(1)}m/s
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -30,8 +30,9 @@ class ActivityDisplay extends React.Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (prevProps.match.params.activityId !== if (
this.props.match.params.activityId) { prevProps.match.params.activityId !== this.props.match.params.activityId
) {
this.props.loadActivity(this.props.match.params.activityId) this.props.loadActivity(this.props.match.params.activityId)
} }
} }
@ -39,23 +40,24 @@ class ActivityDisplay extends React.Component {
displayModal(value) { displayModal(value) {
this.setState(prevState => ({ this.setState(prevState => ({
...prevState, ...prevState,
displayModal: value displayModal: value,
})) }))
} }
updateCoordinates(activePayload) { updateCoordinates(activePayload) {
const coordinates = (activePayload && activePayload.length > 0) const coordinates =
? { activePayload && activePayload.length > 0
latitude: activePayload[0].payload.latitude, ? {
longitude: activePayload[0].payload.longitude, latitude: activePayload[0].payload.latitude,
} longitude: activePayload[0].payload.longitude,
: { }
latitude: null, : {
longitude: null, latitude: null,
} longitude: null,
}
this.setState(prevState => ({ this.setState(prevState => ({
...prevState, ...prevState,
coordinates coordinates,
})) }))
} }
@ -68,9 +70,7 @@ class ActivityDisplay extends React.Component {
? sports.filter(s => s.id === activity.sport_id) ? sports.filter(s => s.id === activity.sport_id)
: [] : []
const segmentId = parseInt(this.props.match.params.segmentId) const segmentId = parseInt(this.props.match.params.segmentId)
const dataType = segmentId >= 0 const dataType = segmentId >= 0 ? 'segment' : 'activity'
? 'segment'
: 'activity'
return ( return (
<div className="activity-page"> <div className="activity-page">
<Helmet> <Helmet>
@ -80,16 +80,17 @@ class ActivityDisplay extends React.Component {
<code>{message}</code> <code>{message}</code>
) : ( ) : (
<div className="container"> <div className="container">
{displayModal && {displayModal && (
<CustomModal <CustomModal
title="Confirmation" title="Confirmation"
text="Are you sure you want to delete this activity?" text="Are you sure you want to delete this activity?"
confirm={() => { confirm={() => {
onDeleteActivity(activity.id) onDeleteActivity(activity.id)
this.displayModal(false) this.displayModal(false)
}} }}
close={() => this.displayModal(false)} close={() => this.displayModal(false)}
/>} />
)}
{activity && sport && activities.length === 1 && ( {activity && sport && activities.length === 1 && (
<div> <div>
<div className="row"> <div className="row">
@ -122,9 +123,11 @@ class ActivityDisplay extends React.Component {
</div> </div>
<div className="col"> <div className="col">
<ActivityDetails <ActivityDetails
activity={dataType === 'activity' activity={
? activity dataType === 'activity'
: activity.segments[segmentId - 1]} ? activity
: activity.segments[segmentId - 1]
}
/> />
</div> </div>
</div> </div>
@ -144,8 +147,8 @@ class ActivityDisplay extends React.Component {
activity={activity} activity={activity}
dataType={dataType} dataType={dataType}
segmentId={segmentId} segmentId={segmentId}
updateCoordinates={ updateCoordinates={e =>
e => this.updateCoordinates(e) this.updateCoordinates(e)
} }
/> />
</div> </div>

View File

@ -4,12 +4,9 @@ import { connect } from 'react-redux'
import ActivityAddOrEdit from './ActivityAddOrEdit' import ActivityAddOrEdit from './ActivityAddOrEdit'
import { getOrUpdateData } from '../../actions' import { getOrUpdateData } from '../../actions'
class ActivityEdit extends React.Component { class ActivityEdit extends React.Component {
componentDidMount() { componentDidMount() {
this.props.loadActivity( this.props.loadActivity(this.props.match.params.activityId)
this.props.match.params.activityId
)
} }
render() { render() {

View File

@ -6,11 +6,8 @@ import { addActivity, editActivity } from '../../../actions/activities'
import { history } from '../../../index' import { history } from '../../../index'
import { gpxLimit } from '../../../utils' import { gpxLimit } from '../../../utils'
function FormWithGpx(props) {
function FormWithGpx (props) { const { activity, loading, onAddActivity, onEditActivity, sports } = props
const {
activity, loading, onAddActivity, onEditActivity, sports
} = props
const sportId = activity ? activity.sport_id : '' const sportId = activity ? activity.sport_id : ''
return ( return (
<form <form
@ -52,9 +49,10 @@ function FormWithGpx (props) {
) : ( ) : (
<div className="form-group"> <div className="form-group">
<label> <label>
<strong>gpx</strong> file or <strong>zip</strong>{' '} {/* prettier-ignore */}
file containing <strong>gpx</strong> (no folder inside, { <strong>gpx</strong> file or <strong>zip</strong> file containing
gpxLimit} files max): {/* prettier-ignore */}
<strong> gpx</strong> (no folder inside, {gpxLimit} files max):
<input <input
accept=".gpx, .zip" accept=".gpx, .zip"
className="form-control form-control-file gpx-file" className="form-control form-control-file gpx-file"
@ -85,10 +83,8 @@ function FormWithGpx (props) {
<input <input
type="submit" type="submit"
className="btn btn-primary btn-lg btn-block" className="btn btn-primary btn-lg btn-block"
onClick={ onClick={event =>
event => activity activity ? onEditActivity(event, activity) : onAddActivity(event)
? onEditActivity(event, activity)
: onAddActivity(event)
} }
value="Submit" value="Submit"
/> />
@ -106,13 +102,14 @@ function FormWithGpx (props) {
export default connect( export default connect(
state => ({ state => ({
loading: state.loading loading: state.loading,
}), }),
dispatch => ({ dispatch => ({
onAddActivity: e => { onAddActivity: e => {
dispatch(setLoading(true)) dispatch(setLoading(true))
const form = new FormData() const form = new FormData()
form.append('file', e.target.form.gpxFile.files[0]) form.append('file', e.target.form.gpxFile.files[0])
/* prettier-ignore */
form.append( form.append(
'data', 'data',
`{"sport_id": ${e.target.form.sport.value `{"sport_id": ${e.target.form.sport.value
@ -121,12 +118,14 @@ export default connect(
dispatch(addActivity(form)) dispatch(addActivity(form))
}, },
onEditActivity: (e, activity) => { onEditActivity: (e, activity) => {
dispatch(editActivity({ dispatch(
id: activity.id, editActivity({
notes: e.target.form.notes.value, id: activity.id,
sport_id: +e.target.form.sport.value, notes: e.target.form.notes.value,
title: e.target.form.title.value, sport_id: +e.target.form.sport.value,
})) title: e.target.form.title.value,
})
)
}, },
}) })
)(FormWithGpx) )(FormWithGpx)

View File

@ -2,14 +2,17 @@ import React from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { import {
addActivityWithoutGpx, editActivity addActivityWithoutGpx,
editActivity,
} from '../../../actions/activities' } from '../../../actions/activities'
import { history } from '../../../index' import { history } from '../../../index'
import { formatActivityDate } from '../../../utils/activities' import { formatActivityDate } from '../../../utils/activities'
function FormWithoutGpx (props) { function FormWithoutGpx(props) {
const { activity, onAddOrEdit, sports } = props const { activity, onAddOrEdit, sports } = props
let activityDate, activityTime, sportId = '' let activityDate,
activityTime,
sportId = ''
if (activity) { if (activity) {
const activityDateTime = formatActivityDate( const activityDateTime = formatActivityDate(
activity.activity_date, activity.activity_date,
@ -21,9 +24,7 @@ function FormWithoutGpx (props) {
} }
return ( return (
<form <form onSubmit={event => event.preventDefault()}>
onSubmit={event => event.preventDefault()}
>
<div className="form-group"> <div className="form-group">
<label> <label>
Title: Title:
@ -78,15 +79,15 @@ function FormWithoutGpx (props) {
<div className="form-group"> <div className="form-group">
<label> <label>
Duration: Duration:
<input <input
name="duration" name="duration"
defaultValue={activity ? activity.duration : ''} defaultValue={activity ? activity.duration : ''}
className="form-control col-xs-4" className="form-control col-xs-4"
pattern="^([0-9]*[0-9]):([0-5][0-9]):([0-5][0-9])$" pattern="^([0-9]*[0-9]):([0-5][0-9]):([0-5][0-9])$"
placeholder="hh:mm:ss" placeholder="hh:mm:ss"
required required
type="text" type="text"
/> />
</label> </label>
</div> </div>
<div className="form-group"> <div className="form-group">
@ -131,12 +132,13 @@ function FormWithoutGpx (props) {
} }
export default connect( export default connect(
() => ({ }), () => ({}),
dispatch => ({ dispatch => ({
onAddOrEdit: (e, activity) => { onAddOrEdit: (e, activity) => {
const d = e.target.form.duration.value.split(':') const d = e.target.form.duration.value.split(':')
const duration = +d[0] * 60 * 60 + +d[1] * 60 + +d[2] const duration = +d[0] * 60 * 60 + +d[1] * 60 + +d[2]
/* prettier-ignore */
const activityDate = `${e.target.form.activity_date.value const activityDate = `${e.target.form.activity_date.value
} ${ e.target.form.activity_time.value}` } ${ e.target.form.activity_time.value}`

View File

@ -9,7 +9,7 @@ import ActivityEdit from './ActivityEdit'
import NotFound from './../Others/NotFound' import NotFound from './../Others/NotFound'
import { isLoggedIn } from '../../utils' import { isLoggedIn } from '../../utils'
function Activity () { function Activity() {
return ( return (
<div> <div>
<Helmet> <Helmet>
@ -19,11 +19,13 @@ function Activity () {
<Switch> <Switch>
<Route exact path="/activities/add" component={ActivityAdd} /> <Route exact path="/activities/add" component={ActivityAdd} />
<Route <Route
exact path="/activities/:activityId" exact
path="/activities/:activityId"
component={ActivityDisplay} component={ActivityDisplay}
/> />
<Route <Route
exact path="/activities/:activityId/edit" exact
path="/activities/:activityId/edit"
component={ActivityEdit} component={ActivityEdit}
/> />
<Route <Route
@ -32,13 +34,13 @@ function Activity () {
/> />
<Route component={NotFound} /> <Route component={NotFound} />
</Switch> </Switch>
) : (<Redirect to="/login" />)} ) : (
<Redirect to="/login" />
)}
</div> </div>
) )
} }
export default connect( export default connect(state => ({
state => ({ user: state.user,
user: state.user, }))(Activity)
})
)(Activity)

View File

@ -2,7 +2,7 @@ import React from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
export default function AdminMenu () { export default function AdminMenu() {
return ( return (
<div> <div>
<Helmet> <Helmet>

View File

@ -17,10 +17,7 @@ class AdminSports extends React.Component {
return ( return (
<div> <div>
<AdminDetail <AdminDetail results={sports} target="sports" />
results={sports}
target="sports"
/>
</div> </div>
) )
} }

View File

@ -6,17 +6,14 @@ import AdminPage from '../generic/AdminPage'
class AdminSports extends React.Component { class AdminSports extends React.Component {
componentDidMount() { componentDidMount() {
this.props.loadSports() this.props.loadSports()
} }
render() { render() {
const { sports } = this.props const { sports } = this.props
return ( return (
<div> <div>
<AdminPage <AdminPage data={sports} target="sports" />
data={sports}
target="sports"
/>
</div> </div>
) )
} }

View File

@ -6,7 +6,7 @@ import { addData } from '../../../actions/index'
import { history } from '../../../index' import { history } from '../../../index'
class AdminSportsAdd extends React.Component { class AdminSportsAdd extends React.Component {
componentDidMount() { } componentDidMount() {}
render() { render() {
const { message, onAddSport } = this.props const { message, onAddSport } = this.props
@ -16,25 +16,17 @@ class AdminSportsAdd extends React.Component {
<Helmet> <Helmet>
<title>FitTrackee - Admin - Add Sport</title> <title>FitTrackee - Admin - Add Sport</title>
</Helmet> </Helmet>
<h1 className="page-title"> <h1 className="page-title">Administration - Sport</h1>
Administration - Sport {message && <code>{message}</code>}
</h1>
{message && (
<code>{message}</code>
)}
<div className="container"> <div className="container">
<div className="row"> <div className="row">
<div className="col-md-2" /> <div className="col-md-2" />
<div className="col-md-8"> <div className="col-md-8">
<div className="card"> <div className="card">
<div className="card-header"> <div className="card-header">Add a sport</div>
Add a sport
</div>
<div className="card-body"> <div className="card-body">
<form onSubmit={event => <form onSubmit={event => event.preventDefault()}>
event.preventDefault()}
>
<div className="form-group"> <div className="form-group">
<label> <label>
Label: Label:

View File

@ -6,7 +6,6 @@ import { deleteData, getOrUpdateData } from '../../../actions/index'
import { history } from '../../../index' import { history } from '../../../index'
class AdminDetail extends React.Component { class AdminDetail extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context) super(props, context)
this.state = { this.state = {
@ -15,13 +14,7 @@ class AdminDetail extends React.Component {
} }
render() { render() {
const { const { message, onDataUpdate, onDataDelete, results, target } = this.props
message,
onDataUpdate,
onDataDelete,
results,
target,
} = this.props
const { isInEdition } = this.state const { isInEdition } = this.state
const title = target.charAt(0).toUpperCase() + target.slice(1) const title = target.charAt(0).toUpperCase() + target.slice(1)
@ -30,100 +23,98 @@ class AdminDetail extends React.Component {
<Helmet> <Helmet>
<title>FitTrackee - Admin</title> <title>FitTrackee - Admin</title>
</Helmet> </Helmet>
<h1 className="page-title"> <h1 className="page-title">Administration - {title}</h1>
Administration - {title}
</h1>
{message ? ( {message ? (
<code>{message}</code> <code>{message}</code>
) : ( ) : (
results.length === 1 && ( results.length === 1 && (
<div className="container"> <div className="container">
<div className="row"> <div className="row">
<div className="col-md-2" /> <div className="col-md-2" />
<div className="col-md-8 card"> <div className="col-md-8 card">
<div className="card-body"> <div className="card-body">
<form onSubmit={event => <form onSubmit={event => event.preventDefault()}>
event.preventDefault()} {Object.keys(results[0])
>
{ Object.keys(results[0])
.filter(key => key.charAt(0) !== '_') .filter(key => key.charAt(0) !== '_')
.map(key => ( .map(key => (
<div className="form-group" key={key}> <div className="form-group" key={key}>
<label> <label>
{key}: {key}:
{key === 'img' ? ( {key === 'img' ? (
<img <img
src={results[0][key] src={
? results[0][key] results[0][key]
: '/img/photo.png'} ? results[0][key]
alt="property" : '/img/photo.png'
/> }
) : ( alt="property"
<input />
className="form-control input-lg" ) : (
name={key} <input
readOnly={key === 'id' || !isInEdition} className="form-control input-lg"
defaultValue={results[0][key]} name={key}
/> readOnly={key === 'id' || !isInEdition}
)} defaultValue={results[0][key]}
</label> />
</div> )}
)) </label>
} </div>
{isInEdition ? ( ))}
<div> {isInEdition ? (
<input <div>
type="submit" <input
className="btn btn-primary btn-lg btn-block" type="submit"
onClick={event => { className="btn btn-primary btn-lg btn-block"
onClick={event => {
onDataUpdate(event, target) onDataUpdate(event, target)
this.setState({ isInEdition: false }) this.setState({ isInEdition: false })
}}
value="Submit"
/>
<input
type="submit"
className="btn btn-secondary btn-lg btn-block"
onClick={event => {
event.target.form.reset()
this.setState({ isInEdition: false })
}}
value="Cancel"
/>
</div>
) : (
<div>
<input
type="submit"
className="btn btn-primary btn-lg btn-block"
onClick={() => this.setState({ isInEdition: true })}
value="Edit"
/>
<input
type="submit"
className="btn btn-danger btn-lg btn-block"
disabled={!results[0]._can_be_deleted}
onClick={event => onDataDelete(event, target)}
title={
results[0]._can_be_deleted
? ''
: "Can't be deleted, associated data exist"
} }
} value="Delete"
value="Submit" />
/> <input
<input type="submit"
type="submit" className="btn btn-secondary btn-lg btn-block"
className="btn btn-secondary btn-lg btn-block" onClick={() => history.push(`/admin/${target}`)}
onClick={event => { value="Back to the list"
event.target.form.reset() />
this.setState({ isInEdition: false }) </div>
}} )}
value="Cancel" </form>
/> </div>
</div>
) : (
<div>
<input
type="submit"
className="btn btn-primary btn-lg btn-block"
onClick={() => this.setState({ isInEdition: true })}
value="Edit"
/>
<input
type="submit"
className="btn btn-danger btn-lg btn-block"
disabled={!results[0]._can_be_deleted}
onClick={event => onDataDelete(event, target)}
title={results[0]._can_be_deleted
? ''
: 'Can\'t be deleted, associated data exist'}
value="Delete"
/>
<input
type="submit"
className="btn btn-secondary btn-lg btn-block"
onClick={() => history.push(`/admin/${target}`)}
value="Back to the list"
/>
</div>
)}
</form>
</div> </div>
<div className="col-md-2" />
</div> </div>
<div className="col-md-2" />
</div> </div>
</div>
) )
)} )}
</div> </div>

View File

@ -5,7 +5,6 @@ import { Link } from 'react-router-dom'
import { history } from '../../../index' import { history } from '../../../index'
export default function AdminPage(props) { export default function AdminPage(props) {
const { data, target } = props const { data, target } = props
const { error } = data const { error } = data
const results = data.data const results = data.data
@ -22,9 +21,7 @@ export default function AdminPage(props) {
<Helmet> <Helmet>
<title>FitTrackee - Admin</title> <title>FitTrackee - Admin</title>
</Helmet> </Helmet>
<h1 className="page-title"> <h1 className="page-title">Administration - {title}</h1>
Administration - {title}
</h1>
{error ? ( {error ? (
<code>{error}</code> <code>{error}</code>
) : ( ) : (
@ -36,40 +33,45 @@ export default function AdminPage(props) {
<table className="table"> <table className="table">
<thead> <thead>
<tr> <tr>
{tbKeys.map( {tbKeys.map(tbKey => (
tbKey => <th key={tbKey} scope="col">{tbKey}</th> <th key={tbKey} scope="col">
)} {tbKey}
</th>
))}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{ results.map((result, idx) => ( {results.map((result, idx) => (
// eslint-disable-next-line react/no-array-index-key // eslint-disable-next-line react/no-array-index-key
<tr key={idx}> <tr key={idx}>
{ Object.keys(result) {Object.keys(result)
.filter(key => key.charAt(0) !== '_') .filter(key => key.charAt(0) !== '_')
.map(key => { .map(key => {
if (key === 'id') { if (key === 'id') {
return ( return (
<th key={key} scope="row"> <th key={key} scope="row">
<Link to={`/admin/${target}/${result[key]}`}> <Link to={`/admin/${target}/${result[key]}`}>
{result[key]} {result[key]}
</Link> </Link>
</th> </th>
) )
} else if (key === 'img') { } else if (key === 'img') {
return (<td key={key}> return (
<img <td key={key}>
className="admin-img" <img
src={result[key] className="admin-img"
? result[key] src={
: '/img/photo.png'} result[key]
alt="logo" ? result[key]
/> : '/img/photo.png'
</td>) }
} alt="logo"
return <td key={key}>{result[key]}</td> />
}) </td>
} )
}
return <td key={key}>{result[key]}</td>
})}
</tr> </tr>
))} ))}
</tbody> </tbody>
@ -88,9 +90,9 @@ export default function AdminPage(props) {
/> />
</div> </div>
</div> </div>
<div className="col-md-2" /> <div className="col-md-2" />
</div>
</div> </div>
</div>
)} )}
</div> </div>
) )

View File

@ -11,7 +11,7 @@ import AccessDenied from './../Others/AccessDenied'
import NotFound from './../Others/NotFound' import NotFound from './../Others/NotFound'
import { isLoggedIn } from '../../utils' import { isLoggedIn } from '../../utils'
function Admin (props) { function Admin(props) {
const { user } = props const { user } = props
return ( return (
<div> <div>
@ -23,26 +23,20 @@ function Admin (props) {
<Switch> <Switch>
<Route exact path="/admin" component={AdminMenu} /> <Route exact path="/admin" component={AdminMenu} />
<Route exact path="/admin/sports" component={AdminSports} /> <Route exact path="/admin/sports" component={AdminSports} />
<Route <Route exact path="/admin/sports/add" component={AdminSportsAdd} />
exact path="/admin/sports/add" <Route exact path="/admin/sports/:sportId" component={AdminSport} />
component={AdminSportsAdd}
/>
<Route
exact path="/admin/sports/:sportId"
component={AdminSport}
/>
<Route component={NotFound} /> <Route component={NotFound} />
</Switch> </Switch>
) : ( ) : (
<AccessDenied /> <AccessDenied />
) )
) : (<Redirect to="/login" />)} ) : (
<Redirect to="/login" />
)}
</div> </div>
) )
} }
export default connect( export default connect(state => ({
state => ({ user: state.user,
user: state.user, }))(Admin)
})
)(Admin)

View File

@ -17,7 +17,6 @@ import UserForm from './User/UserForm'
import { isLoggedIn } from '../utils' import { isLoggedIn } from '../utils'
export default class App extends React.Component { export default class App extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.props = props this.props = props
@ -29,78 +28,57 @@ export default class App extends React.Component {
<NavBar /> <NavBar />
<Switch> <Switch>
<Route <Route
exact path="/" exact
render={() => ( path="/"
isLoggedIn() ? ( render={() =>
<Dashboard /> isLoggedIn() ? <Dashboard /> : <Redirect to="/login" />
) : ( }
<Redirect to="/login" />
)
)}
/> />
<Route <Route
exact path="/register" exact
render={() => ( path="/register"
render={() =>
isLoggedIn() ? ( isLoggedIn() ? (
<Redirect to="/" /> <Redirect to="/" />
) : ( ) : (
<UserForm <UserForm formType={'register'} />
formType={'register'}
/>
) )
)} }
/> />
<Route <Route
exact path="/login" exact
render={() => ( path="/login"
render={() =>
isLoggedIn() ? ( isLoggedIn() ? (
<Redirect to="/" /> <Redirect to="/" />
) : ( ) : (
<UserForm <UserForm formType={'login'} />
formType={'login'}
/>
) )
)} }
/> />
<Route exact path="/logout" component={Logout} /> <Route exact path="/logout" component={Logout} />
<Route <Route
exact path="/profile/edit" exact
render={() => ( path="/profile/edit"
isLoggedIn() ? ( render={() =>
<ProfileEdit /> isLoggedIn() ? <ProfileEdit /> : <UserForm formType={'login'} />
) : ( }
<UserForm
formType={'login'}
/>
)
)}
/> />
<Route <Route
exact path="/profile" exact
render={() => ( path="/profile"
isLoggedIn() ? ( render={() =>
<Profile /> isLoggedIn() ? <Profile /> : <UserForm formType={'login'} />
) : ( }
<UserForm
formType={'login'}
/>
)
)}
/>
<Route
exact path="/activities/history"
component={Activities}
/>
<Route
exact path="/activities/statistics"
component={Statistics}
/> />
<Route exact path="/activities/history" component={Activities} />
<Route exact path="/activities/statistics" component={Statistics} />
<Route path="/activities" component={Activity} /> <Route path="/activities" component={Activity} />
{/* <Route path="/admin" component={Admin} /> */} {/* <Route path="/admin" component={Admin} /> */}
<Route component={NotFound} /> <Route component={NotFound} />
</Switch> </Switch>
<Footer /> <Footer />
</div> </div>
) )
} }
} }

View File

@ -5,7 +5,7 @@ import { formatValue } from '../../../utils/stats'
/** /**
* @return {null} * @return {null}
*/ */
export default function CustomLabel (props) { export default function CustomLabel(props) {
const { displayedData, x, y, width, value } = props const { displayedData, x, y, width, value } = props
if (!value) { if (!value) {
return null return null

View File

@ -2,34 +2,32 @@ import React from 'react'
import { formatDuration } from '../../../utils/stats' import { formatDuration } from '../../../utils/stats'
const formatValue = (displayedData, value) => displayedData === 'duration' const formatValue = (displayedData, value) =>
? formatDuration(value, true) displayedData === 'duration'
: displayedData === 'distance' ? formatDuration(value, true)
: displayedData === 'distance'
? value.toFixed(2) ? value.toFixed(2)
: value : value
/** /**
* @return {null} * @return {null}
*/ */
export default function CustomTooltip (props) { export default function CustomTooltip(props) {
const { active } = props const { active } = props
if (active) { if (active) {
const { displayedData, payload, label } = props const { displayedData, payload, label } = props
let total = 0 let total = 0
payload.map(p => total += p.value) payload.map(p => (total += p.value))
return ( return (
<div className="custom-tooltip"> <div className="custom-tooltip">
<p className="custom-tooltip-label">{label}</p> <p className="custom-tooltip-label">{label}</p>
{payload.map(p => ( {payload.map(p => (
<p key={p.name} style={{ color: p.fill }}> <p key={p.name} style={{ color: p.fill }}>
{p.name}: {formatValue(displayedData, p.value)} {p.unit} {p.name}: {formatValue(displayedData, p.value)} {p.unit}
</p>))
}
{payload.length > 0 && (
<p>
Total: {formatValue(displayedData, total)}
</p> </p>
))}
{payload.length > 0 && (
<p>Total: {formatValue(displayedData, total)}</p>
)} )}
</div> </div>
) )

View File

@ -1,6 +1,11 @@
import React from 'react' import React from 'react'
import { import {
Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis Bar,
BarChart,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts' } from 'recharts'
import { activityColors } from '../../../utils/activities' import { activityColors } from '../../../utils/activities'
@ -8,17 +13,16 @@ import { formatValue } from '../../../utils/stats'
import CustomTooltip from './CustomTooltip' import CustomTooltip from './CustomTooltip'
import CustomLabel from './CustomLabel' import CustomLabel from './CustomLabel'
export default class StatsCharts extends React.PureComponent { export default class StatsCharts extends React.PureComponent {
constructor(props, context) { constructor(props, context) {
super(props, context) super(props, context)
this.state = { this.state = {
displayedData: 'distance' displayedData: 'distance',
} }
} }
handleRadioChange (changeEvent) { handleRadioChange(changeEvent) {
this.setState({ this.setState({
displayedData: changeEvent.target.name displayedData: changeEvent.target.name,
}) })
} }
@ -60,22 +64,14 @@ export default class StatsCharts extends React.PureComponent {
</label> </label>
</div> </div>
<ResponsiveContainer height={300}> <ResponsiveContainer height={300}>
<BarChart <BarChart data={stats[displayedData]} margin={{ top: 15, bottom: 0 }}>
data={stats[displayedData]}
margin={{ top: 15, bottom: 0 }}
>
<XAxis <XAxis
dataKey="date" dataKey="date"
interval={0} // to force to display all ticks interval={0} // to force to display all ticks
/> />
<YAxis <YAxis tickFormatter={value => formatValue(displayedData, value)} />
tickFormatter={value => formatValue(displayedData, value)} <Tooltip
/> content={<CustomTooltip displayedData={displayedData} />}
<Tooltip content={
<CustomTooltip
displayedData={displayedData}
/>
}
/> />
{sports.map((s, i) => ( {sports.map((s, i) => (
<Bar <Bar
@ -86,9 +82,12 @@ export default class StatsCharts extends React.PureComponent {
dataKey={s.label} dataKey={s.label}
stackId="a" stackId="a"
fill={activityColors[i]} fill={activityColors[i]}
label={i === sports.length - 1 label={
? <CustomLabel displayedData={displayedData} /> i === sports.length - 1 ? (
: '' <CustomLabel displayedData={displayedData} />
) : (
''
)
} }
/> />
))} ))}

View File

@ -6,14 +6,14 @@ import { getStats } from '../../../actions/stats'
import { formatStats } from '../../../utils/stats' import { formatStats } from '../../../utils/stats'
import StatsChart from './StatsChart' import StatsChart from './StatsChart'
class Statistics extends React.PureComponent { class Statistics extends React.PureComponent {
componentDidMount() { componentDidMount() {
this.updateData() this.updateData()
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if ((this.props.user.id && (this.props.user.id !== prevProps.user.id)) || if (
(this.props.user.id && this.props.user.id !== prevProps.user.id) ||
this.props.statsParams !== prevProps.statsParams this.props.statsParams !== prevProps.statsParams
) { ) {
this.updateData() this.updateData()
@ -22,22 +22,23 @@ class Statistics extends React.PureComponent {
updateData() { updateData() {
if (this.props.user.id) { if (this.props.user.id) {
this.props.loadActivities( this.props.loadActivities(this.props.user.id, this.props.statsParams)
this.props.user.id,
this.props.statsParams,
)
} }
} }
render() { render() {
const { const {
displayedSports, sports, statistics, statsParams, displayEmpty displayedSports,
sports,
statistics,
statsParams,
displayEmpty,
} = this.props } = this.props
if (!displayEmpty && Object.keys(statistics).length === 0) { if (!displayEmpty && Object.keys(statistics).length === 0) {
return 'No workouts' return 'No workouts'
} }
const stats = formatStats(statistics, sports, statsParams, displayedSports) const stats = formatStats(statistics, sports, statsParams, displayedSports)
return (<StatsChart sports={sports} stats={stats} />) return <StatsChart sports={sports} stats={stats} />
} }
} }
@ -53,7 +54,7 @@ export default connect(
const params = { const params = {
from: format(data.start, dateFormat), from: format(data.start, dateFormat),
to: format(data.end, dateFormat), to: format(data.end, dateFormat),
time: data.duration time: data.duration,
} }
dispatch(getStats(userId, data.type, params)) dispatch(getStats(userId, data.type, params))
}, },

View File

@ -4,19 +4,21 @@ import { Link } from 'react-router-dom'
import { apiUrl, getDateWithTZ } from '../../utils' import { apiUrl, getDateWithTZ } from '../../utils'
export default function ActivityCard (props) { export default function ActivityCard(props) {
const { activity, sports, user } = props const { activity, sports, user } = props
return ( return (
<div className="card activity-card text-center"> <div className="card activity-card text-center">
<div className="card-header"> <div className="card-header">
<Link to={`/activities/${activity.id}`}> <Link to={`/activities/${activity.id}`}>
{sports.filter(sport => sport.id === activity.sport_id) {sports
.map(sport => sport.label)} -{' '} .filter(sport => sport.id === activity.sport_id)
{format( .map(sport => sport.label)}{' '}
getDateWithTZ(activity.activity_date, user.timezone), -{' '}
'dd/MM/yyyy HH:mm' {format(
)} getDateWithTZ(activity.activity_date, user.timezone),
'dd/MM/yyyy HH:mm'
)}
</Link> </Link>
</div> </div>
<div className="card-body"> <div className="card-body">
@ -25,15 +27,14 @@ export default function ActivityCard (props) {
<div className="col"> <div className="col">
<img <img
alt="Map" alt="Map"
src={`${apiUrl}activities/map/${activity.map}` + src={
`?${Date.now()}`} `${apiUrl}activities/map/${activity.map}` + `?${Date.now()}`
}
className="img-fluid" className="img-fluid"
/> />
<div className="map-attribution text-right"> <div className="map-attribution text-right">
<div> <div>
<span className="map-attribution-text"> <span className="map-attribution-text">©</span>
©
</span>
<a <a
className="map-attribution-text" className="map-attribution-text"
href="http://www.openstreetmap.org/copyright" href="http://www.openstreetmap.org/copyright"
@ -48,15 +49,18 @@ export default function ActivityCard (props) {
)} )}
<div className="col"> <div className="col">
<p> <p>
<i className="fa fa-clock-o" aria-hidden="true" />{' '} <i className="fa fa-clock-o" aria-hidden="true" /> Duration:{' '}
Duration: {activity.moving} {activity.moving}
{activity.map ? ( {activity.map ? (
<span><br /><br /></span> <span>
<br />
<br />
</span>
) : ( ) : (
' - ' ' - '
)} )}
<i className="fa fa-road" aria-hidden="true" />{' '} <i className="fa fa-road" aria-hidden="true" /> Distance:{' '}
Distance: {activity.distance} km {activity.distance} km
</p> </p>
</div> </div>
</div> </div>

View File

@ -1,8 +1,16 @@
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
// source: https://blog.flowandform.agency/create-a-custom-calendar-in-react-3df1bfd0b728 // source: https://blog.flowandform.agency/create-a-custom-calendar-in-react-3df1bfd0b728
import { import {
addDays, addMonths, endOfMonth, endOfWeek, format, isSameDay, isSameMonth, addDays,
startOfMonth, startOfWeek, subMonths addMonths,
endOfMonth,
endOfWeek,
format,
isSameDay,
isSameMonth,
startOfMonth,
startOfWeek,
subMonths,
} from 'date-fns' } from 'date-fns'
import React, { Fragment } from 'react' import React, { Fragment } from 'react'
import { connect } from 'react-redux' import { connect } from 'react-redux'
@ -21,7 +29,6 @@ const getStartAndEndMonth = date => {
} }
} }
class Calendar extends React.Component { class Calendar extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context) super(props, context)
@ -42,21 +49,13 @@ class Calendar extends React.Component {
return ( return (
<div className="header row flex-middle"> <div className="header row flex-middle">
<div className="col col-start" onClick={() => this.handlePrevMonth()}> <div className="col col-start" onClick={() => this.handlePrevMonth()}>
<i <i className="fa fa-chevron-left" aria-hidden="true" />
className="fa fa-chevron-left"
aria-hidden="true"
/>
</div> </div>
<div className="col col-center"> <div className="col col-center">
<span> <span>{format(this.state.currentMonth, dateFormat)}</span>
{format(this.state.currentMonth, dateFormat)}
</span>
</div> </div>
<div className="col col-end" onClick={() => this.handleNextMonth()}> <div className="col col-end" onClick={() => this.handleNextMonth()}>
<i <i className="fa fa-chevron-right" aria-hidden="true" />
className="fa fa-chevron-right"
aria-hidden="true"
/>
</div> </div>
</div> </div>
) )
@ -80,11 +79,9 @@ class Calendar extends React.Component {
filterActivities(day) { filterActivities(day) {
const { activities, user } = this.props const { activities, user } = this.props
if (activities) { if (activities) {
return activities return activities.filter(act =>
.filter(act => isSameDay( isSameDay(getDateWithTZ(act.activity_date, user.timezone), day)
getDateWithTZ(act.activity_date, user.timezone), )
day
))
} }
return [] return []
} }
@ -104,14 +101,9 @@ class Calendar extends React.Component {
for (let i = 0; i < 7; i++) { for (let i = 0; i < 7; i++) {
formattedDate = format(day, dateFormat) formattedDate = format(day, dateFormat)
const dayActivities = this.filterActivities(day) const dayActivities = this.filterActivities(day)
const isDisabled = isSameMonth(day, currentMonth) const isDisabled = isSameMonth(day, currentMonth) ? '' : 'disabled'
? ''
: 'disabled'
days.push( days.push(
<div <div className={`col cell img-${isDisabled}`} key={day}>
className={`col cell img-${isDisabled}`}
key={day}
>
<span className="number">{formattedDate}</span> <span className="number">{formattedDate}</span>
{dayActivities.map(act => ( {dayActivities.map(act => (
<Link key={act.id} to={`/activities/${act.id}`}> <Link key={act.id} to={`/activities/${act.id}`}>
@ -129,11 +121,14 @@ class Calendar extends React.Component {
<i <i
className="fa fa-trophy custom-fa-small" className="fa fa-trophy custom-fa-small"
aria-hidden="true" aria-hidden="true"
title={act.records.map(rec => ` ${ title={act.records.map(
recordsLabels.filter( rec =>
r => r.record_type === rec.record_type ` ${
)[0].label recordsLabels.filter(
}`)} r => r.record_type === rec.record_type
)[0].label
}`
)}
/> />
</sup> </sup>
)} )}
@ -196,10 +191,9 @@ export default connect(
dispatch => ({ dispatch => ({
loadMonthActivities: (start, end) => { loadMonthActivities: (start, end) => {
const dateFormat = 'yyyy-MM-dd' const dateFormat = 'yyyy-MM-dd'
dispatch(getMonthActivities( dispatch(
format(start, dateFormat), getMonthActivities(format(start, dateFormat), format(end, dateFormat))
format(end, dateFormat), )
))
}, },
}) })
)(Calendar) )(Calendar)

View File

@ -3,67 +3,59 @@ import { Link } from 'react-router-dom'
import { formatRecord } from '../../utils/activities' import { formatRecord } from '../../utils/activities'
export default function RecordsCard (props) { export default function RecordsCard(props) {
const { records, sports, user } = props const { records, sports, user } = props
const recordsBySport = records.reduce((sportList, record) => { const recordsBySport = records.reduce((sportList, record) => {
const sport = sports.find(s => s.id === record.sport_id) const sport = sports.find(s => s.id === record.sport_id)
if (sportList[sport.label] === void 0) { if (sportList[sport.label] === void 0) {
sportList[sport.label] = { sportList[sport.label] = {
img: sport.img, img: sport.img,
records: [], records: [],
}
} }
sportList[sport.label].records.push(formatRecord(record, user.timezone)) }
return sportList sportList[sport.label].records.push(formatRecord(record, user.timezone))
return sportList
}, {}) }, {})
return ( return (
<div className="card activity-card"> <div className="card activity-card">
<div className="card-header"> <div className="card-header">Personal records</div>
Personal records
</div>
<div className="card-body"> <div className="card-body">
{Object.keys(recordsBySport).length === 0 {Object.keys(recordsBySport).length === 0
? 'No records' ? 'No records'
: (Object.keys(recordsBySport).map(sportLabel => ( : Object.keys(recordsBySport).map(sportLabel => (
<table <table
className="table table-borderless table-sm record-table" className="table table-borderless table-sm record-table"
key={sportLabel} key={sportLabel}
> >
<thead> <thead>
<tr> <tr>
<th colSpan="3"> <th colSpan="3">
<img <img
alt={`${sportLabel} logo`} alt={`${sportLabel} logo`}
className="record-logo" className="record-logo"
src={recordsBySport[sportLabel].img} src={recordsBySport[sportLabel].img}
/> />
{sportLabel} {sportLabel}
</th> </th>
</tr>
</thead>
<tbody>
{recordsBySport[sportLabel].records.map(rec => (
<tr key={rec.id}>
<td>
{rec.record_type}
</td>
<td className="text-right">
{rec.value}
</td>
<td className="text-right">
<Link to={`/activities/${rec.activity_id}`}>
{rec.activity_date}
</Link>
</td>
</tr> </tr>
))} </thead>
</tbody> <tbody>
</table>)) {recordsBySport[sportLabel].records.map(rec => (
) <tr key={rec.id}>
} <td>{rec.record_type}</td>
<td className="text-right">{rec.value}</td>
<td className="text-right">
<Link to={`/activities/${rec.activity_id}`}>
{rec.activity_date}
</Link>
</td>
</tr>
))}
</tbody>
</table>
))}
</div> </div>
</div> </div>
) )
} }

View File

@ -3,7 +3,6 @@ import React from 'react'
import Stats from '../Common/Stats' import Stats from '../Common/Stats'
export default class Statistics extends React.Component { export default class Statistics extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context) super(props, context)
@ -19,9 +18,7 @@ export default class Statistics extends React.Component {
render() { render() {
return ( return (
<div className="card activity-card"> <div className="card activity-card">
<div className="card-header"> <div className="card-header">This month</div>
This month
</div>
<div className="card-body"> <div className="card-body">
<Stats displayEmpty={false} statsParams={this.state} /> <Stats displayEmpty={false} statsParams={this.state} />
</div> </div>

View File

@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
export default function UserStatistics(props) {
export default function UserStatistics (props) {
const { user } = props const { user } = props
const days = user.total_duration.match(/day/g) const days = user.total_duration.match(/day/g)
? `${user.total_duration.split(',')[0]},` ? `${user.total_duration.split(',')[0]},`

View File

@ -25,11 +25,17 @@ class DashBoard extends React.Component {
render() { render() {
const { const {
activities, loadMoreActivities, message, records, sports, user activities,
loadMoreActivities,
message,
records,
sports,
user,
} = this.props } = this.props
const paginationEnd = activities.length > 0 const paginationEnd =
? activities[activities.length - 1].previous_activity === null activities.length > 0
: true ? activities[activities.length - 1].previous_activity === null
: true
const { page } = this.state const { page } = this.state
return ( return (
<div> <div>
@ -39,7 +45,8 @@ class DashBoard extends React.Component {
{message ? ( {message ? (
<code>{message}</code> <code>{message}</code>
) : ( ) : (
(activities && sports.length > 0) && ( activities &&
sports.length > 0 && (
<div className="container dashboard"> <div className="container dashboard">
<UserStatistics user={user} /> <UserStatistics user={user} />
<div className="row"> <div className="row">
@ -51,23 +58,24 @@ class DashBoard extends React.Component {
<Calendar /> <Calendar />
{activities.length > 0 ? ( {activities.length > 0 ? (
activities.map(activity => ( activities.map(activity => (
<ActivityCard <ActivityCard
activity={activity} activity={activity}
key={activity.id} key={activity.id}
sports={sports} sports={sports}
user={user} user={user}
/>) />
)) : ( ))
) : (
<div className="card text-center"> <div className="card text-center">
<div className="card-body"> <div className="card-body">
No workouts. {' '} No workouts.{' '}
<Link to={{ pathname: '/activities/add' }}> <Link to={{ pathname: '/activities/add' }}>
Upload one ! Upload one !
</Link> </Link>
</div> </div>
</div> </div>
)} )}
{!paginationEnd && {!paginationEnd && (
<input <input
type="submit" type="submit"
className="btn btn-default btn-md btn-block" className="btn btn-default btn-md btn-block"
@ -77,7 +85,7 @@ class DashBoard extends React.Component {
this.setState({ page: page + 1 }) this.setState({ page: page + 1 })
}} }}
/> />
} )}
</div> </div>
</div> </div>
</div> </div>

View File

@ -13,7 +13,8 @@ export default function Footer() {
rel="noopener noreferrer" rel="noopener noreferrer"
> >
source code source code
</a> under{' '} </a>{' '}
under{' '}
<a <a
href="https://choosealicense.com/licenses/gpl-3.0/" href="https://choosealicense.com/licenses/gpl-3.0/"
target="_blank" target="_blank"
@ -21,7 +22,7 @@ export default function Footer() {
> >
GPLv3 GPLv3
</a>{' '} </a>{' '}
license -{' '} license -{' '}
<a <a
href="https://samr1.github.io/FitTrackee/" href="https://samr1.github.io/FitTrackee/"
target="_blank" target="_blank"
@ -33,4 +34,3 @@ export default function Footer() {
</footer> </footer>
) )
} }

View File

@ -75,16 +75,16 @@ class NavBar extends React.PureComponent {
</li> </li>
)} )}
{/* {user.admin && ( */} {/* {user.admin && ( */}
{/* <li className="nav-item"> */} {/* <li className="nav-item"> */}
{/* <Link */} {/* <Link */}
{/* className="nav-link" */} {/* className="nav-link" */}
{/* to={{ */} {/* to={{ */}
{/* pathname: '/admin', */} {/* pathname: '/admin', */}
{/* }} */} {/* }} */}
{/* > */} {/* > */}
{/* Admin */} {/* Admin */}
{/* </Link> */} {/* </Link> */}
{/* </li> */} {/* </li> */}
{/* )} */} {/* )} */}
</ul> </ul>
<ul className="navbar-nav flex-row ml-md-auto d-none d-md-flex"> <ul className="navbar-nav flex-row ml-md-auto d-none d-md-flex">
@ -115,8 +115,7 @@ class NavBar extends React.PureComponent {
{picture === true && ( {picture === true && (
<img <img
alt="Avatar" alt="Avatar"
src={`${apiUrl}users/${id}/picture` + src={`${apiUrl}users/${id}/picture` + `?${Date.now()}`}
`?${Date.now()}`}
className="img-fluid App-nav-profile-img" className="img-fluid App-nav-profile-img"
/> />
)} )}
@ -153,11 +152,9 @@ class NavBar extends React.PureComponent {
} }
} }
export default connect( export default connect(({ user }) => ({
({ user }) => ({ id: user.id,
id: user.id, isAuthenticated: user.isAuthenticated,
isAuthenticated: user.isAuthenticated, picture: user.picture,
picture: user.picture, username: user.username,
username: user.username, }))(NavBar)
})
)(NavBar)

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
export default function AccessDenied () { export default function AccessDenied() {
return ( return (
<div> <div>
<Helmet> <Helmet>
@ -9,7 +9,7 @@ export default function AccessDenied () {
</Helmet> </Helmet>
<h1 className="page-title">Access denied</h1> <h1 className="page-title">Access denied</h1>
<p className="App-center"> <p className="App-center">
{'You don\'t have permissions to access this page.'} {"You don't have permissions to access this page."}
</p> </p>
</div> </div>
) )

View File

@ -1,13 +1,13 @@
import React from 'react' import React from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
export default function NotFound () { export default function NotFound() {
return ( return (
<div> <div>
<Helmet> <Helmet>
<title>fittrackee - 404</title> <title>fittrackee - 404</title>
</Helmet> </Helmet>
<h1 className="page-title">Page not found</h1> <h1 className="page-title">Page not found</h1>
</div> </div>
) )
} }

View File

@ -10,7 +10,7 @@ import {
addYears, addYears,
subMonths, subMonths,
subWeeks, subWeeks,
subYears subYears,
} from 'date-fns' } from 'date-fns'
import React from 'react' import React from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
@ -32,7 +32,7 @@ class Statistics extends React.Component {
end: endOfMonth(date), end: endOfMonth(date),
duration: 'month', duration: 'month',
type: 'by_time', type: 'by_time',
} },
} }
} }
@ -51,26 +51,26 @@ class Statistics extends React.Component {
const duration = e.target.name const duration = e.target.name
const date = new Date() const date = new Date()
const start = duration === 'year' const start =
? startOfYear(subYears(date, 9)) duration === 'year'
: duration === 'week' ? startOfYear(subYears(date, 9))
: duration === 'week'
? startOfMonth(subMonths(date, 2)) ? startOfMonth(subMonths(date, 2))
: startOfMonth(subMonths(date, 11)) : startOfMonth(subMonths(date, 11))
const end = duration === 'year' const end =
? endOfYear(date) duration === 'year'
: duration === 'week' ? endOfYear(date)
: duration === 'week'
? endOfWeek(date) ? endOfWeek(date)
: endOfMonth(date) : endOfMonth(date)
this.setState({ statsParams: this.setState({ statsParams: { duration, end, start, type: 'by_time' } })
{ duration, end, start, type: 'by_time' }
})
} }
handleOnChangeSports(sportId) { handleOnChangeSports(sportId) {
const { displayedSports } = this.state const { displayedSports } = this.state
if (displayedSports.includes(sportId)) { if (displayedSports.includes(sportId)) {
this.setState({ this.setState({
displayedSports: displayedSports.filter(s => s !== sportId) displayedSports: displayedSports.filter(s => s !== sportId),
}) })
} else { } else {
this.setState({ displayedSports: displayedSports.concat([sportId]) }) this.setState({ displayedSports: displayedSports.concat([sportId]) })
@ -81,30 +81,34 @@ class Statistics extends React.Component {
const { start, end, duration } = this.state.statsParams const { start, end, duration } = this.state.statsParams
let newStart, newEnd let newStart, newEnd
if (forward) { if (forward) {
newStart = duration === 'year' newStart =
? startOfYear(subYears(start, 1)) duration === 'year'
: duration === 'week' ? startOfYear(subYears(start, 1))
: duration === 'week'
? startOfWeek(subWeeks(start, 1)) ? startOfWeek(subWeeks(start, 1))
: startOfMonth(subMonths(start, 1)) : startOfMonth(subMonths(start, 1))
newEnd = duration === 'year' newEnd =
? endOfYear(subYears(end, 1)) duration === 'year'
: duration === 'week' ? endOfYear(subYears(end, 1))
: duration === 'week'
? endOfWeek(subWeeks(end, 1)) ? endOfWeek(subWeeks(end, 1))
: endOfMonth(subMonths(end, 1)) : endOfMonth(subMonths(end, 1))
} else { } else {
newStart = duration === 'year' newStart =
? startOfYear(addYears(start, 1)) duration === 'year'
: duration === 'week' ? startOfYear(addYears(start, 1))
: duration === 'week'
? startOfWeek(addWeeks(start, 1)) ? startOfWeek(addWeeks(start, 1))
: startOfMonth(addMonths(start, 1)) : startOfMonth(addMonths(start, 1))
newEnd = duration === 'year' newEnd =
? endOfYear(addYears(end, 1)) duration === 'year'
: duration === 'week' ? endOfYear(addYears(end, 1))
: duration === 'week'
? endOfWeek(addWeeks(end, 1)) ? endOfWeek(addWeeks(end, 1))
: endOfMonth(addMonths(end, 1)) : endOfMonth(addMonths(end, 1))
} }
this.setState({ statsParams: this.setState({
{ duration, end: newEnd, start: newStart, type: 'by_time' } statsParams: { duration, end: newEnd, start: newStart, type: 'by_time' },
}) })
} }
@ -118,9 +122,7 @@ class Statistics extends React.Component {
</Helmet> </Helmet>
<div className="container dashboard"> <div className="container dashboard">
<div className="card activity-card"> <div className="card activity-card">
<div className="card-header"> <div className="card-header">Statistics</div>
Statistics
</div>
<div className="card-body"> <div className="card-body">
<div className="chart-filters row"> <div className="chart-filters row">
<div className="col chart-arrows"> <div className="col chart-arrows">
@ -135,7 +137,7 @@ class Statistics extends React.Component {
<div className="col-md-3 time-frames justify-content-around"> <div className="col-md-3 time-frames justify-content-around">
{durations.map(d => ( {durations.map(d => (
<div className="time-frame" key={d}> <div className="time-frame" key={d}>
<label > <label>
<input <input
type="radio" type="radio"
id={d} id={d}
@ -143,7 +145,7 @@ class Statistics extends React.Component {
checked={d === statsParams.duration} checked={d === statsParams.duration}
onChange={e => this.handleOnChangeDuration(e)} onChange={e => this.handleOnChangeDuration(e)}
/> />
<span>{ d }</span> <span>{d}</span>
</label> </label>
</div> </div>
))} ))}
@ -186,8 +188,6 @@ class Statistics extends React.Component {
} }
} }
export default connect( export default connect(state => ({
state => ({ sports: state.sports.data,
sports: state.sports.data, }))(Statistics)
})
)(Statistics)

View File

@ -4,7 +4,7 @@ import { Helmet } from 'react-helmet'
import { history } from '../../index' import { history } from '../../index'
import { isRegistrationAllowed } from '../../utils' import { isRegistrationAllowed } from '../../utils'
export default function Form (props) { export default function Form(props) {
return ( return (
<div> <div>
<Helmet> <Helmet>
@ -17,7 +17,8 @@ export default function Form (props) {
<div className="row"> <div className="row">
<div className="col-md-3" /> <div className="col-md-3" />
<div className="col-md-6"> <div className="col-md-6">
<hr /><br /> <hr />
<br />
{props.formType === 'register' && !isRegistrationAllowed ? ( {props.formType === 'register' && !isRegistrationAllowed ? (
<div className="card"> <div className="card">
<div className="card-body">Registration is disabled.</div> <div className="card-body">Registration is disabled.</div>
@ -26,27 +27,30 @@ export default function Form (props) {
type="submit" type="submit"
className="btn btn-secondary btn-lg btn-block" className="btn btn-secondary btn-lg btn-block"
onClick={() => history.go(-1)} onClick={() => history.go(-1)}
>Back >
Back
</button> </button>
</div> </div>
</div> </div>
) : ( ) : (
<form onSubmit={event => <form
props.handleUserFormSubmit(event, props.formType)} onSubmit={event =>
> props.handleUserFormSubmit(event, props.formType)
{props.formType === 'register' &&
<div className="form-group">
<input
className="form-control input-lg"
name="username"
placeholder="Enter a username"
required
type="text"
value={props.userForm.username}
onChange={props.onHandleFormChange}
/>
</div>
} }
>
{props.formType === 'register' && (
<div className="form-group">
<input
className="form-control input-lg"
name="username"
placeholder="Enter a username"
required
type="text"
value={props.userForm.username}
onChange={props.onHandleFormChange}
/>
</div>
)}
<div className="form-group"> <div className="form-group">
<input <input
className="form-control input-lg" className="form-control input-lg"
@ -69,19 +73,19 @@ export default function Form (props) {
onChange={props.onHandleFormChange} onChange={props.onHandleFormChange}
/> />
</div> </div>
{props.formType === 'register' && {props.formType === 'register' && (
<div className="form-group"> <div className="form-group">
<input <input
className="form-control input-lg" className="form-control input-lg"
name="password_conf" name="password_conf"
placeholder="Enter the password confirmation" placeholder="Enter the password confirmation"
required required
type="password" type="password"
value={props.userForm.password_conf} value={props.userForm.password_conf}
onChange={props.onHandleFormChange} onChange={props.onHandleFormChange}
/> />
</div> </div>
} )}
<input <input
type="submit" type="submit"
className="btn btn-primary btn-lg btn-block" className="btn btn-primary btn-lg btn-block"

View File

@ -16,8 +16,8 @@ class Logout extends React.Component {
<div className="card col-8"> <div className="card col-8">
<div className="card-body"> <div className="card-body">
<div className="text-center"> <div className="text-center">
You are now logged out. You are now logged out. Click <Link to="/login">here</Link> to
Click <Link to="/login">here</Link> to log back in. log back in.
</div> </div>
</div> </div>
</div> </div>
@ -35,6 +35,6 @@ export default connect(
dispatch => ({ dispatch => ({
UserLogout: () => { UserLogout: () => {
dispatch(logout()) dispatch(logout())
} },
}) })
)(Logout) )(Logout)

View File

@ -7,22 +7,20 @@ import { Link } from 'react-router-dom'
import { deletePicture, uploadPicture } from '../../actions/user' import { deletePicture, uploadPicture } from '../../actions/user'
import { apiUrl } from '../../utils' import { apiUrl } from '../../utils'
function Profile ({ message, onDeletePicture, onUploadPicture, user }) { function Profile({ message, onDeletePicture, onUploadPicture, user }) {
return ( return (
<div> <div>
<Helmet> <Helmet>
<title>FitTrackee - Profile</title> <title>FitTrackee - Profile</title>
</Helmet> </Helmet>
{ message !== '' && ( {message !== '' && <code>{message}</code>}
<code>{message}</code>
)}
<div className="container"> <div className="container">
<h1 className="page-title">Profile</h1> <h1 className="page-title">Profile</h1>
<div className="row"> <div className="row">
<div className="col-md-12"> <div className="col-md-12">
<div className="card"> <div className="card">
<div className="card-header userName"> <div className="card-header userName">
{user.username} {' '} {user.username}{' '}
<Link <Link
to={{ to={{
pathname: '/profile/edit', pathname: '/profile/edit',
@ -35,48 +33,49 @@ function Profile ({ message, onDeletePicture, onUploadPicture, user }) {
<div className="row"> <div className="row">
<div className="col-md-8"> <div className="col-md-8">
<p>Email: {user.email}</p> <p>Email: {user.email}</p>
<p>Registration Date: { <p>
format(new Date(user.created_at), 'dd/MM/yyyy HH:mm') Registration Date:{' '}
} {format(new Date(user.created_at), 'dd/MM/yyyy HH:mm')}
</p> </p>
<p>First Name: {user.first_name}</p> <p>First Name: {user.first_name}</p>
<p>Last Name: {user.last_name}</p> <p>Last Name: {user.last_name}</p>
<p>Birth Date: {user.birth_date <p>
Birth Date:{' '}
{user.birth_date
? format(new Date(user.birth_date), 'dd/MM/yyyy') ? format(new Date(user.birth_date), 'dd/MM/yyyy')
: '' : ''}
}
</p> </p>
<p>Location: {user.location}</p> <p>Location: {user.location}</p>
<p>Bio: {user.bio}</p> <p>Bio: {user.bio}</p>
<p>Time zone: {user.timezone}</p> <p>Time zone: {user.timezone}</p>
</div> </div>
<div className="col-md-4"> <div className="col-md-4">
{ user.picture === true && ( {user.picture === true && (
<div> <div>
<img <img
alt="Profile" alt="Profile"
src={`${apiUrl}users/${user.id}/picture` + src={
`?${Date.now()}`} `${apiUrl}users/${user.id}/picture` +
className="img-fluid App-profile-img-small" `?${Date.now()}`
/> }
<br /> className="img-fluid App-profile-img-small"
<button />
type="submit" <br />
onClick={() => onDeletePicture()} <button type="submit" onClick={() => onDeletePicture()}>
> Delete picture
Delete picture </button>
</button> <br />
<br /><br /> <br />
</div> </div>
)} )}
<form <form
encType="multipart/form-data" encType="multipart/form-data"
onSubmit={event => onUploadPicture(event)} onSubmit={event => onUploadPicture(event)}
> >
<input <input
type="file" type="file"
name="picture" name="picture"
accept=".png,.jpg,.gif" accept=".png,.jpg,.gif"
/> />
<br /> <br />
<button type="submit">Send</button> <button type="submit">Send</button>

View File

@ -7,12 +7,11 @@ import TimezonePicker from 'react-timezone'
import { handleProfileFormSubmit } from '../../actions/user' import { handleProfileFormSubmit } from '../../actions/user'
import { history } from '../../index' import { history } from '../../index'
class ProfileEdit extends React.Component { class ProfileEdit extends React.Component {
constructor(props, context) { constructor(props, context) {
super(props, context) super(props, context)
this.state = { this.state = {
formData: {} formData: {},
} }
} }
@ -29,11 +28,13 @@ class ProfileEdit extends React.Component {
initForm() { initForm() {
const { user } = this.props const { user } = this.props
const formData = {} const formData = {}
Object.keys(user).map(k => user[k] === null Object.keys(user).map(k =>
? formData[k] = '' user[k] === null
: k === 'birth_date' ? (formData[k] = '')
? formData[k] = format(new Date(user[k]), 'yyyy-MM-DD') : k === 'birth_date'
: formData[k] = user[k]) ? (formData[k] = format(new Date(user[k]), 'yyyy-MM-DD'))
: (formData[k] = user[k])
)
this.setState({ formData }) this.setState({ formData })
} }
@ -43,7 +44,7 @@ class ProfileEdit extends React.Component {
this.setState(formData) this.setState(formData)
} }
render () { render() {
const { onHandleProfileFormSubmit, message, user } = this.props const { onHandleProfileFormSubmit, message, user } = this.props
const { formData } = this.state const { formData } = this.state
return ( return (
@ -51,9 +52,7 @@ class ProfileEdit extends React.Component {
<Helmet> <Helmet>
<title>FitTrackee - Edit Profile</title> <title>FitTrackee - Edit Profile</title>
</Helmet> </Helmet>
{ message !== '' && ( {message !== '' && <code>{message}</code>}
<code>{message}</code>
)}
{formData.isAuthenticated && ( {formData.isAuthenticated && (
<div className="container"> <div className="container">
<h1 className="page-title">Profile Edition</h1> <h1 className="page-title">Profile Edition</h1>
@ -61,153 +60,153 @@ class ProfileEdit extends React.Component {
<div className="col-md-2" /> <div className="col-md-2" />
<div className="col-md-8"> <div className="col-md-8">
<div className="card"> <div className="card">
<div className="card-header"> <div className="card-header">{user.username}</div>
{user.username}
</div>
<div className="card-body"> <div className="card-body">
<div className="row"> <div className="row">
<div className="col-md-12"> <div className="col-md-12">
<form onSubmit={event => { <form
event.preventDefault() onSubmit={event => {
onHandleProfileFormSubmit(formData) event.preventDefault()
}} onHandleProfileFormSubmit(formData)
> }}
<div className="form-group"> >
<label>Email:
<input
name="email"
className="form-control input-lg"
type="text"
value={formData.email}
readOnly
/>
</label>
</div>
<div className="form-group">
<label>
Registration Date:
<input
name="createdAt"
className="form-control input-lg"
type="text"
value={formData.created_at}
disabled
/>
</label>
</div>
<div className="form-group">
<label>
Password:
<input
name="password"
className="form-control input-lg"
type="password"
onChange={e => this.handleFormChange(e)}
/>
</label>
</div>
<div className="form-group"> <div className="form-group">
<label> <label>
Password Confirmation: Email:
<input <input
name="password_conf" name="email"
className="form-control input-lg" className="form-control input-lg"
type="password" type="text"
onChange={e => this.handleFormChange(e)} value={formData.email}
/> readOnly
</label> />
</div> </label>
<hr /> </div>
<div className="form-group"> <div className="form-group">
<label> <label>
First Name: Registration Date:
<input <input
name="first_name" name="createdAt"
className="form-control input-lg" className="form-control input-lg"
type="text" type="text"
value={formData.first_name} value={formData.created_at}
onChange={e => this.handleFormChange(e)} disabled
/> />
</label> </label>
</div> </div>
<div className="form-group"> <div className="form-group">
<label> <label>
Last Name: Password:
<input <input
name="last_name" name="password"
className="form-control input-lg" className="form-control input-lg"
type="text" type="password"
value={formData.last_name} onChange={e => this.handleFormChange(e)}
onChange={e => this.handleFormChange(e)} />
/> </label>
</label> </div>
</div> <div className="form-group">
<div className="form-group"> <label>
<label> Password Confirmation:
Birth Date <input
<input name="password_conf"
name="birth_date" className="form-control input-lg"
className="form-control input-lg" type="password"
type="date" onChange={e => this.handleFormChange(e)}
value={formData.birth_date} />
onChange={e => this.handleFormChange(e)} </label>
/> </div>
</label> <hr />
</div> <div className="form-group">
<div className="form-group"> <label>
<label> First Name:
Location: <input
<input name="first_name"
name="location" className="form-control input-lg"
className="form-control input-lg" type="text"
type="text" value={formData.first_name}
value={formData.location} onChange={e => this.handleFormChange(e)}
onChange={e => this.handleFormChange(e)} />
/> </label>
</label> </div>
</div> <div className="form-group">
<div className="form-group"> <label>
<label> Last Name:
Bio: <input
<textarea name="last_name"
name="bio" className="form-control input-lg"
className="form-control input-lg" type="text"
maxLength="200" value={formData.last_name}
value={formData.bio} onChange={e => this.handleFormChange(e)}
onChange={e => this.handleFormChange(e)} />
/> </label>
</label> </div>
</div> <div className="form-group">
<div className="form-group"> <label>
<label> Birth Date
Timezone: <input
<TimezonePicker name="birth_date"
className="form-control timezone-custom-height" className="form-control input-lg"
onChange={tz => { type="date"
const e = { target: value={formData.birth_date}
{ onChange={e => this.handleFormChange(e)}
name: 'timezone', />
value: tz ? tz : 'Europe/Paris' </label>
</div>
<div className="form-group">
<label>
Location:
<input
name="location"
className="form-control input-lg"
type="text"
value={formData.location}
onChange={e => this.handleFormChange(e)}
/>
</label>
</div>
<div className="form-group">
<label>
Bio:
<textarea
name="bio"
className="form-control input-lg"
maxLength="200"
value={formData.bio}
onChange={e => this.handleFormChange(e)}
/>
</label>
</div>
<div className="form-group">
<label>
Timezone:
<TimezonePicker
className="form-control timezone-custom-height"
onChange={tz => {
const e = {
target: {
name: 'timezone',
value: tz ? tz : 'Europe/Paris',
},
} }
} this.handleFormChange(e)
this.handleFormChange(e) }}
}} value={formData.timezone}
value={formData.timezone} />
/> </label>
</label> </div>
</div> <input
<input type="submit"
type="submit" className="btn btn-primary btn-lg btn-block"
className="btn btn-primary btn-lg btn-block" value="Submit"
value="Submit" />
/> <input
<input type="submit"
type="submit" className="btn btn-secondary btn-lg btn-block"
className="btn btn-secondary btn-lg btn-block" onClick={() => history.push('/profile')}
onClick={() => history.push('/profile')} value="Cancel"
value="Cancel" />
/> </form>
</form>
</div> </div>
</div> </div>
</div> </div>
@ -220,7 +219,6 @@ class ProfileEdit extends React.Component {
</div> </div>
) )
} }
} }
export default connect( export default connect(

View File

@ -15,7 +15,7 @@ class UserForm extends React.Component {
email: '', email: '',
password: '', password: '',
password_conf: '', password_conf: '',
} },
} }
} }
@ -27,7 +27,7 @@ class UserForm extends React.Component {
emptyForm() { emptyForm() {
const { formData } = this.state const { formData } = this.state
Object.keys(formData).map(k => formData[k] = '') Object.keys(formData).map(k => (formData[k] = ''))
this.setState(formData) this.setState(formData)
} }
@ -38,12 +38,7 @@ class UserForm extends React.Component {
} }
render() { render() {
const { const { formType, message, messages, onHandleUserFormSubmit } = this.props
formType,
message,
messages,
onHandleUserFormSubmit
} = this.props
const { formData } = this.state const { formData } = this.state
return ( return (
<div> <div>
@ -51,16 +46,12 @@ class UserForm extends React.Component {
<Redirect to="/" /> <Redirect to="/" />
) : ( ) : (
<div> <div>
{message !== '' && ( {message !== '' && <code>{message}</code>}
<code>{message}</code>
)}
{messages.length > 0 && ( {messages.length > 0 && (
<code> <code>
<ul> <ul>
{messages.map(msg => ( {messages.map(msg => (
<li key={msg.id}> <li key={msg.id}>{msg.value}</li>
{msg.value}
</li>
))} ))}
</ul> </ul>
</code> </code>
@ -70,8 +61,8 @@ class UserForm extends React.Component {
userForm={formData} userForm={formData}
onHandleFormChange={event => this.onHandleFormChange(event)} onHandleFormChange={event => this.onHandleFormChange(event)}
handleUserFormSubmit={event => { handleUserFormSubmit={event => {
event.preventDefault() event.preventDefault()
onHandleUserFormSubmit(formData, formType) onHandleUserFormSubmit(formData, formType)
}} }}
/> />
</div> </div>

View File

@ -1,14 +1,13 @@
import { createApiRequest } from '../utils' import { createApiRequest } from '../utils'
export default class FitTrackeeApi { export default class FitTrackeeApi {
static getData(target, data = {}) { static getData(target, data = {}) {
let url = target let url = target
if (data.id) { if (data.id) {
url = `${url}/${data.id}` url = `${url}/${data.id}`
} else if (Object.keys(data).length > 0) { } else if (Object.keys(data).length > 0) {
url += '?' url += '?'
Object.keys(data).map(key => url += `&${key}=${data[key]}`) Object.keys(data).map(key => (url += `&${key}=${data[key]}`))
} }
const params = { const params = {
url: url, url: url,
@ -39,7 +38,7 @@ export default class FitTrackeeApi {
static postData(target, data) { static postData(target, data) {
const params = { const params = {
url: `${target}${data.id ? `/${data.id}` : '' }`, url: `${target}${data.id ? `/${data.id}` : ''}`,
method: 'POST', method: 'POST',
body: data, body: data,
type: 'application/json', type: 'application/json',

View File

@ -1,7 +1,6 @@
import { createApiRequest } from '../utils' import { createApiRequest } from '../utils'
export default class FitTrackeeApi { export default class FitTrackeeApi {
static loginOrRegister(target, data) { static loginOrRegister(target, data) {
const params = { const params = {
url: `auth/${target}`, url: `auth/${target}`,

View File

@ -15,7 +15,7 @@ import { loadProfile } from './actions/user'
export const history = createBrowserHistory() export const history = createBrowserHistory()
history.listen(() => { history.listen(() => {
window.scrollTo(0, 0) window.scrollTo(0, 0)
}) })
export const rootNode = document.getElementById('root') export const rootNode = document.getElementById('root')

View File

@ -128,17 +128,18 @@ const user = (state = initial.user, action) => {
const statistics = (state = initial.statistics, action) => const statistics = (state = initial.statistics, action) =>
handleDataAndError(state, 'statistics', action) handleDataAndError(state, 'statistics', action)
export default history => combineReducers({ export default history =>
activities, combineReducers({
calendarActivities, activities,
chartData, calendarActivities,
gpx, chartData,
loading, gpx,
message, loading,
messages, message,
records, messages,
router: connectRouter(history), records,
sports, router: connectRouter(history),
statistics, sports,
user, statistics,
}) user,
})

View File

@ -58,13 +58,13 @@ function registerValidSW(swUrl) {
// the fresh content will have been added to the cache. // the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is // It's the perfect time to display a "New content is
// available; please refresh." message in your web app. // available; please refresh." message in your web app.
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('New content is available; please refresh.') console.log('New content is available; please refresh.')
} else { } else {
// At this point, everything has been precached. // At this point, everything has been precached.
// It's the perfect time to display a // It's the perfect time to display a
// "Content is cached for offline use." message. // "Content is cached for offline use." message.
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log('Content is cached for offline use.') console.log('Content is cached for offline use.')
} }
} }
@ -97,7 +97,7 @@ function checkValidServiceWorker(swUrl) {
} }
}) })
.catch(() => { .catch(() => {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log( console.log(
'No internet connection found. App is running in offline mode.' 'No internet connection found. App is running in offline mode.'
) )

View File

@ -32,7 +32,7 @@ export const getGeoJson = gpxContent => {
export const formatActivityDate = ( export const formatActivityDate = (
dateTime, dateTime,
dateFormat = null, dateFormat = null,
timeFormat = null, timeFormat = null
) => { ) => {
if (!dateFormat) { if (!dateFormat) {
dateFormat = 'yyyy/MM/dd' dateFormat = 'yyyy/MM/dd'
@ -70,16 +70,16 @@ export const formatRecord = (record, tz) => {
case 'FD': case 'FD':
value = `${record.value} km` value = `${record.value} km`
break break
default: // 'LD' default:
// 'LD'
value = record.value // eslint-disable-line prefer-destructuring value = record.value // eslint-disable-line prefer-destructuring
} }
const [recordType] = recordsLabels.filter( const [recordType] = recordsLabels.filter(
r => r.record_type === record.record_type r => r.record_type === record.record_type
) )
return { return {
activity_date: formatActivityDate( activity_date: formatActivityDate(getDateWithTZ(record.activity_date, tz))
getDateWithTZ(record.activity_date, tz) .activity_date,
).activity_date,
activity_id: record.activity_id, activity_id: record.activity_id,
id: record.id, id: record.id,
record_type: recordType.label, record_type: recordType.label,

View File

@ -3,9 +3,10 @@ import { DateTime } from 'luxon'
export const version = '0.2.1-beta' // version stored in 'utils' for now export const version = '0.2.1-beta' // version stored in 'utils' for now
export const apiUrl = `${process.env.REACT_APP_API_URL}/api/` export const apiUrl = `${process.env.REACT_APP_API_URL}/api/`
/* prettier-ignore */
export const thunderforestApiKey = `${ export const thunderforestApiKey = `${
process.env.REACT_APP_THUNDERFOREST_API_KEY process.env.REACT_APP_THUNDERFOREST_API_KEY
}` }`
export const gpxLimit = `${process.env.REACT_APP_GPX_LIMIT_IMPORT}` export const gpxLimit = `${process.env.REACT_APP_GPX_LIMIT_IMPORT}`
export const isRegistrationAllowed = export const isRegistrationAllowed =
process.env.REACT_APP_ALLOW_REGISTRATION !== 'false' process.env.REACT_APP_ALLOW_REGISTRATION !== 'false'
@ -21,12 +22,10 @@ export const generateIds = arr => {
}) })
} }
export const createApiRequest = params => { export const createApiRequest = params => {
const headers = {} const headers = {}
if (!params.noAuthorization) { if (!params.noAuthorization) {
headers.Authorization = `Bearer ${ headers.Authorization = `Bearer ${window.localStorage.getItem('authToken')}`
window.localStorage.getItem('authToken')}`
} }
if (params.type) { if (params.type) {
headers['Content-Type'] = params.type headers['Content-Type'] = params.type
@ -42,22 +41,23 @@ export const createApiRequest = params => {
} }
const request = new Request(`${apiUrl}${params.url}`, requestParams) const request = new Request(`${apiUrl}${params.url}`, requestParams)
return fetch(request) return fetch(request)
.then(response => params.method === 'DELETE' .then(response => (params.method === 'DELETE' ? response : response.json()))
? response
: response.json())
.catch(error => { .catch(error => {
console.error(error) console.error(error)
return new Error('An error occurred. Please contact the administrator.') return new Error('An error occurred. Please contact the administrator.')
}) })
} }
export const getDateWithTZ = (date, tz) => { export const getDateWithTZ = (date, tz) => {
if (!date) { if (!date) {
return '' return ''
} }
const dt = DateTime.fromISO( const dt = DateTime.fromISO(
format(new Date(date), "yyyy-MM-dd'T'HH:mm:ss.SSSxxx")).setZone(tz) format(new Date(date), "yyyy-MM-dd'T'HH:mm:ss.SSSxxx")
).setZone(tz)
return parse( return parse(
dt.toFormat('yyyy-MM-dd HH:mm:ss'), 'yyyy-MM-dd HH:mm:ss', new Date()) dt.toFormat('yyyy-MM-dd HH:mm:ss'),
'yyyy-MM-dd HH:mm:ss',
new Date()
)
} }

View File

@ -1,12 +1,13 @@
import { import {
addDays, addDays,
addMonths, addMonths,
addYears, format, startOfMonth, addYears,
format,
startOfMonth,
startOfWeek, startOfWeek,
startOfYear startOfYear,
} from 'date-fns' } from 'date-fns'
const xAxisFormats = [ const xAxisFormats = [
{ duration: 'week', dateFormat: 'yyyy-MM-dd', xAxis: 'dd/MM' }, { duration: 'week', dateFormat: 'yyyy-MM-dd', xAxis: 'dd/MM' },
{ duration: 'month', dateFormat: 'yyyy-MM', xAxis: 'MM/yyyy' }, { duration: 'month', dateFormat: 'yyyy-MM', xAxis: 'MM/yyyy' },
@ -24,22 +25,21 @@ export const formatDuration = (totalSeconds, formatWithDay = false) => {
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0') const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0')
const seconds = String(totalSeconds % 60).padStart(2, '0') const seconds = String(totalSeconds % 60).padStart(2, '0')
if (formatWithDay) { if (formatWithDay) {
return `${ return `${days === '0' ? '' : `${days}d:`}${
days === '0' ? '' : `${days}d:`
}${
hours === '00' ? '' : `${hours}h:` hours === '00' ? '' : `${hours}h:`
}${minutes}m:${seconds}s` }${minutes}m:${seconds}s`
} }
return `${hours === '00' ? '' : `${hours}:`}${minutes}:${seconds}` return `${hours === '00' ? '' : `${hours}:`}${minutes}:${seconds}`
} }
export const formatValue = (displayedData, value) => value === 0 export const formatValue = (displayedData, value) =>
value === 0
? '' ? ''
: displayedData === 'distance' : displayedData === 'distance'
? `${value.toFixed(2)} km` ? `${value.toFixed(2)} km`
: displayedData === 'duration' : displayedData === 'duration'
? formatDuration(value) ? formatDuration(value)
: value : value
const dateIncrement = (duration, day) => { const dateIncrement = (duration, day) => {
switch (duration) { switch (duration) {
@ -65,16 +65,15 @@ const startDate = (duration, day) => {
} }
} }
export const formatStats = ( export const formatStats = (stats, sports, params, displayedSports) => {
stats, sports, params, displayedSports
) => {
const nbActivitiesStats = [] const nbActivitiesStats = []
const distanceStats = [] const distanceStats = []
const durationStats = [] const durationStats = []
for (let day = startDate(params.duration, params.start); for (
day <= params.end; let day = startDate(params.duration, params.start);
day = dateIncrement(params.duration, day) day <= params.end;
day = dateIncrement(params.duration, day)
) { ) {
const [xAxisFormat] = xAxisFormats.filter( const [xAxisFormat] = xAxisFormats.filter(
x => x.duration === params.duration x => x.duration === params.duration
@ -86,15 +85,17 @@ export const formatStats = (
const dataDuration = { date: xAxis } const dataDuration = { date: xAxis }
if (stats[date]) { if (stats[date]) {
Object.keys(stats[date]).filter( Object.keys(stats[date])
sportId => displayedSports ? displayedSports.includes(+sportId) : true .filter(sportId =>
).map(sportId => { displayedSports ? displayedSports.includes(+sportId) : true
const sportLabel = sports.filter(s => s.id === +sportId)[0].label )
dataNbActivities[sportLabel] = stats[date][sportId].nb_activities .map(sportId => {
dataDistance[sportLabel] = stats[date][sportId].total_distance const sportLabel = sports.filter(s => s.id === +sportId)[0].label
dataDuration[sportLabel] = stats[date][sportId].total_duration dataNbActivities[sportLabel] = stats[date][sportId].nb_activities
return null dataDistance[sportLabel] = stats[date][sportId].total_distance
}) dataDuration[sportLabel] = stats[date][sportId].total_duration
return null
})
} }
nbActivitiesStats.push(dataNbActivities) nbActivitiesStats.push(dataNbActivities)
distanceStats.push(dataDistance) distanceStats.push(dataDistance)
@ -104,6 +105,6 @@ export const formatStats = (
return { return {
activities: nbActivitiesStats, activities: nbActivitiesStats,
distance: distanceStats, distance: distanceStats,
duration: durationStats duration: durationStats,
} }
} }