Have main and listSystems working

This commit is contained in:
Luke Berndt
2021-01-01 12:04:37 -05:00
parent ac6b9903a6
commit 79fe8113e0
24 changed files with 881 additions and 246 deletions

View File

@@ -1,39 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-float infinite 3s ease-in-out;
}
}
.App-header {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
}
.App-link {
color: rgb(112, 76, 182);
}
@keyframes App-logo-float {
0% {
transform: translateY(0);
}
50% {
transform: translateY(10px)
}
100% {
transform: translateY(0px)
}
}

View File

@@ -1,58 +0,0 @@
import React from 'react';
import logo from './logo.svg';
import { Counter } from './features/counter/Counter';
import './App.css';
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<Counter />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<span>
<span>Learn </span>
<a
className="App-link"
href="https://reactjs.org/"
target="_blank"
rel="noopener noreferrer"
>
React
</a>
<span>, </span>
<a
className="App-link"
href="https://redux.js.org/"
target="_blank"
rel="noopener noreferrer"
>
Redux
</a>
<span>, </span>
<a
className="App-link"
href="https://redux-toolkit.js.org/"
target="_blank"
rel="noopener noreferrer"
>
Redux Toolkit
</a>
,<span> and </span>
<a
className="App-link"
href="https://react-redux.js.org/"
target="_blank"
rel="noopener noreferrer"
>
React Redux
</a>
</span>
</header>
</div>
);
}
export default App;

View File

@@ -1,15 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react';
import { Provider } from 'react-redux';
import store from './app/store';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(
<Provider store={store}>
<App />
</Provider>
);
expect(getByText(/learn/i)).toBeInTheDocument();
});

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 {
}

356
frontend/src/Main/Main.js Normal file
View File

