diff options
Diffstat (limited to 'modern/src/common')
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)} + + {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; 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 |