diff options
author | Anton Tananaev <anton@traccar.org> | 2024-04-06 09:22:10 -0700 |
---|---|---|
committer | Anton Tananaev <anton@traccar.org> | 2024-04-06 09:22:10 -0700 |
commit | f418231b6b2f5e030a0d2dcc390c314602b1f740 (patch) | |
tree | 10326adf3792bc2697e06bb5f2b8ef2a8f7e55fe /src/common/components/StatusCard.jsx | |
parent | b392a4af78e01c8e0f50aad5468e9583675b24be (diff) | |
download | trackermap-web-f418231b6b2f5e030a0d2dcc390c314602b1f740.tar.gz trackermap-web-f418231b6b2f5e030a0d2dcc390c314602b1f740.tar.bz2 trackermap-web-f418231b6b2f5e030a0d2dcc390c314602b1f740.zip |
Move modern to the top
Diffstat (limited to 'src/common/components/StatusCard.jsx')
-rw-r--r-- | src/common/components/StatusCard.jsx | 288 |
1 files changed, 288 insertions, 0 deletions
diff --git a/src/common/components/StatusCard.jsx b/src/common/components/StatusCard.jsx new file mode 100644 index 00000000..a63d0f80 --- /dev/null +++ b/src/common/components/StatusCard.jsx @@ -0,0 +1,288 @@ +import React, { useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import Draggable from 'react-draggable'; +import { + Card, + CardContent, + Typography, + CardActions, + IconButton, + Table, + TableBody, + TableRow, + TableCell, + Menu, + MenuItem, + CardMedia, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import CloseIcon from '@mui/icons-material/Close'; +import ReplayIcon from '@mui/icons-material/Replay'; +import PublishIcon from '@mui/icons-material/Publish'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +import PendingIcon from '@mui/icons-material/Pending'; + +import { useTranslation } from './LocalizationProvider'; +import RemoveDialog from './RemoveDialog'; +import PositionValue from './PositionValue'; +import { useDeviceReadonly } from '../util/permissions'; +import usePositionAttributes from '../attributes/usePositionAttributes'; +import { devicesActions } from '../../store'; +import { useCatch, useCatchCallback } from '../../reactHelper'; +import { useAttributePreference } from '../util/preferences'; + +const useStyles = makeStyles((theme) => ({ + card: { + pointerEvents: 'auto', + width: theme.dimensions.popupMaxWidth, + }, + media: { + height: theme.dimensions.popupImageHeight, + display: 'flex', + justifyContent: 'flex-end', + alignItems: 'flex-start', + }, + mediaButton: { + color: theme.palette.primary.contrastText, + mixBlendMode: 'difference', + }, + header: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: theme.spacing(1, 1, 0, 2), + }, + content: { + paddingTop: theme.spacing(1), + paddingBottom: theme.spacing(1), + maxHeight: theme.dimensions.cardContentMaxHeight, + overflow: 'auto', + }, + delete: { + color: theme.palette.error.main, + }, + icon: { + width: '25px', + height: '25px', + filter: 'brightness(0) invert(1)', + }, + table: { + '& .MuiTableCell-sizeSmall': { + paddingLeft: 0, + paddingRight: 0, + }, + }, + cell: { + borderBottom: 'none', + }, + actions: { + justifyContent: 'space-between', + }, + root: ({ desktopPadding }) => ({ + pointerEvents: 'none', + position: 'fixed', + zIndex: 5, + left: '50%', + [theme.breakpoints.up('md')]: { + left: `calc(50% + ${desktopPadding} / 2)`, + bottom: theme.spacing(3), + }, + [theme.breakpoints.down('md')]: { + left: '50%', + bottom: `calc(${theme.spacing(3)} + ${theme.dimensions.bottomBarHeight}px)`, + }, + transform: 'translateX(-50%)', + }), +})); + +const StatusRow = ({ name, content }) => { + const classes = useStyles(); + + return ( + <TableRow> + <TableCell className={classes.cell}> + <Typography variant="body2">{name}</Typography> + </TableCell> + <TableCell className={classes.cell}> + <Typography variant="body2" color="textSecondary">{content}</Typography> + </TableCell> + </TableRow> + ); +}; + +const StatusCard = ({ deviceId, position, onClose, disableActions, desktopPadding = 0 }) => { + const classes = useStyles({ desktopPadding }); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const t = useTranslation(); + + const deviceReadonly = useDeviceReadonly(); + + const shareDisabled = useSelector((state) => state.session.server.attributes.disableShare); + const user = useSelector((state) => state.session.user); + const device = useSelector((state) => state.devices.items[deviceId]); + + const deviceImage = device?.attributes?.deviceImage; + + const positionAttributes = usePositionAttributes(t); + const positionItems = useAttributePreference('positionItems', 'speed,address,totalDistance,course'); + + const [anchorEl, setAnchorEl] = useState(null); + + const [removing, setRemoving] = useState(false); + + const handleRemove = useCatch(async (removed) => { + if (removed) { + const response = await fetch('/api/devices'); + if (response.ok) { + dispatch(devicesActions.refresh(await response.json())); + } else { + throw Error(await response.text()); + } + } + setRemoving(false); + }); + + const handleGeofence = useCatchCallback(async () => { + const newItem = { + name: t('sharedGeofence'), + area: `CIRCLE (${position.latitude} ${position.longitude}, 50)`, + }; + const response = await fetch('/api/geofences', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(newItem), + }); + if (response.ok) { + const item = await response.json(); + const permissionResponse = await fetch('/api/permissions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ deviceId: position.deviceId, geofenceId: item.id }), + }); + if (!permissionResponse.ok) { + throw Error(await permissionResponse.text()); + } + navigate(`/settings/geofence/${item.id}`); + } else { + throw Error(await response.text()); + } + }, [navigate, position]); + + return ( + <> + <div className={classes.root}> + {device && ( + <Draggable + handle={`.${classes.media}, .${classes.header}`} + > + <Card elevation={3} className={classes.card}> + {deviceImage ? ( + <CardMedia + className={classes.media} + image={`/api/media/${device.uniqueId}/${deviceImage}`} + > + <IconButton + size="small" + onClick={onClose} + onTouchStart={onClose} + > + <CloseIcon fontSize="small" className={classes.mediaButton} /> + </IconButton> + </CardMedia> + ) : ( + <div className={classes.header}> + <Typography variant="body2" color="textSecondary"> + {device.name} + </Typography> + <IconButton + size="small" + onClick={onClose} + onTouchStart={onClose} + > + <CloseIcon fontSize="small" /> + </IconButton> + </div> + )} + {position && ( + <CardContent className={classes.content}> + <Table size="small" classes={{ root: classes.table }}> + <TableBody> + {positionItems.split(',').filter((key) => position.hasOwnProperty(key) || position.attributes.hasOwnProperty(key)).map((key) => ( + <StatusRow + key={key} + name={positionAttributes[key]?.name || key} + content={( + <PositionValue + position={position} + property={position.hasOwnProperty(key) ? key : null} + attribute={position.hasOwnProperty(key) ? null : key} + /> + )} + /> + ))} + </TableBody> + </Table> + </CardContent> + )} + <CardActions classes={{ root: classes.actions }} disableSpacing> + <IconButton + color="secondary" + onClick={(e) => setAnchorEl(e.currentTarget)} + disabled={!position} + > + <PendingIcon /> + </IconButton> + <IconButton + onClick={() => navigate('/replay')} + disabled={disableActions || !position} + > + <ReplayIcon /> + </IconButton> + <IconButton + onClick={() => navigate(`/settings/device/${deviceId}/command`)} + disabled={disableActions} + > + <PublishIcon /> + </IconButton> + <IconButton + onClick={() => navigate(`/settings/device/${deviceId}`)} + disabled={disableActions || deviceReadonly} + > + <EditIcon /> + </IconButton> + <IconButton + onClick={() => setRemoving(true)} + disabled={disableActions || deviceReadonly} + className={classes.delete} + > + <DeleteIcon /> + </IconButton> + </CardActions> + </Card> + </Draggable> + )} + </div> + {position && ( + <Menu anchorEl={anchorEl} open={Boolean(anchorEl)} onClose={() => setAnchorEl(null)}> + <MenuItem onClick={() => navigate(`/position/${position.id}`)}><Typography color="secondary">{t('sharedShowDetails')}</Typography></MenuItem> + <MenuItem onClick={handleGeofence}>{t('sharedCreateGeofence')}</MenuItem> + <MenuItem component="a" target="_blank" href={`https://www.google.com/maps/search/?api=1&query=${position.latitude}%2C${position.longitude}`}>{t('linkGoogleMaps')}</MenuItem> + <MenuItem component="a" target="_blank" href={`http://maps.apple.com/?ll=${position.latitude},${position.longitude}`}>{t('linkAppleMaps')}</MenuItem> + <MenuItem component="a" target="_blank" href={`https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=${position.latitude}%2C${position.longitude}&heading=${position.course}`}>{t('linkStreetView')}</MenuItem> + {!shareDisabled && !user.temporary && <MenuItem onClick={() => navigate(`/settings/device/${deviceId}/share`)}>{t('deviceShare')}</MenuItem>} + </Menu> + )} + <RemoveDialog + open={removing} + endpoint="devices" + itemId={deviceId} + onResult={(removed) => handleRemove(removed)} + /> + </> + ); +}; + +export default StatusCard; |