Client - rename existing front

This commit is contained in:
Sam
2021-09-01 20:08:06 +02:00
parent 6ba3f6d54e
commit 6d1de3c3bb
134 changed files with 0 additions and 0 deletions

View File

@ -1,47 +0,0 @@
import FitTrackeeGenericApi from '../fitTrackeeApi'
import { history } from '../index'
import { generateIds } from '../utils'
import { emptyMessages, setError } from './index'
export const setAppConfig = data => ({
type: 'SET_APP_CONFIG',
data,
})
export const setAppStats = data => ({
type: 'SET_APP_STATS',
data,
})
const SetAppErrors = messages => ({ type: 'APP_ERRORS', messages })
export const getAppData = target => dispatch =>
FitTrackeeGenericApi.getData(target)
.then(ret => {
if (ret.status === 'success') {
if (target === 'config') {
dispatch(setAppConfig(ret.data))
} else if (target === 'stats/all') {
dispatch(setAppStats(ret.data))
}
} else {
dispatch(setError(`application|${ret.message}`))
}
})
.catch(error => dispatch(setError(`application|${error}`)))
export const updateAppConfig = formData => dispatch => {
dispatch(emptyMessages())
FitTrackeeGenericApi.updateData('config', formData)
.then(ret => {
if (ret.status === 'success') {
dispatch(setAppConfig(ret.data))
history.push('/admin/application')
} else if (Array.isArray(ret.message)) {
dispatch(SetAppErrors(generateIds(ret.message)))
} else {
dispatch(setError(ret.message))
}
})
.catch(error => dispatch(setError(`application|${error}`)))
}

View File

@ -1,111 +0,0 @@
import i18next from 'i18next'
import FitTrackeeApi from '../fitTrackeeApi/index'
import { history } from '../index'
export const emptyMessages = () => ({
type: 'CLEAN_ALL_MESSAGES',
})
export const setData = (target, data) => ({
type: 'SET_DATA',
data,
target,
})
export const setPaginatedData = (target, data, pagination) => ({
type: 'SET_PAGINATED_DATA',
data,
pagination,
target,
})
export const setError = message => ({
type: 'SET_ERROR',
message,
})
export const setLanguage = language => ({
type: 'SET_LANGUAGE',
language,
})
export const setLoading = loading => ({
type: 'SET_LOADING',
loading,
})
export const updateSportsData = data => ({
type: 'UPDATE_SPORT_DATA',
data,
})
export const updateUsersData = data => ({
type: 'UPDATE_USER_DATA',
data,
})
export const getOrUpdateData =
(action, target, data, canDispatch = true) =>
dispatch => {
dispatch(setLoading(true))
if (data && data.id && target !== 'workouts' && isNaN(data.id)) {
dispatch(setLoading(false))
return dispatch(setError(`${target}|Incorrect id`))
}
dispatch(emptyMessages())
return FitTrackeeApi[action](target, data)
.then(ret => {
if (ret.status === 'success') {
if (canDispatch) {
if (target === 'users' && action === 'getData') {
return dispatch(
setPaginatedData(target, ret.data, ret.pagination)
)
}
dispatch(setData(target, ret.data))
} else if (action === 'updateData' && target === 'sports') {
dispatch(updateSportsData(ret.data.sports[0]))
} else if (action === 'updateData' && target === 'users') {
dispatch(updateUsersData(ret.data.users[0]))
}
} else {
dispatch(setError(`${target}|${ret.message || ret.status}`))
}
dispatch(setLoading(false))
})
.catch(error => {
dispatch(setLoading(false))
dispatch(setError(`${target}|${error}`))
})
}
export const addData = (target, data) => dispatch =>
FitTrackeeApi.addData(target, data)
.then(ret => {
if (ret.status === 'created') {
history.push(`/admin/${target}`)
} else {
dispatch(setError(`${target}|${ret.status}`))
}
})
.catch(error => dispatch(setError(`${target}|${error}`)))
export const deleteData = (target, id) => dispatch => {
if (isNaN(id)) {
return dispatch(setError(target, `${target}|Incorrect id`))
}
return FitTrackeeApi.deleteData(target, id)
.then(ret => {
if (ret.status === 204) {
history.push(`/admin/${target}`)
} else {
dispatch(setError(`${target}|${ret.message || ret.status}`))
}
})
.catch(error => dispatch(setError(`${target}|${error}`)))
}
export const updateLanguage = language => dispatch => {
i18next.changeLanguage(language).then(dispatch(setLanguage(language)))
}

View File

@ -1,13 +0,0 @@
import FitTrackeeGenericApi from '../fitTrackeeApi'
import { setData, setError } from './index'
export const getStats = (userName, type, data) => dispatch =>
FitTrackeeGenericApi.getData(`stats/${userName}/${type}`, data)
.then(ret => {
if (ret.status === 'success') {
dispatch(setData('statistics', ret.data))
} else {
dispatch(setError(`statistics|${ret.message}`))
}
})
.catch(error => dispatch(setError(`statistics|${error}`)))

View File

@ -1,174 +0,0 @@
import FitTrackeeGenericApi from '../fitTrackeeApi'
import FitTrackeeApi from '../fitTrackeeApi/auth'
import { history } from '../index'
import { generateIds } from '../utils'
import { getOrUpdateData, setError, updateLanguage } from './index'
import { getAppData } from './application'
const AuthError = message => ({ type: 'AUTH_ERROR', message })
const AuthErrors = messages => ({ type: 'AUTH_ERRORS', messages })
const PictureError = message => ({ type: 'PICTURE_ERROR', message })
const ProfileSuccess = profil => ({ type: 'PROFILE_SUCCESS', profil })
const ProfileError = message => ({ type: 'PROFILE_ERROR', message })
const ProfileUpdateError = message => ({
type: 'PROFILE_UPDATE_ERROR',
message,
})
export const logout = () => ({ type: 'LOGOUT' })
export const loadProfile = () => dispatch => {
if (window.localStorage.getItem('authToken')) {
return dispatch(getProfile())
}
return { type: 'LOGOUT' }
}
export const getProfile = () => dispatch =>
FitTrackeeGenericApi.getData('auth/profile')
.then(ret => {
if (ret.status === 'success') {
dispatch(getOrUpdateData('getData', 'sports'))
ret.data.isAuthenticated = true
if (ret.data.language) {
dispatch(updateLanguage(ret.data.language))
}
return dispatch(ProfileSuccess(ret.data))
}
return dispatch(ProfileError(ret.message))
})
.catch(error => {
throw error
})
export const loginOrRegisterOrPasswordReset = (target, formData) => dispatch =>
FitTrackeeApi.loginOrRegisterOrPasswordReset(target, formData)
.then(ret => {
if (ret.status === 'success') {
if (target === 'password/reset-request') {
return history.push({
pathname: '/password-reset/sent',
})
}
if (target === 'password/update') {
return history.push({
pathname: '/updated-password',
})
}
if (target === 'login' || target === 'register') {
window.localStorage.setItem('authToken', ret.auth_token)
if (target === 'register') {
dispatch(getAppData('config'))
}
return dispatch(getProfile())
}
}
return dispatch(AuthError(ret.message))
})
.catch(error => {
throw error
})
const RegisterFormControl = (formData, onlyPasswords = false) => {
const errMsg = []
if (
!onlyPasswords &&
(formData.username.length < 3 || formData.username.length > 12)
) {
errMsg.push('3 to 12 characters required for username.')
}
if (formData.password !== formData.password_conf) {
errMsg.push("Password and password confirmation don't match.")
}
if (formData.password.length < 8) {
errMsg.push('8 characters required for password.')
}
return errMsg
}
export const handleUserFormSubmit = (formData, formType) => dispatch => {
if (formType === 'register' || formType === 'password/update') {
const ret = RegisterFormControl(formData, formType === 'password/update')
if (ret.length > 0) {
return dispatch(AuthErrors(generateIds(ret)))
}
}
return dispatch(loginOrRegisterOrPasswordReset(formType, formData))
}
export const handleProfileFormSubmit = formData => dispatch => {
if (!formData.password === formData.password_conf) {
return dispatch(
ProfileUpdateError("Password and password confirmation don't match.")
)
}
delete formData.id
return FitTrackeeGenericApi.postData('auth/profile/edit', formData)
.then(ret => {
if (ret.status === 'success') {
dispatch(getProfile())
return history.push('/profile')
}
dispatch(ProfileUpdateError(ret.message))
})
.catch(error => {
throw error
})
}
export const uploadPicture = event => dispatch => {
event.preventDefault()
const form = new FormData()
form.append('file', event.target.picture.files[0])
event.target.reset()
return FitTrackeeGenericApi.addDataWithFile('auth/picture', form)
.then(ret => {
if (ret.status === 'success') {
return dispatch(getProfile())
}
const msg =
ret.status === 413
? 'Error during picture update, file size exceeds max size.'
: ret.message
return dispatch(PictureError(msg))
})
.catch(error => {
throw error
})
}
export const deletePicture = () => dispatch =>
FitTrackeeApi.deletePicture()
.then(ret => {
if (ret.status === 204) {
return dispatch(getProfile())
}
return dispatch(PictureError(ret.message))
})
.catch(error => {
throw error
})
export const deleteUser =
(username, isAdmin = false) =>
dispatch =>
FitTrackeeGenericApi.deleteData('users', username)
.then(ret => {
if (ret.status === 204) {
dispatch(getAppData('config'))
if (isAdmin) {
history.push('/admin/users')
} else {
dispatch(logout())
history.push('/')
}
} else {
ret.json().then(r => dispatch(setError(`${r.message}`)))
}
})
.catch(error => dispatch(setError(`user|${error}`)))

View File

@ -1,192 +0,0 @@
import FitTrackeeGenericApi from '../fitTrackeeApi'
import { history } from '../index'
import { formatChartData } from '../utils/workouts'
import { setError, setLoading } from './index'
import { loadProfile } from './user'
export const pushWorkouts = workouts => ({
type: 'PUSH_WORKOUTS',
workouts,
})
export const removeWorkout = workoutId => ({
type: 'REMOVE_WORKOUT',
workoutId,
})
export const updateCalendar = workouts => ({
type: 'UPDATE_CALENDAR',
workouts,
})
export const setGpx = gpxContent => ({
type: 'SET_GPX',
gpxContent,
})
export const setChartData = chartData => ({
type: 'SET_CHART_DATA',
chartData,
})
export const addWorkout = form => dispatch =>
FitTrackeeGenericApi.addDataWithFile('workouts', form)
.then(ret => {
if (ret.status === 'created') {
if (ret.data.workouts.length === 0) {
dispatch(setError('workouts|no correct file.'))
} else if (ret.data.workouts.length === 1) {
dispatch(loadProfile())
history.push(`/workouts/${ret.data.workouts[0].id}`)
} else {
// ret.data.workouts.length > 1
dispatch(loadProfile())
history.push('/')
}
} else if (ret.status === 413) {
dispatch(
setError('workouts|File size is greater than the allowed size')
)
} else {
dispatch(setError(`workouts|${ret.message}`))
}
dispatch(setLoading(false))
})
.catch(error => {
dispatch(setLoading(false))
dispatch(setError(`workouts|${error}`))
})
export const addWorkoutWithoutGpx = form => dispatch =>
FitTrackeeGenericApi.addData('workouts/no_gpx', form)
.then(ret => {
if (ret.status === 'created') {
dispatch(loadProfile())
history.push(`/workouts/${ret.data.workouts[0].id}`)
} else {
dispatch(setError(`workouts|${ret.message}`))
}
})
.catch(error => dispatch(setError(`workouts|${error}`)))
export const getWorkoutGpx = workoutId => dispatch => {
if (workoutId) {
return FitTrackeeGenericApi.getData(`workouts/${workoutId}/gpx`)
.then(ret => {
if (ret.status === 'success') {
dispatch(setGpx(ret.data.gpx))
} else {
dispatch(setError(`workouts|${ret.message}`))
}
})
.catch(error => dispatch(setError(`workouts|${error}`)))
}
dispatch(setGpx(null))
}
export const getSegmentGpx = (workoutId, segmentId) => dispatch => {
if (workoutId) {
return FitTrackeeGenericApi.getData(
`workouts/${workoutId}/gpx/segment/${segmentId}`
)
.then(ret => {
if (ret.status === 'success') {
dispatch(setGpx(ret.data.gpx))
} else {
dispatch(setError(`workouts|${ret.message}`))
}
})
.catch(error => dispatch(setError(`workouts|${error}`)))
}
dispatch(setGpx(null))
}
export const getWorkoutChartData = workoutId => dispatch => {
if (workoutId) {
return FitTrackeeGenericApi.getData(`workouts/${workoutId}/chart_data`)
.then(ret => {
if (ret.status === 'success') {
dispatch(setChartData(formatChartData(ret.data.chart_data)))
} else {
dispatch(setError(`workouts|${ret.message}`))
}
})
.catch(error => dispatch(setError(`workouts|${error}`)))
}
dispatch(setChartData(null))
}
export const getSegmentChartData = (workoutId, segmentId) => dispatch => {
if (workoutId) {
return FitTrackeeGenericApi.getData(
`workouts/${workoutId}/chart_data/segment/${segmentId}`
)
.then(ret => {
if (ret.status === 'success') {
dispatch(setChartData(formatChartData(ret.data.chart_data)))
} else {
dispatch(setError(`workouts|${ret.message}`))
}
})
.catch(error => dispatch(setError(`workouts|${error}`)))
}
dispatch(setChartData(null))
}
export const deleteWorkout = id => dispatch =>
FitTrackeeGenericApi.deleteData('workouts', id)
.then(ret => {
if (ret.status === 204) {
Promise.resolve(dispatch(removeWorkout(id)))
.then(() => dispatch(loadProfile()))
.then(() => history.push('/'))
} else {
dispatch(setError(`workouts|${ret.status}`))
}
})
.catch(error => dispatch(setError(`workouts|${error}`)))
export const editWorkout = form => dispatch =>
FitTrackeeGenericApi.updateData('workouts', form)
.then(ret => {
if (ret.status === 'success') {
dispatch(loadProfile())
history.push(`/workouts/${ret.data.workouts[0].id}`)
} else {
dispatch(setError(`workouts|${ret.message}`))
}
dispatch(setLoading(false))
})
.catch(error => {
dispatch(setLoading(false))
dispatch(setError(`workouts|${error}`))
})
export const getMoreWorkouts = params => dispatch =>
FitTrackeeGenericApi.getData('workouts', params)
.then(ret => {
if (ret.status === 'success') {
if (ret.data.workouts.length > 0) {
dispatch(pushWorkouts(ret.data.workouts))
}
} else {
dispatch(setError(`workouts|${ret.message}`))
}
})
.catch(error => dispatch(setError(`workouts|${error}`)))
export const getMonthWorkouts = (from, to) => dispatch =>
FitTrackeeGenericApi.getData('workouts', {
from,
to,
order: 'desc',
per_page: 100,
})
.then(ret => {
if (ret.status === 'success') {
dispatch(updateCalendar(ret.data.workouts))
} else {
dispatch(setError(`workouts|${ret.message}`))
}
})
.catch(error => dispatch(setError(`workouts|${error}`)))

View File

@ -1,228 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import Message from '../Common/Message'
import { getAppData, updateAppConfig } from '../../actions/application'
import { history } from '../../index'
import { getFileSizeInMB } from '../../utils'
class AdminApplication extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
formData: {},
}
}
componentDidMount() {
this.initForm()
}
componentDidUpdate(prevProps) {
if (this.props.appConfig !== prevProps.appConfig) {
this.initForm()
}
}
initForm() {
const { appConfig } = this.props
const formData = {}
Object.keys(appConfig).map(k =>
appConfig[k] === null
? (formData[k] = '')
: ['max_single_file_size', 'max_zip_file_size'].includes(k)
? (formData[k] = getFileSizeInMB(appConfig[k]))
: (formData[k] = appConfig[k])
)
this.setState({ formData })
}
handleFormChange(e) {
const { formData } = this.state
formData[e.target.name] = +e.target.value
this.setState(formData)
}
render() {
const {
isInEdition,
loadAppConfig,
message,
messages,
onHandleConfigFormSubmit,
t,
} = this.props
const { formData } = this.state
return (
<div>
{(message || messages) && (
<Message message={message} messages={messages} t={t} />
)}
{Object.keys(formData).length > 0 && (
<div className="row">
<div className="col-md-12">
<div className="card">
<div className="card-header">
<strong>
{t('administration:Application configuration')}
</strong>
</div>
<div className="card-body">
<form
className={`app-config-form ${
isInEdition ? '' : 'form-disabled'
}`}
onSubmit={e => {
e.preventDefault()
onHandleConfigFormSubmit(formData)
}}
>
<div className="form-group row">
<label
className="col-sm-6 col-form-label"
htmlFor="max_users"
>
{t(
// eslint-disable-next-line max-len
'administration:Max. number of active users'
)}
<sup>
<i
className="fa fa-question-circle"
aria-hidden="true"
title={t('administration:if 0, no limitation')}
/>
</sup>
:
</label>
<input
className="col-sm-5"
id="max_users"
name="max_users"
type="number"
min="0"
value={formData.max_users}
onChange={e => this.handleFormChange(e)}
/>
</div>
<div className="form-group row">
<label
className="col-sm-6 col-form-label"
htmlFor="max_single_file_size"
>
{t(
'administration:Max. size of uploaded files (in Mb)'
)}
:
</label>
<input
className="col-sm-5"
id="max_single_file_size"
name="max_single_file_size"
type="number"
step="0.1"
min="0"
value={formData.max_single_file_size}
onChange={e => this.handleFormChange(e)}
/>
</div>
<div className="form-group row">
<label
className="col-sm-6 col-form-label"
htmlFor="max_zip_file_size"
>
{t('administration:Max. size of zip archive (in Mb)')}:
</label>
<input
className="col-sm-5"
id="max_zip_file_size"
name="max_zip_file_size"
type="number"
step="0.1"
min="0"
value={formData.max_zip_file_size}
onChange={e => this.handleFormChange(e)}
/>
</div>
<div className="form-group row">
<label
className="col-sm-6 col-form-label"
htmlFor="gpx_limit_import"
>
{t('administration:Max. files of zip archive')}
</label>
<input
className="col-sm-5"
id="gpx_limit_import"
name="gpx_limit_import"
type="number"
min="0"
value={formData.gpx_limit_import}
onChange={e => this.handleFormChange(e)}
/>
</div>
{isInEdition ? (
<>
<input
type="submit"
className="btn btn-primary"
value={t('common:Submit')}
/>
<input
type="submit"
className="btn btn-secondary"
onClick={e => {
e.preventDefault()
loadAppConfig()
history.push('/admin/application')
}}
value={t('common:Cancel')}
/>
</>
) : (
<>
<input
type="submit"
className="btn btn-primary"
onClick={e => {
e.preventDefault()
history.push('/admin/application/edit')
}}
value={t('common:Edit')}
/>
<input
type="submit"
className="btn btn-secondary"
onClick={() => history.push('/admin')}
value={t('common:Back')}
/>
</>
)}
</form>
</div>
</div>
</div>
</div>
)}
</div>
)
}
}
export default connect(
state => ({
message: state.message,
messages: state.messages,
}),
dispatch => ({
loadAppConfig: () => {
dispatch(getAppData('config'))
},
onHandleConfigFormSubmit: formData => {
const data = Object.assign({}, formData)
data.max_single_file_size *= 1048576
data.max_zip_file_size *= 1048576
dispatch(updateAppConfig(data))
},
})
)(AdminApplication)

View File

@ -1,71 +0,0 @@
import React from 'react'
import { Link } from 'react-router-dom'
import AdminStats from './AdminStats'
export default function AdminDashboard(props) {
const { appConfig, t } = props
return (
<div className="card workout-card">
<div className="card-header">
<strong>{t('administration:Administration')}</strong>
</div>
<div className="card-body">
<AdminStats />
<br />
<dl className="admin-items">
<dt>
<Link
to={{
pathname: '/admin/application',
}}
>
{t('administration:Application')}
</Link>
</dt>
<dd>
{t(
'administration:Update application configuration ' +
'(maximum number of registered users, maximum files size).'
)}
<br />
<strong>
{t(
`administration:Registration is currently ${
appConfig.is_registration_enabled ? 'enabled' : 'disabled'
}.`
)}
</strong>
</dd>
<br />
<dt>
<Link
to={{
pathname: '/admin/sports',
}}
>
{t('administration:Sports')}
</Link>
</dt>
<dd>{t('administration:Enable/disable sports.')}</dd>
<br />
<dt>
<Link
to={{
pathname: '/admin/users',
}}
>
{t('administration:Users')}
</Link>
</dt>
<dd>
{t(
'administration:Add/remove admin rights, ' +
'delete user account.'
)}
</dd>
</dl>
</div>
</div>
)
}

View File

@ -1,142 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import Message from '../Common/Message'
import { getOrUpdateData } from '../../actions'
import { history } from '../../index'
class AdminSports extends React.Component {
componentDidMount() {
this.props.loadSports()
}
render() {
const { message, sports, t, updateSport } = this.props
return (
<div>
{message && <Message message={message} t={t} />}
<div className="row">
<div className="col">
<div className="card">
<div className="card-header">
<strong>{t('administration:Sports')}</strong>
</div>
<div className="card-body">
{sports.length > 0 && (
<table className="table">
<thead>
<tr>
<th>{t('administration:id')}</th>
<th>{t('administration:Image')}</th>
<th>{t('administration:Label')}</th>
<th>{t('administration:Active')}</th>
<th>{t('administration:Actions')}</th>
</tr>
</thead>
<tbody>
{sports.map(sport => (
<tr key={sport.id}>
<td>
<span className="heading-span-absolute">
{t('administration:id')}
</span>
{sport.id}
</td>
<td>
<span className="heading-span-absolute">
{t('administration:Image')}
</span>
<img
className="admin-img"
src={sport.img ? sport.img : '/img/photo.png'}
alt="sport logo"
/>
</td>
<td>
<span className="heading-span-absolute">
{t('administration:Label')}
</span>
{t(`sports:${sport.label}`)}
</td>
<td>
<span className="heading-span-absolute">
{t('administration:Active')}
</span>
{sport.is_active ? (
<i
className="fa fa-check-square-o custom-fa"
aria-hidden="true"
data-toggle="tooltip"
/>
) : (
<i
className="fa fa-square-o custom-fa"
aria-hidden="true"
data-toggle="tooltip"
/>
)}
</td>
<td>
<span className="heading-span-absolute">
{t('administration:Actions')}
</span>
<input
type="submit"
className={`btn btn-${
sport.is_active ? 'dark' : 'primary'
} btn-sm`}
value={
sport.is_active
? t('administration:Disable')
: t('administration:Enable')
}
onClick={() =>
updateSport(sport.id, !sport.is_active)
}
/>
{sport.has_workouts && (
<span className="admin-message">
<i
className="fa fa-warning custom-fa"
aria-hidden="true"
/>
{t('administration:workouts exist')}
</span>
)}
</td>
</tr>
))}
</tbody>
</table>
)}
<input
type="submit"
className="btn btn-secondary"
onClick={() => history.push('/admin/')}
value={t('common:Back')}
/>
</div>
</div>
</div>
</div>
</div>
)
}
}
export default connect(
state => ({
message: state.message,
sports: state.sports.data,
user: state.user,
}),
dispatch => ({
loadSports: () => {
dispatch(getOrUpdateData('getData', 'sports'))
},
updateSport: (sportId, isActive) => {
const data = { id: sportId, is_active: isActive }
dispatch(getOrUpdateData('updateData', 'sports', data, false))
},
})
)(AdminSports)

