Client - rename existing front

This commit is contained in:
Sam
2021-09-01 20:08:06 +02:00
parent 6ba3f6d54e
commit 6d1de3c3bb
134 changed files with 0 additions and 0 deletions

View File

@ -1,19 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import WorkoutAddOrEdit from './WorkoutAddOrEdit'
function WorkoutAdd(props) {
const { message, sports } = props
return (
<div>
<WorkoutAddOrEdit workout={null} message={message} sports={sports} />
</div>
)
}
export default connect(state => ({
message: state.message,
sports: state.sports.data,
user: state.user,
}))(WorkoutAdd)

View File

@ -1,118 +0,0 @@
import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import FormWithGpx from './WorkoutForms/FormWithGpx'
import FormWithoutGpx from './WorkoutForms/FormWithoutGpx'
import Message from '../Common/Message'
class WorkoutAddEdit extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
withGpx: true,
}
}
handleRadioChange(changeEvent) {
this.setState({
withGpx:
changeEvent.target.name === 'withGpx'
? changeEvent.target.value
: !changeEvent.target.value,
})
}
render() {
const { loading, message, sports, t, workout } = this.props
const { withGpx } = this.state
return (
<div>
<Helmet>
<title>
FitTrackee -{' '}
{workout
? t('workouts:Edit a workout')
: t('workouts:Add a workout')}
</title>
</Helmet>
<br />
<br />
<Message message={message} t={t} />
<div className="container">
<div className="row">
<div className="col-md-2" />
<div className="col-md-8">
<div className="card add-workout">
<h2 className="card-header text-center">
{workout
? t('workouts:Edit a workout')
: t('workouts:Add a workout')}
</h2>
<div className="card-body">
{workout ? (
workout.with_gpx ? (
<FormWithGpx workout={workout} sports={sports} t={t} />
) : (
<FormWithoutGpx workout={workout} sports={sports} t={t} />
)
) : (
<div>
<form>
<div className="form-group row">
<div className="col">
<label className="radioLabel">
<input
className="add-workout-radio"
type="radio"
name="withGpx"
disabled={loading}
checked={withGpx}
onChange={event =>
this.handleRadioChange(event)
}
/>
{t('workouts:with gpx file')}
</label>
</div>
<div className="col">
<label className="radioLabel">
<input
className="add-workout-radio"
type="radio"
name="withoutGpx"
disabled={loading}
checked={!withGpx}
onChange={event =>
this.handleRadioChange(event)
}
/>
{t('workouts:without gpx file')}
</label>
</div>
</div>
</form>
{withGpx ? (
<FormWithGpx sports={sports} t={t} />
) : (
<FormWithoutGpx sports={sports} t={t} />
)}
</div>
)}
</div>
</div>
</div>
<div className="col-md-2" />
</div>
</div>
</div>
)
}
}
export default withTranslation()(
connect(state => ({
loading: state.loading,
}))(WorkoutAddEdit)
)

View File

@ -1,27 +0,0 @@
import React from 'react'
import { GeoJSON, Marker, TileLayer, useMap } from 'react-leaflet'
import hash from 'object-hash'
import { apiUrl } from '../../../utils'
export default function Map({ bounds, coordinates, jsonData, mapAttribution }) {
const map = useMap()
map.fitBounds(bounds)
return (
<>
<TileLayer
// eslint-disable-next-line max-len
attribution={mapAttribution}
url={`${apiUrl}workouts/map_tile/{s}/{z}/{x}/{y}.png`}
/>
<GeoJSON
// hash as a key to force re-rendering
key={hash(jsonData)}
data={jsonData}
/>
{coordinates.latitude && (
<Marker position={[coordinates.latitude, coordinates.longitude]} />
)}
</>
)
}

View File

