Client - add application configuration in Application Admin - #15

This commit is contained in:
Sam 2019-11-13 20:15:50 +01:00
parent 97534b698b
commit 1398c7ff4a
28 changed files with 563 additions and 97 deletions

View File

@ -11,8 +11,8 @@ def init_config():
""" """
init application configuration if not existing in database init application configuration if not existing in database
Note: get some configuration values from env variables (for FitTrackee versions Note: get some configuration values from env variables
prior to v0.3.0) (for FitTrackee versions prior to v0.3.0)
""" """
existing_config = AppConfig.query.one_or_none() existing_config = AppConfig.query.one_or_none()
if not existing_config: if not existing_config:

View File

@ -43,6 +43,10 @@ export const addActivity = form => dispatch =>
dispatch(loadProfile()) dispatch(loadProfile())
history.push('/') history.push('/')
} }
} else if (ret.status === 413) {
dispatch(
setError('activities|File size is greater than the allowed size')
)
} else { } else {
dispatch(setError(`activities|${ret.message}`)) dispatch(setError(`activities|${ret.message}`))
} }

View File

@ -0,0 +1,38 @@
import FitTrackeeGenericApi from '../fitTrackeeApi'
import { setError } from './index'
export const setAppConfig = data => ({
type: 'SET_APP_CONFIG',
data,
})
export const setAppStats = data => ({
type: 'SET_APP_STATS',
data,
})
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 =>
FitTrackeeGenericApi.updateData('config', formData)
.then(ret => {
if (ret.status === 'success') {
dispatch(setAppConfig(ret.data))
} else {
dispatch(setError(`application|${ret.message}`))
}
})
.catch(error => dispatch(setError(`application|${error}`)))

View File

