Source

pages/Primary/Primary.js

// NPM module imports
import React, { Component } from "react";
import PropTypes from "prop-types";
import shortid from "shortid";
import { Switch, Route } from "react-router-dom";
import { connect } from "react-redux";

// Component imports
import Header from "../../components/Header/Header";
import MobileNav from "../../components/MobileNav/MobileNav";
import SideNav from "../../components/SideNav/SideNav";
import ChoirSelection from "../../components/ChoirSelection/ChoirSelection";
import MusicSelection from "../../components/MusicSelection/MusicSelection";
import AlertBar from "../../components/AlertBar/AlertBar";
import Home from "../../components/Home/Home";
import Music from "../../components/Music/Music";
import Progress from "../../components/Progress/Progress";
import ChoirMembers from "../../components/ChoirMembers/ChoirMembers";
import Footer from "../../components/Footer/Footer";

// File imports
import { signOut } from "../../store/actions";
import * as choirSelectionRoutingOptions from "../../components/ChoirSelection/choirSelectionRoutingOptions";
import * as alertBarTypes from "../../components/AlertBar/alertBarTypes";

// Image imports
import homeIconBlue from "../../assets/icons/home-icon-blue.svg";
import practiceIconBlue from "../../assets/icons/practice-icon-blue.svg";
import progressIconBlue from "../../assets/icons/progress-icon-blue.svg";
import choirIconBlue from "../../assets/icons/choir-icon-blue.svg";
import homeIconWhite from "../../assets/icons/home-icon-white.svg";
import practiceIconWhite from "../../assets/icons/practice-icon-white.svg";
import progressIconWhite from "../../assets/icons/progress-icon-white.svg";
import choirIconWhite from "../../assets/icons/choir-icon-white.svg";

// Style imports
import styles from "./Primary.module.scss";

/**
 * Renders the Primary component.
 * This is the container component for all of the components that should include a header, sidebar, and footer.
 * @extends {Component}
 * @component
 * @category Primary
 * @author Dan Levy <danlevy124@gmail.com>
 */
class Primary extends Component {
    /**
     * Sets up state
     * @param {object} props - See PropTypes
     */
    constructor(props) {
        super(props);

        /**
         * Primary component state
         * @property {boolean} isMobileScreenWidth - Indicates if the screen width is mobile (i.e. < 768px)
         * @property {boolean} showMobileNav - Indicates if the mobile navigation should be shown
         * @property {object|null} alertData - Data used to display an alert
         * @property {module:alertBarTypes} alertData.type - The type of alert bar to show
         * @property {string} alertData.heading - The alert heading
         * @property {string} alertData.message - The alert message
         */
        this.state = {
            isMobileScreenWidth: window.innerWidth < 768,
            showMobileNav: false,
            alertData: null,
            mainNavTabs: this.getMainNavTabs(),
        };
    }

    /**
     * The copyright year for the app
     * @type {string}
     */
    _COPYRIGHT_YEAR = "2020";

    /**
     * The current app version number
     * @type {string}
     */
    _VERSION_NUMBER = "0.0.10";

    /**
     * Indicates if the component is mounted.
     * Used for asynchronous tasks.
     * @see https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
     */
    _isMounted = false;

    /**
     * Creates an event listener for window resize
     */
    componentDidMount() {
        this._isMounted = true;
        window.addEventListener("resize", this.handleWindowResize);
        this.showAlertHandler(
            alertBarTypes.INFO,
            "A Reminder",
            "Please use HEADPHONES when practicing! The piano interferes with your analysis."
        );
    }

    /**
     * Removes the window event resize event listener
     */
    componentWillUnmount() {
        this._isMounted = false;
        window.removeEventListener("resize", this.handleWindowResize);
    }

    /**
     * Generates the main navigation tabs
     * @function
     * @returns {array} An array of navigation tabs (data only, not JSX)
     */
    getMainNavTabs = () => {
        const tabs = [];

        tabs.push(
            this.getMainNavTab(
                "Home",
                "/home",
                homeIconBlue,
                homeIconWhite,
                false
            )
        );
        tabs.push(
            this.getMainNavTab(
                "Practice",
                "/practice",
                practiceIconBlue,
                practiceIconWhite,
                true
            )
        );
        tabs.push(
            this.getMainNavTab(
                "Progress",
                "/progress",
                progressIconBlue,
                progressIconWhite,
                false
            )
        );
        tabs.push(
            this.getMainNavTab(
                "Choirs",
                "/choirs",
                choirIconBlue,
                choirIconWhite,
                false
            )
        );

        return tabs;
    };

    /**
     * Creates a main navigation tab
     * @function
     * @param {string} name - The tab name
     * @param {string} blueIcon - A blue version of the icon
     * @param {string} whiteIcon - A white version of the icon
     * @param {boolean} isCurrent - Indicates whether the tab is the current tab
     * @returns {object} A tab (data only, not JSX)
     */
    getMainNavTab = (name, route, blueIcon, whiteIcon, isCurrentTab) => {
        return {
            key: shortid.generate(),
            name,
            route,
            blueIcon,
            whiteIcon,
            isCurrentTab,
        };
    };

    /**
     * Updates state when the window resizes
     * @function
     */
    handleWindowResize = () => {
        this.setState({ isMobileScreenWidth: window.innerWidth < 768 });
    };