View File

@ -1,104 +0,0 @@
import React from 'react'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import { getAppData } from '../../actions/application'
import { getFileSize } from '../../utils'
class AdminStats extends React.Component {
componentDidMount() {
this.props.loadAppStats()
}
render() {
const { appStats, t } = this.props
const uploadDirSize = getFileSize(appStats.uploads_dir_size, false)
return (
<div className="row">
<div className="col-lg-3 col-md-6 col-sm-6">
<div className="card workout-card">
<div className="card-body row">
<div className="col-3">
<i className="fa fa-users fa-3x fa-color" />
</div>
<div className="col-9 text-right">
<div className="huge">
{appStats.users ? appStats.users : 0}
</div>
<div>{`${
appStats.users === 1
? t('administration:user')
: t('administration:users')
}`}</div>
</div>
</div>
</div>
</div>
<div className="col-lg-3 col-md-6 col-sm-6">
<div className="card workout-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">
{appStats.sports ? appStats.sports : 0}
</div>
<div>{`${
appStats.sports === 1 ? t('common:sport') : t('common:sports')
}`}</div>
</div>
</div>
</div>
</div>
<div className="col-lg-3 col-md-6 col-sm-6">
<div className="card workout-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">
{appStats.workouts ? appStats.workouts : 0}
</div>
<div>{`${
appStats.workouts === 1
? t('common:workout')
: t('common:workouts')
}`}</div>
</div>
</div>
</div>
</div>
<div className="col-lg-3 col-md-6 col-sm-6">
<div className="card workout-card">
<div className="card-body row">
<div className="col-3">
<i className="fa fa-folder-open fa-3x fa-color" />
</div>
<div className="col-9 text-right">
<div className="huge">{uploadDirSize.size}</div>
<div>
{uploadDirSize.suffix} ({t('administration:uploads')})
</div>
</div>
</div>
</div>
</div>
</div>
)
}
}
export default withTranslation()(
connect(
state => ({
appStats: state.application.statistics,
}),
dispatch => ({
loadAppStats: () => {
dispatch(getAppData('stats/all'))
},
})
)(AdminStats)
)

View File

@ -1,257 +0,0 @@
import { format } from 'date-fns'
import React from 'react'
import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
import Message from '../Common/Message'
import Pagination from '../Common/Pagination'
import { history } from '../../index'
import { getOrUpdateData } from '../../actions'
import {
apiUrl,
formatUrl,
sortOrders,
translateValues,
userFilters,
} from '../../utils'
class AdminUsers extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
page: null,
per_page: null,
order_by: 'created_at',
order: 'asc',
}
}
componentDidMount() {
this.props.loadUsers(this.initState())
}
componentDidUpdate(prevProps) {
if (prevProps.location.query !== this.props.location.query) {
this.props.loadUsers(this.props.location.query)
}
}
initState() {
const { query } = this.props.location
const newQuery = {
page: query.page,
per_page: query.per_page,
order_by: query.order_by ? query.order_by : 'created_at',
order: query.order ? query.order : 'asc',
}
this.setState(newQuery)
return newQuery
}
updatePage(key, value) {
const query = Object.assign({}, this.state)
query[key] = value
this.setState(query)
const url = formatUrl(this.props.location.pathname, query)
history.push(url)
}
render() {
const { authUser, location, message, t, pagination, updateUser, users } =
this.props
const translatedFilters = translateValues(t, userFilters)
const translatedSortOrders = translateValues(t, sortOrders)
return (
<div>
{message && <Message message={message} t={t} />}
<div className="container">
<div className="row">
<div className="col">
<div className="card">
<div className="card-header">
<strong>{t('administration:Users')}</strong>
</div>
<div className="card-body">
<div className="row user-filters">
<div className="col-lg-4 col-md-6 col-sm-12">
<label htmlFor="order_by">
{t('common:Sort by')}:{' '}
<select
id="order_by"
name="order_by"
value={this.state.order_by}
onChange={e =>
this.updatePage('order_by', e.target.value)
}
>
{translatedFilters.map(filter => (
<option key={filter.key} value={filter.key}>
{filter.label}
</option>
))}
</select>{' '}
</label>
</div>
<div className="col-lg-4 col-md-6 col-sm-12">
<label htmlFor="sort">
{t('common:Sort')}:{' '}
<select
id="sort"
name="sort"
value={this.state.order}
onChange={e =>
this.updatePage('order', e.target.value)
}
>
{translatedSortOrders.map(sort => (
<option key={sort.key} value={sort.key}>
{sort.label}
</option>
))}
</select>{' '}
</label>
</div>
</div>
<table className="table">
<thead>
<tr>
<th>#</th>
<th>{t('user:Username')}</th>
<th>{t('user:Email')}</th>
<th>{t('user:Registration Date')}</th>
<th>{t('workouts:Workouts')}</th>
<th>{t('user:Admin')}</th>
<th>{t('administration:Actions')}</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.username}>
<td>
<span className="heading-span-absolute">#</span>
{user.picture === true ? (
<img
alt="Avatar"
src={`${apiUrl}users/${
user.username
}/picture?${Date.now()}`}
className="img-fluid App-nav-profile-img"
/>
) : (
<i
className="fa fa-user-circle-o fa-2x no-picture"
aria-hidden="true"
/>
)}
</td>
<td>
<span className="heading-span-absolute">
{t('user:Username')}
</span>
<Link to={`/users/${user.username}`}>
{user.username}
</Link>
</td>
<td>
<span className="heading-span-absolute">
{t('user:Email')}
</span>
{user.email}
</td>
<td>
<span className="heading-span-absolute">
{t('user:Registration Date')}
</span>
{format(
new Date(user.created_at),
'dd/MM/yyyy HH:mm'
)}
</td>
<td>
<span className="heading-span-absolute">
{t('workouts:Workouts')}
</span>
{user.nb_workouts}
</td>
<td>
<span className="heading-span-absolute">
{t('user:Admin')}
</span>
{user.admin ? (
<i
className="fa fa-check-square-o custom-fa"
aria-hidden="true"
data-toggle="tooltip"
/>
) : (
<i
className="fa fa-square-o custom-fa"
aria-hidden="true"
data-toggle="tooltip"
/>
)}
</td>
<td>
<span className="heading-span-absolute">
{t('administration:Actions')}
</span>
<input
type="submit"
className={`btn btn-${
user.admin ? 'dark' : 'primary'
} btn-sm`}
disabled={user.username === authUser.username}
value={
user.admin
? t('administration:Remove admin rights')
: t('administration:Add admin rights')
}
onClick={() =>
updateUser(user.username, !user.admin)
}
/>
</td>
</tr>
))}
</tbody>
</table>
<Pagination
pagination={pagination}
pathname={location.pathname}
query={this.state}
t={t}
/>
<input
type="submit"
className="btn btn-secondary"
onClick={() => history.push('/admin/')}
value={t('common:Back')}
/>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
}
export default connect(
state => ({
authUser: state.user,
location: state.router.location,
message: state.message,
pagination: state.users.pagination,
users: state.users.data,
}),
dispatch => ({
loadUsers: query => {
dispatch(getOrUpdateData('getData', 'users', query))
},
updateUser: (userName, isAdmin) => {
const data = { username: userName, admin: isAdmin }
dispatch(getOrUpdateData('updateData', 'users', data, false))
},
})
)(AdminUsers)

View File

@ -1,71 +0,0 @@
import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import { Route, Switch } from 'react-router-dom'
import AdminApplication from './AdminApplication'
import AdminDashboard from './AdminDashboard'
import AdminSports from './AdminSports'
import AdminUsers from './AdminUsers'
import NotFound from './../Others/NotFound'
function Admin(props) {
const { appConfig, t, user } = props
return (
<>
<Helmet>
<title>FitTrackee - {t('administration:Administration')}</title>
</Helmet>
<div className="container dashboard">
{user.admin ? (
<Switch>
<Route
exact
path="/admin"
render={() => <AdminDashboard appConfig={appConfig} t={t} />}
/>
<Route
exact
path="/admin/application"
render={() => (
<AdminApplication
appConfig={appConfig}
t={t}
isInEdition={false}
/>
)}
/>
<Route
exact
path="/admin/application/edit"
render={() => (
<AdminApplication appConfig={appConfig} t={t} isInEdition />
)}
/>
<Route
exact
path="/admin/sports"
render={() => <AdminSports t={t} />}
/>
<Route
exact
path="/admin/users"
render={() => <AdminUsers t={t} />}
/>
<Route component={NotFound} />
</Switch>
) : (
<NotFound />
)}
</div>
</>
)
}
export default withTranslation()(
connect(state => ({
appConfig: state.application.config,
user: state.user,
}))(Admin)
)

View File

@ -1,826 +0,0 @@
html {
height: 100vh;
}
body {
background-color: #eaeaea;
margin: 0;
min-height: 100vh;
padding-bottom: 50px;
position: relative;
}
.App {
padding-bottom: 20px;
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: 32px;
max-height: 32px;
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;
}
input[type="text"], textarea {
width: 100%;
}
label {
width: 100%;
}
.add-workout {
margin-top: 50px;
}
.add-workout-radio {
margin-right: 10px;
}
.admin-img {
max-width: 35px;
max-height: 35px;
}
.admin-items {
list-style-type: square;
}
.admin-message {
color: #7c7c7d;
font-size: 0.9em;
font-style: italic;
margin-left: 10px;
}
.app-config-form label {
font-weight: bold;
}
.btn {
margin-right: 10px;
}
.card {
text-align: left;
}
.chart {
font-size: 0.9em;
}
.chart-workouts {
margin-left: 60px;
}
.chart-arrows {
margin-top: 7px;
}
.chart-filters {
padding-bottom: 10px;
}
.chart-info {
font-size: 0.8em;
font-style: italic;
}
.chart-radio {
display: flex;
font-size: 0.9em;
}
.chart-radio label {
/* display: flex; */
}
.chart-radio input {
margin-right: 10px;
}
.chart-stats {
font-size: 0.8em;
}
.chart-title {
font-size: 1.1em;
margin-bottom: 10px;
}
.col-workout-logo{
padding-right: 0;
}
.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;
}
.custom-fa-small {
font-size: 0.8em;
margin-left: -0.8em;
}
@media only screen and (max-width: 992px) {
.custom-fa-small {
font-size: 0.6em;
}
}
.custom-tooltip {
background-color: #fff;
border: 1px solid lightgrey;
padding: 10px;
}
.custom-tooltip p {
margin: 5px;
}
.custom-tooltip-label {
font-weight: bold;
}
.dashboard {
height: 100%;
}
.dashboard, .history {
margin-top: 30px;
}
.dropdown-wrapper {
width: 50px;
}
.dropdown-list {
background-color: #f8f9fa;
padding: 5px 0;
position: absolute;
text-align: left;
z-index: 10;
}
.dropdown-item {
cursor: default;
font-size: 0.9em;
}
.dropdown-item-selected {
font-weight: bold;
}
.dropdown-item-selected::after {
content: " ✔";
}
.error-message {
margin: 10px 0;
}
.fa-as-link {
cursor:pointer;
color: #40578a;
}
.fa-as-link:hover {
color: #0056b3;
}
.fa-question-circle {
color: #6c757d;
margin-left: 3px;
}
.fa-trophy {
color: goldenrod;
}
.fa-color {
color: #405976;
}
.footer {
background-color: #f8f9fa;
bottom: 0;
color: #8b8c8c;
font-size: 0.9em;
height: 50px;
line-height: 50px;
position: absolute;
width: 100%;
}
/* Chrome, Safari, Edge, Opera */
.form-disabled .form-group input::-webkit-outer-spin-button,
.form-disabled .form-group input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
.form-disabled .form-group input[type=number] {
-moz-appearance: textfield;
}
.form-disabled .form-group input{
border: none;
pointer-events: none;
}
.gpx-file {
height: inherit;
}
.huge {
font-size: 25px;
}
.i18n-flag svg {
height: 100%;
opacity: .9;
width: 15px;
}
.inactive-link {
color: lightgrey;
}
.leaflet-container {
height: 400px;
}
.loader {
animation: spin 2s linear infinite;
border: 8px solid #f3f3f3;
border-top: 8px solid #3498db;
border-radius: 50%;
height: 60px;
margin-left: 41%;
width: 60px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.map-attribution {
bottom: 0;
font-size: 11px;
position: absolute;
}
.map-attribution-text {
background-color: rgba(255, 255, 255, .5);
padding-left: 2px;
padding-right: 2px;
}
.no-picture {
color: #405976;
}
.page-title {
font-size: 2em;
margin: 1em;
text-align: center;
}
.password-forget {
margin: 10px;
font-size: .9em;
font-style: italic;
}
.radioLabel {
text-align: center;
}
.record-logo {
margin-right: 5px;
max-width: 25px;
max-height: 25px;
}
.record-table table, .record-table th, .record-table td{
font-size: 0.85em;
padding: 0.1em;
}
@media only screen and (min-width: 1200px) {
.record-table table, .record-table th, .record-table td{
font-size: 0.9em;
padding: 0.1em;
}
}
.remaining-chars {
font-size: 0.8em;
font-style: italic;
}
.sport-img {
max-width: 35px;
max-height: 35px;
}
.sport-img-medium {
max-width: 45px;
max-height: 45px;
}
.stats-disabled {
opacity: 0.3;
pointer-events: none;
}
.svg-icon {
fill: #405976;
height: 70px;
margin-left: auto;
margin-right: auto;
width: 70px;
}
.time-frames {
align-items: center;
display: inline-flex;
}
.time-frame label {
float: left;
padding: 0 5px;
}
.time-frame label input {
display: none;
}
.time-frame label span {
border: #a9a9a9 solid 1px;
border-radius: 9%;
color: #7b7b7b;
display: block;
font-size: 0.9em;
padding: 2px 6px;
text-align: center;
}
.time-frame input:checked + span {
background-color: #a9a9a9;
color: #ffffff;
}
.timezone-custom {
font-size: .9em !important;
height: inherit !important;
}
.timezone-custom input {
border: 0 !important;
padding: 5px 1px !important;
}
.timezone-custom ul {
background: white;
}
.timezone-picker {
padding: 0;
}
.timezone-picker-textfield {
font-size: 15px;
}
.unlink {
color: black;
}
.user-bio, .workout-notes {
white-space: pre-wrap;
}
.user-filters {
font-size: 0.9em;
margin-bottom: 10px;
}
.user-label {
font-weight: bold;
}
.weather-img {
max-width: 35px;
max-height: 35px;
}
.weather-img-small {
max-width: 20px;
max-height: 20px;
}
.weather-table {
margin-bottom: 0;
}
.weather-table table, .weather-table th, .weather-table td{
font-size: 0.9em;
padding: 0.1em;
}
.workouts-result {
font-size: 0.85em;
}
.workout-card {
margin-bottom: 15px;
}
.col-with-map {
font-size: .87em;
}
@media only screen and (min-width: 1200px) {
.col-with-map {
font-size: 1em;
}
}
.workout-details {
font-size: 0.95em;
}
.workout-date {
font-size: 0.75em;
}
.workout-filter {
font-size: 0.9em;
}
.workout-filter .col-2, .col-5{
padding: 0;
}
.workout-label {
font-size: 0.8em;
color: #666
}
.workout-logo {
margin: 0 5px;
max-width: 20px;
max-height: 20px;
}
.workout-map {
background-color: #eaeaea;
height: 225px;
width: 400px;
}
.workout-no-map {
background-color: #eaeaea;
color: #666666;
font-style: italic;
height: 400px;
line-height: 400px;
}
.workout-notes, .actvity-segments {
font-size: 0.9em;
font-style: italic;
margin-top: 10px;
padding: 5px;
}
.workout-page {
margin-top: 20px;
}
.workout-segments-list {
list-style: square;
}
.workout-sport {
margin-right: 1px;
max-width: 18px;
max-height: 18px;
}
.workout-title img, .workout-title .map-attribution-list {
display: none;
}
.workout-title img {
border: 1px solid lightgrey;
border-radius: 4px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
display: none;
margin-left: 20px;
position: absolute;
z-index: 1000;
}
.workout-title .map-attribution-list {
display: none;
font-size: 11px;
margin-left: 20px;
position: absolute;
z-index: 1000;
}
.workout-title:hover img, .workout-title:hover .map-attribution-list {
display: block;
}
/* responsive table */
/* adapted from https://uglyduck.ca/making-tables-responsive-with-minimal-css/ */
.heading-span,
.heading-span-absolute {
background: #eee;
color: dimgrey;
display: none;
font-size: 10px;
font-weight: bold;
padding: 5px;
text-transform: uppercase;
top: 0;
left: 0;
}
.heading-span-absolute {
position: absolute;
display: none;
}
@media(max-width: 1024px) {
table thead {
left: -9999px;
position: absolute;
visibility: hidden;
}
table tr {
border-bottom: 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-bottom: 40px;
}
table td {
border: 1px solid lightgrey;
margin: 0 -1px -1px 0;
padding-top: 30px !important;
position: relative;
text-align: center;
width: 50%;
}
.record-tr {
margin-bottom: 0;
}
.record-td {
padding-top: 0 !important;
}
.heading-span, .heading-span-absolute {
display: block;
}
}
/* 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);
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);
}
.calendar .body .img-disabled {
opacity: .4;
}
.calendar .body .weekend {
background: #f5f5f5;
}
.calendar .body .today {
background: #eff1f3;
}
.calendar-workout,
.calendar-more {
display: none;
}
.calendar-more {
color: #405976;
font-size: .7em;
margin-left: 0.3em;
}
.calendar-display-more {
background: whitesmoke;
border-radius: 4px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
margin-bottom: 10px;
padding: 10px 15px;
position: absolute;
min-width: 52px;
z-index: 1000;
}
.calendar-workout-more {
display: none;
}
@media only screen and (max-width: 992px) {
.calendar-workout:nth-child(-n+2),
.calendar-workout:nth-child(n+3) ~ .calendar-more,
.calendar-workout-more:nth-child(n+3) {
display: inline-block;
}
}
@media only screen and (min-width: 992px) and (max-width: 1200px) {
.calendar-workout:nth-child(-n+4),
.calendar-workout:nth-child(n+5) ~ .calendar-more,
.calendar-workout-more:nth-child(n+5) {
display: inline-block;
}
}
@media only screen and (min-width: 1200px) {
.calendar-workout:nth-child(-n+6),
.calendar-workout:nth-child(n+7) ~ .calendar-more,
.calendar-workout-more:nth-child(n+7) {
display: inline-block;
}
}

View File

@ -1,90 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import { Route, Switch } from 'react-router-dom'
import './App.css'
import Admin from './Admin'
import Workout from './Workout'
import Workouts from './Workouts'
import CurrentUserProfile from './User/CurrentUserProfile'
import Dashboard from './Dashboard'
import Footer from './Footer'
import Logout from './User/Logout'
import NavBar from './NavBar'
import NotFound from './Others/NotFound'
import PasswordReset from './User/PasswordReset'
import ProfileEdit from './User/ProfileEdit'
import Statistics from './Statistics'
import UserForm from './User/UserForm'
import UserProfile from './User/UserProfile'
import { getAppData } from '../actions/application'
class App extends React.Component {
constructor(props) {
super(props)
this.props = props
}
componentDidMount() {
this.props.loadAppConfig()
}
render() {
return (
<div className="App">
<NavBar />
<Switch>
<Route exact path="/" component={Dashboard} />
<Route
exact
path="/register"
render={() => <UserForm formType={'register'} />}
/>
<Route
exact
path="/login"
render={() => <UserForm formType={'login'} />}
/>
<Route
exact
path="/password-reset"
render={() => <UserForm formType={'password reset'} />}
/>
<Route
exact
path="/password-reset/request"
render={() => <UserForm formType={'reset your password'} />}
/>
<Route
exact
path="/password-reset/sent"
render={() => <PasswordReset action={'sent'} />}
/>
<Route
exact
path="/updated-password"
render={() => <PasswordReset action={'updated'} />}
/>
<Route exact path="/password-reset/sent" component={PasswordReset} />
<Route exact path="/logout" component={Logout} />
<Route exact path="/profile/edit" component={ProfileEdit} />
<Route exact path="/profile" component={CurrentUserProfile} />
<Route exact path="/workouts/history" component={Workouts} />
<Route exact path="/workouts/statistics" component={Statistics} />
<Route exact path="/users/:userName" component={UserProfile} />
<Route path="/workouts" component={Workout} />
<Route path="/admin" component={Admin} />
<Route component={NotFound} />
</Switch>
<Footer />
</div>
)
}
}
export default connect(
() => ({}),
dispatch => ({
loadAppConfig: () => {
dispatch(getAppData('config'))
},
})
)(App)

View File

@ -1,9 +0,0 @@
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)
})

View File

@ -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">&times;</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>
)
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,198 +0,0 @@
// eslint-disable-next-line max-len
// source: https://blog.flowandform.agency/create-a-custom-calendar-in-react-3df1bfd0b728
import {
addDays,
addMonths,
endOfMonth,
endOfWeek,
format,
isSameDay,
isSameMonth,
isToday,
startOfMonth,
startOfWeek,
subMonths,
} from 'date-fns'
import { enGB, fr } from 'date-fns/locale'
import React from 'react'
import { connect } from 'react-redux'
import CalendarWorkouts from './CalendarWorkouts'
import { getMonthWorkouts } from '../../actions/workouts'
import { getDateWithTZ } from '../../utils'
const getStartAndEndMonth = (date, weekStartOnMonday) => {
const monthStart = startOfMonth(date)
const monthEnd = endOfMonth(date)
const weekStartsOn = weekStartOnMonday ? 1 : 0
return {
start: startOfWeek(monthStart, { weekStartsOn }),
end: endOfWeek(monthEnd),
}
}
class Calendar extends React.Component {
constructor(props, context) {
super(props, context)
const calendarDate = new Date()
this.state = {
currentMonth: calendarDate,
startDate: getStartAndEndMonth(calendarDate, props.weekm).start,
endDate: getStartAndEndMonth(calendarDate, props.weekm).end,
weekStartOnMonday: props.weekm,
}
}
componentDidMount() {
this.props.loadMonthWorkouts(this.state.startDate, this.state.endDate)
}
renderHeader(localeOptions) {
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>
{format(this.state.currentMonth, dateFormat, localeOptions)}
</span>
</div>
<div className="col col-end" onClick={() => this.handleNextMonth()}>
<i className="fa fa-chevron-right" aria-hidden="true" />
</div>
</div>
)
}
renderDays(localeOptions) {
const dateFormat = 'EEE'
const days = []
const { startDate } = this.state
for (let i = 0; i < 7; i++) {
days.push(
<div className="col col-center" key={i}>
{format(addDays(startDate, i), dateFormat, localeOptions)}
</div>
)
}
return <div className="days row">{days}</div>
}
filterWorkouts(day) {
const { workouts, user } = this.props
if (workouts) {
return workouts
.filter(act =>
isSameDay(getDateWithTZ(act.workout_date, user.timezone), day)
)
.reverse()
}
return []
}
renderCells() {
const { currentMonth, startDate, endDate, weekStartOnMonday } = 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 = format(day, dateFormat)
const dayWorkouts = this.filterWorkouts(day)
const isDisabled = isSameMonth(day, currentMonth) ? '' : '-disabled'
const isWeekEnd = weekStartOnMonday
? [5, 6].includes(i)
: [0, 6].includes(i)
days.push(
<div
className={`col cell ${isWeekEnd ? ' weekend' : ''}${
isToday(day) ? ' today' : ''
}`}
key={day}
>
<div className={`img${isDisabled}`}>
<span className="number">{formattedDate}</span>
<CalendarWorkouts
dayWorkouts={dayWorkouts}
isDisabled={isDisabled}
sports={sports}
/>
</div>
</div>
)
day = 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.state.weekStartOnMonday
)
this.setState({
currentMonth: calendarDate,
startDate: start,
endDate: end,
})
this.props.loadMonthWorkouts(start, end)
}
handleNextMonth() {
const calendarDate = addMonths(this.state.currentMonth, 1)
this.updateStateDate(calendarDate)
}
handlePrevMonth() {
const calendarDate = subMonths(this.state.currentMonth, 1)
this.updateStateDate(calendarDate)
}
render() {
const localeOptions = {
locale: this.props.language === 'fr' ? fr : enGB,
}
return (
<div className="card workout-card">
<div className="calendar">
{this.renderHeader(localeOptions)}
{this.renderDays(localeOptions)}
{this.renderCells()}
</div>
</div>
)
}
}
export default connect(
state => ({
workouts: state.calendarWorkouts.data,
language: state.language,
sports: state.sports.data,
user: state.user,
}),
dispatch => ({
loadMonthWorkouts: (start, end) => {
const dateFormat = 'yyyy-MM-dd'
dispatch(
getMonthWorkouts(format(start, dateFormat), format(end, dateFormat))
)
},
})
)(Calendar)

View File

@ -1,39 +0,0 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { recordsLabels } from '../../utils/workouts'
export default function CalendarWorkout(props) {
const { isDisabled, isMore, sportImg, workout } = props
return (
<Link
className={`calendar-workout${isMore}`}
to={`/workouts/${workout.id}`}
>
<>
<img
alt="workout sport logo"
className={`workout-sport ${isDisabled}`}
src={sportImg}
title={workout.title}
/>
{workout.records.length > 0 && (
<sup>
<i
className="fa fa-trophy custom-fa-small"
aria-hidden="true"
title={workout.records.map(
rec =>
` ${
recordsLabels.filter(
r => r.record_type === rec.record_type
)[0].label
}`
)}
/>
</sup>
)}
</>
</Link>
)
}

View File

@ -1,59 +0,0 @@
import React from 'react'
import CalendarWorkout from './CalendarWorkout'
export default class CalendarWorkouts extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
isHidden: true,
}
}
handleDisplayMore() {
this.setState({
isHidden: !this.state.isHidden,
})
}
render() {
const { dayWorkouts, isDisabled, sports } = this.props
const { isHidden } = this.state
return (
<div>
{dayWorkouts.map(act => (
<CalendarWorkout
key={act.id}
workout={act}
isDisabled={isDisabled}
isMore=""
sportImg={sports.filter(s => s.id === act.sport_id).map(s => s.img)}
/>
))}
{dayWorkouts.length > 2 && (
<i
className={`fa fa-${isHidden ? 'plus' : 'times'} calendar-more`}
aria-hidden="true"
onClick={() => this.handleDisplayMore()}
title="show more workouts"
/>
)}
{!isHidden && (
<div className="calendar-display-more">
{dayWorkouts.map(act => (
<CalendarWorkout
key={act.id}
workout={act}
isDisabled={isDisabled}
isMore="-more"
sportImg={sports
.filter(s => s.id === act.sport_id)
.map(s => s.img)}
/>
))}
</div>
)}
</div>
)
}
}

View File

@ -1,74 +0,0 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { formatRecord, translateSports } from '../../utils/workouts'
export default function RecordsCard(props) {
const { records, sports, t, user } = props
const translatedSports = translateSports(sports, t)
const recordsBySport = records.reduce((sportList, record) => {
const sport = translatedSports.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, user.timezone))
return sportList
}, {})
return (
<div className="card workout-card">
<div className="card-header">{t('workouts:Personal records')}</div>
<div className="card-body">
{Object.keys(recordsBySport).length === 0
? t('common:No records.')
: Object.keys(recordsBySport)
.sort()
.map(sportLabel => (
<div key={sportLabel}>
<span className="heading-span">
<img
alt={`${sportLabel} logo`}
className="record-logo"
src={recordsBySport[sportLabel].img}
/>
{sportLabel}
</span>
{/* eslint-disable-next-line max-len */}
<table className="table table-borderless table-sm record-table">
<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 className="record-tr" key={rec.id}>
<td className="record-td">
{t(`workouts:${rec.record_type}`)}
</td>
<td className="record-td text-right">{rec.value}</td>
<td className="record-td text-right">
<Link to={`/workouts/${rec.workout_id}`}>
{rec.workout_date}
</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
))}
</div>
</div>
)
}

