Client - add application configuration in Application Admin - #15
This commit is contained in:
		@@ -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:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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}`))
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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 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 => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 => ({
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 { 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,47 +18,48 @@ 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">
 | 
				
			||||||
        <div className="row">
 | 
					        {user.admin ? (
 | 
				
			||||||
          <div className="col-md-3">
 | 
					          <div className="row">
 | 
				
			||||||
            <div className="card activity-card">
 | 
					            <div className="col-md-3">
 | 
				
			||||||
              <div className="card-header">
 | 
					              <div className="card activity-card">
 | 
				
			||||||
                <Link
 | 
					                <div className="card-header">
 | 
				
			||||||
                  to={{
 | 
					                  <Link
 | 
				
			||||||
                    pathname: '/admin/',
 | 
					                    to={{
 | 
				
			||||||
                  }}
 | 
					                      pathname: '/admin/',
 | 
				
			||||||
                >
 | 
					                    }}
 | 
				
			||||||
                  {t('administration:Administration')}
 | 
					                  >
 | 
				
			||||||
                </Link>
 | 
					                    {t('administration:Administration')}
 | 
				
			||||||
              </div>
 | 
					                  </Link>
 | 
				
			||||||
              <div className="card-body">
 | 
					                </div>
 | 
				
			||||||
                <AdminMenu t={t} />
 | 
					                <div className="card-body">
 | 
				
			||||||
 | 
					                  <AdminMenu t={t} />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
              </div>
 | 
					              </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>
 | 
				
			||||||
          <div className="col-md-9">
 | 
					        ) : (
 | 
				
			||||||
            {isLoggedIn() ? (
 | 
					          <NotFound />
 | 
				
			||||||
              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>
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
    }),
 | 
					    }),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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.')
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user