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',
sa.Column('id', sa.Integer(), 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.UniqueConstraint('label')
)

View File

@ -76,6 +76,8 @@ class Sport(db.Model):
__tablename__ = "sports"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
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',
lazy=True,
backref=db.backref('sports', lazy='joined'))
@ -93,7 +95,9 @@ class Sport(db.Model):
return {
'id': self.id,
'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
)
if title is not None:
if title is not None and title != '':
new_activity.title = title
else:
sport = Sport.query.filter_by(id=new_activity.sport_id).first()

View File

@ -3,12 +3,14 @@ import json
expected_sport_1_cycling_result = {
'id': 1,
'label': 'Cycling',
'img': None,
'_can_be_deleted': True
}
expected_sport_2_running_result = {
'id': 2,
'label': 'Running',
'img': None,
'_can_be_deleted': True
}

View File

@ -27,12 +27,30 @@ def init_data():
password='mpwoadmin')
admin.admin = True
db.session.add(admin)
db.session.add(Sport(label='Cycling (Sport)'))
db.session.add(Sport(label='Cycling (Transport)'))
db.session.add(Sport(label='Hiking'))
db.session.add(Sport(label='Mountain Biking'))
db.session.add(Sport(label='Running'))
db.session.add(Sport(label='Walking'))
sport = Sport(label='Cycling (Sport)')
sport.img = '/img/sports/cycling-sport.png'
sport.is_default = True
db.session.add(sport)
sport = Sport(label='Cycling (Transport)')
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()
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="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link
href="//maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css"
rel="stylesheet"
>
href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css"
integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB"
crossorigin="anonymous">
<link
rel="stylesheet"
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 { getData } from '../../actions'
import { deleteActivity } from '../../actions/activities'
import { formatActivityDate } from '../../utils'
class ActivityDisplay extends React.Component {
constructor(props, context) {
@ -24,14 +25,18 @@ class ActivityDisplay extends React.Component {
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)
: []
const activityDate = activity
? formatActivityDate(activity.activity_date)
: null
return (
<div>
<div className="activity-page">
<Helmet>
<title>mpwo - Activity</title>
<title>mpwo - {title}</title>
</Helmet>
<h1 className="page-title">
Activity
</h1>
{message ? (
<code>{message}</code>
) : (
@ -46,79 +51,129 @@ class ActivityDisplay extends React.Component {
}}
close={() => this.setState({ displayModal: false })}
/>}
{activity && sports.length > 0 && (
{activity && sport && (
<div className="row">
<div className="col-md-6">
<div className="col">
<div className="card">
<div className="card-header">
{sports.filter(sport => sport.id === activity.sport_id)
.map(sport => sport.label)} -{' '}
{activity.activity_date}{' '}
<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={() => this.setState({ displayModal: true })}
/>
<div className="row">
<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={() => this.setState({ displayModal: true })}
/><br />
{activityDate && (
<span className="activity-date">
{`${activityDate.activity_date} - ${
activityDate.activity_time}`}
</span>
)}
</div>
</div>
</div>
<div className="card-body">
<p>
<i
className="fa fa-calendar custom-fa"
aria-hidden="true"
/>
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})`
<div className="row">
{activity.with_gpx && (
<div className="col-8">
<ActivityMap activity={activity} />
</div>
)}
</p>
<p>
<i className="fa fa-road custom-fa" aria-hidden="true" />
Distance: {activity.distance} km</p>
<p>
<i
className="fa fa-tachometer custom-fa"
aria-hidden="true"
/>
Average speed: {activity.ave_speed} km/h -{' '}
Max speed : {activity.max_speed} km/h
</p>
{activity.min_alt && activity.max_alt && (
<p><i className="fi-mountains custom-fa" />
Min altitude: {activity.min_alt}m -{' '}
Max altitude: {activity.max_alt}m
</p>
)}
{activity.ascent && activity.descent && (
<p><i className="fa fa-location-arrow custom-fa" />
Ascent: {activity.ascent}m -{' '}
Descent: {activity.descent}m
</p>
)}
</div>
</div>
</div>
<div className="col-md-6">
<div className="card">
<div className="card-header">
Map
</div>
<div className="card-body">
{activity.with_gpx ? (
<ActivityMap activity={activity} />
) : (
'No map'
)}
<div className="col">
<p>
<i
className="fa fa-clock-o custom-fa"
aria-hidden="true"
/>
Duration: {activity.duration}
{activity.records.find(r => r.record_type === 'LD') && (
<sup>
<i
className="fa fa-trophy custom-fa"
aria-hidden="true"
/>
</sup>
)} {' '}
{activity.pauses !== '0:00:00' &&
activity.pauses !== null && (
`(pauses: ${activity.pauses})`
)}
</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>
</div>
</div>
</div>
</div>

View File

@ -36,7 +36,7 @@ class ActivityMap extends React.Component {
<Map
zoom={this.state.zoom}
bounds={bounds}
boundsOptions={{ padding: [20, 20] }}
boundsOptions={{ padding: [10, 10] }}
>
<TileLayer
// eslint-disable-next-line max-len

View File

@ -51,12 +51,21 @@ class AdminDetail extends React.Component {
<div className="form-group" key={key}>
<label>
{key}:
<input
{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>
))

View File

@ -56,6 +56,16 @@ export default function AdminPage(props) {
</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>
})

View File

@ -61,12 +61,33 @@ input, textarea {
margin-bottom: 15px;
}
.activity-date {
font-size: 0.75em;
}
.activity-page {
margin-top: 20px;
}
.add-activity {
margin-top: 50px;
}
.admin-img {
max-width: 35px;
max-height: 35px;
}
.col-activity-logo{
padding-right: 0;
}
.fa-trophy {
color: goldenrod;
}
.leaflet-container {
height: 240px;
height: 400px;
}
.radioLabel {
@ -78,7 +99,7 @@ input, textarea {
border-radius: 5px;
max-width: 500px;
margin: 20% auto;
z-index: 1050;
z-index: 100;
}
.custom-modal-backdrop {
@ -89,13 +110,23 @@ input, textarea {
right: 0;
background-color: rgba(0,0,0,0.3);
padding: 50px;
z-index: 1040;
z-index: 90;
}
.custom-fa {
margin-right: 5px;
}
.sport-img {
max-width: 35px;
max-height: 35px;
}
.sport-img-medium {
max-width: 45px;
max-height: 45px;
}
.unlink {
color: black;
}

View File

@ -27,9 +27,15 @@ export const getGeoJson = gpxContent => {
}
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 {
activity_date: format(dateTime, 'YYYY-MM-DD'),
activity_time: activityDateTime.match(/[0-2][0-9]:[0-5][0-9]/)[0]
activity_date: null,
activity_time: null,
}
}