View File

@ -1,34 +0,0 @@
import { endOfMonth, startOfMonth } from 'date-fns'
import React from 'react'
import Stats from '../Common/Stats'
export default class Statistics extends React.Component {
constructor(props, context) {
super(props, context)
const date = new Date()
this.state = {
start: startOfMonth(date),
end: endOfMonth(date),
duration: 'week',
type: 'by_time',
}
}
render() {
const { t } = this.props
return (
<div className="card workout-card">
<div className="card-header">{t('dashboard:This month')}</div>
<div className="card-body">
<Stats
displayEmpty={false}
statsParams={this.state}
t={t}
withElevation={false}
/>
</div>
</div>
)
}
}

View File

@ -1,78 +0,0 @@
import React from 'react'
export default function UserStatistics(props) {
const { t, user } = props
const days = user.total_duration.match(/day/g)
? `${user.total_duration.split(' ')[0]} ${
user.total_duration.match(/days/g) ? t('common:days') : t('common:day')
}`
: `0 ${t('common:days')},`
let duration = user.total_duration.match(/day/g)
? user.total_duration.split(', ')[1]
: user.total_duration
duration = `${duration.split(':')[0]}h ${duration.split(':')[1]}min`
return (
<div className="row">
<div className="col-lg-3 col-md-6 col-sm-6">
<div className="card workout-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.nb_workouts}</div>
<div>{`${
user.nb_workouts === 1
? t('common:workout')
: t('common:workouts')
}`}</div>
</div>
</div>
</div>
</div>
<div className="col-lg-3 col-md-6 col-sm-6">
<div className="card workout-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">
{Number(user.total_distance).toFixed(2)}
</div>
<div>km</div>
</div>
</div>
</div>
</div>
<div className="col-lg-3 col-md-6 col-sm-6">
<div className="card workout-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">{days}</div>
<div>{duration}</div>
</div>
</div>
</div>
</div>
<div className="col-lg-3 col-md-6 col-sm-6">
<div className="card workout-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.nb_sports}</div>
<div>{`${
user.nb_sports === 1 ? t('common:sport') : t('common:sports')
}`}</div>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -1,70 +0,0 @@
import { format } from 'date-fns'
import React from 'react'
import { Link } from 'react-router-dom'
import StaticMap from '../Common/StaticMap'
import { getDateWithTZ } from '../../utils'
export default function WorkoutCard(props) {
const { sports, t, user, workout } = props
return (
<div className="card workout-card text-center">
<div className="card-header">
<Link to={`/workouts/${workout.id}`}>
{sports
.filter(sport => sport.id === workout.sport_id)
.map(sport => t(`sports:${sport.label}`))}{' '}
-{' '}
{format(
getDateWithTZ(workout.workout_date, user.timezone),
'dd/MM/yyyy HH:mm'
)}
</Link>
</div>
<div className="card-body">
<div className="row">
{workout.map && (
<div className="col">
<StaticMap workout={workout} />
</div>
)}
<div className={`col${workout.map ? ' col-with-map' : ''}`}>
<p>
<i className="fa fa-clock-o" aria-hidden="true" />{' '}
{t('workouts:Duration')}: {workout.moving}
{workout.map ? (
<span>
<br />
<br />
</span>
) : (
' - '
)}
<i className="fa fa-road" aria-hidden="true" />{' '}
{t('workouts:Distance')}: {workout.distance} km
<br />
</p>
{workout.min_alt && workout.max_alt && (
<p>
<i className="fi-mountains custom-fa" />
{t('workouts:Min. altitude')}: {workout.min_alt}m
<br />
{t('workouts:Max. altitude')}: {workout.max_alt}m
<br />
</p>
)}
{workout.ascent && workout.descent && (
<p>
<i className="fa fa-location-arrow custom-fa" />
{t('workouts:Ascent')}: {workout.ascent}m
<br />
{t('workouts:Descent')}: {workout.descent}m
</p>
)}
</div>
</div>
</div>
</div>
)
}

View File

@ -1,114 +0,0 @@
import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import Calendar from './Calendar'
import Message from '../Common/Message'
import NoWorkouts from '../Common/NoWorkouts'
import Records from './Records'
import Statistics from './Statistics'
import UserStatistics from './UserStatistics'
import WorkoutCard from './WorkoutCard'
import { getOrUpdateData } from '../../actions'
import { getMoreWorkouts } from '../../actions/workouts'
class DashBoard extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
page: 1,
}
}
componentDidMount() {
this.props.loadWorkouts()
}
render() {
const { loadMoreWorkouts, message, records, sports, t, user, workouts } =
this.props
const paginationEnd =
workouts.length > 0
? workouts[workouts.length - 1].previous_workout === null
: true
const { page } = this.state
return (
<div>
<Helmet>
<title>FitTrackee - {t('common:Dashboard')}</title>
</Helmet>
{message ? (
<Message message={message} t={t} />
) : (
workouts &&
user.total_duration &&
sports.length > 0 && (
<div className="container dashboard">
<UserStatistics user={user} t={t} />
<div className="row">
<div className="col-md-4">
<Statistics t={t} />
<Records
t={t}
records={records}
sports={sports}
user={user}
/>
</div>
<div className="col-md-8">
<Calendar weekm={user.weekm} />
{workouts.length > 0 ? (
workouts.map(workout => (
<WorkoutCard
workout={workout}
key={workout.id}
sports={sports}
t={t}
user={user}
/>
))
) : (
<NoWorkouts t={t} />
)}
{!paginationEnd && (
<input
type="submit"
className="btn btn-default btn-md btn-block"
value="Load more workouts"
onClick={() => {
loadMoreWorkouts(page + 1)
this.setState({ page: page + 1 })
}}
/>
)}
</div>
</div>
</div>
)
)}
</div>
)
}
}
export default withTranslation()(
connect(
state => ({
workouts: state.workouts.data,
message: state.message,
records: state.records.data,
sports: state.sports.data,
user: state.user,
}),
dispatch => ({
loadWorkouts: () => {
dispatch(getOrUpdateData('getData', 'workouts', { page: 1 }))
dispatch(getOrUpdateData('getData', 'records'))
},
loadMoreWorkouts: page => {
dispatch(getMoreWorkouts({ page }))
},
})
)(DashBoard)
)

View File

@ -1,36 +0,0 @@
import React from 'react'
import { version } from './../../utils'
export default function Footer() {
return (
<footer className="footer">
<div className="container">
<strong>FitTrackee</strong> v{version} -{' '}
<a
href="https://github.com/SamR1/FitTrackee"
target="_blank"
rel="noopener noreferrer"
>
source code
</a>{' '}
under{' '}
<a
href="https://choosealicense.com/licenses/gpl-3.0/"
target="_blank"
rel="noopener noreferrer"
>
GPLv3
</a>{' '}
license -{' '}
<a
href="https://samr1.github.io/FitTrackee/"
target="_blank"
rel="noopener noreferrer"
>
documentation
</a>
</div>
</footer>
)
}

View File

@ -1,75 +0,0 @@
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { ReactComponent as EnFlag } from '../../images/flags/en.svg'
import { ReactComponent as FrFlag } from '../../images/flags/fr.svg'
import { updateLanguage } from '../../actions/index'
export const languages = [
{
name: 'en',
selected: true,
flag: <EnFlag />,
},
{
name: 'fr',
selected: false,
flag: <FrFlag />,
},
]
class Dropdown extends Component {
constructor(props) {
super(props)
this.state = {
isOpen: false,
}
}
toggleDropdown() {
this.setState(prevState => ({
isOpen: !prevState.isOpen,
}))
}
render() {
const { isOpen } = this.state
const { language: selected, onUpdateLanguage } = this.props
return (
<div className="dropdown-wrapper" onClick={() => this.toggleDropdown()}>
<ul className="dropdown-list i18n-flag">
{languages
.filter(language =>
isOpen ? language : language.name === selected
)
.map(language => (
<li
className={`dropdown-item${
language.name === selected && isOpen
? ' dropdown-item-selected'
: ''
}`}
key={language.name}
onClick={() => onUpdateLanguage(language.name, selected)}
>
{language.flag} {language.name}
</li>
))}
</ul>
</div>
)
}
}
export default connect(
state => ({
language: state.language,
}),
dispatch => ({
onUpdateLanguage: (lang, selected) => {
if (lang !== selected) {
dispatch(updateLanguage(lang))
}
},
})
)(Dropdown)

View File

@ -1,173 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import { withTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import LanguageDropdown from './LanguageDropdown'
import { apiUrl } from '../../utils'
class NavBar extends React.PureComponent {
render() {
const { admin, isAuthenticated, picture, t, username } = this.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: '/',
}}
>
{t('common:Dashboard')}
</Link>
</li>
{isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/workouts/history',
}}
>
{t('Workouts')}
</Link>
</li>
)}
{isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/workouts/statistics',
}}
>
{t('common:Statistics')}
</Link>
</li>
)}
{admin && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/admin',
}}
>
Admin
</Link>
</li>
)}
{isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/workouts/add',
}}
>
<strong>{t('common:Add workout')}</strong>
</Link>
</li>
)}
</ul>
{/* prettier-ignore */}
<ul
className="navbar-nav flex-row ml-md-auto d-none d-md-flex"
>
{!isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/register',
}}
>
{t('user:Register')}
</Link>
</li>
)}
{!isAuthenticated && (
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/login',
}}
>
{t('user:Login')}
</Link>
</li>
)}
{isAuthenticated && (
<>
{picture === true ? (
<img
alt="Avatar"
src={`${apiUrl}users/${username}/picture?${Date.now()}`}
className="img-fluid App-nav-profile-img"
/>
) : (
<i
className="fa fa-user-circle-o fa-2x no-picture"
aria-hidden="true"
/>
)}
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/profile',
}}
>
{username}
</Link>
</li>
<li className="nav-item">
<Link
className="nav-link"
to={{
pathname: '/logout',
}}
>
{t('user:Logout')}
</Link>
</li>
</>
)}
<li><LanguageDropdown /></li>
</ul>
</div>
</div>
</nav>
</header>
)
}
}
export default withTranslation()(
connect(({ user }) => ({
admin: user.admin,
isAuthenticated: user.isAuthenticated,
picture: user.picture,
username: user.username,
}))(NavBar)
)

View File

@ -1,24 +0,0 @@
import React from 'react'
import { Helmet } from 'react-helmet'
export default function AccessDenied(props) {
const { t } = props
return (
<div>
<Helmet>
<title>FitTrackee - {t('Access denied')}</title>
</Helmet>
<div className="row">
<div className="col-2" />
<div className="card col-8">
<div className="card-body">
<div className="text-center">
{t("You don't have permissions to access this page.")}
</div>
</div>
</div>
<div className="col-2" />
</div>
</div>
)
}

View File

@ -1,15 +0,0 @@
import React from 'react'
import { Helmet } from 'react-helmet'
import { useTranslation } from 'react-i18next'
export default function NotFound() {
const { t } = useTranslation()
return (
<div>
<Helmet>
<title>fittrackee - 404</title>
</Helmet>
<h1 className="page-title">{t('Page not found')}</h1>
</div>
)
}

View File

@ -1,11 +0,0 @@
import React from 'react'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router'
export default function Root({ store, history, children }) {
return (
<Provider store={store}>
<ConnectedRouter history={history}>{children}</ConnectedRouter>
</Provider>
)
}

View File

@ -1,209 +0,0 @@
import {
endOfMonth,
endOfWeek,
endOfYear,
startOfMonth,
startOfYear,
startOfWeek,
addMonths,
addWeeks,
addYears,
subMonths,
subWeeks,
subYears,
} from 'date-fns'
import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import NoWorkouts from '../Common/NoWorkouts'
import Stats from '../Common/Stats'
import { workoutColors, translateSports } from '../../utils/workouts'
const durations = ['week', 'month', 'year']
class Statistics extends React.Component {
constructor(props, context) {
super(props, context)
const date = new Date()
this.state = {
displayedSports: props.sports.map(sport => sport.id),
statsParams: {
start: startOfMonth(subMonths(date, 11)),
end: endOfMonth(date),
duration: 'month',
type: 'by_time',
},
}
}
componentDidUpdate(prevProps) {
if (this.props.sports !== prevProps.sports) {
this.updateDisplayedSports()
}
}
updateDisplayedSports() {
const { sports } = this.props
this.setState({ displayedSports: sports.map(sport => sport.id) })
}
handleOnChangeDuration(e) {
const duration = e.target.name
const date = new Date()
const start =
duration === 'year'
? startOfYear(subYears(date, 9))
: duration === 'week'
? startOfMonth(subMonths(date, 2))
: startOfMonth(subMonths(date, 11))
const end =
duration === 'year'
? endOfYear(date)
: duration === 'week'
? endOfWeek(date)
: endOfMonth(date)
this.setState({ statsParams: { duration, end, start, type: 'by_time' } })
}
handleOnChangeSports(sportId) {
const { displayedSports } = this.state
if (displayedSports.includes(sportId)) {
this.setState({
displayedSports: displayedSports.filter(s => s !== sportId),
})
} else {
this.setState({ displayedSports: displayedSports.concat([sportId]) })
}
}
handleOnClickArrows(forward) {
const { start, end, duration } = this.state.statsParams
let newStart, newEnd
if (forward) {
newStart =
duration === 'year'
? startOfYear(subYears(start, 1))
: duration === 'week'
? startOfWeek(subWeeks(start, 1))
: startOfMonth(subMonths(start, 1))
newEnd =
duration === 'year'
? endOfYear(subYears(end, 1))
: duration === 'week'
? endOfWeek(subWeeks(end, 1))
: endOfMonth(subMonths(end, 1))
} else {
newStart =
duration === 'year'
? startOfYear(addYears(start, 1))
: duration === 'week'
? startOfWeek(addWeeks(start, 1))
: startOfMonth(addMonths(start, 1))
newEnd =
duration === 'year'
? endOfYear(addYears(end, 1))
: duration === 'week'
? endOfWeek(addWeeks(end, 1))
: endOfMonth(addMonths(end, 1))
}
this.setState({
statsParams: { duration, end: newEnd, start: newStart, type: 'by_time' },
})
}
render() {
const { displayedSports, statsParams } = this.state
const { sports, t, user } = this.props
const translatedSports = translateSports(
sports.filter(sport => user.sports_list.includes(sport.id)),
t
)
return (
<>
<Helmet>
<title>FitTrackee - {t('statistics:Statistics')}</title>
</Helmet>
<div className="container dashboard">
<div className="card workout-card">
<div className="card-header">{t('statistics:Statistics')}</div>
<div
className={`card-body${
user.nb_workouts === 0 ? ' stats-disabled' : ''
}`}
>
<div className="chart-filters row">
<div className="col chart-arrows">
<p className="text-center">
<i
className="fa fa-chevron-left"
aria-hidden="true"
onClick={() => this.handleOnClickArrows(true)}
/>
</p>
</div>
<div className="col-md-3 time-frames justify-content-around">
{durations.map(d => (
<div className="time-frame" key={d}>
<label>
<input
type="radio"
id={d}
name={d}
checked={d === statsParams.duration}
onChange={e => this.handleOnChangeDuration(e)}
/>
<span>{t(`statistics:${d}`)}</span>
</label>
</div>
))}
</div>
<div className="col chart-arrows">
<p className="text-center">
<i
className="fa fa-chevron-right"
aria-hidden="true"
onClick={() => this.handleOnClickArrows(false)}
/>
</p>
</div>
</div>
<Stats
displayEmpty
displayedSports={displayedSports}
statsParams={statsParams}
t={t}
withElevation
/>
<div className="row chart-workouts">
{translatedSports.map(sport => (
<label className="col workout-label" key={sport.id}>
<input
type="checkbox"
checked={displayedSports.includes(sport.id)}
name={sport.label}
onChange={() => this.handleOnChangeSports(sport.id)}
/>
<span style={{ color: workoutColors[sport.id - 1] }}>
{` ${sport.label}`}
</span>
</label>
))}
</div>
</div>
</div>
{user.nb_workouts === 0 && <NoWorkouts t={t} />}
</div>
</>
)
}
}
export default withTranslation()(
connect(state => ({
sports: state.sports.data,
user: state.user,
}))(Statistics)
)

