diff options
Diffstat (limited to 'modern/src')
-rw-r--r-- | modern/src/DevicesList.js | 88 | ||||
-rw-r--r-- | modern/src/MainPage.js | 174 | ||||
-rw-r--r-- | modern/src/components/BottomNav.js | 109 | ||||
-rw-r--r-- | modern/src/theme/dimensions.js | 1 |
4 files changed, 319 insertions, 53 deletions
diff --git a/modern/src/DevicesList.js b/modern/src/DevicesList.js index 85b936ce..294a9fff 100644 --- a/modern/src/DevicesList.js +++ b/modern/src/DevicesList.js @@ -3,21 +3,24 @@ import { useDispatch, useSelector } from 'react-redux'; import { makeStyles } from '@material-ui/core/styles'; import Avatar from '@material-ui/core/Avatar'; import Divider from '@material-ui/core/Divider'; -import IconButton from '@material-ui/core/IconButton'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; import ListItemAvatar from '@material-ui/core/ListItemAvatar'; import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; +import Grid from '@material-ui/core/Grid'; import ListItemText from '@material-ui/core/ListItemText'; -import MoreVertIcon from '@material-ui/icons/MoreVert'; +import SvgIcon from '@material-ui/core/SvgIcon'; import { FixedSizeList } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; +import BatteryFullIcon from '@material-ui/icons/BatteryFull'; +import { ReactComponent as IgnitionIcon } from '../public/images/icon/ignition.svg'; import { devicesActions } from './store'; import EditCollectionView from './EditCollectionView'; import { useEffectAsync } from './reactHelper'; +import { formatPosition } from './common/formatter'; -const useStyles = makeStyles(() => ({ +const useStyles = makeStyles((theme) => ({ list: { maxHeight: '100%', }, @@ -26,29 +29,92 @@ const useStyles = makeStyles(() => ({ height: '25px', filter: 'brightness(0) invert(1)', }, + listItem: { + backgroundColor: 'white', + '&:hover': { + backgroundColor: 'white', + }, + }, + batteryText: { + fontSize: '0.75rem', + fontWeight: 'normal', + lineHeight: '0.875rem', + }, + green: { + color: theme.palette.common.green, + }, + red: { + color: theme.palette.common.red, + }, + gray: { + color: theme.palette.common.gray, + }, + indicators: { + lineHeight: 1, + }, })); +const getStatusColor = (status) => { + switch (status) { + case 'online': + return 'green'; + case 'offline': + return 'red'; + case 'unknown': + default: + return 'gray'; + } +}; + +const getBatteryStatus = (batteryLevel) => { + if (batteryLevel >= 70) { + return 'green'; + } + if (batteryLevel > 30) { + return 'gray'; + } + return 'red'; +}; + const DeviceRow = ({ data, index, style }) => { const classes = useStyles(); const dispatch = useDispatch(); - const { items, onMenuClick } = data; + const { items } = data; const item = items[index]; + const position = useSelector((state) => state.positions.items[item.id]); + const showIgnition = position?.attributes.hasOwnProperty('ignition') && position.attributes.ignition; return ( <div style={style}> <Fragment key={index}> - <ListItem button key={item.id} onClick={() => dispatch(devicesActions.select(item))}> + <ListItem button key={item.id} className={classes.listItem} onClick={() => dispatch(devicesActions.select(item))}> <ListItemAvatar> <Avatar> <img className={classes.icon} src={`images/icon/${item.category || 'default'}.svg`} alt="" /> </Avatar> </ListItemAvatar> - <ListItemText primary={item.name} secondary={item.uniqueId} /> - <ListItemSecondaryAction> - <IconButton onClick={(event) => onMenuClick(event.currentTarget, item.id)}> - <MoreVertIcon /> - </IconButton> + <ListItemText primary={item.name} secondary={item.status} classes={{ secondary: classes[getStatusColor(item.status)] }} /> + <ListItemSecondaryAction className={classes.indicators}> + {position && ( + <Grid container direction="row" alignItems="center" alignContent="center" spacing={2}> + {showIgnition && ( + <Grid item> + <SvgIcon component={IgnitionIcon} /> + </Grid> + )} + {position.attributes.hasOwnProperty('batteryLevel') && ( + <Grid item container xs alignItems="center" alignContent="center"> + <Grid item> + <span className={classes.batteryText}>{formatPosition(position.attributes.batteryLevel, 'batteryLevel')}</span> + </Grid> + <Grid item> + <BatteryFullIcon className={classes[getBatteryStatus(position.attributes.batteryLevel)]} /> + </Grid> + </Grid> + )} + </Grid> + )} </ListItemSecondaryAction> </ListItem> {index < items.length - 1 ? <Divider /> : null} @@ -90,7 +156,7 @@ const DeviceView = ({ updateTimestamp, onMenuClick }) => { }; const DevicesList = () => ( - <EditCollectionView content={DeviceView} editPath="/device" endpoint="devices" /> + <EditCollectionView content={DeviceView} editPath="/device" endpoint="devices" disableAdd /> ); export default DevicesList; diff --git a/modern/src/MainPage.js b/modern/src/MainPage.js index 88608df7..417cdb03 100644 --- a/modern/src/MainPage.js +++ b/modern/src/MainPage.js @@ -1,74 +1,164 @@ -import React from 'react'; -import { isWidthUp, makeStyles, withWidth } from '@material-ui/core'; -import Drawer from '@material-ui/core/Drawer'; -import ContainerDimensions from 'react-container-dimensions'; +import React, { useState, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; +import { + makeStyles, Paper, Toolbar, TextField, IconButton, Button, +} from '@material-ui/core'; + +import { useTheme } from '@material-ui/core/styles'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; +import AddIcon from '@material-ui/icons/Add'; +import CloseIcon from '@material-ui/icons/Close'; +import ArrowBackIcon from '@material-ui/icons/ArrowBack'; +import ListIcon from '@material-ui/icons/ViewList'; + import DevicesList from './DevicesList'; -import MainToolbar from './MainToolbar'; import Map from './map/Map'; import SelectedDeviceMap from './map/SelectedDeviceMap'; import AccuracyMap from './map/AccuracyMap'; import GeofenceMap from './map/GeofenceMap'; import CurrentPositionsMap from './map/CurrentPositionsMap'; import CurrentLocationMap from './map/CurrentLocationMap'; +import BottomNav from './components/BottomNav'; +import t from './common/localization'; const useStyles = makeStyles((theme) => ({ root: { - height: '100%', + height: '100vh', + }, + sidebar: { display: 'flex', flexDirection: 'column', + position: 'absolute', + left: 0, + top: 0, + margin: theme.spacing(1.5), + width: theme.dimensions.drawerWidthDesktop, + bottom: theme.spacing(8), + zIndex: 1301, + transition: 'transform .5s ease', + [theme.breakpoints.down('md')]: { + width: '100%', + margin: 0, + backgroundColor: 'white', + }, }, - content: { - flexGrow: 1, - overflow: 'hidden', - display: 'flex', - flexDirection: 'row', - [theme.breakpoints.down('xs')]: { - flexDirection: 'column-reverse', + sidebarCollapsed: { + transform: `translateX(-${theme.dimensions.drawerWidthDesktop})`, + marginLeft: 0, + [theme.breakpoints.down('md')]: { + transform: 'translateX(-100vw)', }, }, - drawerPaper: { - position: 'relative', - [theme.breakpoints.up('sm')]: { - width: 350, + paper: { + borderRadius: '0px', + }, + toolbar: { + display: 'flex', + padding: theme.spacing(0, 1), + '& > *': { + margin: theme.spacing(0, 1), }, - [theme.breakpoints.down('xs')]: { - height: 250, + }, + deviceList: { + flex: 1, + overflow: 'auto', + padding: theme.spacing(1.5, 0), + }, + sidebarToggle: { + position: 'absolute', + left: theme.spacing(1.5), + top: theme.spacing(3), + borderRadius: '0px', + minWidth: 0, + [theme.breakpoints.down('md')]: { + left: theme.spacing(0), }, - overflow: 'hidden', }, - mapContainer: { - flexGrow: 1, + sidebarToggleBg: { + backgroundColor: 'white', + color: '#777777', + '&:hover': { + backgroundColor: 'white', + }, }, })); -const MainPage = ({ width }) => { +const MainPage = () => { const classes = useStyles(); + const history = useHistory(); + const theme = useTheme(); + + const isTablet = useMediaQuery(theme.breakpoints.down('md')); + const isPhone = useMediaQuery(theme.breakpoints.down('xs')); + + const [deviceName, setDeviceName] = useState(''); + const [collapsed, setCollapsed] = useState(false); + + const handleClose = () => { + setCollapsed(!collapsed); + }; + + // Collapse sidebar for tablets and phones + useEffect(() => { + setCollapsed(isTablet); + }, [isTablet]); return ( <div className={classes.root}> - <MainToolbar /> - <div className={classes.content}> - <Drawer - anchor={isWidthUp('sm', width) ? 'left' : 'bottom'} - variant="permanent" - classes={{ paper: classes.drawerPaper }} - > + <Map> + <CurrentLocationMap /> + <GeofenceMap /> + <AccuracyMap /> + <CurrentPositionsMap /> + <SelectedDeviceMap /> + </Map> + <Button + variant="contained" + color={isPhone ? 'secondary' : 'primary'} + classes={{ containedPrimary: classes.sidebarToggleBg }} + className={classes.sidebarToggle} + onClick={handleClose} + disableElevation + > + <ListIcon /> + {!isPhone ? t('deviceTitle') : ''} + </Button> + <div className={`${classes.sidebar} ${collapsed && classes.sidebarCollapsed}`}> + <Paper className={classes.paper} elevation={isTablet ? 3 : 1}> + <Toolbar className={classes.toolbar} disableGutters> + {isTablet && ( + <IconButton onClick={handleClose}> + <ArrowBackIcon /> + </IconButton> + )} + <TextField + fullWidth + name="deviceName" + value={deviceName} + autoComplete="deviceName" + autoFocus + onChange={(event) => setDeviceName(event.target.value)} + placeholder="Search Devices" + variant="filled" + /> + <IconButton onClick={() => history.push('/device')}> + <AddIcon /> + </IconButton> + {!isTablet && ( + <IconButton onClick={handleClose}> + <CloseIcon /> + </IconButton> + )} + </Toolbar> + </Paper> + <div className={classes.deviceList}> <DevicesList /> - </Drawer> - <div className={classes.mapContainer}> - <ContainerDimensions> - <Map> - <CurrentLocationMap /> - <GeofenceMap /> - <AccuracyMap /> - <CurrentPositionsMap /> - <SelectedDeviceMap /> - </Map> - </ContainerDimensions> </div> </div> + + <BottomNav showOnDesktop /> </div> ); }; -export default withWidth()(MainPage); +export default MainPage; diff --git a/modern/src/components/BottomNav.js b/modern/src/components/BottomNav.js new file mode 100644 index 00000000..6aad1dd9 --- /dev/null +++ b/modern/src/components/BottomNav.js @@ -0,0 +1,109 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { Link, useHistory } from 'react-router-dom'; +import { + makeStyles, Paper, Toolbar, IconButton, useMediaQuery, useTheme, +} from '@material-ui/core'; + +import ReplayIcon from '@material-ui/icons/Replay'; +import DescriptionIcon from '@material-ui/icons/Description'; +import ShuffleIcon from '@material-ui/icons/Shuffle'; +import MapIcon from '@material-ui/icons/Map'; +import LogoutIcon from '@material-ui/icons/ExitToApp'; + +import { sessionActions } from '../store'; +import t from '../common/localization'; + +const useStyles = makeStyles((theme) => ({ + container: { + bottom: theme.spacing(0), + left: '0px', + width: '100%', + position: 'fixed', + zIndex: 1301, + [theme.breakpoints.up('lg')]: { + left: theme.spacing(1.5), + bottom: theme.spacing(1.5), + width: theme.dimensions.drawerWidthDesktop, + }, + }, + paper: { + borderRadius: '0px', + }, + toolbar: { + padding: theme.spacing(0, 2), + display: 'flex', + justifyContent: 'space-around', + maxWidth: theme.dimensions.bottomNavMaxWidth, + margin: 'auto', + }, + navItem: { + display: 'flex', + flexDirection: 'column', + fontSize: '0.75rem', + fontWeight: 'normal', + }, +})); + +const BottomNav = ({ showOnDesktop }) => { + const classes = useStyles(); + const theme = useTheme(); + const history = useHistory(); + + const isDesktop = useMediaQuery(theme.breakpoints.up('lg')); + const dispatch = useDispatch(); + + const NavLink = ({ children, location }) => ( + <IconButton component={Link} classes={{ label: classes.navItem }} to={location}> + {children} + </IconButton> + ); + + const handleLogout = async () => { + const response = await fetch('/api/session', { method: 'DELETE' }); + if (response.ok) { + dispatch(sessionActions.updateUser(null)); + history.push('/login'); + } + }; + + if (isDesktop && !showOnDesktop) return null; + + return ( + <div className={classes.container}> + <Paper className={classes.paper} elevation={isDesktop ? 1 : 3}> + <Toolbar className={classes.toolbar} disableGutters> + + {isDesktop ? ( + <NavLink location="/replay"> + <ReplayIcon /> + {t('reportReplay')} + </NavLink> + ) : ( + <NavLink location="/"> + <MapIcon /> + {t('mapTitle')} + </NavLink> + )} + + <NavLink location="/reports/route"> + <DescriptionIcon /> + {t('reportTitle')} + </NavLink> + + <NavLink location="/settings/notifications"> + <ShuffleIcon /> + {t('settingsTitle')} + </NavLink> + + <IconButton classes={{ label: classes.navItem }} onClick={handleLogout}> + <LogoutIcon /> + {t('loginLogout')} + </IconButton> + </Toolbar> + </Paper> + </div> + ); +}; + +export default BottomNav; diff --git a/modern/src/theme/dimensions.js b/modern/src/theme/dimensions.js index fcdbaee5..b6edc5e9 100644 --- a/modern/src/theme/dimensions.js +++ b/modern/src/theme/dimensions.js @@ -9,4 +9,5 @@ export default { columnWidthNumber: 130, columnWidthString: 160, columnWidthBoolean: 130, + bottomNavMaxWidth: '400px', }; |