Source

components/ChoirMembers/ChoirMembers.js

  1. // NPM module imports
  2. import React, { Component } from "react";
  3. import { connect } from "react-redux";
  4. import PropTypes from "prop-types";
  5. import shortid from "shortid";
  6. // Component imports
  7. import MemberCard from "./MemberCard/MemberCard";
  8. import PageHeader from "../PageHeader/PageHeader";
  9. import LoadingContainer from "../Spinners/LoadingContainer/LoadingContainer";
  10. // File imports
  11. import { getChoirMembers } from "../../vendors/AWS/tmaApi";
  12. import firebase from "../../vendors/Firebase/firebase";
  13. import * as memberColorOptions from "./MemberCard/memberCardColorOptions";
  14. import * as alertBarTypes from "../AlertBar/alertBarTypes";
  15. import { choirMembersError } from "../../vendors/Firebase/logs";
  16. // Style imports
  17. import styles from "./ChoirMembers.module.scss";
  18. /**
  19. * Renders the ChoirMembers component.
  20. * Shows all choir member profiles.
  21. * @extends {Component}
  22. * @component
  23. * @category ChoirMembers
  24. * @author Dan Levy <danlevy124@gmail.com>
  25. */
  26. class ChoirMembers extends Component {
  27. /**
  28. * ChoirMembers component state
  29. * @property {boolean} isLoading - Indicates if the component is in a loading state
  30. * @property {array} members - A list of choir members
  31. */
  32. state = {
  33. isLoading: true,
  34. members: null,
  35. };
  36. /**
  37. * Indicates if the component is mounted.
  38. * Used for asynchronous tasks.
  39. * @see https://reactjs.org/blog/2015/12/16/ismounted-antipattern.html
  40. */
  41. _isMounted = false;
  42. /**
  43. * Sets _isMounted to true
  44. * Gets the choir members
  45. */
  46. componentDidMount() {
  47. this._isMounted = true;
  48. // Gets the choir members
  49. this.getMembers();
  50. }
  51. /**
  52. * Sets _isMounted to false
  53. */
  54. componentWillUnmount() {
  55. this._isMounted = false;
  56. }
  57. /**
  58. * Gets the choir members from the server.
  59. * Updates state with the members.
  60. * @function
  61. */
  62. getMembers = () => {
  63. getChoirMembers({ choirId: this.props.choirId })
  64. .then((response) => {
  65. // Updates state
  66. if (this._isMounted)
  67. this.setState({ isLoading: false, members: response.data });
  68. // Gets each member's profile picture (this is async and will update the correct card when the picture comes in)
  69. this.getMembersProfilePictures();
  70. })
  71. .catch((error) => {
  72. // Logs an error
  73. choirMembersError(
  74. error.response.status,
  75. error.response.data,
  76. "[ChoirMembers/getMembers]"
  77. );
  78. // Shows an error alert
  79. this.props.showAlert(
  80. alertBarTypes.ERROR,
  81. "Error",
  82. error.response.data
  83. );
  84. // Updates state
  85. if (this._isMounted) this.setState({ isLoading: false });
  86. });
  87. };
  88. /**
  89. * Gets each member's profile picture url.
  90. * Updates state with the url.
  91. * @function
  92. */
  93. getMembersProfilePictures = () => {
  94. if (this.state.members) {
  95. for (let i = 0; i < this.state.members.length; i++) {
  96. // Get the member (deep copy)
  97. const member = { ...this.state.members[i] };
  98. if (member.has_picture) {
  99. // Gets the member's picture url from Firebase storage
  100. firebase
  101. .storage()
  102. .ref()
  103. .child(
  104. `users/${member.person_id}/profile_picture_200x200`
  105. )
  106. .getDownloadURL()
  107. .then((url) => {
  108. // Updates state with the url
  109. member.picture_url = url;
  110. const members = [...this.state.members];
  111. members[i] = member;
  112. this.setState({ members });
  113. });
  114. }
  115. }
  116. }
  117. };
  118. /**
  119. * Gets an array of admin member cards and an array of student member cards
  120. * @function
  121. * @returns {object} An object containing an array of admin MemberCard components and an array of student MemberCard components
  122. */
  123. getMemberCards = () => {
  124. const admins = [];
  125. const students = [];
  126. if (this.state.members) {
  127. // Creates a member card for each member of the choir
  128. for (let i = 0; i < this.state.members.length; i++) {
  129. // Gets the current member
  130. const member = this.state.members[i];
  131. if (member.member_type === "admin") {
  132. // Creates an admin member card
  133. admins.push(
  134. this.createMemberCard(member, memberColorOptions.GREEN)
  135. );
  136. } else {
  137. // Creates a student member card
  138. students.push(
  139. this.createMemberCard(member, memberColorOptions.ORANGE)
  140. );
  141. }
  142. }
  143. }
  144. return { admins, students };
  145. };
  146. /**
  147. * Creates a member card component
  148. * @function
  149. * @param {object} member - The member data used to create the member card
  150. * @param {string} color - The color to use for the card
  151. * @returns {object} A MemberCard component
  152. */
  153. createMemberCard(member, color) {
  154. return (
  155. <MemberCard
  156. key={shortid.generate()}
  157. name={`${member.first_name} ${member.last_name}`}
  158. roles={member.member_role}
  159. profilePictureSrc={member.picture_url}
  160. color={color}
  161. />
  162. );
  163. }
  164. /**
  165. * Renders the ChoirMembers component
  166. */
  167. render() {
  168. const { admins, students } = this.getMemberCards();
  169. // The component to display (loading or cards)
  170. let component;
  171. if (this.state.isLoading) {
  172. // Display a loading spinner
  173. component = <LoadingContainer message="Loading members..." />;
  174. } else {
  175. // Display the choir cards
  176. component = (
  177. <div>
  178. <h1 className={styles.choirMembersMemberGroupHeading}>
  179. Administrators
  180. </h1>
  181. <div className={styles.choirMembersCards}>{admins}</div>
  182. <h1 className={styles.choirMembersMemberGroupHeading}>
  183. Students
  184. </h1>
  185. <div className={styles.choirMembersCards}>{students}</div>
  186. </div>
  187. );
  188. }
  189. // Returns the JSX to render
  190. return (
  191. <div className={styles.choirMembers}>
  192. <PageHeader
  193. heading={`${this.props.choirName} Members`}
  194. shouldDisplayBackButton={true}
  195. backButtonTitle={"Choir Selection"}
  196. />
  197. {component}
  198. </div>
  199. );
  200. }
  201. }
  202. // Prop types for the ChoirMembers component
  203. ChoirMembers.propTypes = {
  204. /**
  205. * The id of the selected choir
  206. */
  207. choirId: PropTypes.string.isRequired,
  208. /**
  209. * The name of the selected choir
  210. */
  211. choirName: PropTypes.string.isRequired,
  212. /**
  213. * Shows an alert
  214. */
  215. showAlert: PropTypes.func.isRequired,
  216. };
  217. /**
  218. * Gets the current state from Redux and passes parts of it to the ChoirMembers component as props.
  219. * This function is used only by the react-redux connect function.
  220. * @memberof ChoirMembers
  221. * @param {object} state - The Redux state
  222. * @returns {object} Redux state properties used in the ChoirMembers component
  223. */
  224. const mapStateToProps = (state) => {
  225. return {
  226. choirId: state.choirs.selectedChoirId,
  227. choirName: state.choirs.selectedChoirName,
  228. };
  229. };
  230. export default connect(mapStateToProps)(ChoirMembers);