ahbeng/NUSMods

View on GitHub
website/src/views/components/module-info/AddModuleDropdown.tsx

Summary

Maintainability
A
0 mins
Test Coverage
import { PureComponent } from 'react';
import Downshift from 'downshift';
import classnames from 'classnames';
import { connect } from 'react-redux';
import { get } from 'lodash';

import { Module, ModuleCode, Semester } from 'types/modules';
import { TimetableConfig } from 'types/timetables';

import { addModule, removeModule } from 'actions/timetables';
import { getFirstAvailableSemester, getSemestersOffered } from 'utils/modules';
import config from 'config';
import { State as StoreState } from 'types/state';

import styles from './AddModuleDropdown.scss';

type Props = {
  module: Module;
  timetables: TimetableConfig;
  className?: string;
  block?: boolean;

  addModule: (semester: Semester, moduleCode: ModuleCode) => void;
  removeModule: (semester: Semester, moduleCode: ModuleCode) => void;
};

type State = {
  loading: Semester | null;
};

function isModuleOnTimetable(
  semester: Semester,
  timetables: TimetableConfig,
  module: Module,
): boolean {
  return !!get(timetables, [String(semester), module.moduleCode]);
}

export class AddModuleDropdownComponent extends PureComponent<Props, State> {
  static getDerivedStateFromProps(nextProps: Props, prevState: State) {
    const { timetables, module } = nextProps;
    const { loading } = prevState;

    if (loading != null && isModuleOnTimetable(loading, timetables, module)) {
      return { loading: null };
    }

    return null;
  }

  override state: State = {
    loading: null,
  };

  onSelect(semester: Semester) {
    const { module, timetables } = this.props;

    if (isModuleOnTimetable(semester, timetables, module)) {
      this.props.removeModule(semester, module.moduleCode);
    } else {
      this.setState({ loading: semester });
      this.props.addModule(semester, module.moduleCode);
    }
  }

  buttonLabel(semester: Semester) {
    if (this.state.loading === semester) {
      return 'Adding...';
    }

    const hasModule = isModuleOnTimetable(semester, this.props.timetables, this.props.module);
    return hasModule ? (
      <>
        Remove from <br />
        <strong>{config.semesterNames[semester]}</strong>
      </>
    ) : (
      <>
        Add to <br />
        <strong>{config.semesterNames[semester]}</strong>
      </>
    );
  }

  otherSemesters(exclude: Semester): Semester[] {
    return getSemestersOffered(this.props.module)
      .filter((semester) => semester !== exclude)
      .sort();
  }

  override render() {
    const { block, className, module } = this.props;

    const defaultSemester = getFirstAvailableSemester(module.semesterData);
    const otherSemesters = this.otherSemesters(defaultSemester);
    const id = `add-to-timetable-${module.moduleCode}`;

    /* eslint-disable jsx-a11y/label-has-for */
    return (
      <Downshift>
        {({ getLabelProps, getItemProps, isOpen, toggleMenu, highlightedIndex, getMenuProps }) => (
          <div>
            <label {...getLabelProps({ htmlFor: id })} className="sr-only">
              Add course to timetable
            </label>

            <div
              className={classnames('btn-group', styles.buttonGroup, className, {
                'btn-block': block,
              })}
            >
              <button
                type="button"
                className={classnames('btn btn-outline-primary', {
                  'btn-block': block,
                })}
                onClick={() => this.onSelect(defaultSemester)}
              >
                {this.buttonLabel(defaultSemester)}
              </button>

              {!!otherSemesters.length && (
                <>
                  <button
                    id={id}
                    type="button"
                    className="btn btn-outline-primary dropdown-toggle dropdown-toggle-split"
                    onClick={() => toggleMenu()}
                    data-toggle="dropdown"
                    aria-haspopup="true"
                    aria-expanded={isOpen}
                  >
                    <span className="sr-only">Toggle Dropdown</span>
                  </button>

                  <div
                    className={classnames('dropdown-menu', { show: isOpen })}
                    {...getMenuProps()}
                  >
                    {otherSemesters.map((semester, index) => (
                      <button
                        {...getItemProps({ item: semester })}
                        type="button"
                        key={semester}
                        className={classnames('dropdown-item', styles.dropdownItem, {
                          'dropdown-selected': index === highlightedIndex,
                        })}
                        onClick={() => this.onSelect(semester)}
                      >
                        {this.buttonLabel(semester)}
                      </button>
                    ))}
                  </div>
                </>
              )}
            </div>
          </div>
        )}
      </Downshift>
    );
  }
}

const AddModuleDropdownConnected = connect(
  (state: StoreState) => ({
    timetables: state.timetables.lessons,
  }),
  { addModule, removeModule },
)(AddModuleDropdownComponent);

export default AddModuleDropdownConnected;