View File

@ -1,19 +0,0 @@
import React from 'react'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import ProfileDetail from './ProfileDetail'
function CurrentUserProfile({ t, user }) {
return (
<div>
<ProfileDetail editable t={t} user={user} />
</div>
)
}
export default withTranslation()(
connect(state => ({
user: state.user,
}))(CurrentUserProfile)
)

View File

@ -1,125 +0,0 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Helmet } from 'react-helmet'
import { Link } from 'react-router-dom'
import { history } from '../../index'
export default function Form(props) {
const { t } = useTranslation()
const pageTitle = `user:${props.formType
.charAt(0)
.toUpperCase()}${props.formType.slice(1)}`
return (
<div>
<Helmet>
<title>FitTrackee - {t(`user:${props.formType}`)}</title>
</Helmet>
<h1 className="page-title">{t(pageTitle)}</h1>
<div className="container">
<div className="row">
<div className="col-md-3" />
<div className="col-md-6">
<br />
{props.formType === 'register' && !props.isRegistrationAllowed ? (
<div className="card">
<div className="card-body">Registration is disabled.</div>
<div className="card-body">
<button
type="submit"
className="btn btn-secondary btn-lg btn-block"
onClick={() => history.go(-1)}
>
Back
</button>
</div>
</div>
) : (
<>
<form
onSubmit={event =>
props.handleUserFormSubmit(event, props.formType)
}
>
{props.formType === 'register' && (
<div className="form-group">
<input
className="form-control input-lg"
name="username"
placeholder={t('user:Enter a username')}
required
type="text"
value={props.userForm.username}
onChange={props.onHandleFormChange}
/>
</div>
)}
{props.formType !== 'password reset' && (
<div className="form-group">
<input
className="form-control input-lg"
name="email"
placeholder={t('user:Enter an email address')}
required
type="email"
value={props.userForm.email}
onChange={props.onHandleFormChange}
/>
</div>
)}
{props.formType !== 'reset your password' && (
<>
<div className="form-group">
<input
className="form-control input-lg"
name="password"
placeholder={t('user:Enter a password')}
required
type="password"
value={props.userForm.password}
onChange={props.onHandleFormChange}
/>
</div>
{props.formType !== 'login' && (
<div className="form-group">
<input
className="form-control input-lg"
name="password_conf"
placeholder={t(
'user:Enter the password confirmation'
)}
required
type="password"
value={props.userForm.password_conf}
onChange={props.onHandleFormChange}
/>
</div>
)}
</>
)}
<input
type="submit"
className="btn btn-primary btn-lg btn-block"
value={t('Submit')}
/>
</form>
<p className="password-forget">
{props.formType === 'login' && (
<Link
to={{
pathname: '/password-reset/request',
}}
>
{t('user:Forgot password?')}
</Link>
)}
</p>
</>
)}
</div>
<div className="col-md-3" />
</div>
</div>
</div>
)
}

View File

@ -1,43 +0,0 @@
import React from 'react'
import { Trans } from 'react-i18next'
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 className="container dashboard">
<div className="row">
<div className="col-2" />
<div className="card col-8">
<div className="card-body">
<div className="text-center">
<Trans i18nKey="user:loggedOut">
You are now logged out. Click <Link to="/login">here</Link> to
log back in.
</Trans>
</div>
</div>
</div>
<div className="col-2" />
</div>
</div>
)
}
}
export default connect(
state => ({
user: state.user,
}),
dispatch => ({
UserLogout: () => {
dispatch(logout())
},
})
)(Logout)

View File

@ -1,48 +0,0 @@
import React from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import { ReactComponent as Password } from '../../images/password.svg'
import { ReactComponent as MailSend } from '../../images/mail-send.svg'
export default function PasswordReset(props) {
const { t } = useTranslation()
const { action } = props
return (
<div className="container dashboard">
<div className="row">
<div className="col-2" />
<div className="card col-8">
<div className="card-body">
<div className="text-center ">
{action === 'sent' && (
<>
<div className="svg-icon">
<MailSend />
</div>
{t(
// eslint-disable-next-line max-len
"user:Check your email. If your address is in our database, you'll received an email with a link to reset your password."
)}
</>
)}
{action === 'updated' && (
<>
<div className="svg-icon">
<Password />
</div>
<Trans i18nKey="user:updatedPasswordText">
{/* prettier-ignore */}
Your password have been updated. Click
<Link to="/login">here</Link> to log in.
</Trans>
</>
)}
</div>
</div>
</div>
<div className="col-2" />
</div>
</div>
)
}

View File

@ -1,199 +0,0 @@
import { format } from 'date-fns'
import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import Message from '../Common/Message'
import { deletePicture, uploadPicture } from '../../actions/user'
import { apiUrl, getFileSize } from '../../utils'
import { history } from '../../index'
function ProfileDetail({
appConfig,
displayModal,
editable,
isDeletable,
message,
onDeletePicture,
onUploadPicture,
pathname,
t,
user,
}) {
const createdAt = user.created_at
? format(new Date(user.created_at), 'dd/MM/yyyy HH:mm')
: ''
const birthDate = user.birth_date
? format(new Date(user.birth_date), 'dd/MM/yyyy')
: ''
const fileSizeLimit = getFileSize(appConfig.max_single_file_size)
return (
<div>
<Helmet>
<title>FitTrackee - {t('user:Profile')}</title>
</Helmet>
<Message message={message} t={t} />
<div className="container">
<h1 className="page-title">{t('user:Profile')}</h1>
<div className="row">
<div className="col-md-12">
<div className="card">
<div className="card-header userName">
<strong>{user.username}</strong>
</div>
<div className="card-body">
<div className="row">
<div className="col-md-8">
<p>
{/* eslint-disable-next-line max-len */}
<span className="user-label">
{t('user:Email')}
</span>: {user.email}
</p>
<p>
<span className="user-label">
{t('user:Registration Date')}
</span>
: {createdAt}
</p>
<p>
<span className="user-label">{t('user:First Name')}</span>
: {user.first_name}
</p>
<p>
{/* eslint-disable-next-line max-len */}
<span className="user-label">
{t('user:Last Name')}
</span>: {user.last_name}
</p>
<p>
<span className="user-label">{t('user:Birth Date')}</span>
: {birthDate}
</p>
<p>
{/* eslint-disable-next-line max-len */}
<span className="user-label">
{t('user:Location')}
</span>: {user.location}
</p>
<p>
<span className="user-label">{t('user:Bio')}</span>:{' '}
<span className="user-bio">{user.bio}</span>
</p>
<p>
{/* eslint-disable-next-line max-len */}
<span className="user-label">
{t('user:Language')}
</span>: {user.language}
</p>
<p>
{/* eslint-disable-next-line max-len */}
<span className="user-label">
{t('user:Timezone')}
</span>: {user.timezone}
</p>
<p>
<span className="user-label">
{t('user:First day of week')}
</span>
: {user.weekm ? t('user:Monday') : t('user:Sunday')}
</p>
</div>
<div className="col-md-4">
{user.picture === true && (
<div>
<img
alt="Profile"
src={
`${apiUrl}users/${user.username}/picture` +
`?${Date.now()}`
}
className="img-fluid App-profile-img-small"
/>
{editable && (
<>
<br />
<button
type="submit"
onClick={() => onDeletePicture()}
>
{t('user:Delete picture')}
</button>
<br />
<br />
</>
)}
</div>
)}
{editable && (
<form
encType="multipart/form-data"
onSubmit={event => onUploadPicture(event)}
>
<input
type="file"
name="picture"
accept=".png,.jpg,.gif"
/>
<br />
<button type="submit">{t('user:Send')}</button>
{` (max. size: ${fileSizeLimit})`}
</form>
)}{' '}
</div>
</div>
{editable && (
<button
className="btn btn-primary"
onClick={() => history.push('/profile/edit')}
>
{t('common:Edit')}
</button>
)}
{isDeletable && (
<button
className="btn btn-danger"
onClick={() => displayModal(true)}
>
{t('user:Delete user account')}
</button>
)}
<button
className="btn btn-secondary"
onClick={() =>
pathname === '/profile' ? history.push('/') : history.go(-1)
}
>
{t(
pathname === '/profile'
? 'common:Back to home'
: 'common:Back'
)}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
export default withTranslation()(
connect(
state => ({
appConfig: state.application.config,
pathname: state.router.location.pathname,
message: state.message,
}),
dispatch => ({
onDeletePicture: () => {
dispatch(deletePicture())
},
onUploadPicture: event => {
dispatch(uploadPicture(event))
},
})
)(ProfileDetail)
)

View File

@ -1,314 +0,0 @@
import { format } from 'date-fns'
import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import TimezonePicker from 'react-timezone'
import Message from '../Common/Message'
import { deleteUser, handleProfileFormSubmit } from '../../actions/user'
import { history } from '../../index'
import { languages } from '../NavBar/LanguageDropdown'
import CustomModal from '../Common/CustomModal'
import CustomTextArea from '../Common/CustomTextArea'
class ProfileEdit extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
formData: {},
displayModal: false,
}
}
componentDidMount() {
this.initForm()
}
componentDidUpdate(prevProps) {
if (prevProps.user !== this.props.user) {
this.initForm()
}
}
initForm() {
const { user } = this.props
const formData = {}
Object.keys(user).map(k =>
user[k] === null
? (formData[k] = '')
: k === 'birth_date'
? (formData[k] = format(new Date(user[k]), 'yyyy-MM-dd'))
: (formData[k] = user[k])
)
this.setState({ formData })
}
handleFormChange(e) {
const { formData } = this.state
if (e.target.name === 'weekm') {
formData.weekm = e.target.value === 'Monday'
} else {
formData[e.target.name] = e.target.value
}
this.setState(formData)
}
displayModal(value) {
this.setState(prevState => ({
...prevState,
displayModal: value,
}))
}
render() {
const { message, onDeleteUser, onHandleProfileFormSubmit, t, user } =
this.props
const { displayModal, formData } = this.state
return (
<div>
<Helmet>
<title>FitTrackee - {t('user:Profile Edition')}</title>
</Helmet>
{formData.isAuthenticated && (
<div className="container">
{displayModal && (
<CustomModal
title={t('common:Confirmation')}
text={t(
'user:Are you sure you want to delete your account? ' +
'All data will be deleted, this cannot be undone.'
)}
confirm={() => {
onDeleteUser(user.username)
this.displayModal(false)
}}
close={() => this.displayModal(false)}
/>
)}
<h1 className="page-title">{t('user: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 => {
event.preventDefault()
onHandleProfileFormSubmit(formData)
}}
>
<div className="form-group">
<label>
{t('user:Email')}:
<input
name="email"
className="form-control input-lg"
type="text"
value={formData.email}
readOnly
/>
</label>
</div>
<div className="form-group">
<label>
{t('user:Registration Date')}:
<input
name="createdAt"
className="form-control input-lg"
type="text"
value={formData.created_at}
disabled
/>
</label>
</div>
<div className="form-group">
<label>
{t('user:Password')}:
<input
name="password"
className="form-control input-lg"
type="password"
onChange={e => this.handleFormChange(e)}
/>
</label>
</div>
<div className="form-group">
<label>
{t('user:Password Confirmation')}:
<input
name="password_conf"
className="form-control input-lg"
type="password"
onChange={e => this.handleFormChange(e)}
/>
</label>
</div>
<hr />
<div className="form-group">
<label>
{t('user:First Name')}:
<input
name="first_name"
className="form-control input-lg"
type="text"
value={formData.first_name}
onChange={e => this.handleFormChange(e)}
/>
</label>
</div>
<div className="form-group">
<label>
{t('user:Last Name')}:
<input
name="last_name"
className="form-control input-lg"
type="text"
value={formData.last_name}
onChange={e => this.handleFormChange(e)}
/>
</label>
</div>
<div className="form-group">
<label>
{t('user:Birth Date')}
<input
name="birth_date"
className="form-control input-lg"
type="date"
value={formData.birth_date}
onChange={e => this.handleFormChange(e)}
/>
</label>
</div>
<div className="form-group">
<label>
{t('user:Location')}:
<input
name="location"
className="form-control input-lg"
type="text"
value={formData.location}
onChange={e => this.handleFormChange(e)}
/>
</label>
</div>
<div className="form-group">
<label>
{t('user:Bio')}:
<CustomTextArea
charLimit={200}
name="bio"
defaultValue={formData.bio}
onTextChange={e => this.handleFormChange(e)}
/>
</label>
</div>
<div className="form-group">
<label>
{t('user:Language')}:
<select
name="language"
className="form-control input-lg"
value={formData.language}
onChange={e => this.handleFormChange(e)}
>
{languages.map(lang => (
<option value={lang.name} key={lang.name}>
{lang.name}
</option>
))}
</select>
</label>
</div>
<div className="form-group">
<label>
{t('user:Timezone')}:
<TimezonePicker
className="form-control timezone-custom"
onChange={tz => {
const e = {
target: {
name: 'timezone',
value: tz ? tz : 'Europe/Paris',
},
}
this.handleFormChange(e)
}}
value={formData.timezone}
/>
</label>
</div>
<div className="form-group">
<label>
{t('user:First day of week')}:
<select
name="weekm"
className="form-control input-lg"
value={formData.weekm ? 'Monday' : 'Sunday'}
onChange={e => this.handleFormChange(e)}
>
<option value="Sunday">
{t('user:Sunday')}
</option>
<option value="Monday">
{t('user:Monday')}
</option>
</select>
</label>
</div>
<button type="submit" className="btn btn-primary">
{t('common:Submit')}
</button>
<button
className="btn btn-danger"
onClick={event => {
event.preventDefault()
this.displayModal(true)
}}
>
{t('user:Delete my account')}
</button>
<button
type="submit"
className="btn btn-secondary"
onClick={() => history.push('/profile')}
>
{t('common:Cancel')}
</button>
</form>
<Message message={message} t={t} />
</div>
</div>
</div>
</div>
</div>
<div className="col-md-2" />
</div>
</div>
)}
</div>
)
}
}
export default withTranslation()(
connect(
state => ({
location: state.router.location,
message: state.message,
user: state.user,
}),
dispatch => ({
onDeleteUser: username => {
dispatch(deleteUser(username))
},
onHandleProfileFormSubmit: formData => {
dispatch(handleProfileFormSubmit(formData))
},
})
)(ProfileEdit)
)

View File

@ -1,99 +0,0 @@
import React from 'react'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import { Redirect } from 'react-router-dom'
import Form from './Form'
import Message from '../Common/Message'
import { handleUserFormSubmit } from '../../actions/user'
import { isLoggedIn } from '../../utils'
class UserForm extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
formData: {
username: '',
email: '',
password: '',
password_conf: '',
},
}
}
componentDidUpdate(prevProps) {
if (prevProps.location.pathname !== this.props.location.pathname) {
this.emptyForm()
}
}
emptyForm() {
const { formData } = this.state
Object.keys(formData).map(k => (formData[k] = ''))
this.setState(formData)
}
onHandleFormChange(e) {
const { formData } = this.state
formData[e.target.name] = e.target.value
this.setState(formData)
}
render() {
const {
formType,
isRegistrationAllowed,
message,
messages,
onHandleUserFormSubmit,
t,
} = this.props
const { formData } = this.state
const { token } = this.props.location.query
return (
<div>
{isLoggedIn() || (formType === 'password reset' && !token) ? (
<Redirect to="/" />
) : (
<div>
<Message message={message} messages={messages} t={t} />
<Form
isRegistrationAllowed={isRegistrationAllowed}
formType={formType}
userForm={formData}
onHandleFormChange={event => this.onHandleFormChange(event)}
handleUserFormSubmit={event => {
event.preventDefault()
if (formType === 'password reset') {
formData.token = token
}
onHandleUserFormSubmit(formData, formType)
}}
/>
</div>
)}
</div>
)
}
}
export default withTranslation()(
connect(
state => ({
isRegistrationAllowed: state.application.config.is_registration_enabled,
location: state.router.location,
message: state.message,
messages: state.messages,
}),
dispatch => ({
onHandleUserFormSubmit: (formData, formType) => {
formType =
formType === 'password reset'
? 'password/update'
: formType === 'reset your password'
? 'password/reset-request'
: formType
dispatch(handleUserFormSubmit(formData, formType))
},
})
)(UserForm)
)

View File

@ -1,86 +0,0 @@
import React from 'react'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import CustomModal from '../Common/CustomModal'
import ProfileDetail from './ProfileDetail'
import { getOrUpdateData } from '../../actions'
import { deleteUser } from '../../actions/user'
class UserProfile extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
displayModal: false,
}
}
componentDidMount() {
this.props.loadUser(this.props.match.params.userName)
}
componentDidUpdate(prevProps) {
if (prevProps.match.params.userName !== this.props.match.params.userName) {
this.props.loadUser(this.props.match.params.userName)
}
}
displayModal(value) {
this.setState(prevState => ({
...prevState,
displayModal: value,
}))
}
render() {
const { t, currentUser, onDeleteUser, users } = this.props
const { displayModal } = this.state
const [user] = users
const editable = user ? currentUser.username === user.username : false
return (
<div>
{displayModal && (
<CustomModal
title={t('common:Confirmation')}
text={t(
'user:Are you sure you want to delete this account? ' +
'All data will be deleted, this cannot be undone.'
)}
confirm={() => {
onDeleteUser(user.username)
this.displayModal(false)
}}
close={() => this.displayModal(false)}
/>
)}
{user && (
<ProfileDetail
editable={editable}
isDeletable={currentUser.admin && !editable}
onDeleteUser={onDeleteUser}
displayModal={e => this.displayModal(e)}
t={t}
user={user}
/>
)}
</div>
)
}
}
export default withTranslation()(
connect(
state => ({
currentUser: state.user,
users: state.users.data,
}),
dispatch => ({
onDeleteUser: username => {
dispatch(deleteUser(username, true))
},
loadUser: userName => {
dispatch(getOrUpdateData('getData', 'users', { username: userName }))
},
})
)(UserProfile)
)

View File

@ -1,19 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import WorkoutAddOrEdit from './WorkoutAddOrEdit'
function WorkoutAdd(props) {
const { message, sports } = props
return (
<div>
<WorkoutAddOrEdit workout={null} message={message} sports={sports} />
</div>
)
}
export default connect(state => ({
message: state.message,
sports: state.sports.data,
user: state.user,
}))(WorkoutAdd)

View File

@ -1,118 +0,0 @@
import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import FormWithGpx from './WorkoutForms/FormWithGpx'
import FormWithoutGpx from './WorkoutForms/FormWithoutGpx'
import Message from '../Common/Message'
class WorkoutAddEdit 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 { loading, message, sports, t, workout } = this.props
const { withGpx } = this.state
return (
<div>
<Helmet>
<title>
FitTrackee -{' '}
{workout
? t('workouts:Edit a workout')
: t('workouts:Add a workout')}
</title>
</Helmet>
<br />
<br />
<Message message={message} t={t} />
<div className="container">
<div className="row">
<div className="col-md-2" />
<div className="col-md-8">
<div className="card add-workout">
<h2 className="card-header text-center">
{workout
? t('workouts:Edit a workout')
: t('workouts:Add a workout')}
</h2>
<div className="card-body">
{workout ? (
workout.with_gpx ? (
<FormWithGpx workout={workout} sports={sports} t={t} />
) : (
<FormWithoutGpx workout={workout} sports={sports} t={t} />
)
) : (
<div>
<form>
<div className="form-group row">
<div className="col">
<label className="radioLabel">
<input
className="add-workout-radio"
type="radio"
name="withGpx"
disabled={loading}
checked={withGpx}
onChange={event =>
this.handleRadioChange(event)
}
/>
{t('workouts:with gpx file')}
</label>
</div>
<div className="col">
<label className="radioLabel">
<input
className="add-workout-radio"
type="radio"
name="withoutGpx"
disabled={loading}
checked={!withGpx}
onChange={event =>
this.handleRadioChange(event)
}
/>
{t('workouts:without gpx file')}
</label>
</div>
</div>
</form>
{withGpx ? (
<FormWithGpx sports={sports} t={t} />
) : (
<FormWithoutGpx sports={sports} t={t} />
)}
</div>
)}
</div>
</div>
</div>
<div className="col-md-2" />
</div>
</div>
</div>
)
}
}
export default withTranslation()(
connect(state => ({
loading: state.loading,
}))(WorkoutAddEdit)
)

View File

@ -1,27 +0,0 @@
import React from 'react'
import { GeoJSON, Marker, TileLayer, useMap } from 'react-leaflet'
import hash from 'object-hash'
import { apiUrl } from '../../../utils'
export default function Map({ bounds, coordinates, jsonData, mapAttribution }) {
const map = useMap()
map.fitBounds(bounds)
return (
<>
<TileLayer
// eslint-disable-next-line max-len
attribution={mapAttribution}
url={`${apiUrl}workouts/map_tile/{s}/{z}/{x}/{y}.png`}
/>
<GeoJSON
// hash as a key to force re-rendering
key={hash(jsonData)}
data={jsonData}
/>
{coordinates.latitude && (
<Marker position={[coordinates.latitude, coordinates.longitude]} />
)}
</>
)
}

View File

