Client - display segments - #14

This commit is contained in:
Sam 2019-08-25 20:23:11 +02:00
parent 86cb015279
commit b2af180e05
10 changed files with 199 additions and 46 deletions

View File

@ -81,6 +81,22 @@ export const getActivityGpx = activityId => dispatch => {
dispatch(setGpx(null)) dispatch(setGpx(null))
} }
export const getSegmentGpx = (activityId, segmentId) => dispatch => {
if (activityId) {
return FitTrackeeGenericApi
.getData(`activities/${activityId}/gpx/segment/${segmentId}`)
.then(ret => {
if (ret.status === 'success') {
dispatch(setGpx(ret.data.gpx))
} else {
dispatch(setError(`activities: ${ret.message}`))
}
})
.catch(error => dispatch(setError(`activities: ${error}`)))
}
dispatch(setGpx(null))
}
export const getActivityChartData = activityId => dispatch => { export const getActivityChartData = activityId => dispatch => {
if (activityId) { if (activityId) {
@ -98,6 +114,22 @@ export const getActivityChartData = activityId => dispatch => {
dispatch(setChartData(null)) dispatch(setChartData(null))
} }
export const getSegmentChartData = (activityId, segmentId) => dispatch => {
if (activityId) {
return FitTrackeeGenericApi
.getData(`activities/${activityId}/chart_data/segment/${segmentId}`)
.then(ret => {
if (ret.status === 'success') {
dispatch(setChartData(formatChartData(ret.data.chart_data)))
} else {
dispatch(setError(`activities: ${ret.message}`))
}
})
.catch(error => dispatch(setError(`activities: ${error}`)))
}
dispatch(setChartData(null))
}
export const deleteActivity = id => dispatch => FitTrackeeGenericApi export const deleteActivity = id => dispatch => FitTrackeeGenericApi
.deleteData('activities', id) .deleteData('activities', id)

View File

@ -6,32 +6,47 @@ import { formatActivityDate } from '../../../utils/activities'
export default function ActivityCardHeader(props) { export default function ActivityCardHeader(props) {
const { activity, displayModal, sport, title, user } = props const {
activity, dataType, displayModal, segmentId, sport, title, user
} = props
const activityDate = activity const activityDate = activity
? formatActivityDate( ? formatActivityDate(
getDateWithTZ(activity.activity_date, user.timezone) getDateWithTZ(activity.activity_date, user.timezone)
) )
: null : null
const previousUrl = dataType === 'segment' && segmentId !== 0
? `/activities/${activity.id}/segment/${segmentId - 1}`
: dataType === 'activity' && activity.previous_activity
? `/activities/${activity.previous_activity}`
: null
const nextUrl =
dataType === 'segment' && segmentId < activity.segments.length - 1
? `/activities/${activity.id}/segment/${segmentId + 1}`
: dataType === 'activity' && activity.next_activity
? `/activities/${activity.next_activity}`
: null
return ( return (
<div className="container"> <div className="container">
<div className="row"> <div className="row">
<div className="col-auto"> <div className="col-auto">
{activity.previous_activity ? ( {previousUrl ? (
<Link <Link
className="unlink" className="unlink"
to={`/activities/${activity.previous_activity}`} to={previousUrl}
> >
<i <i
className="fa fa-chevron-left" className="fa fa-chevron-left"
aria-hidden="true" aria-hidden="true"
title="See previous activity" title={`See previous ${dataType}`}
/> />
</Link> </Link>
) : ( ) : (
<i <i
className="fa fa-chevron-left inactive-link" className="fa fa-chevron-left inactive-link"
aria-hidden="true" aria-hidden="true"
title="No previous activity" title={`No previous ${dataType}`}
/> />
)} )}
</div> </div>
@ -43,23 +58,37 @@ export default function ActivityCardHeader(props) {
/> />
</div> </div>
<div className="col"> <div className="col">
{title}{' '} {dataType === 'activity' ? (
<Link <>
className="unlink" {title}{' '}
to={`/activities/${activity.id}/edit`} <Link
> className="unlink"
to={`/activities/${activity.id}/edit`}
>
<i
className="fa fa-edit custom-fa"
aria-hidden="true"
title="Edit activity"
/>
</Link>
<i <i
className="fa fa-edit custom-fa" className="fa fa-trash custom-fa"
aria-hidden="true" aria-hidden="true"
title="Edit activity" onClick={() => displayModal(true)}
title="Delete activity"
/> />
</Link> </>
<i ) : (
className="fa fa-trash custom-fa" <>
aria-hidden="true" <Link
onClick={() => displayModal(true)} to={`/activities/${activity.id}`}
title="Delete activity" >
/><br /> {title}
</Link>{' '}
- segment {segmentId + 1}
</>
)}
<br />
{activityDate && ( {activityDate && (
<span className="activity-date"> <span className="activity-date">
{`${activityDate.activity_date} - ${activityDate.activity_time}`} {`${activityDate.activity_date} - ${activityDate.activity_time}`}
@ -67,22 +96,22 @@ export default function ActivityCardHeader(props) {
)} )}
</div> </div>
<div className="col-auto"> <div className="col-auto">
{activity.next_activity ? ( {nextUrl ? (
<Link <Link
className="unlink" className="unlink"
to={`/activities/${activity.next_activity}`} to={nextUrl}
> >
<i <i
className="fa fa-chevron-right" className="fa fa-chevron-right"
aria-hidden="true" aria-hidden="true"
title="See next activity" title={`See next ${dataType}`}
/> />
</Link> </Link>
) : ( ) : (
<i <i
className="fa fa-chevron-right inactive-link" className="fa fa-chevron-right inactive-link"
aria-hidden="true" aria-hidden="true"
title="No next activity" title={`No next ${dataType}`}
/> />
)} )}
</div> </div>

View File

@ -5,7 +5,9 @@ import {
Area, ComposedChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis Area, ComposedChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis
} from 'recharts' } from 'recharts'
import { getActivityChartData } from '../../../actions/activities' import {
getActivityChartData, getSegmentChartData
} from '../../../actions/activities'
class ActivityCharts extends React.Component { class ActivityCharts extends React.Component {
@ -18,14 +20,24 @@ class ActivityCharts extends React.Component {
} }
componentDidMount() { componentDidMount() {
this.props.loadActivityData(this.props.activity.id) if (this.props.dataType === 'activity') {
this.props.loadActivityData(this.props.activity.id)
} else {
this.props.loadSegmentData(this.props.activity.id, this.props.segmentId)
}
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (prevProps.activity.id !== if (this.props.dataType === 'activity' && (
this.props.activity.id) { prevProps.activity.id !== this.props.activity.id)
) {
this.props.loadActivityData(this.props.activity.id) this.props.loadActivityData(this.props.activity.id)
} }
if (this.props.dataType === 'segment' && (
prevProps.segmentId !== this.props.segmentId)
) {
this.props.loadSegmentData(this.props.activity.id, this.props.segmentId)
}
} }
componentWillUnmount() { componentWillUnmount() {
@ -196,5 +208,8 @@ export default connect(
loadActivityData: activityId => { loadActivityData: activityId => {
dispatch(getActivityChartData(activityId)) dispatch(getActivityChartData(activityId))
}, },
loadSegmentData: (activityId, segmentId) => {
dispatch(getSegmentChartData(activityId, segmentId))
},
}) })
)(ActivityCharts) )(ActivityCharts)

View File

@ -5,7 +5,6 @@ import ActivityWeather from './ActivityWeather'
export default function ActivityDetails(props) { export default function ActivityDetails(props) {
const { activity } = props const { activity } = props
const withPauses = activity.pauses !== '0:00:00' && activity.pauses !== null const withPauses = activity.pauses !== '0:00:00' && activity.pauses !== null
const recordLDexists = activity.records.find(r => r.record_type === 'LD')
return ( return (
<div className="activity-details"> <div className="activity-details">
<p> <p>
@ -14,7 +13,8 @@ export default function ActivityDetails(props) {
aria-hidden="true" aria-hidden="true"
/> />
Duration: {activity.moving} Duration: {activity.moving}
{recordLDexists && ( {activity.records && activity.records.find(r => r.record_type === 'LD'
) && (
<sup> <sup>
<i <i
className="fa fa-trophy custom-fa" className="fa fa-trophy custom-fa"
@ -35,7 +35,7 @@ export default function ActivityDetails(props) {
aria-hidden="true" aria-hidden="true"
/> />
Distance: {activity.distance} km Distance: {activity.distance} km
{activity.records.find(r => r.record_type === 'FD' {activity.records && activity.records.find(r => r.record_type === 'FD'
) && ( ) && (
<sup> <sup>
<i <i
@ -51,7 +51,7 @@ export default function ActivityDetails(props) {
aria-hidden="true" aria-hidden="true"
/> />
Average speed: {activity.ave_speed} km/h Average speed: {activity.ave_speed} km/h
{activity.records.find(r => r.record_type === 'AS' {activity.records && activity.records.find(r => r.record_type === 'AS'
) && ( ) && (
<sup> <sup>
<i <i
@ -62,7 +62,7 @@ export default function ActivityDetails(props) {
)} )}
<br /> <br />
Max speed : {activity.max_speed} km/h Max speed : {activity.max_speed} km/h
{activity.records.find(r => r.record_type === 'MS' {activity.records && activity.records.find(r => r.record_type === 'MS'
) && ( ) && (
<sup> <sup>
<i <i

View File

@ -3,7 +3,7 @@ import React from 'react'
import { GeoJSON, Map, Marker, TileLayer } from 'react-leaflet' import { GeoJSON, Map, Marker, TileLayer } from 'react-leaflet'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { getActivityGpx } from '../../../actions/activities' import { getActivityGpx, getSegmentGpx } from '../../../actions/activities'
import { thunderforestApiKey } from '../../../utils' import { thunderforestApiKey } from '../../../utils'
import { getGeoJson } from '../../../utils/activities' import { getGeoJson } from '../../../utils/activities'
@ -17,14 +17,24 @@ class ActivityMap extends React.Component {
} }
componentDidMount() { componentDidMount() {
this.props.loadActivityGpx(this.props.activity.id) if (this.props.dataType === 'activity') {
this.props.loadActivityGpx(this.props.activity.id)
} else {
this.props.loadSegmentGpx(this.props.activity.id, this.props.segmentId)
}
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (prevProps.activity.id !== if (this.props.dataType === 'activity' && (
this.props.activity.id) { prevProps.activity.id !== this.props.activity.id)
) {
this.props.loadActivityGpx(this.props.activity.id) this.props.loadActivityGpx(this.props.activity.id)
} }
if (this.props.dataType === 'segment' && (
prevProps.segmentId !== this.props.segmentId)
) {
this.props.loadSegmentGpx(this.props.activity.id, this.props.segmentId)
}
} }
componentWillUnmount() { componentWillUnmount() {
@ -32,7 +42,9 @@ class ActivityMap extends React.Component {
} }
render() { render() {
const { activity, coordinates, gpxContent } = this.props const {
activity, coordinates, gpxContent
} = this.props
const { jsonData } = getGeoJson(gpxContent) const { jsonData } = getGeoJson(gpxContent)
const bounds = [ const bounds = [
[activity.bounds[0], activity.bounds[1]], [activity.bounds[0], activity.bounds[1]],
@ -79,5 +91,8 @@ export default connect(
loadActivityGpx: activityId => { loadActivityGpx: activityId => {
dispatch(getActivityGpx(activityId)) dispatch(getActivityGpx(activityId))
}, },
loadSegmentGpx: (activityId, segmentId) => {
dispatch(getSegmentGpx(activityId, segmentId))
},
}) })
)(ActivityMap) )(ActivityMap)

View File

@ -3,11 +3,15 @@ import React from 'react'
export default function ActivityNotes(props) { export default function ActivityNotes(props) {
const { notes } = props const { notes } = props
return ( return (
<div className="card"> <div className="row">
<div className="card-body"> <div className="col">
Notes <div className="card activity-card">
<div className="activity-notes"> <div className="card-body">
{notes ? notes : 'No notes'} Notes
<div className="activity-notes">
{notes ? notes : 'No notes'}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

@ -8,6 +8,7 @@ import ActivityDetails from './ActivityDetails'
import ActivityMap from './ActivityMap' import ActivityMap from './ActivityMap'
import ActivityNoMap from './ActivityNoMap' import ActivityNoMap from './ActivityNoMap'
import ActivityNotes from './ActivityNotes' import ActivityNotes from './ActivityNotes'
import ActivitySegments from './ActivitySegments'
import CustomModal from '../../Common/CustomModal' import CustomModal from '../../Common/CustomModal'
import { getOrUpdateData } from '../../../actions' import { getOrUpdateData } from '../../../actions'
import { deleteActivity } from '../../../actions/activities' import { deleteActivity } from '../../../actions/activities'
@ -66,7 +67,10 @@ class ActivityDisplay extends React.Component {
const [sport] = activity const [sport] = activity
? sports.filter(s => s.id === activity.sport_id) ? sports.filter(s => s.id === activity.sport_id)
: [] : []
const segmentId = parseInt(this.props.match.params.segmentId)
const dataType = segmentId >= 0
? 'segment'
: 'activity'
return ( return (
<div className="activity-page"> <div className="activity-page">
<Helmet> <Helmet>
@ -94,6 +98,8 @@ class ActivityDisplay extends React.Component {
<div className="card-header"> <div className="card-header">
<ActivityCardHeader <ActivityCardHeader
activity={activity} activity={activity}
dataType={dataType}
segmentId={segmentId}
sport={sport} sport={sport}
title={title} title={title}
user={user} user={user}
@ -107,13 +113,19 @@ class ActivityDisplay extends React.Component {
<ActivityMap <ActivityMap
activity={activity} activity={activity}
coordinates={coordinates} coordinates={coordinates}
dataType={dataType}
segmentId={segmentId}
/> />
) : ( ) : (
<ActivityNoMap /> <ActivityNoMap />
)} )}
</div> </div>
<div className="col"> <div className="col">
<ActivityDetails activity={activity} /> <ActivityDetails
activity={dataType === 'activity'
? activity
: activity.segments[segmentId]}
/>
</div> </div>
</div> </div>
</div> </div>
@ -130,6 +142,8 @@ class ActivityDisplay extends React.Component {
<div className="chart-title">Chart</div> <div className="chart-title">Chart</div>
<ActivityCharts <ActivityCharts
activity={activity} activity={activity}
dataType={dataType}
segmentId={segmentId}
updateCoordinates={ updateCoordinates={
e => this.updateCoordinates(e) e => this.updateCoordinates(e)
} }
@ -141,7 +155,14 @@ class ActivityDisplay extends React.Component {
</div> </div>
</div> </div>
)} )}
<ActivityNotes notes={activity.notes} /> {dataType === 'activity' && (
<>
<ActivityNotes notes={activity.notes} />
{activity.segments.length > 1 && (
<ActivitySegments segments={activity.segments} />
)}
</>
)}
</div> </div>
)} )}
</div> </div>

View File

@ -26,6 +26,10 @@ function Activity () {
exact path="/activities/:activityId/edit" exact path="/activities/:activityId/edit"
component={ActivityEdit} component={ActivityEdit}
/> />
<Route
path="/activities/:activityId/segment/:segmentId"
component={ActivityDisplay}
/>
<Route component={NotFound} /> <Route component={NotFound} />
</Switch> </Switch>
) : (<Redirect to="/login" />)} ) : (<Redirect to="/login" />)}

View File

@ -102,7 +102,7 @@ label {
line-height: 400px; line-height: 400px;
} }
.activity-notes { .activity-notes, .actvitiy-segments {
font-size: 0.9em; font-size: 0.9em;
font-style: italic; font-style: italic;
margin-top: 10px; margin-top: 10px;