Commit ffedfe28 authored by Andrew J Hrdy's avatar Andrew J Hrdy

Merge branch 'typescript' into 'development'

Typescript

See merge request !13
parents 0d1e3476 8417c33e
Pipeline #3964 passed with stages
in 1 minute and 59 seconds
{
"parser": "babel-eslint",
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"node": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module"
},
"plugins": [
"react"
],
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"rules": {
"no-unused-vars": "warn",
"no-console": "off",
"no-case-declarations": "off",
"no-template-curly-in-string": "warn",
"block-scoped-var": "error",
"no-empty-function": "error",
"no-implicit-globals": "error",
"no-multi-spaces": "error",
"no-self-compare": "warn",
"no-shadow": "warn",
"no-shadow-restricted-names": "error",
"no-undef-init": "error",
"array-bracket-spacing": "error",
"block-spacing": "error",
"brace-style": "error",
"camelcase": "error",
"comma-dangle": "warn",
"comma-spacing": "warn",
"computed-property-spacing": "error",
"func-call-spacing": "error",
"indent": [
"error",
4,
{
"SwitchCase": 1,
"ignoredNodes": [
"JSXAttribute",
"JSXSpreadAttribute"
]
}
],
"jsx-quotes": "error",
"key-spacing": "warn",
"keyword-spacing": "warn",
"no-multiple-empty-lines": "warn",
"no-tabs": "error",
"no-trailing-spaces": "error",
"no-whitespace-before-property": "error",
"object-curly-newline": [
"warn",
{
"ObjectExpression": {
"minProperties": 1
},
"ObjectPattern": "never"
}
],
"object-curly-spacing": "warn",
"object-property-newline": "warn",
"operator-assignment": "warn",
"operator-linebreak": ["warn", "after"],
"quote-props": [
"warn",
"as-needed"
],
"quotes": [
"warn",
"single"
],
"semi": "error",
"semi-spacing": [
"error",
{
"before": false,
"after": true
}
],
"space-before-blocks": "error",
"space-before-function-paren": [
"error",
"never"
],
"space-in-parens": "error",
"space-infix-ops": "error",
"space-unary-ops": [
"error",
{
"words": true,
"nonwords": false
}
],
"arrow-parens": "warn",
"arrow-spacing": [
"warn",
{
"before": true,
"after": true
}
],
"no-duplicate-imports": "warn",
"no-useless-computed-key": "warn",
"no-useless-rename": "warn",
"no-var": "error",
"prefer-arrow-callback": "warn",
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "warn",
"rest-spread-spacing": "error",
"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",
"react/jsx-indent": "error",
"react/jsx-pascal-case": "error",
"react/jsx-wrap-multilines": "warn",
"react/no-deprecated": "off"
}
}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -4,25 +4,33 @@
"homepage": "./",
"private": true,
"dependencies": {
"array-sort": "^0.1.4",
"@material-ui/core": "^3.9.2",
"@material-ui/icons": "^3.0.2",
"@types/classnames": "^2.2.7",
"@types/history": "^4.7.2",
"@types/mapbox-gl": "^0.51.4",
"@types/material-ui": "^0.21.6",
"@types/node": "^11.9.4",
"@types/phone-formatter": "0.0.2",
"@types/react": "^16.8.3",
"@types/react-dom": "^16.8.1",
"@types/react-redux": "^7.0.1",
"classnames": "^2.2.5",
"mapbox-gl": "^0.49.0",
"material-ui": "1.0.0-beta.25",
"material-ui-icons": "1.0.0-alpha.19",
"connected-react-router": "^6.3.1",
"history": "^4.7.2",
"jss": "^9.8.7",
"mapbox-gl": "^0.53.0",
"phone-formatter": "^0.0.2",
"promise": "7.1.1",
"prop-types": "^15.5.10",
"react": "^15.6.1",
"react-autosuggest": "^9.3.2",
"react-classnames": "^0.1.2",
"react-dom": "^15.6.1",
"react-mapbox-gl": "^3.9.2",
"promise": "8.0.2",
"react": "^16.8.1",
"react-dom": "^16.8.1",
"react-mapbox-gl": "^4.2.0",
"react-piwik": "^1.6.0",
"react-redux": "^5.0.5",
"react-router-dom": "^4.1.2",
"react-router-redux": "^5.0.0-alpha.9",
"redux": "^3.7.2",
"redux-thunk": "^2.2.0",
"react-redux": "^6.0.0",
"react-router": "^4.3.1",
"redux": "^4.0.1",
"redux-devtools-extension": "^2.13.8",
"redux-saga": "^1.0.1",
"typeface-roboto": "0.0.50"
},
"devDependencies": {
......@@ -30,26 +38,27 @@
"babel": "^6.23.0",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.3",
"babel-loader": "^7.1.4",
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-preset-env": "^1.7.0",
"babel-preset-react": "^6.24.1",
"clean-webpack-plugin": "^0.1.19",
"css-loader": "^0.28.4",
"eslint": "4.19.1",
"eslint-loader": "^2.0.0",
"eslint-plugin-react": "^7.8.2",
"file-loader": "^1.1.11",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.4.0",
"node-sass": "^4.9.0",
"optimize-css-assets-webpack-plugin": "^4.0.2",
"postcss-loader": "2.0.6",
"react-addons-perf": "^15.4.2",
"sass-loader": "^7.0.2",
"source-map-loader": "^0.2.4",
"style-loader": "^0.18.2",
"sw-precache-webpack-plugin": "0.11.3",
"ts-loader": "^5.3.3",
"tslint": "^5.12.1",
"tslint-loader": "^3.5.4",
"tslint-react": "^3.6.0",
"typescript": "^3.3.3",
"uglifyjs-webpack-plugin": "^1.2.6",
"webpack": "^4.4.1",
"webpack-cli": "^2.1.5",
......
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';
export const ADD_FAVORITE_FACILITY = 'ADD_FAVORITE_FACILITY';
export const REMOVE_FAVORITE_FACILITY = 'REMOVE_FAVORITE_FACILITY';
export const SET_ALL_FAVORITES = 'SET_ALL_FAVORITES';
export const SORT_FACILITY_CARDS = 'SORT_FACILITY_CARDS';
\ No newline at end of file
import {GET_ALERTS, GET_FACILITIES, SET_ALERTS, SET_FACILITIES, SORT_FACILITY_CARDS, VIEW_ALERT} from './action-types';
const API_GET_FACILITIES = process.env.API_GET_FACILITIES ? process.env.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';
export const getFacilities = () => (dispatch) => {
dispatch({
type: GET_FACILITIES
});
const request = new Request(API_GET_FACILITIES, {
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(setFacilities(JSON.stringify(json)));
});
};
export const setFacilities = (facilities) => {
try {
localStorage.setItem('facilities', facilities);
} catch (e) {
//Empty
}
return {
type: SET_FACILITIES,
facilities: JSON.parse(facilities)
};
};
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 {
ADD_FAVORITE_FACILITY, REMOVE_FAVORITE_FACILITY, SET_ALL_FAVORITES, SET_CAMPUS_REGION, SET_SEARCH_TERM,
SET_SELECTED_FACILITY, SET_SIDEBAR
} from './action-types';
export const setSidebar = (setOpen) => ({
type: SET_SIDEBAR,
setOpen
});
export const setSelectedFacility = (facility) => ({
type: SET_SELECTED_FACILITY,
facility
});
export const setSearchTerm = (term) => ({
type: SET_SEARCH_TERM,
term
});
export const setCampusRegion = (campusRegion) => ({
type: SET_CAMPUS_REGION,
campusRegion
});
export const addFavoriteFacility = (slug) => ({
type: ADD_FAVORITE_FACILITY,
slug
});
export const removeFavoriteFacility = (slug) => ({
type: REMOVE_FAVORITE_FACILITY,
slug
});
export const setAllFavorites = (favorites) => ({
type: SET_ALL_FAVORITES,
favorites
});
import React from 'react';
import classNames from 'classnames';
import {findLink} from '../utils/nameUtils';
import Chip from 'material-ui/Chip';
import Button from 'material-ui/Button';
import ArrowForwardIcon from 'material-ui-icons/ArrowForward';
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 getBody = () => {
/*
API V2.2 removed the message field and replaced it with
subject and body. In order to ensure backwards compatability,
use the message field if it exists, otherwise use body.
TODO: Eventually, this check will be useless when older alerts
are phased out and should be removed to minimize complexity.
Alternatively, move this into the mapper once TypeScript is added.
*/
const body = alert.message ? alert.message : alert.body;
const links = findLink(body);
if (!links) {
return (
<span className={'alert-body'}>
{body}
</span>
);
}
return (
<span className={'alert-body'}>
{body.substring(0, links.index)}
<a href={links[0]} className={'alert-link'} target="_blank" rel="noopener noreferrer">{links[0]}</a>
{body.substring(links.index + links[0].length)}
</span>
);
};
const getChipLabel = () => alert.urgency_tag.charAt(0).toUpperCase() + alert.urgency_tag.slice(1);
return (
<div className={'alert'}>
<div className={'alert-subject-container'}>
<h3 className={'alert-subject'}>{alert.subject}</h3>
<Chip label={getChipLabel()} className={classNames('alert-urgency-chip', getUrgencyClass())} />
</div>
{getBody()}
{
alert.url &&
<span className={'alert-url-container'}>
<Button size={'small'} href={alert.url} target="_blank" rel="noopener noreferrer" classes={{
root: 'alert-url-button-root'
}}>
More Information
<ArrowForwardIcon />
</Button>
</span>
}
</div>
);
};
export default Alert;
\ No newline at end of file
import * as React from 'react';
import * as classNames from 'classnames';
import { findLink } from '../utils/nameUtils';
import { IAlert } from '../models/alert.model';
import Chip from '@material-ui/core/Chip';
import Button from '@material-ui/core/Button';
import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
class Alert extends React.Component<AlertProps> {
constructor(props: AlertProps) {
super(props);
}
/**
* Converts the alert's urgency tag to the corresponding
* css class.
*
* @memberof Alert
*/
getUrgencyClass = () => {
switch (this.props.alert.urgency_tag) {
case 'emergency':
return 'alert-emergency';
case 'major':
return 'alert-major';
case 'minor':
return 'alert-minor';
case 'info':
default:
return 'alert-info';
}
}
/**
* Converts the alert's text body to proper JSX
*
* @memberof Alert
*/
getBody = () => {
const alert: IAlert = this.props.alert;
const links = findLink(alert.body);
if (!links) {
return (
<span className={'alert-body'}>
{alert.body}
</span>
);
}
return (
<span className={'alert-body'}>
{alert.body.substring(0, links.index)}
<a href={links[0]} className={'alert-link'} target="_blank" rel="noopener noreferrer">{links[0]}</a>
{alert.body.substring(links.index + links[0].length)}
</span>
);
}
getChipLabel = () => this.props.alert.urgency_tag.charAt(0).toUpperCase() + this.props.alert.urgency_tag.slice(1);
render() {
const alert: IAlert = this.props.alert;
return (
<div className={'alert'}>
<div className={'alert-subject-container'}>
<h3 className={'alert-subject'}>{alert.subject}</h3>
<Chip label={this.getChipLabel()} className={classNames('alert-urgency-chip', this.getUrgencyClass())} />
</div>
{this.getBody()}
{
alert.url &&
<span className={'alert-url-container'}>
<Button size={'small'} href={alert.url} target="_blank" rel="noopener noreferrer" classes={{
root: 'alert-url-button-root'
}}>
More Information
<ArrowForwardIcon />
</Button>
</span>
}
</div>
);
}
}
export interface AlertProps {
alert: IAlert;
}
export default Alert;
\ No newline at end of file
import React from 'react';
import AppBar from 'material-ui/AppBar';
import Toolbar from 'material-ui/Toolbar';
import Typography from 'material-ui/Typography';
import Button from 'material-ui/Button';
import IconButton from 'material-ui/IconButton';
import MenuIcon from 'material-ui-icons/Menu';
import * as React from 'react';
import * as classNames from 'classnames';
import SearchBar from '../containers/SearchBar';
import classNames from 'classnames';
import AlertContainer from '../containers/AlertContainer';
class CustomAppBar extends React.Component {
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Button from '@material-ui/core/Button';
import IconButton from '@material-ui/core/IconButton';
import MenuIcon from '@material-ui/icons/Menu';
class CustomAppBar extends React.Component<{}, CustomAppBarState> {
constructor() {
super();
constructor(props: {}) {
super(props);
this.state = {
isAppBarExpanded: false,
isSearchExpanded: false
......@@ -21,6 +23,12 @@ class CustomAppBar extends React.Component {
this.toggleExpand = this.toggleExpand.bind(this);
}
/**
* Toggles whether the app bar is expanded. This is
* mobile only functionality.
*
* @memberof CustomAppBar
*/
toggleExpand() {
this.setState({
isAppBarExpanded: !this.state.isAppBarExpanded
......@@ -35,25 +43,29 @@ class CustomAppBar extends React.Component {
<Toolbar className={'app-bar-tool-bar'}>
<div className={'app-bar-logo-name'}>
<img src={require('../images/SRCT_square.svg')} className={'app-bar-logo'}/>
<Typography type="title" className={classNames('app-bar-title', 'app-bar-text-color')}>
<Typography variant="h6" className={classNames('app-bar-title', 'app-bar-text-color')}>
What's Open
</Typography>
</div>
<div className={'app-bar-right-section'}>
<div className={'app-bar-alert-container'}>
<AlertContainer/>
</div>
<SearchBar onSearchExpand={() => this.setState({
isSearchExpanded: true
})}
onSearchCollapse={() => this.setState({
isSearchExpanded: false
})}/>
onSearchCollapse={() => this.setState({
isSearchExpanded: false
})}/>
<IconButton onClick={this.toggleExpand} aria-label="Menu"
className={classNames('app-bar-menu-button', 'app-bar-text-color')}>
<MenuIcon/>
</IconButton>
</div>
<div
className={classNames('app-bar-link-container', !this.state.isAppBarExpanded && 'app-bar-hide')}>
<Button className={classNames('app-bar-link-button', 'app-bar-text-color')}
......@@ -72,4 +84,9 @@ class CustomAppBar extends React.Component {
}
}
export interface CustomAppBarState {
isAppBarExpanded: boolean;
isSearchExpanded: boolean;
}
export default CustomAppBar;
import React from 'react';
import * as React from 'react';
import { IFacility, CampusRegion } from '../models/facility.model';
import FacilityCard from '../containers/FacilityCard';
import Grid from 'material-ui/Grid';
class CardContainer extends React.Component {
constructor(props) {
import Grid from '@material-ui/core/Grid';
class CardContainer extends React.Component<CardContainerProps> {
constructor(props: CardContainerProps) {
super(props);
this.state = {
selectedSlug: null
};
}
filterCards = (facility) => {
/**
* A filtering function for facilities. Returns true
* if the facility should be shown, otherwise false.
*
* Note: Filtering is done based on the current search term.
*
* @memberof CardContainer
*/
filterCards = (facility: IFacility): boolean => {
if (facility.facility_location.campus_region.toLowerCase() !== this.props.campusRegion.toLowerCase()) {
return false;
}
......@@ -21,6 +30,7 @@ class CardContainer extends React.Component {
const facilityCategory = facility.facility_category.name.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
const facilityTags = facility.facility_product_tags;
const friendlyName = facility.facility_location.friendly_building.toLowerCase();
facilityTags.forEach((tag) => {
return tag.toLowerCase();
});
......@@ -33,25 +43,17 @@ class CardContainer extends React.Component {
return facilityName.includes(lSearchTerm) || facilityLocation.includes(lSearchTerm) ||
facilityCategory.includes(lSearchTerm) || hasTag || friendlyName.includes(lSearchTerm);