@ -1,106 +0,0 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { getDateWithTZ } from '../../../utils'
import { formatWorkoutDate } from '../../../utils/workouts'
export default function WorkoutCardHeader(props) {
const { dataType, displayModal, segmentId, sport, t, title, user, workout } =
props
const workoutDate = workout
? formatWorkoutDate(getDateWithTZ(workout.workout_date, user.timezone))
: null
const previousUrl =
dataType === 'segment' && segmentId !== 1
? `/workouts/${workout.id}/segment/${segmentId - 1}`
: dataType === 'workout' && workout.previous_workout
? `/workouts/${workout.previous_workout}`
: null
const nextUrl =
dataType === 'segment' && segmentId < workout.segments.length
? `/workouts/${workout.id}/segment/${segmentId + 1}`
: dataType === 'workout' && workout.next_workout
? `/workouts/${workout.next_workout}`
: null
return (
<div className="container">
<div className="row">
<div className="col-auto">
{previousUrl ? (
<Link className="unlink" to={previousUrl}>
<i
className="fa fa-chevron-left"
aria-hidden="true"
title={t(`workouts:See previous ${dataType}`)}
/>
</Link>
) : (
<i
className="fa fa-chevron-left inactive-link"
aria-hidden="true"
title={t(`workouts:No previous ${dataType}`)}
/>
)}
</div>
<div className="col-auto col-workout-logo">
<img className="sport-img-medium" src={sport.img} alt="sport logo" />
</div>
<div className="col">
{dataType === 'workout' ? (
<>
{title}{' '}
<Link className="unlink" to={`/workouts/${workout.id}/edit`}>
<i
className="fa fa-edit custom-fa"
aria-hidden="true"
title={t('workouts:Edit workout')}
/>
</Link>
<i
className="fa fa-trash custom-fa"
aria-hidden="true"
onClick={() => displayModal(true)}
title={t('workouts:Delete workout')}
/>
</>
) : (
<>
{/* prettier-ignore */}
<Link
to={`/workouts/${workout.id}`}
>
{title}
</Link>{' '}
- {t('workouts:segment')} {segmentId}
</>
)}
<br />
{workoutDate && (
<span className="workout-date">
{`${workoutDate.workout_date} - ${workoutDate.workout_time}`}
</span>
)}
</div>
<div className="col-auto">
{nextUrl ? (
<Link className="unlink" to={nextUrl}>
<i
className="fa fa-chevron-right"
aria-hidden="true"
title={t(`workouts:See next ${dataType}`)}
/>
</Link>
) : (
<i
className="fa fa-chevron-right inactive-link"
aria-hidden="true"
title={t(`workouts:No next ${dataType}`)}
/>
)}
</div>
</div>
</div>
)
}

View File