    /**
     * Shows or hides the hamburger menu
     * @function
     */
    showHideHamburgerMenu = () => {
        this.setState((prevState) => ({
            showMobileNav: !prevState.showMobileNav,
        }));
    };

    /**
     * Gets confirmation from user and then signs the user out
     * @function
     */
    signOutClickedHandler = () => {
        if (window.confirm("Do you want to sign out?")) {
            this.props.signOut();
        }
    };

    /**
     * Updates the isCurrentTab boolean for all main navigation tabs based on the tab that was clicked on.
     * Updates state with the new tabs (data only, not JSX)
     * @function
     * @param {string} key - The key of the tab that was clicked on (uses the same key prop that React uses)
     */
    navLinkClickedHandler = (key) => {
        if (this._isMounted) {
            this.setState((prevState) => {
                // Makes a deep copy of the old main nav tabs
                const oldTabs = [...prevState.mainNavTabs];

                // Generates the new main nav tabs
                const newTabs = oldTabs.map((tab) => {
                    // Makes a deep copy of the tab
                    const newTab = { ...tab };

                    // Updates isCurrentTab boolean
                    if (tab.key === key) {
                        newTab.isCurrentTab = true;
                    } else {
                        newTab.isCurrentTab = false;
                    }

                    return newTab;
                });

                // Updates state with the new tabs
                return { mainNavTabs: newTabs };
            });
        }
    };

    /**
     * Sets alertData in state when a new alert is triggered
     * @function
     * @param {alertBarTypes} - The type of alert bar to show
     * @param {string} - The alert heading
     * @param {string} - The alert message
     */
    showAlertHandler = (type, heading, message) => {
        this.setState({
            alertData: { type, heading, message },
        });
    };

    /**
     * Sets alertData in state to null in state when the alert disappears
     * @function
     */
    alertIsDoneHandler = () => {
        this.setState({ alertData: null });
    };

    /**
     * Gets the main navigation based on the screen size
     * @function
     * @returns {object} - A JSX element representing the main navigation
     */
    getMainNav = () => {
        if (this.state.isMobileScreenWidth) {
            // Returns the mobile navigation component
            return (
                <MobileNav
                    tabs={this.state.mainNavTabs}
                    show={this.state.showMobileNav}
                    onNavLinkClick={(key) => {
                        this.showHideHamburgerMenu();
                        this.navLinkClickedHandler(key);
                    }}
                    onSignOutClick={this.signOutClickedHandler}
                />
            );
        } else {
            // Returns the side navigation component
            return (
                <SideNav
                    tabs={this.state.mainNavTabs}
                    onNavLinkClick={this.navLinkClickedHandler}
                    onSignOutClick={this.signOutClickedHandler}
                    copyrightYear={this._COPYRIGHT_YEAR}
                    versionNumber={this._VERSION_NUMBER}
                />
            );
        }
    };

    /**
     * Renders the Primary component
     * @returns {object} The JSX to render
     */
    render() {
        return (
            <div className={styles.primary}>
                {/* Shows an alert if one exists */}
                {this.state.alertData ? (
                    <AlertBar
                        type={this.state.alertData.type}
                        heading={this.state.alertData.heading}
                        message={this.state.alertData.message}
                        done={this.alertIsDoneHandler}
                    />
                ) : null}

                <Header
                    hamburgerMenuClicked={this.showHideHamburgerMenu}
                    isMobileScreenWidth={this.state.isMobileScreenWidth}
                />

                {/* The main navigation */}
                {this.getMainNav()}

                {/* Determines which component to display */}
                <Switch>
                    <Route path="/practice/choirs/:choirId/music/:musicId">
                        <Music showAlert={this.showAlertHandler} />
                    </Route>

                    <Route path="/practice/choirs/:choirId">
                        <MusicSelection showAlert={this.showAlertHandler} />
                    </Route>

                    <Route path="/choirs/:choirId">
                        <ChoirMembers showAlert={this.showAlertHandler} />
                    </Route>

                    <Route path="/practice">
                        <ChoirSelection
                            routing={
                                choirSelectionRoutingOptions.MUSIC_SELECTION
                            }
                            showAlert={this.showAlertHandler}
                        />
                    </Route>

                    <Route path="/progress">
                        <Progress />
                    </Route>

                    <Route path="/choirs">
                        <ChoirSelection
                            routing={choirSelectionRoutingOptions.CHOIR_MEMBERS}
                            showAlert={this.showAlertHandler}
                        />
                    </Route>

                    <Route path="/home">
                        <Home />
                    </Route>
                </Switch>

                {/* Displays a footer on mobile screen sizes */}
                {this.state.isMobileScreenWidth ? (
                    <Footer
                        copyrightYear={this._COPYRIGHT_YEAR}
                        versionNumber={this._VERSION_NUMBER}
                    />
                ) : null}
            </div>
        );
    }
}

// Prop types for the Primary component
Primary.propTypes = {
    /**
     * Tells Redux to sign the user out
     */
    signOut: PropTypes.func.isRequired,
};

/**
 * Passes certain Redux actions to the Primary component as props.
 * This function is used only by the react-redux connect function.
 * @memberof Primary
 * @param {function} dispatch - The react-redux dispatch function
 * @returns {object} Redux actions used in the Primary component
 */
const mapDispatchToProps = (dispatch) => {
    return {
        signOut: () => dispatch(signOut()),
    };
};

export default connect(null, mapDispatchToProps)(Primary);