API & Client: Activity display update
@ -21,6 +21,8 @@ def upgrade():
|
|||||||
op.create_table('sports',
|
op.create_table('sports',
|
||||||
sa.Column('id', sa.Integer(), nullable=False),
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
sa.Column('label', sa.String(length=50), nullable=False),
|
sa.Column('label', sa.String(length=50), nullable=False),
|
||||||
|
sa.Column('img', sa.String(length=255), nullable=True),
|
||||||
|
sa.Column('is_default', sa.Boolean(), default=False, nullable=False),
|
||||||
sa.PrimaryKeyConstraint('id'),
|
sa.PrimaryKeyConstraint('id'),
|
||||||
sa.UniqueConstraint('label')
|
sa.UniqueConstraint('label')
|
||||||
)
|
)
|
||||||
|
@ -76,6 +76,8 @@ class Sport(db.Model):
|
|||||||
__tablename__ = "sports"
|
__tablename__ = "sports"
|
||||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||||
label = db.Column(db.String(50), unique=True, nullable=False)
|
label = db.Column(db.String(50), unique=True, nullable=False)
|
||||||
|
img = db.Column(db.String(255), unique=True, nullable=True)
|
||||||
|
is_default = db.Column(db.Boolean, default=False, nullable=False)
|
||||||
activities = db.relationship('Activity',
|
activities = db.relationship('Activity',
|
||||||
lazy=True,
|
lazy=True,
|
||||||
backref=db.backref('sports', lazy='joined'))
|
backref=db.backref('sports', lazy='joined'))
|
||||||
@ -93,7 +95,9 @@ class Sport(db.Model):
|
|||||||
return {
|
return {
|
||||||
'id': self.id,
|
'id': self.id,
|
||||||
'label': self.label,
|
'label': self.label,
|
||||||
'_can_be_deleted': len(self.activities) == 0
|
'img': self.img,
|
||||||
|
'_can_be_deleted':
|
||||||
|
len(self.activities) == 0 and not self.is_default
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ def create_activity(
|
|||||||
duration=duration
|
duration=duration
|
||||||
)
|
)
|
||||||
|
|
||||||
if title is not None:
|
if title is not None and title != '':
|
||||||
new_activity.title = title
|
new_activity.title = title
|
||||||
else:
|
else:
|
||||||
sport = Sport.query.filter_by(id=new_activity.sport_id).first()
|
sport = Sport.query.filter_by(id=new_activity.sport_id).first()
|
||||||
|
@ -3,12 +3,14 @@ import json
|
|||||||
expected_sport_1_cycling_result = {
|
expected_sport_1_cycling_result = {
|
||||||
'id': 1,
|
'id': 1,
|
||||||
'label': 'Cycling',
|
'label': 'Cycling',
|
||||||
|
'img': None,
|
||||||
'_can_be_deleted': True
|
'_can_be_deleted': True
|
||||||
}
|
}
|
||||||
|
|
||||||
expected_sport_2_running_result = {
|
expected_sport_2_running_result = {
|
||||||
'id': 2,
|
'id': 2,
|
||||||
'label': 'Running',
|
'label': 'Running',
|
||||||
|
'img': None,
|
||||||
'_can_be_deleted': True
|
'_can_be_deleted': True
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,12 +27,30 @@ def init_data():
|
|||||||
password='mpwoadmin')
|
password='mpwoadmin')
|
||||||
admin.admin = True
|
admin.admin = True
|
||||||
db.session.add(admin)
|
db.session.add(admin)
|
||||||
db.session.add(Sport(label='Cycling (Sport)'))
|
sport = Sport(label='Cycling (Sport)')
|
||||||
db.session.add(Sport(label='Cycling (Transport)'))
|
sport.img = '/img/sports/cycling-sport.png'
|
||||||
db.session.add(Sport(label='Hiking'))
|
sport.is_default = True
|
||||||
db.session.add(Sport(label='Mountain Biking'))
|
db.session.add(sport)
|
||||||
db.session.add(Sport(label='Running'))
|
sport = Sport(label='Cycling (Transport)')
|
||||||
db.session.add(Sport(label='Walking'))
|
sport.img = '/img/sports/cycling-transport.png'
|
||||||
|
sport.is_default = True
|
||||||
|
db.session.add(sport)
|
||||||
|
sport = Sport(label='Hiking')
|
||||||
|
sport.img = '/img/sports/hiking.png'
|
||||||
|
sport.is_default = True
|
||||||
|
db.session.add(sport)
|
||||||
|
sport = Sport(label='Mountain Biking')
|
||||||
|
sport.img = '/img/sports/mountain-biking.png'
|
||||||
|
sport.is_default = True
|
||||||
|
db.session.add(sport)
|
||||||
|
sport = Sport(label='Running')
|
||||||
|
sport.img = '/img/sports/running.png'
|
||||||
|
sport.is_default = True
|
||||||
|
db.session.add(sport)
|
||||||
|
sport = Sport(label='Walking')
|
||||||
|
sport.img = '/img/sports/walking.png'
|
||||||
|
sport.is_default = True
|
||||||
|
db.session.add(sport)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
print('Initial data stored in database.')
|
print('Initial data stored in database.')
|
||||||
|
|
||||||
|
BIN
mpwo_client/public/img/photo.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
mpwo_client/public/img/sports/cycling-sport.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
mpwo_client/public/img/sports/cycling-transport.png
Normal file
After Width: | Height: | Size: 5.5 KiB |
BIN
mpwo_client/public/img/sports/hiking.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
mpwo_client/public/img/sports/mountain-biking.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
mpwo_client/public/img/sports/running.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
mpwo_client/public/img/sports/walking.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
@ -7,9 +7,10 @@
|
|||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
|
||||||
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
|
||||||
<link
|
<link
|
||||||
href="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css"
|
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
>
|
href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css"
|
||||||
|
integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB"
|
||||||
|
crossorigin="anonymous">
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdn.jsdelivr.net/npm/fork-awesome@1.0.11/css/fork-awesome.min.css"
|
href="https://cdn.jsdelivr.net/npm/fork-awesome@1.0.11/css/fork-awesome.min.css"
|
||||||
|
@ -7,6 +7,7 @@ import ActivityMap from './ActivityMap'
|
|||||||
import CustomModal from './../Others/CustomModal'
|
import CustomModal from './../Others/CustomModal'
|
||||||
import { getData } from '../../actions'
|
import { getData } from '../../actions'
|
||||||
import { deleteActivity } from '../../actions/activities'
|
import { deleteActivity } from '../../actions/activities'
|
||||||
|
import { formatActivityDate } from '../../utils'
|
||||||
|
|
||||||
class ActivityDisplay extends React.Component {
|
class ActivityDisplay extends React.Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
@ -24,14 +25,18 @@ class ActivityDisplay extends React.Component {
|
|||||||
const { activities, message, onDeleteActivity, sports } = this.props
|
const { activities, message, onDeleteActivity, sports } = this.props
|
||||||
const { displayModal } = this.state
|
const { displayModal } = this.state
|
||||||
const [activity] = activities
|
const [activity] = activities
|
||||||
|
const title = activity ? activity.title : 'Activity'
|
||||||
|
const [sport] = activity
|
||||||
|
? sports.filter(s => s.id === activity.sport_id)
|
||||||
|
: []
|
||||||
|
const activityDate = activity
|
||||||
|
? formatActivityDate(activity.activity_date)
|
||||||
|
: null
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="activity-page">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>mpwo - Activity</title>
|
<title>mpwo - {title}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<h1 className="page-title">
|
|
||||||
Activity
|
|
||||||
</h1>
|
|
||||||
{message ? (
|
{message ? (
|
||||||
<code>{message}</code>
|
<code>{message}</code>
|
||||||
) : (
|
) : (
|
||||||
@ -46,79 +51,129 @@ class ActivityDisplay extends React.Component {
|
|||||||
}}
|
}}
|
||||||
close={() => this.setState({ displayModal: false })}
|
close={() => this.setState({ displayModal: false })}
|
||||||
/>}
|
/>}
|
||||||
{activity && sports.length > 0 && (
|
{activity && sport && (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-md-6">
|
<div className="col">
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
{sports.filter(sport => sport.id === activity.sport_id)
|
<div className="row">
|
||||||
.map(sport => sport.label)} -{' '}
|
<div className="col-auto col-activity-logo">
|
||||||
{activity.activity_date}{' '}
|
<img
|
||||||
<Link
|
className="sport-img-medium"
|
||||||
className="unlink"
|
src={sport.img}
|
||||||
to={`/activities/${activity.id}/edit`}
|
alt="sport logo"
|
||||||
>
|
/>
|
||||||
<i className="fa fa-edit custom-fa" aria-hidden="true" />
|
</div>
|
||||||
</Link>
|
<div className="col">
|
||||||
<i
|
{title}{' '}
|
||||||
className="fa fa-trash custom-fa"
|
<Link
|
||||||
aria-hidden="true"
|
className="unlink"
|
||||||
onClick={() => this.setState({ displayModal: true })}
|
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={() => this.setState({ displayModal: true })}
|
||||||
|
/><br />
|
||||||
|
{activityDate && (
|
||||||
|
<span className="activity-date">
|
||||||
|
{`${activityDate.activity_date} - ${
|
||||||
|
activityDate.activity_time}`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<p>
|
<div className="row">
|
||||||
<i
|
{activity.with_gpx && (
|
||||||
className="fa fa-calendar custom-fa"
|
<div className="col-8">
|
||||||
aria-hidden="true"
|
<ActivityMap activity={activity} />
|
||||||
/>
|
</div>
|
||||||
Start at {activity.activity_date}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<i className="fa fa-clock-o custom-fa" aria-hidden="true" />
|
|
||||||
Duration: {activity.duration} {' '}
|
|
||||||
{activity.pauses !== '0:00:00' &&
|
|
||||||
activity.pauses !== null && (
|
|
||||||
`(pauses: ${activity.pauses})`
|
|
||||||
)}
|
)}
|
||||||
</p>
|
<div className="col">
|
||||||
<p>
|
<p>
|
||||||
<i className="fa fa-road custom-fa" aria-hidden="true" />
|
<i
|
||||||
Distance: {activity.distance} km</p>
|
className="fa fa-clock-o custom-fa"
|
||||||
<p>
|
aria-hidden="true"
|
||||||
<i
|
/>
|
||||||
className="fa fa-tachometer custom-fa"
|
Duration: {activity.duration}
|
||||||
aria-hidden="true"
|
{activity.records.find(r => r.record_type === 'LD') && (
|
||||||
/>
|
<sup>
|
||||||
Average speed: {activity.ave_speed} km/h -{' '}
|
<i
|
||||||
Max speed : {activity.max_speed} km/h
|
className="fa fa-trophy custom-fa"
|
||||||
</p>
|
aria-hidden="true"
|
||||||
{activity.min_alt && activity.max_alt && (
|
/>
|
||||||
<p><i className="fi-mountains custom-fa" />
|
</sup>
|
||||||
Min altitude: {activity.min_alt}m -{' '}
|
)} {' '}
|
||||||
Max altitude: {activity.max_alt}m
|
{activity.pauses !== '0:00:00' &&
|
||||||
</p>
|
activity.pauses !== null && (
|
||||||
)}
|
`(pauses: ${activity.pauses})`
|
||||||
{activity.ascent && activity.descent && (
|
)}
|
||||||
<p><i className="fa fa-location-arrow custom-fa" />
|
</p>
|
||||||
Ascent: {activity.ascent}m -{' '}
|
<p>
|
||||||
Descent: {activity.descent}m
|
<i
|
||||||
</p>
|
className="fa fa-road custom-fa"
|
||||||
)}
|
aria-hidden="true"
|
||||||
</div>
|
/>
|
||||||
</div>
|
Distance: {activity.distance} km
|
||||||
</div>
|
{activity.records.find(r => r.record_type === 'FD') && (
|
||||||
<div className="col-md-6">
|
<sup>
|
||||||
<div className="card">
|
<i
|
||||||
<div className="card-header">
|
className="fa fa-trophy custom-fa"
|
||||||
Map
|
aria-hidden="true"
|
||||||
</div>
|
/>
|
||||||
<div className="card-body">
|
</sup>
|
||||||
{activity.with_gpx ? (
|
)}
|
||||||
<ActivityMap activity={activity} />
|
</p>
|
||||||
) : (
|
<p>
|
||||||
'No map'
|
<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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -36,7 +36,7 @@ class ActivityMap extends React.Component {
|
|||||||
<Map
|
<Map
|
||||||
zoom={this.state.zoom}
|
zoom={this.state.zoom}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
boundsOptions={{ padding: [20, 20] }}
|
boundsOptions={{ padding: [10, 10] }}
|
||||||
>
|
>
|
||||||
<TileLayer
|
<TileLayer
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
|
@ -51,12 +51,21 @@ class AdminDetail extends React.Component {
|
|||||||
<div className="form-group" key={key}>
|
<div className="form-group" key={key}>
|
||||||
<label>
|
<label>
|
||||||
{key}:
|
{key}:
|
||||||
<input
|
{key === 'img' ? (
|
||||||
|
<img
|
||||||
|
src={results[0][key]
|
||||||
|
? results[0][key]
|
||||||
|
: '/img/photo.png'}
|
||||||
|
alt="property"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
className="form-control input-lg"
|
className="form-control input-lg"
|
||||||
name={key}
|
name={key}
|
||||||
readOnly={key === 'id' || !isInEdition}
|
readOnly={key === 'id' || !isInEdition}
|
||||||
defaultValue={results[0][key]}
|
defaultValue={results[0][key]}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
|
@ -56,6 +56,16 @@ export default function AdminPage(props) {
|
|||||||
</Link>
|
</Link>
|
||||||
</th>
|
</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>
|
return <td key={key}>{result[key]}</td>
|
||||||
})
|
})
|
||||||
|
@ -61,12 +61,33 @@ input, textarea {
|
|||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.activity-date {
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-page {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.add-activity {
|
.add-activity {
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-img {
|
||||||
|
max-width: 35px;
|
||||||
|
max-height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-activity-logo{
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-trophy {
|
||||||
|
color: goldenrod;
|
||||||
|
}
|
||||||
|
|
||||||
.leaflet-container {
|
.leaflet-container {
|
||||||
height: 240px;
|
height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radioLabel {
|
.radioLabel {
|
||||||
@ -78,7 +99,7 @@ input, textarea {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
margin: 20% auto;
|
margin: 20% auto;
|
||||||
z-index: 1050;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-modal-backdrop {
|
.custom-modal-backdrop {
|
||||||
@ -89,13 +110,23 @@ input, textarea {
|
|||||||
right: 0;
|
right: 0;
|
||||||
background-color: rgba(0,0,0,0.3);
|
background-color: rgba(0,0,0,0.3);
|
||||||
padding: 50px;
|
padding: 50px;
|
||||||
z-index: 1040;
|
z-index: 90;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-fa {
|
.custom-fa {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sport-img {
|
||||||
|
max-width: 35px;
|
||||||
|
max-height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sport-img-medium {
|
||||||
|
max-width: 45px;
|
||||||
|
max-height: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
.unlink {
|
.unlink {
|
||||||
color: black;
|
color: black;
|
||||||
}
|
}
|
||||||
|
@ -27,9 +27,15 @@ export const getGeoJson = gpxContent => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const formatActivityDate = activityDateTime => {
|
export const formatActivityDate = activityDateTime => {
|
||||||
const dateTime = parse(activityDateTime)
|
if (activityDateTime) {
|
||||||
|
const dateTime = parse(activityDateTime)
|
||||||
|
return {
|
||||||
|
activity_date: format(dateTime, 'YYYY-MM-DD'),
|
||||||
|
activity_time: activityDateTime.match(/[0-2][0-9]:[0-5][0-9]/)[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
activity_date: format(dateTime, 'YYYY-MM-DD'),
|
activity_date: null,
|
||||||
activity_time: activityDateTime.match(/[0-2][0-9]:[0-5][0-9]/)[0]
|
activity_time: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|