diff options
Diffstat (limited to 'modern/src/common/components')
-rw-r--r-- | modern/src/common/components/BottomMenu.js | 99 | ||||
-rw-r--r-- | modern/src/common/components/NavBar.js | 20 | ||||
-rw-r--r-- | modern/src/common/components/PositionValue.js | 92 | ||||
-rw-r--r-- | modern/src/common/components/RemoveDialog.js | 37 | ||||
-rw-r--r-- | modern/src/common/components/SideNav.js | 34 |
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)} + + {admin && (<Link component={RouterLink} to={`/settings/accumulators/${position.deviceId}`}>⚙</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; |