Client - add application configuration in Application Admin - #15
This commit is contained in:
parent
97534b698b
commit
1398c7ff4a
@ -11,8 +11,8 @@ def init_config():
|
||||
"""
|
||||
init application configuration if not existing in database
|
||||
|
||||
Note: get some configuration values from env variables (for FitTrackee versions
|
||||
prior to v0.3.0)
|
||||
Note: get some configuration values from env variables
|
||||
(for FitTrackee versions prior to v0.3.0)
|
||||
"""
|
||||
existing_config = AppConfig.query.one_or_none()
|
||||
if not existing_config:
|
||||
|
@ -43,6 +43,10 @@ export const addActivity = form => dispatch =>
|
||||
dispatch(loadProfile())
|
||||
history.push('/')
|
||||
}
|
||||
} else if (ret.status === 413) {
|
||||
dispatch(
|
||||
setError('activities|File size is greater than the allowed size')
|
||||
)
|
||||
} else {
|
||||
dispatch(setError(`activities|${ret.message}`))
|
||||
}
|
||||
|
38
fittrackee_client/src/actions/application.js
Normal file
38
fittrackee_client/src/actions/application.js
Normal 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}`)))
|
@ -1,22 +1,6 @@
|
||||
import FitTrackeeGenericApi from '../fitTrackeeApi'
|
||||
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 =>
|
||||
FitTrackeeGenericApi.getData(`stats/${userId}/${type}`, data)
|
||||
.then(ret => {
|
||||
|
@ -5,17 +5,27 @@ import { connect } from 'react-redux'
|
||||
import { setLoading } from '../../../actions/index'
|
||||
import { addActivity, editActivity } from '../../../actions/activities'
|
||||
import { history } from '../../../index'
|
||||
import { fileSizeLimit, gpxLimit, zipSizeLimit } from '../../../utils'
|
||||
import { getFileSize } from '../../../utils'
|
||||
import { translateSports } from '../../../utils/activities'
|
||||
|
||||
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 translatedSports = translateSports(sports, t, true)
|
||||
// prettier-ignore
|
||||
const zipTooltip =
|
||||
`${t('activities:no folder inside')}, ${gpxLimit} ${
|
||||
t('activities:files max')}, ${t('activities:max size')}: ${zipSizeLimit}`
|
||||
const zipTooltip = `${t('activities:no folder inside')}, ${
|
||||
appConfig.gpx_limit_import
|
||||
} ${t('activities:files max')}, ${t('activities:max size')}: ${getFileSize(
|
||||
appConfig.max_zip_file_size
|
||||
)}`
|
||||
const fileSizeLimit = getFileSize(appConfig.max_single_file_size)
|
||||
return (
|
||||
<form
|
||||
encType="multipart/form-data"
|
||||
@ -130,6 +140,7 @@ function FormWithGpx(props) {
|
||||
|
||||
export default connect(
|
||||
state => ({
|
||||
appConfig: state.application.config,
|
||||
loading: state.loading,
|
||||
}),
|
||||
dispatch => ({
|
||||
|
@ -3,15 +3,16 @@ import { Helmet } from 'react-helmet'
|
||||
|
||||
import AdminStats from './AdminStats'
|
||||
|
||||
export default function AdminDashboard() {
|
||||
export default function AdminDashboard(props) {
|
||||
const { t } = props
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - Administration</title>
|
||||
<title>{t('administration:FitTrackee administration')}</title>
|
||||
</Helmet>
|
||||
<div className="card activity-card">
|
||||
<div className="card-header">
|
||||
<strong>FitTrackee administration</strong>
|
||||
<strong>{t('administration:FitTrackee administration')}</strong>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<AdminStats />
|
||||
|
@ -2,7 +2,7 @@ import React from 'react'
|
||||
import { withTranslation } from 'react-i18next'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import { getAppStats } from '../../actions/stats'
|
||||
import { getAppData } from '../../actions/application'
|
||||
|
||||
class AdminStats extends React.Component {
|
||||
componentDidMount() {
|
||||
@ -80,7 +80,7 @@ export default withTranslation()(
|
||||
}),
|
||||
dispatch => ({
|
||||
loadAppStats: () => {
|
||||
dispatch(getAppStats())
|
||||
dispatch(getAppData('stats/all'))
|
||||
},
|
||||
})
|
||||
)(AdminStats)
|
||||
|
@ -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>
|
||||
)
|
||||
}
|
@ -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)
|
54
fittrackee_client/src/components/Admin/Application/index.jsx
Normal file
54
fittrackee_client/src/components/Admin/Application/index.jsx
Normal 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)
|
@ -2,14 +2,13 @@ import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
import { withTranslation } from 'react-i18next'
|
||||
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 AdminMenu from './AdminMenu'
|
||||
import AdminSports from './Sports'
|
||||
import AccessDenied from './../Others/AccessDenied'
|
||||
import NotFound from './../Others/NotFound'
|
||||
import { isLoggedIn } from '../../utils'
|
||||
|
||||
function Admin(props) {
|
||||
const { t, user } = props
|
||||
@ -19,47 +18,48 @@ function Admin(props) {
|
||||
<title>FitTrackee - {t('administration:Administration')}</title>
|
||||
</Helmet>
|
||||
<div className="container dashboard">
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<div className="card activity-card">
|
||||
<div className="card-header">
|
||||
<Link
|
||||
to={{
|
||||
pathname: '/admin/',
|
||||
}}
|
||||
>
|
||||
{t('administration:Administration')}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<AdminMenu t={t} />
|
||||
{user.admin ? (
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<div className="card activity-card">
|
||||
<div className="card-header">
|
||||
<Link
|
||||
to={{
|
||||
pathname: '/admin/',
|
||||
}}
|
||||
>
|
||||
{t('administration:Administration')}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<AdminMenu t={t} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-9">
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/admin"
|
||||
render={() => <AdminDashboard t={t} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/admin/application"
|
||||
render={() => <AdminApplication t={t} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/admin/sports"
|
||||
render={() => <AdminSports t={t} />}
|
||||
/>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-9">
|
||||
{isLoggedIn() ? (
|
||||
user.admin ? (
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path="/admin"
|
||||
render={() => <AdminDashboard t={t} />}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/admin/sports"
|
||||
render={() => <AdminSports t={t} />}
|
||||
/>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
) : (
|
||||
<AccessDenied />
|
||||
)
|
||||
) : (
|
||||
<Redirect to="/login" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<NotFound />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -177,6 +177,10 @@ label {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.app-config-form label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.card {
|
||||
text-align: left;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Redirect, Route, Switch } from 'react-router-dom'
|
||||
|
||||
import './App.css'
|
||||
@ -14,13 +15,17 @@ import Profile from './User/Profile'
|
||||
import ProfileEdit from './User/ProfileEdit'
|
||||
import Statistics from './Statistics'
|
||||
import UserForm from './User/UserForm'
|
||||
import { getAppData } from '../actions/application'
|
||||
import { isLoggedIn } from '../utils'
|
||||
|
||||
export default class App extends React.Component {
|
||||
class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.props = props
|
||||
}
|
||||
componentDidMount() {
|
||||
this.props.loadAppConfig()
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
@ -74,7 +79,12 @@ export default class App extends React.Component {
|
||||
<Route exact path="/activities/history" component={Activities} />
|
||||
<Route exact path="/activities/statistics" component={Statistics} />
|
||||
<Route path="/activities" component={Activity} />
|
||||
<Route path="/admin" component={Admin} />
|
||||
<Route
|
||||
path="/admin"
|
||||
render={() =>
|
||||
isLoggedIn() ? <Admin /> : <UserForm formType={'login'} />
|
||||
}
|
||||
/>
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
<Footer />
|
||||
@ -82,3 +92,11 @@ export default class App extends React.Component {
|
||||
)
|
||||
}
|
||||
}
|
||||
export default connect(
|
||||
() => ({}),
|
||||
dispatch => ({
|
||||
loadAppConfig: () => {
|
||||
dispatch(getAppData('config'))
|
||||
},
|
||||
})
|
||||
)(App)
|
||||
|
@ -1,16 +1,24 @@
|
||||
import React from 'react'
|
||||
import { Helmet } from 'react-helmet'
|
||||
|
||||
export default function AccessDenied() {
|
||||
export default function AccessDenied(props) {
|
||||
const { t } = props
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>FitTrackee - Access denied</title>
|
||||
<title>FitTrackee - {t('Access denied')}</title>
|
||||
</Helmet>
|
||||
<h1 className="page-title">Access denied</h1>
|
||||
<p className="App-center">
|
||||
{"You don't have permissions to access this page."}
|
||||
</p>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
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">Page not found</h1>
|
||||
<h1 className="page-title">{t('Page not found')}</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'
|
||||
import { Helmet } from 'react-helmet'
|
||||
|
||||
import { history } from '../../index'
|
||||
import { isRegistrationAllowed } from '../../utils'
|
||||
|
||||
export default function Form(props) {
|
||||
const { t } = useTranslation()
|
||||
@ -22,7 +21,7 @@ export default function Form(props) {
|
||||
<div className="col-md-6">
|
||||
<hr />
|
||||
<br />
|
||||
{props.formType === 'register' && !isRegistrationAllowed ? (
|
||||
{props.formType === 'register' && !props.isRegistrationAllowed ? (
|
||||
<div className="card">
|
||||
<div className="card-body">Registration is disabled.</div>
|
||||
<div className="card-body">
|
||||
|
@ -7,15 +7,23 @@ import { Link } from 'react-router-dom'
|
||||
|
||||
import Message from '../Common/Message'
|
||||
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
|
||||
? 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>
|
||||
@ -118,6 +126,7 @@ function Profile({ message, onDeletePicture, onUploadPicture, t, user }) {
|
||||
export default withTranslation()(
|
||||
connect(
|
||||
state => ({
|
||||
appConfig: state.application.config,
|
||||
message: state.message,
|
||||
user: state.user,
|
||||
}),
|
||||
|
@ -42,6 +42,7 @@ class UserForm extends React.Component {
|
||||
render() {
|
||||
const {
|
||||
formType,
|
||||
isRegistrationAllowed,
|
||||
message,
|
||||
messages,
|
||||
onHandleUserFormSubmit,
|
||||
@ -56,6 +57,7 @@ class UserForm extends React.Component {
|
||||
<div>
|
||||
<Message message={message} messages={messages} t={t} />
|
||||
<Form
|
||||
isRegistrationAllowed={isRegistrationAllowed}
|
||||
formType={formType}
|
||||
userForm={formData}
|
||||
onHandleFormChange={event => this.onHandleFormChange(event)}
|
||||
@ -73,6 +75,7 @@ class UserForm extends React.Component {
|
||||
export default withTranslation()(
|
||||
connect(
|
||||
state => ({
|
||||
isRegistrationAllowed: state.application.config.is_registration_enabled,
|
||||
location: state.router.location,
|
||||
message: state.message,
|
||||
messages: state.messages,
|
||||
|
@ -48,7 +48,7 @@ export default class FitTrackeeApi {
|
||||
|
||||
static updateData(target, data) {
|
||||
const params = {
|
||||
url: `${target}/${data.id}`,
|
||||
url: `${target}${data.id ? `/${data.id}` : ''}`,
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
type: 'application/json',
|
||||
|
@ -4,12 +4,21 @@
|
||||
"activities exist": "activities exist",
|
||||
"Administration": "Administration",
|
||||
"Application": "Application",
|
||||
"Application configuration": "Application configuration",
|
||||
"Back": "Back",
|
||||
"Disable": "Disable",
|
||||
"Enable": "Enable",
|
||||
"Enable registration": "Enable registration",
|
||||
"FitTrackee administration": "FitTrackee administration",
|
||||
"id": "id",
|
||||
"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)",
|
||||
"Sports": "Sports",
|
||||
"user": "user",
|
||||
"Users": "Users",
|
||||
|
@ -1,14 +1,18 @@
|
||||
{
|
||||
"Add workout": "Add workout",
|
||||
"Back": "Back",
|
||||
"Cancel": "Cancel",
|
||||
"Dashboard": "Dashboard",
|
||||
"Edit": "Edit",
|
||||
"day": "day",
|
||||
"days": "days",
|
||||
"Login": "Login",
|
||||
"Logout": "Logout",
|
||||
"No": "No",
|
||||
"no": "no",
|
||||
"No records.": "No records.",
|
||||
"No workouts.": "No workouts.",
|
||||
"Page not found": "Page not found",
|
||||
"Register": "Register",
|
||||
"Statistics": "Statistics",
|
||||
"Sport": "Sport",
|
||||
@ -21,5 +25,6 @@
|
||||
"Workouts": "Workouts",
|
||||
"workout": "workout",
|
||||
"workouts": "workouts",
|
||||
"Yes": "Yes"
|
||||
"Yes": "Yes",
|
||||
"yes": "yes"
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
"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.",
|
||||
|
@ -4,12 +4,21 @@
|
||||
"Administration": "Administration",
|
||||
"activities exist": "des activités existent",
|
||||
"Application": "Application",
|
||||
"Application configuration": "Configuration de l'application",
|
||||
"Back": "Retour",
|
||||
"Disable": "désactiver",
|
||||
"Enable": "activer",
|
||||
"Enable registration": "Activer les inscriptions",
|
||||
"FitTrackee administration": "Administration de FitTrackee",
|
||||
"id": "id",
|
||||
"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)",
|
||||
"Sports": "Sports",
|
||||
"user": "user",
|
||||
"Users": "Utilisateurs",
|
||||
|
@ -1,14 +1,18 @@
|
||||
{
|
||||
"Add workout": "Ajouter une activité",
|
||||
"Back": "Revenir à la page précédente",
|
||||
"Cancel": "Annuler",
|
||||
"Dashboard": "Tableau de Bord",
|
||||
"Edit": "Modifier",
|
||||
"day": "jour",
|
||||
"days": "jours",
|
||||
"Login": "Se connecter",
|
||||
"Logout": "Se déconnecter",
|
||||
"No": "Non",
|
||||
"no": "non",
|
||||
"No records.": "Pas de records.",
|
||||
"No workouts.": "Pas d'activités.",
|
||||
"Page not found": "Page introuvable",
|
||||
"Register": "S'inscrire",
|
||||
"Statistics": "Statistiques",
|
||||
"Sport": "Sport",
|
||||
@ -21,5 +25,6 @@
|
||||
"Workouts": "Activités",
|
||||
"workout": "activité",
|
||||
"workouts": "activités",
|
||||
"Yes": "Oui"
|
||||
"Yes": "Oui",
|
||||
"yes": "oui"
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
"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.",
|
||||
|
@ -34,6 +34,12 @@ const activities = (state = initial.activities, 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,
|
||||
|
@ -14,6 +14,14 @@ export default {
|
||||
},
|
||||
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,
|
||||
},
|
||||
},
|
||||
calendarActivities: {
|
||||
...emptyData,
|
||||
|
@ -2,7 +2,7 @@ import { format, parse } from 'date-fns'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
const suffixes = ['bytes', 'KB', 'MB', 'GB', 'TB']
|
||||
const getFileSize = fileSize => {
|
||||
export const getFileSize = fileSize => {
|
||||
const i = Math.floor(Math.log(fileSize) / Math.log(1024))
|
||||
return (
|
||||
(!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 apiUrl = `${process.env.REACT_APP_API_URL}/api/`
|
||||
/* prettier-ignore */
|
||||
export const thunderforestApiKey = `${
|
||||
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
|
||||
|
||||
@ -56,7 +52,11 @@ export const createApiRequest = params => {
|
||||
}
|
||||
const request = new Request(`${apiUrl}${params.url}`, requestParams)
|
||||
return fetch(request)
|
||||
.then(response => (params.method === 'DELETE' ? response : response.json()))
|
||||
.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.')
|
||||
|
Loading…
Reference in New Issue
Block a user