Commit c6eb5df3 authored by Andrew Hrdy's avatar Andrew Hrdy

Started alerts with bell icon design.

parent 0f8bb75e
Pipeline #2061 passed with stage
in 1 minute and 38 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",
......
import React from 'react';
import classNames from 'classnames';
import {findLink} from '../utils/nameUtils';
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 alert.message;
}
return (
<span>
{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>
);
};
return (
<div className={classNames('alert', getUrgencyClass())}>
{getMessage()}
</div>
);
};
export default Alert;
\ No newline at end of file
import React from 'react';
import NotificationSystem from 'react-notification-system';
class AlertContainer extends React.Component {
notificationSystem;
constructor() {
super();
/*
The alerts that have been shown need to be stored because componentWillReceiveProps will be called on
change detection. Because of this, whenever an action occurs on the screen, the alerts will be reshown.
*/
this.state = {
shownAlertIds: []
};
}
componentWillReceiveProps(nextProps) {
//TODO: Only show alerts that the user has not seen before.
nextProps.alerts.filter((alert) => !this.state.shownAlertIds.includes(alert.id)).filter(this.isAlertActive).forEach((alert) => {
this.addNotification(alert);
});
}
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;
};
addNotification = (alert) => {
//TODO: Support alerts with links
this.state.shownAlertIds.push(alert.id);
this.notificationSystem.addNotification({
message: alert.message,
level: this.resolveNotificationLevel(alert),
position: 'bl',
autoDismiss: 6,
dismissible: true,
uid: alert.id
});
};
/**
* The What's Open API tells us if an alert is an info, minor, major, or emergency. However, the library that
* deals with notifications requires either a info, success, warning, or error. This function maps the
* What's Open API to the library.
*
* @param alert The What's Open Alert.
* @returns {string} The notification level used by the third party library.
*/
resolveNotificationLevel = (alert) => {
switch (alert.urgency_tag) {
case 'emergency':
return 'error';
case 'major':
return 'warning';
case 'minor':
return 'success';
default:
case 'info':
return 'info';
}
};
render() {
return (
<div>
<NotificationSystem ref={(c) => {
this.notificationSystem = c;
}}/>
</div>
);
}
}
export default AlertContainer;
\ 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,6 +39,7 @@ class CustomAppBar extends React.Component {
What's Open
</Typography>
</div>
<AlertContainer/>
<div className={'app-bar-search-menu'}>
<SearchBar onSearchExpand={() => this.setState({
isSearchExpanded: true
......@@ -53,11 +55,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 Button from 'material-ui/Button';
import Popover from 'material-ui/Popover';
import Alert from '../components/Alert';
import NotificationsIcon from 'material-ui-icons/Notifications';
import {connect} from 'react-redux';
class AlertContainer extends React.Component {
constructor() {
super();
this.state = {
isOpen: false,
anchorEl: null
};
}
handleOpen = () => {
this.setState({
isOpen: true
});
};
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;
return (
<div>
<Button fab mini color={'primary'} ref={this.handleBtnRef} onClick={this.handleOpen}>
<NotificationsIcon/>
</Button>
<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'}>
{alerts.filter(this.isAlertActive).map((alert) => {
return (
<Alert key={alert.id} alert={alert} />
);
})}
</div>
</Popover>
</div>
);
}
}
function mapStateToProps(state) {
return {
alerts: state.alerts
};
}
export default connect(mapStateToProps)(AlertContainer);
......@@ -2,7 +2,6 @@ import React from 'react';
import {connect} from 'react-redux';
import {setAllFavorites, setSelectedFacility, setSidebar} from '../actions/ui';
import AppBar from '../components/AppBar';
import AlertContainer from '../components/AlertContainer';
import Sidebar from '../components/Sidebar';
import {getAlerts, getFacilities, setAlerts, setFacilities, sortByFavorites} from '../actions/api';
import CardContainer from '../components/CardContainer';
......@@ -39,12 +38,11 @@ class Layout extends React.Component {
};
render() {
const {isSidebarOpen, selectedFacility, facilities, alerts, searchTerm, campusRegion, setSidebar, setSelectedFacility} = this.props;
const {isSidebarOpen, selectedFacility, facilities, searchTerm, campusRegion, setSidebar, setSelectedFacility} = this.props;
return (
<div className={'layout-root'}>
<AppBar isOpen={false}/>
<AlertContainer alerts={alerts}/>
<div className={'layout-container'}>
<div className={'layout-main-content'}>
<div className={'layout-card-container'}>
......
.alert {
margin: 8px;
padding: 6px 24px;
border-radius: 5px;
color: rgba(255, 255, 255, 1);
display: flex;
flex-wrap: wrap;
align-items: center;
box-shadow: 0px 3px 5px -1px rgba(0, 0, 0, 0.2), 0px 6px 10px 0px rgba(0, 0, 0, 0.14), 0px 1px 18px 0px rgba(0, 0, 0, 0.12);
font-size: 0.875rem;
font-weight: 400;
font-family: "Roboto", "Helvetica", "Arial", sans-serif;
line-height: 1.46429em;
}
.alert-link {
color: rgba(255, 255, 255, 1) !important;
}
.alert-info {
background-color: #42A5F5;
}
.alert-minor {
background-color: #66BB6A;
}
.alert-major {
background-color: #FFA726;
}
.alert-emergency {
background-color: #EF5350;
}
\ No newline at end of file
.notification {
height: auto !important;
.notification-message {
font-family: "Roboto", "-apple-system", "Helvetica", "Arial", sans-serif;
line-height: 24px;
font-size: 14px;
font-weight: 500;
}
}
@media screen and (max-width: 500px) {
.notifications-bl {
width: 100% !important;
}
}
\ No newline at end of file
.alert-container-popover {
max-width: 500px;
}
\ No newline at end of file
@import './variables.scss';
@import './mixins.scss';
/* Components */
@import 'components/alertContainer';
@import './components/alert';
@import './components/appBar';
@import './components/cardContainer';
@import './components/facilityCategory';
......@@ -13,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