mirror of
https://github.com/openmhz/trunk-server.git
synced 2026-11-12 08:47:40 -05:00
@@ -2,7 +2,7 @@
|
||||
var ObjectID = require('mongodb').ObjectID;
|
||||
var db = require('../db');
|
||||
var mongoose = require("mongoose");
|
||||
var Call = require("../models/call");
|
||||
var {callModel:Call} = require("../models/call");
|
||||
var Star = require("../models/star");
|
||||
|
||||
var defaultNumResults = 50;
|
||||
@@ -246,7 +246,7 @@ exports.remove_star = function(req, res, next) {
|
||||
try {
|
||||
var o_id = ObjectID.createFromHexString(objectId);
|
||||
} catch (err) {
|
||||
console.warn("[" + req.params.shortName + "] Error - /:shortName/call/:id generating ObjectID " + err);
|
||||
console.warn("[" + req.params.shortName + "] Error - /remove_star/:id generating ObjectID " + err);
|
||||
res.status(500);
|
||||
res.send(JSON.stringify({
|
||||
success: false,
|
||||
@@ -281,7 +281,7 @@ exports.add_star = function(req, res, next) {
|
||||
try {
|
||||
var o_id = ObjectID.createFromHexString(objectId);
|
||||
} catch (err) {
|
||||
console.warn("[" + req.params.shortName + "] Error - /:shortName/call/:id generating ObjectID " + err);
|
||||
console.warn("[" + req.params.shortName + "] Error - /add_star/:id generating ObjectID " + err);
|
||||
res.status(500);
|
||||
res.send(JSON.stringify({
|
||||
success: false,
|
||||
|
||||
95
backend/controllers/events.js
Normal file
95
backend/controllers/events.js
Normal file
@@ -0,0 +1,95 @@
|
||||
var ObjectID = require('mongodb').ObjectID;
|
||||
var db = require('../db');
|
||||
var mongoose = require("mongoose");
|
||||
var Event = require("../models/event");
|
||||
var { callModel: Call, callSchema } = require("../models/call");
|
||||
|
||||
exports.getEvent = async function (req, res, next) {
|
||||
|
||||
try {
|
||||
var objectId = req.params.id;
|
||||
const event = await Event.findById(objectId).exec();
|
||||
res.contentType('json');
|
||||
res.send(JSON.stringify(event));
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
res.status(500);
|
||||
res.send(`Error fetching Event ${req.params.id}: ` + err);
|
||||
}
|
||||
}
|
||||
|
||||
exports.getEvents = async function (req, res, next) {
|
||||
try {
|
||||
let events = await Event.find({},["title", "description", "startTime", "endTime", "expireTime", "numCalls", "createdAt"]).exec();
|
||||
res.contentType('json');
|
||||
res.send(JSON.stringify(events));
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
res.status(500);
|
||||
res.send("Error fetching Events" + err);
|
||||
}
|
||||
}
|
||||
|
||||
const compareCalls = (a, b) => {
|
||||
const aTimestamp = new Date(a.time).getTime()
|
||||
const bTimestamp = new Date(b.time).getTime()
|
||||
if (aTimestamp > bTimestamp) {
|
||||
return -1
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
exports.addNewEvent = async function (req, res, next) {
|
||||
process.nextTick(async function () {
|
||||
let event = new Event();
|
||||
if (!req.body) {
|
||||
console.error("No Request Body")
|
||||
next();
|
||||
}
|
||||
//console.log(req);
|
||||
console.log(req.body);
|
||||
const title = req.body.title;
|
||||
const description = req.body.description;
|
||||
const callIds = req.body.callIds;
|
||||
let objectIds = [];
|
||||
callIds.forEach((id) => {
|
||||
objectIds.push(new ObjectID(id));
|
||||
})
|
||||
|
||||
event.title = title.replace(/[^a-zA-Z0-9 \.\-\_]/g, "");
|
||||
event.description = description.replace(/[^a-zA-Z0-9 \.\-\_]/g, "");
|
||||
try {
|
||||
let calls = await Call.find({ "_id": { $in: objectIds } });
|
||||
if (callIds.length != calls.length) {
|
||||
res.status(500);
|
||||
res.send("Not all of the Calls were found");
|
||||
}
|
||||
calls = calls.sort(compareCalls);
|
||||
event.startTime = new Date(calls[0].time);
|
||||
event.endTime = new Date(calls[calls.length - 1].time);
|
||||
event.expireTime = event.startTime;
|
||||
calls.forEach((call) => {
|
||||
if (event.shortNames.indexOf(call.shortName) === -1) {
|
||||
event.shortNames.push(call.shortName);
|
||||
}
|
||||
});
|
||||
event.calls = calls;
|
||||
event.numCalls = calls.length;
|
||||
console.log(await event.save());
|
||||
res.contentType('json');
|
||||
res.send(JSON.stringify({
|
||||
success: false,
|
||||
error: "Upload failed"
|
||||
}));
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
res.status(500);
|
||||
res.send("Error parsing sourcelist " + err);
|
||||
}
|
||||
|
||||
|
||||
})
|
||||
};
|
||||
@@ -8,7 +8,7 @@ var mongoose = require("mongoose");
|
||||
var System = require("../models/system");
|
||||
var Talkgroup = require("../models/talkgroup");
|
||||
var Group = require("../models/group");
|
||||
var Call = require("../models/call");
|
||||
var {callModel:Call} = require("../models/call");
|
||||
var secrets = require("../config/secrets");
|
||||
var util = require("util")
|
||||
var config = require('../config/config.json');
|
||||
|
||||
@@ -13,7 +13,7 @@ var systems = require("./controllers/systems");
|
||||
var talkgroups = require("./controllers/talkgroups");
|
||||
var groups = require("./controllers/groups");
|
||||
var stats = require("./controllers/stats");
|
||||
|
||||
var events = require("./controllers/events");
|
||||
var config = require('./config/config.json');
|
||||
|
||||
var mongoose = require("mongoose");
|
||||
@@ -169,7 +169,10 @@ function addSystemClients(req, res, next) {
|
||||
|
||||
|
||||
/*------ CALLS ----------*/
|
||||
|
||||
app.get('/card/:id', calls.get_card);
|
||||
app.post('/add_star/:id', calls.add_star);
|
||||
app.post('/remove_star/:id', calls.remove_star);
|
||||
app.get('/:shortName/call/:id', calls.get_call);
|
||||
app.get('/:shortName/calls/latest', calls.get_latest_calls);
|
||||
app.get('/:shortName/calls/next', calls.get_next_calls);
|
||||
@@ -177,8 +180,7 @@ app.get('/:shortName/calls/newer', calls.get_newer_calls);
|
||||
app.get('/:shortName/calls/older', calls.get_older_calls);
|
||||
app.get('/:shortName/calls/:time/older', calls.get_iphone_calls);
|
||||
app.get('/:shortName/calls', calls.get_calls);
|
||||
app.post('/:shortName/star/:id', calls.add_star);
|
||||
app.post('/:shortName/removeStar/:id', calls.remove_star);
|
||||
|
||||
|
||||
/*------ UPLOADS ---------- upload.single('call'), uploads.upload,*/
|
||||
app.post('/:shortName/upload', upload.single('call'), uploads.upload, async function (req, res) {
|
||||
@@ -209,6 +211,10 @@ app.get('/:shortName/talkgroups', talkgroups.get_talkgroups);
|
||||
/*------ GROUPS ----------*/
|
||||
app.get('/:shortName/groups', groups.get_groups);
|
||||
|
||||
/*------ EVENTS ----------*/
|
||||
app.post('/events', events.addNewEvent);
|
||||
app.get('/events', events.getEvents);
|
||||
app.get('/events/:id', events.getEvent);
|
||||
|
||||
/*------ STATS ----------*/
|
||||
app.get('/:shortName/errors', stats.get_errors);
|
||||
|
||||
@@ -26,4 +26,5 @@ var callSchema = mongoose.Schema({
|
||||
});
|
||||
|
||||
|
||||
module.exports = mongoose.model('Call', callSchema);
|
||||
exports.callSchema = callSchema;
|
||||
exports.callModel = mongoose.model('Call', callSchema);
|
||||
|
||||
23
backend/models/event.js
Normal file
23
backend/models/event.js
Normal file
@@ -0,0 +1,23 @@
|
||||
var mongoose = require('mongoose');
|
||||
var {callSchema} = require('./call');
|
||||
console.log(callSchema)
|
||||
|
||||
var eventSchema = mongoose.Schema({
|
||||
title: String,
|
||||
description: String,
|
||||
expireTime: Date,
|
||||
startTime: Date,
|
||||
endTime: Date,
|
||||
shortNames: [String],
|
||||
calls: [callSchema],
|
||||
numCalls: Number
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
});
|
||||
|
||||
// add virtual if You want
|
||||
eventSchema.virtual('callCount').get(function () {
|
||||
return this.calls.length;
|
||||
});
|
||||
module.exports = mongoose.model('Event', eventSchema);
|
||||
4339
backend/package-lock.json
generated
Normal file
4339
backend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
113
frontend/src/Call/CallInfo.js
Normal file
113
frontend/src/Call/CallInfo.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Header,
|
||||
Divider,
|
||||
List,
|
||||
Segment,
|
||||
Statistic,
|
||||
Icon,
|
||||
Menu,
|
||||
Tab
|
||||
} from "semantic-ui-react";
|
||||
import PlaylistBuilder from "./PlaylistBuilder"
|
||||
// ----------------------------------------------------
|
||||
function CallInfo(props) {
|
||||
|
||||
|
||||
|
||||
let srcList = "";
|
||||
let callLength = "-";
|
||||
let callFreq = "-";
|
||||
let callDate = "-";
|
||||
let callTime = "-";
|
||||
let talkgroupNum = "-";
|
||||
let callDownload = "";
|
||||
let callTweet = "";
|
||||
if (props.call) {
|
||||
const currentCall = 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;
|
||||
callDownload = currentCall.url;
|
||||
callTweet = "https://twitter.com/intent/tweet?url=" + encodeURIComponent(document.location.origin + props.link) + "&via=" + encodeURIComponent("OpenMHz");
|
||||
}
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const handleTabChange = (e, data) => setActiveTab(data.activeIndex);
|
||||
|
||||
|
||||
|
||||
const panes = [
|
||||
{
|
||||
menuItem: 'Call Info', render: () => {
|
||||
return (
|
||||
<div>
|
||||
<Tab.Pane attached='bottom'>
|
||||
<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>
|
||||
|
||||
</Tab.Pane>
|
||||
<Menu>
|
||||
<a href={callTweet}><Menu.Item name="tweet" ><Icon name='twitter' />Tweet</Menu.Item></a>
|
||||
<a href={callDownload}><Menu.Item name="download"><Icon name="download" />Download</Menu.Item></a>
|
||||
<a href={props.link}><Menu.Item name="link"><Icon name="at" />Link</Menu.Item></a>
|
||||
</Menu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
menuItem: 'Playlist', render: () => {
|
||||
return (
|
||||
<PlaylistBuilder />
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
return (
|
||||
<div>
|
||||
<Tab menu={{ attached: 'top' }} panes={panes} defaultActiveIndex={activeTab} onTabChange={handleTabChange} />
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default CallInfo;
|
||||
@@ -344,6 +344,7 @@ function Calls(props) {
|
||||
<span> </span><Icon name="close" inverted={true} link={true} size='small' /></Menu.Item>
|
||||
<Link to="/"><Menu.Item link>Home</Menu.Item></Link>
|
||||
<Link to="/systems"><Menu.Item link>Systems</Menu.Item></Link>
|
||||
<Link to="/events"><Menu.Item link>Events</Menu.Item></Link>
|
||||
<Link to="/about"><Menu.Item link>About</Menu.Item></Link>
|
||||
</Sidebar>
|
||||
<Menu fixed="top">
|
||||
|
||||
146
frontend/src/Call/GroupModal.js
Normal file
146
frontend/src/Call/GroupModal.js
Normal file
@@ -0,0 +1,146 @@
|
||||
import React, { useState } from "react";
|
||||
import { useGetGroupsQuery } from '../features/api/apiSlice'
|
||||
import { setGroupFilter } from "../features/callPlayer/callPlayerSlice";
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
Icon,
|
||||
Dropdown
|
||||
} from "semantic-ui-react";
|
||||
import "./FilterModal.css";
|
||||
|
||||
function GroupModal (props) {
|
||||
const { data, isError, isLoading, isSuccess,error } = useGetGroupsQuery(props.shortName);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [selectedGroup, setSelectedGroup] = useState(false);
|
||||
const dispatch = useDispatch()
|
||||
const onClose = props.onClose;
|
||||
|
||||
function handleDone(onClose) {
|
||||
|
||||
if (selectedGroup) {
|
||||
dispatch(setGroupFilter(selectedGroup))
|
||||
//dispatch({type: types.SET_GROUP_FILTER, groupId: selectedGroup })
|
||||
onClose(true);
|
||||
} else {
|
||||
onClose(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let groupOptions = []
|
||||
|
||||
if (isSuccess) {
|
||||
|
||||
for (const num in data) {
|
||||
const group = data[num];
|
||||
var obj = {
|
||||
key: group._id,
|
||||
value: group._id,
|
||||
text: group.groupName
|
||||
}
|
||||
groupOptions.push(obj);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<Modal open={props.open} onClose={() => setOpen(false)} onOpen={() => setOpen(true)} 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={groupOptions} name='selectedGroup' onChange={(e, data) => setSelectedGroup(data.value)} />
|
||||
|
||||
</Modal.Description>
|
||||
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={()=> handleDone(onClose)} >
|
||||
<Icon name='headphones' /> Listen
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
|
||||
)
|
||||
}
|
||||
/*
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
componentDidUpdate(prevProps) {
|
||||
|
||||
const filterChanged = (prevProps.selectedGroup !== this.props.selectedGroup);
|
||||
if (filterChanged) {
|
||||
this.setState({ selectedGroup: this.props.selectedGroup});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
|
||||
var groupList = [];
|
||||
if (this.props.groups) {
|
||||
for (const num in this.props.groups) {
|
||||
const group = this.props.groups[num];
|
||||
var obj = {
|
||||
key: group._id,
|
||||
value: group._id,
|
||||
text: group.groupName
|
||||
}
|
||||
groupList.push(obj);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
<Modal open={this.props.open} onClose={this.handleClose} size='tiny' >
|
||||
<Modal.Header>Select a Group</Modal.Header>
|
||||
<Modal.Content >
|
||||
<Modal.Description>
|
||||
<p>Choose the type of calls you want to listen to</p>
|
||||
<Dropdown placeholder='Group...' fluid selection options={groupList} name='selectedGroup' onChange={this.handleInputChange} />
|
||||
|
||||
</Modal.Description>
|
||||
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button onClick={this.handleDone} >
|
||||
<Icon name='headphones' /> Listen
|
||||
</Button>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
|
||||
)
|
||||
}
|
||||
}*/
|
||||
|
||||
export default GroupModal;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useCallLink } from "./CallLinks";
|
||||
import { useGetGroupsQuery, useGetTalkgroupsQuery } from '../../features/api/apiSlice'
|
||||
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom';
|
||||
@@ -9,9 +9,11 @@ import {
|
||||
Segment,
|
||||
Statistic,
|
||||
Icon,
|
||||
Menu
|
||||
Menu,
|
||||
Tab
|
||||
} from "semantic-ui-react";
|
||||
|
||||
import PlaylistBuilder from "./PlaylistBuilder"
|
||||
import CallInfoPane from "./CallInfoPane"
|
||||
// ----------------------------------------------------
|
||||
function CallInfo(props) {
|
||||
const { shortName } = useParams();
|
||||
@@ -44,51 +46,35 @@ function CallInfo(props) {
|
||||
talkgroupNum = currentCall.talkgroupNum;
|
||||
|
||||
}
|
||||
const {callLink,callDownload,callTweet} = useCallLink(props.call)
|
||||
const { callLink, callDownload, callTweet } = useCallLink(props.call)
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
const handleTabChange = (e, data) => setActiveTab(data.activeIndex);
|
||||
|
||||
const panes = [
|
||||
{
|
||||
menuItem: { key: 'info', icon: 'info', content: 'Call Info' }, render: () => {
|
||||
return (
|
||||
|
||||
<Tab.Pane attached='bottom'>
|
||||
<CallInfoPane call={props.call} />
|
||||
</Tab.Pane>
|
||||
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
menuItem: { key: 'list', icon: 'list', content: 'Create Event' }, render: () => {
|
||||
return (
|
||||
<PlaylistBuilder />
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Segment padded attached>
|
||||
<Header as='h1'>{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>
|
||||
<Menu attached='bottom'>
|
||||
<a href={callTweet}><Menu.Item name="tweet" ><Icon name='twitter' />Tweet</Menu.Item></a>
|
||||
<a href={callDownload}><Menu.Item name="download"><Icon name="download" />Download</Menu.Item></a>
|
||||
<a href={callLink}><Menu.Item name="link"><Icon name="at" />Link</Menu.Item></a>
|
||||
</Menu>
|
||||
<Tab menu={{ attached: 'top', fluid: true, widths: 2 }} panes={panes} defaultActiveIndex={activeTab} onTabChange={handleTabChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
98
frontend/src/Call/components/CallInfoPane.js
Normal file
98
frontend/src/Call/components/CallInfoPane.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import React, { useState } from "react";
|
||||
import { useCallLink } from "./CallLinks";
|
||||
import {skipToken} from '@reduxjs/toolkit/query'
|
||||
import { useGetGroupsQuery, useGetTalkgroupsQuery } from '../../features/api/apiSlice'
|
||||
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Header,
|
||||
Divider,
|
||||
List,
|
||||
Segment,
|
||||
Statistic,
|
||||
Icon,
|
||||
Menu,
|
||||
Tab
|
||||
} from "semantic-ui-react";
|
||||
import PlaylistBuilder from "./PlaylistBuilder"
|
||||
// ----------------------------------------------------
|
||||
function CallInfoPane(props) {
|
||||
|
||||
|
||||
let srcList = "";
|
||||
let callLength = "-";
|
||||
let callFreq = "-";
|
||||
let callDate = "-";
|
||||
let callTime = "-";
|
||||
let talkgroupNum = "-";
|
||||
let header = "Call Info"
|
||||
const currentCall = props.call?props.call:false;
|
||||
|
||||
|
||||
|
||||
const { data: talkgroupsData, isSuccess: isTalkgroupsSuccess } = useGetTalkgroupsQuery(currentCall?currentCall.shortName:skipToken);
|
||||
|
||||
if (currentCall) {
|
||||
if ((talkgroupsData) && talkgroupsData.talkgroups[currentCall.talkgroupNum]) {
|
||||
header = talkgroupsData.talkgroups[currentCall.talkgroupNum].description;
|
||||
}
|
||||
const time = new Date(currentCall.time);
|
||||
callTime = time.toLocaleTimeString();
|
||||
callDate = time.toLocaleDateString();
|
||||
if (currentCall.freq) {
|
||||
const 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;
|
||||
|
||||
}
|
||||
const { callLink, callDownload, callTweet } = useCallLink(props.call)
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header as='h1'>{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>
|
||||
<Divider />
|
||||
<Menu secondary fluid widths={2}>
|
||||
<Menu.Item name="download" href={callDownload}><Icon name="download" />Download</Menu.Item>
|
||||
<Menu.Item name="link" href={callLink}><Icon name="at" />Link</Menu.Item>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CallInfoPane;
|
||||
@@ -7,16 +7,21 @@ import {
|
||||
|
||||
|
||||
import { addStar, removeStar } from "../../features/calls/callsSlice";
|
||||
|
||||
import { useDispatch } from 'react-redux'
|
||||
|
||||
const CallItem = (props) => {
|
||||
|
||||
const call = props.call;
|
||||
const shortName = props.shortName;
|
||||
const talkgroups = props.talkgroups;
|
||||
const activeCall = props.activeCall;
|
||||
const [starVisible, setStarVisible] = useState(false);
|
||||
const [starClicked, setStarClicked] = useState(false);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const call = props.call;
|
||||
|
||||
const time = new Date(call.time);
|
||||
const activeCall = props.activeCall;
|
||||
|
||||
|
||||
const handleStarClicked = (e) => {
|
||||
e.preventDefault();
|
||||
@@ -34,6 +39,23 @@ const CallItem = (props) => {
|
||||
let rowSelected = {};
|
||||
let starButton;
|
||||
let starClickable = {};
|
||||
|
||||
const onDragStart = (event) => {
|
||||
// It receives a DragEvent
|
||||
// which inherits properties from
|
||||
// MouseEvent and Event
|
||||
// so we can access the element
|
||||
// through event.currentTarget
|
||||
|
||||
// Later, we'll save
|
||||
// in a hook variable
|
||||
// the item being dragged
|
||||
const callId = event.currentTarget.dataset.callid;
|
||||
event.dataTransfer.setData("call-id", callId);
|
||||
console.log("Dragging: " + callId);
|
||||
|
||||
}
|
||||
|
||||
if (!starClicked) {
|
||||
starClickable = { link: true };
|
||||
}
|
||||
@@ -62,20 +84,18 @@ const CallItem = (props) => {
|
||||
}
|
||||
}
|
||||
let talkgroup;
|
||||
if ((typeof props.talkgroups == 'undefined') || (typeof props.talkgroups[call.talkgroupNum] == 'undefined')) {
|
||||
if ((typeof talkgroups== 'undefined') || (typeof talkgroups.talkgroups[call.talkgroupNum] == 'undefined')) {
|
||||
talkgroup = call.talkgroupNum;
|
||||
} else {
|
||||
talkgroup = props.talkgroups[call.talkgroupNum].description;
|
||||
talkgroup = talkgroups.talkgroups[call.talkgroupNum].description;
|
||||
}
|
||||
return (
|
||||
<Table.Row onClick={(e) => props.onClick({ call: call }, e)} {...rowSelected}>
|
||||
<Table.Row draggable="true" onClick={(e) => props.onClick({ call: call }, e)} {...rowSelected} onDragStart={onDragStart} data-callid={call._id}>
|
||||
<Table.Cell> {call.len} </Table.Cell>
|
||||
<Table.Cell> {talkgroup} </Table.Cell>
|
||||
<Table.Cell> {time.toLocaleTimeString()} </Table.Cell>
|
||||
<Table.Cell onMouseEnter={() => setStarVisible(true)} onMouseLeave={() => setStarVisible(false)} onClick={handleStarClicked}>{starButton}</Table.Cell>
|
||||
</Table.Row>
|
||||
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import { useParams } from 'react-router-dom';
|
||||
import CallItem from "./CallItem";
|
||||
import {
|
||||
Icon,
|
||||
@@ -6,11 +7,13 @@ import {
|
||||
Ref
|
||||
} from "semantic-ui-react";
|
||||
import "../CallPlayer.css";
|
||||
import { useGetTalkgroupsQuery } from '../../features/api/apiSlice'
|
||||
|
||||
// ----------------------------------------------------
|
||||
const ListCalls = (props) => {
|
||||
const activeCallRef = useRef();
|
||||
|
||||
const { shortName } = useParams();
|
||||
const { data: talkgroupsData, isSuccess: isTalkgroupsSuccess } = useGetTalkgroupsQuery(shortName);
|
||||
//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
|
||||
|
||||
@@ -42,9 +45,9 @@ const ListCalls = (props) => {
|
||||
{
|
||||
props.callsData.ids.map((callId, index) => {
|
||||
if (callId === props.activeCallId) {
|
||||
return (<Ref innerRef={activeCallRef} key={index} ><CallItem activeCall={true} call={props.callsData.entities[callId]} talkgroups={props.talkgroups} key={index} onClick={props.playCall}/></Ref>)
|
||||
return (<Ref innerRef={activeCallRef} key={index} ><CallItem activeCall={true} call={props.callsData.entities[callId]} talkgroups={talkgroupsData} key={index} onClick={props.playCall}/></Ref>)
|
||||
} else {
|
||||
return <CallItem activeCall={false} call={props.callsData.entities[callId]} talkgroups={props.talkgroups} key={index} onClick={props.playCall}/>
|
||||
return <CallItem activeCall={false} call={props.callsData.entities[callId]} talkgroups={talkgroupsData} key={index} onClick={props.playCall}/>
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
185
frontend/src/Call/components/PlaylistBuilder.js
Normal file
185
frontend/src/Call/components/PlaylistBuilder.js
Normal file
@@ -0,0 +1,185 @@
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { useParams } from 'react-router-dom';
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Header,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
Segment,
|
||||
Statistic,
|
||||
Icon,
|
||||
Menu,
|
||||
Tab,
|
||||
Table,
|
||||
Modal,
|
||||
Button,
|
||||
Image,
|
||||
Form,
|
||||
Message
|
||||
} from "semantic-ui-react";
|
||||
import { useAddNewEventMutation, useGetTalkgroupsQuery } from '../../features/api/apiSlice'
|
||||
|
||||
function PlaylistBuilder(props) {
|
||||
const { shortName } = useParams();
|
||||
const { data: talkgroupsData, isSuccess: isTalkgroupsSuccess } = useGetTalkgroupsQuery(shortName);
|
||||
const [addNewEvent, { isLoading }] = useAddNewEventMutation()
|
||||
const { loading: callsLoading, data: callsData } = useSelector((state) => state.calls);
|
||||
const [playlist, setPlaylist] = useState([]);
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [title, setTitle] = useState("")
|
||||
const [description, setDescription] = useState("")
|
||||
const handleTitleChange = (e, { name, value }) => setTitle(value);
|
||||
const handleDescriptionChange = (e, { name, value }) => setDescription(value);
|
||||
|
||||
|
||||
const compareCalls = (a, b) => {
|
||||
const aTimestamp = new Date(a.time).getTime()
|
||||
const bTimestamp = new Date(b.time).getTime()
|
||||
if (aTimestamp > bTimestamp) {
|
||||
return -1
|
||||
} else {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
const addCall = (call) => {
|
||||
const found = playlist.find(c => c._id == call._id);
|
||||
if (!found) {
|
||||
playlist.push(call);
|
||||
setPlaylist([...playlist.sort(compareCalls)])
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const onDragOver = (event) => {
|
||||
// It also receives a DragEvent.
|
||||
// Later, we'll read the position
|
||||
// of the item from event.currentTarget
|
||||
// and store the updated list state
|
||||
|
||||
// We need to prevent the default behavior
|
||||
// of this event, in order for the onDrop
|
||||
// event to fire.
|
||||
// It may sound weird, but the default is
|
||||
// to cancel out the drop.
|
||||
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
|
||||
const onDrop = (event) => {
|
||||
// Here, we will:
|
||||
// - update the rendered list
|
||||
// - and reset the DnD state
|
||||
var callId = event.dataTransfer.getData("call-id");
|
||||
console.log("Dropping: " + callId);
|
||||
addCall(callsData.entities[callId])
|
||||
}
|
||||
let listItems = "";
|
||||
|
||||
listItems = playlist.map((call, index) => {
|
||||
let tgAlpha = call.talkgroupNum;
|
||||
if (talkgroupsData) {
|
||||
const talkgroup = talkgroupsData.talkgroups[call.talkgroupNum]
|
||||
tgAlpha = talkgroup.alpha?talkgroup.alpha:tgAlpha;
|
||||
}
|
||||
|
||||
const time = new Date(call.time);
|
||||
const callTime = time.toLocaleTimeString();
|
||||
const callDate = time.toLocaleDateString();
|
||||
|
||||
return ( <Table.Row key={index}>
|
||||
<Table.Cell>{call.len} </Table.Cell>
|
||||
<Table.Cell>{tgAlpha}</Table.Cell>
|
||||
<Table.Cell>{callTime}</Table.Cell>
|
||||
</Table.Row>
|
||||
)
|
||||
})
|
||||
|
||||
const submitPlaylist = () => {
|
||||
setOpen(true)
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
console.log(title)
|
||||
setOpen(false)
|
||||
const callIds = playlist.map( (call) => call._id)
|
||||
const event = {
|
||||
title,
|
||||
description,
|
||||
callIds
|
||||
}
|
||||
addNewEvent(event)
|
||||
setTitle("")
|
||||
setDescription("")
|
||||
setPlaylist([])
|
||||
}
|
||||
|
||||
let content = (<Header as="h3" icon textAlign='center' style={{paddingTop: "3em"}}><Icon name='target' />Drag Calls Here</Header>)
|
||||
if (playlist.length > 0) {
|
||||
content = (<Table basic="very">
|
||||
<Table.Body>
|
||||
{listItems}
|
||||
</Table.Body>
|
||||
</Table>)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
onClose={() => setOpen(false)}
|
||||
onOpen={() => setOpen(true)}
|
||||
open={open}
|
||||
>
|
||||
<Modal.Header>Submit an Event</Modal.Header>
|
||||
<Modal.Content>
|
||||
|
||||
<Modal.Description>
|
||||
<Message icon>
|
||||
<Icon name='warning sign' />
|
||||
<Message.Content>The Events feature is a work in progress! Things may break, change, or get deleted. Contact me with ideas or problems: luke@robotastic.com</Message.Content>
|
||||
</Message>
|
||||
<Header>Event</Header>
|
||||
<Form>
|
||||
|
||||
<Form.Input
|
||||
name="title"
|
||||
value={title}
|
||||
label="Title"
|
||||
onChange={handleTitleChange}
|
||||
/>
|
||||
<Form.TextArea
|
||||
name="description"
|
||||
value={description}
|
||||
label="Description"
|
||||
placeholder='Please describe the event'
|
||||
onChange={handleDescriptionChange}
|
||||
/>
|
||||
</Form>
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
<Modal.Actions>
|
||||
<Button color='black' onClick={() => setOpen(false)}>
|
||||
Nope
|
||||
</Button>
|
||||
<Button
|
||||
content="Submit"
|
||||
labelPosition='right'
|
||||
icon='checkmark'
|
||||
onClick={handleSubmit}
|
||||
positive
|
||||
/>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
<Tab.Pane attached='bottom' onDragOver={onDragOver} onDrop={onDrop}>
|
||||
{content}
|
||||
</Tab.Pane>
|
||||
<Menu fluid widths={2}>
|
||||
<Menu.Item name="clear" onClick={() => setPlaylist([])}><Icon name='delete' />Clear</Menu.Item>
|
||||
<Menu.Item active name="submit" onClick={submitPlaylist}><Icon name="cloud upload" />Submit</Menu.Item>
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlaylistBuilder;
|
||||
27
frontend/src/Common/NavBar.js
Normal file
27
frontend/src/Common/NavBar.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Card,
|
||||
Icon,
|
||||
Menu,
|
||||
Divider
|
||||
} from "semantic-ui-react";
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
|
||||
|
||||
const NavBar = (props) => {
|
||||
|
||||
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<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>
|
||||
<Link to="/events"><Menu.Item link>Events</Menu.Item></Link>
|
||||
<Link to="/about"><Menu.Item link>About</Menu.Item></Link>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default NavBar;
|
||||
24
frontend/src/Event/EventCallInfo.js
Normal file
24
frontend/src/Event/EventCallInfo.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import React, { useState } from "react";
|
||||
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Header,
|
||||
Divider,
|
||||
List,
|
||||
Segment,
|
||||
Statistic,
|
||||
Icon,
|
||||
Menu,
|
||||
Tab
|
||||
} from "semantic-ui-react";
|
||||
import CallInfoPane from "../Call/components/CallInfoPane"
|
||||
// ----------------------------------------------------
|
||||
function EventCallInfo(props) {
|
||||
return (
|
||||
<Segment>
|
||||
<CallInfoPane call={props.call} />
|
||||
</Segment>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default EventCallInfo;
|
||||
52
frontend/src/Event/EventCallItem.js
Normal file
52
frontend/src/Event/EventCallItem.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
Table,
|
||||
Icon,
|
||||
Label
|
||||
} from "semantic-ui-react";
|
||||
|
||||
import { useGetTalkgroupsQuery } from '../features/api/apiSlice'
|
||||
|
||||
|
||||
const EventCallItem = (props) => {
|
||||
const call = props.call;
|
||||
const activeCall = props.activeCall;
|
||||
const { data: talkgroupsData, isSuccess: isTalkgroupsSuccess } = useGetTalkgroupsQuery(call.shortName);
|
||||
|
||||
|
||||
const time = new Date(call.time);
|
||||
|
||||
|
||||
|
||||
let rowSelected = {};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if (activeCall) {
|
||||
rowSelected = {
|
||||
positive: true,
|
||||
color: "blue",
|
||||
key: "blue",
|
||||
inverted: "true"
|
||||
}
|
||||
}
|
||||
let talkgroup;
|
||||
if ((typeof talkgroupsData == 'undefined') || (typeof talkgroupsData.talkgroups[call.talkgroupNum] == 'undefined')) {
|
||||
talkgroup = call.talkgroupNum;
|
||||
} else {
|
||||
talkgroup = talkgroupsData.talkgroups[call.talkgroupNum].description;
|
||||
}
|
||||
return (
|
||||
<Table.Row onClick={(e) => 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> {call.shortName}</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
}
|
||||
|
||||
export default EventCallItem;
|
||||
133
frontend/src/Event/EventPlayer.js
Normal file
133
frontend/src/Event/EventPlayer.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import React, { useEffect, useLayoutEffect, useState, useRef } from "react";
|
||||
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom';
|
||||
import {skipToken} from '@reduxjs/toolkit/query'
|
||||
import MediaPlayer from "../Call/components/MediaPlayer";
|
||||
|
||||
import SupportModal from "../Call/components/SupportModal";
|
||||
|
||||
import EventCallInfo from "./EventCallInfo";
|
||||
import ListEventCalls from "./ListEventCalls";
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
|
||||
import {
|
||||
Container,
|
||||
Label,
|
||||
Rail,
|
||||
Sticky,
|
||||
Menu,
|
||||
Icon,
|
||||
Sidebar
|
||||
} from "semantic-ui-react";
|
||||
|
||||
|
||||
import { useCallLink } from "../Call/components/CallLinks";
|
||||
import "../Call/CallPlayer.css";
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------------------
|
||||
function EventPlayer(props) {
|
||||
|
||||
|
||||
const eventData = props.eventData;
|
||||
const selectCallId = props.selectCallId;
|
||||
|
||||
|
||||
const [autoPlay, setAutoPlay] = useState(true);
|
||||
const [currentCall, setCurrentCall] = useState(false);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
|
||||
|
||||
const {callLink,callDownload,callTweet} = useCallLink(currentCall)
|
||||
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const positionRef = useRef(); // lets us get the Y Scroll offset for the Call List
|
||||
const shouldPlayAddCallRef = useRef(); // we need to do this to make the current value of isPlaying available in the socket message callback
|
||||
shouldPlayAddCallRef.current = (!isPlaying && autoPlay)?true:false;
|
||||
|
||||
let currentCallId = false;
|
||||
|
||||
if (currentCall) {
|
||||
currentCallId = currentCall._id;
|
||||
}
|
||||
|
||||
const handlePlayPause = (playing) => {
|
||||
setIsPlaying(playing);
|
||||
}
|
||||
|
||||
|
||||
const handleAutoPlay = (currentAutoPlay) => {
|
||||
setAutoPlay(!currentAutoPlay);
|
||||
}
|
||||
|
||||
const playCall = (data) => {
|
||||
setCurrentCall(data.call);
|
||||
setIsPlaying(true);
|
||||
}
|
||||
|
||||
|
||||
const callEnded = () => {
|
||||
if (eventData) {
|
||||
const currentIndex = eventData.calls.findIndex(call => call._id === currentCallId);
|
||||
if (autoPlay && (currentIndex > 0)) {
|
||||
|
||||
const nextCall = eventData.calls[currentIndex - 1];
|
||||
|
||||
setCurrentCall(nextCall);
|
||||
setIsPlaying(true);
|
||||
} else {
|
||||
setIsPlaying(false);
|
||||
}
|
||||
} else {
|
||||
console.log("Somehow called, callEnded() but callsData was false");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if ( selectCallId && eventData && !currentCall) {
|
||||
const call = eventData.calls.find( (call) => call._id == selectCallId);
|
||||
if (call) {
|
||||
setCurrentCall(call);
|
||||
}
|
||||
}
|
||||
}, [selectCallId, eventData])
|
||||
|
||||
|
||||
return (
|
||||
<div ref={positionRef}>
|
||||
<Container className="main" >
|
||||
<Sidebar.Pushable>
|
||||
<Sidebar.Pusher
|
||||
style={{ minHeight: '100vh' }}
|
||||
>
|
||||
|
||||
<ListEventCalls eventData={eventData} activeCallId={isPlaying?currentCallId:false} playCall={playCall} />
|
||||
|
||||
</Sidebar.Pusher>
|
||||
</Sidebar.Pushable>
|
||||
<Rail position='right' className="desktop-only" >
|
||||
<Sticky offset={60} context={positionRef}>
|
||||
<EventCallInfo call={currentCall} />
|
||||
</Sticky>
|
||||
</Rail>
|
||||
</Container>
|
||||
|
||||
<Menu fixed="bottom" compact inverted >
|
||||
<Menu.Item active={autoPlay} onClick={() => handleAutoPlay(autoPlay)}><Icon name="level up" /><span className="desktop-only">Autoplay</span></Menu.Item>
|
||||
<MediaPlayer call={currentCall} onEnded={callEnded} onPlayPause={handlePlayPause} />
|
||||
<Menu.Menu position="right" className="desktop-only">
|
||||
<Menu.Item><SupportModal /></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 EventPlayer;
|
||||
59
frontend/src/Event/ListEventCalls.js
Normal file
59
frontend/src/Event/ListEventCalls.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
import EventCallItem from "./EventCallItem";
|
||||
import {
|
||||
Icon,
|
||||
Table,
|
||||
Ref
|
||||
} from "semantic-ui-react";
|
||||
import "../Call/CallPlayer.css";
|
||||
|
||||
// ----------------------------------------------------
|
||||
const ListEventCalls = (props) => {
|
||||
const activeCallRef = useRef();
|
||||
|
||||
//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
|
||||
|
||||
useEffect(() => {
|
||||
if (activeCallRef.current) {
|
||||
activeCallRef.current.scrollIntoView({
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
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>System</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
{props.eventData &&
|
||||
<Table.Body>
|
||||
|
||||
{ props.eventData.calls.map((call, index) => {
|
||||
if (call._id === props.activeCallId) {
|
||||
return (<Ref innerRef={activeCallRef} key={index} ><EventCallItem activeCall={true} call={call} key={index} onClick={props.playCall} showStar={false}/></Ref>)
|
||||
} else {
|
||||
return <EventCallItem activeCall={false} call={call} key={index} onClick={props.playCall} showStar={false}/>
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
</Table.Body>
|
||||
}
|
||||
</Table>
|
||||
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export default ListEventCalls;
|
||||
63
frontend/src/Event/ListEvents.js
Normal file
63
frontend/src/Event/ListEvents.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import React, { Component } from "react";
|
||||
import { Link, useNavigate } from 'react-router-dom'
|
||||
|
||||
import {
|
||||
Container,
|
||||
Header,
|
||||
Card,
|
||||
Icon,
|
||||
Menu,
|
||||
Divider,
|
||||
Table
|
||||
} from "semantic-ui-react";
|
||||
import NavBar from "../Common/NavBar"
|
||||
import { useGetEventsQuery } from "../features/api/apiSlice";
|
||||
|
||||
// ----------------------------------------------------
|
||||
|
||||
const ListEvents = (props) => {
|
||||
|
||||
const dateRange = (start, end) => {
|
||||
if (start.getYear() != end.getYear()) {
|
||||
return start.toLocaleDateString('en-us', { weekday:"short", year:"numeric", month:"short", day:"numeric"}) + " " + start.toLocaleTimeString() + " - " + end.toLocaleDateString('en-us', { weekday:"short", year:"numeric", month:"short", day:"numeric"}) + " " + end.toLocaleTimeString();
|
||||
} else if (start.getMonth() != end.getMonth()) {
|
||||
return start.toLocaleDateString('en-us', { weekday:"short", month:"short", day:"numeric"}) + " " + start.toLocaleTimeString() + " - " + end.toLocaleDateString('en-us', { weekday:"short", month:"short", day:"numeric"}) + " " + end.toLocaleTimeString();
|
||||
|
||||
} else if (start.getDay() != end.getDay()) {
|
||||
return start.toLocaleDateString('en-us', { weekday:"short", month:"short", day:"numeric"}) + " " + start.toLocaleTimeString() + " - " + end.toLocaleDateString('en-us', { weekday:"short", day:"numeric"}) + " " + end.toLocaleTimeString();
|
||||
|
||||
} else {
|
||||
return start.toLocaleDateString('en-us', { weekday:"short", month:"short", day:"numeric"}) + " " + start.toLocaleTimeString() + " - " + end.toLocaleTimeString();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
const {data:events, isSuccess} = useGetEventsQuery();
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div>
|
||||
<NavBar/>
|
||||
<Container >
|
||||
<Divider horizontal style={{paddingTop:"5em", paddingBottom:"2em"}}><Header as="h1">Events<Icon name='rss' /></Header></Divider>
|
||||
<Table basic="very">
|
||||
<Table.Body>
|
||||
|
||||
{isSuccess&&events.map((event, index) =>
|
||||
<Table.Row key={index}>
|
||||
<Table.Cell><Link to={"/events/"+event._id}><Header as='h3'>{event.title}</Header></Link></Table.Cell>
|
||||
<Table.Cell>{event.description}</Table.Cell>
|
||||
<Table.Cell>{event.numCalls} Calls</Table.Cell>
|
||||
<Table.Cell>{dateRange(new Date(event.startTime),new Date(event.endTime))}</Table.Cell>
|
||||
</Table.Row>
|
||||
)}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default ListEvents;
|
||||
159
frontend/src/Event/ViewEvent.js
Normal file
159
frontend/src/Event/ViewEvent.js
Normal file
@@ -0,0 +1,159 @@
|
||||
import React, { useEffect, useLayoutEffect, useState, useRef } from "react";
|
||||
import { Link, useLocation, useParams, useNavigate } from 'react-router-dom';
|
||||
import EventPlayer from "./EventPlayer";
|
||||
import { useSelector, useDispatch } from 'react-redux'
|
||||
import { useGetEventQuery } from '../features/api/apiSlice'
|
||||
import queryString from '../query-string';
|
||||
import {
|
||||
Container,
|
||||
Label,
|
||||
Menu,
|
||||
Icon,
|
||||
Sidebar,
|
||||
Grid,
|
||||
Statistic,
|
||||
Header
|
||||
|
||||
} from "semantic-ui-react";
|
||||
import "../Call/CallPlayer.css";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// ----------------------------------------------------
|
||||
function ViewEvent(props) {
|
||||
|
||||
const { id } = useParams();
|
||||
|
||||
const { data: eventData, isSuccess: isEventSuccess } = useGetEventQuery(id);
|
||||
const [autoPlay, setAutoPlay] = useState(true);
|
||||
const [currentCall, setCurrentCall] = useState(false);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [sidebarOpened, setSidebarOpened] = useState(false);
|
||||
const [selectCallId, setSelectCallId] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
||||
|
||||
const uri = queryString.parse(useLocation().search);
|
||||
const pathname = useLocation().pathname;
|
||||
|
||||
let currentCallId = false;
|
||||
|
||||
if (currentCall) {
|
||||
currentCallId = currentCall._id;
|
||||
}
|
||||
|
||||
|
||||
const handlePusherClick = () => {
|
||||
if (sidebarOpened) setSidebarOpened(false);
|
||||
}
|
||||
|
||||
const handleSidebarToggle = () => setSidebarOpened(!sidebarOpened);
|
||||
|
||||
|
||||
const setStateFromUri = async () => {
|
||||
|
||||
// is this just for one call?
|
||||
if (uri.hasOwnProperty('call-id')) {
|
||||
const _id = uri['call-id'];
|
||||
setSelectCallId(_id);
|
||||
}
|
||||
}
|
||||
|
||||
const dateRange = (start, end) => {
|
||||
if (start.getYear() != end.getYear()) {
|
||||
return {"part1": start.toLocaleDateString('en-us', { weekday:"short", year:"numeric", month:"short", day:"numeric"}) + " " + start.toLocaleTimeString([], {hour: 'numeric', minute:'2-digit'}), "part2": end.toLocaleDateString('en-us', { weekday:"short", year:"numeric", month:"short", day:"numeric"}) + " " + end.toLocaleTimeString([], {hour: 'numeric', minute:'2-digit'})};
|
||||
} else if (start.getMonth() != end.getMonth()) {
|
||||
return {"part1": start.toLocaleDateString('en-us', { weekday:"short", month:"short", day:"numeric"}) + " " + start.toLocaleTimeString([], {hour: 'numeric', minute:'2-digit'}), "part2": end.toLocaleDateString('en-us', { weekday:"short", month:"short", day:"numeric"}) + " " + end.toLocaleTimeString([], {hour: 'numeric', minute:'2-digit'})};
|
||||
|
||||
} else if (start.getDay() != end.getDay()) {
|
||||
return {"part1": start.toLocaleDateString('en-us', { weekday:"short", month:"short", day:"numeric"}) + " " + start.toLocaleTimeString([], {hour: 'numeric', minute:'2-digit'}), "part2": end.toLocaleDateString('en-us', { weekday:"short", day:"numeric"}) + " " + end.toLocaleTimeString([], {hour: 'numeric', minute:'2-digit'})};
|
||||
|
||||
} else {
|
||||
return {"part1": start.toLocaleDateString('en-us', { weekday:"short", month:"short", day:"numeric"}) + " " + start.toLocaleTimeString([], {hour: 'numeric', minute:'2-digit'}), "part2": end.toLocaleTimeString([], {hour: 'numeric', minute:'2-digit'})};
|
||||
|
||||
}
|
||||
}
|
||||
let dates = {part1: "", part2: ""}
|
||||
let expirationDate = ""
|
||||
if (eventData) {
|
||||
dates = dateRange(new Date(eventData.startTime),new Date(eventData.endTime))
|
||||
expirationDate = new Date(eventData.expireTime).toLocaleDateString('en-us', { weekday:"short", month:"short", day:"numeric"})
|
||||
}
|
||||
/*
|
||||
const dateRange = (start, end) => {
|
||||
if (start.getYear() != end.getYear()) {
|
||||
return start.toLocaleDateString('en-us', { weekday:"short", year:"numeric", month:"short", day:"numeric"}) + " " + start.toLocaleTimeString() + " - " + end.toLocaleDateString('en-us', { weekday:"short", year:"numeric", month:"short", day:"numeric"}) + " " + end.toLocaleTimeString();
|
||||
} else if (start.getMonth() != end.getMonth()) {
|
||||
return start.toLocaleDateString('en-us', { weekday:"short", month:"short", day:"numeric"}) + " " + start.toLocaleTimeString() + " - " + end.toLocaleDateString('en-us', { weekday:"short", month:"short", day:"numeric"}) + " " + end.toLocaleTimeString();
|
||||
|
||||
} else if (start.getDay() != end.getDay()) {
|
||||
return start.toLocaleDateString('en-us', { weekday:"short", month:"short", day:"numeric"}) + " " + start.toLocaleTimeString() + " - " + end.toLocaleDateString('en-us', { weekday:"short", day:"numeric"}) + " " + end.toLocaleTimeString();
|
||||
|
||||
} else {
|
||||
return start.toLocaleDateString('en-us', { weekday:"short", month:"short", day:"numeric"}) + " " + start.toLocaleTimeString() + " - " + end.toLocaleTimeString();
|
||||
|
||||
}
|
||||
}
|
||||
let dates = "";
|
||||
if (eventData) {
|
||||
dates = dateRange(new Date(eventData.startTime),new Date(eventData.endTime));
|
||||
}*/
|
||||
return (
|
||||
<div >
|
||||
<Sidebar as={Menu} animation='overlay' inverted vertical visible={sidebarOpened}
|
||||
onClick={handlePusherClick} duration={50} width='thin'>
|
||||
<Menu.Item onClick={handlePusherClick} >
|
||||
<span> </span><Icon name="close" inverted={true} link={true} size='small' /></Menu.Item>
|
||||
<Link to="/"><Menu.Item link>Home</Menu.Item></Link>
|
||||
<Link to="/systems"><Menu.Item link>Systems</Menu.Item></Link>
|
||||
<Link to="/events"><Menu.Item link>Events</Menu.Item></Link>
|
||||
<Link to="/about"><Menu.Item link>About</Menu.Item></Link>
|
||||
</Sidebar>
|
||||
<Menu fixed="top">
|
||||
<Menu.Item onClick={handleSidebarToggle}>
|
||||
<Icon name='sidebar' />
|
||||
</Menu.Item>
|
||||
<Container className="desktop-only" fluid textAlign='left' style={{ fontSize: '1.5rem', paddingLeft: '0.5em', paddingTop: '.5em' }}>
|
||||
{eventData && eventData.title}
|
||||
</Container>
|
||||
</Menu>
|
||||
<Grid style={{ paddingTop: '4em' }}>
|
||||
<Grid.Column mobile={1} tablet={1} computer={1} ></Grid.Column>
|
||||
<Grid.Column mobile={16} tablet={8} computer={4}>
|
||||
<Container text >
|
||||
{eventData&&
|
||||
<>
|
||||
<p>{eventData.description}</p>
|
||||
<Header size='tiny'>Expires {expirationDate}</Header>
|
||||
</>
|
||||
}
|
||||
</Container>
|
||||
</Grid.Column>
|
||||
<Grid.Column mobile={8} tablet={8} computer={4}>
|
||||
<Header as="h2">{dates.part1} <br/>→ {dates.part2}</Header>
|
||||
</Grid.Column>
|
||||
<Grid.Column mobile={8} tablet={8} computer={2}>
|
||||
|
||||
<Statistic>
|
||||
<Statistic.Value>{eventData && eventData.numCalls}</Statistic.Value>
|
||||
<Statistic.Label>Calls</Statistic.Label>
|
||||
</Statistic>
|
||||
</Grid.Column>
|
||||
|
||||
<Grid.Column mobile={1} tablet={1} computer={5}>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
{eventData && <EventPlayer eventData={eventData} selectCallId={selectCallId} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default ViewEvent;
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Menu,
|
||||
Divider
|
||||
} from "semantic-ui-react";
|
||||
|
||||
import NavBar from "../Common/NavBar"
|
||||
import { selectAllSystems, selectActiveSystems, useGetSystemsQuery } from "../features/api/apiSlice";
|
||||
|
||||
// ----------------------------------------------------
|
||||
@@ -21,10 +21,7 @@ const ListSystems = (props) => {
|
||||
const navigate = useNavigate();
|
||||
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>
|
||||
<NavBar/>
|
||||
<Container >
|
||||
<Divider horizontal style={{paddingTop:"5em", paddingBottom:"2em"}}><Header as="h1">Radio Systems<Icon name='rss' /></Header></Divider>
|
||||
<Card.Group itemsPerRow={4} stackable={true}>
|
||||
|
||||
@@ -21,10 +21,25 @@ export const apiSlice = createApi({
|
||||
// The URL for the request is '/fakeApi/posts'
|
||||
query: () => ({ url: '/systems' })
|
||||
}),
|
||||
getEvents: builder.query({
|
||||
// The URL for the request is '/fakeApi/posts'
|
||||
query: () => ({ url: '/events' })
|
||||
}),
|
||||
getEvent: builder.query({
|
||||
// The URL for the request is '/fakeApi/posts'
|
||||
query: (eventId) => ({ url: `/events/${eventId}` })
|
||||
}),
|
||||
getTalkgroups: builder.query({
|
||||
// The URL for the request is '/fakeApi/posts'
|
||||
query: (shortName) => ({ url: `/${shortName}/talkgroups` })
|
||||
}),
|
||||
addNewEvent: builder.mutation({
|
||||
query: initialEvent => ({
|
||||
url: '/events',
|
||||
method: 'POST',
|
||||
body: initialEvent
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
@@ -45,4 +60,4 @@ export const selectActiveSystems = createSelector(
|
||||
)
|
||||
|
||||
// Export the auto-generated hook for the `getPosts` query endpoint
|
||||
export const { useGetGroupsQuery, useGetSystemsQuery, useGetTalkgroupsQuery } = apiSlice
|
||||
export const { useGetGroupsQuery, useGetSystemsQuery, useGetTalkgroupsQuery, useGetEventsQuery, useGetEventQuery, useAddNewEventMutation } = apiSlice
|
||||
|
||||
@@ -129,8 +129,7 @@ const initialState = {
|
||||
export const addStar = createAsyncThunk(
|
||||
'calls/addStar',
|
||||
async(callId,{getState, requestIdleCallback}) => {
|
||||
const {shortName} = getState().callPlayer;
|
||||
const url = process.env.REACT_APP_BACKEND_SERVER + "/" + shortName.toLowerCase() + "/star/" + callId
|
||||
const url = process.env.REACT_APP_BACKEND_SERVER + "/add_star/" + callId
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -148,8 +147,7 @@ export const addStar = createAsyncThunk(
|
||||
export const removeStar = createAsyncThunk(
|
||||
'calls/removeStar',
|
||||
async(callId,{getState, requestIdleCallback}) => {
|
||||
const {shortName} = getState().callPlayer;
|
||||
const url = process.env.REACT_APP_BACKEND_SERVER + "/" + shortName.toLowerCase() + "/removeStar/" + callId
|
||||
const url = process.env.REACT_APP_BACKEND_SERVER + "/remove_star/" + callId
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
||||
@@ -21,6 +21,10 @@ import Calls from "./Call/Calls"
|
||||
|
||||
import AboutComponent from "./About/AboutComponent"
|
||||
|
||||
// Event
|
||||
|
||||
import ListEvents from "./Event/ListEvents"
|
||||
import ViewEvent from "./Event/ViewEvent"
|
||||
|
||||
const store = configureStore(/* provide initial state if any */)
|
||||
|
||||
@@ -33,6 +37,8 @@ root.render(
|
||||
<Route exact path="/" element={<Main/>} />
|
||||
<Route exact path="/systems" element={<ListSystems/>} />
|
||||
<Route exact path="/system/:shortName" element={<Calls/>} />
|
||||
<Route exact path="/events" element={<ListEvents/>} />
|
||||
<Route exact path="/events/:id" element={<ViewEvent/>} />
|
||||
<Route exact path="/about" element={<AboutComponent/>} />
|
||||
|
||||
</Routes>
|
||||
|
||||
6
node_modules/.package-lock.json
generated
vendored
Normal file
6
node_modules/.package-lock.json
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "trunk-server",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
6
package-lock.json
generated
Normal file
6
package-lock.json
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "trunk-server",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
Reference in New Issue
Block a user