aboutsummaryrefslogtreecommitdiff
path: root/modern
diff options
context:
space:
mode:
authorAnton Tananaev <anton@traccar.org>2022-05-01 15:26:08 -0700
committerAnton Tananaev <anton@traccar.org>2022-05-01 15:26:08 -0700
commitaf6b60f85ba09a9d1a258cf9d1b6b61d4b8e4fe5 (patch)
tree6fe30c87a48ad5a438554f8cb594028b49efcfef /modern
parent90f292b7739835202842d88eeaf55a531d29d3c3 (diff)
downloadtrackermap-web-af6b60f85ba09a9d1a258cf9d1b6b61d4b8e4fe5.tar.gz
trackermap-web-af6b60f85ba09a9d1a258cf9d1b6b61d4b8e4fe5.tar.bz2
trackermap-web-af6b60f85ba09a9d1a258cf9d1b6b61d4b8e4fe5.zip
Migrate to a card for popup
Diffstat (limited to 'modern')
-rw-r--r--modern/src/App.js2
-rw-r--r--modern/src/DevicesList.js2
-rw-r--r--modern/src/GeofencesList.js2
-rw-r--r--modern/src/MainPage.js29
-rw-r--r--modern/src/map/PositionsMap.js47
-rw-r--r--modern/src/map/StatusCard.js136
-rw-r--r--modern/src/map/StatusView.js140
-rw-r--r--modern/src/store/devices.js2
-rw-r--r--modern/src/theme/dimensions.js1
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,