From ea34fccab6c73e1fd36add170210ca79d9910501 Mon Sep 17 00:00:00 2001 From: aadalal <57609353+AaDalal@users.noreply.github.com> Date: Fri, 22 Dec 2023 22:52:23 -0500 Subject: [PATCH 1/5] Add course attributes filtering --- Pipfile | 11 - frontend/plan/actions/index.js | 65 +++-- frontend/plan/components/analytics.tsx | 6 +- .../plan/components/search/SchoolAttrs.tsx | 230 ++++++++++++++++++ frontend/plan/components/search/SchoolReq.tsx | 163 ------------- frontend/plan/components/search/SearchBar.tsx | 95 ++++---- .../components/selector/CourseDetails.tsx | 10 +- frontend/plan/package.json | 3 +- frontend/plan/reducers/filters.js | 53 ++-- frontend/plan/types.ts | 19 +- frontend/yarn.lock | 5 + 11 files changed, 375 insertions(+), 285 deletions(-) delete mode 100644 Pipfile create mode 100644 frontend/plan/components/search/SchoolAttrs.tsx delete mode 100644 frontend/plan/components/search/SchoolReq.tsx diff --git a/Pipfile b/Pipfile deleted file mode 100644 index c398b0d5a..000000000 --- a/Pipfile +++ /dev/null @@ -1,11 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] - -[dev-packages] - -[requires] -python_version = "3.10" diff --git a/frontend/plan/actions/index.js b/frontend/plan/actions/index.js index 4389db62a..af30302f6 100644 --- a/frontend/plan/actions/index.js +++ b/frontend/plan/actions/index.js @@ -23,9 +23,9 @@ export const COURSE_SEARCH_ERROR = "COURSE_SEARCH_ERROR"; export const COURSE_SEARCH_LOADING = "COURSE_SEARCH_LOADING"; export const COURSE_SEARCH_SUCCESS = "COURSE_SEARCH_SUCCESS"; -export const LOAD_REQUIREMENTS = "LOAD_REQUIREMENTS"; -export const ADD_SCHOOL_REQ = "ADD_SCHOOL_REQ"; -export const REM_SCHOOL_REQ = "REM_SCHOOL_REQ"; +export const LOAD_ATTRIBUTES = "LOAD_ATTRIBUTES"; +export const ADD_SCHOOL_ATTR = "ADD_SCHOOL_ATTR"; +export const REM_SCHOOL_ATTR = "REM_SCHOOL_ATTR"; export const UPDATE_SEARCH_TEXT = "UPDATE_SEARCH_TEXT"; export const UPDATE_RANGE_FILTER = "UPDATE_RANGE_FILTER"; @@ -188,8 +188,8 @@ export const checkForDefaultSchedules = (schedulesFromBackend) => ( } }; -export const loadRequirements = () => (dispatch) => - doAPIRequest("/base/current/requirements/").then( +export const loadAttributes = () => (dispatch) => + doAPIRequest("/base/attributes/").then( (response) => response.json().then( (data) => { @@ -198,16 +198,29 @@ export const loadRequirements = () => (dispatch) => SEAS: [], WH: [], NURS: [], + LPS: [], + DSGN: [], + GSE: [], + LAW: [], + MED: [], + VET: [], + MODE: [], }; - const selObj = {}; + const selAttrs = {}; data.forEach((element) => { - obj[element.school].push(element); - selObj[element.id] = 0; + const school = + element.school === "NUR" ? "NURS" : element.school; + console.log("school", school); + if (obj[school] === undefined) { + return; + } + obj[school].push(element); + selAttrs[element.code] = 0; }); dispatch({ - type: LOAD_REQUIREMENTS, + type: LOAD_ATTRIBUTES, obj, - selObj, + selObj: selAttrs, }); }, (error) => { @@ -224,19 +237,19 @@ export const loadRequirements = () => (dispatch) => function buildCourseSearchUrl(filterData) { let queryString = `/base/current/search/courses/?search=${filterData.searchString}`; - // Requirements filter - const reqs = []; - if (filterData.selectedReq) { - for (const key of Object.keys(filterData.selectedReq)) { - if (filterData.selectedReq[key]) { - reqs.push(key); + // Course attribute filter (e.g., filter by Wharton CCP requirements) + const attributes = []; + if (filterData.selectedAttrs) { + for (const code of Object.keys(filterData.selectedAttrs)) { + if (filterData.selectedAttrs[code]) { + attributes.push(code); } } - if (reqs.length > 0) { - queryString += `&requirements=${reqs[0]}`; - for (let i = 1; i < reqs.length; i += 1) { - queryString += `,${reqs[i]}`; + if (attributes.length > 0) { + queryString += `&attributes=${attributes[0]}`; + for (let i = 1; i < attributes.length; i += 1) { + queryString += `*${attributes[i]}`; } } } @@ -398,17 +411,17 @@ export function sectionInfoSearchError(error) { }; } -export function addSchoolReq(reqID) { +export function addSchoolAttr(attrCode) { return { - type: ADD_SCHOOL_REQ, - reqID, + type: ADD_SCHOOL_ATTR, + attrCode, }; } -export function remSchoolReq(reqID) { +export function remSchoolAttr(attrCode) { return { - type: REM_SCHOOL_REQ, - reqID, + type: REM_SCHOOL_ATTR, + attrCode, }; } diff --git a/frontend/plan/components/analytics.tsx b/frontend/plan/components/analytics.tsx index b4ea79f69..d615d4857 100644 --- a/frontend/plan/components/analytics.tsx +++ b/frontend/plan/components/analytics.tsx @@ -1,7 +1,7 @@ import ReactGA from "react-ga"; import { - ADD_SCHOOL_REQ, - REM_SCHOOL_REQ, + ADD_SCHOOL_ATTR, + REM_SCHOOL_ATTR, UPDATE_SEARCH_TEXT, UPDATE_RANGE_FILTER, CHANGE_MY_SCHEDULE, @@ -30,7 +30,7 @@ export const logException = (description = "", fatal = false) => { } }; -const filterActions = [ADD_SCHOOL_REQ, REM_SCHOOL_REQ, UPDATE_RANGE_FILTER]; +const filterActions = [ADD_SCHOOL_ATTR, REM_SCHOOL_ATTR, UPDATE_RANGE_FILTER]; const schedActions = [ CHANGE_MY_SCHEDULE, RENAME_SCHEDULE, diff --git a/frontend/plan/components/search/SchoolAttrs.tsx b/frontend/plan/components/search/SchoolAttrs.tsx new file mode 100644 index 000000000..aebdad7d3 --- /dev/null +++ b/frontend/plan/components/search/SchoolAttrs.tsx @@ -0,0 +1,230 @@ +import React, { useState } from "react"; +import styled from "styled-components"; +import { FilterData, School, Attribute } from "../../types"; +import { + Column, + RadioInput, + RadioLabel, + CheckboxInput, + CheckboxLabel, +} from "../bulma_derived_components"; +import fuzzysort from "fuzzysort"; + +interface SchoolAttrsProps { + startSearch: (searchObj: FilterData) => void; + filterData: FilterData; + schoolAttrs: { [K in School]: Attribute[] }; + addSchoolAttr: (s: string) => void; + remSchoolAttr: (s: string) => void; +} + +const SchoolReqContainer = styled.div` + margin: -0.75rem; + padding-top: 0.2rem; + padding-left: 0.8rem; + padding-right: 0.8rem; + min-width: 27rem; + display: flex; + + p { + font-size: 0.7rem; + text-align: left; + } + + @media all and (max-width: 480px) { + font-size: 1rem; + min-width: 25rem !important; + p { + font-size: 0.8rem; + } + } +`; + +const SchoolColumn = styled.div` + display: block; + padding: 0.75rem; + flex: none; + width: 33.3333%; + + @media screen and (min-width: 769px) { + flex: none; + width: 25%; + } +`; + +const ReqBorder = styled.div` + border-right: 1px solid #c1c1c1; + display: block; + padding: 0.75rem; + flex: none; + width: 8.33333%; +`; + +const ReqColumn = styled(Column)` + height: 20rem; + overflow-y: auto; +`; + +const ReqList = styled.ul` + height: 15em; + max-height: 15em; + overflow-y: auto; +`; + +type SchoolDisplay = "College" | "Engineering" | "Nursing" | "Wharton" | "LPS" | "Veterinary" | "Design" | "Grade Mode" | "Medicine" | "GSE" | "Law"; + +export function SchoolAttrs({ + startSearch, + filterData, + schoolAttrs, + addSchoolAttr, + remSchoolAttr, +}: SchoolAttrsProps) { + const schools: SchoolDisplay[] = [ + "College", + "Engineering", + "Nursing", + "Wharton", + "Grade Mode", + "LPS", + "Veterinary", + "Design", + "Medicine", + "GSE", + "Law", + ]; + const [selSchool, setSelSchool] = useState("College"); + const [searchAttr, setSearchAttr] = useState(""); + + const schoolCode: { [K in SchoolDisplay]: School } = { + College: School.COLLEGE, + Engineering: School.SEAS, + Wharton: School.WHARTON, + Nursing: School.NURSING, + LPS: School.LPS, + Design: School.DESIGN, + "Grade Mode": School.GRADE_MODE, + Medicine: School.MEDICINE, + "GSE": School.GSE, + Law: School.LAW, + Veterinary: School.VET, + }; + + const schoolHandleChange = (event: React.ChangeEvent) => { + // Cast is sound because the value of an input comes directly from + // the "schools" variable + setSelSchool(event.target.value as SchoolDisplay); + }; + + const searchAttrs = fuzzysort.go( + searchAttr, + schoolAttrs[schoolCode[selSchool]] + .filter(attr => !filterData.selectedAttrs[attr.code]) + || [], + { keys: ["description", "code"], all: true } + ).map(result => result.obj); + + const selectedAttrs = schoolAttrs[schoolCode[selSchool]].filter(attr => filterData.selectedAttrs[attr.code]); + + return ( + + +

+ School +

+
    + {schools.map((school) => ( +
  • + + {/* eslint-disable-next-line jsx-a11y/label-has-for */} + {school} +
  • + ))} +
+
+ + +

+ {`${selSchool} Attributes`} +

+ setSearchAttr(e.target.value)} + placeholder="Search Course Attributes" + /> + + {selectedAttrs.map((attribute) => ( +
  • + { + const toggleState = !filterData.selectedAttrs[ + attribute.code + ]; + if (filterData.selectedAttrs[attribute.code]) { + remSchoolAttr(attribute.code); + } else { + addSchoolAttr(attribute.code); + } + startSearch({ + ...filterData, + selectedAttrs: { + ...filterData.selectedAttrs, + [attribute.code]: toggleState, + }, + }); + }} + /> + {/* eslint-disable-next-line jsx-a11y/label-has-for */} + + {attribute.description} + +
  • + ))} + {searchAttrs.map((attribute) => ( +
  • + { + const toggleState = !filterData.selectedAttrs[ + attribute.code + ]; + if (filterData.selectedAttrs[attribute.code]) { + remSchoolAttr(attribute.code); + } else { + addSchoolAttr(attribute.code); + } + startSearch({ + ...filterData, + selectedAttrs: { + ...filterData.selectedAttrs, + [attribute.code]: toggleState, + }, + }); + }} + /> + {/* eslint-disable-next-line jsx-a11y/label-has-for */} + + {attribute.description} + +
  • + ))} +
    +
    +
    + ); +} diff --git a/frontend/plan/components/search/SchoolReq.tsx b/frontend/plan/components/search/SchoolReq.tsx deleted file mode 100644 index 1dab99d07..000000000 --- a/frontend/plan/components/search/SchoolReq.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import React, { useState } from "react"; -import styled from "styled-components"; -import { FilterData, School, Requirement } from "../../types"; -import { - Column, - RadioInput, - RadioLabel, - CheckboxInput, - CheckboxLabel, -} from "../bulma_derived_components"; - -interface SchoolReqProps { - startSearch: (searchObj: FilterData) => void; - filterData: FilterData; - schoolReq: { [K in School]: Requirement[] }; - addSchoolReq: (s: string) => void; - remSchoolReq: (s: string) => void; -} - -const SchoolReqContainer = styled.div` - margin: -0.75rem; - padding-top: 0.2rem; - padding-left: 0.8rem; - padding-right: 0.8rem; - min-width: 27rem; - display: flex; - - p { - font-size: 0.7rem; - text-align: left; - } - - @media all and (max-width: 480px) { - font-size: 1rem; - min-width: 25rem !important; - p { - font-size: 0.8rem; - } - } -`; - -const SchoolColumn = styled.div` - display: block; - padding: 0.75rem; - flex: none; - width: 33.3333%; - - @media screen and (min-width: 769px) { - flex: none; - width: 25%; - } -`; - -const ReqBorder = styled.div` - border-right: 1px solid #c1c1c1; - display: block; - padding: 0.75rem; - flex: none; - width: 8.33333%; -`; - -const ReqColumn = styled.div` - display: block; - padding: 0.75rem; -`; - -type SchoolDisplay = "College" | "Engineering" | "Nursing" | "Wharton"; - -export function SchoolReq({ - startSearch, - filterData, - schoolReq, - addSchoolReq, - remSchoolReq, -}: SchoolReqProps) { - const schools: SchoolDisplay[] = [ - "College", - "Engineering", - "Nursing", - "Wharton", - ]; - const [selSchool, setSelSchool] = useState("College"); - - const schoolCode: { [K in SchoolDisplay]: School } = { - College: School.COLLEGE, - Engineering: School.SEAS, - Wharton: School.WHARTON, - Nursing: School.NURSING, - }; - - const schoolHandleChange = (event: React.ChangeEvent) => { - // Cast is sound because the value of an input comes directly from - // the "schools" variable - setSelSchool(event.target.value as SchoolDisplay); - }; - - return ( - - -

    - School -

    -
      - {schools.map((school) => ( -
    • - - {/* eslint-disable-next-line jsx-a11y/label-has-for */} - {school} -
    • - ))} -
    -
    - - -

    - {`${selSchool} Requirements`} -

    -
      - {selSchool === "Nursing" && ( -

      Nursing requirements are coming soon!

      - )} - {schoolReq[schoolCode[selSchool]].map((req) => ( -
    • - { - const toggleState = !filterData.selectedReq[ - req.id - ]; - if (filterData.selectedReq[req.id]) { - remSchoolReq(req.id); - } else { - addSchoolReq(req.id); - } - startSearch({ - ...filterData, - selectedReq: { - ...filterData.selectedReq, - [req.id]: toggleState, - }, - }); - }} - /> - {/* eslint-disable-next-line jsx-a11y/label-has-for */} - - {req.name} - -
    • - ))} -
    -
    -
    - ); -} diff --git a/frontend/plan/components/search/SearchBar.tsx b/frontend/plan/components/search/SearchBar.tsx index 04c876844..2c79e2ce5 100644 --- a/frontend/plan/components/search/SearchBar.tsx +++ b/frontend/plan/components/search/SearchBar.tsx @@ -7,20 +7,20 @@ import AccountIndicator from "pcx-shared-components/src/accounts/AccountIndicato import { useRouter } from "next/router"; import { DropdownButton } from "./DropdownButton"; import { ButtonFilter } from "./ButtonFilter"; -import { SchoolReq } from "./SchoolReq"; +import { SchoolAttrs } from "./SchoolAttrs"; import { RangeFilter } from "./RangeFilter"; import { CheckboxFilter } from "./CheckboxFilter"; import { DayTimeFilter } from "./DayTimeFilter"; import { SearchField } from "./SearchField"; import { initialState as defaultFilters } from "../../reducers/filters"; import initiateSync from "../syncutils"; -import { FilterData, User, Requirement } from "../../types"; +import { FilterData, User, Attribute } from "../../types"; import { fetchCourseSearch, - loadRequirements, - addSchoolReq, - remSchoolReq, + loadAttributes, + addSchoolAttr, + remSchoolAttr, updateSearchText, updateRangeFilter, updateCheckboxFilter, @@ -37,21 +37,21 @@ const DAY_TIME_ENABLED = true; // removed: interface SearchBarProps { startSearch: (searchObj: FilterData) => void; - loadRequirements: () => void; - schoolReq: { - SEAS: Requirement[]; - WH: Requirement[]; - SAS: Requirement[]; - NURS: Requirement[]; + loadAttributes: () => void; + schoolAttrs: { + SEAS: Attribute[]; + WH: Attribute[]; + SAS: Attribute[]; + NURS: Attribute[]; }; filterData: FilterData; - addSchoolReq: (school: string) => void; - remSchoolReq: (school: string) => void; + addSchoolAttr: (school: string) => void; + remSchoolAttr: (school: string) => void; updateSearchText: (text: string) => void; updateRangeFilter: (field: string) => (values: [number, number]) => void; clearAll: () => void; clearFilter: (search: string) => void; - defaultReqs: { [x: string]: boolean }; + defaultAttrs: { [x: string]: boolean }; clearSearchResults: () => void; isLoadingCourseInfo: boolean; isSearchingCourseInfo: boolean; @@ -77,16 +77,16 @@ interface SearchBarProps { function shouldSearch(filterData: FilterData) { const searchString = filterData.searchString.length >= 3; - let selectedReq = false; - if (filterData.selectedReq) { - for (const key of Object.keys(filterData.selectedReq)) { - if (filterData.selectedReq[key]) { - selectedReq = true; + let selectedAttrs = false; + if (filterData.selectedAttrs) { + for (const code of Object.keys(filterData.selectedAttrs)) { + if (filterData.selectedAttrs[code]) { + selectedAttrs = true; break; } } } - return searchString || selectedReq; + return searchString || selectedAttrs; } const MobileSearchBarOuterContainer = styled.div` @@ -272,16 +272,16 @@ const DropdownContainer = styled.div` function SearchBar({ /* eslint-disable no-shadow */ startSearch, // from redux - dispatches fetch course search function (actions/index.js) - loadRequirements, - schoolReq, + loadAttributes, + schoolAttrs, filterData, - addSchoolReq, - remSchoolReq, + addSchoolAttr, + remSchoolAttr, updateSearchText, updateRangeFilter, clearAll, clearFilter, - defaultReqs, + defaultAttrs, clearSearchResults, isLoadingCourseInfo, isSearchingCourseInfo, @@ -303,10 +303,9 @@ function SearchBar({ SearchBarProps) { const router = useRouter(); - //TODO: Add requirements support back - // useEffect(() => { - // loadRequirements(); - // }, [loadRequirements]); + useEffect(() => { + loadAttributes(); + }, [loadAttributes]); useEffect(() => { // ensure that the user is logged in before initiating the sync @@ -328,10 +327,10 @@ SearchBarProps) { property: V ) => () => { clearFilter(property); - if (property === "selectedReq") { + if (property === "selectedAttrs") { conditionalStartSearch({ ...filterData, - selectedReq: defaultReqs, + selectedAttrs: defaultAttrs, }); } else { conditionalStartSearch({ @@ -368,14 +367,20 @@ SearchBarProps) { const dropDowns = ( - {/* + - // TODO: re-enable */} + ({ - schoolReq: state.filters.schoolReq, + schoolAttrs: state.filters.schoolAttrs, filterData: state.filters.filterData, - defaultReqs: state.filters.defaultReqs, + defaultAttrs: state.filters.defaultAttrs, isLoadingCourseInfo: state.sections.courseInfoLoading, isSearchingCourseInfo: state.sections.searchInfoLoading, user: state.login.user, @@ -674,11 +679,11 @@ const mapDispatchToProps = (dispatch) => ({ login: (user: User) => dispatch(login(user)), logout: () => dispatch(logout()), clearAllScheduleData: () => dispatch(clearAllScheduleData()), - loadRequirements: () => dispatch(loadRequirements()), + loadAttributes: () => dispatch(loadAttributes()), startSearch: (filterData: FilterData) => dispatch(fetchCourseSearch(filterData)), - addSchoolReq: (reqID: string) => dispatch(addSchoolReq(reqID)), - remSchoolReq: (reqID: string) => dispatch(remSchoolReq(reqID)), + addSchoolAttr: (reqID: string) => dispatch(addSchoolAttr(reqID)), + remSchoolAttr: (reqID: string) => dispatch(remSchoolAttr(reqID)), updateSearchText: (s: string) => dispatch(updateSearchText(s)), updateRangeFilter: (field: string) => (values: [number, number]) => dispatch(updateRangeFilter(field, values)), diff --git a/frontend/plan/components/selector/CourseDetails.tsx b/frontend/plan/components/selector/CourseDetails.tsx index 1226c2e5c..deab2eadd 100644 --- a/frontend/plan/components/selector/CourseDetails.tsx +++ b/frontend/plan/components/selector/CourseDetails.tsx @@ -19,8 +19,6 @@ const schoolToLetter = (school: School) => { return ""; } }; -const getReqCode = (school: School, name: string) => - `${schoolToLetter(school)}: ${name}`; const annotatePrerequisites = ( text: string | null, @@ -95,7 +93,7 @@ const Icon = styled.span` export default function CourseDetails({ course: { - requirements = [], + attributes = [], crosslistings = [], description, prerequisites: prereqText, @@ -119,15 +117,15 @@ export default function CourseDetails({   Difficulty:   - {requirements.length > 0 ? ( + {attributes.length > 0 ? (
  •   Fulfills:   - getReqCode(school, name) + elements={attributes.map(({ school, description }) => + description )} limit={1} /> diff --git a/frontend/plan/package.json b/frontend/plan/package.json index d2bde68f1..9f6c040c2 100644 --- a/frontend/plan/package.json +++ b/frontend/plan/package.json @@ -28,6 +28,7 @@ "bulma-popover": "^1.0.0", "cross-fetch": "^3.0.4", "express": "^4.17.1", + "fuzzysort": "^2.0.4", "http-proxy-middleware": "^1.0.4", "ics": "^2.37.0", "next": "9.4.2", @@ -66,4 +67,4 @@ "prettier": "^2.0.5", "redux-logger": "^3.0.6" } -} \ No newline at end of file +} diff --git a/frontend/plan/reducers/filters.js b/frontend/plan/reducers/filters.js index 9398cdfb6..4dbd62aa3 100644 --- a/frontend/plan/reducers/filters.js +++ b/frontend/plan/reducers/filters.js @@ -1,7 +1,7 @@ import { - LOAD_REQUIREMENTS, - ADD_SCHOOL_REQ, - REM_SCHOOL_REQ, + LOAD_ATTRIBUTES, + ADD_SCHOOL_ATTR, + REM_SCHOOL_ATTR, UPDATE_SEARCH_TEXT, UPDATE_RANGE_FILTER, CLEAR_FILTER, @@ -11,16 +11,23 @@ import { } from "../actions"; export const initialState = { - schoolReq: { + schoolAttrs: { SAS: [], SEAS: [], NURS: [], WH: [], + LPS: [], + DSGN: [], + GSE: [], + LAW: [], + MED: [], + VET: [], + MODE: [], }, filterData: { searchString: "", searchType: "courseIDSearch", - selectedReq: null, + selectedAttrs: null, difficulty: [0, 4], course_quality: [0, 4], instructor_quality: [0, 4], @@ -48,20 +55,20 @@ export const initialState = { "schedule-fit": -1, is_open: 0, }, - defaultReqs: null, + defaultAttrs: null, }; export const filters = (state = initialState, action) => { switch (action.type) { - case LOAD_REQUIREMENTS: + case LOAD_ATTRIBUTES: return { ...state, - schoolReq: action.obj, + schoolAttrs: action.obj, filterData: { ...state.filterData, - selectedReq: action.selObj, + selectedAttrs: action.selObj, }, - defaultReqs: action.selObj, + defaultAttrs: action.selObj, }; case UPDATE_SEARCH_TEXT: @@ -73,26 +80,26 @@ export const filters = (state = initialState, action) => { }, }; - case ADD_SCHOOL_REQ: + case ADD_SCHOOL_ATTR: return { ...state, filterData: { ...state.filterData, - selectedReq: { - ...state.filterData.selectedReq, - [action.reqID]: 1, + selectedAttrs: { + ...state.filterData.selectedAttrs, + [action.attrCode]: 1, }, }, }; - case REM_SCHOOL_REQ: + case REM_SCHOOL_ATTR: return { ...state, filterData: { ...state.filterData, - selectedReq: { - ...state.filterData.selectedReq, - [action.reqID]: 0, + selectedAttrs: { + ...state.filterData.selectedAttrs, + [action.attrCode]: 0, }, }, }; @@ -129,12 +136,12 @@ export const filters = (state = initialState, action) => { }; case CLEAR_FILTER: - if (action.propertyName === "selectedReq") { + if (action.propertyName === "selectedAttrs") { return { ...state, filterData: { ...state.filterData, - selectedReq: state.defaultReqs, + selectedAttrs: state.defaultAttrs, }, }; } @@ -154,10 +161,10 @@ export const filters = (state = initialState, action) => { filterData: { ...initialState.filterData, searchString: state.filterData.searchString, - selectedReq: state.defaultReqs, + selectedAttrs: state.defaultAttrs, }, - defaultReqs: state.defaultReqs, - schoolReq: state.schoolReq, + defaultAttrs: state.defaultAttrs, + schoolAttrs: state.schoolAttrs, }; default: return state; diff --git a/frontend/plan/types.ts b/frontend/plan/types.ts index 7057941b5..7cda0f0c8 100644 --- a/frontend/plan/types.ts +++ b/frontend/plan/types.ts @@ -1,8 +1,15 @@ -export enum School { +export enum School { // TODO: need to add more here? SEAS = "SEAS", WHARTON = "WH", COLLEGE = "SAS", NURSING = "NURS", + LPS = "LPS", + DESIGN = "DSGN", + GSE = "GSE", + LAW = "LAW", + MEDICINE = "MED", + GRADE_MODE = "MODE", // this isn't really a school, but it's used for course attributes + VET = "VET" } export enum Status { @@ -119,12 +126,10 @@ export interface Profile { phone: string | null; } -export interface Requirement { - id: string; +export interface Attribute { code: string; school: School; - semester: string; - name: string; + description: string; } export interface Course { @@ -140,7 +145,7 @@ export interface Course { recommendation_score: number; work_required: number; crosslistings?: string[]; - requirements?: Requirement[]; + attributes?: Attribute[]; num_sections: number; } @@ -177,7 +182,7 @@ export interface Friendship { export interface FilterData { searchString: string; searchType: string; - selectedReq: { [K in string]: boolean }; + selectedAttrs: { [K in string]: boolean }; difficulty: [number, number]; course_quality: [number, number]; // upper and lower bound for course_quality instructor_quality: [number, number]; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 864664eeb..8c061cdaa 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -7737,6 +7737,11 @@ fuzzysort@^1.1.4: resolved "https://registry.yarnpkg.com/fuzzysort/-/fuzzysort-1.1.4.tgz#a0510206ed44532cbb52cf797bf5a3cb12acd4ba" integrity sha512-JzK/lHjVZ6joAg3OnCjylwYXYVjRiwTY6Yb25LvfpJHK8bjisfnZJ5bY8aVWwTwCXgxPNgLAtmHL+Hs5q1ddLQ== +fuzzysort@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/fuzzysort/-/fuzzysort-2.0.4.tgz#a21d1ce8947eaf2797dc3b7c28c36db9d1165f84" + integrity sha512-Api1mJL+Ad7W7vnDZnWq5pGaXJjyencT+iKGia2PlHUcSsSzWwIQ3S1isiMpwpavjYtGd2FzhUIhnnhOULZgDw== + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" From 8bd6cb257e663188acfe1665b2e955874cf5fd35 Mon Sep 17 00:00:00 2001 From: aadalal <57609353+AaDalal@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:19:12 -0500 Subject: [PATCH 2/5] Add counts for the attributes --- .../plan/components/search/SchoolAttrs.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/frontend/plan/components/search/SchoolAttrs.tsx b/frontend/plan/components/search/SchoolAttrs.tsx index aebdad7d3..0f137a44a 100644 --- a/frontend/plan/components/search/SchoolAttrs.tsx +++ b/frontend/plan/components/search/SchoolAttrs.tsx @@ -71,6 +71,18 @@ const ReqList = styled.ul` overflow-y: auto; `; +const AttrCount = styled.span` + background: #ea5a48; + color: #fff; + border-radius: 0.875rem; + font-size: 0.5625rem; + font-weight: 700; + letter-spacing: -0.5px; + margin-left: 0.25rem; + padding: .125rem .375rem; + text-align: center; +`; + type SchoolDisplay = "College" | "Engineering" | "Nursing" | "Wharton" | "LPS" | "Veterinary" | "Design" | "Grade Mode" | "Medicine" | "GSE" | "Law"; export function SchoolAttrs({ @@ -116,6 +128,10 @@ export function SchoolAttrs({ setSelSchool(event.target.value as SchoolDisplay); }; + const countSchoolAttrs = (school: SchoolDisplay) => { + return schoolAttrs[schoolCode[school]].filter(attr => filterData.selectedAttrs[attr.code]).length; + } + const searchAttrs = fuzzysort.go( searchAttr, schoolAttrs[schoolCode[selSchool]] @@ -143,7 +159,12 @@ export function SchoolAttrs({ onChange={schoolHandleChange} /> {/* eslint-disable-next-line jsx-a11y/label-has-for */} - {school} + + {school} + {countSchoolAttrs(school) > 0 && + {countSchoolAttrs(school)} + } +
  • ))} From b8e923e0426d71d9176db5ade790e0a4b8d57d5a Mon Sep 17 00:00:00 2001 From: aadalal <57609353+AaDalal@users.noreply.github.com> Date: Fri, 22 Dec 2023 23:26:56 -0500 Subject: [PATCH 3/5] Add addnl schools --- frontend/plan/components/search/SearchBar.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/plan/components/search/SearchBar.tsx b/frontend/plan/components/search/SearchBar.tsx index 2c79e2ce5..75390ab89 100644 --- a/frontend/plan/components/search/SearchBar.tsx +++ b/frontend/plan/components/search/SearchBar.tsx @@ -43,6 +43,13 @@ interface SearchBarProps { WH: Attribute[]; SAS: Attribute[]; NURS: Attribute[]; + LPS: Attribute[]; + GSE: Attribute[]; + MED: Attribute[]; + VET: Attribute[]; + MODE: Attribute[]; + DSGN: Attribute[]; + LAW: Attribute[]; }; filterData: FilterData; addSchoolAttr: (school: string) => void; From 2e7738d8f073731d05237496b880c9a10253eb0c Mon Sep 17 00:00:00 2001 From: aadalal <57609353+AaDalal@users.noreply.github.com> Date: Sat, 27 Jan 2024 17:06:42 -0500 Subject: [PATCH 4/5] Prevent checking checkbox from causing modal to close --- frontend/plan/components/search/SchoolAttrs.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/plan/components/search/SchoolAttrs.tsx b/frontend/plan/components/search/SchoolAttrs.tsx index 0f137a44a..1e515ea78 100644 --- a/frontend/plan/components/search/SchoolAttrs.tsx +++ b/frontend/plan/components/search/SchoolAttrs.tsx @@ -189,7 +189,7 @@ export function SchoolAttrs({ type="checkbox" value={attribute.code} checked={filterData.selectedAttrs[attribute.code]} - onChange={() => { + onChange={(event) => { const toggleState = !filterData.selectedAttrs[ attribute.code ]; @@ -205,6 +205,9 @@ export function SchoolAttrs({ [attribute.code]: toggleState, }, }); + + // Prevent this event from propogating and causing the modal to close + event.stopPropagation() }} /> {/* eslint-disable-next-line jsx-a11y/label-has-for */} @@ -220,7 +223,7 @@ export function SchoolAttrs({ type="checkbox" value={attribute.code} checked={filterData.selectedAttrs[attribute.code]} - onChange={() => { + onChange={(event) => { const toggleState = !filterData.selectedAttrs[ attribute.code ]; @@ -236,6 +239,9 @@ export function SchoolAttrs({ [attribute.code]: toggleState, }, }); + + // Prevent this event from propogating and causing the modal to close + event.stopPropagation() }} /> {/* eslint-disable-next-line jsx-a11y/label-has-for */} From 645c5dbc06bdf4df511b387ad43f99758f47fba7 Mon Sep 17 00:00:00 2001 From: aadalal <57609353+AaDalal@users.noreply.github.com> Date: Sat, 27 Jan 2024 17:11:30 -0500 Subject: [PATCH 5/5] remove console.log --- frontend/plan/actions/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/plan/actions/index.js b/frontend/plan/actions/index.js index af30302f6..a86c131ae 100644 --- a/frontend/plan/actions/index.js +++ b/frontend/plan/actions/index.js @@ -210,7 +210,6 @@ export const loadAttributes = () => (dispatch) => data.forEach((element) => { const school = element.school === "NUR" ? "NURS" : element.school; - console.log("school", school); if (obj[school] === undefined) { return; }