Source

components/MusicSelection/MusicSelection.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 PageHeader from "../PageHeader/PageHeader";
import MusicCard from "./MusicCard/MusicCard";
import LoadingContainer from "../Spinners/LoadingContainer/LoadingContainer";

// File imports
import { getSheetMusic, doesUserGetFeedback } from "../../vendors/AWS/tmaApi";
import * as alertBarTypes from "../AlertBar/alertBarTypes";
import { musicSelectionError } from "../../vendors/Firebase/logs";
import {
    musicSelectedForPractice,
    setUserGetsFeedback,
} from "../../store/actions/index";
import * as cardColorOptions from "./MusicCard/musicCardColorOptions";

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

/**
 * Renders the MusicSelection component
 * @extends {Component}
 * @component
 * @category MusicSelection
 * @author Dan Levy <danlevy124@gmail.com>
 */
class MusicSelection extends Component {
    /**
     * MusicSelection component state
     * @property {boolean} isLoading - Indicates if the component is in a loading state
     * @property {array} musicList - An array of sheet music associated with the selected choir
     */
    state = {
        isLoading: true,
        musicList: null,
    };

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

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

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

    /**
     * Gets the list of music associated with the selected choir.
     * Updates state with the music list.
     * @function
     */
    getMusicList = () => {
        // Starts loading
        if (this._isMounted) this.setState({ isLoading: true });

        // Gets the list of music
        getSheetMusic({ choirId: this.props.choirId })
            .then((snapshot) => {
                // Updates state with the music list
                if (this._isMounted)
                    this.setState({
                        musicList: snapshot.data.sheet_music,
                    });
            })
            .then(
                doesUserGetFeedback.bind(this, { choirId: this.props.choirId })
            )
            .then((snapshot) => {
                this.props.setUserGetsFeedback(snapshot.data.gets_feedback);
                if (this._isMounted) this.setState({ isLoading: false });
            })
            .catch((error) => {
                // Logs an error
                musicSelectionError(
                    error.response.status,
                    error.response.data,
                    "[MusicSelection/getMusicList]"
                );

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

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

    /**
     * If the browser is not a mobile browser,
     * (1) Updates Redux with the selected piece of music, and
     * (2) Routes to the practice music page
     * @param {string} id - The id of the selected piece of music
     * @function
     */
    viewSongClickedHandler = (id) => {
        if (this.props.isMobileBrowser) {
            // Shows an alert
            this.showMobileBrowserAlert();
        } else {
            // Updates Redux and routes to the correct page
            this.props.musicSelected(id);
            this.props.history.push(
                `${this.props.match.url}/music/${id}/practice`
            );
        }
    };

    /**
     * If the browser is not a mobile browser,
     * (1) Updates Redux with the selected piece of music, and
     * (2) Routes to the music performance page
     * @param {string} id - The id of the selected piece of music
     * @function
     */
    viewPerformanceClickedHandler = (id) => {
        if (this.props.isMobileBrowser) {
            // Shows an alert
            this.showMobileBrowserAlert();
        } else {
            // Updates Redux and routes to the correct page
            this.props.musicSelected(id);
            this.props.history.push(
                `${this.props.match.url}/music/${id}/performance`
            );
        }
    };

    /**
     * Shows an alert indicating that the user cannot access the selected page on a mobile browser
     * @function
     */
    showMobileBrowserAlert = () => {
        this.props.showAlert(
            alertBarTypes.WARNING,
            "We're Sorry",
            "You can't view or play music on this mobile device due to processing limitations"
        );
    };

    /**
     * Gets an array of MusicCard components
     * @function
     * @returns {array} An array of MusicCard components
     */
    getMusicCards = () => {
        // 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;

        // Returns an array of MusicCard components
        return this.state.musicList.map((musicPiece) => {
            // 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 MusicCard to the map function
            return (
                <MusicCard
                    key={shortid.generate()}
                    title={musicPiece.title}
                    composers={musicPiece.composer_names}
                    cardColor={colorOptions[colorIndex]}
                    onViewSongClick={() =>
                        this.viewSongClickedHandler(musicPiece.sheet_music_id)
                    }
                    onViewPerformanceClick={() =>
                        this.viewPerformanceClickedHandler(
                            musicPiece.sheet_music_id
                        )
                    }
                    shouldShowViewPerformancesButton={this.props.doesUserGetFeedback}
                />
            );
        });
    };

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

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

        // Returns the JSX to render
        return (
            <div className={styles.musicSelection}>
                <PageHeader
                    heading={`${this.props.choirName} - Music`}
                    shouldDisplayBackButton={true}
                    backButtonTitle={"Choir Selection"}
                />
                {component}
            </div>
        );
    }
}

// Prop types for the MusicSelection component
MusicSelection.propTypes = {
    /**
     * The id of the selected choir
     */
    choirId: PropTypes.string.isRequired,

    /**
     * The name of the selected choir
     */
    choirName: PropTypes.string.isRequired,

    /**
     * Indicates if the browser is a mobile browser
     */
    isMobileBrowser: PropTypes.bool.isRequired,

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

    /**
     * Indicates if the user gets feedback
     */
    doesUserGetFeedback: PropTypes.bool,

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

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

    /**
     * Updates Redux with music data
     */
    musicSelected: PropTypes.func.isRequired,

    /**
     * Sets if the user gets feedback
     */
    setUserGetsFeedback: PropTypes.func.isRequired,
};

/**
 * Gets the current state from Redux and passes parts of it to the MusicSelection component as props.
 * This function is used only by the react-redux connect function.
 * @memberof MusicSelection
 * @param {object} state - The Redux state
 * @returns {object} Redux state properties used in the MusicSelection component
 */
const mapStateToProps = (state) => {
    return {
        choirId: state.practice.selectedChoirId,
        choirName: state.practice.selectedChoirName,
        isMobileBrowser: state.app.isMobileBrowser,
        doesUserGetFeedback: state.practice.doesUserGetFeedback,
    };
};

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

export default withRouter(
    connect(mapStateToProps, mapDispatchToProps)(MusicSelection)
);