Source

components/ChoirSelection/ChoirSelection.js

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

// Component imports
import ChoirCard from "./ChoirCards/ChoirCard/ChoirCard";
import ChoirOptionCard from "./ChoirCards/ChoirOptionCard/ChoirOptionCard";
import PageHeader from "../PageHeader/PageHeader";
import LoadingContainer from "../Spinners/LoadingContainer/LoadingContainer";

// Image imports
import plusIcon from "../../assets/icons/plus-icon.svg";
import questionIcon from "../../assets/icons/question-icon.svg";

// File imports
import { choirSelectionError } from "../../vendors/Firebase/logs";
import { getUsersChoirs, joinChoir } from "../../vendors/AWS/tmaApi";
import * as alertBarTypes from "../AlertBar/alertBarTypes";
import {
    choirSelectedForPractice,
    choirSelectedForChoirs,
} from "../../store/actions/index";
import * as routingOptions from "./choirSelectionRoutingOptions";
import * as cardColorOptions from "./ChoirCards/choirCardColorOptions";

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

/**
 * Renders the choir selection component
 * Shows a card for each choir, as well as card(s) for choir option(s).
 * @extends {Component}
 * @component
 * @category ChoirSelection
 * @author Dan Levy <danlevy124@gmail.com>
 */
class ChoirSelection extends Component {
    /**
     * ChoirSelection component state
     * @property {boolean} isLoading - Indicates if the component is in a loading state
     * @property {array} choirs - An array of choirs that the user is a member of
     */
    state = {
        isLoading: true,
        choirs: null,
    };

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

    /**
     * Sets _isMounted to true.
     * Gets the list of choirs.
     */
    componentDidMount() {
        this._isMounted = true;
        this.getChoirList();
    }

    /**
     * Sets _isMounted to false
     */
    componentWillUnmount() {
        this._isMounted = false;
    }

    /**
     * Gets the list of choirs that the user is a member of
     * @function
     */
    getChoirList() {
        // Updates state
        if (this._isMounted) this.setState({ isLoading: true });

        // Gets the choir list
        getUsersChoirs()
            .then((snapshot) => {
                // Updates state
                if (this._isMounted)
                    this.setState({
                        choirs: snapshot.data.choirs,
                        isLoading: false,
                    });
            })
            .catch((error) => {
                // Logs an error
                choirSelectionError(
                    error.response.status,
                    error.response.data,
                    "[ChoirSelection/getChoirList]"
                );

                // Shows an alert
                this.props.showAlert(
                    alertBarTypes.ERROR,
                    "Error",
                    error.response.data
                );

                // Updates state
                if (this._isMounted) this.setState({ isLoading: false });
            });
    }

    /**
     * Updates Redux with the selected choir id and choir name
     * Routes to the new url
     * @param {string} id - The selected choir id
     * @param {string} name - The selected choir name
     */
    choirClickedHandler = (id, name) => {
        let routeUrl; // The url to route to

        // Calls the correct dispatch function and sets the routeUrl depending on the routing prop value
        switch (this.props.routing) {
            case routingOptions.MUSIC_SELECTION:
                this.props.choirSelectedForPractice(id, name);
                routeUrl = `${this.props.match.url}/choirs/${id}`;
                break;
            case routingOptions.CHOIR_MEMBERS:
                this.props.choirSelectedForChoirs(id, name);
                routeUrl = `${this.props.match.url}/${id}`;
                break;
            default:
                console.log(
                    "Invalid routing option was given. See choirSelectionRoutingOptions.js"
                );
        }

        // Routes to the new url
        this.props.history.push(routeUrl);
    };

    /**
     * Attempts to join a new choir
     * @function
     */
    newChoirClickHandler = () => {
        // Asks the user to enter an access code
        const accessCode = prompt("Please enter the access code given to you");

        if (accessCode) {
            // Adds user to the list of pending members for the choir
            // An admin for the choir must verfiy this user before this user can access the choir
            joinChoir({ memberType: "student", memberRole: "x", accessCode })
                .then(() => {
                    // Show a success alert
                    this.props.showAlert(
                        alertBarTypes.SUCCESS,
                        "Hang Tight",
                        "Your request to join the choir has been sent. Please wait for a choir administrator to confirm your request."
                    );
                })
                .catch((error) => {
                    // Logs the error
                    choirSelectionError(
                        error.response.status,
                        error.response.data,
                        "[ChoirSelection/newChoirClickHandler]"
                    );

                    // Shows an alert
                    this.props.showAlert(
                        alertBarTypes.ERROR,
                        "Error",
                        error.response.data
                    );
                });
        }
    };

    // TODO: Add this functionality
    viewPendingRequestsClickHandler = () => {};

    /**
     * Creates a ChoirCard component for each choir.
     * Also creates a ChoirOptionCard for each choir option.
     * @returns - An array of choir card components
     */
    getChoirComponents = () => {
        const cards = this.getChoirCards();

        // Adds a card for adding a new choir
        cards.push(
            <ChoirOptionCard
                key={shortid.generate()}
                iconSrc={plusIcon}
                name="New Choir"
                cardColor={cardColorOptions.ORANGE}
                onClick={this.newChoirClickHandler}
            />
        );

        // Adds a card for viewing pending choir requests
        cards.push(
            <ChoirOptionCard
                key={shortid.generate()}
                iconSrc={questionIcon}
                name="View Pending Choir Requests"
                cardColor={cardColorOptions.TERTIARY_BLUE}
                onClick={this.viewPendingRequestsClickHandler}
            />
        );

        // Returns the array of choir cards
        return cards;
    };

    /**
     * Gets an array of ChoirCard components
     * @function
     * @returns {array} An array of ChoirCard components
     */
    getChoirCards = () => {
        // An array of color options
        const colorOptions = Object.values(cardColorOptions);

        // Index starts at -1 because it is incremented before its first use
        let colorIndex = -1;

        // The cards to return
        let cards = [];

        // Maps the choirs to cards
        if (this.state.choirs) {
            cards = this.state.choirs.map((choir) => {
                // Gets the next color
                colorIndex++;

                // >= is tested instead of > because colorIndex is incremented before this check
                if (colorIndex >= colorOptions.length) {
                    // Start back at the beginning of the colorOptions array
                    colorIndex = 0;
                }

                // Returns a ChoirCard to the map function
                return (
                    <ChoirCard
                        key={shortid.generate()}
                        headerImgSrc={choir.picture_url}
                        name={choir.choir_name}
                        description={choir.description}
                        cardColor={colorOptions[colorIndex]}
                        onClick={() =>
                            this.choirClickedHandler(
                                choir.choir_id,
                                choir.choir_name
                            )
                        }
                    />
                );
            });
        }

        return cards;
    };

    /**
     * Renders the ChoirSelection component
     */
    render() {
        // The component to display (loading or cards)
        let component;

        if (this.state.isLoading) {
            // Display a loading spinner
            component = <LoadingContainer message="Loading choirs..." />;
        } else {
            // Display the choir cards
            component = (
                <div className={styles.choirSelectionCards}>
                    {this.getChoirComponents()}
                </div>
            );
        }

        // Returns the JSX to render
        return (
            <div className={styles.choirSelection}>
                <PageHeader
                    heading="Choir Selection"
                    shouldDisplayBackButton={false}
                />
                {component}
            </div>
        );
    }
}

// Prop types for the ChoirSelection component
ChoirSelection.propTypes = {
    /**
     * Where to route when a choir card is clicked on.
     * see [options]{@link module:choirSelectionRoutingOptions}.
     */
    routing: PropTypes.oneOf([
        routingOptions.MUSIC_SELECTION,
        routingOptions.CHOIR_MEMBERS,
    ]).isRequired,

    /**
     * React Router history object.
     * This is provided by the withRouter function.
     */
    history: PropTypes.object.isRequired,

    /**
     * Shows an alert
     */
    showAlert: PropTypes.func.isRequired,

    /**
     * Updates Redux with choir data.
     * Used when selecting a choir to practice for.
     */
    choirSelectedForPractice: PropTypes.func.isRequired,

    /**
     * Updates Redux with choir data.
     * Used when selecting a choir to view members of.
     */
    choirSelectedForChoirs: PropTypes.func.isRequired,
};

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

export default withRouter(connect(null, mapDispatchToProps)(ChoirSelection));