Skip to content

Commit

Permalink
Merge pull request #13 from alexgladd/develop
Browse files Browse the repository at this point in the history
Merge release 0.2.0
  • Loading branch information
alexgladd authored Jan 26, 2020
2 parents 8e70433 + accfdaa commit c42a6f8
Show file tree
Hide file tree
Showing 16 changed files with 7,211 additions and 3,558 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

language: node_js
node_js:
- 8
- 12

cache:
- yarn
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Contributions welcome! Note that all active development is done on the [develop]

**Prerequisites**

* Node.js 8.x
* Node.js 12.x
* Yarn

**Setup**
Expand All @@ -34,6 +34,6 @@ Contributions welcome! Note that all active development is done on the [develop]

## License

Copyright (c) 2018 Alex Gladd
Copyright (c) 2020 Alex Gladd

This project is offered under the [MIT license](LICENSE.md)
21 changes: 17 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
{
"name": "bm-hotgroups",
"version": "0.1.1",
"version": "0.2.0",
"private": true,
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.0-14",
"@fortawesome/free-brands-svg-icons": "^5.1.0",
"@fortawesome/free-solid-svg-icons": "^5.1.0-11",
"@fortawesome/react-fontawesome": "^0.1.0-11",
"lodash": "^4.17.10",
"lodash": "^4.17.13",
"moment": "^2.22.2",
"prop-types": "^15.7.2",
"react": "^16.4.1",
"react-dom": "^16.4.1",
"react-ga": "^2.5.3",
"react-scripts": "1.1.4",
"react-scripts": "3.3.0",
"socket.io-client": "^2.1.1"
},
"scripts": {
Expand All @@ -22,6 +23,18 @@
"eject": "react-scripts eject"
},
"engines": {
"node": "8.x"
"node": "12.x"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
2 changes: 1 addition & 1 deletion public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#dd4b39">
<meta name="description" content="Brandmeister DMR network talkgroup activity monitor and aggregator; see which Brandmeister talkgroups are most active">
<meta name="keywords" content="brandmeister, brandmeister talkgroups, brandmeister network, brandmeister activity">
<meta name="keywords" content="brandmeister, brandmeister talkgroups, brandmeister network, brandmeister activity, brandmeister most active">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
Expand Down
6 changes: 3 additions & 3 deletions public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"short_name": "Brandmeister Activity",
"name": "Brandmeister Top Activity",
"icons": [
{
"src": "favicon.ico",
Expand All @@ -10,6 +10,6 @@
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"theme_color": "#dd4b39",
"background_color": "#ffffff"
}
70 changes: 70 additions & 0 deletions src/components/Filter.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import React from 'react';
import PropTypes from 'prop-types';
import './Filters.css';

const propTypes = {
type: PropTypes.oneOf(['checkbox', 'text']).isRequired,
label: PropTypes.string.isRequired,
state: PropTypes.oneOfType([
PropTypes.string, PropTypes.bool
]).isRequired,
onChange: PropTypes.func.isRequired
};

const defaultProps = {
onChange(e) { return; }
};

export default class Filter extends React.Component {
renderCheckbox() {
const { label, state, onChange } = this.props;
const id = `cb-${label}`;

return (
<React.Fragment>
<input type="checkbox" id={id} checked={state} onChange={onChange} />
<label htmlFor={id}>{label}</label>
</React.Fragment>
);
}

renderTextbox() {
const { label, state, onChange } = this.props;
const id = `txt-${label}`;

return (
<React.Fragment>
<label htmlFor={id}>{`${label}: `}</label>
<input type="text" id={id} placeholder={`Enter ${label}...`} spellCheck="false"
value={state} onChange={onChange} />
</React.Fragment>
);
}

render() {
const { type } = this.props;

let input;
switch (type) {
case 'checkbox':
input = this.renderCheckbox();
break;

case 'text':
input = this.renderTextbox();
break;

default:
input = "";
}

return (
<div className="Filter">
{ input }
</div>
);
}
}

Filter.propTypes = propTypes;
Filter.defaultProps = defaultProps;
23 changes: 23 additions & 0 deletions src/components/FilterBar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import PropTypes from 'prop-types';
import './Filters.css';

const propTypes = {
children: PropTypes.arrayOf(PropTypes.element).isRequired
};

const defaultProps = {};

export default class FilterBar extends React.Component {
render() {
const { children } = this.props;
return (
<div className="Filters">
{ children }
</div>
);
}
}

FilterBar.propTypes = propTypes;
FilterBar.defaultProps = defaultProps;
57 changes: 57 additions & 0 deletions src/components/Filters.css
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,62 @@ Common filters styles
display: flex;
justify-content: center;
margin: 0 auto;
}

.Filter {
color: #f6f1f2;
font-size: 0.75rem;
margin: 6px;
padding: 4px 6px;
background-color: #898695;
border: 1px solid #898695;
border-radius: 6px;
}

.Filter input[type="checkbox"] {
opacity: 0;
}
.Filter input[type="checkbox"] + label {
position: relative;
padding-left: 18px;
margin-left: -18px;
}
.Filter input[type="checkbox"] + label::before {
position: absolute;
top: 1px;
left: 0px;
content: "";
display: inline-block;
height: 12px;
width: 12px;
border: 2px solid #de9e8e;
border-radius: 4px;
background-color: #f6f1f2;
}
.Filters input[type="checkbox"] + label:after {
position: absolute;
left: 3px;
top: 5px;
content: none;
display: inline-block;
height: 4px;
width: 8px;
border-left: 2px solid #f6f1f2;
border-bottom: 2px solid #f6f1f2;
transform: rotate(-45deg);
}
.Filter input[type="checkbox"]:checked + label::after {
content: "";
}
.Filter input[type="checkbox"]:checked + label::before {
background-color: #de9e8e;
}

.Filter input[type="text"] {
border: 2px solid #de9e8e;
border-radius: 4px;
background-color: #f6f1f2;
}
.Filter input[type="text"]:focus {
outline: none;
}
4 changes: 2 additions & 2 deletions src/components/Header.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Header styles

#Header {
display: flex;
align-items: baseline;
align-items: center;
position: fixed;
top: 0;
width: 100vw;
Expand All @@ -16,7 +16,7 @@ Header styles

#Header h1 {
flex: 1 1 auto;
font-size: 1.25rem;
font-size: 26px;
margin: 0;
}

