Commit 17e18825 authored by Andrew Hrdy's avatar Andrew Hrdy

Started conversion to typescript. Very WIP.

parent 517d420d
Pipeline #3942 failed with stage
in 1 minute and 8 seconds
This diff is collapsed.
import {GET_ALERTS, GET_FACILITIES, SET_ALERTS, SET_FACILITIES, SORT_FACILITY_CARDS, VIEW_ALERT} from './action-types';
import { Dispatch } from 'redux';
import { IFacility } from '../models/facility.model';
import { IAlert } from '../models/alert.model';
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) => {
export const getFacilities = () => (dispatch: Dispatch<any>) => {
dispatch({
type: GET_FACILITIES
});
......@@ -20,20 +24,20 @@ export const getFacilities = () => (dispatch) => {
return res.json();
}).then((json) => {
dispatch(setFacilities(JSON.stringify(json)));
dispatch(setFacilities(json));
});
};
export const setFacilities = (facilities) => {
export const setFacilities = (facilities: IFacility[]) => {
try {
localStorage.setItem('facilities', facilities);
localStorage.setItem('facilities', JSON.stringify(facilities));
} catch (e) {
//Empty
}
return {
type: SET_FACILITIES,
facilities: JSON.parse(facilities)
facilities: facilities
};
};
......@@ -41,7 +45,7 @@ export const sortFacilityCards = () => ({
type: SORT_FACILITY_CARDS
});
export const getAlerts = () => (dispatch) => {
export const getAlerts = () => (dispatch: Dispatch<any>) => {
dispatch({
type: GET_ALERTS
});
......@@ -62,7 +66,7 @@ export const getAlerts = () => (dispatch) => {
});
};
export const setAlerts = (alerts) => {
export const setAlerts = (alerts: IAlert[]) => {
const viewedAlerts = JSON.parse(localStorage.getItem('viewedAlerts'));
if (viewedAlerts) {
......@@ -77,7 +81,7 @@ export const setAlerts = (alerts) => {
};
};
export const viewAlert = (alert) => {
export const viewAlert = (alert: IAlert) => {
try {
let viewedAlerts = JSON.parse(localStorage.getItem('viewedAlerts'));
if (!viewedAlerts) {
......
......@@ -2,38 +2,39 @@ import {
ADD_FAVORITE_FACILITY, REMOVE_FAVORITE_FACILITY, SET_ALL_FAVORITES, SET_CAMPUS_REGION, SET_SEARCH_TERM,
SET_SELECTED_FACILITY, SET_SIDEBAR
} from './action-types';
import { IFacility, CampusRegion } from '../models/facility.model';
export const setSidebar = (setOpen) => ({
export const setSidebar = (setOpen: boolean) => ({
type: SET_SIDEBAR,
setOpen
});
export const setSelectedFacility = (facility) => ({
export const setSelectedFacility = (facility: IFacility) => ({
type: SET_SELECTED_FACILITY,
facility
});
export const setSearchTerm = (term) => ({
export const setSearchTerm = (term: string) => ({
type: SET_SEARCH_TERM,
term
});
export const setCampusRegion = (campusRegion) => ({
export const setCampusRegion = (campusRegion: CampusRegion) => ({
type: SET_CAMPUS_REGION,
campusRegion
});
export const addFavoriteFacility = (slug) => ({
export const addFavoriteFacility = (slug: string) => ({
type: ADD_FAVORITE_FACILITY,
slug
});
export const removeFavoriteFacility = (slug) => ({
export const removeFavoriteFacility = (slug: string) => ({
type: REMOVE_FAVORITE_FACILITY,
slug
});
export const setAllFavorites = (favorites) => ({
export const setAllFavorites = (favorites: string[]) => ({
type: SET_ALL_FAVORITES,
favorites
});
......
import React from 'react';
import * as React from 'react';
import classNames from 'classnames';
import {findLink} from '../utils/nameUtils';
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';
const Alert = ({alert}) => {
const getUrgencyClass = () => {
switch (alert.urgency_tag) {
class Alert extends React.Component<AlertProps> {
constructor(props: AlertProps) {
super(props);
}
getUrgencyClass = () => {
switch (this.props.alert.urgency_tag) {
case 'emergency':
return 'alert-emergency';
case 'major':
......@@ -19,8 +26,8 @@ const Alert = ({alert}) => {
return 'alert-info';
}
};
const getBody = () => {
getBody = () => {
/*
API V2.2 removed the message field and replaced it with
subject and body. In order to ensure backwards compatability,
......@@ -31,50 +38,59 @@ const Alert = ({alert}) => {
Alternatively, move this into the mapper once TypeScript is added.
*/
const body = alert.message ? alert.message : alert.body;
const links = findLink(body);
const alert: IAlert = this.props.alert;
const links = findLink(alert.body);
if (!links) {
return (
<span className={'alert-body'}>
{body}
{alert.body}
</span>
);
}
return (
<span className={'alert-body'}>
{body.substring(0, links.index)}
{alert.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)}
{alert.body.substring(links.index + links[0].length)}
</span>
);
};
const getChipLabel = () => alert.urgency_tag.charAt(0).toUpperCase() + alert.urgency_tag.slice(1);
getChipLabel = () => this.props.alert.urgency_tag.charAt(0).toUpperCase() + this.props.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())} />
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>
)
}
}
{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 * as React from 'react';
import AppBar from '@material-ui/core/AppBar';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
......@@ -9,10 +9,10 @@ import SearchBar from '../containers/SearchBar';
import classNames from 'classnames';
import AlertContainer from '../containers/AlertContainer';
class CustomAppBar extends React.Component {
class CustomAppBar extends React.Component<{}, CustomAppBarState> {
constructor() {
super();
super({});
this.state = {
isAppBarExpanded: false,
isSearchExpanded: false
......@@ -72,4 +72,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/core/Grid';
class CardContainer extends React.Component {
constructor(props) {
class CardContainer extends React.Component<CardContainerProps, CardContainerState> {
constructor(props: CardContainerProps) {
super(props);
this.state = {
selectedSlug: null
};
}
filterCards = (facility) => {
filterCards = (facility: IFacility) => {
if (facility.facility_location.campus_region.toLowerCase() !== this.props.campusRegion.toLowerCase()) {
return false;
}
......@@ -21,6 +25,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();
});
......@@ -35,7 +40,7 @@ class CardContainer extends React.Component {
facilityCategory.includes(lSearchTerm) || hasTag || friendlyName.includes(lSearchTerm);
};
handleFacilityClick = (slug) => {
handleFacilityClick = (slug: string) => {
const newValue = (slug !== this.state.selectedSlug) ? slug : null;
this.setState({
......@@ -45,10 +50,12 @@ class CardContainer extends React.Component {
render() {
const {facilities} = this.props;
return (
<Grid container className={'card-container-root'} spacing={24} justify={'center'} alignItems={'flex-end'}>
{facilities.filter(this.filterCards).map((item) => {
const isSelected = (item.slug === this.state.selectedSlug);
return (
<Grid key={item.slug} item>
<FacilityCard facility={item} facilities={facilities} isSelected={isSelected} setSelected={this.handleFacilityClick}/>
......@@ -59,4 +66,15 @@ class CardContainer extends React.Component {
);
}
}
export interface CardContainerProps {
facilities: IFacility[];
searchTerm: string;
campusRegion: CampusRegion;
}
export interface CardContainerState {
selectedSlug: string;
}
export default CardContainer;
\ No newline at end of file
import React from 'react';
import * as React from 'react';
import ReactMapboxGl, {Marker, Popup} from 'react-mapbox-gl';
import {getMaxBounds, getCenterOfCampusRegion} from '../utils/mapboxUtils';
import mapboxgl from 'mapbox-gl';
import {removeBrackets} from '../utils/nameUtils';
import { IFacility, CampusRegion, IFacilityLocation } from '../models/facility.model';
import MenuItem from '@material-ui/core/Menu';
import Select from '@material-ui/core/Select';
import FormControl from '@material-ui/core/FormControl';
import {getMaxBounds, getCenterOfCampusRegion} from '../utils/mapboxUtils';
import mapboxgl from 'mapbox-gl';
import Typography from '@material-ui/core/Typography';
import {removeBrackets} from '../utils/nameUtils';
const mapboxToken = 'pk.eyJ1IjoibWR1ZmZ5OCIsImEiOiJjaXk2a2lxODQwMDdyMnZzYTdyb3M4ZTloIn0.mSocl7zUnZBO6-CV9cvmnA';
......@@ -18,50 +20,35 @@ const Mark = {
border: '3px solid #EAA29B'
};
class FacilitiesMap extends React.Component {
constructor(props) {
class FacilitiesMap extends React.Component<FacilitiesMapProps, FacilitiesMapState> {
private Map: any;
constructor(props: FacilitiesMapProps) {
super(props);
const {facility, interactive = true} = this.props;
const campusRegion = facility && facility.facility_location ? facility.facility_location.campus_region : 'fairfax';
const {facility, interactive = true} = props;
const campusRegion = facility && facility.facility_location ? facility.facility_location.campus_region : CampusRegion.Fairfax;
this.Map = ReactMapboxGl({
accessToken: mapboxToken,
interactive: interactive,
attributionControl: false
});
const facilityLocationExists = facility && facility.facility_location && facility.facility_location.campus_region === campusRegion;
/**
* facilityLocations is an array of the type:
* {
* location: {}
* facilities: [{}, {}, ...]
* }
*/
this.state = {
maxBounds: getMaxBounds(campusRegion),
campusRegion: campusRegion,
zoom: [17],
center: facilityLocationExists ? facility.facility_location.coordinate_location.coordinates : getCenterOfCampusRegion(campusRegion),
fitBoundsOptions: {},
center: facility ? facility.facility_location.coordinate_location.coordinates : getCenterOfCampusRegion(campusRegion),
facilityLocations: [],
selectedLocation: null,
isLoaded: false
};
// if (interactive) {
// this.state.center = facilityLocationExists ? facility.facility_location.coordinate_location.coordinates : getCenterOfCampusRegion(campusRegion);
// }else{
// setTimeout(() => {
// this.setState({
// center: facilityLocationExists ? facility.facility_location.coordinate_location.coordinates : getCenterOfCampusRegion(campusRegion)
// });
// }, 500);
// }
}
componentWillReceiveProps(nextProps) {
componentWillReceiveProps(nextProps: FacilitiesMapProps) {
const {facility, facilities} = nextProps;
const campusRegion = facility && facility.facility_location ? facility.facility_location.campus_region : 'fairfax';
const campusRegion = facility && facility.facility_location ? facility.facility_location.campus_region : CampusRegion.Fairfax;
this.changeRegion(campusRegion, facility);
......@@ -70,11 +57,7 @@ class FacilitiesMap extends React.Component {
}
}
changeRegion = (campusRegion, facility) => {
if (!facility) {
facility = this.props.facility;
}
changeRegion = (campusRegion: CampusRegion, facility: IFacility = this.props.facility) => {
const facilityLocationExists = facility && facility.facility_location && facility.facility_location.campus_region === campusRegion;
const newState = {
......@@ -88,8 +71,8 @@ class FacilitiesMap extends React.Component {
}, 100);
};
generateLocationArray = (facilities) => {
const locations = [];
generateLocationArray = (facilities: IFacility[]) => {
const locations: FacilityMapLocation[] = [];
facilities.forEach((facility) => {
const location = locations.find((loc) => loc.location.id === facility.facility_location.id);
......@@ -108,10 +91,9 @@ class FacilitiesMap extends React.Component {
});
}
selectLocation = (location) => {
selectLocation = (location: FacilityMapLocation) => {
const {interactive = true} = this.props;
const oldSelectedLocation = this.state.selectedLocation;
const oldZoom = this.state.zoom;
if (!interactive) {
return;
......@@ -126,10 +108,10 @@ class FacilitiesMap extends React.Component {
render() {
const {interactive = true} = this.props;
const {maxBounds, fitBoundsOptions, facilityLocations, selectedLocation, center, zoom} = this.state;
const {maxBounds, facilityLocations, selectedLocation, center, zoom} = this.state;
return (
<this.Map
onStyleLoad={(map) => {
onStyleLoad={(map: any) => {
if (interactive) {
map.addControl(new mapboxgl.GeolocateControl({
positionOptions: {
......@@ -153,7 +135,6 @@ class FacilitiesMap extends React.Component {
}}
center={center}
fitBounds={maxBounds}
fitBoundsOptions={fitBoundsOptions}
zoom={zoom}
maxBounds={maxBounds}>
......@@ -163,11 +144,11 @@ class FacilitiesMap extends React.Component {
<Select
disableUnderline
value={this.state.campusRegion}
onChange={(e) => this.changeRegion(e.target.value)}>
<MenuItem value={'fairfax'}>Fairfax</MenuItem>
<MenuItem value={'arlington'}>Arlington</MenuItem>
<MenuItem value={'prince william'}>SciTech</MenuItem>
<MenuItem value={'front royal'}>Front Royal</MenuItem>
onChange={(e) => this.changeRegion((CampusRegion as any)[e.target.value])}>
<MenuItem value={CampusRegion.Fairfax}>Fairfax</MenuItem>
<MenuItem value={CampusRegion.Arlington}>Arlington</MenuItem>
<MenuItem value={CampusRegion.PrinceWilliam}>SciTech</MenuItem>
<MenuItem value={CampusRegion.FrontRoyal}>Front Royal</MenuItem>
</Select>
</FormControl>
)}
......@@ -211,4 +192,26 @@ class FacilitiesMap extends React.Component {
}
}
export interface FacilitiesMapProps {
facility: IFacility;
facilities: IFacility[];
interactive: boolean;
}
interface FacilityMapLocation {
location: IFacilityLocation;
facilities: IFacility[];
}
export interface FacilitiesMapState {
maxBounds: number[][];
campusRegion: CampusRegion;
zoom: number[];
center: number[];
facilityLocations: FacilityMapLocation[];
selectedLocation: FacilityMapLocation;
isLoaded: boolean;
}
export default FacilitiesMap;
\ No newline at end of file
import React from 'react';
import Typography from '@material-ui/core/Typography';
const FacilityCategory = ({category}) => {
return (
<div className={'facility-category-wrapper'}>
<Typography variant={'body1'} noWrap>
{category.name}
</Typography>
</div>
);
};
export default FacilityCategory;
\ No newline at end of file
import * as React from 'react';
import { IFacilityCategory } from '../models/facility.model';
import Typography from '@material-ui/core/Typography';
class FacilityCategory extends React.Component<FacilityCategoryProps> {
constructor(props: FacilityCategoryProps) {
super(props);
}
render() {
return (
<div className={'facility-category-wrapper'}>
<Typography variant={'body1'} noWrap>
{this.props.category.name}
</Typography>
</div>
)
}
}
export interface FacilityCategoryProps {
category: IFacilityCategory;
}
export default FacilityCategory;
\ No newline at end of file
import React from 'react';
import * as React from 'react';
import {removeBrackets} from '../utils/nameUtils';
import { IFacility } from '../models/facility.model';
import WeekHours from './WeekHours';
import MapDialog from './MapDialog';
import Dialog from '@material-ui/core/Dialog';
import Grid from '@material-ui/core/Grid';
import WeekHours from './WeekHours';
import Button from '@material-ui/core/Button';
import Avatar from '@material-ui/core/Avatar';
import Typography from '@material-ui/core/Typography';
import MapDialog from './MapDialog';
import CloseIcon from '@material-ui/icons/Close';
import IconButton from '@material-ui/core/IconButton';
import LocationOnIcon from '@material-ui/icons/LocationOn';
import {removeBrackets} from '../utils/nameUtils';
class FacilityDialog extends React.Component {
class FacilityDialog extends React.Component<FacilityDialogProps, FacilityDialogState> {
constructor(props) {
constructor(props: FacilityDialogProps) {
super(props);
this.state = {
......@@ -84,4 +87,15 @@ class FacilityDialog extends React.Component {
}
}
export interface FacilityDialogProps {
facility: IFacility;
facilities: IFacility[];
isOpen: boolean;
onClose: () => void;
}
export interface FacilityDialogState {
isMapOpen: boolean;
}
export default FacilityDialog;
\ No newline at end of file
import React from 'react';
import Typography from '@material-ui/core/Typography';
import FacilityUtils from '../utils/facilityUtils';
import * as React from 'react';
import classNames from 'classnames';
import FacilityUtils from '../utils/facilityUtils';
import { IFacility } from '../models/facility.model';
const FacilityStatus = ({facility}) => {
import Typography from '@material-ui/core/Typography';
class FacilityStatus extends React.Component<FacilityStatusProps> {
constructor(props: FacilityStatusProps) {
super(props);
}
/**
* Generates information about the facility's status.
*
* @returns {{label: string, color: *, icon: *}} Information about the facility.
*/
const generateStatusInfo = () => {
* Generates information about the facility's status.
*
* @returns {{label: string, isOpen: boolean}} Information about the facility.
*/
generateStatusInfo = () => {
let label;
let isOpen;
if (FacilityUtils.isFacilityOpen(facility)) {
if (FacilityUtils.isFacilityOpen(this.props.facility)) {
label = 'OPEN';
isOpen = true;
} else {
......@@ -28,14 +34,20 @@ const FacilityStatus = ({facility}) => {
};
};
const statusInfo = generateStatusInfo();
render() {
const statusInfo = this.generateStatusInfo();
return (
<Typography variant={'caption'}
className={classNames('facility-status-text', statusInfo.isOpen ? 'facility-status-open' : 'facility-status-closed')}>
{statusInfo.label}
</Typography>
)
}
}
return (
<Typography variant={'caption'}
className={classNames('facility-status-text', statusInfo.isOpen ? 'facility-status-open' : 'facility-status-closed')}>
{statusInfo.label}
</Typography>
);
};
export interface FacilityStatusProps {
facility: IFacility;
}
export default FacilityStatus;
\ No newline at end of file
import React from 'react';
import FavoriteBorderIcon from '@material-ui/icons/FavoriteBorder';
import FavoriteIcon from '@material-ui/icons/Favorite';
import PropTypes from 'prop-types';
import * as React from 'react';
import classNames from 'classnames';