aboutsummaryrefslogtreecommitdiff
path: root/modern/src/common
diff options
context:
space:
mode:
Diffstat (limited to 'modern/src/common')
-rw-r--r--modern/src/common/components/BottomMenu.js99
-rw-r--r--modern/src/common/components/NavBar.js20
-rw-r--r--modern/src/common/components/PositionValue.js92
-rw-r--r--modern/src/common/components/RemoveDialog.js37
-rw-r--r--modern/src/common/components/SideNav.js34
-rw-r--r--modern/src/common/theme/dimensions.js15
-rw-r--r--modern/src/common/theme/index.js12
-rw-r--r--modern/src/common/theme/overrides.js87
-rw-r--r--modern/src/common/theme/palette.js16
-rw-r--r--modern/src/common/util/converter.js (renamed from modern/src/common/converter.js)0
-rw-r--r--modern/src/common/util/deviceCategories.js (renamed from modern/src/common/deviceCategories.js)0
-rw-r--r--modern/src/common/util/duration.js (renamed from modern/src/common/duration.js)0
-rw-r--r--modern/src/common/util/formatter.js (renamed from modern/src/common/formatter.js)0
-rw-r--r--modern/src/common/util/permissions.js (renamed from modern/src/common/permissions.js)0
-rw-r--r--modern/src/common/util/preferences.js (renamed from modern/src/common/preferences.js)0
-rw-r--r--modern/src/common/util/stringUtils.js (renamed from modern/src/common/stringUtils.js)0
-rw-r--r--modern/src/common/util/usePersistedState.js (renamed from modern/src/common/usePersistedState.js)0
-rw-r--r--modern/src/common/util/useQuery.js (renamed from modern/src/common/useQuery.js)0
18 files changed, 412 insertions, 0 deletions
diff --git a/modern/src/common/components/BottomMenu.js b/modern/src/common/components/BottomMenu.js
new file mode 100644
index 00000000..d26b4ae2
--- /dev/null
+++ b/modern/src/common/components/BottomMenu.js
@@ -0,0 +1,99 @@
+import React, { useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import { useHistory, useLocation } from 'react-router-dom';
+import {
+ Paper, BottomNavigation, BottomNavigationAction, Menu, MenuItem, Typography,
+} from '@material-ui/core';
+
+import DescriptionIcon from '@material-ui/icons/Description';
+import SettingsIcon from '@material-ui/icons/Settings';
+import MapIcon from '@material-ui/icons/Map';
+import PersonIcon from '@material-ui/icons/Person';
+import ExitToAppIcon from '@material-ui/icons/ExitToApp';
+
+import { sessionActions } from '../../store';
+import { useTranslation } from '../../LocalizationProvider';
+import { useReadonly } from '../util/permissions';
+
+const BottomMenu = () => {
+ const history = useHistory();
+ const location = useLocation();
+ const dispatch = useDispatch();
+ const t = useTranslation();
+
+ const readonly = useReadonly();
+ const userId = useSelector((state) => state.session.user?.id);
+
+ const [anchorEl, setAnchorEl] = useState(null);
+
+ const currentSelection = () => {
+ if (location.pathname.startsWith('/user')) {
+ return 3;
+ } if (location.pathname.startsWith('/settings')) {
+ return 2;
+ } if (location.pathname.startsWith('/reports')) {
+ return 1;
+ } if (location.pathname === '/') {
+ return 0;
+ }
+ return null;
+ };
+
+ const handleAccount = () => {
+ setAnchorEl(null);
+ history.push(`/user/${userId}`);
+ };
+
+ const handleLogout = async () => {
+ setAnchorEl(null);
+ await fetch('/api/session', { method: 'DELETE' });
+ history.push('/login');
+ dispatch(sessionActions.updateUser(null));
+ };
+
+ const handleSelection = (event, value) => {
+ switch (value) {
+ case 0:
+ history.push('/');
+ break;
+ case 1:
+ history.push('/reports/route');
+ break;
+ case 2:
+ history.push('/settings/preferences');
+ break;
+ case 3:
+ if (readonly) {
+ handleLogout();
+ } else {
+ setAnchorEl(event.currentTarget);
+ }
+ break;
+ default:
+ break;
+ }
+ };
+
+ return (
+ <Paper square elevation={3}>
+ <BottomNavigation value={currentSelection()} onChange={handleSelection} showLabels>
+ <BottomNavigationAction label={t('mapTitle')} icon={<MapIcon />} />
+ <BottomNavigationAction label={t('reportTitle')} icon={<DescriptionIcon />} />
+ <BottomNavigationAction label={t('settingsTitle')} icon={<SettingsIcon />} />
+ {readonly
+ ? (<BottomNavigationAction label={t('loginLogout')} icon={<ExitToAppIcon />} />)
+ : (<BottomNavigationAction label={t('settingsUser')} icon={<PersonIcon />} />)}
+ </BottomNavigation>
+ <Menu anchorEl={anchorEl} keepMounted open={Boolean(anchorEl)} onClose={() => setAnchorEl(null)}>
+ <MenuItem onClick={handleAccount}>
+ <Typography color="textPrimary">{t('settingsUser')}</Typography>
+ </MenuItem>
+ <MenuItem onClick={handleLogout}>
+ <Typography color="error">{t('loginLogout')}</Typography>
+ </MenuItem>
+ </Menu>
+ </Paper>
+ );
+};
+
+export default BottomMenu;
diff --git a/modern/src/common/components/NavBar.js b/modern/src/common/components/NavBar.js
new file mode 100644
index 00000000..ac689e76
--- /dev/null
+++ b/modern/src/common/components/NavBar.js
@@ -0,0 +1,20 @@
+import React from 'react';
+import {
+ AppBar, Toolbar, Typography, IconButton,
+} from '@material-ui/core';
+import MenuIcon from '@material-ui/icons/Menu';
+
+const Navbar = ({ setOpenDrawer, title }) => (
+ <AppBar position="sticky" color="inherit">
+ <Toolbar>
+ <IconButton color="inherit" edge="start" onClick={() => setOpenDrawer(true)}>
+ <MenuIcon />
+ </IconButton>
+ <Typography variant="h6" noWrap>
+ {title}
+ </Typography>
+ </Toolbar>
+ </AppBar>
+);
+
+export default Navbar;
diff --git a/modern/src/common/components/PositionValue.js b/modern/src/common/components/PositionValue.js
new file mode 100644
index 00000000..b160be34
--- /dev/null
+++ b/modern/src/common/components/PositionValue.js
@@ -0,0 +1,92 @@
+import React, { useEffect, useState } from 'react';
+import { Link } from '@material-ui/core';
+import { Link as RouterLink } from 'react-router-dom';
+import {
+ formatAlarm, formatBoolean, formatCoordinate, formatCourse, formatDistance, formatNumber, formatPercentage, formatSpeed, formatTime,
+} from '../util/formatter';
+import { useAttributePreference, usePreference } from '../util/preferences';
+import { useTranslation } from '../../LocalizationProvider';
+import { useAdministrator } from '../util/permissions';
+
+const PositionValue = ({ position, property, attribute }) => {
+ const t = useTranslation();
+
+ const admin = useAdministrator();
+
+ const key = property || attribute;
+ const value = property ? position[property] : position.attributes[attribute];
+
+ const distanceUnit = useAttributePreference('distanceUnit');
+ const speedUnit = useAttributePreference('speedUnit');
+ const coordinateFormat = usePreference('coordinateFormat');
+
+ const [address, setAddress] = useState();
+
+ useEffect(() => {
+ setAddress(position.address);
+ }, [position]);
+
+ const showAddress = async () => {
+ const query = new URLSearchParams({
+ latitude: position.latitude,
+ longitude: position.longitude,
+ });
+ const response = await fetch(`/api/server/geocode?${query.toString()}`);
+ if (response.ok) {
+ setAddress(await response.text());
+ }
+ };
+
+ const formatValue = () => {
+ switch (key) {
+ case 'fixTime':
+ case 'deviceTime':
+ case 'serverTime':
+ return formatTime(value);
+ case 'latitude':
+ return formatCoordinate('latitude', value, coordinateFormat);
+ case 'longitude':
+ return formatCoordinate('longitude', value, coordinateFormat);
+ case 'speed':
+ return formatSpeed(value, speedUnit, t);
+ case 'course':
+ return formatCourse(value);
+ case 'batteryLevel':
+ return formatPercentage(value);
+ case 'alarm':
+ return formatAlarm(value, t);
+ case 'odometer':
+ case 'distance':
+ case 'totalDistance':
+ return formatDistance(value, distanceUnit, t);
+ default:
+ if (typeof value === 'number') {
+ return formatNumber(value);
+ } if (typeof value === 'boolean') {
+ return formatBoolean(value, t);
+ }
+ return value;
+ }
+ };
+
+ switch (key) {
+ case 'totalDistance':
+ case 'hours':
+ return (
+ <>
+ {formatValue(value)}
+ &nbsp;&nbsp;
+ {admin && (<Link component={RouterLink} to={`/settings/accumulators/${position.deviceId}`}>&#9881;</Link>)}
+ </>
+ );
+ case 'address':
+ if (address) {
+ return (<>{address}</>);
+ }
+ return (<Link href="#" onClick={showAddress}>{t('sharedShowAddress')}</Link>);
+ default:
+ return (<>{formatValue(value)}</>);
+ }
+};
+
+export default PositionValue;
diff --git a/modern/src/common/components/RemoveDialog.js b/modern/src/common/components/RemoveDialog.js
new file mode 100644
index 00000000..1b75e926
--- /dev/null
+++ b/modern/src/common/components/RemoveDialog.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import Button from '@material-ui/core/Button';
+import Dialog from '@material-ui/core/Dialog';
+import DialogActions from '@material-ui/core/DialogActions';
+import DialogContent from '@material-ui/core/DialogContent';
+import DialogContentText from '@material-ui/core/DialogContentText';
+import { useTranslation } from '../../LocalizationProvider';
+
+const RemoveDialog = ({
+ open, endpoint, itemId, onResult,
+}) => {
+ const t = useTranslation();
+
+ const handleRemove = async () => {
+ const response = await fetch(`/api/${endpoint}/${itemId}`, { method: 'DELETE' });
+ if (response.ok) {
+ onResult(true);
+ }
+ };
+
+ return (
+ <Dialog
+ open={open}
+ onClose={() => { onResult(false); }}
+ >
+ <DialogContent>
+ <DialogContentText>{t('sharedRemoveConfirm')}</DialogContentText>
+ </DialogContent>
+ <DialogActions>
+ <Button color="primary" onClick={handleRemove}>{t('sharedRemove')}</Button>
+ <Button autoFocus onClick={() => onResult(false)}>{t('sharedCancel')}</Button>
+ </DialogActions>
+ </Dialog>
+ );
+};
+
+export default RemoveDialog;
diff --git a/modern/src/common/components/SideNav.js b/modern/src/common/components/SideNav.js
new file mode 100644
index 00000000..ad7c212a
--- /dev/null
+++ b/modern/src/common/components/SideNav.js
@@ -0,0 +1,34 @@
+import React, { Fragment } from 'react';
+import {
+ List, ListItem, ListItemText, ListItemIcon, Divider, ListSubheader,
+} from '@material-ui/core';
+import { Link, useLocation } from 'react-router-dom';
+
+const SideNav = ({ routes }) => {
+ const location = useLocation();
+
+ return (
+ <List disablePadding style={{ paddingTop: '16px' }}>
+ {routes.map((route) => (route.subheader ? (
+ <Fragment key={route.subheader}>
+ <Divider />
+ <ListSubheader>{route.subheader}</ListSubheader>
+ </Fragment>
+ ) : (
+ <ListItem
+ disableRipple
+ component={Link}
+ key={route.href}
+ button
+ to={route.href}
+ selected={location.pathname.match(route.match || route.href) !== null}
+ >
+ <ListItemIcon>{route.icon}</ListItemIcon>
+ <ListItemText primary={route.name} />
+ </ListItem>
+ )))}
+ </List>
+ );
+};
+
+export default SideNav;
diff --git a/modern/src/common/theme/dimensions.js b/modern/src/common/theme/dimensions.js
new file mode 100644
index 00000000..fa7d3b25
--- /dev/null
+++ b/modern/src/common/theme/dimensions.js
@@ -0,0 +1,15 @@
+export default {
+ inputHeight: '42px',
+ borderRadius: '4px',
+ sidebarWidth: '28%',
+ sidebarWidthTablet: '52px',
+ drawerWidthDesktop: '360px',
+ drawerWidthTablet: '320px',
+ bottomBarHeight: 56,
+ columnWidthDate: 160,
+ columnWidthNumber: 130,
+ columnWidthString: 160,
+ columnWidthBoolean: 130,
+ popupMapOffset: 300,
+ popupMaxWidth: 272,
+};
diff --git a/modern/src/common/theme/index.js b/modern/src/common/theme/index.js
new file mode 100644
index 00000000..02865c23
--- /dev/null
+++ b/modern/src/common/theme/index.js
@@ -0,0 +1,12 @@
+import { createTheme } from '@material-ui/core/styles';
+import palette from './palette';
+import overrides from './overrides';
+import dimensions from './dimensions';
+
+const theme = createTheme({
+ palette,
+ overrides,
+ dimensions,
+});
+
+export default theme;
diff --git a/modern/src/common/theme/overrides.js b/modern/src/common/theme/overrides.js
new file mode 100644
index 00000000..d1fe844c
--- /dev/null
+++ b/modern/src/common/theme/overrides.js
@@ -0,0 +1,87 @@
+import dimensions from './dimensions';
+
+export default {
+ MuiFormControl: {
+ root: {
+ marginTop: 5,
+ marginBottom: 5,
+ },
+ },
+ MuiInputLabel: {
+ filled: {
+ transform: 'translate(12px, 14px) scale(1)',
+ '&$shrink': {
+ transform: 'translate(12px, -14px) scale(0.72)',
+ },
+ },
+ },
+ MuiFilledInput: {
+ root: {
+ height: dimensions.inputHeight,
+ borderRadius: dimensions.borderRadius,
+ backgroundColor: 'rgba(0, 0, 0, 0.035)',
+ },
+ input: {
+ height: dimensions.inputHeight,
+ borderRadius: dimensions.borderRadius,
+ paddingTop: '11.5px',
+ paddingBottom: '11.5px',
+ boxSizing: 'border-box',
+ '&:-webkit-autofill': {
+ WebkitBoxShadow: '0 0 0 100px #eeeeee inset',
+ },
+ },
+ underline: {
+ '&:before': {
+ borderBottom: 'none',
+ },
+ '&:after': {
+ borderBottom: 'none',
+ },
+ '&:hover:before': {
+ borderBottom: 'none',
+ },
+ },
+ },
+ MuiSelect: {
+ select: {
+ borderRadius: dimensions.borderRadius,
+ '&&:focus': {
+ borderRadius: dimensions.borderRadius,
+ },
+ },
+ },
+ MuiButton: {
+ root: {
+ height: dimensions.inputHeight,
+ marginTop: 5,
+ marginBottom: 5,
+ '&$disabled': {
+ opacity: 0.4,
+ color: undefined,
+ },
+ },
+ contained: {
+ '&$disabled': {
+ opacity: 0.4,
+ color: undefined,
+ backgroundColor: undefined,
+ },
+ },
+ },
+ MuiFormHelperText: {
+ root: {
+ marginBottom: -10,
+ },
+ contained: {
+ marginLeft: 12,
+ },
+ },
+ MuiAutocomplete: {
+ inputRoot: {
+ '&.MuiFilledInput-root': {
+ paddingTop: 0,
+ },
+ },
+ },
+};
diff --git a/modern/src/common/theme/palette.js b/modern/src/common/theme/palette.js
new file mode 100644
index 00000000..02261950
--- /dev/null
+++ b/modern/src/common/theme/palette.js
@@ -0,0 +1,16 @@
+export default {
+ primary: {
+ main: '#1a237e',
+ },
+ secondary: {
+ main: '#4caf50',
+ contrastText: '#ffffff',
+ },
+ colors: {
+ white: '#ffffff',
+ positive: '#4caf50',
+ medium: '#ffa000',
+ negative: '#f44336',
+ neutral: '#9e9e9e',
+ },
+};
diff --git a/modern/src/common/converter.js b/modern/src/common/util/converter.js
index 61e2dfe6..61e2dfe6 100644
--- a/modern/src/common/converter.js
+++ b/modern/src/common/util/converter.js
diff --git a/modern/src/common/deviceCategories.js b/modern/src/common/util/deviceCategories.js
index f5d749aa..f5d749aa 100644
--- a/modern/src/common/deviceCategories.js
+++ b/modern/src/common/util/deviceCategories.js
diff --git a/modern/src/common/duration.js b/modern/src/common/util/duration.js
index aae74868..aae74868 100644
--- a/modern/src/common/duration.js
+++ b/modern/src/common/util/duration.js
diff --git a/modern/src/common/formatter.js b/modern/src/common/util/formatter.js
index 08e29bc4..08e29bc4 100644
--- a/modern/src/common/formatter.js
+++ b/modern/src/common/util/formatter.js
diff --git a/modern/src/common/permissions.js b/modern/src/common/util/permissions.js
index 72ca0b08..72ca0b08 100644
--- a/modern/src/common/permissions.js
+++ b/modern/src/common/util/permissions.js
diff --git a/modern/src/common/preferences.js b/modern/src/common/util/preferences.js
index aba3c82c..aba3c82c 100644
--- a/modern/src/common/preferences.js
+++ b/modern/src/common/util/preferences.js
diff --git a/modern/src/common/stringUtils.js b/modern/src/common/util/stringUtils.js
index fc997fe0..fc997fe0 100644
--- a/modern/src/common/stringUtils.js
+++ b/modern/src/common/util/stringUtils.js
diff --git a/modern/src/common/usePersistedState.js b/modern/src/common/util/usePersistedState.js
index 8bc4401f..8bc4401f 100644
--- a/modern/src/common/usePersistedState.js
+++ b/modern/src/common/util/usePersistedState.js
diff --git a/modern/src/common/useQuery.js b/modern/src/common/util/useQuery.js
index f246df7c..f246df7c 100644
--- a/modern/src/common/useQuery.js
+++ b/modern/src/common/util/useQuery.js