@ -1,106 +0,0 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { getDateWithTZ } from '../../../utils'
import { formatWorkoutDate } from '../../../utils/workouts'
export default function WorkoutCardHeader(props) {
const { dataType, displayModal, segmentId, sport, t, title, user, workout } =
props
const workoutDate = workout
? formatWorkoutDate(getDateWithTZ(workout.workout_date, user.timezone))
: null
const previousUrl =
dataType === 'segment' && segmentId !== 1
? `/workouts/${workout.id}/segment/${segmentId - 1}`
: dataType === 'workout' && workout.previous_workout
? `/workouts/${workout.previous_workout}`
: null
const nextUrl =
dataType === 'segment' && segmentId < workout.segments.length
? `/workouts/${workout.id}/segment/${segmentId + 1}`
: dataType === 'workout' && workout.next_workout
? `/workouts/${workout.next_workout}`
: null
return (
<div className="container">
<div className="row">
<div className="col-auto">
{previousUrl ? (
<Link className="unlink" to={previousUrl}>
<i
className="fa fa-chevron-left"
aria-hidden="true"
title={t(`workouts:See previous ${dataType}`)}
/>
</Link>
) : (
<i
className="fa fa-chevron-left inactive-link"
aria-hidden="true"
title={t(`workouts:No previous ${dataType}`)}
/>
)}
</div>
<div className="col-auto col-workout-logo">
<img className="sport-img-medium" src={sport.img} alt="sport logo" />
</div>
<div className="col">
{dataType === 'workout' ? (
<>
{title}{' '}
<Link className="unlink" to={`/workouts/${workout.id}/edit`}>
<i
className="fa fa-edit custom-fa"
aria-hidden="true"
title={t('workouts:Edit workout')}
/>
</Link>
<i
className="fa fa-trash custom-fa"
aria-hidden="true"
onClick={() => displayModal(true)}
title={t('workouts:Delete workout')}
/>
</>
) : (
<>
{/* prettier-ignore */}
<Link
to={`/workouts/${workout.id}`}
>
{title}
</Link>{' '}
- {t('workouts:segment')} {segmentId}
</>
)}
<br />
{workoutDate && (
<span className="workout-date">
{`${workoutDate.workout_date} - ${workoutDate.workout_time}`}
</span>
)}
</div>
<div className="col-auto">
{nextUrl ? (
<Link className="unlink" to={nextUrl}>
<i
className="fa fa-chevron-right"
aria-hidden="true"
title={t(`workouts:See next ${dataType}`)}
/>
</Link>
) : (
<i
className="fa fa-chevron-right inactive-link"
aria-hidden="true"
title={t(`workouts:No next ${dataType}`)}
/>
)}
</div>
</div>
</div>
)
}

View File

@ -1,240 +0,0 @@
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 {
getSegmentChartData,
getWorkoutChartData,
} from '../../../actions/workouts'
class WorkoutCharts extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
displayDistance: true,
dataToHide: [],
}
}
componentDidMount() {
if (this.props.dataType === 'workout') {
this.props.loadWorkoutData(this.props.workout.id)
} else {
this.props.loadSegmentData(this.props.workout.id, this.props.segmentId)
}
}
componentDidUpdate(prevProps) {
if (
(this.props.dataType === 'workout' &&
prevProps.workout.id !== this.props.workout.id) ||
(this.props.dataType === 'workout' && prevProps.dataType === 'segment')
) {
this.props.loadWorkoutData(this.props.workout.id)
}
if (
this.props.dataType === 'segment' &&
prevProps.segmentId !== this.props.segmentId
) {
this.props.loadSegmentData(this.props.workout.id, this.props.segmentId)
}
}
componentWillUnmount() {
this.props.loadWorkoutData(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, t, updateCoordinates } = 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)}
/>
{t('workouts:distance')}
</label>
<label className="radioLabel col-md-1">
<input
type="radio"
name="duration"
checked={!displayDistance}
onChange={e => this.handleRadioChange(e)}
/>
{t('workouts: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)}
/>
{t('workouts:speed')}
</label>
<label className="radioLabel col-md-1">
<input
type="checkbox"
name="elevation"
checked={this.displayData('elevation')}
onChange={e => this.handleLegendChange(e)}
/>
{t('workouts: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 }}
onMouseMove={e => updateCoordinates(e.activePayload)}
onMouseLeave={() => updateCoordinates(null)}
>
<XAxis
allowDecimals={false}
dataKey={xDataKey}
label={{
value: t(`workouts:${xDataKey}`),
offset: 0,
position: 'bottom',
}}
scale={xScale}
interval={xInterval}
tickFormatter={value =>
displayDistance ? value : format(value, 'HH:mm:ss')
}
type="number"
/>
<YAxis
label={{
value: `${t('workouts:speed')} (km/h)`,
angle: -90,
position: 'left',
}}
yAxisId="left"
/>
<YAxis
label={{
value: `${t('workouts:elevation')} (m)`,
angle: -90,
position: 'right',
}}
yAxisId="right"
orientation="right"
/>
{this.displayData('elevation') && (
<Area
yAxisId="right"
type="linear"
dataKey="elevation"
name={t('workouts:elevation')}
fill="#e5e5e5"
stroke="#cccccc"
dot={false}
unit=" m"
/>
)}
{this.displayData('speed') && (
<Line
yAxisId="left"
type="linear"
dataKey="speed"
name={t('workouts:speed')}
stroke="#8884d8"
strokeWidth={2}
dot={false}
unit=" km/h"
/>
)}
<Tooltip
labelFormatter={value =>
displayDistance
? `${t('workouts:distance')}: ${value} km`
: `${t('workouts:duration')}: ${format(
value,
'HH:mm:ss'
)}`
}
/>
</ComposedChart>
</ResponsiveContainer>
</div>
<div className="chart-info">
{t('workouts:data from gpx, without any cleaning')}
</div>
</div>
) : (
t('workouts:No data to display')
)}
</div>
)
}
}
export default connect(
state => ({
chartData: state.chartData,
}),
dispatch => ({
loadWorkoutData: workoutId => {
dispatch(getWorkoutChartData(workoutId))
},
loadSegmentData: (workoutId, segmentId) => {
dispatch(getSegmentChartData(workoutId, segmentId))
},
})
)(WorkoutCharts)

View File

@ -1,73 +0,0 @@
import React from 'react'
import WorkoutWeather from './WorkoutWeather'
export default function WorkoutDetails(props) {
const { t, workout } = props
const withPauses = workout.pauses !== '0:00:00' && workout.pauses !== null
return (
<div className="workout-details">
<p>
<i className="fa fa-clock-o custom-fa" aria-hidden="true" />
{t('workouts:Duration')}: {workout.moving}
{workout.records &&
workout.records.find(record => record.record_type === 'LD') && (
<sup>
<i className="fa fa-trophy custom-fa" aria-hidden="true" />
</sup>
)}
{withPauses && (
<span>
<br />({t('workouts:pauses')}: {workout.pauses},{' '}
{t('workouts:total duration')}: {workout.duration})
</span>
)}
</p>
<p>
<i className="fa fa-road custom-fa" aria-hidden="true" />
{t('workouts:Distance')}: {workout.distance} km
{workout.records &&
workout.records.find(record => record.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" />
{t('workouts:Average speed')}: {workout.ave_speed} km/h
{workout.records &&
workout.records.find(record => record.record_type === 'AS') && (
<sup>
<i className="fa fa-trophy custom-fa" aria-hidden="true" />
</sup>
)}
<br />
{t('workouts:Max. speed')}: {workout.max_speed} km/h
{workout.records &&
workout.records.find(record => record.record_type === 'MS') && (
<sup>
<i className="fa fa-trophy custom-fa" aria-hidden="true" />
</sup>
)}
</p>
{workout.min_alt && workout.max_alt && (
<p>
<i className="fi-mountains custom-fa" />
{t('workouts:Min. altitude')}: {workout.min_alt}m
<br />
{t('workouts:Max. altitude')}: {workout.max_alt}m
</p>
)}
{workout.ascent && workout.descent && (
<p>
<i className="fa fa-location-arrow custom-fa" />
{t('workouts:Ascent')}: {workout.ascent}m
<br />
{t('workouts:Descent')}: {workout.descent}m
</p>
)}
<WorkoutWeather workout={workout} t={t} />
</div>
)
}

View File

@ -1,87 +0,0 @@
import React from 'react'
import { MapContainer } from 'react-leaflet'
import { connect } from 'react-redux'
import Map from './Map'
import { getSegmentGpx, getWorkoutGpx } from '../../../actions/workouts'
import { getGeoJson } from '../../../utils/workouts'
class WorkoutMap extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
zoom: 13,
}
}
componentDidMount() {
if (this.props.dataType === 'workout') {
this.props.loadWorkoutGpx(this.props.workout.id)
} else {
this.props.loadSegmentGpx(this.props.workout.id, this.props.segmentId)
}
}
componentDidUpdate(prevProps) {
if (
(this.props.dataType === 'workout' &&
prevProps.workout.id !== this.props.workout.id) ||
(this.props.dataType === 'workout' && prevProps.dataType === 'segment')
) {
this.props.loadWorkoutGpx(this.props.workout.id)
}
if (
this.props.dataType === 'segment' &&
prevProps.segmentId !== this.props.segmentId
) {
this.props.loadSegmentGpx(this.props.workout.id, this.props.segmentId)
}
}
componentWillUnmount() {
this.props.loadWorkoutGpx(null)
}
render() {
const { coordinates, gpxContent, mapAttribution, workout } = this.props
const { jsonData } = getGeoJson(gpxContent)
const bounds = [
[workout.bounds[0], workout.bounds[1]],
[workout.bounds[2], workout.bounds[3]],
]
return (
<div>
{jsonData && (
<MapContainer
zoom={this.state.zoom}
bounds={bounds}
boundsOptions={{ padding: [10, 10] }}
>
<Map
bounds={bounds}
coordinates={coordinates}
jsonData={jsonData}
mapAttribution={mapAttribution}
/>
</MapContainer>
)}
</div>
)
}
}
export default connect(
state => ({
gpxContent: state.gpx,
mapAttribution: state.application.config.map_attribution,
}),
dispatch => ({
loadWorkoutGpx: workoutId => {
dispatch(getWorkoutGpx(workoutId))
},
loadSegmentGpx: (workoutId, segmentId) => {
dispatch(getSegmentGpx(workoutId, segmentId))
},
})
)(WorkoutMap)

View File

@ -1,8 +0,0 @@
import React from 'react'
export default function WorkoutNoMap(props) {
const { t } = props
return (
<div className="workout-no-map text-center">{t('workouts:No Map')}</div>
)
}

View File

