diff --git a/src/client/PrivateUserRoute.tsx b/src/client/PrivateUserRoute.tsx index 5d9d38ed..5cc9f52f 100644 --- a/src/client/PrivateUserRoute.tsx +++ b/src/client/PrivateUserRoute.tsx @@ -5,7 +5,12 @@ import { Route, Redirect, RouteProps } from 'react-router-dom'; import { ApplicationState } from './reducers'; interface StateProps { + + // Is the current user authenticated. authenticated: boolean; + + // Is there an authentication process in progress - this is used to prevent + // race conditions. authFinished: boolean; } @@ -16,6 +21,9 @@ interface PrivateUserRouteProps { type Props = RouteProps & StateProps & PrivateUserRouteProps; +/** + * Lock down a view by authentication state. + */ class PrivateUserRoute extends React.Component { render() { return ( diff --git a/src/client/README.md b/src/client/README.md new file mode 100644 index 00000000..e9e90964 --- /dev/null +++ b/src/client/README.md @@ -0,0 +1,40 @@ +## src/client - Frontend + +This directory is the home for the frontend for tesc.events. + +tesc.events' frontend is written entirely in [React.js](https://reactjs.org/). It was initially written in JavaScript, but [was ported over to TypeScript in April 2019](https://github.com/UCSDTESC/Check-in/pull/131) + +The entry point to the code is in [main.tsx](https://github.com/UCSDTESC/Check-in/blob/master/src/client/main.tsx) + + +### Directory Tree +``` +├── PrivateRoute.tsx + * Wrapper Component around react-router-dom's Route to handle rendering only if the user requesting the page is authenticated. Used on admin-side routes. +├── PrivateUserRoute.tsx + * Wrapper Component around react-router-dom's Route to handle rendering only if the user requesting the page is authenticated. Used on user-side routes. +├── README.md + * 😊 +├── actions + * This directory holds application-level Redux Actions. +├── auth + * This directory holds the components, Redux Actions and Redux Reducers related to the login/logout functionality of both users and admins. +├── components + * Components that are required "globally" or in multiple places in the application are put here. Things like the Navbar, Footer, iOS Switch, Loading spinners etc. go here. +├── data + * The `data/` directory holds `Api.ts` and `User.ts`, which provide the application with methods to make API calls to our backend. +├── layouts + * A Layout defines the way our application looks. The application has different layouts depending on what kind of page you are looking at - admins and sponsors have sidebars, and users dont. Layouts are linked to a specific route in the `routes.tsx` file. +├── main.tsx + * This is the entrypoint to our application. It sets up our app to be used with Redux, react-router and Cookies and makes the intial React.Component.render() call. +├── pages + * Each `XYZPage` in the `pages` directory is linked to a specific page of the app. Each page is it’s own directory with an `index.tsx` file in it that defines that page. Each page can also define Redux Actions, Reducers and Components that it will use in `XYZPage/actions`, `XYZPage/reducers` and `XYZPage/components +├── reducers + * This directory holds application-level Redux Reducers. +├── routes.tsx + * This directory defines react-router-dom's routes for the application. +├── static + * This directory holds "constant" data that the application needs - a list of universities, majors and so on. +└── typings + * TypeScript type definitions for JavaScript packages used in this application that are directly supplied by us - not by node_modules +``` \ No newline at end of file diff --git a/src/client/components/EventForm.tsx b/src/client/components/EventForm.tsx index 9ee7c86e..81b840a5 100644 --- a/src/client/components/EventForm.tsx +++ b/src/client/components/EventForm.tsx @@ -21,10 +21,19 @@ interface EventFormProps { editing?: boolean; } +// The props of this component are the props returned by the redux-form HOC and it's native props type Props = InjectedFormProps & EventFormProps; +/** + * This is the redux-form to create a new event + */ class EventForm extends React.Component { + /** + * Create a file droppable field for the event logo + * + * @returns {Component} + */ createLogoUpload() { return ( { ); } + /** + * Show an alert that flags the event as a non-TESC hosted event. + * + * @returns {React.StatelessComponent} + */ showThirdPartyText: React.StatelessComponent = ({values}) => { if (values && values.organisedBy && values.organisedBy.input.value !== 'TESC') { return ( diff --git a/src/client/components/Fields.tsx b/src/client/components/Fields.tsx index 83b90f0f..07bddf5c 100644 --- a/src/client/components/Fields.tsx +++ b/src/client/components/Fields.tsx @@ -1,4 +1,8 @@ -import { UserDiversityOptions, UserYearOptions, UserGenderOptions, UserPronounOptions, UserShirtSizeOptions } from '@Shared/UserEnums'; +import { UserDiversityOptions, + UserYearOptions, + UserGenderOptions, + UserPronounOptions, + UserShirtSizeOptions } from '@Shared/UserEnums'; import React from 'react'; import { Field, WrappedFieldProps } from 'redux-form'; import majors from '~/static/Majors.json'; @@ -48,7 +52,7 @@ export function errorClass(className: string, touched: boolean, error: boolean) } export const errorTextInput: React.StatelessComponent = ({ input, className, placeholder, type, - meta: { touched, error } }) => { + meta: { touched, error } }) => { const errorClassName = errorClass(className, touched, error); return (
@@ -81,7 +85,7 @@ export const errorRadio: React.StatelessComponent = ({ input, }; export const errorTextArea: React.StatelessComponent = ({ input, className, placeholder, maxLength, - meta: { touched, error } }) => { + meta: { touched, error } }) => { const errorClassName = errorClass(className, touched, error); return (
@@ -122,7 +126,7 @@ export const errorMonthPicker: React.StatelessComponent = ({ i }; export const errorTShirtSizePicker: React.StatelessComponent = ({ input, className, type, - meta: { touched, error } }) => { + meta: { touched, error } }) => { const errorClassName = errorClass(className, touched, error); const sizes = Object.values(UserShirtSizeOptions); const values = Object.keys(UserShirtSizeOptions); @@ -143,7 +147,7 @@ export const errorTShirtSizePicker: React.StatelessComponent = }; export const errorGenderPicker: React.StatelessComponent = ({ input, className, type, - meta: { touched, error } }) => { + meta: { touched, error } }) => { const errorClassName = errorClass(className, touched, error); return ( @@ -162,7 +166,7 @@ export const errorGenderPicker: React.StatelessComponent = ({ }; export const errorPronounPicker: React.StatelessComponent = ({ input, className, type, - meta: { touched, error } }) => { + meta: { touched, error } }) => { const errorClassName = errorClass(className, touched, error); return ( @@ -181,7 +185,7 @@ export const errorPronounPicker: React.StatelessComponent = ({ }; export const errorDiversityOptions: React.StatelessComponent = ({ input, className, type, - meta: { touched, error } }) => { + meta: { touched, error } }) => { const errorClassName = errorClass(className, touched, error); return ( @@ -200,7 +204,7 @@ export const errorDiversityOptions: React.StatelessComponent = }; export const errorYearPicker: React.StatelessComponent = ({ input, className, type, - meta: { touched, error } }) => { + meta: { touched, error } }) => { const errorClassName = errorClass(className, touched, error); return ( @@ -219,7 +223,7 @@ export const errorYearPicker: React.StatelessComponent = ({ in }; export const errorMajorPicker: React.StatelessComponent = ({ input, className, type, - meta: { touched, error } }) => { + meta: { touched, error } }) => { const errorClassName = errorClass(className, touched, error); return ( diff --git a/src/client/components/Hero.tsx b/src/client/components/Hero.tsx index 4b9d6864..b67039a3 100644 --- a/src/client/components/Hero.tsx +++ b/src/client/components/Hero.tsx @@ -22,8 +22,7 @@ export default class Hero extends React.Component { } return ( -
-
+
); } } diff --git a/src/client/components/User.tsx b/src/client/components/User.tsx index c397de05..9db922b7 100644 --- a/src/client/components/User.tsx +++ b/src/client/components/User.tsx @@ -1,4 +1,5 @@ import { TESCUser, TESCEvent, Question } from '@Shared/ModelTypes'; +import { generateQRCodeURL } from '@Shared/QRCodes'; import { QuestionType } from '@Shared/Questions'; import { getRoleRank, Role } from '@Shared/Roles'; import { isAcceptableStatus, isRejectableStatus, isWaitlistableStatus, UserStatus } from '@Shared/UserStatus'; @@ -9,7 +10,6 @@ import { connect } from 'react-redux'; import { Field, reduxForm, InjectedFormProps } from 'redux-form'; import { sendAcceptanceEmail, sendRejectionEmail, sendWaitlistEmail } from '~/data/AdminApi'; import { ApplicationState } from '~/reducers'; -import { generateQRCodeURL } from '@Shared/QRCodes' import { AlertType } from '../pages/AlertPage'; @@ -114,7 +114,7 @@ class User extends React.Component { className="form-control" component="select" type={fieldType}> - + )}
@@ -343,7 +343,7 @@ class User extends React.Component { {this.renderFormCheckbox('Bussing', 'bussing', 'col-sm-4')} {this.renderFormCheckbox('Sanitized', 'sanitized', 'col-sm-4')} - {this.renderFormDropdown('Status', 'status', + {this.renderFormDropdown('Status', 'status', Object.values(UserStatus), 'col-sm-4')}
diff --git a/src/client/data/AdminApi.ts b/src/client/data/AdminApi.ts index a2e31cec..802b5a98 100644 --- a/src/client/data/AdminApi.ts +++ b/src/client/data/AdminApi.ts @@ -23,8 +23,8 @@ import request, { SuperAgentRequest } from 'superagent'; import nocache from 'superagent-no-cache'; import pref from 'superagent-prefix'; import Cookies from 'universal-cookie'; -import { NewAdminModalFormData } from '~/components/NewAdminModal'; import { EventFormData } from '~/components/EventForm'; +import { NewAdminModalFormData } from '~/components/NewAdminModal'; import CookieTypes from '~/static/Cookies'; import { promisify } from './helpers'; @@ -195,7 +195,6 @@ export const editExistingEvent = (id: string, event: Partial) => closeTimeDay )).toISOString(true); - const promiseReq = request .post(`/events/edit/${id}`) .set('Authorization', cookies.get(CookieTypes.admin.token)) @@ -204,14 +203,14 @@ export const editExistingEvent = (id: string, event: Partial) => closeTime, } as RegisterEventRequest)) .use(adminApiPrefix) - .use(nocache) + .use(nocache); - if (logo) { - promiseReq.attach('logo', logo[0]) + if (logo) { + promiseReq.attach('logo', logo[0]); } - return promisify(promiseReq); -} + return promisify(promiseReq); +}; /** * Request to register a new admin. @@ -250,7 +249,7 @@ export const downloadResumes = (applicants: string[]): SuperAgentRequest => .post('/resumes') .send({ applicants } as DownloadResumesRequest) .set('Authorization', cookies.get(CookieTypes.admin.token)) - .use(adminApiPrefix) + .use(adminApiPrefix); /** * Requests the status of an ongoing download. @@ -450,4 +449,4 @@ export const sendWaitlistEmail = (user: TESCUser) => .send({ user } as StatusEmailRequest) .use(adminApiPrefix) .use(nocache) - ); \ No newline at end of file + ); diff --git a/src/client/layouts/public.tsx b/src/client/layouts/public.tsx index f399e131..f75a2319 100644 --- a/src/client/layouts/public.tsx +++ b/src/client/layouts/public.tsx @@ -12,4 +12,4 @@ class PublicLayout extends React.Component { } } -export default PublicLayout; \ No newline at end of file +export default PublicLayout; diff --git a/src/client/layouts/sponsor.tsx b/src/client/layouts/sponsor.tsx index 85d51ca5..7d796eb3 100644 --- a/src/client/layouts/sponsor.tsx +++ b/src/client/layouts/sponsor.tsx @@ -90,7 +90,7 @@ class SponsorLayout extends React.Component { const blob = new Blob([res.text], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); - + link.href = url; link.setAttribute('download', `${this.props.user.username}-${Date.now()}.csv`); document.body.appendChild(link); @@ -98,7 +98,7 @@ class SponsorLayout extends React.Component { hideLoading(); this.setState({ - isDownloading: false + isDownloading: false, }); }) .catch(console.error); diff --git a/src/client/main.tsx b/src/client/main.tsx index 17f91144..a110335e 100644 --- a/src/client/main.tsx +++ b/src/client/main.tsx @@ -22,6 +22,7 @@ const store = createStore(reducer, applyMiddleware(reduxThunk as ThunkMiddleware) )); +// Configuring the application for routing, cookies, and redux. render( ( diff --git a/src/client/pages/AdminsPage/actions/index.ts b/src/client/pages/AdminsPage/actions/index.ts index 10dc9065..ad5d28ae 100644 --- a/src/client/pages/AdminsPage/actions/index.ts +++ b/src/client/pages/AdminsPage/actions/index.ts @@ -3,6 +3,8 @@ import { createStandardAction } from 'typesafe-actions'; import * as Types from './types'; -// Admins +// TODO: Not sure what this action does export const addAdmins = createStandardAction(Types.ADD_ADMINS)(); + +// Replace the `admins` state with the payload export const replaceAdmins = createStandardAction(Types.REPLACE_ADMINS)(); diff --git a/src/client/pages/AdminsPage/components/AdminList.tsx b/src/client/pages/AdminsPage/components/AdminList.tsx index 31370393..e8a39b9e 100644 --- a/src/client/pages/AdminsPage/components/AdminList.tsx +++ b/src/client/pages/AdminsPage/components/AdminList.tsx @@ -4,8 +4,15 @@ import React from 'react'; import { Button } from 'reactstrap'; interface AdminListProps { + + // The admins in the system admins: Admin[]; + + // Function to be called when the delete button is pressed onDeleteAdmin: (adminId: string) => void; + + // Is the admin in editing mode + // TODO: remove - legacy feature editing: boolean; } @@ -19,12 +26,15 @@ interface AdminListState { columns: DisplayColumnMap; } +/** + * This component renders a list of admins on the admins page + */ export default class AdminList extends React.Component { state: Readonly = { columns: { username: 'Username', role: 'Role', - lastAccessed: 'Last Accessed' + lastAccessed: 'Last Accessed', }, }; @@ -46,11 +56,11 @@ export default class AdminList extends React.Component { const {columns} = this.state; - const adminToBeRendered = {...admin, - lastAccessed: admin.lastAccessed - ? moment(admin.lastAccessed).fromNow() - : 'Never Logged In' - } + const adminToBeRendered = {...admin, + lastAccessed: admin.lastAccessed + ? moment(admin.lastAccessed).fromNow() + : 'Never Logged In', + }; return Object.keys(columns).map(column => ( {/* @@ -58,7 +68,7 @@ export default class AdminList extends React.Component) - ) + ); } render() { diff --git a/src/client/pages/AdminsPage/index.tsx b/src/client/pages/AdminsPage/index.tsx index db6b392d..c36a76c0 100644 --- a/src/client/pages/AdminsPage/index.tsx +++ b/src/client/pages/AdminsPage/index.tsx @@ -24,17 +24,35 @@ const mapDispatchToProps = (dispatch: ApplicationDispatch) => bindActionCreators interface AdminsPageProps { } +/** + * This component receives props in 3 ways - + * 1) The explicit props provied to it by AdminsPageProps + * 2) The redux state provided to it by mapStateToProps + * 3) The dispatch functions provided to it by mapDispatchToProps + * + * So, the props of this component is the union of the return types of mapStateToProps, + * mapDispatchToProps and AdminsPageProps + */ type Props = ReturnType & ReturnType & AdminsPageProps; interface AdminsPageState { + + // Tracks whether the New Admin Modal is open or not isRegisterModalOpen: boolean; } +/** + * This pageshows a list of all admins in the system. It also can create and delete admins. + * This page is locked to users with the Developer role (The logic for this is in AdminLayout). + */ class AdminsPage extends React.Component { state: Readonly = { isRegisterModalOpen: false, }; + /** + * Get admins from the backend and put them into the redux state. + */ loadAdmins = () => loadAllAdmins() .then(res => this.props.replaceAdmins(res)) @@ -43,14 +61,22 @@ class AdminsPage extends React.Component { componentDidMount() { this.props.showLoading(); + // Hide loading state after the API returns the admins this.loadAdmins() .then(() => this.props.hideLoading()); } + /** + * Toggle the isRegisterModalOpen state variable to show or hide new admin modal + */ toggleRegisterModal = () => this.setState({ isRegisterModalOpen: !this.state.isRegisterModalOpen, }); + /** + * Create a new admin in the database and update the frontend to reflect the change + * @param {NewAdminModalFormData} newAdmin the new admin to be added to the system + */ registerNewAdmin = (newAdmin: NewAdminModalFormData) => { registerAdmin(newAdmin) .then(this.loadAdmins) @@ -58,6 +84,10 @@ class AdminsPage extends React.Component { .catch(console.error); } + /** + * Delete an admin from the system, and update the frontend to reflect the change + * @param {String} adminId the admin to be deleted from the system + */ onDeleteAdmin = (adminId: string) => deleteAdmin(adminId) .then(this.loadAdmins) diff --git a/src/client/pages/AdminsPage/reducers/Admins.ts b/src/client/pages/AdminsPage/reducers/Admins.ts index e86d7639..f8f7c22c 100644 --- a/src/client/pages/AdminsPage/reducers/Admins.ts +++ b/src/client/pages/AdminsPage/reducers/Admins.ts @@ -7,6 +7,7 @@ import * as Types from '../actions/types'; const initialState: Admin[] = []; +// Tell redux what to do when it sees ADD_ADMINS and REPLACE_ADMINS export default handleActions({ [Types.ADD_ADMINS]: (state, action: ActionType) => ([ ...state, diff --git a/src/client/pages/AlertPage.tsx b/src/client/pages/AlertPage.tsx index 1b85094f..f2347792 100644 --- a/src/client/pages/AlertPage.tsx +++ b/src/client/pages/AlertPage.tsx @@ -35,6 +35,7 @@ export const AlertPageAbove: React.StatelessComponent = (props) => { * Allows for extension with bootstrap alerts in the state. */ export default class AlertPage extends React.Component { + /** * Creates a new alert to render to the top of the screen. * @param {String} message The message to display in the alert. @@ -52,7 +53,7 @@ export default class AlertPage extends React.Compon } /** - * Creates a new error alert if there was a login error. + * Creates a new error alert if there was an error. * @param {PageAlert} alert The alert to display. * @param {String} key The given key for the element map. * @param {Boolean} container Determines whether the alert is wrapped in a container. @@ -75,12 +76,21 @@ export default class AlertPage extends React.Compon } } + /** + * Empties the alerts in the state. + */ clearAlerts = () => { this.setState({ alerts: [], }); }; + /** + * Show the alerts currently in state. + * @param {Boolean} container Determines whether the alert is wrapped in a container. + * @param {className} Override the alert with a different className. + * @returns {Component} + */ renderAlerts = (container: boolean = false, className: string = 'alert-page__alert') => { return ( diff --git a/src/client/pages/ApplyPage/components/ApplyPageSection.tsx b/src/client/pages/ApplyPage/components/ApplyPageSection.tsx index f2ecf52f..5fd47204 100644 --- a/src/client/pages/ApplyPage/components/ApplyPageSection.tsx +++ b/src/client/pages/ApplyPage/components/ApplyPageSection.tsx @@ -3,10 +3,19 @@ import React from 'react'; import { InjectedFormProps } from 'redux-form'; export interface ApplyPageSectionProps { + + // The event this application is for event: TESCEvent; + + // function to be called when the back button is pressed goToPreviousPage?: () => void; } +/** + * This is an abstraction that creates a React component with the props for + * the current event being applied to and a function that makes the ApplyPage + * move the page. Each section of the application simply extends this class. + */ export default class ApplyPageSection extends React.Component

, S> { diff --git a/src/client/pages/ApplyPage/components/Header.tsx b/src/client/pages/ApplyPage/components/Header.tsx index 6d7c418b..1f7192c4 100644 --- a/src/client/pages/ApplyPage/components/Header.tsx +++ b/src/client/pages/ApplyPage/components/Header.tsx @@ -2,11 +2,20 @@ import { Logo } from '@Shared/ModelTypes'; import React from 'react'; interface HeaderProps { + + // Name of the event name: string; + + // Logo of the event logo: Logo; + + // Description of the event description: string; } +/** + * This is the header for the application. It shows the event name, description and logo. + */ export default class Header extends React.Component { render() { const {name, logo, description} = this.props; diff --git a/src/client/pages/ApplyPage/components/PersonalSection.tsx b/src/client/pages/ApplyPage/components/PersonalSection.tsx index aeeb5ef3..61299dc2 100644 --- a/src/client/pages/ApplyPage/components/PersonalSection.tsx +++ b/src/client/pages/ApplyPage/components/PersonalSection.tsx @@ -9,7 +9,9 @@ import ApplyPageSection, { ApplyPageSectionProps } from './ApplyPageSection'; import UniversityField from './UniversityField'; interface PersonalSectionProps extends ApplyPageSectionProps { + // Function to be called when the email changes, to check if the user exists in the database onEmailChange: (newEmail: string) => void; + // The current event being applied to event: TESCEvent; } @@ -19,6 +21,9 @@ export enum InstitutionType { HighSchool = 'hs', } +/** + * Override defined form data to track the data in the way the UI shows it. + */ export interface PersonalSectionFormData extends RegisterUserPersonalSectionRequest { birthdateMonth: number; birthdateDay: number; @@ -27,7 +32,16 @@ export interface PersonalSectionFormData extends RegisterUserPersonalSectionRequ resume?: File[]; } +/** + * This is the first page of the event application. This handles the user's personal details. + */ class PersonalSection extends ApplyPageSection { + + /** + * Create the email component of the application. + * Use onBlur to check if the email exists when the user takes their focus off this field. + * @returns {Component} + */ createEmailField() { return ( { state: Readonly = { teamState: undefined, @@ -46,7 +49,12 @@ class ResponseSection extends ApplyPageSection; } - // TODO: Make into a statically-typed method + /** + * Render the custom questions for this event, given the type of question to be rendered + * TODO: Make into a statically-typed method + * @param {CustomQuestions} customQuestions the custom questions of this event + * @param {QuestionType} type The type of question to be rendered + */ renderCustomQuestions(customQuestions: CustomQuestions, type: QuestionType) { let inputField: (fieldName: string, value: any, ...otherArgs: any[]) => JSX.Element | JSX.Element[] = null; diff --git a/src/client/pages/ApplyPage/components/SubmittedSection.tsx b/src/client/pages/ApplyPage/components/SubmittedSection.tsx index 6e3d8618..5bef7274 100644 --- a/src/client/pages/ApplyPage/components/SubmittedSection.tsx +++ b/src/client/pages/ApplyPage/components/SubmittedSection.tsx @@ -6,6 +6,9 @@ import ApplyPageSection, { ApplyPageSectionProps } from './ApplyPageSection'; interface SubmittedSectionProps extends ApplyPageSectionProps { } +/** + * This is the application success page + */ class SubmittedSection extends ApplyPageSection<{}, SubmittedSectionProps> { renderTeamCode = (info: WrappedFieldProps) => (

diff --git a/src/client/pages/ApplyPage/components/UniversityField.tsx b/src/client/pages/ApplyPage/components/UniversityField.tsx index 2e2ed6f0..19e68a31 100644 --- a/src/client/pages/ApplyPage/components/UniversityField.tsx +++ b/src/client/pages/ApplyPage/components/UniversityField.tsx @@ -11,7 +11,16 @@ interface UniversityFieldProps { type Props = WrappedFieldProps & UniversityFieldProps; +/** + * This component creates the university picker. It is used in ./PersonalSection.tsx + */ export default class UniversityField extends React.Component { + + /** + * Function that is called when the user selects a suggestion from the AutoSuggest + * TODO: better documentation + * @param {String} suggestion The suggestion that the user selected + */ onUniversitySelected = (suggestion: string) => { const {input} = this.props; input.onChange(suggestion); diff --git a/src/client/pages/ApplyPage/components/UserSection.tsx b/src/client/pages/ApplyPage/components/UserSection.tsx index 0c8d75de..239379e7 100644 --- a/src/client/pages/ApplyPage/components/UserSection.tsx +++ b/src/client/pages/ApplyPage/components/UserSection.tsx @@ -18,6 +18,9 @@ export interface UserSectionFormData { confirmPassword?: string; } +/** + * This is the 3rd page of the application. It handles tesc.events account creation and MLH provisions + */ class UserSection extends ApplyPageSection { /** * Create a checkbox to accept the Code of Conduct. diff --git a/src/client/pages/ApplyPage/index.tsx b/src/client/pages/ApplyPage/index.tsx index 18488f4e..aa22c4c5 100644 --- a/src/client/pages/ApplyPage/index.tsx +++ b/src/client/pages/ApplyPage/index.tsx @@ -19,19 +19,34 @@ interface ApplyPageProps { } type Props = ApplyPageProps & RouteComponentProps<{ + // eventAlias for the event this application is for eventAlias: string; }>; interface ApplyPageState { + + // Tracks which application page the user is on page: number; + + // Application error error: Error; + + // Tracks if the user has yet to submit the application isSubmitting: boolean; + + // The event this application is for event: TESCEvent; + + // Does the user have an exisiting tesc.events account? emailExists: boolean; } +// The complete application is the union of data from its 3 pages export type ApplyPageFormData = PersonalSectionFormData & ResponseSectionFormData & UserSectionFormData; +/** + * This page is the application for an event. It implements a multi page application form. + */ class ApplyPage extends React.Component { state: Readonly = { page: 1, @@ -88,7 +103,13 @@ class ApplyPage extends React.Component { this.loadPageFromHash(); } + /** + * Sanitise user input, used before the final submit. + * @param {ApplyPageFormData} values the user's application + */ sanitiseValues(values: ApplyPageFormData) { + + // parse date numbers into a datestring values.birthdate = new Date( values.birthdateYear, values.birthdateMonth - 1, @@ -130,6 +151,7 @@ class ApplyPage extends React.Component { return; } + // checks if user exists with an API call checkUserExists(email) .then((ret) => { this.setState({ @@ -152,6 +174,7 @@ class ApplyPage extends React.Component { isSubmitting: true, }); + // Send Application to backend registerUser(this.props.match.params.eventAlias, values) .then(() => { // Log successful application with Google Analytics @@ -160,6 +183,7 @@ class ApplyPage extends React.Component { action: 'Successful', }); + // Show success page this.nextPage(); }) .catch((err) => { diff --git a/src/client/pages/CheckinPage/actions/index.ts b/src/client/pages/CheckinPage/actions/index.ts index 071f85ee..7df07d86 100644 --- a/src/client/pages/CheckinPage/actions/index.ts +++ b/src/client/pages/CheckinPage/actions/index.ts @@ -5,6 +5,11 @@ import * as Api from '~/data/AdminApi'; import * as Types from './types'; +/** + * Thunk to make API request to check in the given user. + * @param user The user to be checked in + * @param eventId The event to be checked into. + */ export const userCheckin = (user: TESCUser, eventId: string): ApplicationAction> => (dispatch: ApplicationDispatch) => Api.checkinUser(user._id, eventId) @@ -13,4 +18,7 @@ export const userCheckin = (user: TESCUser, eventId: string): ApplicationAction< }) .catch(console.error); +/** + * Redux action to to be dispatched when a user in checked in. + */ export const _userCheckin = createStandardAction(Types.CHECKIN_USER)(); diff --git a/src/client/pages/CheckinPage/components/KeyboardScanner.tsx b/src/client/pages/CheckinPage/components/KeyboardScanner.tsx index c2d84dd2..e5e0ab81 100644 --- a/src/client/pages/CheckinPage/components/KeyboardScanner.tsx +++ b/src/client/pages/CheckinPage/components/KeyboardScanner.tsx @@ -8,11 +8,20 @@ interface KeyboardScannerState { currentCode: string; } +/** + * React Component to render an input where an organizer can scan in a user ID + * through a QR scanner. + */ export default class KeyboardScanner extends Scanner<{}, KeyboardScannerState> { state: Readonly = { currentCode: '', }; + /** + * Callback to be made after the user ID is scanned. + * + * @param {React.FormEvent} event the input event being responded to. + */ onChangeInput = (event: React.FormEvent) => { const newValue = event.currentTarget.value; diff --git a/src/client/pages/CheckinPage/components/LiabilityWaiverModal.tsx b/src/client/pages/CheckinPage/components/LiabilityWaiverModal.tsx index 9b20cc27..89182e7b 100644 --- a/src/client/pages/CheckinPage/components/LiabilityWaiverModal.tsx +++ b/src/client/pages/CheckinPage/components/LiabilityWaiverModal.tsx @@ -3,12 +3,23 @@ import React from 'react'; import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; interface LiabilityWaiverModalProps { + + // The event for which we want to show the liability waiver. event: TESCEvent; + + // Callback to be called after agreement to the waiver onWaiverAgree: () => void; + + // Callback to toggle this modal toggleModal: () => void; + + // The state of this modal. isOpen: boolean; } +/** + * The Liability waiver that a user must agree to to be let into the event. + */ export default class LiabilityWaiverModal extends React.Component { render() { const {event, isOpen, toggleModal, onWaiverAgree} = this.props; diff --git a/src/client/pages/CheckinPage/components/ManualScanner.tsx b/src/client/pages/CheckinPage/components/ManualScanner.tsx index 500726dc..c06ae6ee 100644 --- a/src/client/pages/CheckinPage/components/ManualScanner.tsx +++ b/src/client/pages/CheckinPage/components/ManualScanner.tsx @@ -4,10 +4,13 @@ import React from 'react'; import Scanner from './Scanner'; interface ManualScannerProps { + + // All users in this event. users: TESCUser[]; } interface ManualScannerState { + // All eligible users for the search query. filteredApplicants: TESCUser[]; } @@ -16,10 +19,17 @@ export default class ManualScanner extends Scanner) => { const {users} = this.props; const name = event.currentTarget.value; + + // Do not issue a search for less than 3 charactes. if (name.length < 3) { return; } @@ -33,6 +43,12 @@ export default class ManualScanner extends Scanner { this.setState({ filteredApplicants: [], diff --git a/src/client/pages/CheckinPage/components/Scanner.tsx b/src/client/pages/CheckinPage/components/Scanner.tsx index ae6df33a..e630ac87 100644 --- a/src/client/pages/CheckinPage/components/Scanner.tsx +++ b/src/client/pages/CheckinPage/components/Scanner.tsx @@ -4,6 +4,9 @@ interface ScannerProps { onUserScanned: (userId: string) => void; } +/** + * Abstraction to ensure the `onUserScanned` prop is provided to the scanner tabs. + */ export default class Scanner

extends React.Component

{ } diff --git a/src/client/pages/CheckinPage/components/WebcamScanner.tsx b/src/client/pages/CheckinPage/components/WebcamScanner.tsx index db09ccc1..a9a2ecd4 100644 --- a/src/client/pages/CheckinPage/components/WebcamScanner.tsx +++ b/src/client/pages/CheckinPage/components/WebcamScanner.tsx @@ -5,7 +5,16 @@ import { USER_ID_LENGTH } from '..'; import Scanner from './Scanner'; +/** + * Renders a QR Code Scanner and reads the user ID. + */ export default class WebcamScanner extends Scanner { + + /** + * The callback to be called with the scanned data from the QR code. + * + * @param {String} data The data scanned from the QR code. + */ onScan = (data: string) => { if (data === null || data.length !== USER_ID_LENGTH) { return; diff --git a/src/client/pages/CheckinPage/index.tsx b/src/client/pages/CheckinPage/index.tsx index ac51b0b4..ddd60f56 100644 --- a/src/client/pages/CheckinPage/index.tsx +++ b/src/client/pages/CheckinPage/index.tsx @@ -108,11 +108,19 @@ class CheckinPage extends TabularPage { } } + /** + * Show error when checkin fails + * + * @param {String} error the error message to show. + */ onCheckinError = (error: string) => { this.clearAlerts(); this.createAlert(error, AlertType.Danger); }; + /** + * Show success message when checkin is successful + */ onCheckinSuccessful = () => { const { lastUser } = this.state; @@ -131,6 +139,11 @@ class CheckinPage extends TabularPage { .then(addUsers); } + /** + * Logic flow to determine whether the user should be allowed into the event. + * + * @param {TESCUser} user The user being checked. + */ validateUser = (user: TESCUser) => new Promise((resolve, reject) => { // Ensure they're eligible @@ -154,6 +167,11 @@ class CheckinPage extends TabularPage { return resolve(user); }) + /** + * Make network request to check in the user. + * + * @param {TESCUser} user the user to be checked in. + */ checkinUser = (user: TESCUser): Promise => new Promise((resolve, reject) => { const { event } = this.props; @@ -166,6 +184,11 @@ class CheckinPage extends TabularPage { .catch(reject); }); + /** + * Callback to prep the component for validating the user. + * + * @param {String} userId the userId scanned from the QR code. + */ onScan = (userId: string) => { const { lastUser } = this.state; @@ -196,11 +219,17 @@ class CheckinPage extends TabularPage { this.toggleModal(); } + /** + * Toggle liability waiver modal visibility state. + */ toggleModal = () => this.setState({ isLiabilityShowing: !this.state.isLiabilityShowing, }); + /** + * Higher level flow for the checkin process. + */ callCheckin = () => { this.setState({ isLiabilityShowing: false, @@ -221,6 +250,10 @@ class CheckinPage extends TabularPage { }); } + /** + * Render tab where the user can use a scanner to scan in a user id from a QR code. + * @param props {Props} Not used in this function. + */ renderKeyboardTab(props: Props) { return ( @@ -229,6 +262,10 @@ class CheckinPage extends TabularPage { ); } + /** + * Render tab where the application can scan a QR code. + * @param props {Props} Not used in this function. + */ renderWebcamTab(props: Props) { return ( @@ -237,6 +274,10 @@ class CheckinPage extends TabularPage { ); } + /** + * Render tab where the user can type in an email / name to check in a user. + * @param props {Props} Not used in this function. + */ renderManualTab() { return ( diff --git a/src/client/pages/CheckinPage/reducers/Checkin.ts b/src/client/pages/CheckinPage/reducers/Checkin.ts index 50019037..f2f812dc 100644 --- a/src/client/pages/CheckinPage/reducers/Checkin.ts +++ b/src/client/pages/CheckinPage/reducers/Checkin.ts @@ -8,6 +8,10 @@ import * as Types from '../actions/types'; const initialState: TESCUser[] = []; +/** + * Redux dispatch listener for when a user is checked in to reflect the change in + * Redux state. + */ export default handleActions({ [Types.CHECKIN_USER]: (state, action: ActionType) => ([ ...state.filter(x => x._id !== action.payload._id), { diff --git a/src/client/pages/DashboardPage/components/AdminDashboard.tsx b/src/client/pages/DashboardPage/components/AdminDashboard.tsx index 00487143..e9ffc6a0 100644 --- a/src/client/pages/DashboardPage/components/AdminDashboard.tsx +++ b/src/client/pages/DashboardPage/components/AdminDashboard.tsx @@ -6,10 +6,17 @@ import React from 'react'; import EventList from './EventList'; interface AdminDashboardProps { + + // Events that the admin is permitted to see events: TESCEvent[]; + + // The current user (aka admin) requesting the page user: JWTAdminAuthToken; } +/** + * This is the admin dashboard. It is primarily a wrapper around EventList for now. + */ export default class AdminDashboard extends React.Component { render() { diff --git a/src/client/pages/DashboardPage/components/EventList.tsx b/src/client/pages/DashboardPage/components/EventList.tsx index f098b2a1..f567af67 100644 --- a/src/client/pages/DashboardPage/components/EventList.tsx +++ b/src/client/pages/DashboardPage/components/EventList.tsx @@ -5,11 +5,21 @@ import { Link } from 'react-router-dom'; import EventCard from '~/components/EventCard'; interface EventListProps { + + // events to be rendered in the list events: TESCEvent[]; + + // track if we need a direct link to the resume page resumeLink?: boolean; + + // can this user create an event? canCreate?: boolean; } +/** + * This component renders the cards for events on DashboardPage. + * It also has an "Add Event" button if the user is capable of user creation. + */ export default class EventList extends React.Component { render() { const { events, resumeLink, canCreate } = this.props; diff --git a/src/client/pages/DashboardPage/components/SponsorDashboard.tsx b/src/client/pages/DashboardPage/components/SponsorDashboard.tsx index d14db74e..8ec31242 100644 --- a/src/client/pages/DashboardPage/components/SponsorDashboard.tsx +++ b/src/client/pages/DashboardPage/components/SponsorDashboard.tsx @@ -4,9 +4,15 @@ import React from 'react'; import EventList from './EventList'; interface SponsorDashboardProps { + + // Events that this sponsor is allowed to see events: TESCEvent[]; } +/** + * This is the sponsor's event list. It uses EventList's resumeLink prop to + * enforce a link to the sponsor portal on clicking the card. + */ export default class SponsorDashboard extends React.Component { render() { const {events} = this.props; diff --git a/src/client/pages/DashboardPage/index.tsx b/src/client/pages/DashboardPage/index.tsx index 46169aa6..8f584d93 100644 --- a/src/client/pages/DashboardPage/index.tsx +++ b/src/client/pages/DashboardPage/index.tsx @@ -24,12 +24,27 @@ const mapDispatchToProps = (dispatch: ApplicationDispatch) => bindActionCreators interface DashboardPageProps { } +/** + * This component receives props in 3 ways - + * 1) The explicit props provied to it by DashboardPageProps + * 2) The redux state provided to it by mapStateToProps + * 3) The dispatch functions provided to it by mapDispatchToProps + * + * So, the props of this component is the union of the return types of mapStateToProps, + * mapDispatchToProps and DashboardPageProps + */ type Props = ReturnType & ReturnType & DashboardPageProps; +/** + * This is the page that an admin sees when they first log into tesc.events. + * It shows the list of events that admin is permitted to see + */ class DashboardPage extends React.Component { + componentDidMount() { this.props.showLoading(); + // Hide the loading state at the end of this promise this.props.loadAllAdminEvents() .catch(console.error) .finally(this.props.hideLoading); diff --git a/src/client/pages/EventPage/components/AppsOverTimeStatistics.tsx b/src/client/pages/EventPage/components/AppsOverTimeStatistics.tsx index 0e0c6b55..f9346408 100644 --- a/src/client/pages/EventPage/components/AppsOverTimeStatistics.tsx +++ b/src/client/pages/EventPage/components/AppsOverTimeStatistics.tsx @@ -6,7 +6,7 @@ import EventStatisticsComponent from './EventStatisticsComponent'; export default class AppsOverTimeStatistics extends EventStatisticsComponent { render() { const { statistics } = this.props; - + // Create the data array needed to make the line chart const appsTimeData: Array<{ date: string; appCount: number }> = []; @@ -60,12 +60,12 @@ export default class AppsOverTimeStatistics extends EventStatisticsComponent { y="appCount" animate={{ duration: 1000, - easing: "cubic" + easing: 'cubic', }} style={{ data: { strokeWidth: 2, - strokeLinecap: "round" + strokeLinecap: 'round', }, }} /> diff --git a/src/client/pages/EventPage/components/BulkChange.tsx b/src/client/pages/EventPage/components/BulkChange.tsx index aefa7b4d..c5a23782 100644 --- a/src/client/pages/EventPage/components/BulkChange.tsx +++ b/src/client/pages/EventPage/components/BulkChange.tsx @@ -3,7 +3,11 @@ import React from 'react'; import { Field, reduxForm, InjectedFormProps } from 'redux-form'; export interface BulkChangeFormData { + + // New line separated users: string; + + // New status to be set status: string; } @@ -11,8 +15,12 @@ interface BulkChangeProps { } +// Use a union of BulkChangeProps and Redux Form's Props type Props = InjectedFormProps & BulkChangeProps; +/** + * This component implements a feature to update multiple users' statuses by their user IDs + */ class BulkChange extends React.Component { render() { const {handleSubmit, pristine, submitting} = this.props; diff --git a/src/client/pages/EventPage/components/CheckinStatistics.tsx b/src/client/pages/EventPage/components/CheckinStatistics.tsx index 22f6ac55..daacfc79 100644 --- a/src/client/pages/EventPage/components/CheckinStatistics.tsx +++ b/src/client/pages/EventPage/components/CheckinStatistics.tsx @@ -3,6 +3,9 @@ import { Link } from 'react-router-dom'; import EventStatisticsComponent from './EventStatisticsComponent'; +/** + * This component shows the number of checkedin users in the header of the page + */ export default class CheckinStatistics extends EventStatisticsComponent { render() { const {event, statistics} = this.props; diff --git a/src/client/pages/EventPage/components/CustomQuestion.tsx b/src/client/pages/EventPage/components/CustomQuestion.tsx index d4ac013f..6a8ad299 100644 --- a/src/client/pages/EventPage/components/CustomQuestion.tsx +++ b/src/client/pages/EventPage/components/CustomQuestion.tsx @@ -4,11 +4,21 @@ import FA from 'react-fontawesome'; import ToggleSwitch from '~/components/ToggleSwitch'; interface CustomQuestionProps { + + // The question this component describes question: Question; + + // Callback for the delete question button onDelete: () => void; + + // Callback for the questions' required switch toggle onChangeRequired: (newRequired: boolean) => void; } +/** + * This component renders a custom question on ./SettingsTab.tsx + * It handles the functionality for toggling the required field, and deleting the question. + */ class CustomQuestion extends React.Component { render() { diff --git a/src/client/pages/EventPage/components/CustomQuestionsEdit.tsx b/src/client/pages/EventPage/components/CustomQuestionsEdit.tsx index c8a98ca5..3091a4ac 100644 --- a/src/client/pages/EventPage/components/CustomQuestionsEdit.tsx +++ b/src/client/pages/EventPage/components/CustomQuestionsEdit.tsx @@ -6,23 +6,44 @@ import CustomQuestion from './CustomQuestion'; import QuestionInput from './QuestionInput'; interface CustomQuestionsProps { + + // Custom questions for this event. customQuestions: CustomQuestions; + + // State for if the custom question edit has completed on the backend isLoading?: boolean; + + // Callbacks for CUD operations for custom questions. onAddCustomQuestion: (type: QuestionType, ...opts: any[]) => void; onUpdateCustomQuestion: (toUpdate: Question) => void; onDeleteCustomQuestion: (type: QuestionType, toDelete: Question) => void; } +/** + * Custom Qestions editor. + */ export default class CustomQuestionsEdit extends React.Component { + + /** + * Callback for when the required state is toggled for a given question. + * + * @param {Question} question The question to be edited + */ onChangeRequired = (question: Question) => { const {onUpdateCustomQuestion} = this.props; + // Call function sent in props. onUpdateCustomQuestion({ ...question, isRequired: !question.isRequired, }); }; + /** + * Render the list of questions for a given type of question + * + * @param {QuestionType} type The type of the question to be rendered. + */ renderQuestions = (type: QuestionType) => { const {onDeleteCustomQuestion} = this.props; const {[type]: questions} = this.props.customQuestions; diff --git a/src/client/pages/EventPage/components/EventOptionsEdit.tsx b/src/client/pages/EventPage/components/EventOptionsEdit.tsx index 627c0d9b..982f08b1 100644 --- a/src/client/pages/EventPage/components/EventOptionsEdit.tsx +++ b/src/client/pages/EventPage/components/EventOptionsEdit.tsx @@ -5,15 +5,24 @@ import { UncontrolledTooltip } from 'reactstrap'; import ToggleSwitch from '~/components/ToggleSwitch'; interface EventOptionsProps { + // Initial options of the event options: TESCEventOptions; + + // Callback function for when the update button is clicked onOptionsUpdate: (newOptions: TESCEventOptions) => void; + + // The event these options are for event: TESCEvent; } +// The current, edited state of the options interface EventOptionsState { options: TESCEventOptions; } +/** + * This component implements event option editing (toggling) + */ export default class EventOptionsEdit extends React.Component { constructor(props: EventOptionsProps) { super(props); @@ -23,6 +32,11 @@ export default class EventOptionsEdit extends React.Component () => { this.setState({ // TODO: Fix dynamically property @@ -31,6 +45,12 @@ export default class EventOptionsEdit extends React.Component ( @@ -44,6 +64,7 @@ export default class EventOptionsEdit extends React.ComponentUnique Universities, diff --git a/src/client/pages/EventPage/components/EventStatisticsComponent.tsx b/src/client/pages/EventPage/components/EventStatisticsComponent.tsx index f6f284dd..e2ece7b3 100644 --- a/src/client/pages/EventPage/components/EventStatisticsComponent.tsx +++ b/src/client/pages/EventPage/components/EventStatisticsComponent.tsx @@ -7,6 +7,9 @@ interface EventStatisticsComponentProps { statistics: EventStatistics | null; } +/** + * This is an abstraction that is used by statistics components to provide an event and statistics prop to them. + */ export default class EventStatisticsComponent

extends React.Component

{ diff --git a/src/client/pages/EventPage/components/GenderStatistics.tsx b/src/client/pages/EventPage/components/GenderStatistics.tsx index f04959a4..3db4f321 100644 --- a/src/client/pages/EventPage/components/GenderStatistics.tsx +++ b/src/client/pages/EventPage/components/GenderStatistics.tsx @@ -7,7 +7,16 @@ import EventStatisticsComponent from './EventStatisticsComponent'; const PIE_CHART_COLOURS = ['#8E44AD', '#43D2F0', '#AEF9D6', '#EF767A', '#7D7ABC']; +/** + * This component renders a statistics pie chart for the gender breakdown of an event + */ export default class GenderStatistics extends EventStatisticsComponent { + + /** + * Render the gender breakdown in text form + * + * @param {EventStatistics} statistics The statistics of the event + */ renderStats(statistics: EventStatistics) { return [

Gender Distribution
, diff --git a/src/client/pages/EventPage/components/OrganiserList.tsx b/src/client/pages/EventPage/components/OrganiserList.tsx index 9b7281b0..630c6073 100644 --- a/src/client/pages/EventPage/components/OrganiserList.tsx +++ b/src/client/pages/EventPage/components/OrganiserList.tsx @@ -7,27 +7,48 @@ import NewAdminModal, { NewAdminModalFormData } from '~/components/NewAdminModal import OrganiserSelect from '~/components/OrganiserSelect'; interface OrganiserListProps { + + // List of organisers for the event organisers: Admin[]; + + // Callback function to show a modal to add a new organiser to the event addNewOrganiser: (toAdd: AdminSelectType) => void; + + // Callback to add the organiser to the event registerNewOrganiser: (newOrganiser: NewAdminModalFormData) => void; } interface OrganiserListState { + + // The new organiser to be added to the event newOrganiser: AdminSelectType; + + // Boolean to track if the new organiser modal is open or not isRegisterModalOpen: boolean; } +/** + * This component renders an organiser list on the administrators tab of an event + */ export default class OrganiserList extends React.Component { state: Readonly = { newOrganiser: null, isRegisterModalOpen: false, }; + /** + * Update the components newOrganiser state to the new data + * + * @param {AdminSelectType} newOrganiser the new organiser to be set + */ changeNewOrganiser = (newOrganiser: AdminSelectType) => this.setState({ newOrganiser, }); + /** + * Add an (existing organiser) to the system from the dropdown + */ onAddNewOrganiser = () => { const {newOrganiser} = this.state; @@ -36,12 +57,20 @@ export default class OrganiserList extends React.Component { this.props.registerNewOrganiser(values); this.toggleRegisterModal(); }; + /** + * Toggle the react state to show the modal or not. + */ toggleRegisterModal = () => this.setState({ isRegisterModalOpen: !this.state.isRegisterModalOpen, }); diff --git a/src/client/pages/EventPage/components/PreviewApplication.tsx b/src/client/pages/EventPage/components/PreviewApplication.tsx index e8b73df7..33f83642 100644 --- a/src/client/pages/EventPage/components/PreviewApplication.tsx +++ b/src/client/pages/EventPage/components/PreviewApplication.tsx @@ -1,7 +1,11 @@ import React from 'react'; -import ApplyPage from '../../ApplyPage'; import { Alert } from 'reactstrap'; +import ApplyPage from '../../ApplyPage'; + +/** + * Renders application in preview mode. + */ class PreviewApplication extends React.Component { render() { @@ -16,4 +20,4 @@ class PreviewApplication extends React.Component { } } -export default PreviewApplication; \ No newline at end of file +export default PreviewApplication; diff --git a/src/client/pages/EventPage/components/QuestionInput.tsx b/src/client/pages/EventPage/components/QuestionInput.tsx index e43c2d5c..138f461c 100644 --- a/src/client/pages/EventPage/components/QuestionInput.tsx +++ b/src/client/pages/EventPage/components/QuestionInput.tsx @@ -12,14 +12,22 @@ interface QuestionInputState { isRequired: boolean; } +/** + * This component creates an input field to create a custom question. + */ export default class QuestionInput extends React.Component { state: Readonly = { question: '', isRequired: false, }; + /** + * Callback function called when the add button is pressed to add this question to the system + */ addQuestion = () => { this.props.onAddQuestion(this.state); + + // reset the component state this.setState({ question: '', isRequired: false, diff --git a/src/client/pages/EventPage/components/ResumeStatistics.tsx b/src/client/pages/EventPage/components/ResumeStatistics.tsx index f81d9480..f74c680b 100644 --- a/src/client/pages/EventPage/components/ResumeStatistics.tsx +++ b/src/client/pages/EventPage/components/ResumeStatistics.tsx @@ -9,6 +9,9 @@ interface ResumeStatisticsProps { className: string; } +/** + * Event header link to show the number of resumes in the system + */ export default class ResumeStatistics extends EventStatisticsComponent { /** * Renders the tooltip that explains how to approve resumes. diff --git a/src/client/pages/EventPage/components/SponsorList.tsx b/src/client/pages/EventPage/components/SponsorList.tsx index d7794b27..66385e98 100644 --- a/src/client/pages/EventPage/components/SponsorList.tsx +++ b/src/client/pages/EventPage/components/SponsorList.tsx @@ -7,27 +7,48 @@ import NewAdminModal, { NewAdminModalFormData } from '~/components/NewAdminModal import SponsorSelect from '~/components/SponsorSelect'; interface SponsorListProps { + + // The sponsors for this event sponsors: Admin[]; + + // Callback function to add an existing sponsor to the event addNewSponsor: (toAdd: AdminSelectType) => void; + + // Function called to create a new sponsor in the system registerNewSponsor: (newSponsor: NewAdminModalFormData) => void; } interface SponsorListState { + + // The new sponsor to be added newSponsor: AdminSelectType; + + // Boolean to track if the create sponsor modal is open or not. isRegisterModalOpen: boolean; } +/** + * This component renders a sponsor list on the administrators tab of an event + */ export default class SponsorList extends React.Component { state: Readonly = { newSponsor: null, isRegisterModalOpen: false, }; + /** + * Update the components newSponsor state to the new data + * + * @param {AdminSelectType} newOrganiser the new sponsor to be set + */ changeNewSponsor = (newSponsor: AdminSelectType) => this.setState({ newSponsor, }); + /** + * Add an (existing sponsor) to the system from the dropdown + */ onAddNewSponsor = () => { const {newSponsor} = this.state; @@ -36,12 +57,20 @@ export default class SponsorList extends React.Component { this.props.registerNewSponsor(values); this.toggleRegisterModal(); }; + /** + * Toggle the react state to show the modal or not. + */ toggleRegisterModal = () => this.setState({ isRegisterModalOpen: !this.state.isRegisterModalOpen, }); diff --git a/src/client/pages/EventPage/components/ViewApplication.tsx b/src/client/pages/EventPage/components/ViewApplication.tsx index af971097..f70a901f 100644 --- a/src/client/pages/EventPage/components/ViewApplication.tsx +++ b/src/client/pages/EventPage/components/ViewApplication.tsx @@ -1,18 +1,18 @@ +import { TESCEvent } from '@Shared/ModelTypes'; import React from 'react'; +import FA from 'react-fontawesome'; import { Link } from 'react-router-dom'; -import { TESCEvent } from '@Shared/ModelTypes'; import { UncontrolledTooltip } from 'reactstrap/lib/Uncontrolled'; -import FA from 'react-fontawesome'; type ViewApplicationProps = { - event: TESCEvent -} + event: TESCEvent; +}; class ViewApplication extends React.Component { render() { const {event} = this.props; - const isEventClosed = new Date(event.closeTime) < new Date + const isEventClosed = new Date(event.closeTime) < new Date; return ( <> {isEventClosed && { } } -export default ViewApplication; \ No newline at end of file +export default ViewApplication; diff --git a/src/client/pages/EventPage/index.tsx b/src/client/pages/EventPage/index.tsx index a7c64c65..22061d27 100644 --- a/src/client/pages/EventPage/index.tsx +++ b/src/client/pages/EventPage/index.tsx @@ -11,6 +11,8 @@ import Loading from '~/components/Loading'; import { loadEventStatistics, loadAllTeams, editExistingEvent } from '~/data/AdminApi'; import { ApplicationState } from '~/reducers'; +import EventForm, { EventFormData } from '../../components/EventForm'; +import createValidator from '../NewEventPage/validate'; import TabularPage, { TabularPageState, TabularPageProps, TabPage, TabularPageNav } from '../TabularPage'; import { @@ -19,16 +21,16 @@ import { } from './actions'; import CheckinStatistics from './components/CheckinStatistics'; import ResumeStatistics from './components/ResumeStatistics'; +import ViewApplication from './components/ViewApplication'; import ActionsTab from './tabs/ActionsTab'; import AdministratorsTab from './tabs/AdministratorsTab'; import SettingsTab from './tabs/SettingsTab'; import StatisticsTab from './tabs/StatisticsTab'; import TeamsTab from './tabs/TeamsTab'; -import ViewApplication from './components/ViewApplication'; -import EventForm, { EventFormData } from '../../components/EventForm'; -import createValidator from '../NewEventPage/validate'; type RouteProps = RouteComponentProps<{ + + // The eventAlias for the event we want to render the dashboard for eventAlias: string; }>; @@ -59,6 +61,15 @@ const mapDispatchToProps = (dispatch: ApplicationDispatch) => bindActionCreators interface EventPageProps extends TabularPageProps { } +/** + * This component receives props in 3 ways - + * 1) The explicit props provied to it by EventPageProps + * 2) The redux state provided to it by mapStateToProps + * 3) The dispatch functions provided to it by mapDispatchToProps + * + * So, the props of this component is the union of the return types of mapStateToProps, + * mapDispatchToProps and EventPageProps + */ export type Props = RouteProps & ReturnType & ReturnType & EventPageProps; @@ -66,6 +77,12 @@ interface EventPageState extends TabularPageState { teams: TESCTeam[]; } +/** + * This page renders the main page for an event. + * It has tabs and links to the other pages related to this event. + * + * This component is extending from TabularPage, which has the tabbing functionality abstracted away + */ class EventPage extends TabularPage { tabPages: Readonly = [ { @@ -228,7 +245,7 @@ class EventPage extends TabularPage { closeTimeMonth: eventDate.getMonth(), closeTimeYear: eventDate.getFullYear(), logo: undefined, - } + }; const editEvent = async (eventData: EventFormData) => { try { @@ -241,12 +258,12 @@ class EventPage extends TabularPage { } catch (e) { this.props.addEventDangerAlert(eventData.alias, e.message, 'Edit Event'); } - } + }; return (
{ + + /** + * This function is called when the user clicks the 'Export All Users' button. + */ exportUsers = () => { const eventAlias = this.props.event.alias; + + // call API's exportUsers. + // surprisingly, this is okay syntax because exportUsers and this.exportUsers are different + // TODO: name API call better exportUsers(eventAlias, false) .end((err, res) => { // Download as file @@ -25,8 +39,13 @@ export default class ActionsTab extends EventPageTab { }); } + /** + * This function is called when the user clicks the 'Export All Emails' button. + */ exportEmails = () => { const eventAlias = this.props.event.alias; + exportUsers(eventAlias, true) + exportUsers(eventAlias, true) .end((err, res) => { // Download as file @@ -41,6 +60,10 @@ export default class ActionsTab extends EventPageTab { }); } + /** + * This function is called when the Bulk Change button is clicked on the form. + * @param {BulkChangeFormData} values the values of the Bulk Change Form + */ onBulkChange = (values: BulkChangeFormData) => { const { event } = this.props; const { users, status } = values; @@ -48,6 +71,7 @@ export default class ActionsTab extends EventPageTab { // Split users into array const usersSplit = users.split(/\n/); + // call API's bulk change bulkChange(usersSplit, status) .then(() => { this.props.addEventSuccessAlert(event.alias, 'Successfully updated users!', 'Bulk Change'); @@ -79,7 +103,7 @@ export default class ActionsTab extends EventPageTab { > Export User Emails - +
@@ -89,4 +113,4 @@ export default class ActionsTab extends EventPageTab {
); } -} \ No newline at end of file +} diff --git a/src/client/pages/EventPage/tabs/AdministratorsTab.tsx b/src/client/pages/EventPage/tabs/AdministratorsTab.tsx index 24c4a5a9..d959a40c 100644 --- a/src/client/pages/EventPage/tabs/AdministratorsTab.tsx +++ b/src/client/pages/EventPage/tabs/AdministratorsTab.tsx @@ -12,10 +12,23 @@ interface AdministratorsTabProps { } interface AdminReference { + + // the database ID of an admin _id: string; + + // the username of an admin username: string; } +/** + * View Administrators in this event + * + * This tab currently has: + * - View current organizers + * - View current sponsors + * - Create a new sponsor + * - Create a new organizer + */ export default class AdministratorsTab extends EventPageTab { /** * Parses from a react-select element into an admin object. diff --git a/src/client/pages/EventPage/tabs/EventPageTab.tsx b/src/client/pages/EventPage/tabs/EventPageTab.tsx index 926504d3..92f8ba90 100644 --- a/src/client/pages/EventPage/tabs/EventPageTab.tsx +++ b/src/client/pages/EventPage/tabs/EventPageTab.tsx @@ -7,5 +7,9 @@ interface EventPageTabProps { event: TESCEvent; } +/** + * This is an abstraction that provides every EventPage tab with access to the event directly. + * This means that we don't have to explicitly tell every tab which event we are currently on. + */ export default class EventPageTab extends React.Component

{ } diff --git a/src/client/pages/EventPage/tabs/SettingsTab.tsx b/src/client/pages/EventPage/tabs/SettingsTab.tsx index 0c185d69..0b79052d 100644 --- a/src/client/pages/EventPage/tabs/SettingsTab.tsx +++ b/src/client/pages/EventPage/tabs/SettingsTab.tsx @@ -14,10 +14,19 @@ import EventPageTab from './EventPageTab'; interface SettingsTabProps { } +// TODO: not sure why this exists as opposed to a boolean? interface SettingsTabState { customQuestionsRequests: number; } +/** + * This is the settings tag for an event. This tab currently has: + * + * - Toggling event options + * - Add custom question + * - Update custom question + * - Delete custom question + */ export default class SettingsTab extends EventPageTab { state: Readonly = { customQuestionsRequests: 0, @@ -25,7 +34,7 @@ export default class SettingsTab extends EventPageTab { const { event, addEventSuccessAlert, addEventDangerAlert } = this.props; @@ -41,18 +50,31 @@ export default class SettingsTab extends EventPageTab { this.setState({ customQuestionsRequests: this.state.customQuestionsRequests + 1, }); }; + /** + * Set React state to hide loader after server response has been seen. + */ stopCustomQuestionsLoading = () => { this.setState({ customQuestionsRequests: this.state.customQuestionsRequests - 1, }); }; + /** + * Handles the CustomQuestions callback for when custom questions should be added. + * + * @param {QuestionType} type The type of question being added + * @param {Question} question The new question to send to the server. + */ onAddCustomQuestion = (type: QuestionType, question: Question) => { const { event, loadAllAdminEvents, addEventDangerAlert } = this.props; @@ -67,6 +89,12 @@ export default class SettingsTab extends EventPageTab { const { event, loadAllAdminEvents, addEventDangerAlert } = this.props; @@ -81,6 +109,12 @@ export default class SettingsTab extends EventPageTab { const { event, loadAllAdminEvents, addEventDangerAlert } = this.props; diff --git a/src/client/pages/EventPage/tabs/StatisticsTab.tsx b/src/client/pages/EventPage/tabs/StatisticsTab.tsx index 3a695c60..7123028e 100644 --- a/src/client/pages/EventPage/tabs/StatisticsTab.tsx +++ b/src/client/pages/EventPage/tabs/StatisticsTab.tsx @@ -2,9 +2,9 @@ import { EventStatistics } from '@Shared/api/Responses'; import React from 'react'; import Loading from '~/components/Loading'; +import AppsOverTimeStatistics from '../components/AppsOverTimeStatistics'; import EventStatisticsCharts from '../components/EventStatisticsCharts'; import GenderStatistics from '../components/GenderStatistics'; -import AppsOverTimeStatistics from '../components/AppsOverTimeStatistics'; import EventPageTab from './EventPageTab'; @@ -12,6 +12,12 @@ interface StatisticsTabProps { statistics: EventStatistics | null; } +/** + * This is the tab that shows the user statistics for an event. This tab currently has: + * + * - Status Breakdown Piechart + * - Gender Breakdown Piechart + */ export default class StatisticsTab extends EventPageTab { render() { diff --git a/src/client/pages/ForgotPage.tsx b/src/client/pages/ForgotPage.tsx index b088316f..5fcb8552 100644 --- a/src/client/pages/ForgotPage.tsx +++ b/src/client/pages/ForgotPage.tsx @@ -8,12 +8,20 @@ interface ForgotPageState { success: string; } +/** + * The Forgot password page + */ class ForgotPage extends React.Component { state: Readonly = { error: '', success: '', }; + /** + * Send the Forgot Password Email + * + * @param {ForgotFormData} values the forgot form data + */ sendForgotPassword = (values: ForgotFormData) => { if (!values.email) { return { diff --git a/src/client/pages/HomePage/components/CurrentEvents.tsx b/src/client/pages/HomePage/components/CurrentEvents.tsx index 647232a7..66e1d109 100644 --- a/src/client/pages/HomePage/components/CurrentEvents.tsx +++ b/src/client/pages/HomePage/components/CurrentEvents.tsx @@ -6,6 +6,9 @@ interface CurrentEventProps { events: TESCEvent[]; } +/** + * This component shows all upcoming events with open applications + */ export default class CurrentEvents extends React.Component { render() { const { events } = this.props; diff --git a/src/client/pages/HomePage/components/UserEvents.tsx b/src/client/pages/HomePage/components/UserEvents.tsx index 5819f021..6e6e8a93 100644 --- a/src/client/pages/HomePage/components/UserEvents.tsx +++ b/src/client/pages/HomePage/components/UserEvents.tsx @@ -3,9 +3,14 @@ import React from 'react'; import { Link } from 'react-router-dom'; interface UserEventsProps { + + // The events that the user has applied to events: TESCEvent[]; } +/** + * This component shows all existing applications for the user. + */ export default class UserEvents extends React.Component { render() { const { events } = this.props; diff --git a/src/client/pages/HomePage/index.tsx b/src/client/pages/HomePage/index.tsx index 5d2eece1..712a2125 100644 --- a/src/client/pages/HomePage/index.tsx +++ b/src/client/pages/HomePage/index.tsx @@ -31,7 +31,12 @@ interface HomePageProps { type Props = ReturnType & ReturnType & HomePageProps; +/** + * This is the main tesc.event homepage. If the user requesting it is authenticated, it will show + * the user their existing applications. If the user is not authenticated, it will show all open applications + */ class HomePage extends React.Component { + componentDidMount() { this.props.showLoading(); @@ -53,6 +58,11 @@ class HomePage extends React.Component { return true; } + /** + * Renders the user's existing applications + * + * @param {TESCEvent[]} events the events that this user has applied to + */ userEvents(events: TESCEvent[]) { return (

@@ -61,6 +71,12 @@ class HomePage extends React.Component { ); } + /** + * Renders the current events that are open for applications + * + * @param {TESCEvent[]} events the events to be rendered + * @param {Boolean} small display mode for the event cards + */ currentEvents(events: TESCEvent[], small: boolean = false) { return (
@@ -72,6 +88,7 @@ class HomePage extends React.Component { render() { const { events, userEvents } = this.props; + // show the sidebar if the authenticated user has any applications const showSidebar = Object.values(userEvents).length > 0; let currentEvents: TESCEvent[] = []; diff --git a/src/client/pages/LoginPage.tsx b/src/client/pages/LoginPage.tsx index cc634dca..819e1319 100644 --- a/src/client/pages/LoginPage.tsx +++ b/src/client/pages/LoginPage.tsx @@ -28,6 +28,9 @@ interface LoginPageState extends AlertPageState { redirectToReferrer: boolean; } +/** + * Page for users to login to their accounts. + */ class LoginPage extends AlertPage { state: Readonly = { alerts: [], @@ -56,6 +59,11 @@ class LoginPage extends AlertPage { } } + /** + * Make API call for the user trying to log in. + * + * @param {LoginFormData} formProps The data in the form. + */ loginUser = (formProps: LoginFormData) => { const {loginUser, history} = this.props; return loginUser(formProps) diff --git a/src/client/pages/NewEventPage/index.tsx b/src/client/pages/NewEventPage/index.tsx index 261496c6..bc44ae34 100644 --- a/src/client/pages/NewEventPage/index.tsx +++ b/src/client/pages/NewEventPage/index.tsx @@ -6,11 +6,11 @@ import { withRouter, RouteComponentProps } from 'react-router'; import { UncontrolledAlert } from 'reactstrap'; import { bindActionCreators } from 'redux'; import { ApplicationDispatch } from '~/actions'; +import EventForm, { EventFormData } from '~/components/EventForm'; import { registerNewEvent } from '~/data/AdminApi'; import { addEventSuccessAlert } from '../EventPage/actions'; -import EventForm, { EventFormData } from '~/components/EventForm'; import createValidator from './validate'; const mapDispatchToProps = (dispatch: ApplicationDispatch) => bindActionCreators({ @@ -28,12 +28,22 @@ interface NewEventPageState { err: Error; } +/** + * This page holds the form to create a new event in the system + */ class NewEventPage extends React.Component { state: Readonly = { err: null, }; + /** + * Create a new event in the system + * + * @param {NewEventFormData} event the event to be created + */ createNewEvent = (event: EventFormData) => { + + // send event to API registerNewEvent(event) .then((res: TESCEvent) => { this.setState({ err: null }); diff --git a/src/client/pages/NewEventPage/validate.ts b/src/client/pages/NewEventPage/validate.ts index d6a025ce..b539bd25 100644 --- a/src/client/pages/NewEventPage/validate.ts +++ b/src/client/pages/NewEventPage/validate.ts @@ -1,11 +1,13 @@ +/** + * New Event Form validator (Redux Form) + */ const createValidator = (requireLogo = true, allowPastDates = false) => (values: any) => { - console.log(values) const errors: any = {}; const required = ['name', 'alias', 'closeTimeMonth', 'closeTimeDay', 'closeTimeYear', 'email', 'homepage', 'description']; - requireLogo && required.push('logo') + requireLogo && required.push('logo'); const notValid = required.filter(name => !(name in values) || !values[name]); notValid.forEach(name => errors[name] = 'Required'); diff --git a/src/client/pages/NotFound.tsx b/src/client/pages/NotFound.tsx index 8b6834cc..1fd2ac9f 100644 --- a/src/client/pages/NotFound.tsx +++ b/src/client/pages/NotFound.tsx @@ -1,6 +1,9 @@ import React from 'react'; import { Link } from 'react-router-dom'; +/** + * The 404 Page of the application + */ class NotFoundPage extends React.Component { render() { return ( diff --git a/src/client/pages/README.md b/src/client/pages/README.md new file mode 100644 index 00000000..d0b77308 --- /dev/null +++ b/src/client/pages/README.md @@ -0,0 +1,44 @@ +## src/client/pages - Pages. Pages Everywhere. + +An `XYZPage` in this directory implements a page in the application. Any `XYZPage` can define redux actions in `XYZPage/actions`, reducers in `XYZPage/reducers` or components it uses in `XYZPage/components`. + +# Directory Tree +``` +├── AdminsPage + * [Admin with role Developer only] It shows a list of all admins in the system. +├── AlertPage.tsx + * This is an abstraction that allows a page to have "alerts" at the top of it. An `XYZPage` can extend this class to implement alerts. +├── ApplyPage + * This is the hackathon registration page. +├── CheckinPage + * [Admin only] This page is the QR Code Checkin system for checkin into an event. +├── DashboardPage + * [Admin only] This page is the "opening" dashboard for an admin when they log into the system. + * It decides what to show to the admin depending on their role and the API response +├── EventPage + * [Admin only] This is the main dashboard for an event. It has tabs (extends `TabularPage.tsx`) for Actions, Administrators, Settings and Statistics +├── ForgotPage.tsx + * This page implements the forgot password functionality for users. +├── HomePage + * This is the main page that loads when a user goes to www.tesc.events + * This page handles 2 states + - showing all apply-able events when the user is not logged in, and show + - showing all existing applications when the user is logged in +├── LoginPage.tsx + * This page shows a username and password field for a user to login. +├── NewEventPage + * [Admin only] This page allows an admin to create a new event. +├── NotFound.tsx + * This is the 404 page. react-router is set up to show this when none of the routes are rendered. +├── ResetPage.tsx + * [User only] This page allows a user to reset their password (linked from a password reset email) +├── ResumesPage + * [Admin only] This is the sponsor-tool. It provides a dashboard with sorting / filtering features that is given to sponsors who pay for access to tesc.events +├── TabularPage.tsx + * This is an abstraction that lets a page that extends it implement tabs. +├── UserPage + * [User only] This is the page that lets a user view / edit their hackathon application +└── UsersPage + * [Admin only] This is the page that shows admins a list of users that have applied to an event and lets them update the application if needed. + * This page also has extensive sorting features for admins. +``` \ No newline at end of file diff --git a/src/client/pages/ResetPage.tsx b/src/client/pages/ResetPage.tsx index 8164be22..818ad6e0 100644 --- a/src/client/pages/ResetPage.tsx +++ b/src/client/pages/ResetPage.tsx @@ -12,6 +12,9 @@ type Props = RouteComponentProps<{ resetString: string; }>; +/** + * Password Reset Page + */ class ResetPage extends React.Component { state: Readonly = { error: '', diff --git a/src/client/pages/ResumesPage/components/ResumeList.tsx b/src/client/pages/ResumesPage/components/ResumeList.tsx index dad98060..1a15e0d8 100644 --- a/src/client/pages/ResumesPage/components/ResumeList.tsx +++ b/src/client/pages/ResumesPage/components/ResumeList.tsx @@ -3,8 +3,14 @@ import React from 'react'; import ToggleSwitch from '~/components/ToggleSwitch'; interface ResumeListProps { + + // Callback for when the compact state toggle is clicked onCompactChange: () => void; + + // The compacted state isCompacted: boolean; + + // The users to render applicants: TESCUser[]; } @@ -13,11 +19,16 @@ interface ColumnMap { } interface ResumeListState { + + // Columns shown on screen columns: ColumnMap; smallColumns: string[]; mediumColumns: string[]; } +/** + * This is the resume list that is rendered in the sponsor tool + */ class ResumeList extends React.Component { state: Readonly = { columns: { diff --git a/src/client/pages/ResumesPage/index.tsx b/src/client/pages/ResumesPage/index.tsx index b14e2c0e..c292cb5e 100644 --- a/src/client/pages/ResumesPage/index.tsx +++ b/src/client/pages/ResumesPage/index.tsx @@ -1,23 +1,23 @@ -import { TESCUser } from '@Shared/ModelTypes'; -import { UserStatus } from '@Shared/UserStatus'; -import React from 'react'; -import { connect } from 'react-redux'; -import { showLoading, hideLoading } from 'react-redux-loading-bar'; -import { RouteComponentProps } from 'react-router'; -import { bindActionCreators } from 'redux'; -import { ApplicationDispatch, loadAllAdminEvents } from '~/actions'; -import { loadAllSponsorUsers } from '~/data/AdminApi'; -import { ApplicationState } from '~/reducers'; -import { applyResumeFilter } from '~/static/Resumes'; - -import { replaceApplicants, replaceFiltered } from './actions'; -import ResumeList from './components/ResumeList'; - -type RouteProps = RouteComponentProps<{ + import { TESCUser } from '@Shared/ModelTypes'; + import { UserStatus } from '@Shared/UserStatus'; + import React from 'react'; + import { connect } from 'react-redux'; + import { showLoading, hideLoading } from 'react-redux-loading-bar'; + import { RouteComponentProps } from 'react-router'; + import { bindActionCreators } from 'redux'; + import { ApplicationDispatch, loadAllAdminEvents } from '~/actions'; + import { loadAllSponsorUsers } from '~/data/AdminApi'; + import { ApplicationState } from '~/reducers'; + import { applyResumeFilter } from '~/static/Resumes'; + + import { replaceApplicants, replaceFiltered } from './actions'; + import ResumeList from './components/ResumeList'; + + type RouteProps = RouteComponentProps<{ eventAlias: string; }>; -const mapStateToProps = (state: ApplicationState, ownProps: RouteProps) => { + const mapStateToProps = (state: ApplicationState, ownProps: RouteProps) => { const eventAlias = ownProps.match.params.eventAlias; return { event: state.admin.events[eventAlias], @@ -27,7 +27,7 @@ const mapStateToProps = (state: ApplicationState, ownProps: RouteProps) => { }; }; -const mapDispatchToProps = (dispatch: ApplicationDispatch) => bindActionCreators({ + const mapDispatchToProps = (dispatch: ApplicationDispatch) => bindActionCreators({ replaceApplicants, showLoading, hideLoading, @@ -35,21 +35,33 @@ const mapDispatchToProps = (dispatch: ApplicationDispatch) => bindActionCreators loadAllAdminEvents, }, dispatch); -interface ResumesPageProps { + interface ResumesPageProps { } -type Props = RouteProps & ReturnType & ReturnType & ResumesPageProps; +// The props of this event are the union of the react-router data, redux actions and dispatch, and the +// regular props of the component + type Props = RouteComponentProps<{ + eventAlias: string; +}> & ReturnType & ReturnType & ResumesPageProps; + + interface ResumesPageState { -interface ResumesPageState { + // Boolean to track compact state isCompacted: boolean; } -class ResumesPage extends React.Component { +/** + * This is the sponsor tool that shows a list of applicants to an event and their resumes + */ + class ResumesPage extends React.Component { state: Readonly = { isCompacted: false, }; - toggleCompacted = () => this.setState({ isCompacted: !this.state.isCompacted }); + /** + * Toggle to compact react state + */ + toggleCompacted = () => this.setState({isCompacted: !this.state.isCompacted}); componentDidMount() { const { showLoading, hideLoading } = this.props; @@ -100,4 +112,4 @@ class ResumesPage extends React.Component { } } -export default connect(mapStateToProps, mapDispatchToProps)(ResumesPage); + export default connect(mapStateToProps, mapDispatchToProps)(ResumesPage); diff --git a/src/client/pages/UserPage/components/BussingModal.tsx b/src/client/pages/UserPage/components/BussingModal.tsx index 19eabf63..fc415ea2 100644 --- a/src/client/pages/UserPage/components/BussingModal.tsx +++ b/src/client/pages/UserPage/components/BussingModal.tsx @@ -2,12 +2,27 @@ import React from 'react'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; interface BussingModalProps { + + // Boolean to track if the modal is open isOpen: boolean; + + // Is a bus available for this school? availableBus?: string; + + // Callback for when the bus is clicked onChooseBus: (choice: boolean) => void; } +/** + * This is the modal that a user can use to show that they will be getting a bus. + */ export default class BussingModal extends React.Component { + + /** + * Callback for the bus choose button + * + * @param {Boolean} bussing boolean to track bussing status + */ onChooseBus = (bussing: boolean) => () => this.props.onChooseBus(bussing); diff --git a/src/client/pages/UserPage/components/RSVPConfirm.tsx b/src/client/pages/UserPage/components/RSVPConfirm.tsx index d0c5a0e3..2e0c27de 100644 --- a/src/client/pages/UserPage/components/RSVPConfirm.tsx +++ b/src/client/pages/UserPage/components/RSVPConfirm.tsx @@ -16,12 +16,16 @@ interface RSVPConfirmState { status: boolean; } +/** + * This is the component that renders the RSVP workflow by showing RSVPModal and BussingModal + */ export default class RSVPConfirm extends React.Component { state: Readonly = { page: 0, status: undefined, }; + // set React state to move to next step of the workflow nextPage = () => this.setState({page: this.state.page + 1}); onChooseStatus = (status: boolean) => { @@ -42,6 +46,11 @@ export default class RSVPConfirm extends React.Component { const {onUpdate, onClose} = this.props; diff --git a/src/client/pages/UserPage/components/RSVPModal.tsx b/src/client/pages/UserPage/components/RSVPModal.tsx index c1e48a21..dc2655c2 100644 --- a/src/client/pages/UserPage/components/RSVPModal.tsx +++ b/src/client/pages/UserPage/components/RSVPModal.tsx @@ -3,14 +3,27 @@ import React from 'react'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; interface RSVPModalProps { + + // Toggle function for the model toggle: () => void; + + // Variable to track open or close state of the modal isOpen: boolean; + // Callback function for when the status is chosen onChooseStatus: (statusChoice: boolean) => void; + + // The event for which the current application is on event: TESCEvent; } export default class RSVPModal extends React.Component { + + /** + * Callback for when an RSVP status is chosen + * + * @param {Boolean} status RSVP true or false + */ onChooseStatus = (status: boolean) => () => this.props.onChooseStatus(status); render() { diff --git a/src/client/pages/UserPage/components/UserProfile.tsx b/src/client/pages/UserPage/components/UserProfile.tsx index bde8da5f..5ff720c1 100644 --- a/src/client/pages/UserPage/components/UserProfile.tsx +++ b/src/client/pages/UserPage/components/UserProfile.tsx @@ -1,12 +1,13 @@ import { TESCUser, TESCEvent, TESCTeam } from '@Shared/ModelTypes'; -import { UserStatus } from '@Shared/UserStatus'; +import { generateQRCodeURL } from '@Shared/QRCodes'; import { UserGenderOptions, UserPronounOptions, UserShirtSizeOptions } from '@Shared/UserEnums'; +import { UserStatus } from '@Shared/UserStatus'; +import { JSXElement } from 'babel-types'; import React from 'react'; +import FA from 'react-fontawesome'; import { Field, reduxForm, InjectedFormProps, WrappedFieldProps } from 'redux-form'; import { CustomFieldProps } from '~/components/Fields'; import FileField from '~/components/FileField'; -import { generateQRCodeURL } from '@Shared/QRCodes'; -import FA from 'react-fontawesome'; export interface UserProfileFormData { gender: string; @@ -23,14 +24,31 @@ export interface UserProfileFormData { } interface UserProfileProps { + + // The user for which the profile is rendered user: TESCUser; + + // The event for which the application is on event: TESCEvent; + + // Callback function to toggle RSVP status toggleRSVP: () => void; } type Props = InjectedFormProps & UserProfileProps; +/** + * This is the component that shows the user their data on the application page + * + * It also provides functionality to edit their application + */ class UserProfile extends React.Component { + /** + * Render the gender selection. + * + * @param {Object} _ an object with an input and className field + * @returns {React.StatelessComponent} + */ genderSelect: React.StatelessComponent = ({ input, className }) => { return ( @@ -63,7 +87,7 @@ class UserProfile extends React.Component { /** * Renders the status for the navigation bar. - * @param {String} status The status of the user in the database. + * @param {UserStatus} status The status of the user in the database. * @returns {Component} */ renderUserStatus(status: UserStatus) { @@ -162,10 +186,22 @@ class UserProfile extends React.Component { ); } + /** + * Render the phone number and santitize it + * + * @param {String} phone the phone number string + * @returns {String} + */ renderPhoneNumber = (phone: string) => ( phone.replace(/(\d{3})(\d{3})(\d{4})/, '($1) $2-$3') ); + /** + * Render the Applicant's info + * + * @param {TESCUser} user the info of the applicant + * @returns {JSXElement} + */ renderApplicantInfoSection = (user: TESCUser) => { const institution = user.university || user.highSchool; return ( @@ -229,6 +265,12 @@ class UserProfile extends React.Component { ); }; + /** + * Render a user's preference section. + * + * @param {TESCUser} user the user's data + * @returns {JSXElement} + */ renderPreferencesSection = (user: TESCUser) => (

Preferences

diff --git a/src/client/pages/UserPage/index.tsx b/src/client/pages/UserPage/index.tsx index 69fa0be5..05b9e77e 100644 --- a/src/client/pages/UserPage/index.tsx +++ b/src/client/pages/UserPage/index.tsx @@ -33,6 +33,8 @@ const mapDispatchToProps = (dispatch: ApplicationDispatch) => bindActionCreators interface UserPageProps { } +// The props of this event are the union of the react-router data, redux actions and dispatch, and the +// regular props of the component type Props = RouteComponentProps<{ eventAlias: string; }> & ReturnType & ReturnType & UserPageProps; @@ -41,6 +43,9 @@ interface UserPageState extends AlertPageState { showRSVP: boolean; } +/** + * This is the page that shows a user their application + */ class UserPage extends AlertPage { state: Readonly = { alerts: [], @@ -91,7 +96,10 @@ class UserPage extends AlertPage { }); } - toggleRSVP = () => this.setState({ showRSVP: !this.state.showRSVP }); + /** + * Toggle React state to show the RSVP modal + */ + toggleRSVP = () => this.setState({showRSVP: !this.state.showRSVP}); /** * Requests that the server RSVP the current user with the given values. diff --git a/src/client/pages/UsersPage/components/UserList.tsx b/src/client/pages/UsersPage/components/UserList.tsx index 573350f2..28d78fae 100644 --- a/src/client/pages/UsersPage/components/UserList.tsx +++ b/src/client/pages/UsersPage/components/UserList.tsx @@ -10,6 +10,8 @@ import { AlertType } from '../../AlertPage'; const styles = require('react-table/react-table.css'); interface UserListProps { + + // users that have applied to an event users: TESCUser[]; columns: AutofillColumn[]; event: TESCEvent; @@ -17,7 +19,17 @@ interface UserListProps { onUserUpdate: (newUser: TESCUser) => void; } +/** + * This renders the react-table for the users that have applied to an event + */ class UserList extends React.Component { + + /** + * Render the User component, used by react-table's SubComponent + * + * @param {TESCUser} row the user to be rendered + * @returns {JSXElement} + */ expandComponent = (row: TESCUser) => (
{ data={this.props.users} column={{ ...ReactTableDefaults.column, - Cell: ({value}) => value ? String(value): value + Cell: ({value}) => value ? String(value): value, }} columns={this.props.columns} defaultPageSize={10} diff --git a/src/client/pages/UsersPage/index.tsx b/src/client/pages/UsersPage/index.tsx index 410a5789..22a33e5f 100644 --- a/src/client/pages/UsersPage/index.tsx +++ b/src/client/pages/UsersPage/index.tsx @@ -44,6 +44,7 @@ type RouteProps = RouteComponentProps<{ eventAlias: string; }>; +// The props of this page is the union of the react-router, redux and explicit props type Props = RouteProps & ReturnType & ReturnType & UsersPageProps; interface UsersPageState extends AlertPageState { @@ -131,6 +132,8 @@ class UsersPage extends AlertPage { /** * Handles an update to a user in the list. + * + * @param {TESCUser} user the user to be updated */ onUserUpdate = (user: TESCUser) => { this.props.updateUser(user) diff --git a/src/client/routes.tsx b/src/client/routes.tsx index edfa1a4d..2f197391 100644 --- a/src/client/routes.tsx +++ b/src/client/routes.tsx @@ -22,14 +22,15 @@ import ConfirmPage from './auth/user/Confirm'; import UserLogout from './auth/user/Logout'; import { authoriseUser, finishAuthorisation as finishUserAuth, logoutUser } from './auth/user/actions'; import AdminLayout from './layouts/admin'; -import SponsorLayout from './layouts/sponsor'; import PublicLayout from './layouts/public'; +import SponsorLayout from './layouts/sponsor'; import UserLayout from './layouts/user'; import AdminsPage from './pages/AdminsPage'; import ApplyPage from './pages/ApplyPage'; import CheckinPage from './pages/CheckinPage'; import Dashboard from './pages/DashboardPage'; import EventPage from './pages/EventPage'; +import PreviewApplication from './pages/EventPage/components/PreviewApplication'; import ForgotPage from './pages/ForgotPage'; import HomePage from './pages/HomePage'; import LoginPage from './pages/LoginPage'; @@ -39,7 +40,18 @@ import ResetPage from './pages/ResetPage'; import ResumesPage from './pages/ResumesPage'; import UserPage from './pages/UserPage'; import UsersPage from './pages/UsersPage'; -import PreviewApplication from './pages/EventPage/components/PreviewApplication'; + +/* + PrivateRoute.tsx and PrivateUserRoute.tsx are wrapper components around + react-router-dom's Route component to handle authentication state. +*/ + +// Authentication Components & Actions +// TODO: Document better + +// Importing the different layouts (page structures) for the application + +// Importing all the pages for the app, used later when setting up routes. const mapDispatchToProps = (dispatch: ApplicationDispatch) => bindActionCreators({ authoriseAdmin, @@ -140,6 +152,12 @@ class Routes extends React.Component { ); } + /** + * Render a route with the User layout. + * @param {JSX.IntrinsicElements} component The child component to render within the + * layout. + * @returns {Component} + */ renderUser = (RenderComponent: any) => { return (props: RouteComponentProps) => ( @@ -150,7 +168,7 @@ class Routes extends React.Component { } renderPublic = (RenderComponent: any) => { - return (props: RouteComponentProps) => + return (props: RouteComponentProps) => ( @@ -213,7 +231,6 @@ class Routes extends React.Component { /> {/* User Routes */} -