Commit 4caa8d79 authored by Andrew Hrdy's avatar Andrew Hrdy

Add more documentation, fix indentation

parent 2f0beef8
Pipeline #5503 passed with stages
in 1 minute and 44 seconds
......@@ -6,9 +6,15 @@ import ArrowForwardIcon from '@material-ui/icons/ArrowForward';
import { Chip, Button } from '@material-ui/core';
interface AlertProps {
/**
* The alert that this component should display.
*/
alert: IAlert;
}
/**
* Represents a specific alert in an alert container.
*/
export default ({alert}: AlertProps) => {
/**
......@@ -31,8 +37,6 @@ export default ({alert}: AlertProps) => {
/**
* Converts the alert's text body to proper JSX
*
* @memberof Alert
*/
const getBody = () => {
const links = findLink(alert.body);
......
......@@ -4,7 +4,16 @@ import SearchBar from '../containers/SearchBar';
import AlertContainer from '../containers/AlertContainer';
import { AppBar, Toolbar, Typography, Button } from '@material-ui/core';
/**
* Represents the bar at the top of the UI.
*/
export default () => {
/**
* If true, the app bar is displaying the search bar; otherwise, the
* app bar is displaying the normal UI.
*
* Note: This only applies to mobile users.
*/
const [isSearchExpanded, setIsSearchExpanded] = React.useState<boolean>(false);
return (
......@@ -14,6 +23,7 @@ export default () => {
<Toolbar className={'app-bar-tool-bar'}>
<div className={'app-bar-logo-name'}>
<img src={'assets/icons/whats-open-512x512.png'} className={'app-bar-logo'}/>
<Typography variant="h6" className={classNames('app-bar-title', 'app-bar-text-color')}>
What's Open
</Typography>
......@@ -32,6 +42,7 @@ export default () => {
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" rel="noopener">
Feedback
......
......@@ -4,18 +4,28 @@ import FacilityCard from '../containers/FacilityCard';
import { Grid } from '@material-ui/core';
interface CardContainerProps {
/**
* An array of facilities to show in this container.
*/
facilities: IFacility[];
/**
* If true, show the favorite icons on each card.
*/
showFavoriteIcons: boolean;
}
/**
* A responsive grid of facility cards.
*/
export default ({facilities, showFavoriteIcons}: CardContainerProps) => {
return (
<Grid container={true} className={'card-container-root'} spacing={3} justify={'center'} alignItems={'flex-end'}>
{facilities.map(item => (
<Grid key={item.slug} item={true}>
<FacilityCard facility={item} showFavoriteIcon={showFavoriteIcons} />
</Grid>
))}
{facilities.map(item => (
<Grid key={item.slug} item={true}>
<FacilityCard facility={item} showFavoriteIcon={showFavoriteIcons} />
</Grid>
))}
</Grid>
);
};
\ No newline at end of file
......@@ -3,9 +3,15 @@ import { IFacilityCategory } from '../models/facility.model';
import { Typography } from '@material-ui/core';
export interface FacilityCategoryProps {
/**
* The category to display.
*/
category: IFacilityCategory;
}
/**
* Displays the name of the facility category.
*/
export default ({category}: FacilityCategoryProps) => {
return (
<div className={'facility-category-wrapper'}>
......
......@@ -5,39 +5,23 @@ import { IFacility } from '../models/facility.model';
import { Typography } from '@material-ui/core';
interface FacilityStatusProps {
/**
* The facility to check openness on.
*/
facility: IFacility;
}
/**
* Displays either Open or Closed depending whether
* a facility is opened or not.
*/
export default ({facility}: FacilityStatusProps) => {
/**
* Generates information about the facility's status.
*
* @returns {{label: string, isOpen: boolean}} Information about the facility.
*/
const generateStatusInfo = () => {
let label;
let isOpen;
if (isFacilityOpen(facility)) {
label = 'OPEN';
isOpen = true;
} else {
label = 'CLOSED';
isOpen = false;
}
return {
label: label,
isOpen: isOpen
};
};
const statusInfo = generateStatusInfo();
const isOpen = isFacilityOpen(facility);
return (
<Typography variant={'caption'}
className={classNames('facility-status-text', statusInfo.isOpen ? 'facility-status-open' : 'facility-status-closed')}>
{statusInfo.label}
className={classNames('facility-status-text', isOpen ? 'facility-status-open' : 'facility-status-closed')}>
{isOpen ? 'OPEN' : 'CLOSED'}
</Typography>
);
};
\ No newline at end of file
......@@ -6,24 +6,50 @@ import FavoriteIcon from '@material-ui/icons/Favorite';
import { trackPiwikEvent } from '../piwik/piwik';
interface FavoriteButtonProps {
/**
* The slug of the facility this favorite button
* is linked to.
*/
slug: string;
/**
* True if the facility is initially favorited;
* otherwise false.
*/
initialState: boolean;
/**
* Add a facility to the favorite list.
*/
addFavoriteFacility: (slug: string) => void;
/**
* Remove a facility from the favorite list.
*/
removeFavoriteFacility: (slug: string) => void;
}
/**
* Displays a button that can be toggled to favorite
* or unfavorite a facility.
*/
export default ({slug, initialState, addFavoriteFacility, removeFavoriteFacility}: FavoriteButtonProps) => {
/**
* If true, the facility is favorited
*/
const [isFavorite, setIsFavorite] = React.useState<boolean>(initialState);
const handleClick = (event: React.MouseEvent) => {
event.stopPropagation(); // Stops the card from being selected in the sidebar.
event.preventDefault(); // Also stops the card from being selected in the sidebar.
// Stops the card from being selected in the sidebar.
event.stopPropagation();
// Also stops the card from being selected in the sidebar.
event.preventDefault();
const newState = !isFavorite;
setIsFavorite(newState);
// Track the action and call the corresponding action.
if (!newState) {
trackPiwikEvent('card-action', 'un-favorite');
......
import * as React from 'react';
import { CircularProgress, Typography } from '@material-ui/core';
/**
* A spinner with a loading text. The loading
* text has an animated ... following it.
*/
export default () => (
<div className={'loading-spinner-container'}>
<CircularProgress className={'loading-spinner'} />
......
......@@ -9,11 +9,31 @@ import CloseIcon from '@material-ui/icons/Close';
import * as classNames from 'classnames';
interface SidebarProps {
/**
* The facility to show detailed information of
* in the sidebar.
*/
facility: IFacility;
/**
* If true, the sidebar is visible; otherwise
* it is hidden.
*/
isVisible: boolean;
/**
* Function to close the sidebar.
*/
closeSidebar: () => void;
}
/**
* A sidebar that displays detailed information
* about a facility.
*
* Note: This component is only used in the
* desktop version.
*/
export default ({facility, isVisible, closeSidebar}: SidebarProps) => {
return (
<div className={classNames('sidebar-container', isVisible && 'sidebar-container-open')}>
......
import * as React from 'react';
export interface TextwTitleProps {
/**
* The label associated with the content.
*/
label: string;
/**
* JSX content to be displayed.
*/
content: any;
}
/**
* Displays a JSX element with a label
*/
export default ({label, content}: TextwTitleProps) => {
return (
<div>
......
......@@ -3,6 +3,10 @@ import { getHoursByDay } from '../utils/facility.util';
import { IFacility } from '../models/facility.model';
import { Grid, Typography } from '@material-ui/core';
/**
* A mapping of week day (via index) to label.
* (0 = Mon, 1 = Tue, etc.)
*/
const weekDays = [
'Mon',
'Tue',
......@@ -14,9 +18,17 @@ const weekDays = [
];
interface WeekHoursProps {
facility: IFacility;
/**
* The facility whose hours of operation is
* displayed.
*/
facility: IFacility;
}
/**
* Displays a facilities hours of operation for an
* entire week.
*/
export default ({facility}: WeekHoursProps) => {
const output = [];
......
......@@ -8,16 +8,44 @@ import CloseIcon from '@material-ui/icons/Close';
import { ApplicationState } from '../store';
import { addViewedAlertsAction } from '../store/alert/alert.actions';
/**
* A container that shows a notification icon. When pressed, a
* popover opens showing all of the Alerts.
*/
export default () => {
/**
* List of all active alerts.
*/
const alerts: IAlert[] = useSelector((state: ApplicationState) => state.alerts.alerts);
/**
* List of alert IDs that have been viewed.
*/
const viewedAlerts: number[] = useSelector((state: ApplicationState) => state.alerts.viewedAlerts);
const dispatch = useDispatch();
/**
* Specify a list of alert IDs to mark as viewed.
*/
const addViewedAlerts = (alertIds: number[]) => dispatch(addViewedAlertsAction(alertIds));
/**
* If true, the notification popover is open;
* otherwise it is closed.
*/
const [isOpen, setIsOpen] = React.useState<boolean>(false);
/**
* The element to anchor the popover to.
*/
const [anchorEl, setAnchorEl] = React.useState<HTMLElement>(null);
/**
* Callback for when the icon button is opened.
*
* @param event The click event
*/
const handleOpen = (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
setIsOpen(true);
setAnchorEl(event.currentTarget);
......@@ -25,11 +53,20 @@ export default () => {
addViewedAlerts(alerts.map(alert => alert.id));
};
/**
* Callback to handle the close of the popover.
*/
const handleClose = () => {
setIsOpen(false);
setAnchorEl(null);
};
/**
* Returns true if the alert is currently
* active; otherwise false.
*
* @param alert The alert to test.
*/
const isAlertActive = (alert: IAlert) => {
const curDate = new Date();
const startDate = new Date(alert.start_datetime);
......@@ -38,6 +75,12 @@ export default () => {
return curDate > startDate && curDate < endDate;
};
/**
* Returns true if the alert has already
* been viewed; otherwise false.
*
* @param alert The alert to test.
*/
const hasAlertBeenViewed = (alert: IAlert) => {
return viewedAlerts.includes(alert.id);
};
......@@ -58,6 +101,7 @@ export default () => {
<NotificationsIcon />
</IconButton>
<Popover
open={isOpen}
anchorEl={anchorEl}
......
......@@ -11,20 +11,53 @@ import { setSelectedFacilityAction } from '../store/ui/ui.actions';
import LoadingSpinner from '../components/LoadingSpinner';
import { SearchBarState } from '../store/ui/ui.reducer';
/**
* The root for the desktop view.
*/
export default () => {
/**
* A list of all the facilities.
*/
const facilities: IFacility[] = useSelector((state: ApplicationState) => state.facilities.facilities);
/**
* The term that is currently being searched for.
*/
const searchTerm: SearchBarState = useSelector((state: ApplicationState) => state.ui.search);
/**
* The slug of the selected facility.
*/
const selectedFacilitySlug: string = useSelector((state: ApplicationState) => state.ui.selectedFacility);
/**
* True if the facilities are currently being
* fetched; otherwise false.
*/
const isFetching = useSelector((state: ApplicationState) => state.facilities.isFetching);
const dispatch = useDispatch();
/**
* Function to select a facility.
*
* @param slug The slug of the facility to select.
*/
const selectFacility = (slug: string) => dispatch(setSelectedFacilityAction(slug));
const selectedFacility = facilities.find(f => f.slug === selectedFacilitySlug);
/**
* The facility to show in the sidebar.
*
* Important: This may be different than the selected facility.
*/
const [sidebarFacility, setSidebarFacility] = React.useState<IFacility>(selectedFacility);
/**
* This is needed to prevent the sidebar from quickly closing
* then reopening when a facility is selected.
*/
if ((sidebarFacility === undefined && selectedFacility !== undefined) || (selectedFacility !== undefined && sidebarFacility.slug !== selectedFacility.slug)) {
setSidebarFacility(selectedFacility);
}
......
......@@ -13,25 +13,87 @@ import { Card, CardContent, Typography, Grid } from '@material-ui/core';
import LocationOnIcon from '@material-ui/icons/LocationOn';
interface FacilityCardProps {
/**
* The facility to show in the card.
*/
facility: IFacility;
/**
* True if the favorite icon should be
* shown; otherwise false.
*/
showFavoriteIcon: boolean;
}
/**
* A card that displays information about the
* specified facility.
*
* This component uses React.memo to memoize the component
* on the facilities modified date. This is an important
* performance increase for the following scenario:
* 1. Application loads using cached facilities
* 2. Application fetches updated facilities and
* only one facility is updated.
* 3. Because of memoization, only that one updated
* facility will be re-rendered while the rest
* will succeed the memoization condition.
*/
export default React.memo(({facility, showFavoriteIcon}: FacilityCardProps) => {
/**
* The list of favorited facility slugs.
*/
const favorites = useSelector((state: ApplicationState) => state.ui.favorites);
/**
* The slug of the selected facility.
*/
const selectedFacility = useSelector((state: ApplicationState) => state.ui.selectedFacility);
const dispatch = useDispatch();
/**
* Adds a facility to the favorite list.
*
* @param slug The facility slug.
*/
const addFavoriteFacility = (slug: string) => dispatch(addFavoriteFacilityAction(slug));
/**
* Remove a facility from the favorite list.
*
* @param slug The facility slug.
*/
const removeFavoriteFacility = (slug: string) => dispatch(removeFavoriteFacilityAction(slug));
/**
* Selects a facility.
*
* @param slug The facility slug.
*/
const selectSelectedFacility = (slug: string) => dispatch(setSelectedFacilityAction(slug));
/**
* Returns true if this facility is selected.
*/
const isFacilitySelected = (): boolean => selectedFacility === facility.slug;
/**
* Selects this facility if it is not selected; otherwise,
* unselect this facility.
*/
const selectFacility = () => selectSelectedFacility(isFacilitySelected() ? '' : facility.slug);
/**
* Mapping from JavaScript's day of the week to the API's
* day of the week.
*/
const dayOfWeek = [6, 0, 1, 2, 3, 4, 5][new Date().getDay()];
/**
* Gets the text of corresponding to this facilities hours
* of operation for the current day of the week.
*/
const getDisplayHours = () => {
const currentHour = new Date().getHours();
const todaysHours = getHoursByDay(facility, dayOfWeek);
......
......@@ -12,18 +12,43 @@ import FavoriteButton from '../components/FavoriteButton';
import TextwTitle from '../components/TextwTitle';
interface FacilityDetailProps {
facility: IFacility;
onClose: () => void;
/**
* The facility to show.
*/
facility: IFacility;
/**
* Function to call when closing.
*/
onClose: () => void;
}
/**
* Shows specific information about a facility.
*
* Note: This component is only used in the
* mobile version.
*/
export default ({facility, onClose}: FacilityDetailProps) => {
/**
* True if this facility is favorited; otherwise false.
*/
const isFavorite = useSelector((state: ApplicationState) => state.ui.favorites).includes(facility.slug);
const dispatch = useDispatch();
/**
* Add a facility as a favorite.
*
* @param slug The slug of the facility to add.
*/
const addFavorite = (slug: string) => dispatch(addFavoriteFacilityAction(slug));
/**
* Removes a facility as a favorite.
*
* @param slug The slug of the facility to remove.
*/
const removeFavorite = (slug: string) => dispatch(removeFavoriteFacilityAction(slug));
return (
......
......@@ -12,20 +12,54 @@ import { setSelectedFacilityAction } from '../store/ui/ui.actions';
import { useSwipeable } from 'react-swipeable';
import LoadingSpinner from '../components/LoadingSpinner';
/**
* The root for the mobile view.
*/
export default () => {
/**
* A list of all the facilities.
*/
const facilities: IFacility[] = useSelector((state: ApplicationState) => state.facilities.facilities);
/**
* The term that is currently being searched for.
*/
const searchTerm: SearchBarState = useSelector((state: ApplicationState) => state.ui.search);
/**
* The slug of the selected facility.
*/
const selectedFacilitySlug: string = useSelector((state: ApplicationState) => state.ui.selectedFacility);
/**
* True if the facilities are currently being
* fetched; otherwise false.
*/
const isFetching = useSelector((state: ApplicationState) => state.facilities.isFetching);
const dispatch = useDispatch();
/**
* Function to select a facility.
*
* @param slug The slug of the facility to select.
*/
const selectFacility = (slug: string) => dispatch(setSelectedFacilityAction(slug));
const selectedFacility = facilities.find(f => f.slug === selectedFacilitySlug);
/**
* The facility to show in the drawer.
*
* Important: This may be different than the selected facility.
*/
const [drawerFacility, setDrawerFacility] = React.useState<IFacility>(selectedFacility);
/**
* This is needed to prevent the drawer from quickly closing
* then reopening when a facility is selected.
*/
if ((drawerFacility === undefined && selectedFacility !== undefined) || (selectedFacility !== undefined && drawerFacility.slug !== selectedFacility.slug)) {
setDrawerFacility(selectedFacility);
}
......
......@@ -10,19 +10,66 @@ import CloseIcon from '@material-ui/icons/Close';
import ArrowBackIcon from '@material-ui/icons/ArrowBack';
interface SearchBarProps {
/**
* Callback for when the search bar is expanded.
*
* Note: This is only used for the mobile version.
*/
onSearchExpand: () => void;
/**
* Callback for when the search bar is collapsed.
*
* Note: This is only used for the mobile version.
*/
onSearchCollapse: () => void;
}
/**
* A search bar used to filter facilities.
*/
export default ({onSearchExpand, onSearchCollapse}: SearchBarProps) => {
/**
* True if the search bar is currently being focused.
*/
const [isFocused, setIsFocused] = React.useState<boolean>(false);
/**
* True if the search bar is opened.
*
* Note: This is only used for the mobile version.
*/
const [isMobileOpen, setIsMobileOpen] = React.useState<boolean>(false);