@ -1,19 +0,0 @@
import React from 'react'
export default function WorkoutNotes(props) {
const { notes, t } = props
return (
<div className="row">
<div className="col">
<div className="card workout-card">
<div className="card-body">
Notes
<div className="workout-notes">
{notes && notes !== '' ? notes : t('workouts:No notes')}
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -1,38 +0,0 @@
import React from 'react'
import { Link } from 'react-router-dom'
export default function WorkoutSegments(props) {
const { segments, t } = props
return (
<div className="row">
<div className="col">
<div className="card workout-card">
<div className="card-body">
{t('workouts:Segments')}
<div className="workout-segments">
<ul>
{segments.map((segment, index) => (
<li
className="workout-segments-list"
// eslint-disable-next-line react/no-array-index-key
key={`segment-${index}`}
>
<Link
to={`/workouts/${segment.workout_id}/segment/${
index + 1
}`}
>
{t('workouts:segment')} {index + 1}
</Link>{' '}
({t('workouts:distance')}: {segment.distance} km,{' '}
{t('workouts:duration')}: {segment.duration})
</li>
))}
</ul>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -1,75 +0,0 @@
import React from 'react'
export default function WorkoutWeather(props) {
const { t, workout } = props
return (
<div className="container">
{workout.weather_start && workout.weather_end && (
<table className="table table-borderless weather-table text-center">
<thead>
<tr>
<th />
<th>
{t('workouts:Start')}
<br />
<img
className="weather-img"
src={`/img/weather/${workout.weather_start.icon}.png`}
alt={`workout weather (${workout.weather_start.icon})`}
title={workout.weather_start.summary}
/>
</th>
<th>
{t('workouts:End')}
<br />
<img
className="weather-img"
src={`/img/weather/${workout.weather_end.icon}.png`}
alt={`workout weather (${workout.weather_end.icon})`}
title={workout.weather_end.summary}
/>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<img
className="weather-img-small"
src="/img/weather/temperature.png"
alt="Temperatures"
/>
</td>
<td>{Number(workout.weather_start.temperature).toFixed(1)}°C</td>
<td>{Number(workout.weather_end.temperature).toFixed(1)}°C</td>
</tr>
<tr>
<td>
<img
className="weather-img-small"
src="/img/weather/pour-rain.png"
alt="Temperatures"
/>
</td>
<td>
{Number(workout.weather_start.humidity * 100).toFixed(1)}%
</td>
<td>{Number(workout.weather_end.humidity * 100).toFixed(1)}%</td>
</tr>
<tr>
<td>
<img
className="weather-img-small"
src="/img/weather/breeze.png"
alt="Temperatures"
/>
</td>
<td>{Number(workout.weather_start.wind).toFixed(1)}m/s</td>
<td>{Number(workout.weather_end.wind).toFixed(1)}m/s</td>
</tr>
</tbody>
</table>
)}
</div>
)
}

View File

@ -1,202 +0,0 @@
import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import CustomModal from '../../Common/CustomModal'
import Message from '../../Common/Message'
import WorkoutCardHeader from './WorkoutCardHeader'
import WorkoutCharts from './WorkoutCharts'
import WorkoutDetails from './WorkoutDetails'
import WorkoutMap from './WorkoutMap'
import WorkoutNoMap from './WorkoutNoMap'
import WorkoutNotes from './WorkoutNotes'
import WorkoutSegments from './WorkoutSegments'
import { getOrUpdateData } from '../../../actions'
import { deleteWorkout } from '../../../actions/workouts'
class WorkoutDisplay extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
displayModal: false,
coordinates: {
latitude: null,
longitude: null,
},
}
}
componentDidMount() {
this.props.loadWorkout(this.props.match.params.workoutId)
}
componentDidUpdate(prevProps) {
if (
prevProps.match.params.workoutId !== this.props.match.params.workoutId
) {
this.props.loadWorkout(this.props.match.params.workoutId)
}
}
displayModal(value) {
this.setState(prevState => ({
...prevState,
displayModal: value,
}))
}
updateCoordinates(activePayload) {
const coordinates =
activePayload && activePayload.length > 0
? {
latitude: activePayload[0].payload.latitude,
longitude: activePayload[0].payload.longitude,
}
: {
latitude: null,
longitude: null,
}
this.setState(prevState => ({
...prevState,
coordinates,
}))
}
render() {
const { message, onDeleteWorkout, sports, t, user, workouts } = this.props
const { coordinates, displayModal } = this.state
const [workout] = workouts
const title = workout ? workout.title : t('workouts:Workout')
const [sport] = workout ? sports.filter(s => s.id === workout.sport_id) : []
const segmentId = parseInt(this.props.match.params.segmentId)
const dataType = segmentId >= 0 ? 'segment' : 'workout'
return (
<div className="workout-page">
<Helmet>
<title>FitTrackee - {title}</title>
</Helmet>
{message ? (
<Message message={message} t={t} />
) : (
<div className="container">
{displayModal && (
<CustomModal
title={t('common:Confirmation')}
text={t(
'workouts:Are you sure you want to delete this workout?'
)}
confirm={() => {
onDeleteWorkout(workout.id)
this.displayModal(false)
}}
close={() => this.displayModal(false)}
/>
)}
{workout && sport && workouts.length === 1 && (
<div>
<div className="row">
<div className="col">
<div className="card workout-card">
<div className="card-header">
<WorkoutCardHeader
workout={workout}
dataType={dataType}
segmentId={segmentId}
sport={sport}
t={t}
title={title}
user={user}
displayModal={() => this.displayModal(true)}
/>
</div>
<div className="card-body">
<div className="row">
<div className="col-md-8">
{workout.with_gpx ? (
<WorkoutMap
workout={workout}
coordinates={coordinates}
dataType={dataType}
segmentId={segmentId}
/>
) : (
<WorkoutNoMap t={t} />
)}
</div>
<div className="col">
<WorkoutDetails
workout={
dataType === 'workout'
? workout
: workout.segments[segmentId - 1]
}
t={t}
/>
</div>
</div>
</div>
</div>
</div>
</div>
{workout.with_gpx && (
<div className="row">
<div className="col">
<div className="card workout-card">
<div className="card-body">
<div className="row">
<div className="col">
<div className="chart-title">
{t('workouts:Chart')}
</div>
<WorkoutCharts
workout={workout}
dataType={dataType}
segmentId={segmentId}
t={t}
updateCoordinates={e =>
this.updateCoordinates(e)
}
/>
</div>
</div>
</div>
</div>
</div>
</div>
)}
{dataType === 'workout' && (
<>
<WorkoutNotes notes={workout.notes} t={t} />
{workout.segments.length > 1 && (
<WorkoutSegments segments={workout.segments} t={t} />
)}
</>
)}
</div>
)}
</div>
)}
</div>
)
}
}
export default withTranslation()(
connect(
state => ({
workouts: state.workouts.data,
message: state.message,
sports: state.sports.data,
user: state.user,
}),
dispatch => ({
loadWorkout: workoutId => {
dispatch(getOrUpdateData('getData', 'workouts', { id: workoutId }))
},
onDeleteWorkout: workoutId => {
dispatch(deleteWorkout(workoutId))
},
})
)(WorkoutDisplay)
)

View File

@ -1,41 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import WorkoutAddOrEdit from './WorkoutAddOrEdit'
import { getOrUpdateData } from '../../actions'
class WorkoutEdit extends React.Component {
componentDidMount() {
this.props.loadWorkout(this.props.match.params.workoutId)
}
render() {
const { message, sports, workouts } = this.props
const [workout] = workouts
return (
<div>
{sports.length > 0 && (
<WorkoutAddOrEdit
workout={workout}
message={message}
sports={sports}
/>
)}
</div>
)
}
}
export default connect(
state => ({
workouts: state.workouts.data,
message: state.message,
sports: state.sports.data,
user: state.user,
}),
dispatch => ({
loadWorkout: workoutId => {
dispatch(getOrUpdateData('getData', 'workouts', { id: workoutId }))
},
})
)(WorkoutEdit)

View File

@ -1,170 +0,0 @@
import React from 'react'
import { Trans } from 'react-i18next'
import { connect } from 'react-redux'
import { setLoading } from '../../../actions/index'
import { addWorkout, editWorkout } from '../../../actions/workouts'
import { history } from '../../../index'
import { getFileSize } from '../../../utils'
import { translateSports } from '../../../utils/workouts'
import CustomTextArea from '../../Common/CustomTextArea'
function FormWithGpx(props) {
const {
appConfig,
loading,
onAddWorkout,
onEditWorkout,
sports,
t,
workout,
} = props
const sportId = workout ? workout.sport_id : ''
const translatedSports = translateSports(sports, t, true)
const zipTooltip = `${t('workouts:no folder inside')}, ${
appConfig.gpx_limit_import
} ${t('workouts:files max')}, ${t('workouts:max size')}: ${getFileSize(
appConfig.max_zip_file_size
)}`
const fileSizeLimit = getFileSize(appConfig.max_single_file_size)
return (
<form
encType="multipart/form-data"
method="post"
onSubmit={event => event.preventDefault()}
>
<div className="form-group">
<label>
{t('common:Sport')}:
<select
className="form-control input-lg"
defaultValue={sportId}
disabled={loading}
name="sport"
required
>
<option value="" />
{translatedSports.map(sport => (
<option key={sport.id} value={sport.id}>
{sport.label}
</option>
))}
</select>
</label>
</div>
{workout ? (
<div className="form-group">
<label>
{t('workouts:Title')}:
<input
name="title"
defaultValue={workout ? workout.title : ''}
disabled={loading}
className="form-control input-lg"
/>
</label>
</div>
) : (
<div className="form-group">
<label>
<Trans i18nKey="workouts:gpxFile">
<strong>gpx</strong> file
</Trans>
<sup>
<i
className="fa fa-question-circle"
aria-hidden="true"
data-toggle="tooltip"
title={`${t('workouts:max size')}: ${fileSizeLimit}`}
/>
</sup>{' '}
<Trans i18nKey="workouts:zipFile">
or <strong> zip</strong> file containing <strong>gpx </strong>
files
</Trans>
<sup>
<i
className="fa fa-question-circle"
aria-hidden="true"
data-toggle="tooltip"
data-placement="top"
title={zipTooltip}
/>
</sup>{' '}
:
<input
accept=".gpx, .zip"
className="form-control form-control-file gpx-file"
disabled={loading}
name="gpxFile"
required
type="file"
/>
</label>
</div>
)}
<div className="form-group">
<label>
{t('workouts:Notes')}:
<CustomTextArea
charLimit={500}
defaultValue={workout ? workout.notes : ''}
loading={loading}
name="notes"
/>
</label>
</div>
{loading ? (
<div className="loader" />
) : (
<div>
<input
type="submit"
className="btn btn-primary"
onClick={event =>
workout ? onEditWorkout(event, workout) : onAddWorkout(event)
}
value={t('common:Submit')}
/>
<input
type="submit"
className="btn btn-secondary"
onClick={() => history.push('/')}
value={t('common:Cancel')}
/>
</div>
)}
</form>
)
}
export default connect(
state => ({
appConfig: state.application.config,
loading: state.loading,
}),
dispatch => ({
onAddWorkout: e => {
dispatch(setLoading(true))
const form = new FormData()
form.append('file', e.target.form.gpxFile.files[0])
/* prettier-ignore */
form.append(
'data',
`{"sport_id": ${e.target.form.sport.value
}, "notes": "${e.target.form.notes.value}"}`
)
dispatch(addWorkout(form))
},
onEditWorkout: (e, workout) => {
dispatch(
editWorkout({
id: workout.id,
notes: e.target.form.notes.value,
sport_id: +e.target.form.sport.value,
title: e.target.form.title.value,
})
)
},
})
)(FormWithGpx)

View File

@ -1,162 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import { addWorkoutWithoutGpx, editWorkout } from '../../../actions/workouts'
import { history } from '../../../index'
import { getDateWithTZ } from '../../../utils'
import { formatWorkoutDate, translateSports } from '../../../utils/workouts'
import CustomTextArea from '../../Common/CustomTextArea'
function FormWithoutGpx(props) {
const { onAddOrEdit, sports, t, user, workout } = props
const translatedSports = translateSports(sports, t, true)
let workoutDate,
workoutTime,
sportId = ''
if (workout) {
const workoutDateTime = formatWorkoutDate(
getDateWithTZ(workout.workout_date, user.timezone),
'yyyy-MM-dd'
)
workoutDate = workoutDateTime.workout_date
workoutTime = workoutDateTime.workout_time
sportId = workout.sport_id
}
return (
<form onSubmit={event => event.preventDefault()}>
<div className="form-group">
<label>
{t('workouts:Title')}:
<input
name="title"
defaultValue={workout ? workout.title : ''}
className="form-control input-lg"
/>
</label>
</div>
<div className="form-group">
<label>
{t('common:Sport')}:
<select
className="form-control input-lg"
defaultValue={sportId}
name="sport_id"
required
>
<option value="" />
{translatedSports.map(sport => (
<option key={sport.id} value={sport.id}>
{sport.label}
</option>
))}
</select>
</label>
</div>
<div className="form-group">
<label>
{t('workouts:Workout Date')}:
<div className="container">
<div className="row">
<input
name="workout_date"
defaultValue={workoutDate}
className="form-control col-md"
required
type="date"
/>
<input
name="workout_time"
defaultValue={workoutTime}
className="form-control col-md"
required
type="time"
/>
</div>
</div>
</label>
</div>
<div className="form-group">
<label>
{t('workouts:Duration')}:
<input
name="duration"
defaultValue={workout ? workout.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>
{t('workouts:Distance')} (km):
<input
name="distance"
defaultValue={workout ? workout.distance : ''}
className="form-control input-lg"
min={0}
required
step="0.001"
type="number"
/>
</label>
</div>
<div className="form-group">
<label>
{t('workouts:Notes')}:
<CustomTextArea
charLimit={500}
defaultValue={workout ? workout.notes : ''}
name="notes"
/>
</label>
</div>
<input
type="submit"
className="btn btn-primary"
onClick={event => onAddOrEdit(event, workout)}
value={t('common:Submit')}
/>
<input
type="submit"
className="btn btn-secondary"
onClick={() => history.push('/')}
value={t('common:Cancel')}
/>
</form>
)
}
export default connect(
state => ({
user: state.user,
}),
dispatch => ({
onAddOrEdit: (e, workout) => {
const d = e.target.form.duration.value.split(':')
const duration = +d[0] * 60 * 60 + +d[1] * 60 + +d[2]
/* prettier-ignore */
const workoutDate = `${e.target.form.workout_date.value
} ${ e.target.form.workout_time.value}`
const data = {
workout_date: workoutDate,
distance: +e.target.form.distance.value,
duration,
notes: e.target.form.notes.value,
sport_id: +e.target.form.sport_id.value,
title: e.target.form.title.value,
}
if (workout) {
data.id = workout.id
dispatch(editWorkout(data))
} else {
dispatch(addWorkoutWithoutGpx(data))
}
},
})
)(FormWithoutGpx)

View File

@ -1,38 +0,0 @@
import React from 'react'
import { connect } from 'react-redux'
import { Redirect, Route, Switch } from 'react-router-dom'
import NotFound from './../Others/NotFound'
import WorkoutAdd from './WorkoutAdd'
import WorkoutDisplay from './WorkoutDisplay'
import WorkoutEdit from './WorkoutEdit'
import { isLoggedIn } from '../../utils'
function Workout() {
return (
<div>
{isLoggedIn() ? (
<Switch>
<Route exact path="/workouts/add" component={WorkoutAdd} />
<Route exact path="/workouts/:workoutId" component={WorkoutDisplay} />
<Route
exact
path="/workouts/:workoutId/edit"
component={WorkoutEdit}
/>
<Route
path="/workouts/:workoutId/segment/:segmentId"
component={WorkoutDisplay}
/>
<Route component={NotFound} />
</Switch>
) : (
<Redirect to="/login" />
)}
</div>
)
}
export default connect(state => ({
user: state.user,
}))(Workout)

View File

@ -1,189 +0,0 @@
import React from 'react'
import { translateSports } from '../../utils/workouts'
export default class WorkoutsFilter extends React.PureComponent {
render() {
const { loadWorkouts, sports, t, updateParams } = this.props
const translatedSports = translateSports(sports, t)
return (
<div className="card">
<div className="card-body workout-filter">
<form onSubmit={event => event.preventDefault()}>
<div className="form-group">
<label>
{t('workouts:From')}:
<input
className="form-control col-md"
name="from"
onChange={e => updateParams(e)}
type="date"
/>
</label>
<label>
{t('workouts:To')}:
<input
className="form-control col-md"
name="to"
onChange={e => updateParams(e)}
type="date"
/>
</label>
</div>
<div className="form-group">
<label>
{t('common:Sport')}:
<select
className="form-control input-lg"
name="sport_id"
onChange={e => updateParams(e)}
>
<option value="" />
{translatedSports.map(sport => (
<option key={sport.id} value={sport.id}>
{sport.label}
</option>
))}
</select>
</label>
</div>
<div className="form-group">
<label>
{t('workouts:Distance')} (km):
<div className="container">
<div className="row">
<div className="col-5">
<input
className="form-control"
min={0}
name="distance_from"
onChange={e => updateParams(e)}
step="1"
type="number"
/>
</div>
<div className="col-2 align-middle text-center">
{t('common:to')}
</div>
<div className="col-5">
<input
className="form-control"
min={0}
name="distance_to"
onChange={e => updateParams(e)}
step="1"
type="number"
/>
</div>
</div>
</div>
</label>
</div>
<div className="form-group">
<label>
{t('workouts:Duration')}:
<div className="container">
<div className="row">
<div className="col-5">
<input
className="form-control"
name="duration_from"
onChange={e => updateParams(e)}
pattern="^([0-9]*[0-9]):([0-5][0-9])$"
placeholder="hh:mm"
type="text"
/>
</div>
<div className="col-2 align-middle text-center">
{t('common:to')}
</div>
<div className="col-5">
<input
className="form-control"
name="duration_to"
onChange={e => updateParams(e)}
pattern="^([0-9]*[0-9]):([0-5][0-9])$"
placeholder="hh:mm"
type="text"
/>
</div>
</div>
</div>
</label>
</div>
<div className="form-group">
<label>
{t('workouts:Average speed')} (km/h):
<div className="container">
<div className="row">
<div className="col-5">
<input
className="form-control"
min={0}
name="ave_speed_from"
onChange={e => updateParams(e)}
step="1"
type="number"
/>
</div>
<div className="col-2 align-middle text-center">
{t('common:to')}
</div>
<div className="col-5">
<input
className="form-control"
min={0}
name="ave_speed_to"
onChange={e => updateParams(e)}
step="1"
type="number"
/>
</div>
</div>
</div>
</label>
</div>
<div className="form-group">
<label>
{t('workouts:Max. speed')} (km/h):
<div className="container">
<div className="row">
<div className="col-5">
<input
className="form-control"
min={0}
name="max_speed_from"
onChange={e => updateParams(e)}
step="1"
type="number"
/>
</div>
<div className="col-2 align-middle text-center">
{t('common:to')}
</div>
<div className="col-5">
<input
className="form-control"
min={0}
name="max_speed_to"
onChange={e => updateParams(e)}
step="1"
type="number"
/>
</div>
</div>
</div>
</label>
</div>
<input
className="btn btn-primary btn-lg btn-block"
onClick={() => loadWorkouts()}
type="submit"
value={t('workouts:Filter')}
/>
</form>
</div>
</div>
)
}
}

View File

@ -1,111 +0,0 @@
import { format } from 'date-fns'
import React from 'react'
import { Link } from 'react-router-dom'
import StaticMap from '../Common/StaticMap'
import { getDateWithTZ } from '../../utils'
export default class WorkoutsList extends React.PureComponent {
render() {
const { loading, sports, t, user, workouts } = this.props
return (
<div className="card workout-card">
<div className="card-body">
<table className="table">
<thead>
<tr>
<th scope="col" />
<th scope="col">{t('common:Workout')}</th>
<th scope="col">{t('workouts:Date')}</th>
<th scope="col">{t('workouts:Distance')}</th>
<th scope="col">{t('workouts:Duration')}</th>
<th scope="col">{t('workouts:Ave. speed')}</th>
<th scope="col">{t('workouts:Max. speed')}</th>
<th scope="col">{t('workouts:Ascent')}</th>
<th scope="col">{t('workouts:Descent')}</th>
</tr>
</thead>
<tbody>
{!loading &&
sports &&
workouts.map((workout, idx) => (
// eslint-disable-next-line react/no-array-index-key
<tr key={idx}>
<td>
<span className="heading-span-absolute">
{t('common:Sport')}
</span>
<img
className="workout-sport"
src={sports
.filter(s => s.id === workout.sport_id)
.map(s => s.img)}
alt="workout sport logo"
/>
</td>
<td className="workout-title">
<span className="heading-span-absolute">
{t('common:Workout')}
</span>
<Link to={`/workouts/${workout.id}`}>
{workout.title}
</Link>
{workout.map && (
<StaticMap workout={workout} display="list" />
)}
</td>
<td>
<span className="heading-span-absolute">
{t('workouts:Date')}
</span>
{format(
getDateWithTZ(workout.workout_date, user.timezone),
'dd/MM/yyyy HH:mm'
)}
</td>
<td className="text-right">
<span className="heading-span-absolute">
{t('workouts:Distance')}
</span>
{Number(workout.distance).toFixed(2)} km
</td>
<td className="text-right">
<span className="heading-span-absolute">
{t('workouts:Duration')}
</span>
{workout.moving}
</td>
<td className="text-right">
<span className="heading-span-absolute">
{t('workouts:Ave. speed')}
</span>
{workout.ave_speed} km/h
</td>
<td className="text-right">
<span className="heading-span-absolute">
{t('workouts:Max. speed')}
</span>
{workout.max_speed} km/h
</td>
<td className="text-right">
<span className="heading-span-absolute">
{t('workouts:Ascent')}
</span>
{workout.ascent === null ? '' : `${workout.ascent} m`}
</td>
<td className="text-right">
<span className="heading-span-absolute">
{t('workouts:Descent')}
</span>
{workout.descent === null ? '' : `${workout.descent} m`}
</td>
</tr>
))}
</tbody>
</table>
{loading && <div className="loader" />}
</div>
</div>
)
}
}

View File

@ -1,120 +0,0 @@
import React from 'react'
import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux'
import Message from '../Common/Message'
import NoWorkouts from '../Common/NoWorkouts'
import WorkoutsFilter from './WorkoutsFilter'
import WorkoutsList from './WorkoutsList'
import { getOrUpdateData } from '../../actions'
import { getMoreWorkouts } from '../../actions/workouts'
class Workouts extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
params: {
page: 1,
per_page: 10,
},
}
}
componentDidMount() {
this.props.loadWorkouts(this.state.params)
}
setParams(e) {
const { params } = this.state
if (e.target.value === '') {
delete params[e.target.name]
} else {
params[e.target.name] = e.target.value
}
params.page = 1
this.setState(params)
}
render() {
const {
loading,
loadWorkouts,
loadMoreWorkouts,
message,
sports,
t,
user,
workouts,
} = this.props
const { params } = this.state
const paginationEnd =
workouts.length > 0
? workouts[workouts.length - 1].previous_workout === null
: true
return (
<div>
<Helmet>
<title>FitTrackee - {t('common:Workouts')}</title>
</Helmet>
{message ? (
<Message message={message} t={t} />
) : (
<div className="container history">
<div className="row">
<div className="col-md-3">
<WorkoutsFilter
sports={sports}
loadWorkouts={() => loadWorkouts(params)}
t={t}
updateParams={e => this.setParams(e)}
/>
</div>
<div className="col-md-9 workouts-result">
<WorkoutsList
workouts={workouts}
loading={loading}
sports={sports}
t={t}
user={user}
/>
{!paginationEnd && (
<input
type="submit"
className="btn btn-default btn-md btn-block"
value="Load more workouts"
onClick={() => {
params.page += 1
loadMoreWorkouts(params)
this.setState(params)
}}
/>
)}
{workouts.length === 0 && <NoWorkouts t={t} />}
</div>
</div>
</div>
)}
</div>
)
}
}
export default withTranslation()(
connect(
state => ({
workouts: state.workouts.data,
loading: state.loading,
message: state.message,
sports: state.sports.data,
user: state.user,
}),
dispatch => ({
loadWorkouts: params => {
dispatch(getOrUpdateData('getData', 'workouts', params))
},
loadMoreWorkouts: params => {
dispatch(getMoreWorkouts(params))
},
})
)(Workouts)
)

View File

@ -1,22 +0,0 @@
import { createApiRequest } from '../utils'
export default class FitTrackeeApi {
static loginOrRegisterOrPasswordReset(target, data) {
const params = {
url: `auth/${target}`,
method: 'POST',
noAuthorization: true,
body: data,
type: 'application/json',
}
return createApiRequest(params)
}
static deletePicture() {
const params = {
url: 'auth/picture',
method: 'DELETE',
}
return createApiRequest(params)
}
}

View File

@ -1,63 +0,0 @@
import { createApiRequest, formatUrl } from '../utils'
export default class FitTrackeeApi {
static getData(target, data = {}) {
const url = formatUrl(target, data)
const params = {
url: url,
method: 'GET',
type: 'application/json',
}
return createApiRequest(params)
}
static addData(target, data) {
const params = {
url: target,
method: 'POST',
body: data,
type: 'application/json',
}
return createApiRequest(params)
}
static addDataWithFile(target, data) {
const params = {
url: target,
method: 'POST',
body: data,
}
return createApiRequest(params)
}
static postData(target, data) {
const params = {
url: `${target}${data.id ? `/${data.id}` : ''}`,
method: 'POST',
body: data,
type: 'application/json',
}
return createApiRequest(params)
}
static updateData(target, data) {
const params = {
url: `${target}${
data.id ? `/${data.id}` : data.username ? `/${data.username}` : ''
}`,
method: 'PATCH',
body: data,
type: 'application/json',
}
return createApiRequest(params)
}
static deleteData(target, id) {
const params = {
url: `${target}/${id}`,
method: 'DELETE',
type: 'application/json',
}
return createApiRequest(params)
}
}

View File

@ -1,23 +0,0 @@
import i18n from 'i18next'
import LanguageDetector from 'i18next-browser-languagedetector'
import XHR from 'i18next-xhr-backend'
import { resources } from './locales'
i18n
.use(XHR)
.use(LanguageDetector)
.init({
debug: process.env.NODE_ENV === 'development',
lng: 'en',
fallbackLng: 'en',
keySeparator: false,
interpolation: {
escapeValue: false,
},
resources,
ns: ['common'],
defaultNS: 'common',
})
export default i18n

View File

@ -1,27 +0,0 @@
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512"
viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg">
<path d="m466.916 27.803h-421.832c-24.859 0-45.084 20.225-45.084 45.084v366.226c0 24.859 20.225 45.084 45.084 45.084h421.832c24.859 0 45.084-20.225 45.084-45.084v-366.226c0-24.859-20.225-45.084-45.084-45.084z"
fill="#f0f9ff"/>
<path d="m198.58 188.334-181.344-150.862c-7.75 6.107-13.456 14.691-15.905 24.554l164.142 136.551h33.102z"
fill="#f40055"/>
<path d="m313.425 198.576h33.93l163.447-135.973c-2.325-9.923-7.93-18.592-15.613-24.796l-181.764 151.211z"
fill="#c20044"/>
<path d="m165.472 313.425-164.141 136.549c2.449 9.863 8.155 18.447 15.905 24.553l181.344-150.861-.005-10.241z"
fill="#f40055"/>
<path d="m313.425 313.425v9.557l181.765 151.211c7.683-6.204 13.288-14.874 15.613-24.796l-163.446-135.971z"
fill="#c20044"/>
<path d="m53.273 27.803 145.302 120.879v-120.879z" fill="#406bd4"/>
<path d="m313.425 150.571v-122.768h148.082z" fill="#3257b0"/>
<path d="m394.732 198.575 117.268-97.556v97.556z" fill="#3257b0"/>
<g fill="#406bd4">
<path d="m0 99.317v99.258h119.313z"/>
<path d="m0 313.425v97.699l117.44-97.699z"/>
<path d="m50.49 484.197 148.085-122.676v122.676z"/>
</g>
<path d="m313.425 484.197v-124.139l149.221 124.139z" fill="#3257b0"/>
<path d="m512 409.423-115.395-95.998h115.395z" fill="#3257b0"/>
<path d="m512 222.142h-222.142v-194.339h-67.716v194.339h-222.142v67.716h222.142v194.339h67.716v-194.339h222.142z"
fill="#f40055"/>
<path d="m289.858 222.142v-194.339h-33.858v456.394h33.858v-194.339h222.142v-67.716z"
fill="#c20044"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1 +0,0 @@
<svg id="Capa_1" enable-background="new 0 0 512 512" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg"><path d="m173.899 31.804h-8.707l-4.397-4h-115.711c-24.859-.001-45.084 20.224-45.084 45.083v366.226c0 24.859 20.225 45.084 45.084 45.084h115.711l6.348-4h6.755v-448.393z" fill="#406bd4"/><path d="m466.916 27.803h-115.711l-4.523 4h-5.141v448.393h4.141l5.523 4h115.711c24.859 0 45.084-20.225 45.084-45.084v-366.225c0-24.859-20.225-45.084-45.084-45.084z" fill="#c20044"/><path d="m160.795 27.803h190.409v456.394h-190.409z" fill="#f0f9ff"/><path d="m256 27.803h95.205v456.394h-95.205z" fill="#cee5f5"/></svg>

Before

Width:  |  Height:  |  Size: 637 B

