Commit 67ade9f5 authored by Andrew Hrdy's avatar Andrew Hrdy

Alerts added. Closes #14.

parents 4a8fbd7a 3e50c173
Pipeline #2072 passed with stage
in 1 minute and 39 seconds
......@@ -117,6 +117,7 @@
"template-curly-spacing": "error",
"react/no-unescaped-entities": "off",
"react/prop-types": "off",
"react/no-find-dom-node": "off",
"react/boolean-prop-naming": "warn",
"react/no-danger": "error",
"react/no-typos": "warn",
......
......@@ -9470,6 +9470,16 @@
}
}
},
"react-notification-system": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/react-notification-system/-/react-notification-system-0.2.16.tgz",
"integrity": "sha1-m52iCw00eGtgBXxStCUW6hKVN0o=",
"requires": {
"create-react-class": "15.6.2",
"object-assign": "4.1.1",
"prop-types": "15.6.0"
}
},
"react-popper": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-0.7.4.tgz",
......@@ -10187,7 +10197,6 @@
"os-tmpdir": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
},
"osenv": {
......
export const SET_SIDEBAR = 'SET_SIDEBAR';
export const SET_FACILITIES = 'SET_FACILITIES';
export const GET_FACILITIES = 'GET_FACILITIES';
export const SET_ALERTS = 'SET_ALERTS';
export const GET_ALERTS = 'GET_ALERTS';
export const VIEW_ALERT = 'VIEW_ALERT';
export const SET_SELECTED_FACILITY = 'SET_SELECTED_FACILITY';
export const SET_SEARCH_TERM = 'SET_SEARCH_TERM';
export const SET_CAMPUS_REGION = 'SET_CAMPUS_REGION';
......
import {GET_FACILITIES, SET_FACILITIES, SORT_FACILITY_CARDS } from './action-types';
import {GET_ALERTS, GET_FACILITIES, SET_ALERTS, SET_FACILITIES, SORT_FACILITY_CARDS, VIEW_ALERT } from './action-types';
const API_GET_FACILITIES = 'https://api.srct.gmu.edu/whatsopen/v2/facilities/';
const API_GET_ALERTS = 'https://api.srct.gmu.edu/whatsopen/v2/alerts/?ordering=urgency_tag';
// SHOP MASON ONLY
//const API_GET_FACILITIES = 'https://api.srct.gmu.edu/whatsopen/v2/facilities/?facility_classifier=shopmason';
export const getFacilities = () => (dispatch) => {
......@@ -41,3 +42,60 @@ export const sortFacilityCards = () => ({
type: SORT_FACILITY_CARDS
});
export const getAlerts = () => (dispatch) => {
dispatch({
type: GET_ALERTS
});
const request = new Request(API_GET_ALERTS, {
method: 'GET'
});
return fetch(request)
.then((res) => {
if (res.status < 200 || res.status >= 300) {
throw new Error(res.statusText);
}
return res.json();
}).then((json) => {
dispatch(setAlerts(json));
});
};
export const setAlerts = (alerts) => {
const viewedAlerts = JSON.parse(localStorage.getItem('viewedAlerts'));
if (viewedAlerts) {
alerts.forEach((alert) => {
alert['viewed'] = viewedAlerts.includes(alert.id);
});
}
return {
type: SET_ALERTS,
alerts: alerts
};
};
export const viewAlert = (alert) => {
try {
let viewedAlerts = JSON.parse(localStorage.getItem('viewedAlerts'));
if (!viewedAlerts) {
viewedAlerts = [];
}
if (!viewedAlerts.includes(alert.id)) {
viewedAlerts.push(alert.id);
}
localStorage.setItem('viewedAlerts', JSON.stringify(viewedAlerts));
} catch (e) {
//Empty
}
return {
type: VIEW_ALERT,
alert
};
};
import React from 'react';
import classNames from 'classnames';
import {findLink} from '../utils/nameUtils';
import Chip from 'material-ui/Chip';
const Alert = ({alert}) => {
const getUrgencyClass = () => {
switch (alert.urgency_tag) {
case 'emergency':
return 'alert-emergency';
case 'major':
return 'alert-major';
case 'minor':
return 'alert-minor';
case 'info':
default:
return 'alert-info';
}
};
const getMessage = () => {
const links = findLink(alert.message);
if (!links) {
return (<span className={'alert-message'}>
{alert.message}
</span>);
}
return (
<span className={'alert-message'}>
{alert.message.substring(0, links.index)}
<a href={links[0]} className={'alert-link'} target="_blank" rel="noopener">{links[0]}</a>
{alert.message.substring(links.index + links[0].length)}
</span>
);
};
const getChipLabel = () => alert.urgency_tag.charAt(0).toUpperCase() + alert.urgency_tag.slice(1);
return (
<div className={'alert'}>
{getMessage()}
<Chip label={getChipLabel()} className={classNames('alert-urgency-chip', getUrgencyClass())}/>
</div>
);
};
export default Alert;
\ No newline at end of file
......@@ -7,6 +7,7 @@ import IconButton from 'material-ui/IconButton';
import MenuIcon from 'material-ui-icons/Menu';
import SearchBar from '../containers/SearchBar';
import classNames from 'classnames';
import AlertContainer from '../containers/AlertContainer';
class CustomAppBar extends React.Component {
......@@ -38,7 +39,10 @@ class CustomAppBar extends React.Component {
What's Open
</Typography>
</div>
<div className={'app-bar-search-menu'}>
<div className={'app-bar-right-section'}>
<div className={'app-bar-alert-container'}>
<AlertContainer/>
</div>
<SearchBar onSearchExpand={() => this.setState({
isSearchExpanded: true
})}
......@@ -53,11 +57,11 @@ class CustomAppBar extends React.Component {
<div
className={classNames('app-bar-link-container', !this.state.isAppBarExpanded && 'app-bar-hide')}>
<Button className={classNames('app-bar-link-button', 'app-bar-text-color')}
href={'https://srct.gmu.edu/'} target="_blank">
href={'https://srct.gmu.edu/'} target="_blank" rel="noopener">
About
</Button>
<Button className={classNames('app-bar-link-button', 'app-bar-text-color')}
href={'https://srct.gmu.edu/contact/'} target="_blank">
href={'https://srct.gmu.edu/contact/'} target="_blank" rel="noopener">
Feedback
</Button>
</div>
......
import React from 'react';
import {findDOMNode} from 'react-dom';
import IconButton from 'material-ui/IconButton';
import Popover from 'material-ui/Popover';
import Alert from '../components/Alert';
import NotificationsIcon from 'material-ui-icons/Notifications';
import CloseIcon from 'material-ui-icons/Close';
import Typography from 'material-ui/Typography';
import {connect} from 'react-redux';
import {viewAlert} from '../actions/api';
class AlertContainer extends React.Component {
constructor() {
super();
this.state = {
isOpen: false,
anchorEl: null
};
}
handleOpen = () => {
this.setState({
isOpen: true
});
this.props.alerts.forEach((alert) => this.props.viewAlert(alert));
};
handleClose = () => {
this.setState({
isOpen: false
});
};
handleBtnRef = (c) => {
this.setState({
anchorEl: findDOMNode(c)
});
};
isAlertActive = (alert) => {
const curDate = new Date();
const startDate = new Date(alert.start_datetime);
const endDate = new Date(alert.end_datetime);
return curDate > startDate && curDate < endDate;
};
render() {
const {alerts} = this.props;
const activeAlerts = alerts.filter(this.isAlertActive);
return (
<div>
<IconButton classes={{
root: 'alert-container-btn'
}} ref={this.handleBtnRef} onClick={this.handleOpen}>
{activeAlerts.filter((alert) => !alert.viewed).length !== 0 &&
<span className={'alert-container-number'}>
<Typography type={'caption'} className={'alert-container-number-text'}>
{activeAlerts.length}
</Typography>
</span>}
<NotificationsIcon>
</NotificationsIcon>
</IconButton>
<Popover
open={this.state.isOpen}
anchorEl={this.state.anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'center'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'center'
}}
onClose={this.handleClose}>
<div className={'alert-container-popover'}>
<div className={'alert-container-header'}>
<Typography type={'title'} className={'alert-container-title'}>
Alerts
</Typography>
<IconButton className={'alert-container-close-btn'} onClick={this.handleClose}>
<CloseIcon className={'alert-container-close-icon'}/>
</IconButton>
</div>
{
activeAlerts.length === 0 ?
<div>
<div className={'alert'}>
<span>There are no alerts at this time.</span>
</div>
</div> :
<div>
{activeAlerts.map((alert) => {
return (
<Alert key={alert.id} alert={alert}/>
);
})}
</div>
}
</div>
</Popover>
</div>
);
}
}
function mapStateToProps(state) {
return {
alerts: state.alerts
};
}
export default connect(mapStateToProps, {
viewAlert
})(AlertContainer);
import React from 'react';
import {connect} from 'react-redux';
// import {setAllFavorites} from '../actions/ui';
import AppBar from '../components/AppBar';
import Sidebar from '../components/Sidebar';
import {getFacilities, setFacilities, sortFacilityCards} from '../actions/api';
import {getAlerts, getFacilities, setAlerts, setFacilities, sortFacilityCards} from '../actions/api';
import {setSidebar, setSelectedFacility, setAllFavorites} from '../actions/ui';
import CardContainer from '../components/CardContainer';
......@@ -34,6 +33,7 @@ class Layout extends React.Component {
}
this.props.getFacilities();
this.props.getAlerts();
};
render() {
......@@ -41,16 +41,18 @@ class Layout extends React.Component {
return (
<div className={'layout-root'}>
<AppBar isOpen={false} />
<AppBar isOpen={false}/>
<div className={'layout-container'}>
<div className={'layout-main-content'}>
<div className={'layout-card-container'}>
<CardContainer styles={'layout-card-container'} searchTerm={searchTerm}
campusRegion={campusRegion} facilities={facilities} />
campusRegion={campusRegion} facilities={facilities}/>
</div>
</div>
<Sidebar facilities={facilities} facility={selectedFacility} isSidebarOpen={isSidebarOpen} setSidebar={setSidebar} setSelectedFacility={setSelectedFacility} campusRegion={campusRegion}/>
<Sidebar facilities={facilities} facility={selectedFacility} isSidebarOpen={isSidebarOpen}
setSidebar={setSidebar} setSelectedFacility={setSelectedFacility}
campusRegion={campusRegion}/>
</div>
</div>
);
......@@ -60,6 +62,7 @@ class Layout extends React.Component {
function mapStateToProps(state) {
return {
facilities: state.facilities.data,
alerts: state.alerts,
favorites: state.ui.favorites,
searchTerm: state.ui.search.term,
campusRegion: state.ui.search.campusRegion,
......@@ -72,8 +75,10 @@ function mapStateToProps(state) {
export default connect(mapStateToProps, {
getFacilities,
setFacilities,
getAlerts,
setAlerts,
setAllFavorites,
sortFacilityCards,
setSidebar,
setSelectedFacility,
setSelectedFacility
})(Layout);
import {GET_FACILITIES, SET_FACILITIES, SORT_FACILITY_CARDS } from '../actions/action-types';
import {GET_ALERTS, GET_FACILITIES, SET_ALERTS, SET_FACILITIES, SORT_FACILITY_CARDS, VIEW_ALERT } from '../actions/action-types';
import cloneDeep from 'lodash/cloneDeep';
import facilityUtils from '../utils/facilityUtils';
const defaultState = {
const defaultFacilityState = {
isLoading: false,
data: []
};
export const facilities = (state = defaultState, action, ui) => {
const facilitySort = (a,b) =>{
export const facilities = (state = defaultFacilityState, action, ui) => {
const facilitySort = (a, b) => {
const favoriteCheck = ui.favorites.includes(b.slug) - ui.favorites.includes(a.slug);
if (favoriteCheck !== 0) {
......@@ -15,7 +15,7 @@ export const facilities = (state = defaultState, action, ui) => {
}
const openCheck = facilityUtils.isFacilityOpen(b) - facilityUtils.isFacilityOpen(a);
if(openCheck !== 0) {
if (openCheck !== 0) {
return openCheck;
}
......@@ -50,3 +50,24 @@ export const facilities = (state = defaultState, action, ui) => {
return state;
}
};
export const alerts = (state = [], action) => {
switch (action.type) {
case GET_ALERTS:
return state;
case SET_ALERTS:
return [...state, ...action.alerts];
case VIEW_ALERT:
const index = state.findIndex((alert) => alert.id === action.alert.id);
const alert = cloneDeep(state[index]);
alert.viewed = true;
const stateClone = state.slice();
stateClone[index] = alert;
return stateClone;
default:
return state;
}
};
import {routerReducer} from 'react-router-redux';
import ui from './ui';
import {facilities} from './api';
import {facilities, alerts} from './api';
const reducers = (state = {}, action) => ({
router: routerReducer(state.router, action),
ui: ui(state.ui, action),
facilities: facilities(state.facilities, action, state.ui)
facilities: facilities(state.facilities, action, state.ui),
alerts: alerts(state.alerts, action)
});
export default reducers;
.alert {
padding: 8px 16px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.875rem;
font-weight: 400;
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
line-height: 1.46429em;
cursor: default;
position: relative;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
&:hover {
background-color: rgba(0, 0, 0, 0.03);
}
}
.alert-message {
display: flex;
flex-wrap: wrap;
margin-right: 10px;
}
.alert-urgency-chip {
height: 20px !important;
font-size: 12px !important;
line-height: 1;
color: white !important;
}
.alert-link {
color: #0645AD;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-all;
}
.alert-info {
background-color: #42A5F5 !important;
}
.alert-minor {
background-color: #66BB6A !important;
}
.alert-major {
background-color: #FFA726 !important;
}
.alert-emergency {
background-color: #EF5350 !important;
}
\ No newline at end of file
......@@ -15,7 +15,7 @@
color: #354052;
}
.app-bar-logo-name, .app-bar-search-menu {
.app-bar-logo-name, .app-bar-right-section {
display: flex;
align-items: center;
}
......@@ -54,7 +54,7 @@
}
.app-bar-search-expanded {
.app-bar-logo, .app-bar-title, .app-bar-menu-button, .app-bar-link-container {
.app-bar-logo, .app-bar-title, .app-bar-alert-container, .app-bar-menu-button, .app-bar-link-container {
display: none !important;
}
}
......
.alert-container-btn {
color: rgba(0, 0, 0, 0.4) !important;
}
.alert-container-popover {
max-width: 450px;
max-height: 300px;
}
.alert-container-close-btn {
width: 20px !important;
height: 20px !important;
}
.alert-container-close-icon {
width: 20px !important;
height: 20px !important;
}
.alert-container-header {
padding: 8px 16px;
background-color: rgba(0, 0, 0, 0.06);
display: flex;
align-items: center;
justify-content: space-between;
}
.alert-container-title {
font-weight: 400 !important;
font-size: 20px !important;
}
.alert-container-number {
position: absolute;
top: 3px;
right: 5px;
background-color: #FF0000;
padding: 4px;
border-radius: 100%;
height: 10px;
width: 10px;
}
.alert-container-number-text {
color: rgba(255, 255, 255, 1) !important;
height: 10px !important;
line-height: 10px !important;
}
\ No newline at end of file
@import './variables.scss';
@import './mixins.scss';
/* Components */
@import './components/alert';
@import './components/appBar';
@import './components/cardContainer';
@import './components/facilityCategory';
......@@ -12,6 +13,7 @@
@import './components/textwTitle';
@import './components/weekHours';
/* Containers */
@import './containers/alertContainer';
@import './containers/facilityCard';
@import './containers/layout';
@import './containers/searchBar';
......
......@@ -16,4 +16,15 @@ export const removeBrackets = (name) => {
}
return name;
};
const linkRegex = /(?:(?:https?|ftp|file):\/\/|www\.|ftp\.)(?:\([-A-Z0-9+&@#\/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#\/%=~_|$?!:,.])*(?:\([-A-Z0-9+&@#\/%=~_|$?!:,.]*\)|[A-Z0-9+&@#\/%=~_|$])/igm;
/**
* Finds where links appear in a string.
*
* @param val The string
* @returns {RegExpExecArray | null}
*/
export const findLink = (val) => {
return linkRegex.exec(val);
};
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment