diff options
author | Anton Tananaev <anton@traccar.org> | 2022-05-01 15:26:08 -0700 |
---|---|---|
committer | Anton Tananaev <anton@traccar.org> | 2022-05-01 15:26:08 -0700 |
commit | af6b60f85ba09a9d1a258cf9d1b6b61d4b8e4fe5 (patch) | |
tree | 6fe30c87a48ad5a438554f8cb594028b49efcfef | |
parent | 90f292b7739835202842d88eeaf55a531d29d3c3 (diff) | |
download | trackermap-web-af6b60f85ba09a9d1a258cf9d1b6b61d4b8e4fe5.tar.gz trackermap-web-af6b60f85ba09a9d1a258cf9d1b6b61d4b8e4fe5.tar.bz2 trackermap-web-af6b60f85ba09a9d1a258cf9d1b6b61d4b8e4fe5.zip |
Migrate to a card for popup
-rw-r--r-- | modern/src/App.js | 2 | ||||
-rw-r--r-- | modern/src/DevicesList.js | 2 | ||||
-rw-r--r-- | modern/src/GeofencesList.js | 2 | ||||
-rw-r--r-- | modern/src/MainPage.js | 29 | ||||
-rw-r--r-- | modern/src/map/PositionsMap.js | 47 | ||||
-rw-r--r-- | modern/src/map/StatusCard.js | 136 | ||||
-rw-r--r-- | modern/src/map/StatusView.js | 140 | ||||
-rw-r--r-- | modern/src/store/devices.js | 2 | ||||
-rw-r--r-- | modern/src/theme/dimensions.js | 1 |
9 files changed, 174 insertions, 187 deletions
diff --git a/modern/src/App.js b/modern/src/App.js index 1d7b2cae..e12a2e78 100644 --- a/modern/src/App.js +++ b/modern/src/App.js @@ -68,7 +68,7 @@ const App = () => { if (response.ok) { const items = await response.json(); if (items.length > 0) { - dispatch(devicesActions.select(items[0])); + dispatch(devicesActions.select(items[0].id)); } } history.push('/'); diff --git a/modern/src/DevicesList.js b/modern/src/DevicesList.js index bcd6786a..5b7a2db9 100644 --- a/modern/src/DevicesList.js +++ b/modern/src/DevicesList.js @@ -80,7 +80,7 @@ const DeviceRow = ({ data, index, style }) => { return ( <div style={style}> - <ListItem button key={item.id} className={classes.listItem} onClick={() => dispatch(devicesActions.select(item))}> + <ListItem button key={item.id} className={classes.listItem} onClick={() => dispatch(devicesActions.select(item.id))}> <ListItemAvatar> <Avatar> <img className={classes.icon} src={`images/icon/${item.category || 'default'}.svg`} alt="" /> diff --git a/modern/src/GeofencesList.js b/modern/src/GeofencesList.js index 705bdcc3..1883edf3 100644 --- a/modern/src/GeofencesList.js +++ b/modern/src/GeofencesList.js @@ -34,7 +34,7 @@ const GeofenceView = ({ onMenuClick }) => { <List className={classes.list}> {Object.values(items).map((item, index, list) => ( <Fragment key={item.id}> - <ListItem button key={item.id} onClick={() => dispatch(devicesActions.select(item))}> + <ListItem button key={item.id} onClick={() => dispatch(devicesActions.select(item.id))}> <ListItemText primary={item.name} /> <ListItemSecondaryAction> <IconButton onClick={(event) => onMenuClick(event.currentTarget, item.id)}> diff --git a/modern/src/MainPage.js b/modern/src/MainPage.js index e504ff29..82ed3756 100644 --- a/modern/src/MainPage.js +++ b/modern/src/MainPage.js @@ -22,6 +22,9 @@ import BottomMenu from './components/BottomMenu'; import { useTranslation } from './LocalizationProvider'; import PoiMap from './map/PoiMap'; import MapPadding from './map/MapPadding'; +import StatusCard from './map/StatusCard'; +import { useDispatch, useSelector } from 'react-redux'; +import { devicesActions } from './store'; const useStyles = makeStyles((theme) => ({ root: { @@ -35,8 +38,7 @@ const useStyles = makeStyles((theme) => ({ top: 0, margin: theme.spacing(1.5), width: theme.dimensions.drawerWidthDesktop, - bottom: 56, - zIndex: 1301, + bottom: theme.dimensions.bottomBarHeight, transition: 'transform .5s ease', backgroundColor: 'white', [theme.breakpoints.down('md')]: { @@ -64,6 +66,18 @@ const useStyles = makeStyles((theme) => ({ deviceList: { flex: 1, }, + statusCard: { + position: 'fixed', + [theme.breakpoints.up('md')]: { + left: `calc(50% + ${theme.dimensions.drawerWidthDesktop} / 2)`, + bottom: theme.spacing(3), + }, + [theme.breakpoints.down('md')]: { + left: '50%', + bottom: theme.spacing(3) + theme.dimensions.bottomBarHeight, + }, + transform: 'translateX(-50%)', + }, sidebarToggle: { position: 'absolute', left: theme.spacing(1.5), @@ -92,12 +106,15 @@ const useStyles = makeStyles((theme) => ({ const MainPage = () => { const classes = useStyles(); const history = useHistory(); + const dispatch = useDispatch(); const theme = useTheme(); const t = useTranslation(); const isTablet = useMediaQuery(theme.breakpoints.down('md')); const isPhone = useMediaQuery(theme.breakpoints.down('xs')); + const selectedDeviceId = useSelector((state) => state.devices.selectedId); + const [searchKeyword, setSearchKeyword] = useState(''); const [collapsed, setCollapsed] = useState(false); @@ -162,6 +179,14 @@ const MainPage = () => { </div> </Paper> <BottomMenu /> + {selectedDeviceId && + <div className={classes.statusCard}> + <StatusCard + deviceId={selectedDeviceId} + onClose={() => dispatch(devicesActions.select(null))} + /> + </div> + } </div> ); }; diff --git a/modern/src/map/PositionsMap.js b/modern/src/map/PositionsMap.js index d82347f1..033ec760 100644 --- a/modern/src/map/PositionsMap.js +++ b/modern/src/map/PositionsMap.js @@ -1,22 +1,15 @@ -import React, { useCallback, useEffect } from 'react'; -import ReactDOM from 'react-dom'; -import { ThemeProvider } from '@material-ui/core/styles'; -import maplibregl from 'maplibre-gl'; -import { Provider, useSelector } from 'react-redux'; +import { useCallback, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; import { map } from './Map'; -import store from '../store'; -import StatusView from './StatusView'; -import theme from '../theme'; import { getStatusColor } from '../common/formatter'; -import { LocalizationProvider } from '../LocalizationProvider'; +import { devicesActions } from '../store'; const PositionsMap = ({ positions }) => { const id = 'positions'; const clusters = `${id}-clusters`; - const history = useHistory(); + const dispatch = useDispatch(); const devices = useSelector((state) => state.devices.items); const createFeature = (devices, position) => { @@ -34,36 +27,8 @@ const PositionsMap = ({ positions }) => { const onMarkerClick = useCallback((event) => { const feature = event.features[0]; - const coordinates = feature.geometry.coordinates.slice(); - while (Math.abs(event.lngLat.lng - coordinates[0]) > 180) { - coordinates[0] += event.lngLat.lng > coordinates[0] ? 360 : -360; - } - - const placeholder = document.createElement('div'); - ReactDOM.render( - <Provider store={store}> - <LocalizationProvider> - <ThemeProvider theme={theme}> - <StatusView - deviceId={feature.properties.deviceId} - onShowDetails={(positionId) => history.push(`/position/${positionId}`)} - onShowHistory={() => history.push('/replay')} - onEditClick={(deviceId) => history.push(`/device/${deviceId}`)} - /> - </ThemeProvider> - </LocalizationProvider> - </Provider>, - placeholder, - ); - - new maplibregl.Popup({ - offset: 25, - anchor: 'top', - }) - .setDOMContent(placeholder) - .setLngLat(coordinates) - .addTo(map); - }, [history]); + dispatch(devicesActions.select(feature.properties.deviceId)); + }, [dispatch]); const onClusterClick = useCallback((event) => { const features = map.queryRenderedFeatures(event.point, { diff --git a/modern/src/map/StatusCard.js b/modern/src/map/StatusCard.js new file mode 100644 index 00000000..47f7724d --- /dev/null +++ b/modern/src/map/StatusCard.js @@ -0,0 +1,136 @@ +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; +import { + makeStyles, Button, Card, CardContent, Typography, CardActions, CardHeader, IconButton, Avatar, Table, TableBody, TableRow, TableCell, TableContainer, +} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import ReplayIcon from '@material-ui/icons/Replay'; +import ExitToAppIcon from '@material-ui/icons/ExitToApp'; +import EditIcon from '@material-ui/icons/Edit'; +import DeleteIcon from '@material-ui/icons/Delete'; + +import { useTranslation } from '../LocalizationProvider'; +import { formatDistance, formatPosition, formatSpeed, formatStatus } from '../common/formatter'; +import RemoveDialog from '../RemoveDialog'; +import { useAttributePreference } from '../common/preferences'; + +const useStyles = makeStyles((theme) => ({ + paper: { + width: '300px', + }, + negative: { + color: theme.palette.colors.negative, + }, + listItemContainer: { + maxWidth: '240px', + }, + icon: { + width: '25px', + height: '25px', + filter: 'brightness(0) invert(1)', + }, + table: { + '& .MuiTableCell-sizeSmall': { + paddingLeft: theme.spacing(0.5), + paddingRight: theme.spacing(0.5), + }, + }, + cell: { + borderBottom: 'none', + }, +})); + +const StatusRow = ({ name, value }) => { + const classes = useStyles(); + + return ( + <TableRow> + <TableCell className={classes.cell}> + <Typography variant="body2">{name}</Typography> + </TableCell> + <TableCell className={classes.cell}> + <Typography variant="body2" color="textSecondary">{value}</Typography> + </TableCell> + </TableRow> + ); +} + +const StatusCard = ({ deviceId, onClose }) => { + const classes = useStyles(); + const history = useHistory(); + const t = useTranslation(); + + const device = useSelector((state) => state.devices.items[deviceId]); + const position = useSelector((state) => state.positions.items[deviceId]); + + const distanceUnit = useAttributePreference('distanceUnit'); + const speedUnit = useAttributePreference('speedUnit'); + + const [removeDialogShown, setRemoveDialogShown] = useState(false); + + return ( + <> + {device && + <Card> + <CardHeader + avatar={ + <Avatar> + <img className={classes.icon} src={`images/icon/${device.category || 'default'}.svg`} alt="" /> + </Avatar> + } + action={ + <IconButton onClick={onClose}> + <CloseIcon /> + </IconButton> + } + title={device.name} + subheader={formatStatus(device.status, t)} + /> + {position && + <CardContent> + <TableContainer> + <Table size="small" classes={{ root: classes.table }}> + <TableBody> + <StatusRow name={t('positionSpeed')} value={formatSpeed(position.speed, speedUnit, t)} /> + <StatusRow name={t('positionBattery')} value={formatSpeed(position.speed, speedUnit, t)} /> + {position.attributes.odometer + ? <StatusRow name={t('positionOdometer')} value={formatDistance(position.attributes.odometer, distanceUnit, t)} /> + : <StatusRow name={t('deviceTotalDistance')} value={formatDistance(position.attributes.totalDistance, distanceUnit, t)} /> + } + <StatusRow name={t('positionCourse')} value={formatPosition(position.course, 'course', t)} /> + </TableBody> + </Table> + </TableContainer> + </CardContent> + } + <CardActions disableSpacing> + <Button onClick={() => history.push(`/position/${position.id}`)} disabled={!position} color="secondary"> + {t('sharedInfoTitle')} + </Button> + <IconButton onClick={() => history.push('/replay')} disabled={!position}> + <ReplayIcon /> + </IconButton> + <IconButton> + <ExitToAppIcon /> + </IconButton> + <IconButton onClick={() => history.push(`/device/${deviceId}`)}> + <EditIcon /> + </IconButton> + <IconButton onClick={() => setRemoveDialogShown(true)} className={classes.negative}> + <DeleteIcon /> + </IconButton> + </CardActions> + </Card> + } + <RemoveDialog + open={removeDialogShown} + endpoint="devices" + itemId={deviceId} + onResult={() => setRemoveDialogShown(false)} + /> + </> + ); +}; + +export default StatusCard; diff --git a/modern/src/map/StatusView.js b/modern/src/map/StatusView.js deleted file mode 100644 index 2c3a7568..00000000 --- a/modern/src/map/StatusView.js +++ /dev/null @@ -1,140 +0,0 @@ -import React, { useState } from 'react'; -import { - makeStyles, Paper, IconButton, Grid, Button, -} from '@material-ui/core'; -import List from '@material-ui/core/List'; -import ListItem from '@material-ui/core/ListItem'; -import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; -import ListItemText from '@material-ui/core/ListItemText'; -import { useSelector } from 'react-redux'; - -import ReplayIcon from '@material-ui/icons/Replay'; -import ExitToAppIcon from '@material-ui/icons/ExitToApp'; -import EditIcon from '@material-ui/icons/Edit'; -import DeleteIcon from '@material-ui/icons/Delete'; - -import { - formatPosition, getStatusColor, getBatteryStatus, formatDistance, formatSpeed, formatStatus, -} from '../common/formatter'; -import { useAttributePreference } from '../common/preferences'; -import RemoveDialog from '../RemoveDialog'; -import { useTranslation } from '../LocalizationProvider'; - -const useStyles = makeStyles((theme) => ({ - paper: { - width: '300px', - }, - negative: { - color: theme.palette.colors.negative, - }, - listItemContainer: { - maxWidth: '240px', - }, -})); - -const StatusView = ({ - deviceId, onShowDetails, onShowHistory, onEditClick, -}) => { - const classes = useStyles(); - const t = useTranslation(); - - const [removeDialogShown, setRemoveDialogShown] = useState(false); - const device = useSelector((state) => state.devices.items[deviceId]); - const position = useSelector((state) => state.positions.items[deviceId]); - - const distanceUnit = useAttributePreference('distanceUnit'); - const speedUnit = useAttributePreference('speedUnit'); - - const handleClick = (e) => { - e.preventDefault(); - onShowDetails(position.id); - }; - - const handleEditClick = (e) => { - e.preventDefault(); - onEditClick(deviceId); - }; - - const handleRemove = () => { - setRemoveDialogShown(true); - }; - - const handleRemoveResult = () => { - setRemoveDialogShown(false); - }; - - return ( - <> - <Paper className={classes.paper} elevation={0} square> - <Grid container direction="column"> - <Grid item> - <List> - <ListItem classes={{ container: classes.listItemContainer }}> - <ListItemText primary={t('deviceStatus')} /> - <ListItemSecondaryAction> - <span className={classes[getStatusColor(device.status)]}>{formatStatus(device.status, t)}</span> - </ListItemSecondaryAction> - </ListItem> - <ListItem classes={{ container: classes.listItemContainer }}> - <ListItemText primary={t('positionSpeed')} /> - <ListItemSecondaryAction> - {formatSpeed(position.speed, speedUnit, t)} - </ListItemSecondaryAction> - </ListItem> - {position.attributes.batteryLevel && ( - <ListItem classes={{ container: classes.listItemContainer }}> - <ListItemText primary={t('positionBattery')} /> - <ListItemSecondaryAction> - <span className={classes[getBatteryStatus(position.attributes.batteryLevel)]}> - {formatPosition(position.attributes.batteryLevel, 'batteryLevel', t)} - </span> - </ListItemSecondaryAction> - </ListItem> - )} - <ListItem classes={{ container: classes.listItemContainer }}> - <ListItemText primary={t('positionDistance')} /> - <ListItemSecondaryAction> - {formatDistance(position.attributes.totalDistance, distanceUnit, t)} - </ListItemSecondaryAction> - </ListItem> - <ListItem classes={{ container: classes.listItemContainer }}> - <ListItemText primary={t('positionCourse')} /> - <ListItemSecondaryAction> - {formatPosition(position.course, 'course', t)} - </ListItemSecondaryAction> - </ListItem> - </List> - </Grid> - <Grid item container> - <Grid item> - <Button color="secondary" onClick={handleClick}>{t('sharedInfoTitle')}</Button> - </Grid> - <Grid item> - <IconButton onClick={onShowHistory}> - <ReplayIcon /> - </IconButton> - </Grid> - <Grid item> - <IconButton> - <ExitToAppIcon /> - </IconButton> - </Grid> - <Grid item> - <IconButton onClick={handleEditClick}> - <EditIcon /> - </IconButton> - </Grid> - <Grid item> - <IconButton onClick={handleRemove} className={classes.negative}> - <DeleteIcon /> - </IconButton> - </Grid> - </Grid> - </Grid> - </Paper> - <RemoveDialog open={removeDialogShown} endpoint="devices" itemId={deviceId} onResult={handleRemoveResult} /> - </> - ); -}; - -export default StatusView; diff --git a/modern/src/store/devices.js b/modern/src/store/devices.js index cca23cb9..9cfde0a8 100644 --- a/modern/src/store/devices.js +++ b/modern/src/store/devices.js @@ -15,7 +15,7 @@ const { reducer, actions } = createSlice({ action.payload.forEach((item) => state.items[item.id] = item); }, select(state, action) { - state.selectedId = action.payload.id; + state.selectedId = action.payload; }, remove(state, action) { delete state.items[action.payload]; diff --git a/modern/src/theme/dimensions.js b/modern/src/theme/dimensions.js index fcdbaee5..e852f0b4 100644 --- a/modern/src/theme/dimensions.js +++ b/modern/src/theme/dimensions.js @@ -5,6 +5,7 @@ export default { sidebarWidthTablet: '52px', drawerWidthDesktop: '360px', drawerWidthTablet: '320px', + bottomBarHeight: 56, columnWidthDate: 160, columnWidthNumber: 130, columnWidthString: 160, |