View File

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 17.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 345.834 345.834" style="enable-background:new 0 0 345.834 345.834;" xml:space="preserve">
<g>
<path d="M339.798,260.429c0.13-0.026,0.257-0.061,0.385-0.094c0.109-0.028,0.219-0.051,0.326-0.084
c0.125-0.038,0.247-0.085,0.369-0.129c0.108-0.039,0.217-0.074,0.324-0.119c0.115-0.048,0.226-0.104,0.338-0.157
c0.109-0.052,0.22-0.1,0.327-0.158c0.107-0.057,0.208-0.122,0.312-0.184c0.107-0.064,0.215-0.124,0.319-0.194
c0.111-0.074,0.214-0.156,0.321-0.236c0.09-0.067,0.182-0.13,0.27-0.202c0.162-0.133,0.316-0.275,0.466-0.421
c0.027-0.026,0.056-0.048,0.083-0.075c0.028-0.028,0.052-0.059,0.079-0.088c0.144-0.148,0.284-0.3,0.416-0.46
c0.077-0.094,0.144-0.192,0.216-0.289c0.074-0.1,0.152-0.197,0.221-0.301c0.074-0.111,0.139-0.226,0.207-0.34
c0.057-0.096,0.118-0.19,0.171-0.289c0.062-0.115,0.114-0.234,0.169-0.351c0.049-0.104,0.101-0.207,0.146-0.314
c0.048-0.115,0.086-0.232,0.128-0.349c0.041-0.114,0.085-0.227,0.12-0.343c0.036-0.118,0.062-0.238,0.092-0.358
c0.029-0.118,0.063-0.234,0.086-0.353c0.028-0.141,0.045-0.283,0.065-0.425c0.014-0.1,0.033-0.199,0.043-0.3
c0.025-0.249,0.038-0.498,0.038-0.748V92.76c0-4.143-3.357-7.5-7.5-7.5h-236.25c-0.066,0-0.13,0.008-0.196,0.01
c-0.143,0.004-0.285,0.01-0.427,0.022c-0.113,0.009-0.225,0.022-0.337,0.037c-0.128,0.016-0.255,0.035-0.382,0.058
c-0.119,0.021-0.237,0.046-0.354,0.073c-0.119,0.028-0.238,0.058-0.356,0.092c-0.117,0.033-0.232,0.069-0.346,0.107
c-0.117,0.04-0.234,0.082-0.349,0.128c-0.109,0.043-0.216,0.087-0.322,0.135c-0.118,0.053-0.235,0.11-0.351,0.169
c-0.099,0.051-0.196,0.103-0.292,0.158c-0.116,0.066-0.23,0.136-0.343,0.208c-0.093,0.06-0.184,0.122-0.274,0.185
c-0.106,0.075-0.211,0.153-0.314,0.235c-0.094,0.075-0.186,0.152-0.277,0.231c-0.09,0.079-0.179,0.158-0.266,0.242
c-0.099,0.095-0.194,0.194-0.288,0.294c-0.047,0.05-0.097,0.094-0.142,0.145c-0.027,0.03-0.048,0.063-0.074,0.093
c-0.094,0.109-0.182,0.223-0.27,0.338c-0.064,0.084-0.13,0.168-0.19,0.254c-0.078,0.112-0.15,0.227-0.222,0.343
c-0.059,0.095-0.12,0.189-0.174,0.286c-0.063,0.112-0.118,0.227-0.175,0.342c-0.052,0.105-0.106,0.21-0.153,0.317
c-0.049,0.113-0.092,0.23-0.135,0.345c-0.043,0.113-0.087,0.225-0.124,0.339c-0.037,0.115-0.067,0.232-0.099,0.349
c-0.032,0.12-0.066,0.239-0.093,0.36c-0.025,0.113-0.042,0.228-0.062,0.342c-0.022,0.13-0.044,0.26-0.06,0.39
c-0.013,0.108-0.019,0.218-0.027,0.328c-0.01,0.14-0.019,0.28-0.021,0.421c-0.001,0.041-0.006,0.081-0.006,0.122v46.252
c0,4.143,3.357,7.5,7.5,7.5s7.5-3.357,7.5-7.5v-29.595l66.681,59.037c-0.348,0.245-0.683,0.516-0.995,0.827l-65.687,65.687v-49.288
c0-4.143-3.357-7.5-7.5-7.5s-7.5,3.357-7.5,7.5v9.164h-38.75c-4.143,0-7.5,3.357-7.5,7.5s3.357,7.5,7.5,7.5h38.75v43.231
c0,4.143,3.357,7.5,7.5,7.5h236.25c0.247,0,0.494-0.013,0.74-0.037c0.115-0.011,0.226-0.033,0.339-0.049
C339.542,260.469,339.67,260.454,339.798,260.429z M330.834,234.967l-65.688-65.687c-0.042-0.042-0.087-0.077-0.13-0.117
l49.383-41.897c3.158-2.68,3.546-7.412,0.866-10.571c-2.678-3.157-7.41-3.547-10.571-0.866l-84.381,71.59l-98.444-87.158h208.965
V234.967z M185.878,179.888c0.535-0.535,0.969-1.131,1.308-1.765l28.051,24.835c1.418,1.255,3.194,1.885,4.972,1.885
c1.726,0,3.451-0.593,4.853-1.781l28.587-24.254c0.26,0.38,0.553,0.743,0.89,1.08l65.687,65.687H120.191L185.878,179.888z"/>
<path d="M7.5,170.676h126.667c4.143,0,7.5-3.357,7.5-7.5s-3.357-7.5-7.5-7.5H7.5c-4.143,0-7.5,3.357-7.5,7.5
S3.357,170.676,7.5,170.676z"/>
<path d="M20.625,129.345H77.5c4.143,0,7.5-3.357,7.5-7.5s-3.357-7.5-7.5-7.5H20.625c-4.143,0-7.5,3.357-7.5,7.5
S16.482,129.345,20.625,129.345z"/>
<path d="M62.5,226.51h-55c-4.143,0-7.5,3.357-7.5,7.5s3.357,7.5,7.5,7.5h55c4.143,0,7.5-3.357,7.5-7.5S66.643,226.51,62.5,226.51z"
/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1,65 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512.001 512.001" style="enable-background:new 0 0 512.001 512.001;" xml:space="preserve">
<g>
<g>
<path d="M468.683,287.265h-69.07c-4.147,0-7.508,3.361-7.508,7.508c0,4.147,3.361,7.508,7.508,7.508h69.07
c4.147,0,7.508-3.361,7.508-7.508C476.191,290.626,472.83,287.265,468.683,287.265z"/>
</g>
</g>
<g>
<g>
<path d="M105.012,268.377L85.781,256l19.231-12.376c3.487-2.243,4.495-6.888,2.251-10.376c-2.244-3.486-6.888-4.497-10.376-2.25
l-17.471,11.243v-20.776c0-4.147-3.361-7.508-7.508-7.508c-4.147,0-7.508,3.361-7.508,7.508v20.775l-17.47-11.243
c-3.486-2.245-8.132-1.238-10.376,2.25c-2.245,3.487-1.237,8.133,2.25,10.376L58.034,256l-19.231,12.376
c-3.487,2.244-4.495,6.889-2.25,10.376c1.435,2.23,3.852,3.446,6.32,3.446c1.391,0,2.799-0.386,4.056-1.196l17.47-11.243v20.775
c0,4.147,3.361,7.508,7.508,7.508c4.147,0,7.508-3.361,7.508-7.508V269.76l17.471,11.243c1.257,0.809,2.664,1.196,4.056,1.196
c2.467,0,4.885-1.216,6.32-3.446C109.507,275.266,108.499,270.62,105.012,268.377z"/>
</g>
</g>
<g>
<g>
<path d="M194.441,268.377L175.21,256l19.231-12.376c3.487-2.244,4.495-6.889,2.25-10.376c-2.245-3.486-6.888-4.497-10.376-2.25
l-17.47,11.243v-20.775c0-4.147-3.361-7.508-7.508-7.508c-4.147,0-7.508,3.361-7.508,7.508v20.776l-17.471-11.243
c-3.487-2.245-8.133-1.238-10.376,2.25c-2.245,3.487-1.237,8.133,2.25,10.376L147.463,256l-19.231,12.376
c-3.487,2.244-4.495,6.889-2.25,10.376c1.435,2.23,3.852,3.446,6.32,3.446c1.391,0,2.799-0.386,4.056-1.196l17.471-11.243v20.776
c0,4.147,3.361,7.508,7.508,7.508c4.147,0,7.508-3.361,7.508-7.508V269.76l17.47,11.243c1.257,0.809,2.664,1.196,4.056,1.196
c2.467,0,4.885-1.216,6.32-3.446C198.936,275.266,197.928,270.62,194.441,268.377z"/>
</g>
</g>
<g>
<g>
<path d="M283.871,268.377L264.64,256l19.231-12.376c3.487-2.243,4.495-6.888,2.251-10.376c-2.245-3.486-6.888-4.497-10.376-2.25
l-17.471,11.243v-20.775c0-4.147-3.361-7.508-7.508-7.508c-4.147,0-7.508,3.361-7.508,7.508v20.775l-17.471-11.243
c-3.486-2.245-8.134-1.238-10.376,2.25c-2.245,3.487-1.237,8.133,2.25,10.376L236.892,256l-19.231,12.376
c-3.487,2.244-4.495,6.889-2.25,10.376c1.435,2.23,3.852,3.446,6.32,3.446c1.391,0,2.799-0.386,4.056-1.196l17.471-11.243v20.775
c0,4.147,3.361,7.508,7.508,7.508c4.147,0,7.508-3.361,7.508-7.508V269.76l17.471,11.243c1.257,0.809,2.664,1.196,4.056,1.196
c2.467,0,4.886-1.216,6.32-3.446C288.366,275.266,287.358,270.62,283.871,268.377z"/>
</g>
</g>
<g>
<g>
<path d="M373.3,268.377L354.069,256l19.231-12.376c3.487-2.244,4.495-6.889,2.25-10.376c-2.244-3.486-6.888-4.497-10.376-2.25
l-17.471,11.243v-20.776c0-4.147-3.361-7.508-7.508-7.508c-4.147,0-7.508,3.361-7.508,7.508v20.775l-17.47-11.243
c-3.486-2.245-8.132-1.238-10.376,2.25c-2.245,3.487-1.237,8.133,2.25,10.376L326.322,256l-19.231,12.376
c-3.487,2.244-4.495,6.889-2.25,10.376c1.435,2.23,3.852,3.446,6.32,3.446c1.391,0,2.799-0.386,4.056-1.196l17.47-11.243v20.776
c0,4.147,3.361,7.508,7.508,7.508c4.147,0,7.508-3.361,7.508-7.508V269.76l17.471,11.243c1.257,0.809,2.664,1.196,4.056,1.196
c2.467,0,4.885-1.216,6.32-3.446C377.795,275.266,376.787,270.62,373.3,268.377z"/>
</g>
</g>
<g>
<g>
<path d="M271.792,330.359H15.016V181.642h93.1c4.147,0,7.508-3.361,7.508-7.508c0-4.147-3.361-7.508-7.508-7.508H12.513
C5.613,166.626,0,172.24,0,179.14v153.722c0,6.9,5.613,12.513,12.513,12.513h259.278c4.147,0,7.508-3.361,7.508-7.508
C279.299,333.72,275.939,330.359,271.792,330.359z"/>
</g>
</g>
<g>
<g>
<path d="M499.487,166.626H162.174c-4.147,0-7.508,3.361-7.508,7.508c0,4.147,3.361,7.508,7.508,7.508h334.811v148.716H323.848
c-4.147,0-7.508,3.361-7.508,7.508c0,4.147,3.361,7.508,7.508,7.508h175.64c6.9,0,12.513-5.613,12.513-12.513V179.14
C512.001,172.24,506.387,166.626,499.487,166.626z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1,46 +0,0 @@
/* eslint-disable react/jsx-filename-extension */
import { createBrowserHistory } from 'history'
import React from 'react'
import { I18nextProvider } from 'react-i18next'
import ReactDOM from 'react-dom'
import { routerMiddleware } from 'connected-react-router'
import { applyMiddleware, compose, createStore } from 'redux'
import thunk from 'redux-thunk'
import i18n from './i18n'
import App from './components/App'
import Root from './components/Root'
import registerServiceWorker from './registerServiceWorker'
import createRootReducer from './reducers'
import { loadProfile } from './actions/user'
import { historyEnhancer } from './utils/history'
export const history = historyEnhancer(createBrowserHistory())
history.listen(() => {
window.scrollTo(0, 0)
})
export const rootNode = document.getElementById('root')
export const store = createStore(
createRootReducer(history),
window.__STATE__, // Server state
(window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose)(
applyMiddleware(routerMiddleware(history), thunk)
)
)
if (window.localStorage.authToken !== null) {
store.dispatch(loadProfile())
}
ReactDOM.render(
<Root store={store} history={history}>
<I18nextProvider i18n={i18n}>
<App />
</I18nextProvider>
</Root>,
rootNode
)
registerServiceWorker()

View File

@ -1,34 +0,0 @@
{
"Actions": "Actions",
"Active": "Active",
"workouts exist": "workouts exist",
"Add admin rights": "Add admin rights",
"Add/remove admin rights, delete user account.": "Add/remove admin rights, delete user account.",
"Administration": "Administration",
"Application": "Application",
"Application configuration": "Application configuration",
"Back": "Back",
"Disable": "Disable",
"Enable": "Enable",
"Enable/disable sports.": "Enable/disable sports.",
"FitTrackee administration": "FitTrackee administration",
"id": "id",
"if 0, no limitation": "if 0, no limitation",
"Image": "Image",
"Label": "Label",
"Max. number of active users": "Max. number of active users",
"Max. files of zip archive": "Max. files of zip archive",
"Max. size of uploaded files": "Max. size of uploaded files",
"Max. size of uploaded files (in Mb)": "Max. size of uploaded files (in Mb)",
"Max. size of zip archive": "Max. size of zip archive",
"Max. size of zip archive (in Mb)": "Max. size of zip archive (in Mb)",
"Registration is currently disabled.": "Registration is currently disabled.",
"Registration is currently enabled.": "Registration is currently enabled.",
"Remove admin rights": "Remove admin rights",
"Sports": "Sports",
"Update application configuration (maximum number of registered users, maximum files size).": "Update application configuration (maximum number of registered users, maximum files size).",
"uploads": "uploads",
"user": "user",
"Users": "Users",
"users": "users"
}

View File

@ -1,40 +0,0 @@
{
"workouts count": "workouts count",
"Add workout": "Add workout",
"admin rights": "admin rights",
"ascending": "ascending",
"Back": "Back",
"Back to home": "Back to home",
"Cancel": "Cancel",
"Confirmation": "Confirmation",
"Dashboard": "Dashboard",
"descending": "descending",
"Edit": "Edit",
"day": "day",
"days": "days",
"Next": "Next",
"No": "No",
"no": "no",
"No records.": "No records.",
"No workouts.": "No workouts.",
"Page not found": "Page not found",
"Previous": "Prev",
"registration date": "registration date",
"remaining characters": "remaining characters",
"Sort": "Sort",
"Sort by": "Sort by",
"Sport": "Sport",
"sport": "sport",
"Sports": "Sports",
"sports": "sports",
"Statistics": "Statistics",
"Submit": "Submit",
"to": "to",
"user name": "user name",
"Workout": "Workout",
"Workouts": "Workouts",
"workout": "workout",
"workouts": "workouts",
"Yes": "Yes",
"yes": "yes"
}

View File

@ -1,5 +0,0 @@
{
"Personal records": "Personal records",
"This month": "This month",
"Upload one !": "Upload one !"
}

View File

@ -1,40 +0,0 @@
{
"3 to 12 characters required for username.": "3 to 12 characters required for username.",
"8 characters required for password.": "8 characters required for password.",
"An error occurred. Please contact the administrator.": "An error occurred. Please contact the administrator.",
"application": "application",
"Error during picture deletion.": "Error during picture deletion.",
"Error during picture update.": "Error during picture update.",
"Error during picture update, file size exceeds max size.": "Error during picture update, file size exceeds max size.",
"Error. Registration is disabled.": "Error. Registration is disabled.",
"Error. Please try again or contact the administrator.": "Error. Please try again or contact the administrator.",
"File extension not allowed.": "File extension not allowed.",
"File size is greater than the allowed size": "File size is greater than the allowed size",
"Incorrect id": "Incorrect id",
"Invalid credentials.": "Invalid credentials.",
"Invalid payload.": "Invalid payload.",
"Invalid token. Please log in again.": "Invalid token. Please log in again.",
"Max. files in a zip archive must be greater than 0": "Max. files in a zip archive must be greater than 0",
"Max. size of uploaded files must be greater than 0": "Max. size of uploaded files must be greater than 0",
"Max. size of zip archive must be equal or greater than max. size of uploaded files": "Max. size of zip archive must be equal or greater than max. size of uploaded files",
"Max. size of zip archive must be greater than 0": "Max. size of zip archive must be greater than 0",
"No file part.": "No file part.",
"No picture.": "No picture.",
"No selected file.": "No selected file.",
"no correct file.": "no correct file.",
"no gpx file for this workout": "no gpx file for this workout",
"Password and password confirmation don't match.": "Password and password confirmation don't match.",
"Provide a valid auth token": "Provide a valid auth token",
"records": "records",
"Signature expired. Please log in again.": "Signature expired. Please log in again.",
"Sorry. That user already exists.": "Sorry. That user already exists.",
"Sport can not be disabled, workouts exist." : "Sport can not be disabled, workouts exist.",
"Sport does not exist.": "Sport does not exist.",
"sports": "sports",
"statistics": "statistiques",
"User does not exist.": "User does not exist.",
"Valid email must be provided.\n": "Valid email must be provided.",
"workouts": "workouts",
"You can not delete your account, no other user has admin rights.": "You can not delete your account, no other user has admin rights.",
"You do not have permissions.": "You do not have permissions."
}

View File

@ -1,19 +0,0 @@
import EnWorkoutsTranslations from './workouts.json'
import EnAdministrationTranslations from './administration.json'
import EnCommonTranslations from './common.json'
import EnDashboardTranslations from './dashboard.json'
import EnMessagesTranslations from './messages.json'
import EnSportsTranslations from './sports.json'
import EnStatisticsTranslations from './statistics.json'
import EnUserTranslations from './user.json'
export const enResources = {
workouts: EnWorkoutsTranslations,
administration: EnAdministrationTranslations,
common: EnCommonTranslations,
dashboard: EnDashboardTranslations,
messages: EnMessagesTranslations,
sports: EnSportsTranslations,
statistics: EnStatisticsTranslations,
user: EnUserTranslations,
}

View File

@ -1,13 +0,0 @@
{
"Cycling (Sport)": "Cycling (Sport)",
"Cycling (Transport)": "Cycling (Transport)",
"Hiking": "Hiking",
"Mountain Biking": "Mountain Biking",
"Mountain Biking (Electric)": "Mountain Biking (Electric)",
"Running": "Running",
"Walking": "Walking",
"Trail" : "Trail",
"Skiing (Alpine)" : "Skiing (Alpine)",
"Skiing (Cross Country)" : "Skiing (Cross Country)",
"Rowing" : "Rowing"
}

View File

@ -1,11 +0,0 @@
{
"workouts": "workouts",
"distance": "distance",
"duration": "duration",
"ascent": "ascent",
"descent": "descent",
"month": "month",
"Statistics": "Statistics",
"year": "year",
"week": "week"
}

View File

@ -1,45 +0,0 @@
{
"Admin": "Admin",
"Are you sure you want to delete this account? All data will be deleted, this cannot be undone.": "Are you sure you want to delete this account? All data will be deleted, this cannot be undone.",
"Are you sure you want to delete your account? All data will be deleted, this cannot be undone.": "Are you sure you want to delete your account? All data will be deleted, this cannot be undone.",
"Bio": "Bio",
"Birth Date": "Birth Date",
"Check your email. If your address is in our database, you'll received an email with a link to reset your password.": "Check your email. If your address is in our database, you'll received an email with a link to reset your password.",
"Delete my account": "Delete my account",
"Delete picture": "Delete picture",
"Delete user account": "Delete user account",
"Edit Profile": "Edit Profile",
"Email": "Email",
"Enter a username": "Enter a username",
"Enter an email address": "Enter an email address",
"Enter a password": "Enter a password",
"Enter the password confirmation": "Enter the password confirmation",
"First day of week": "First day of week",
"First Name": "First Name",
"Forgot password?": "Forgot password?",
"Invalid token. Please request a new token.": "Invalid token. Please request a new token.",
"Language": "Language",
"Last Name": "Last Name",
"Location": "Location",
"loggedOut": "You are now logged out. Click <1>here</1> to log back in.",
"Login": "Login",
"login": "login",
"Logout": "Logout",
"Monday": "Monday",
"Password": "Password",
"Password Confirmation": "Password Confirmation",
"Password reset": "Password reset",
"password reset": "password reset",
"Profile": "Profile",
"Profile Edition": "Profile Edition",
"Register": "Register",
"register": "register",
"Registration Date": "Registration Date",
"Reset your password": "Reset your password",
"reset your password": "reset your password",
"Send": "Send",
"Sunday": "Sunday",
"Timezone": "Timezone",
"updatedPasswordText": "Your password have been updated. Click <1>here</1> to log in." ,
"Username": "Username"
}

View File

@ -1,58 +0,0 @@
{
"Workouts": "Workouts",
"Workout": "Workout",
"Workout Date": "Workout Date",
"Add a workout": "Add a workout",
"Are you sure you want to delete this workout?": "Are you sure you want to delete this workout?",
"Ave. speed": "Ave. speed",
"Ascent": "Ascent",
"Average speed": "Average speed",
"Chart": "Chart",
"data from gpx, without any cleaning": "data from gpx, without any cleaning",
"Date": "Date",
"Delete workout": "Delete workout",
"Descent": "Descent",
"Distance": "Distance",
"distance": "distance",
"Duration": "Duration",
"duration": "duration",
"Edit a workout": "Edit a workout",
"Edit workout": "Edit workout",
"elevation": "elevation",
"End": "End",
"Farest distance": "Farest distance",
"Filter": "Filter",
"From": "From",
"gpxFile": "<strong>gpx</strong> file",
"Longest duration": "Longest duration",
"Max. altitude" : "Max. altitude",
"Max. speed": "Max. speed",
"Min. altitude": "Min. altitude",
"no folder inside": "no folder inside",
"files max": "files max",
"max size": "max size",
"No data to display": "No data to display",
"No Map": "No Map",
"No next workout": "No next workout",
"No next segment": "No next segment",
"No notes": "No notes",
"No previous workout": "No previous workout",
"No previous segment": "No previous segment",
"Notes": "Notes",
"pauses": "pauses",
"Personal records": "Personal records",
"See next workout": "See next workout",
"See next segment": "See next segment",
"See previous workout": "See previous workout",
"See previous segment": "See previous segment",
"segment": "segment",
"Segments": "Segments",
"speed": "speed",
"Start": "Start",
"Title": "Title",
"To": "To",
"total duration": "total duration",
"with gpx file": "with gpx file",
"without gpx file": "without gpx file",
"zipFile": "or <strong> zip</strong> file containing <strong>gpx </strong> files"
}

View File

@ -1,34 +0,0 @@
{
"Actions": "Actions",
"Active": "Active",
"Add admin rights": "Ajouter des droits d'admin",
"Add/remove admin rights, delete user account.": "Ajouter/retirer des droits d'adminsitration, supprimer des comptes utilisateurs.",
"Administration": "Administration",
"workouts exist": "des séances existent",
"Application": "Application",
"Application configuration": "Configuration de l'application",
"Back": "Retour",
"Disable": "désactiver",
"Enable": "activer",
"Enable/disable sports.": "Activer/désactiver des sports.",
"FitTrackee administration": "Administration de FitTrackee",
"id": "id",
"if 0, no limitation": "si égal à 0, pas limite d'inscription",
"Image": "Image",
"Label": "Label",
"Max. number of active users": "Nombre maximum d'utilisateurs actifs",
"Max. files of zip archive": "Nombre max. de fichiers dans une archive zip",
"Max. size of uploaded files": "Taille max. des fichiers",
"Max. size of uploaded files (in Mb)": "Taille max. des fichiers (en Mo)",
"Max. size of zip archive": "Taille max. des archives zip",
"Max. size of zip archive (in Mb)": "Taille max. des archives zip (en Mo)",
"Registration is currently disabled.": "Les inscriptions sont actuellement désactivées.",
"Registration is currently enabled.": "Les inscriptions sont actuellement activées.",
"Remove admin rights": "Retirer des droits d'admin",
"Sports": "Sports",
"Update application configuration (maximum number of registered users, maximum files size).": "Configurer l'application (nombre maximum d'utilisateurs inscrits, taille maximale des fichers).",
"uploads": "fichiers",
"user": "user",
"Users": "Utilisateurs",
"users": "utilisateurs"
}

View File

@ -1,40 +0,0 @@
{
"workouts count": "nombre d'séances",
"Add workout": "Ajouter une séance",
"admin rights": "droits d'admin",
"ascending": "ascendant",
"Back": "Revenir à la page précédente",
"Back to home": "Retour à l'accueil",
"Cancel": "Annuler",
"Confirmation": "Confirmation",
"Dashboard": "Tableau de Bord",
"descending": "descendant",
"Edit": "Modifier",
"day": "jour",
"days": "jours",
"Next": "Page suivante",
"No": "Non",
"no": "non",
"No records.": "Pas de records.",
"No workouts.": "Pas de séances.",
"Page not found": "Page introuvable",
"Previous": "Page précédente",
"remaining characters": "nombre de caractères restants ",
"registration date": "date d'inscription",
"Sort": "Tri",
"Sort by": "Trier par",
"Sport": "Sport",
"sport": "sport",
"Sports": "Sports",
"sports": "sports",
"Statistics": "Statistiques",
"Submit": "Valider",
"to": "à",
"user name": "utilisateur",
"Workout": "Séance",
"Workouts": "Séances",
"workout": "séance",
"workouts": "séances",
"Yes": "Oui",
"yes": "oui"
}

View File

@ -1,5 +0,0 @@
{
"Personal records": "Mes records",
"This month": "Ce mois",
"Upload one !": "Ajoutez votre première séance !"
}

View File