@ -1,240 +0,0 @@
import { format } from 'date-fns'
import React from 'react'
import { connect } from 'react-redux'
import {
Area,
ComposedChart,
Line,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts'
import {
getSegmentChartData,
getWorkoutChartData,
} from '../../../actions/workouts'
class WorkoutCharts extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
displayDistance: true,
dataToHide: [],
}
}
componentDidMount() {
if (this.props.dataType === 'workout') {
this.props.loadWorkoutData(this.props.workout.id)
} else {
this.props.loadSegmentData(this.props.workout.id, this.props.segmentId)
}
}
componentDidUpdate(prevProps) {
if (
(this.props.dataType === 'workout' &&
prevProps.workout.id !== this.props.workout.id) ||
(this.props.dataType === 'workout' && prevProps.dataType === 'segment')
) {
this.props.loadWorkoutData(this.props.workout.id)
}
if (
this.props.dataType === 'segment' &&
prevProps.segmentId !== this.props.segmentId
) {
this.props.loadSegmentData(this.props.workout.id, this.props.segmentId)
}
}
componentWillUnmount() {
this.props.loadWorkoutData(null)
}
handleRadioChange(changeEvent) {
this.setState({
displayDistance:
changeEvent.target.name === 'distance'
? changeEvent.target.value
: !changeEvent.target.value,
})
}
handleLegendChange(e) {
const { dataToHide } = this.state
const name = e.target.name // eslint-disable-line prefer-destructuring
if (dataToHide.find(d => d === name)) {
dataToHide.splice(dataToHide.indexOf(name), 1)
} else {
dataToHide.push(name)
}
this.setState({ dataToHide })
}
displayData(name) {
const { dataToHide } = this.state
return !dataToHide.find(d => d === name)
}
render() {
const { chartData, t, updateCoordinates } = this.props
const { displayDistance } = this.state
const xInterval = chartData ? parseInt(chartData.length / 10, 10) : 0
let xDataKey, xScale
if (displayDistance) {
xDataKey = 'distance'
xScale = 'linear'
} else {
xDataKey = 'duration'
xScale = 'time'
}
return (
<div className="container">
{chartData && chartData.length > 0 ? (
<div>
<div className="row chart-radio">
<label className="radioLabel col-md-1">
<input
type="radio"
name="distance"
checked={displayDistance}
onChange={e => this.handleRadioChange(e)}
/>
{t('workouts:distance')}
</label>
<label className="radioLabel col-md-1">
<input
type="radio"
name="duration"
checked={!displayDistance}
onChange={e => this.handleRadioChange(e)}
/>
{t('workouts:duration')}
</label>
</div>
<div className="row chart-radio">
<div className="col-md-5" />
<label className="radioLabel col-md-1">
<input
type="checkbox"
name="speed"
checked={this.displayData('speed')}
onChange={e => this.handleLegendChange(e)}
/>
{t('workouts:speed')}
</label>
<label className="radioLabel col-md-1">
<input
type="checkbox"
name="elevation"
checked={this.displayData('elevation')}
onChange={e => this.handleLegendChange(e)}
/>
{t('workouts:elevation')}
</label>
<div className="col-md-5" />
</div>
<div className="row chart">
<ResponsiveContainer height={300}>
<ComposedChart
data={chartData}
margin={{ top: 15, right: 30, left: 20, bottom: 15 }}
onMouseMove={e => updateCoordinates(e.activePayload)}
onMouseLeave={() => updateCoordinates(null)}
>
<XAxis
allowDecimals={false}
dataKey={xDataKey}
label={{
value: t(`workouts:${xDataKey}`),
offset: 0,
position: 'bottom',
}}
scale={xScale}
interval={xInterval}
tickFormatter={value =>
displayDistance ? value : format(value, 'HH:mm:ss')
}
type="number"
/>
<YAxis
label={{
value: `${t('workouts:speed')} (km/h)`,
angle: -90,
position: 'left',
}}
yAxisId="left"
/>
<YAxis
label={{
value: `${t('workouts:elevation')} (m)`,
angle: -90,
position: 'right',
}}
yAxisId="right"
orientation="right"
/>
{this.displayData('elevation') && (
<Area
yAxisId="right"
type="linear"
dataKey="elevation"
name={t('workouts:elevation')}
fill="#e5e5e5"
stroke="#cccccc"
dot={false}
unit=" m"
/>
)}
{this.displayData('speed') && (
<Line
yAxisId="left"
type="linear"
dataKey="speed"
name={t('workouts:speed')}
stroke="#8884d8"
strokeWidth={2}
dot={false}
unit=" km/h"
/>
)}
<Tooltip
labelFormatter={value =>
displayDistance
? `${t('workouts:distance')}: ${value} km`
: `${t('workouts:duration')}: ${format(
value,
'HH:mm:ss'
)}`
}
/>
</ComposedChart>
</ResponsiveContainer>
</div>
<div className="chart-info">
{t('workouts:data from gpx, without any cleaning')}
</div>
</div>
) : (
t('workouts:No data to display')
)}
</div>
)
}
}
export default connect(
state => ({
chartData: state.chartData,
}),
dispatch => ({
loadWorkoutData: workoutId => {
dispatch(getWorkoutChartData(workoutId))
},
loadSegmentData: (workoutId, segmentId) => {
dispatch(getSegmentChartData(workoutId, segmentId))
},
})
)(WorkoutCharts)

View File

