diff --git a/package.json b/package.json index cbcf165d8..98ed39ff6 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@babel/plugin-proposal-decorators": "^7.17.8", "@nestjs/throttler": "^5.0.1", "cross-env": "^7.0.3", + "fuse.js": "^7.0.0", "nodemailer": "^6.9.1" }, "devDependencies": { diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index ef6f87577..b9e33d53b 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -3,3 +3,19 @@ export const isStrongPassword = (password: string): boolean => { const containsLettersAndNumbersRegex = /^(?=.*[a-zA-Z])(?=.*[0-9])/; return password.length >= 8 && containsLettersAndNumbersRegex.test(password); }; + +/** + * Comparator function for sorting majors by name Criteria: ignores spacing and + * special characters when sorting + */ +export const majorNameComparator = (a: string, b: string) => { + const trimmedA = a + .replace(/[^A-Z0-9]/gi, "") + .trim() + .toLowerCase(); + const trimmedB = b + .replace(/[^A-Z0-9]/gi, "") + .trim() + .toLowerCase(); + return trimmedB.localeCompare(trimmedA); +}; diff --git a/packages/frontend/components/Form/Select.tsx b/packages/frontend/components/Form/Select.tsx index d255feb0c..cb9806404 100644 --- a/packages/frontend/components/Form/Select.tsx +++ b/packages/frontend/components/Form/Select.tsx @@ -4,8 +4,10 @@ import { FormErrorMessage, FormHelperText, } from "@chakra-ui/react"; +import Fuse from "fuse.js"; import { Control, FieldError, useController } from "react-hook-form"; import Select from "react-select"; +import { FilterOptionOption } from "react-select/dist/declarations/src/filters"; type PlanSelectProps = { error?: FieldError; @@ -25,6 +27,8 @@ type PlanSelectProps = { isSearchable?: boolean; /** An option in the select dropdown that indicates "no selection". */ noValueOptionLabel?: string; + /** Fuzzy options to use */ + useFuzzySearch?: boolean; }; export const PlanSelect: React.FC = ({ @@ -38,7 +42,27 @@ export const PlanSelect: React.FC = ({ isNumeric, isSearchable, noValueOptionLabel, + useFuzzySearch, }) => { + const filterOptions = useFuzzySearch + ? (option: FilterOptionOption, inputValue: string) => { + if (inputValue.length !== 0) { + const list = new Fuse(options, { + isCaseSensitive: false, + shouldSort: true, + ignoreLocation: true, + findAllMatches: true, + includeScore: true, + threshold: 0.4, + }).search(inputValue); + + return list.map((element) => element.item).includes(option.label); + } else { + return true; + } + } + : null; + const { field: { onChange: onChangeUpdateValue, value, ...fieldRest }, fieldState: { error }, @@ -90,6 +114,7 @@ export const PlanSelect: React.FC = ({ value={selectedOption} isSearchable={isSearchable} defaultValue={noValueOption} + filterOption={filterOptions} {...fieldRest} /> {helperText && {helperText}} diff --git a/packages/frontend/components/Plan/AddPlanModal.tsx b/packages/frontend/components/Plan/AddPlanModal.tsx index 48d5347ef..be612eed9 100644 --- a/packages/frontend/components/Plan/AddPlanModal.tsx +++ b/packages/frontend/components/Plan/AddPlanModal.tsx @@ -245,6 +245,7 @@ export const AddPlanModal: React.FC = ({ rules={{ required: "Major is required." }} helperText='First select your catalog year. If you still cannot find your major, select "No Major" above.' isSearchable + useFuzzySearch />