@ -1,22 +1,6 @@
import FitTrackeeGenericApi from '../fitTrackeeApi' import FitTrackeeGenericApi from '../fitTrackeeApi'
import { setData, setError } from './index' import { setData, setError } from './index'
export const setAppStats = data => ({
type: 'SET_APP_STATS',
data,
})
export const getAppStats = () => dispatch =>
FitTrackeeGenericApi.getData('stats/all')
.then(ret => {
if (ret.status === 'success') {
dispatch(setAppStats(ret.data))
} else {
dispatch(setError(`application|${ret.message}`))
}
})
.catch(error => dispatch(setError(`application|${error}`)))
export const getStats = (userId, type, data) => dispatch => export const getStats = (userId, type, data) => dispatch =>
FitTrackeeGenericApi.getData(`stats/${userId}/${type}`, data) FitTrackeeGenericApi.getData(`stats/${userId}/${type}`, data)
.then(ret => { .then(ret => {

View File

@ -5,17 +5,27 @@ import { connect } from 'react-redux'
import { setLoading } from '../../../actions/index' import { setLoading } from '../../../actions/index'
import { addActivity, editActivity } from '../../../actions/activities' import { addActivity, editActivity } from '../../../actions/activities'
import { history } from '../../../index' import { history } from '../../../index'
import { fileSizeLimit, gpxLimit, zipSizeLimit } from '../../../utils' import { getFileSize } from '../../../utils'
import { translateSports } from '../../../utils/activities' import { translateSports } from '../../../utils/activities'
function FormWithGpx(props) { function FormWithGpx(props) {
const { activity, loading, onAddActivity, onEditActivity, sports, t } = props const {
activity,
appConfig,
loading,
onAddActivity,
onEditActivity,
sports,
t,
} = props
const sportId = activity ? activity.sport_id : '' const sportId = activity ? activity.sport_id : ''
const translatedSports = translateSports(sports, t, true) const translatedSports = translateSports(sports, t, true)
// prettier-ignore const zipTooltip = `${t('activities:no folder inside')}, ${
const zipTooltip = appConfig.gpx_limit_import
`${t('activities:no folder inside')}, ${gpxLimit} ${ } ${t('activities:files max')}, ${t('activities:max size')}: ${getFileSize(
t('activities:files max')}, ${t('activities:max size')}: ${zipSizeLimit}` appConfig.max_zip_file_size
)}`
const fileSizeLimit = getFileSize(appConfig.max_single_file_size)
return ( return (
<form <form
encType="multipart/form-data" encType="multipart/form-data"
@ -130,6 +140,7 @@ function FormWithGpx(props) {
export default connect( export default connect(
state => ({ state => ({
appConfig: state.application.config,
loading: state.loading, loading: state.loading,
}), }),
dispatch => ({ dispatch => ({

View File

@ -3,15 +3,16 @@ import { Helmet } from 'react-helmet'
import AdminStats from './AdminStats' import AdminStats from './AdminStats'
export default function AdminDashboard() { export default function AdminDashboard(props) {
const { t } = props
return ( return (
<div> <div>
<Helmet> <Helmet>
<title>FitTrackee - Administration</title> <title>{t('administration:FitTrackee administration')}</title>
</Helmet> </Helmet>
<div className="card activity-card"> <div className="card activity-card">
<div className="card-header"> <div className="card-header">
<strong>FitTrackee administration</strong> <strong>{t('administration:FitTrackee administration')}</strong>
</div> </div>
<div className="card-body"> <div className="card-body">
<AdminStats /> <AdminStats />

View File

@ -2,7 +2,7 @@ import React from 'react'
import { withTranslation } from 'react-i18next' import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { getAppStats } from '../../actions/stats' import { getAppData } from '../../actions/application'
class AdminStats extends React.Component { class AdminStats extends React.Component {
componentDidMount() { componentDidMount() {
@ -80,7 +80,7 @@ export default withTranslation()(
}), }),
dispatch => ({ dispatch => ({
loadAppStats: () => { loadAppStats: () => {
dispatch(getAppStats()) dispatch(getAppData('stats/all'))
}, },
}) })
)(AdminStats) )(AdminStats)

View File

@ -0,0 +1,89 @@
import React from 'react'
import { Helmet } from 'react-helmet'
import Message from '../../Common/Message'
import { history } from '../../../index'
import { getFileSize } from '../../../utils'
export default function Config({ appConfig, message, t, updateIsInEdition }) {
return (
<div>
<Helmet>
<title>
FitTrackee - {t('administration:Application configuration')}
</title>
</Helmet>
<Message message={message} t={t} />
<div className="container">
<div className="row">
<div className="col-md-12">
<div className="card">
<div className="card-header">
{t('administration:Application configuration')}
</div>
<div className="card-body">
<div className="row">
<div className="col">
<table className="table">
<tbody>
<tr>
<th scope="row">
{t('administration:Enable registration')}:
</th>
<td>
{appConfig.registration
? t('common:yes')
: t('common:no')}
</td>
</tr>
<tr>
<th scope="row">
{t('administration:Max. number of active users')}:
</th>
<td>{appConfig.max_users}</td>
</tr>
<tr>
<th scope="row">
{t(
'administration:Max. size of ' + 'uploaded files'
)}
:
</th>
<td>{getFileSize(appConfig.max_single_file_size)}</td>
</tr>
<tr>
<th scope="row">
{t('administration:Max. size of zip archive')}:
</th>
<td>{getFileSize(appConfig.max_zip_file_size)}</td>
</tr>
<tr>
<th scope="row">
{t('administration:Max. files of zip archive')}:
</th>
<td>{appConfig.gpx_limit_import}</td>
</tr>
</tbody>
</table>
<input
type="submit"
className="btn btn-primary btn-lg btn-block"
onClick={() => updateIsInEdition()}
value={t('common:Edit')}
/>
<input
type="submit"
className="btn btn-secondary btn-lg btn-block"
onClick={() => history.push('/admin')}
value={t('common:Back')}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,198 @@
import React from 'react'
import { connect } from 'react-redux'
import { Helmet } from 'react-helmet'
import Message from '../../Common/Message'
import { updateAppConfig } from '../../../actions/application'
import { getFileSizeInMB } from '../../../utils'
class AdminApplication extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
formData: {},
}
}
componentDidMount() {
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
if (e.target.name === 'registration') {
formData[e.target.name] = e.target.checked
} else {
formData[e.target.name] = +e.target.value
}
this.setState(formData)
}
render() {
const {
message,
onHandleConfigFormSubmit,
t,
updateIsInEdition,
} = this.props
const { formData } = this.state
return (
<div>
<Helmet>
<title>
FitTrackee - {t('administration:Application configuration')}
</title>
</Helmet>
{message && <Message message={message} t={t} />}
{Object.keys(formData).length > 0 && (
<div className="container">
<div className="row">
<div className="col-md-12">
<div className="card">
<div className="card-header">
{t('administration:Application configuration')}
</div>
<div className="card-body">
<form
className="app-config-form"
onSubmit={event => {
event.preventDefault()
onHandleConfigFormSubmit(formData)
updateIsInEdition()
}}
>
<div className="form-group row">
<label
className="col-sm-6 col-form-label"
htmlFor="registration"
>
{t('administration:Enable registration')}:
</label>
<input
className="col-sm-5"
id="registration"
name="registration"
type="checkbox"
checked={formData.registration}
onChange={e => this.handleFormChange(e)}
/>
</div>
<div className="form-group row">
<label
className="col-sm-6 col-form-label"
htmlFor="max_users"
>
{t('administration:Max. number of active users')}:
</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>
<input
type="submit"
className="btn btn-primary btn-lg btn-block"
value={t('common:Submit')}
/>
<input
type="submit"
className="btn btn-secondary btn-lg btn-block"
onClick={() => updateIsInEdition()}
value={t('common:Cancel')}
/>
</form>
</div>
</div>
</div>
</div>
</div>
)}
</div>
)
}
}
export default connect(
() => ({}),
dispatch => ({
onHandleConfigFormSubmit: formData => {
formData.max_single_file_size *= 1048576
formData.max_zip_file_size *= 1048576
dispatch(updateAppConfig(formData))
},
})
)(AdminApplication)

View File

@ -0,0 +1,54 @@
import React from 'react'
import { connect } from 'react-redux'
import { Helmet } from 'react-helmet'
import Config from './Config'
import ConfigForm from './ConfigForm'
import Message from '../../Common/Message'
class AdminApplication extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
isInEdition: false,
}
}
render() {
const { appConfig, message, t } = this.props
const { isInEdition } = this.state
return (
<div>
<Helmet>
<title>FitTrackee - {t('administration:Administration')}</title>
</Helmet>
{message && <Message message={message} t={t} />}
{isInEdition ? (
<ConfigForm
appConfig={appConfig}
message={message}
updateIsInEdition={() => {
this.setState({ isInEdition: false })
}}
t={t}
/>
) : (
<Config
appConfig={appConfig}
message={message}
t={t}
updateIsInEdition={() => {
this.setState({ isInEdition: true })
}}
/>
)}
</div>
)
}
}
export default connect(state => ({
appConfig: state.application.config,
message: state.message,
user: state.user,
}))(AdminApplication)

View File

@ -2,14 +2,13 @@ import React from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { withTranslation } from 'react-i18next' import { withTranslation } from 'react-i18next'
import { connect } from 'react-redux' import { connect } from 'react-redux'
import { Link, Redirect, Route, Switch } from 'react-router-dom' import { Link, Route, Switch } from 'react-router-dom'
import AdminApplication from './Application'
import AdminDashboard from './AdminDashboard' import AdminDashboard from './AdminDashboard'
import AdminMenu from './AdminMenu' import AdminMenu from './AdminMenu'
import AdminSports from './Sports' import AdminSports from './Sports'
import AccessDenied from './../Others/AccessDenied'
import NotFound from './../Others/NotFound' import NotFound from './../Others/NotFound'
import { isLoggedIn } from '../../utils'
function Admin(props) { function Admin(props) {
const { t, user } = props const { t, user } = props
@ -19,6 +18,7 @@ function Admin(props) {
<title>FitTrackee - {t('administration:Administration')}</title> <title>FitTrackee - {t('administration:Administration')}</title>
</Helmet> </Helmet>
<div className="container dashboard"> <div className="container dashboard">
{user.admin ? (
<div className="row"> <div className="row">
<div className="col-md-3"> <div className="col-md-3">
<div className="card activity-card"> <div className="card activity-card">
@ -37,14 +37,17 @@ function Admin(props) {
</div> </div>
</div> </div>
<div className="col-md-9"> <div className="col-md-9">
{isLoggedIn() ? (
user.admin ? (
<Switch> <Switch>
<Route <Route
exact exact
path="/admin" path="/admin"
render={() => <AdminDashboard t={t} />} render={() => <AdminDashboard t={t} />}
/> />
<Route
exact
path="/admin/application"
render={() => <AdminApplication t={t} />}
/>
<Route <Route
exact exact
path="/admin/sports" path="/admin/sports"
@ -52,16 +55,13 @@ function Admin(props) {
/> />
<Route component={NotFound} /> <Route component={NotFound} />
</Switch> </Switch>
</div>
</div>
) : ( ) : (
<AccessDenied /> <NotFound />
)
) : (
<Redirect to="/login" />
)} )}
</div> </div>
</div> </div>
</div>
</div>
) )
} }

View File

@ -177,6 +177,10 @@ label {
margin-left: 10px; margin-left: 10px;
} }
.app-config-form label {
font-weight: bold;
}
.card { .card {
text-align: left; text-align: left;
} }

View File

@ -1,4 +1,5 @@
import React from 'react' import React from 'react'
import { connect } from 'react-redux'
import { Redirect, Route, Switch } from 'react-router-dom' import { Redirect, Route, Switch } from 'react-router-dom'
import './App.css' import './App.css'
@ -14,13 +15,17 @@ import Profile from './User/Profile'
import ProfileEdit from './User/ProfileEdit' import ProfileEdit from './User/ProfileEdit'
import Statistics from './Statistics' import Statistics from './Statistics'
import UserForm from './User/UserForm' import UserForm from './User/UserForm'
import { getAppData } from '../actions/application'
import { isLoggedIn } from '../utils' import { isLoggedIn } from '../utils'
export default class App extends React.Component { class App extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.props = props this.props = props
} }
componentDidMount() {
this.props.loadAppConfig()
}
render() { render() {
return ( return (
@ -74,7 +79,12 @@ export default class App extends React.Component {
<Route exact path="/activities/history" component={Activities} /> <Route exact path="/activities/history" component={Activities} />
<Route exact path="/activities/statistics" component={Statistics} /> <Route exact path="/activities/statistics" component={Statistics} />
<Route path="/activities" component={Activity} /> <Route path="/activities" component={Activity} />
<Route path="/admin" component={Admin} /> <Route
path="/admin"
render={() =>
isLoggedIn() ? <Admin /> : <UserForm formType={'login'} />
}
/>
<Route component={NotFound} /> <Route component={NotFound} />
</Switch> </Switch>
<Footer /> <Footer />
@ -82,3 +92,11 @@ export default class App extends React.Component {
) )
} }
} }
export default connect(
() => ({}),
dispatch => ({
loadAppConfig: () => {
dispatch(getAppData('config'))
},
})
)(App)

View File

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

View File

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

View File

@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { history } from '../../index' import { history } from '../../index'
import { isRegistrationAllowed } from '../../utils'
export default function Form(props) { export default function Form(props) {
const { t } = useTranslation() const { t } = useTranslation()
@ -22,7 +21,7 @@ export default function Form(props) {
<div className="col-md-6"> <div className="col-md-6">
<hr /> <hr />
<br /> <br />
{props.formType === 'register' && !isRegistrationAllowed ? ( {props.formType === 'register' && !props.isRegistrationAllowed ? (
<div className="card"> <div className="card">
<div className="card-body">Registration is disabled.</div> <div className="card-body">Registration is disabled.</div>
<div className="card-body"> <div className="card-body">

View File

@ -7,15 +7,23 @@ import { Link } from 'react-router-dom'
import Message from '../Common/Message' import Message from '../Common/Message'
import { deletePicture, uploadPicture } from '../../actions/user' import { deletePicture, uploadPicture } from '../../actions/user'
import { apiUrl, fileSizeLimit } from '../../utils' import { apiUrl, getFileSize } from '../../utils'
function Profile({ message, onDeletePicture, onUploadPicture, t, user }) { function Profile({
appConfig,
message,
onDeletePicture,
onUploadPicture,
t,
user,
}) {
const createdAt = user.created_at const createdAt = user.created_at
? format(new Date(user.created_at), 'dd/MM/yyyy HH:mm') ? format(new Date(user.created_at), 'dd/MM/yyyy HH:mm')
: '' : ''
const birthDate = user.birth_date const birthDate = user.birth_date
? format(new Date(user.birth_date), 'dd/MM/yyyy') ? format(new Date(user.birth_date), 'dd/MM/yyyy')
: '' : ''
const fileSizeLimit = getFileSize(appConfig.max_single_file_size)
return ( return (
<div> <div>
<Helmet> <Helmet>
@ -118,6 +126,7 @@ function Profile({ message, onDeletePicture, onUploadPicture, t, user }) {
export default withTranslation()( export default withTranslation()(
connect( connect(
state => ({ state => ({
appConfig: state.application.config,
message: state.message, message: state.message,
user: state.user, user: state.user,
}), }),

View File

@ -42,6 +42,7 @@ class UserForm extends React.Component {
render() { render() {
const { const {
formType, formType,
isRegistrationAllowed,
message, message,
messages, messages,
onHandleUserFormSubmit, onHandleUserFormSubmit,
@ -56,6 +57,7 @@ class UserForm extends React.Component {
<div> <div>
<Message message={message} messages={messages} t={t} /> <Message message={message} messages={messages} t={t} />
<Form <Form
isRegistrationAllowed={isRegistrationAllowed}
formType={formType} formType={formType}
userForm={formData} userForm={formData}
onHandleFormChange={event => this.onHandleFormChange(event)} onHandleFormChange={event => this.onHandleFormChange(event)}
@ -73,6 +75,7 @@ class UserForm extends React.Component {
export default withTranslation()( export default withTranslation()(
connect( connect(
state => ({ state => ({
isRegistrationAllowed: state.application.config.is_registration_enabled,
location: state.router.location, location: state.router.location,
message: state.message, message: state.message,
messages: state.messages, messages: state.messages,

View File

@ -48,7 +48,7 @@ export default class FitTrackeeApi {
static updateData(target, data) { static updateData(target, data) {
const params = { const params = {
url: `${target}/${data.id}`, url: `${target}${data.id ? `/${data.id}` : ''}`,
method: 'PATCH', method: 'PATCH',
body: data, body: data,
type: 'application/json', type: 'application/json',

View File

@ -4,12 +4,21 @@
"activities exist": "activities exist", "activities exist": "activities exist",
"Administration": "Administration", "Administration": "Administration",
"Application": "Application", "Application": "Application",
"Application configuration": "Application configuration",
"Back": "Back", "Back": "Back",
"Disable": "Disable", "Disable": "Disable",
"Enable": "Enable", "Enable": "Enable",
"Enable registration": "Enable registration",
"FitTrackee administration": "FitTrackee administration",
"id": "id", "id": "id",
"Image": "Image", "Image": "Image",
"Label": "Label", "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)",
"Sports": "Sports", "Sports": "Sports",
"user": "user", "user": "user",
"Users": "Users", "Users": "Users",

View File

@ -1,14 +1,18 @@
{ {
"Add workout": "Add workout", "Add workout": "Add workout",
"Back": "Back",
"Cancel": "Cancel", "Cancel": "Cancel",
"Dashboard": "Dashboard", "Dashboard": "Dashboard",
"Edit": "Edit",
"day": "day", "day": "day",
"days": "days", "days": "days",
"Login": "Login", "Login": "Login",
"Logout": "Logout", "Logout": "Logout",
"No": "No", "No": "No",
"no": "no",
"No records.": "No records.", "No records.": "No records.",
"No workouts.": "No workouts.", "No workouts.": "No workouts.",
"Page not found": "Page not found",
"Register": "Register", "Register": "Register",
"Statistics": "Statistics", "Statistics": "Statistics",
"Sport": "Sport", "Sport": "Sport",
@ -21,5 +25,6 @@
"Workouts": "Workouts", "Workouts": "Workouts",
"workout": "workout", "workout": "workout",
"workouts": "workouts", "workouts": "workouts",
"Yes": "Yes" "Yes": "Yes",
"yes": "yes"
} }

View File

@ -9,6 +9,7 @@
"Error. Registration is disabled.": "Error. Registration is disabled.", "Error. Registration is disabled.": "Error. Registration is disabled.",
"Error. Please try again or contact the administrator.": "Error. Please try again or contact the administrator.", "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 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", "Incorrect id": "Incorrect id",
"Invalid credentials.": "Invalid credentials.", "Invalid credentials.": "Invalid credentials.",
"Invalid payload.": "Invalid payload.", "Invalid payload.": "Invalid payload.",

View File

@ -4,12 +4,21 @@
"Administration": "Administration", "Administration": "Administration",
"activities exist": "des activités existent", "activities exist": "des activités existent",
"Application": "Application", "Application": "Application",
"Application configuration": "Configuration de l'application",
"Back": "Retour", "Back": "Retour",
"Disable": "désactiver", "Disable": "désactiver",
"Enable": "activer", "Enable": "activer",
"Enable registration": "Activer les inscriptions",
"FitTrackee administration": "Administration de FitTrackee",
"id": "id", "id": "id",
"Image": "Image", "Image": "Image",
"Label": "Label", "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)",
"Sports": "Sports", "Sports": "Sports",
"user": "user", "user": "user",
"Users": "Utilisateurs", "Users": "Utilisateurs",

View File

@ -1,14 +1,18 @@
{ {
"Add workout": "Ajouter une activité", "Add workout": "Ajouter une activité",
"Back": "Revenir à la page précédente",
"Cancel": "Annuler", "Cancel": "Annuler",
"Dashboard": "Tableau de Bord", "Dashboard": "Tableau de Bord",
"Edit": "Modifier",
"day": "jour", "day": "jour",
"days": "jours", "days": "jours",
"Login": "Se connecter", "Login": "Se connecter",
"Logout": "Se déconnecter", "Logout": "Se déconnecter",
"No": "Non", "No": "Non",
"no": "non",
"No records.": "Pas de records.", "No records.": "Pas de records.",
"No workouts.": "Pas d'activités.", "No workouts.": "Pas d'activités.",
"Page not found": "Page introuvable",
"Register": "S'inscrire", "Register": "S'inscrire",
"Statistics": "Statistiques", "Statistics": "Statistiques",
"Sport": "Sport", "Sport": "Sport",
@ -21,5 +25,6 @@
"Workouts": "Activités", "Workouts": "Activités",
"workout": "activité", "workout": "activité",
"workouts": "activités", "workouts": "activités",
"Yes": "Oui" "Yes": "Oui",
"yes": "oui"
} }

View File

@ -9,6 +9,7 @@
"Error. Registration is disabled.": "Erreur. L'inscription est désactivé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", "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 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", "Incorrect id": "Id incorrect",
"Invalid credentials.": "Identifiants invalides.", "Invalid credentials.": "Identifiants invalides.",
"Invalid payload.": "Données incorrectes.", "Invalid payload.": "Données incorrectes.",

View File

@ -34,6 +34,12 @@ const activities = (state = initial.activities, action) => {
} }
const application = (state = initial.application, action) => { const application = (state = initial.application, action) => {
if (action.type === 'SET_APP_CONFIG') {
return {
...state,
config: action.data,
}
}
if (action.type === 'SET_APP_STATS') { if (action.type === 'SET_APP_STATS') {
return { return {
...state, ...state,

View File

@ -14,6 +14,14 @@ export default {
}, },
application: { application: {
statistics: {}, 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,
},
}, },
calendarActivities: { calendarActivities: {
...emptyData, ...emptyData,

View File

@ -2,7 +2,7 @@ import { format, parse } from 'date-fns'
import { DateTime } from 'luxon' import { DateTime } from 'luxon'
const suffixes = ['bytes', 'KB', 'MB', 'GB', 'TB'] const suffixes = ['bytes', 'KB', 'MB', 'GB', 'TB']
const getFileSize = fileSize => { export const getFileSize = fileSize => {
const i = Math.floor(Math.log(fileSize) / Math.log(1024)) const i = Math.floor(Math.log(fileSize) / Math.log(1024))
return ( return (
(!fileSize && '0 bytes') || (!fileSize && '0 bytes') ||
@ -10,21 +10,17 @@ const getFileSize = fileSize => {
) )
} }
export const getFileSizeInMB = fileSize => {
const value = fileSize / 1048576
return (!fileSize && 0) || +value.toFixed(2)
}
export const version = '0.3.0-beta' // version stored in 'utils' for now export const version = '0.3.0-beta' // version stored in 'utils' for now
export const apiUrl = `${process.env.REACT_APP_API_URL}/api/` export const apiUrl = `${process.env.REACT_APP_API_URL}/api/`
/* prettier-ignore */ /* prettier-ignore */
export const thunderforestApiKey = `${ export const thunderforestApiKey = `${
process.env.REACT_APP_THUNDERFOREST_API_KEY process.env.REACT_APP_THUNDERFOREST_API_KEY
}` }`
export const gpxLimit = `${process.env.REACT_APP_GPX_LIMIT_IMPORT}`
export const fileSizeLimit = getFileSize(
+process.env.REACT_APP_MAX_SINGLE_FILE_SIZE
)
export const zipSizeLimit = getFileSize(
+process.env.REACT_APP_MAX_ZIP_FILE_SIZE
)
export const isRegistrationAllowed =
process.env.REACT_APP_ALLOW_REGISTRATION !== 'false'
export const isLoggedIn = () => !!window.localStorage.authToken export const isLoggedIn = () => !!window.localStorage.authToken
@ -56,7 +52,11 @@ export const createApiRequest = params => {
} }
const request = new Request(`${apiUrl}${params.url}`, requestParams) const request = new Request(`${apiUrl}${params.url}`, requestParams)
return fetch(request) return fetch(request)
.then(response => (params.method === 'DELETE' ? response : response.json())) .then(response =>
params.method === 'DELETE' || response.status === 413
? response
: response.json()
)
.catch(error => { .catch(error => {
console.error(error) console.error(error)
return new Error('An error occurred. Please contact the administrator.') return new Error('An error occurred. Please contact the administrator.')