@ -1,73 +0,0 @@
import React from 'react'
import WorkoutWeather from './WorkoutWeather'
export default function WorkoutDetails(props) {
const { t, workout } = props
const withPauses = workout.pauses !== '0:00:00' && workout.pauses !== null
return (
<div className="workout-details">
<p>
<i className="fa fa-clock-o custom-fa" aria-hidden="true" />
{t('workouts:Duration')}: {workout.moving}
{workout.records &&
workout.records.find(record => record.record_type === 'LD') && (
<sup>
<i className="fa fa-trophy custom-fa" aria-hidden="true" />
</sup>
)}
{withPauses && (
<span>
<br />({t('workouts:pauses')}: {workout.pauses},{' '}
{t('workouts:total duration')}: {workout.duration})
</span>
)}
</p>
<p>
<i className="fa fa-road custom-fa" aria-hidden="true" />
{t('workouts:Distance')}: {workout.distance} km
{workout.records &&
workout.records.find(record => record.record_type === 'FD') && (
<sup>
<i className="fa fa-trophy custom-fa" aria-hidden="true" />
</sup>
)}
</p>
<p>
<i className="fa fa-tachometer custom-fa" aria-hidden="true" />
{t('workouts:Average speed')}: {workout.ave_speed} km/h
{workout.records &&
workout.records.find(record => record.record_type === 'AS') && (
<sup>
<i className="fa fa-trophy custom-fa" aria-hidden="true" />
</sup>
)}
<br />
{t('workouts:Max. speed')}: {workout.max_speed} km/h
{workout.records &&
workout.records.find(record => record.record_type === 'MS') && (
<sup>
<i className="fa fa-trophy custom-fa" aria-hidden="true" />
</sup>
)}
</p>
{workout.min_alt && workout.max_alt && (
<p>
<i className="fi-mountains custom-fa" />
{t('workouts:Min. altitude')}: {workout.min_alt}m
<br />
{t('workouts:Max. altitude')}: {workout.max_alt}m
</p>
)}
{workout.ascent && workout.descent && (
<p>
<i className="fa fa-location-arrow custom-fa" />
{t('workouts:Ascent')}: {workout.ascent}m
<br />
{t('workouts:Descent')}: {workout.descent}m
</p>
)}
<WorkoutWeather workout={workout} t={t} />
</div>
)
}

View File

@ -1,87 +0,0 @@
import React from 'react'
import { MapContainer } from 'react-leaflet'
import { connect } from 'react-redux'
import Map from './Map'
import { getSegmentGpx, getWorkoutGpx } from '../../../actions/workouts'
import { getGeoJson } from '../../../utils/workouts'
class WorkoutMap extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
zoom: 13,
}
}
componentDidMount() {
if (this.props.dataType === 'workout') {
this.props.loadWorkoutGpx(this.props.workout.id)
} else {
this.props.loadSegmentGpx(this.props.workout.id, this.props.segmentId)
}
}
componentDidUpdate(prevProps) {
if (
(this.props.dataType === 'workout' &&
prevProps.workout.id !== this.props.workout.id) ||
(this.props.dataType === 'workout' && prevProps.dataType === 'segment')
) {
this.props.loadWorkoutGpx(this.props.workout.id)
}
if (
this.props.dataType === 'segment' &&
prevProps.segmentId !== this.props.segmentId
) {
this.props.loadSegmentGpx(this.props.workout.id, this.props.segmentId)
}
}
componentWillUnmount() {
this.props.loadWorkoutGpx(null)
}
render() {
const { coordinates, gpxContent, mapAttribution, workout } = this.props
const { jsonData } = getGeoJson(gpxContent)
const bounds = [
[workout.bounds[0], workout.bounds[1]],
[workout.bounds[2], workout.bounds[3]],
]
return (
<div>
{jsonData && (
<MapContainer
zoom={this.state.zoom}
bounds={bounds}
boundsOptions={{ padding: [10, 10] }}
>
<Map
bounds={bounds}
coordinates={coordinates}
jsonData={jsonData}
mapAttribution={mapAttribution}
/>
</MapContainer>
)}
</div>
)
}
}
export default connect(
state => ({
gpxContent: state.gpx,
mapAttribution: state.application.config.map_attribution,
}),
dispatch => ({
loadWorkoutGpx: workoutId => {
dispatch(getWorkoutGpx(workoutId))
},
loadSegmentGpx: (workoutId, segmentId) => {
dispatch(getSegmentGpx(workoutId, segmentId))
},
})
)(WorkoutMap)

View File

@ -1,8 +0,0 @@
import React from 'react'
export default function WorkoutNoMap(props) {
const { t } = props
return (
<div className="workout-no-map text-center">{t('workouts:No Map')}</div>
)
}

