diff options
author | Anton Tananaev <anton@traccar.org> | 2022-07-03 11:00:47 -0700 |
---|---|---|
committer | Anton Tananaev <anton@traccar.org> | 2022-07-03 11:00:47 -0700 |
commit | 0fb931d6b2193891eb8cefd002ef941eb6daed75 (patch) | |
tree | 18f966fb2cc04b75f10305565de968f68d6036bb | |
parent | 0ab655eb5c031e1a2aac18ef475aff6c7724b227 (diff) | |
download | trackermap-web-0fb931d6b2193891eb8cefd002ef941eb6daed75.tar.gz trackermap-web-0fb931d6b2193891eb8cefd002ef941eb6daed75.tar.bz2 trackermap-web-0fb931d6b2193891eb8cefd002ef941eb6daed75.zip |
Add simple events drawer
-rw-r--r-- | modern/src/SocketController.js | 2 | ||||
-rw-r--r-- | modern/src/common/theme/dimensions.js | 1 | ||||
-rw-r--r-- | modern/src/main/EventsDrawer.js | 55 | ||||
-rw-r--r-- | modern/src/main/MainPage.js | 18 | ||||
-rw-r--r-- | modern/src/map/notification/MapNotification.js | 29 | ||||
-rw-r--r-- | modern/src/map/notification/notification.css | 7 | ||||
-rw-r--r-- | modern/src/store/events.js | 22 | ||||
-rw-r--r-- | modern/src/store/index.js | 3 |
8 files changed, 122 insertions, 15 deletions
diff --git a/modern/src/SocketController.js b/modern/src/SocketController.js index 79661353..bfc0378d 100644 --- a/modern/src/SocketController.js +++ b/modern/src/SocketController.js @@ -10,6 +10,7 @@ import { snackBarDurationLongMs } from './common/util/duration'; import usePersistedState from './common/util/usePersistedState'; import alarm from './resources/alarm.mp3'; +import { eventsActions } from './store/events'; const SocketController = () => { const dispatch = useDispatch(); @@ -60,6 +61,7 @@ const SocketController = () => { dispatch(positionsActions.update(data.positions)); } if (data.events) { + dispatch(eventsActions.add(data.events)); setEvents(data.events); } }; diff --git a/modern/src/common/theme/dimensions.js b/modern/src/common/theme/dimensions.js index c5974ec3..2e8d7e5d 100644 --- a/modern/src/common/theme/dimensions.js +++ b/modern/src/common/theme/dimensions.js @@ -5,6 +5,7 @@ export default { drawerWidthTablet: '320px', drawerHeightPhone: '250px', filterFormWidth: '160px', + eventsDrawerWidth: '320px', bottomBarHeight: 56, popupMapOffset: 300, popupMaxWidth: 272, diff --git a/modern/src/main/EventsDrawer.js b/modern/src/main/EventsDrawer.js new file mode 100644 index 00000000..91d6e3db --- /dev/null +++ b/modern/src/main/EventsDrawer.js @@ -0,0 +1,55 @@ +import React from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + Drawer, IconButton, List, ListItem, ListItemText, Toolbar, Typography, +} from '@mui/material'; +import { makeStyles } from '@mui/styles'; +import DeleteIcon from '@mui/icons-material/Delete'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import { eventsActions } from '../store'; + +const useStyles = makeStyles((theme) => ({ + drawer: { + width: theme.dimensions.eventsDrawerWidth, + }, + header: { + flexGrow: 1, + }, +})); + +const EventsDrawer = ({ open, onClose }) => { + const classes = useStyles(); + const dispatch = useDispatch(); + const t = useTranslation(); + + const events = useSelector((state) => state.events.items); + + return ( + <Drawer + anchor="right" + open={open} + onClose={onClose} + > + <Toolbar> + <Typography variant="h6" className={classes.header}> + {t('reportEvents')} + </Typography> + <IconButton color="inherit" edge="end" onClick={() => dispatch(eventsActions.deleteAll())}> + <DeleteIcon /> + </IconButton> + </Toolbar> + <List className={classes.drawer} dense> + {events.map((event) => ( + <ListItem key={event.id}> + <ListItemText primary={event.attributes.message} /> + <IconButton size="small" onClick={() => dispatch(eventsActions.delete(event))}> + <DeleteIcon fontSize="small" className={classes.negative} /> + </IconButton> + </ListItem> + ))} + </List> + </Drawer> + ); +}; + +export default EventsDrawer; diff --git a/modern/src/main/MainPage.js b/modern/src/main/MainPage.js index e322dca0..31248271 100644 --- a/modern/src/main/MainPage.js +++ b/modern/src/main/MainPage.js @@ -37,6 +37,7 @@ import MapOverlay from '../map/overlay/MapOverlay'; import MapGeocoder from '../map/geocoder/MapGeocoder'; import MapScale from '../map/MapScale'; import MapNotification from '../map/notification/MapNotification'; +import EventsDrawer from './EventsDrawer'; const useStyles = makeStyles((theme) => ({ root: { @@ -164,17 +165,21 @@ const MainPage = () => { const filterRef = useRef(); const [filterAnchorEl, setFilterAnchorEl] = useState(null); - const [collapsed, setCollapsed] = useState(false); + const [devicesOpen, setDevicesOpen] = useState(false); + const [eventsOpen, setEventsOpen] = useState(false); + + const eventHandler = useCallback(() => setEventsOpen(true), [setEventsOpen]); + const eventsAvailable = useSelector((state) => !!state.events.items.length); const handleClose = () => { - setCollapsed(!collapsed); + setDevicesOpen(!devicesOpen); }; - useEffect(() => setCollapsed(!desktop), [desktop]); + useEffect(() => setDevicesOpen(desktop), [desktop]); useEffect(() => { if (!desktop && mapMapOnSelect && selectedDeviceId) { - setCollapsed(true); + setDevicesOpen(false); } }, [desktop, mapMapOnSelect, selectedDeviceId]); @@ -218,7 +223,7 @@ const MainPage = () => { <MapScale /> <MapCurrentLocation /> <MapGeocoder /> - <MapNotification /> + <MapNotification enabled={eventsAvailable} onClick={eventHandler} /> {desktop && <MapPadding left={parseInt(theme.dimensions.drawerWidthDesktop, 10)} />} <Button variant="contained" @@ -231,7 +236,7 @@ const MainPage = () => { <ListIcon /> <div className={classes.sidebarToggleText}>{t('deviceTitle')}</div> </Button> - <Paper square elevation={3} className={`${classes.sidebar} ${collapsed && classes.sidebarCollapsed}`}> + <Paper square elevation={3} className={`${classes.sidebar} ${!devicesOpen && classes.sidebarCollapsed}`}> <Paper square elevation={3} className={classes.toolbarContainer}> <Toolbar className={classes.toolbar} disableGutters> {!desktop && ( @@ -329,6 +334,7 @@ const MainPage = () => { <BottomMenu /> </div> )} + <EventsDrawer open={eventsOpen} onClose={() => setEventsOpen(false)} /> {selectedDeviceId && ( <div className={classes.statusCard}> <StatusCard diff --git a/modern/src/map/notification/MapNotification.js b/modern/src/map/notification/MapNotification.js index 5be80fac..81038f95 100644 --- a/modern/src/map/notification/MapNotification.js +++ b/modern/src/map/notification/MapNotification.js @@ -1,21 +1,23 @@ -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { map } from '../core/MapView'; import './notification.css'; +const statusClass = (status) => `maplibregl-ctrl-icon maplibre-ctrl-notification maplibre-ctrl-notification-${status}`; + class NotificationControl { constructor(eventHandler) { this.eventHandler = eventHandler; } onAdd() { - const button = document.createElement('button'); - button.className = 'maplibregl-ctrl-icon maplibre-ctrl-notification'; - button.type = 'button'; - button.onclick = this.eventHandler; + this.button = document.createElement('button'); + this.button.className = statusClass('off'); + this.button.type = 'button'; + this.button.onclick = () => this.eventHandler(this); this.container = document.createElement('div'); this.container.className = 'maplibregl-ctrl-group maplibregl-ctrl'; - this.container.appendChild(button); + this.container.appendChild(this.button); return this.container; } @@ -23,14 +25,23 @@ class NotificationControl { onRemove() { this.container.parentNode.removeChild(this.container); } + + setEnabled(enabled) { + this.button.className = statusClass(enabled ? 'on' : 'off'); + } } -const MapNotification = () => { +const MapNotification = ({ enabled, onClick }) => { + const control = useMemo(() => new NotificationControl(onClick), [onClick]); + useEffect(() => { - const control = new NotificationControl(() => {}); map.addControl(control); return () => map.removeControl(control); - }, []); + }, [onClick]); + + useEffect(() => { + control.setEnabled(enabled); + }, [enabled]); return null; }; diff --git a/modern/src/map/notification/notification.css b/modern/src/map/notification/notification.css index b0c82357..73db13bb 100644 --- a/modern/src/map/notification/notification.css +++ b/modern/src/map/notification/notification.css @@ -2,5 +2,12 @@ background-repeat: no-repeat; background-position: center; pointer-events: auto; +} + +.maplibre-ctrl-notification-on { + background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' version='1.1' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23f44336' d='m6.4728 3.4802-1.1412-1.1412c-1.9152 1.4604-3.1761 3.7108-3.2878 6.2645h1.596c0.1197-2.1148 1.205-3.9662 2.833-5.1233zm9.8875 5.1233h1.596c-0.1197-2.5537-1.3806-4.8041-3.2878-6.2645l-1.1332 1.1412c1.612 1.1571 2.7053 3.0085 2.825 5.1233zm-1.5721 0.39901c0-2.4499-1.3088-4.5008-3.5911-5.0435v-0.54265c0-0.66236-0.53467-1.197-1.197-1.197-0.66236 0-1.197 0.53467-1.197 1.197v0.54265c-2.2903 0.54265-3.5911 2.5856-3.5911 5.0435v3.9901l-1.596 1.596v0.79802h12.768v-0.79802l-1.596-1.596zm-4.7881 8.7782c0.11172 0 0.21546-8e-3 0.31921-0.03192 0.51871-0.11172 0.94166-0.46285 1.1491-0.94166 0.0798-0.19152 0.1197-0.39901 0.1197-0.62246h-3.1921c0.00798 0.87782 0.71822 1.596 1.604 1.596z' stroke-width='.79802'/%3E%3C/svg%3E"); +} + +.maplibre-ctrl-notification-off { background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' version='1.1' viewBox='0 0 20 20' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m6.4728 3.4802-1.1412-1.1412c-1.9152 1.4604-3.1761 3.7108-3.2878 6.2645h1.596c0.1197-2.1148 1.205-3.9662 2.833-5.1233zm9.8875 5.1233h1.596c-0.1197-2.5537-1.3806-4.8041-3.2878-6.2645l-1.1332 1.1412c1.612 1.1571 2.7053 3.0085 2.825 5.1233zm-1.5721 0.39901c0-2.4499-1.3088-4.5008-3.5911-5.0435v-0.54265c0-0.66236-0.53467-1.197-1.197-1.197-0.66236 0-1.197 0.53467-1.197 1.197v0.54265c-2.2903 0.54265-3.5911 2.5856-3.5911 5.0435v3.9901l-1.596 1.596v0.79802h12.768v-0.79802l-1.596-1.596zm-4.7881 8.7782c0.11172 0 0.21546-8e-3 0.31921-0.03192 0.51871-0.11172 0.94166-0.46285 1.1491-0.94166 0.0798-0.19152 0.1197-0.39901 0.1197-0.62246h-3.1921c0.00798 0.87782 0.71822 1.596 1.604 1.596z' stroke-width='.79802'/%3E%3C/svg%3E"); } diff --git a/modern/src/store/events.js b/modern/src/store/events.js new file mode 100644 index 00000000..d4f313c0 --- /dev/null +++ b/modern/src/store/events.js @@ -0,0 +1,22 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const { reducer, actions } = createSlice({ + name: 'events', + initialState: { + items: [], + }, + reducers: { + add(state, action) { + state.items.unshift(...action.payload); + }, + delete(state, action) { + state.items = state.items.filter((item) => item.id !== action.payload.id); + }, + deleteAll(state) { + state.items = []; + }, + }, +}); + +export { actions as eventsActions }; +export { reducer as eventsReducer }; diff --git a/modern/src/store/index.js b/modern/src/store/index.js index 2ab0c982..9fce43cf 100644 --- a/modern/src/store/index.js +++ b/modern/src/store/index.js @@ -4,6 +4,7 @@ import { errorsReducer as errors } from './errors'; import { sessionReducer as session } from './session'; import { devicesReducer as devices } from './devices'; import { positionsReducer as positions } from './positions'; +import { eventsReducer as events } from './events'; import { geofencesReducer as geofences } from './geofences'; import { groupsReducer as groups } from './groups'; import { driversReducer as drivers } from './drivers'; @@ -15,6 +16,7 @@ const reducer = combineReducers({ session, devices, positions, + events, geofences, groups, drivers, @@ -25,6 +27,7 @@ export { errorsActions } from './errors'; export { sessionActions } from './session'; export { devicesActions } from './devices'; export { positionsActions } from './positions'; +export { eventsActions } from './events'; export { geofencesActions } from './geofences'; export { groupsActions } from './groups'; export { driversActions } from './drivers'; |