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