From b23734c4a39efb686c4b75062f06252590adbfa0 Mon Sep 17 00:00:00 2001 From: Luke Berndt Date: Sun, 22 Jan 2023 21:56:53 -0500 Subject: [PATCH] Events is functional!!! Just need to clean some things up and test --- backend/controllers/events.js | 39 ++++-- backend/index.js | 3 +- backend/models/event.js | 18 ++- frontend/src/Call/Calls.js | 1 + frontend/src/Call/components/CallItem.js | 13 +- frontend/src/Call/components/ListCalls.js | 4 +- frontend/src/Common/NavBar.js | 27 ++++ frontend/src/Event/EventPlayer.js | 133 +++++++++++++++++++ frontend/src/Event/ListEventCalls.js | 59 +++++++++ frontend/src/Event/ListEvents.js | 63 +++++++++ frontend/src/Event/ViewEvent.js | 152 ++++++++++++++++++++++ frontend/src/System/ListSystems.js | 7 +- frontend/src/features/api/apiSlice.js | 10 +- frontend/src/index.js | 6 + 14 files changed, 506 insertions(+), 29 deletions(-) create mode 100644 frontend/src/Common/NavBar.js create mode 100644 frontend/src/Event/EventPlayer.js create mode 100644 frontend/src/Event/ListEventCalls.js create mode 100644 frontend/src/Event/ListEvents.js create mode 100644 frontend/src/Event/ViewEvent.js diff --git a/backend/controllers/events.js b/backend/controllers/events.js index bc4f74d..c84421e 100644 --- a/backend/controllers/events.js +++ b/backend/controllers/events.js @@ -4,12 +4,34 @@ var mongoose = require("mongoose"); var Event = require("../models/event"); var { callModel: Call, callSchema } = require("../models/call"); -/* -var oids = []; -ids.forEach(function(item){ -oids.push(new ObjectId(item)); -}); -*/ +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() @@ -45,8 +67,8 @@ exports.addNewEvent = async function (req, res, next) { res.send("Not all of the Calls were found"); } calls = calls.sort(compareCalls); - event.startTime = new Date(calls[0].time).getTime(); - event.endTime = new Date(calls[calls.length - 1].time).getTime(); + 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) { @@ -54,6 +76,7 @@ exports.addNewEvent = async function (req, res, next) { } }); event.calls = calls; + event.numCalls = calls.length; console.log(await event.save()); res.contentType('json'); res.send(JSON.stringify({ diff --git a/backend/index.js b/backend/index.js index ac3ae85..26f2b42 100644 --- a/backend/index.js +++ b/backend/index.js @@ -212,7 +212,8 @@ 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); diff --git a/backend/models/event.js b/backend/models/event.js index 232ec2b..516a5e2 100644 --- a/backend/models/event.js +++ b/backend/models/event.js @@ -2,18 +2,22 @@ var mongoose = require('mongoose'); var {callSchema} = require('./call'); console.log(callSchema) -var groupSchema = mongoose.Schema({ +var eventSchema = mongoose.Schema({ title: String, description: String, - expireTime: Number, - startTime: Number, - endTime: Number, + expireTime: Date, + startTime: Date, + endTime: Date, shortNames: [String], - calls: [callSchema] + calls: [callSchema], + numCalls: Number }, { timestamps: true }); - -module.exports = mongoose.model('Event', groupSchema); +// add virtual if You want +eventSchema.virtual('callCount').get(function () { + return this.calls.length; +}); +module.exports = mongoose.model('Event', eventSchema); diff --git a/frontend/src/Call/Calls.js b/frontend/src/Call/Calls.js index 9ad7793..8155a20 100644 --- a/frontend/src/Call/Calls.js +++ b/frontend/src/Call/Calls.js @@ -366,6 +366,7 @@ function Calls(props) { Home Systems + Events About diff --git a/frontend/src/Call/components/CallItem.js b/frontend/src/Call/components/CallItem.js index 101a05f..4e7e269 100644 --- a/frontend/src/Call/components/CallItem.js +++ b/frontend/src/Call/components/CallItem.js @@ -7,16 +7,19 @@ import { import { addStar, removeStar } from "../../features/calls/callsSlice"; +import { useGetTalkgroupsQuery } from '../../features/api/apiSlice' import { useDispatch } from 'react-redux' const CallItem = (props) => { - + const call = props.call; + const activeCall = props.activeCall; const [starVisible, setStarVisible] = useState(false); const [starClicked, setStarClicked] = useState(false); + const { data: talkgroupsData, isSuccess: isTalkgroupsSuccess } = useGetTalkgroupsQuery(call.shortName); const dispatch = useDispatch(); - const call = props.call; + const time = new Date(call.time); - const activeCall = props.activeCall; + const handleStarClicked = (e) => { e.preventDefault(); @@ -79,10 +82,10 @@ const CallItem = (props) => { } } let talkgroup; - if ((typeof props.talkgroups == 'undefined') || (typeof props.talkgroups[call.talkgroupNum] == 'undefined')) { + if ((typeof talkgroupsData == 'undefined') || (typeof talkgroupsData.talkgroups[call.talkgroupNum] == 'undefined')) { talkgroup = call.talkgroupNum; } else { - talkgroup = props.talkgroups[call.talkgroupNum].description; + talkgroup = talkgroupsData.talkgroups[call.talkgroupNum].description; } return ( props.onClick({ call: call }, e)} {...rowSelected} onDragStart={onDragStart} data-callid={call._id}> diff --git a/frontend/src/Call/components/ListCalls.js b/frontend/src/Call/components/ListCalls.js index 3a6059d..09d041a 100644 --- a/frontend/src/Call/components/ListCalls.js +++ b/frontend/src/Call/components/ListCalls.js @@ -40,9 +40,9 @@ const ListCalls = (props) => { { props.callsData.ids.map((callId, index) => { if (callId === props.activeCallId) { - return () + return () } else { - return + return } }) diff --git a/frontend/src/Common/NavBar.js b/frontend/src/Common/NavBar.js new file mode 100644 index 0000000..511f7f9 --- /dev/null +++ b/frontend/src/Common/NavBar.js @@ -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 ( + + Home + Listen + Events + About + + ); + } + + + export default NavBar; \ No newline at end of file diff --git a/frontend/src/Event/EventPlayer.js b/frontend/src/Event/EventPlayer.js new file mode 100644 index 0000000..bd2c20a --- /dev/null +++ b/frontend/src/Event/EventPlayer.js @@ -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 CallInfo from "../Call/components/CallInfo"; +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 ( +
+ + + + + + + + + + + + + + + + + handleAutoPlay(autoPlay)}>Autoplay + + + + Download + Link + + + +
+ ); +} + + +export default EventPlayer; diff --git a/frontend/src/Event/ListEventCalls.js b/frontend/src/Event/ListEventCalls.js new file mode 100644 index 0000000..a7b88d1 --- /dev/null +++ b/frontend/src/Event/ListEventCalls.js @@ -0,0 +1,59 @@ +import React, { useEffect, useRef } from "react"; +import CallItem from "../Call/components/CallItem"; +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 ( + + + + + Len + Talkgroup + Time + + + + {props.eventData && + + + { props.eventData.calls.map((call, index) => { + if (call._id === props.activeCallId) { + return () + } else { + return + } + }) + + } + + + } +
+ + + ); + +} + +export default ListEventCalls; diff --git a/frontend/src/Event/ListEvents.js b/frontend/src/Event/ListEvents.js new file mode 100644 index 0000000..b6c61a7 --- /dev/null +++ b/frontend/src/Event/ListEvents.js @@ -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 ( +
+ + +
Events
+ + + + {isSuccess&&events.map((event, index) => + +
{event.title}
+ {event.description} + {event.numCalls} Calls + {dateRange(new Date(event.startTime),new Date(event.endTime))} +
+ )} +
+
+
+
+ ); + } + + +export default ListEvents; diff --git a/frontend/src/Event/ViewEvent.js b/frontend/src/Event/ViewEvent.js new file mode 100644 index 0000000..3227744 --- /dev/null +++ b/frontend/src/Event/ViewEvent.js @@ -0,0 +1,152 @@ +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(), "part2": end.toLocaleDateString('en-us', { weekday:"short", year:"numeric", month:"short", day:"numeric"}) + " " + end.toLocaleTimeString()}; + } else if (start.getMonth() != end.getMonth()) { + return {"part1": start.toLocaleDateString('en-us', { weekday:"short", month:"short", day:"numeric"}) + " " + start.toLocaleTimeString(), "part2": end.toLocaleDateString('en-us', { weekday:"short", month:"short", day:"numeric"}) + " " + end.toLocaleTimeString()}; + + } else if (start.getDay() != end.getDay()) { + return {"part1": start.toLocaleDateString('en-us', { weekday:"short", month:"short", day:"numeric"}) + " " + start.toLocaleTimeString(), "part2": end.toLocaleDateString('en-us', { weekday:"short", day:"numeric"}) + " " + end.toLocaleTimeString()}; + + } else { + return {"part1": start.toLocaleDateString('en-us', { weekday:"short", month:"short", day:"numeric"}) + " " + start.toLocaleTimeString(), "part2": end.toLocaleTimeString()}; + + } +} + let dates = {part1: "", part2: ""} + if (eventData) { + dates = dateRange(new Date(eventData.startTime),new Date(eventData.endTime)) + } +/* + 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 ( +
+ + + + Home + Systems + Events + About + + + + + + + {eventData && eventData.title} + + + + + + + {eventData && eventData.description} + + + +
{dates.part1} →
{dates.part2}
+
+ + + + {eventData.numCalls} + Calls + + + + + +
+ +
+ ); +} + + +export default ViewEvent; diff --git a/frontend/src/System/ListSystems.js b/frontend/src/System/ListSystems.js index 79772e6..de92381 100644 --- a/frontend/src/System/ListSystems.js +++ b/frontend/src/System/ListSystems.js @@ -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 (
- - Home - About - +
Radio Systems
diff --git a/frontend/src/features/api/apiSlice.js b/frontend/src/features/api/apiSlice.js index 29e98e4..c76b23c 100644 --- a/frontend/src/features/api/apiSlice.js +++ b/frontend/src/features/api/apiSlice.js @@ -21,6 +21,14 @@ 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` }) @@ -52,4 +60,4 @@ export const selectActiveSystems = createSelector( ) // Export the auto-generated hook for the `getPosts` query endpoint -export const { useGetGroupsQuery, useGetSystemsQuery, useGetTalkgroupsQuery, useAddNewEventMutation } = apiSlice +export const { useGetGroupsQuery, useGetSystemsQuery, useGetTalkgroupsQuery, useGetEventsQuery, useGetEventQuery, useAddNewEventMutation } = apiSlice diff --git a/frontend/src/index.js b/frontend/src/index.js index a2cd078..9790773 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -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( } /> } /> } /> + } /> + } /> } />