mirror of
https://github.com/openmhz/trunk-server.git
synced 2026-11-13 09:17:30 -05:00
basic Create React App working
This commit is contained in:
11
frontend-old/.babelrc
Normal file
11
frontend-old/.babelrc
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"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"]}]
|
||||
]
|
||||
}
|
||||
3
frontend-old/.dockerignore
Normal file
3
frontend-old/.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
lib
|
||||
public
|
||||
35
frontend-old/.gitignore
vendored
Normal file
35
frontend-old/.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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
|
||||
8
frontend-old/.storybook/config.js
Normal file
8
frontend-old/.storybook/config.js
Normal file
@@ -0,0 +1,8 @@
|
||||
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);
|
||||
22
frontend-old/Dockerfile
Normal file
22
frontend-old/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
# 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"]
|
||||
10
frontend-old/devStart.js
Normal file
10
frontend-old/devStart.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
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-old/package-lock.json
generated
Normal file
15729
frontend-old/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
102
frontend-old/package.json
Normal file
102
frontend-old/package.json
Normal file
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
6
frontend-old/src/client/Call/CalendarModal.css
Normal file
6
frontend-old/src/client/Call/CalendarModal.css
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
|
||||
.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
71
frontend-old/src/client/Call/CalendarModal.js
Normal file
71
frontend-old/src/client/Call/CalendarModal.js
Normal file
@@ -0,0 +1,71 @@
|
||||
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;
|
||||
28
frontend-old/src/client/Call/CalendarModalContainer.js
Normal file
28
frontend-old/src/client/Call/CalendarModalContainer.js
Normal file
@@ -0,0 +1,28 @@
|
||||
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)
|
||||
86
frontend-old/src/client/Call/CallInfo.js
Normal file
86
frontend-old/src/client/Call/CallInfo.js
Normal file
@@ -0,0 +1,86 @@
|
||||
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;
|
||||
123
frontend-old/src/client/Call/CallItem.js
Normal file
123
frontend-old/src/client/Call/CallItem.js
Normal file
@@ -0,0 +1,123 @@
|
||||
|
||||
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)
|
||||
|
||||
105
frontend-old/src/client/Call/CallPlayer.css
Normal file
105
frontend-old/src/client/Call/CallPlayer.css
Normal file
@@ -0,0 +1,105 @@
|
||||
|
||||
/*******************************
|
||||
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;
|
||||
}
|
||||
}
|
||||
579
frontend-old/src/client/Call/CallPlayer.js
Normal file
579
frontend-old/src/client/Call/CallPlayer.js
Normal file
@@ -0,0 +1,579 @@
|
||||
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;
|
||||
45
frontend-old/src/client/Call/CallPlayerContainer.js
Normal file
45
frontend-old/src/client/Call/CallPlayerContainer.js
Normal file
@@ -0,0 +1,45 @@
|
||||
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)
|
||||
3
frontend-old/src/client/Call/FilterModal.css
Normal file
3
frontend-old/src/client/Call/FilterModal.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.active.tab {
|
||||
min-height: 250px;
|
||||
}
|
||||
145
frontend-old/src/client/Call/FilterModal.js
Normal file
145
frontend-old/src/client/Call/FilterModal.js
Normal file
@@ -0,0 +1,145 @@
|
||||
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;
|
||||
34
frontend-old/src/client/Call/FilterModalContainer.js
Normal file
34
frontend-old/src/client/Call/FilterModalContainer.js
Normal file
@@ -0,0 +1,34 @@
|
||||
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)
|
||||
85
frontend-old/src/client/Call/GroupModal.js
Normal file
85
frontend-old/src/client/Call/GroupModal.js
Normal file
@@ -0,0 +1,85 @@
|
||||
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;
|
||||
27
frontend-old/src/client/Call/GroupModalContainer.js
Normal file
27
frontend-old/src/client/Call/GroupModalContainer.js
Normal file
@@ -0,0 +1,27 @@
|
||||
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)
|
||||
65
frontend-old/src/client/Call/ListCalls.js
Normal file
65
frontend-old/src/client/Call/ListCalls.js
Normal file
@@ -0,0 +1,65 @@
|
||||
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;
|
||||
159
frontend-old/src/client/Call/MediaPlayer.js
Normal file
159
frontend-old/src/client/Call/MediaPlayer.js
Normal file
@@ -0,0 +1,159 @@
|
||||
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;
|
||||
56
frontend-old/src/client/Call/SupportModal.js
Normal file
56
frontend-old/src/client/Call/SupportModal.js
Normal file
@@ -0,0 +1,56 @@
|
||||
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;
|
||||
356
frontend-old/src/client/Call/call-actions.js
Normal file
356
frontend-old/src/client/Call/call-actions.js
Normal file
@@ -0,0 +1,356 @@
|
||||
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});
|
||||
};
|
||||
}
|
||||
22
frontend-old/src/client/Call/call-constants.js
Normal file
22
frontend-old/src/client/Call/call-constants.js
Normal file
@@ -0,0 +1,22 @@
|
||||
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"
|
||||
227
frontend-old/src/client/Call/call-reducer.js
Normal file
227
frontend-old/src/client/Call/call-reducer.js
Normal file
@@ -0,0 +1,227 @@
|
||||
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;
|
||||
177
frontend-old/src/client/Group/CreateGroup.js
Normal file
177
frontend-old/src/client/Group/CreateGroup.js
Normal file
@@ -0,0 +1,177 @@
|
||||
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;
|
||||
25
frontend-old/src/client/Group/CreateGroupContainer.js
Normal file
25
frontend-old/src/client/Group/CreateGroupContainer.js
Normal file
@@ -0,0 +1,25 @@
|
||||
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)
|
||||
77
frontend-old/src/client/Group/ListGroups.js
Normal file
77
frontend-old/src/client/Group/ListGroups.js
Normal file
@@ -0,0 +1,77 @@
|
||||
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;
|
||||
73
frontend-old/src/client/Group/group-actions.js
Normal file
73
frontend-old/src/client/Group/group-actions.js
Normal file
@@ -0,0 +1,73 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
3
frontend-old/src/client/Group/group-constants.js
Normal file
3
frontend-old/src/client/Group/group-constants.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const FETCH_GROUP = "FETCH_GROUP"
|
||||
export const FETCH_SUCCESS_GROUP = "FETCH_SUCCESS_GROUP"
|
||||
export const FETCH_ERROR_GROUP = "FETCH_ERROR_GROUP"
|
||||
62
frontend-old/src/client/Group/group-reducer.js
Normal file
62
frontend-old/src/client/Group/group-reducer.js
Normal file
@@ -0,0 +1,62 @@
|
||||
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;
|
||||
82
frontend-old/src/client/Main/Main.css
Normal file
82
frontend-old/src/client/Main/Main.css
Normal file
@@ -0,0 +1,82 @@
|
||||
@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 {
|
||||
|
||||
|
||||
}
|
||||
382
frontend-old/src/client/Main/Main.js
Normal file
382
frontend-old/src/client/Main/Main.js
Normal file
@@ -0,0 +1,382 @@
|
||||
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;
|
||||
16
frontend-old/src/client/Main/MainContainer.js
Normal file
16
frontend-old/src/client/Main/MainContainer.js
Normal file
@@ -0,0 +1,16 @@
|
||||
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)
|
||||
74
frontend-old/src/client/System/ListSystems.js
Normal file
74
frontend-old/src/client/System/ListSystems.js
Normal file
@@ -0,0 +1,74 @@
|
||||
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;
|
||||
16
frontend-old/src/client/System/ListSystemsContainer.js
Normal file
16
frontend-old/src/client/System/ListSystemsContainer.js
Normal file
@@ -0,0 +1,16 @@
|
||||
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)
|
||||
103
frontend-old/src/client/System/SystemCard.js
Normal file
103
frontend-old/src/client/System/SystemCard.js
Normal file
@@ -0,0 +1,103 @@
|
||||
|
||||
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;
|
||||
77
frontend-old/src/client/System/system-actions.js
Normal file
77
frontend-old/src/client/System/system-actions.js
Normal file
@@ -0,0 +1,77 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
3
frontend-old/src/client/System/system-constants.js
Normal file
3
frontend-old/src/client/System/system-constants.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export const FETCH_SYSTEM = "FETCH_SYSTEM"
|
||||
export const FETCH_SUCCESS_SYSTEM = "FETCH_SUCCESS_SYSTEM"
|
||||
export const FETCH_ERROR_SYSTEM = "FETCH_ERROR_SYSTEM"
|
||||
67
frontend-old/src/client/System/system-reducer.js
Normal file
67
frontend-old/src/client/System/system-reducer.js
Normal file
@@ -0,0 +1,67 @@
|
||||
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;
|
||||
66
frontend-old/src/client/Talkgroups/ListTalkgroups.js
Normal file
66
frontend-old/src/client/Talkgroups/ListTalkgroups.js
Normal file
@@ -0,0 +1,66 @@
|
||||
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;
|
||||
108
frontend-old/src/client/Talkgroups/talkgroup-actions.js
Normal file
108
frontend-old/src/client/Talkgroups/talkgroup-actions.js
Normal file
@@ -0,0 +1,108 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
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"
|
||||
72
frontend-old/src/client/Talkgroups/talkgroup-reducer.js
Normal file
72
frontend-old/src/client/Talkgroups/talkgroup-reducer.js
Normal file
@@ -0,0 +1,72 @@
|
||||
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;
|
||||
81
frontend-old/src/client/client.js
Normal file
81
frontend-old/src/client/client.js
Normal file
@@ -0,0 +1,81 @@
|
||||
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"))
|
||||
17
frontend-old/src/client/components/container/App.js
Normal file
17
frontend-old/src/client/components/container/App.js
Normal file
@@ -0,0 +1,17 @@
|
||||
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
|
||||
42
frontend-old/src/client/components/pure/About.js
Normal file
42
frontend-old/src/client/components/pure/About.js
Normal file
@@ -0,0 +1,42 @@
|
||||
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;
|
||||
31
frontend-old/src/client/components/pure/Default.js
Normal file
31
frontend-old/src/client/components/pure/Default.js
Normal file
@@ -0,0 +1,31 @@
|
||||
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
|
||||
31
frontend-old/src/client/components/pure/Message.js
Normal file
31
frontend-old/src/client/components/pure/Message.js
Normal file
@@ -0,0 +1,31 @@
|
||||
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;
|
||||
5
frontend-old/src/client/components/style/style.css
Normal file
5
frontend-old/src/client/components/style/style.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.field {
|
||||
padding-top: 10px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
11
frontend-old/src/client/query-string.js
Normal file
11
frontend-old/src/client/query-string.js
Normal file
@@ -0,0 +1,11 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
15
frontend-old/src/client/reducers/index.js
Normal file
15
frontend-old/src/client/reducers/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
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
|
||||
})
|
||||
34
frontend-old/src/client/socket/SetupSocket.js
Normal file
34
frontend-old/src/client/socket/SetupSocket.js
Normal file
@@ -0,0 +1,34 @@
|
||||
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
|
||||
32
frontend-old/src/client/stories/index.js
Normal file
32
frontend-old/src/client/stories/index.js
Normal file
@@ -0,0 +1,32 @@
|
||||
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)}/>)
|
||||
|
||||
});
|
||||
BIN
frontend-old/src/font/DidactGothic-Regular.ttf
Executable file
BIN
frontend-old/src/font/DidactGothic-Regular.ttf
Executable file
Binary file not shown.
BIN
frontend-old/src/resources/DidactGothic-Regular.ttf
Executable file
BIN
frontend-old/src/resources/DidactGothic-Regular.ttf
Executable file
Binary file not shown.
BIN
frontend-old/src/resources/logo.png
Normal file
BIN
frontend-old/src/resources/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
28
frontend-old/src/server/config/express.js
Normal file
28
frontend-old/src/server/config/express.js
Normal file
@@ -0,0 +1,28 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
104
frontend-old/src/server/index.js
Normal file
104
frontend-old/src/server/index.js
Normal file
@@ -0,0 +1,104 @@
|
||||
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]`)
|
||||
}
|
||||
})
|
||||
4
frontend-old/src/styling/heading.less
Normal file
4
frontend-old/src/styling/heading.less
Normal file
@@ -0,0 +1,4 @@
|
||||
@type : 'element';
|
||||
@element : 'icon';
|
||||
|
||||
@import (multiple) '../../theme.config';
|
||||
61
frontend-old/src/styling/semantic.less
Normal file
61
frontend-old/src/styling/semantic.less
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
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"; }
|
||||
83
frontend-old/src/styling/theme.config
Normal file
83
frontend-old/src/styling/theme.config
Normal file
@@ -0,0 +1,83 @@
|
||||
/*******************************
|
||||
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';
|
||||
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/button.overrides
Normal file
3
frontend-old/src/styling/theme/elements/button.overrides
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/button.variables
Normal file
3
frontend-old/src/styling/theme/elements/button.variables
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/flag.overrides
Normal file
3
frontend-old/src/styling/theme/elements/flag.overrides
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/flag.variables
Normal file
3
frontend-old/src/styling/theme/elements/flag.variables
Normal file
@@ -0,0 +1,3 @@
|
||||
/*-------------------
|
||||
Flag Variables
|
||||
--------------------*/
|
||||
3
frontend-old/src/styling/theme/elements/header.overrides
Normal file
3
frontend-old/src/styling/theme/elements/header.overrides
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/header.variables
Normal file
3
frontend-old/src/styling/theme/elements/header.variables
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/icon.overrides
Normal file
3
frontend-old/src/styling/theme/elements/icon.overrides
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/icon.variables
Normal file
3
frontend-old/src/styling/theme/elements/icon.variables
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/image.overrides
Normal file
3
frontend-old/src/styling/theme/elements/image.overrides
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/image.variables
Normal file
3
frontend-old/src/styling/theme/elements/image.variables
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/input.overrides
Normal file
3
frontend-old/src/styling/theme/elements/input.overrides
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/input.variables
Normal file
3
frontend-old/src/styling/theme/elements/input.variables
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/label.overrides
Normal file
3
frontend-old/src/styling/theme/elements/label.overrides
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/label.variables
Normal file
3
frontend-old/src/styling/theme/elements/label.variables
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/list.overrides
Normal file
3
frontend-old/src/styling/theme/elements/list.overrides
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/list.variables
Normal file
3
frontend-old/src/styling/theme/elements/list.variables
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/loader.overrides
Normal file
3
frontend-old/src/styling/theme/elements/loader.overrides
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/loader.variables
Normal file
3
frontend-old/src/styling/theme/elements/loader.variables
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
User Variable Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/rail.overrides
Normal file
3
frontend-old/src/styling/theme/elements/rail.overrides
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
Site Overrides
|
||||
*******************************/
|
||||
3
frontend-old/src/styling/theme/elements/rail.variables
Normal file
3
frontend-old/src/styling/theme/elements/rail.variables
Normal file
@@ -0,0 +1,3 @@
|
||||
/*******************************
|
||||
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