View File

@ -1,19 +0,0 @@
import React from 'react'
export default function WorkoutNotes(props) {
const { notes, t } = props
return (
<div className="row">
<div className="col">
<div className="card workout-card">
<div className="card-body">
Notes
<div className="workout-notes">
{notes && notes !== '' ? notes : t('workouts:No notes')}
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -1,38 +0,0 @@
import React from 'react'
import { Link } from 'react-router-dom'
export default function WorkoutSegments(props) {
const { segments, t } = props
return (
<div className="row">
<div className="col">
<div className="card workout-card">
<div className="card-body">
{t('workouts:Segments')}
<div className="workout-segments">
<ul>
{segments.map((segment, index) => (
<li
className="workout-segments-list"
// eslint-disable-next-line react/no-array-index-key
key={`segment-${index}`}
>
<Link
to={`/workouts/${segment.workout_id}/segment/${
index + 1
}`}
>
{t('workouts:segment')} {index + 1}
</Link>{' '}
({t('workouts:distance')}: {segment.distance} km,{' '}
{t('workouts:duration')}: {segment.duration})
</li>
))}
</ul>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -1,75 +0,0 @@
import React from 'react'
export default function WorkoutWeather(props) {
const { t, workout } = props
return (
<div className="container">
{workout.weather_start && workout.weather_end && (
<table className="table table-borderless weather-table text-center">
<thead>
<tr>
<th />
<th>
{t('workouts:Start')}
<br />
<img
className="weather-img"
src={`/img/weather/${workout.weather_start.icon}.png`}
alt={`workout weather (${workout.weather_start.icon})`}
title={workout.weather_start.summary}
/>
</th>
<th>
{t('workouts:End')}
<br />
<img
className="weather-img"
src={`/img/weather/${workout.weather_end.icon}.png`}
alt={`workout weather (${workout.weather_end.icon})`}
title={workout.weather_end.summary}
/>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<img
className="weather-img-small"
src="/img/weather/temperature.png"
alt="Temperatures"
/>
</td>
<td>{Number(workout.weather_start.temperature).toFixed(1)}°C</td>
<td>{Number(workout.weather_end.temperature).toFixed(1)}°C</td>
</tr>
<tr>
<td>
<img
className="weather-img-small"
src="/img/weather/pour-rain.png"
alt="Temperatures"
/>
</td>
<td>
{Number(workout.weather_start.humidity * 100).toFixed(1)}%
</td>
<td>{Number(workout.weather_end.humidity * 100).toFixed(1)}%</td>
</tr>
<tr>
<td>
<img
className="weather-img-small"
src="/img/weather/breeze.png"
alt="Temperatures"
/>
</td>
<td>{Number(workout.weather_start.wind).toFixed(1)}m/s</td>
<td>{Number(workout.weather_end.wind).toFixed(1)}m/s</td>
</tr>
</tbody>
</table>
)}
</div>
)
}

View File

@ -1,202 +0,0 @@
import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import CustomModal from '../../Common/CustomModal'
import Message from '../../Common/Message'
import WorkoutCardHeader from './WorkoutCardHeader'
import WorkoutCharts from './WorkoutCharts'
import WorkoutDetails from './WorkoutDetails'
import WorkoutMap from './WorkoutMap'
import WorkoutNoMap from './WorkoutNoMap'
import WorkoutNotes from './WorkoutNotes'
import WorkoutSegments from './WorkoutSegments'
import { getOrUpdateData } from '../../../actions'
import { deleteWorkout } from '../../../actions/workouts'
class WorkoutDisplay extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
displayModal: false,
coordinates: {
latitude: null,
longitude: null,
},
}
}
componentDidMount() {
this.props.loadWorkout(this.props.match.params.workoutId)
}
componentDidUpdate(prevProps) {
if (
prevProps.match.params.workoutId !== this.props.match.params.workoutId
) {
this.props.loadWorkout(this.props.match.params.workoutId)
}
}
displayModal(value) {
this.setState(prevState => ({
...prevState,
displayModal: value,
}))
}
updateCoordinates(activePayload) {
const coordinates =
activePayload && activePayload.length > 0
? {
latitude: activePayload[0].payload.latitude,
longitude: activePayload[0].payload.longitude,
}
: {
latitude: null,
longitude: null,
}
this.setState(prevState => ({
...prevState,
coordinates,
}))
}
render() {
const { message, onDeleteWorkout, sports, t, user, workouts } = this.props
const { coordinates, displayModal } = this.state
const [workout] = workouts
const title = workout ? workout.title : t('workouts:Workout')
const [sport] = workout ? sports.filter(s => s.id === workout.sport_id) : []
const segmentId = parseInt(this.props.match.params.segmentId)
const dataType = segmentId >= 0 ? 'segment' : 'workout'
return (
<div className="workout-page">
<Helmet>
<title>FitTrackee - {title}</title>
</Helmet>
{message ? (
<Message message={message} t={t} />
) : (
<div className="container">
{displayModal && (
<CustomModal
title={t('common:Confirmation')}
text={t(
'workouts:Are you sure you want to delete this workout?'
)}
confirm={() => {
onDeleteWorkout(workout.id)
this.displayModal(false)
}}
close={() => this.displayModal(false)}
/>
)}
{workout && sport && workouts.length === 1 && (
<div>
<div className="row">
<div className="col">
<div className="card workout-card">
<div className="card-header">
<WorkoutCardHeader
workout={workout}
dataType={dataType}
segmentId={segmentId}
sport={sport}
t={t}
title={title}
user={user}
displayModal={() => this.displayModal(true)}
/>
</div>
<div className="card-body">
<div className="row">
<div className="col-md-8">
{workout.with_gpx ? (
<WorkoutMap
workout={workout}
coordinates={coordinates}
dataType={dataType}
segmentId={segmentId}
/>
) : (
<WorkoutNoMap t={t} />
)}
</div>
<div className="col">
<WorkoutDetails
workout={
dataType === 'workout'
? workout
: workout.segments[segmentId - 1]
}
t={t}
/>
</div>
</div>
</div>
</div>
</div>
</div>
{workout.with_gpx && (
<div className="row">
<div className="col">
<div className="card workout-card">
<div className="card-body">
<div className="row">
<div className="col">
<div className="chart-title">
{t('workouts:Chart')}
</div>
<WorkoutCharts
workout={workout}
dataType={dataType}
segmentId={segmentId}
t={t}
updateCoordinates={e =>
this.updateCoordinates(e)
}
/>
</div>
</div>
</div>
</div>
</div>
</div>
)}
{dataType === 'workout' && (
<>
<WorkoutNotes notes={workout.notes} t={t} />
{workout.segments.length > 1 && (
<WorkoutSegments segments={workout.segments} t={t} />
)}
</>
)}
</div>
)}
</div>
)}
</div>
)
}
}
export default withTranslation()(
connect(
state => ({
workouts: state.workouts.data,
message: state.message,
sports: state.sports.data,
user: state.user,
}),
dispatch => ({
loadWorkout: workoutId => {
dispatch(getOrUpdateData('getData', 'workouts', { id: workoutId }))
},
onDeleteWorkout: workoutId => {
dispatch(deleteWorkout(workoutId))
},
})
)(WorkoutDisplay)
)

View File

@ -1,41 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import WorkoutAddOrEdit from './WorkoutAddOrEdit'
import { getOrUpdateData } from '../../actions'
class WorkoutEdit extends React.Component {
componentDidMount() {
this.props.loadWorkout(this.props.match.params.workoutId)
}
render() {
const { message, sports, workouts } = this.props
const [workout] = workouts
return (
<div>
{sports.length > 0 && (
<WorkoutAddOrEdit
workout={workout}
message={message}
sports={sports}
/>
)}
</div>
)
}
}
export default connect(
state => ({
workouts: state.workouts.data,
message: state.message,
sports: state.sports.data,
user: state.user,
}),
dispatch => ({
loadWorkout: workoutId => {
dispatch(getOrUpdateData('getData', 'workouts', { id: workoutId }))
},
})
)(WorkoutEdit)

View File

@ -1,170 +0,0 @@
import React from 'react'
import { Trans } from 'react-i18next'
import { connect } from 'react-redux'
import { setLoading } from '../../../actions/index'
import { addWorkout, editWorkout } from '../../../actions/workouts'
import { history } from '../../../index'
import { getFileSize } from '../../../utils'
import { translateSports } from '../../../utils/workouts'
import CustomTextArea from '../../Common/CustomTextArea'
function FormWithGpx(props) {
const {
appConfig,
loading,
onAddWorkout,
onEditWorkout,
sports,
t,
workout,
} = props
const sportId = workout ? workout.sport_id : ''
const translatedSports = translateSports(sports, t, true)
const zipTooltip = `${t('workouts:no folder inside')}, ${
appConfig.gpx_limit_import
} ${t('workouts:files max')}, ${t('workouts:max size')}: ${getFileSize(
appConfig.max_zip_file_size
)}`
const fileSizeLimit = getFileSize(appConfig.max_single_file_size)
return (
<form
encType="multipart/form-data"
method="post"
onSubmit={event => event.preventDefault()}
>
<div className="form-group">
<label>
{t('common:Sport')}:
<select
className="form-control input-lg"
defaultValue={sportId}
disabled={loading}
name="sport"
required
>
<option value="" />
{translatedSports.map(sport => (
<option key={sport.id} value={sport.id}>
{sport.label}
</option>
))}
</select>
</label>
</div>
{workout ? (
<div className="form-group">
<label>
{t('workouts:Title')}:
<input
name="title"
defaultValue={workout ? workout.title : ''}
disabled={loading}
className="form-control input-lg"
/>
</label>
</div>
) : (
<div className="form-group">
<label>
<Trans i18nKey="workouts:gpxFile">
<strong>gpx</strong> file
</Trans>
<sup>
<i
className="fa fa-question-circle"
aria-hidden="true"
data-toggle="tooltip"
title={`${t('workouts:max size')}: ${fileSizeLimit}`}
/>
</sup>{' '}
<Trans i18nKey="workouts:zipFile">
or <strong> zip</strong> file containing <strong>gpx </strong>
files
</Trans>
<sup>
<i
className="fa fa-question-circle"
aria-hidden="true"
data-toggle="tooltip"
data-placement="top"
title={zipTooltip}
/>
</sup>{' '}
:
<input
accept=".gpx, .zip"
className="form-control form-control-file gpx-file"
disabled={loading}
name="gpxFile"
required
type="file"
/>
</label>
</div>
)}
<div className="form-group">
<label>
{t('workouts:Notes')}:
<CustomTextArea
charLimit={500}
defaultValue={workout ? workout.notes : ''}
loading={loading}
name="notes"
/>
</label>
</div>
{loading ? (
<div className="loader" />
) : (
<div>
<input
type="submit"
className="btn btn-primary"
onClick={event =>
workout ? onEditWorkout(event, workout) : onAddWorkout(event)
}
value={t('common:Submit')}
/>
<input
type="submit"
className="btn btn-secondary"
onClick={() => history.push('/')}
value={t('common:Cancel')}
/>
</div>
)}
</form>
)
}
export default connect(
state => ({
appConfig: state.application.config,
loading: state.loading,
}),
dispatch => ({
onAddWorkout: e => {
dispatch(setLoading(true))
const form = new FormData()
form.append('file', e.target.form.gpxFile.files[0])
/* prettier-ignore */
form.append(
'data',
`{"sport_id": ${e.target.form.sport.value
}, "notes": "${e.target.form.notes.value}"}`
)
dispatch(addWorkout(form))
},
onEditWorkout: (e, workout) => {
dispatch(
editWorkout({
id: workout.id,
notes: e.target.form.notes.value,
sport_id: +e.target.form.sport.value,
title: e.target.form.title.value,
})
)
},
})
)(FormWithGpx)

View File

@ -1,162 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import { addWorkoutWithoutGpx, editWorkout } from '../../../actions/workouts'
import { history } from '../../../index'
import { getDateWithTZ } from '../../../utils'
import { formatWorkoutDate, translateSports } from '../../../utils/workouts'
import CustomTextArea from '../../Common/CustomTextArea'
function FormWithoutGpx(props) {
const { onAddOrEdit, sports, t, user, workout } = props
const translatedSports = translateSports(sports, t, true)
let workoutDate,
workoutTime,
sportId = ''
if (workout) {
const workoutDateTime = formatWorkoutDate(
getDateWithTZ(workout.workout_date, user.timezone),
'yyyy-MM-dd'
)
workoutDate = workoutDateTime.workout_date
workoutTime = workoutDateTime.workout_time
sportId = workout.sport_id
}
return (
<form onSubmit={event => event.preventDefault()}>
<div className="form-group">
<label>
{t('workouts:Title')}:
<input
name="title"
defaultValue={workout ? workout.title : ''}
className="form-control input-lg"
/>
</label>
</div>
<div className="form-group">
<label>
{t('common:Sport')}:
<select
className="form-control input-lg"
defaultValue={sportId}
name="sport_id"
required
>
<option value="" />
{translatedSports.map(sport => (
<option key={sport.id} value={sport.id}>
{sport.label}
</option>
))}
</select>
</label>
</div>
<div className="form-group">
<label>
{t('workouts:Workout Date')}:
<div className="container">
<div className="row">
<input
name="workout_date"
defaultValue={workoutDate}
className="form-control col-md"
required
type="date"
/>
<input
name="workout_time"
defaultValue={workoutTime}
className="form-control col-md"
required
type="time"
/>
</div>
</div>
</label>
</div>
<div className="form-group">
<label>
{t('workouts:Duration')}:
<input
name="duration"
defaultValue={workout ? workout.duration : ''}
className="form-control col-xs-4"
pattern="^([0-9]*[0-9]):([0-5][0-9]):([0-5][0-9])$"
placeholder="hh:mm:ss"
required
type="text"
/>
</label>
</div>
<div className="form-group">
<label>
{t('workouts:Distance')} (km):
<input
name="distance"
defaultValue={workout ? workout.distance : ''}
className="form-control input-lg"
min={0}
required
step="0.001"
type="number"
/>
</label>
</div>
<div className="form-group">
<label>
{t('workouts:Notes')}:
<CustomTextArea
charLimit={500}
defaultValue={workout ? workout.notes : ''}
name="notes"
/>
</label>
</div>
<input
type="submit"
className="btn btn-primary"
onClick={event => onAddOrEdit(event, workout)}
value={t('common:Submit')}
/>
<input
type="submit"
className="btn btn-secondary"
onClick={() => history.push('/')}
value={t('common:Cancel')}
/>
</form>
)
}
export default connect(
state => ({
user: state.user,
}),
dispatch => ({
onAddOrEdit: (e, workout) => {
const d = e.target.form.duration.value.split(':')
const duration = +d[0] * 60 * 60 + +d[1] * 60 + +d[2]
/* prettier-ignore */
const workoutDate = `${e.target.form.workout_date.value
} ${ e.target.form.workout_time.value}`
const data = {
workout_date: workoutDate,
distance: +e.target.form.distance.value,
duration,
notes: e.target.form.notes.value,
sport_id: +e.target.form.sport_id.value,
title: e.target.form.title.value,
}
if (workout) {
data.id = workout.id
dispatch(editWorkout(data))
} else {
dispatch(addWorkoutWithoutGpx(data))
}
},
})
)(FormWithoutGpx)

View File

@ -1,38 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import { Redirect, Route, Switch } from 'react-router-dom'
import NotFound from './../Others/NotFound'
import WorkoutAdd from './WorkoutAdd'
import WorkoutDisplay from './WorkoutDisplay'
import WorkoutEdit from './WorkoutEdit'
import { isLoggedIn } from '../../utils'
function Workout() {
return (
<div>
{isLoggedIn() ? (
<Switch>
<Route exact path="/workouts/add" component={WorkoutAdd} />
<Route exact path="/workouts/:workoutId" component={WorkoutDisplay} />
<Route
exact
path="/workouts/:workoutId/edit"
component={WorkoutEdit}
/>
<Route
path="/workouts/:workoutId/segment/:segmentId"
component={WorkoutDisplay}
/>
<Route component={NotFound} />
</Switch>
) : (
<Redirect to="/login" />
)}
</div>
)
}
export default connect(state => ({
user: state.user,
}))(Workout)