API & Client: refactor (rename mpwo to fittrackee)

This commit is contained in:
Sam
2018-06-07 14:45:43 +02:00
parent 1f36de74ba
commit f65d636f85
81 changed files with 99 additions and 98 deletions

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

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

View File

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

View File

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

View File

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

View File

@ -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='&copy; <a href="http://www.thunderforest.com/">Thunderforest</a>, &copy; <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)

View File

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

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

View File

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

View File

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

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