First Commit

This commit is contained in:
Luke Berndt
2020-06-09 22:11:11 -04:00
parent 8f49aa463c
commit 3b0d3f290f
551 changed files with 193130 additions and 2 deletions

View File

@@ -0,0 +1,6 @@
.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list {
padding-left: 0;
padding-right: 0;
}

View File

@@ -0,0 +1,71 @@
import React, { Component } from "react";
import {
Modal,
Button,
Icon,
Header
} from "semantic-ui-react";
import "./CalendarModal.css";
import DatePicker from 'react-datepicker';
import subDays from 'date-fns/sub_days'
import 'react-datepicker/dist/react-datepicker.css';
class CalendarModal extends Component {
constructor(props) {
super(props)
this.handleDateChange = this.handleDateChange.bind(this);
this.handleDone = this.handleDone.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = {
open: false,
startDate: new Date()
}
}
handleDateChange = (date) => {
this.setState({
startDate: date
});
}
handleClose = () => this.props.onClose(false);
handleDone(event) {
this.props.callActions.setDateFilter(this.state.startDate.getTime());
this.props.onClose(true);
}
render() {
return (
<Modal open={this.props.open} onClose={this.handleClose} centered={false} size="tiny">
<Modal.Header>Select a Date & Time</Modal.Header>
<Modal.Content >
<Modal.Description>
<DatePicker
selected={this.state.startDate}
onChange={this.handleDateChange}
maxDate={new Date()}
minDate={subDays(new Date(), this.props.archive)}
showTimeSelect
timeFormat="HH:mm"
timeIntervals={15}
dateFormat="LLL"
inline
/>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={this.handleDone} >
<Icon name='checkmark' /> Done
</Button>
</Modal.Actions>
</Modal>
)
}
}
export default CalendarModal;

View File

@@ -0,0 +1,28 @@
import { connect } from "react-redux"
import * as callActionCreators from "./call-actions"
import { bindActionCreators } from 'redux'
import CalendarModal from "./CalendarModal"
function mapStateToProps(state, props) {
return {
groups: state.group.items[props.shortName],
talkgroups: state.talkgroup.items[props.shortName],
talkgroupsIsWaiting: state.talkgroup.isWaiting,
groupsIsWaiting: state.group.isWaiting,
shortName: props.shortName,
activeTab: state.call.filterType,
selectedTalkgroups: state.call.filterTalkgroups
}
}
function mapDispatchToProps(dispatch) {
return {
callActions: bindActionCreators(callActionCreators, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(CalendarModal)

View File

@@ -0,0 +1,86 @@
import React from "react";
import {
Header,
Divider,
List,
Segment,
Statistic,
Icon
} from "semantic-ui-react";
// ----------------------------------------------------
class CallInfo extends React.Component {
constructor(props) {
super(props);
}
render() {
var srcList = "";
var callLength = "-";
var callFreq = "-";
var callDate = "-";
var callTime = "-";
var talkgroupNum = "-";
if (this.props.call) {
const currentCall = this.props.call;
var time = new Date(currentCall.time);
callTime = time.toLocaleTimeString();
callDate = time.toLocaleDateString();
if (currentCall.freq) {
var freq = currentCall.freq / 1000000;
callFreq = Math.round(freq * 1000) / 1000;
}
srcList = currentCall.srcList.map((source, index) => <List.Item key={index}>{source.src}
[{source.pos}]</List.Item>);
callLength = currentCall.len;
talkgroupNum = currentCall.talkgroupNum;
}
return (
<Segment padded>
<Header as='h1'>{this.props.header}</Header>
<List bulleted horizontal link>
{srcList}
</List>
<Divider/>
<Statistic size='small'>
<Statistic.Label>Seconds</Statistic.Label>
<Statistic.Value>{callLength}</Statistic.Value>
</Statistic>
<Statistic size='small'>
<Statistic.Label>Talkgroup</Statistic.Label>
<Statistic.Value>{talkgroupNum}</Statistic.Value>
</Statistic>
<List divided verticalAlign='middle'>
<List.Item>
<Icon name="wait"/>
<List.Content>
{callTime}
</List.Content>
</List.Item>
<List.Item>
<Icon name="calendar outline"/>
<List.Content>
{callDate}
</List.Content>
</List.Item>
<List.Item>
<Icon name="cubes"/>
<List.Content>
{callFreq} MHz
</List.Content>
</List.Item>
</List>
</Segment>
);
}
}
export default CallInfo;

View File

@@ -0,0 +1,123 @@
import React, { Component } from "react";
import {
Table,
Icon,
Label
} from "semantic-ui-react";
import { connect } from "react-redux"
import { bindActionCreators } from 'redux'
import * as callActionCreators from "./call-actions"
function mapStateToProps(state, props) {
return {
shortName: props.shortName
}
}
function mapDispatchToProps(dispatch) {
return {
callActions: bindActionCreators(callActionCreators, dispatch)
}
}
class CallItem extends Component {
constructor(props) {
super(props);
this.hideStar = this.hideStar.bind(this);
this.showStar = this.showStar.bind(this);
this.addStar = this.addStar.bind(this);
this.state = {
starVisible: false,
starClicked: false
}
}
sourceString(call) {
var srcString = "";
if (call.srcList) {
for (var src in call.srcList) {
/* srcNum = call.srcList[src].src;
if (sources.hasOwnProperty(srcNum)) {
srcString = srcString + sources[srcNum].codeName + ", ";
}*/
}
}
return srcString;
}
addStar(e) {
e.preventDefault();
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
if (!this.state.starClicked) {
this.props.callActions.addStar(this.props.call._id);
this.setState({starClicked: true});
}
}
hideStar() {
this.setState({ starVisible: false })
}
showStar() {
this.setState({ starVisible: true })
}
render() {
const call = this.props.call;
const time = new Date(call.time);
const current = new Date(time).setHours(0, 0, 0, 0);
var rowSelected={};
let starButton;
var starClickable = {};
if (!this.state.starClicked) {
starClickable = {link: true };
}
if (!call.star && this.state.starVisible) {
starButton = <Icon name='star outline' />
}
if (call.star) {
starButton = <Icon name='star' />
}
if (call.star && call.star > 1) {
starButton = (<Icon.Group >
<Icon {...starClickable} name='star' />
<Label circular color='red' size='mini' floating>
{call.star}
</Label>
</Icon.Group>)
}
if (this.props.activeCall) {
rowSelected={positive: true}
}
var talkgroup;
if ((typeof this.props.talkgroups == 'undefined') || (typeof this.props.talkgroups[call.talkgroupNum] == 'undefined')) {
talkgroup = call.talkgroupNum;
} else {
talkgroup = this.props.talkgroups[call.talkgroupNum].description;
}
return (
<Table.Row onClick={(e) => this.props.onClick({call: call}, e)} {...rowSelected}>
<Table.Cell> {call.len} </Table.Cell>
<Table.Cell> {talkgroup} </Table.Cell>
<Table.Cell> {time.toLocaleTimeString()} </Table.Cell>
<Table.Cell onMouseEnter={this.showStar} onMouseLeave={this.hideStar} onClick={this.addStar}>{starButton}</Table.Cell>
</Table.Row>
);
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(CallItem)

View File

@@ -0,0 +1,105 @@
/*******************************
Responsive
*******************************/
/* Defaults */
.ui.progress:first-child {
width: 150px;
margin: 0px;
}
/*
.ui.menu .item {
padding-top: 0px;
padding-bottom: 0px;
}*/
/* Needed to fix an error with Semantic UI react
https://github.com/Semantic-Org/Semantic-UI-React/issues/2558
https://github.com/Semantic-Org/Semantic-UI-React/issues/2550
*/
.ui.modal {
margin-top: 0 !important;
position: unset;
}
.main.container {
width: auto !important;
max-width: 960px !important;
margin-top: 55px;
}
@media only screen and (min-width: 1024px) {
.main.container {
margin-left: 3em !important;
margin-right: 387px !important;
}
/* Rail Default */
.main.container > .right.rail
{
margin-left: 3em;
padding-top: 2em;
width: 319px;
right: 20px;
left: auto;
}
}
@media only screen and (min-width: 768px) {
/* .desktop-only {
display:block !important;
}*/
.mobile-only {
display:none !important;
}
}
@media only screen and (min-width:768px) and (max-width: 1023px) {
.main.container {
margin-right: 250px !important;
}
/* Rail Default */
.main.container > .right.rail
{
margin-left: 3em;
width: 250px;
}
#calls {
margin-left: 25px;
margin-right: 25px;
}
}
@media only screen and (max-width: 767px) {
.mobile-only {
display:block !important;
}
#call-header {
display:none !important;
}
.main.container
{
margin-left: 0px !important;
margin-right: 0px !important;
}
/*
.ui.table:not(.unstackable) tr > td.desktop-only {
display:none !important;
} */
.desktop-only {
display:none !important;
}
}

View File

@@ -0,0 +1,590 @@
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';
import ReactGA from 'react-ga';
// ----------------------------------------------------
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);
ReactGA.event({
category: 'Live',
action: 'Toggle Live'
});
}
}
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();
ReactGA.event({
category: 'Filter',
action: 'Set Date'
});
//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;
}
ReactGA.event({
category: 'Filter',
action: 'Set Filter',
value: nextProps.filterType
});
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;

View File

