From d8c6b5bad0001ff858b2aee3b731d680096ebd8d Mon Sep 17 00:00:00 2001 From: Xavilien Date: Fri, 16 Feb 2024 00:06:38 -0500 Subject: [PATCH 01/16] MVP for professor search --- backend/src/app.ts | 3 ++ backend/src/controllers/professors.mts | 31 +++++++++++ frontend/src/app/api/professors.ts | 24 +++++++++ frontend/src/app/cache.ts | 24 +++++++++ frontend/src/app/professors.ts | 21 ++++++++ frontend/src/app/store.ts | 10 ++++ frontend/src/components/InstructorDetail.tsx | 1 - frontend/src/components/ProfessorSearch.tsx | 42 +++++++++++++++ .../src/components/ProfessorSearchList.tsx | 53 +++++++++++++++++++ frontend/src/components/SideNav.tsx | 7 +++ frontend/src/pages/professors.tsx | 31 +++++++++++ 11 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 backend/src/controllers/professors.mts create mode 100644 frontend/src/app/api/professors.ts create mode 100644 frontend/src/app/professors.ts create mode 100644 frontend/src/components/ProfessorSearch.tsx create mode 100644 frontend/src/components/ProfessorSearchList.tsx create mode 100644 frontend/src/pages/professors.tsx diff --git a/backend/src/app.ts b/backend/src/app.ts index 29ac801..1951046 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -6,6 +6,7 @@ import { generateSigningRequestHandler, KeyStore } from "passlink-server"; import { isUser } from "./controllers/user.mjs"; import { getAllCourses, getCourseByID, getCourses, getFilteredCourses } from "./controllers/courses.mjs"; import { getFCEs } from "./controllers/fces.mjs"; +import { getProfessors } from "./controllers/professors.mjs"; // because there is a bug in the typing for passlink // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -44,6 +45,8 @@ app.route("/courses/search/").post(isUser, getFilteredCourses); app.route("/fces").post(isUser, getFCEs); +app.route("/professors").get(getProfessors); + // the next parameter is needed! // eslint-disable-next-line @typescript-eslint/no-unused-vars const errorHandler: ErrorRequestHandler = (err, req, res, next) => { diff --git a/backend/src/controllers/professors.mts b/backend/src/controllers/professors.mts new file mode 100644 index 0000000..78e780e --- /dev/null +++ b/backend/src/controllers/professors.mts @@ -0,0 +1,31 @@ +import { RequestHandler } from "express"; +import { PrismaReturn } from "../util.mjs"; +import prisma from "../models/prisma.mjs"; + +const getAllProfessorsDbQuery = { + select: { + name: true, + }, +}; + +export interface GetProfessors { + params: unknown; + resBody: PrismaReturn>; + reqBody: unknown; + query: unknown; +} + +export const getProfessors: RequestHandler< + GetProfessors["params"], + GetProfessors["resBody"], + GetProfessors["reqBody"], + GetProfessors["query"] +> = async (req, res, next) => { + try { + const professors = await prisma.professors.findMany(getAllProfessorsDbQuery); + professors.sort((a, b) => a.name.localeCompare(b.name)); + res.json(professors); + } catch (e) { + next(e); + } +}; diff --git a/frontend/src/app/api/professors.ts b/frontend/src/app/api/professors.ts new file mode 100644 index 0000000..d1336c3 --- /dev/null +++ b/frontend/src/app/api/professors.ts @@ -0,0 +1,24 @@ +import { createAsyncThunk } from "@reduxjs/toolkit"; +import { RootState } from "../store"; + +type FetchAllProfessorsType = { name: string }[]; + +export const fetchAllProfessors = createAsyncThunk< + FetchAllProfessorsType, + void, + { state: RootState } +>("fetchAllProfessors", async (_, thunkAPI) => { + const url = `${process.env.backendUrl}/professors`; + const state = thunkAPI.getState(); + + if (state.cache.allProfessors.length > 0) return; + + return ( + await fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + ).json(); +}); diff --git a/frontend/src/app/cache.ts b/frontend/src/app/cache.ts index 9c5b14a..bed9c48 100644 --- a/frontend/src/app/cache.ts +++ b/frontend/src/app/cache.ts @@ -9,6 +9,7 @@ import { FetchCourseInfosByPageResult, } from "./api/course"; import { fetchFCEInfosByCourse, fetchFCEInfosByInstructor } from "./api/fce"; +import { fetchAllProfessors } from "./api/professors"; /** * This cache lasts for the duration of the user session @@ -29,6 +30,9 @@ interface CacheState { coursesLoading: boolean; exactResultsCourses: string[]; allCourses: { courseID: string; name: string }[]; + allProfessors: { name: string }[]; + professorsLoading: boolean; + professorPage: number; } const initialState: CacheState = { @@ -45,6 +49,9 @@ const initialState: CacheState = { coursesLoading: false, exactResultsCourses: [], allCourses: [], + allProfessors: [], + professorsLoading: false, + professorPage: 1, }; export const selectCourseResults = @@ -78,6 +85,11 @@ export const selectFCEResultsForInstructor = (state: RootState): FCE[] | undefined => state.cache.instructorResults[name]; +export const selectProfessors = (search: string) => (state: RootState) => + state.cache.allProfessors.filter((prof) => + prof.name.toLowerCase().includes(search.toLowerCase()) + ); + export const cacheSlice = createSlice({ name: "cache", initialState, @@ -92,6 +104,9 @@ export const cacheSlice = createSlice({ setCoursesLoading: (state, action: PayloadAction) => { state.coursesLoading = action.payload; }, + setProfessorPage: (state, action: PayloadAction) => { + state.professorPage = action.payload; + }, }, extraReducers: (builder) => { builder @@ -187,6 +202,15 @@ export const cacheSlice = createSlice({ state.instructorResults[action.meta.arg] = action.payload; }); + + builder + .addCase(fetchAllProfessors.pending, (state) => { + state.professorsLoading = true; + }) + .addCase(fetchAllProfessors.fulfilled, (state, action) => { + state.professorsLoading = false; + if (action.payload) state.allProfessors = action.payload; + }); }, }); diff --git a/frontend/src/app/professors.ts b/frontend/src/app/professors.ts new file mode 100644 index 0000000..f99420b --- /dev/null +++ b/frontend/src/app/professors.ts @@ -0,0 +1,21 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +export interface ProfessorsState { + search: string; +} + +const initialState: ProfessorsState = { + search: "", +}; + +export const professorsSlice = createSlice({ + name: "professors", + initialState, + reducers: { + updateSearch: (state, action: PayloadAction) => { + state.search = action.payload; + }, + }, +}); + +export const reducer = professorsSlice.reducer; diff --git a/frontend/src/app/store.ts b/frontend/src/app/store.ts index fb38abd..c660ddf 100644 --- a/frontend/src/app/store.ts +++ b/frontend/src/app/store.ts @@ -9,6 +9,7 @@ import { UserSchedulesState, } from "./userSchedules"; import { reducer as uiReducer, UIState } from "./ui"; +import { reducer as professorsReducer, ProfessorsState } from "./professors"; import debounce from "lodash/debounce"; import { FLUSH, @@ -62,6 +63,15 @@ const reducers = combineReducers({ }, uiReducer ), + professors: persistReducer( + { + key: "professors", + version: 1, + storage, + stateReconciler: autoMergeLevel2, + }, + professorsReducer + ), }); export const store = configureStore({ diff --git a/frontend/src/components/InstructorDetail.tsx b/frontend/src/components/InstructorDetail.tsx index 9e1ef08..3b91278 100644 --- a/frontend/src/components/InstructorDetail.tsx +++ b/frontend/src/components/InstructorDetail.tsx @@ -16,7 +16,6 @@ const InstructorDetail = ({ name }: Props) => { const loggedIn = useAppSelector((state) => state.user.loggedIn); const fces = useAppSelector(selectFCEResultsForInstructor(name)); - console.log(fces); useEffect(() => { if (name) void dispatch(fetchFCEInfosByInstructor(name)); diff --git a/frontend/src/components/ProfessorSearch.tsx b/frontend/src/components/ProfessorSearch.tsx new file mode 100644 index 0000000..52f9434 --- /dev/null +++ b/frontend/src/components/ProfessorSearch.tsx @@ -0,0 +1,42 @@ +import { MagnifyingGlassIcon } from "@heroicons/react/24/solid"; +import React from "react"; +import { useAppDispatch, useAppSelector } from "../app/hooks"; +import { professorsSlice } from "../app/professors"; +import { cacheSlice } from "../app/cache"; +import {selectProfessors} from "../app/cache"; + +const ProfessorSearch = () => { + const dispatch = useAppDispatch(); + const search = useAppSelector((state) => state.professors.search); + + const onChange = (e: React.ChangeEvent) => { + dispatch(professorsSlice.actions.updateSearch(e.target.value)); + dispatch(cacheSlice.actions.setProfessorPage(1)); + }; + + const results = useAppSelector(selectProfessors(search)); + const numResults = results.length; + + return ( + <> +
+ + + + +
+
+
{numResults} results
+
+ + ); +}; + +export default ProfessorSearch; diff --git a/frontend/src/components/ProfessorSearchList.tsx b/frontend/src/components/ProfessorSearchList.tsx new file mode 100644 index 0000000..b1f7ff0 --- /dev/null +++ b/frontend/src/components/ProfessorSearchList.tsx @@ -0,0 +1,53 @@ +import { useAppDispatch, useAppSelector } from "../app/hooks"; +import Loading from "./Loading"; +import { Pagination } from "./Pagination"; +import React, { useEffect } from "react"; +import InstructorDetail from "./InstructorDetail"; +import { fetchAllProfessors } from "../app/api/professors"; +import { selectProfessors, cacheSlice } from "../app/cache"; + +const ProfessorSearchList = () => { + const dispatch = useAppDispatch(); + + useEffect(() => { + void dispatch(fetchAllProfessors()); + }, []); + + const search = useAppSelector((state) => state.professors.search); + const results = useAppSelector(selectProfessors(search)); + const pages = Math.ceil(results.length / 10) + const curPage = useAppSelector((state) => state.cache.professorPage); + const loading = useAppSelector((state) => state.cache.professorsLoading); + + const handlePageClick = (page: number) => { + dispatch(cacheSlice.actions.setProfessorPage(page + 1)); + }; + + return ( +
+ {loading ? ( + + ) : ( + <> +
+ {results && + results + .slice(curPage * 10 - 10, curPage * 10) + .map((professor) => ( + + ))} +
+
+ +
+ + )} +
+ ); +}; + +export default ProfessorSearchList; diff --git a/frontend/src/components/SideNav.tsx b/frontend/src/components/SideNav.tsx index e10b6d0..375a934 100644 --- a/frontend/src/components/SideNav.tsx +++ b/frontend/src/components/SideNav.tsx @@ -3,6 +3,7 @@ import { ClockIcon, MagnifyingGlassIcon, StarIcon, + UserCircleIcon, } from "@heroicons/react/24/outline"; import React from "react"; import Link from "next/link"; @@ -80,6 +81,12 @@ export const SideNav = ({ activePage }) => { newTab active={false} /> + ); }; diff --git a/frontend/src/pages/professors.tsx b/frontend/src/pages/professors.tsx new file mode 100644 index 0000000..f1df367 --- /dev/null +++ b/frontend/src/pages/professors.tsx @@ -0,0 +1,31 @@ +import type { NextPage } from "next"; +import Topbar from "../components/Topbar"; +import ProfessorSearch from "../components/ProfessorSearch"; +import ProfessorSearchList from "../components/ProfessorSearchList"; +import React from "react"; +import { Page } from "../components/Page"; + +const ProfessorsPage: NextPage = () => { + return ( + + Work in progress + {/**/} + {/**/} + + } + content={ + <> + + + + + + } + activePage="professors" + /> + ); +}; + +export default ProfessorsPage; From cb48cfe5f21c66ee359037b36a52f6034906bd4a Mon Sep 17 00:00:00 2001 From: Xavilien Date: Fri, 16 Feb 2024 09:10:29 -0500 Subject: [PATCH 02/16] Only change the page if page is not already the first --- frontend/src/components/ProfessorSearch.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/ProfessorSearch.tsx b/frontend/src/components/ProfessorSearch.tsx index 52f9434..9e060a1 100644 --- a/frontend/src/components/ProfessorSearch.tsx +++ b/frontend/src/components/ProfessorSearch.tsx @@ -7,11 +7,12 @@ import {selectProfessors} from "../app/cache"; const ProfessorSearch = () => { const dispatch = useAppDispatch(); + const page = useAppSelector((state) => state.cache.professorPage); const search = useAppSelector((state) => state.professors.search); const onChange = (e: React.ChangeEvent) => { dispatch(professorsSlice.actions.updateSearch(e.target.value)); - dispatch(cacheSlice.actions.setProfessorPage(1)); + if (page !== 1) dispatch(cacheSlice.actions.setProfessorPage(1)); }; const results = useAppSelector(selectProfessors(search)); From 752b14dd16ac6418de479400ba3182a10dd1c09b Mon Sep 17 00:00:00 2001 From: Xavilien Date: Fri, 16 Feb 2024 09:10:51 -0500 Subject: [PATCH 03/16] Use constant for number of results per page --- frontend/src/components/ProfessorSearchList.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/ProfessorSearchList.tsx b/frontend/src/components/ProfessorSearchList.tsx index b1f7ff0..3bf97cb 100644 --- a/frontend/src/components/ProfessorSearchList.tsx +++ b/frontend/src/components/ProfessorSearchList.tsx @@ -6,6 +6,8 @@ import InstructorDetail from "./InstructorDetail"; import { fetchAllProfessors } from "../app/api/professors"; import { selectProfessors, cacheSlice } from "../app/cache"; +const RESULTS_PER_PAGE = 5; + const ProfessorSearchList = () => { const dispatch = useAppDispatch(); @@ -15,7 +17,7 @@ const ProfessorSearchList = () => { const search = useAppSelector((state) => state.professors.search); const results = useAppSelector(selectProfessors(search)); - const pages = Math.ceil(results.length / 10) + const pages = Math.ceil(results.length / RESULTS_PER_PAGE); const curPage = useAppSelector((state) => state.cache.professorPage); const loading = useAppSelector((state) => state.cache.professorsLoading); @@ -32,7 +34,7 @@ const ProfessorSearchList = () => {
{results && results - .slice(curPage * 10 - 10, curPage * 10) + .slice(curPage * RESULTS_PER_PAGE - RESULTS_PER_PAGE, curPage * RESULTS_PER_PAGE) .map((professor) => ( ))} From 96fcbc06b1f00eaa79f01f4c97f40c85acf680d5 Mon Sep 17 00:00:00 2001 From: Xavilien Date: Fri, 16 Feb 2024 09:22:51 -0500 Subject: [PATCH 04/16] Made a little efficiency improvement for those who hold down backspace when deleting --- frontend/src/app/professors.ts | 5 +++++ frontend/src/components/ProfessorSearch.tsx | 6 ++++++ frontend/src/components/ProfessorSearchList.tsx | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/professors.ts b/frontend/src/app/professors.ts index f99420b..ee49f10 100644 --- a/frontend/src/app/professors.ts +++ b/frontend/src/app/professors.ts @@ -2,10 +2,12 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; export interface ProfessorsState { search: string; + typing: boolean; } const initialState: ProfessorsState = { search: "", + typing: false, }; export const professorsSlice = createSlice({ @@ -15,6 +17,9 @@ export const professorsSlice = createSlice({ updateSearch: (state, action: PayloadAction) => { state.search = action.payload; }, + updateTyping: (state, action: PayloadAction) => { + state.typing = action.payload; + }, }, }); diff --git a/frontend/src/components/ProfessorSearch.tsx b/frontend/src/components/ProfessorSearch.tsx index 9e060a1..3ca4ccf 100644 --- a/frontend/src/components/ProfessorSearch.tsx +++ b/frontend/src/components/ProfessorSearch.tsx @@ -15,6 +15,10 @@ const ProfessorSearch = () => { if (page !== 1) dispatch(cacheSlice.actions.setProfessorPage(1)); }; + const updateTyping = (e: React.KeyboardEvent, typing: boolean) => { + if (e.key === "Backspace") dispatch(professorsSlice.actions.updateTyping(typing)); + } + const results = useAppSelector(selectProfessors(search)); const numResults = results.length; @@ -30,6 +34,8 @@ const ProfessorSearch = () => { type="search" value={search} onChange={onChange} + onKeyDown={(e) => {updateTyping(e, true)}} + onKeyUp={(e) => {updateTyping(e, false)}} placeholder="Search professors by name..." />
diff --git a/frontend/src/components/ProfessorSearchList.tsx b/frontend/src/components/ProfessorSearchList.tsx index 3bf97cb..5486799 100644 --- a/frontend/src/components/ProfessorSearchList.tsx +++ b/frontend/src/components/ProfessorSearchList.tsx @@ -20,6 +20,7 @@ const ProfessorSearchList = () => { const pages = Math.ceil(results.length / RESULTS_PER_PAGE); const curPage = useAppSelector((state) => state.cache.professorPage); const loading = useAppSelector((state) => state.cache.professorsLoading); + const typing = useAppSelector((state) => state.professors.typing); const handlePageClick = (page: number) => { dispatch(cacheSlice.actions.setProfessorPage(page + 1)); @@ -27,7 +28,7 @@ const ProfessorSearchList = () => { return (
- {loading ? ( + {loading || typing ? ( ) : ( <> From 69cf4e2b6246f803c13325b9a15c1ac7459f4688 Mon Sep 17 00:00:00 2001 From: Xavilien Date: Sat, 17 Feb 2024 20:52:44 -0500 Subject: [PATCH 05/16] Used debouncer to fix laggy typing --- frontend/src/app/cache.ts | 3 +++ frontend/src/app/professors.ts | 5 ----- frontend/src/app/store.ts | 11 +++++++++++ frontend/src/components/ProfessorSearch.tsx | 10 +++------- frontend/src/components/ProfessorSearchList.tsx | 5 ++--- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/frontend/src/app/cache.ts b/frontend/src/app/cache.ts index bed9c48..5c9beb7 100644 --- a/frontend/src/app/cache.ts +++ b/frontend/src/app/cache.ts @@ -107,6 +107,9 @@ export const cacheSlice = createSlice({ setProfessorPage: (state, action: PayloadAction) => { state.professorPage = action.payload; }, + setProfessorsLoading: (state, action: PayloadAction) => { + state.professorsLoading = action.payload; + }, }, extraReducers: (builder) => { builder diff --git a/frontend/src/app/professors.ts b/frontend/src/app/professors.ts index ee49f10..f99420b 100644 --- a/frontend/src/app/professors.ts +++ b/frontend/src/app/professors.ts @@ -2,12 +2,10 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; export interface ProfessorsState { search: string; - typing: boolean; } const initialState: ProfessorsState = { search: "", - typing: false, }; export const professorsSlice = createSlice({ @@ -17,9 +15,6 @@ export const professorsSlice = createSlice({ updateSearch: (state, action: PayloadAction) => { state.search = action.payload; }, - updateTyping: (state, action: PayloadAction) => { - state.typing = action.payload; - }, }, }); diff --git a/frontend/src/app/store.ts b/frontend/src/app/store.ts index c660ddf..ebd6b6b 100644 --- a/frontend/src/app/store.ts +++ b/frontend/src/app/store.ts @@ -106,6 +106,17 @@ export const throttledFilter = () => { debouncedFilter(); }; +const debouncedProfessorFilter = debounce(() => { + setTimeout(() => { + void store.dispatch(cacheSlice.actions.setProfessorsLoading(false)); + }, 0); +}, 300); + +export const throttledProfessorFilter = () => { + void store.dispatch(cacheSlice.actions.setProfessorsLoading(true)); + debouncedProfessorFilter(); +} + export type AppState = ReturnType; export type AppDispatch = typeof store.dispatch; diff --git a/frontend/src/components/ProfessorSearch.tsx b/frontend/src/components/ProfessorSearch.tsx index 3ca4ccf..669cd4b 100644 --- a/frontend/src/components/ProfessorSearch.tsx +++ b/frontend/src/components/ProfessorSearch.tsx @@ -3,7 +3,8 @@ import React from "react"; import { useAppDispatch, useAppSelector } from "../app/hooks"; import { professorsSlice } from "../app/professors"; import { cacheSlice } from "../app/cache"; -import {selectProfessors} from "../app/cache"; +import { selectProfessors } from "../app/cache"; +import { throttledProfessorFilter } from "../app/store"; const ProfessorSearch = () => { const dispatch = useAppDispatch(); @@ -13,12 +14,9 @@ const ProfessorSearch = () => { const onChange = (e: React.ChangeEvent) => { dispatch(professorsSlice.actions.updateSearch(e.target.value)); if (page !== 1) dispatch(cacheSlice.actions.setProfessorPage(1)); + throttledProfessorFilter(); }; - const updateTyping = (e: React.KeyboardEvent, typing: boolean) => { - if (e.key === "Backspace") dispatch(professorsSlice.actions.updateTyping(typing)); - } - const results = useAppSelector(selectProfessors(search)); const numResults = results.length; @@ -34,8 +32,6 @@ const ProfessorSearch = () => { type="search" value={search} onChange={onChange} - onKeyDown={(e) => {updateTyping(e, true)}} - onKeyUp={(e) => {updateTyping(e, false)}} placeholder="Search professors by name..." />
diff --git a/frontend/src/components/ProfessorSearchList.tsx b/frontend/src/components/ProfessorSearchList.tsx index 5486799..30fb8b4 100644 --- a/frontend/src/components/ProfessorSearchList.tsx +++ b/frontend/src/components/ProfessorSearchList.tsx @@ -6,7 +6,7 @@ import InstructorDetail from "./InstructorDetail"; import { fetchAllProfessors } from "../app/api/professors"; import { selectProfessors, cacheSlice } from "../app/cache"; -const RESULTS_PER_PAGE = 5; +const RESULTS_PER_PAGE = 10; const ProfessorSearchList = () => { const dispatch = useAppDispatch(); @@ -20,7 +20,6 @@ const ProfessorSearchList = () => { const pages = Math.ceil(results.length / RESULTS_PER_PAGE); const curPage = useAppSelector((state) => state.cache.professorPage); const loading = useAppSelector((state) => state.cache.professorsLoading); - const typing = useAppSelector((state) => state.professors.typing); const handlePageClick = (page: number) => { dispatch(cacheSlice.actions.setProfessorPage(page + 1)); @@ -28,7 +27,7 @@ const ProfessorSearchList = () => { return (
- {loading || typing ? ( + {loading ? ( ) : ( <> From 540549192f63873c4a6a9c21a817ffb6b57ada2a Mon Sep 17 00:00:00 2001 From: Xavilien Date: Sat, 17 Feb 2024 21:09:12 -0500 Subject: [PATCH 06/16] Fixed bug that prevents aggregate filters from having any effect --- frontend/src/components/InstructorDetail.tsx | 4 +++- frontend/src/components/InstructorFCEDetail.tsx | 10 +++++----- frontend/src/pages/professors.tsx | 5 ++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/InstructorDetail.tsx b/frontend/src/components/InstructorDetail.tsx index 3b91278..76c54ab 100644 --- a/frontend/src/components/InstructorDetail.tsx +++ b/frontend/src/components/InstructorDetail.tsx @@ -17,6 +17,8 @@ const InstructorDetail = ({ name }: Props) => { const fces = useAppSelector(selectFCEResultsForInstructor(name)); + const aggregationOptions = useAppSelector((state) => state.user.fceAggregation); + useEffect(() => { if (name) void dispatch(fetchFCEInfosByInstructor(name)); }, [dispatch, loggedIn, name]); @@ -41,7 +43,7 @@ const InstructorDetail = ({ name }: Props) => { {/* TODO: Add more information about instructor using Directory API */}
- +
diff --git a/frontend/src/components/InstructorFCEDetail.tsx b/frontend/src/components/InstructorFCEDetail.tsx index 7d6aa8c..79e8a9d 100644 --- a/frontend/src/components/InstructorFCEDetail.tsx +++ b/frontend/src/components/InstructorFCEDetail.tsx @@ -1,12 +1,12 @@ import React from "react"; import { FCE } from "../app/types"; import { FCETable } from "./FCETable"; +import { AggregateFCEsOptions } from "../app/fce"; -export const InstructorFCEDetail = ({ fces }: { fces: FCE[] }) => { - const aggregationOptions = { - numSemesters: 10, - counted: { spring: true, summer: true, fall: true }, - }; +export const InstructorFCEDetail = ({ fces, aggregationOptions }: { + fces: FCE[], + aggregationOptions: AggregateFCEsOptions +}) => { return ( { return ( - Work in progress - {/**/} - {/**/} + } content={ From 3776b71115a448adfc7f58bbe5035a16bb20ed27 Mon Sep 17 00:00:00 2001 From: Xavilien Date: Sat, 17 Feb 2024 21:51:52 -0500 Subject: [PATCH 07/16] Fixed an annoying UI issue where there is a loading bar for every single professor entry by hiding loading for professors list but showing it for individual pages --- frontend/src/components/InstructorDetail.tsx | 5 +++-- frontend/src/components/ProfessorSearchList.tsx | 2 +- frontend/src/pages/instructor/[name].tsx | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/InstructorDetail.tsx b/frontend/src/components/InstructorDetail.tsx index 76c54ab..7f56e38 100644 --- a/frontend/src/components/InstructorDetail.tsx +++ b/frontend/src/components/InstructorDetail.tsx @@ -9,9 +9,10 @@ import { Card } from "./Card"; type Props = { name: string; + showLoading: boolean; }; -const InstructorDetail = ({ name }: Props) => { +const InstructorDetail = ({ name, showLoading }: Props) => { const dispatch = useAppDispatch(); const loggedIn = useAppSelector((state) => state.user.loggedIn); @@ -25,7 +26,7 @@ const InstructorDetail = ({ name }: Props) => { if (!fces) { return ( -
+
); diff --git a/frontend/src/components/ProfessorSearchList.tsx b/frontend/src/components/ProfessorSearchList.tsx index 30fb8b4..ab087b2 100644 --- a/frontend/src/components/ProfessorSearchList.tsx +++ b/frontend/src/components/ProfessorSearchList.tsx @@ -36,7 +36,7 @@ const ProfessorSearchList = () => { results .slice(curPage * RESULTS_PER_PAGE - RESULTS_PER_PAGE, curPage * RESULTS_PER_PAGE) .map((professor) => ( - + ))}
diff --git a/frontend/src/pages/instructor/[name].tsx b/frontend/src/pages/instructor/[name].tsx index 93a40bb..1cb4281 100644 --- a/frontend/src/pages/instructor/[name].tsx +++ b/frontend/src/pages/instructor/[name].tsx @@ -10,7 +10,7 @@ const InstructorPage: NextPage = () => { const name = router.query.name as string; return ( - } sidebar={} /> + } sidebar={} /> ); }; From f6694ef896ed9c5aa3e8ec6cb35ba2c0ec37d124 Mon Sep 17 00:00:00 2001 From: Xavilien Date: Mon, 26 Feb 2024 11:46:49 -0500 Subject: [PATCH 08/16] Refactored professors to instructors --- backend/src/app.ts | 4 +- backend/src/controllers/instructors.mts | 31 +++++++++++++++ backend/src/controllers/professors.mts | 31 --------------- .../app/api/{professors.ts => instructors.ts} | 12 +++--- frontend/src/app/cache.ts | 38 +++++++++---------- .../src/app/{professors.ts => instructors.ts} | 10 ++--- frontend/src/app/store.ts | 18 ++++----- ...ofessorSearch.tsx => InstructorSearch.tsx} | 24 ++++++------ ...earchList.tsx => InstructorSearchList.tsx} | 24 ++++++------ frontend/src/components/SideNav.tsx | 6 +-- .../pages/{professors.tsx => instructors.tsx} | 14 +++---- 11 files changed, 106 insertions(+), 106 deletions(-) create mode 100644 backend/src/controllers/instructors.mts delete mode 100644 backend/src/controllers/professors.mts rename frontend/src/app/api/{professors.ts => instructors.ts} (51%) rename frontend/src/app/{professors.ts => instructors.ts} (56%) rename frontend/src/components/{ProfessorSearch.tsx => InstructorSearch.tsx} (61%) rename frontend/src/components/{ProfessorSearchList.tsx => InstructorSearchList.tsx} (58%) rename frontend/src/pages/{professors.tsx => instructors.tsx} (56%) diff --git a/backend/src/app.ts b/backend/src/app.ts index 1951046..0d4b1da 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -6,7 +6,7 @@ import { generateSigningRequestHandler, KeyStore } from "passlink-server"; import { isUser } from "./controllers/user.mjs"; import { getAllCourses, getCourseByID, getCourses, getFilteredCourses } from "./controllers/courses.mjs"; import { getFCEs } from "./controllers/fces.mjs"; -import { getProfessors } from "./controllers/professors.mjs"; +import { getInstructors } from "./controllers/instructors.mjs"; // because there is a bug in the typing for passlink // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -45,7 +45,7 @@ app.route("/courses/search/").post(isUser, getFilteredCourses); app.route("/fces").post(isUser, getFCEs); -app.route("/professors").get(getProfessors); +app.route("/instructors").get(getInstructors); // the next parameter is needed! // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/backend/src/controllers/instructors.mts b/backend/src/controllers/instructors.mts new file mode 100644 index 0000000..e185509 --- /dev/null +++ b/backend/src/controllers/instructors.mts @@ -0,0 +1,31 @@ +import { RequestHandler } from "express"; +import { PrismaReturn } from "../util.mjs"; +import prisma from "../models/prisma.mjs"; + +const getAllInstructorsDbQuery = { + select: { + name: true, + }, +}; + +export interface GetInstructors { + params: unknown; + resBody: PrismaReturn>; + reqBody: unknown; + query: unknown; +} + +export const getInstructors: RequestHandler< + GetInstructors["params"], + GetInstructors["resBody"], + GetInstructors["reqBody"], + GetInstructors["query"] +> = async (req, res, next) => { + try { + const instructors = await prisma.professors.findMany(getAllInstructorsDbQuery); + instructors.sort((a, b) => a.name.localeCompare(b.name)); + res.json(instructors); + } catch (e) { + next(e); + } +}; diff --git a/backend/src/controllers/professors.mts b/backend/src/controllers/professors.mts deleted file mode 100644 index 78e780e..0000000 --- a/backend/src/controllers/professors.mts +++ /dev/null @@ -1,31 +0,0 @@ -import { RequestHandler } from "express"; -import { PrismaReturn } from "../util.mjs"; -import prisma from "../models/prisma.mjs"; - -const getAllProfessorsDbQuery = { - select: { - name: true, - }, -}; - -export interface GetProfessors { - params: unknown; - resBody: PrismaReturn>; - reqBody: unknown; - query: unknown; -} - -export const getProfessors: RequestHandler< - GetProfessors["params"], - GetProfessors["resBody"], - GetProfessors["reqBody"], - GetProfessors["query"] -> = async (req, res, next) => { - try { - const professors = await prisma.professors.findMany(getAllProfessorsDbQuery); - professors.sort((a, b) => a.name.localeCompare(b.name)); - res.json(professors); - } catch (e) { - next(e); - } -}; diff --git a/frontend/src/app/api/professors.ts b/frontend/src/app/api/instructors.ts similarity index 51% rename from frontend/src/app/api/professors.ts rename to frontend/src/app/api/instructors.ts index d1336c3..610fa0a 100644 --- a/frontend/src/app/api/professors.ts +++ b/frontend/src/app/api/instructors.ts @@ -1,17 +1,17 @@ import { createAsyncThunk } from "@reduxjs/toolkit"; import { RootState } from "../store"; -type FetchAllProfessorsType = { name: string }[]; +type FetchAllInstructorsType = { name: string }[]; -export const fetchAllProfessors = createAsyncThunk< - FetchAllProfessorsType, +export const fetchAllInstructors = createAsyncThunk< + FetchAllInstructorsType, void, { state: RootState } ->("fetchAllProfessors", async (_, thunkAPI) => { - const url = `${process.env.backendUrl}/professors`; +>("fetchAllInstructors", async (_, thunkAPI) => { + const url = `${process.env.backendUrl}/instructors`; const state = thunkAPI.getState(); - if (state.cache.allProfessors.length > 0) return; + if (state.cache.allInstructors.length > 0) return; return ( await fetch(url, { diff --git a/frontend/src/app/cache.ts b/frontend/src/app/cache.ts index 5c9beb7..e5878b0 100644 --- a/frontend/src/app/cache.ts +++ b/frontend/src/app/cache.ts @@ -9,7 +9,7 @@ import { FetchCourseInfosByPageResult, } from "./api/course"; import { fetchFCEInfosByCourse, fetchFCEInfosByInstructor } from "./api/fce"; -import { fetchAllProfessors } from "./api/professors"; +import { fetchAllInstructors } from "./api/instructors"; /** * This cache lasts for the duration of the user session @@ -30,9 +30,9 @@ interface CacheState { coursesLoading: boolean; exactResultsCourses: string[]; allCourses: { courseID: string; name: string }[]; - allProfessors: { name: string }[]; - professorsLoading: boolean; - professorPage: number; + allInstructors: { name: string }[]; + instructorsLoading: boolean; + instructorPage: number; } const initialState: CacheState = { @@ -49,9 +49,9 @@ const initialState: CacheState = { coursesLoading: false, exactResultsCourses: [], allCourses: [], - allProfessors: [], - professorsLoading: false, - professorPage: 1, + allInstructors: [], + instructorsLoading: false, + instructorPage: 1, }; export const selectCourseResults = @@ -85,9 +85,9 @@ export const selectFCEResultsForInstructor = (state: RootState): FCE[] | undefined => state.cache.instructorResults[name]; -export const selectProfessors = (search: string) => (state: RootState) => - state.cache.allProfessors.filter((prof) => - prof.name.toLowerCase().includes(search.toLowerCase()) +export const selectInstructors = (search: string) => (state: RootState) => + state.cache.allInstructors.filter((instructor) => + instructor.name.toLowerCase().includes(search.toLowerCase()) ); export const cacheSlice = createSlice({ @@ -104,11 +104,11 @@ export const cacheSlice = createSlice({ setCoursesLoading: (state, action: PayloadAction) => { state.coursesLoading = action.payload; }, - setProfessorPage: (state, action: PayloadAction) => { - state.professorPage = action.payload; + setInstructorPage: (state, action: PayloadAction) => { + state.instructorPage = action.payload; }, - setProfessorsLoading: (state, action: PayloadAction) => { - state.professorsLoading = action.payload; + setInstructorsLoading: (state, action: PayloadAction) => { + state.instructorsLoading = action.payload; }, }, extraReducers: (builder) => { @@ -207,12 +207,12 @@ export const cacheSlice = createSlice({ }); builder - .addCase(fetchAllProfessors.pending, (state) => { - state.professorsLoading = true; + .addCase(fetchAllInstructors.pending, (state) => { + state.instructorsLoading = true; }) - .addCase(fetchAllProfessors.fulfilled, (state, action) => { - state.professorsLoading = false; - if (action.payload) state.allProfessors = action.payload; + .addCase(fetchAllInstructors.fulfilled, (state, action) => { + state.instructorsLoading = false; + if (action.payload) state.allInstructors = action.payload; }); }, }); diff --git a/frontend/src/app/professors.ts b/frontend/src/app/instructors.ts similarity index 56% rename from frontend/src/app/professors.ts rename to frontend/src/app/instructors.ts index f99420b..85e765a 100644 --- a/frontend/src/app/professors.ts +++ b/frontend/src/app/instructors.ts @@ -1,15 +1,15 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -export interface ProfessorsState { +export interface InstructorsState { search: string; } -const initialState: ProfessorsState = { +const initialState: InstructorsState = { search: "", }; -export const professorsSlice = createSlice({ - name: "professors", +export const instructorsSlice = createSlice({ + name: "instructors", initialState, reducers: { updateSearch: (state, action: PayloadAction) => { @@ -18,4 +18,4 @@ export const professorsSlice = createSlice({ }, }); -export const reducer = professorsSlice.reducer; +export const reducer = instructorsSlice.reducer; diff --git a/frontend/src/app/store.ts b/frontend/src/app/store.ts index ebd6b6b..7554ec6 100644 --- a/frontend/src/app/store.ts +++ b/frontend/src/app/store.ts @@ -9,7 +9,7 @@ import { UserSchedulesState, } from "./userSchedules"; import { reducer as uiReducer, UIState } from "./ui"; -import { reducer as professorsReducer, ProfessorsState } from "./professors"; +import { reducer as instructorsReducer, InstructorsState } from "./instructors"; import debounce from "lodash/debounce"; import { FLUSH, @@ -63,14 +63,14 @@ const reducers = combineReducers({ }, uiReducer ), - professors: persistReducer( + instructors: persistReducer( { - key: "professors", + key: "instructors", version: 1, storage, stateReconciler: autoMergeLevel2, }, - professorsReducer + instructorsReducer ), }); @@ -106,15 +106,15 @@ export const throttledFilter = () => { debouncedFilter(); }; -const debouncedProfessorFilter = debounce(() => { +const debouncedInstructorFilter = debounce(() => { setTimeout(() => { - void store.dispatch(cacheSlice.actions.setProfessorsLoading(false)); + void store.dispatch(cacheSlice.actions.setInstructorsLoading(false)); }, 0); }, 300); -export const throttledProfessorFilter = () => { - void store.dispatch(cacheSlice.actions.setProfessorsLoading(true)); - debouncedProfessorFilter(); +export const throttledInstructorFilter = () => { + void store.dispatch(cacheSlice.actions.setInstructorsLoading(true)); + debouncedInstructorFilter(); } export type AppState = ReturnType; diff --git a/frontend/src/components/ProfessorSearch.tsx b/frontend/src/components/InstructorSearch.tsx similarity index 61% rename from frontend/src/components/ProfessorSearch.tsx rename to frontend/src/components/InstructorSearch.tsx index 669cd4b..106dccf 100644 --- a/frontend/src/components/ProfessorSearch.tsx +++ b/frontend/src/components/InstructorSearch.tsx @@ -1,23 +1,23 @@ import { MagnifyingGlassIcon } from "@heroicons/react/24/solid"; import React from "react"; import { useAppDispatch, useAppSelector } from "../app/hooks"; -import { professorsSlice } from "../app/professors"; +import { instructorsSlice } from "../app/instructors"; import { cacheSlice } from "../app/cache"; -import { selectProfessors } from "../app/cache"; -import { throttledProfessorFilter } from "../app/store"; +import { selectInstructors } from "../app/cache"; +import { throttledInstructorFilter } from "../app/store"; -const ProfessorSearch = () => { +const InstructorSearch = () => { const dispatch = useAppDispatch(); - const page = useAppSelector((state) => state.cache.professorPage); - const search = useAppSelector((state) => state.professors.search); + const page = useAppSelector((state) => state.cache.instructorPage); + const search = useAppSelector((state) => state.instructors.search); const onChange = (e: React.ChangeEvent) => { - dispatch(professorsSlice.actions.updateSearch(e.target.value)); - if (page !== 1) dispatch(cacheSlice.actions.setProfessorPage(1)); - throttledProfessorFilter(); + dispatch(instructorsSlice.actions.updateSearch(e.target.value)); + if (page !== 1) dispatch(cacheSlice.actions.setInstructorPage(1)); + throttledInstructorFilter(); }; - const results = useAppSelector(selectProfessors(search)); + const results = useAppSelector(selectInstructors(search)); const numResults = results.length; return ( @@ -32,7 +32,7 @@ const ProfessorSearch = () => { type="search" value={search} onChange={onChange} - placeholder="Search professors by name..." + placeholder="Search instructors by name..." />
@@ -42,4 +42,4 @@ const ProfessorSearch = () => { ); }; -export default ProfessorSearch; +export default InstructorSearch; diff --git a/frontend/src/components/ProfessorSearchList.tsx b/frontend/src/components/InstructorSearchList.tsx similarity index 58% rename from frontend/src/components/ProfessorSearchList.tsx rename to frontend/src/components/InstructorSearchList.tsx index ab087b2..bc11dc3 100644 --- a/frontend/src/components/ProfessorSearchList.tsx +++ b/frontend/src/components/InstructorSearchList.tsx @@ -3,26 +3,26 @@ import Loading from "./Loading"; import { Pagination } from "./Pagination"; import React, { useEffect } from "react"; import InstructorDetail from "./InstructorDetail"; -import { fetchAllProfessors } from "../app/api/professors"; -import { selectProfessors, cacheSlice } from "../app/cache"; +import { fetchAllInstructors } from "../app/api/instructors"; +import { selectInstructors, cacheSlice } from "../app/cache"; const RESULTS_PER_PAGE = 10; -const ProfessorSearchList = () => { +const InstructorSearchList = () => { const dispatch = useAppDispatch(); useEffect(() => { - void dispatch(fetchAllProfessors()); + void dispatch(fetchAllInstructors()); }, []); - const search = useAppSelector((state) => state.professors.search); - const results = useAppSelector(selectProfessors(search)); + const search = useAppSelector((state) => state.instructors.search); + const results = useAppSelector(selectInstructors(search)); const pages = Math.ceil(results.length / RESULTS_PER_PAGE); - const curPage = useAppSelector((state) => state.cache.professorPage); - const loading = useAppSelector((state) => state.cache.professorsLoading); + const curPage = useAppSelector((state) => state.cache.instructorPage); + const loading = useAppSelector((state) => state.cache.instructorsLoading); const handlePageClick = (page: number) => { - dispatch(cacheSlice.actions.setProfessorPage(page + 1)); + dispatch(cacheSlice.actions.setInstructorPage(page + 1)); }; return ( @@ -35,8 +35,8 @@ const ProfessorSearchList = () => { {results && results .slice(curPage * RESULTS_PER_PAGE - RESULTS_PER_PAGE, curPage * RESULTS_PER_PAGE) - .map((professor) => ( - + .map((instructor) => ( + ))}
@@ -52,4 +52,4 @@ const ProfessorSearchList = () => { ); }; -export default ProfessorSearchList; +export default InstructorSearchList; diff --git a/frontend/src/components/SideNav.tsx b/frontend/src/components/SideNav.tsx index 375a934..3c6dcd2 100644 --- a/frontend/src/components/SideNav.tsx +++ b/frontend/src/components/SideNav.tsx @@ -83,9 +83,9 @@ export const SideNav = ({ activePage }) => { />
); diff --git a/frontend/src/pages/professors.tsx b/frontend/src/pages/instructors.tsx similarity index 56% rename from frontend/src/pages/professors.tsx rename to frontend/src/pages/instructors.tsx index 0c44d72..e862980 100644 --- a/frontend/src/pages/professors.tsx +++ b/frontend/src/pages/instructors.tsx @@ -1,12 +1,12 @@ import type { NextPage } from "next"; import Topbar from "../components/Topbar"; -import ProfessorSearch from "../components/ProfessorSearch"; -import ProfessorSearchList from "../components/ProfessorSearchList"; +import InstructorSearch from "../components/InstructorSearch"; +import InstructorSearchList from "../components/InstructorSearchList"; import React from "react"; import { Page } from "../components/Page"; import Aggregate from "../components/Aggregate"; -const ProfessorsPage: NextPage = () => { +const InstructorsPage: NextPage = () => { return ( { content={ <> - + - + } - activePage="professors" + activePage="instructors" /> ); }; -export default ProfessorsPage; +export default InstructorsPage; From a2761460e1f3b542be3eb120ea2eaa280adb278c Mon Sep 17 00:00:00 2001 From: Xavilien Date: Mon, 26 Feb 2024 12:04:44 -0500 Subject: [PATCH 09/16] Use fuse for fuzzy search for instructors --- frontend/package-lock.json | 14 ++++++++++++++ frontend/package.json | 1 + frontend/src/app/cache.ts | 18 ++++++++++++++---- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d18ae9c..bc3baf5 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,6 +16,7 @@ "@types/uuid": "^8.3.4", "axios": "^0.24.0", "downshift": "^6.1.7", + "fuse.js": "^7.0.0", "jose": "^4.8.1", "namecase": "^1.1.2", "next": "^13.0.6", @@ -6211,6 +6212,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuse.js": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz", + "integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==", + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -16196,6 +16205,11 @@ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true }, + "fuse.js": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz", + "integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==" + }, "gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 0d9222c..23f9423 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "@types/uuid": "^8.3.4", "axios": "^0.24.0", "downshift": "^6.1.7", + "fuse.js": "^7.0.0", "jose": "^4.8.1", "namecase": "^1.1.2", "next": "^13.0.6", diff --git a/frontend/src/app/cache.ts b/frontend/src/app/cache.ts index e5878b0..a6d9de9 100644 --- a/frontend/src/app/cache.ts +++ b/frontend/src/app/cache.ts @@ -10,6 +10,7 @@ import { } from "./api/course"; import { fetchFCEInfosByCourse, fetchFCEInfosByInstructor } from "./api/fce"; import { fetchAllInstructors } from "./api/instructors"; +import Fuse from "fuse.js"; /** * This cache lasts for the duration of the user session @@ -85,10 +86,19 @@ export const selectFCEResultsForInstructor = (state: RootState): FCE[] | undefined => state.cache.instructorResults[name]; -export const selectInstructors = (search: string) => (state: RootState) => - state.cache.allInstructors.filter((instructor) => - instructor.name.toLowerCase().includes(search.toLowerCase()) - ); +export const selectInstructors = (search: string) => (state: RootState) => { + if (!search) return state.cache.allInstructors; + + const options = { + keys: ['name'], + includeScore: true, + }; + + const fuse = new Fuse(state.cache.allInstructors, options); + const result = fuse.search(search); + + return result.map(({ item }) => item); +}; export const cacheSlice = createSlice({ name: "cache", From c709eebe058a4ab3a2920005ca27188a1c4c936a Mon Sep 17 00:00:00 2001 From: Xavilien Date: Mon, 26 Feb 2024 12:04:59 -0500 Subject: [PATCH 10/16] Remove unused variable --- backend/src/controllers/instructors.mts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/controllers/instructors.mts b/backend/src/controllers/instructors.mts index e185509..1a67a33 100644 --- a/backend/src/controllers/instructors.mts +++ b/backend/src/controllers/instructors.mts @@ -20,7 +20,7 @@ export const getInstructors: RequestHandler< GetInstructors["resBody"], GetInstructors["reqBody"], GetInstructors["query"] -> = async (req, res, next) => { +> = async (_, res, next) => { try { const instructors = await prisma.professors.findMany(getAllInstructorsDbQuery); instructors.sort((a, b) => a.name.localeCompare(b.name)); From b54b9332f9c74d6f923100a15acbb27ab3d279a3 Mon Sep 17 00:00:00 2001 From: Xavilien Date: Mon, 26 Feb 2024 12:06:24 -0500 Subject: [PATCH 11/16] Added missing dispatch dependency --- frontend/src/components/InstructorSearchList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/InstructorSearchList.tsx b/frontend/src/components/InstructorSearchList.tsx index bc11dc3..42afc98 100644 --- a/frontend/src/components/InstructorSearchList.tsx +++ b/frontend/src/components/InstructorSearchList.tsx @@ -13,7 +13,7 @@ const InstructorSearchList = () => { useEffect(() => { void dispatch(fetchAllInstructors()); - }, []); + }, [dispatch]); const search = useAppSelector((state) => state.instructors.search); const results = useAppSelector(selectInstructors(search)); From d75fda4dba073b3d96ae38db0530f6b0aca87d69 Mon Sep 17 00:00:00 2001 From: Xavilien Date: Mon, 26 Feb 2024 12:34:58 -0500 Subject: [PATCH 12/16] Refactored some code so that search is speedier --- frontend/src/app/cache.ts | 36 +++++++++++-------- frontend/src/app/store.ts | 2 ++ frontend/src/components/InstructorSearch.tsx | 3 +- .../src/components/InstructorSearchList.tsx | 5 ++- 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/frontend/src/app/cache.ts b/frontend/src/app/cache.ts index a6d9de9..faec451 100644 --- a/frontend/src/app/cache.ts +++ b/frontend/src/app/cache.ts @@ -34,6 +34,7 @@ interface CacheState { allInstructors: { name: string }[]; instructorsLoading: boolean; instructorPage: number; + selectedInstructors: { name: string }[]; } const initialState: CacheState = { @@ -53,6 +54,7 @@ const initialState: CacheState = { allInstructors: [], instructorsLoading: false, instructorPage: 1, + selectedInstructors: [], }; export const selectCourseResults = @@ -86,20 +88,6 @@ export const selectFCEResultsForInstructor = (state: RootState): FCE[] | undefined => state.cache.instructorResults[name]; -export const selectInstructors = (search: string) => (state: RootState) => { - if (!search) return state.cache.allInstructors; - - const options = { - keys: ['name'], - includeScore: true, - }; - - const fuse = new Fuse(state.cache.allInstructors, options); - const result = fuse.search(search); - - return result.map(({ item }) => item); -}; - export const cacheSlice = createSlice({ name: "cache", initialState, @@ -120,6 +108,21 @@ export const cacheSlice = createSlice({ setInstructorsLoading: (state, action: PayloadAction) => { state.instructorsLoading = action.payload; }, + selectInstructors: (state, action: PayloadAction) => { + const search = action.payload + if (!search) { + state.selectedInstructors = state.allInstructors; + return; + } + + const options = { + keys: ['name'], + includeScore: true, + }; + + const fuse = new Fuse(state.allInstructors, options); + state.selectedInstructors = fuse.search(search).map(({item}) => item); + } }, extraReducers: (builder) => { builder @@ -222,7 +225,10 @@ export const cacheSlice = createSlice({ }) .addCase(fetchAllInstructors.fulfilled, (state, action) => { state.instructorsLoading = false; - if (action.payload) state.allInstructors = action.payload; + if (action.payload) { + state.allInstructors = action.payload; + state.selectedInstructors = action.payload; + } }); }, }); diff --git a/frontend/src/app/store.ts b/frontend/src/app/store.ts index 7554ec6..6d1aef8 100644 --- a/frontend/src/app/store.ts +++ b/frontend/src/app/store.ts @@ -108,6 +108,8 @@ export const throttledFilter = () => { const debouncedInstructorFilter = debounce(() => { setTimeout(() => { + const state = store.getState(); + void store.dispatch(cacheSlice.actions.selectInstructors(state.instructors.search)); void store.dispatch(cacheSlice.actions.setInstructorsLoading(false)); }, 0); }, 300); diff --git a/frontend/src/components/InstructorSearch.tsx b/frontend/src/components/InstructorSearch.tsx index 106dccf..d674cf0 100644 --- a/frontend/src/components/InstructorSearch.tsx +++ b/frontend/src/components/InstructorSearch.tsx @@ -3,7 +3,6 @@ import React from "react"; import { useAppDispatch, useAppSelector } from "../app/hooks"; import { instructorsSlice } from "../app/instructors"; import { cacheSlice } from "../app/cache"; -import { selectInstructors } from "../app/cache"; import { throttledInstructorFilter } from "../app/store"; const InstructorSearch = () => { @@ -17,7 +16,7 @@ const InstructorSearch = () => { throttledInstructorFilter(); }; - const results = useAppSelector(selectInstructors(search)); + const results = useAppSelector((state) => state.cache.selectedInstructors); const numResults = results.length; return ( diff --git a/frontend/src/components/InstructorSearchList.tsx b/frontend/src/components/InstructorSearchList.tsx index 42afc98..6590eb3 100644 --- a/frontend/src/components/InstructorSearchList.tsx +++ b/frontend/src/components/InstructorSearchList.tsx @@ -4,7 +4,7 @@ import { Pagination } from "./Pagination"; import React, { useEffect } from "react"; import InstructorDetail from "./InstructorDetail"; import { fetchAllInstructors } from "../app/api/instructors"; -import { selectInstructors, cacheSlice } from "../app/cache"; +import { cacheSlice } from "../app/cache"; const RESULTS_PER_PAGE = 10; @@ -15,8 +15,7 @@ const InstructorSearchList = () => { void dispatch(fetchAllInstructors()); }, [dispatch]); - const search = useAppSelector((state) => state.instructors.search); - const results = useAppSelector(selectInstructors(search)); + const results = useAppSelector((state) => state.cache.selectedInstructors); const pages = Math.ceil(results.length / RESULTS_PER_PAGE); const curPage = useAppSelector((state) => state.cache.instructorPage); const loading = useAppSelector((state) => state.cache.instructorsLoading); From 9edb0eb318cc410905ed2ae601122f857bdeb4ec Mon Sep 17 00:00:00 2001 From: Xavilien Date: Thu, 14 Mar 2024 00:15:11 -0400 Subject: [PATCH 13/16] Changed getAllInstructors to actually get all instructors rather than just professors --- backend/src/controllers/instructors.mts | 15 +++++++++++---- frontend/src/app/api/instructors.ts | 2 +- frontend/src/app/cache.ts | 6 +++--- frontend/src/components/InstructorSearchList.tsx | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/backend/src/controllers/instructors.mts b/backend/src/controllers/instructors.mts index 1a67a33..4325c26 100644 --- a/backend/src/controllers/instructors.mts +++ b/backend/src/controllers/instructors.mts @@ -4,13 +4,13 @@ import prisma from "../models/prisma.mjs"; const getAllInstructorsDbQuery = { select: { - name: true, + instructor: true, }, }; export interface GetInstructors { params: unknown; - resBody: PrismaReturn>; + resBody: PrismaReturn>; reqBody: unknown; query: unknown; } @@ -22,8 +22,15 @@ export const getInstructors: RequestHandler< GetInstructors["query"] > = async (_, res, next) => { try { - const instructors = await prisma.professors.findMany(getAllInstructorsDbQuery); - instructors.sort((a, b) => a.name.localeCompare(b.name)); + const instructors = await prisma.fces.findMany({ + select: { + instructor: true, + }, + orderBy: { + instructor: "asc", + }, + distinct: ["instructor"] + }); res.json(instructors); } catch (e) { next(e); diff --git a/frontend/src/app/api/instructors.ts b/frontend/src/app/api/instructors.ts index 610fa0a..5c0d245 100644 --- a/frontend/src/app/api/instructors.ts +++ b/frontend/src/app/api/instructors.ts @@ -1,7 +1,7 @@ import { createAsyncThunk } from "@reduxjs/toolkit"; import { RootState } from "../store"; -type FetchAllInstructorsType = { name: string }[]; +type FetchAllInstructorsType = { instructor: string }[]; export const fetchAllInstructors = createAsyncThunk< FetchAllInstructorsType, diff --git a/frontend/src/app/cache.ts b/frontend/src/app/cache.ts index faec451..5cab37b 100644 --- a/frontend/src/app/cache.ts +++ b/frontend/src/app/cache.ts @@ -31,10 +31,10 @@ interface CacheState { coursesLoading: boolean; exactResultsCourses: string[]; allCourses: { courseID: string; name: string }[]; - allInstructors: { name: string }[]; + allInstructors: { instructor: string }[]; instructorsLoading: boolean; instructorPage: number; - selectedInstructors: { name: string }[]; + selectedInstructors: { instructor: string }[]; } const initialState: CacheState = { @@ -116,7 +116,7 @@ export const cacheSlice = createSlice({ } const options = { - keys: ['name'], + keys: ['instructor'], includeScore: true, }; diff --git a/frontend/src/components/InstructorSearchList.tsx b/frontend/src/components/InstructorSearchList.tsx index 6590eb3..18754d0 100644 --- a/frontend/src/components/InstructorSearchList.tsx +++ b/frontend/src/components/InstructorSearchList.tsx @@ -35,7 +35,7 @@ const InstructorSearchList = () => { results .slice(curPage * RESULTS_PER_PAGE - RESULTS_PER_PAGE, curPage * RESULTS_PER_PAGE) .map((instructor) => ( - + ))}
From 12ca24bf260cf210fe1ccb213ff6166f91c38251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cxavilien=E2=80=9D?= <“xavilien@gmail.com”> Date: Sat, 16 Mar 2024 15:59:44 -0400 Subject: [PATCH 14/16] Set the Fuse search to be a global variable in cache.ts --- frontend/src/app/cache.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/cache.ts b/frontend/src/app/cache.ts index 5cab37b..aeb0bda 100644 --- a/frontend/src/app/cache.ts +++ b/frontend/src/app/cache.ts @@ -32,6 +32,7 @@ interface CacheState { exactResultsCourses: string[]; allCourses: { courseID: string; name: string }[]; allInstructors: { instructor: string }[]; + allInstructorsFuse: Fuse<{ instructor: string }>; instructorsLoading: boolean; instructorPage: number; selectedInstructors: { instructor: string }[]; @@ -52,6 +53,7 @@ const initialState: CacheState = { exactResultsCourses: [], allCourses: [], allInstructors: [], + allInstructorsFuse: new Fuse([], {keys: ['instructor'], includeScore: true}), instructorsLoading: false, instructorPage: 1, selectedInstructors: [], @@ -115,13 +117,7 @@ export const cacheSlice = createSlice({ return; } - const options = { - keys: ['instructor'], - includeScore: true, - }; - - const fuse = new Fuse(state.allInstructors, options); - state.selectedInstructors = fuse.search(search).map(({item}) => item); + state.selectedInstructors = state.allInstructorsFuse.search(search).map(({item}) => item); } }, extraReducers: (builder) => { @@ -227,6 +223,13 @@ export const cacheSlice = createSlice({ state.instructorsLoading = false; if (action.payload) { state.allInstructors = action.payload; + + const options = { + keys: ['instructor'], + includeScore: true, + }; + state.allInstructorsFuse = new Fuse(state.allInstructors, options); + state.selectedInstructors = action.payload; } }); From e9c0982e90e39d5c392f3217d7dbe9a42f7a9d59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cxavilien=E2=80=9D?= <“xavilien@gmail.com”> Date: Sat, 16 Mar 2024 17:29:41 -0400 Subject: [PATCH 15/16] Set the Fuse search to be a global variable in cache.ts --- frontend/src/app/cache.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/cache.ts b/frontend/src/app/cache.ts index aeb0bda..578cd6d 100644 --- a/frontend/src/app/cache.ts +++ b/frontend/src/app/cache.ts @@ -10,7 +10,7 @@ import { } from "./api/course"; import { fetchFCEInfosByCourse, fetchFCEInfosByInstructor } from "./api/fce"; import { fetchAllInstructors } from "./api/instructors"; -import Fuse from "fuse.js"; +import Fuse, { FuseIndex } from "fuse.js"; /** * This cache lasts for the duration of the user session @@ -32,7 +32,7 @@ interface CacheState { exactResultsCourses: string[]; allCourses: { courseID: string; name: string }[]; allInstructors: { instructor: string }[]; - allInstructorsFuse: Fuse<{ instructor: string }>; + fuseIndex: { [key: string]: any }; instructorsLoading: boolean; instructorPage: number; selectedInstructors: { instructor: string }[]; @@ -53,7 +53,7 @@ const initialState: CacheState = { exactResultsCourses: [], allCourses: [], allInstructors: [], - allInstructorsFuse: new Fuse([], {keys: ['instructor'], includeScore: true}), + fuseIndex: null, instructorsLoading: false, instructorPage: 1, selectedInstructors: [], @@ -116,8 +116,9 @@ export const cacheSlice = createSlice({ state.selectedInstructors = state.allInstructors; return; } - - state.selectedInstructors = state.allInstructorsFuse.search(search).map(({item}) => item); + const fuseIndex : FuseIndex<{ instructor: string }> = Fuse.parseIndex(state.fuseIndex); + const fuse = new Fuse(state.allInstructors, {}, fuseIndex); + state.selectedInstructors = fuse.search(search).map(({item}) => item); } }, extraReducers: (builder) => { @@ -223,13 +224,7 @@ export const cacheSlice = createSlice({ state.instructorsLoading = false; if (action.payload) { state.allInstructors = action.payload; - - const options = { - keys: ['instructor'], - includeScore: true, - }; - state.allInstructorsFuse = new Fuse(state.allInstructors, options); - + state.fuseIndex = Fuse.createIndex(["instructor"], action.payload).toJSON(); state.selectedInstructors = action.payload; } }); From 0ae222341af40a0c1351b8fbb0aae0a567ca6ad7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cxavilien=E2=80=9D?= <“xavilien@gmail.com”> Date: Sun, 17 Mar 2024 00:14:45 -0400 Subject: [PATCH 16/16] Changed type of fuseIndex --- frontend/src/app/cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/cache.ts b/frontend/src/app/cache.ts index 578cd6d..34c8cd1 100644 --- a/frontend/src/app/cache.ts +++ b/frontend/src/app/cache.ts @@ -53,7 +53,7 @@ const initialState: CacheState = { exactResultsCourses: [], allCourses: [], allInstructors: [], - fuseIndex: null, + fuseIndex: {}, instructorsLoading: false, instructorPage: 1, selectedInstructors: [],