API & Client: Activity display update

This commit is contained in:
Sam 2018-05-16 23:52:55 +02:00
parent 3047655e1f
commit ca80a8b6d5
19 changed files with 229 additions and 91 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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