@@ -0,0 +1,45 @@
import { connect } from "react-redux"
import * as callActionCreators from "./call-actions"
import * as talkgroupActionCreators from "../Talkgroups/talkgroup-actions"
import * as groupActionCreators from "../Group/group-actions"
import * as systemActionCreators from "../System/system-actions"
import { bindActionCreators } from 'redux'
import CallPlayer from "./CallPlayer"
function mapStateToProps(state, props) {
return {
callsById: state.call.byId,
callsAllIds: state.call.allIds,
oldestCallTime: state.call.oldestCallTime,
newestCallTime: state.call.newestCallTime,
live: state.call.live,
filterDate: state.call.filterDate,
filterType: state.call.filterType,
filterTalkgroups: state.call.filterTalkgroups,
filterGroupId: state.call.filterGroupId,
filterStarred: state.call.filterStarred,
system: state.system.items.find((item) => item.shortName === props.match.params.shortName),
groups: state.group.items[props.match.params.shortName],
talkgroups: state.talkgroup.items[props.match.params.shortName],
callsIsWaiting: state.call.isWaiting,
systemIsWaiting: state.system.isWaiting,
talkgroupsIsWaiting: state.talkgroup.isWaiting,
groupsIsWaiting: state.group.isWaiting,
shortName: props.match.params.shortName
}
}
function mapDispatchToProps(dispatch) {
return {
groupActions: bindActionCreators(groupActionCreators, dispatch),
systemActions: bindActionCreators(systemActionCreators, dispatch),
callActions: bindActionCreators(callActionCreators, dispatch),
talkgroupActions: bindActionCreators(talkgroupActionCreators, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(CallPlayer)

View File

@@ -0,0 +1,3 @@
.active.tab {
min-height: 250px;
}

View File

@@ -0,0 +1,145 @@
import React, { Component } from "react";
import {
Modal,
Button,
Icon,
Tab,
Header,
Dropdown,
Checkbox,
Divider
} from "semantic-ui-react";
import "./FilterModal.css";
class FilterModal extends Component {
constructor(props) {
super(props)
this.handleInputChange = this.handleInputChange.bind(this);
this.handleTabChange = this.handleTabChange.bind(this);
this.toggleStarred = this.toggleStarred.bind(this);
this.handleDone = this.handleDone.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = {
open: false,
selectedTalkgroup: this.props.selectedTalkgroups,
selectedGroup: this.props.selectedGroup,
filterStarred: this.props.filterStarred,
activeTab: this.props.activeTab
}
}
handleTabChange = (e, data) => this.setState({activeTab: data.activeIndex});
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
toggleStarred = () => this.setState(prevState => ({ filterStarred: !prevState.filterStarred }));
handleClose = () => this.props.onClose(false);
handleDone(event) {
switch(this.state.activeTab) {
case 0:
this.props.callActions.setAllFilter();
this.props.onClose(true);
break;
case 1:
if (this.state.selectedGroup) {
this.props.callActions.setGroupFilter(this.state.selectedGroup);
this.props.onClose(true);
} else {
this.props.onClose(false);
}
break;
case 2:
if (this.state.selectedTalkgroup) {
this.props.callActions.setTalkgroupFilter(this.state.selectedTalkgroup);
this.props.onClose(true);
} else {
this.props.onClose(false);
}
break;
}
if (this.state.filterStarred != this.props.filterStarred) {
this.props.callActions.setStarred(this.state.filterStarred);
this.props.onClose(true);
}
}
componentWillUpdate(nextProps) {
const filterChanged = ((nextProps.selectedTalkgroups !== this.props.selectedTalkgroups) || (nextProps.selectedGroup !== this.props.selectedGroup));
if (filterChanged) {
this.setState({selectedTalkgroup: nextProps.selectedTalkgroups, selectedGroup: nextProps.selectedGroup, activeTab: nextProps.activeTab});
}
}
render() {
var talkgroupList = [];
if (this.props.talkgroups) {
for (const num in this.props.talkgroups) {
const talkgroup = this.props.talkgroups[num];
var obj = {
key: talkgroup.num,
value: talkgroup.num,
text: talkgroup.description
}
talkgroupList.push(obj);
}
}
var groupList = [];
if (this.props.groups) {
for (const num in this.props.groups) {
const group = this.props.groups[num];
var obj = {
key: group._id,
value: group._id,
text: group.groupName
}
groupList.push(obj);
}
}
const panes = [
{ menuItem: 'All', render: () =>
{ return (
<Tab.Pane attached={false}>
<Header>All Calls</Header>
<p>Dispaly all of the calls.</p>
</Tab.Pane>
)}},
{ menuItem: 'Groups', render: () =>
{ return (
<Tab.Pane attached={false}>
<Dropdown placeholder='Groups' fluid selection options={groupList} value={this.state.selectedGroup} name='selectedGroup' onChange={this.handleInputChange} />
</Tab.Pane>
)} },
{ menuItem: 'Talkgroups', render: () =>
{ return (
<Tab.Pane attached={false}>
<Dropdown placeholder='Talkgroups' fluid multiple selection options={talkgroupList} value={this.state.selectedTalkgroup} name='selectedTalkgroup' onChange={this.handleInputChange} />
</Tab.Pane>
)} },
]
return (
<Modal open={this.props.open} onClose={this.handleClose} centered={false} size="tiny">
<Modal.Header>Select a Filter</Modal.Header>
<Modal.Content >
<Modal.Description>
<Tab menu={{ pointing: true }} panes={panes} defaultActiveIndex={this.props.activeTab} onTabChange={this.handleTabChange}/>
<Divider/>
<Checkbox label='Show only Starred calls' checked={this.state.filterStarred} name='filterStarred' onChange={this.toggleStarred}/>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={this.handleDone} >
<Icon name='checkmark' /> Done
</Button>
</Modal.Actions>
</Modal>
)
}
}
export default FilterModal;

View File

@@ -0,0 +1,34 @@
import { connect } from "react-redux"
import * as talkgroupActionCreators from "../Talkgroups/talkgroup-actions"
import * as groupActionCreators from "../Group/group-actions"
import * as callActionCreators from "./call-actions"
import { bindActionCreators } from 'redux'
import FilterModal from "./FilterModal"
function mapStateToProps(state, props) {
return {
groups: state.group.items[props.shortName],
talkgroups: state.talkgroup.items[props.shortName],
talkgroupsIsWaiting: state.talkgroup.isWaiting,
groupsIsWaiting: state.group.isWaiting,
shortName: props.shortName,
activeTab: state.call.filterType,
selectedTalkgroups: state.call.filterTalkgroups,
selectedGroup: state.call.filterGroupId,
filterStarred: state.call.filterStarred
}
}
function mapDispatchToProps(dispatch) {
return {
groupActions: bindActionCreators(groupActionCreators, dispatch),
talkgroupActions: bindActionCreators(talkgroupActionCreators, dispatch),
callActions: bindActionCreators(callActionCreators, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(FilterModal)

View File

@@ -0,0 +1,85 @@
import React, { Component } from "react";
import {
Modal,
Button,
Icon,
Dropdown
} from "semantic-ui-react";
import "./FilterModal.css";
class GroupModal extends Component {
constructor(props) {
super(props)
this.handleInputChange = this.handleInputChange.bind(this);
this.handleDone = this.handleDone.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = {
open: false,
selectedGroup: false
}
}
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
handleClose = () => this.props.onClose(false);
handleDone(event) {
if (this.state.selectedGroup) {
this.props.callActions.setGroupFilter(this.state.selectedGroup);
this.props.onClose(true);
} else {
this.props.onClose(false);
}
}
componentWillUpdate(nextProps) {
const filterChanged = (nextProps.selectedGroup !== this.props.selectedGroup);
if (filterChanged) {
this.setState({ selectedGroup: nextProps.selectedGroup});
}
}
render() {
var groupList = [];
if (this.props.groups) {
for (const num in this.props.groups) {
const group = this.props.groups[num];
var obj = {
key: group._id,
value: group._id,
text: group.groupName
}
groupList.push(obj);
}
}
return (
<Modal open={this.props.open} onClose={this.handleClose} size='tiny' >
<Modal.Header>Select a Group</Modal.Header>
<Modal.Content >
<Modal.Description>
<p>Choose the type of calls you want to listen to</p>
<Dropdown placeholder='Group...' fluid selection options={groupList} name='selectedGroup' onChange={this.handleInputChange} />
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={this.handleDone} >
<Icon name='headphones' /> Listen
</Button>
</Modal.Actions>
</Modal>
)
}
}
export default GroupModal;

View File

@@ -0,0 +1,27 @@
import { connect } from "react-redux"
import * as groupActionCreators from "../Group/group-actions"
import * as callActionCreators from "./call-actions"
import { bindActionCreators } from 'redux'
import GroupModal from "./GroupModal"
function mapStateToProps(state, props) {
return {
groups: state.group.items[props.shortName],
groupsIsWaiting: state.group.isWaiting,
shortName: props.shortName,
selectedGroup: state.call.filterGroupId
}
}
function mapDispatchToProps(dispatch) {
return {
groupActions: bindActionCreators(groupActionCreators, dispatch),
callActions: bindActionCreators(callActionCreators, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(GroupModal)

View File

@@ -0,0 +1,65 @@
import React from "react";
import {Link} from 'react-router-dom';
import CallItem from "./CallItem";
import {
Container,
Label,
Rail,
Sticky,
Menu,
Icon,
Table,
Sidebar
} from "semantic-ui-react";
import "./CallPlayer.css";
// ----------------------------------------------------
class ListCalls extends React.Component {
constructor(props) {
super(props);
}
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
render() {
return (
<Table id="calls" unstackable>
<Table.Header >
<Table.Row>
<Table.HeaderCell>Len</Table.HeaderCell>
<Table.HeaderCell>Talkgroup</Table.HeaderCell>
<Table.HeaderCell>Time</Table.HeaderCell>
<Table.HeaderCell><Icon name='star' /></Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{this.props.callsAllIds.map((callId, index) => <CallItem activeCall={callId == this.props.activeCallId ? true : false} call={this.props.callsById[callId]} talkgroups={this.props.talkgroups} key={index} onClick={this.props.playCall}/>)}
</Table.Body>
</Table>
);
}
}
export default ListCalls;

View File

@@ -0,0 +1,159 @@
import React from "react";
import {
Menu,
Icon,
Progress,
Label
} from "semantic-ui-react";
import ReactAudioPlayer from 'react-audio-player'
class MediaPlayer extends React.Component {
constructor(props) {
super(props);
this.updatePlayProgress = this.updatePlayProgress.bind(this);
this.handlePlay = this.handlePlay.bind(this);
this.handlePause = this.handlePause.bind(this);
this.playPause = this.playPause.bind(this);
this.audioRef = React.createRef();
this.state = {
sourceIndex: 0,
playProgress: 0,
isPlaying: false
}
}
handlePause = () => { this.setState({isPlaying: false}); }
handlePlay = () => { this.setState({isPlaying: true}); }
playPause = () => {const audio = this.audioRef.current.audioEl; if (this.state.isPlaying) {audio.pause();} else {audio.play();}}
playSource(callUrl) {
const audio = this.audioRef.current.audioEl;
var onEnded = this.props.onEnded;
audio.src=callUrl;
var playPromise = audio.play();
// In browsers that don’t yet support this functionality,
// playPromise won’t be defined.
if (playPromise !== undefined) {
playPromise.then(function() {
}).catch(function(error) {
console.log("Automatic playback failed: " + error);
onEnded();
// Show a UI element to let the user manually start playback.
});
}
}
updatePlayProgress() {
const audio = this.audioRef.current.audioEl;
const {currentTime, duration} = audio;
var call = this.props.call;
// this checks to see if it should display the next Source ID
if (call && ((call.srcList.length - 1) >= (this.state.sourceIndex + 1)) && (currentTime > call.srcList[this.state.sourceIndex + 1].pos)) {
this.setState({
sourceIndex: this.state.sourceIndex + 1
});
}
// updates the play percentage progress and current playing time
this.setState({
playProgress: currentTime / duration * 100,
playTime: Math.floor(currentTime)
});
}
componentWillUpdate(nextProps) {
if (nextProps.call && (nextProps.call != this.props.call)) {
this.setState({
sourceIndex: 0
});
}
}
componentDidUpdate() {
}
render(){
var playEnabled = { "disabled": true }
var sourceId = "-";
var callUrl = null;
if (this.props.call) {
callUrl = media_server + this.props.call.filename;
}
if (this.props.call) {
if (this.props.call.srcList.length > this.state.sourceIndex) {
sourceId = this.props.call.srcList[this.state.sourceIndex].src;
}
playEnabled = {};
}
return (
<Menu.Menu>
<ReactAudioPlayer
ref={this.audioRef}
onPause={this.handlePause}
onPlay={this.handlePlay}
listenInterval={500}
onListen={this.updatePlayProgress}
onEnded={this.props.onEnded}
autoPlay
/>
<Menu.Item onClick={this.playPause} >
{
this.state.isPlaying
? (<Icon name="pause"/>)
: (<Icon name="play"/>)
}
</Menu.Item>
<Menu.Item>
<Progress inverted percent={this.state.playProgress}/>
<Label color="black">
{this.state.playTime}
Sec
</Label>
<Label color="black" className="desktop-only">
{sourceId}
</Label>
</Menu.Item>
</Menu.Menu>
/* <Media>
{
mediaProps => <Menu.Menu>
<Menu.Item onClick={() => mediaProps.playPause()} {...playEnabled} >
<Player src={callUrl} vendor="audio" onEnded={this.props.onEnded} autoPlay={true} onTimeUpdate={this.updatePlayProgress}/>
{
mediaProps.isPlaying
? (<Icon name="pause"/>)
: (<Icon name="play"/>)
}
</Menu.Item><Menu.Item>
<Progress inverted percent={this.state.playProgress}/>
<Label color="black">
{this.state.playTime}
Sec
</Label>
<Label color="black" className="desktop-only">
{sourceId}
</Label>
</Menu.Item>
</Menu.Menu>
}
</Media>*/
)
}
}
export default MediaPlayer;

View File

@@ -0,0 +1,56 @@
import React, { Component } from "react";
import {
Modal,
Button,
Icon,
Dropdown
} from "semantic-ui-react";
import "./FilterModal.css";
class SupportModal extends Component {
constructor(props) {
super(props)
this.handleDone = this.handleDone.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = {
open: false
}
}
handleClose = () => this.props.onClose(false);
handleDone(event) {
this.props.onClose();
}
render() {
return (
<Modal open={this.props.open} onClose={this.handleClose} size='tiny' >
<Modal.Header>Support OpenMHz</Modal.Header>
<Modal.Content image>
<Icon size='massive' name="coffee"/>
<Modal.Description>
<p>If OpenMHz brings you joy, think about becoming a supporter! It will cover hosting costs and help keep me focused on development.</p>
<p>No worries if not, I am not having problems keeping it going.</p>
<a href="https://github.com/sponsors/robotastic"><Button>
Support
</Button></a>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={this.handleDone} >
Done
</Button>
</Modal.Actions>
</Modal>
)
}
}
export default SupportModal;

View File

@@ -0,0 +1,356 @@
import axios from "axios";
//import { browserHistory } from "react-router"
import * as types from "./call-constants";
import { push } from 'connected-react-router';
const FETCH_SUCCESS_STATISTIC = "FETCH_SUCCESS_STATISTIC"
function beginFetchCallInfo() {
return { type: types.FETCH_CALL_INFO };
}
function fetchCallInfoSuccess(data) {
return { type: types.FETCH_SUCCESS_CALL_INFO,
data };
}
function fetchCallInfoError(data) {
return { type: types.FETCH_ERROR_CALL_INFO,
data };
}
function prependCall() {
return { type: types.PREPEND_CALL };
}
function beginAddStar() {
return { type: types.ADD_STAR };
}
function addStarSuccess(data) {
return { type: types.ADD_STAR_SUCCESS, data };
}
function addStarError(data) {
return { type: types.ADD_STAR_ERROR, data };
}
function beginFetchCall() {
return { type: types.FETCH_CALL };
}
function fetchCallSuccess(data) {
return { type: types.FETCH_SUCCESS_CALL,
data };
}
function fetchOlderCallSuccess(data) {
return { type: types.FETCH_SUCCESS_OLDER_CALL,
data };
}
function fetchNewerCallSuccess(data) {
return { type: types.FETCH_SUCCESS_NEWER_CALL,
data };
}
function fetchCallError(data) {
return { type: types.FETCH_ERROR_CALL,
data };
}
export function changeUrl(url) {
return dispatch => {
dispatch(push(url));
};
}
// Example of an Async Action Creator
// http://redux.js.org/docs/advanced/AsyncActions.html
function shouldFetchCalls(state, subreddit) {
return true;
if ((!state.call.items) || (state.call.items.length <1)) {
return true
} else if (state.call.isWaiting) {
return false
} else {
return false
}
}
function buildUrlParams(a) {
return Object.keys(a).map(function(k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(a[k])
}).join('&')
}
function buildCallUrl(state, date, direction) {
var params = {};
var url = "";
if ((typeof direction === 'string') && (typeof date === 'number')) {
url = url + '/' + direction;
params["time"] = date;
} else {
if (state.call.filterDate){
url = url + '/older'
params["time"] = state.call.filterDate;
}
}
if (state.call.filterStarred) {
params["filter-starred"] = true;
}
switch(state.call.filterType) {
case 1:
params["filter-type"] = "group";
params["filter-code"] = state.call.filterGroupId;
break;
case 2:
params["filter-type"] = "talkgroup";
params["filter-code"] = state.call.filterTalkgroups;
break;
default:
case 0:
}
/*
if (this.filterCode != "") {
params["filter-code"] = this.filterCode;
}
if (this.filterType == this.FilterType.Group) {
params["filter-type"] = "group";
params["filter-name"] = this.filterName;
}
if (this.filterType == this.FilterType.Talkgroup) {
params["filter-type"] = "talkgroup";
}
if (this.filterType == this.FilterType.Unit) {
params["filter-type"] = "unit";
}
*/
// url = startUp.backend_server + "/" + startUp.shortName.toLowerCase() + "/calls" + url + '?' + buildUrlParams(params);
url = backend_server + "/" + state.call.shortName + "/calls" + url + '?' + buildUrlParams(params);
console.log("Trying to fetch data from this url: " + url);
return url;
}
export function fetchNewerCalls( timestamp) {
return (dispatch, getState) => {
const state = getState();
if (shouldFetchCalls(state)) {
dispatch(beginFetchCall());
const url = buildCallUrl(state, timestamp, "newer")
return axios
.get(url)
.then(response => {
if ((response.data) && (response.data.calls.length >0)) {
var data = {shortName: state.call.shortName, calls: response.data.calls}
dispatch(fetchNewerCallSuccess(data));
} else {
dispatch(fetchCallError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
};
}
export function fetchOlderCalls( timestamp) {
return (dispatch, getState) => {
const state = getState();
if (shouldFetchCalls(state)) {
dispatch(beginFetchCall());
const url = buildCallUrl(state, timestamp, "older")
return axios
.get(url)
.then(response => {
if (response.data) {
var data = {shortName: state.call.shortName, calls: response.data.calls}
dispatch(fetchOlderCallSuccess(data));
} else {
dispatch(fetchCallError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
};
}
export function addStar( callId) {
return (dispatch, getState) => {
const state = getState();
dispatch(beginAddStar());
const url = backend_server + "/" + state.call.shortName.toLowerCase() + "/star/" + callId
return axios
.post(url,{}, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
}).then(response => {
if (response.data.success) {
dispatch(addStarSuccess(response.data.call));
} else {
dispatch(addStarError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
};
export function fetchCallInfo( callId) {
return (dispatch, getState) => {
const state = getState();
dispatch(beginFetchCallInfo());
const url = backend_server + "/" + state.call.shortName.toLowerCase() + "/call/" + callId
return axios
.get(url)
.then(response => {
if (response.data.success) {
dispatch(fetchCallInfoSuccess(response.data.call));
} else {
dispatch(fetchCallInfoError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
};
export function fetchCalls() {
return (dispatch, getState) => {
const state = getState();
if (shouldFetchCalls(state)) {
dispatch(beginFetchCall());
const url=buildCallUrl(state);
return axios
.get(url)
.then(response => {
if (response.data) {
// check to make sure you actually got some calls back.
if (response.data.calls.length > 0) {
var data = {shortName: state.call.shortName, calls: response.data.calls}
dispatch(fetchCallSuccess(data));
} else {
dispatch(fetchCallError());
}
} else {
dispatch(fetchCallError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
};
}
export function addCall(call) {
return (dispatch, getState) => {
dispatch({ type: types.UPSERT_CALL, data: call});
};
}
export function setStarred(starred) {
return (dispatch, getState) => {
dispatch({ type: types.SET_STARRED_FILTER, data: starred});
};
}
export function setLive(live) {
return (dispatch, getState) => {
dispatch({ type: types.SET_LIVE, data: live});
};
}
export function setCurrentCallId(callId) {
return (dispatch, getState) => {
dispatch({ type: types.SET_CURRENT_CALL_ID, data: callId});
};
}
// should be as a unix timestamp
export function setDateFilter(date) {
return (dispatch, getState) => {
dispatch({ type: types.SET_DATE_FILTER, date: date});
};
}
export function setAllFilter() {
return (dispatch, getState) => {
dispatch({ type: types.SET_ALL_FILTER});
};
}
export function setTalkgroupFilter(tgs) {
return (dispatch, getState) => {
dispatch({ type: types.SET_TALKGROUP_FILTER, talkgroups: tgs});
};
}
export function setGroupFilter(groupId) {
return (dispatch, getState) => {
dispatch({ type: types.SET_GROUP_FILTER, groupId: groupId});
};
}
export function setShortName(shortName) {
return (dispatch, getState) => {
dispatch({ type: types.SET_SHORT_NAME, data: shortName});
};
}
export function setFilter(filter) {
return (dispatch, getState) => {
dispatch({ type: types.SET_FILTER, data: filter});
};
}

View File

@@ -0,0 +1,22 @@
export const PREPEND_CALL = "PREPEND_CALL"
export const ADD_STAR = "ADD_STAR"
export const ADD_STAR_SUCCESS = "ADD_STAR_SUCCESS"
export const ADD_STAR_ERROR = "ADD_STAR_ERROR"
export const UPSERT_CALL = "UPSERT_CALL"
export const FETCH_CALL = "FETCH_CALL"
export const FETCH_SUCCESS_CALL = "FETCH_SUCCESS_CALL"
export const FETCH_SUCCESS_NEWER_CALL = "FETCH_SUCCESS_NEWER_CALL"
export const FETCH_SUCCESS_OLDER_CALL = "FETCH_SUCCESS_OLDER_CALL"
export const FETCH_ERROR_CALL = "FETCH_ERROR_CALL"
export const SET_SHORT_NAME = "SET_SHORT_NAME"
export const FETCH_CALL_INFO = "FETCH_CALL_INFO"
export const FETCH_SUCCESS_CALL_INFO = "FETCH_SUCCESS_CALL_INFO"
export const FETCH_ERROR_CALL_INFO = "FETCH_ERROR_CALL_INFO"
export const SET_ALL_FILTER = "SET_ALL_FILTER"
export const SET_GROUP_FILTER = "SET_GROUP_FILTER"
export const SET_TALKGROUP_FILTER = "SET_TALKGROUP_FILTER"
export const SET_DATE_FILTER = "SET_DATE_FILTER"
export const SET_FILTER = "SET_FILTER"
export const SET_STARRED_FILTER = "SET_STARRED_FILTER"
export const SET_LIVE = "SET_LIVE"
export const SET_CURRENT_CALL_ID = "SET_CURRENT_CALL_ID"

View File

@@ -0,0 +1,227 @@
import {
FETCH_CALL_INFO,
FETCH_SUCCESS_CALL_INFO,
FETCH_ERROR_CALL_INFO,
FETCH_CALL,
PREPEND_CALL,
UPSERT_CALL,
FETCH_SUCCESS_CALL,
FETCH_SUCCESS_OLDER_CALL,
FETCH_SUCCESS_NEWER_CALL,
FETCH_ERROR_CALL,
SET_SHORT_NAME,
SET_ALL_FILTER,
SET_GROUP_FILTER,
SET_TALKGROUP_FILTER,
SET_DATE_FILTER,
SET_STARRED_FILTER,
SET_FILTER,
SET_CURRENT_CALL_ID,
SET_LIVE,
ADD_STAR_SUCCESS
} from "./call-constants";
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues);
}
function updateCallInItems(calls, action) {
return calls.map( (item) => {
if (item._id !== action.data._id) {
// This isn't the item we care about - keep it as-is
return item;
}
// Otherwise, this is the one we want - return an updated value
return {
...action.data
};
});
}
/*
function updateObjectInArray(array, action) {
return array.map((item, index) => {
if (index !== action.index) {
// This isn't the item we care about - keep it as-is
return item
}
// Otherwise, this is the one we want - return an updated value
return {
...item,
...action.item
}
})
}
function updateObjectInArray(array, shortName, updatedItem) {
return array.map( item => {
if(item.shortName !== shortName) {
// This isn't the item we care about - keep it as-is
return item;
}
// Otherwise, this is the one we want - return an updated value
return {
...item,
...updatedItem
};
});
}
*/
const now = new Date();
const call = (
state = {
isWaiting: false,
shortName: "",
byId: {},
allIds: [],
oldestCallTime: now,
newestCallTime: now,
filterType: 0,
filterTalkgroups: [],
filterGroupId: false,
filterDate: false,
filterStarred: false,
currentCallId: false,
live: false
},
action
) => {
switch (action.type) {
case SET_CURRENT_CALL_ID:
return Object.assign({}, state, {currentCallId: action.data});
case SET_FILTER:
{
var filter={};
filter.live = !action.data.live ? state.live : action.data.live;
filter.filterStarred = !action.data.filterStarred ? false : action.data.filterStarred;
filter.filterType = !action.data.filterType ? 0 : action.data.filterType;
filter.filterTalkgroups = !action.data.filterTalkgroups ? [] : action.data.filterTalkgroups;
filter.filterGroupId = !action.data.filterGroupId ? false : action.data.filterGroupId;
filter.filterDate = !action.data.filterDate ? 0 : action.data.filterDate;
return Object.assign({}, state, {
live: action.data.live,
filterType: action.data.filterType,
filterTalkgroups: action.data.filterTalkgroups,
filterGroupId: action.data.filterGroupId,
filterDate: action.data.filterDate,
filterStarred: action.data.filterStarred
});
}
case SET_LIVE:
return Object.assign({}, state, {live: action.data});
case SET_SHORT_NAME:
return Object.assign({}, state, {shortName: action.data});
case SET_DATE_FILTER:
return Object.assign({}, state, {filterDate: action.date});
case SET_STARRED_FILTER:
return Object.assign({}, state, {filterStarred: action.data});
case SET_ALL_FILTER:
return Object.assign({}, state, {filterType: 0, filterTalkgroups:[], filterGroupId: false});
case SET_GROUP_FILTER:
return Object.assign({}, state, {filterType: 1, filterTalkgroups:[], filterGroupId: action.groupId});
case SET_TALKGROUP_FILTER:
return Object.assign({}, state, {filterType: 2, filterTalkgroups: action.talkgroups, filterGroupId: false});
case FETCH_CALL_INFO:
case FETCH_CALL:
return Object.assign({}, state, { isWaiting: true});
case FETCH_SUCCESS_CALL_INFO:
return Object.assign({}, state, {
byId: {...state.byId, [action.data._id]: action.data },
isWaiting: false
});
case ADD_STAR_SUCCESS:
return Object.assign({}, state,{
byId: {...state.byId, [action.data._id]: action.data },
isWaiting: false
});
// This will set the items to the rx data
case FETCH_SUCCESS_CALL:
{
const byId = action.data.calls.reduce((acc, call) => ({ ...acc, [call._id]: call }), {});
const allIds = action.data.calls.map(call => call._id);
return Object.assign({}, state, {
isWaiting: false,
shortName: action.data.shortName,
newestCallTime: new Date(action.data.calls[0].time),
oldestCallTime: new Date(action.data.calls[action.data.calls.length-1].time),
byId: { ...byId},
allIds: [...allIds]
});
}
case UPSERT_CALL:
if (typeof state.byId[action.data._id] !== "undefined") {
return Object.assign({}, state, {
byId: {...state.byId, [action.data._id]: action.data },
});
} else {
return Object.assign({}, state, {
newestCallTime: new Date(action.data.time),
byId: {[action.data._id]: action.data, ...state.byId},
allIds: [action.data._id, ...state.allIds]
});
}
case PREPEND_CALL:
return Object.assign({}, state, {
isWaiting: false,
newestCallTime: new Date(action.data.time),
byId: {[action.data._id]: action.data, ...state.byId},
allIds: [action.data._id, ...state.allIds]
});
// This will add the array of Calls in the data to the beginning of the existing array of Calls
case FETCH_SUCCESS_NEWER_CALL:
const byId = action.data.calls.reduce((acc, call) => ({ ...acc, [call._id]: call }), {});
const allIds = action.data.calls.map(call => call._id).reverse();
return Object.assign({}, state, {
isWaiting: false,
shortName: action.data.shortName,
newestCallTime: new Date(action.data.calls[action.data.calls.length-1].time),
byId: {...state.byId, ...byId},
allIds: [...allIds, ...state.allIds]
});
// This will add the array of Calls in the data to the end of the existing array of Calls
case FETCH_SUCCESS_OLDER_CALL:
{
const byId = action.data.calls.reduce((acc, call) => ({ ...acc, [call._id]: call }), {});
const allIds = action.data.calls.map(call => call._id);
return Object.assign({}, state, {
isWaiting: false,
shortName: action.data.shortName,
oldestCallTime: new Date(action.data.calls[action.data.calls.length-1].time),
byId: {...state.byId, ...byId},
allIds: [ ...state.allIds, ...allIds]
});
}
case FETCH_ERROR_CALL_INFO:
case FETCH_ERROR_CALL:
return Object.assign({}, state, {
isWaiting: false
});
default:
return state;
}
};
export default call;

View File

@@ -0,0 +1,177 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import {
Container,
Header,
Form,
Grid,
Rail,
Segment,
List,
Input,
Button,
Message,
Icon,
Table
} from "semantic-ui-react";
// ----------------------------------------------------
const requestMessageStyle = {
color: "red"
};
// ----------------------------------------------------
class CreateGroup extends Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
// Set initial state
this.state = {
groupName: "",
group: {
name: "",
items: []
},
talkgroups: (this.props.talkgroups ? this.props.talkgroups : [])
};
}
componentDidMount() {
//this.props.groupActions.fetchGroups(this.props.shortName);
this.props.talkgroupActions.fetchTalkgroups(this.props.shortName);
}
componentWillReceiveProps(nextProps) {
this.setState({ talkgroups: nextProps.talkgroups });
}
removeTalkgroup(event, index) {
const item = {
num: this.state.group.items[index].num,
description: this.state.group.items[index].description
};
const talkgroups = this.state.talkgroups.slice(0); // copy
for (var i = 0, len = talkgroups.length; i < len; i++) {
if (item.num < talkgroups[i].num) {
talkgroups.splice(i, 0, item);
break;
}
}
this.setState({
group: { items: this.state.group.items
.slice(0, index)
.concat(this.state.group.items.slice(index + 1))},
talkgroups: talkgroups
});
}
handleInputChange = (e, { name, value }) => this.setState({ [name]: value });
addTalkgroup(event, index) {
const item = {
num: this.state.talkgroups[index].num,
description: this.state.talkgroups[index].description
};
this.setState({
group: { items: this.state.group.items.concat(item) },
talkgroups: this.state.talkgroups
.slice(0, index)
.concat(this.state.talkgroups.slice(index + 1))
});
}
handleSubmit() {
var talkgroupNums = this.state.group.items.map(a => a.num);
const data = {shortName: this.props.shortName, groupName: this.state.groupName, talkgroups: JSON.stringify(talkgroupNums) }
this.props.groupActions.createGroup(data).then(requestMessage => {
if (requestMessage) {
// report to the user is there was a problem during registration
this.setState({
requestMessage
});
} else {
this.props.groupActions.changeUrl("/system/" + this.props.shortName + "/2");
}
});
}
render() {
const talkgroups = this.state.talkgroups;
const group = this.state.group.items;
const railStyle = {
top: "15px",
minHeight: "200px"
};
return (
<div>
{talkgroups.length ? (
<Container text>
<Message
icon='upload'
header='Import Talkgroups'
content='Before you can create groups of Talkgroups, you need to import some talkgroups.'
/>
</Container>
) : (
<Grid container columns={2}>
<Grid.Column>
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Number</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell />
</Table.Row>
</Table.Header>
<Table.Body>
{talkgroups.map((talkgroup, i) => (
<Table.Row link="true" key={talkgroup.num + "-" + i}>
<Table.Cell>{talkgroup.num}</Table.Cell>
<Table.Cell>{talkgroup.description}</Table.Cell>
<Table.Cell>
<Icon
link
name="plus"
onClick={e => this.addTalkgroup(e, i)}
/>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
<Rail position="right">
<Segment style={railStyle}>
<Input action={{ onClick: this.handleSubmit, content: 'Save' }} name="groupName" onChange={this.handleInputChange} placeholder="Group Name" />
<List divided relaxed>
{group.map((talkgroup, i) => (
<List.Item key={"Group-" + i}>
<List.Icon
name="remove"
verticalAlign="middle"
link={true}
onClick={e => this.removeTalkgroup(e, i)}
/>
<List.Content>
{talkgroup.description}
</List.Content>
</List.Item>
))}
</List>
</Segment>
</Rail>
</Grid.Column>
</Grid>
)}
</div>
);
}
}
export default CreateGroup;

View File

@@ -0,0 +1,25 @@
import { connect } from "react-redux"
import * as groupActionCreators from "./group-actions"
import * as talkgroupActionCreators from "../Talkgroups/talkgroup-actions"
import { bindActionCreators } from 'redux'
import CreateGroup from "./CreateGroup"
//groups: state.group.items[props.match.params.shortName],
function mapStateToProps(state,props) {
return {
talkgroups: state.talkgroup.items[props.match.params.shortName],
shortName: props.match.params.shortName
}
}
function mapDispatchToProps(dispatch) {
return {
groupActions: bindActionCreators(groupActionCreators,dispatch),
talkgroupActions: bindActionCreators(talkgroupActionCreators, dispatch)
}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(CreateGroup)

View File

@@ -0,0 +1,77 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { Link } from 'react-router-dom'
import {
Container,
Header,
Form,
Grid,
Segment,
Input,
Button,
Message,
Icon,
Table
} from "semantic-ui-react";
// ----------------------------------------------------
// ----------------------------------------------------
class ListGroups extends Component {
constructor(props) {
super(props);
}
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
render() {
const groups = this.props.groups;
if (groups) {
return (
<Table >
<Table.Header>
<Table.Row>
<Table.HeaderCell>Name</Table.HeaderCell>
<Table.HeaderCell>Talkgroup Count</Table.HeaderCell>
<Table.HeaderCell></Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{groups.map((group, i) => (
<Table.Row key={ "Group-" + i}>
<Table.Cell>{group.groupName}</Table.Cell>
<Table.Cell>{group.talkgroups.length}</Table.Cell>
<Table.Cell><Icon
name="up arrow"
link={true}
onClick={e => this.props.reorderGroup(i, i-1)}
/><Icon
name="down arrow"
link={true}
onClick={e => this.props.reorderGroup(i, i+1)}
/><Icon
name="remove"
link={true}
onClick={e => this.props.deleteGroup(group._id)}
/>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
);
} else {
return (
<div/>
);
}
}
}
export default ListGroups;

View File

@@ -0,0 +1,73 @@
import axios from "axios";
//import { browserHistory } from "react-router"
import * as types from "./group-constants";
import { push } from 'connected-react-router';
// "Log In" action creators
function beginFetchGroup() {
return { type: types.FETCH_GROUP };
}
function fetchGroupSuccess(data) {
return {
type: types.FETCH_SUCCESS_GROUP,
data
};
}
function fetchGroupError(data) {
return {
type: types.FETCH_ERROR_GROUP,
data
};
}
export function changeUrl(url) {
return dispatch => {
dispatch(push(url));
};
}
// Example of an Async Action Creator
// http://redux.js.org/docs/advanced/AsyncActions.html
function shouldFetchGroups(state,shortName) {
if (!state.group.items || !state.group.items[shortName]) {
return true;
} else if (state.group.isWaiting) {
return false;
} else {
return false;
}
}
export function fetchGroups(shortName) {
return (dispatch, getState) => {
if (shouldFetchGroups(getState(), shortName)) {
dispatch(beginFetchGroup());
return axios
.get(backend_server + "/" + shortName + "/groups")
.then(response => {
if (true) { //response.data.success) {
const data = { shortName: shortName, groups: response.data };
dispatch(fetchGroupSuccess(data));
} else {
dispatch(fetchGroupError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
}
};
}

View File

@@ -0,0 +1,3 @@
export const FETCH_GROUP = "FETCH_GROUP"
export const FETCH_SUCCESS_GROUP = "FETCH_SUCCESS_GROUP"
export const FETCH_ERROR_GROUP = "FETCH_ERROR_GROUP"

View File

@@ -0,0 +1,62 @@
import {
FETCH_GROUP,
FETCH_SUCCESS_GROUP,
FETCH_ERROR_GROUP
} from "./group-constants";
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues);
}
function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if(item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item;
}
// Use the provided callback to create an updated item
const updatedItem = updateItemCallback(item);
return updatedItem;
});
return updatedItems;
}
const group = (
state = {
orderChange: false,
isWaiting: false,
items: {}},
action
) => {
switch (action.type) {
case FETCH_GROUP:
return Object.assign({}, state, { isWaiting: true});
case FETCH_SUCCESS_GROUP:
return Object.assign({}, state, {
isWaiting: false,
items: {
...state.items,
[action.data.shortName]: action.data.groups
}
});
case FETCH_ERROR_GROUP:
return Object.assign({}, state, {
isWaiting: false
});
default:
return state;
}
};
export default group;

View File

@@ -0,0 +1,82 @@
@font-face {
font-family: didact;
src: url("../../resources/DidactGothic-Regular.ttf");
}
#menu-bar {
background-color: #365474;
}
.header {
font-family: didact !important;
}
#header-bg {
position: relative;
}
#hero {
height: 725px;
}
.z-0 {
z-index: 0;
}
.z-5 {
z-index: 5;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.static-gradient-bg {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB8AAAAfBAMAAADtgAsKAAAAD1BMVEUAAAAAAAAAAAAAAAAAAABPDueNAAAABXRSTlMVGRsSHUz7Vs0AAAErSURBVCjPHY+BDeUgDEPttAM4wACk/QMEdYHSu/1nOjghQIodPxnRfuPnfWQO0F240K5TvRLKdYXvdAIH8ej+0yTEsh1CUYplr+j91ouhCl3WhEnpNuACvW3vUAgic8woHoA/ojO5MN6uN9EPdIJpRu7IBfTN5+I4aIAowPxSu74OAV7gfFuLJVQeAKS41Kes5k4H6tW8uML0yJcjHTE3WepIAfx+8JhJ6aCeRMRK7EQs9RjPxhheOhZL+P//zh8TW4gKArKGY2m3H1xZgq0ykzzdh9vrWPu4c1eNc0bs+suKQAKmtg4gT4kqleFxCzT1zftKUzmYi0CqWynyr4YSwhq2q/s8pf9l+oD9nbeZAXRAlJ0wFMd43ICcpTanOIQXEDTjdK54eqlV+Q9M1SO8F3P79wAAAABJRU5ErkJggg==");
width: 100%;
height: 100%;
}
/*
.static-gradient.blue {
background: -webkit-gradient(linear,left top,right top,from(#2754F0),to(#86ACFE));
background: linear-gradient(to right, #2754F0, #86ACFE);
}*/
.static-gradient.blue {
background: -webkit-gradient(linear,left top,right top,from(#9f0000),to(#ff7543));
background: linear-gradient(to right, #9f0000, #ff7543);
}
.static-gradient {
width: 100%;
height: 100%;
overflow: hidden;
-webkit-transform: skewY(-7deg);
transform: skewY(-7deg);
-webkit-transform-origin: 0;
transform-origin: 0;
border-bottom: 2px solid rgba(128,128,128,.5);
bottom: 0;
}
#menu-bg {
background-color: #3b5f85;
}
#footer {
background: -webkit-gradient(linear, bottom, top,from(#9f0000),to(#ff7543));
background: linear-gradient(to top, #9f0000, #ff7543);
}
.ui.button {
}

View File

@@ -0,0 +1,384 @@
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>
)
var site_name = process.env['SITE_NAME'] != null ? process.env['SITE_NAME'] : "OpenMHz";
/*
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 + " listners"
}
}
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;

View File

@@ -0,0 +1,16 @@
import { connect } from "react-redux"
import * as systemActions from "../System/system-actions"
import Main from "./Main"
function mapStateToProps(state) {
return {
system: state.system
}
}
export default connect(
mapStateToProps,
systemActions
)(Main)

View File

@@ -0,0 +1,74 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { Link } from 'react-router-dom'
import SystemCard from "./SystemCard";
import {
Container,
Header,
Card,
Form,
Grid,
Segment,
Input,
Button,
Message,
Icon,
Menu,
Divider
} from "semantic-ui-react";
// ----------------------------------------------------
const requestMessageStyle = {
color: "red"
};
function SystemList(props) {
const systems = props.systems;
const listItems = numbers.map((number) =>
<li>{number}</li>
);
return (
<ul>{listItems}</ul>
);
}
// ----------------------------------------------------
class ListSystems extends Component {
constructor(props) {
super(props);
}
state = {
requestMessage: "",
}
componentDidMount() {
this.props.fetchSystems();
}
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
render() {
const systems = this.props.system.items;
return (
<div>
<Menu fixed="top">
<Link to="/"><Menu.Item link><Icon name='arrow left' /> Home</Menu.Item></Link>
<Link to="/about"><Menu.Item link>About</Menu.Item></Link>
</Menu>
<Container >
<Divider horizontal style={{paddingTop:"5em", paddingBottom:"2em"}}><Header as="h1">Radio Systems<Icon name='rss' /></Header></Divider>
<Card.Group itemsPerRow={4} stackable={true}>
{systems.map((system) =>
system.active&&<SystemCard system={system} key={system.shortName} onClick={(e) => this.props.changeUrl("/system/" + system.shortName)}/>
// <SystemCard system={system} key={system.shortName} onClick={(e) => this.props.changeUrl("/system/" + system.shortName)}/>
)}
</Card.Group>
</Container>
</div>
);
}
}
export default ListSystems;

View File

@@ -0,0 +1,16 @@
import { connect } from "react-redux"
import * as systemActions from "./system-actions"
import ListSystems from "./ListSystems"
function mapStateToProps(state) {
return {
system: state.system
}
}
export default connect(
mapStateToProps,
systemActions
)(ListSystems)

View File

@@ -0,0 +1,103 @@
import React, { Component } from "react";
import {
Card,
Icon,
Label,
Header,
Grid
} from "semantic-ui-react";
class SystemCard extends Component {
constructor(props) {
super(props);
}
render() {
var freePlanValue = 0;
var proPlanValue = 10;
var location = "";
const system = this.props.system;
var clientCount = "";
var callAvg = "";
var plan = null;
var archive = "";
let label;
var description = system.description;
if (this.props.keepShort && (description.length > 100)) {
description = description.substring(0,96) + "...";
}
if (system.callAvg) {
callAvg = Math.round( system.callAvg * 10) / 10 + " call/min"
}
if (system.clientCount == 1 ) {
clientCount = "1 listener";
} else if (system.clientCount > 1 ) {
clientCount = system.clientCount + " listners"
}
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;

View File

@@ -0,0 +1,77 @@
import axios from "axios";
//import { browserHistory } from "react-router"
import * as types from "./system-constants";
import { push } from 'connected-react-router';
const FETCH_SUCCESS_STATISTIC = "FETCH_SUCCESS_STATISTIC"
function beginFetchSystem() {
return { type: types.FETCH_SYSTEM };
}
function fetchSystemSuccess(data) {
return { type: types.FETCH_SUCCESS_SYSTEM,
data };
}
function fetchSystemError(data) {
return { type: types.FETCH_ERROR_SYSTEM,
data };
}
function makeUserRequest(method, data, api = "/login") {
// returns a Promise
return axios({
method: method,
url: api,
data: data
});
}
export function changeUrl(url) {
return dispatch => {
dispatch(push(url));
};
}
// Example of an Async Action Creator
// http://redux.js.org/docs/advanced/AsyncActions.html
function shouldFetchSystems(state, subreddit) {
if ((!state.system.items) || (state.system.items.length <1)) {
return true
} else if (state.system.isWaiting) {
return false
} else {
return false
}
}
export function fetchSystems() {
return (dispatch, getState) => {
if (shouldFetchSystems(getState())) {
dispatch(beginFetchSystem());
return axios
.get(backend_server + "/systems")
.then(response => {
if (response.data.success) {
dispatch(fetchSystemSuccess(response.data.systems));
} else {
dispatch(fetchSystemError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
};
}

View File

@@ -0,0 +1,3 @@
export const FETCH_SYSTEM = "FETCH_SYSTEM"
export const FETCH_SUCCESS_SYSTEM = "FETCH_SUCCESS_SYSTEM"
export const FETCH_ERROR_SYSTEM = "FETCH_ERROR_SYSTEM"

View File

@@ -0,0 +1,67 @@
import {
CREATE_SYSTEM,
CREATE_SUCCESS_SYSTEM,
CREATE_ERROR_SYSTEM,
UPDATE_SYSTEM,
UPDATE_SUCCESS_SYSTEM,
UPDATE_ERROR_SYSTEM,
DELETE_SYSTEM,
DELETE_SUCCESS_SYSTEM,
DELETE_ERROR_SYSTEM,
FETCH_SYSTEM,
FETCH_SUCCESS_SYSTEM,
FETCH_ERROR_SYSTEM
} from "./system-constants";
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues);
}
function updateObjectInArray(array, shortName, updatedItem) {
return array.map( item => {
if(item.shortName !== shortName) {
// This isn't the item we care about - keep it as-is
return item;
}
// Otherwise, this is the one we want - return an updated value
return {
...item,
...updatedItem
};
});
}
const system = (
state = {
isWaiting: false,
items: []},
action
) => {
switch (action.type) {
case FETCH_SYSTEM:
return Object.assign({}, state, { isWaiting: true});
case FETCH_SUCCESS_SYSTEM:
return Object.assign({}, state, {
isWaiting: false,
items: action.data
});
case FETCH_ERROR_SYSTEM:
return Object.assign({}, state, {
isWaiting: false
});
default:
return state;
}
};
export default system;

View File

@@ -0,0 +1,66 @@
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { Link } from 'react-router-dom'
import {
Container,
Header,
Form,
Grid,
Segment,
Input,
Button,
Message,
Icon,
Table
} from "semantic-ui-react";
// ----------------------------------------------------
// ----------------------------------------------------
class ListTalkgroups extends Component {
constructor(props) {
super(props);
}
//https://stackoverflow.com/questions/36559661/how-can-i-dispatch-from-child-components-in-react-redux
//https://stackoverflow.com/questions/42597602/react-onclick-pass-event-with-parameter
render() {
const talkgroups = this.props.talkgroups;
if (talkgroups) {
return (
<Table celled>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Mode</Table.HeaderCell>
<Table.HeaderCell>Number</Table.HeaderCell>
<Table.HeaderCell>Alpha</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{talkgroups.map((talkgroup, i) => (
<Table.Row key={talkgroup.num + "-" + i}>
<Table.Cell>{talkgroup.mode}</Table.Cell>
<Table.Cell>{talkgroup.num}</Table.Cell>
<Table.Cell>{talkgroup.alpha}</Table.Cell>
<Table.Cell>{talkgroup.description}</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
);
} else {
return (
<div/>
);
}
}
}
export default ListTalkgroups;

View File

@@ -0,0 +1,108 @@
import axios from 'axios';
//import { browserHistory } from "react-router"
import * as types from "./talkgroup-constants";
import { push } from 'connected-react-router';
// "Log In" action creators
function beginImportTalkgroup() {
return { type: types.IMPORT_TALKGROUP };
}
function importTalkgroupSuccess(data) {
return {
type: types.IMPORT_SUCCESS_TALKGROUP,
data
};
}
function importTalkgroupError(data) {
return {
type: types.IMPORT_ERROR_TALKGROUP,
data
};
}
function beginExportTalkgroup() {
return { type: types.EXPORT_TALKGROUP };
}
function exportTalkgroupSuccess() {
return { type: types.EXPORT_SUCCESS_TALKGROUP };
}
function exportTalkgroupError() {
return { type: types.EXPORT_ERROR_TALKGROUP };
}
function beginFetchTalkgroup() {
return { type: types.FETCH_TALKGROUP };
}
function fetchTalkgroupSuccess(data) {
return { type: types.FETCH_SUCCESS_TALKGROUP,
data };
}
function fetchTalkgroupError(data) {
return { type: types.FETCH_ERROR_TALKGROUP,
data };
}
function makeUserRequest(method, data, api) {
// returns a Promise
return axios({
method: method,
url: api,
data: data
});
}
export function changeUrl(url) {
return dispatch => {
dispatch(push(url));
};
}
// https://stackoverflow.com/questions/41878838/how-do-i-set-multipart-in-axios-with-react
function shouldFetchTalkgroups(state, shortName) {
if (!state.talkgroup.items.hasOwnProperty(shortName)) {
return true
} else if (state.talkgroup.isWaiting) {
return false
} else {
return false
}
}
export function fetchTalkgroups(shortName) {
return (dispatch, getState) => {
if (shouldFetchTalkgroups(getState(), shortName)) {
dispatch(beginFetchTalkgroup());
return axios
.get(backend_server + "/" + shortName + "/talkgroups")
.then(response => {
if (response.data) {
var data = {shortName: shortName, talkgroups: response.data.talkgroups}
dispatch(fetchTalkgroupSuccess( data));
} else {
dispatch(fetchTalkgroupError());
let registerMessage = response.data.message;
return registerMessage;
}
})
.catch(response => {
if (response instanceof Error) {
// Something happened in setting up the request that triggered an Error
console.log("Error", response.message);
}
});
};
};
}

View File

@@ -0,0 +1,9 @@
export const IMPORT_TALKGROUP = "IMPORT_TALKGROUP"
export const IMPORT_SUCCESS_TALKGROUP = "IMPORT_SUCCESS_TALKGROUP"
export const IMPORT_ERROR_TALKGROUP = "IMPORT_ERROR_TALKGROUP"
export const EXPORT_TALKGROUP = "EXPORT_TALKGROUP"
export const EXPORT_SUCCESS_TALKGROUP = "EXPORT_SUCCESS_TALKGROUP"
export const EXPORT_ERROR_TALKGROUP = "EXPORT_ERROR_TALKGROUP"
export const FETCH_TALKGROUP = "FETCH_TALKGROUP"
export const FETCH_SUCCESS_TALKGROUP = "FETCH_SUCCESS_TALKGROUP"
export const FETCH_ERROR_TALKGROUP = "FETCH_ERROR_TALKGROUP"

View File

@@ -0,0 +1,72 @@
import {
IMPORT_TALKGROUP,
IMPORT_SUCCESS_TALKGROUP,
IMPORT_ERROR_TALKGROUP,
EXPORT_TALKGROUP,
EXPORT_SUCCESS_TALKGROUP,
EXPORT_ERROR_TALKGROUP,
FETCH_TALKGROUP,
FETCH_SUCCESS_TALKGROUP,
FETCH_ERROR_TALKGROUP
} from "./talkgroup-constants";
function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues);
}
function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if(item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item;
}
// Use the provided callback to create an updated item
const updatedItem = updateItemCallback(item);
return updatedItem;
});
return updatedItems;
}
const talkgroup = (
state = {
isWaiting: false,
items: {}},
action
) => {
switch (action.type) {
case FETCH_TALKGROUP:
case IMPORT_TALKGROUP:
case EXPORT_TALKGROUP:
return Object.assign({}, state, { isWaiting: true});
case IMPORT_SUCCESS_TALKGROUP:
case FETCH_SUCCESS_TALKGROUP:
return Object.assign({}, state, {
isWaiting: false,
items: {
...state.items,
[action.data.shortName]: action.data.talkgroups
}
});
case FETCH_ERROR_TALKGROUP:
case IMPORT_ERROR_TALKGROUP:
case EXPORT_ERROR_TALKGROUP:
case EXPORT_SUCCESS_TALKGROUP:
return Object.assign({}, state, {
isWaiting: false
});
default:
return state;
}
};
export default talkgroup;

View File

@@ -0,0 +1,86 @@
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 ReactGA from 'react-ga';
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()
ReactGA.initialize('UA-45563211-1');
history.listen((location, action) => {
ReactGA.pageview(location.pathname + location.search);
console.log(location.pathname)
});
// 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"))

View File

@@ -0,0 +1,17 @@
import React, { Component } from "react"
import '../../../styling/semantic.less'
class App extends Component {
constructor(props) {
super(props);
}
render() {
return(
<div>
{this.props.children}
</div>
)
}
}
export default App

View File

@@ -0,0 +1,42 @@
import React, { Component } from "react";
import { Link } from 'react-router-dom'
import { Container, Header, Divider, Icon, Menu } from "semantic-ui-react";
class About extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<Menu fixed="top">
<Link to="/"><Menu.Item link><Icon name='arrow left' /> Home</Menu.Item></Link>
<Link to="/systems"><Menu.Item link>Listen</Menu.Item></Link>
</Menu>
<Container text>
<Divider horizontal style={{paddingTop:"4em", paddingBottom:"2em"}}><Header as="h1">About<Icon name='compass' /></Header></Divider>
<p>I wrote <a href="https://github.com/robotastic/trunk-recorder">Trunk Recorder</a> because I was curious
about what my local fire station was up to and I put together the original version of OpenMHz because I
figured other people might want to listen to the recordings too.</p>
<p>The latest version of this site makes it easy for other people running Trunk Recorder to share their recrodings.
I am hoping that making it easier to listen to what our local fire, police and EMS have to go through everyday will
lead to greater appreciation for all the work they do that goes largely unseen. I am personally amazed by the numebr
of medical runs the DC's Fire and EMS department has to cover every day, and the shocking number of them that are ODs.</p>
<p>I will do my best to keep adding new features to this site. One thing I want to add is visualizations to make it easier to find interesting calls.
Let me know if you have any ideas.</p>
<p>Right now, each system will only have a 5 day archive. This is to help make sure things don't get overloaded and make sure I don't get a crazy bill
at the end of the month.</p>
<p> - Luke <a href="mailto:admin@email.com?Subject=OpenMHz" target="_top">admin@email.com</a> </p>
</Container>
</div>
);
}
}
export default About;

View File

@@ -0,0 +1,31 @@
import React, { Component } from "react"
class Default extends Component {
constructor(props) {
super(props);
}
render() {
return(
<div>
<h2>React-Passport-Redux-Example</h2>
<p>Demonstration of PassportJS authentication in React w/ Redux. Use the links above to register a new user and login/logout. You will only be able to view the MyProfile page when you are logged in. Logging in will redirect you to MyProfile if you had clicked that link beforehand.</p>
<p>I'll be doing my best to document code with useful comments so look for more detail there. Note: you should be able to implement the authentication functionality using less packages. I was working on another project before deciding to make this example repo so some bloat may have travelled over from that.</p>
<p>In full, this repo demonstrates:</p>
<ul>
<li><strong>React</strong> for the view layer </li>
<li><strong>PassportJS</strong> for authentication (using a Local Strategy)</li>
<li><strong>Redux</strong> to handle our state</li>
<li><strong>React Router</strong> for routing client-side</li>
<li><strong>Express</strong> for handling server requests (REST and page requests)</li>
<li><strong>MongoDB</strong> for our database, with <strong>Mongoose</strong> handling our schema</li>
<li><strong>Webpack</strong> to bundle our client-side code</li>
<li>Hot reloading using <strong>webpack-dev-middleware</strong> and <strong>webpack-hot-middleware</strong></li>
</ul>
<p>Shoutout to GitHub member <b>choonkending</b> whose <a href="https://github.com/choonkending/react-webpack-node" target="_blank">repo</a> was a big help. Pull requests and comments / issue reports are most welcome!</p>
</div>
)
}
}
export default Default

View File

@@ -0,0 +1,31 @@
import React, { Component } from "react";
import { Modal, Icon, Button, Header } from "semantic-ui-react";
class Message extends Component {
constructor(props) {
super(props);
}
_logout = event => {
event.preventDefault();
this.props.manualLogout();
};
render() {
return (
<Modal open={this.props.open} onClose={this.props.onClose} basic size="small">
<Header icon="info" content={this.props.title} />
<Modal.Content>
<p>{this.props.message}</p>
</Modal.Content>
<Modal.Actions>
<Button color='green' onClick={this.props.onClose} inverted>
<Icon name='checkmark' /> Got it
</Button>
</Modal.Actions>
</Modal>
);
}
}
export default Message;

View File

@@ -0,0 +1,5 @@
.field {
padding-top: 10px;
padding-left: 10px;
padding-right: 10px;
}

View File

@@ -0,0 +1,11 @@
export default {
parse: function parse(queryString) {
var query = {};
var pairs = (queryString[0] === '?' ? queryString.substr(1) : queryString).split('&');
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i].split('=');
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || '');
}
return query;
}
}

View File

@@ -0,0 +1,15 @@
import { combineReducers } from "redux"
import { connectRouter } from 'connected-react-router'
import system from "../System/system-reducer"
import talkgroup from "../Talkgroups/talkgroup-reducer"
import group from "../Group/group-reducer"
import call from "../Call/call-reducer"
export default (history) => combineReducers({
router: connectRouter(history),
system,
talkgroup,
group,
call
})

View File

@@ -0,0 +1,34 @@
import * as types from '../Call/call-constants'
import * as callActionCreators from "../Call/call-actions"
const setupSocket = (cb, sn, filterType, filterCode) => {
const shortName = sn
const socket = new WebSocket(socket_server + '/')
const callback = cb;
socket.onopen = () => {
socket.send(JSON.stringify({
type: 'add',
filterCode: filterCode,
filterType: filterType,
filterName: "",
shortName: shortName
}))
}
socket.onmessage = (event) => {
callback(event.data);
}
socket.updateFilter = (filterType,filterCode, filterName) => {
if (socket.readyState == WebSocket.OPEN) {
socket.send(JSON.stringify({
type: 'update',
filterCode: filterCode,
filterType: filterType,
filterName: filterName,
shortName: shortName
}))
}
}
return socket
}
export default setupSocket

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { storiesOf } from '@storybook/react';
import { Button } from '@storybook/react/demo';
import SystemCard from '../System/SystemCard.js';
import 'semantic-ui-css/semantic.min.css'
storiesOf('Button', module)
.add('with text', () => (
<Button>Hello Button</Button>
))
.add('with some emoji', () => (
<Button><span role="img" aria-label="so cool">😀 😎 👍 💯</span></Button>
));
export const system = {active: false,
callAvg: 1,
city: "",
clientCount: 1,
country: "",
description: "This is an awesome system",
name: "Awesome Town",
planType: 10,
shortName: "awesome",
screenName: "luke",
state: "AL",
systemType: "state"}
storiesOf('SystemCard', module).add("standard", () => {
return (<SystemCard system={system} key={system.shortName} onClick={(e) => this.props.changeUrl("/system/" + system.shortName)}/>)
});

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,28 @@
import path from "path"
import express from "express"
import bodyParser from "body-parser"
export default function(app) {
app.set("port", 3000)
// X-Powered-By header has no functional value.
// Keeping it makes it easier for an attacker to build the site's profile
// It can be removed safely
app.disable("x-powered-by")
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(express.static(path.join(process.cwd(), 'public')));
var node_env = process.env.NODE_ENV;
console.log('--------------------------');
console.log('===> 😊 Starting Server . . .');
console.log('===> Environment: ' + node_env);
if(node_env === 'production') {
console.log('===> 🚦 Note: In order for authentication to work in production');
console.log('===> you will need a secure HTTPS connection');
}
}

View File

@@ -0,0 +1,103 @@
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};
</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]`)
}
})

View File

@@ -0,0 +1,4 @@
@type : 'element';
@element : 'icon';
@import (multiple) '../../theme.config';

View File

@@ -0,0 +1,61 @@
/*
Import this file into your LESS project to use Semantic UI without build tools:
import 'styling/styles.less'
Here you can disable unused styling for unused components.
*/
/* Global */
& { @import "~semantic-ui-less/definitions/globals/reset.less"; }
& { @import "~semantic-ui-less/definitions/globals/site.less"; }
/* Elements */
& { @import "~semantic-ui-less/definitions/elements/button.less"; }
& { @import "~semantic-ui-less/definitions/elements/container.less"; }
& { @import "~semantic-ui-less/definitions/elements/divider.less"; }
& { @import "~semantic-ui-less/definitions/elements/flag.less"; }
& { @import "~semantic-ui-less/definitions/elements/header.less"; }
& { @import "~semantic-ui-less/definitions/elements/icon.less"; }
& { @import "~semantic-ui-less/definitions/elements/image.less"; }
& { @import "~semantic-ui-less/definitions/elements/input.less"; }
& { @import "~semantic-ui-less/definitions/elements/label.less"; }
& { @import "~semantic-ui-less/definitions/elements/list.less"; }
& { @import "~semantic-ui-less/definitions/elements/loader.less"; }
& { @import "~semantic-ui-less/definitions/elements/rail.less"; }
& { @import "~semantic-ui-less/definitions/elements/reveal.less"; }
& { @import "~semantic-ui-less/definitions/elements/segment.less"; }
& { @import "~semantic-ui-less/definitions/elements/step.less"; }
///* Collections */
& { @import "~semantic-ui-less/definitions/collections/breadcrumb.less"; }
& { @import "~semantic-ui-less/definitions/collections/form.less"; }
& { @import "~semantic-ui-less/definitions/collections/grid.less"; }
& { @import "~semantic-ui-less/definitions/collections/menu.less"; }
& { @import "~semantic-ui-less/definitions/collections/message.less"; }
& { @import "~semantic-ui-less/definitions/collections/table.less"; }
/* Views */
& { @import "~semantic-ui-less/definitions/views/ad.less"; }
& { @import "~semantic-ui-less/definitions/views/card.less"; }
& { @import "~semantic-ui-less/definitions/views/comment.less"; }
& { @import "~semantic-ui-less/definitions/views/feed.less"; }
& { @import "~semantic-ui-less/definitions/views/item.less"; }
& { @import "~semantic-ui-less/definitions/views/statistic.less"; }
/* Modules */
& { @import "~semantic-ui-less/definitions/modules/accordion.less"; }
& { @import "~semantic-ui-less/definitions/modules/checkbox.less"; }
& { @import "~semantic-ui-less/definitions/modules/dimmer.less"; }
& { @import "~semantic-ui-less/definitions/modules/dropdown.less"; }
& { @import "~semantic-ui-less/definitions/modules/embed.less"; }
& { @import "~semantic-ui-less/definitions/modules/modal.less"; }
& { @import "~semantic-ui-less/definitions/modules/nag.less"; }
& { @import "~semantic-ui-less/definitions/modules/popup.less"; }
& { @import "~semantic-ui-less/definitions/modules/progress.less"; }
& { @import "~semantic-ui-less/definitions/modules/rating.less"; }
& { @import "~semantic-ui-less/definitions/modules/search.less"; }
& { @import "~semantic-ui-less/definitions/modules/shape.less"; }
& { @import "~semantic-ui-less/definitions/modules/sidebar.less"; }
& { @import "~semantic-ui-less/definitions/modules/sticky.less"; }
& { @import "~semantic-ui-less/definitions/modules/tab.less"; }
& { @import "~semantic-ui-less/definitions/modules/transition.less"; }

View File

@@ -0,0 +1,83 @@
/*******************************
Theme Selection
*******************************/
/* To override a theme for an individual element specify theme name below */
/* Global */
@site : 'default';
@reset : 'default';
/* Elements */
@button : 'default';
@container : 'default';
@divider : 'default';
@flag : 'default';
@header : 'default';
@icon : 'default';
@image : 'default';
@input : 'default';
@label : 'default';
@list : 'default';
@loader : 'default';
@rail : 'default';
@reveal : 'default';
@segment : 'default';
@step : 'default';
/* Collections */
@breadcrumb : 'default';
@form : 'default';
@grid : 'default';
@menu : 'default';
@message : 'default';
@table : 'default';
/* Modules */
@accordion : 'default';
@checkbox : 'default';
@dimmer : 'default';
@dropdown : 'default';
@embed : 'default';
@modal : 'default';
@nag : 'default';
@popup : 'default';
@progress : 'default';
@rating : 'default';
@search : 'default';
@shape : 'default';
@sidebar : 'default';
@sticky : 'default';
@tab : 'default';
@transition : 'default';
/* Views */
@ad : 'default';
@card : 'default';
@comment : 'default';
@feed : 'default';
@item : 'default';
@statistic : 'default';
/*******************************
Folders
*******************************/
/* Path to theme packages */
@themesFolder : 'themes';
/* Path to site override folder */
@siteFolder : '../../src/styling/theme';
/*******************************
Import Theme
*******************************/
@import (multiple) "~semantic-ui-less/theme.less";
/*******************************
Import Overrides
*******************************/
@fontPath : '../../../themes/@{theme}/assets/fonts';

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*-------------------
Flag Variables
--------------------*/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Variable Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
Site Overrides
*******************************/

View File

@@ -0,0 +1,3 @@
/*******************************
User Global Variables
*******************************/

Some files were not shown because too many files have changed in this diff Show More