Client - replace 'Activity' with 'Workout' - fix #58
This commit is contained in:
19
fittrackee_client/src/components/Workout/WorkoutAdd.jsx
Normal file
19
fittrackee_client/src/components/Workout/WorkoutAdd.jsx
Normal file
@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import WorkoutAddOrEdit from './WorkoutAddOrEdit'
|
||||
|
||||
function WorkoutAdd(props) {
|
||||
const { message, sports } = props
|
||||
return (
|
||||
<div>
|
||||
<WorkoutAddOrEdit workout={null} message={message} sports={sports} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
message: state.message,
|
||||
sports: state.sports.data,
|
||||
user: state.user,
|
||||
}))(WorkoutAdd)
|
118
fittrackee_client/src/components/Workout/WorkoutAddOrEdit.jsx
Normal file
118
fittrackee_client/src/components/Workout/WorkoutAddOrEdit.jsx
Normal file
@ -0,0 +1,118 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { withTranslation } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import FormWithGpx from './WorkoutForms/FormWithGpx'
|
||||
import FormWithoutGpx from './WorkoutForms/FormWithoutGpx'
|
||||
import Message from '../Common/Message'
|
||||
|
||||
class WorkoutAddEdit extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
withGpx: true,
|
||||
}
|
||||
}
|
||||
|
||||
handleRadioChange(changeEvent) {
|
||||
this.setState({
|
||||
withGpx:
|
||||
changeEvent.target.name === 'withGpx'
|
||||
? changeEvent.target.value
|
||||
: !changeEvent.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { loading, message, sports, t, workout } = this.props
|
||||
const { withGpx } = this.state
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>
|
||||
FitTrackee -{' '}
|
||||
{workout
|
||||
? t('workouts:Edit a workout')
|
||||
: t('workouts:Add a workout')}
|
||||
</title>
|
||||
</Helmet>
|
||||
<br />
|
||||
<br />
|
||||
<Message message={message} t={t} />
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-2" />
|
||||
<div className="col-md-8">
|
||||
<div className="card add-workout">
|
||||
<h2 className="card-header text-center">
|
||||
{workout
|
||||
? t('workouts:Edit a workout')
|
||||
: t('workouts:Add a workout')}
|
||||
</h2>
|
||||
<div className="card-body">
|
||||
{workout ? (
|
||||
workout.with_gpx ? (
|
||||
<FormWithGpx workout={workout} sports={sports} t={t} />
|
||||
) : (
|
||||
<FormWithoutGpx workout={workout} sports={sports} t={t} />
|
||||
)
|
||||
) : (
|
||||
<div>
|
||||
<form>
|
||||
<div className="form-group row">
|
||||
<div className="col">
|
||||
<label className="radioLabel">
|
||||
<input
|
||||
className="add-workout-radio"
|
||||
type="radio"
|
||||
name="withGpx"
|
||||
disabled={loading}
|
||||
checked={withGpx}
|
||||
onChange={event =>
|
||||
this.handleRadioChange(event)
|
||||
}
|
||||
/>
|
||||
{t('workouts:with gpx file')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="col">
|
||||
<label className="radioLabel">
|
||||
<input
|
||||
className="add-workout-radio"
|
||||
type="radio"
|
||||
name="withoutGpx"
|
||||
disabled={loading}
|
||||
checked={!withGpx}
|
||||
onChange={event =>
|
||||
this.handleRadioChange(event)
|
||||
}
|
||||
/>
|
||||
{t('workouts:without gpx file')}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{withGpx ? (
|
||||
<FormWithGpx sports={sports} t={t} />
|
||||
) : (
|
||||
<FormWithoutGpx sports={sports} t={t} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation()(
|
||||
connect(state => ({
|
||||
loading: state.loading,
|
||||
}))(WorkoutAddEdit)
|
||||
)
|
@ -0,0 +1,27 @@
|
||||
import React from 'react'
|
||||
import { GeoJSON, Marker, TileLayer, useMap } from 'react-leaflet'
|
||||
import hash from 'object-hash'
|
||||
|
||||
import { apiUrl } from '../../../utils'
|
||||
|
||||
export default function Map({ bounds, coordinates, jsonData, mapAttribution }) {
|
||||
const map = useMap()
|
||||
map.fitBounds(bounds)
|
||||
return (
|
||||
<>
|
||||
<TileLayer
|
||||
// eslint-disable-next-line max-len
|
||||
attribution={mapAttribution}
|
||||
url={`${apiUrl}workouts/map_tile/{s}/{z}/{x}/{y}.png`}
|
||||
/>
|
||||
<GeoJSON
|
||||
// hash as a key to force re-rendering
|
||||
key={hash(jsonData)}
|
||||
data={jsonData}
|
||||
/>
|
||||
{coordinates.latitude && (
|
||||
<Marker position={[coordinates.latitude, coordinates.longitude]} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { getDateWithTZ } from '../../../utils'
|
||||
import { formatWorkoutDate } from '../../../utils/workouts'
|
||||
|
||||
export default function WorkoutCardHeader(props) {
|
||||
const {
|
||||
dataType,
|
||||
displayModal,
|
||||
segmentId,
|
||||
sport,
|
||||
t,
|
||||
title,
|
||||
user,
|
||||
workout,
|
||||
} = props
|
||||
const workoutDate = workout
|
||||
? formatWorkoutDate(getDateWithTZ(workout.workout_date, user.timezone))
|
||||
: null
|
||||
|
||||
const previousUrl =
|
||||
dataType === 'segment' && segmentId !== 1
|
||||
? `/workouts/${workout.id}/segment/${segmentId - 1}`
|
||||
: dataType === 'workout' && workout.previous_workout
|
||||
? `/workouts/${workout.previous_workout}`
|
||||
: null
|
||||
const nextUrl =
|
||||
dataType === 'segment' && segmentId < workout.segments.length
|
||||
? `/workouts/${workout.id}/segment/${segmentId + 1}`
|
||||
: dataType === 'workout' && workout.next_workout
|
||||
? `/workouts/${workout.next_workout}`
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-auto">
|
||||
{previousUrl ? (
|
||||
<Link className="unlink" to={previousUrl}>
|
||||
<i
|
||||
className="fa fa-chevron-left"
|
||||
aria-hidden="true"
|
||||
title={t(`workouts:See previous ${dataType}`)}
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<i
|
||||
className="fa fa-chevron-left inactive-link"
|
||||
aria-hidden="true"
|
||||
title={t(`workouts:No previous ${dataType}`)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-auto col-workout-logo">
|
||||
<img className="sport-img-medium" src={sport.img} alt="sport logo" />
|
||||
</div>
|
||||
<div className="col">
|
||||
{dataType === 'workout' ? (
|
||||
<>
|
||||
{title}{' '}
|
||||
<Link className="unlink" to={`/workouts/${workout.id}/edit`}>
|
||||
<i
|
||||
className="fa fa-edit custom-fa"
|
||||
aria-hidden="true"
|
||||
title={t('workouts:Edit workout')}
|
||||
/>
|
||||
</Link>
|
||||
<i
|
||||
className="fa fa-trash custom-fa"
|
||||
aria-hidden="true"
|
||||
onClick={() => displayModal(true)}
|
||||
title={t('workouts:Delete workout')}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* prettier-ignore */}
|
||||
<Link
|
||||
to={`/workouts/${workout.id}`}
|
||||
>
|
||||
{title}
|
||||
</Link>{' '}
|
||||
- {t('workouts:segment')} {segmentId}
|
||||
</>
|
||||
)}
|
||||
<br />
|
||||
{workoutDate && (
|
||||
<span className="workout-date">
|
||||
{`${workoutDate.workout_date} - ${workoutDate.workout_time}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
{nextUrl ? (
|
||||
<Link className="unlink" to={nextUrl}>
|
||||
<i
|
||||
className="fa fa-chevron-right"
|
||||
aria-hidden="true"
|
||||
title={t(`workouts:See next ${dataType}`)}
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<i
|
||||
className="fa fa-chevron-right inactive-link"
|
||||
aria-hidden="true"
|
||||
title={t(`workouts:No next ${dataType}`)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,240 @@
|
||||
import { format } from 'date-fns'
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import {
|
||||
Area,
|
||||
ComposedChart,
|
||||
Line,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts'
|
||||
|
||||
import {
|
||||
getSegmentChartData,
|
||||
getWorkoutChartData,
|
||||
} from '../../../actions/workouts'
|
||||
|
||||
class WorkoutCharts extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
displayDistance: true,
|
||||
dataToHide: [],
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.dataType === 'workout') {
|
||||
this.props.loadWorkoutData(this.props.workout.id)
|
||||
} else {
|
||||
this.props.loadSegmentData(this.props.workout.id, this.props.segmentId)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
(this.props.dataType === 'workout' &&
|
||||
prevProps.workout.id !== this.props.workout.id) ||
|
||||
(this.props.dataType === 'workout' && prevProps.dataType === 'segment')
|
||||
) {
|
||||
this.props.loadWorkoutData(this.props.workout.id)
|
||||
}
|
||||
if (
|
||||
this.props.dataType === 'segment' &&
|
||||
prevProps.segmentId !== this.props.segmentId
|
||||
) {
|
||||
this.props.loadSegmentData(this.props.workout.id, this.props.segmentId)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.loadWorkoutData(null)
|
||||
}
|
||||
|
||||
handleRadioChange(changeEvent) {
|
||||
this.setState({
|
||||
displayDistance:
|
||||
changeEvent.target.name === 'distance'
|
||||
? changeEvent.target.value
|
||||
: !changeEvent.target.value,
|
||||
})
|
||||
}
|
||||
|
||||
handleLegendChange(e) {
|
||||
const { dataToHide } = this.state
|
||||
const name = e.target.name // eslint-disable-line prefer-destructuring
|
||||
if (dataToHide.find(d => d === name)) {
|
||||
dataToHide.splice(dataToHide.indexOf(name), 1)
|
||||
} else {
|
||||
dataToHide.push(name)
|
||||
}
|
||||
this.setState({ dataToHide })
|
||||
}
|
||||
|
||||
displayData(name) {
|
||||
const { dataToHide } = this.state
|
||||
return !dataToHide.find(d => d === name)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { chartData, t, updateCoordinates } = this.props
|
||||
const { displayDistance } = this.state
|
||||
const xInterval = chartData ? parseInt(chartData.length / 10, 10) : 0
|
||||
let xDataKey, xScale
|
||||
if (displayDistance) {
|
||||
xDataKey = 'distance'
|
||||
xScale = 'linear'
|
||||
} else {
|
||||
xDataKey = 'duration'
|
||||
xScale = 'time'
|
||||
}
|
||||
return (
|
||||
<div className="container">
|
||||
{chartData && chartData.length > 0 ? (
|
||||
<div>
|
||||
<div className="row chart-radio">
|
||||
<label className="radioLabel col-md-1">
|
||||
<input
|
||||
type="radio"
|
||||
name="distance"
|
||||
checked={displayDistance}
|
||||
onChange={e => this.handleRadioChange(e)}
|
||||
/>
|
||||
{t('workouts:distance')}
|
||||
</label>
|
||||
<label className="radioLabel col-md-1">
|
||||
<input
|
||||
type="radio"
|
||||
name="duration"
|
||||
checked={!displayDistance}
|
||||
onChange={e => this.handleRadioChange(e)}
|
||||
/>
|
||||
{t('workouts:duration')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="row chart-radio">
|
||||
<div className="col-md-5" />
|
||||
<label className="radioLabel col-md-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="speed"
|
||||
checked={this.displayData('speed')}
|
||||
onChange={e => this.handleLegendChange(e)}
|
||||
/>
|
||||
{t('workouts:speed')}
|
||||
</label>
|
||||
<label className="radioLabel col-md-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="elevation"
|
||||
checked={this.displayData('elevation')}
|
||||
onChange={e => this.handleLegendChange(e)}
|
||||
/>
|
||||
{t('workouts:elevation')}
|
||||
</label>
|
||||
<div className="col-md-5" />
|
||||
</div>
|
||||
<div className="row chart">
|
||||
<ResponsiveContainer height={300}>
|
||||
<ComposedChart
|
||||
data={chartData}
|
||||
margin={{ top: 15, right: 30, left: 20, bottom: 15 }}
|
||||
onMouseMove={e => updateCoordinates(e.activePayload)}
|
||||
onMouseLeave={() => updateCoordinates(null)}
|
||||
>
|
||||
<XAxis
|
||||
allowDecimals={false}
|
||||
dataKey={xDataKey}
|
||||
label={{
|
||||
value: t(`workouts:${xDataKey}`),
|
||||
offset: 0,
|
||||
position: 'bottom',
|
||||
}}
|
||||
scale={xScale}
|
||||
interval={xInterval}
|
||||
tickFormatter={value =>
|
||||
displayDistance ? value : format(value, 'HH:mm:ss')
|
||||
}
|
||||
type="number"
|
||||
/>
|
||||
<YAxis
|
||||
label={{
|
||||
value: `${t('workouts:speed')} (km/h)`,
|
||||
angle: -90,
|
||||
position: 'left',
|
||||
}}
|
||||
yAxisId="left"
|
||||
/>
|
||||
<YAxis
|
||||
label={{
|
||||
value: `${t('workouts:elevation')} (m)`,
|
||||
angle: -90,
|
||||
position: 'right',
|
||||
}}
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
/>
|
||||
{this.displayData('elevation') && (
|
||||
<Area
|
||||
yAxisId="right"
|
||||
type="linear"
|
||||
dataKey="elevation"
|
||||
name={t('workouts:elevation')}
|
||||
fill="#e5e5e5"
|
||||
stroke="#cccccc"
|
||||
dot={false}
|
||||
unit=" m"
|
||||
/>
|
||||
)}
|
||||
{this.displayData('speed') && (
|
||||
<Line
|
||||
yAxisId="left"
|
||||
type="linear"
|
||||
dataKey="speed"
|
||||
name={t('workouts:speed')}
|
||||
stroke="#8884d8"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
unit=" km/h"
|
||||
/>
|
||||
)}
|
||||
<Tooltip
|
||||
labelFormatter={value =>
|
||||
displayDistance
|
||||
? `${t('workouts:distance')}: ${value} km`
|
||||
: `${t('workouts:duration')}: ${format(
|
||||
value,
|
||||
'HH:mm:ss'
|
||||
)}`
|
||||
}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="chart-info">
|
||||
{t('workouts:data from gpx, without any cleaning')}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
t('workouts:No data to display')
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
chartData: state.chartData,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadWorkoutData: workoutId => {
|
||||
dispatch(getWorkoutChartData(workoutId))
|
||||
},
|
||||
loadSegmentData: (workoutId, segmentId) => {
|
||||
dispatch(getSegmentChartData(workoutId, segmentId))
|
||||
},
|
||||
})
|
||||
)(WorkoutCharts)
|
@ -0,0 +1,73 @@
|
||||
import React from 'react'
|
||||
|
||||
import WorkoutWeather from './WorkoutWeather'
|
||||
|
||||
export default function WorkoutDetails(props) {
|
||||
const { t, workout } = props
|
||||
const withPauses = workout.pauses !== '0:00:00' && workout.pauses !== null
|
||||
return (
|
||||
<div className="workout-details">
|
||||
<p>
|
||||
<i className="fa fa-clock-o custom-fa" aria-hidden="true" />
|
||||
{t('workouts:Duration')}: {workout.moving}
|
||||
{workout.records &&
|
||||
workout.records.find(record => record.record_type === 'LD') && (
|
||||
<sup>
|
||||
<i className="fa fa-trophy custom-fa" aria-hidden="true" />
|
||||
</sup>
|
||||
)}
|
||||
{withPauses && (
|
||||
<span>
|
||||
<br />({t('workouts:pauses')}: {workout.pauses},{' '}
|
||||
{t('workouts:total duration')}: {workout.duration})
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<i className="fa fa-road custom-fa" aria-hidden="true" />
|
||||
{t('workouts:Distance')}: {workout.distance} km
|
||||
{workout.records &&
|
||||
workout.records.find(record => record.record_type === 'FD') && (
|
||||
<sup>
|
||||
<i className="fa fa-trophy custom-fa" aria-hidden="true" />
|
||||
</sup>
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<i className="fa fa-tachometer custom-fa" aria-hidden="true" />
|
||||
{t('workouts:Average speed')}: {workout.ave_speed} km/h
|
||||
{workout.records &&
|
||||
workout.records.find(record => record.record_type === 'AS') && (
|
||||
<sup>
|
||||
<i className="fa fa-trophy custom-fa" aria-hidden="true" />
|
||||
</sup>
|
||||
)}
|
||||
<br />
|
||||
{t('workouts:Max. speed')}: {workout.max_speed} km/h
|
||||
{workout.records &&
|
||||
workout.records.find(record => record.record_type === 'MS') && (
|
||||
<sup>
|
||||
<i className="fa fa-trophy custom-fa" aria-hidden="true" />
|
||||
</sup>
|
||||
)}
|
||||
</p>
|
||||
{workout.min_alt && workout.max_alt && (
|
||||
<p>
|
||||
<i className="fi-mountains custom-fa" />
|
||||
{t('workouts:Min. altitude')}: {workout.min_alt}m
|
||||
<br />
|
||||
{t('workouts:Max. altitude')}: {workout.max_alt}m
|
||||
</p>
|
||||
)}
|
||||
{workout.ascent && workout.descent && (
|
||||
<p>
|
||||
<i className="fa fa-location-arrow custom-fa" />
|
||||
{t('workouts:Ascent')}: {workout.ascent}m
|
||||
<br />
|
||||
{t('workouts:Descent')}: {workout.descent}m
|
||||
</p>
|
||||
)}
|
||||
<WorkoutWeather workout={workout} t={t} />
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
import React from 'react'
|
||||
import { MapContainer } from 'react-leaflet'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import Map from './Map'
|
||||
import { getSegmentGpx, getWorkoutGpx } from '../../../actions/workouts'
|
||||
import { getGeoJson } from '../../../utils/workouts'
|
||||
|
||||
class WorkoutMap extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
zoom: 13,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.dataType === 'workout') {
|
||||
this.props.loadWorkoutGpx(this.props.workout.id)
|
||||
} else {
|
||||
this.props.loadSegmentGpx(this.props.workout.id, this.props.segmentId)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
(this.props.dataType === 'workout' &&
|
||||
prevProps.workout.id !== this.props.workout.id) ||
|
||||
(this.props.dataType === 'workout' && prevProps.dataType === 'segment')
|
||||
) {
|
||||
this.props.loadWorkoutGpx(this.props.workout.id)
|
||||
}
|
||||
if (
|
||||
this.props.dataType === 'segment' &&
|
||||
prevProps.segmentId !== this.props.segmentId
|
||||
) {
|
||||
this.props.loadSegmentGpx(this.props.workout.id, this.props.segmentId)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.loadWorkoutGpx(null)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { coordinates, gpxContent, mapAttribution, workout } = this.props
|
||||
const { jsonData } = getGeoJson(gpxContent)
|
||||
const bounds = [
|
||||
[workout.bounds[0], workout.bounds[1]],
|
||||
[workout.bounds[2], workout.bounds[3]],
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
{jsonData && (
|
||||
<MapContainer
|
||||
zoom={this.state.zoom}
|
||||
bounds={bounds}
|
||||
boundsOptions={{ padding: [10, 10] }}
|
||||
>
|
||||
<Map
|
||||
bounds={bounds}
|
||||
coordinates={coordinates}
|
||||
jsonData={jsonData}
|
||||
mapAttribution={mapAttribution}
|
||||
/>
|
||||
</MapContainer>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
gpxContent: state.gpx,
|
||||
mapAttribution: state.application.config.map_attribution,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadWorkoutGpx: workoutId => {
|
||||
dispatch(getWorkoutGpx(workoutId))
|
||||
},
|
||||
loadSegmentGpx: (workoutId, segmentId) => {
|
||||
dispatch(getSegmentGpx(workoutId, segmentId))
|
||||
},
|
||||
})
|
||||
)(WorkoutMap)
|
@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function WorkoutNoMap(props) {
|
||||
const { t } = props
|
||||
return (
|
||||
<div className="workout-no-map text-center">{t('workouts:No Map')}</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function WorkoutNotes(props) {
|
||||
const { notes, t } = props
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<div className="card workout-card">
|
||||
<div className="card-body">
|
||||
Notes
|
||||
<div className="workout-notes">
|
||||
{notes ? notes : t('workouts:No notes')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export default function WorkoutSegments(props) {
|
||||
const { segments, t } = props
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<div className="card workout-card">
|
||||
<div className="card-body">
|
||||
{t('workouts:Segments')}
|
||||
<div className="workout-segments">
|
||||
<ul>
|
||||
{segments.map((segment, index) => (
|
||||
<li
|
||||
className="workout-segments-list"
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={`segment-${index}`}
|
||||
>
|
||||
<Link
|
||||
to={`/workouts/${segment.workout_id}/segment/${
|
||||
index + 1
|
||||
}`}
|
||||
>
|
||||
{t('workouts:segment')} {index + 1}
|
||||
</Link>{' '}
|
||||
({t('workouts:distance')}: {segment.distance} km,{' '}
|
||||
{t('workouts:duration')}: {segment.duration})
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function WorkoutWeather(props) {
|
||||
const { t, workout } = props
|
||||
return (
|
||||
<div className="container">
|
||||
{workout.weather_start && workout.weather_end && (
|
||||
<table className="table table-borderless weather-table text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>
|
||||
{t('workouts:Start')}
|
||||
<br />
|
||||
<img
|
||||
className="weather-img"
|
||||
src={`/img/weather/${workout.weather_start.icon}.png`}
|
||||
alt={`workout weather (${workout.weather_start.icon})`}
|
||||
title={workout.weather_start.summary}
|
||||
/>
|
||||
</th>
|
||||
<th>
|
||||
{t('workouts:End')}
|
||||
<br />
|
||||
<img
|
||||
className="weather-img"
|
||||
src={`/img/weather/${workout.weather_end.icon}.png`}
|
||||
alt={`workout weather (${workout.weather_end.icon})`}
|
||||
title={workout.weather_end.summary}
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
className="weather-img-small"
|
||||
src="/img/weather/temperature.png"
|
||||
alt="Temperatures"
|
||||
/>
|
||||
</td>
|
||||
<td>{Number(workout.weather_start.temperature).toFixed(1)}°C</td>
|
||||
<td>{Number(workout.weather_end.temperature).toFixed(1)}°C</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
className="weather-img-small"
|
||||
src="/img/weather/pour-rain.png"
|
||||
alt="Temperatures"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
{Number(workout.weather_start.humidity * 100).toFixed(1)}%
|
||||
</td>
|
||||
<td>{Number(workout.weather_end.humidity * 100).toFixed(1)}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<img
|
||||
className="weather-img-small"
|
||||
src="/img/weather/breeze.png"
|
||||
alt="Temperatures"
|
||||
/>
|
||||
</td>
|
||||
<td>{Number(workout.weather_start.wind).toFixed(1)}m/s</td>
|
||||
<td>{Number(workout.weather_end.wind).toFixed(1)}m/s</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,202 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { withTranslation } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import CustomModal from '../../Common/CustomModal'
|
||||
import Message from '../../Common/Message'
|
||||
import WorkoutCardHeader from './WorkoutCardHeader'
|
||||
import WorkoutCharts from './WorkoutCharts'
|
||||
import WorkoutDetails from './WorkoutDetails'
|
||||
import WorkoutMap from './WorkoutMap'
|
||||
import WorkoutNoMap from './WorkoutNoMap'
|
||||
import WorkoutNotes from './WorkoutNotes'
|
||||
import WorkoutSegments from './WorkoutSegments'
|
||||
import { getOrUpdateData } from '../../../actions'
|
||||
import { deleteWorkout } from '../../../actions/workouts'
|
||||
|
||||
class WorkoutDisplay extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
displayModal: false,
|
||||
coordinates: {
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadWorkout(this.props.match.params.workoutId)
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.match.params.workoutId !== this.props.match.params.workoutId
|
||||
) {
|
||||
this.props.loadWorkout(this.props.match.params.workoutId)
|
||||
}
|
||||
}
|
||||
|
||||
displayModal(value) {
|
||||
this.setState(prevState => ({
|
||||
...prevState,
|
||||
displayModal: value,
|
||||
}))
|
||||
}
|
||||
|
||||
updateCoordinates(activePayload) {
|
||||
const coordinates =
|
||||
activePayload && activePayload.length > 0
|
||||
? {
|
||||
latitude: activePayload[0].payload.latitude,
|
||||
longitude: activePayload[0].payload.longitude,
|
||||
}
|
||||
: {
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
}
|
||||
this.setState(prevState => ({
|
||||
...prevState,
|
||||
coordinates,
|
||||
}))
|
||||
}
|
||||
|
||||
render() {
|
||||
const { message, onDeleteWorkout, sports, t, user, workouts } = this.props
|
||||
const { coordinates, displayModal } = this.state
|
||||
const [workout] = workouts
|
||||
const title = workout ? workout.title : t('workouts:Workout')
|
||||
const [sport] = workout ? sports.filter(s => s.id === workout.sport_id) : []
|
||||
const segmentId = parseInt(this.props.match.params.segmentId)
|
||||
const dataType = segmentId >= 0 ? 'segment' : 'workout'
|
||||
return (
|
||||
<div className="workout-page">
|
||||
<Helmet>
|
||||
<title>FitTrackee - {title}</title>
|
||||
</Helmet>
|
||||
{message ? (
|
||||
<Message message={message} t={t} />
|
||||
) : (
|
||||
<div className="container">
|
||||
{displayModal && (
|
||||
<CustomModal
|
||||
title={t('common:Confirmation')}
|
||||
text={t(
|
||||
'workouts:Are you sure you want to delete this workout?'
|
||||
)}
|
||||
confirm={() => {
|
||||
onDeleteWorkout(workout.id)
|
||||
this.displayModal(false)
|
||||
}}
|
||||
close={() => this.displayModal(false)}
|
||||
/>
|
||||
)}
|
||||
{workout && sport && workouts.length === 1 && (
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<div className="card workout-card">
|
||||
<div className="card-header">
|
||||
<WorkoutCardHeader
|
||||
workout={workout}
|
||||
dataType={dataType}
|
||||
segmentId={segmentId}
|
||||
sport={sport}
|
||||
t={t}
|
||||
title={title}
|
||||
user={user}
|
||||
displayModal={() => this.displayModal(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
<div className="col-md-8">
|
||||
{workout.with_gpx ? (
|
||||
<WorkoutMap
|
||||
workout={workout}
|
||||
coordinates={coordinates}
|
||||
dataType={dataType}
|
||||
segmentId={segmentId}
|
||||
/>
|
||||
) : (
|
||||
<WorkoutNoMap t={t} />
|
||||
)}
|
||||
</div>
|
||||
<div className="col">
|
||||
<WorkoutDetails
|
||||
workout={
|
||||
dataType === 'workout'
|
||||
? workout
|
||||
: workout.segments[segmentId - 1]
|
||||
}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{workout.with_gpx && (
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<div className="card workout-card">
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<div className="chart-title">
|
||||
{t('workouts:Chart')}
|
||||
</div>
|
||||
<WorkoutCharts
|
||||
workout={workout}
|
||||
dataType={dataType}
|
||||
segmentId={segmentId}
|
||||
t={t}
|
||||
updateCoordinates={e =>
|
||||
this.updateCoordinates(e)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{dataType === 'workout' && (
|
||||
<>
|
||||
<WorkoutNotes notes={workout.notes} t={t} />
|
||||
{workout.segments.length > 1 && (
|
||||
<WorkoutSegments segments={workout.segments} t={t} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation()(
|
||||
connect(
|
||||
state => ({
|
||||
workouts: state.workouts.data,
|
||||
message: state.message,
|
||||
sports: state.sports.data,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadWorkout: workoutId => {
|
||||
dispatch(getOrUpdateData('getData', 'workouts', { id: workoutId }))
|
||||
},
|
||||
onDeleteWorkout: workoutId => {
|
||||
dispatch(deleteWorkout(workoutId))
|
||||
},
|
||||
})
|
||||
)(WorkoutDisplay)
|
||||
)
|
41
fittrackee_client/src/components/Workout/WorkoutEdit.jsx
Normal file
41
fittrackee_client/src/components/Workout/WorkoutEdit.jsx
Normal file
@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import WorkoutAddOrEdit from './WorkoutAddOrEdit'
|
||||
import { getOrUpdateData } from '../../actions'
|
||||
|
||||
class WorkoutEdit extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.loadWorkout(this.props.match.params.workoutId)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { message, sports, workouts } = this.props
|
||||
const [workout] = workouts
|
||||
return (
|
||||
<div>
|
||||
{sports.length > 0 && (
|
||||
<WorkoutAddOrEdit
|
||||
workout={workout}
|
||||
message={message}
|
||||
sports={sports}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
workouts: state.workouts.data,
|
||||
message: state.message,
|
||||
sports: state.sports.data,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadWorkout: workoutId => {
|
||||
dispatch(getOrUpdateData('getData', 'workouts', { id: workoutId }))
|
||||
},
|
||||
})
|
||||
)(WorkoutEdit)
|
@ -0,0 +1,170 @@
|
||||
import React from 'react'
|
||||
import { Trans } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { setLoading } from '../../../actions/index'
|
||||
import { addWorkout, editWorkout } from '../../../actions/workouts'
|
||||
import { history } from '../../../index'
|
||||
import { getFileSize } from '../../../utils'
|
||||
import { translateSports } from '../../../utils/workouts'
|
||||
|
||||
function FormWithGpx(props) {
|
||||
const {
|
||||
appConfig,
|
||||
loading,
|
||||
onAddWorkout,
|
||||
onEditWorkout,
|
||||
sports,
|
||||
t,
|
||||
workout,
|
||||
} = props
|
||||
const sportId = workout ? workout.sport_id : ''
|
||||
const translatedSports = translateSports(sports, t, true)
|
||||
const zipTooltip = `${t('workouts:no folder inside')}, ${
|
||||
appConfig.gpx_limit_import
|
||||
} ${t('workouts:files max')}, ${t('workouts:max size')}: ${getFileSize(
|
||||
appConfig.max_zip_file_size
|
||||
)}`
|
||||
const fileSizeLimit = getFileSize(appConfig.max_single_file_size)
|
||||
return (
|
||||
<form
|
||||
encType="multipart/form-data"
|
||||
method="post"
|
||||
onSubmit={event => event.preventDefault()}
|
||||
>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('common:Sport')}:
|
||||
<select
|
||||
className="form-control input-lg"
|
||||
defaultValue={sportId}
|
||||
disabled={loading}
|
||||
name="sport"
|
||||
required
|
||||
>
|
||||
<option value="" />
|
||||
{translatedSports.map(sport => (
|
||||
<option key={sport.id} value={sport.id}>
|
||||
{sport.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
{workout ? (
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('workouts:Title')}:
|
||||
<input
|
||||
name="title"
|
||||
defaultValue={workout ? workout.title : ''}
|
||||
disabled={loading}
|
||||
className="form-control input-lg"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
) : (
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<Trans i18nKey="workouts:gpxFile">
|
||||
<strong>gpx</strong> file
|
||||
</Trans>
|
||||
<sup>
|
||||
<i
|
||||
className="fa fa-question-circle"
|
||||
aria-hidden="true"
|
||||
data-toggle="tooltip"
|
||||
title={`${t('workouts:max size')}: ${fileSizeLimit}`}
|
||||
/>
|
||||
</sup>{' '}
|
||||
<Trans i18nKey="workouts:zipFile">
|
||||
or <strong> zip</strong> file containing <strong>gpx </strong>
|
||||
files
|
||||
</Trans>
|
||||
<sup>
|
||||
<i
|
||||
className="fa fa-question-circle"
|
||||
aria-hidden="true"
|
||||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
title={zipTooltip}
|
||||
/>
|
||||
</sup>{' '}
|
||||
:
|
||||
<input
|
||||
accept=".gpx, .zip"
|
||||
className="form-control form-control-file gpx-file"
|
||||
disabled={loading}
|
||||
name="gpxFile"
|
||||
required
|
||||
type="file"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('workouts:Notes')}:
|
||||
<textarea
|
||||
name="notes"
|
||||
defaultValue={workout ? workout.notes : ''}
|
||||
disabled={loading}
|
||||
className="form-control input-lg"
|
||||
maxLength="500"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="loader" />
|
||||
) : (
|
||||
<div>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
onClick={event =>
|
||||
workout ? onEditWorkout(event, workout) : onAddWorkout(event)
|
||||
}
|
||||
value={t('common:Submit')}
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => history.push('/')}
|
||||
value={t('common:Cancel')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
appConfig: state.application.config,
|
||||
loading: state.loading,
|
||||
}),
|
||||
dispatch => ({
|
||||
onAddWorkout: e => {
|
||||
dispatch(setLoading(true))
|
||||
const form = new FormData()
|
||||
form.append('file', e.target.form.gpxFile.files[0])
|
||||
/* prettier-ignore */
|
||||
form.append(
|
||||
'data',
|
||||
`{"sport_id": ${e.target.form.sport.value
|
||||
}, "notes": "${e.target.form.notes.value}"}`
|
||||
)
|
||||
dispatch(addWorkout(form))
|
||||
},
|
||||
onEditWorkout: (e, workout) => {
|
||||
dispatch(
|
||||
editWorkout({
|
||||
id: workout.id,
|
||||
notes: e.target.form.notes.value,
|
||||
sport_id: +e.target.form.sport.value,
|
||||
title: e.target.form.title.value,
|
||||
})
|
||||
)
|
||||
},
|
||||
})
|
||||
)(FormWithGpx)
|
@ -0,0 +1,162 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { addWorkoutWithoutGpx, editWorkout } from '../../../actions/workouts'
|
||||
import { history } from '../../../index'
|
||||
import { getDateWithTZ } from '../../../utils'
|
||||
import { formatWorkoutDate, translateSports } from '../../../utils/workouts'
|
||||
|
||||
function FormWithoutGpx(props) {
|
||||
const { onAddOrEdit, sports, t, user, workout } = props
|
||||
const translatedSports = translateSports(sports, t, true)
|
||||
let workoutDate,
|
||||
workoutTime,
|
||||
sportId = ''
|
||||
if (workout) {
|
||||
const workoutDateTime = formatWorkoutDate(
|
||||
getDateWithTZ(workout.workout_date, user.timezone),
|
||||
'yyyy-MM-dd'
|
||||
)
|
||||
workoutDate = workoutDateTime.workout_date
|
||||
workoutTime = workoutDateTime.workout_time
|
||||
sportId = workout.sport_id
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={event => event.preventDefault()}>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('workouts:Title')}:
|
||||
<input
|
||||
name="title"
|
||||
defaultValue={workout ? workout.title : ''}
|
||||
className="form-control input-lg"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('common:Sport')}:
|
||||
<select
|
||||
className="form-control input-lg"
|
||||
defaultValue={sportId}
|
||||
name="sport_id"
|
||||
required
|
||||
>
|
||||
<option value="" />
|
||||
{translatedSports.map(sport => (
|
||||
<option key={sport.id} value={sport.id}>
|
||||
{sport.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('workouts:Workout Date')}:
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<input
|
||||
name="workout_date"
|
||||
defaultValue={workoutDate}
|
||||
className="form-control col-md"
|
||||
required
|
||||
type="date"
|
||||
/>
|
||||
<input
|
||||
name="workout_time"
|
||||
defaultValue={workoutTime}
|
||||
className="form-control col-md"
|
||||
required
|
||||
type="time"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('workouts:Duration')}:
|
||||
<input
|
||||
name="duration"
|
||||
defaultValue={workout ? workout.duration : ''}
|
||||
className="form-control col-xs-4"
|
||||
pattern="^([0-9]*[0-9]):([0-5][0-9]):([0-5][0-9])$"
|
||||
placeholder="hh:mm:ss"
|
||||
required
|
||||
type="text"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('workouts:Distance')} (km):
|
||||
<input
|
||||
name="distance"
|
||||
defaultValue={workout ? workout.distance : ''}
|
||||
className="form-control input-lg"
|
||||
min={0}
|
||||
required
|
||||
step="0.001"
|
||||
type="number"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
{t('workouts:Notes')}:
|
||||
<textarea
|
||||
name="notes"
|
||||
defaultValue={workout ? workout.notes : ''}
|
||||
className="form-control input-lg"
|
||||
maxLength="500"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary"
|
||||
onClick={event => onAddOrEdit(event, workout)}
|
||||
value={t('common:Submit')}
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => history.push('/')}
|
||||
value={t('common:Cancel')}
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
onAddOrEdit: (e, workout) => {
|
||||
const d = e.target.form.duration.value.split(':')
|
||||
const duration = +d[0] * 60 * 60 + +d[1] * 60 + +d[2]
|
||||
|
||||
/* prettier-ignore */
|
||||
const workoutDate = `${e.target.form.workout_date.value
|
||||
} ${ e.target.form.workout_time.value}`
|
||||
|
||||
const data = {
|
||||
workout_date: workoutDate,
|
||||
distance: +e.target.form.distance.value,
|
||||
duration,
|
||||
notes: e.target.form.notes.value,
|
||||
sport_id: +e.target.form.sport_id.value,
|
||||
title: e.target.form.title.value,
|
||||
}
|
||||
if (workout) {
|
||||
data.id = workout.id
|
||||
dispatch(editWorkout(data))
|
||||
} else {
|
||||
dispatch(addWorkoutWithoutGpx(data))
|
||||
}
|
||||
},
|
||||
})
|
||||
)(FormWithoutGpx)
|
38
fittrackee_client/src/components/Workout/index.jsx
Normal file
38
fittrackee_client/src/components/Workout/index.jsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Redirect, Route, Switch } from 'react-router-dom'
|
||||
|
||||
import NotFound from './../Others/NotFound'
|
||||
import WorkoutAdd from './WorkoutAdd'
|
||||
import WorkoutDisplay from './WorkoutDisplay'
|
||||
import WorkoutEdit from './WorkoutEdit'
|
||||
import { isLoggedIn } from '../../utils'
|
||||
|
||||
function Workout() {
|
||||
return (
|
||||
<div>
|
||||
{isLoggedIn() ? (
|
||||
<Switch>
|
||||
<Route exact path="/workouts/add" component={WorkoutAdd} />
|
||||
<Route exact path="/workouts/:workoutId" component={WorkoutDisplay} />
|
||||
<Route
|
||||
exact
|
||||
path="/workouts/:workoutId/edit"
|
||||
component={WorkoutEdit}
|
||||
/>
|
||||
<Route
|
||||
path="/workouts/:workoutId/segment/:segmentId"
|
||||
component={WorkoutDisplay}
|
||||
/>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
) : (
|
||||
<Redirect to="/login" />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(state => ({
|
||||
user: state.user,
|
||||
}))(Workout)
|
Reference in New Issue
Block a user