aboutsummaryrefslogtreecommitdiff
path: root/modern/src
diff options
context:
space:
mode:
Diffstat (limited to 'modern/src')
-rw-r--r--modern/src/App.js21
-rw-r--r--modern/src/DeviceList.js52
-rw-r--r--modern/src/LoginPage.js148
-rw-r--r--modern/src/MainMap.js172
-rw-r--r--modern/src/MainPage.js91
-rw-r--r--modern/src/MainToolbar.js123
-rw-r--r--modern/src/SocketController.js61
-rw-r--r--modern/src/actions/index.js19
-rw-r--r--modern/src/index.js21
-rw-r--r--modern/src/reducers/index.js35
-rw-r--r--modern/src/registerServiceWorker.js117
-rw-r--r--modern/src/setupProxy.js6
12 files changed, 866 insertions, 0 deletions
diff --git a/modern/src/App.js b/modern/src/App.js
new file mode 100644
index 0000000..31b8b69
--- /dev/null
+++ b/modern/src/App.js
@@ -0,0 +1,21 @@
+import React, { Component, Fragment } from 'react';
+import { Switch, Route } from 'react-router-dom'
+import CssBaseline from '@material-ui/core/CssBaseline';
+import MainPage from './MainPage';
+import LoginPage from './LoginPage';
+
+class App extends Component {
+ render() {
+ return (
+ <Fragment>
+ <CssBaseline />
+ <Switch>
+ <Route exact path='/' component={MainPage} />
+ <Route exact path='/login' component={LoginPage} />
+ </Switch>
+ </Fragment>
+ );
+ }
+}
+
+export default App;
diff --git a/modern/src/DeviceList.js b/modern/src/DeviceList.js
new file mode 100644
index 0000000..03c5126
--- /dev/null
+++ b/modern/src/DeviceList.js
@@ -0,0 +1,52 @@
+import React, { Component, Fragment } from 'react';
+import { connect } from 'react-redux';
+import List from '@material-ui/core/List';
+import ListItem from '@material-ui/core/ListItem';
+import ListItemAvatar from '@material-ui/core/ListItemAvatar';
+import ListItemText from '@material-ui/core/ListItemText';
+import Avatar from '@material-ui/core/Avatar';
+import LocationOnIcon from '@material-ui/icons/LocationOn';
+import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction';
+import IconButton from '@material-ui/core/IconButton';
+import MoreVertIcon from '@material-ui/icons/MoreVert';
+import Divider from '@material-ui/core/Divider';
+import { selectDevice } from './actions';
+
+const mapStateToProps = state => ({
+ devices: Array.from(state.devices.values())
+});
+
+class DeviceList extends Component {
+ handleClick(device) {
+ this.props.dispatch(selectDevice(device));
+ }
+
+ render() {
+ const devices = this.props.devices.map((device, index, list) =>
+ <Fragment key={device.id.toString()}>
+ <ListItem button onClick={(e) => this.handleClick(device)}>
+ <ListItemAvatar>
+ <Avatar>
+ <LocationOnIcon />
+ </Avatar>
+ </ListItemAvatar>
+ <ListItemText primary={device.name} secondary={device.uniqueId} />
+ <ListItemSecondaryAction>
+ <IconButton>
+ <MoreVertIcon />
+ </IconButton>
+ </ListItemSecondaryAction>
+ </ListItem>
+ {index < list.length - 1 ? <Divider /> : null}
+ </Fragment>
+ );
+
+ return (
+ <List>
+ {devices}
+ </List>
+ );
+ }
+}
+
+export default connect(mapStateToProps)(DeviceList);
diff --git a/modern/src/LoginPage.js b/modern/src/LoginPage.js
new file mode 100644
index 0000000..c094fa3
--- /dev/null
+++ b/modern/src/LoginPage.js
@@ -0,0 +1,148 @@
+import React, { Component } from 'react';
+import Button from '@material-ui/core/Button';
+import FormHelperText from '@material-ui/core/FormHelperText';
+import FormControl from '@material-ui/core/FormControl';
+import Input from '@material-ui/core/Input';
+import InputLabel from '@material-ui/core/InputLabel';
+import Paper from '@material-ui/core/Paper';
+import withStyles from '@material-ui/core/styles/withStyles';
+
+const styles = theme => ({
+ root: {
+ width: 'auto',
+ display: 'block', // Fix IE11 issue.
+ marginLeft: theme.spacing(3),
+ marginRight: theme.spacing(3),
+ [theme.breakpoints.up(400 + theme.spacing(3 * 2))]: {
+ width: 400,
+ marginLeft: 'auto',
+ marginRight: 'auto',
+ },
+ },
+ paper: {
+ marginTop: theme.spacing(8),
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ padding: `${theme.spacing(3)}px`,
+ },
+ logo: {
+ margin: `${theme.spacing(2)}px 0 ${theme.spacing(1)}px`
+ },
+ buttons: {
+ width: '100%',
+ display: 'flex',
+ flexDirection: 'row'
+ },
+ button: {
+ flex: '1 1 0',
+ margin: `${theme.spacing(3)}px ${theme.spacing(1)}px 0`
+ },
+});
+
+class LoginPage extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ filled: false,
+ loading: false,
+ failed: false,
+ email: "",
+ password: ""
+ };
+ this.handleChange = this.handleChange.bind(this);
+ this.handleRegister = this.handleRegister.bind(this);
+ this.handleLogin = this.handleLogin.bind(this);
+ }
+
+ handleChange(event) {
+ this.setState({
+ [event.target.id]: event.target.value
+ });
+ }
+
+ handleRegister() {
+ // TODO implement registration
+ }
+
+ handleLogin(event) {
+ event.preventDefault();
+ const { email, password } = this.state;
+ fetch("/api/session", {
+ method: "POST",
+ body: new URLSearchParams(`email=${email}&password=${password}`)
+ }).then(response => {
+ if (response.ok) {
+ this.props.history.push('/'); // TODO avoid calling sessions twice
+ } else {
+ this.setState({
+ failed: true,
+ password: ""
+ });
+ }
+ });
+ }
+
+ render() {
+ const { classes } = this.props;
+ const { failed, email, password } = this.state;
+ return (
+ <main className={classes.root}>
+ <Paper className={classes.paper}>
+
+ <img className={classes.logo} src="/logo.svg" alt="Traccar" />
+
+ <form onSubmit={this.handleLogin}>
+
+ <FormControl margin="normal" required fullWidth error={failed}>
+ <InputLabel htmlFor="email">Email</InputLabel>
+ <Input
+ id="email"
+ value={email}
+ autoComplete="email"
+ autoFocus
+ onChange={this.handleChange} />
+ { failed && <FormHelperText>Invalid username or password</FormHelperText> }
+ </FormControl>
+
+ <FormControl margin="normal" required fullWidth>
+ <InputLabel htmlFor="password">Password</InputLabel>
+ <Input
+ id="password"
+ type="password"
+ value={password}
+ autoComplete="current-password"
+ onChange={this.handleChange} />
+ </FormControl>
+
+ <div className={classes.buttons}>
+
+ <Button
+ type="button"
+ variant="contained"
+ disabled
+ className={classes.button}
+ onClick={this.handleRegister}>
+ Register
+ </Button>
+
+ <Button
+ type="submit"
+ variant="contained"
+ color="primary"
+ disabled={!email || !password}
+ className={classes.button}>
+ Login
+ </Button>
+
+ </div>
+
+ </form>
+
+ </Paper>
+ </main>
+ );
+ }
+}
+
+export default withStyles(styles)(LoginPage);
diff --git a/modern/src/MainMap.js b/modern/src/MainMap.js
new file mode 100644
index 0000000..35b933b
--- /dev/null
+++ b/modern/src/MainMap.js
@@ -0,0 +1,172 @@
+import 'mapbox-gl/dist/mapbox-gl.css';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import mapboxgl from 'mapbox-gl';
+
+const calculateMapCenter = (state) => {
+ if (state.selectedDevice) {
+ const position = state.positions.get(state.selectedDevice);
+ if (position) {
+ return [position.longitude, position.latitude];
+ }
+ }
+ return null;
+}
+
+const mapFeatureProperties = (state, position) => {
+ return {
+ name: state.devices.get(position.deviceId).name
+ }
+}
+
+const mapStateToProps = state => ({
+ mapCenter: calculateMapCenter(state),
+ data: {
+ type: 'FeatureCollection',
+ features: Array.from(state.positions.values()).map(position => ({
+ type: 'Feature',
+ geometry: {
+ type: 'Point',
+ coordinates: [position.longitude, position.latitude]
+ },
+ properties: mapFeatureProperties(state, position)
+ }))
+ }
+});
+
+class MainMap extends Component {
+ componentDidMount() {
+ const map = new mapboxgl.Map({
+ container: this.mapContainer,
+ style: 'https://cdn.traccar.com/map/basic.json',
+ center: [0, 0],
+ zoom: 1
+ });
+
+ map.on('load', () => this.mapDidLoad(map));
+ }
+
+ loadImage(key, url) {
+ return new Promise(resolutionFunc => {
+ const image = new Image();
+ image.onload = () => {
+ const canvas = document.createElement('canvas');
+ canvas.width = image.width * window.devicePixelRatio;
+ canvas.height = image.height * window.devicePixelRatio;
+ canvas.style.width = `${image.width}px`;
+ canvas.style.height = `${image.height}px`;
+ const context = canvas.getContext('2d');
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
+ this.map.addImage(key, context.getImageData(0, 0, canvas.width, canvas.height), {
+ pixelRatio: window.devicePixelRatio
+ });
+ resolutionFunc()
+ }
+ image.src = url;
+ });
+ }
+
+ mapDidLoad(map) {
+ this.map = map;
+
+ Promise.all([
+ this.loadImage('background', 'images/background.svg'),
+ this.loadImage('icon-marker', 'images/icon/marker.svg')
+ ]).then(() => {
+ this.imagesDidLoad();
+ });
+ }
+
+ imagesDidLoad() {
+ this.map.addSource('positions', {
+ 'type': 'geojson',
+ 'data': this.props.data
+ });
+
+ this.map.addLayer({
+ 'id': 'device-background',
+ 'type': 'symbol',
+ 'source': 'positions',
+ 'layout': {
+ 'icon-image': 'background',
+ 'icon-allow-overlap': true,
+ 'text-field': '{name}',
+ 'text-allow-overlap': true,
+ 'text-anchor': 'bottom',
+ 'text-offset': [0, -2],
+ 'text-font': ['Roboto Regular'],
+ 'text-size': 12
+ },
+ 'paint':{
+ 'text-halo-color': 'white',
+ 'text-halo-width': 1
+ }
+ });
+
+ this.map.addLayer({
+ 'id': 'device-icon',
+ 'type': 'symbol',
+ 'source': 'positions',
+ 'layout': {
+ 'icon-image': 'icon-marker',
+ 'icon-allow-overlap': true
+ }
+ });
+
+ this.map.addControl(new mapboxgl.NavigationControl());
+
+ this.map.fitBounds(this.calculateBounds(), {
+ padding: 100,
+ maxZoom: 9
+ });
+ }
+
+ calculateBounds() {
+ if (this.props.data.features) {
+ const first = this.props.data.features[0].geometry.coordinates;
+ const bounds = [[...first], [...first]];
+ for (let feature of this.props.data.features) {
+ const longitude = feature.geometry.coordinates[0]
+ const latitude = feature.geometry.coordinates[1]
+ if (longitude < bounds[0][0]) {
+ bounds[0][0] = longitude;
+ } else if (longitude > bounds[1][0]) {
+ bounds[1][0] = longitude;
+ }
+ if (latitude < bounds[0][1]) {
+ bounds[0][1] = latitude;
+ } else if (latitude > bounds[1][1]) {
+ bounds[1][1] = latitude;
+ }
+ }
+ return bounds;
+ } else {
+ return [[0, 0], [0, 0]];
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ if (this.map) {
+ if (prevProps.mapCenter !== this.props.mapCenter) {
+ this.map.easeTo({
+ center: this.props.mapCenter
+ });
+ }
+ if (prevProps.data.features !== this.props.data.features) {
+ this.map.getSource('positions').setData(this.props.data);
+ }
+ }
+ }
+
+ render() {
+ const style = {
+ position: 'relative',
+ overflow: 'hidden',
+ width: '100%',
+ height: '100%'
+ };
+ return <div style={style} ref={el => this.mapContainer = el} />;
+ }
+}
+
+export default connect(mapStateToProps)(MainMap);
diff --git a/modern/src/MainPage.js b/modern/src/MainPage.js
new file mode 100644
index 0000000..450a5e0
--- /dev/null
+++ b/modern/src/MainPage.js
@@ -0,0 +1,91 @@
+import React, { Component } from 'react';
+import ContainerDimensions from 'react-container-dimensions';
+import MainToobar from './MainToolbar';
+import MainMap from './MainMap';
+import Drawer from '@material-ui/core/Drawer';
+import withStyles from '@material-ui/core/styles/withStyles';
+import SocketController from './SocketController';
+import withWidth, { isWidthUp } from '@material-ui/core/withWidth';
+import DeviceList from './DeviceList';
+
+const styles = theme => ({
+ root: {
+ height: "100vh",
+ display: "flex",
+ flexDirection: "column"
+ },
+ content: {
+ flexGrow: 1,
+ overflow: "hidden",
+ display: "flex",
+ flexDirection: "row",
+ [theme.breakpoints.down('xs')]: {
+ flexDirection: "column-reverse"
+ }
+ },
+ drawerPaper: {
+ position: 'relative',
+ [theme.breakpoints.up('sm')]: {
+ width: 350
+ },
+ [theme.breakpoints.down('xs')]: {
+ height: 250
+ }
+ },
+ mapContainer: {
+ flexGrow: 1
+ }
+});
+
+class MainPage extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true
+ };
+ }
+
+ componentDidMount() {
+ fetch('/api/session').then(response => {
+ if (response.ok) {
+ this.setState({
+ loading: false
+ });
+ } else {
+ this.props.history.push('/login');
+ }
+ });
+ }
+
+ render() {
+ const { classes } = this.props;
+ const { loading } = this.state;
+ if (loading) {
+ return (
+ <div>Loading...</div>
+ );
+ } else {
+ return (
+ <div className={classes.root}>
+ <SocketController />
+ <MainToobar history={this.props.history} />
+ <div className={classes.content}>
+ <Drawer
+ anchor={isWidthUp('sm', this.props.width) ? "left" : "bottom"}
+ variant="permanent"
+ classes={{ paper: classes.drawerPaper }}>
+ <DeviceList />
+ </Drawer>
+ <div className={classes.mapContainer}>
+ <ContainerDimensions>
+ <MainMap/>
+ </ContainerDimensions>
+ </div>
+ </div>
+ </div>
+ );
+ }
+ }
+}
+
+export default withWidth()(withStyles(styles)(MainPage));
diff --git a/modern/src/MainToolbar.js b/modern/src/MainToolbar.js
new file mode 100644
index 0000000..6e2e4d6
--- /dev/null
+++ b/modern/src/MainToolbar.js
@@ -0,0 +1,123 @@
+import React, { Component, Fragment } from 'react';
+import { withStyles } from '@material-ui/core/styles';
+import AppBar from '@material-ui/core/AppBar';
+import Toolbar from '@material-ui/core/Toolbar';
+import Typography from '@material-ui/core/Typography';
+import Button from '@material-ui/core/Button';
+import IconButton from '@material-ui/core/IconButton';
+import MenuIcon from '@material-ui/icons/Menu';
+import Drawer from '@material-ui/core/Drawer';
+import List from '@material-ui/core/List';
+import Divider from '@material-ui/core/Divider';
+import ListItem from '@material-ui/core/ListItem';
+import ListItemIcon from '@material-ui/core/ListItemIcon';
+import ListItemText from '@material-ui/core/ListItemText';
+import DashboardIcon from '@material-ui/icons/Dashboard';
+import BarChartIcon from '@material-ui/icons/BarChart';
+import SettingsIcon from '@material-ui/icons/Settings';
+
+const styles = theme => ({
+ flex: {
+ flexGrow: 1
+ },
+ appBar: {
+ zIndex: theme.zIndex.drawer + 1,
+ },
+ list: {
+ width: 250
+ },
+ menuButton: {
+ marginLeft: -12,
+ marginRight: 20,
+ }
+});
+
+class MainToobar extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ drawer: false
+ };
+ this.openDrawer = this.openDrawer.bind(this);
+ this.closeDrawer = this.closeDrawer.bind(this);
+ this.handleLogout = this.handleLogout.bind(this);
+ }
+
+ openDrawer() {
+ this.setState({
+ drawer: true
+ });
+ };
+
+ closeDrawer() {
+ this.setState({
+ drawer: false
+ });
+ };
+
+ handleLogout() {
+ fetch("/api/session", {
+ method: "DELETE"
+ }).then(response => {
+ if (response.ok) {
+ this.props.history.push('/login');
+ }
+ });
+ }
+
+ render() {
+ const { classes } = this.props;
+ return (
+ <Fragment>
+ <AppBar position="static" className={classes.appBar}>
+ <Toolbar>
+ <IconButton
+ className={classes.menuButton}
+ color="inherit"
+ onClick={this.openDrawer}>
+ <MenuIcon />
+ </IconButton>
+ <Typography variant="h6" color="inherit" className={classes.flex}>
+ Traccar
+ </Typography>
+ <Button color="inherit" onClick={this.handleLogout}>Logout</Button>
+ </Toolbar>
+ </AppBar>
+ <Drawer open={this.state.drawer} onClose={this.closeDrawer}>
+ <div
+ tabIndex={0}
+ className={classes.list}
+ role="button"
+ onClick={this.closeDrawer}
+ onKeyDown={this.closeDrawer}>
+ <List>
+ <ListItem button>
+ <ListItemIcon>
+ <DashboardIcon />
+ </ListItemIcon>
+ <ListItemText primary="Dashboard" />
+ </ListItem>
+ <ListItem button disabled>
+ <ListItemIcon>
+ <BarChartIcon />
+ </ListItemIcon>
+ <ListItemText primary="Reports" />
+ </ListItem>
+ </List>
+ <Divider />
+ <List>
+ <ListItem button disabled>
+ <ListItemIcon>
+ <SettingsIcon />
+ </ListItemIcon>
+ <ListItemText primary="Settings" />
+ </ListItem>
+ </List>
+ </div>
+ </Drawer>
+ </Fragment>
+ );
+ }
+}
+
+export default withStyles(styles)(MainToobar);
diff --git a/modern/src/SocketController.js b/modern/src/SocketController.js
new file mode 100644
index 0000000..b89845f
--- /dev/null
+++ b/modern/src/SocketController.js
@@ -0,0 +1,61 @@
+import { Component } from 'react';
+import { connect } from 'react-redux';
+import { updateDevices, updatePositions } from './actions';
+
+const displayNotifications = events => {
+ if ("Notification" in window) {
+ if (Notification.permission === "granted") {
+ for (const event of events) {
+ const notification = new Notification(`Event: ${event.type}`);
+ setTimeout(notification.close.bind(notification), 4 * 1000);
+ }
+ } else if (Notification.permission !== "denied") {
+ Notification.requestPermission(permission => {
+ if (permission === "granted") {
+ displayNotifications(events);
+ }
+ });
+ }
+ }
+};
+
+class SocketController extends Component {
+ connectSocket() {
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+ const socket = new WebSocket(protocol + '//' + window.location.host + '/api/socket');
+
+ socket.onclose = () => {
+ setTimeout(() => this.connectSocket(), 60 * 1000);
+ };
+
+ socket.onmessage = (event) => {
+ const data = JSON.parse(event.data);
+ if (data.devices) {
+ this.props.dispatch(updateDevices(data.devices));
+ }
+ if (data.positions) {
+ this.props.dispatch(updatePositions(data.positions));
+ }
+ if (data.events) {
+ displayNotifications(data.events);
+ }
+ }
+ }
+
+ componentDidMount() {
+ fetch('/api/devices').then(response => {
+ if (response.ok) {
+ response.json().then(devices => {
+ this.props.dispatch(updateDevices(devices));
+ });
+ }
+ this.connectSocket();
+ });
+ }
+
+ render() {
+ return null;
+ }
+}
+
+export default connect()(SocketController);
diff --git a/modern/src/actions/index.js b/modern/src/actions/index.js
new file mode 100644
index 0000000..5527810
--- /dev/null
+++ b/modern/src/actions/index.js
@@ -0,0 +1,19 @@
+export const updateDevices = devices => ({
+ type: 'UPDATE_DEVICES',
+ devices
+})
+
+export const updatePositions = positions => ({
+ type: 'UPDATE_POSITIONS',
+ positions
+});
+
+export const updateEvents = events => ({
+ type: 'UPDATE_EVENTS',
+ events
+})
+
+export const selectDevice = device => ({
+ type: 'SELECT_DEVICE',
+ device
+})
diff --git a/modern/src/index.js b/modern/src/index.js
new file mode 100644
index 0000000..527a4d6
--- /dev/null
+++ b/modern/src/index.js
@@ -0,0 +1,21 @@
+import 'typeface-roboto'
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { BrowserRouter } from 'react-router-dom'
+import { Provider } from 'react-redux';
+import { createStore } from 'redux'
+import rootReducer from './reducers';
+import App from './App';
+import registerServiceWorker from './registerServiceWorker';
+
+const store = createStore(rootReducer);
+
+ReactDOM.render((
+ <Provider store={store}>
+ <BrowserRouter>
+ <App />
+ </BrowserRouter>
+ </Provider>
+), document.getElementById('root'));
+
+registerServiceWorker();
diff --git a/modern/src/reducers/index.js b/modern/src/reducers/index.js
new file mode 100644
index 0000000..752a4c3
--- /dev/null
+++ b/modern/src/reducers/index.js
@@ -0,0 +1,35 @@
+const initialState = {
+ devices: new Map(),
+ positions: new Map()
+};
+
+function updateMap(map, array, key) {
+ for (let value of array) {
+ map.set(value[key], value);
+ }
+ return map;
+}
+
+function rootReducer(state = initialState, action) {
+ switch (action.type) {
+ case 'UPDATE_DEVICES':
+ return Object.assign({}, {
+ ...state,
+ devices: updateMap(state.devices, action.devices, 'id')
+ });
+ case 'UPDATE_POSITIONS':
+ return Object.assign({}, {
+ ...state,
+ positions: updateMap(state.positions, action.positions, 'deviceId')
+ });
+ case 'SELECT_DEVICE':
+ return Object.assign({}, {
+ ...state,
+ selectedDevice: action.device.id
+ });
+ default:
+ return state;
+ }
+}
+
+export default rootReducer
diff --git a/modern/src/registerServiceWorker.js b/modern/src/registerServiceWorker.js
new file mode 100644
index 0000000..a3e6c0c
--- /dev/null
+++ b/modern/src/registerServiceWorker.js
@@ -0,0 +1,117 @@
+// In production, we register a service worker to serve assets from local cache.
+
+// This lets the app load faster on subsequent visits in production, and gives
+// it offline capabilities. However, it also means that developers (and users)
+// will only see deployed updates on the "N+1" visit to a page, since previously
+// cached resources are updated in the background.
+
+// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
+// This link also includes instructions on opting out of this behavior.
+
+const isLocalhost = Boolean(
+ window.location.hostname === 'localhost' ||
+ // [::1] is the IPv6 localhost address.
+ window.location.hostname === '[::1]' ||
+ // 127.0.0.1/8 is considered localhost for IPv4.
+ window.location.hostname.match(
+ /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
+ )
+);
+
+export default function register() {
+ if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
+ // The URL constructor is available in all browsers that support SW.
+ const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
+ if (publicUrl.origin !== window.location.origin) {
+ // Our service worker won't work if PUBLIC_URL is on a different origin
+ // from what our page is served on. This might happen if a CDN is used to
+ // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
+ return;
+ }
+
+ window.addEventListener('load', () => {
+ const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
+
+ if (isLocalhost) {
+ // This is running on localhost. Lets check if a service worker still exists or not.
+ checkValidServiceWorker(swUrl);
+
+ // Add some additional logging to localhost, pointing developers to the
+ // service worker/PWA documentation.
+ navigator.serviceWorker.ready.then(() => {
+ console.log(
+ 'This web app is being served cache-first by a service ' +
+ 'worker. To learn more, visit https://goo.gl/SC7cgQ'
+ );
+ });
+ } else {
+ // Is not local host. Just register service worker
+ registerValidSW(swUrl);
+ }
+ });
+ }
+}
+
+function registerValidSW(swUrl) {
+ navigator.serviceWorker
+ .register(swUrl)
+ .then(registration => {
+ registration.onupdatefound = () => {
+ const installingWorker = registration.installing;
+ installingWorker.onstatechange = () => {
+ if (installingWorker.state === 'installed') {
+ if (navigator.serviceWorker.controller) {
+ // At this point, the old content will have been purged and
+ // the fresh content will have been added to the cache.
+ // It's the perfect time to display a "New content is
+ // available; please refresh." message in your web app.
+ console.log('New content is available; please refresh.');
+ } else {
+ // At this point, everything has been precached.
+ // It's the perfect time to display a
+ // "Content is cached for offline use." message.
+ console.log('Content is cached for offline use.');
+ }
+ }
+ };
+ };
+ })
+ .catch(error => {
+ console.error('Error during service worker registration:', error);
+ });
+}
+
+function checkValidServiceWorker(swUrl) {
+ // Check if the service worker can be found. If it can't reload the page.
+ fetch(swUrl)
+ .then(response => {
+ // Ensure service worker exists, and that we really are getting a JS file.
+ if (
+ response.status === 404 ||
+ response.headers.get('content-type').indexOf('javascript') === -1
+ ) {
+ // No service worker found. Probably a different app. Reload the page.
+ navigator.serviceWorker.ready.then(registration => {
+ registration.unregister().then(() => {
+ window.location.reload();
+ });
+ });
+ } else {
+ // Service worker found. Proceed as normal.
+ registerValidSW(swUrl);
+ }
+ })
+ .catch(() => {
+ console.log(
+ 'No internet connection found. App is running in offline mode.'
+ );
+ });
+}
+
+export function unregister() {
+ if ('serviceWorker' in navigator) {
+ navigator.serviceWorker.ready.then(registration => {
+ registration.unregister();
+ });
+ }
+}
diff --git a/modern/src/setupProxy.js b/modern/src/setupProxy.js
new file mode 100644
index 0000000..71312d5
--- /dev/null
+++ b/modern/src/setupProxy.js
@@ -0,0 +1,6 @@
+const proxy = require('http-proxy-middleware');
+
+module.exports = function(app) {
+ app.use(proxy('/api/socket', { target: 'ws://localhost:8082', ws: true }));
+ app.use(proxy('/api', { target: 'http://localhost:8082' }));
+};