Source

components/MusicSelection/MusicSelection.js

  1. // NPM module imports
  2. import React, { Component } from "react";
  3. import PropTypes from "prop-types";
  4. import { connect } from "react-redux";
  5. import { withRouter } from "react-router-dom";
  6. import shortid from "shortid";
  7. // Component imports
  8. import PageHeader from "../PageHeader/PageHeader";
  9. import MusicCard from "./MusicCard/MusicCard";
  10. import LoadingContainer from "../Spinners/LoadingContainer/LoadingContainer";
  11. // File imports
  12. import { getSheetMusic, doesUserGetFeedback } from "../../vendors/AWS/tmaApi";
  13. import * as alertBarTypes from "../AlertBar/alertBarTypes";
  14. import { musicSelectionError } from "../../vendors/Firebase/logs";
  15. import {
  16. musicSelectedForPractice,
  17. setUserGetsFeedback,
  18. } from "../../store/actions/index";
  19. import * as cardColorOptions from "./MusicCard/musicCardColorOptions";
  20. // Style imports
  21. import styles from "./MusicSelection.module.scss";
  22. /**
  23. * Renders the MusicSelection component
  24. * @extends {Component}
  25. * @component
  26. * @category MusicSelection
  27. * @author Dan Levy <danlevy124@gmail.com>
  28. */
  29. class MusicSelection extends Component {
  30. /**
  31. * MusicSelection component state
  32. * @property {boolean} isLoading - Indicates if the component is in a loading state
  33. * @property {array} musicList - An array of sheet music associated with the selected choir
  34. */
  35. state = {
  36. isLoading: true,
  37. musicList: null,
  38. };
  39. /**
  40. * Indicates if the component is mounted.
  41. * Used for asynchronous tasks.
  42. * @see https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
  43. * @type {boolean}
  44. */
  45. _isMounted = false;
  46. /**
  47. * Sets _isMounted to true
  48. * Gets the list of music
  49. */
  50. componentDidMount() {
  51. this._isMounted = true;
  52. this.getMusicList();
  53. }
  54. /**
  55. * Sets _isMounted to false
  56. */
  57. componentWillUnmount() {
  58. this._isMounted = false;
  59. }
  60. /**
  61. * Gets the list of music associated with the selected choir.
  62. * Updates state with the music list.
  63. * @function
  64. */
  65. getMusicList = () => {
  66. // Starts loading
  67. if (this._isMounted) this.setState({ isLoading: true });
  68. // Gets the list of music
  69. getSheetMusic({ choirId: this.props.choirId })
  70. .then((snapshot) => {
  71. // Updates state with the music list
  72. if (this._isMounted)
  73. this.setState({
  74. musicList: snapshot.data.sheet_music,
  75. });
  76. })
  77. .then(
  78. doesUserGetFeedback.bind(this, { choirId: this.props.choirId })
  79. )
  80. .then((snapshot) => {
  81. this.props.setUserGetsFeedback(snapshot.data.gets_feedback);
  82. if (this._isMounted) this.setState({ isLoading: false });
  83. })
  84. .catch((error) => {
  85. // Logs an error
  86. musicSelectionError(
  87. error.response.status,
  88. error.response.data,
  89. "[MusicSelection/getMusicList]"
  90. );
  91. // Shows an error alert
  92. this.props.showAlert(
  93. alertBarTypes.ERROR,
  94. "Error",
  95. error.response.data
  96. );
  97. // Updates state
  98. if (this._isMounted) this.setState({ isLoading: false });
  99. });
  100. };
  101. /**
  102. * If the browser is not a mobile browser,
  103. * (1) Updates Redux with the selected piece of music, and
  104. * (2) Routes to the practice music page
  105. * @param {string} id - The id of the selected piece of music
  106. * @function
  107. */
  108. viewSongClickedHandler = (id) => {
  109. if (this.props.isMobileBrowser) {
  110. // Shows an alert
  111. this.showMobileBrowserAlert();
  112. } else {
  113. // Updates Redux and routes to the correct page
  114. this.props.musicSelected(id);
  115. this.props.history.push(
  116. `${this.props.match.url}/music/${id}/practice`
  117. );
  118. }
  119. };
  120. /**
  121. * If the browser is not a mobile browser,
  122. * (1) Updates Redux with the selected piece of music, and
  123. * (2) Routes to the music performance page
  124. * @param {string} id - The id of the selected piece of music
  125. * @function
  126. */
  127. viewPerformanceClickedHandler = (id) => {
  128. if (this.props.isMobileBrowser) {
  129. // Shows an alert
  130. this.showMobileBrowserAlert();
  131. } else {
  132. // Updates Redux and routes to the correct page
  133. this.props.musicSelected(id);
  134. this.props.history.push(
  135. `${this.props.match.url}/music/${id}/performance`
  136. );
  137. }
  138. };
  139. /**
  140. * Shows an alert indicating that the user cannot access the selected page on a mobile browser
  141. * @function
  142. */
  143. showMobileBrowserAlert = () => {
  144. this.props.showAlert(
  145. alertBarTypes.WARNING,
  146. "We're Sorry",
  147. "You can't view or play music on this mobile device due to processing limitations"
  148. );
  149. };
  150. /**
  151. * Gets an array of MusicCard components
  152. * @function
  153. * @returns {array} An array of MusicCard components
  154. */
  155. getMusicCards = () => {
  156. // An array of color options
  157. const colorOptions = Object.values(cardColorOptions);
  158. // Index starts at -1 because it is incremented before its first use
  159. let colorIndex = -1;
  160. // Returns an array of MusicCard components
  161. return this.state.musicList.map((musicPiece) => {
  162. // Gets the next color
  163. colorIndex++;
  164. // >= is tested instead of > because colorIndex is incremented before this check
  165. if (colorIndex >= colorOptions.length) {
  166. // Start back at the beginning of the colorOptions array
  167. colorIndex = 0;
  168. }
  169. // Returns a MusicCard to the map function
  170. return (
  171. <MusicCard
  172. key={shortid.generate()}
  173. title={musicPiece.title}
  174. composers={musicPiece.composer_names}
  175. cardColor={colorOptions[colorIndex]}
  176. onViewSongClick={() =>
  177. this.viewSongClickedHandler(musicPiece.sheet_music_id)
  178. }
  179. onViewPerformanceClick={() =>
  180. this.viewPerformanceClickedHandler(
  181. musicPiece.sheet_music_id
  182. )
  183. }
  184. shouldShowViewPerformancesButton={this.props.doesUserGetFeedback}
  185. />
  186. );
  187. });
  188. };
  189. /**
  190. * Renders the MusicSelection component
  191. */
  192. render() {
  193. // The component to display (loading or cards)
  194. let component;
  195. if (this.state.isLoading) {
  196. // Display a loading spinner
  197. component = <LoadingContainer message="Loading music..." />;
  198. } else {
  199. // Display the music cards
  200. component = (
  201. <div className={styles.musicSelectionCards}>
  202. {this.getMusicCards()}
  203. </div>
  204. );
  205. }
  206. // Returns the JSX to render
  207. return (
  208. <div className={styles.musicSelection}>
  209. <PageHeader
  210. heading={`${this.props.choirName} - Music`}
  211. shouldDisplayBackButton={true}
  212. backButtonTitle={"Choir Selection"}
  213. />
  214. {component}
  215. </div>
  216. );
  217. }
  218. }
  219. // Prop types for the MusicSelection component
  220. MusicSelection.propTypes = {
  221. /**
  222. * The id of the selected choir
  223. */
  224. choirId: PropTypes.string.isRequired,
  225. /**
  226. * The name of the selected choir
  227. */
  228. choirName: PropTypes.string.isRequired,
  229. /**
  230. * Indicates if the browser is a mobile browser
  231. */
  232. isMobileBrowser: PropTypes.bool.isRequired,
  233. /**
  234. * React Router history object.
  235. * This is provided by the withRouter function.
  236. */
  237. history: PropTypes.object.isRequired,
  238. /**
  239. * Indicates if the user gets feedback
  240. */
  241. doesUserGetFeedback: PropTypes.bool,
  242. /**
  243. * React Router match object.
  244. * This is provided by the withRouter function.
  245. */
  246. match: PropTypes.object.isRequired,
  247. /**
  248. * Shows an alert
  249. */
  250. showAlert: PropTypes.func.isRequired,
  251. /**
  252. * Updates Redux with music data
  253. */
  254. musicSelected: PropTypes.func.isRequired,
  255. /**
  256. * Sets if the user gets feedback
  257. */
  258. setUserGetsFeedback: PropTypes.func.isRequired,
  259. };
  260. /**
  261. * Gets the current state from Redux and passes parts of it to the MusicSelection component as props.
  262. * This function is used only by the react-redux connect function.
  263. * @memberof MusicSelection
  264. * @param {object} state - The Redux state
  265. * @returns {object} Redux state properties used in the MusicSelection component
  266. */
  267. const mapStateToProps = (state) => {
  268. return {
  269. choirId: state.practice.selectedChoirId,
  270. choirName: state.practice.selectedChoirName,
  271. isMobileBrowser: state.app.isMobileBrowser,
  272. doesUserGetFeedback: state.practice.doesUserGetFeedback,
  273. };
  274. };
  275. /**
  276. * Passes certain Redux actions to the MusicSelection component as props.
  277. * This function is used only by the react-redux connect function.
  278. * @memberof MusicSelection
  279. * @param {function} dispatch - The react-redux dispatch function
  280. * @returns {object} Redux actions used in the MusicSelection component
  281. */
  282. const mapDispatchToProps = (dispatch) => {
  283. return {
  284. musicSelected: (id) => dispatch(musicSelectedForPractice(id)),
  285. setUserGetsFeedback: (doesUserGetFeedback) =>
  286. dispatch(setUserGetsFeedback(doesUserGetFeedback)),
  287. };
  288. };
  289. export default withRouter(
  290. connect(mapStateToProps, mapDispatchToProps)(MusicSelection)
  291. );