Expand Down
84 changes: 75 additions & 9 deletions src/components/TopCallsigns.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faUser } from '@fortawesome/free-solid-svg-icons'
import moment from 'moment';
import ReactGA from 'react-ga';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUser } from '@fortawesome/free-solid-svg-icons';
import _ from 'lodash';
import FilterBar from './FilterBar';
import Filter from './Filter';
import { hasNameFilter, createNameFilter, hasCallsignFilter, createCallsignFilter } from '../util/filters';
import { formatTime } from '../util/session';
import './TopCallsigns.css';
import './Filters.css';
import './Tables.css';

const propTypes = {
Expand All @@ -19,20 +22,74 @@ export default class TopCallsigns extends React.Component {
super(props);

this.state = {
viewCount: 20
viewCount: 20,
namedOnly: false,
callsignOnly: false,
nameFilter: '',
callsignFilter: ''
};
}

componentDidUpdate(prevProps, prevState) {
// only run this logic in production
if (process.env.NODE_ENV !== 'production') return;

const { namedOnly, nameFilter, callsignOnly, callsignFilter } = this.state;

if (namedOnly !== prevState.namedOnly) {
// console.log(`Named Only ${namedOnly}`)
ReactGA.event({ category: 'Top Callsigns Filters', action: `Named Only ${namedOnly}` });
}

if (callsignOnly !== prevState.callsignOnly) {
// console.log(`Callsign Only ${callsignOnly}`)
ReactGA.event({ category: 'Top Callsigns Filters', action: `Callsign Only ${callsignOnly}` });
}

if (!_.isEmpty(nameFilter) && _.isEmpty(prevState.nameFilter)) {
// console.log('Name filter added')
ReactGA.event({ category: 'Top Callsigns Filters', action: 'Name filter added' });
} else if (_.isEmpty(nameFilter) && !_.isEmpty(prevState.nameFilter)) {
// console.log('Name filter removed');
ReactGA.event({ category: 'Top Callsigns Filters', action: 'Name filter removed' });
}

if (!_.isEmpty(callsignFilter) && _.isEmpty(prevState.callsignFilter)) {
// console.log('Callsign filter added')
ReactGA.event({ category: 'Top Callsigns Filters', action: 'Callsign filter added' });
} else if (_.isEmpty(callsignFilter) && !_.isEmpty(prevState.callsignFilter)) {
// console.log('Callsign filter removed');
ReactGA.event({ category: 'Top Callsigns Filters', action: 'Callsign filter removed' });
}
}

render() {
const { callsigns } = this.props;
const { viewCount } = this.state;
const { viewCount, namedOnly, callsignOnly, nameFilter, callsignFilter } = this.state;

let filteredCallsigns = callsigns;
if (callsignOnly) {
filteredCallsigns = _.filter(filteredCallsigns, hasCallsignFilter);
}

if (namedOnly) {
filteredCallsigns = _.filter(filteredCallsigns, hasNameFilter);
}

if (!_.isEmpty(callsignFilter)) {
filteredCallsigns = _.filter(filteredCallsigns, createCallsignFilter(callsignFilter));
}

if (!_.isEmpty(nameFilter)) {
filteredCallsigns = _.filter(filteredCallsigns, createNameFilter(nameFilter));
}

let topCallsigns = callsigns.map((cs, idx) => (
let topCallsigns = filteredCallsigns.map((cs, idx) => (
<tr key={idx}>
<td>{ cs.label }</td>
<td>{ cs.name }</td>
<td>{ `${cs.talkTime} seconds` }</td>
<td>{ moment.unix(cs.lastActive).format('ddd h:mm:ssa') }</td>
<td>{ formatTime(cs.lastActive) }</td>
</tr>
)).slice(0, viewCount);

Expand All @@ -48,7 +105,16 @@ export default class TopCallsigns extends React.Component {
<div id="TopCallsigns">
<h2><FontAwesomeIcon icon={faUser} /> Top Callsigns</h2>

<div className="Filters"></div>
<FilterBar>
<Filter type="checkbox" label="Has callsign" state={callsignOnly}
onChange={ (e) => this.setState({ callsignOnly: e.target.checked }) } />
<Filter type="checkbox" label="Has name" state={namedOnly}
onChange={ (e) => this.setState({ namedOnly: e.target.checked }) } />
<Filter type="text" label="Callsign" state={callsignFilter}
onChange={ (e) => this.setState({ callsignFilter: e.target.value }) } />
<Filter type="text" label="Name" state={nameFilter}
onChange={ (e) => this.setState({ nameFilter: e.target.value }) } />
</FilterBar>

<table>
<thead><tr>
Expand Down
Loading

0 comments on commit c42a6f8

Please sign in to comment.