API & Client: refactor (rename mpwo to fittrackee)
This commit is contained in:
26
fittrackee_client/src/components/Activity/ActivityAdd.jsx
Normal file
26
fittrackee_client/src/components/Activity/ActivityAdd.jsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import ActivityAddOrEdit from './ActivityAddOrEdit'
|
||||
|
||||
|
||||
function ActivityAdd (props) {
|
||||
const { message, sports } = props
|
||||
return (
|
||||
<div>
|
||||
<ActivityAddOrEdit
|
||||
activity={null}
|
||||
message={message}
|
||||
sports={sports}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
message: state.message,
|
||||
sports: state.sports.data,
|
||||
user: state.user,
|
||||
}),
|
||||
)(ActivityAdd)
|
106
fittrackee_client/src/components/Activity/ActivityAddOrEdit.jsx
Normal file
106
fittrackee_client/src/components/Activity/ActivityAddOrEdit.jsx
Normal file
@ -0,0 +1,106 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import FormWithGpx from './ActivityForms/FormWithGpx'
|
||||
import FormWithoutGpx from './ActivityForms/FormWithoutGpx'
|
||||
|
||||
class ActivityAddEdit 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 { activity, loading, message, sports } = this.props
|
||||
const { withGpx } = this.state
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - {activity
|
||||
? 'Edit a workout'
|
||||
: 'Add a workout'}
|
||||
</title>
|
||||
</Helmet>
|
||||
<br /><br />
|
||||
{message && (
|
||||
<code>{message}</code>
|
||||
)}
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-2" />
|
||||
<div className="col-md-8">
|
||||
<div className="card add-activity">
|
||||
<h2 className="card-header text-center">
|
||||
{activity ? 'Edit a workout' : 'Add a workout'}
|
||||
</h2>
|
||||
<div className="card-body">
|
||||
{activity ? (
|
||||
activity.with_gpx ? (
|
||||
<FormWithGpx activity={activity} sports={sports} />
|
||||
) : (
|
||||
<FormWithoutGpx activity={activity} sports={sports} />
|
||||
)
|
||||
) : (
|
||||
<div>
|
||||
<form>
|
||||
<div className="form-group row">
|
||||
<div className="col">
|
||||
<label className="radioLabel">
|
||||
<input
|
||||
type="radio"
|
||||
name="withGpx"
|
||||
disabled={loading}
|
||||
checked={withGpx}
|
||||
onChange={event => this.handleRadioChange(event)}
|
||||
/>
|
||||
with gpx file
|
||||
</label>
|
||||
</div>
|
||||
<div className="col">
|
||||
<label className="radioLabel">
|
||||
<input
|
||||
type="radio"
|
||||
name="withoutGpx"
|
||||
disabled={loading}
|
||||
checked={!withGpx}
|
||||
onChange={event => this.handleRadioChange(event)}
|
||||
/>
|
||||
without gpx file
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{withGpx ? (
|
||||
<FormWithGpx sports={sports} />
|
||||
) : (
|
||||
<FormWithoutGpx sports={sports} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
loading: state.loading
|
||||
}),
|
||||
)(ActivityAddEdit)
|
@ -0,0 +1,83 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { formatActivityDate } from '../../../utils'
|
||||
|
||||
|
||||
export default function ActivityCardHeader(props) {
|
||||
const { activity, displayModal, sport, title } = props
|
||||
const activityDate = activity
|
||||
? formatActivityDate(activity.activity_date)
|
||||
: null
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-auto">
|
||||
{activity.next_activity ? (
|
||||
<Link
|
||||
className="unlink"
|
||||
to={`/activities/${activity.next_activity}`}
|
||||
>
|
||||
<i
|
||||
className="fa fa-chevron-left"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<i
|
||||
className="fa fa-chevron-left inactive-link"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-auto col-activity-logo">
|
||||
<img
|
||||
className="sport-img-medium"
|
||||
src={sport.img}
|
||||
alt="sport logo"
|
||||
/>
|
||||
</div>
|
||||
<div className="col">
|
||||
{title}{' '}
|
||||
<Link
|
||||
className="unlink"
|
||||
to={`/activities/${activity.id}/edit`}
|
||||
>
|
||||
<i
|
||||
className="fa fa-edit custom-fa"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Link>
|
||||
<i
|
||||
className="fa fa-trash custom-fa"
|
||||
aria-hidden="true"
|
||||
onClick={() => displayModal(true)}
|
||||
/><br />
|
||||
{activityDate && (
|
||||
<span className="activity-date">
|
||||
{`${activityDate.activity_date} - ${activityDate.activity_time}`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-auto">
|
||||
{activity.previous_activity ? (
|
||||
<Link
|
||||
className="unlink"
|
||||
to={`/activities/${activity.previous_activity}`}
|
||||
>
|
||||
<i
|
||||
className="fa fa-chevron-right"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<i
|
||||
className="fa fa-chevron-right inactive-link"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,196 @@
|
||||
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 { getActivityChartData } from '../../../actions/activities'
|
||||
|
||||
|
||||
class ActivityCharts extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
displayDistance: true,
|
||||
dataToHide: []
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadActivityData(this.props.activity.id)
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.activity.id !==
|
||||
this.props.activity.id) {
|
||||
this.props.loadActivityData(this.props.activity.id)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.loadActivityData(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 } = 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)}
|
||||
/>
|
||||
distance
|
||||
</label>
|
||||
<label className="radioLabel col-md-1">
|
||||
<input
|
||||
type="radio"
|
||||
name="duration"
|
||||
checked={!displayDistance}
|
||||
onChange={e => this.handleRadioChange(e)}
|
||||
/>
|
||||
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)}
|
||||
/>
|
||||
speed
|
||||
</label>
|
||||
<label className="radioLabel col-md-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="elevation"
|
||||
checked={this.displayData('elevation')}
|
||||
onChange={e => this.handleLegendChange(e)}
|
||||
/>
|
||||
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 }}
|
||||
>
|
||||
<XAxis
|
||||
allowDecimals={false}
|
||||
dataKey={xDataKey}
|
||||
label={{ value: xDataKey, offset: 0, position: 'bottom' }}
|
||||
scale={xScale}
|
||||
interval={xInterval}
|
||||
tickFormatter={value => displayDistance
|
||||
? value
|
||||
: format(value, 'HH:mm:ss')}
|
||||
type="number"
|
||||
/>
|
||||
<YAxis
|
||||
label={{
|
||||
value: 'speed (km/h)', angle: -90, position: 'left'
|
||||
}}
|
||||
yAxisId="left"
|
||||
/>
|
||||
<YAxis
|
||||
label={{
|
||||
value: 'altitude (m)', angle: -90, position: 'right'
|
||||
}}
|
||||
yAxisId="right" orientation="right"
|
||||
/>
|
||||
{this.displayData('elevation') && (
|
||||
<Area
|
||||
yAxisId="right"
|
||||
type="linear"
|
||||
dataKey="elevation"
|
||||
fill="#e5e5e5"
|
||||
stroke="#cccccc"
|
||||
dot={false}
|
||||
/>
|
||||
)}
|
||||
{this.displayData('speed') && (
|
||||
<Line
|
||||
yAxisId="left"
|
||||
type="linear"
|
||||
dataKey="speed"
|
||||
stroke="#8884d8"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
/>
|
||||
)}
|
||||
<Tooltip
|
||||
labelFormatter={value => displayDistance
|
||||
? `distance: ${value} km`
|
||||
: `duration: ${format(value, 'HH:mm:ss')}`}
|
||||
/>
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
<div className="chart-info">
|
||||
data from gpx, without any cleaning
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
'No data to display'
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
chartData: state.chartData
|
||||
}),
|
||||
dispatch => ({
|
||||
loadActivityData: activityId => {
|
||||
dispatch(getActivityChartData(activityId))
|
||||
},
|
||||
})
|
||||
)(ActivityCharts)
|
@ -0,0 +1,93 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function ActivityDetails(props) {
|
||||
const { activity } = props
|
||||
const withPauses = activity.pauses !== '0:00:00' && activity.pauses !== null
|
||||
const recordLDexists = activity.records.find(r => r.record_type === 'LD')
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<i
|
||||
className="fa fa-clock-o custom-fa"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Duration: {activity.duration}
|
||||
{withPauses && (
|
||||
<span>
|
||||
{' '}
|
||||
(pauses: {activity.pauses})
|
||||
<br />
|
||||
Moving duration: {activity.moving}
|
||||
</span>
|
||||
)}
|
||||
{recordLDexists && (
|
||||
<sup>
|
||||
<i
|
||||
className="fa fa-trophy custom-fa"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</sup>
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<i
|
||||
className="fa fa-road custom-fa"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Distance: {activity.distance} km
|
||||
{activity.records.find(r => r.record_type === 'FD'
|
||||
) && (
|
||||
<sup>
|
||||
<i
|
||||
className="fa fa-trophy custom-fa"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</sup>
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
<i
|
||||
className="fa fa-tachometer custom-fa"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Average speed: {activity.ave_speed} km/h
|
||||
{activity.records.find(r => r.record_type === 'AS'
|
||||
) && (
|
||||
<sup>
|
||||
<i
|
||||
className="fa fa-trophy custom-fa"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</sup>
|
||||
)}
|
||||
<br />
|
||||
Max speed : {activity.max_speed} km/h
|
||||
{activity.records.find(r => r.record_type === 'MS'
|
||||
) && (
|
||||
<sup>
|
||||
<i
|
||||
className="fa fa-trophy custom-fa"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</sup>
|
||||
)}
|
||||
</p>
|
||||
{activity.min_alt && activity.max_alt && (
|
||||
<p>
|
||||
<i className="fi-mountains custom-fa" />
|
||||
Min altitude: {activity.min_alt}m
|
||||
<br />
|
||||
Max altitude: {activity.max_alt}m
|
||||
</p>
|
||||
)}
|
||||
{activity.ascent && activity.descent && (
|
||||
<p>
|
||||
<i className="fa fa-location-arrow custom-fa" />
|
||||
Ascent: {activity.ascent}m
|
||||
<br />
|
||||
Descent: {activity.descent}m
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
import hash from 'object-hash'
|
||||
import React from 'react'
|
||||
import { GeoJSON, Map, TileLayer } from 'react-leaflet'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { getActivityGpx } from '../../../actions/activities'
|
||||
import { getGeoJson, thunderforestApiKey } from '../../../utils'
|
||||
|
||||
class ActivityMap extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
zoom: 13,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadActivityGpx(this.props.activity.id)
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.activity.id !==
|
||||
this.props.activity.id) {
|
||||
this.props.loadActivityGpx(this.props.activity.id)
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.loadActivityGpx(null)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { activity, gpxContent } = this.props
|
||||
const { jsonData } = getGeoJson(gpxContent)
|
||||
const bounds = [
|
||||
[activity.bounds[0], activity.bounds[1]],
|
||||
[activity.bounds[2], activity.bounds[3]]
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
{jsonData && (
|
||||
<Map
|
||||
zoom={this.state.zoom}
|
||||
bounds={bounds}
|
||||
boundsOptions={{ padding: [10, 10] }}
|
||||
>
|
||||
<TileLayer
|
||||
// eslint-disable-next-line max-len
|
||||
attribution='© <a href="http://www.thunderforest.com/">Thunderforest</a>, © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
// eslint-disable-next-line max-len
|
||||
url={`https://{s}.tile.thunderforest.com/outdoors/{z}/{x}/{y}.png?apikey=${thunderforestApiKey}`}
|
||||
/>
|
||||
<GeoJSON
|
||||
// hash as a key to force re-rendering
|
||||
key={hash(jsonData)}
|
||||
data={jsonData}
|
||||
/>
|
||||
</Map>
|
||||
)}
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
gpxContent: state.gpx
|
||||
}),
|
||||
dispatch => ({
|
||||
loadActivityGpx: activityId => {
|
||||
dispatch(getActivityGpx(activityId))
|
||||
},
|
||||
})
|
||||
)(ActivityMap)
|
@ -0,0 +1,132 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import ActivityCardHeader from './ActivityCardHeader'
|
||||
import ActivityCharts from './ActivityCharts'
|
||||
import ActivityDetails from './ActivityDetails'
|
||||
import ActivityMap from './ActivityMap'
|
||||
import CustomModal from './../../Others/CustomModal'
|
||||
import { getData } from '../../../actions'
|
||||
import { deleteActivity } from '../../../actions/activities'
|
||||
|
||||
class ActivityDisplay extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
displayModal: false,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadActivity(this.props.match.params.activityId)
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.match.params.activityId !==
|
||||
this.props.match.params.activityId) {
|
||||
this.props.loadActivity(this.props.match.params.activityId)
|
||||
}
|
||||
}
|
||||
|
||||
displayModal(value) {
|
||||
this.setState({ displayModal: value })
|
||||
}
|
||||
|
||||
render() {
|
||||
const { activities, message, onDeleteActivity, sports } = this.props
|
||||
const { displayModal } = this.state
|
||||
const [activity] = activities
|
||||
const title = activity ? activity.title : 'Activity'
|
||||
const [sport] = activity
|
||||
? sports.filter(s => s.id === activity.sport_id)
|
||||
: []
|
||||
|
||||
return (
|
||||
<div className="activity-page">
|
||||
<Helmet>
|
||||
<title>FitTrackee - {title}</title>
|
||||
</Helmet>
|
||||
{message ? (
|
||||
<code>{message}</code>
|
||||
) : (
|
||||
<div className="container">
|
||||
{displayModal &&
|
||||
<CustomModal
|
||||
title="Confirmation"
|
||||
text="Are you sure you want to delete this activity?"
|
||||
confirm={() => {
|
||||
onDeleteActivity(activity.id)
|
||||
this.displayModal(false)
|
||||
}}
|
||||
close={() => this.displayModal(false)}
|
||||
/>}
|
||||
{activity && sport && activities.length === 1 && (
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<ActivityCardHeader
|
||||
activity={activity}
|
||||
sport={sport}
|
||||
title={title}
|
||||
displayModal={() => this.displayModal(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
{activity.with_gpx && (
|
||||
<div className="col-8">
|
||||
<ActivityMap activity={activity} />
|
||||
</div>
|
||||
)}
|
||||
<div className="col">
|
||||
<ActivityDetails activity={activity} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{activity.with_gpx && (
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<div className="chart-title">Chart</div>
|
||||
<ActivityCharts activity={activity} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
activities: state.activities.data,
|
||||
message: state.message,
|
||||
sports: state.sports.data,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadActivity: activityId => {
|
||||
dispatch(getData('activities', { id: activityId }))
|
||||
},
|
||||
onDeleteActivity: activityId => {
|
||||
dispatch(deleteActivity(activityId))
|
||||
},
|
||||
})
|
||||
)(ActivityDisplay)
|
44
fittrackee_client/src/components/Activity/ActivityEdit.jsx
Normal file
44
fittrackee_client/src/components/Activity/ActivityEdit.jsx
Normal file
@ -0,0 +1,44 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import ActivityAddOrEdit from './ActivityAddOrEdit'
|
||||
import { getData } from '../../actions'
|
||||
|
||||
|
||||
class ActivityEdit extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.loadActivity(
|
||||
this.props.match.params.activityId
|
||||
)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { activities, message, sports } = this.props
|
||||
const [activity] = activities
|
||||
return (
|
||||
<div>
|
||||
{sports.length > 0 && (
|
||||
<ActivityAddOrEdit
|
||||
activity={activity}
|
||||
message={message}
|
||||
sports={sports}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
activities: state.activities.data,
|
||||
message: state.message,
|
||||
sports: state.sports.data,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadActivity: activityId => {
|
||||
dispatch(getData('activities', { id: activityId }))
|
||||
},
|
||||
})
|
||||
)(ActivityEdit)
|
@ -0,0 +1,116 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { setLoading } from '../../../actions/index'
|
||||
import { addActivity, editActivity } from '../../../actions/activities'
|
||||
import { history } from '../../../index'
|
||||
|
||||
|
||||
function FormWithGpx (props) {
|
||||
const {
|
||||
activity, loading, onAddActivity, onEditActivity, sports
|
||||
} = props
|
||||
const sportId = activity ? activity.sport_id : ''
|
||||
return (
|
||||
<form
|
||||
encType="multipart/form-data"
|
||||
method="post"
|
||||
onSubmit={event => event.preventDefault()}
|
||||
>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Sport:
|
||||
<select
|
||||
className="form-control input-lg"
|
||||
defaultValue={sportId}
|
||||
disabled={loading}
|
||||
name="sport"
|
||||
required
|
||||
>
|
||||
<option value="" />
|
||||
{sports.map(sport => (
|
||||
<option key={sport.id} value={sport.id}>
|
||||
{sport.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
{activity ? (
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Title:
|
||||
<input
|
||||
name="title"
|
||||
defaultValue={activity ? activity.title : ''}
|
||||
disabled={loading}
|
||||
className="form-control input-lg"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
) : (
|
||||
<div className="form-group">
|
||||
<label>
|
||||
<strong>gpx</strong> file or <strong>zip</strong>{' '}
|
||||
file containing <strong>gpx</strong> (no folder inside):
|
||||
<input
|
||||
accept=".gpx, .zip"
|
||||
className="form-control input-lg"
|
||||
disabled={loading}
|
||||
name="gpxFile"
|
||||
required
|
||||
type="file"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{loading ? (
|
||||
<div className="loader" />
|
||||
) : (
|
||||
<div>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary btn-lg btn-block"
|
||||
onClick={
|
||||
event => activity
|
||||
? onEditActivity(event, activity)
|
||||
: onAddActivity(event)
|
||||
}
|
||||
value="Submit"
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-secondary btn-lg btn-block"
|
||||
onClick={() => history.go(-1)}
|
||||
value="Cancel"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
loading: state.loading
|
||||
}),
|
||||
dispatch => ({
|
||||
onAddActivity: e => {
|
||||
dispatch(setLoading())
|
||||
const form = new FormData()
|
||||
form.append('file', e.target.form.gpxFile.files[0])
|
||||
form.append(
|
||||
'data', `{"sport_id": ${e.target.form.sport.value}}`
|
||||
)
|
||||
dispatch(addActivity(form))
|
||||
},
|
||||
onEditActivity: (e, activity) => {
|
||||
dispatch(setLoading())
|
||||
dispatch(editActivity({
|
||||
id: activity.id,
|
||||
sport_id: +e.target.form.sport.value,
|
||||
title: e.target.form.title.value,
|
||||
}))
|
||||
},
|
||||
})
|
||||
)(FormWithGpx)
|
@ -0,0 +1,144 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import {
|
||||
addActivityWithoutGpx, editActivity
|
||||
} from '../../../actions/activities'
|
||||
import { history } from '../../../index'
|
||||
import { formatActivityDate } from '../../../utils'
|
||||
|
||||
function FormWithoutGpx (props) {
|
||||
const { activity, onAddOrEdit, sports } = props
|
||||
let activityDate, activityTime, sportId = ''
|
||||
if (activity) {
|
||||
const activityDateTime = formatActivityDate(activity.activity_date)
|
||||
activityDate = activityDateTime.activity_date
|
||||
activityTime = activityDateTime.activity_time
|
||||
sportId = activity.sport_id
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={event => event.preventDefault()}
|
||||
>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Title:
|
||||
<input
|
||||
name="title"
|
||||
defaultValue={activity ? activity.title : ''}
|
||||
className="form-control input-lg"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Sport:
|
||||
<select
|
||||
className="form-control input-lg"
|
||||
defaultValue={sportId}
|
||||
name="sport_id"
|
||||
required
|
||||
>
|
||||
<option value="" />
|
||||
{sports.map(sport => (
|
||||
<option key={sport.id} value={sport.id}>
|
||||
{sport.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Activity Date:
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<input
|
||||
name="activity_date"
|
||||
defaultValue={activityDate}
|
||||
className="form-control col-md"
|
||||
required
|
||||
type="date"
|
||||
/>
|
||||
<input
|
||||
name="activity_time"
|
||||
defaultValue={activityTime}
|
||||
className="form-control col-md"
|
||||
required
|
||||
type="time"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Duration:
|
||||
<input
|
||||
name="duration"
|
||||
defaultValue={activity ? activity.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>
|
||||
Distance (km):
|
||||
<input
|
||||
name="distance"
|
||||
defaultValue={activity ? activity.distance : ''}
|
||||
className="form-control input-lg"
|
||||
min={0}
|
||||
required
|
||||
step="0.001"
|
||||
type="number"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary btn-lg btn-block"
|
||||
onClick={event => onAddOrEdit(event, activity)}
|
||||
value="Submit"
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-secondary btn-lg btn-block"
|
||||
onClick={() => history.go(-1)}
|
||||
value="Cancel"
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
() => ({ }),
|
||||
dispatch => ({
|
||||
onAddOrEdit: (e, activity) => {
|
||||
const d = e.target.form.duration.value.split(':')
|
||||
const duration = +d[0] * 60 * 60 + +d[1] * 60 + +d[2]
|
||||
|
||||
const activityDate = `${e.target.form.activity_date.value
|
||||
} ${ e.target.form.activity_time.value}`
|
||||
|
||||
const data = {
|
||||
activity_date: activityDate,
|
||||
distance: +e.target.form.distance.value,
|
||||
duration,
|
||||
sport_id: +e.target.form.sport_id.value,
|
||||
title: e.target.form.title.value,
|
||||
}
|
||||
if (activity) {
|
||||
data.id = activity.id
|
||||
dispatch(editActivity(data))
|
||||
} else {
|
||||
dispatch(addActivityWithoutGpx(data))
|
||||
}
|
||||
},
|
||||
})
|
||||
)(FormWithoutGpx)
|
40
fittrackee_client/src/components/Activity/index.jsx
Normal file
40
fittrackee_client/src/components/Activity/index.jsx
Normal file
@ -0,0 +1,40 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { connect } from 'react-redux'
|
||||
import { Redirect, Route, Switch } from 'react-router-dom'
|
||||
|
||||
import ActivityAdd from './ActivityAdd'
|
||||
import ActivityDisplay from './ActivityDisplay'
|
||||
import ActivityEdit from './ActivityEdit'
|
||||
import NotFound from './../Others/NotFound'
|
||||
import { isLoggedIn } from '../../utils'
|
||||
|
||||
function Activity () {
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - Admin</title>
|
||||
</Helmet>
|
||||
{isLoggedIn() ? (
|
||||
<Switch>
|
||||
<Route exact path="/activities/add" component={ActivityAdd} />
|
||||
<Route
|
||||
exact path="/activities/:activityId"
|
||||
component={ActivityDisplay}
|
||||
/>
|
||||
<Route
|
||||
exact path="/activities/:activityId/edit"
|
||||
component={ActivityEdit}
|
||||
/>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
) : (<Redirect to="/login" />)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
user: state.user,
|
||||
})
|
||||
)(Activity)
|
Reference in New Issue
Block a user