From 99e949570529d1b5a173f34e8b61b9d37549d582 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 11 Sep 2023 19:49:39 -0700 Subject: [PATCH 01/22] feat: initial migrate test --- api/src/controllers/courses.ts | 4 +- api/src/controllers/schedule.ts | 21 ++++++---- api/src/helpers/gql.ts | 46 ++++++---------------- site/src/component/AppHeader/AppHeader.tsx | 9 +---- stacks/backend.ts | 9 ++++- 5 files changed, 37 insertions(+), 52 deletions(-) diff --git a/api/src/controllers/courses.ts b/api/src/controllers/courses.ts index 47049596..e947980c 100644 --- a/api/src/controllers/courses.ts +++ b/api/src/controllers/courses.ts @@ -22,7 +22,7 @@ router.get('/api', (req: Request<{}, {}, {}, { courseID: string }>, res) => { console.log(req.query.courseID) r.then((response) => response.json()) - .then((data) => res.send(data)) + .then((data) => res.send(data.payload)) }); /** @@ -53,7 +53,7 @@ router.post('/api/batch', (req: Request<{}, {}, { courses: string[] }>, res) => */ router.get('/api/grades', (req: Request<{}, {}, {}, { department: string; number: string; }>, res) => { - let r = fetch(process.env.PUBLIC_API_URL + 'grades/raw?department=' + encodeURIComponent(req.query.department) + '&number=' + req.query.number); + let r = fetch(process.env.PUBLIC_API_URL + 'grades/raw?department=' + encodeURIComponent(req.query.department) + '&courseNumber=' + req.query.number); r.then((response) => response.json()) .then((data) => res.send(data)) diff --git a/api/src/controllers/schedule.ts b/api/src/controllers/schedule.ts index 77663a26..bd62d796 100644 --- a/api/src/controllers/schedule.ts +++ b/api/src/controllers/schedule.ts @@ -30,15 +30,20 @@ router.get("/getTerms", function (req, res) { /** * Get the current week */ -router.get('/api/currentWeek', function (req, res, next) { - getWeek().then(week => res.send(week)) +router.get('/api/currentWeek', async function (_, res) { + console.log(process.env.PUBLIC_API_URL) + const apiResp = await fetch(`${process.env.PUBLIC_API_URL}/week`); + const json = await apiResp.json(); + res.send(json.payload) }); /** * Get the current quarter on websoc */ - router.get('/api/currentQuarter', function (req, res, next) { - getCurrentQuarter().then(currentQuarter => res.send(currentQuarter)) +router.get('/api/currentQuarter', async function (_, res) { + const apiResp = await fetch(`${process.env.PUBLIC_API_URL}/websoc/terms`); + const json = await apiResp.json(); + res.send(json.payload[0].longName) }); @@ -46,8 +51,9 @@ router.get('/api/currentWeek', function (req, res, next) { * Proxy for WebSOC, using PeterPortal API */ router.get('/api/:term/:department/:number', async function (req, res) { + const [year, quarter] = req.params.term.split(" "); const result = await callPPAPIWebSoc({ - term: req.params.term, + year, quarter, department: req.params.department, courseNumber: req.params.number }); @@ -58,15 +64,16 @@ router.get('/api/:term/:department/:number', async function (req, res) { * Proxy for WebSOC, using PeterPortal API */ router.get('/api/:term/:professor', async function (req, res) { + const [year, quarter] = req.params.term.split(" "); const result = await callPPAPIWebSoc({ - term: req.params.term, + year, quarter, instructorName: req.params.professor }); res.send(result); }); async function callPPAPIWebSoc(params: Record) { - const url: URL = new URL(process.env.PUBLIC_API_URL + 'schedule/soc?' + + const url: URL = new URL(process.env.PUBLIC_API_URL + 'websoc?' + new URLSearchParams(params)) return await fetch(url).then(response => response.json()); } diff --git a/api/src/helpers/gql.ts b/api/src/helpers/gql.ts index 33bdc350..892e6048 100644 --- a/api/src/helpers/gql.ts +++ b/api/src/helpers/gql.ts @@ -12,38 +12,23 @@ export function getCourseQuery(courseIDs: string[]) { number school title - course_level - department_alias + courseLevel units description - department_name - instructor_history{ - name - ucinetid - shortened_name - } - prerequisite_tree - prerequisite_list { - id - department - number - title - } - prerequisite_text - prerequisite_for { - id - department - number - title - } + departmentName + instructorHistory + prerequisiteTree + prerequisiteList + prerequisiteText + prerequisiteFor repeatability concurrent - same_as + sameAs restriction overlap corequisite - ge_list - ge_text + geList + geText terms }, ` @@ -64,18 +49,13 @@ export function getProfessorQuery(ucinetids: string[]) { result += ` ${'_' + i}: instructor(ucinetid: "${ucinetid}"){ name - shortened_name + shortenedName ucinetid title department schools - related_departments - course_history { - id - department - number - title - } + relatedDepartments + courseHistory }, ` }) diff --git a/site/src/component/AppHeader/AppHeader.tsx b/site/src/component/AppHeader/AppHeader.tsx index bd44cfc4..d4a4252c 100644 --- a/site/src/component/AppHeader/AppHeader.tsx +++ b/site/src/component/AppHeader/AppHeader.tsx @@ -29,14 +29,7 @@ const AppHeader: FC<{}> = props => { // Get the current week data axios.get('/api/schedule/api/currentWeek') .then(res => { - // case for break and finals week - if (res.data.week == -1) { - setWeek(res.data.display); - } - // case when school is in session - else { - setWeek('Week ' + res.data.week + ' • ' + res.data.quarter); - } + setWeek(res.data.display); }); }, []) diff --git a/stacks/backend.ts b/stacks/backend.ts index 49254430..b8d96f1e 100644 --- a/stacks/backend.ts +++ b/stacks/backend.ts @@ -35,8 +35,13 @@ export function BackendStack({app, stack}: StackContext) { environment: { MONGO_URL: process.env.MONGO_URL, SESSION_SECRET: process.env.SESSION_SECRET, - PUBLIC_API_URL: process.env.PUBLIC_API_URL, - PUBLIC_API_GRAPHQL_URL: process.env.PUBLIC_API_GRAPHQL_URL, + /** + * TODO: needs to be restored before merging. + */ + // PUBLIC_API_URL: process.env.PUBLIC_API_URL, + // PUBLIC_API_GRAPHQL_URL: process.env.PUBLIC_API_GRAPHQL_URL, + PUBLIC_API_URL: "https://api-next.peterportal.org/v1/rest/", + PUBLIC_API_GRAPHQL_URL: "https://api-next.peterportal.org/v1/graphql", GOOGLE_CLIENT: process.env.GOOGLE_CLIENT, GOOGLE_SECRET: process.env.GOOGLE_SECRET, PRODUCTION_DOMAIN: process.env.PRODUCTION_DOMAIN, From e95e9bbdaef4b200fdcd44dccec0db50a73a0c86 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Wed, 13 Sep 2023 19:55:37 -0700 Subject: [PATCH 02/22] feat: stable ground (no more ts errors) --- api/src/controllers/courses.ts | 7 +- api/src/helpers/gql.ts | 9 +- site/package-lock.json | 45 +++++++-- site/package.json | 3 +- site/src/component/GradeDist/GradeDist.tsx | 2 +- site/src/component/PrereqTree/PrereqTree.tsx | 81 +++++++++------- site/src/component/Review/SubReview.tsx | 4 +- site/src/component/ReviewForm/ReviewForm.tsx | 8 +- .../component/SearchModule/SearchModule.tsx | 4 +- site/src/component/SideInfo/SideInfo.tsx | 12 +-- site/src/helpers/util.tsx | 57 +++++------ site/src/hooks/courseData.tsx | 95 ------------------- site/src/hooks/professorData.tsx | 52 ---------- site/src/pages/ProfessorPage/index.tsx | 4 +- site/src/pages/RoadmapPage/Course.tsx | 10 +- site/src/pages/RoadmapPage/Planner.tsx | 24 +++-- site/src/pages/RoadmapPage/Quarter.tsx | 2 +- site/src/pages/RoadmapPage/Year.tsx | 2 +- site/src/pages/SearchPage/CoursePopup.tsx | 10 +- .../src/pages/SearchPage/ProfessorHitItem.tsx | 4 +- site/src/pages/SearchPage/ProfessorPopup.tsx | 2 +- site/src/types/types.ts | 44 ++++----- 22 files changed, 187 insertions(+), 294 deletions(-) delete mode 100644 site/src/hooks/courseData.tsx delete mode 100644 site/src/hooks/professorData.tsx diff --git a/api/src/controllers/courses.ts b/api/src/controllers/courses.ts index e947980c..de615cb1 100644 --- a/api/src/controllers/courses.ts +++ b/api/src/controllers/courses.ts @@ -43,8 +43,13 @@ router.post('/api/batch', (req: Request<{}, {}, { courses: string[] }>, res) => }) }); + r.then((response) => response.json()) - .then((data) => res.json(data.data)) + .then((data) => { + const d = data.data + console.log(data) + res.json(d) + }) } }); diff --git a/api/src/helpers/gql.ts b/api/src/helpers/gql.ts index 892e6048..dcf2c532 100644 --- a/api/src/helpers/gql.ts +++ b/api/src/helpers/gql.ts @@ -6,14 +6,15 @@ export function getCourseQuery(courseIDs: string[]) { courseIDs.forEach((courseID, i) => { // use number id here because cannot use special character names result += ` - ${'_' + i}: course(id: "${courseID}"){ + ${'_' + i}: course(courseId: "${courseID}") { id department - number + courseNumber school title courseLevel - units + minUnits + maxUnits description departmentName instructorHistory @@ -26,7 +27,7 @@ export function getCourseQuery(courseIDs: string[]) { sameAs restriction overlap - corequisite + corequisites geList geText terms diff --git a/site/package-lock.json b/site/package-lock.json index 5dfeb90e..2792396d 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -33,7 +33,7 @@ "react-twemoji": "^0.5.0", "semantic-ui-react": "^2.1.4", "typescript": "^4.3.5", - "websoc-fuzzy-search": "^0.8.0-rc.1" + "websoc-fuzzy-search": "1.0.1" }, "devDependencies": { "@types/jest": "^26.0.24", @@ -45,6 +45,7 @@ "@types/react-redux": "^7.1.18", "@types/react-router-dom": "^5.1.8", "@types/redux": "^3.6.0", + "peterportal-api-next-types": "^1.0.0-rc.3", "sass": "^1.49.0" } }, @@ -21287,6 +21288,11 @@ "node": ">=6" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -21406,6 +21412,12 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", "integrity": "sha512-YHk5ez1hmMR5LOkb9iJkLKqoBlL7WD5M8ljC75ZfzXriuBIVNuecaXuU7e+hOwyqf24Wxhh7Vxgt7Hnw9288Tg==" }, + "node_modules/peterportal-api-next-types": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/peterportal-api-next-types/-/peterportal-api-next-types-1.0.0-rc.3.tgz", + "integrity": "sha512-TlylpK4OcfxG91aze6urHv9ei7/gX/GLrR7v8ktPVX31r/xPfYS77ygi6wcawwguka6dk8/JHQukYdy5tlOYgA==", + "dev": true + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -27790,9 +27802,13 @@ } }, "node_modules/websoc-fuzzy-search": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/websoc-fuzzy-search/-/websoc-fuzzy-search-0.8.1.tgz", - "integrity": "sha512-Iwj2Z4f82fwFXRPEo9GGc6foKcMagzzcwFqpyUremBLXUK/foEuWcDVWHRU4TqSiXYV4YV5z5N1PZk8w4cuNrw==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/websoc-fuzzy-search/-/websoc-fuzzy-search-1.0.1.tgz", + "integrity": "sha512-1UlDdT2OvMxVIczNSQzI+vSoojfagbORdwtMQiLAnG1zVLG9Po6x5+VWNysi8w5xoxE2NootQH72HzoenLygDg==", + "dependencies": { + "base64-arraybuffer": "1.0.2", + "pako": "2.1.0" + } }, "node_modules/websocket-driver": { "version": "0.7.4", @@ -44268,6 +44284,11 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, + "pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, "param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -44360,6 +44381,12 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", "integrity": "sha512-YHk5ez1hmMR5LOkb9iJkLKqoBlL7WD5M8ljC75ZfzXriuBIVNuecaXuU7e+hOwyqf24Wxhh7Vxgt7Hnw9288Tg==" }, + "peterportal-api-next-types": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/peterportal-api-next-types/-/peterportal-api-next-types-1.0.0-rc.3.tgz", + "integrity": "sha512-TlylpK4OcfxG91aze6urHv9ei7/gX/GLrR7v8ktPVX31r/xPfYS77ygi6wcawwguka6dk8/JHQukYdy5tlOYgA==", + "dev": true + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -48919,9 +48946,13 @@ "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==" }, "websoc-fuzzy-search": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/websoc-fuzzy-search/-/websoc-fuzzy-search-0.8.1.tgz", - "integrity": "sha512-Iwj2Z4f82fwFXRPEo9GGc6foKcMagzzcwFqpyUremBLXUK/foEuWcDVWHRU4TqSiXYV4YV5z5N1PZk8w4cuNrw==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/websoc-fuzzy-search/-/websoc-fuzzy-search-1.0.1.tgz", + "integrity": "sha512-1UlDdT2OvMxVIczNSQzI+vSoojfagbORdwtMQiLAnG1zVLG9Po6x5+VWNysi8w5xoxE2NootQH72HzoenLygDg==", + "requires": { + "base64-arraybuffer": "1.0.2", + "pako": "2.1.0" + } }, "websocket-driver": { "version": "0.7.4", diff --git a/site/package.json b/site/package.json index f6d86737..0eca025f 100644 --- a/site/package.json +++ b/site/package.json @@ -28,7 +28,7 @@ "react-twemoji": "^0.5.0", "semantic-ui-react": "^2.1.4", "typescript": "^4.3.5", - "websoc-fuzzy-search": "^0.8.0-rc.1" + "websoc-fuzzy-search": "1.0.1" }, "scripts": { "start": "react-scripts start", @@ -61,6 +61,7 @@ "@types/react-redux": "^7.1.18", "@types/react-router-dom": "^5.1.8", "@types/redux": "^3.6.0", + "peterportal-api-next-types": "^1.0.0-rc.3", "sass": "^1.49.0" }, "homepage": "https://peterportal.org" diff --git a/site/src/component/GradeDist/GradeDist.tsx b/site/src/component/GradeDist/GradeDist.tsx index 7c8a8b6f..7cfa18b1 100644 --- a/site/src/component/GradeDist/GradeDist.tsx +++ b/site/src/component/GradeDist/GradeDist.tsx @@ -47,7 +47,7 @@ const GradeDist: FC = (props) => { } } else if (props.professor) { - url = `/api/professors/api/grades/${props.professor.shortened_name}`; + url = `/api/professors/api/grades/${props.professor.shortenedName}`; } const res = axios.get(url, { params: params diff --git a/site/src/component/PrereqTree/PrereqTree.tsx b/site/src/component/PrereqTree/PrereqTree.tsx index 63c09dd7..386f4ef4 100644 --- a/site/src/component/PrereqTree/PrereqTree.tsx +++ b/site/src/component/PrereqTree/PrereqTree.tsx @@ -1,16 +1,24 @@ import React, { FC } from 'react'; import './PrereqTree.scss'; import { Grid, Popup } from 'semantic-ui-react'; +import type { Prerequisite, PrerequisiteTree } from "peterportal-api-next-types"; -import { PrerequisiteJSONNode, PrerequisiteJSON, CourseGQLData, CourseLookup } from '../../types/types'; +import { CourseGQLData, CourseLookup } from '../../types/types'; interface NodeProps { node: string; label: string; - content: string; index?: number; } +type PrerequisiteNode = Prerequisite | PrerequisiteTree; + +const phraseMapping = { + AND: 'all of', + OR: 'one of', + NOT: 'none of', +}; + const Node: FC = (props) => { return (
@@ -20,52 +28,61 @@ const Node: FC = (props) => { {props.label} } - content={props.content} basic position='top center' wide='very' /> + basic position='top center' wide='very' />
); } interface TreeProps { prerequisiteNames: CourseLookup; - prerequisiteJSON: PrerequisiteJSONNode; + prerequisiteJSON: PrerequisiteNode; key?: string; index?: number; } -const Tree: FC = (props) => { - let prerequisite = props.prerequisiteJSON; - let isValueNode = typeof prerequisite === 'string'; +const PrereqTreeNode: FC = (props) => { + const prerequisite = props.prerequisiteJSON; + const isValueNode = Object.prototype.hasOwnProperty.call(prerequisite, 'prereqType'); // if value is a string, render leaf node if (isValueNode) { - let id = (prerequisite as string).replace(/\s+/g, ''); - let content = prerequisite; - if (props.prerequisiteNames.hasOwnProperty(id)) { - content = props.prerequisiteNames[id].title; - } + const prereq = prerequisite as Prerequisite; return (
  • - +
  • - ) + ); } // if value is an object, render the rest of the sub tree else { + const prereqTree = prerequisite as Record; return (
    -
    - {prerequisite.hasOwnProperty('OR') ? 'one of' : 'all of'} -
    +
    + { + Object.entries(phraseMapping).filter(([subtreeType, _]) => + Object.prototype.hasOwnProperty.call(prerequisite, subtreeType) + )[0][1] + } +
      - {(prerequisite as PrerequisiteJSON)[Object.keys(prerequisite)[0]].map( - (child, index) => ( - - ) - )} + {prereqTree[Object.keys(prerequisite)[0]].map((child, index) => ( + + ))}
    @@ -78,8 +95,8 @@ interface PrereqProps extends CourseGQLData { } const PrereqTree: FC = (props) => { - let hasPrereqs = props.prerequisite_tree !== ''; - let hasDependencies = Object.keys(props.prerequisite_for).length !== 0; + let hasPrereqs = JSON.stringify(props.prerequisiteTree) !== '{}'; + let hasDependencies = Object.keys(props.prerequisiteFor).length !== 0; if (props.id === undefined) return <>; else if (!hasPrereqs && !hasDependencies) @@ -107,10 +124,10 @@ const PrereqTree: FC = (props) => { <>
      - {Object.values(props.prerequisite_for).map( + {Object.values(props.prerequisiteFor).map( (dependency, index) => (
    • - +
    • ) )} @@ -134,14 +151,14 @@ const PrereqTree: FC = (props) => {
      } */} {/* Display the class id */} - + {/* Spawns the root of the prerequisite tree */} {hasPrereqs && (
      -
      @@ -162,8 +179,8 @@ const PrereqTree: FC = (props) => { }} >

      - {props.prerequisite_text !== '' && Prerequisite: } - {props.prerequisite_text} + {props.prerequisiteText !== '' && Prerequisite: } + {props.prerequisiteText}

    diff --git a/site/src/component/Review/SubReview.tsx b/site/src/component/Review/SubReview.tsx index 82e1225c..3f282658 100644 --- a/site/src/component/Review/SubReview.tsx +++ b/site/src/component/Review/SubReview.tsx @@ -88,10 +88,10 @@ const SubReview: FC = ({ review, course, professor, colors, colo

    {professor && - {professor.course_history[review.courseID].department + ' ' + professor.course_history[review.courseID].number} + {professor.courseHistory[review.courseID].department + ' ' + professor.courseHistory[review.courseID].number} } {course && - {course.instructor_history[review.professorID].name} + {course.instructorHistory[review.professorID].name} } {(!course && !professor) &&
    {review.courseID} {review.professorID} diff --git a/site/src/component/ReviewForm/ReviewForm.tsx b/site/src/component/ReviewForm/ReviewForm.tsx index 8427d899..8bf09f7d 100644 --- a/site/src/component/ReviewForm/ReviewForm.tsx +++ b/site/src/component/ReviewForm/ReviewForm.tsx @@ -166,8 +166,8 @@ const ReviewForm: FC = (props) => { (setProfessor(document.getElementsByName(e.target.value)[0].id))}> - {Object.keys(props.course?.instructor_history!).map((ucinetid, i) => { - const name = props.course?.instructor_history[ucinetid].shortened_name; + {Object.keys(props.course?.instructorHistory!).map((ucinetid, i) => { + const name = props.course?.instructorHistory[ucinetid].shortenedName; return ( // @ts-ignore name attribute isn't supported @@ -190,8 +190,8 @@ const ReviewForm: FC = (props) => { (setCourse(document.getElementsByName(e.target.value)[0].id))}> - {Object.keys(props.professor?.course_history!).map((courseID, i) => { - const name = props.professor?.course_history[courseID].department + ' ' + props.professor?.course_history[courseID].number; + {Object.keys(props.professor?.courseHistory!).map((courseID, i) => { + const name = props.professor?.courseHistory[courseID].department + ' ' + props.professor?.courseHistory[courseID].number; return ( // @ts-ignore name attribute isn't supported diff --git a/site/src/component/SearchModule/SearchModule.tsx b/site/src/component/SearchModule/SearchModule.tsx index 2fb7e3f0..666aff1e 100644 --- a/site/src/component/SearchModule/SearchModule.tsx +++ b/site/src/component/SearchModule/SearchModule.tsx @@ -55,10 +55,10 @@ const SearchModule: FC = ({ index }) => { }) let names: string[] = []; if (index == 'courses') { - names = Object.keys(nameResults); + names = Object.keys(nameResults ?? {}); } else if (index == 'professors') { - names = Object.keys(nameResults).map(n => nameResults[n].metadata.ucinetid) as string[]; + names = Object.keys(nameResults ?? {}).map(n => (nameResults![n].metadata as { ucinetid: string }).ucinetid) as string[]; } console.log('From frontend search', names) dispatch(setNames({ index, names })); diff --git a/site/src/component/SideInfo/SideInfo.tsx b/site/src/component/SideInfo/SideInfo.tsx index e6e5abe9..a63110f9 100644 --- a/site/src/component/SideInfo/SideInfo.tsx +++ b/site/src/component/SideInfo/SideInfo.tsx @@ -178,8 +178,8 @@ const SideInfo: FC = (props) => { }}> { sortedReviews.map((key, index) => - {props.searchType == 'course' && (props.course?.instructor_history[key] ? props.course?.instructor_history[key].shortened_name : key)} - {props.searchType == 'professor' && (props.professor?.course_history[key] ? (props.professor?.course_history[key].department + ' ' + props.professor?.course_history[key].number) : key)} + {props.searchType == 'course' && (props.course?.instructorHistory[key] ? props.course?.instructorHistory[key].shortenedName : key)} + {props.searchType == 'professor' && (props.professor?.courseHistory[key] ? (props.professor?.courseHistory[key].department + ' ' + props.professor?.courseHistory[key].number) : key)} ) } @@ -234,12 +234,12 @@ const SideInfo: FC = (props) => {
    {highestReview && } + displayName={props.searchType == 'course' ? props.course?.instructorHistory[highestReview].shortenedName! : + (props.professor?.courseHistory[highestReview] ? props.professor?.courseHistory[highestReview].department + ' ' + props.professor?.courseHistory[highestReview].number : highestReview)} />} {lowestReview && } + displayName={props.searchType == 'course' ? props.course?.instructorHistory[lowestReview].shortenedName! : + (props.professor?.courseHistory[lowestReview] ? props.professor?.courseHistory[lowestReview].department + ' ' + props.professor?.courseHistory[lowestReview].number : lowestReview)} />}
    ) diff --git a/site/src/helpers/util.tsx b/site/src/helpers/util.tsx index 4f88bbd4..0fbc5f6e 100644 --- a/site/src/helpers/util.tsx +++ b/site/src/helpers/util.tsx @@ -5,17 +5,17 @@ export function getCourseTags(course: CourseGQLData) { // data to be displayed in pills let tags: string[] = []; // course level - let courseLevel = course.course_level; + let courseLevel = course.courseLevel; if (courseLevel) { tags.push(`${courseLevel.substring(0, courseLevel.indexOf('('))}`); } // ge - course.ge_list.forEach(ge => { + course.geList.forEach(ge => { tags.push(`${ge.substring(0, ge.indexOf(':'))}`); }) // units - let units = course.units[0] - tags.push(`${units} unit${units != 1 ? 's' : ''}`); + const { minUnits, maxUnits } = course; + tags.push(`${minUnits === maxUnits ? maxUnits : `${minUnits}-${maxUnits}`} unit${(minUnits === maxUnits ? (maxUnits !== 1 ? 's' : '') : 's')}`); return tags; } @@ -23,7 +23,7 @@ export function getCourseTags(course: CourseGQLData) { export function searchAPIResult(type: SearchType, name: string) { return new Promise(res => { let index: SearchIndex; - if (type == 'course') { + if (type === 'course') { index = 'courses'; } else { @@ -85,44 +85,31 @@ function transformCourseGQL(data: CourseGQLResponse) { let instructorHistoryLookup: ProfessorLookup = {}; let prerequisiteListLookup: CourseLookup = {}; let prerequisiteForLookup: CourseLookup = {}; - // maps professor's ucinetid to professor basic details - data.instructor_history.forEach(professor => { - if (professor) { - instructorHistoryLookup[professor.ucinetid] = professor; - } - }) - // maps course's id to course basic details - data.prerequisite_list.forEach(course => { - if (course) { - prerequisiteListLookup[course.id] = course; - } - }) - // maps course's id to course basic details - data.prerequisite_for.forEach(course => { - if (course) { - prerequisiteForLookup[course.id] = course; - } - }) - // create copy to override fields with lookups + axios.post<{ [key: string]: CourseGQLResponse }> + (`/api/courses/api/batch`, {"courses": data.prerequisiteList}) + .then(r => prerequisiteListLookup = r.data); + axios.post<{ [key: string]: CourseGQLResponse }> + (`/api/courses/api/batch`, {"courses": data.prerequisiteFor}) + .then(r => prerequisiteForLookup = r.data); + axios.post<{ [key: string]: ProfessorGQLResponse }> + (`/api/professors/api/batch`, {"courses": data.instructorHistory}) + .then(r => instructorHistoryLookup = r.data); + // create copy to override fields with lookups let course = { ...data } as unknown as CourseGQLData; - course.instructor_history = instructorHistoryLookup; - course.prerequisite_list = prerequisiteListLookup; - course.prerequisite_for = prerequisiteForLookup; - + course.instructorHistory = instructorHistoryLookup; + course.prerequisiteList = prerequisiteListLookup; + course.prerequisiteFor = prerequisiteForLookup; return course; } function transformProfessorGQL(data: ProfessorGQLResponse) { let courseHistoryLookup: CourseLookup = {}; - // maps course's id to course basic details - data.course_history.forEach(course => { - if (course) { - courseHistoryLookup[course.id] = course; - } - }) + axios.post<{ [key: string]: CourseGQLResponse }> + (`/api/courses/api/batch`, {"courses": Object.keys(data.courseHistory)}) + .then(r => courseHistoryLookup = r.data); // create copy to override fields with lookups let professor = { ...data } as unknown as ProfessorGQLData; - professor.course_history = courseHistoryLookup; + professor.courseHistory = courseHistoryLookup; return professor; } \ No newline at end of file diff --git a/site/src/hooks/courseData.tsx b/site/src/hooks/courseData.tsx deleted file mode 100644 index 0ad34a08..00000000 --- a/site/src/hooks/courseData.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import { gql, useQuery } from '@apollo/client'; -import { CourseGQLData, SubProfessor, SubCourse, ProfessorLookup, CourseLookup } from '../types/types'; - -interface CourseGQLResponse { - course: Omit & { - instructor_history: SubProfessor[]; - prerequisite_list: SubCourse[]; - prerequisite_for: SubCourse[]; - } -} - -// given a course id, get the gql equivalent -function useCourseGQL(courseID: string | undefined) { - const query = gql` - query { - course(id: "${courseID}"){ - id - department - number - school - title - course_level - department_alias - units - description - department_name - instructor_history{ - name - ucinetid - shortened_name - } - prerequisite_tree - prerequisite_list { - id - department - number - title - } - prerequisite_text - prerequisite_for { - id - department - number - title - } - repeatability - concurrent - same_as - restriction - overlap - corequisite - ge_list - ge_text - terms - } - }`; - const { loading, error, data } = useQuery(query); - if (loading || error) { - return { loading, error, course: null }; - } - else { - if (!courseID || !data?.course) { - return { loading, error, course: null }; - } - let instructorHistoryLookup: ProfessorLookup = {}; - let prerequisiteListLookup: CourseLookup = {}; - let prerequisiteForLookup: CourseLookup = {}; - // maps professor's ucinetid to professor basic details - data!.course.instructor_history.forEach(professor => { - if (professor) { - instructorHistoryLookup[professor.ucinetid] = professor; - } - }) - // maps course's id to course basic details - data!.course.prerequisite_list.forEach(course => { - if (course) { - prerequisiteListLookup[course.id] = course; - } - }) - // maps course's id to course basic details - data!.course.prerequisite_for.forEach(course => { - if (course) { - prerequisiteForLookup[course.id] = course; - } - }) - // create copy to override fields with lookups - let course = { ...data!.course } as unknown as CourseGQLData; - course.instructor_history = instructorHistoryLookup; - course.prerequisite_list = prerequisiteListLookup; - course.prerequisite_for = prerequisiteForLookup; - return { loading, error, course: course }; - } -} - -export { useCourseGQL } \ No newline at end of file diff --git a/site/src/hooks/professorData.tsx b/site/src/hooks/professorData.tsx deleted file mode 100644 index 28b9ff76..00000000 --- a/site/src/hooks/professorData.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { gql, useQuery } from '@apollo/client'; -import { ProfessorGQLData, SubCourse, CourseLookup } from '../types/types'; - -interface ProfessorGQLResponse { - instructor: Omit & { - course_history: SubCourse[]; - } -} - -// given a course id, get the gql equivalent -function useProfessorGQL(professorID: string | undefined) { - const query = gql` - query { - instructor(ucinetid:"${professorID}"){ - name - shortened_name - ucinetid - title - department - schools - related_departments - course_history { - id - department - number - title - } - } - }`; - const { loading, error, data } = useQuery(query); - if (loading || error) { - return { loading, error, professor: null }; - } - else { - if (!professorID || !data?.instructor) { - return { loading, error, professor: null }; - } - let courseHistoryLookup: CourseLookup = {}; - // maps course's id to course basic details - data!.instructor.course_history.forEach(course => { - if (course) { - courseHistoryLookup[course.id] = course; - } - }) - // create copy to override fields with lookups - let professor = { ...data!.instructor } as unknown as ProfessorGQLData; - professor.course_history = courseHistoryLookup; - return { loading, error, professor: professor }; - } -} - -export { useProfessorGQL } \ No newline at end of file diff --git a/site/src/pages/ProfessorPage/index.tsx b/site/src/pages/ProfessorPage/index.tsx index 328414a7..11805b7f 100644 --- a/site/src/pages/ProfessorPage/index.tsx +++ b/site/src/pages/ProfessorPage/index.tsx @@ -51,7 +51,7 @@ const ProfessorPage: FC> = (props) => {
    + tags={[professorGQLData.ucinetid, professorGQLData.shortenedName]} professor={professorGQLData} />
    @@ -59,7 +59,7 @@ const ProfessorPage: FC> = (props) => {

    🗓️ Schedule of Classes

    - +

    diff --git a/site/src/pages/RoadmapPage/Course.tsx b/site/src/pages/RoadmapPage/Course.tsx index 5ed73a7d..f661746b 100644 --- a/site/src/pages/RoadmapPage/Course.tsx +++ b/site/src/pages/RoadmapPage/Course.tsx @@ -13,18 +13,18 @@ interface CourseProps extends CourseGQLData { } const Course: FC = (props) => { - let { id, department, number, title, units, description, prerequisite_text, corequisite, requiredCourses, onDelete } = props; + let { id, department, number, title, minUnits, maxUnits, description, prerequisiteText, corequisite, requiredCourses, onDelete } = props; const CoursePopover =
    {department + ' ' + number} {title}
    - {units[0]} units + {minUnits === maxUnits ? minUnits : `${minUnits}-${maxUnits}`} units
    {description}
    - {prerequisite_text &&
    - Prerequisite: {prerequisite_text} + {prerequisiteText &&
    + Prerequisite: {prerequisiteText}
    } {corequisite &&
    Corequisite: {corequisite} @@ -71,7 +71,7 @@ const Course: FC = (props) => { delay={100}> } -
    {units[0]} units
    +
    {minUnits === maxUnits ? minUnits : `${minUnits}-${maxUnits}`} units
    ); diff --git a/site/src/pages/RoadmapPage/Planner.tsx b/site/src/pages/RoadmapPage/Planner.tsx index ce15540e..d3ef6911 100644 --- a/site/src/pages/RoadmapPage/Planner.tsx +++ b/site/src/pages/RoadmapPage/Planner.tsx @@ -10,6 +10,7 @@ import { selectYearPlans, setYearPlans, setInvalidCourses, setTransfers, addYear import { useFirstRender } from "../../hooks/firstRenderer"; import { InvalidCourseData, SavedRoadmap, PlannerData, PlannerYearData, PlannerQuarterData, SavedPlannerData, SavedPlannerYearData, SavedPlannerQuarterData, BatchCourseData, MongoRoadmap } from '../../types/types'; import { searchAPIResults } from '../../helpers/util'; +import {Prerequisite, PrerequisiteTree} from "peterportal-api-next-types"; const Planner: FC = () => { const dispatch = useAppDispatch(); @@ -18,7 +19,7 @@ const Planner: FC = () => { const data = useAppSelector(selectYearPlans); const transfers = useAppSelector(state => state.roadmap.transfers); - const [missingPrerequisites, setMissingPrerequisites] = useState(new Set); + const [missingPrerequisites, setMissingPrerequisites] = useState(new Set()); useEffect(() => { // if is first render, load from local storage @@ -135,7 +136,7 @@ const Planner: FC = () => { data.forEach(year => { year.quarters.forEach(quarter => { quarter.courses.forEach(course => { - unitCount += course.units[0]; + unitCount += course.minUnits; courseCount += 1; }) }) @@ -155,14 +156,14 @@ const Planner: FC = () => { // store courses that have been taken let taken: Set = new Set(transfers.map(transfer => transfer.name)); let invalidCourses: InvalidCourseData[] = []; - let missing: Set = new Set; + let missing = new Set(); data.forEach((year, yi) => { year.quarters.forEach((quarter, qi) => { let taking: Set = new Set(quarter.courses.map(course => course.department + ' ' + course.number)); quarter.courses.forEach((course, ci) => { // if has prerequisite - if (course.prerequisite_tree) { - let required = validateCourse(taken, JSON.parse(course.prerequisite_tree), taking, course.corequisite); + if (course.prerequisiteTree) { + let required = validateCourse(taken, course.prerequisiteTree, taking, course.corequisite); // prerequisite not fulfilled, has some required classes to take if (required.size > 0) { console.log('invalid course', course.id); @@ -194,23 +195,20 @@ const Planner: FC = () => { dispatch(setInvalidCourses(invalidCourses)); } - type PrerequisiteNode = NestedPrerequisiteNode | string; - interface NestedPrerequisiteNode { - AND?: PrerequisiteNode[]; - OR?: PrerequisiteNode[]; - } + type PrerequisiteNode = Prerequisite | PrerequisiteTree; // returns set of courses that need to be taken to fulfill requirements const validateCourse = (taken: Set, prerequisite: PrerequisiteNode, taking: Set, corequisite: string): Set => { // base case just a course - if (typeof prerequisite === 'string') { + if ("prereqType" in prerequisite) { + const id = prerequisite?.courseId ?? prerequisite?.examName ?? ""; // already taken prerequisite or is currently taking the corequisite - if (taken.has(prerequisite) || (corequisite.includes(prerequisite) && taking.has(prerequisite))) { + if (taken.has(id) || (corequisite.includes(id) && taking.has(id))) { return new Set(); } // need to take this prerequisite still else { - return new Set([prerequisite]); + return new Set([id]); } } // has nested prerequisites diff --git a/site/src/pages/RoadmapPage/Quarter.tsx b/site/src/pages/RoadmapPage/Quarter.tsx index db91a668..b64c2f5e 100644 --- a/site/src/pages/RoadmapPage/Quarter.tsx +++ b/site/src/pages/RoadmapPage/Quarter.tsx @@ -35,7 +35,7 @@ const Quarter: FC = ({ year, yearIndex, quarterIndex, data }) => { let unitCount = 0; let courseCount = 0; data.courses.forEach(course => { - unitCount += course.units[0]; + unitCount += course.minUnits; courseCount += 1; }) return [unitCount, courseCount]; diff --git a/site/src/pages/RoadmapPage/Year.tsx b/site/src/pages/RoadmapPage/Year.tsx index 3aafe29f..48264806 100644 --- a/site/src/pages/RoadmapPage/Year.tsx +++ b/site/src/pages/RoadmapPage/Year.tsx @@ -64,7 +64,7 @@ const Year: FC = ({ yearIndex, data }) => { let courseCount = 0; data.quarters.forEach(quarter => { quarter.courses.forEach(course => { - unitCount += course.units[0]; + unitCount += course.minUnits; courseCount += 1; }) }) diff --git a/site/src/pages/SearchPage/CoursePopup.tsx b/site/src/pages/SearchPage/CoursePopup.tsx index b1802b93..744911d1 100644 --- a/site/src/pages/SearchPage/CoursePopup.tsx +++ b/site/src/pages/SearchPage/CoursePopup.tsx @@ -24,14 +24,14 @@ const CoursePopup: FC = () => { let scoredProfessors = new Set(res.data.map(v => v.name)); // add known scores res.data.forEach(entry => { - if (course.instructor_history[entry.name]) { - scores.push({ name: course.instructor_history[entry.name].shortened_name, score: entry.score, key: entry.name }) + if (course.instructorHistory[entry.name]) { + scores.push({ name: course.instructorHistory[entry.name].shortenedName, score: entry.score, key: entry.name }) } }) // add unknown score - Object.keys(course.instructor_history).forEach(ucinetid => { + Object.keys(course.instructorHistory).forEach(ucinetid => { if (!scoredProfessors.has(ucinetid)) { - scores.push({ name: course.instructor_history[ucinetid].shortened_name, score: -1, key: ucinetid }) + scores.push({ name: course.instructorHistory[ucinetid].shortenedName, score: -1, key: ucinetid }) } }) // sort by highest score @@ -46,7 +46,7 @@ const CoursePopup: FC = () => { let infos = [ { title: 'Prerequisite', - content: course.prerequisite_text + content: course.prerequisiteText }, { title: 'Restrictions', diff --git a/site/src/pages/SearchPage/ProfessorHitItem.tsx b/site/src/pages/SearchPage/ProfessorHitItem.tsx index 02834de6..6a10edd1 100644 --- a/site/src/pages/SearchPage/ProfessorHitItem.tsx +++ b/site/src/pages/SearchPage/ProfessorHitItem.tsx @@ -42,9 +42,9 @@ const ProfessorHitItem: FC = (props: ProfessorHitItemProp {props.title} - {Object.keys(props.course_history).length > 0 && + {Object.keys(props.courseHistory).length > 0 &&

    Recently taught:  - {Object.keys(props.course_history).map((item: string, index: number) => { + {Object.keys(props.courseHistory).map((item: string, index: number) => { return {(index ? ', ' : '')} diff --git a/site/src/pages/SearchPage/ProfessorPopup.tsx b/site/src/pages/SearchPage/ProfessorPopup.tsx index a6aa9f7c..16d53706 100644 --- a/site/src/pages/SearchPage/ProfessorPopup.tsx +++ b/site/src/pages/SearchPage/ProfessorPopup.tsx @@ -21,7 +21,7 @@ const ProfessorPopup: FC = () => { .then(res => { let scoredCourses = new Set(res.data.map(v => v.name)); res.data.forEach(v => v.key = v.name) - Object.keys(professor.course_history).forEach(course => { + Object.keys(professor.courseHistory).forEach(course => { // remove spaces course = course.replace(/\s+/g, ''); // add unknown score diff --git a/site/src/types/types.ts b/site/src/types/types.ts index c951790a..64965d4e 100644 --- a/site/src/types/types.ts +++ b/site/src/types/types.ts @@ -28,7 +28,7 @@ export interface CourseData { export interface ProfessorData { name: string; - shortened_name: string; + shortenedName: string; ucinetid: string; title: string; department: string; @@ -231,47 +231,47 @@ export interface CourseGQLData { number: string; school: string; title: string; - course_level: string; - department_alias: string[]; - units: number[]; + courseLevel: string; + minUnits: number; + maxUnits: number; description: string; - department_name: string; - instructor_history: ProfessorLookup; - prerequisite_tree: string; - prerequisite_list: CourseLookup; - prerequisite_text: string; - prerequisite_for: CourseLookup; + departmentName: string; + instructorHistory: ProfessorLookup; + prerequisiteTree: Record; + prerequisiteList: CourseLookup; + prerequisiteText: string; + prerequisiteFor: CourseLookup; repeatability: string; concurrent: string; same_as: string; restriction: string; overlap: string; corequisite: string; - ge_list: string[]; - ge_text: string; + geList: string[]; + gText: string; terms: string[]; } export interface ProfessorGQLData { name: string; - shortened_name: string; + shortenedName: string; ucinetid: string; title: string; department: string; schools: string[]; - related_departments: string[]; - course_history: CourseLookup; + relatedDepartments: string[]; + courseHistory: CourseLookup; } // PPAPI format -export type CourseGQLResponse = Omit & { - instructor_history: SubProfessor[]; - prerequisite_list: SubCourse[]; - prerequisite_for: SubCourse[]; +export type CourseGQLResponse = Omit & { + instructorHistory: string[]; + prerequisiteList: string[]; + prerequisiteFor: string[]; } -export type ProfessorGQLResponse = Omit & { - course_history: SubCourse[]; +export type ProfessorGQLResponse = Omit & { + courseHistory: Record; } // maps ucinetid to subprofessor @@ -288,7 +288,7 @@ export interface CourseLookup { export interface SubProfessor { name: string; ucinetid: string; - shortened_name: string; + shortenedName: string; } // subset of course details needed for display purposes From 0be7ea05dd7620e24caa5ac8bf7e8044704aae84 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Wed, 13 Sep 2023 22:48:44 -0700 Subject: [PATCH 03/22] feat: everything works(?) --- api/src/controllers/courses.ts | 4 +- api/src/controllers/professors.ts | 6 +- api/src/controllers/reviews.ts | 6 +- api/src/controllers/schedule.ts | 6 +- api/src/controllers/users.ts | 4 +- site/src/component/GradeDist/Chart.tsx | 6 +- site/src/component/GradeDist/GradeDist.tsx | 18 +++-- site/src/component/GradeDist/Pie.tsx | 6 +- site/src/component/PrereqTree/PrereqTree.tsx | 7 +- site/src/component/Review/SubReview.tsx | 2 +- site/src/component/ReviewForm/ReviewForm.tsx | 4 +- site/src/component/Schedule/Schedule.tsx | 23 ++++-- site/src/component/SideInfo/SideInfo.tsx | 6 +- site/src/helpers/util.tsx | 13 ++-- site/src/pages/CoursePage/index.tsx | 4 +- site/src/pages/RoadmapPage/Course.tsx | 8 +- site/src/pages/RoadmapPage/Planner.tsx | 2 +- site/src/pages/SearchPage/CourseHitItem.tsx | 2 +- site/src/types/types.ts | 79 +------------------- 19 files changed, 76 insertions(+), 130 deletions(-) diff --git a/api/src/controllers/courses.ts b/api/src/controllers/courses.ts index de615cb1..6d66cddd 100644 --- a/api/src/controllers/courses.ts +++ b/api/src/controllers/courses.ts @@ -61,7 +61,9 @@ router.get('/api/grades', let r = fetch(process.env.PUBLIC_API_URL + 'grades/raw?department=' + encodeURIComponent(req.query.department) + '&courseNumber=' + req.query.number); r.then((response) => response.json()) - .then((data) => res.send(data)) + .then((data) => { + res.send(data.payload) + }) }); export default router; \ No newline at end of file diff --git a/api/src/controllers/professors.ts b/api/src/controllers/professors.ts index e5d334f2..f8ccee27 100644 --- a/api/src/controllers/professors.ts +++ b/api/src/controllers/professors.ts @@ -39,7 +39,9 @@ router.post('/api/batch', (req: Request<{}, {}, { professors: string[] }>, res) }); r.then((response) => response.json()) - .then((data) => res.json(data.data)) + .then((data) => { + res.json(data.data) + }) } }); @@ -54,7 +56,7 @@ router.get('/api/grades/:name', function (req, res, next) { r.then((response) => { status = response.status; return response.json(); - }).then((data) => res.status(status).send(data)) + }).then((data) => res.status(status).send(data.payload)) }); export default router; \ No newline at end of file diff --git a/api/src/controllers/reviews.ts b/api/src/controllers/reviews.ts index 84b2599f..c424a6c9 100644 --- a/api/src/controllers/reviews.ts +++ b/api/src/controllers/reviews.ts @@ -197,7 +197,7 @@ router.delete('/', async (req, res, next) => { * Upvote or downvote a review */ router.patch("/vote", async function (req, res) { - if (req.session.passport != null) { + if (req.session?.passport != null) { //get id and delta score from initial vote let id = req.body["id"]; let deltaScore = req.body["upvote"] ? 1 : -1; @@ -263,7 +263,7 @@ router.patch("/vote", async function (req, res) { */ router.patch("/getVoteColor", async function (req, res) { //make sure user is logged in - if (req.session.passport != null) { + if (req.session?.passport != null) { //query of the user's email and the review id let query = { userID: req.session.passport.user.email, @@ -290,7 +290,7 @@ router.patch("/getVoteColor", async function (req, res) { * Get multiple review colors */ router.patch("/getVoteColors", async function (req, res) { - if (req.session.passport != null) { + if (req.session?.passport != null) { //query of the user's email and the review id let ids = req.body["ids"]; let colors = []; diff --git a/api/src/controllers/schedule.ts b/api/src/controllers/schedule.ts index bd62d796..7d10940a 100644 --- a/api/src/controllers/schedule.ts +++ b/api/src/controllers/schedule.ts @@ -32,7 +32,7 @@ router.get("/getTerms", function (req, res) { */ router.get('/api/currentWeek', async function (_, res) { console.log(process.env.PUBLIC_API_URL) - const apiResp = await fetch(`${process.env.PUBLIC_API_URL}/week`); + const apiResp = await fetch(`${process.env.PUBLIC_API_URL}week`); const json = await apiResp.json(); res.send(json.payload) }); @@ -41,7 +41,7 @@ router.get('/api/currentWeek', async function (_, res) { * Get the current quarter on websoc */ router.get('/api/currentQuarter', async function (_, res) { - const apiResp = await fetch(`${process.env.PUBLIC_API_URL}/websoc/terms`); + const apiResp = await fetch(`${process.env.PUBLIC_API_URL}websoc/terms`); const json = await apiResp.json(); res.send(json.payload[0].longName) }); @@ -75,7 +75,7 @@ router.get('/api/:term/:professor', async function (req, res) { async function callPPAPIWebSoc(params: Record) { const url: URL = new URL(process.env.PUBLIC_API_URL + 'websoc?' + new URLSearchParams(params)) - return await fetch(url).then(response => response.json()); + return await fetch(url).then(response => response.json()).then(json => json.payload); } export default router; \ No newline at end of file diff --git a/api/src/controllers/users.ts b/api/src/controllers/users.ts index f7f5d0e8..2383d515 100644 --- a/api/src/controllers/users.ts +++ b/api/src/controllers/users.ts @@ -19,11 +19,11 @@ router.get('/', function (req, res, next) { */ router.get('/isAdmin', function (req, res, next) { // not logged in - if (!req.session.passport) { + if (!req.session?.passport) { res.json({ admin: false }); } else { - res.json({ admin: req.session.passport.admin ? true : false }); + res.json({ admin: req.session.passport.admin }); } }); diff --git a/site/src/component/GradeDist/Chart.tsx b/site/src/component/GradeDist/Chart.tsx index b762b57f..65630291 100644 --- a/site/src/component/GradeDist/Chart.tsx +++ b/site/src/component/GradeDist/Chart.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { ResponsiveBar, BarTooltipDatum } from '@nivo/bar' -import { GradeDistData } from '../../types/types'; +import { GradesRaw } from "peterportal-api-next-types"; const colors = { 'A': '#60A3D1', 'B': '#81C284', 'C': '#F5D77F', 'D': '#ECAD6D', 'F': '#E8966D', 'P': '#EBEBEB', 'NP': '#EBEBEB' } const getColor = (bar: Bar) => colors[bar.id] @@ -20,7 +20,7 @@ interface Bar { } interface ChartProps { - gradeData: GradeDistData; + gradeData: GradesRaw; quarter: string; professor?: string; course?: string; @@ -52,7 +52,7 @@ export default class Chart extends React.Component { this.props.gradeData.forEach(data => { if ((data.quarter + ' ' + data.year === this.props.quarter || this.props.quarter == 'ALL') - && (data.instructor === this.props.professor || (data.department + ' ' + data.number) === this.props.course)) { + && (data.instructors.includes(this.props.professor ?? "") || (data.department + ' ' + data.courseNumber) === this.props.course)) { gradeACount += data.gradeACount; gradeBCount += data.gradeBCount; gradeCCount += data.gradeCCount; diff --git a/site/src/component/GradeDist/GradeDist.tsx b/site/src/component/GradeDist/GradeDist.tsx index 7cfa18b1..9da459d8 100644 --- a/site/src/component/GradeDist/GradeDist.tsx +++ b/site/src/component/GradeDist/GradeDist.tsx @@ -5,7 +5,8 @@ import Pie from './Pie'; import './GradeDist.scss' import axios from 'axios' -import { CourseGQLData, ProfessorGQLData, GradeDistData } from '../../types/types'; +import { CourseGQLData, ProfessorGQLData } from '../../types/types'; +import {GradesRaw} from "peterportal-api-next-types"; interface GradeDistProps { course?: CourseGQLData; @@ -26,7 +27,7 @@ const GradeDist: FC = (props) => { * @param props attributes received from the parent element */ - const [gradeDistData, setGradeDistData] = useState(null!); + const [gradeDistData, setGradeDistData] = useState(null!); const [chartType, setChartType] = useState('bar'); const [currentQuarter, setCurrentQuarter] = useState(''); const [currentProf, setCurrentProf] = useState(''); @@ -43,16 +44,17 @@ const GradeDist: FC = (props) => { url = `/api/courses/api/grades`; params = { department: props.course.department, - number: props.course.number + number: props.course.courseNumber } } else if (props.professor) { url = `/api/professors/api/grades/${props.professor.shortenedName}`; } - const res = axios.get(url, { + const res = axios.get(url, { params: params }) .then(res => { + console.log(res) setGradeDistData(res.data); }).catch(error => { setGradeDistData([]); @@ -95,10 +97,10 @@ const GradeDist: FC = (props) => { gradeDistData .filter(entry => { - if (props.course && entry.instructor === currentProf) { + if (props.course && entry.instructors.includes(currentProf)) { return true; } - if (props.professor && (entry.department + ' ' + entry.number) == currentCourse) { + if (props.professor && (entry.department + ' ' + entry.courseNumber) == currentCourse) { return true; } return false; @@ -119,7 +121,7 @@ const GradeDist: FC = (props) => { let result: Entry[] = []; gradeDistData - .forEach(match => professors.add(match.instructor)); + .forEach(match => match.instructors.forEach((prof) => professors.add(prof))); professors.forEach(professor => result.push( { value: professor, text: professor } @@ -138,7 +140,7 @@ const GradeDist: FC = (props) => { let result: Entry[] = []; gradeDistData - .forEach(match => courses.add(match.department + ' ' + match.number)); + .forEach(match => courses.add(match.department + ' ' + match.courseNumber)); courses.forEach(course => result.push( { value: course, text: course } diff --git a/site/src/component/GradeDist/Pie.tsx b/site/src/component/GradeDist/Pie.tsx index edf7f46d..fe7e7e8e 100644 --- a/site/src/component/GradeDist/Pie.tsx +++ b/site/src/component/GradeDist/Pie.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { ResponsivePie, PieTooltipProps } from '@nivo/pie'; -import { GradeDistData } from '../../types/types'; +import { GradesRaw } from "peterportal-api-next-types"; const gradeScale = ['A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-'] const gpaScale = [4.0, 3.7, 3.3, 3.0, 2.7, 2.3, 2.0, 1.7, 1.3, 1.0, 0, 7] @@ -14,7 +14,7 @@ interface Slice { } interface PieProps { - gradeData: GradeDistData; + gradeData: GradesRaw; quarter: string; professor?: string; course?: string; @@ -41,7 +41,7 @@ export default class Pie extends React.Component { this.props.gradeData.forEach(data => { if ((data.quarter + ' ' + data.year === this.props.quarter || this.props.quarter == 'ALL') - && (data.instructor === this.props.professor || (data.department + ' ' + data.number) === this.props.course)) { + && (data.instructors.includes(this.props.professor ?? "") || (data.department + ' ' + data.courseNumber) === this.props.course)) { gradeACount += data.gradeACount; gradeBCount += data.gradeBCount; gradeCCount += data.gradeCCount; diff --git a/site/src/component/PrereqTree/PrereqTree.tsx b/site/src/component/PrereqTree/PrereqTree.tsx index 386f4ef4..7efa72a0 100644 --- a/site/src/component/PrereqTree/PrereqTree.tsx +++ b/site/src/component/PrereqTree/PrereqTree.tsx @@ -8,6 +8,7 @@ import { CourseGQLData, CourseLookup } from '../../types/types'; interface NodeProps { node: string; label: string; + content: string; index?: number; } @@ -53,7 +54,7 @@ const PrereqTreeNode: FC = (props) => { label={`${prereq.courseId ?? prereq.examName ?? ''}${ prereq?.minGrade ? ` (min grade = ${prereq?.minGrade})` : '' }${prereq?.coreq ? ' (coreq)' : ''}`} - node={'prerequisite-node'} + content={prereq.courseId ?? ""} node={'prerequisite-node'} /> ); @@ -127,7 +128,7 @@ const PrereqTree: FC = (props) => { {Object.values(props.prerequisiteFor).map( (dependency, index) => (

  • - +
  • ) )} @@ -151,7 +152,7 @@ const PrereqTree: FC = (props) => {
    } */} {/* Display the class id */} - + {/* Spawns the root of the prerequisite tree */} {hasPrereqs && ( diff --git a/site/src/component/Review/SubReview.tsx b/site/src/component/Review/SubReview.tsx index 3f282658..56a1d75c 100644 --- a/site/src/component/Review/SubReview.tsx +++ b/site/src/component/Review/SubReview.tsx @@ -88,7 +88,7 @@ const SubReview: FC = ({ review, course, professor, colors, colo

    {professor && - {professor.courseHistory[review.courseID].department + ' ' + professor.courseHistory[review.courseID].number} + {professor.courseHistory[review.courseID].department + ' ' + professor.courseHistory[review.courseID].courseNumber} } {course && {course.instructorHistory[review.professorID].name} diff --git a/site/src/component/ReviewForm/ReviewForm.tsx b/site/src/component/ReviewForm/ReviewForm.tsx index 8bf09f7d..d98b6265 100644 --- a/site/src/component/ReviewForm/ReviewForm.tsx +++ b/site/src/component/ReviewForm/ReviewForm.tsx @@ -191,7 +191,7 @@ const ReviewForm: FC = (props) => { onChange={(e) => (setCourse(document.getElementsByName(e.target.value)[0].id))}> {Object.keys(props.professor?.courseHistory!).map((courseID, i) => { - const name = props.professor?.courseHistory[courseID].department + ' ' + props.professor?.courseHistory[courseID].number; + const name = props.professor?.courseHistory[courseID].department + ' ' + props.professor?.courseHistory[courseID].courseNumber; return ( // @ts-ignore name attribute isn't supported @@ -209,7 +209,7 @@ const ReviewForm: FC = (props) => { -

    It's your turn to review {props.course ? (props.course?.department + ' ' + props.course?.number) : props.professor?.name}

    +

    It's your turn to review {props.course ? (props.course?.department + ' ' + props.course?.courseNumber) : props.professor?.name}

    diff --git a/site/src/component/Schedule/Schedule.tsx b/site/src/component/Schedule/Schedule.tsx index f048a0c8..ca7cc3d7 100644 --- a/site/src/component/Schedule/Schedule.tsx +++ b/site/src/component/Schedule/Schedule.tsx @@ -6,7 +6,8 @@ import Table from 'react-bootstrap/Table'; import ProgressBar from 'react-bootstrap/ProgressBar'; import Button from 'react-bootstrap/Button'; -import { WebsocResponse, Section } from '../../types/types'; +import { WebsocAPIResponse as WebsocResponse, WebsocSection as Section } from 'peterportal-api-next-types'; +import {hourMinuteTo12HourString} from "../../helpers/util"; interface ScheduleProps { courseID?: string; @@ -73,7 +74,7 @@ const Schedule: FC = (props) => { ) } - else if (section.status == 'WAITL') { + else if (section.status == 'Waitl') { return ( // @ts-ignore @@ -99,7 +100,7 @@ const Schedule: FC = (props) => {

    ) } - else if (section.status == 'WAITL') { + else if (section.status == 'Waitl') { return (
    @@ -123,9 +124,19 @@ const Schedule: FC = (props) => { {section.sectionCode} {section.sectionType} {section.sectionNum} {section.units} - {section.instructors[0]} - {section.meetings[0].time} - {section.meetings[0].bldg} + {section.instructors.join("\n")} + { + section.meetings.map( + meeting => meeting.timeIsTBA + ? "TBA" + : `${meeting.days} ${ + hourMinuteTo12HourString(meeting.startTime!) + } - ${ + hourMinuteTo12HourString(meeting.endTime!) + }` + ).join("\n") + } + {section.meetings.map(meeting => meeting.bldg).join("\n")} diff --git a/site/src/component/SideInfo/SideInfo.tsx b/site/src/component/SideInfo/SideInfo.tsx index a63110f9..02a4c0f6 100644 --- a/site/src/component/SideInfo/SideInfo.tsx +++ b/site/src/component/SideInfo/SideInfo.tsx @@ -179,7 +179,7 @@ const SideInfo: FC = (props) => { { sortedReviews.map((key, index) => {props.searchType == 'course' && (props.course?.instructorHistory[key] ? props.course?.instructorHistory[key].shortenedName : key)} - {props.searchType == 'professor' && (props.professor?.courseHistory[key] ? (props.professor?.courseHistory[key].department + ' ' + props.professor?.courseHistory[key].number) : key)} + {props.searchType == 'professor' && (props.professor?.courseHistory[key] ? (props.professor?.courseHistory[key].department + ' ' + props.professor?.courseHistory[key].courseNumber) : key)} ) } @@ -235,11 +235,11 @@ const SideInfo: FC = (props) => { {highestReview && } + (props.professor?.courseHistory[highestReview] ? props.professor?.courseHistory[highestReview].department + ' ' + props.professor?.courseHistory[highestReview].courseNumber : highestReview)} />} {lowestReview && } + (props.professor?.courseHistory[lowestReview] ? props.professor?.courseHistory[lowestReview].department + ' ' + props.professor?.courseHistory[lowestReview].courseNumber : lowestReview)} />}
    ) diff --git a/site/src/helpers/util.tsx b/site/src/helpers/util.tsx index 0fbc5f6e..e17c0212 100644 --- a/site/src/helpers/util.tsx +++ b/site/src/helpers/util.tsx @@ -86,13 +86,13 @@ function transformCourseGQL(data: CourseGQLResponse) { let prerequisiteListLookup: CourseLookup = {}; let prerequisiteForLookup: CourseLookup = {}; axios.post<{ [key: string]: CourseGQLResponse }> - (`/api/courses/api/batch`, {"courses": data.prerequisiteList}) + (`/api/courses/api/batch`, {"courses": data.prerequisiteList.map((x) => x.replace(/ /g, ""))}) .then(r => prerequisiteListLookup = r.data); axios.post<{ [key: string]: CourseGQLResponse }> - (`/api/courses/api/batch`, {"courses": data.prerequisiteFor}) + (`/api/courses/api/batch`, {"courses": data.prerequisiteFor.map((x) => x.replace(/ /g, ""))}) .then(r => prerequisiteForLookup = r.data); axios.post<{ [key: string]: ProfessorGQLResponse }> - (`/api/professors/api/batch`, {"courses": data.instructorHistory}) + (`/api/professors/api/batch`, {"professors": data.instructorHistory}) .then(r => instructorHistoryLookup = r.data); // create copy to override fields with lookups let course = { ...data } as unknown as CourseGQLData; @@ -105,11 +105,14 @@ function transformCourseGQL(data: CourseGQLResponse) { function transformProfessorGQL(data: ProfessorGQLResponse) { let courseHistoryLookup: CourseLookup = {}; axios.post<{ [key: string]: CourseGQLResponse }> - (`/api/courses/api/batch`, {"courses": Object.keys(data.courseHistory)}) + (`/api/courses/api/batch`, {"courses": Object.keys(data.courseHistory).map((x) => x.replace(/ /g, ""))}) .then(r => courseHistoryLookup = r.data); // create copy to override fields with lookups let professor = { ...data } as unknown as ProfessorGQLData; professor.courseHistory = courseHistoryLookup; return professor; -} \ No newline at end of file +} + +export const hourMinuteTo12HourString = ({ hour, minute }: { hour: number, minute: number }) => + `${hour % 12}:${minute.toString().padStart(2, "0")} ${Math.floor(hour / 12) === 0 ? "AM" : "PM"}`; diff --git a/site/src/pages/CoursePage/index.tsx b/site/src/pages/CoursePage/index.tsx index 1c69b85f..96c1ef47 100644 --- a/site/src/pages/CoursePage/index.tsx +++ b/site/src/pages/CoursePage/index.tsx @@ -51,7 +51,7 @@ const CoursePage: FC> = (props) => {
    -
    @@ -69,7 +69,7 @@ const CoursePage: FC> = (props) => {

    🗓️ Schedule of Classes

    - +
    diff --git a/site/src/pages/RoadmapPage/Course.tsx b/site/src/pages/RoadmapPage/Course.tsx index f661746b..aaec9297 100644 --- a/site/src/pages/RoadmapPage/Course.tsx +++ b/site/src/pages/RoadmapPage/Course.tsx @@ -13,12 +13,12 @@ interface CourseProps extends CourseGQLData { } const Course: FC = (props) => { - let { id, department, number, title, minUnits, maxUnits, description, prerequisiteText, corequisite, requiredCourses, onDelete } = props; + let { id, department, courseNumber, title, minUnits, maxUnits, description, prerequisiteText, corequisite, requiredCourses, onDelete } = props; const CoursePopover =
    -
    {department + ' ' + number} {title}
    +
    {department + ' ' + courseNumber} {title}
    {minUnits === maxUnits ? minUnits : `${minUnits}-${maxUnits}`} units
    @@ -40,14 +40,14 @@ const Course: FC = (props) => { const courseRoute = () => { - return '/course/' + props.department.replace(/\s+/g, '') + props.number.replace(/\s+/g, '') + return '/course/' + props.department.replace(/\s+/g, '') + props.courseNumber.replace(/\s+/g, '') } return (
    - {department + ' ' + number} + {department + ' ' + courseNumber} { let missing = new Set(); data.forEach((year, yi) => { year.quarters.forEach((quarter, qi) => { - let taking: Set = new Set(quarter.courses.map(course => course.department + ' ' + course.number)); + let taking: Set = new Set(quarter.courses.map(course => course.department + ' ' + course.courseNumber)); quarter.courses.forEach((course, ci) => { // if has prerequisite if (course.prerequisiteTree) { diff --git a/site/src/pages/SearchPage/CourseHitItem.tsx b/site/src/pages/SearchPage/CourseHitItem.tsx index 2140cd51..20bfdee9 100644 --- a/site/src/pages/SearchPage/CourseHitItem.tsx +++ b/site/src/pages/SearchPage/CourseHitItem.tsx @@ -37,7 +37,7 @@ const CourseHitItem: FC = (props) => {

    - {props.department} {props.number} {props.title} + {props.department} {props.courseNumber} {props.title}

    diff --git a/site/src/types/types.ts b/site/src/types/types.ts index 64965d4e..8cd7bf9f 100644 --- a/site/src/types/types.ts +++ b/site/src/types/types.ts @@ -40,28 +40,6 @@ export interface ProfessorData { export type BatchCourseData = { [key: string]: CourseGQLData }; export type BatchProfessorData = { [key: string]: ProfessorGQLData }; -export type GradeDistData = GradeData[]; - -export interface GradeData { - year: string; - quarter: string; - department: string; - number: string; - code: number; - section: string; - instructor: string; - type: string; - gradeACount: number; - gradeBCount: number; - gradeCCount: number; - gradeDCount: number; - gradeFCount: number; - gradePCount: number; - gradeNPCount: number; - gradeWCount: number; - averageGPA: number; -} - export interface ReviewData { _id?: string; professorID: string; @@ -228,7 +206,7 @@ export interface VoteColor { export interface CourseGQLData { id: string; department: string; - number: string; + courseNumber: string; school: string; title: string; courseLevel: string; @@ -295,59 +273,6 @@ export interface SubProfessor { export interface SubCourse { id: string; department: string; - number: string; - title: string; -} - -/* - * WebSoc schedule types - */ -export interface WebsocResponse { - schools: School[] -} -export interface School { - schoolName: string; - schoolComment: string; - departments: Department[]; -} -export interface Department { - deptName: string; - deptCode: string; - deptComment: string; - courses: Course[]; - sectionCodeRangeComments: string[]; - courseNumberRangeComments: string[]; -} -export interface Course { courseNumber: string; - courseTitle: string; - courseComment: string; - prerequisiteLink: string; - sections: Section[]; -} -export interface Section { - sectionCode: string; - sectionType: string; - sectionNum: string; - units: string; - instructors: string[]; - meetings: Meeting[]; - finalExam: string; - maxCapacity: string; - numCurrentlyEnrolled: EnrollmentCount; - numOnWaitlist: string; - numRequested: string; - numNewOnlyReserved: string; - restrictions: string; - status: string; - sectionComment: string; -} -export interface Meeting { - days: string; - time: string; - bldg: string; + title: string; } -export interface EnrollmentCount { - totalEnrolled: string; - sectionEnrolled: string; -} \ No newline at end of file From 9203d9af23bfcd53499d959c4fd7b02da91dbe16 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Wed, 13 Sep 2023 22:55:36 -0700 Subject: [PATCH 04/22] fix: update footer --- site/src/component/Footer/Footer.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/site/src/component/Footer/Footer.tsx b/site/src/component/Footer/Footer.tsx index b205ebb3..aa94cbcd 100644 --- a/site/src/component/Footer/Footer.tsx +++ b/site/src/component/Footer/Footer.tsx @@ -1,16 +1,16 @@ import React, { FC } from 'react' import './Footer.scss' -const Footer: FC = (props) => { +const Footer: FC = () => { return ( <> From aad7f0e64499d66e7f5c3e0c9b07f00be2561f26 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Thu, 14 Sep 2023 19:07:37 -0700 Subject: [PATCH 05/22] feat: more cleanup --- api/src/controllers/schedule.ts | 2 - api/src/helpers/currentQuarter.ts | 45 ----- api/src/helpers/week.ts | 243 ------------------------- site/src/pages/RoadmapPage/Course.tsx | 8 +- site/src/pages/RoadmapPage/Planner.tsx | 2 +- site/src/types/types.ts | 6 +- 6 files changed, 8 insertions(+), 298 deletions(-) delete mode 100644 api/src/helpers/currentQuarter.ts delete mode 100644 api/src/helpers/week.ts diff --git a/api/src/controllers/schedule.ts b/api/src/controllers/schedule.ts index 7d10940a..959ce335 100644 --- a/api/src/controllers/schedule.ts +++ b/api/src/controllers/schedule.ts @@ -3,8 +3,6 @@ */ import express from 'express'; -import { getWeek } from '../helpers/week'; -import { getCurrentQuarter } from '../helpers/currentQuarter'; import fetch from 'node-fetch'; var router = express.Router(); diff --git a/api/src/helpers/currentQuarter.ts b/api/src/helpers/currentQuarter.ts deleted file mode 100644 index 6beca877..00000000 --- a/api/src/helpers/currentQuarter.ts +++ /dev/null @@ -1,45 +0,0 @@ -import fetch from 'node-fetch'; -import cheerio from 'cheerio'; -import { COLLECTION_NAMES, setValue, getValue } from './mongo'; - -export function getCurrentQuarter() { - return new Promise(async (resolve, reject) => { - // check if is in the cache - let cacheKey = `currentQuarter` - let cacheValue = await getValue(COLLECTION_NAMES.SCHEDULE, cacheKey) - if (cacheValue) { - // how many days since last update - let age = Math.round(((new Date()).getTime() - cacheValue.date) / (1000 * 60 * 60 * 24)); - // use cached value if within a day old - if (age < 1) { - resolve(cacheValue.value); - return; - } - } - - // get websoc page - let url = `https://www.reg.uci.edu/perl/WebSoc`; - let res = await fetch(url); - let text = await res.text(); - // scrape websoc - let $ = cheerio.load(text); - // get the quarter dropdown - let selects = $('select[name="YearTerm"]').toArray(); - if (selects.length > 0) { - // get the first item - let firstQuarter = $(selects[0]).find('option[selected="selected"]').first(); - if (firstQuarter) { - // get the text in the selected item - let currentQuarter = $(firstQuarter).text(); - // cache the value and the date acquired - setValue(COLLECTION_NAMES.SCHEDULE, cacheKey, { - date: new Date(), - value: currentQuarter - }); - resolve(currentQuarter); - return; - } - } - reject(''); - }) -} \ No newline at end of file diff --git a/api/src/helpers/week.ts b/api/src/helpers/week.ts deleted file mode 100644 index 44d76600..00000000 --- a/api/src/helpers/week.ts +++ /dev/null @@ -1,243 +0,0 @@ -/** - @module WeekHelper -*/ - -import fetch from 'node-fetch'; -import cheerio, { CheerioAPI, Element } from 'cheerio'; -import { COLLECTION_NAMES, setValue, getValue } from './mongo'; -import { QuarterMapping, WeekData } from '../types/types'; -import dayjs from 'dayjs'; -import utc from 'dayjs/plugin/utc'; -import timezone from 'dayjs/plugin/timezone'; - - -dayjs.extend(utc) -dayjs.extend(timezone) - -const PACIFIC_TIME = 'America/Los_Angeles'; -dayjs.tz.setDefault(PACIFIC_TIME); - -const MONDAY = 1; // day index (sunday=0, ..., saturday=6) - -/** - * Get the current week and quarter. A display string is also provided. - */ -export function getWeek(): Promise { - return new Promise(async resolve => { - // current date - let date = dayjs().tz(); - // current year - let year = date.year(); - - // check for current year to current year + 1 - let quarterMapping1 = await getQuarterMapping(year) as QuarterMapping; - let potentialWeek = findWeek(date, quarterMapping1); - // if the date lies within this page - if (potentialWeek) { - resolve(potentialWeek); - return; - } - - // check for current year - 1 to current year - let quarterMapping2 = await getQuarterMapping(year - 1) as QuarterMapping; - potentialWeek = findWeek(date, quarterMapping2); - if (potentialWeek) { - resolve(potentialWeek); - } - else { - // date not in any school term, probably in break - resolve({ - week: -1, - quarter: 'N/A', - display: 'Enjoy your break!😎', - }); - } - }) -} - -/** - * - * @param date Today's date - * @param quarterMapping Maps a quarter to its start and end date - * @returns Week description if it lies within the quarter - */ -function findWeek(date: dayjs.Dayjs, quarterMapping: QuarterMapping): WeekData { - let result: WeekData = undefined!; - // iterate through each quarter - Object.keys(quarterMapping).forEach(function (quarter) { - let beginDate = new Date(quarterMapping[quarter]['begin']); - let endDate = new Date(quarterMapping[quarter]['end']); - - let begin: dayjs.Dayjs; - let end: dayjs.Dayjs; - - // begin/end dates are incorrectly in UTC+0, likely due to AWS servers being in UTC+0 by default - // so for example, Winter 2023 starts on Monday Jan 9, 2023 PST - // the begin date on production is incorrectly Jan 9, 2023 00:00 UTC+0, when it should be - // Jan 9, 2023 0:00 UTC-8 (since Irvine is in PST which is 8 hours behind UTC) - // we want to fix this offset for accurate comparsions - if (beginDate.getUTCHours() === 0) { - begin = dayjs.tz(beginDate.toISOString()); - end = dayjs.tz(endDate.toISOString()); - } else { // default case if the dates aren't in UTC+0 and are in correct timezone - begin = dayjs(beginDate).tz(); - end = dayjs(endDate).tz(); - } - - // adjust instruction end date to the beginning of the following day - end = end.add(1, 'day'); - - // moves day back to Monday (if it isn't already such as in fall quarter which starts on a Thursday) - // so that each new week starts on a Monday (rather than on Thursday as it was incorrectly calculating for fall quarter) - begin = begin.day(MONDAY); - - // check if the date lies within the start/end range - if (date >= begin && date < end) { - let isFallQuarter = quarter.toLowerCase().includes('fall'); - let week = Math.floor(date.diff(begin, 'weeks')) + (isFallQuarter ? 0 : 1); // if it's fall quarter, start counting at week 0, otherwise 1 - let display = `Week ${week} • ${quarter}` - result = { - week: week, - quarter: quarter, - display: display - } - } - // check if date is after instruction end date and by no more than 1 week - finals week - else if (date >= end && date < end.add(1, 'week')) { - let display = `Finals Week • ${quarter}. Good Luck!🤞` - result = { - week: -1, - quarter: quarter, - display: display - } - } - }); - return result; -} - -/** - * Given a year, get quarter to date range mapping - * @param year Academic year to search for - * @returns Mapping of quarters to its start and end date - */ -async function getQuarterMapping(year: number): Promise { - return new Promise(async resolve => { - // check if is in the cache - let cacheKey = `quarterMapping${year}` - let cacheValue = await getValue(COLLECTION_NAMES.SCHEDULE, cacheKey) - if (cacheValue) { - resolve(cacheValue); - return; - } - // maps quarter description to day range - let quarterToDayMapping = {} - // url to academic calendar - let url = `https://reg.uci.edu/calendars/quarterly/${year}-${year + 1}/quarterly${year % 100}-${(year % 100) + 1}.html` - let res = await fetch(url); - let text = await res.text(); - // scrape the calendar - let $ = cheerio.load(text); - // load all tables on the page - let tables = $('table[class="calendartable"]').toArray() - // process each table - tables.forEach(table => { - processTable(table, $, quarterToDayMapping, year); - }) - await setValue(COLLECTION_NAMES.SCHEDULE, cacheKey, quarterToDayMapping); - resolve(quarterToDayMapping); - }) -} - -/** - * Parses the quarter names from table labels and processes each row to find the start and end dates - * @param table Cherio table element - * @param $ Cherio command - * @param quarterToDayMapping Mapping to store data into - * @param year Beginning academic year - */ -function processTable(table: Element, $: CheerioAPI, quarterToDayMapping: QuarterMapping, year: number) { - // find the tbody - let tbody = $(table).find('tbody'); - // reference all rows in the table - let rows = tbody.find('tr').toArray() as Element[]; - // the first row has all the labels for the table - let tableLabels = $(rows[0]).find('td').toArray() as Element[]; - rows.forEach(row => { - // process each row - processRow(row, $, quarterToDayMapping, tableLabels, year) - }); -} - -/** - * Checks if a row contains info on beginning or end date - * @param row Cherio row element - * @param $ Cherio command - * @param quarterToDayMapping Mapping to store data into - * @param tableLabels Column labels in the current table - * @param year Beginning academic year - */ -function processRow(row: Element, $: CheerioAPI, quarterToDayMapping: QuarterMapping, tableLabels: Element[], year: number) { - // get all information from row - let rowInfo = $(row).find('td').toArray() - // start date - if ($(rowInfo[0]).text() == 'Instruction begins') { - // for each season - for (let i = 1; i < 4; i++) { - let dateEntry = $(rowInfo[i]).text() - let dateLabel = strip($(tableLabels[i]).text()); - quarterToDayMapping[dateLabel] = { 'begin': processDate(dateEntry, dateLabel, year), 'end': new Date() }; - } - } - // end date - else if ($(rowInfo[0]).text() == 'Instruction ends') { - // for each season - for (let i = 1; i < 4; i++) { - let dateEntry = $(rowInfo[i]).text() - let dateLabel = strip($(tableLabels[i]).text()); - quarterToDayMapping[dateLabel]['end'] = processDate(dateEntry, dateLabel, year); - } - } -} - -/** - * Form a date object based on data from the calendar - * @example - * // returns Date(1/17/2020) - * processDate('Jan 17', 'Winter 2020', 2019) - * @example - * // returns Date(7/30/2021) - * processDate('Jul 30', 'Summer Session 10WK', 2020) - * @param dateEntry Date entry on the calendar - * @param dateLabel Date label on the calendar - * @param year Beginning academic year - * @returns Date for the corresponding table entry - */ -function processDate(dateEntry: string, dateLabel: string, year: number): Date { - let splitDateEntry = dateEntry.split(' '); - let month = splitDateEntry[0]; - let day = splitDateEntry[1]; - let labelYear = dateLabel.split(' ')[1]; - // 'Winter 2020' => 2020, but 'Summer Session I' => Session - // Exception for Summer Session - let correctYear = isInteger(labelYear) ? parseInt(labelYear) : year + 1; - - return dayjs.tz(`${correctYear}-${month}-${day}`).toDate(); -} - -/** - * Remove trailing/leading whitespace from string - * @param str Original string - * @returns New string with whitespace removed - */ -function strip(str: string): string { - return str.replace(/^\s+|\s+$/g, ''); -} - -/** - * Determine if a number is an integer or not - * @param num Number to test - * @returns True if is an integer - */ -function isInteger(num: string): boolean { - return !isNaN(parseInt(num, 10)) -} \ No newline at end of file diff --git a/site/src/pages/RoadmapPage/Course.tsx b/site/src/pages/RoadmapPage/Course.tsx index aaec9297..47b7cd4b 100644 --- a/site/src/pages/RoadmapPage/Course.tsx +++ b/site/src/pages/RoadmapPage/Course.tsx @@ -13,7 +13,7 @@ interface CourseProps extends CourseGQLData { } const Course: FC = (props) => { - let { id, department, courseNumber, title, minUnits, maxUnits, description, prerequisiteText, corequisite, requiredCourses, onDelete } = props; + let { id, department, courseNumber, title, minUnits, maxUnits, description, prerequisiteText, corequisites, requiredCourses, onDelete } = props; const CoursePopover = @@ -24,10 +24,10 @@ const Course: FC = (props) => {
    {description}
    {prerequisiteText &&
    - Prerequisite: {prerequisiteText} + Prerequisites: {prerequisiteText}
    } - {corequisite &&
    - Corequisite: {corequisite} + {corequisites &&
    + Corequisites: {corequisites}
    }
    diff --git a/site/src/pages/RoadmapPage/Planner.tsx b/site/src/pages/RoadmapPage/Planner.tsx index 7d3bb108..82059d95 100644 --- a/site/src/pages/RoadmapPage/Planner.tsx +++ b/site/src/pages/RoadmapPage/Planner.tsx @@ -163,7 +163,7 @@ const Planner: FC = () => { quarter.courses.forEach((course, ci) => { // if has prerequisite if (course.prerequisiteTree) { - let required = validateCourse(taken, course.prerequisiteTree, taking, course.corequisite); + let required = validateCourse(taken, course.prerequisiteTree, taking, course.corequisites); // prerequisite not fulfilled, has some required classes to take if (required.size > 0) { console.log('invalid course', course.id); diff --git a/site/src/types/types.ts b/site/src/types/types.ts index 8cd7bf9f..78c8eb74 100644 --- a/site/src/types/types.ts +++ b/site/src/types/types.ts @@ -221,12 +221,12 @@ export interface CourseGQLData { prerequisiteFor: CourseLookup; repeatability: string; concurrent: string; - same_as: string; + sameAs: string; restriction: string; overlap: string; - corequisite: string; + corequisites: string; geList: string[]; - gText: string; + geText: string; terms: string[]; } From a818227c63bafb8de5f4ddecf6a5e108d80c08fd Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Thu, 14 Sep 2023 19:53:14 -0700 Subject: [PATCH 06/22] fix: properly case GitHub --- site/src/component/Footer/Footer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/component/Footer/Footer.tsx b/site/src/component/Footer/Footer.tsx index aa94cbcd..4f2da790 100644 --- a/site/src/component/Footer/Footer.tsx +++ b/site/src/component/Footer/Footer.tsx @@ -6,7 +6,7 @@ const Footer: FC = () => { <>
    From 932a80bcb3ad4115b4a1c3e88a28d64b54004f3b Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Tue, 19 Sep 2023 21:31:22 -0700 Subject: [PATCH 07/22] fix: sort grade dist quarter dropdown, misc fixes --- site/src/component/GradeDist/GradeDist.tsx | 19 +++++++++++++++++-- site/src/helpers/util.tsx | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/site/src/component/GradeDist/GradeDist.tsx b/site/src/component/GradeDist/GradeDist.tsx index 9da459d8..da2f5483 100644 --- a/site/src/component/GradeDist/GradeDist.tsx +++ b/site/src/component/GradeDist/GradeDist.tsx @@ -6,7 +6,7 @@ import './GradeDist.scss' import axios from 'axios' import { CourseGQLData, ProfessorGQLData } from '../../types/types'; -import {GradesRaw} from "peterportal-api-next-types"; +import { GradesRaw } from "peterportal-api-next-types"; interface GradeDistProps { course?: CourseGQLData; @@ -22,6 +22,7 @@ interface Entry { type ChartTypes = 'bar' | 'pie'; const GradeDist: FC = (props) => { + const quarterOrder = ["Winter", "Spring", "Summer1", "Summer10wk", "Summer2", "Fall"] /* * Initialize a GradeDist block on the webpage. * @param props attributes received from the parent element @@ -108,7 +109,21 @@ const GradeDist: FC = (props) => { .forEach(data => quarters.add(data.quarter + ' ' + data.year)); quarters.forEach(quarter => result.push({ value: quarter, text: quarter })); - setQuarterEntries(result); + setQuarterEntries(result.sort((a, b) => { + if (a.value === "ALL") { + return -1; + } + if (b.value === "ALL") { + return 1; + } + const [thisQuarter, thisYear] = a.value.split(" "); + const [thatQuarter, thatYear] = b.value.split(" "); + if (thisYear === thatYear) { + return quarterOrder.indexOf(thisQuarter) - quarterOrder.indexOf(thatQuarter) + } else { + return Number.parseInt(thisYear, 10) - Number.parseInt(thatYear, 10); + } + })); setCurrentQuarter(result[0].value); } diff --git a/site/src/helpers/util.tsx b/site/src/helpers/util.tsx index e17c0212..5bd0bb91 100644 --- a/site/src/helpers/util.tsx +++ b/site/src/helpers/util.tsx @@ -106,7 +106,7 @@ function transformProfessorGQL(data: ProfessorGQLResponse) { let courseHistoryLookup: CourseLookup = {}; axios.post<{ [key: string]: CourseGQLResponse }> (`/api/courses/api/batch`, {"courses": Object.keys(data.courseHistory).map((x) => x.replace(/ /g, ""))}) - .then(r => courseHistoryLookup = r.data); + .then(r => courseHistoryLookup = Object.fromEntries(Object.values(r).map(x => [x.id, x]))); // create copy to override fields with lookups let professor = { ...data } as unknown as ProfessorGQLData; professor.courseHistory = courseHistoryLookup; From fd8489985007ae7f958178e2f811d863e79edcac Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Tue, 19 Sep 2023 21:59:40 -0700 Subject: [PATCH 08/22] fix: use async because it is almost 2024 --- api/src/controllers/schedule.ts | 1 - site/src/helpers/util.tsx | 85 +++++++++++++++------------------ 2 files changed, 39 insertions(+), 47 deletions(-) diff --git a/api/src/controllers/schedule.ts b/api/src/controllers/schedule.ts index 959ce335..4ee22c21 100644 --- a/api/src/controllers/schedule.ts +++ b/api/src/controllers/schedule.ts @@ -29,7 +29,6 @@ router.get("/getTerms", function (req, res) { * Get the current week */ router.get('/api/currentWeek', async function (_, res) { - console.log(process.env.PUBLIC_API_URL) const apiResp = await fetch(`${process.env.PUBLIC_API_URL}week`); const json = await apiResp.json(); res.send(json.payload) diff --git a/site/src/helpers/util.tsx b/site/src/helpers/util.tsx index 5bd0bb91..2dfc1415 100644 --- a/site/src/helpers/util.tsx +++ b/site/src/helpers/util.tsx @@ -43,36 +43,31 @@ export function searchAPIResult(type: SearchType, name: string) { } // helper function to query from API and transform to data used in redux -export function searchAPIResults(index: SearchIndex, names: string[]) { - return new Promise(res => { - // Get results from backend search - axios.post<{ [key: string]: CourseGQLResponse | ProfessorGQLResponse }>(`/api/${index}/api/batch`, { [index]: names }) - .then(searchResponse => { - let data = searchResponse.data; - let transformed: BatchCourseData | BatchProfessorData = {}; - Object.keys(data).forEach(id => { - // filter out null reponses - if (data[id]) { - // use specific key based on index - let key = '' - if (index == 'courses') { - key = (data[id] as CourseGQLResponse).id; - } - else { - key = (data[id] as ProfessorGQLResponse).ucinetid; - } - // perform transformation - transformed[key] = transformGQLData(index, data[id]) - } - }) - console.log('From backend search', transformed); - res(transformed); - }) - }) +export async function searchAPIResults(index: SearchIndex, names: string[]): Promise { + const res = await axios.post<{ [key: string]: CourseGQLResponse | ProfessorGQLResponse }> + (`/api/${index}/api/batch`, { [index]: names }); + const data = res.data; + const transformed: BatchCourseData | BatchProfessorData = {}; + for (const id in data) { + if (data[id]) { + // use specific key based on index + let key = '' + if (index == 'courses') { + key = (data[id] as CourseGQLResponse).id; + } + else { + key = (data[id] as ProfessorGQLResponse).ucinetid; + } + // perform transformation + transformed[key] = await transformGQLData(index, data[id]) + } + } + console.log('From backend search', transformed); + return transformed; } // transforms PPAPI gql schema to our needs -export function transformGQLData(index: SearchIndex, data: CourseGQLResponse | ProfessorGQLResponse) { +export async function transformGQLData(index: SearchIndex, data: CourseGQLResponse | ProfessorGQLResponse) { if (index == 'courses') { return transformCourseGQL(data as CourseGQLResponse); } @@ -81,36 +76,34 @@ export function transformGQLData(index: SearchIndex, data: CourseGQLResponse | P } } -function transformCourseGQL(data: CourseGQLResponse) { - let instructorHistoryLookup: ProfessorLookup = {}; - let prerequisiteListLookup: CourseLookup = {}; - let prerequisiteForLookup: CourseLookup = {}; - axios.post<{ [key: string]: CourseGQLResponse }> - (`/api/courses/api/batch`, {"courses": data.prerequisiteList.map((x) => x.replace(/ /g, ""))}) - .then(r => prerequisiteListLookup = r.data); - axios.post<{ [key: string]: CourseGQLResponse }> - (`/api/courses/api/batch`, {"courses": data.prerequisiteFor.map((x) => x.replace(/ /g, ""))}) - .then(r => prerequisiteForLookup = r.data); - axios.post<{ [key: string]: ProfessorGQLResponse }> - (`/api/professors/api/batch`, {"professors": data.instructorHistory}) - .then(r => instructorHistoryLookup = r.data); +async function transformCourseGQL(data: CourseGQLResponse) { + const instructorHistoryLookup: ProfessorLookup = await + axios.post<{ [key: string]: ProfessorGQLResponse }> + (`/api/professors/api/batch`, {"professors": data.instructorHistory}) + .then(r => r.data); + const prerequisiteListLookup: CourseLookup = await + axios.post<{ [key: string]: CourseGQLResponse }> + (`/api/courses/api/batch`, {"courses": data.prerequisiteList.map((x) => x.replace(/ /g, ""))}) + .then(r => r.data); + const prerequisiteForLookup: CourseLookup = await + axios.post<{ [key: string]: CourseGQLResponse }> + (`/api/courses/api/batch`, {"courses": data.prerequisiteFor.map((x) => x.replace(/ /g, ""))}) + .then(r => r.data); // create copy to override fields with lookups - let course = { ...data } as unknown as CourseGQLData; + const course = { ...data } as unknown as CourseGQLData; course.instructorHistory = instructorHistoryLookup; course.prerequisiteList = prerequisiteListLookup; course.prerequisiteFor = prerequisiteForLookup; return course; } -function transformProfessorGQL(data: ProfessorGQLResponse) { - let courseHistoryLookup: CourseLookup = {}; - axios.post<{ [key: string]: CourseGQLResponse }> +async function transformProfessorGQL(data: ProfessorGQLResponse) { + const courseHistoryLookup = await axios.post<{ [key: string]: CourseGQLResponse }> (`/api/courses/api/batch`, {"courses": Object.keys(data.courseHistory).map((x) => x.replace(/ /g, ""))}) - .then(r => courseHistoryLookup = Object.fromEntries(Object.values(r).map(x => [x.id, x]))); + .then(r => Object.fromEntries(Object.values(r.data).map(x => [x.id, x]))); // create copy to override fields with lookups let professor = { ...data } as unknown as ProfessorGQLData; professor.courseHistory = courseHistoryLookup; - return professor; } From 6a6a913dc14e5ae78cc0d21207c44c5a62995021 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Wed, 29 Nov 2023 15:56:13 -0800 Subject: [PATCH 09/22] fix: address most comment changes --- api/package-lock.json | 9 +++-- api/package.json | 1 - api/src/controllers/courses.ts | 6 +-- api/src/controllers/professors.ts | 4 +- site/src/component/GradeDist/GradeDist.tsx | 6 +-- site/src/component/Review/SubReview.tsx | 2 +- .../component/SearchModule/SearchModule.tsx | 6 ++- site/src/component/SideInfo/SideInfo.tsx | 4 +- site/src/helpers/util.tsx | 2 +- site/src/types/types.ts | 39 ------------------- 10 files changed, 19 insertions(+), 60 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index 3b16a145..dd913f49 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -17,7 +17,6 @@ "connect-mongodb-session": "^3.1.1", "cookie-parser": "^1.4.5", "cors": "^2.8.5", - "dayjs": "^1.11.7", "dotenv": "^8.2.0", "dotenv-flow": "^3.2.0", "ejs": "^3.1.5", @@ -4902,7 +4901,9 @@ "node_modules/dayjs": { "version": "1.11.7", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", - "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==", + "dev": true, + "peer": true }, "node_modules/debug": { "version": "2.6.9", @@ -16636,7 +16637,9 @@ "dayjs": { "version": "1.11.7", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", - "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==", + "dev": true, + "peer": true }, "debug": { "version": "2.6.9", diff --git a/api/package.json b/api/package.json index 9430aab9..168e8c64 100644 --- a/api/package.json +++ b/api/package.json @@ -12,7 +12,6 @@ "connect-mongodb-session": "^3.1.1", "cookie-parser": "^1.4.5", "cors": "^2.8.5", - "dayjs": "^1.11.7", "dotenv": "^8.2.0", "dotenv-flow": "^3.2.0", "ejs": "^3.1.5", diff --git a/api/src/controllers/courses.ts b/api/src/controllers/courses.ts index 6d66cddd..3fe397d9 100644 --- a/api/src/controllers/courses.ts +++ b/api/src/controllers/courses.ts @@ -45,11 +45,7 @@ router.post('/api/batch', (req: Request<{}, {}, { courses: string[] }>, res) => r.then((response) => response.json()) - .then((data) => { - const d = data.data - console.log(data) - res.json(d) - }) + .then((data) => res.json(data.data)) } }); diff --git a/api/src/controllers/professors.ts b/api/src/controllers/professors.ts index f8ccee27..3e93e3ee 100644 --- a/api/src/controllers/professors.ts +++ b/api/src/controllers/professors.ts @@ -39,9 +39,7 @@ router.post('/api/batch', (req: Request<{}, {}, { professors: string[] }>, res) }); r.then((response) => response.json()) - .then((data) => { - res.json(data.data) - }) + .then((data) => res.json(data.data)) } }); diff --git a/site/src/component/GradeDist/GradeDist.tsx b/site/src/component/GradeDist/GradeDist.tsx index da2f5483..5079209f 100644 --- a/site/src/component/GradeDist/GradeDist.tsx +++ b/site/src/component/GradeDist/GradeDist.tsx @@ -44,7 +44,7 @@ const GradeDist: FC = (props) => { if (props.course) { url = `/api/courses/api/grades`; params = { - department: props.course.department, + department: props.course.department.replace(/ /g, ''), number: props.course.courseNumber } } @@ -119,9 +119,9 @@ const GradeDist: FC = (props) => { const [thisQuarter, thisYear] = a.value.split(" "); const [thatQuarter, thatYear] = b.value.split(" "); if (thisYear === thatYear) { - return quarterOrder.indexOf(thisQuarter) - quarterOrder.indexOf(thatQuarter) + return quarterOrder.indexOf(thatQuarter) - quarterOrder.indexOf(thisQuarter); } else { - return Number.parseInt(thisYear, 10) - Number.parseInt(thatYear, 10); + return Number.parseInt(thatYear, 10) - Number.parseInt(thisYear, 10); } })); setCurrentQuarter(result[0].value); diff --git a/site/src/component/Review/SubReview.tsx b/site/src/component/Review/SubReview.tsx index e8b10aa2..62d351d3 100644 --- a/site/src/component/Review/SubReview.tsx +++ b/site/src/component/Review/SubReview.tsx @@ -91,7 +91,7 @@ const SubReview: FC = ({ review, course, professor, colors, colo {professor.courseHistory[review.courseID].department + ' ' + professor.courseHistory[review.courseID].courseNumber} } {course && - {course.instructorHistory[review.professorID].name} + {Object.values(course.instructorHistory)?.find(({ ucinetid }) => ucinetid === review.professorID)?.name} } {(!course && !professor) &&
    {review.courseID} {review.professorID} diff --git a/site/src/component/SearchModule/SearchModule.tsx b/site/src/component/SearchModule/SearchModule.tsx index 08ea2837..7c64a48e 100644 --- a/site/src/component/SearchModule/SearchModule.tsx +++ b/site/src/component/SearchModule/SearchModule.tsx @@ -54,10 +54,12 @@ const SearchModule: FC = ({ index }) => { }) let names: string[] = []; if (index === 'courses') { - names = Object.keys(nameResults); + names = Object.keys(nameResults ?? {}); } else if (index === 'professors') { - names = Object.keys(nameResults).map(n => nameResults[n].metadata.ucinetid) as string[]; + names = Object.keys(nameResults ?? {}).map(n => ((nameResults ?? {})[n].metadata as { + ucinetid: string + }).ucinetid) as string[]; } console.log('From frontend search', names) dispatch(setNames({ index, names })); diff --git a/site/src/component/SideInfo/SideInfo.tsx b/site/src/component/SideInfo/SideInfo.tsx index 02a4c0f6..97588577 100644 --- a/site/src/component/SideInfo/SideInfo.tsx +++ b/site/src/component/SideInfo/SideInfo.tsx @@ -234,11 +234,11 @@ const SideInfo: FC = (props) => {
    {highestReview && ucinetid === highestReview)?.shortenedName ?? '' : (props.professor?.courseHistory[highestReview] ? props.professor?.courseHistory[highestReview].department + ' ' + props.professor?.courseHistory[highestReview].courseNumber : highestReview)} />} {lowestReview && ucinetid === lowestReview)?.shortenedName ?? '' : (props.professor?.courseHistory[lowestReview] ? props.professor?.courseHistory[lowestReview].department + ' ' + props.professor?.courseHistory[lowestReview].courseNumber : lowestReview)} />}
    diff --git a/site/src/helpers/util.tsx b/site/src/helpers/util.tsx index 2dfc1415..29c38d77 100644 --- a/site/src/helpers/util.tsx +++ b/site/src/helpers/util.tsx @@ -108,4 +108,4 @@ async function transformProfessorGQL(data: ProfessorGQLResponse) { } export const hourMinuteTo12HourString = ({ hour, minute }: { hour: number, minute: number }) => - `${hour % 12}:${minute.toString().padStart(2, "0")} ${Math.floor(hour / 12) === 0 ? "AM" : "PM"}`; + `${hour === 12 ? 12 : hour % 12}:${minute.toString().padStart(2, "0")} ${Math.floor(hour / 12) === 0 ? "AM" : "PM"}`; diff --git a/site/src/types/types.ts b/site/src/types/types.ts index 78c8eb74..27c3c5e1 100644 --- a/site/src/types/types.ts +++ b/site/src/types/types.ts @@ -1,42 +1,3 @@ -export interface CourseData { - id: string; - department: string; - number: string; - school: string; - title: string; - course_level: string; - department_alias: string[]; - units: number[]; - description: string; - department_name: string; - professor_history: string[]; - prerequisite_tree: string; - prerequisite_list: string[]; - prerequisite_text: string; - prerequisite_for: string[]; - repeatability: string; - grading_option: string; - concurrent: string; - same_as: string; - restriction: string; - overlap: string; - corequisite: string; - ge_list: string[]; - ge_text: string; - terms: string[] -} - -export interface ProfessorData { - name: string; - shortenedName: string; - ucinetid: string; - title: string; - department: string; - schools: string[]; - related_departments: string[]; - course_history: string[] -} - export type BatchCourseData = { [key: string]: CourseGQLData }; export type BatchProfessorData = { [key: string]: ProfessorGQLData }; From 4dc87c35958ba0d471ef00a5eddf12fc0a687577 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Wed, 29 Nov 2023 20:48:02 -0800 Subject: [PATCH 10/22] fix: show class title in prereq tree nodes, correct links --- api/src/controllers/courses.ts | 20 +++++++------------- site/src/component/PrereqTree/PrereqTree.tsx | 7 ++++--- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/api/src/controllers/courses.ts b/api/src/controllers/courses.ts index 3fe397d9..f02ebe5d 100644 --- a/api/src/controllers/courses.ts +++ b/api/src/controllers/courses.ts @@ -33,19 +33,13 @@ router.post('/api/batch', (req: Request<{}, {}, { courses: string[] }>, res) => res.json({}); } else { - let r = fetch(process.env.PUBLIC_API_GRAPHQL_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - query: getCourseQuery(req.body.courses) - }) - }); - - - r.then((response) => response.json()) - .then((data) => res.json(data.data)) + Promise.all( + req.body.courses.map( + (course) => fetch(process.env.PUBLIC_API_URL + 'courses/' + course) + .then((r) => r.json()) + .then((r) => r.payload) + ) + ).then((data) => res.json(data.filter((x) => x))); } }); diff --git a/site/src/component/PrereqTree/PrereqTree.tsx b/site/src/component/PrereqTree/PrereqTree.tsx index 7efa72a0..332d731e 100644 --- a/site/src/component/PrereqTree/PrereqTree.tsx +++ b/site/src/component/PrereqTree/PrereqTree.tsx @@ -25,10 +25,11 @@ const Node: FC = (props) => {
    + {props.label} } + content={props.content.length ? props.content : props.label} basic position='top center' wide='very' />
    ); @@ -51,10 +52,10 @@ const PrereqTreeNode: FC = (props) => { return (
  • id === prereq.courseId?.replace(/ /g, '') ?? prereq.examName ?? '')?.title ?? ""} node={'prerequisite-node'} />
  • ); From f4f15ac3f2eda89422a4d3396db7e0c519aa1581 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Wed, 29 Nov 2023 20:59:23 -0800 Subject: [PATCH 11/22] fix: less bad solution for prereq titles --- api/src/controllers/courses.ts | 4 ++-- site/src/component/PrereqTree/PrereqTree.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/controllers/courses.ts b/api/src/controllers/courses.ts index f02ebe5d..7e6f6f33 100644 --- a/api/src/controllers/courses.ts +++ b/api/src/controllers/courses.ts @@ -37,9 +37,9 @@ router.post('/api/batch', (req: Request<{}, {}, { courses: string[] }>, res) => req.body.courses.map( (course) => fetch(process.env.PUBLIC_API_URL + 'courses/' + course) .then((r) => r.json()) - .then((r) => r.payload) + .then((r) => r.payload ? [r.payload.id, r.payload] : []) ) - ).then((data) => res.json(data.filter((x) => x))); + ).then((data) => res.json(Object.fromEntries(data))); } }); diff --git a/site/src/component/PrereqTree/PrereqTree.tsx b/site/src/component/PrereqTree/PrereqTree.tsx index 332d731e..abef6f62 100644 --- a/site/src/component/PrereqTree/PrereqTree.tsx +++ b/site/src/component/PrereqTree/PrereqTree.tsx @@ -55,7 +55,7 @@ const PrereqTreeNode: FC = (props) => { label={`${prereq.courseId?.replace(/ /g, '') ?? prereq.examName ?? ''}${ prereq?.minGrade ? ` (min grade = ${prereq?.minGrade})` : '' }${prereq?.coreq ? ' (coreq)' : ''}`} - content={Object.values(props.prerequisiteNames).find(({ id }) => id === prereq.courseId?.replace(/ /g, '') ?? prereq.examName ?? '')?.title ?? ""} node={'prerequisite-node'} + content={props.prerequisiteNames[prereq.courseId?.replace(/ /g, '') ?? prereq.examName ?? '']?.title ?? ""} node={'prerequisite-node'} /> ); From e20bb5b6da1eb5465195f28e145881fe20a6d0ce Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Sun, 3 Dec 2023 14:23:12 -0800 Subject: [PATCH 12/22] feat: fetch course/instructor data on demand --- site/src/helpers/util.tsx | 24 ++++------------------- site/src/pages/CoursePage/index.tsx | 27 +++++++++++++++++++++----- site/src/pages/ProfessorPage/index.tsx | 14 +++++++++---- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/site/src/helpers/util.tsx b/site/src/helpers/util.tsx index 29c38d77..ce889bd6 100644 --- a/site/src/helpers/util.tsx +++ b/site/src/helpers/util.tsx @@ -77,33 +77,17 @@ export async function transformGQLData(index: SearchIndex, data: CourseGQLRespon } async function transformCourseGQL(data: CourseGQLResponse) { - const instructorHistoryLookup: ProfessorLookup = await - axios.post<{ [key: string]: ProfessorGQLResponse }> - (`/api/professors/api/batch`, {"professors": data.instructorHistory}) - .then(r => r.data); - const prerequisiteListLookup: CourseLookup = await - axios.post<{ [key: string]: CourseGQLResponse }> - (`/api/courses/api/batch`, {"courses": data.prerequisiteList.map((x) => x.replace(/ /g, ""))}) - .then(r => r.data); - const prerequisiteForLookup: CourseLookup = await - axios.post<{ [key: string]: CourseGQLResponse }> - (`/api/courses/api/batch`, {"courses": data.prerequisiteFor.map((x) => x.replace(/ /g, ""))}) - .then(r => r.data); // create copy to override fields with lookups const course = { ...data } as unknown as CourseGQLData; - course.instructorHistory = instructorHistoryLookup; - course.prerequisiteList = prerequisiteListLookup; - course.prerequisiteFor = prerequisiteForLookup; + course.instructorHistory = Object.fromEntries(data.instructorHistory.map((x) => [x, null!])); + course.prerequisiteList = Object.fromEntries(data.prerequisiteList.map((x) => [x, null!])); + course.prerequisiteFor = Object.fromEntries(data.prerequisiteFor.map((x) => [x, null!])) return course; } async function transformProfessorGQL(data: ProfessorGQLResponse) { - const courseHistoryLookup = await axios.post<{ [key: string]: CourseGQLResponse }> - (`/api/courses/api/batch`, {"courses": Object.keys(data.courseHistory).map((x) => x.replace(/ /g, ""))}) - .then(r => Object.fromEntries(Object.values(r.data).map(x => [x.id, x]))); - // create copy to override fields with lookups let professor = { ...data } as unknown as ProfessorGQLData; - professor.courseHistory = courseHistoryLookup; + professor.courseHistory = Object.fromEntries(Object.entries(data.courseHistory).map(([x, _]) => [x, null!])); return professor; } diff --git a/site/src/pages/CoursePage/index.tsx b/site/src/pages/CoursePage/index.tsx index 96c1ef47..8bd4db4c 100644 --- a/site/src/pages/CoursePage/index.tsx +++ b/site/src/pages/CoursePage/index.tsx @@ -13,10 +13,12 @@ import Error from '../../component/Error/Error'; import { useAppSelector, useAppDispatch } from '../../store/hooks'; import { setCourse } from '../../store/slices/popupSlice'; -import { SearchType, SearchIndex, CourseGQLData } from '../../types/types'; +import { CourseGQLData } from '../../types/types'; import { getCourseTags, searchAPIResult } from '../../helpers/util'; import './CoursePage.scss'; +import axios from "axios"; + const CoursePage: FC> = (props) => { const dispatch = useAppDispatch(); const courseGQLData = useAppSelector(state => state.popup.course); @@ -24,18 +26,33 @@ const CoursePage: FC> = (props) => { useEffect(() => { // make a gql query if directly landed on this page - if (courseGQLData == null || courseGQLData.id != props.match.params.id) { - searchAPIResult('course', props.match.params.id) + if (courseGQLData == null || courseGQLData.id !== props.match.params.id) { + (searchAPIResult('course', props.match.params.id) as Promise) .then(course => { - console.log("COURSE", course) if (course) { - dispatch(setCourse(course as CourseGQLData)) + Promise.all([ + axios.post + (`/api/professors/api/batch`, {"professors": Object.keys(course.instructorHistory)}) + .then(r => r.data), + axios.post + (`/api/courses/api/batch`, {"courses": Object.keys(course.prerequisiteList).map((x) => x.replace(/ /g, ""))}) + .then(r => r.data), + axios.post + (`/api/courses/api/batch`, {"courses": Object.keys(course.prerequisiteFor).map((x) => x.replace(/ /g, ""))}) + .then(r => r.data), + ]).then(([instructorHistory, prerequisiteList, prerequisiteFor]) => { + course.instructorHistory = instructorHistory; + course.prerequisiteList = prerequisiteList; + course.prerequisiteFor = prerequisiteFor; + dispatch(setCourse(course)); + }) } else { setError(`Course ${props.match.params.id} does not exist!`); } }) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // if course does not exists diff --git a/site/src/pages/ProfessorPage/index.tsx b/site/src/pages/ProfessorPage/index.tsx index 11805b7f..b396c053 100644 --- a/site/src/pages/ProfessorPage/index.tsx +++ b/site/src/pages/ProfessorPage/index.tsx @@ -13,7 +13,7 @@ import Error from '../../component/Error/Error'; import { setProfessor } from '../../store/slices/popupSlice'; import { useAppSelector, useAppDispatch } from '../../store/hooks'; -import { ProfessorGQLData } from '../../types/types'; +import {CourseGQLResponse, ProfessorGQLData} from '../../types/types'; import { searchAPIResult } from '../../helpers/util'; const ProfessorPage: FC> = (props) => { @@ -23,17 +23,23 @@ const ProfessorPage: FC> = (props) => { useEffect(() => { // make a gql query if directly landed on this page - if (professorGQLData == null || professorGQLData.ucinetid != props.match.params.id) { - searchAPIResult('professor', props.match.params.id) + if (professorGQLData == null || professorGQLData.ucinetid !== props.match.params.id) { + (searchAPIResult('professor', props.match.params.id) as Promise) .then(professor => { if (professor) { - dispatch(setProfessor(professor as ProfessorGQLData)) + axios.post<{ [key: string]: CourseGQLResponse }> + (`/api/courses/api/batch`, {"courses": Object.keys(professor.courseHistory).map((x) => x.replace(/ /g, ""))}) + .then(r => { + professor.courseHistory = Object.fromEntries(Object.values(r.data).map(x => [x.id, x])); + dispatch(setProfessor(professor)); + }) } else { setError(`Professor ${props.match.params.id} does not exist!`); } }) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // if professor does not exists From 4796fe2b22d469bdfa588f378ca8f938de41c929 Mon Sep 17 00:00:00 2001 From: Kyle Pan <66697740+kylerpan@users.noreply.github.com> Date: Thu, 19 Oct 2023 20:29:02 -0700 Subject: [PATCH 13/22] fixed prereq box to only appear when there is text (#367) --- site/src/component/PrereqTree/PrereqTree.tsx | 26 +++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/site/src/component/PrereqTree/PrereqTree.tsx b/site/src/component/PrereqTree/PrereqTree.tsx index abef6f62..070dec5f 100644 --- a/site/src/component/PrereqTree/PrereqTree.tsx +++ b/site/src/component/PrereqTree/PrereqTree.tsx @@ -173,18 +173,20 @@ const PrereqTree: FC = (props) => {
    } */}
    -
    -

    - {props.prerequisiteText !== '' && Prerequisite: } - {props.prerequisiteText} -

    -
    + {props.prerequisiteText !== '' && ( +
    +

    + Prerequisite: + {props.prerequisiteText} +

    +
    + )}
    ); From 0baca8ce5cd9a977706f159a6a46d6b389231267 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 4 Dec 2023 13:41:33 -0800 Subject: [PATCH 14/22] fix: grab review form fix from #375 Co-authored-by: Jacob Sommer --- site/src/component/ReviewForm/ReviewForm.tsx | 25 +++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/site/src/component/ReviewForm/ReviewForm.tsx b/site/src/component/ReviewForm/ReviewForm.tsx index 28c40023..a0f3c0bb 100644 --- a/site/src/component/ReviewForm/ReviewForm.tsx +++ b/site/src/component/ReviewForm/ReviewForm.tsx @@ -161,16 +161,15 @@ const ReviewForm: FC = (props) => { } // select instructor if in course context - const instructorSelect = props.course && + const instructorSelect = props.course && Taken With - (setProfessor(document.getElementsByName(e.target.value)[0].id))}> - - {Object.keys(props.course?.instructorHistory!).map((ucinetid, i) => { + (setProfessor(e.target.value))}> + + {Object.keys(props.course?.instructorHistory).map((ucinetid) => { const name = props.course?.instructorHistory[ucinetid].shortenedName; return ( - // @ts-ignore name attribute isn't supported - + ) })} @@ -183,18 +182,16 @@ const ReviewForm: FC = (props) => { Missing instructor - // select course if in professor context const courseSelect = props.professor && Course Taken - (setCourse(document.getElementsByName(e.target.value)[0].id))}> - - {Object.keys(props.professor?.courseHistory!).map((courseID, i) => { + (setCourse(e.target.value))}> + + {Object.keys(props.professor?.courseHistory).map((courseID) => { const name = props.professor?.courseHistory[courseID].department + ' ' + props.professor?.courseHistory[courseID].courseNumber; return ( - // @ts-ignore name attribute isn't supported - + ) })} From 406e43df7e985436795958d3dd484d8720e87607 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 4 Dec 2023 14:22:24 -0800 Subject: [PATCH 15/22] feat: use graphql for courses again, set endpoint to staging --- api/src/controllers/courses.ts | 23 ++++++++++++++++------- stacks/backend.ts | 4 ++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/api/src/controllers/courses.ts b/api/src/controllers/courses.ts index 7e6f6f33..bc51fa59 100644 --- a/api/src/controllers/courses.ts +++ b/api/src/controllers/courses.ts @@ -33,13 +33,22 @@ router.post('/api/batch', (req: Request<{}, {}, { courses: string[] }>, res) => res.json({}); } else { - Promise.all( - req.body.courses.map( - (course) => fetch(process.env.PUBLIC_API_URL + 'courses/' + course) - .then((r) => r.json()) - .then((r) => r.payload ? [r.payload.id, r.payload] : []) - ) - ).then((data) => res.json(Object.fromEntries(data))); + let r = fetch(process.env.PUBLIC_API_GRAPHQL_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + query: getCourseQuery(req.body.courses) + }) + }); + + r.then((response) => response.json()) + .then((data) => res.json( + Object.fromEntries( + Object.entries(data.data).filter(([_, x]) => x !== null).map(([_, x]) => [(x as { id: string }).id, x]) + ) + )) } }); diff --git a/stacks/backend.ts b/stacks/backend.ts index 04590bf2..ed871912 100644 --- a/stacks/backend.ts +++ b/stacks/backend.ts @@ -40,8 +40,8 @@ export function BackendStack({app, stack}: StackContext) { */ // PUBLIC_API_URL: process.env.PUBLIC_API_URL, // PUBLIC_API_GRAPHQL_URL: process.env.PUBLIC_API_GRAPHQL_URL, - PUBLIC_API_URL: "https://api-next.peterportal.org/v1/rest/", - PUBLIC_API_GRAPHQL_URL: "https://api-next.peterportal.org/v1/graphql", + PUBLIC_API_URL: "https://staging-116.api-next.peterportal.org/v1/rest/", + PUBLIC_API_GRAPHQL_URL: "https://staging-116.api-next.peterportal.org/v1/graphql", GOOGLE_CLIENT: process.env.GOOGLE_CLIENT, GOOGLE_SECRET: process.env.GOOGLE_SECRET, PRODUCTION_DOMAIN: process.env.PRODUCTION_DOMAIN, From c5222973920c16505c598eb2a9d6b20cd1ed82b1 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 18 Dec 2023 00:28:28 -0800 Subject: [PATCH 16/22] chore: migrate to latest api staging --- stacks/backend.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stacks/backend.ts b/stacks/backend.ts index ed871912..448e11b3 100644 --- a/stacks/backend.ts +++ b/stacks/backend.ts @@ -40,8 +40,8 @@ export function BackendStack({app, stack}: StackContext) { */ // PUBLIC_API_URL: process.env.PUBLIC_API_URL, // PUBLIC_API_GRAPHQL_URL: process.env.PUBLIC_API_GRAPHQL_URL, - PUBLIC_API_URL: "https://staging-116.api-next.peterportal.org/v1/rest/", - PUBLIC_API_GRAPHQL_URL: "https://staging-116.api-next.peterportal.org/v1/graphql", + PUBLIC_API_URL: "https://staging-117.api-next.peterportal.org/v1/rest/", + PUBLIC_API_GRAPHQL_URL: "https://staging-117.api-next.peterportal.org/v1/graphql", GOOGLE_CLIENT: process.env.GOOGLE_CLIENT, GOOGLE_SECRET: process.env.GOOGLE_SECRET, PRODUCTION_DOMAIN: process.env.PRODUCTION_DOMAIN, From 09e763a9ad865dbf2e5d8f0cec90a0c8add50e77 Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 18 Dec 2023 04:07:42 -0800 Subject: [PATCH 17/22] fix: make prereq tree node labels consistently spaced --- site/src/component/PrereqTree/PrereqTree.tsx | 95 ++++++++++---------- 1 file changed, 50 insertions(+), 45 deletions(-) diff --git a/site/src/component/PrereqTree/PrereqTree.tsx b/site/src/component/PrereqTree/PrereqTree.tsx index e166b120..29614b70 100644 --- a/site/src/component/PrereqTree/PrereqTree.tsx +++ b/site/src/component/PrereqTree/PrereqTree.tsx @@ -1,7 +1,7 @@ import React, { FC } from 'react'; import './PrereqTree.scss'; import { Grid, Popup } from 'semantic-ui-react'; -import type { Prerequisite, PrerequisiteTree } from "peterportal-api-next-types"; +import type { Prerequisite, PrerequisiteTree } from 'peterportal-api-next-types'; import { CourseGQLData, CourseLookup } from '../../types/types'; @@ -22,15 +22,20 @@ const phraseMapping = { const Node: FC = (props) => { return ( -
    +
    + !props.label.startsWith('AP ') ? ( + {props.label} ) : ( - + ) } content={props.content.length ? props.content : props.label} @@ -59,10 +64,11 @@ const PrereqTreeNode: FC = (props) => { return (
  • ); @@ -74,16 +80,16 @@ const PrereqTreeNode: FC = (props) => {
    -
    - { - Object.entries(phraseMapping).filter(([subtreeType, _]) => - Object.prototype.hasOwnProperty.call(prerequisite, subtreeType) - )[0][1] - } -
    +
    + { + Object.entries(phraseMapping).filter(([subtreeType, _]) => + Object.prototype.hasOwnProperty.call(prerequisite, subtreeType), + )[0][1] + } +
    -
    -
      +
      +
        {prereqTree[Object.keys(prerequisite)[0]].map((child, index) => ( = (props) => {
        {/* Display dependencies */} {hasDependencies && ( <>
          -
          - {Object.values(props.prerequisiteFor).map( - (dependency, index) => ( -
        • - -
        • - ) - )} +
          + {Object.values(props.prerequisiteFor).map((dependency, index) => ( +
        • + +
        • + ))}
        -
        - +
        +
        needs
        @@ -155,15 +163,12 @@ const PrereqTree: FC = (props) => {
        } */} {/* Display the class id */} - + {/* Spawns the root of the prerequisite tree */} {hasPrereqs && (
        - +
        )} @@ -175,16 +180,16 @@ const PrereqTree: FC = (props) => {
        {props.prerequisiteText !== '' && (
        -

        - Prerequisite: - {props.prerequisiteText} -

        +

        + Prerequisite: + {props.prerequisiteText} +

        )}
        From 8be94039c16a8e363a4a8537c6530d5efa604f2e Mon Sep 17 00:00:00 2001 From: Eddy Chen <89349085+ecxyzzy@users.noreply.github.com> Date: Mon, 18 Dec 2023 17:50:09 -0800 Subject: [PATCH 18/22] fix: reincorporate #391 changes --- site/src/component/ReviewForm/ReviewForm.tsx | 353 +++++++++++-------- 1 file changed, 210 insertions(+), 143 deletions(-) diff --git a/site/src/component/ReviewForm/ReviewForm.tsx b/site/src/component/ReviewForm/ReviewForm.tsx index a0f3c0bb..9db24642 100644 --- a/site/src/component/ReviewForm/ReviewForm.tsx +++ b/site/src/component/ReviewForm/ReviewForm.tsx @@ -1,6 +1,6 @@ -import React, { FC, FormEvent, ChangeEvent, useState, useEffect } from 'react' -import './ReviewForm.scss' -import axios from 'axios' +import React, { FC, FormEvent, ChangeEvent, useState, useEffect } from 'react'; +import './ReviewForm.scss'; +import axios from 'axios'; import { useCookies } from 'react-cookie'; import { Icon } from 'semantic-ui-react'; import Form from 'react-bootstrap/Form'; @@ -10,7 +10,7 @@ import Col from 'react-bootstrap/Col'; import Button from 'react-bootstrap/Button'; import RangeSlider from 'react-bootstrap-range-slider'; import Modal from 'react-bootstrap/Modal'; -import ReCAPTCHA from "react-google-recaptcha"; +import ReCAPTCHA from 'react-google-recaptcha'; import { addReview } from '../../store/slices/reviewSlice'; import { useAppDispatch, useAppSelector } from '../../store/hooks'; @@ -23,19 +23,25 @@ interface ReviewFormProps extends ReviewProps { const ReviewForm: FC = (props) => { const dispatch = useAppDispatch(); - const grades = [ - 'A+', 'A', 'A-', - 'B+', 'B', 'B-', - 'C+', 'C', 'C-', - 'D+', 'D', 'D-', - 'F', 'P', 'NP' - ]; + const grades = ['A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F', 'P', 'NP']; const tags = [ - 'Clear grading criteria', 'Tough grader', 'Amazing lectures', 'Test heavy', - 'Get ready to read', 'Extra credit', 'Participation matters', 'Graded by few things', - "Skip class? You won't pass", 'Accessible outside class', 'Beware of pop quizzes', - 'Lots of homework', 'So many papers', 'Lecture heavy', 'Group projects', 'Gives good feedback' - ] + 'Clear grading criteria', + 'Tough grader', + 'Amazing lectures', + 'Test heavy', + 'Get ready to read', + 'Extra credit', + 'Participation matters', + 'Graded by few things', + "Skip class? You won't pass", + 'Accessible outside class', + 'Beware of pop quizzes', + 'Lots of homework', + 'So many papers', + 'Lecture heavy', + 'Group projects', + 'Gives good feedback', + ]; const [professor, setProfessor] = useState(props.professor?.ucinetid || ''); const [course, setCourse] = useState(props.course?.id || ''); @@ -56,7 +62,7 @@ const ReviewForm: FC = (props) => { const [overCharLimit, setOverCharLimit] = useState(false); const [cookies, setCookie] = useCookies(['user']); const [validated, setValidated] = useState(false); - const showForm = useAppSelector(state => state.review.formOpen); + const showForm = useAppSelector((state) => state.review.formOpen); useEffect(() => { // get user info from cookie @@ -64,28 +70,29 @@ const ReviewForm: FC = (props) => { setUserID(cookies.user.id); setUserName(cookies.user.name); } - }, []) + }, []); useEffect(() => { // upon opening this form if (showForm) { // if not logged in, close the form if (!cookies.hasOwnProperty('user')) { - alert('You must be logged in to add a review!') + alert('You must be logged in to add a review!'); props.closeForm(); } } - }, [showForm]) + }, [showForm]); const postReview = async (review: ReviewData) => { - const res = await axios.post('/api/reviews', review); - if (res.data.hasOwnProperty('error')) { + const res = await axios.post('/api/reviews', review).catch((err) => err.response); + if (res.status === 400) { + alert('You have already submitted a review for this course/professor'); + } else if (res.data.hasOwnProperty('error')) { alert('You must be logged in to add a review!'); + } else { + setSubmitted(true); } - else { - dispatch(addReview(res.data)); - } - } + }; const submitForm = (event: React.FormEvent) => { // validate form @@ -128,17 +135,16 @@ const ReviewForm: FC = (props) => { textbook: textbook, attendance: attendance, tags: selectedTags, - verified: false + verified: false, }; if (content.length > 500) { setOverCharLimit(true); - } - else { + } else { setOverCharLimit(false); postReview(review); setSubmitted(true); } - } + }; const selectTag = (tag: string) => { // remove tag @@ -153,163 +159,206 @@ const ReviewForm: FC = (props) => { let newSelectedTags = [...selectedTags]; newSelectedTags.push(tag); setSelectedTags(newSelectedTags); - } - else { + } else { alert('Cannot select more than 3 tags'); } } - } + }; // select instructor if in course context - const instructorSelect = props.course && - Taken With - (setProfessor(e.target.value))}> - - {Object.keys(props.course?.instructorHistory).map((ucinetid) => { - const name = props.course?.instructorHistory[ucinetid].shortenedName; - return ( - - ) - })} - - - - Can't find your professor? - - - - Missing instructor - - + const instructorSelect = props.course && ( + + Taken With + setProfessor(e.target.value)} + > + + {Object.keys(props.course?.instructorHistory).map((ucinetid) => { + const name = props.course?.instructorHistory[ucinetid].shortenedName; + return ( + + ); + })} + + + + Can't find your professor? + + + Missing instructor + + ); // select course if in professor context - const courseSelect = props.professor && - Course Taken - (setCourse(e.target.value))}> - - {Object.keys(props.professor?.courseHistory).map((courseID) => { - const name = props.professor?.courseHistory[courseID].department + ' ' + props.professor?.courseHistory[courseID].courseNumber; - return ( - - ) - })} - - - Missing course - - + const courseSelect = props.professor && ( + + Course Taken + setCourse(e.target.value)} + > + + {Object.keys(props.professor?.courseHistory).map((courseID) => { + const name = + props.professor?.courseHistory[courseID].department + + ' ' + + props.professor?.courseHistory[courseID].courseNumber; + return ( + + ); + })} + + Missing course + + ); const reviewForm = (
        - + -

        It's your turn to review {props.course ? (props.course?.department + ' ' + props.course?.courseNumber) : props.professor?.name}

        +

        + It's your turn to review{' '} + {props.course ? props.course?.department + ' ' + props.course?.courseNumber : props.professor?.name} +

        - + -
        +
        {instructorSelect} {courseSelect} - + Grade - setGradeReceived(e.target.value)}> - + setGradeReceived(e.target.value)} + > + {grades.map((grade, i) => ( ))} - - Missing grade - + Missing grade
        - + Taken During -
        - - setQuarterTaken(e.target.value)}> - +
        + + setQuarterTaken(e.target.value)} + > + {['Fall', 'Winter', 'Spring', 'Summer1', 'Summer10wk', 'Summer2'].map((quarter, i) => ( ))} - - Missing quarter - + Missing quarter - - setYearTaken(e.target.value)}> - + + setYearTaken(e.target.value)} + > + {Array.from(new Array(10), (x, i) => new Date().getFullYear() - i).map((year, i) => ( ))} - - Missing year - + Missing year
        - + - + Rate the {props.course ? 'Course' : 'Professor'} ) => setQuality(parseInt(e.currentTarget.value))} /> - + - + Level of Difficulty ) => setDifficulty(parseInt(e.currentTarget.value))} /> - + - + ) => setTakeAgain(e.target.checked)} /> ) => setTextbook(e.target.checked)} /> ) => setAttendance(e.target.checked)} /> @@ -321,22 +370,30 @@ const ReviewForm: FC = (props) => { - + Select up to 3 tags
        - {tags.map((tag, i) => - ) => { selectTag(tag) }}> + {tags.map((tag, i) => ( + ) => { + selectTag(tag); + }} + > {tag} - )} + ))}
        - + Tell us more about this {props.course ? 'course' : 'professor'} = (props) => { onChange={(e) => { setContent(e.target.value); if (overCharLimit && e.target.value.length < 500) { - setOverCharLimit(false) + setOverCharLimit(false); } }} /> {/*