aboutsummaryrefslogtreecommitdiff
path: root/modern/src
diff options
context:
space:
mode:
Diffstat (limited to 'modern/src')
-rw-r--r--modern/src/App.js7
-rw-r--r--modern/src/LoginPage.js152
-rw-r--r--modern/src/Logo.js31
-rw-r--r--modern/src/components/LoginForm.js125
-rw-r--r--modern/src/components/RegisterForm.js122
-rw-r--r--modern/src/components/ResetPasswordForm.js12
-rw-r--r--modern/src/theme/dimensions.js6
-rw-r--r--modern/src/theme/index.js12
-rw-r--r--modern/src/theme/overrides.js49
-rw-r--r--modern/src/theme/palette.js17
10 files changed, 417 insertions, 116 deletions
diff --git a/modern/src/App.js b/modern/src/App.js
index 5fd10deb..7db5f128 100644
--- a/modern/src/App.js
+++ b/modern/src/App.js
@@ -1,4 +1,5 @@
import React from 'react';
+import { ThemeProvider } from '@material-ui/core/styles';
import { Switch, Route } from 'react-router-dom'
import CssBaseline from '@material-ui/core/CssBaseline';
import MainPage from './MainPage';
@@ -31,11 +32,13 @@ import MaintenancePage from './settings/MaintenancePage';
import StatisticsPage from './admin/StatisticsPage';
import CachingController from './CachingController';
+import theme from './theme';
+
const App = () => {
const initialized = useSelector(state => !!state.session.server && !!state.session.user);
return (
- <>
+ <ThemeProvider theme={theme}>
<CssBaseline />
<SocketController />
<CachingController />
@@ -72,7 +75,7 @@ const App = () => {
)}
</Route>
</Switch>
- </>
+ </ThemeProvider>
);
}
diff --git a/modern/src/LoginPage.js b/modern/src/LoginPage.js
index d3be3978..3d4b17f4 100644
--- a/modern/src/LoginPage.js
+++ b/modern/src/LoginPage.js
@@ -1,140 +1,64 @@
import React, { useState } from 'react';
-import { useDispatch } from 'react-redux';
-import { useHistory } from 'react-router-dom';
-import { sessionActions } from './store';
-import Button from '@material-ui/core/Button';
-import FormControl from '@material-ui/core/FormControl';
-import Paper from '@material-ui/core/Paper';
-import { makeStyles } from '@material-ui/core';
-import TextField from '@material-ui/core/TextField';
-import RegisterDialog from './RegisterDialog';
-import { useSelector } from 'react-redux';
+import { useMediaQuery, makeStyles, Paper } from '@material-ui/core';
+import { useTheme } from '@material-ui/core/styles';
-import t from './common/localization';
+import LoginForm from './components/LoginForm';
const useStyles = makeStyles(theme => ({
root: {
- width: 'auto',
- marginLeft: theme.spacing(3),
- marginRight: theme.spacing(3),
- [theme.breakpoints.up(400 + theme.spacing(3 * 2))]: {
- width: 400,
- marginLeft: 'auto',
- marginRight: 'auto',
+ display: 'flex',
+ height: '100vh',
+ },
+ sidebar: {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ background: theme.palette.common.purple,
+ paddingBottom: theme.spacing(5),
+ width: theme.dimensions.sidebarWidth,
+ [theme.breakpoints.down('md')]: {
+ width: theme.dimensions.tabletSidebarWidth,
+ },
+ [theme.breakpoints.down('xs')]: {
+ width: '0px',
},
},
paper: {
- marginTop: theme.spacing(8),
- display: 'flex',
+ display:'flex',
flexDirection: 'column',
+ justifyContent: 'center',
alignItems: 'center',
- padding: theme.spacing(3),
- },
- logo: {
- marginTop: theme.spacing(2)
- },
- buttons: {
- marginTop: theme.spacing(1),
- display: 'flex',
- justifyContent: 'space-evenly',
- '& > *': {
- flexBasis: '40%',
+ flex: 1,
+ boxShadow: '-2px 0px 16px rgba(0, 0, 0, 0.25)',
+ [theme.breakpoints.up('lg')]: {
+ padding: theme.spacing(0, 25, 0, 0)
},
},
+ form: {
+ maxWidth: theme.spacing(52),
+ padding: theme.spacing(5),
+ width: "100%",
+ },
}));
const LoginPage = () => {
- const dispatch = useDispatch();
-
- const [failed, setFailed] = useState(false);
- const [email, setEmail] = useState('');
- const [password, setPassword] = useState('');
- const [registerDialogShown, setRegisterDialogShown] = useState(false);
-
const classes = useStyles();
- const history = useHistory();
-
- const registrationEnabled = useSelector(state => state.session.server ? state.session.server['registration'] : false);
-
- const handleEmailChange = (event) => {
- setEmail(event.target.value);
- }
-
- const handlePasswordChange = (event) => {
- setPassword(event.target.value);
- }
-
- const handleRegister = () => {
- setRegisterDialogShown(true);
- }
-
- const handleRegisterResult = () => {
- setRegisterDialogShown(false);
- }
+ const theme = useTheme();
- const handleLogin = async (event) => {
- event.preventDefault();
- const response = await fetch('/api/session', { method: 'POST', body: new URLSearchParams(`email=${email}&password=${password}`) });
- if (response.ok) {
- const user = await response.json();
- dispatch(sessionActions.updateUser(user));
- history.push('/');
- } else {
- setFailed(true);
- setPassword('');
- }
- }
+ const [CurrentForm, setCurrentForm] = useState(() => LoginForm);
+ const matches = useMediaQuery(theme.breakpoints.down('md'));
return (
<main className={classes.root}>
+ <div className={classes.sidebar}>
+ {!matches && <img src='/logo.svg' alt='Traccar' /> }
+ </div>
<Paper className={classes.paper}>
- <img className={classes.logo} src='/logo.svg' alt='Traccar' />
- <form onSubmit={handleLogin}>
-
- <TextField
- margin='normal'
- required
- fullWidth
- error={failed}
- label={t('userEmail')}
- name='email'
- value={email}
- autoComplete='email'
- autoFocus
- onChange={handleEmailChange}
- helperText={failed && 'Invalid username or password'} />
-
- <TextField
- margin='normal'
- required
- fullWidth
- error={failed}
- label={t('userPassword')}
- name='password'
- value={password}
- type='password'
- autoComplete='current-password'
- onChange={handlePasswordChange} />
-
- <FormControl fullWidth margin='normal'>
- <div className={classes.buttons}>
- <Button type='button' variant='contained' onClick={handleRegister} disabled={!registrationEnabled}>
- {t('loginRegister')}
- </Button>
- <Button type='submit' variant='contained' color='primary' disabled={!email || !password}>
- {t('loginLogin')}
- </Button>
- </div>
- </FormControl>
+ <form className={classes.form}>
+ <CurrentForm setCurrentForm={setCurrentForm} />
</form>
-
- {registerDialogShown &&
- <RegisterDialog showDialog={true} onResult={handleRegisterResult} />
- }
-
</Paper>
</main>
- );
+ )
}
-
export default LoginPage;
diff --git a/modern/src/Logo.js b/modern/src/Logo.js
new file mode 100644
index 00000000..bea14d8c
--- /dev/null
+++ b/modern/src/Logo.js
@@ -0,0 +1,31 @@
+import React from 'react';
+
+const Logo = ({ fill }) => {
+
+ return (
+ <svg xmlns='http://www.w3.org/2000/svg' height="64" viewBox="0 0 240 64" width="240" version="1.1">
+ <g id="layer1">
+ <rect id="rect3778" height="64" width="236.1" y="0" x="0" fill="none"/>
+ <ellipse id="path3038" rx="28.995" ry="28.995" transform="rotate(-30)" cy="43.713" cx="11.713" stroke-width="10.699" fill="#fff"/>
+ <g fill= {fill}>
+ <circle id="path2993" strokeWidth="1.3262" transform="rotate(-30)" cy="43.713" cx="9.4364" r="2.2765"/>
+ <path id="path3004" d="m37.012 24.177-2.8428 3.6128c0.66345 0.52205 1.3255 1.1576 1.7734 1.9333 0.4479 0.77578 0.66726 1.6669 0.78764 2.5025l4.5502-0.65558c-0.193-1.42-0.633-2.804-1.394-4.123s-1.74-2.391-2.874-3.27z" strokeWidth="1.0095"/>
+ <path id="path3014" d="m42.504 16.9-2.8428 3.6128c1.607 1.2355 3.0914 2.7935 4.1679 4.6581s1.6835 3.9291 1.95 5.9386l4.5502-0.65558c-0.33967-2.5954-1.1669-5.1513-2.5573-7.5594-1.3903-2.4081-3.1901-4.4025-5.268-5.9944z" strokeWidth="1.0095"/>
+ <path id="path3036" d="m2.607 52.819a9.1058 9.1058 0 0 1 -7.8859 -4.5529 9.1058 9.1058 0 0 1 0 -9.1058 9.1058 9.1058 0 0 1 7.8859 -4.5529l-2e-7 9.1058z" transform="rotate(-30)" strokeWidth="3.6204"/>
+ <path id="path3038-8" d="m17.502 6.8895c-13.868 8.0065-18.619 25.74-10.612 39.608 8.006 13.868 25.739 18.619 39.608 10.613 13.868-8.007 18.619-25.74 10.613-39.609-8.007-13.868-25.74-18.619-39.609-10.612zm1.706 2.9541c12.237-7.0648 27.884-2.8722 34.948 9.3644 7.065 12.237 2.873 27.884-9.364 34.948-12.237 7.065-27.884 2.873-34.948-9.364-7.0652-12.237-2.8726-27.884 9.364-34.948z" strokeWidth="1.0095"/>
+ <g id="text3003" ariaLabel="Traccar">
+ <path id="path4172" d="m89.719 48.671h-3.915v-30.192h-10.663v-3.4775h25.241v3.4775h-10.663v30.192z"/>
+ <path id="path4174" d="m116.36 22.969q1.6812 0 3.0169 0.27636l-0.52968 3.5466q-1.566-0.34544-2.7636-0.34544-3.063 0-5.2508 2.4872-2.1648 2.4872-2.1648 6.195v13.541h-3.8229v-25.241h3.1551l0.43756 4.675h0.18424q1.4048-2.4642 3.3854-3.7999t4.3526-1.3357z"/>
+ <path id="path4176" d="m139.62 48.671-0.75998-3.5926h-0.18424q-1.8884 2.3721-3.7769 3.2242-1.8654 0.82907-4.675 0.82907-3.7538 0-5.8956-1.9345-2.1187-1.9345-2.1187-5.5041 0-7.6459 12.229-8.0143l4.2835-0.13818v-1.566q0-2.9708-1.2897-4.3756-1.2666-1.4278-4.0763-1.4278-3.1551 0-7.1392 1.9345l-1.1745-2.9248q1.8654-1.0133 4.0762-1.589 2.2339-0.57574 4.4678-0.57574 4.5138 0 6.6786 2.0036 2.1878 2.0036 2.1878 6.4253v17.226h-2.8326zm-8.6361-2.6945q3.5696 0 5.5962-1.9575 2.0496-1.9575 2.0496-5.4811v-2.2799l-3.8229 0.16121q-4.5599 0.16121-6.5865 1.4278-2.0036 1.2436-2.0036 3.892 0 2.0727 1.2436 3.1551 1.2666 1.0824 3.5236 1.0824z"/>
+ <path id="path4178" d="m160.44 49.131q-5.4811 0-8.498-3.3623-2.9939-3.3854-2.9939-9.5573 0-6.3332 3.0399-9.7876 3.063-3.4545 8.7052-3.4545 1.8194 0 3.6387 0.3915t2.8557 0.92119l-1.1745 3.2472q-1.2666-0.50665-2.7636-0.82907-1.4969-0.34544-2.6484-0.34544-7.6919 0-7.6919 9.8106 0 4.652 1.8654 7.1392 1.8884 2.4872 5.5732 2.4872 3.1551 0 6.4713-1.3588v3.3854q-2.5333 1.3127-6.3792 1.3127z"/>
+ <path id="path4180" d="m182.92 49.131q-5.4811 0-8.498-3.3623-2.9939-3.3854-2.9939-9.5573 0-6.3332 3.0399-9.7876 3.063-3.4545 8.7052-3.4545 1.8193 0 3.6387 0.3915t2.8557 0.92119l-1.1745 3.2472q-1.2666-0.50665-2.7636-0.82907-1.4969-0.34544-2.6484-0.34544-7.6919 0-7.6919 9.8106 0 4.652 1.8654 7.1392 1.8884 2.4872 5.5732 2.4872 3.1551 0 6.4714-1.3588v3.3854q-2.5333 1.3127-6.3792 1.3127z"/>
+ <path id="path4182" d="m210.83 48.671-0.75998-3.5926h-0.18424q-1.8884 2.3721-3.7769 3.2242-1.8654 0.82907-4.675 0.82907-3.7538 0-5.8956-1.9345-2.1187-1.9345-2.1187-5.5041 0-7.6459 12.229-8.0143l4.2835-0.13818v-1.566q0-2.9708-1.2897-4.3756-1.2666-1.4278-4.0762-1.4278-3.1551 0-7.1392 1.9345l-1.1745-2.9248q1.8654-1.0133 4.0762-1.589 2.2339-0.57574 4.4678-0.57574 4.5138 0 6.6786 2.0036 2.1878 2.0036 2.1878 6.4253v17.226h-2.8326zm-8.6361-2.6945q3.5696 0 5.5962-1.9575 2.0496-1.9575 2.0496-5.4811v-2.2799l-3.8229 0.16121q-4.5599 0.16121-6.5865 1.4278-2.0036 1.2436-2.0036 3.892 0 2.0727 1.2436 3.1551 1.2666 1.0824 3.5235 1.0824z"/>
+ <path id="path4184" d="m233.08 22.969q1.6812 0 3.0169 0.27636l-0.52968 3.5466q-1.566-0.34544-2.7636-0.34544-3.0629 0-5.2508 2.4872-2.1648 2.4872-2.1648 6.195v13.541h-3.8229v-25.241h3.1551l0.43757 4.675h0.18423q1.4048-2.4642 3.3854-3.7999t4.3526-1.3357z"/>
+ </g>
+ </g>
+ </g>
+ </svg>
+ )
+}
+
+export default Logo;
diff --git a/modern/src/components/LoginForm.js b/modern/src/components/LoginForm.js
new file mode 100644
index 00000000..d52a51dd
--- /dev/null
+++ b/modern/src/components/LoginForm.js
@@ -0,0 +1,125 @@
+import React, { useState } from 'react';
+import { Grid, useMediaQuery, makeStyles, InputLabel, Select, MenuItem, FormControl, Button, TextField, Link } from '@material-ui/core';
+import { useTheme } from '@material-ui/core/styles';
+import { useDispatch, useSelector } from 'react-redux';
+import { useHistory } from 'react-router-dom';
+import { sessionActions } from './../store';
+import t from './../common/localization';
+import RegisterForm from './RegisterForm';
+import ResetPasswordForm from './ResetPasswordForm';
+
+const useStyles = makeStyles(theme => ({
+ logoContainer: {
+ textAlign: 'center',
+ },
+ resetPassword: {
+ cursor: 'pointer',
+ }
+}));
+
+const forms = {
+ register: () => RegisterForm,
+ resetPassword: () => ResetPasswordForm,
+};
+
+const LoginForm = ({ setCurrentForm }) => {
+
+ const classes = useStyles();
+ const dispatch = useDispatch();
+ const history = useHistory();
+ const theme = useTheme();
+ const matches = useMediaQuery(theme.breakpoints.down('md'));
+
+ const [failed, setFailed] = useState(false);
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const registrationEnabled = useSelector(state => state.session.server ? state.session.server['registration'] : false);
+
+ const handleEmailChange = (event) => {
+ setEmail(event.target.value);
+ }
+
+ const handlePasswordChange = (event) => {
+ setPassword(event.target.value);
+ }
+
+ const handleLogin = async (event) => {
+ event.preventDefault();
+ const response = await fetch('/api/session', { method: 'POST', body: new URLSearchParams(`email=${email}&password=${password}`) });
+ if (response.ok) {
+ const user = await response.json();
+ dispatch(sessionActions.updateUser(user));
+ history.push('/');
+ } else {
+ setFailed(true);
+ setPassword('');
+ }
+ }
+
+ return (
+ <Grid container direction='column' spacing={3}>
+ <Grid item className={classes.logoContainer}>
+ {matches && <img src='/logo.svg' alt='Traccar' />}
+ </Grid>
+ <Grid item>
+ <TextField
+ required
+ fullWidth
+ error={failed}
+ label={t('userEmail')}
+ name='email'
+ value={email}
+ autoComplete='email'
+ autoFocus
+ onChange={handleEmailChange}
+ helperText={failed && 'Invalid username or password'}
+ variant='filled' />
+ </Grid>
+ <Grid item>
+ <TextField
+ required
+ fullWidth
+ error={failed}
+ label={t('userPassword')}
+ name='password'
+ value={password}
+ type='password'
+ autoComplete='current-password'
+ onChange={handlePasswordChange}
+ variant='filled' />
+ </Grid>
+ <Grid item>
+ <Button
+ onClick={handleLogin}
+ variant='contained'
+ color='secondary'
+ disabled={!email || !password}
+ fullWidth>
+ {t('loginLogin')}
+ </Button>
+ </Grid>
+ <Grid item container>
+ <Grid item>
+ <Button onClick={() => setCurrentForm(forms.register)} disabled={!registrationEnabled} color="secondary">
+ {t('loginRegister')}
+ </Button>
+ </Grid>
+ <Grid item xs>
+ <FormControl variant="filled" fullWidth>
+ <InputLabel>{t('loginLanguage')}</InputLabel>
+ <Select>
+ <MenuItem value="en">English</MenuItem>
+ </Select>
+ </FormControl>
+ </Grid>
+ </Grid>
+ <Grid item container justify="flex-end">
+ <Grid item>
+ <Link onClick={() => setCurrentForm(forms.resetPassword)} className={classes.resetPassword} underline="none">{t('loginReset')}</Link>
+ </Grid>
+ </Grid>
+ </Grid>
+ )
+}
+
+export default LoginForm;
diff --git a/modern/src/components/RegisterForm.js b/modern/src/components/RegisterForm.js
new file mode 100644
index 00000000..6d013f70
--- /dev/null
+++ b/modern/src/components/RegisterForm.js
@@ -0,0 +1,122 @@
+import React, { useState } from 'react';
+import { Grid, Button, TextField, Typography, Link, makeStyles, Snackbar } from '@material-ui/core';
+import ArrowBackIcon from '@material-ui/icons/ArrowBack';
+import LoginForm from './LoginForm';
+import t from './../common/localization';
+
+const useStyles = makeStyles(theme => ({
+ register: {
+ fontSize: theme.spacing(3),
+ fontWeight: 500
+ },
+ link: {
+ fontSize: theme.spacing(3),
+ fontWeight: 500,
+ marginTop: theme.spacing(0.5),
+ cursor: 'pointer'
+ }
+}));
+
+const forms = {
+ login: () => LoginForm,
+};
+
+const RegisterForm = ({ setCurrentForm }) => {
+
+ const classes = useStyles();
+ const [name, setName] = useState('');
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [snackbarOpen, setSnackbarOpen] = useState(false);
+
+ const submitDisabled = () => {
+ return !name || !/(.+)@(.+)\.(.{2,})/.test(email) || !password;
+ }
+
+ const handleRegister = async () => {
+ const response = await fetch('/api/users', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({name, email, password})
+ });
+
+ if (response.ok) {
+ setSnackbarOpen(true);
+ }
+ }
+
+ return (
+ <>
+ <Snackbar
+ anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
+ open={snackbarOpen}
+ onClose={() => setCurrentForm(forms.login)}
+ autoHideDuration={6000}
+ message={t('loginCreated')} />
+ <Grid container direction='column' spacing={3}>
+ <Grid container item>
+ <Grid item>
+ <Typography className={classes.link} color='primary'>
+ <Link onClick={() => setCurrentForm(forms.login)}>
+ <ArrowBackIcon />
+ </Link>
+ </Typography>
+ </Grid>
+ <Grid item xs>
+ <Typography className={classes.register} color='primary'>
+ {t('loginRegister')}
+ </Typography>
+ </Grid>
+ </Grid>
+ <Grid item>
+ <TextField
+ required
+ fullWidth
+ label={t('sharedName')}
+ name='name'
+ value={name || ''}
+ autoComplete='name'
+ autoFocus
+ onChange={event => setName(event.target.value)}
+ variant='filled' />
+ </Grid>
+ <Grid item>
+ <TextField
+ required
+ fullWidth
+ type='email'
+ label={t('userEmail')}
+ name='email'
+ value={email || ''}
+ autoComplete='email'
+ onChange={event => setEmail(event.target.value)}
+ variant='filled' />
+ </Grid>
+ <Grid item>
+ <TextField
+ required
+ fullWidth
+ label={t('userPassword')}
+ name='password'
+ value={password || ''}
+ type='password'
+ autoComplete='current-password'
+ onChange={event => setPassword(event.target.value)}
+ variant='filled' />
+ </Grid>
+ <Grid item>
+ <Button
+ variant='contained'
+ color="secondary"
+ onClick={handleRegister}
+ disabled={submitDisabled()}
+ fullWidth>
+ {t('loginRegister')}
+ </Button>
+ </Grid>
+ </Grid>
+ </>
+ )
+}
+
+export default RegisterForm;
diff --git a/modern/src/components/ResetPasswordForm.js b/modern/src/components/ResetPasswordForm.js
new file mode 100644
index 00000000..c268f808
--- /dev/null
+++ b/modern/src/components/ResetPasswordForm.js
@@ -0,0 +1,12 @@
+import React from 'react';
+
+const ResetPasswordForm = () => {
+
+ return (
+ <>
+ <div>Reset Password Comming Soon!!!</div>
+ </>
+ )
+}
+
+export default ResetPasswordForm;
diff --git a/modern/src/theme/dimensions.js b/modern/src/theme/dimensions.js
new file mode 100644
index 00000000..e36fc23b
--- /dev/null
+++ b/modern/src/theme/dimensions.js
@@ -0,0 +1,6 @@
+export default {
+ inputHeight: '42px',
+ borderRadius: '4px',
+ sidebarWidth: '28%',
+ tabletSidebarWidth: '52px'
+};
diff --git a/modern/src/theme/index.js b/modern/src/theme/index.js
new file mode 100644
index 00000000..5a3b2a9c
--- /dev/null
+++ b/modern/src/theme/index.js
@@ -0,0 +1,12 @@
+import { createMuiTheme } from '@material-ui/core/styles';
+import palette from './palette';
+import overrides from './overrides';
+import dimensions from './dimensions';
+
+const theme = createMuiTheme({
+ palette,
+ overrides,
+ dimensions
+});
+
+export default theme;
diff --git a/modern/src/theme/overrides.js b/modern/src/theme/overrides.js
new file mode 100644
index 00000000..c8d64a9d
--- /dev/null
+++ b/modern/src/theme/overrides.js
@@ -0,0 +1,49 @@
+import dimensions from './dimensions';
+
+export default {
+ MuiFormControl: {
+ root: {
+ height: dimensions.inputHeight,
+ }
+ },
+ MuiInputLabel: {
+ filled: {
+ transform: 'translate(12px, 14px) scale(1)',
+ '&$shrink' :{
+ transform: 'translate(12px, -12px) scale(0.75)'
+ }
+ },
+ },
+ MuiFilledInput: {
+ root: {
+ height: dimensions.inputHeight,
+ borderRadius: dimensions.borderRadius,
+ background: 'rgba(0, 0, 0, 0.035)',
+ },
+ input: {
+ height: dimensions.inputHeight,
+ borderRadius: dimensions.borderRadius,
+ paddingTop: '10px',
+ boxSizing: 'border-box',
+ '&:-webkit-autofill': {
+ WebkitBoxShadow: '0 0 0 100px #eeeeee inset',
+ },
+ },
+ underline: {
+ "&:before": {
+ borderBottom: 'none',
+ },
+ "&:after": {
+ borderBottom: 'none',
+ },
+ "&:hover:before": {
+ borderBottom: 'none',
+ },
+ }
+ },
+ MuiButton: {
+ root: {
+ height: dimensions.inputHeight,
+ }
+ }
+};
diff --git a/modern/src/theme/palette.js b/modern/src/theme/palette.js
new file mode 100644
index 00000000..5c93cfd2
--- /dev/null
+++ b/modern/src/theme/palette.js
@@ -0,0 +1,17 @@
+const traccarPurple = '#333366';
+const traccarGreen = '#4CAF50';
+const traccarWhite = '#FFF';
+
+export default {
+ common: {
+ purple: traccarPurple,
+ green: traccarGreen
+ },
+ primary: {
+ main: traccarPurple
+ },
+ secondary: {
+ main: traccarGreen,
+ contrastText: traccarWhite
+ }
+};