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)
|
35
fittrackee_client/src/components/Admin/Sports/AdminMenu.jsx
Normal file
35
fittrackee_client/src/components/Admin/Sports/AdminMenu.jsx
Normal file
@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export default function AdminMenu () {
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - Admin - Sports</title>
|
||||
</Helmet>
|
||||
<h1 className="page-title">Administration</h1>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-2" />
|
||||
<div className="col-md-8 card">
|
||||
<div className="card-body">
|
||||
<ul className="admin-items">
|
||||
<li>
|
||||
<Link
|
||||
to={{
|
||||
pathname: '/admin/sports',
|
||||
}}
|
||||
>
|
||||
Sports
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
39
fittrackee_client/src/components/Admin/Sports/AdminSport.jsx
Normal file
39
fittrackee_client/src/components/Admin/Sports/AdminSport.jsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { getData } from '../../../actions'
|
||||
import AdminDetail from '../generic/AdminDetail'
|
||||
|
||||
class AdminSports extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.loadSport(this.props.match.params.sportId)
|
||||
}
|
||||
componentWillUnmount() {
|
||||
// reload all Sports
|
||||
this.props.loadSport(null)
|
||||
}
|
||||
render() {
|
||||
const { sports } = this.props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminDetail
|
||||
results={sports}
|
||||
target="sports"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
sports: state.sports.data,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadSport: sportId => {
|
||||
dispatch(getData('sports', { id: sportId }))
|
||||
},
|
||||
})
|
||||
)(AdminSports)
|
@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { getData } from '../../../actions'
|
||||
import AdminPage from '../generic/AdminPage'
|
||||
|
||||
class AdminSports extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.loadSports()
|
||||
}
|
||||
render() {
|
||||
const { sports } = this.props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdminPage
|
||||
data={sports}
|
||||
target="sports"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
sports: state.sports,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadSports: () => {
|
||||
dispatch(getData('sports'))
|
||||
},
|
||||
})
|
||||
)(AdminSports)
|
@ -0,0 +1,83 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { addData } from '../../../actions/index'
|
||||
import { history } from '../../../index'
|
||||
|
||||
class AdminSportsAdd extends React.Component {
|
||||
componentDidMount() { }
|
||||
|
||||
render() {
|
||||
const { message, onAddSport } = this.props
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - Admin - Add Sport</title>
|
||||
</Helmet>
|
||||
<h1 className="page-title">
|
||||
Administration - Sport
|
||||
</h1>
|
||||
{message && (
|
||||
<code>{message}</code>
|
||||
)}
|
||||
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-2" />
|
||||
<div className="col-md-8">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
Add a sport
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<form onSubmit={event =>
|
||||
event.preventDefault()}
|
||||
>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Label:
|
||||
<input
|
||||
name="label"
|
||||
className="form-control input-lg"
|
||||
type="text"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary btn-lg btn-block"
|
||||
onClick={event => onAddSport(event)}
|
||||
value="Submit"
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-secondary btn-lg btn-block"
|
||||
onClick={() => history.push('/admin/sports')}
|
||||
value="Cancel"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
message: state.message,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
onAddSport: e => {
|
||||
const data = { label: e.target.form.label.value }
|
||||
dispatch(addData('sports', data))
|
||||
},
|
||||
})
|
||||
)(AdminSportsAdd)
|
155
fittrackee_client/src/components/Admin/generic/AdminDetail.jsx
Normal file
155
fittrackee_client/src/components/Admin/generic/AdminDetail.jsx
Normal file
@ -0,0 +1,155 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { deleteData, updateData } from '../../../actions/index'
|
||||
import { history } from '../../../index'
|
||||
|
||||
class AdminDetail extends React.Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
isInEdition: false,
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
message,
|
||||
onDataUpdate,
|
||||
onDataDelete,
|
||||
results,
|
||||
target,
|
||||
} = this.props
|
||||
const { isInEdition } = this.state
|
||||
const title = target.charAt(0).toUpperCase() + target.slice(1)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - Admin</title>
|
||||
</Helmet>
|
||||
<h1 className="page-title">
|
||||
Administration - {title}
|
||||
</h1>
|
||||
{message ? (
|
||||
<code>{message}</code>
|
||||
) : (
|
||||
results.length === 1 && (
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-2" />
|
||||
<div className="col-md-8 card">
|
||||
<div className="card-body">
|
||||
<form onSubmit={event =>
|
||||
event.preventDefault()}
|
||||
>
|
||||
{ Object.keys(results[0])
|
||||
.filter(key => key.charAt(0) !== '_')
|
||||
.map(key => (
|
||||
<div className="form-group" key={key}>
|
||||
<label>
|
||||
{key}:
|
||||
{key === 'img' ? (
|
||||
<img
|
||||
src={results[0][key]
|
||||
? results[0][key]
|
||||
: '/img/photo.png'}
|
||||
alt="property"
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
className="form-control input-lg"
|
||||
name={key}
|
||||
readOnly={key === 'id' || !isInEdition}
|
||||
defaultValue={results[0][key]}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{isInEdition ? (
|
||||
<div>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary btn-lg btn-block"
|
||||
onClick={event => {
|
||||
onDataUpdate(event, target)
|
||||
this.setState({ isInEdition: false })
|
||||
}
|
||||
}
|
||||
value="Submit"
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-secondary btn-lg btn-block"
|
||||
onClick={event => {
|
||||
event.target.form.reset()
|
||||
this.setState({ isInEdition: false })
|
||||
}}
|
||||
value="Cancel"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary btn-lg btn-block"
|
||||
onClick={() => this.setState({ isInEdition: true })}
|
||||
value="Edit"
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-danger btn-lg btn-block"
|
||||
disabled={!results[0]._can_be_deleted}
|
||||
onClick={event => onDataDelete(event, target)}
|
||||
title={results[0]._can_be_deleted
|
||||
? ''
|
||||
: 'Can\'t be deleted, associated data exist'}
|
||||
value="Delete"
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-secondary btn-lg btn-block"
|
||||
onClick={() => history.push(`/admin/${target}`)}
|
||||
value="Back to the list"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
message: state.message,
|
||||
}),
|
||||
dispatch => ({
|
||||
onDataDelete: (e, target) => {
|
||||
const id = e.target.form.id.value
|
||||
dispatch(deleteData(target, id))
|
||||
},
|
||||
onDataUpdate: (e, target) => {
|
||||
const data = [].slice
|
||||
.call(e.target.form.elements)
|
||||
.reduce(function(map, obj) {
|
||||
if (obj.name) {
|
||||
map[obj.name] = obj.value
|
||||
}
|
||||
return map
|
||||
}, {})
|
||||
dispatch(updateData(target, data))
|
||||
},
|
||||
})
|
||||
)(AdminDetail)
|
97
fittrackee_client/src/components/Admin/generic/AdminPage.jsx
Normal file
97
fittrackee_client/src/components/Admin/generic/AdminPage.jsx
Normal file
@ -0,0 +1,97 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { history } from '../../../index'
|
||||
|
||||
export default function AdminPage(props) {
|
||||
|
||||
const { data, target } = props
|
||||
const { error } = data
|
||||
const results = data.data
|
||||
const tbKeys = []
|
||||
if (results.length > 0) {
|
||||
Object.keys(results[0])
|
||||
.filter(key => key.charAt(0) !== '_')
|
||||
.map(key => tbKeys.push(key))
|
||||
}
|
||||
const title = target.charAt(0).toUpperCase() + target.slice(1)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - Admin</title>
|
||||
</Helmet>
|
||||
<h1 className="page-title">
|
||||
Administration - {title}
|
||||
</h1>
|
||||
{error ? (
|
||||
<code>{error}</code>
|
||||
) : (
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-2" />
|
||||
<div className="col-md-8 card">
|
||||
<div className="card-body">
|
||||
<table className="table">
|
||||
<thead>
|
||||
<tr>
|
||||
{tbKeys.map(
|
||||
tbKey => <th key={tbKey} scope="col">{tbKey}</th>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ results.map((result, idx) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<tr key={idx}>
|
||||
{ Object.keys(result)
|
||||
.filter(key => key.charAt(0) !== '_')
|
||||
.map(key => {
|
||||
if (key === 'id') {
|
||||
return (
|
||||
<th key={key} scope="row">
|
||||
<Link to={`/admin/${target}/${result[key]}`}>
|
||||
{result[key]}
|
||||
</Link>
|
||||
</th>
|
||||
)
|
||||
} else if (key === 'img') {
|
||||
return (<td key={key}>
|
||||
<img
|
||||
className="admin-img"
|
||||
src={result[key]
|
||||
? result[key]
|
||||
: '/img/photo.png'}
|
||||
alt="logo"
|
||||
/>
|
||||
</td>)
|
||||
}
|
||||
return <td key={key}>{result[key]}</td>
|
||||
})
|
||||
}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary btn-lg btn-block"
|
||||
onClick={() => history.push(`/admin/${target}/add`)}
|
||||
value="Add a new item"
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-secondary btn-lg btn-block"
|
||||
onClick={() => history.push('/admin/')}
|
||||
value="Back"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
48
fittrackee_client/src/components/Admin/index.jsx
Normal file
48
fittrackee_client/src/components/Admin/index.jsx
Normal file
@ -0,0 +1,48 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { connect } from 'react-redux'
|
||||
import { Redirect, Route, Switch } from 'react-router-dom'
|
||||
|
||||
import AdminMenu from './Sports/AdminMenu'
|
||||
import AdminSport from './Sports/AdminSport'
|
||||
import AdminSports from './Sports/AdminSports'
|
||||
import AdminSportsAdd from './Sports/AdminSportsAdd'
|
||||
import AccessDenied from './../Others/AccessDenied'
|
||||
import NotFound from './../Others/NotFound'
|
||||
import { isLoggedIn } from '../../utils'
|
||||
|
||||
function Admin (props) {
|
||||
const { user } = props
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - Admin</title>
|
||||
</Helmet>
|
||||
{isLoggedIn() ? (
|
||||
user.isAdmin ? (
|
||||
<Switch>
|
||||
<Route exact path="/admin" component={AdminMenu} />
|
||||
<Route exact path="/admin/sports" component={AdminSports} />
|
||||
<Route
|
||||
exact path="/admin/sports/add"
|
||||
component={AdminSportsAdd}
|
||||
/>
|
||||
<Route
|
||||
exact path="/admin/sports/:sportId"
|
||||
component={AdminSport}
|
||||
/>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
) : (
|
||||
<AccessDenied />
|
||||
)
|
||||
) : (<Redirect to="/login" />)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
user: state.user,
|
||||
})
|
||||
)(Admin)
|
341
fittrackee_client/src/components/App.css
Normal file
341
fittrackee_client/src/components/App.css
Normal file
@ -0,0 +1,341 @@
|
||||
.App {
|
||||
background-color: #eaeaea;
|
||||
min-height: 100vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #222;
|
||||
height: 150px;
|
||||
padding: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-title {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.App-intro {
|
||||
font-size: large;
|
||||
}
|
||||
|
||||
.App-nav-profile-img {
|
||||
max-width: 35px;
|
||||
max-height: 35px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.App-profile-img-small {
|
||||
max-width: 150px;
|
||||
max-height: 150px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
a {
|
||||
color: #40578a;
|
||||
}
|
||||
|
||||
.card {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
label {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 2em;
|
||||
margin: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.activity-card {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.activity-date {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.activity-page {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.activity-sport {
|
||||
margin-right: 1px;
|
||||
max-width: 20px;
|
||||
max-height: 20px;
|
||||
}
|
||||
|
||||
.add-activity {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.admin-img {
|
||||
max-width: 35px;
|
||||
max-height: 35px;
|
||||
}
|
||||
|
||||
.col-activity-logo{
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.fa-trophy {
|
||||
color: goldenrod;
|
||||
}
|
||||
|
||||
.fa-color {
|
||||
color: #405976;
|
||||
}
|
||||
|
||||
.leaflet-container {
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
.radioLabel {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chart {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.chart-info {
|
||||
font-size: 0.8em;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.chart-month {
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.chart-radio {
|
||||
display: flex;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.chart-radio label {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.chart-radio input {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.chart-title {
|
||||
font-size: 1.1em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.custom-modal {
|
||||
background-color: #fff;
|
||||
border-radius: 5px;
|
||||
max-width: 500px;
|
||||
margin: 20% auto;
|
||||
z-index: 1250;
|
||||
}
|
||||
|
||||
.custom-modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0,0,0,0.3);
|
||||
padding: 50px;
|
||||
z-index: 1240;
|
||||
}
|
||||
|
||||
.custom-fa {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.dashboard {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.sport-img {
|
||||
max-width: 35px;
|
||||
max-height: 35px;
|
||||
}
|
||||
|
||||
.huge {
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.inactive-link {
|
||||
color: lightgrey;
|
||||
}
|
||||
|
||||
.img-disabled {
|
||||
opacity: .4;
|
||||
}
|
||||
|
||||
.record-logo {
|
||||
margin-right: 5px;
|
||||
max-width: 25px;
|
||||
max-height: 25px;
|
||||
}
|
||||
|
||||
.record-table table, .record-table th, .record-table td{
|
||||
font-size: 0.9em;
|
||||
padding: 0.1em;
|
||||
}
|
||||
|
||||
.sport-img-medium {
|
||||
max-width: 45px;
|
||||
max-height: 45px;
|
||||
}
|
||||
.unlink {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.loader {
|
||||
animation: spin 2s linear infinite;
|
||||
border: 16px solid #f3f3f3;
|
||||
border-top: 16px solid #3498db;
|
||||
border-radius: 50%;
|
||||
height: 120px;
|
||||
margin-left: 41%;
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
|
||||
/* calendar */
|
||||
:root {
|
||||
--main-color: #1a8fff;
|
||||
--text-color: #777;
|
||||
--text-color-light: #ccc;
|
||||
--border-color: #eee;
|
||||
--bg-color: #f9f9f9;
|
||||
--neutral-color: #fff;
|
||||
}
|
||||
|
||||
.calendar .col-start {
|
||||
justify-content: flex-start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.calendar .col-center {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.calendar .col-end {
|
||||
justify-content: flex-end;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.calendar {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background: var(--neutral-color);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.calendar .header {
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
/*font-size: 115%;*/
|
||||
padding: 0.5em 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.calendar .header .icon {
|
||||
cursor: pointer;
|
||||
transition: .15s ease-out;
|
||||
}
|
||||
|
||||
.calendar .header .icon:hover {
|
||||
transform: scale(1.75);
|
||||
transition: .25s ease-out;
|
||||
color: var(--main-color);
|
||||
}
|
||||
|
||||
.calendar .header .icon:first-of-type {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.calendar .header .icon:last-of-type {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.calendar .days {
|
||||
text-transform: uppercase;
|
||||
font-weight: 400;
|
||||
color: var(--text-color-light);
|
||||
font-size: 70%;
|
||||
padding: .75em 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.calendar .body .cell {
|
||||
position: relative;
|
||||
height: 3em;
|
||||
border-right: 1px solid var(--border-color);
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background: var(--neutral-color);
|
||||
}
|
||||
|
||||
.calendar .body .cell:hover {
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
.calendar .body .selected {
|
||||
border-left: 10px solid transparent;
|
||||
border-image: linear-gradient(45deg, #1a8fff 0%,#53cbf1 40%);
|
||||
}
|
||||
|
||||
.calendar .body .row {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.calendar .body .row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.calendar .body .cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.calendar .body .cell .number {
|
||||
position: absolute;
|
||||
font-size: 82.5%;
|
||||
line-height: 1;
|
||||
top: .75em;
|
||||
right: .75em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.calendar .body .disabled {
|
||||
color: var(--text-color-light);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.calendar .body .col {
|
||||
flex-grow: 0;
|
||||
flex-basis: calc(100%/7);
|
||||
width: calc(100%/7);
|
||||
}
|
94
fittrackee_client/src/components/App.jsx
Normal file
94
fittrackee_client/src/components/App.jsx
Normal file
@ -0,0 +1,94 @@
|
||||
import React from 'react'
|
||||
import { Redirect, Route, Switch } from 'react-router-dom'
|
||||
|
||||
import './App.css'
|
||||
import Admin from './Admin'
|
||||
import Activity from './Activity/index'
|
||||
import Dashboard from './Dashboard'
|
||||
import Logout from './User/Logout'
|
||||
import NavBar from './NavBar'
|
||||
import NotFound from './Others/NotFound'
|
||||
import Profile from './User/Profile'
|
||||
import ProfileEdit from './User/ProfileEdit'
|
||||
import UserForm from './User/UserForm'
|
||||
import { isLoggedIn } from '../utils'
|
||||
|
||||
export default class App extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.props = props
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="App">
|
||||
<NavBar />
|
||||
<Switch>
|
||||
<Route
|
||||
exact path="/"
|
||||
render={() => (
|
||||
isLoggedIn() ? (
|
||||
<Dashboard />
|
||||
) : (
|
||||
<Redirect to="/login" />
|
||||
)
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact path="/register"
|
||||
render={() => (
|
||||
isLoggedIn() ? (
|
||||
<Redirect to="/" />
|
||||
) : (
|
||||
<UserForm
|
||||
formType={'Register'}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact path="/login"
|
||||
render={() => (
|
||||
isLoggedIn() ? (
|
||||
<Redirect to="/" />
|
||||
) : (
|
||||
<UserForm
|
||||
formType={'Login'}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
/>
|
||||
<Route exact path="/logout" component={Logout} />
|
||||
<Route
|
||||
exact path="/profile/edit"
|
||||
render={() => (
|
||||
isLoggedIn() ? (
|
||||
<ProfileEdit />
|
||||
) : (
|
||||
<UserForm
|
||||
formType={'Login'}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact path="/profile"
|
||||
render={() => (
|
||||
isLoggedIn() ? (
|
||||
<Profile />
|
||||
) : (
|
||||
<UserForm
|
||||
formType={'Login'}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
/>
|
||||
<Route path="/activities" component={Activity} />
|
||||
<Route path="/admin" component={Admin} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
9
fittrackee_client/src/components/App.test.jsx
Normal file
9
fittrackee_client/src/components/App.test.jsx
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
import App from './App'
|
||||
|
||||
it('renders without crashing', () => {
|
||||
const div = document.createElement('div')
|
||||
ReactDOM.render(<App />, div)
|
||||
})
|
48
fittrackee_client/src/components/Dashboard/ActivityCard.jsx
Normal file
48
fittrackee_client/src/components/Dashboard/ActivityCard.jsx
Normal file
@ -0,0 +1,48 @@
|
||||
import { format } from 'date-fns'
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { apiUrl } from '../../utils'
|
||||
|
||||
export default function ActivityCard (props) {
|
||||
const { activity, sports } = props
|
||||
|
||||
return (
|
||||
<div className="card activity-card text-center">
|
||||
<div className="card-header">
|
||||
<Link to={`/activities/${activity.id}`}>
|
||||
{sports.filter(sport => sport.id === activity.sport_id)
|
||||
.map(sport => sport.label)} -{' '}
|
||||
{format(activity.activity_date, 'DD/MM/YYYY HH:mm')}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
{activity.map && (
|
||||
<div className="col">
|
||||
<img
|
||||
alt="Map"
|
||||
src={`${apiUrl}activities/map/${activity.map}` +
|
||||
`?${Date.now()}`}
|
||||
className="img-fluid"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="col">
|
||||
<p>
|
||||
<i className="fa fa-clock-o" aria-hidden="true" />{' '}
|
||||
Duration: {activity.duration}
|
||||
{activity.map ? (
|
||||
<span><br /><br /></span>
|
||||
) : (
|
||||
' - '
|
||||
)}
|
||||
<i className="fa fa-road" aria-hidden="true" />{' '}
|
||||
Distance: {activity.distance} km
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
69
fittrackee_client/src/components/Dashboard/Records.jsx
Normal file
69
fittrackee_client/src/components/Dashboard/Records.jsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { formatRecord } from '../../utils'
|
||||
|
||||
export default function RecordsCard (props) {
|
||||
const { records, sports } = props
|
||||
const recordsBySport = records.reduce((sportList, record) => {
|
||||
const sport = sports.find(s => s.id === record.sport_id)
|
||||
if (sportList[sport.label] === void 0) {
|
||||
sportList[sport.label] = {
|
||||
img: sport.img,
|
||||
records: [],
|
||||
}
|
||||
}
|
||||
sportList[sport.label].records.push(formatRecord(record))
|
||||
return sportList
|
||||
}, {})
|
||||
|
||||
return (
|
||||
<div className="card activity-card">
|
||||
<div className="card-header">
|
||||
Personal records
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{Object.keys(recordsBySport).length === 0
|
||||
? 'No records'
|
||||
: (Object.keys(recordsBySport).map(sportLabel => (
|
||||
<table
|
||||
className="table table-borderless record-table"
|
||||
key={sportLabel}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th colSpan="3">
|
||||
<img
|
||||
alt={`${sportLabel} logo`}
|
||||
className="record-logo"
|
||||
src={recordsBySport[sportLabel].img}
|
||||
/>
|
||||
{sportLabel}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{recordsBySport[sportLabel].records.map(rec => (
|
||||
<tr key={rec.id}>
|
||||
<td>
|
||||
{rec.record_type}
|
||||
</td>
|
||||
<td>
|
||||
{rec.value}
|
||||
</td>
|
||||
<td>
|
||||
<Link to={`/activities/${rec.activity_id}`}>
|
||||
{rec.activity_date}
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>))
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
138
fittrackee_client/src/components/Dashboard/Statistics.jsx
Normal file
138
fittrackee_client/src/components/Dashboard/Statistics.jsx
Normal file
@ -0,0 +1,138 @@
|
||||
import { endOfMonth, format, startOfMonth } from 'date-fns'
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import {
|
||||
Bar, BarChart, ResponsiveContainer, Tooltip, XAxis, YAxis
|
||||
} from 'recharts'
|
||||
|
||||
import { getStats } from '../../actions/stats'
|
||||
import { activityColors, formatStats } from '../../utils'
|
||||
|
||||
|
||||
class Statistics extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
const date = new Date()
|
||||
this.state = {
|
||||
start: startOfMonth(date),
|
||||
end: endOfMonth(date),
|
||||
displayedData: 'distance'
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadMonthActivities(
|
||||
this.props.user.id,
|
||||
this.state.start,
|
||||
this.state.end,
|
||||
)
|
||||
}
|
||||
|
||||
handleRadioChange (changeEvent) {
|
||||
this.setState({
|
||||
displayedData: changeEvent.target.name
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { sports, statistics } = this.props
|
||||
const { displayedData, end, start } = this.state
|
||||
const stats = formatStats(statistics, sports, start, end)
|
||||
return (
|
||||
<div className="card activity-card">
|
||||
<div className="card-header">
|
||||
This month
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{Object.keys(statistics).length === 0 ? (
|
||||
'No workouts'
|
||||
) : (
|
||||
<div className="chart-month">
|
||||
<div className="row chart-radio">
|
||||
<label className="radioLabel col">
|
||||
<input
|
||||
type="radio"
|
||||
name="distance"
|
||||
checked={displayedData === 'distance'}
|
||||
onChange={e => this.handleRadioChange(e)}
|
||||
/>
|
||||
distance
|
||||
</label>
|
||||
<label className="radioLabel col">
|
||||
<input
|
||||
type="radio"
|
||||
name="duration"
|
||||
checked={displayedData === 'duration'}
|
||||
onChange={e => this.handleRadioChange(e)}
|
||||
/>
|
||||
duration
|
||||
</label>
|
||||
<label className="radioLabel col">
|
||||
<input
|
||||
type="radio"
|
||||
name="activities"
|
||||
checked={displayedData === 'activities'}
|
||||
onChange={e => this.handleRadioChange(e)}
|
||||
/>
|
||||
activities
|
||||
</label>
|
||||
</div>
|
||||
<ResponsiveContainer height={300}>
|
||||
<BarChart
|
||||
data={stats[displayedData]}
|
||||
margin={{ top: 15, bottom: 15 }}
|
||||
>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
interval={0} // to force to display all ticks
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={value => displayedData === 'distance'
|
||||
? `${value} km`
|
||||
: displayedData === 'duration'
|
||||
? format(new Date(value * 1000), 'HH:mm')
|
||||
: value
|
||||
}
|
||||
/>
|
||||
<Tooltip />
|
||||
{sports.map((s, i) => (
|
||||
<Bar
|
||||
key={s.id}
|
||||
dataKey={s.label}
|
||||
formatter={value => displayedData === 'duration'
|
||||
? format(new Date(value * 1000), 'HH:mm')
|
||||
: value
|
||||
}
|
||||
stackId="a"
|
||||
fill={activityColors[i]}
|
||||
unit={displayedData === 'distance' ? ' km' : ''}
|
||||
/>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
sports: state.sports.data,
|
||||
statistics: state.statistics.data,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadMonthActivities: (userId, start, end) => {
|
||||
const dateFormat = 'YYYY-MM-DD'
|
||||
const params = {
|
||||
start: format(start, dateFormat),
|
||||
end: format(end, dateFormat),
|
||||
time: 'week'
|
||||
}
|
||||
dispatch(getStats(userId, 'by_time', params))
|
||||
},
|
||||
})
|
||||
)(Statistics)
|
@ -0,0 +1,64 @@
|
||||
import React from 'react'
|
||||
|
||||
|
||||
export default function UserStatistics (props) {
|
||||
const { user } = props
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col">
|
||||
<div className="card activity-card">
|
||||
<div className="card-body row">
|
||||
<div className="col-3">
|
||||
<i className="fa fa-calendar fa-3x fa-color" />
|
||||
</div>
|
||||
<div className="col-9 text-right">
|
||||
<div className="huge">{user.nbActivities}</div>
|
||||
<div>{`workout${user.nbActivities === 1 ? '' : 's'}`}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="card activity-card">
|
||||
<div className="card-body row">
|
||||
<div className="col-3">
|
||||
<i className="fa fa-road fa-3x fa-color" />
|
||||
</div>
|
||||
<div className="col-9 text-right">
|
||||
<div className="huge">
|
||||
{Math.round(user.totalDistance * 100) / 100}
|
||||
</div>
|
||||
<div>km</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="card activity-card">
|
||||
<div className="card-body row">
|
||||
<div className="col-3">
|
||||
<i className="fa fa-clock-o fa-3x fa-color" />
|
||||
</div>
|
||||
<div className="col-9 text-right">
|
||||
<div className="huge">{user.totalDuration}</div>
|
||||
<div>total duration</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="card activity-card">
|
||||
<div className="card-body row">
|
||||
<div className="col-3">
|
||||
<i className="fa fa-tags fa-3x fa-color" />
|
||||
</div>
|
||||
<div className="col-9 text-right">
|
||||
<div className="huge">{user.nbSports}</div>
|
||||
<div>{`sport${user.nbSports === 1 ? '' : 's'}`}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
107
fittrackee_client/src/components/Dashboard/index.jsx
Normal file
107
fittrackee_client/src/components/Dashboard/index.jsx
Normal file
@ -0,0 +1,107 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import ActivityCard from './ActivityCard'
|
||||
import Calendar from './../Others/Calendar'
|
||||
import Records from './Records'
|
||||
import Statistics from './Statistics'
|
||||
import UserStatistics from './UserStatistics'
|
||||
import { getData } from '../../actions'
|
||||
import { getMoreActivities } from '../../actions/activities'
|
||||
|
||||
class DashBoard extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
page: 1,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadActivities()
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
activities, loadMoreActivities, message, records, sports, user
|
||||
} = this.props
|
||||
const paginationEnd = activities.length > 0
|
||||
? activities[activities.length - 1].previous_activity === null
|
||||
: true
|
||||
const { page } = this.state
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - Dashboard</title>
|
||||
</Helmet>
|
||||
{message ? (
|
||||
<code>{message}</code>
|
||||
) : (
|
||||
(activities && sports.length > 0) && (
|
||||
<div className="container dashboard">
|
||||
<UserStatistics user={user} />
|
||||
<div className="row">
|
||||
<div className="col-md-4">
|
||||
<Statistics />
|
||||
<Records records={records} sports={sports} />
|
||||
</div>
|
||||
<div className="col-md-8">
|
||||
<Calendar />
|
||||
{activities.length > 0 ? (
|
||||
activities.map(activity => (
|
||||
<ActivityCard
|
||||
activity={activity}
|
||||
key={activity.id}
|
||||
sports={sports}
|
||||
/>)
|
||||
)) : (
|
||||
<div className="card text-center">
|
||||
<div className="card-body">
|
||||
No workouts. {' '}
|
||||
<Link to={{ pathname: '/activities/add' }}>
|
||||
Upload one !
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!paginationEnd &&
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-default btn-md btn-block"
|
||||
value="Load more activities"
|
||||
onClick={() => {
|
||||
loadMoreActivities(page + 1)
|
||||
this.setState({ page: page + 1 })
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
activities: state.activities.data,
|
||||
message: state.message,
|
||||
records: state.records.data,
|
||||
sports: state.sports.data,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadActivities: () => {
|
||||
dispatch(getData('activities', { page: 1 }))
|
||||
dispatch(getData('records'))
|
||||
},
|
||||
loadMoreActivities: page => {
|
||||
dispatch(getMoreActivities(page))
|
||||
},
|
||||
})
|
||||
)(DashBoard)
|
131
fittrackee_client/src/components/NavBar/index.jsx
Normal file
131
fittrackee_client/src/components/NavBar/index.jsx
Normal file
@ -0,0 +1,131 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { apiUrl } from '../../utils'
|
||||
|
||||
|
||||
function NavBar(props) {
|
||||
return (
|
||||
<header>
|
||||
<nav className="navbar navbar-expand-lg navbar-light bg-light">
|
||||
<div className="container">
|
||||
<span className="navbar-brand">FitTrackee</span>
|
||||
<button
|
||||
className="navbar-toggler"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
data-target="#navbarSupportedContent"
|
||||
aria-controls="navbarSupportedContent"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<span className="navbar-toggler-icon" />
|
||||
</button>
|
||||
<div className="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul className="navbar-nav mr-auto">
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
className="nav-link"
|
||||
to={{
|
||||
pathname: '/',
|
||||
}}
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
</li>
|
||||
{props.user.isAuthenticated && (
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
className="nav-link"
|
||||
to={{
|
||||
pathname: '/activities/add',
|
||||
}}
|
||||
>
|
||||
Add workout
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
{props.user.isAdmin && (
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
className="nav-link"
|
||||
to={{
|
||||
pathname: '/admin',
|
||||
}}
|
||||
>
|
||||
Admin
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
<ul className="navbar-nav flex-row ml-md-auto d-none d-md-flex">
|
||||
{!props.user.isAuthenticated && (
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
className="nav-link"
|
||||
to={{
|
||||
pathname: '/register',
|
||||
}}
|
||||
>
|
||||
Register
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
{!props.user.isAuthenticated && (
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
className="nav-link"
|
||||
to={{
|
||||
pathname: '/login',
|
||||
}}
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
{props.user.picture === true && (
|
||||
<img
|
||||
alt="Avatar"
|
||||
src={`${apiUrl}users/${props.user.id}/picture` +
|
||||
`?${Date.now()}`}
|
||||
className="img-fluid App-nav-profile-img"
|
||||
/>
|
||||
)}
|
||||
{props.user.isAuthenticated && (
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
className="nav-link"
|
||||
to={{
|
||||
pathname: '/profile',
|
||||
}}
|
||||
>
|
||||
{props.user.username}
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
{props.user.isAuthenticated && (
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
className="nav-link"
|
||||
to={{
|
||||
pathname: '/logout',
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
user: state.user,
|
||||
})
|
||||
)(NavBar)
|
16
fittrackee_client/src/components/Others/AccessDenied.jsx
Normal file
16
fittrackee_client/src/components/Others/AccessDenied.jsx
Normal file
@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
|
||||
export default function AccessDenied () {
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - Access denied</title>
|
||||
</Helmet>
|
||||
<h1 className="page-title">Access denied</h1>
|
||||
<p className="App-center">
|
||||
{'You don\'t have permissions to access this page.'}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
180
fittrackee_client/src/components/Others/Calendar.jsx
Normal file
180
fittrackee_client/src/components/Others/Calendar.jsx
Normal file
@ -0,0 +1,180 @@
|
||||
// eslint-disable-next-line max-len
|
||||
// source: https://blog.flowandform.agency/create-a-custom-calendar-in-react-3df1bfd0b728
|
||||
import dateFns from 'date-fns'
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { getMonthActivities } from '../../actions/activities'
|
||||
|
||||
const getStartAndEndMonth = date => {
|
||||
const monthStart = dateFns.startOfMonth(date)
|
||||
const monthEnd = dateFns.endOfMonth(date)
|
||||
return {
|
||||
start: dateFns.startOfWeek(monthStart),
|
||||
end: dateFns.endOfWeek(monthEnd),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Calendar extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
const calendarDate = new Date()
|
||||
this.state = {
|
||||
currentMonth: calendarDate,
|
||||
startDate: getStartAndEndMonth(calendarDate).start,
|
||||
endDate: getStartAndEndMonth(calendarDate).end,
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.loadMonthActivities(this.state.startDate, this.state.endDate)
|
||||
}
|
||||
|
||||
renderHeader() {
|
||||
const dateFormat = 'MMM YYYY'
|
||||
return (
|
||||
<div className="header row flex-middle">
|
||||
<div className="col col-start" onClick={() => this.handlePrevMonth()}>
|
||||
<i
|
||||
className="fa fa-chevron-left"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className="col col-center">
|
||||
<span>
|
||||
{dateFns.format(this.state.currentMonth, dateFormat)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="col col-end" onClick={() => this.handleNextMonth()}>
|
||||
<i
|
||||
className="fa fa-chevron-right"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderDays() {
|
||||
const dateFormat = 'ddd'
|
||||
const days = []
|
||||
const { startDate } = this.state
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
days.push(
|
||||
<div className="col col-center" key={i}>
|
||||
{dateFns.format(dateFns.addDays(startDate, i), dateFormat)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="days row">{days}</div>
|
||||
}
|
||||
|
||||
filterActivities(day) {
|
||||
const { activities } = this.props
|
||||
if (activities) {
|
||||
return activities
|
||||
.filter(act => dateFns.isSameDay(act.activity_date, day))
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
renderCells() {
|
||||
const { currentMonth, startDate, endDate } = this.state
|
||||
const { sports } = this.props
|
||||
|
||||
const dateFormat = 'D'
|
||||
const rows = []
|
||||
|
||||
let days = []
|
||||
let day = startDate
|
||||
let formattedDate = ''
|
||||
|
||||
while (day <= endDate) {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
formattedDate = dateFns.format(day, dateFormat)
|
||||
const dayActivities = this.filterActivities(day)
|
||||
const isDisabled = dateFns.isSameMonth(day, currentMonth)
|
||||
? ''
|
||||
: 'disabled'
|
||||
days.push(
|
||||
<div
|
||||
className={`col cell img-${isDisabled}`}
|
||||
key={day}
|
||||
>
|
||||
<span className="number">{formattedDate}</span>
|
||||
{dayActivities.map(act => (
|
||||
<Link key={act.id} to={`/activities/${act.id}`}>
|
||||
<img
|
||||
className={`activity-sport ${isDisabled}`}
|
||||
src={sports
|
||||
.filter(s => s.id === act.sport_id)
|
||||
.map(s => s.img)}
|
||||
alt="activity sport logo"
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
day = dateFns.addDays(day, 1)
|
||||
}
|
||||
rows.push(
|
||||
<div className="row" key={day}>
|
||||
{days}
|
||||
</div>
|
||||
)
|
||||
days = []
|
||||
}
|
||||
return <div className="body">{rows}</div>
|
||||
}
|
||||
|
||||
updateStateDate (calendarDate) {
|
||||
const { start, end } = getStartAndEndMonth(calendarDate)
|
||||
this.setState({
|
||||
currentMonth: calendarDate,
|
||||
startDate: start,
|
||||
endDate: end,
|
||||
})
|
||||
this.props.loadMonthActivities(start, end)
|
||||
}
|
||||
|
||||
handleNextMonth () {
|
||||
const calendarDate = dateFns.addMonths(this.state.currentMonth, 1)
|
||||
this.updateStateDate(calendarDate)
|
||||
}
|
||||
|
||||
handlePrevMonth () {
|
||||
const calendarDate = dateFns.subMonths(this.state.currentMonth, 1)
|
||||
this.updateStateDate(calendarDate)
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="card activity-card">
|
||||
<div className="calendar">
|
||||
{this.renderHeader()}
|
||||
{this.renderDays()}
|
||||
{this.renderCells()}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
activities: state.calendarActivities.data,
|
||||
sports: state.sports.data,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadMonthActivities: (start, end) => {
|
||||
const dateFormat = 'YYYY-MM-DD'
|
||||
dispatch(getMonthActivities(
|
||||
dateFns.format(start, dateFormat),
|
||||
dateFns.format(end, dateFormat),
|
||||
))
|
||||
},
|
||||
})
|
||||
)(Calendar)
|
42
fittrackee_client/src/components/Others/CustomModal.jsx
Normal file
42
fittrackee_client/src/components/Others/CustomModal.jsx
Normal file
@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
|
||||
export default function CustomModal(props) {
|
||||
return (
|
||||
<div className="custom-modal-backdrop">
|
||||
<div className="custom-modal">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">{props.title}</h5>
|
||||
<button
|
||||
type="button"
|
||||
className="close"
|
||||
aria-label="Close"
|
||||
onClick={() => props.close}
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<p>{props.text}</p>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={() => props.confirm()}
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => props.close()}
|
||||
>
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
13
fittrackee_client/src/components/Others/NotFound.jsx
Normal file
13
fittrackee_client/src/components/Others/NotFound.jsx
Normal file
@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
|
||||
export default function NotFound () {
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>fittrackee - 404</title>
|
||||
</Helmet>
|
||||
<h1 className="page-title">Page not found</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
11
fittrackee_client/src/components/Root.jsx
Normal file
11
fittrackee_client/src/components/Root.jsx
Normal file
@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import { Provider } from 'react-redux'
|
||||
import { ConnectedRouter } from 'react-router-redux'
|
||||
|
||||
export default function Root({ store, history, children }) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>{children}</ConnectedRouter>
|
||||
</Provider>
|
||||
)
|
||||
}
|
79
fittrackee_client/src/components/User/Form.jsx
Normal file
79
fittrackee_client/src/components/User/Form.jsx
Normal file
@ -0,0 +1,79 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
|
||||
export default function Form (props) {
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - {props.formType}</title>
|
||||
</Helmet>
|
||||
<h1 className="page-title">{props.formType}</h1>
|
||||
<div className="container">
|
||||
<div className="row">
|
||||
<div className="col-md-3" />
|
||||
<div className="col-md-6">
|
||||
<hr /><br />
|
||||
<form onSubmit={event =>
|
||||
props.handleUserFormSubmit(event, props.formType)}
|
||||
>
|
||||
{props.formType === 'Register' &&
|
||||
<div className="form-group">
|
||||
<input
|
||||
className="form-control input-lg"
|
||||
name="username"
|
||||
placeholder="Enter a username"
|
||||
required
|
||||
type="text"
|
||||
value={props.userForm.username}
|
||||
onChange={props.onHandleFormChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<div className="form-group">
|
||||
<input
|
||||
className="form-control input-lg"
|
||||
name="email"
|
||||
placeholder="Enter an email address"
|
||||
required
|
||||
type="email"
|
||||
value={props.userForm.email}
|
||||
onChange={props.onHandleFormChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<input
|
||||
className="form-control input-lg"
|
||||
name="password"
|
||||
placeholder="Enter a password"
|
||||
required
|
||||
type="password"
|
||||
value={props.userForm.password}
|
||||
onChange={props.onHandleFormChange}
|
||||
/>
|
||||
</div>
|
||||
{props.formType === 'Register' &&
|
||||
<div className="form-group">
|
||||
<input
|
||||
className="form-control input-lg"
|
||||
name="passwordConf"
|
||||
placeholder="Enter the password confirmation"
|
||||
required
|
||||
type="password"
|
||||
value={props.userForm.passwordConf}
|
||||
onChange={props.onHandleFormChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary btn-lg btn-block"
|
||||
value="Submit"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<div className="col-md-3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
31
fittrackee_client/src/components/User/Logout.jsx
Normal file
31
fittrackee_client/src/components/User/Logout.jsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { logout } from '../../actions/user'
|
||||
|
||||
class Logout extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.UserLogout()
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<p className="App-center">
|
||||
You are now logged out.
|
||||
Click <Link to="/login">here</Link> to log back in.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
UserLogout: () => {
|
||||
dispatch(logout())
|
||||
}
|
||||
})
|
||||
)(Logout)
|
102
fittrackee_client/src/components/User/Profile.jsx
Normal file
102
fittrackee_client/src/components/User/Profile.jsx
Normal file
@ -0,0 +1,102 @@
|
||||
import { format } from 'date-fns'
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { deletePicture, uploadPicture } from '../../actions/user'
|
||||
import { apiUrl } from '../../utils'
|
||||
|
||||
function Profile ({ message, onDeletePicture, onUploadPicture, user }) {
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - {user.username} - Profile</title>
|
||||
</Helmet>
|
||||
{ message !== '' && (
|
||||
<code>{message}</code>
|
||||
)}
|
||||
<div className="container">
|
||||
<h1 className="page-title">Profile</h1>
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<div className="card">
|
||||
<div className="card-header userName">
|
||||
{user.username} {' '}
|
||||
<Link
|
||||
to={{
|
||||
pathname: '/profile/edit',
|
||||
}}
|
||||
>
|
||||
<i className="fa fa-pencil-square-o" aria-hidden="true" />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
<div className="col-md-8">
|
||||
<p>Email: {user.email}</p>
|
||||
<p>Registration Date: {
|
||||
format(new Date(user.createdAt), 'DD/MM/YYYY HH:mm')
|
||||
}</p>
|
||||
<p>First Name: {user.firstName}</p>
|
||||
<p>Last Name: {user.lastName}</p>
|
||||
<p>Birth Date: {user.birthDate}</p>
|
||||
<p>Location: {user.location}</p>
|
||||
<p>Bio: {user.bio}</p>
|
||||
</div>
|
||||
<div className="col-md-4">
|
||||
{ user.picture === true && (
|
||||
<div>
|
||||
<img
|
||||
alt="Profile"
|
||||
src={`${apiUrl}users/${user.id}/picture` +
|
||||
`?${Date.now()}`}
|
||||
className="img-fluid App-profile-img-small"
|
||||
/>
|
||||
<br />
|
||||
<button
|
||||
type="submit"
|
||||
onClick={() => onDeletePicture()}
|
||||
>
|
||||
Delete picture
|
||||
</button>
|
||||
<br /><br />
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
encType="multipart/form-data"
|
||||
onSubmit={event => onUploadPicture(event)}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
name="picture"
|
||||
accept=".png,.jpg,.gif"
|
||||
/>
|
||||
<br />
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
message: state.message,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
onDeletePicture: () => {
|
||||
dispatch(deletePicture())
|
||||
},
|
||||
onUploadPicture: event => {
|
||||
dispatch(uploadPicture(event))
|
||||
},
|
||||
})
|
||||
)(Profile)
|
197
fittrackee_client/src/components/User/ProfileEdit.jsx
Normal file
197
fittrackee_client/src/components/User/ProfileEdit.jsx
Normal file
@ -0,0 +1,197 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import {
|
||||
initProfileForm,
|
||||
updateProfileFormData,
|
||||
handleProfileFormSubmit
|
||||
} from '../../actions/user'
|
||||
import { history } from '../../index'
|
||||
|
||||
|
||||
class ProfileEdit extends React.Component {
|
||||
componentDidMount() {
|
||||
this.props.initForm(this.props.user)
|
||||
}
|
||||
render () {
|
||||
const { formProfile,
|
||||
onHandleFormChange,
|
||||
onHandleProfileFormSubmit,
|
||||
message,
|
||||
user
|
||||
} = this.props
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - {user.username} - Edit Profile</title>
|
||||
</Helmet>
|
||||
{ message !== '' && (
|
||||
<code>{message}</code>
|
||||
)}
|
||||
<div className="container">
|
||||
<h1 className="page-title">Profile Edition</h1>
|
||||
<div className="row">
|
||||
<div className="col-md-2" />
|
||||
<div className="col-md-8">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
{user.username}
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="row">
|
||||
<div className="col-md-12">
|
||||
<form onSubmit={event =>
|
||||
onHandleProfileFormSubmit(event)}
|
||||
>
|
||||
<div className="form-group">
|
||||
<label>Email:
|
||||
<input
|
||||
name="email"
|
||||
className="form-control input-lg"
|
||||
type="text"
|
||||
value={user.email}
|
||||
readOnly
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Registration Date:
|
||||
<input
|
||||
name="createdAt"
|
||||
className="form-control input-lg"
|
||||
type="text"
|
||||
value={user.createdAt}
|
||||
readOnly
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Password:
|
||||
<input
|
||||
name="password"
|
||||
className="form-control input-lg"
|
||||
type="password"
|
||||
onChange={onHandleFormChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Password Confirmation:
|
||||
<input
|
||||
name="passwordConf"
|
||||
className="form-control input-lg"
|
||||
type="password"
|
||||
onChange={onHandleFormChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<hr />
|
||||
<div className="form-group">
|
||||
<label>
|
||||
First Name:
|
||||
<input
|
||||
name="firstName"
|
||||
className="form-control input-lg"
|
||||
type="text"
|
||||
value={formProfile.firstName}
|
||||
onChange={onHandleFormChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Last Name:
|
||||
<input
|
||||
name="lastName"
|
||||
className="form-control input-lg"
|
||||
type="text"
|
||||
value={formProfile.lastName}
|
||||
onChange={onHandleFormChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Birth Date
|
||||
<input
|
||||
name="birthDate"
|
||||
className="form-control input-lg"
|
||||
type="date"
|
||||
value={formProfile.birthDate}
|
||||
onChange={onHandleFormChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Location:
|
||||
<input
|
||||
name="location"
|
||||
className="form-control input-lg"
|
||||
type="text"
|
||||
value={formProfile.location}
|
||||
onChange={onHandleFormChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>
|
||||
Bio:
|
||||
<textarea
|
||||
name="bio"
|
||||
className="form-control input-lg"
|
||||
maxLength="200"
|
||||
type="text"
|
||||
value={formProfile.bio}
|
||||
onChange={onHandleFormChange}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-primary btn-lg btn-block"
|
||||
value="Submit"
|
||||
/>
|
||||
<input
|
||||
type="submit"
|
||||
className="btn btn-secondary btn-lg btn-block"
|
||||
onClick={() => history.push('/profile')}
|
||||
value="Cancel"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
formProfile: state.formProfile.formProfile,
|
||||
message: state.message,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
initForm: () => {
|
||||
dispatch(initProfileForm())
|
||||
},
|
||||
onHandleFormChange: event => {
|
||||
dispatch(updateProfileFormData(event.target.name, event.target.value))
|
||||
},
|
||||
onHandleProfileFormSubmit: event => {
|
||||
dispatch(handleProfileFormSubmit(event))
|
||||
},
|
||||
})
|
||||
)(ProfileEdit)
|
84
fittrackee_client/src/components/User/UserForm.jsx
Normal file
84
fittrackee_client/src/components/User/UserForm.jsx
Normal file
@ -0,0 +1,84 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Redirect } from 'react-router-dom'
|
||||
|
||||
import Form from './Form'
|
||||
import {
|
||||
emptyForm,
|
||||
handleFormChange,
|
||||
handleUserFormSubmit
|
||||
} from '../../actions/user'
|
||||
import { isLoggedIn } from '../../utils'
|
||||
|
||||
class UserForm extends React.Component {
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
(prevProps.location.pathname !== this.props.location.pathname)
|
||||
) {
|
||||
this.props.onEmptyForm()
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
formData,
|
||||
formType,
|
||||
message,
|
||||
messages,
|
||||
onHandleFormChange,
|
||||
onHandleUserFormSubmit
|
||||
} = this.props
|
||||
return (
|
||||
<div>
|
||||
{isLoggedIn() ? (
|
||||
<Redirect to="/" />
|
||||
) : (
|
||||
<div>
|
||||
{message !== '' && (
|
||||
<code>{message}</code>
|
||||
)}
|
||||
{messages.length > 0 && (
|
||||
<code>
|
||||
<ul>
|
||||
{messages.map(msg => (
|
||||
<li key={msg.id}>
|
||||
{msg.value}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</code>
|
||||
)}
|
||||
<Form
|
||||
formType={formType}
|
||||
userForm={formData}
|
||||
onHandleFormChange={event => onHandleFormChange(event)}
|
||||
handleUserFormSubmit={event =>
|
||||
onHandleUserFormSubmit(event, formType)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
export default connect(
|
||||
state => ({
|
||||
formData: state.formData.formData,
|
||||
location: state.router.location,
|
||||
message: state.message,
|
||||
messages: state.messages,
|
||||
}),
|
||||
dispatch => ({
|
||||
onEmptyForm: () => {
|
||||
dispatch(emptyForm())
|
||||
},
|
||||
onHandleFormChange: event => {
|
||||
dispatch(handleFormChange(event.target.name, event.target.value))
|
||||
},
|
||||
onHandleUserFormSubmit: (event, formType) => {
|
||||
dispatch(handleUserFormSubmit(event, formType))
|
||||
},
|
||||
})
|
||||
)(UserForm)
|
Reference in New Issue
Block a user