@@ -0,0 +1,356 @@
import React, { Component } from "react";
import { Link } from 'react-router-dom'
import SystemCard from "../System/SystemCard";
import "./Main.css";
import { createMedia } from "@artsy/fresnel";
import {
Button,
Container,
Divider,
Grid,
Header,
Icon,
Menu,
Segment,
Sidebar,
Transition,
} from 'semantic-ui-react'
/* Responsive component was removed from Semantic UI. This is discussed here: https://github.com/Semantic-Org/Semantic-UI-React/pull/4008 */
const AppMedia = createMedia({
breakpoints: {
mobile: 320,
tablet: 768,
computer: 992,
largeScreen: 1200,
widescreen: 1920
}
});
const mediaStyles = AppMedia.createMediaStyle();
const { Media } = AppMedia;
/* eslint-disable react/no-multi-comp */
/* Heads up! HomepageHeading uses inline styling, however it's not the best practice. Use CSS or styled components for
* such things.
*/
const HomepageHeading = ({ mobile }) => (
<Container text style={{paddingBottom: '0px'}} >
<Header
as='h1'
content='Share the Air'
style={{
fontSize: mobile ? '2em' : '4em',
color: "#FFF",
fontWeight: 'normal',
marginBottom: 0,
marginTop: mobile ? '1.5em' : '0em',
}}
/>
<Header
as='h2'
content='Listen to Police and Fire radio from across the US'
style={{
fontSize: mobile ? '1.5em' : '1.7em',
color: "#FFF",
fontWeight: 'normal',
marginTop: mobile ? '0.5em' : '1.5em',
marginBottom: mobile ? '0.5em' : '0em',
}}
/>
</Container>
)
/*
HomepageHeading.propTypes = {
mobile: PropTypes.bool,
}
*/
/* Heads up!
* Neither Semantic UI nor Semantic UI React offer a responsive navbar, however, it can be implemented easily.
* It can be more complicated, but you can create really flexible markup.
*/
/*
General flow:
- componentDidMount(): it will call fetchSystem() this will perform an HTTP request if Systems does not exist, and then add System to Props
*/
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 (
<Media greaterThanOrEqual="tablet">
<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>{process.env.REACT_APP_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}
</Media>
)
}
}
/*
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 (
<Media lessThan="tablet">
<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>{process.env.REACT_APP_SITE_NAME}</Menu.Item>
</Menu>
</Container>
<HomepageHeading mobile />
</Segment>
</div>
{children}
</Sidebar.Pusher>
</Sidebar.Pushable>
</Media>
)
}
}
/*
MobileContainer.propTypes = {
children: PropTypes.node,
}
*/
const ResponsiveContainer = ({ children }) => (
<div>
<DesktopContainer>{children}</DesktopContainer>
<MobileContainer>{children}</MobileContainer>
</div>
)
// ----------------------------------------------------
class Main extends Component {
constructor(props) {
super(props);
this.advanceSystem = this.advanceSystem.bind(this);
this.state = { duration: 500, visible: true, currentSystem: 0}
}
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.props.system.items.length) {
currentSystem = 0;
}
this.setState({ visible: !this.state.visible, currentSystem });
}
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.props.system.items[this.state.currentSystem];
return (
<>
<style>{mediaStyles}</style>
<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,49 @@
import React, { Component } from "react";
import { Link } from 'react-router-dom'
import SystemCard from "./SystemCard";
import {
Container,
Header,
Card,
Icon,
Menu,
Divider
} from "semantic-ui-react";
// ----------------------------------------------------
class ListSystems extends Component {
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,71 @@
import React, { Component } from "react";
import {
Card,
Icon,
Label,
Grid
} from "semantic-ui-react";
class SystemCard extends Component {
render() {
var location = "";
const system = this.props.system;
var clientCount = "";
var callAvg = "";
var description = system.description;
if (this.props.keepShort && (description.length > 100)) {
description = description.substring(0, 96) + "...";
}
if (system.callAvg) {
callAvg = Math.round(system.callAvg * 10) / 10 + " call/min"
}
if (system.clientCount === 1) {
clientCount = "1 listener";
} else if (system.clientCount > 1) {
clientCount = system.clientCount + " listeners"
}
if (system) {
switch (system.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>
<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>
{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(process.env.REACT_APP_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,71 @@
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:
const activeSystems = action.data.filter(system =>{
if (system.active) {return true}
return false;
});
return Object.assign({}, state, {
isWaiting: false,
items: activeSystems
});
case FETCH_ERROR_SYSTEM:
return Object.assign({}, state, {
isWaiting: false
});
default:
return state;
}
};
export default system;

View File

@@ -3,10 +3,14 @@ import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import { Route, Switch } from 'react-router' // react-router v4/v5
import { ConnectedRouter } from 'connected-react-router'
import configureStore, { history } from './app/configureStore'
import configureStore, { history } from './redux-router/configureStore'
import 'semantic-ui-css/semantic.min.css'
import App from './App';
// Main
import MainContainer from "./Main/MainContainer"
// System
import ListSystemsContainer from "./System/ListSystemsContainer"
const store = configureStore(/* provide initial state if any */)
@@ -15,7 +19,8 @@ ReactDOM.render(
<ConnectedRouter history={history}> { /* place ConnectedRouter under Provider */ }
<> { /* your usual react-router v4/v5 routing */ }
<Switch>
<Route exact path="/" render={() => (<App />)} />
<Route exact path="/" component={MainContainer} />
<Route exact path="/systems" component={ListSystemsContainer} />
<Route render={() => (<div>Miss</div>)} />
</Switch>
</>

View File

@@ -2,11 +2,11 @@
// from https://github.com/supasate/connected-react-router
import { combineReducers } from 'redux'
import { connectRouter } from 'connected-react-router'
import counterReducer from '../features/counter/counterSlice';
import systemReducer from "../System/system-reducer"
const createRootReducer = (history) => combineReducers({
router: connectRouter(history),
counter: counterReducer,
system: systemReducer,
})
export default createRootReducer

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB