Client - rename existing front
This commit is contained in:
@ -1,44 +0,0 @@
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function CustomModal(props) {
|
||||
const { t } = useTranslation()
|
||||
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()}
|
||||
>
|
||||
{t('common:Yes')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => props.close()}
|
||||
>
|
||||
{t('common:No')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
import React from 'react'
|
||||
import { withTranslation } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
class CustomTextArea extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
text: props.defaultValue ? props.defaultValue : '',
|
||||
}
|
||||
}
|
||||
|
||||
handleOnChange(changeEvent) {
|
||||
this.setState({ text: changeEvent.target.value })
|
||||
if (this.props.onTextChange) {
|
||||
this.props.onTextChange(changeEvent)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { charLimit, loading, name, t } = this.props
|
||||
const { text } = this.state
|
||||
return (
|
||||
<>
|
||||
<textarea
|
||||
name={name}
|
||||
defaultValue={text}
|
||||
disabled={loading ? loading : false}
|
||||
className="form-control input-lg"
|
||||
maxLength={charLimit}
|
||||
onChange={event => this.handleOnChange(event)}
|
||||
/>
|
||||
<div className="remaining-chars">
|
||||
{t('common:remaining characters')}: {text.length}/{charLimit}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withTranslation()(
|
||||
connect(state => ({
|
||||
loading: state.loading,
|
||||
}))(CustomTextArea)
|
||||
)
|
@ -1,33 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
export default class Message extends React.PureComponent {
|
||||
render() {
|
||||
const { message, messages, t } = this.props
|
||||
const singleMessage =
|
||||
message === '' || !message
|
||||
? ''
|
||||
: message.split('|').length > 1
|
||||
? `${t(`messages:${message.split('|')[0]}`)}: ${t(
|
||||
`messages:${message.split('|')[1]}`
|
||||
)}`
|
||||
: t(`messages:${message}`)
|
||||
return (
|
||||
<div className="error-message">
|
||||
{singleMessage !== '' && <code>{singleMessage}</code>}
|
||||
{messages &&
|
||||
messages.length > 0 &&
|
||||
(messages.length === 1 ? (
|
||||
<code>{messages[0].value}</code>
|
||||
) : (
|
||||
<code>
|
||||
<ul>
|
||||
{messages.map(msg => (
|
||||
<li key={msg.id}>{t(`messages:${msg.value}`)}</li>
|
||||
))}
|
||||
</ul>
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export default class NoWorkouts extends React.PureComponent {
|
||||
render() {
|
||||
const { t } = this.props
|
||||
return (
|
||||
<div className="card text-center">
|
||||
<div className="card-body">
|
||||
{t('common:No workouts.')}{' '}
|
||||
<Link to={{ pathname: '/workouts/add' }}>
|
||||
{t('dashboard:Upload one !')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import { formatUrl, rangePagination } from '../../utils'
|
||||
|
||||
export default class Pagination extends React.PureComponent {
|
||||
getUrl(value) {
|
||||
const { query, pathname } = this.props
|
||||
const newQuery = Object.assign({}, query)
|
||||
let page = query.page ? +query.page : 1
|
||||
switch (value) {
|
||||
case 'prev':
|
||||
page -= 1
|
||||
break
|
||||
case 'next':
|
||||
page += 1
|
||||
break
|
||||
default:
|
||||
page = +value
|
||||
}
|
||||
newQuery.page = page
|
||||
return formatUrl(pathname, newQuery)
|
||||
}
|
||||
|
||||
render() {
|
||||
const { pagination, t } = this.props
|
||||
return (
|
||||
<>
|
||||
{pagination && Object.keys(pagination).length > 0 && (
|
||||
<nav aria-label="Page navigation example">
|
||||
<ul className="pagination justify-content-center">
|
||||
<li
|
||||
className={`page-item ${pagination.has_prev ? '' : 'disabled'}`}
|
||||
>
|
||||
<Link
|
||||
className="page-link"
|
||||
to={this.getUrl('prev')}
|
||||
aria-disabled={!pagination.has_prev}
|
||||
>
|
||||
{t('common:Previous')}
|
||||
</Link>
|
||||
</li>
|
||||
{rangePagination(pagination.pages).map(page => (
|
||||
<li
|
||||
key={page}
|
||||
className={`page-item ${
|
||||
page === pagination.page ? 'active' : ''
|
||||
}`}
|
||||
>
|
||||
<Link className="page-link" to={this.getUrl(page)}>
|
||||
{page}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
<li
|
||||
className={`page-item ${pagination.has_next ? '' : 'disabled'}`}
|
||||
>
|
||||
<Link
|
||||
className="page-link"
|
||||
to={this.getUrl('next')}
|
||||
aria-disabled={!pagination.has_next}
|
||||
>
|
||||
{t('common:Next')}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import { apiUrl } from '../../utils'
|
||||
|
||||
export default class StaticMap extends React.PureComponent {
|
||||
render() {
|
||||
const { display, workout } = this.props
|
||||
|
||||
return (
|
||||
<div className={`workout-map${display === 'list' ? '-list' : ''}`}>
|
||||
<img
|
||||
src={`${apiUrl}workouts/map/${workout.map}?${Date.now()}`}
|
||||
alt="workout map"
|
||||
/>
|
||||
<div className={`map-attribution${display === 'list' ? '-list' : ''}`}>
|
||||
<span className="map-attribution-text">©</span>
|
||||
<a
|
||||
className="map-attribution-text"
|
||||
href="http://www.openstreetmap.org/copyright"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
OpenStreetMap
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import { formatValue } from '../../../utils/stats'
|
||||
|
||||
/**
|
||||
* @return {null}
|
||||
*/
|
||||
export default function CustomLabel(props) {
|
||||
const { displayedData, x, y, width, value } = props
|
||||
if (!value) {
|
||||
return null
|
||||
}
|
||||
const radius = 10
|
||||
const formattedValue = formatValue(displayedData, value)
|
||||
|
||||
return (
|
||||
<g>
|
||||
<text
|
||||
x={x + width / 2}
|
||||
y={y - radius}
|
||||
fill="#666"
|
||||
fontSize="11"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
{formattedValue}
|
||||
</text>
|
||||
</g>
|
||||
)
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
import { formatDuration } from '../../../utils/stats'
|
||||
|
||||
const formatValue = (displayedData, value) =>
|
||||
displayedData === 'duration'
|
||||
? formatDuration(value, true)
|
||||
: ['distance', 'ascent', 'descent'].includes(displayedData)
|
||||
? value.toFixed(2)
|
||||
: value
|
||||
|
||||
/**
|
||||
* @return {null}
|
||||
*/
|
||||
export default function CustomTooltip(props) {
|
||||
const { active } = props
|
||||
if (active) {
|
||||
const { displayedData, payload, label } = props
|
||||
let total = 0
|
||||
payload.map(p => (total += p.value))
|
||||
return (
|
||||
<div className="custom-tooltip">
|
||||
<p className="custom-tooltip-label">{label}</p>
|
||||
{payload.map(p => (
|
||||
<p key={p.name} style={{ color: p.fill }}>
|
||||
{p.name}: {formatValue(displayedData, p.value)} {p.unit}
|
||||
</p>
|
||||
))}
|
||||
{payload.length > 0 && (
|
||||
<p>Total: {formatValue(displayedData, total)}</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
@ -1,122 +0,0 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Bar,
|
||||
BarChart,
|
||||
ResponsiveContainer,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts'
|
||||
|
||||
import { formatValue } from '../../../utils/stats'
|
||||
import { workoutColors } from '../../../utils/workouts'
|
||||
import CustomTooltip from './CustomTooltip'
|
||||
import CustomLabel from './CustomLabel'
|
||||
|
||||
export default class StatsCharts extends React.PureComponent {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
displayedData: 'distance',
|
||||
}
|
||||
}
|
||||
handleRadioChange(changeEvent) {
|
||||
this.setState({
|
||||
displayedData: changeEvent.target.name,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { displayedData } = this.state
|
||||
const { sports, stats, t, withElevation } = this.props
|
||||
if (Object.keys(stats).length === 0) {
|
||||
return t('common:No workouts.')
|
||||
}
|
||||
return (
|
||||
<div className="chart-stats">
|
||||
<div className="row chart-radio">
|
||||
<label className="radioLabel col">
|
||||
<input
|
||||
type="radio"
|
||||
name="distance"
|
||||
checked={displayedData === 'distance'}
|
||||
onChange={e => this.handleRadioChange(e)}
|
||||
/>
|
||||
{t('statistics:distance')}
|
||||
</label>
|
||||
<label className="radioLabel col">
|
||||
<input
|
||||
type="radio"
|
||||
name="duration"
|
||||
checked={displayedData === 'duration'}
|
||||
onChange={e => this.handleRadioChange(e)}
|
||||
/>
|
||||
{t('statistics:duration')}
|
||||
</label>
|
||||
{withElevation && (
|
||||
<>
|
||||
<label className="radioLabel col">
|
||||
<input
|
||||
type="radio"
|
||||
name="ascent"
|
||||
checked={displayedData === 'ascent'}
|
||||
onChange={e => this.handleRadioChange(e)}
|
||||
/>
|
||||
{t('statistics:ascent')}
|
||||
</label>
|
||||
<label className="radioLabel col">
|
||||
<input
|
||||
type="radio"
|
||||
name="descent"
|
||||
checked={displayedData === 'descent'}
|
||||
onChange={e => this.handleRadioChange(e)}
|
||||
/>
|
||||
{t('statistics:descent')}
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
<label className="radioLabel col">
|
||||
<input
|
||||
type="radio"
|
||||
name="workouts"
|
||||
checked={displayedData === 'workouts'}
|
||||
onChange={e => this.handleRadioChange(e)}
|
||||
/>
|
||||
{t('statistics:workouts')}
|
||||
</label>
|
||||
</div>
|
||||
<ResponsiveContainer height={300}>
|
||||
<BarChart data={stats[displayedData]} margin={{ top: 15, bottom: 0 }}>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
interval={0} // to force to display all ticks
|
||||
/>
|
||||
<YAxis tickFormatter={value => formatValue(displayedData, value)} />
|
||||
<Tooltip
|
||||
content={<CustomTooltip displayedData={displayedData} />}
|
||||
/>
|
||||
{sports.map((s, i) => (
|
||||
<Bar
|
||||
// disable for now due to problems w/ CustomLabel
|
||||
// see https://github.com/recharts/recharts/issues/829
|
||||
isAnimationActive={false}
|
||||
key={s.id}
|
||||
dataKey={s.label}
|
||||
stackId="a"
|
||||
fill={workoutColors[i]}
|
||||
label={
|
||||
i === sports.length - 1 ? (
|
||||
<CustomLabel displayedData={displayedData} />
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
name={t(`sports:${s.label}`)}
|
||||
/>
|
||||
))}
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
import { format } from 'date-fns'
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { getStats } from '../../../actions/stats'
|
||||
import { formatStats } from '../../../utils/stats'
|
||||
import StatsChart from './StatsChart'
|
||||
|
||||
class Statistics extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
this.updateData()
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
(this.props.user.username &&
|
||||
this.props.user.username !== prevProps.user.username) ||
|
||||
this.props.statsParams !== prevProps.statsParams
|
||||
) {
|
||||
this.updateData()
|
||||
}
|
||||
}
|
||||
|
||||
updateData() {
|
||||
if (this.props.user.username) {
|
||||
this.props.loadWorkouts(
|
||||
this.props.user.username,
|
||||
this.props.user.weekm,
|
||||
this.props.statsParams
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
displayedSports,
|
||||
sports,
|
||||
statistics,
|
||||
statsParams,
|
||||
displayEmpty,
|
||||
t,
|
||||
user,
|
||||
withElevation,
|
||||
} = this.props
|
||||
if (!displayEmpty && Object.keys(statistics).length === 0) {
|
||||
return <span>{t('common:No workouts.')}</span>
|
||||
}
|
||||
const stats = formatStats(
|
||||
statistics,
|
||||
sports,
|
||||
statsParams,
|
||||
displayedSports,
|
||||
user.weekm
|
||||
)
|
||||
return (
|
||||
<StatsChart
|
||||
sports={sports}
|
||||
stats={stats}
|
||||
t={t}
|
||||
withElevation={withElevation}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
sports: state.sports.data,
|
||||
statistics: state.statistics.data,
|
||||
user: state.user,
|
||||
}),
|
||||
dispatch => ({
|
||||
loadWorkouts: (userName, weekm, data) => {
|
||||
const dateFormat = 'yyyy-MM-dd'
|
||||
// depends on user config (first day of week)
|
||||
const time =
|
||||
data.duration === 'week'
|
||||
? `${data.duration}${weekm ? 'm' : ''}`
|
||||
: data.duration
|
||||
const params = {
|
||||
from: format(data.start, dateFormat),
|
||||
to: format(data.end, dateFormat),
|
||||
time: time,
|
||||
}
|
||||
dispatch(getStats(userName, data.type, params))
|
||||
},
|
||||
})
|
||||
)(Statistics)
|
Reference in New Issue
Block a user