@ -1,40 +0,0 @@
{
"3 to 12 characters required for username.": "3 à 12 caractères requis pour le nom.",
"8 characters required for password.": "8 caractères minimum pour le mot de passe.",
"An error occurred. Please contact the administrator.": "Une erreur s'est produite. Merci de contacter l'administrateur.",
"application": "application",
"Error during picture deletion.": "Erreur lors de la suppression de l'image.",
"Error during picture update.": "Erreur lors de la mise à jour de l'image.",
"Error during picture update, file size exceeds max size.": "Erreur lors de la mise à jour de l'image, la taille du ficher dépasse la taille maximum autorisée",
"Error. Registration is disabled.": "Erreur. L'inscription est désactivée.",
"Error. Please try again or contact the administrator.": "Erreur. Veuillez réessayer ou contacter l'administrateur",
"File extension not allowed.": "Extension de fichier non autorisée.",
"File size is greater than the allowed size": "La taille du fichier est supérieure à la limite autorisée",
"Incorrect id": "Id incorrect",
"Invalid credentials.": "Identifiants invalides.",
"Invalid payload.": "Données incorrectes.",
"Invalid token. Please log in again.": "Jeton invalide. Merci de vous reconnecter.",
"Max. files in a zip archive must be greater than 0": "Le nombre max. de fichiers dans une archive doit être supérieur à 0",
"Max. size of uploaded files must be greater than 0": "La taille max. des fichiers doit être supérieure à 0",
"Max. size of zip archive must be equal or greater than max. size of uploaded files": "La taille max. d'une archive doit être supérieure ou égale à la taille max. d'un fichier",
"Max. size of zip archive must be greater than 0": "La taille max. d'une archive doit être supérieure à 0",
"No file part.": "Pas de fichier fourni.",
"No picture.": "Pas d'image.",
"No selected file.": "Pas de fichier sélectionné.",
"no correct file.": "fichier incorrect",
"no gpx file for this workout": "pas de fichier gpx pour cette séance",
"Password and password confirmation don't match.": "Les mots de passe saisis sont différents.",
"Provide a valid auth token": "Merci de fournir un jeton valide",
"records": "records",
"Signature expired. Please log in again.": "Signature expirée. Merci de vous reconnecter.",
"Sorry. That user already exists.": "Désolé. Cet utilisateur existe déjà.",
"Sport can not be disabled, workouts exist." : "Le sport ne peut être désactivé, des séancees existent",
"Sport does not exist.": "Le sport n'existe pas.",
"sports": "sports",
"statistics": "statistics",
"User does not exist.": "L'utilisateur n'existe pas.",
"Valid email must be provided.\n": "L'email fourni n'est pas valide.",
"workouts": "séances",
"You can not delete your account, no other user has admin rights.": "Vous ne pouvez pas supprimer votre compte, aucun autre utilisateur n'a des droits d'administration.",
"You do not have permissions.": "Vous n'avez pas les permissions nécessaires."
}

View File

@ -1,19 +0,0 @@
import FrWorkoutsTranslations from './workouts.json'
import FrAdministrationTranslations from './administration.json'
import FrCommonTranslations from './common.json'
import FrDashboardTranslations from './dashboard.json'
import FrMessagesTranslations from './messages.json'
import FrSportsTranslations from './sports.json'
import FrStatisticsTranslations from './statistics.json'
import FrUserTranslations from './user.json'
export const frResources = {
workouts: FrWorkoutsTranslations,
administration: FrAdministrationTranslations,
common: FrCommonTranslations,
dashboard: FrDashboardTranslations,
messages: FrMessagesTranslations,
sports: FrSportsTranslations,
statistics: FrStatisticsTranslations,
user: FrUserTranslations,
}

View File

@ -1,13 +0,0 @@
{
"Cycling (Sport)": "Vélo (Sport)",
"Cycling (Transport)": "Vélo (Transport)",
"Hiking": "Randonnée",
"Mountain Biking": "VTT",
"Mountain Biking (Electric)": "VTT (Electrique)",
"Running": "Course",
"Walking": "Marche",
"Trail" : "Trail",
"Skiing (Alpine)" : "Ski (Alpin)",
"Skiing (Cross Country)" : "Ski (Randonnée)",
"Rowing" : "Aviron"
}

View File

@ -1,11 +0,0 @@
{
"workouts": "séances",
"distance": "distance",
"duration": "durée",
"ascent": "dénivelé +",
"descent": "dénivelé -",
"month": "mois",
"Statistics": "Statistiques",
"year": "année",
"week": "semaine"
}

View File

@ -1,45 +0,0 @@
{
"Admin": "Admin",
"Are you sure you want to delete this account? All data will be deleted, this cannot be undone.": "Etes-vous sûr de vouloir supprimer ce compte ? Toutes les données seront définitivement effacés.",
"Are you sure you want to delete your account? All data will be deleted, this cannot be undone.": "Etes-vous sûr de vouloir supprimer votre compte ? Toutes les données seront définitivement effacés.",
"Bio": "Bio",
"Birth Date": "Date de naissance",
"Check your email. If your address is in our database, you'll received an email with a link to reset your password.": "Vérifiez vore boite mail. Si vote adresse est dans notre base de données, vous recevrez un email avec un lien pour réinitialiser votre mot de passe",
"Delete my account": "Supprimer mon compte",
"Delete picture": "Supprimer l'image",
"Delete user account": "Supprimer le compte",
"Edit Profile": "Editer le profil",
"Email": "Email",
"Enter a username": "Saisir un nom",
"Enter an email address": "Saisir une adresse e-mail",
"Enter a password": "Saisir un mot de passe",
"Enter the password confirmation": "Confirmer le mot de passe",
"First day of week": "Premier jour de la semaine",
"First Name": "Prénom",
"Forgot password?": "Mot de passe oublié ?",
"Invalid token. Please request a new token.": "Token invalid. Veuillez demander un nouveau token.",
"Language": "Langue",
"Last Name": "Nom",
"Location": "Lieu",
"loggedOut": "Vous êtes déconnecté. Cliquez <1>ici</1> pour vous reconnecter.",
"Login": "Se connecter",
"login": "se connecter",
"Logout": "Se déconnecter",
"Monday": "Lundi",
"Password": "Mot de passe",
"Password Confirmation": "Confirmation du mot de passe",
"Password reset": "Réinitialiser votre mot de passe",
"password reset": "réinitialiser votre mot de passe",
"Profile": "Profil",
"Profile Edition": "Edition du profil",
"Register": "S'inscrire",
"register": "s'inscrire",
"Registration Date": "Date d'inscription",
"Reset your password": "Réinitialiser votre mot de passe",
"reset your password": "réinitialiser votre mot de passe",
"Send": "Envoyer",
"Sunday": "Dimanche",
"Timezone": "Fuseau horaire",
"updatedPasswordText": "Votre mot de passe a été mis à jour. Cliquez <1>ici</1> pour vous connecter.",
"Username": "Nom d'utilisateur"
}

View File

@ -1,58 +0,0 @@
{
"Workouts": "Séances",
"Workout": "Séance",
"Workout Date": "Date de l'séance",
"Add a workout": "Ajouter une séance",
"Are you sure you want to delete this workout?": "Etes-vous sûr de vouloir supprimer cette séance ?",
"Ave. speed": "Vitesse moyenne",
"Ascent": "Dénivelé +",
"Average speed": "Vitesse moyenne",
"Chart": "Analyse",
"data from gpx, without any cleaning": "données issues du fichier gpx, sans correction",
"Date": "Date",
"Delete workout": "Supprimer l'séance",
"Descent": "Dénivelé -",
"Distance": "Distance",
"distance": "distance",
"Duration": "Durée",
"duration": "durée",
"Edit a workout": "Editer une séance",
"Edit workout": "Editer une workout",
"elevation": "altitude",
"End": "Arrivée",
"Farest distance": "Distance la + longue",
"Filter": "Filtrer",
"From": "A partir de",
"gpxFile": "fichier <strong>gpx</strong>",
"Longest duration": "Durée la + longue",
"Max. altitude" : "Altitude max",
"Max. speed": "Vitesse max",
"Min. altitude": "Altitude min",
"no folder inside": "pas de répertoire",
"files max": " fichiers max",
"max size": "taille max",
"No data to display": "Pas de données à afficher",
"No Map": "Pas de carte",
"No next workout": "Pas d'séance suivante",
"No next segment": "Pas de segment suivant",
"No notes": "Pas de notes",
"No previous workout": "Pas d'séance précédente",
"No previous segment": "Pas de segment précédent",
"Notes": "Notes",
"pauses": "pauses",
"Personal records": "Records personnels",
"See next workout": "Voir l'séance suivante",
"See next segment": "Voir le segment suivant",
"See previous workout": "Voir l'séance précédente",
"See previous segment": "Voir le segment précédent",
"segment": "segment",
"Segments": "Segments",
"Start": "Départ",
"speed": "vitesse",
"Title": "Titre",
"To": "Jusqu'au",
"total duration": "durée totale",
"with gpx file": "avec un fichier gpx",
"without gpx file": "sans fichier gpx",
"zipFile": "ou un fichier <strong> zip</strong> contenant des fichiers <strong>gpx</strong>"
}

View File

@ -1,7 +0,0 @@
import { enResources } from './en/resources'
import { frResources } from './fr/resources'
export const resources = {
en: enResources,
fr: frResources,
}

View File

@ -1,7 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -1,213 +0,0 @@
import { connectRouter } from 'connected-react-router'
import { combineReducers } from 'redux'
import initial from './initial'
const handleDataAndError = (state, type, action) => {
if (action.target !== type) {
return state
}
if (action.type === 'SET_DATA') {
return {
...state,
data: action.data[action.target],
}
}
if (action.type === 'SET_PAGINATED_DATA') {
return {
...state,
data: action.data[action.target],
pagination: action.pagination,
}
}
return state
}
const workouts = (state = initial.workouts, action) => {
switch (action.type) {
case 'LOGOUT':
return initial.workouts
case 'PUSH_WORKOUTS':
return {
...state,
data: state.data.concat(action.workouts),
}
case 'REMOVE_WORKOUT':
return {
...state,
data: state.data.filter(workout => workout.id !== action.workoutId),
}
default:
return handleDataAndError(state, 'workouts', action)
}
}
const application = (state = initial.application, action) => {
if (action.type === 'SET_APP_CONFIG') {
return {
...state,
config: action.data,
}
}
if (action.type === 'SET_APP_STATS') {
return {
...state,
statistics: action.data,
}
}
return state
}
const calendarWorkouts = (state = initial.calendarWorkouts, action) => {
switch (action.type) {
case 'LOGOUT':
return initial.calendarWorkouts
case 'UPDATE_CALENDAR':
return {
...state,
data: action.workouts,
}
default:
return handleDataAndError(state, 'calendarWorkouts', action)
}
}
const chartData = (state = initial.chartData, action) => {
if (action.type === 'SET_CHART_DATA') {
return action.chartData
}
return state
}
const gpx = (state = initial.gpx, action) => {
if (action.type === 'SET_GPX') {
return action.gpxContent
}
return state
}
const language = (state = initial.language, action) => {
if (action.type === 'SET_LANGUAGE') {
return action.language
}
return state
}
const loading = (state = initial.loading, action) => {
if (action.type === 'SET_LOADING') {
return action.loading
}
return state
}
const message = (state = initial.message, action) => {
switch (action.type) {
case 'AUTH_ERROR':
case 'PROFILE_ERROR':
case 'PROFILE_UPDATE_ERROR':
case 'PICTURE_ERROR':
case 'SET_ERROR':
return action.message
case 'CLEAN_ALL_MESSAGES':
case 'LOGOUT':
case 'PROFILE_SUCCESS':
case 'SET_APP_CONFIG':
case 'SET_RESULTS':
case '@@router/LOCATION_CHANGE':
return ''
default:
return state
}
}
const messages = (state = initial.messages, action) => {
switch (action.type) {
case 'AUTH_ERRORS':
case 'APP_ERRORS':
return action.messages
case 'CLEAN_ALL_MESSAGES':
case 'LOGOUT':
case 'PROFILE_SUCCESS':
case '@@router/LOCATION_CHANGE':
return []
default:
return state
}
}
const records = (state = initial.records, action) => {
if (action.type === 'LOGOUT') {
return initial.records
}
return handleDataAndError(state, 'records', action)
}
const sports = (state = initial.sports, action) => {
if (action.type === 'UPDATE_SPORT_DATA') {
return {
...state,
data: state.data.map(sport => {
if (sport.id === action.data.id) {
sport.is_active = action.data.is_active
}
return sport
}),
}
}
return handleDataAndError(state, 'sports', action)
}
const users = (state = initial.users, action) => {
if (action.type === 'UPDATE_USER_DATA') {
return {
...state,
data: state.data.map(user => {
if (user.username === action.data.username) {
user.admin = action.data.admin
}
return user
}),
}
}
return handleDataAndError(state, 'users', action)
}
const user = (state = initial.user, action) => {
switch (action.type) {
case 'AUTH_ERROR':
case 'PROFILE_ERROR':
case 'LOGOUT':
window.localStorage.removeItem('authToken')
return initial.user
case 'PROFILE_SUCCESS':
return action.profil
default:
return state
}
}
const statistics = (state = initial.statistics, action) => {
if (action.type === 'LOGOUT') {
return initial.statistics
}
return handleDataAndError(state, 'statistics', action)
}
export default history =>
combineReducers({
workouts,
application,
calendarWorkouts,
chartData,
gpx,
language,
loading,
message,
messages,
records,
router: connectRouter(history),
sports,
statistics,
user,
users,
})

View File

@ -1,45 +0,0 @@
const emptyData = {
data: [],
}
export default {
language: 'en',
message: '',
messages: [],
user: {
isAuthenticated: false,
},
workouts: {
...emptyData,
},
application: {
statistics: {},
config: {
gpx_limit_import: null,
is_registration_enabled: null,
max_single_file_size: null,
max_users: null,
max_zip_file_size: null,
registration: null,
},
},
calendarWorkouts: {
...emptyData,
},
chartData: [],
// check if storing gpx content is OK
gpx: null,
loading: false,
records: {
...emptyData,
},
sports: {
...emptyData,
},
statistics: {
data: {},
},
users: {
...emptyData,
},
}

View File

@ -1,113 +0,0 @@
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
)
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location)
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets;
// see https://github.com/facebookincubator/create-react-app/issues/2374
return
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
if (isLocalhost) {
// This is running on localhost.
// Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl)
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl)
}
})
}
}
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
// eslint-disable-next-line no-console
console.log('New content is available; please refresh.')
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
// eslint-disable-next-line no-console
console.log('Content is cached for offline use.')
}
}
}
}
})
.catch(error => {
console.error('Error during service worker registration:', error)
})
}
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload()
})
})
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl)
}
})
.catch(() => {
// eslint-disable-next-line no-console
console.log(
'No internet connection found. App is running in offline mode.'
)
})
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister()
})
}
}

View File

@ -1,44 +0,0 @@
const routesWithoutAuthentication = [
'/login',
'/register',
'/password-reset',
'/password-reset/request',
'/password-reset/sent',
'/updated-password',
]
const updatePath = (toPath, newPath) => {
if (typeof toPath === 'string' || toPath instanceof String) {
toPath = newPath
} else {
toPath.pathname = newPath
}
return toPath
}
const pathInterceptor = toPath => {
if (
!window.localStorage.authToken &&
!routesWithoutAuthentication.includes(toPath.pathname)
) {
toPath = updatePath(toPath, '/login')
}
if (
window.localStorage.authToken &&
routesWithoutAuthentication.includes(toPath.pathname)
) {
toPath = updatePath(toPath, '/')
}
return toPath
}
export const historyEnhancer = originalHistory => {
originalHistory.location = pathInterceptor(originalHistory.location)
return {
...originalHistory,
push: (path, ...args) =>
originalHistory.push(pathInterceptor(path), ...args),
replace: (path, ...args) =>
originalHistory.replace(pathInterceptor(path), ...args),
}
}

View File

@ -1,126 +0,0 @@
import { format, parse } from 'date-fns'
import { DateTime } from 'luxon'
const suffixes = ['bytes', 'KB', 'MB', 'GB', 'TB']
export const getFileSize = (fileSize, asText = true) => {
const i = Math.floor(Math.log(fileSize) / Math.log(1024))
if (!fileSize) {
return asText ? '0 bytes' : { size: 0, suffix: 'bytes' }
}
const size = (fileSize / Math.pow(1024, i)).toFixed(1)
const suffix = suffixes[i]
return asText ? `${size}${suffix}` : { size, suffix }
}
export const getFileSizeInMB = fileSize => {
const value = fileSize / 1048576
return (!fileSize && 0) || +value.toFixed(2)
}
export const version = '0.4.9' // version stored in 'utils' for now
export const apiUrl =
process.env.NODE_ENV === 'production'
? '/api/'
: `${process.env.REACT_APP_API_URL}/api/`
export const userFilters = [
{ key: 'workouts_count', label: 'workouts count' },
{ key: 'admin', label: 'admin rights' },
{ key: 'created_at', label: 'registration date' },
{ key: 'username', label: 'user name' },
]
export const sortOrders = [
{ key: 'asc', label: 'ascending' },
{ key: 'desc', label: 'descending' },
]
export const isLoggedIn = () => !!window.localStorage.authToken
export const generateIds = arr => {
let i = 0
return arr.map(val => {
const obj = { id: i, value: val }
i++
return obj
})
}
export const createApiRequest = params => {
const headers = {}
if (!params.noAuthorization) {
headers.Authorization = `Bearer ${window.localStorage.getItem('authToken')}`
}
if (params.type) {
headers['Content-Type'] = params.type
}
const requestParams = {
method: params.method,
headers: headers,
}
if (params.type === 'application/json' && params.body) {
requestParams.body = JSON.stringify(params.body)
} else if (params.body) {
requestParams.body = params.body
}
const request = new Request(`${apiUrl}${params.url}`, requestParams)
return fetch(request)
.then(response =>
params.method === 'DELETE' || response.status === 413
? response
: response.json()
)
.catch(error => {
console.error(error)
return new Error('An error occurred. Please contact the administrator.')
})
}
export const getDateWithTZ = (date, tz) => {
if (!date) {
return ''
}
const dt = DateTime.fromISO(
format(new Date(date), "yyyy-MM-dd'T'HH:mm:ss.SSSxxx")
).setZone(tz)
return parse(
dt.toFormat('yyyy-MM-dd HH:mm:ss'),
'yyyy-MM-dd HH:mm:ss',
new Date()
)
}
export const capitalize = target =>
target.charAt(0).toUpperCase() + target.slice(1)
export const rangePagination = pages =>
Array.from({ length: pages }, (_, i) => i + 1)
const sortValues = (a, b) => {
const valueALabel = a.label.toLowerCase()
const valueBLabel = b.label.toLowerCase()
return valueALabel > valueBLabel ? 1 : valueALabel < valueBLabel ? -1 : 0
}
export const translateValues = (t, values, key = 'common') =>
values
.map(value => ({
...value,
label: t(`${key}:${value.label}`),
}))
.sort(sortValues)
export const formatUrl = (pathname, query) => {
let url = pathname
if (query.id || (pathname === 'users' && query.username)) {
url = `${url}/${query.username ? query.username : query.id}`
} else if (Object.keys(query).length > 0) {
url += '?'
Object.keys(query)
.filter(key => query[key])
.map(
(key, index) => (url += `${index === 0 ? '' : '&'}${key}=${query[key]}`)
)
}
return url
}

View File

@ -1,124 +0,0 @@
import {
addDays,
addMonths,
addYears,
format,
startOfMonth,
startOfWeek,
startOfYear,
} from 'date-fns'
const xAxisFormats = [
{ duration: 'week', dateFormat: 'yyyy-MM-dd', xAxis: 'dd/MM' },
{ duration: 'month', dateFormat: 'yyyy-MM', xAxis: 'MM/yyyy' },
{ duration: 'year', dateFormat: 'yyyy', xAxis: 'yyyy' },
]
export const formatDuration = (totalSeconds, formatWithDay = false) => {
let days = '0'
if (formatWithDay) {
days = String(Math.floor(totalSeconds / 86400))
totalSeconds %= 86400
}
const hours = String(Math.floor(totalSeconds / 3600)).padStart(2, '0')
totalSeconds %= 3600
const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, '0')
const seconds = String(totalSeconds % 60).padStart(2, '0')
if (formatWithDay) {
return `${days === '0' ? '' : `${days}d:`}${
hours === '00' ? '' : `${hours}h:`
}${minutes}m:${seconds}s`
}
return `${hours === '00' ? '' : `${hours}:`}${minutes}:${seconds}`
}
export const formatValue = (displayedData, value) =>
value === 0
? ''
: displayedData === 'distance'
? `${value.toFixed(2)} km`
: displayedData === 'duration'
? formatDuration(value)
: displayedData === 'ascent'
? `${value.toFixed(2)} km`
: displayedData === 'descent'
? `${value.toFixed(2)} km`
: value
const dateIncrement = (duration, day) => {
switch (duration) {
case 'week':
return addDays(day, 7)
case 'year':
return addYears(day, 1)
case 'month':
default:
return addMonths(day, 1)
}
}
const startDate = (duration, day, weekm) => {
switch (duration) {
case 'week':
return startOfWeek(day, { weekStartsOn: weekm ? 1 : 0 })
case 'year':
return startOfYear(day)
case 'month':
default:
return startOfMonth(day)
}
}
export const formatStats = (stats, sports, params, displayedSports, weekm) => {
const nbWorkoutsStats = []
const distanceStats = []
const durationStats = []
const ascentStats = []
const descentStats = []
for (
let day = startDate(params.duration, params.start, weekm);
day <= params.end;
day = dateIncrement(params.duration, day)
) {
const [xAxisFormat] = xAxisFormats.filter(
x => x.duration === params.duration
)
const date = format(day, xAxisFormat.dateFormat)
const xAxis = format(day, xAxisFormat.xAxis)
const dataNbWorkouts = { date: xAxis }
const dataDistance = { date: xAxis }
const dataDuration = { date: xAxis }
const dataAscent = { date: xAxis }
const dataDescent = { date: xAxis }
if (stats[date]) {
Object.keys(stats[date])
.filter(sportId =>
displayedSports ? displayedSports.includes(+sportId) : true
)
.map(sportId => {
const sportLabel = sports.filter(s => s.id === +sportId)[0].label
dataNbWorkouts[sportLabel] = stats[date][sportId].nb_workouts
dataDistance[sportLabel] = stats[date][sportId].total_distance
dataDuration[sportLabel] = stats[date][sportId].total_duration
dataAscent[sportLabel] = stats[date][sportId].total_ascent / 1000
dataDescent[sportLabel] = stats[date][sportId].total_descent / 1000
return null
})
}
nbWorkoutsStats.push(dataNbWorkouts)
distanceStats.push(dataDistance)
durationStats.push(dataDuration)
ascentStats.push(dataAscent)
descentStats.push(dataDescent)
}
return {
workouts: nbWorkoutsStats,
distance: distanceStats,
duration: durationStats,
ascent: ascentStats,
descent: descentStats,
}
}

Some files were not shown because too many files have changed in this diff Show More