aboutsummaryrefslogtreecommitdiff
path: root/modern/src/common/components
diff options
context:
space:
mode:
authorAnton Tananaev <anton@traccar.org>2022-05-08 11:48:09 -0700
committerAnton Tananaev <anton@traccar.org>2022-05-08 11:48:09 -0700
commit2352071211b61c10fa5bf5736baaff7809d18bf0 (patch)
tree743e4adc1cc35fb3585b912daaa8719ae5757f60 /modern/src/common/components
parent044733ff543156d76437daae8edb66850d785ac9 (diff)
downloadtrackermap-web-2352071211b61c10fa5bf5736baaff7809d18bf0.tar.gz
trackermap-web-2352071211b61c10fa5bf5736baaff7809d18bf0.tar.bz2
trackermap-web-2352071211b61c10fa5bf5736baaff7809d18bf0.zip
Organize common code
Diffstat (limited to 'modern/src/common/components')
-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
5 files changed, 282 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;