mirror of
https://github.com/openmhz/trunk-server.git
synced 2026-11-13 01:07:42 -05:00
basic Create React App working
This commit is contained in:
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"presets":
|
||||
["@babel/preset-env",
|
||||
"@babel/react"],
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-proposal-export-default-from",
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
["lodash", {"id": ["lodash", "semantic-ui-react"]}]
|
||||
]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
node_modules
|
||||
lib
|
||||
public
|
||||
1
frontend/.eslintcache
Normal file
1
frontend/.eslintcache
Normal file
@@ -0,0 +1 @@
|
||||
[{"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/index.js":"1","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/features/counter/Counter.js":"2","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/App.js":"3","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/app/reducers.js":"4","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/features/counter/counterSlice.js":"5","/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/app/configureStore.js":"6"},{"size":824,"mtime":1609425644251,"results":"7","hashOfConfig":"8"},{"size":1626,"mtime":499162500000,"results":"9","hashOfConfig":"8"},{"size":1463,"mtime":499162500000,"results":"10","hashOfConfig":"8"},{"size":375,"mtime":1609425733654,"results":"11","hashOfConfig":"8"},{"size":1551,"mtime":499162500000,"results":"12","hashOfConfig":"8"},{"size":759,"mtime":1609425817988,"results":"13","hashOfConfig":"8"},{"filePath":"14","messages":"15","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"1du4mf8",{"filePath":"16","messages":"17","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"18","messages":"19","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"20","messages":"21","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"22","messages":"23","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"24","messages":"25","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/index.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/features/counter/Counter.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/App.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/app/reducers.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/features/counter/counterSlice.js",[],"/Users/luke/Projects/OpenMHz/trunk-server-public/frontend/src/app/configureStore.js",[]]
|
||||
58
frontend/.gitignore
vendored
58
frontend/.gitignore
vendored
@@ -1,35 +1,23 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (http://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directory
|
||||
node_modules
|
||||
assets
|
||||
lib
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { configure } from '@storybook/react';
|
||||
|
||||
function loadStories() {
|
||||
require('../src/client/stories/index.js');
|
||||
// You can require as many stories as you need.
|
||||
}
|
||||
|
||||
configure(loadStories, module);
|
||||
@@ -1,22 +0,0 @@
|
||||
# docker build -t trunk-server-frontend .
|
||||
# do a `git pull` in trunk-server-frontend to update
|
||||
|
||||
FROM node:8.11
|
||||
|
||||
|
||||
ENV HOME=/home/app
|
||||
ENV NODE_OPTIONS="--max_old_space_size=8000"
|
||||
|
||||
RUN mkdir -p /home/app
|
||||
COPY ./package.json /tmp
|
||||
RUN cd /tmp && npm install
|
||||
RUN cp -a /tmp/node_modules /home/app
|
||||
|
||||
# add `/usr/src/app/node_modules/.bin` to $PATH
|
||||
ENV PATH /usr/app/node_modules/.bin:$PATH
|
||||
|
||||
WORKDIR $HOME
|
||||
COPY . $HOME/
|
||||
RUN npm run build
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
68
frontend/README.md
Normal file
68
frontend/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template.
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `yarn start`
|
||||
|
||||
Runs the app in the development mode.<br />
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br />
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `yarn test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.<br />
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `yarn build`
|
||||
|
||||
Builds the app for production to the `build` folder.<br />
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br />
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `yarn eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
|
||||
### Code Splitting
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
|
||||
|
||||
### Analyzing the Bundle Size
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
|
||||
|
||||
### Making a Progressive Web App
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
|
||||
|
||||
### Deployment
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
|
||||
|
||||
### `yarn build` fails to minify
|
||||
|
||||
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
|
||||
@@ -1,10 +0,0 @@
|
||||
/*
|
||||
Called during development only. Allows us to use ES6 and JSX in our server code.
|
||||
*/
|
||||
|
||||
require('@babel/register')({
|
||||
presets: ["@babel/preset-env",
|
||||
"@babel/react"]
|
||||
});
|
||||
|
||||
require("./src/server/index")
|
||||
15729
frontend/package-lock.json
generated
15729
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,102 +1,41 @@
|
||||
{
|
||||
"name": "react-passport-redux-example",
|
||||
"version": "1.0.0",
|
||||
"description": "Demonstration of PassportJS authentication in React w/ Redux.",
|
||||
"main": "devStart.js",
|
||||
"scripts": {
|
||||
"build:lib": "babel src/server --out-dir lib/server",
|
||||
"build:umd": "webpack --config webpack/webpack.config.dev.js",
|
||||
"build:umd:min": "webpack --config webpack/webpack.config.prod.js",
|
||||
"build": "npm run clean && npm run build:lib && npm run build:umd && npm run build:umd:min",
|
||||
"postinstall": "npm run build",
|
||||
"clean": "rimraf lib public/assets",
|
||||
"server": "better-npm-run server",
|
||||
"server:dev": "better-npm-run server:dev",
|
||||
"start": "npm run server",
|
||||
"start:dev": "npm run server:dev",
|
||||
"storybook": "start-storybook -p 9001 -c .storybook"
|
||||
},
|
||||
"betterScripts": {
|
||||
"server": {
|
||||
"command": "node lib/server/index.js",
|
||||
"env": {
|
||||
"NODE_ENV": "production",
|
||||
"MIN_EXT": ".min"
|
||||
}
|
||||
},
|
||||
"server:dev": {
|
||||
"command": "node devStart.js",
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/cli": "7.2.3",
|
||||
"@babel/plugin-proposal-class-properties": "7.3.3",
|
||||
"@babel/preset-env": "7.3.1",
|
||||
"@babel/preset-react": "7.0.0",
|
||||
"@babel/preset-stage-1": "7.0.0",
|
||||
"@babel/register": "7.0.0",
|
||||
"autoprefixer": "9.4.8",
|
||||
"axios": "0.18.0",
|
||||
"bcrypt-nodejs": "0.0.3",
|
||||
"better-npm-run": "0.1.1",
|
||||
"body-parser": "1.18.3",
|
||||
"connected-react-router": "6.3.1",
|
||||
"date-fns": "1.30.1",
|
||||
"express": "4.16.4",
|
||||
"express-session": "^1.15.6",
|
||||
"file-loader": "3.0.1",
|
||||
"fs-extra": "7.0.1",
|
||||
"less": "^3.9.0",
|
||||
"less-loader": "4.1.0",
|
||||
"lodash": "4.17.11",
|
||||
"moment": "2.24.0",
|
||||
"postcss-flexbugs-fixes": "4.1.0",
|
||||
"postcss-loader": "3.0.0",
|
||||
"query-string": "6.2.0",
|
||||
"react": "16.8.3",
|
||||
"react-audio-player": "0.11.0",
|
||||
"react-datepicker": "2.1.0",
|
||||
"react-dom": "16.8.3",
|
||||
"react-ga": "2.5.7",
|
||||
"react-media-player": "0.7.6",
|
||||
"react-redux": "6.0.1",
|
||||
"react-router": "4.3.1",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"react-router-redux": "^5.0.0-alpha.9",
|
||||
"react-scroll-to-component": "1.0.2",
|
||||
"react-waypoint": "9.0.0",
|
||||
"redux": "^4.0.1",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"semantic-ui-react": "0.85.0",
|
||||
"url-loader": "1.1.2",
|
||||
"victory": "^31.2.0",
|
||||
"webpack-merge": "4.2.1"
|
||||
"@reduxjs/toolkit": "^1.1.0",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"bootstrap": "4.5.3",
|
||||
"connected-react-router": "6.8.0",
|
||||
"react": "^17.0.1",
|
||||
"react-bootstrap": "1.4.0",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-redux": "^7.1.3",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-scripts": "4.0.1",
|
||||
"redux-thunk": "2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.3.3",
|
||||
"@babel/plugin-proposal-export-default-from": "7.2.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "7.3.2",
|
||||
"@storybook/react": "4.1.13",
|
||||
"babel-loader": "8.0.5",
|
||||
"babel-plugin-lodash": "3.3.4",
|
||||
"babel-plugin-react-transform": "^3.0.0",
|
||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"css-loader": "2.1.0",
|
||||
"detect-node": "2.0.4",
|
||||
"extract-text-webpack-plugin": "4.0.0-beta.0",
|
||||
"mini-css-extract-plugin": "0.5.0",
|
||||
"redux-logger": "3.0.6",
|
||||
"rimraf": "2.6.3",
|
||||
"semantic-ui-css": "2.4.1",
|
||||
"semantic-ui-less": "2.4.1",
|
||||
"style-loader": "0.23.1",
|
||||
"webpack": "4.29.5",
|
||||
"webpack-cli": "3.2.3",
|
||||
"webpack-dev-middleware": "^3.6.0",
|
||||
"webpack-hot-middleware": "2.24.3"
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
43
frontend/public/index.html
Normal file
43
frontend/public/index.html
Normal file
@@ -0,0 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React Redux App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
BIN
frontend/public/logo192.png
Normal file
BIN
frontend/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
BIN
frontend/public/logo512.png
Normal file
BIN
frontend/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
25
frontend/public/manifest.json
Normal file
25
frontend/public/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
2
frontend/public/robots.txt
Normal file
2
frontend/public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
39
frontend/src/App.css
Normal file
39
frontend/src/App.css
Normal file
@@ -0,0 +1,39 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-float infinite 3s ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: rgb(112, 76, 182);
|
||||
}
|
||||
|
||||
@keyframes App-logo-float {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(10px)
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px)
|
||||
}
|
||||
}
|
||||
58
frontend/src/App.js
Normal file
58
frontend/src/App.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import logo from './logo.svg';
|
||||
import { Counter } from './features/counter/Counter';
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<Counter />
|
||||
<p>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
<span>
|
||||
<span>Learn </span>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
React
|
||||
</a>
|
||||
<span>, </span>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://redux.js.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Redux
|
||||
</a>
|
||||
<span>, </span>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://redux-toolkit.js.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Redux Toolkit
|
||||
</a>
|
||||
,<span> and </span>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://react-redux.js.org/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
React Redux
|
||||
</a>
|
||||
</span>
|
||||
</header>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
15
frontend/src/App.test.js
Normal file
15
frontend/src/App.test.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Provider } from 'react-redux';
|
||||
import store from './app/store';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
const { getByText } = render(
|
||||
<Provider store={store}>
|
||||
<App />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
expect(getByText(/learn/i)).toBeInTheDocument();
|
||||
});
|
||||
26
frontend/src/app/configureStore.js
Normal file
26
frontend/src/app/configureStore.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// https://github.com/supasate/connected-react-router
|
||||
// configureStore.js
|
||||
|
||||
import { createBrowserHistory } from 'history'
|
||||
import { applyMiddleware, compose, createStore } from 'redux'
|
||||
import { routerMiddleware } from 'connected-react-router'
|
||||
import createRootReducer from './reducers'
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
export const history = createBrowserHistory()
|
||||
|
||||
export default function configureStore(preloadedState) {
|
||||
const store = createStore(
|
||||
createRootReducer(history), // root reducer with router state
|
||||
preloadedState,
|
||||
compose(
|
||||
applyMiddleware(
|
||||
routerMiddleware(history), // for dispatching history actions
|
||||
thunk,
|
||||
// ... other middlewares ...
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
return store
|
||||
}
|
||||
13
frontend/src/app/reducers.js
Normal file
13
frontend/src/app/reducers.js
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
// from https://github.com/supasate/connected-react-router
|
||||
import { combineReducers } from 'redux'
|
||||
import { connectRouter } from 'connected-react-router'
|
||||
import counterReducer from '../features/counter/counterSlice';
|
||||
|
||||
const createRootReducer = (history) => combineReducers({
|
||||
router: connectRouter(history),
|
||||
counter: counterReducer,
|
||||
})
|
||||
export default createRootReducer
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
|
||||
|
||||
.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
Icon,
|
||||
Header
|
||||
} from "semantic-ui-react";
|
||||
import "./CalendarModal.css";
|
||||
import DatePicker from 'react-datepicker';
|
||||
import subDays from 'date-fns/sub_days'
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
class CalendarModal extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.handleDateChange = this.handleDateChange.bind(this);
|
||||
this.handleDone = this.handleDone.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
|
||||
this.state = {
|
||||
open: false,
|
||||
startDate: new Date()
|
||||
}
|
||||
}
|
||||
|
||||
handleDateChange = (date) => {
|
||||
this.setState({
|
||||
startDate: date
|
||||
});
|
||||
}
|
||||
|
||||
handleClose = () => this.props.onClose(false);
|
||||
handleDone(event) {
|
||||
this.props.callActions.setDateFilter(this.state.startDate.getTime());
|
||||
this.props.onClose(true);
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
|
||||
<Modal open={this.props.open} onClose={this.handleClose} centered={false} size="tiny">
|
||||
<Modal.Header>Select a Date & Time</Modal.Header>
|
||||
<Modal.Content >
|
||||
<Modal.Description>
|
||||
<DatePicker
|
||||
selected={this.state.startDate}
|
||||
onChange={this.handleDateChange}
|
||||
maxDate={new Date()}
|
||||
minDate={subDays(new Date(), this.props.archive)}
|
||||
showTimeSelect
|
||||
timeFormat="HH:mm"
|
||||
timeIntervals={15}
|
||||
dateFormat="LLL"
|
||||
inline
|
||||
/>
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={this.handleDone} >
|
||||
<Icon name='checkmark' /> Done
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default CalendarModal;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { connect } from "react-redux"
|
||||
import * as callActionCreators from "./call-actions"
|
||||
import { bindActionCreators } from 'redux'
|
||||
import CalendarModal from "./CalendarModal"
|
||||
|
||||
function mapStateToProps(state, props) {
|
||||
return {
|
||||
groups: state.group.items[props.shortName],
|
||||
talkgroups: state.talkgroup.items[props.shortName],
|
||||
talkgroupsIsWaiting: state.talkgroup.isWaiting,
|
||||
groupsIsWaiting: state.group.isWaiting,
|
||||
shortName: props.shortName,
|
||||
activeTab: state.call.filterType,
|
||||
selectedTalkgroups: state.call.filterTalkgroups
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
callActions: bindActionCreators(callActionCreators, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(CalendarModal)
|
||||
@@ -1,86 +0,0 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Header,
|
||||
Divider,
|
||||
List,
|
||||
Segment,
|
||||
Statistic,
|
||||
Icon
|
||||
} from "semantic-ui-react";
|
||||
|
||||
// ----------------------------------------------------
|
||||
class CallInfo extends React.Component {
|
||||
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
var srcList = "";
|
||||
var callLength = "-";
|
||||
var callFreq = "-";
|
||||
var callDate = "-";
|
||||
var callTime = "-";
|
||||
var talkgroupNum = "-";
|
||||
if (this.props.call) {
|
||||
const currentCall = this.props.call;
|
||||
var time = new Date(currentCall.time);
|
||||
callTime = time.toLocaleTimeString();
|
||||
callDate = time.toLocaleDateString();
|
||||
if (currentCall.freq) {
|
||||
var freq = currentCall.freq / 1000000;
|
||||
callFreq = Math.round(freq * 1000) / 1000;
|
||||
}
|
||||
|
||||
srcList = currentCall.srcList.map((source, index) => <List.Item key={index}>{source.src}
|
||||
[{source.pos}]</List.Item>);
|
||||
callLength = currentCall.len;
|
||||
talkgroupNum = currentCall.talkgroupNum;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Segment padded>
|
||||
<Header as='h1'>{this.props.header}</Header>
|
||||
<List bulleted horizontal link>
|
||||
{srcList}
|
||||
</List>
|
||||
<Divider/>
|
||||
<Statistic size='small'>
|
||||
<Statistic.Label>Seconds</Statistic.Label>
|
||||
<Statistic.Value>{callLength}</Statistic.Value>
|
||||
</Statistic>
|
||||
<Statistic size='small'>
|
||||
<Statistic.Label>Talkgroup</Statistic.Label>
|
||||
<Statistic.Value>{talkgroupNum}</Statistic.Value>
|
||||
</Statistic>
|
||||
|
||||
<List divided verticalAlign='middle'>
|
||||
<List.Item>
|
||||
<Icon name="wait"/>
|
||||
<List.Content>
|
||||
{callTime}
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Icon name="calendar outline"/>
|
||||
<List.Content>
|
||||
{callDate}
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
<List.Item>
|
||||
<Icon name="cubes"/>
|
||||
<List.Content>
|
||||
{callFreq} MHz
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
</List>
|
||||
</Segment>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CallInfo;
|
||||
@@ -1,123 +0,0 @@
|
||||
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
Table,
|
||||
Icon,
|
||||
Label
|
||||
} from "semantic-ui-react";
|
||||
import { connect } from "react-redux"
|
||||
import { bindActionCreators } from 'redux'
|
||||
import * as callActionCreators from "./call-actions"
|
||||
|
||||
|
||||
function mapStateToProps(state, props) {
|
||||
return {
|
||||
shortName: props.shortName
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
callActions: bindActionCreators(callActionCreators, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class CallItem extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.hideStar = this.hideStar.bind(this);
|
||||
this.showStar = this.showStar.bind(this);
|
||||
this.addStar = this.addStar.bind(this);
|
||||
this.state = {
|
||||
starVisible: false,
|
||||
starClicked: false
|
||||
}
|
||||
}
|
||||
sourceString(call) {
|
||||
var srcString = "";
|
||||
if (call.srcList) {
|
||||
for (var src in call.srcList) {
|
||||
/* srcNum = call.srcList[src].src;
|
||||
if (sources.hasOwnProperty(srcNum)) {
|
||||
srcString = srcString + sources[srcNum].codeName + ", ";
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
return srcString;
|
||||
}
|
||||
|
||||
addStar(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
if (!this.state.starClicked) {
|
||||
|
||||
this.props.callActions.addStar(this.props.call._id);
|
||||
this.setState({starClicked: true});
|
||||
}
|
||||
}
|
||||
|
||||
hideStar() {
|
||||
this.setState({ starVisible: false })
|
||||
}
|
||||
|
||||
showStar() {
|
||||
this.setState({ starVisible: true })
|
||||
}
|
||||
|
||||
render() {
|
||||
const call = this.props.call;
|
||||
const time = new Date(call.time);
|
||||
const current = new Date(time).setHours(0, 0, 0, 0);
|
||||
var rowSelected={};
|
||||
let starButton;
|
||||
var starClickable = {};
|
||||
if (!this.state.starClicked) {
|
||||
starClickable = {link: true };
|
||||
}
|
||||
if (!call.star && this.state.starVisible) {
|
||||
starButton = <Icon name='star outline' />
|
||||
}
|
||||
|
||||
if (call.star) {
|
||||
starButton = <Icon name='star' />
|
||||
}
|
||||
if (call.star && call.star > 1) {
|
||||
starButton = (<Icon.Group >
|
||||
<Icon {...starClickable} name='star' />
|
||||
<Label circular color='red' size='mini' floating>
|
||||
{call.star}
|
||||
</Label>
|
||||
</Icon.Group>)
|
||||
}
|
||||
|
||||
if (this.props.activeCall) {
|
||||
rowSelected={positive: true}
|
||||
}
|
||||
var talkgroup;
|
||||
if ((typeof this.props.talkgroups == 'undefined') || (typeof this.props.talkgroups[call.talkgroupNum] == 'undefined')) {
|
||||
talkgroup = call.talkgroupNum;
|
||||
} else {
|
||||
talkgroup = this.props.talkgroups[call.talkgroupNum].description;
|
||||
}
|
||||
return (
|
||||
<Table.Row onClick={(e) => this.props.onClick({call: call}, e)} {...rowSelected}>
|
||||
<Table.Cell> {call.len} </Table.Cell>
|
||||
<Table.Cell> {talkgroup} </Table.Cell>
|
||||
<Table.Cell> {time.toLocaleTimeString()} </Table.Cell>
|
||||
<Table.Cell onMouseEnter={this.showStar} onMouseLeave={this.hideStar} onClick={this.addStar}>{starButton}</Table.Cell>
|
||||
</Table.Row>
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(CallItem)
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
|
||||
/*******************************
|
||||
Responsive
|
||||
*******************************/
|
||||
/* Defaults */
|
||||
|
||||
.ui.progress:first-child {
|
||||
width: 150px;
|
||||
margin: 0px;
|
||||
}
|
||||
/*
|
||||
.ui.menu .item {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
}*/
|
||||
|
||||
/* Needed to fix an error with Semantic UI react
|
||||
https://github.com/Semantic-Org/Semantic-UI-React/issues/2558
|
||||
https://github.com/Semantic-Org/Semantic-UI-React/issues/2550
|
||||
*/
|
||||
|
||||
.ui.modal {
|
||||
margin-top: 0 !important;
|
||||
position: unset;
|
||||
}
|
||||
|
||||
.main.container {
|
||||
|
||||
width: auto !important;
|
||||
max-width: 960px !important;
|
||||
margin-top: 55px;
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (min-width: 1024px) {
|
||||
|
||||
.main.container {
|
||||
margin-left: 3em !important;
|
||||
margin-right: 387px !important;
|
||||
}
|
||||
|
||||
/* Rail Default */
|
||||
.main.container > .right.rail
|
||||
{
|
||||
margin-left: 3em;
|
||||
padding-top: 2em;
|
||||
width: 319px;
|
||||
right: 20px;
|
||||
left: auto;
|
||||
}
|
||||
|
||||
}
|
||||
@media only screen and (min-width: 768px) {
|
||||
/* .desktop-only {
|
||||
display:block !important;
|
||||
}*/
|
||||
|
||||
.mobile-only {
|
||||
display:none !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media only screen and (min-width:768px) and (max-width: 1023px) {
|
||||
|
||||
|
||||
.main.container {
|
||||
margin-right: 250px !important;
|
||||
}
|
||||
|
||||
/* Rail Default */
|
||||
.main.container > .right.rail
|
||||
{
|
||||
margin-left: 3em;
|
||||
width: 250px;
|
||||
}
|
||||
|
||||
#calls {
|
||||
margin-left: 25px;
|
||||
margin-right: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 767px) {
|
||||
|
||||
.mobile-only {
|
||||
display:block !important;
|
||||
}
|
||||
#call-header {
|
||||
display:none !important;
|
||||
}
|
||||
.main.container
|
||||
{
|
||||
margin-left: 0px !important;
|
||||
margin-right: 0px !important;
|
||||
}
|
||||
/*
|
||||
.ui.table:not(.unstackable) tr > td.desktop-only {
|
||||
display:none !important;
|
||||
} */
|
||||
.desktop-only {
|
||||
display:none !important;
|
||||
}
|
||||
}
|
||||
@@ -1,579 +0,0 @@
|
||||
import React from "react";
|
||||
import {Link} from 'react-router-dom';
|
||||
import CallItem from "./CallItem";
|
||||
import MediaPlayer from "./MediaPlayer";
|
||||
import FilterModal from "./FilterModalContainer";
|
||||
import GroupModal from "./GroupModalContainer";
|
||||
import SupportModal from "./SupportModal";
|
||||
import CalendarModal from "./CalendarModalContainer";
|
||||
import CallInfo from "./CallInfo";
|
||||
import ListCalls from "./ListCalls";
|
||||
import {
|
||||
Container,
|
||||
Label,
|
||||
Rail,
|
||||
Sticky,
|
||||
Menu,
|
||||
Icon,
|
||||
Table,
|
||||
Sidebar
|
||||
} from "semantic-ui-react";
|
||||
import {Waypoint} from 'react-waypoint';
|
||||
import "./CallPlayer.css";
|
||||
//import setupSocket from '../socket/SetupSocket.js'
|
||||
import queryString from '../query-string';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------------------
|
||||
class CallPlayer extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.loadNewerCalls = this.loadNewerCalls.bind(this);
|
||||
this.loadOlderCalls = this.loadOlderCalls.bind(this);
|
||||
this.playCall = this.playCall.bind(this);
|
||||
this.updateUri = this.updateUri.bind(this);
|
||||
this.switchAutoPlay = this.switchAutoPlay.bind(this);
|
||||
this.callEnded = this.callEnded.bind(this);
|
||||
this.addCall = this.addCall.bind(this);
|
||||
this.handlePusherClick = this.handlePusherClick.bind(this);
|
||||
this.handleSidebarToggle = this.handleSidebarToggle.bind(this);
|
||||
this.handleSupportToggle = this.handleSupportToggle.bind(this);
|
||||
this.handleFilterClose = this.handleFilterClose.bind(this);
|
||||
this.handleGroupClose = this.handleGroupClose.bind(this);
|
||||
this.handleSupportClose = this.handleSupportClose.bind(this);
|
||||
this.handleCalendarToggle = this.handleCalendarToggle.bind(this);
|
||||
this.handleCalendarClose = this.handleCalendarClose.bind(this);
|
||||
this.handleLiveToggle = this.handleLiveToggle.bind(this);
|
||||
this.getFilter = this.getFilter.bind(this);
|
||||
this.callList = React.createRef();
|
||||
this.setupSocket = this.setupSocket.bind(this);
|
||||
this.endSocket = this.endSocket.bind(this);
|
||||
this.changeUrl = this.changeUrl.bind(this);
|
||||
this.currentCallRef = React.createRef();
|
||||
this.audioRef = React.createRef();
|
||||
this.state = {
|
||||
requestMessage: "",
|
||||
callUrl: "",
|
||||
autoPlay: true,
|
||||
callId: false,
|
||||
playTime: 0,
|
||||
sourceIndex: 0,
|
||||
urlOptions: false,
|
||||
isPlaying: false,
|
||||
sidebarOpened: false,
|
||||
filterVisible: false,
|
||||
groupVisible: false,
|
||||
calendarVisible: false,
|
||||
supportVisible: false
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
switchAutoPlay = () => this.setState({
|
||||
autoPlay: !this.state.autoPlay
|
||||
})
|
||||
handlePusherClick = () => {
|
||||
const { sidebarOpened } = this.state
|
||||
|
||||
if (sidebarOpened) this.setState({ sidebarOpened: false })
|
||||
}
|
||||
|
||||
handleSidebarToggle = () => this.setState({ sidebarOpened: !this.state.sidebarOpened })
|
||||
|
||||
handleFilterToggle = () => this.setState({
|
||||
filterVisible: !this.state.filterVisible
|
||||
})
|
||||
|
||||
handleSupportToggle = () => this.setState({
|
||||
supportVisible: !this.state.supportVisible
|
||||
})
|
||||
|
||||
handleContextRef = contextRef => this.setState({contextRef})
|
||||
changeUrl = url => this.props.callActions.changeUrl(url)
|
||||
|
||||
loadNewerCalls() {
|
||||
if (!this.props.callsIsWaiting) {
|
||||
console.log("Loading Newer Calls");
|
||||
this.props.callActions.fetchNewerCalls( this.props.newestCallTime.getTime());
|
||||
} else {
|
||||
console.log("Calls still loading - can't load newer calls");
|
||||
}
|
||||
}
|
||||
|
||||
loadOlderCalls() {
|
||||
if (!this.props.callsIsWaiting) {
|
||||
console.log("Loading Older Calls");
|
||||
this.props.callActions.fetchOlderCalls( this.props.oldestCallTime.getTime());
|
||||
} else {
|
||||
console.log("Calls still loading - can't load older calls");
|
||||
}
|
||||
}
|
||||
|
||||
handleCalendarToggle = () => this.setState({
|
||||
calendarVisible: !this.state.calendarVisible
|
||||
})
|
||||
|
||||
getFilter() {
|
||||
var filter = {type: 'all', code: "", filterStarred: false};
|
||||
switch(this.props.filterType) {
|
||||
case 1:
|
||||
filter.type = "group";
|
||||
filter.code = this.props.filterGroupId;
|
||||
break;
|
||||
case 2:
|
||||
filter.type = "talkgroup";
|
||||
filter.code = this.props.filterTalkgroups;
|
||||
break;
|
||||
}
|
||||
filter.filterStarred = this.props.filterStarred;
|
||||
return filter;
|
||||
}
|
||||
endSocket() {
|
||||
socket.removeAllListeners("new message");
|
||||
socket.removeAllListeners("reconnect");
|
||||
}
|
||||
setupSocket() {
|
||||
socket.on('new message', this.addCall);
|
||||
socket.on('reconnect', (attempts) => {
|
||||
console.log("Socket Reconnected after attempts: " + attempts); // true
|
||||
if (this.props.live) {
|
||||
var filter = this.getFilter();
|
||||
this.startSocket(this.props.shortName, filter.type, filter.code, filter.filterStarred);
|
||||
}
|
||||
})
|
||||
}
|
||||
startSocket(shortName, filterType="", filterCode="", filterStarred=false) {
|
||||
socket.emit("start", {
|
||||
filterCode: filterCode,
|
||||
filterType: filterType,
|
||||
filterName: "OpenMHz",
|
||||
filterStarred: filterStarred,
|
||||
shortName: shortName
|
||||
});
|
||||
}
|
||||
stopSocket(){
|
||||
socket.emit("stop");
|
||||
}
|
||||
handleLiveToggle() {
|
||||
if (!this.props.live) {
|
||||
this.props.callActions.setDateFilter(false);
|
||||
this.props.callActions.setLive(false);
|
||||
this.setState({ callUrl: "", callId: false});
|
||||
this.props.callActions.fetchCalls();
|
||||
var filter = this.getFilter();
|
||||
this.startSocket(this.props.shortName, filter.type, filter.code, filter.filterStarred);
|
||||
//this.socket = setupSocket(this.addCall, this.props.shortName);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
updateUri(props) {
|
||||
var search = "?"
|
||||
switch (props.filterType) {
|
||||
|
||||
case 1:
|
||||
search = search + `filter-type=group&filter-code=${props.filterGroupId}`;
|
||||
break;
|
||||
case 2:
|
||||
search = search + `filter-type=talkgroup&filter-code=${props.filterTalkgroups}`;
|
||||
break;
|
||||
}
|
||||
if (props.filterDate) {
|
||||
if (search.length!=1) {
|
||||
search = search + '&';
|
||||
}
|
||||
search = search + `time=${props.filterDate}`;
|
||||
}
|
||||
|
||||
if (props.filterStarred) {
|
||||
if (search.length!=1) {
|
||||
search = search + '&';
|
||||
}
|
||||
search = search + `starred=true`;
|
||||
}
|
||||
|
||||
this.props.history.push({
|
||||
pathname: this.props.history.location.pathname,
|
||||
search: search
|
||||
});
|
||||
}
|
||||
|
||||
handleCalendarClose(didUpdate) {
|
||||
this.setState({
|
||||
calendarVisible: !this.state.calendarVisible
|
||||
});
|
||||
if (didUpdate) {
|
||||
this.props.callActions.setLive(false);
|
||||
this.props.callActions.fetchCalls();
|
||||
|
||||
this.setState({ callUrl: "", callId: false});
|
||||
this.stopSocket();
|
||||
//this.socket.close();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
handleGroupClose(didUpdate) {
|
||||
this.setState({
|
||||
groupVisible: !this.state.groupVisible
|
||||
});
|
||||
this.props.callActions.fetchCalls();
|
||||
}
|
||||
|
||||
handleSupportClose() {
|
||||
this.setState({
|
||||
supportVisible: !this.state.supportVisible
|
||||
});
|
||||
}
|
||||
|
||||
handleFilterClose(didUpdate) {
|
||||
this.setState({
|
||||
filterVisible: !this.state.filterVisible
|
||||
});
|
||||
if (didUpdate) {
|
||||
this.setState({callUrl: "", callId: false});
|
||||
this.props.callActions.fetchCalls();
|
||||
}
|
||||
}
|
||||
|
||||
callEnded(data) {
|
||||
const audio = this.audioRef.current;
|
||||
const currentIndex = this.props.callsAllIds.findIndex(callId => callId === this.state.callId);
|
||||
if (this.state.autoPlay && (currentIndex > 0)) {
|
||||
const nextCallId = this.props.callsAllIds[currentIndex - 1];
|
||||
const nextCall = this.props.callsById[nextCallId];
|
||||
var callUrl = media_server + nextCall.filename;
|
||||
if (nextCall.url) {
|
||||
callUrl = nextCall.url;
|
||||
}
|
||||
|
||||
this.setState({callUrl: callUrl, callId: nextCallId, sourceIndex: 0, isPlaying: true}, () => { audio.playSource(callUrl)}); //scrollToComponent(this.currentCallRef.current);
|
||||
this.props.callActions.fetchCallInfo( nextCallId);
|
||||
} else {
|
||||
this.setState({isPlaying: false});
|
||||
}
|
||||
}
|
||||
|
||||
playCall(data) {
|
||||
const audio = this.audioRef.current;
|
||||
var callUrl = media_server + data.call.filename;
|
||||
if (data.call.url) {
|
||||
callUrl = data.call.url;
|
||||
}
|
||||
this.setState({
|
||||
callUrl: callUrl,
|
||||
callId: data.call._id,
|
||||
sourceIndex: 0,
|
||||
isPlaying: true
|
||||
}, () => { audio.playSource(callUrl);}); //scrollToComponent(this.currentCallRef.current);
|
||||
this.props.callActions.fetchCallInfo( data.call._id);
|
||||
}
|
||||
|
||||
addCall(data) {
|
||||
const message = JSON.parse(data)
|
||||
switch (message.type) {
|
||||
case 'calls':
|
||||
const refs = this.refs.current;
|
||||
this.props.callActions.addCall(message);
|
||||
const newCall = this.props.callsById[this.props.callsAllIds[0]];
|
||||
if (!this.state.isPlaying && this.state.autoPlay) {
|
||||
this.playCall({call: newCall});
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.endSocket();
|
||||
}
|
||||
componentDidMount() {
|
||||
//const parsed = queryString.parse(location.search);
|
||||
//console.log(parsed);
|
||||
this.props.callActions.setShortName(this.props.shortName);
|
||||
var filter = {
|
||||
filterDate: false,
|
||||
filterType: "all",
|
||||
filterTalkgroups: [],
|
||||
filterGroupId: false,
|
||||
filterStarred: false,
|
||||
live: true
|
||||
};
|
||||
const uri = queryString.parse(this.props.location.search);
|
||||
|
||||
// is there a star filter?
|
||||
if (uri.hasOwnProperty('starred')) {
|
||||
const starred = uri['starred'];
|
||||
filter.filterStarred = starred == 'true'? true : false;
|
||||
this.setState({urlOptions: true});
|
||||
}
|
||||
|
||||
// is there a time based filter?
|
||||
if (uri.hasOwnProperty('time')) {
|
||||
const date = new Date(parseInt(uri['time']));
|
||||
filter.filterDate = date.getTime();
|
||||
filter.live = false;
|
||||
this.setState({urlOptions: true});
|
||||
}
|
||||
|
||||
// is this just for one call?
|
||||
if (uri.hasOwnProperty('call-id')) {
|
||||
const _id = uri['call-id'];
|
||||
filter.live = false;
|
||||
this.setState({
|
||||
callId: _id,
|
||||
sourceIndex: 0,
|
||||
urlOptions: true,
|
||||
autoPlay: false
|
||||
});
|
||||
this.props.callActions.fetchCallInfo( _id);
|
||||
}
|
||||
|
||||
|
||||
var filterType="all";
|
||||
var filterCode="";
|
||||
|
||||
// is there a Filter set?
|
||||
if ((uri.hasOwnProperty('filter-code')) && (uri.hasOwnProperty('filter-type'))) {
|
||||
this.setState({urlOptions: true});
|
||||
|
||||
// The Filter is a group
|
||||
if (uri['filter-type'] == "group") {
|
||||
filter.filterType = 1;
|
||||
filter.filterGroupId = uri["filter-code"];
|
||||
filterType="group";
|
||||
filterCode = uri["filter-code"];
|
||||
}
|
||||
|
||||
// The Filter is talkgroups
|
||||
if (uri['filter-type'] == 'talkgroup') {
|
||||
const tg = uri["filter-code"].split(',').map(Number);
|
||||
filter.filterType=2;
|
||||
filter.filterTalkgroups=tg;
|
||||
filterType="talkgroup";
|
||||
filterCode = tg;
|
||||
}
|
||||
}
|
||||
|
||||
this.props.callActions.setFilter(filter);
|
||||
this.props.callActions.fetchCalls();
|
||||
this.props.talkgroupActions.fetchTalkgroups(this.props.shortName);
|
||||
this.props.groupActions.fetchGroups(this.props.shortName);
|
||||
this.props.systemActions.fetchSystems();
|
||||
this.setupSocket();
|
||||
|
||||
if (filter.live) {
|
||||
this.startSocket(this.props.shortName, filterType, filterCode, filter.filterStarred);
|
||||
}
|
||||
}
|
||||
compareArray(a1, a2) {
|
||||
var i = a1.length;
|
||||
while (i--) {
|
||||
if (a1[i] !== a2[i])
|
||||
return false;
|
||||
}
|
||||
return true
|
||||
}
|
||||
componentWillUpdate(nextProps) {
|
||||
if (!this.props.groups && nextProps.groups) {
|
||||
if (!this.state.urlOptions && (nextProps.groups.length > 0 )) {
|
||||
this.setState({
|
||||
groupVisible: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
const filterChanged = (nextProps.filterType !== this.props.filterType) || !this.compareArray(nextProps.filterTalkgroups, this.props.filterTalkgroups) || (nextProps.filterGroupId !== this.props.filterGroupId) || (nextProps.filterDate !== this.props.filterDate) || (nextProps.filterStarred !== this.props.filterStarred);
|
||||
if (filterChanged) {
|
||||
var typeString = 'all';
|
||||
var filterCode = '';
|
||||
switch (nextProps.filterType) {
|
||||
|
||||
case 1:
|
||||
typeString = 'group';
|
||||
filterCode = nextProps.filterGroupId;
|
||||
break;
|
||||
case 2:
|
||||
typeString = 'talkgroup';
|
||||
filterCode = nextProps.filterTalkgroups;
|
||||
break;
|
||||
}
|
||||
|
||||
this.updateUri(nextProps);
|
||||
if (this.props.live) {
|
||||
this.startSocket(this.props.shortName, typeString, filterCode, nextProps.filterStarred);
|
||||
}
|
||||
}
|
||||
|
||||
const callsChanged = nextProps.callsAllIds[0] !== this.props.callsAllIds[0];
|
||||
if (callsChanged) {
|
||||
const {contextRef} = this.state;
|
||||
this.scrollPos = contextRef.scrollTop;
|
||||
this.prevHeight = contextRef.clientHeight;
|
||||
this.autoScroll = true;
|
||||
}
|
||||
|
||||
if (this.state.callId && !this.state.callUrl) {
|
||||
const call = this.props.callsById[this.state.callId];
|
||||
if (call) {
|
||||
if (call.url) {
|
||||
this.setState({callUrl: call.url });
|
||||
} else {
|
||||
this.setState({callUrl: media_server + call.filename });
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
componentDidUpdate() {
|
||||
const {contextRef} = this.state
|
||||
//console.log( contextRef.clientHeight - this.prevHeight );
|
||||
if (this.autoScroll) {
|
||||
this.autoScroll = false;
|
||||
window.scrollBy(0, contextRef.clientHeight - this.prevHeight);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
|
||||
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
|
||||
render() {
|
||||
const {contextRef} = this.state
|
||||
const { sidebarOpened } = this.state
|
||||
var archiveLabel = "";
|
||||
if (this.props.filterDate) {
|
||||
const filterDate = new Date(this.props.filterDate);
|
||||
archiveLabel = filterDate.toLocaleDateString() + " " + filterDate.toLocaleTimeString()
|
||||
}
|
||||
|
||||
var callInfoHeader = "Call Info";
|
||||
const {sidebarVisible} = this.state
|
||||
|
||||
const currentCall = this.props.callsById[this.state.callId];
|
||||
|
||||
var callLink = ""
|
||||
var callDownload = ""
|
||||
if (currentCall) {
|
||||
if ((this.props.talkgroups) && this.props.talkgroups[currentCall.talkgroupNum]) {
|
||||
callInfoHeader = this.props.talkgroups[currentCall.talkgroupNum].description;
|
||||
}
|
||||
const callDate = new Date(currentCall.time);
|
||||
callLink = "/system/" + this.props.shortName + "?call-id=" + currentCall._id + "&time=" + (callDate.getTime()+1);
|
||||
if (currentCall.url) {
|
||||
callDownload = currentCall.url;
|
||||
} else {
|
||||
callDownload = media_server + currentCall.filename;
|
||||
}
|
||||
}
|
||||
var archive = freePlanArchive;
|
||||
if (this.props.system && (this.props.system.planType == proPlanValue)) {
|
||||
archive = proPlanArchive;
|
||||
}
|
||||
var filterLabel = "All"
|
||||
switch (this.props.filterType) {
|
||||
case 1:
|
||||
filterLabel = "Group"
|
||||
break;
|
||||
case 2:
|
||||
filterLabel = "Talkgroups"
|
||||
break;
|
||||
}
|
||||
/* if (this.props.calls) {
|
||||
var callRows = calls.map((call) =>
|
||||
<CallItem call={call} key={call.shortName}/>
|
||||
))
|
||||
}*/
|
||||
return (
|
||||
<div ref={this.handleContextRef}>
|
||||
<FilterModal shortName={this.props.shortName} open={this.state.filterVisible} onClose={this.handleFilterClose}/>
|
||||
<CalendarModal open={this.state.calendarVisible} onClose={this.handleCalendarClose} archive={archive} key={this.props.shortName}/>
|
||||
<GroupModal shortName={this.props.shortName} open={this.state.groupVisible} onClose={this.handleGroupClose}/>
|
||||
<SupportModal open={this.state.supportVisible} onClose={this.handleSupportClose}/>
|
||||
<Sidebar as={Menu} animation='overlay' inverted vertical visible={sidebarOpened}
|
||||
onClick={this.handlePusherClick} duration={250}>
|
||||
<Menu.Item>
|
||||
<span> </span><Icon name="close" onClick={this.handlePusherClick} inverted={true} link={true} size="large"/></Menu.Item>
|
||||
<Link to="/"><Menu.Item>Home</Menu.Item></Link>
|
||||
<Link to="/systems"><Menu.Item link>Systems</Menu.Item></Link>
|
||||
<Link to="/about"><Menu.Item link>About</Menu.Item></Link>
|
||||
</Sidebar>
|
||||
<Menu fixed="top">
|
||||
<Menu.Item onClick={this.handleSidebarToggle}>
|
||||
<Icon name='sidebar' />
|
||||
</Menu.Item>
|
||||
<Menu.Item name='filter-btn' onClick={this.handleFilterToggle}>
|
||||
<Icon name="filter"/>
|
||||
<span className="desktop-only">Filter</span>
|
||||
<Label horizontal={true} color="grey" className="desktop-only">{filterLabel}</Label>
|
||||
</Menu.Item>
|
||||
<Container className="desktop-only" textAlign='center' style={{fontSize:'1.5rem', paddingLeft: '1em', paddingTop: '.5em'}}>
|
||||
{this.props.system &&this.props.system.name}
|
||||
</Container>
|
||||
<Menu.Menu position="right">
|
||||
<Menu.Item name='archive-btn' onClick={this.handleCalendarToggle} active={!this.props.live}>
|
||||
<Icon name="calendar"/>
|
||||
<span className="desktop-only">
|
||||
{
|
||||
archiveLabel
|
||||
? archiveLabel
|
||||
: "Archive"
|
||||
}
|
||||
</span>
|
||||
</Menu.Item>
|
||||
<Menu.Item name='live-btn' onClick={this.handleLiveToggle} active={this.props.live}>
|
||||
<Icon name="unmute"/>
|
||||
<span className="desktop-only">Live</span>
|
||||
</Menu.Item>
|
||||
</Menu.Menu>
|
||||
</Menu>
|
||||
|
||||
|
||||
|
||||
|
||||
<Container className="main">
|
||||
<Sidebar.Pushable>
|
||||
<Sidebar.Pusher
|
||||
onClick={this.handlePusherClick}
|
||||
style={{ minHeight: '100vh' }}
|
||||
>
|
||||
<Waypoint onEnter={this.loadNewerCalls}/>
|
||||
<ListCalls callsAllIds={this.props.callsAllIds} callsById={this.props.callsById} activeCallId={this.state.callId} talkgroups={this.props.talkgroups} playCall={this.playCall} />
|
||||
</Sidebar.Pusher>
|
||||
</Sidebar.Pushable>
|
||||
<Waypoint onEnter={this.loadOlderCalls}/>
|
||||
<Rail position='right' className="desktop-only" dimmed={sidebarOpened}>
|
||||
<Sticky context={contextRef} offset={60}>
|
||||
<CallInfo call={currentCall} header={callInfoHeader} />
|
||||
</Sticky>
|
||||
</Rail>
|
||||
</Container>
|
||||
|
||||
<Menu fixed="bottom" compact inverted >
|
||||
<Menu.Item active={this.state.autoPlay} onClick={() => this.switchAutoPlay()}><Icon name="level up"/><span className="desktop-only">Autoplay</span></Menu.Item>
|
||||
<MediaPlayer ref={this.audioRef} call={currentCall} onEnded={this.callEnded}/>
|
||||
<Menu.Menu position="right" className="desktop-only">
|
||||
<Menu.Item onClick={this.handleSupportToggle}><Icon name="coffee"/>Support OpenMHz</Menu.Item>
|
||||
<Menu.Item><a href={callDownload}><Icon name="download"/>Download</a></Menu.Item>
|
||||
<Menu.Item><a href={callLink}><Icon name="at"/>Link</a></Menu.Item>
|
||||
</Menu.Menu>
|
||||
</Menu>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CallPlayer;
|
||||
@@ -1,45 +0,0 @@
|
||||
import { connect } from "react-redux"
|
||||
import * as callActionCreators from "./call-actions"
|
||||
import * as talkgroupActionCreators from "../Talkgroups/talkgroup-actions"
|
||||
import * as groupActionCreators from "../Group/group-actions"
|
||||
import * as systemActionCreators from "../System/system-actions"
|
||||
import { bindActionCreators } from 'redux'
|
||||
import CallPlayer from "./CallPlayer"
|
||||
|
||||
function mapStateToProps(state, props) {
|
||||
return {
|
||||
callsById: state.call.byId,
|
||||
callsAllIds: state.call.allIds,
|
||||
oldestCallTime: state.call.oldestCallTime,
|
||||
newestCallTime: state.call.newestCallTime,
|
||||
live: state.call.live,
|
||||
filterDate: state.call.filterDate,
|
||||
filterType: state.call.filterType,
|
||||
filterTalkgroups: state.call.filterTalkgroups,
|
||||
filterGroupId: state.call.filterGroupId,
|
||||
filterStarred: state.call.filterStarred,
|
||||
system: state.system.items.find((item) => item.shortName === props.match.params.shortName),
|
||||
groups: state.group.items[props.match.params.shortName],
|
||||
talkgroups: state.talkgroup.items[props.match.params.shortName],
|
||||
callsIsWaiting: state.call.isWaiting,
|
||||
systemIsWaiting: state.system.isWaiting,
|
||||
talkgroupsIsWaiting: state.talkgroup.isWaiting,
|
||||
groupsIsWaiting: state.group.isWaiting,
|
||||
shortName: props.match.params.shortName
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
groupActions: bindActionCreators(groupActionCreators, dispatch),
|
||||
systemActions: bindActionCreators(systemActionCreators, dispatch),
|
||||
callActions: bindActionCreators(callActionCreators, dispatch),
|
||||
talkgroupActions: bindActionCreators(talkgroupActionCreators, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(CallPlayer)
|
||||
@@ -1,3 +0,0 @@
|
||||
.active.tab {
|
||||
min-height: 250px;
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
Icon,
|
||||
Tab,
|
||||
Header,
|
||||
Dropdown,
|
||||
Checkbox,
|
||||
Divider
|
||||
} from "semantic-ui-react";
|
||||
import "./FilterModal.css";
|
||||
|
||||
|
||||
class FilterModal extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
this.handleTabChange = this.handleTabChange.bind(this);
|
||||
this.toggleStarred = this.toggleStarred.bind(this);
|
||||
this.handleDone = this.handleDone.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
|
||||
this.state = {
|
||||
open: false,
|
||||
selectedTalkgroup: this.props.selectedTalkgroups,
|
||||
selectedGroup: this.props.selectedGroup,
|
||||
filterStarred: this.props.filterStarred,
|
||||
activeTab: this.props.activeTab
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handleTabChange = (e, data) => this.setState({activeTab: data.activeIndex});
|
||||
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
|
||||
toggleStarred = () => this.setState(prevState => ({ filterStarred: !prevState.filterStarred }));
|
||||
handleClose = () => this.props.onClose(false);
|
||||
handleDone(event) {
|
||||
switch(this.state.activeTab) {
|
||||
case 0:
|
||||
this.props.callActions.setAllFilter();
|
||||
this.props.onClose(true);
|
||||
break;
|
||||
case 1:
|
||||
if (this.state.selectedGroup) {
|
||||
this.props.callActions.setGroupFilter(this.state.selectedGroup);
|
||||
this.props.onClose(true);
|
||||
} else {
|
||||
this.props.onClose(false);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (this.state.selectedTalkgroup) {
|
||||
this.props.callActions.setTalkgroupFilter(this.state.selectedTalkgroup);
|
||||
this.props.onClose(true);
|
||||
} else {
|
||||
this.props.onClose(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (this.state.filterStarred != this.props.filterStarred) {
|
||||
this.props.callActions.setStarred(this.state.filterStarred);
|
||||
this.props.onClose(true);
|
||||
}
|
||||
|
||||
}
|
||||
componentWillUpdate(nextProps) {
|
||||
|
||||
const filterChanged = ((nextProps.selectedTalkgroups !== this.props.selectedTalkgroups) || (nextProps.selectedGroup !== this.props.selectedGroup));
|
||||
if (filterChanged) {
|
||||
this.setState({selectedTalkgroup: nextProps.selectedTalkgroups, selectedGroup: nextProps.selectedGroup, activeTab: nextProps.activeTab});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
var talkgroupList = [];
|
||||
if (this.props.talkgroups) {
|
||||
for (const num in this.props.talkgroups) {
|
||||
const talkgroup = this.props.talkgroups[num];
|
||||
var obj = {
|
||||
key: talkgroup.num,
|
||||
value: talkgroup.num,
|
||||
text: talkgroup.description
|
||||
}
|
||||
talkgroupList.push(obj);
|
||||
}
|
||||
}
|
||||
var groupList = [];
|
||||
if (this.props.groups) {
|
||||
for (const num in this.props.groups) {
|
||||
const group = this.props.groups[num];
|
||||
var obj = {
|
||||
key: group._id,
|
||||
value: group._id,
|
||||
text: group.groupName
|
||||
}
|
||||
groupList.push(obj);
|
||||
}
|
||||
}
|
||||
const panes = [
|
||||
{ menuItem: 'All', render: () =>
|
||||
{ return (
|
||||
<Tab.Pane attached={false}>
|
||||
<Header>All Calls</Header>
|
||||
<p>Dispaly all of the calls.</p>
|
||||
</Tab.Pane>
|
||||
)}},
|
||||
{ menuItem: 'Groups', render: () =>
|
||||
{ return (
|
||||
<Tab.Pane attached={false}>
|
||||
<Dropdown placeholder='Groups' fluid selection options={groupList} value={this.state.selectedGroup} name='selectedGroup' onChange={this.handleInputChange} />
|
||||
|
||||
</Tab.Pane>
|
||||
)} },
|
||||
{ menuItem: 'Talkgroups', render: () =>
|
||||
{ return (
|
||||
<Tab.Pane attached={false}>
|
||||
<Dropdown placeholder='Talkgroups' fluid multiple selection options={talkgroupList} value={this.state.selectedTalkgroup} name='selectedTalkgroup' onChange={this.handleInputChange} />
|
||||
</Tab.Pane>
|
||||
)} },
|
||||
]
|
||||
return (
|
||||
|
||||
<Modal open={this.props.open} onClose={this.handleClose} centered={false} size="tiny">
|
||||
<Modal.Header>Select a Filter</Modal.Header>
|
||||
<Modal.Content >
|
||||
<Modal.Description>
|
||||
<Tab menu={{ pointing: true }} panes={panes} defaultActiveIndex={this.props.activeTab} onTabChange={this.handleTabChange}/>
|
||||
<Divider/>
|
||||
<Checkbox label='Show only Starred calls' checked={this.state.filterStarred} name='filterStarred' onChange={this.toggleStarred}/>
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={this.handleDone} >
|
||||
<Icon name='checkmark' /> Done
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default FilterModal;
|
||||
@@ -1,34 +0,0 @@
|
||||
import { connect } from "react-redux"
|
||||
import * as talkgroupActionCreators from "../Talkgroups/talkgroup-actions"
|
||||
import * as groupActionCreators from "../Group/group-actions"
|
||||
import * as callActionCreators from "./call-actions"
|
||||
import { bindActionCreators } from 'redux'
|
||||
import FilterModal from "./FilterModal"
|
||||
|
||||
function mapStateToProps(state, props) {
|
||||
return {
|
||||
groups: state.group.items[props.shortName],
|
||||
talkgroups: state.talkgroup.items[props.shortName],
|
||||
talkgroupsIsWaiting: state.talkgroup.isWaiting,
|
||||
groupsIsWaiting: state.group.isWaiting,
|
||||
shortName: props.shortName,
|
||||
activeTab: state.call.filterType,
|
||||
selectedTalkgroups: state.call.filterTalkgroups,
|
||||
selectedGroup: state.call.filterGroupId,
|
||||
filterStarred: state.call.filterStarred
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
groupActions: bindActionCreators(groupActionCreators, dispatch),
|
||||
talkgroupActions: bindActionCreators(talkgroupActionCreators, dispatch),
|
||||
callActions: bindActionCreators(callActionCreators, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(FilterModal)
|
||||
@@ -1,85 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
Icon,
|
||||
Dropdown
|
||||
} from "semantic-ui-react";
|
||||
import "./FilterModal.css";
|
||||
|
||||
|
||||
class GroupModal extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
this.handleDone = this.handleDone.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
|
||||
this.state = {
|
||||
open: false,
|
||||
selectedGroup: false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
|
||||
handleClose = () => this.props.onClose(false);
|
||||
|
||||
handleDone(event) {
|
||||
|
||||
if (this.state.selectedGroup) {
|
||||
this.props.callActions.setGroupFilter(this.state.selectedGroup);
|
||||
this.props.onClose(true);
|
||||
} else {
|
||||
this.props.onClose(false);
|
||||
}
|
||||
|
||||
}
|
||||
componentWillUpdate(nextProps) {
|
||||
|
||||
const filterChanged = (nextProps.selectedGroup !== this.props.selectedGroup);
|
||||
if (filterChanged) {
|
||||
this.setState({ selectedGroup: nextProps.selectedGroup});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
|
||||
var groupList = [];
|
||||
if (this.props.groups) {
|
||||
for (const num in this.props.groups) {
|
||||
const group = this.props.groups[num];
|
||||
var obj = {
|
||||
key: group._id,
|
||||
value: group._id,
|
||||
text: group.groupName
|
||||
}
|
||||
groupList.push(obj);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<Modal open={this.props.open} onClose={this.handleClose} size='tiny' >
|
||||
<Modal.Header>Select a Group</Modal.Header>
|
||||
<Modal.Content >
|
||||
<Modal.Description>
|
||||
<p>Choose the type of calls you want to listen to</p>
|
||||
<Dropdown placeholder='Group...' fluid selection options={groupList} name='selectedGroup' onChange={this.handleInputChange} />
|
||||
|
||||
</Modal.Description>
|
||||
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={this.handleDone} >
|
||||
<Icon name='headphones' /> Listen
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default GroupModal;
|
||||
@@ -1,27 +0,0 @@
|
||||
import { connect } from "react-redux"
|
||||
import * as groupActionCreators from "../Group/group-actions"
|
||||
import * as callActionCreators from "./call-actions"
|
||||
import { bindActionCreators } from 'redux'
|
||||
import GroupModal from "./GroupModal"
|
||||
|
||||
function mapStateToProps(state, props) {
|
||||
return {
|
||||
groups: state.group.items[props.shortName],
|
||||
groupsIsWaiting: state.group.isWaiting,
|
||||
shortName: props.shortName,
|
||||
selectedGroup: state.call.filterGroupId
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
groupActions: bindActionCreators(groupActionCreators, dispatch),
|
||||
callActions: bindActionCreators(callActionCreators, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(GroupModal)
|
||||
@@ -1,65 +0,0 @@
|
||||
import React from "react";
|
||||
import {Link} from 'react-router-dom';
|
||||
import CallItem from "./CallItem";
|
||||
import {
|
||||
Container,
|
||||
Label,
|
||||
Rail,
|
||||
Sticky,
|
||||
Menu,
|
||||
Icon,
|
||||
Table,
|
||||
Sidebar
|
||||
} from "semantic-ui-react";
|
||||
import "./CallPlayer.css";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------------------
|
||||
class ListCalls extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
|
||||
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
|
||||
render() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<Table id="calls" unstackable>
|
||||
<Table.Header >
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Len</Table.HeaderCell>
|
||||
<Table.HeaderCell>Talkgroup</Table.HeaderCell>
|
||||
<Table.HeaderCell>Time</Table.HeaderCell>
|
||||
<Table.HeaderCell><Icon name='star' /></Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
|
||||
{this.props.callsAllIds.map((callId, index) => <CallItem activeCall={callId == this.props.activeCallId ? true : false} call={this.props.callsById[callId]} talkgroups={this.props.talkgroups} key={index} onClick={this.props.playCall}/>)}
|
||||
|
||||
</Table.Body>
|
||||
</Table>
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ListCalls;
|
||||
@@ -1,159 +0,0 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Menu,
|
||||
Icon,
|
||||
Progress,
|
||||
Label
|
||||
} from "semantic-ui-react";
|
||||
import ReactAudioPlayer from 'react-audio-player'
|
||||
|
||||
|
||||
class MediaPlayer extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.updatePlayProgress = this.updatePlayProgress.bind(this);
|
||||
this.handlePlay = this.handlePlay.bind(this);
|
||||
this.handlePause = this.handlePause.bind(this);
|
||||
this.playPause = this.playPause.bind(this);
|
||||
this.audioRef = React.createRef();
|
||||
this.state = {
|
||||
sourceIndex: 0,
|
||||
playProgress: 0,
|
||||
isPlaying: false
|
||||
}
|
||||
}
|
||||
handlePause = () => { this.setState({isPlaying: false}); }
|
||||
handlePlay = () => { this.setState({isPlaying: true}); }
|
||||
playPause = () => {const audio = this.audioRef.current.audioEl; if (this.state.isPlaying) {audio.pause();} else {audio.play();}}
|
||||
playSource(callUrl) {
|
||||
const audio = this.audioRef.current.audioEl;
|
||||
var onEnded = this.props.onEnded;
|
||||
audio.src=callUrl;
|
||||
|
||||
var playPromise = audio.play();
|
||||
|
||||
// In browsers that don’t yet support this functionality,
|
||||
// playPromise won’t be defined.
|
||||
if (playPromise !== undefined) {
|
||||
playPromise.then(function() {
|
||||
|
||||
}).catch(function(error) {
|
||||
console.log("Automatic playback failed: " + error);
|
||||
onEnded();
|
||||
// Show a UI element to let the user manually start playback.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
updatePlayProgress() {
|
||||
const audio = this.audioRef.current.audioEl;
|
||||
const {currentTime, duration} = audio;
|
||||
var call = this.props.call;
|
||||
|
||||
// this checks to see if it should display the next Source ID
|
||||
if (call && ((call.srcList.length - 1) >= (this.state.sourceIndex + 1)) && (currentTime > call.srcList[this.state.sourceIndex + 1].pos)) {
|
||||
this.setState({
|
||||
sourceIndex: this.state.sourceIndex + 1
|
||||
});
|
||||
}
|
||||
|
||||
// updates the play percentage progress and current playing time
|
||||
this.setState({
|
||||
playProgress: currentTime / duration * 100,
|
||||
playTime: Math.floor(currentTime)
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
if (nextProps.call && (nextProps.call != this.props.call)) {
|
||||
this.setState({
|
||||
sourceIndex: 0
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
componentDidUpdate() {
|
||||
}
|
||||
|
||||
|
||||
render(){
|
||||
var playEnabled = { "disabled": true }
|
||||
var sourceId = "-";
|
||||
var callUrl = null;
|
||||
if (this.props.call) {
|
||||
callUrl = media_server + this.props.call.filename;
|
||||
}
|
||||
if (this.props.call) {
|
||||
if (this.props.call.srcList.length > this.state.sourceIndex) {
|
||||
sourceId = this.props.call.srcList[this.state.sourceIndex].src;
|
||||
}
|
||||
playEnabled = {};
|
||||
}
|
||||
return (
|
||||
<Menu.Menu>
|
||||
<ReactAudioPlayer
|
||||
ref={this.audioRef}
|
||||
onPause={this.handlePause}
|
||||
onPlay={this.handlePlay}
|
||||
listenInterval={500}
|
||||
onListen={this.updatePlayProgress}
|
||||
onEnded={this.props.onEnded}
|
||||
autoPlay
|
||||
/>
|
||||
|
||||
|
||||
<Menu.Item onClick={this.playPause} >
|
||||
|
||||
{
|
||||
this.state.isPlaying
|
||||
? (<Icon name="pause"/>)
|
||||
: (<Icon name="play"/>)
|
||||
}
|
||||
</Menu.Item>
|
||||
<Menu.Item>
|
||||
<Progress inverted percent={this.state.playProgress}/>
|
||||
<Label color="black">
|
||||
{this.state.playTime}
|
||||
Sec
|
||||
</Label>
|
||||
<Label color="black" className="desktop-only">
|
||||
{sourceId}
|
||||
</Label>
|
||||
|
||||
</Menu.Item>
|
||||
</Menu.Menu>
|
||||
|
||||
|
||||
/* <Media>
|
||||
{
|
||||
mediaProps => <Menu.Menu>
|
||||
|
||||
<Menu.Item onClick={() => mediaProps.playPause()} {...playEnabled} >
|
||||
<Player src={callUrl} vendor="audio" onEnded={this.props.onEnded} autoPlay={true} onTimeUpdate={this.updatePlayProgress}/>
|
||||
|
||||
{
|
||||
mediaProps.isPlaying
|
||||
? (<Icon name="pause"/>)
|
||||
: (<Icon name="play"/>)
|
||||
}
|
||||
</Menu.Item><Menu.Item>
|
||||
<Progress inverted percent={this.state.playProgress}/>
|
||||
<Label color="black">
|
||||
{this.state.playTime}
|
||||
Sec
|
||||
</Label>
|
||||
<Label color="black" className="desktop-only">
|
||||
{sourceId}
|
||||
</Label>
|
||||
|
||||
</Menu.Item>
|
||||
</Menu.Menu>
|
||||
}
|
||||
</Media>*/
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
export default MediaPlayer;
|
||||
@@ -1,56 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
Icon,
|
||||
Dropdown
|
||||
} from "semantic-ui-react";
|
||||
import "./FilterModal.css";
|
||||
|
||||
|
||||
class SupportModal extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.handleDone = this.handleDone.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
|
||||
this.state = {
|
||||
open: false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
handleClose = () => this.props.onClose(false);
|
||||
|
||||
handleDone(event) {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
|
||||
<Modal open={this.props.open} onClose={this.handleClose} size='tiny' >
|
||||
<Modal.Header>Support OpenMHz</Modal.Header>
|
||||
<Modal.Content image>
|
||||
<Icon size='massive' name="coffee"/>
|
||||
<Modal.Description>
|
||||
<p>If OpenMHz brings you joy, think about becoming a supporter! It will cover hosting costs and help keep me focused on development.</p>
|
||||
<p>No worries if not, I am not having problems keeping it going.</p>
|
||||
<a href="https://github.com/sponsors/robotastic"><Button>
|
||||
Support
|
||||
</Button></a>
|
||||
</Modal.Description>
|
||||
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={this.handleDone} >
|
||||
Done
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default SupportModal;
|
||||
@@ -1,356 +0,0 @@
|
||||
import axios from "axios";
|
||||
//import { browserHistory } from "react-router"
|
||||
import * as types from "./call-constants";
|
||||
import { push } from 'connected-react-router';
|
||||
|
||||
const FETCH_SUCCESS_STATISTIC = "FETCH_SUCCESS_STATISTIC"
|
||||
|
||||
function beginFetchCallInfo() {
|
||||
return { type: types.FETCH_CALL_INFO };
|
||||
}
|
||||
|
||||
function fetchCallInfoSuccess(data) {
|
||||
return { type: types.FETCH_SUCCESS_CALL_INFO,
|
||||
data };
|
||||
}
|
||||
|
||||
function fetchCallInfoError(data) {
|
||||
return { type: types.FETCH_ERROR_CALL_INFO,
|
||||
data };
|
||||
}
|
||||
|
||||
function prependCall() {
|
||||
return { type: types.PREPEND_CALL };
|
||||
}
|
||||
|
||||
function beginAddStar() {
|
||||
return { type: types.ADD_STAR };
|
||||
}
|
||||
|
||||
function addStarSuccess(data) {
|
||||
return { type: types.ADD_STAR_SUCCESS, data };
|
||||
}
|
||||
|
||||
function addStarError(data) {
|
||||
return { type: types.ADD_STAR_ERROR, data };
|
||||
}
|
||||
|
||||
function beginFetchCall() {
|
||||
return { type: types.FETCH_CALL };
|
||||
}
|
||||
|
||||
function fetchCallSuccess(data) {
|
||||
return { type: types.FETCH_SUCCESS_CALL,
|
||||
data };
|
||||
}
|
||||
|
||||
|
||||
|
||||
function fetchOlderCallSuccess(data) {
|
||||
return { type: types.FETCH_SUCCESS_OLDER_CALL,
|
||||
data };
|
||||
}
|
||||
|
||||
function fetchNewerCallSuccess(data) {
|
||||
return { type: types.FETCH_SUCCESS_NEWER_CALL,
|
||||
data };
|
||||
}
|
||||
|
||||
function fetchCallError(data) {
|
||||
return { type: types.FETCH_ERROR_CALL,
|
||||
data };
|
||||
}
|
||||
|
||||
|
||||
export function changeUrl(url) {
|
||||
return dispatch => {
|
||||
dispatch(push(url));
|
||||
};
|
||||
}
|
||||
|
||||
// Example of an Async Action Creator
|
||||
// http://redux.js.org/docs/advanced/AsyncActions.html
|
||||
|
||||
function shouldFetchCalls(state, subreddit) {
|
||||
return true;
|
||||
if ((!state.call.items) || (state.call.items.length <1)) {
|
||||
return true
|
||||
} else if (state.call.isWaiting) {
|
||||
return false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function buildUrlParams(a) {
|
||||
return Object.keys(a).map(function(k) {
|
||||
return encodeURIComponent(k) + '=' + encodeURIComponent(a[k])
|
||||
}).join('&')
|
||||
}
|
||||
|
||||
function buildCallUrl(state, date, direction) {
|
||||
var params = {};
|
||||
var url = "";
|
||||
|
||||
|
||||
if ((typeof direction === 'string') && (typeof date === 'number')) {
|
||||
url = url + '/' + direction;
|
||||
params["time"] = date;
|
||||
} else {
|
||||
if (state.call.filterDate){
|
||||
url = url + '/older'
|
||||
params["time"] = state.call.filterDate;
|
||||
}
|
||||
}
|
||||
if (state.call.filterStarred) {
|
||||
params["filter-starred"] = true;
|
||||
}
|
||||
switch(state.call.filterType) {
|
||||
case 1:
|
||||
params["filter-type"] = "group";
|
||||
params["filter-code"] = state.call.filterGroupId;
|
||||
break;
|
||||
case 2:
|
||||
params["filter-type"] = "talkgroup";
|
||||
params["filter-code"] = state.call.filterTalkgroups;
|
||||
break;
|
||||
default:
|
||||
case 0:
|
||||
}
|
||||
/*
|
||||
|
||||
if (this.filterCode != "") {
|
||||
params["filter-code"] = this.filterCode;
|
||||
}
|
||||
|
||||
if (this.filterType == this.FilterType.Group) {
|
||||
params["filter-type"] = "group";
|
||||
params["filter-name"] = this.filterName;
|
||||
}
|
||||
|
||||
if (this.filterType == this.FilterType.Talkgroup) {
|
||||
params["filter-type"] = "talkgroup";
|
||||
}
|
||||
|
||||
if (this.filterType == this.FilterType.Unit) {
|
||||
params["filter-type"] = "unit";
|
||||
}
|
||||
|
||||
*/
|
||||
// url = startUp.backend_server + "/" + startUp.shortName.toLowerCase() + "/calls" + url + '?' + buildUrlParams(params);
|
||||
url = backend_server + "/" + state.call.shortName + "/calls" + url + '?' + buildUrlParams(params);
|
||||
console.log("Trying to fetch data from this url: " + url);
|
||||
return url;
|
||||
}
|
||||
|
||||
export function fetchNewerCalls( timestamp) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
if (shouldFetchCalls(state)) {
|
||||
dispatch(beginFetchCall());
|
||||
const url = buildCallUrl(state, timestamp, "newer")
|
||||
return axios
|
||||
.get(url)
|
||||
.then(response => {
|
||||
if ((response.data) && (response.data.calls.length >0)) {
|
||||
var data = {shortName: state.call.shortName, calls: response.data.calls}
|
||||
dispatch(fetchNewerCallSuccess(data));
|
||||
} else {
|
||||
dispatch(fetchCallError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function fetchOlderCalls( timestamp) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
if (shouldFetchCalls(state)) {
|
||||
dispatch(beginFetchCall());
|
||||
const url = buildCallUrl(state, timestamp, "older")
|
||||
return axios
|
||||
.get(url)
|
||||
.then(response => {
|
||||
if (response.data) {
|
||||
var data = {shortName: state.call.shortName, calls: response.data.calls}
|
||||
dispatch(fetchOlderCallSuccess(data));
|
||||
} else {
|
||||
dispatch(fetchCallError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function addStar( callId) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
dispatch(beginAddStar());
|
||||
|
||||
const url = backend_server + "/" + state.call.shortName.toLowerCase() + "/star/" + callId
|
||||
return axios
|
||||
.post(url,{}, {
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}).then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(addStarSuccess(response.data.call));
|
||||
} else {
|
||||
dispatch(addStarError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
|
||||
export function fetchCallInfo( callId) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
dispatch(beginFetchCallInfo());
|
||||
|
||||
const url = backend_server + "/" + state.call.shortName.toLowerCase() + "/call/" + callId
|
||||
return axios
|
||||
.get(url)
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(fetchCallInfoSuccess(response.data.call));
|
||||
} else {
|
||||
dispatch(fetchCallInfoError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export function fetchCalls() {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
if (shouldFetchCalls(state)) {
|
||||
dispatch(beginFetchCall());
|
||||
const url=buildCallUrl(state);
|
||||
return axios
|
||||
.get(url)
|
||||
.then(response => {
|
||||
if (response.data) {
|
||||
// check to make sure you actually got some calls back.
|
||||
if (response.data.calls.length > 0) {
|
||||
var data = {shortName: state.call.shortName, calls: response.data.calls}
|
||||
dispatch(fetchCallSuccess(data));
|
||||
} else {
|
||||
dispatch(fetchCallError());
|
||||
}
|
||||
} else {
|
||||
dispatch(fetchCallError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function addCall(call) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: types.UPSERT_CALL, data: call});
|
||||
};
|
||||
}
|
||||
|
||||
export function setStarred(starred) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: types.SET_STARRED_FILTER, data: starred});
|
||||
};
|
||||
}
|
||||
|
||||
export function setLive(live) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: types.SET_LIVE, data: live});
|
||||
};
|
||||
}
|
||||
|
||||
export function setCurrentCallId(callId) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: types.SET_CURRENT_CALL_ID, data: callId});
|
||||
};
|
||||
}
|
||||
|
||||
// should be as a unix timestamp
|
||||
export function setDateFilter(date) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: types.SET_DATE_FILTER, date: date});
|
||||
};
|
||||
}
|
||||
|
||||
export function setAllFilter() {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: types.SET_ALL_FILTER});
|
||||
};
|
||||
}
|
||||
|
||||
export function setTalkgroupFilter(tgs) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: types.SET_TALKGROUP_FILTER, talkgroups: tgs});
|
||||
};
|
||||
}
|
||||
|
||||
export function setGroupFilter(groupId) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: types.SET_GROUP_FILTER, groupId: groupId});
|
||||
};
|
||||
}
|
||||
|
||||
export function setShortName(shortName) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: types.SET_SHORT_NAME, data: shortName});
|
||||
};
|
||||
}
|
||||
|
||||
export function setFilter(filter) {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: types.SET_FILTER, data: filter});
|
||||
};
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
export const PREPEND_CALL = "PREPEND_CALL"
|
||||
export const ADD_STAR = "ADD_STAR"
|
||||
export const ADD_STAR_SUCCESS = "ADD_STAR_SUCCESS"
|
||||
export const ADD_STAR_ERROR = "ADD_STAR_ERROR"
|
||||
export const UPSERT_CALL = "UPSERT_CALL"
|
||||
export const FETCH_CALL = "FETCH_CALL"
|
||||
export const FETCH_SUCCESS_CALL = "FETCH_SUCCESS_CALL"
|
||||
export const FETCH_SUCCESS_NEWER_CALL = "FETCH_SUCCESS_NEWER_CALL"
|
||||
export const FETCH_SUCCESS_OLDER_CALL = "FETCH_SUCCESS_OLDER_CALL"
|
||||
export const FETCH_ERROR_CALL = "FETCH_ERROR_CALL"
|
||||
export const SET_SHORT_NAME = "SET_SHORT_NAME"
|
||||
export const FETCH_CALL_INFO = "FETCH_CALL_INFO"
|
||||
export const FETCH_SUCCESS_CALL_INFO = "FETCH_SUCCESS_CALL_INFO"
|
||||
export const FETCH_ERROR_CALL_INFO = "FETCH_ERROR_CALL_INFO"
|
||||
export const SET_ALL_FILTER = "SET_ALL_FILTER"
|
||||
export const SET_GROUP_FILTER = "SET_GROUP_FILTER"
|
||||
export const SET_TALKGROUP_FILTER = "SET_TALKGROUP_FILTER"
|
||||
export const SET_DATE_FILTER = "SET_DATE_FILTER"
|
||||
export const SET_FILTER = "SET_FILTER"
|
||||
export const SET_STARRED_FILTER = "SET_STARRED_FILTER"
|
||||
export const SET_LIVE = "SET_LIVE"
|
||||
export const SET_CURRENT_CALL_ID = "SET_CURRENT_CALL_ID"
|
||||
@@ -1,227 +0,0 @@
|
||||
import {
|
||||
FETCH_CALL_INFO,
|
||||
FETCH_SUCCESS_CALL_INFO,
|
||||
FETCH_ERROR_CALL_INFO,
|
||||
FETCH_CALL,
|
||||
PREPEND_CALL,
|
||||
UPSERT_CALL,
|
||||
FETCH_SUCCESS_CALL,
|
||||
FETCH_SUCCESS_OLDER_CALL,
|
||||
FETCH_SUCCESS_NEWER_CALL,
|
||||
FETCH_ERROR_CALL,
|
||||
SET_SHORT_NAME,
|
||||
SET_ALL_FILTER,
|
||||
SET_GROUP_FILTER,
|
||||
SET_TALKGROUP_FILTER,
|
||||
SET_DATE_FILTER,
|
||||
SET_STARRED_FILTER,
|
||||
SET_FILTER,
|
||||
SET_CURRENT_CALL_ID,
|
||||
SET_LIVE,
|
||||
ADD_STAR_SUCCESS
|
||||
} from "./call-constants";
|
||||
|
||||
function updateObject(oldObject, newValues) {
|
||||
// Encapsulate the idea of passing a new object as the first parameter
|
||||
// to Object.assign to ensure we correctly copy data instead of mutating
|
||||
return Object.assign({}, oldObject, newValues);
|
||||
}
|
||||
|
||||
function updateCallInItems(calls, action) {
|
||||
return calls.map( (item) => {
|
||||
if (item._id !== action.data._id) {
|
||||
// This isn't the item we care about - keep it as-is
|
||||
return item;
|
||||
}
|
||||
|
||||
// Otherwise, this is the one we want - return an updated value
|
||||
return {
|
||||
...action.data
|
||||
};
|
||||
});
|
||||
}
|
||||
/*
|
||||
function updateObjectInArray(array, action) {
|
||||
return array.map((item, index) => {
|
||||
if (index !== action.index) {
|
||||
// This isn't the item we care about - keep it as-is
|
||||
return item
|
||||
}
|
||||
|
||||
// Otherwise, this is the one we want - return an updated value
|
||||
return {
|
||||
...item,
|
||||
...action.item
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function updateObjectInArray(array, shortName, updatedItem) {
|
||||
return array.map( item => {
|
||||
if(item.shortName !== shortName) {
|
||||
// This isn't the item we care about - keep it as-is
|
||||
return item;
|
||||
}
|
||||
|
||||
// Otherwise, this is the one we want - return an updated value
|
||||
return {
|
||||
...item,
|
||||
...updatedItem
|
||||
};
|
||||
});
|
||||
}
|
||||
*/
|
||||
const now = new Date();
|
||||
const call = (
|
||||
state = {
|
||||
isWaiting: false,
|
||||
shortName: "",
|
||||
byId: {},
|
||||
allIds: [],
|
||||
oldestCallTime: now,
|
||||
newestCallTime: now,
|
||||
filterType: 0,
|
||||
filterTalkgroups: [],
|
||||
filterGroupId: false,
|
||||
filterDate: false,
|
||||
filterStarred: false,
|
||||
currentCallId: false,
|
||||
live: false
|
||||
},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
|
||||
case SET_CURRENT_CALL_ID:
|
||||
return Object.assign({}, state, {currentCallId: action.data});
|
||||
|
||||
case SET_FILTER:
|
||||
{
|
||||
var filter={};
|
||||
filter.live = !action.data.live ? state.live : action.data.live;
|
||||
filter.filterStarred = !action.data.filterStarred ? false : action.data.filterStarred;
|
||||
filter.filterType = !action.data.filterType ? 0 : action.data.filterType;
|
||||
filter.filterTalkgroups = !action.data.filterTalkgroups ? [] : action.data.filterTalkgroups;
|
||||
filter.filterGroupId = !action.data.filterGroupId ? false : action.data.filterGroupId;
|
||||
filter.filterDate = !action.data.filterDate ? 0 : action.data.filterDate;
|
||||
return Object.assign({}, state, {
|
||||
live: action.data.live,
|
||||
filterType: action.data.filterType,
|
||||
filterTalkgroups: action.data.filterTalkgroups,
|
||||
filterGroupId: action.data.filterGroupId,
|
||||
filterDate: action.data.filterDate,
|
||||
filterStarred: action.data.filterStarred
|
||||
});
|
||||
}
|
||||
|
||||
case SET_LIVE:
|
||||
return Object.assign({}, state, {live: action.data});
|
||||
|
||||
case SET_SHORT_NAME:
|
||||
return Object.assign({}, state, {shortName: action.data});
|
||||
|
||||
case SET_DATE_FILTER:
|
||||
return Object.assign({}, state, {filterDate: action.date});
|
||||
|
||||
case SET_STARRED_FILTER:
|
||||
return Object.assign({}, state, {filterStarred: action.data});
|
||||
|
||||
case SET_ALL_FILTER:
|
||||
return Object.assign({}, state, {filterType: 0, filterTalkgroups:[], filterGroupId: false});
|
||||
|
||||
case SET_GROUP_FILTER:
|
||||
return Object.assign({}, state, {filterType: 1, filterTalkgroups:[], filterGroupId: action.groupId});
|
||||
|
||||
case SET_TALKGROUP_FILTER:
|
||||
return Object.assign({}, state, {filterType: 2, filterTalkgroups: action.talkgroups, filterGroupId: false});
|
||||
|
||||
case FETCH_CALL_INFO:
|
||||
case FETCH_CALL:
|
||||
return Object.assign({}, state, { isWaiting: true});
|
||||
|
||||
case FETCH_SUCCESS_CALL_INFO:
|
||||
return Object.assign({}, state, {
|
||||
byId: {...state.byId, [action.data._id]: action.data },
|
||||
isWaiting: false
|
||||
|
||||
});
|
||||
|
||||
case ADD_STAR_SUCCESS:
|
||||
return Object.assign({}, state,{
|
||||
byId: {...state.byId, [action.data._id]: action.data },
|
||||
isWaiting: false
|
||||
|
||||
});
|
||||
// This will set the items to the rx data
|
||||
case FETCH_SUCCESS_CALL:
|
||||
{
|
||||
const byId = action.data.calls.reduce((acc, call) => ({ ...acc, [call._id]: call }), {});
|
||||
const allIds = action.data.calls.map(call => call._id);
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
shortName: action.data.shortName,
|
||||
newestCallTime: new Date(action.data.calls[0].time),
|
||||
oldestCallTime: new Date(action.data.calls[action.data.calls.length-1].time),
|
||||
byId: { ...byId},
|
||||
allIds: [...allIds]
|
||||
});
|
||||
}
|
||||
|
||||
case UPSERT_CALL:
|
||||
if (typeof state.byId[action.data._id] !== "undefined") {
|
||||
return Object.assign({}, state, {
|
||||
byId: {...state.byId, [action.data._id]: action.data },
|
||||
});
|
||||
} else {
|
||||
return Object.assign({}, state, {
|
||||
newestCallTime: new Date(action.data.time),
|
||||
byId: {[action.data._id]: action.data, ...state.byId},
|
||||
allIds: [action.data._id, ...state.allIds]
|
||||
});
|
||||
}
|
||||
|
||||
case PREPEND_CALL:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
newestCallTime: new Date(action.data.time),
|
||||
byId: {[action.data._id]: action.data, ...state.byId},
|
||||
allIds: [action.data._id, ...state.allIds]
|
||||
});
|
||||
|
||||
// This will add the array of Calls in the data to the beginning of the existing array of Calls
|
||||
case FETCH_SUCCESS_NEWER_CALL:
|
||||
const byId = action.data.calls.reduce((acc, call) => ({ ...acc, [call._id]: call }), {});
|
||||
const allIds = action.data.calls.map(call => call._id).reverse();
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
shortName: action.data.shortName,
|
||||
newestCallTime: new Date(action.data.calls[action.data.calls.length-1].time),
|
||||
byId: {...state.byId, ...byId},
|
||||
allIds: [...allIds, ...state.allIds]
|
||||
});
|
||||
|
||||
// This will add the array of Calls in the data to the end of the existing array of Calls
|
||||
case FETCH_SUCCESS_OLDER_CALL:
|
||||
{
|
||||
const byId = action.data.calls.reduce((acc, call) => ({ ...acc, [call._id]: call }), {});
|
||||
const allIds = action.data.calls.map(call => call._id);
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
shortName: action.data.shortName,
|
||||
oldestCallTime: new Date(action.data.calls[action.data.calls.length-1].time),
|
||||
byId: {...state.byId, ...byId},
|
||||
allIds: [ ...state.allIds, ...allIds]
|
||||
});
|
||||
}
|
||||
case FETCH_ERROR_CALL_INFO:
|
||||
case FETCH_ERROR_CALL:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default call;
|
||||
@@ -1,177 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Form,
|
||||
Grid,
|
||||
Rail,
|
||||
Segment,
|
||||
List,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Icon,
|
||||
Table
|
||||
} from "semantic-ui-react";
|
||||
|
||||
// ----------------------------------------------------
|
||||
const requestMessageStyle = {
|
||||
color: "red"
|
||||
};
|
||||
|
||||
// ----------------------------------------------------
|
||||
class CreateGroup extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
// Set initial state
|
||||
this.state = {
|
||||
groupName: "",
|
||||
group: {
|
||||
name: "",
|
||||
items: []
|
||||
},
|
||||
talkgroups: (this.props.talkgroups ? this.props.talkgroups : [])
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
//this.props.groupActions.fetchGroups(this.props.shortName);
|
||||
|
||||
this.props.talkgroupActions.fetchTalkgroups(this.props.shortName);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.setState({ talkgroups: nextProps.talkgroups });
|
||||
}
|
||||
|
||||
removeTalkgroup(event, index) {
|
||||
const item = {
|
||||
num: this.state.group.items[index].num,
|
||||
description: this.state.group.items[index].description
|
||||
};
|
||||
|
||||
const talkgroups = this.state.talkgroups.slice(0); // copy
|
||||
|
||||
for (var i = 0, len = talkgroups.length; i < len; i++) {
|
||||
if (item.num < talkgroups[i].num) {
|
||||
talkgroups.splice(i, 0, item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.setState({
|
||||
group: { items: this.state.group.items
|
||||
.slice(0, index)
|
||||
.concat(this.state.group.items.slice(index + 1))},
|
||||
talkgroups: talkgroups
|
||||
});
|
||||
}
|
||||
|
||||
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
|
||||
|
||||
addTalkgroup(event, index) {
|
||||
const item = {
|
||||
num: this.state.talkgroups[index].num,
|
||||
description: this.state.talkgroups[index].description
|
||||
};
|
||||
|
||||
this.setState({
|
||||
group: { items: this.state.group.items.concat(item) },
|
||||
talkgroups: this.state.talkgroups
|
||||
.slice(0, index)
|
||||
.concat(this.state.talkgroups.slice(index + 1))
|
||||
});
|
||||
}
|
||||
handleSubmit() {
|
||||
var talkgroupNums = this.state.group.items.map(a => a.num);
|
||||
const data = {shortName: this.props.shortName, groupName: this.state.groupName, talkgroups: JSON.stringify(talkgroupNums) }
|
||||
this.props.groupActions.createGroup(data).then(requestMessage => {
|
||||
if (requestMessage) {
|
||||
// report to the user is there was a problem during registration
|
||||
this.setState({
|
||||
requestMessage
|
||||
});
|
||||
} else {
|
||||
this.props.groupActions.changeUrl("/system/" + this.props.shortName + "/2");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const talkgroups = this.state.talkgroups;
|
||||
const group = this.state.group.items;
|
||||
const railStyle = {
|
||||
top: "15px",
|
||||
minHeight: "200px"
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
{talkgroups.length ? (
|
||||
<Container text>
|
||||
<Message
|
||||
icon='upload'
|
||||
header='Import Talkgroups'
|
||||
content='Before you can create groups of Talkgroups, you need to import some talkgroups.'
|
||||
/>
|
||||
</Container>
|
||||
) : (
|
||||
<Grid container columns={2}>
|
||||
<Grid.Column>
|
||||
<Table>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Number</Table.HeaderCell>
|
||||
|
||||
<Table.HeaderCell>Description</Table.HeaderCell>
|
||||
<Table.HeaderCell />
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{talkgroups.map((talkgroup, i) => (
|
||||
<Table.Row link="true" key={talkgroup.num + "-" + i}>
|
||||
<Table.Cell>{talkgroup.num}</Table.Cell>
|
||||
|
||||
<Table.Cell>{talkgroup.description}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Icon
|
||||
link
|
||||
name="plus"
|
||||
onClick={e => this.addTalkgroup(e, i)}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
<Rail position="right">
|
||||
<Segment style={railStyle}>
|
||||
<Input action={{ onClick: this.handleSubmit, content: 'Save' }} name="groupName" onChange={this.handleInputChange} placeholder="Group Name" />
|
||||
<List divided relaxed>
|
||||
{group.map((talkgroup, i) => (
|
||||
<List.Item key={"Group-" + i}>
|
||||
<List.Icon
|
||||
name="remove"
|
||||
verticalAlign="middle"
|
||||
link={true}
|
||||
onClick={e => this.removeTalkgroup(e, i)}
|
||||
/>
|
||||
<List.Content>
|
||||
{talkgroup.description}
|
||||
|
||||
</List.Content>
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
</Segment>
|
||||
</Rail>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CreateGroup;
|
||||
@@ -1,25 +0,0 @@
|
||||
import { connect } from "react-redux"
|
||||
import * as groupActionCreators from "./group-actions"
|
||||
import * as talkgroupActionCreators from "../Talkgroups/talkgroup-actions"
|
||||
import { bindActionCreators } from 'redux'
|
||||
import CreateGroup from "./CreateGroup"
|
||||
|
||||
//groups: state.group.items[props.match.params.shortName],
|
||||
function mapStateToProps(state,props) {
|
||||
return {
|
||||
talkgroups: state.talkgroup.items[props.match.params.shortName],
|
||||
shortName: props.match.params.shortName
|
||||
}
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return {
|
||||
groupActions: bindActionCreators(groupActionCreators,dispatch),
|
||||
talkgroupActions: bindActionCreators(talkgroupActionCreators, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(CreateGroup)
|
||||
@@ -1,77 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Form,
|
||||
Grid,
|
||||
Segment,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Icon,
|
||||
Table
|
||||
} from "semantic-ui-react";
|
||||
|
||||
// ----------------------------------------------------
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------------------
|
||||
class ListGroups extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
}
|
||||
|
||||
|
||||
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
|
||||
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
|
||||
render() {
|
||||
const groups = this.props.groups;
|
||||
if (groups) {
|
||||
return (
|
||||
|
||||
<Table >
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Name</Table.HeaderCell>
|
||||
<Table.HeaderCell>Talkgroup Count</Table.HeaderCell>
|
||||
<Table.HeaderCell></Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{groups.map((group, i) => (
|
||||
<Table.Row key={ "Group-" + i}>
|
||||
<Table.Cell>{group.groupName}</Table.Cell>
|
||||
<Table.Cell>{group.talkgroups.length}</Table.Cell>
|
||||
<Table.Cell><Icon
|
||||
name="up arrow"
|
||||
link={true}
|
||||
onClick={e => this.props.reorderGroup(i, i-1)}
|
||||
/><Icon
|
||||
name="down arrow"
|
||||
link={true}
|
||||
onClick={e => this.props.reorderGroup(i, i+1)}
|
||||
/><Icon
|
||||
name="remove"
|
||||
link={true}
|
||||
onClick={e => this.props.deleteGroup(group._id)}
|
||||
/>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ListGroups;
|
||||
@@ -1,73 +0,0 @@
|
||||
import axios from "axios";
|
||||
//import { browserHistory } from "react-router"
|
||||
import * as types from "./group-constants";
|
||||
import { push } from 'connected-react-router';
|
||||
|
||||
// "Log In" action creators
|
||||
|
||||
|
||||
function beginFetchGroup() {
|
||||
return { type: types.FETCH_GROUP };
|
||||
}
|
||||
|
||||
function fetchGroupSuccess(data) {
|
||||
return {
|
||||
type: types.FETCH_SUCCESS_GROUP,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
function fetchGroupError(data) {
|
||||
return {
|
||||
type: types.FETCH_ERROR_GROUP,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function changeUrl(url) {
|
||||
return dispatch => {
|
||||
dispatch(push(url));
|
||||
};
|
||||
}
|
||||
|
||||
// Example of an Async Action Creator
|
||||
// http://redux.js.org/docs/advanced/AsyncActions.html
|
||||
|
||||
|
||||
function shouldFetchGroups(state,shortName) {
|
||||
if (!state.group.items || !state.group.items[shortName]) {
|
||||
return true;
|
||||
} else if (state.group.isWaiting) {
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function fetchGroups(shortName) {
|
||||
return (dispatch, getState) => {
|
||||
if (shouldFetchGroups(getState(), shortName)) {
|
||||
dispatch(beginFetchGroup());
|
||||
return axios
|
||||
.get(backend_server + "/" + shortName + "/groups")
|
||||
.then(response => {
|
||||
if (true) { //response.data.success) {
|
||||
const data = { shortName: shortName, groups: response.data };
|
||||
dispatch(fetchGroupSuccess(data));
|
||||
} else {
|
||||
dispatch(fetchGroupError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export const FETCH_GROUP = "FETCH_GROUP"
|
||||
export const FETCH_SUCCESS_GROUP = "FETCH_SUCCESS_GROUP"
|
||||
export const FETCH_ERROR_GROUP = "FETCH_ERROR_GROUP"
|
||||
@@ -1,62 +0,0 @@
|
||||
import {
|
||||
FETCH_GROUP,
|
||||
FETCH_SUCCESS_GROUP,
|
||||
FETCH_ERROR_GROUP
|
||||
} from "./group-constants";
|
||||
|
||||
function updateObject(oldObject, newValues) {
|
||||
// Encapsulate the idea of passing a new object as the first parameter
|
||||
// to Object.assign to ensure we correctly copy data instead of mutating
|
||||
return Object.assign({}, oldObject, newValues);
|
||||
}
|
||||
|
||||
function updateItemInArray(array, itemId, updateItemCallback) {
|
||||
const updatedItems = array.map(item => {
|
||||
if(item.id !== itemId) {
|
||||
// Since we only want to update one item, preserve all others as they are now
|
||||
return item;
|
||||
}
|
||||
|
||||
// Use the provided callback to create an updated item
|
||||
const updatedItem = updateItemCallback(item);
|
||||
return updatedItem;
|
||||
});
|
||||
|
||||
return updatedItems;
|
||||
}
|
||||
|
||||
const group = (
|
||||
state = {
|
||||
orderChange: false,
|
||||
isWaiting: false,
|
||||
items: {}},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
|
||||
|
||||
case FETCH_GROUP:
|
||||
return Object.assign({}, state, { isWaiting: true});
|
||||
|
||||
|
||||
case FETCH_SUCCESS_GROUP:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
items: {
|
||||
...state.items,
|
||||
[action.data.shortName]: action.data.groups
|
||||
}
|
||||
});
|
||||
|
||||
case FETCH_ERROR_GROUP:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false
|
||||
});
|
||||
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default group;
|
||||
@@ -1,82 +0,0 @@
|
||||
@font-face {
|
||||
font-family: didact;
|
||||
src: url("../../resources/DidactGothic-Regular.ttf");
|
||||
}
|
||||
|
||||
#menu-bar {
|
||||
|
||||
background-color: #365474;
|
||||
}
|
||||
|
||||
.header {
|
||||
font-family: didact !important;
|
||||
}
|
||||
|
||||
#header-bg {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#hero {
|
||||
height: 725px;
|
||||
}
|
||||
|
||||
.z-0 {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.z-5 {
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.absolute {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.static-gradient-bg {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB8AAAAfBAMAAADtgAsKAAAAD1BMVEUAAAAAAAAAAAAAAAAAAABPDueNAAAABXRSTlMVGRsSHUz7Vs0AAAErSURBVCjPHY+BDeUgDEPttAM4wACk/QMEdYHSu/1nOjghQIodPxnRfuPnfWQO0F240K5TvRLKdYXvdAIH8ej+0yTEsh1CUYplr+j91ouhCl3WhEnpNuACvW3vUAgic8woHoA/ojO5MN6uN9EPdIJpRu7IBfTN5+I4aIAowPxSu74OAV7gfFuLJVQeAKS41Kes5k4H6tW8uML0yJcjHTE3WepIAfx+8JhJ6aCeRMRK7EQs9RjPxhheOhZL+P//zh8TW4gKArKGY2m3H1xZgq0ykzzdh9vrWPu4c1eNc0bs+suKQAKmtg4gT4kqleFxCzT1zftKUzmYi0CqWynyr4YSwhq2q/s8pf9l+oD9nbeZAXRAlJ0wFMd43ICcpTanOIQXEDTjdK54eqlV+Q9M1SO8F3P79wAAAABJRU5ErkJggg==");
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
/*
|
||||
.static-gradient.blue {
|
||||
background: -webkit-gradient(linear,left top,right top,from(#2754F0),to(#86ACFE));
|
||||
background: linear-gradient(to right, #2754F0, #86ACFE);
|
||||
}*/
|
||||
|
||||
.static-gradient.blue {
|
||||
background: -webkit-gradient(linear,left top,right top,from(#9f0000),to(#ff7543));
|
||||
background: linear-gradient(to right, #9f0000, #ff7543);
|
||||
}
|
||||
|
||||
.static-gradient {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
-webkit-transform: skewY(-7deg);
|
||||
transform: skewY(-7deg);
|
||||
-webkit-transform-origin: 0;
|
||||
transform-origin: 0;
|
||||
border-bottom: 2px solid rgba(128,128,128,.5);
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#menu-bg {
|
||||
|
||||
background-color: #3b5f85;
|
||||
|
||||
}
|
||||
|
||||
#footer {
|
||||
background: -webkit-gradient(linear, bottom, top,from(#9f0000),to(#ff7543));
|
||||
background: linear-gradient(to top, #9f0000, #ff7543);
|
||||
}
|
||||
|
||||
.ui.button {
|
||||
|
||||
|
||||
}
|
||||
@@ -1,382 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Link } from 'react-router-dom'
|
||||
import SystemCard from "../System/SystemCard";
|
||||
import "./Main.css";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Container,
|
||||
Divider,
|
||||
Grid,
|
||||
Header,
|
||||
Icon,
|
||||
Image,
|
||||
List,
|
||||
Menu,
|
||||
Responsive,
|
||||
Segment,
|
||||
Sidebar,
|
||||
Transition,
|
||||
Visibility,
|
||||
|
||||
} from 'semantic-ui-react'
|
||||
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
/* Heads up! HomepageHeading uses inline styling, however it's not the best practice. Use CSS or styled components for
|
||||
* such things.
|
||||
*/
|
||||
const HomepageHeading = ({ mobile }) => (
|
||||
<Container text style={{paddingBottom: '0px'}} >
|
||||
<Header
|
||||
as='h1'
|
||||
content='Share the Air'
|
||||
style={{
|
||||
fontSize: mobile ? '2em' : '4em',
|
||||
color: "#FFF",
|
||||
fontWeight: 'normal',
|
||||
marginBottom: 0,
|
||||
marginTop: mobile ? '1.5em' : '0em',
|
||||
}}
|
||||
/>
|
||||
<Header
|
||||
as='h2'
|
||||
content='Listen to Police and Fire radio from across the US'
|
||||
style={{
|
||||
fontSize: mobile ? '1.5em' : '1.7em',
|
||||
color: "#FFF",
|
||||
fontWeight: 'normal',
|
||||
marginTop: mobile ? '0.5em' : '1.5em',
|
||||
marginBottom: mobile ? '0.5em' : '0em',
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
|
||||
/*
|
||||
HomepageHeading.propTypes = {
|
||||
mobile: PropTypes.bool,
|
||||
}
|
||||
*/
|
||||
/* Heads up!
|
||||
* Neither Semantic UI nor Semantic UI React offer a responsive navbar, however, it can be implemented easily.
|
||||
* It can be more complicated, but you can create really flexible markup.
|
||||
*/
|
||||
class DesktopContainer extends Component {
|
||||
state = {}
|
||||
|
||||
hideFixedMenu = () => this.setState({ fixed: false })
|
||||
showFixedMenu = () => this.setState({ fixed: true })
|
||||
|
||||
render() {
|
||||
const { children } = this.props
|
||||
const { fixed } = this.state
|
||||
|
||||
return (
|
||||
<Responsive minWidth={Responsive.onlyTablet.minWidth}>
|
||||
<div className="relative">
|
||||
<div className="static-gradient blue absolute z-0" >
|
||||
<div className="static-gradient-bg absolute z-5"></div>
|
||||
</div>
|
||||
<Segment
|
||||
textAlign='center'
|
||||
style={{ padding: '1em 0em',
|
||||
borderBottom: '0px',
|
||||
boxShadow: 'none',
|
||||
height: '400px'
|
||||
}}
|
||||
vertical
|
||||
>
|
||||
|
||||
<Menu
|
||||
fixed={fixed ? 'top' : null}
|
||||
inverted={true}
|
||||
pointing={false}
|
||||
secondary={true}
|
||||
size='huge'
|
||||
style={{ marginRight: '0px'}}
|
||||
>
|
||||
<Container>
|
||||
<Menu.Item ><Header as='h3' inverted>{site_name}</Header></Menu.Item>
|
||||
<Link to="/systems"><Menu.Item link >Listen</Menu.Item></Link>
|
||||
<Link to="/about"><Menu.Item link >About</Menu.Item></Link>
|
||||
</Container>
|
||||
</Menu>
|
||||
<HomepageHeading />
|
||||
</Segment>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
</Responsive>
|
||||
)
|
||||
}
|
||||
}
|
||||
/*
|
||||
DesktopContainer.propTypes = {
|
||||
children: PropTypes.node,
|
||||
}
|
||||
*/
|
||||
class MobileContainer extends Component {
|
||||
state = {}
|
||||
|
||||
handlePusherClick = () => {
|
||||
const { sidebarOpened } = this.state
|
||||
|
||||
if (sidebarOpened) this.setState({ sidebarOpened: false })
|
||||
}
|
||||
|
||||
handleToggle = () => this.setState({ sidebarOpened: !this.state.sidebarOpened })
|
||||
|
||||
render() {
|
||||
const { children } = this.props
|
||||
const { sidebarOpened } = this.state
|
||||
|
||||
return (
|
||||
<Responsive maxWidth={Responsive.onlyMobile.maxWidth}>
|
||||
<Sidebar.Pushable>
|
||||
<Sidebar as={Menu} animation='uncover' inverted vertical visible={sidebarOpened} id="menu-bar">
|
||||
<Menu.Item active>
|
||||
Home
|
||||
</Menu.Item>
|
||||
<Menu.Item ><Link to="/systems">Systems</Link></Menu.Item>
|
||||
<Menu.Item ><Link to="/about">About</Link></Menu.Item>
|
||||
</Sidebar>
|
||||
|
||||
<Sidebar.Pusher
|
||||
dimmed={sidebarOpened}
|
||||
onClick={this.handlePusherClick}
|
||||
style={{ minHeight: '100vh' }}
|
||||
>
|
||||
<div className='relative'>
|
||||
<div className="static-gradient blue absolute z-0">
|
||||
<div className="static-gradient-bg absolute"></div>
|
||||
</div>
|
||||
<Segment
|
||||
textAlign='center'
|
||||
style={{ minHeight: 350, padding: '1em 0em',
|
||||
borderBottom: '0px',
|
||||
boxShadow: 'none',
|
||||
height: '300px'}}
|
||||
vertical
|
||||
>
|
||||
|
||||
<Container>
|
||||
|
||||
<Menu inverted secondary size='large'>
|
||||
<Menu.Item onClick={this.handleToggle}>
|
||||
<Icon name='sidebar' />
|
||||
</Menu.Item>
|
||||
<Menu.Item header>{site_name}</Menu.Item>
|
||||
</Menu>
|
||||
</Container>
|
||||
<HomepageHeading mobile />
|
||||
</Segment>
|
||||
</div>
|
||||
{children}
|
||||
</Sidebar.Pusher>
|
||||
</Sidebar.Pushable>
|
||||
</Responsive>
|
||||
)
|
||||
}
|
||||
}
|
||||
/*
|
||||
MobileContainer.propTypes = {
|
||||
children: PropTypes.node,
|
||||
}
|
||||
*/
|
||||
const ResponsiveContainer = ({ children }) => (
|
||||
<div>
|
||||
<DesktopContainer>{children}</DesktopContainer>
|
||||
<MobileContainer>{children}</MobileContainer>
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
// ----------------------------------------------------
|
||||
const requestMessageStyle = {
|
||||
color: "red"
|
||||
};
|
||||
|
||||
function SystemList(props) {
|
||||
const systems = props.systems;
|
||||
const listItems = numbers.map((number) =>
|
||||
<li>{number}</li>
|
||||
);
|
||||
return (
|
||||
<ul>{listItems}</ul>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
class Main extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.advanceSystem = this.advanceSystem.bind(this);
|
||||
var activeSystems = []
|
||||
if (props.system) {
|
||||
activeSystems = props.system.items.filter(system =>{
|
||||
if (system.active) {return true}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
this.state = { duration: 500, visible: true, currentSystem: 0, activeSystems}
|
||||
}
|
||||
|
||||
|
||||
|
||||
handleChange = (e, { name, value }) => this.setState({ [name]: value })
|
||||
|
||||
handleVisibility = () => this.setState({ visible: !this.state.visible })
|
||||
|
||||
advanceSystem() {
|
||||
|
||||
var currentSystem = this.state.currentSystem + 1;
|
||||
if (currentSystem > this.state.activeSystems.length) {
|
||||
currentSystem = 0;
|
||||
}
|
||||
this.setState({ visible: !this.state.visible, currentSystem });
|
||||
|
||||
}
|
||||
|
||||
componentWillUpdate(nextProps) {
|
||||
|
||||
if (nextProps.system.items && (nextProps.system.items !== this.props.system.items)) {
|
||||
const activeSystems = nextProps.system.items.filter(system =>{
|
||||
if (system.active) {return true}
|
||||
return false;
|
||||
});
|
||||
this.setState({activeSystems});
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
this.props.fetchSystems();
|
||||
this.interval = setInterval(() => this.advanceSystem(), 3000);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
clearInterval(this.interval);
|
||||
}
|
||||
|
||||
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
|
||||
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
|
||||
render() {
|
||||
const system = this.state.activeSystems[this.state.currentSystem];
|
||||
var clientCount = "";
|
||||
var callAvg = "";
|
||||
if (system) {
|
||||
|
||||
|
||||
if (system.callAvg) {
|
||||
callAvg = Math.round( system.callAvg * 10) / 10 + " call/min avg"
|
||||
}
|
||||
if (system.clientCount == 1 ) {
|
||||
clientCount = "1 listener";
|
||||
} else if (system.clientCount > 1 ) {
|
||||
clientCount = system.clientCount + " listeners"
|
||||
}
|
||||
}
|
||||
return (
|
||||
<ResponsiveContainer>
|
||||
<div style={{ top: '-120px', position: 'relative' }}>
|
||||
<Segment style={{ padding: ' 0em', height: '350px', backgroudColor: '#FFF'}} vertical>
|
||||
<Grid columns='equal' stackable textAlign='center' style={{ height: '350px', marginRight: '0px' }}>
|
||||
<Grid.Row textAlign='center'>
|
||||
<Grid.Column style={{ paddingBottom: '4em', paddingTop: '2em', maxWidth: 450 }}>
|
||||
{ system && (<Transition visible={this.state.visible} animation='pulse' duration={500}>
|
||||
|
||||
<SystemCard keepShort={true} system={system} key={system.shortName} onClick={(e) => this.props.changeUrl("/system/" + system.shortName)}/>
|
||||
|
||||
</Transition>)}
|
||||
</Grid.Column>
|
||||
<Grid.Column style={{ paddingBottom: '0em', paddingTop: '6em', maxWidth: 450 }}>
|
||||
<Link to="/systems">
|
||||
<Button primary size='huge' id="listen" animated>
|
||||
<Button.Content visible><Icon name='headphones' />
|
||||
<Icon name='right arrow' /></Button.Content>
|
||||
<Button.Content hidden>Listen</Button.Content>
|
||||
</Button>
|
||||
</Link>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
|
||||
</Segment>
|
||||
<Segment style={{ padding: '0em' }} vertical>
|
||||
<Grid celled='internally' columns='equal' stackable>
|
||||
<Grid.Row textAlign='center'>
|
||||
<Grid.Column style={{ paddingBottom: '5em', paddingTop: '5em' }}>
|
||||
<Header as='h3' style={{ fontSize: '2em' }}>
|
||||
<Icon color='orange' name='list' />All The Calls
|
||||
</Header>
|
||||
<p style={{ fontSize: '1.33em' }}>Every call, on every talkgroup is recorded</p>
|
||||
</Grid.Column>
|
||||
<Grid.Column style={{ paddingBottom: '5em', paddingTop: '5em' }}>
|
||||
<Header as='h3' style={{ fontSize: '2em' }}>
|
||||
<Icon color='orange' name='checked calendar' />
|
||||
Go back in time
|
||||
</Header>
|
||||
<p style={{ fontSize: '1.33em' }}>
|
||||
Missed a call? Not a problem, everything is archived!
|
||||
</p>
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Segment>
|
||||
<Segment style={{ padding: '8em 0em' }} vertical>
|
||||
<Container text>
|
||||
<Header as='h3' style={{ fontSize: '2em' }}>
|
||||
Don't just scan, hear it all!
|
||||
</Header>
|
||||
<p style={{ fontSize: '1.33em' }}>
|
||||
Most large cities use trunked radio systems to get the most use out of their assigned radio spectrum. With trunked systems, the transmission are constantly hopping to different frequencies. Using a cheap Software Defined Radio (SDR), it is possible capture all of the transmission on a system.
|
||||
Instead of scanning to a single frequency, SDR capturea wide swathes of spectrum, covering all of the frequencies a system could use.
|
||||
</p>
|
||||
<Divider
|
||||
as='h4'
|
||||
className='header'
|
||||
horizontal
|
||||
style={{ margin: '3em 0em', textTransform: 'uppercase' }}
|
||||
>
|
||||
Get Involved
|
||||
</Divider>
|
||||
<Header as='h3' style={{ fontSize: '2em' }}>
|
||||
Want to record your local radio system?
|
||||
</Header>
|
||||
<p style={{ fontSize: '1.33em' }}>
|
||||
Join us! Record a local radio system and share it with the world. If you have a spare computer, it is easy to get started. With a $25 SDR or two, you can capture an entire system.
|
||||
It does take a little bit of work to get things setup, but we are here to help and answer questions.
|
||||
</p>
|
||||
<a href="https://github.com/robotastic/trunk-recorder/blob/master/README.md">
|
||||
<Button size='large'>
|
||||
Learn More
|
||||
</Button>
|
||||
</a>
|
||||
</Container>
|
||||
</Segment>
|
||||
</div>
|
||||
<Segment inverted vertical style={{ padding: '5em 0em' }} id="footer">
|
||||
<Container>
|
||||
<Grid divided inverted stackable>
|
||||
<Grid.Row>
|
||||
<Grid.Column width={6}>
|
||||
|
||||
</Grid.Column>
|
||||
<Grid.Column width={4}>
|
||||
<Header as='h4' inverted>
|
||||
The End <Icon name='rocket' />
|
||||
</Header>
|
||||
</Grid.Column>
|
||||
<Grid.Column width={6}>
|
||||
|
||||
|
||||
</Grid.Column>
|
||||
</Grid.Row>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Segment>
|
||||
|
||||
</ResponsiveContainer>);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Main;
|
||||
@@ -1,16 +0,0 @@
|
||||
import { connect } from "react-redux"
|
||||
import * as systemActions from "../System/system-actions"
|
||||
import Main from "./Main"
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
system: state.system
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
systemActions
|
||||
)(Main)
|
||||
@@ -1,74 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Link } from 'react-router-dom'
|
||||
import SystemCard from "./SystemCard";
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Card,
|
||||
Form,
|
||||
Grid,
|
||||
Segment,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Icon,
|
||||
Menu,
|
||||
Divider
|
||||
} from "semantic-ui-react";
|
||||
|
||||
// ----------------------------------------------------
|
||||
const requestMessageStyle = {
|
||||
color: "red"
|
||||
};
|
||||
|
||||
function SystemList(props) {
|
||||
const systems = props.systems;
|
||||
const listItems = numbers.map((number) =>
|
||||
<li>{number}</li>
|
||||
);
|
||||
return (
|
||||
<ul>{listItems}</ul>
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------
|
||||
class ListSystems extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
}
|
||||
|
||||
state = {
|
||||
requestMessage: "",
|
||||
|
||||
}
|
||||
componentDidMount() {
|
||||
this.props.fetchSystems();
|
||||
}
|
||||
|
||||
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
|
||||
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
|
||||
render() {
|
||||
const systems = this.props.system.items;
|
||||
return (
|
||||
<div>
|
||||
<Menu fixed="top">
|
||||
<Link to="/"><Menu.Item link><Icon name='arrow left' /> Home</Menu.Item></Link>
|
||||
<Link to="/about"><Menu.Item link>About</Menu.Item></Link>
|
||||
</Menu>
|
||||
<Container >
|
||||
<Divider horizontal style={{paddingTop:"5em", paddingBottom:"2em"}}><Header as="h1">Radio Systems<Icon name='rss' /></Header></Divider>
|
||||
<Card.Group itemsPerRow={4} stackable={true}>
|
||||
{systems.map((system) =>
|
||||
system.active&&<SystemCard system={system} key={system.shortName} onClick={(e) => this.props.changeUrl("/system/" + system.shortName)}/>
|
||||
// <SystemCard system={system} key={system.shortName} onClick={(e) => this.props.changeUrl("/system/" + system.shortName)}/>
|
||||
)}
|
||||
</Card.Group>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ListSystems;
|
||||
@@ -1,16 +0,0 @@
|
||||
import { connect } from "react-redux"
|
||||
import * as systemActions from "./system-actions"
|
||||
import ListSystems from "./ListSystems"
|
||||
|
||||
function mapStateToProps(state) {
|
||||
return {
|
||||
system: state.system
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
systemActions
|
||||
)(ListSystems)
|
||||
@@ -1,103 +0,0 @@
|
||||
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
Card,
|
||||
Icon,
|
||||
Label,
|
||||
Header,
|
||||
Grid
|
||||
} from "semantic-ui-react";
|
||||
|
||||
|
||||
class SystemCard extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
var freePlanValue = 0;
|
||||
var proPlanValue = 10;
|
||||
|
||||
var location = "";
|
||||
const system = this.props.system;
|
||||
var clientCount = "";
|
||||
var callAvg = "";
|
||||
var plan = null;
|
||||
var archive = "";
|
||||
let label;
|
||||
var description = system.description;
|
||||
if (this.props.keepShort && (description.length > 100)) {
|
||||
description = description.substring(0,96) + "...";
|
||||
}
|
||||
if (system.callAvg) {
|
||||
callAvg = Math.round( system.callAvg * 10) / 10 + " call/min"
|
||||
}
|
||||
if (system.clientCount == 1 ) {
|
||||
clientCount = "1 listener";
|
||||
} else if (system.clientCount > 1 ) {
|
||||
clientCount = system.clientCount + " listeners"
|
||||
}
|
||||
if (system) {
|
||||
|
||||
|
||||
switch(system.planType) {
|
||||
case freePlanValue:
|
||||
plan = "Free";
|
||||
archive = "7 days";
|
||||
break;
|
||||
case proPlanValue:
|
||||
plan = "Pro";
|
||||
archive = "30 days";
|
||||
label = <Label attached='top right' color='teal' >Pro</Label>;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (system.systemType) {
|
||||
case "state":
|
||||
location = system.state;
|
||||
break;
|
||||
case "city":
|
||||
location = system.city + ", " + system.state;
|
||||
break;
|
||||
case "county":
|
||||
location = system.county + ", " + system.state;
|
||||
break;
|
||||
case "international":
|
||||
location = system.country;
|
||||
break;
|
||||
default:
|
||||
location = system.city + ", " + system.state;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Card link onClick={this.props.onClick} raised={true} centered={true} >
|
||||
|
||||
<Card.Content>
|
||||
|
||||
<Card.Header >{system.name}</Card.Header>
|
||||
{label}
|
||||
<Card.Meta>{system.screenName&&<Label size="mini"><Icon name='user' /> {system.screenName}</Label>}</Card.Meta>
|
||||
</Card.Content>
|
||||
<Card.Content description={description} />
|
||||
<Card.Content extra>
|
||||
<Icon name='marker' />
|
||||
{location}
|
||||
<Grid columns={2}>
|
||||
<Grid.Column>
|
||||
|
||||
|
||||
{clientCount&&<div><Icon name='headphones' />{clientCount}</div>}
|
||||
|
||||
</Grid.Column>
|
||||
<Grid.Column>
|
||||
{/* <Icon name='archive'/> {archive} */}
|
||||
{callAvg&&<div><Icon name='file audio outline' />{callAvg}</div>}
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
</Card.Content>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SystemCard;
|
||||
@@ -1,77 +0,0 @@
|
||||
import axios from "axios";
|
||||
//import { browserHistory } from "react-router"
|
||||
import * as types from "./system-constants";
|
||||
import { push } from 'connected-react-router';
|
||||
|
||||
const FETCH_SUCCESS_STATISTIC = "FETCH_SUCCESS_STATISTIC"
|
||||
|
||||
function beginFetchSystem() {
|
||||
return { type: types.FETCH_SYSTEM };
|
||||
}
|
||||
|
||||
function fetchSystemSuccess(data) {
|
||||
return { type: types.FETCH_SUCCESS_SYSTEM,
|
||||
data };
|
||||
}
|
||||
|
||||
function fetchSystemError(data) {
|
||||
return { type: types.FETCH_ERROR_SYSTEM,
|
||||
data };
|
||||
}
|
||||
|
||||
|
||||
function makeUserRequest(method, data, api = "/login") {
|
||||
// returns a Promise
|
||||
return axios({
|
||||
method: method,
|
||||
url: api,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
export function changeUrl(url) {
|
||||
return dispatch => {
|
||||
dispatch(push(url));
|
||||
};
|
||||
}
|
||||
|
||||
// Example of an Async Action Creator
|
||||
// http://redux.js.org/docs/advanced/AsyncActions.html
|
||||
|
||||
function shouldFetchSystems(state, subreddit) {
|
||||
if ((!state.system.items) || (state.system.items.length <1)) {
|
||||
return true
|
||||
} else if (state.system.isWaiting) {
|
||||
return false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function fetchSystems() {
|
||||
return (dispatch, getState) => {
|
||||
if (shouldFetchSystems(getState())) {
|
||||
dispatch(beginFetchSystem());
|
||||
|
||||
return axios
|
||||
.get(backend_server + "/systems")
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
dispatch(fetchSystemSuccess(response.data.systems));
|
||||
} else {
|
||||
dispatch(fetchSystemError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export const FETCH_SYSTEM = "FETCH_SYSTEM"
|
||||
export const FETCH_SUCCESS_SYSTEM = "FETCH_SUCCESS_SYSTEM"
|
||||
export const FETCH_ERROR_SYSTEM = "FETCH_ERROR_SYSTEM"
|
||||
@@ -1,67 +0,0 @@
|
||||
import {
|
||||
CREATE_SYSTEM,
|
||||
CREATE_SUCCESS_SYSTEM,
|
||||
CREATE_ERROR_SYSTEM,
|
||||
UPDATE_SYSTEM,
|
||||
UPDATE_SUCCESS_SYSTEM,
|
||||
UPDATE_ERROR_SYSTEM,
|
||||
DELETE_SYSTEM,
|
||||
DELETE_SUCCESS_SYSTEM,
|
||||
DELETE_ERROR_SYSTEM,
|
||||
FETCH_SYSTEM,
|
||||
FETCH_SUCCESS_SYSTEM,
|
||||
FETCH_ERROR_SYSTEM
|
||||
} from "./system-constants";
|
||||
|
||||
function updateObject(oldObject, newValues) {
|
||||
// Encapsulate the idea of passing a new object as the first parameter
|
||||
// to Object.assign to ensure we correctly copy data instead of mutating
|
||||
return Object.assign({}, oldObject, newValues);
|
||||
}
|
||||
|
||||
function updateObjectInArray(array, shortName, updatedItem) {
|
||||
return array.map( item => {
|
||||
if(item.shortName !== shortName) {
|
||||
// This isn't the item we care about - keep it as-is
|
||||
return item;
|
||||
}
|
||||
|
||||
// Otherwise, this is the one we want - return an updated value
|
||||
return {
|
||||
...item,
|
||||
...updatedItem
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const system = (
|
||||
state = {
|
||||
isWaiting: false,
|
||||
items: []},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
|
||||
case FETCH_SYSTEM:
|
||||
|
||||
return Object.assign({}, state, { isWaiting: true});
|
||||
|
||||
|
||||
case FETCH_SUCCESS_SYSTEM:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
items: action.data
|
||||
});
|
||||
|
||||
case FETCH_ERROR_SYSTEM:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default system;
|
||||
@@ -1,66 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { Link } from 'react-router-dom'
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Form,
|
||||
Grid,
|
||||
Segment,
|
||||
Input,
|
||||
Button,
|
||||
Message,
|
||||
Icon,
|
||||
Table
|
||||
} from "semantic-ui-react";
|
||||
|
||||
// ----------------------------------------------------
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------------------
|
||||
class ListTalkgroups extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
}
|
||||
|
||||
|
||||
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
|
||||
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
|
||||
render() {
|
||||
const talkgroups = this.props.talkgroups;
|
||||
if (talkgroups) {
|
||||
return (
|
||||
|
||||
<Table celled>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>Mode</Table.HeaderCell>
|
||||
<Table.HeaderCell>Number</Table.HeaderCell>
|
||||
<Table.HeaderCell>Alpha</Table.HeaderCell>
|
||||
<Table.HeaderCell>Description</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{talkgroups.map((talkgroup, i) => (
|
||||
<Table.Row key={talkgroup.num + "-" + i}>
|
||||
<Table.Cell>{talkgroup.mode}</Table.Cell>
|
||||
<Table.Cell>{talkgroup.num}</Table.Cell>
|
||||
<Table.Cell>{talkgroup.alpha}</Table.Cell>
|
||||
<Table.Cell>{talkgroup.description}</Table.Cell>
|
||||
</Table.Row>
|
||||
))}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ListTalkgroups;
|
||||
@@ -1,108 +0,0 @@
|
||||
import axios from 'axios';
|
||||
//import { browserHistory } from "react-router"
|
||||
import * as types from "./talkgroup-constants";
|
||||
import { push } from 'connected-react-router';
|
||||
|
||||
// "Log In" action creators
|
||||
function beginImportTalkgroup() {
|
||||
return { type: types.IMPORT_TALKGROUP };
|
||||
}
|
||||
|
||||
function importTalkgroupSuccess(data) {
|
||||
return {
|
||||
type: types.IMPORT_SUCCESS_TALKGROUP,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
function importTalkgroupError(data) {
|
||||
return {
|
||||
type: types.IMPORT_ERROR_TALKGROUP,
|
||||
data
|
||||
};
|
||||
}
|
||||
|
||||
function beginExportTalkgroup() {
|
||||
return { type: types.EXPORT_TALKGROUP };
|
||||
}
|
||||
|
||||
function exportTalkgroupSuccess() {
|
||||
return { type: types.EXPORT_SUCCESS_TALKGROUP };
|
||||
}
|
||||
|
||||
function exportTalkgroupError() {
|
||||
return { type: types.EXPORT_ERROR_TALKGROUP };
|
||||
}
|
||||
|
||||
|
||||
function beginFetchTalkgroup() {
|
||||
return { type: types.FETCH_TALKGROUP };
|
||||
}
|
||||
|
||||
function fetchTalkgroupSuccess(data) {
|
||||
return { type: types.FETCH_SUCCESS_TALKGROUP,
|
||||
data };
|
||||
}
|
||||
|
||||
function fetchTalkgroupError(data) {
|
||||
return { type: types.FETCH_ERROR_TALKGROUP,
|
||||
data };
|
||||
}
|
||||
|
||||
|
||||
function makeUserRequest(method, data, api) {
|
||||
// returns a Promise
|
||||
return axios({
|
||||
method: method,
|
||||
url: api,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
|
||||
export function changeUrl(url) {
|
||||
return dispatch => {
|
||||
dispatch(push(url));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// https://stackoverflow.com/questions/41878838/how-do-i-set-multipart-in-axios-with-react
|
||||
|
||||
|
||||
|
||||
function shouldFetchTalkgroups(state, shortName) {
|
||||
if (!state.talkgroup.items.hasOwnProperty(shortName)) {
|
||||
return true
|
||||
} else if (state.talkgroup.isWaiting) {
|
||||
return false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function fetchTalkgroups(shortName) {
|
||||
return (dispatch, getState) => {
|
||||
if (shouldFetchTalkgroups(getState(), shortName)) {
|
||||
dispatch(beginFetchTalkgroup());
|
||||
|
||||
return axios
|
||||
.get(backend_server + "/" + shortName + "/talkgroups")
|
||||
.then(response => {
|
||||
if (response.data) {
|
||||
var data = {shortName: shortName, talkgroups: response.data.talkgroups}
|
||||
dispatch(fetchTalkgroupSuccess( data));
|
||||
} else {
|
||||
dispatch(fetchTalkgroupError());
|
||||
let registerMessage = response.data.message;
|
||||
return registerMessage;
|
||||
}
|
||||
})
|
||||
.catch(response => {
|
||||
if (response instanceof Error) {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log("Error", response.message);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export const IMPORT_TALKGROUP = "IMPORT_TALKGROUP"
|
||||
export const IMPORT_SUCCESS_TALKGROUP = "IMPORT_SUCCESS_TALKGROUP"
|
||||
export const IMPORT_ERROR_TALKGROUP = "IMPORT_ERROR_TALKGROUP"
|
||||
export const EXPORT_TALKGROUP = "EXPORT_TALKGROUP"
|
||||
export const EXPORT_SUCCESS_TALKGROUP = "EXPORT_SUCCESS_TALKGROUP"
|
||||
export const EXPORT_ERROR_TALKGROUP = "EXPORT_ERROR_TALKGROUP"
|
||||
export const FETCH_TALKGROUP = "FETCH_TALKGROUP"
|
||||
export const FETCH_SUCCESS_TALKGROUP = "FETCH_SUCCESS_TALKGROUP"
|
||||
export const FETCH_ERROR_TALKGROUP = "FETCH_ERROR_TALKGROUP"
|
||||
@@ -1,72 +0,0 @@
|
||||
import {
|
||||
IMPORT_TALKGROUP,
|
||||
IMPORT_SUCCESS_TALKGROUP,
|
||||
IMPORT_ERROR_TALKGROUP,
|
||||
EXPORT_TALKGROUP,
|
||||
EXPORT_SUCCESS_TALKGROUP,
|
||||
EXPORT_ERROR_TALKGROUP,
|
||||
FETCH_TALKGROUP,
|
||||
FETCH_SUCCESS_TALKGROUP,
|
||||
FETCH_ERROR_TALKGROUP
|
||||
} from "./talkgroup-constants";
|
||||
|
||||
function updateObject(oldObject, newValues) {
|
||||
// Encapsulate the idea of passing a new object as the first parameter
|
||||
// to Object.assign to ensure we correctly copy data instead of mutating
|
||||
return Object.assign({}, oldObject, newValues);
|
||||
}
|
||||
|
||||
function updateItemInArray(array, itemId, updateItemCallback) {
|
||||
const updatedItems = array.map(item => {
|
||||
if(item.id !== itemId) {
|
||||
// Since we only want to update one item, preserve all others as they are now
|
||||
return item;
|
||||
}
|
||||
|
||||
// Use the provided callback to create an updated item
|
||||
const updatedItem = updateItemCallback(item);
|
||||
return updatedItem;
|
||||
});
|
||||
|
||||
return updatedItems;
|
||||
}
|
||||
|
||||
const talkgroup = (
|
||||
state = {
|
||||
isWaiting: false,
|
||||
items: {}},
|
||||
action
|
||||
) => {
|
||||
switch (action.type) {
|
||||
|
||||
case FETCH_TALKGROUP:
|
||||
case IMPORT_TALKGROUP:
|
||||
case EXPORT_TALKGROUP:
|
||||
return Object.assign({}, state, { isWaiting: true});
|
||||
|
||||
case IMPORT_SUCCESS_TALKGROUP:
|
||||
case FETCH_SUCCESS_TALKGROUP:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false,
|
||||
items: {
|
||||
...state.items,
|
||||
[action.data.shortName]: action.data.talkgroups
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
case FETCH_ERROR_TALKGROUP:
|
||||
case IMPORT_ERROR_TALKGROUP:
|
||||
case EXPORT_ERROR_TALKGROUP:
|
||||
case EXPORT_SUCCESS_TALKGROUP:
|
||||
return Object.assign({}, state, {
|
||||
isWaiting: false
|
||||
});
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default talkgroup;
|
||||
@@ -1,81 +0,0 @@
|
||||
import React from "react"
|
||||
import { render } from "react-dom"
|
||||
import { Provider } from "react-redux"
|
||||
import { Switch, Route} from 'react-router-dom'
|
||||
import { createStore, combineReducers, applyMiddleware } from 'redux'
|
||||
import thunk from "redux-thunk"
|
||||
import {createLogger} from "redux-logger"
|
||||
import { ConnectedRouter } from 'connected-react-router'
|
||||
import { routerMiddleware } from 'connected-react-router'
|
||||
import isNode from "detect-node"
|
||||
import createRootReducer from './reducers'
|
||||
import createHistory from 'history/createBrowserHistory'
|
||||
|
||||
import App from "./components/container/App"
|
||||
import About from "./components/pure/About"
|
||||
|
||||
// Main
|
||||
import MainContainer from "./Main/MainContainer"
|
||||
|
||||
// System
|
||||
import ListSystemsContainer from "./System/ListSystemsContainer"
|
||||
|
||||
// Call
|
||||
import CallPlayerContainer from "./Call/CallPlayerContainer"
|
||||
|
||||
// Create a history of your choosing (we're using a browser history in this case)
|
||||
const history = createHistory()
|
||||
|
||||
|
||||
|
||||
// Build the middleware for intercepting and dispatching navigation actions
|
||||
const historyMiddleware = routerMiddleware(history)
|
||||
|
||||
let middleware = [
|
||||
thunk,
|
||||
historyMiddleware
|
||||
|
||||
]
|
||||
|
||||
// only add Redux-Logger on the client-side because it causes problems server-side.
|
||||
if (!isNode) {
|
||||
middleware.push(createLogger())
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Add the reducer to your store on the `router` key
|
||||
// Also apply our middleware for navigatin
|
||||
const store = createStore(
|
||||
createRootReducer(history), // root reducer with router state
|
||||
applyMiddleware(thunk,
|
||||
historyMiddleware, createLogger())
|
||||
)
|
||||
|
||||
/*
|
||||
<Route path="/system/:shortName" component={Restricted(SystemContainer, store)} />
|
||||
<Route path="/update-system/:shortName" component={Restricted(EditSystemContainer, store)} />
|
||||
<Route path="/create-system" component={Restricted(CreateSystemContainer, store)} />
|
||||
<Route path="/list-systems" component={Restricted(ListSystemsContainer, store)} />
|
||||
|
||||
<Route path="/system/:shortName" component={Restricted(SystemContainer, store)} />
|
||||
<Route path="/update-system/:shortName" component={EditSystemContainer} />
|
||||
<Route path="/create-system" component={CreateSystemContainer} />
|
||||
<Route path="/list-systems" component={ListSystemsContainer} />
|
||||
*/
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<div>
|
||||
<App>
|
||||
<Switch>
|
||||
<Route path="/about" component={About} />
|
||||
<Route exact path="/" component={MainContainer} />
|
||||
<Route exact path="/systems" component={ListSystemsContainer} />
|
||||
<Route exact path="/system/:shortName" component={CallPlayerContainer} />
|
||||
</Switch>
|
||||
</App>
|
||||
</div>
|
||||
</ConnectedRouter>
|
||||
</Provider>
|
||||
, document.getElementById("app"))
|
||||
@@ -1,17 +0,0 @@
|
||||
import React, { Component } from "react"
|
||||
import '../../../styling/semantic.less'
|
||||
class App extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return(
|
||||
<div>
|
||||
{this.props.children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -1,42 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Container, Header, Divider, Icon, Menu } from "semantic-ui-react";
|
||||
|
||||
class About extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Menu fixed="top">
|
||||
<Link to="/"><Menu.Item link><Icon name='arrow left' /> Home</Menu.Item></Link>
|
||||
<Link to="/systems"><Menu.Item link>Listen</Menu.Item></Link>
|
||||
</Menu>
|
||||
<Container text>
|
||||
<Divider horizontal style={{paddingTop:"4em", paddingBottom:"2em"}}><Header as="h1">About<Icon name='compass' /></Header></Divider>
|
||||
|
||||
<p>I wrote <a href="https://github.com/robotastic/trunk-recorder">Trunk Recorder</a> because I was curious
|
||||
about what my local fire station was up to and I put together the original version of OpenMHz because I
|
||||
figured other people might want to listen to the recordings too.</p>
|
||||
<p>The latest version of this site makes it easy for other people running Trunk Recorder to share their recrodings.
|
||||
I am hoping that making it easier to listen to what our local fire, police and EMS have to go through everyday will
|
||||
lead to greater appreciation for all the work they do that goes largely unseen. I am personally amazed by the numebr
|
||||
of medical runs the DC's Fire and EMS department has to cover every day, and the shocking number of them that are ODs.</p>
|
||||
<p>I will do my best to keep adding new features to this site. One thing I want to add is visualizations to make it easier to find interesting calls.
|
||||
Let me know if you have any ideas.</p>
|
||||
<p>Right now, each system will only have a 5 day archive. This is to help make sure things don't get overloaded and make sure I don't get a crazy bill
|
||||
at the end of the month.</p>
|
||||
|
||||
<p> - Luke <a href="mailto:admin@email.com?Subject=OpenMHz" target="_top">admin@email.com</a> </p>
|
||||
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default About;
|
||||
@@ -1,31 +0,0 @@
|
||||
import React, { Component } from "react"
|
||||
|
||||
class Default extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
return(
|
||||
<div>
|
||||
<h2>React-Passport-Redux-Example</h2>
|
||||
<p>Demonstration of PassportJS authentication in React w/ Redux. Use the links above to register a new user and login/logout. You will only be able to view the MyProfile page when you are logged in. Logging in will redirect you to MyProfile if you had clicked that link beforehand.</p>
|
||||
<p>I'll be doing my best to document code with useful comments so look for more detail there. Note: you should be able to implement the authentication functionality using less packages. I was working on another project before deciding to make this example repo so some bloat may have travelled over from that.</p>
|
||||
<p>In full, this repo demonstrates:</p>
|
||||
<ul>
|
||||
<li><strong>React</strong> for the view layer </li>
|
||||
<li><strong>PassportJS</strong> for authentication (using a Local Strategy)</li>
|
||||
<li><strong>Redux</strong> to handle our state</li>
|
||||
<li><strong>React Router</strong> for routing client-side</li>
|
||||
<li><strong>Express</strong> for handling server requests (REST and page requests)</li>
|
||||
<li><strong>MongoDB</strong> for our database, with <strong>Mongoose</strong> handling our schema</li>
|
||||
<li><strong>Webpack</strong> to bundle our client-side code</li>
|
||||
<li>Hot reloading using <strong>webpack-dev-middleware</strong> and <strong>webpack-hot-middleware</strong></li>
|
||||
</ul>
|
||||
<p>Shoutout to GitHub member <b>choonkending</b> whose <a href="https://github.com/choonkending/react-webpack-node" target="_blank">repo</a> was a big help. Pull requests and comments / issue reports are most welcome!</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Default
|
||||
@@ -1,31 +0,0 @@
|
||||
import React, { Component } from "react";
|
||||
import { Modal, Icon, Button, Header } from "semantic-ui-react";
|
||||
|
||||
class Message extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
_logout = event => {
|
||||
event.preventDefault();
|
||||
this.props.manualLogout();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal open={this.props.open} onClose={this.props.onClose} basic size="small">
|
||||
<Header icon="info" content={this.props.title} />
|
||||
<Modal.Content>
|
||||
<p>{this.props.message}</p>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button color='green' onClick={this.props.onClose} inverted>
|
||||
<Icon name='checkmark' /> Got it
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Message;
|
||||
@@ -1,5 +0,0 @@
|
||||
.field {
|
||||
padding-top: 10px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
export default {
|
||||
parse: function parse(queryString) {
|
||||
var query = {};
|
||||
var pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');
|
||||
for (var i = 0; i < pairs.length; i++) {
|
||||
var pair = pairs[i].split('=');
|
||||
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
|
||||
}
|
||||
return query;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { combineReducers } from "redux"
|
||||
import { connectRouter } from 'connected-react-router'
|
||||
import system from "../System/system-reducer"
|
||||
import talkgroup from "../Talkgroups/talkgroup-reducer"
|
||||
import group from "../Group/group-reducer"
|
||||
import call from "../Call/call-reducer"
|
||||
|
||||
|
||||
export default (history) => combineReducers({
|
||||
router: connectRouter(history),
|
||||
system,
|
||||
talkgroup,
|
||||
group,
|
||||
call
|
||||
})
|
||||
@@ -1,34 +0,0 @@
|
||||
import * as types from '../Call/call-constants'
|
||||
import * as callActionCreators from "../Call/call-actions"
|
||||
const setupSocket = (cb, sn, filterType, filterCode) => {
|
||||
const shortName = sn
|
||||
const socket = new WebSocket(socket_server + '/')
|
||||
const callback = cb;
|
||||
socket.onopen = () => {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'add',
|
||||
filterCode: filterCode,
|
||||
filterType: filterType,
|
||||
filterName: "",
|
||||
shortName: shortName
|
||||
}))
|
||||
}
|
||||
socket.onmessage = (event) => {
|
||||
callback(event.data);
|
||||
}
|
||||
|
||||
socket.updateFilter = (filterType,filterCode, filterName) => {
|
||||
if (socket.readyState == WebSocket.OPEN) {
|
||||
socket.send(JSON.stringify({
|
||||
type: 'update',
|
||||
filterCode: filterCode,
|
||||
filterType: filterType,
|
||||
filterName: filterName,
|
||||
shortName: shortName
|
||||
}))
|
||||
}
|
||||
}
|
||||
return socket
|
||||
}
|
||||
|
||||
export default setupSocket
|
||||
@@ -1,32 +0,0 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { Button } from '@storybook/react/demo';
|
||||
import SystemCard from '../System/SystemCard.js';
|
||||
import 'semantic-ui-css/semantic.min.css'
|
||||
|
||||
storiesOf('Button', module)
|
||||
.add('with text', () => (
|
||||
<Button>Hello Button</Button>
|
||||
))
|
||||
.add('with some emoji', () => (
|
||||
<Button><span role="img" aria-label="so cool">😀 😎 👍 💯</span></Button>
|
||||
));
|
||||
|
||||
|
||||
export const system = {active: false,
|
||||
callAvg: 1,
|
||||
city: "",
|
||||
clientCount: 1,
|
||||
country: "",
|
||||
description: "This is an awesome system",
|
||||
name: "Awesome Town",
|
||||
planType: 10,
|
||||
shortName: "awesome",
|
||||
screenName: "luke",
|
||||
state: "AL",
|
||||
systemType: "state"}
|
||||
|
||||
storiesOf('SystemCard', module).add("standard", () => {
|
||||
return (<SystemCard system={system} key={system.shortName} onClick={(e) => this.props.changeUrl("/system/" + system.shortName)}/>)
|
||||
|
||||
});
|
||||
60
frontend/src/features/counter/Counter.js
Normal file
60
frontend/src/features/counter/Counter.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import {
|
||||
decrement,
|
||||
increment,
|
||||
incrementByAmount,
|
||||
incrementAsync,
|
||||
selectCount,
|
||||
} from './counterSlice';
|
||||
import styles from './Counter.module.css';
|
||||
|
||||
export function Counter() {
|
||||
const count = useSelector(selectCount);
|
||||
const dispatch = useDispatch();
|
||||
const [incrementAmount, setIncrementAmount] = useState('2');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.row}>
|
||||
<button
|
||||
className={styles.button}
|
||||
aria-label="Increment value"
|
||||
onClick={() => dispatch(increment())}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<span className={styles.value}>{count}</span>
|
||||
<button
|
||||
className={styles.button}
|
||||
aria-label="Decrement value"
|
||||
onClick={() => dispatch(decrement())}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.row}>
|
||||
<input
|
||||
className={styles.textbox}
|
||||
aria-label="Set increment amount"
|
||||
value={incrementAmount}
|
||||
onChange={e => setIncrementAmount(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className={styles.button}
|
||||
onClick={() =>
|
||||
dispatch(incrementByAmount(Number(incrementAmount) || 0))
|
||||
}
|
||||
>
|
||||
Add Amount
|
||||
</button>
|
||||
<button
|
||||
className={styles.asyncButton}
|
||||
onClick={() => dispatch(incrementAsync(Number(incrementAmount) || 0))}
|
||||
>
|
||||
Add Async
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
74
frontend/src/features/counter/Counter.module.css
Normal file
74
frontend/src/features/counter/Counter.module.css
Normal file
@@ -0,0 +1,74 @@
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.row:not(:last-child) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 78px;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
margin-top: 2px;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
.button {
|
||||
appearance: none;
|
||||
background: none;
|
||||
font-size: 32px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
outline: none;
|
||||
border: 2px solid transparent;
|
||||
color: rgb(112, 76, 182);
|
||||
padding-bottom: 4px;
|
||||
cursor: pointer;
|
||||
background-color: rgba(112, 76, 182, 0.1);
|
||||
border-radius: 2px;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.textbox {
|
||||
font-size: 32px;
|
||||
padding: 2px;
|
||||
width: 64px;
|
||||
text-align: center;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.button:hover, .button:focus {
|
||||
border: 2px solid rgba(112, 76, 182, 0.4);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
background-color: rgba(112, 76, 182, 0.2);
|
||||
}
|
||||
|
||||
.asyncButton {
|
||||
composes: button;
|
||||
position: relative;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.asyncButton:after {
|
||||
content: "";
|
||||
background-color: rgba(112, 76, 182, 0.15);
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
transition: width 1s linear, opacity 0.5s ease 1s;
|
||||
}
|
||||
|
||||
.asyncButton:active:after {
|
||||
width: 0%;
|
||||
opacity: 1;
|
||||
transition: 0s
|
||||
}
|
||||
42
frontend/src/features/counter/counterSlice.js
Normal file
42
frontend/src/features/counter/counterSlice.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
export const counterSlice = createSlice({
|
||||
name: 'counter',
|
||||
initialState: {
|
||||
value: 0,
|
||||
},
|
||||
reducers: {
|
||||
increment: state => {
|
||||
// Redux Toolkit allows us to write "mutating" logic in reducers. It
|
||||
// doesn't actually mutate the state because it uses the Immer library,
|
||||
// which detects changes to a "draft state" and produces a brand new
|
||||
// immutable state based off those changes
|
||||
state.value += 1;
|
||||
},
|
||||
decrement: state => {
|
||||
state.value -= 1;
|
||||
},
|
||||
incrementByAmount: (state, action) => {
|
||||
state.value += action.payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
|
||||
|
||||
// The function below is called a thunk and allows us to perform async logic. It
|
||||
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
|
||||
// will call the thunk with the `dispatch` function as the first argument. Async
|
||||
// code can then be executed and other actions can be dispatched
|
||||
export const incrementAsync = amount => dispatch => {
|
||||
setTimeout(() => {
|
||||
dispatch(incrementByAmount(amount));
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
// The function below is called a selector and allows us to select a value from
|
||||
// the state. Selectors can also be defined inline where they're used instead of
|
||||
// in the slice file. For example: `useSelector((state) => state.counter.value)`
|
||||
export const selectCount = state => state.counter.value;
|
||||
|
||||
export default counterSlice.reducer;
|
||||
Binary file not shown.
13
frontend/src/index.css
Normal file
13
frontend/src/index.css
Normal file
@@ -0,0 +1,13 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
25
frontend/src/index.js
Normal file
25
frontend/src/index.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux'
|
||||
import { Route, Switch } from 'react-router' // react-router v4/v5
|
||||
import { ConnectedRouter } from 'connected-react-router'
|
||||
import configureStore, { history } from './app/configureStore'
|
||||
|
||||
import App from './App';
|
||||
|
||||
|
||||
const store = configureStore(/* provide initial state if any */)
|
||||
|
||||
ReactDOM.render(
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}> { /* place ConnectedRouter under Provider */ }
|
||||
<> { /* your usual react-router v4/v5 routing */ }
|
||||
<Switch>
|
||||
<Route exact path="/" render={() => (<App />)} />
|
||||
<Route render={() => (<div>Miss</div>)} />
|
||||
</Switch>
|
||||
</>
|
||||
</ConnectedRouter>
|
||||
</Provider>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
1
frontend/src/logo.svg
Normal file
1
frontend/src/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><g fill="#764ABC"><path d="M65.6 65.4c2.9-.3 5.1-2.8 5-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 1.5.7 2.8 1.6 3.7-3.4 6.7-8.6 11.6-16.4 15.7-5.3 2.8-10.8 3.8-16.3 3.1-4.5-.6-8-2.6-10.2-5.9-3.2-4.9-3.5-10.2-.8-15.5 1.9-3.8 4.9-6.6 6.8-8-.4-1.3-1-3.5-1.3-5.1-14.5 10.5-13 24.7-8.6 31.4 3.3 5 10 8.1 17.4 8.1 2 0 4-.2 6-.7 12.8-2.5 22.5-10.1 28-21.4z"/><path d="M83.2 53c-7.6-8.9-18.8-13.8-31.6-13.8H50c-.9-1.8-2.8-3-4.9-3h-.2c-3.1.1-5.5 2.7-5.4 5.8.1 3 2.6 5.4 5.6 5.4h.2c2.2-.1 4.1-1.5 4.9-3.4H52c7.6 0 14.8 2.2 21.3 6.5 5 3.3 8.6 7.6 10.6 12.8 1.7 4.2 1.6 8.3-.2 11.8-2.8 5.3-7.5 8.2-13.7 8.2-4 0-7.8-1.2-9.8-2.1-1.1 1-3.1 2.6-4.5 3.6 4.3 2 8.7 3.1 12.9 3.1 9.6 0 16.7-5.3 19.4-10.6 2.9-5.8 2.7-15.8-4.8-24.3z"/><path d="M32.4 67.1c.1 3 2.6 5.4 5.6 5.4h.2c3.1-.1 5.5-2.7 5.4-5.8-.1-3-2.6-5.4-5.6-5.4h-.2c-.2 0-.5 0-.7.1-4.1-6.8-5.8-14.2-5.2-22.2.4-6 2.4-11.2 5.9-15.5 2.9-3.7 8.5-5.5 12.3-5.6 10.6-.2 15.1 13 15.4 18.3 1.3.3 3.5 1 5 1.5-1.2-16.2-11.2-24.6-20.8-24.6-9 0-17.3 6.5-20.6 16.1-4.6 12.8-1.6 25.1 4 34.8-.5.7-.8 1.8-.7 2.9z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,28 +0,0 @@
|
||||
import path from "path"
|
||||
import express from "express"
|
||||
import bodyParser from "body-parser"
|
||||
|
||||
|
||||
export default function(app) {
|
||||
app.set("port", 3000)
|
||||
|
||||
// X-Powered-By header has no functional value.
|
||||
// Keeping it makes it easier for an attacker to build the site's profile
|
||||
// It can be removed safely
|
||||
app.disable("x-powered-by")
|
||||
|
||||
app.use(bodyParser.json())
|
||||
app.use(bodyParser.urlencoded({ extended: true }))
|
||||
app.use(express.static(path.join(process.cwd(), 'public')));
|
||||
|
||||
|
||||
|
||||
var node_env = process.env.NODE_ENV;
|
||||
console.log('--------------------------');
|
||||
console.log('===> 😊 Starting Server . . .');
|
||||
console.log('===> Environment: ' + node_env);
|
||||
if(node_env === 'production') {
|
||||
console.log('===> 🚦 Note: In order for authentication to work in production');
|
||||
console.log('===> you will need a secure HTTPS connection');
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import fs from "fs"
|
||||
import express from "express"
|
||||
import webpack from "webpack"
|
||||
import path from "path"
|
||||
import config from "../../webpack/webpack.config.dev.js"
|
||||
import configureExpress from "./config/express"
|
||||
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
const app = express()
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
const isDev = process.env.NODE_ENV === "development"
|
||||
|
||||
// if in development mode set up the middleware required for hot reloading and rebundling
|
||||
if (isDev) {
|
||||
|
||||
const compiler = webpack(config)
|
||||
|
||||
app.use(require("webpack-dev-middleware")(compiler, {
|
||||
noInfo: true,
|
||||
publicPath: config.output.publicPath
|
||||
}))
|
||||
|
||||
app.use(require("webpack-hot-middleware")(compiler))
|
||||
}
|
||||
|
||||
var backend_server = process.env['BACKEND_SERVER'] != null ? process.env['BACKEND_SERVER'] : 'https://api.openmhz.com';
|
||||
var frontend_server = process.env['FRONTEND_SERVER'] != null ? process.env['FRONTEND_SERVER'] : 'https://openmhz.com';
|
||||
var media_server = process.env['MEDIA_SERVER'] != null ? process.env['MEDIA_SERVER'] : 'https://media.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
var socket_server = process.env['SOCKET_SERVER'] != null ? process.env['SOCKET_SERVER'] : 'wss://api.openmhz.com'; //'https://s3.amazonaws.com/robotastic';
|
||||
var site_name = process.env['SITE_NAME'] != null ? process.env['SITE_NAME'] : "OpenMHz";
|
||||
const proPlanValue = process.env['PRO_PLAN'] != null ? process.env['PRO_PLAN'] : 10;
|
||||
const freePlanValue = process.env['FREE_PLAN'] != null ? process.env['FREE_PLAN'] : 0;
|
||||
const proPlanArchive = process.env['PRO_PLAN_ARCHIVE'] != null ? process.env['PRO_PLAN_ARCHIVE'] : 30;
|
||||
const freePlanArchive = process.env['FREE_PLAN_ARCHIVE'] != null ? process.env['FREE_PLAN_ARCHIVE'] : 7;
|
||||
// -------------------------------------------
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
configureExpress(app)
|
||||
|
||||
// -------------------------------------------
|
||||
app.use(express.static("public"));
|
||||
app.get("*", (req, res, next) => {
|
||||
|
||||
// if we are in production mode then an extension will be provided, usually ".min"
|
||||
const minified = process.env.MIN_EXT || ""
|
||||
|
||||
// this is the HTML we will send to the client when they request any page. React and React Router
|
||||
// will take over once the scripts are loaded client-side
|
||||
const appHTML =
|
||||
`<!doctype html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
|
||||
<title>${site_name}</title>
|
||||
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="//cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css"
|
||||
/>
|
||||
<script>
|
||||
const backend_server = "${backend_server}";
|
||||
const media_server = "${media_server}";
|
||||
const socket_server = "${socket_server}";
|
||||
const proPlanValue = ${proPlanValue};
|
||||
const freePlanValue = ${freePlanValue};
|
||||
const proPlanArchive = ${proPlanArchive};
|
||||
const freePlanArchive = ${freePlanArchive};
|
||||
const site_name = "${site_name}";
|
||||
</script>
|
||||
<script src="${backend_server}/socket.io/socket.io.js"></script>
|
||||
<script>
|
||||
const socket = io('${backend_server}');
|
||||
</script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, 'Helvetica Neue', Helvetica, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="/assets/app${minified}.js"></script>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
res.status(200).end(appHTML)
|
||||
|
||||
})
|
||||
|
||||
// start listening to incoming requests
|
||||
app.listen(app.get("port"), app.get("host"), (err) => {
|
||||
if (err) {
|
||||
console.err(err.stack)
|
||||
} else {
|
||||
console.log(`App listening on port ${app.get("port")} [${process.env.NODE_ENV} mode]`)
|
||||
}
|
||||
})
|
||||
137
frontend/src/serviceWorker.js
Normal file
137
frontend/src/serviceWorker.js
Normal file
@@ -0,0 +1,137 @@
|
||||
// This optional code is used to register a service worker.
|
||||
// register() is not called by default.
|
||||
|
||||
// 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 subsequent visits to a page, after all the
|
||||
// existing tabs open on the page have been closed, since previously cached
|
||||
// resources are updated in the background.
|
||||
|
||||
// To learn more about the benefits of this model and instructions on how to
|
||||
// opt-in, read https://bit.ly/CRA-PWA
|
||||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::1] is the IPv6 localhost address.
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(
|
||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
||||
)
|
||||
);
|
||||
|
||||
export function register(config) {
|
||||
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.href);
|
||||
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/facebook/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. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
|
||||
// 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://bit.ly/CRA-PWA'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerValidSW(swUrl, config) {
|
||||
navigator.serviceWorker
|
||||
.register(swUrl)
|
||||
.then(registration => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
if (navigator.serviceWorker.controller) {
|
||||
// At this point, the updated precached content has been fetched,
|
||||
// but the previous service worker will still serve the older
|
||||
// content until all client tabs are closed.
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
|
||||
);
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
}
|
||||
} 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.');
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl, config) {
|
||||
// Check if the service worker can be found. If it can't reload the page.
|
||||
fetch(swUrl, {
|
||||
headers: { 'Service-Worker': 'script' },
|
||||
})
|
||||
.then(response => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.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, config);
|
||||
}
|
||||
})
|
||||
.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();
|
||||
});
|
||||
}
|
||||
}
|
||||
5
frontend/src/setupTests.js
Normal file
5
frontend/src/setupTests.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
@@ -1,4 +0,0 @@
|
||||
@type : 'element';
|
||||
@element : 'icon';
|
||||
|
||||
@import (multiple) '../../theme.config';
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
Import this file into your LESS project to use Semantic UI without build tools:
|
||||
import 'styling/styles.less'
|
||||
|
||||
Here you can disable unused styling for unused components.
|
||||
*/
|
||||
|
||||
/* Global */
|
||||
& { @import "~semantic-ui-less/definitions/globals/reset.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/globals/site.less"; }
|
||||
|
||||
/* Elements */
|
||||
& { @import "~semantic-ui-less/definitions/elements/button.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/elements/container.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/elements/divider.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/elements/flag.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/elements/header.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/elements/icon.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/elements/image.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/elements/input.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/elements/label.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/elements/list.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/elements/loader.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/elements/rail.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/elements/reveal.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/elements/segment.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/elements/step.less"; }
|
||||
|
||||
///* Collections */
|
||||
& { @import "~semantic-ui-less/definitions/collections/breadcrumb.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/collections/form.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/collections/grid.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/collections/menu.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/collections/message.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/collections/table.less"; }
|
||||
|
||||
/* Views */
|
||||
& { @import "~semantic-ui-less/definitions/views/ad.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/views/card.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/views/comment.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/views/feed.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/views/item.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/views/statistic.less"; }
|
||||
|
||||
/* Modules */
|
||||
& { @import "~semantic-ui-less/definitions/modules/accordion.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/modules/checkbox.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/modules/dimmer.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/modules/dropdown.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/modules/embed.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/modules/modal.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/modules/nag.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/modules/popup.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/modules/progress.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/modules/rating.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/modules/search.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/modules/shape.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/modules/sidebar.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/modules/sticky.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/modules/tab.less"; }
|
||||
& { @import "~semantic-ui-less/definitions/modules/transition.less"; }
|
||||
@@ -1,83 +0,0 @@
|
||||
/*******************************
|
||||
Theme Selection
|
||||
*******************************/
|
||||
|
||||
/* To override a theme for an individual element specify theme name below */
|
||||
|
||||
/* Global */
|
||||
@site : 'default';
|
||||
@reset : 'default';
|
||||
|
||||
/* Elements */
|
||||
@button : 'default';
|
||||
@container : 'default';
|
||||
@divider : 'default';
|
||||
@flag : 'default';
|
||||
@header : 'default';
|
||||
@icon : 'default';
|
||||
@image : 'default';
|
||||
@input : 'default';
|
||||
@label : 'default';
|
||||
@list : 'default';
|
||||
@loader : 'default';
|
||||
@rail : 'default';
|
||||
@reveal : 'default';
|
||||
@segment : 'default';
|
||||
@step : 'default';
|
||||
|
||||
/* Collections */
|
||||
@breadcrumb : 'default';
|
||||
@form : 'default';
|
||||
@grid : 'default';
|
||||
@menu : 'default';
|
||||
@message : 'default';
|
||||
@table : 'default';
|
||||
|
||||
/* Modules */
|
||||
@accordion : 'default';
|
||||
@checkbox : 'default';
|
||||
@dimmer : 'default';
|
||||
@dropdown : 'default';
|
||||
@embed : 'default';
|
||||
@modal : 'default';
|
||||
@nag : 'default';
|
||||
@popup : 'default';
|
||||
@progress : 'default';
|
||||
@rating : 'default';
|
||||
@search : 'default';
|
||||
@shape : 'default';
|
||||
@sidebar : 'default';
|
||||
@sticky : 'default';
|
||||
@tab : 'default';
|
||||
@transition : 'default';
|
||||
|
||||
/* Views */
|
||||
@ad : 'default';
|
||||
@card : 'default';
|
||||
@comment : 'default';
|
||||
@feed : 'default';
|
||||
@item : 'default';
|
||||
@statistic : 'default';
|
||||
|
||||
/*******************************
|
||||
Folders
|
||||
*******************************/
|
||||
|
||||
/* Path to theme packages */
|
||||
@themesFolder : 'themes';
|
||||
|
||||
/* Path to site override folder */
|
||||
@siteFolder : '../../src/styling/theme';
|
||||
|
||||
/*******************************
|
||||
Import Theme
|
||||
*******************************/
|
||||
|
||||
|
||||
@import (multiple) "~semantic-ui-less/theme.less";
|
||||
|
||||
/*******************************
|
||||
Import Overrides
|
||||
*******************************/
|
||||
|
||||
@fontPath : '../../../themes/@{theme}/assets/fonts';
|
||||
@@ -1,3 +0,0 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
@@ -1,3 +0,0 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
@@ -1,3 +0,0 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
@@ -1,3 +0,0 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
@@ -1,3 +0,0 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
@@ -1,3 +0,0 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
@@ -1,3 +0,0 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
@@ -1,3 +0,0 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
@@ -1,3 +0,0 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
@@ -1,3 +0,0 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
@@ -1,3 +0,0 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
@@ -1,3 +0,0 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
@@ -1,3 +0,0 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
@@ -1,3 +0,0 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user