department-of-veterans-affairs/vets-website

View on GitHub
src/applications/gi/containers/ProgramsList.jsx

Summary

Maintainability
A
1 hr
Test Coverage
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import {
  VaButton,
  VaPagination,
  VaTextInput,
} from '@department-of-veterans-affairs/component-library/dist/react-bindings';
import {
  formatProgramType,
  mapToAbbreviation,
  getAbbreviationsAsArray,
} from '../utils/helpers';
import { fetchInstitutionPrograms } from '../actions';

const ProgramsList = ({ match }) => {
  const dispatch = useDispatch();
  const { loading, error, institutionPrograms } = useSelector(
    state => state.institutionPrograms,
  );
  const institutionName = localStorage.getItem('institutionName');
  const { programType, facilityCode } = match.params;
  const formattedProgramType = formatProgramType(programType);
  const abbreviatedProgramTypes = mapToAbbreviation(programType);
  const abbreviatedList = getAbbreviationsAsArray(abbreviatedProgramTypes);

  const [currentPage, setCurrentPage] = useState(1);
  const itemsPerPage = 20;

  const [searchQuery, setSearchQuery] = useState('');
  const [submittedQuery, setSubmittedQuery] = useState('');
  const [searchError, setSearchError] = useState(null);

  const [key, setKey] = useState(0);

  const triggerRerender = () => {
    setKey(prevKey => prevKey + 1);
  };

  const filteredPrograms = institutionPrograms.filter(program =>
    program.attributes.description
      ?.toLowerCase()
      .includes(submittedQuery.toLowerCase()),
  );

  useEffect(
    () => {
      window.scrollTo(0, 0);
      dispatch(fetchInstitutionPrograms(facilityCode, abbreviatedProgramTypes));
    },
    [dispatch],
  );

  const handleSearchInput = e => {
    setSearchQuery(e.target.value);
    setSearchError(null);
  };

  const handleSearchSubmit = e => {
    e.preventDefault();
    if (!searchQuery.trim()) {
      setSearchError('Please fill in a program name and then select search.');
      return;
    }
    setSubmittedQuery(searchQuery);
    setCurrentPage(1);
  };

  const handleReset = () => {
    setSearchQuery('');
    setSubmittedQuery('');
    setCurrentPage(1);
    triggerRerender();
    setSearchError(null);
  };

  // Calculate total pages and slice programs for pagination
  const totalPages = Math.ceil(filteredPrograms.length / itemsPerPage);
  const currentPrograms = filteredPrograms.slice(
    (currentPage - 1) * itemsPerPage,
    currentPage * itemsPerPage,
  );

  // Calculate start and end indices for the displayed programs
  const startIndex = (currentPage - 1) * itemsPerPage + 1;
  const endIndex = Math.min(
    currentPage * itemsPerPage,
    filteredPrograms.length,
  );

  const handlePageChange = page => {
    setCurrentPage(page);
    window.scrollTo(0, 0);
  };

  if (error) {
    return (
      <div className="row vads-u-padding--1p5 mobile-lg:vads-u-padding--0">
        <h1 className="vads-u-margin-bottom--4">{institutionName}</h1>
        <h2 className="vads-u-margin-top--0 vads-u-margin-bottom--4">
          {formattedProgramType}
        </h2>
        <va-alert status="error" data-e2e-id="alert-box">
          <h2 slot="headline">We can’t load the program list right now</h2>
          <p>
            We’re sorry. There’s a problem with our system. Try again later.
          </p>
        </va-alert>
      </div>
    );
  }
  if (loading) {
    return (
      <div className="row vads-u-padding--1p5 mobile-lg:vads-u-padding--0">
        <va-loading-indicator
          label="Loading"
          message="Loading your programs..."
        />
      </div>
    );
  }

  return (
    <div className="programs-list-container row vads-u-padding--1p5 mobile-lg:vads-u-padding--0">
      <h1 className="vads-u-margin-bottom--4">{institutionName}</h1>
      <h2 className="vads-u-margin-top--0 vads-u-margin-bottom--4">
        {formattedProgramType}
      </h2>
      <div
        className={`${institutionPrograms.length < 21 &&
          'vads-u-margin-bottom--4'}`}
      >
        <h4 className="abbreviations" data-testid="abbreviations-container">
          Abbreviation(s)
        </h4>
        {/* eslint-disable-next-line jsx-a11y/no-redundant-roles */}
        <ul className="list-style" role="list">
          {abbreviatedList.map(abb => (
            <li className="vads-u-margin-bottom--0" key={abb}>
              {abb}
            </li>
          ))}
        </ul>
      </div>
      {institutionPrograms.length > 20 && (
        <div
          key={key}
          className="search-container va-flex vads-u-align-items--flex-end"
        >
          <VaTextInput
            error={searchError}
            className="search-input"
            label="Search for a program name:"
            message-aria-describedby="Search for a program name"
            name="search-input"
            onInput={handleSearchInput}
            onKeyDown={e => e.key === 'Enter' && handleSearchSubmit(e)}
            show-input-error
          />
          <VaButton
            className="search-btn"
            onClick={handleSearchSubmit}
            text="Search"
          />
          <VaButton
            className="reset-search"
            onClick={handleReset}
            secondary
            text="Reset search"
          />
        </div>
      )}
      {filteredPrograms.length > 0 ? (
        <p id="results-summary">
          {submittedQuery ? (
            <>
              {`Showing ${startIndex}-${endIndex} of ${
                filteredPrograms.length
              } results for `}
              "<strong>{submittedQuery}</strong>"
            </>
          ) : (
            <>
              {`Showing ${startIndex}-${endIndex} of ${
                filteredPrograms.length
              } programs`}
            </>
          )}
        </p>
      ) : (
        <p id="no-results-message">
          {`We didn’t find any results for `}"
          <strong>{`${submittedQuery}`}</strong>
          ." Please enter a valid program name.
        </p>
      )}
      {/* eslint-disable-next-line jsx-a11y/no-redundant-roles */}
      <ul className="remove-bullets" role="list">
        {currentPrograms.map(({ id, attributes: { description } }) => (
          <li className="vads-u-margin-bottom--2" key={id}>
            {description}
          </li>
        ))}
      </ul>
      <VaPagination
        page={currentPage}
        pages={totalPages}
        maxPageListLength={7}
        showLastPage
        onPageSelect={e => handlePageChange(e.detail.page)}
        className="vads-u-border-top--0 vads-u-padding-top--0 vads-u-padding-bottom--5"
      />
    </div>
  );
};

ProgramsList.propTypes = {
  match: PropTypes.shape({
    params: PropTypes.shape({
      programType: PropTypes.string.isRequired,
      facilityCode: PropTypes.string.isRequired,
    }).isRequired,
  }).isRequired,
};

export default ProgramsList;