From 33193afc4f6a8e2bfeb70a18ef0418d276bcf2ff Mon Sep 17 00:00:00 2001 From: pyccj Date: Thu, 27 Apr 2023 14:42:51 -0700 Subject: [PATCH 1/2] - added materialUI package - updated roadmapSlice to support multiple plans --- site/package.json | 3 + site/src/pages/RoadmapPage/AddCoursePopup.tsx | 16 +- site/src/pages/RoadmapPage/Planner.tsx | 2 +- site/src/pages/RoadmapPage/Quarter.tsx | 2 +- .../pages/RoadmapPage/RoadmapMultiplan.scss | 6 + .../pages/RoadmapPage/RoadmapMultiplan.tsx | 163 ++++++++++++++++++ site/src/pages/RoadmapPage/Transfer.tsx | 4 +- site/src/pages/RoadmapPage/index.tsx | 4 +- site/src/store/slices/roadmapSlice.ts | 137 +++++++++++---- 9 files changed, 287 insertions(+), 50 deletions(-) create mode 100644 site/src/pages/RoadmapPage/RoadmapMultiplan.scss create mode 100644 site/src/pages/RoadmapPage/RoadmapMultiplan.tsx diff --git a/site/package.json b/site/package.json index d690c6c5..ea3b852d 100644 --- a/site/package.json +++ b/site/package.json @@ -4,6 +4,9 @@ "private": true, "dependencies": { "@apollo/client": "^3.3.15", + "@emotion/react": "^11.10.6", + "@emotion/styled": "^11.10.6", + "@mui/material": "^5.11.12", "@nivo/bar": "^0.69.1", "@nivo/core": "^0.69.0", "@nivo/pie": "^0.69.0", diff --git a/site/src/pages/RoadmapPage/AddCoursePopup.tsx b/site/src/pages/RoadmapPage/AddCoursePopup.tsx index 7d12dabf..dada0ffa 100644 --- a/site/src/pages/RoadmapPage/AddCoursePopup.tsx +++ b/site/src/pages/RoadmapPage/AddCoursePopup.tsx @@ -1,21 +1,19 @@ -import React, { FC, ChangeEvent, useState, useEffect } from 'react'; -import Form from 'react-bootstrap/Form'; +import React, { FC, useState } from 'react'; import Button from 'react-bootstrap/Button'; -import './AddCoursePopup.scss'; -import { useAppDispatch, useAppSelector } from '../../store/hooks'; -import { moveCourse, setShowAddCourse, setShowSearch } from '../../store/slices/roadmapSlice'; -import { ReportData } from '../../types/types'; -import { useCookies } from 'react-cookie'; +import Form from 'react-bootstrap/Form'; import Modal from 'react-bootstrap/Modal'; import { isMobile } from 'react-device-detect'; +import { useAppDispatch, useAppSelector } from '../../store/hooks'; +import { moveCourse, setShowAddCourse } from '../../store/slices/roadmapSlice'; +import './AddCoursePopup.scss'; interface AddCoursePopupProps { } const AddCoursePopup: FC = (props) => { const dispatch = useAppDispatch(); - const planner = useAppSelector(state => state.roadmap.yearPlans); - const showForm = useAppSelector(state => state.roadmap.showAddCourse); + const planner = useAppSelector(state => state.roadmap.plans[state.roadmap.currentPlanIndex].content.yearPlans); + const showForm = useAppSelector(state => state.roadmap.plans[state.roadmap.currentPlanIndex].content.showAddCourse); const [year, setYear] = useState(-1); const [quarter, setQuarter] = useState(-1); const [validated, setValidated] = useState(false); diff --git a/site/src/pages/RoadmapPage/Planner.tsx b/site/src/pages/RoadmapPage/Planner.tsx index 1b0e5b0e..db332892 100644 --- a/site/src/pages/RoadmapPage/Planner.tsx +++ b/site/src/pages/RoadmapPage/Planner.tsx @@ -16,7 +16,7 @@ const Planner: FC = () => { const [cookies, setCookie] = useCookies(['user']); const isFirstRenderer = useFirstRender(); const data = useAppSelector(selectYearPlans); - const transfers = useAppSelector(state => state.roadmap.transfers); + const transfers = useAppSelector(state => state.roadmap.plans[state.roadmap.currentPlanIndex].content.transfers); const [missingPrerequisites, setMissingPrerequisites] = useState(new Set); diff --git a/site/src/pages/RoadmapPage/Quarter.tsx b/site/src/pages/RoadmapPage/Quarter.tsx index db91a668..2a0707cf 100644 --- a/site/src/pages/RoadmapPage/Quarter.tsx +++ b/site/src/pages/RoadmapPage/Quarter.tsx @@ -21,7 +21,7 @@ interface QuarterProps { const Quarter: FC = ({ year, yearIndex, quarterIndex, data }) => { const dispatch = useAppDispatch(); let quarterTitle = data.name.charAt(0).toUpperCase() + data.name.slice(1); - const invalidCourses = useAppSelector(state => state.roadmap.invalidCourses); + const invalidCourses = useAppSelector(state => state.roadmap.plans[state.roadmap.currentPlanIndex].content.invalidCourses); const [showQuarterMenu, setShowQuarterMenu] = useState(false); const [target, setTarget] = useState(null!); diff --git a/site/src/pages/RoadmapPage/RoadmapMultiplan.scss b/site/src/pages/RoadmapPage/RoadmapMultiplan.scss new file mode 100644 index 00000000..c4461b69 --- /dev/null +++ b/site/src/pages/RoadmapPage/RoadmapMultiplan.scss @@ -0,0 +1,6 @@ +.multi-plan-selector { + display: flex; + flex-direction: row; + justify-content: space-between; + height: 60px; +} \ No newline at end of file diff --git a/site/src/pages/RoadmapPage/RoadmapMultiplan.tsx b/site/src/pages/RoadmapPage/RoadmapMultiplan.tsx new file mode 100644 index 00000000..0bf52812 --- /dev/null +++ b/site/src/pages/RoadmapPage/RoadmapMultiplan.tsx @@ -0,0 +1,163 @@ +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import TextField from '@mui/material/TextField'; +import { FC, useState } from "react"; +import { TextArea } from 'semantic-ui-react'; +import { useAppDispatch, useAppSelector } from "src/store/hooks"; +import { addRoadmapPlan, deleteRoadmapPlan, initialState, setPlanIndex, setPlanName } from "../../store/slices/roadmapSlice"; +import { Box } from '@mui/material'; +import Dropdown from 'react-bootstrap/esm/Dropdown'; +import "./RoadmapMultiplan.scss"; +import * as Icon from "react-bootstrap-icons"; + + +const RoadmapMultiplan: FC = () => { + const dispatch = useAppDispatch(); + const allPlans = useAppSelector(state => state.roadmap); + const [currentPlanIndex, setCurrentPlanIndex] = useState(allPlans.currentPlanIndex); + const [isOpen, setIsOpen] = useState(false); + const [isEdit, setIsEdit] = useState(false); + const [isDelete, setIsDelete] = useState(false); + const [newPlanName, setNewPlanName] = useState(allPlans.plans[allPlans.currentPlanIndex].name); + + // name: name of the plan, content: stores the content of plan + const { name, content } = allPlans.plans[currentPlanIndex]; + + const addNewPlan = (name: string) => { + dispatch(addRoadmapPlan({ name: name, content: initialState })); + }; + + const deleteCurrentPlan = () => { + setCurrentPlanIndex(0); + dispatch(setPlanIndex(0)); + dispatch(deleteRoadmapPlan({ planIndex: currentPlanIndex })); + setIsDelete(false); + }; + + const handleSubmitNewPlan = () => { + setIsOpen(false); + addNewPlan(newPlanName); + const newIndex = allPlans.plans.length; + setCurrentPlanIndex(newIndex); + dispatch(setPlanIndex(newIndex)); + }; + + + const modifyPlanName = () => { + setIsEdit(false); + dispatch(setPlanName({ index: allPlans.currentPlanIndex, name: newPlanName })); + }; + + + return ( +
+ + + + + + + setIsEdit(true)}>Edit Plan Name + setIsOpen(true)}>Add Plan + + + + {name} + + setIsOpen(false)} + PaperProps={{ sx: { width: "30%", height: "20%" } }} + style={{ marginTop: 20 }} + > + New Plan + + { setNewPlanName(e.target.value) }} + style={{ width: "100%" }} + /> + + + + setIsEdit(false)} + PaperProps={{ sx: { width: "30%", height: "20%" } }} + style={{ marginTop: 20 }} + > + Edit Plan Name + + { setNewPlanName(e.target.value) }} + style={{ width: "100%" }} + /> + + + + setIsDelete(false)} + PaperProps={{ sx: { width: "30%", height: "20%" } }} + style={{ marginTop: 20 }} + > + Delete Plan + +

Are you sure about deleting current plan?

+ + + + +
+
+
+
+ +
+ ); +}; + + +export default RoadmapMultiplan; \ No newline at end of file diff --git a/site/src/pages/RoadmapPage/Transfer.tsx b/site/src/pages/RoadmapPage/Transfer.tsx index d67874bc..7160463f 100644 --- a/site/src/pages/RoadmapPage/Transfer.tsx +++ b/site/src/pages/RoadmapPage/Transfer.tsx @@ -64,8 +64,8 @@ const TransferEntry: FC = (props) => { const Transfer: FC = ({ missingPrereqNames }) => { const dispatch = useAppDispatch(); - const transfers = useAppSelector(state => state.roadmap.transfers); - const show = useAppSelector(state => state.roadmap.showTransfer); + const transfers = useAppSelector(state => state.roadmap.plans[state.roadmap.currentPlanIndex].content.transfers); + const show = useAppSelector(state => state.roadmap.plans[state.roadmap.currentPlanIndex].content.showTransfer); const handleClose = () => dispatch(setShowTransfer(false)); console.log("missing courses: ", missingPrereqNames); diff --git a/site/src/pages/RoadmapPage/index.tsx b/site/src/pages/RoadmapPage/index.tsx index 263fa276..eb585dc3 100644 --- a/site/src/pages/RoadmapPage/index.tsx +++ b/site/src/pages/RoadmapPage/index.tsx @@ -7,10 +7,11 @@ import { useAppDispatch, useAppSelector } from '../../store/hooks'; import { moveCourse, deleteCourse } from '../../store/slices/roadmapSlice'; import AddCoursePopup from './AddCoursePopup'; import { isMobile, isBrowser } from 'react-device-detect'; +import RoadmapMultiplan from './RoadmapMultiplan'; const RoadmapPage: FC = () => { const dispatch = useAppDispatch(); - const showSearch = useAppSelector(state => state.roadmap.showSearch); + const showSearch = useAppSelector(state => state.roadmap.plans[state.roadmap.currentPlanIndex].content.showSearch); const onDragEnd = useCallback((result: DropResult) => { if (result.reason === 'DROP') { @@ -88,6 +89,7 @@ const RoadmapPage: FC = () => { return ( <> +
diff --git a/site/src/store/slices/roadmapSlice.ts b/site/src/store/slices/roadmapSlice.ts index c1fefa50..cadf1012 100644 --- a/site/src/store/slices/roadmapSlice.ts +++ b/site/src/store/slices/roadmapSlice.ts @@ -1,6 +1,6 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' -import type { RootState } from '../store' -import { PlannerData, PlannerYearData, CourseGQLData, YearIdentifier, QuarterIdentifier, CourseIdentifier, InvalidCourseData, TransferData, PlannerQuarterData } from '../../types/types'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { CourseGQLData, CourseIdentifier, InvalidCourseData, PlannerData, PlannerQuarterData, PlannerYearData, QuarterIdentifier, TransferData, YearIdentifier } from '../../types/types'; +import type { RootState } from '../store'; // Define a type for the slice state interface RoadmapState { @@ -20,8 +20,9 @@ interface RoadmapState { showAddCourse: boolean; } + // Define the initial state using that type -const initialState: RoadmapState = { +export const initialState: RoadmapState = { yearPlans: [], activeCourse: null!, invalidCourses: [], @@ -31,6 +32,42 @@ const initialState: RoadmapState = { showAddCourse: false } +/** added for multiple planner */ +// create roadmap plan object +interface RoadmapPlan { + name: string; + content: RoadmapState; +} + +interface RoadmapPlanIdentifier { + planIndex: number; +} + +interface SetPlanNamePayload { + index: number; + name: string; +} + +// default plan to display for uesr +const defaultPlan: RoadmapPlan = { + name: "Schedule 1", + content: initialState +}; + +// have an array of RoadmapPlan; use index to access them later +interface RoadmapPlans { + plans: RoadmapPlan[]; + currentPlanIndex: number; +}; + +// define initial empty plans +const initialPlans: RoadmapPlans = { + plans: [defaultPlan], + currentPlanIndex: 0 +}; +/** added for multiple planner */ + + // Payload to pass in to move a course interface MoveCoursePayload { from: CourseIdentifier; @@ -60,10 +97,11 @@ interface SetTransferPayload { transfer: TransferData; } + export const roadmapSlice = createSlice({ name: 'roadmap', // `createSlice` will infer the state type from the `initialState` argument - initialState, + initialState: initialPlans, reducers: { // Use the PayloadAction type to declare the contents of `action.payload` moveCourse: (state, action: PayloadAction) => { @@ -78,25 +116,25 @@ export const roadmapSlice = createSlice({ // not from the searchbar if (fromYear != -1) { // remove course from list - let courseList = state.yearPlans[fromYear].quarters[fromQuarter].courses; + let courseList = state.plans[state.currentPlanIndex].content.yearPlans[fromYear].quarters[fromQuarter].courses; [removed] = courseList.splice(fromCourse, 1); } // from the searchbar else { // active course has the current dragging course - removed = state.activeCourse; + removed = state.plans[state.currentPlanIndex].content.activeCourse; } // add course to list - let courseList = state.yearPlans[toYear].quarters[toQuarter].courses; + let courseList = state.plans[state.currentPlanIndex].content.yearPlans[toYear].quarters[toQuarter].courses; courseList.splice(toCourse, 0, removed!); }, deleteCourse: (state, action: PayloadAction) => { - state.yearPlans[action.payload.yearIndex].quarters[action.payload.quarterIndex].courses.splice(action.payload.courseIndex, 1); + state.plans[state.currentPlanIndex].content.yearPlans[action.payload.yearIndex].quarters[action.payload.quarterIndex].courses.splice(action.payload.courseIndex, 1); }, addQuarter: (state, action: PayloadAction) => { let startYear = action.payload.startYear; - let currentYears = state.yearPlans.map(e => e.startYear); + let currentYears = state.plans[state.currentPlanIndex].content.yearPlans.map(e => e.startYear); let newQuarter = action.payload.quarterData; // if year doesn't exist @@ -106,7 +144,7 @@ export const roadmapSlice = createSlice({ } let yearIndex: number = currentYears.indexOf(startYear); - let currentQuarters = state.yearPlans[yearIndex].quarters.map(e => e.name); + let currentQuarters = state.plans[state.currentPlanIndex].content.yearPlans[yearIndex].quarters.map(e => e.name); // if duplicate quarter if (currentQuarters.includes(newQuarter.name)) { @@ -127,16 +165,16 @@ export const roadmapSlice = createSlice({ } } - state.yearPlans[yearIndex].quarters.splice(index, 0, newQuarter); + state.plans[state.currentPlanIndex].content.yearPlans[yearIndex].quarters.splice(index, 0, newQuarter); }, deleteQuarter: (state, action: PayloadAction) => { - state.yearPlans[action.payload.yearIndex].quarters.splice(action.payload.quarterIndex, 1); + state.plans[state.currentPlanIndex].content.yearPlans[action.payload.yearIndex].quarters.splice(action.payload.quarterIndex, 1); }, clearQuarter: (state, action: PayloadAction) => { - state.yearPlans[action.payload.yearIndex].quarters[action.payload.quarterIndex].courses = []; + state.plans[state.currentPlanIndex].content.yearPlans[action.payload.yearIndex].quarters[action.payload.quarterIndex].courses = []; }, addYear: (state, action: PayloadAction) => { - let currentYears = state.yearPlans.map(e => e.startYear); + let currentYears = state.plans[state.currentPlanIndex].content.yearPlans.map(e => e.startYear); let newYear = action.payload.yearData.startYear; // if duplicate year @@ -153,10 +191,10 @@ export const roadmapSlice = createSlice({ } } - state.yearPlans.splice(index, 0, action.payload.yearData); + state.plans[state.currentPlanIndex].content.yearPlans.splice(index, 0, action.payload.yearData); }, editYear: (state, action: PayloadAction) => { - let currentYears = state.yearPlans.map(e => e.startYear); + let currentYears = state.plans[state.currentPlanIndex].content.yearPlans.map(e => e.startYear); let newYear = action.payload.startYear; let yearIndex = action.payload.index; @@ -167,59 +205,86 @@ export const roadmapSlice = createSlice({ } // edit year & sort years - state.yearPlans[yearIndex].startYear = newYear; - state.yearPlans.sort((a, b) => a.startYear - b.startYear); + state.plans[state.currentPlanIndex].content.yearPlans[yearIndex].startYear = newYear; + state.plans[state.currentPlanIndex].content.yearPlans.sort((a, b) => a.startYear - b.startYear); }, deleteYear: (state, action: PayloadAction) => { - state.yearPlans.splice(action.payload.yearIndex, 1); + state.plans[state.currentPlanIndex].content.yearPlans.splice(action.payload.yearIndex, 1); }, clearYear: (state, action: PayloadAction) => { - for (let i = 0; i < state.yearPlans[action.payload.yearIndex].quarters.length; i++) { - state.yearPlans[action.payload.yearIndex].quarters[i].courses = []; + for (let i = 0; i < state.plans[state.currentPlanIndex].content.yearPlans[action.payload.yearIndex].quarters.length; i++) { + state.plans[state.currentPlanIndex].content.yearPlans[action.payload.yearIndex].quarters[i].courses = []; } }, clearPlanner: (state) => { - state.yearPlans = []; + state.plans[state.currentPlanIndex].content.yearPlans = []; }, setActiveCourse: (state, action: PayloadAction) => { - state.activeCourse = action.payload; + state.plans[state.currentPlanIndex].content.activeCourse = action.payload; }, setYearPlans: (state, action: PayloadAction) => { - state.yearPlans = action.payload; + state.plans[state.currentPlanIndex].content.yearPlans = action.payload; }, setInvalidCourses: (state, action: PayloadAction) => { - state.invalidCourses = action.payload; + state.plans[state.currentPlanIndex].content.invalidCourses = action.payload; }, setShowTransfer: (state, action: PayloadAction) => { - state.showTransfer = action.payload; + state.plans[state.currentPlanIndex].content.showTransfer = action.payload; }, addTransfer: (state, action: PayloadAction) => { - state.transfers.push(action.payload); + state.plans[state.currentPlanIndex].content.transfers.push(action.payload); }, setTransfer: (state, action: PayloadAction) => { - state.transfers[action.payload.index] = action.payload.transfer; + state.plans[state.currentPlanIndex].content.transfers[action.payload.index] = action.payload.transfer; }, setTransfers: (state, action: PayloadAction) => { - state.transfers = action.payload; + state.plans[state.currentPlanIndex].content.transfers = action.payload; }, deleteTransfer: (state, action: PayloadAction) => { - state.transfers.splice(action.payload, 1); + state.plans[state.currentPlanIndex].content.transfers.splice(action.payload, 1); }, setShowSearch: (state, action: PayloadAction) => { - state.showSearch = action.payload; + state.plans[state.currentPlanIndex].content.showSearch = action.payload; }, setShowAddCourse: (state, action: PayloadAction) => { - state.showAddCourse = action.payload; + state.plans[state.currentPlanIndex].content.showAddCourse = action.payload; + }, + /** added for multiple plans */ + setRoadmapPlan: (state, action: PayloadAction) => { + state.plans = action.payload.plans; + }, + addRoadmapPlan: (state, action: PayloadAction) => { + state.plans.push(action.payload); + }, + deleteRoadmapPlan: (state, action: PayloadAction) => { + state.plans.splice(action.payload.planIndex, 1); + if (state.plans.length === 0) { + state.plans.push(defaultPlan); + } }, + setPlanIndex: (state, action: PayloadAction) => { + state.currentPlanIndex = action.payload; + }, + setPlanName: (state, action: PayloadAction) => { + let index = action.payload.index; + state.plans[index].name = action.payload.name; + } + /** added for multiple plans */ }, }) + export const { moveCourse, deleteCourse, addQuarter, deleteQuarter, clearQuarter, clearYear, addYear, editYear, deleteYear, clearPlanner, setActiveCourse, setYearPlans, setInvalidCourses, setShowTransfer, addTransfer, setTransfer, - setTransfers, deleteTransfer, setShowSearch, setShowAddCourse } = roadmapSlice.actions + setTransfers, deleteTransfer, setShowSearch, setShowAddCourse, + setRoadmapPlan, addRoadmapPlan, deleteRoadmapPlan, setPlanIndex, setPlanName } = roadmapSlice.actions + + +// export const { setRoadmapPlan, addRoadmapPlan, deleteRoadmapPlan } = roadmapMultiplePlanSlice.actions; // Other code such as selectors can use the imported `RootState` type -export const selectYearPlans = (state: RootState) => state.roadmap.yearPlans; +export const selectYearPlans = (state: RootState) => state.roadmap.plans[state.roadmap.currentPlanIndex].content.yearPlans; + -export default roadmapSlice.reducer \ No newline at end of file +export default roadmapSlice.reducer; From 943d69a02716e0c1c58df4b652e6cc8789c6586f Mon Sep 17 00:00:00 2001 From: pyccj Date: Fri, 9 Jun 2023 16:38:41 -0700 Subject: [PATCH 2/2] Merge remote-tracking branch 'origin/master' into multiple-plans --- .github/workflows/node.js.yml | 8 +- api/package-lock.json | 85 +---------- api/package.json | 4 +- api/src/controllers/schedule.ts | 16 ++- api/src/helpers/week.ts | 132 +++++++++++++----- pull_request_template.md | 6 +- site/package.json | 1 - site/src/component/Schedule/Schedule.tsx | 2 +- .../component/SearchPopup/SearchPopup.scss | 4 +- site/src/pages/RoadmapPage/Course.tsx | 2 +- site/src/pages/SearchPage/SearchPage.scss | 13 ++ site/src/types/types.ts | 53 +++++++ site/src/types/websoc-api.d.ts | 51 ------- site/tsconfig.json | 1 - 14 files changed, 194 insertions(+), 184 deletions(-) delete mode 100644 site/src/types/websoc-api.d.ts diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 80734ffb..b1c25135 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -4,9 +4,9 @@ on: push: branches: - master - pull_request: - branches: - - master +# pull_request: +# branches: +# - master jobs: deploy-aws: @@ -72,4 +72,4 @@ jobs: entrypoint: /bin/sh env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} \ No newline at end of file + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/api/package-lock.json b/api/package-lock.json index c62280e4..5d1c9b9e 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -4,11 +4,6 @@ "lockfileVersion": 1, "requires": true, "dependencies": { - "@assemblyscript/loader": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", - "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==" - }, "@aws-crypto/ie11-detection": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@aws-crypto/ie11-detection/-/ie11-detection-2.0.2.tgz", @@ -1415,14 +1410,6 @@ "get-intrinsic": "^1.0.2" } }, - "camaro": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/camaro/-/camaro-6.1.0.tgz", - "integrity": "sha512-Aa9q2fxR+9i90Z8BOH3JBt2TCa+JZbhaqj0Mmx4zsTUe8G1UyA4djLXjQimcp2+WByZWTYVEK7TPPzmdi7MObw==", - "requires": { - "piscina": "^2.1.0" - } - }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -1670,6 +1657,11 @@ "assert-plus": "^1.0.0" } }, + "dayjs": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1882,11 +1874,6 @@ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" }, - "eventemitter-asyncresource": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", - "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==" - }, "events": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", @@ -2303,21 +2290,6 @@ "has-symbols": "^1.0.2" } }, - "hdr-histogram-js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.1.tgz", - "integrity": "sha512-uPZxl1dAFnjUFHWLZmt93vUUvtHeaBay9nVNHu38SdOjMSF/4KqJUqa1Seuj08ptU1rEb6AHvB41X8n/zFZ74Q==", - "requires": { - "@assemblyscript/loader": "^0.10.1", - "base64-js": "^1.2.0", - "pako": "^1.0.3" - } - }, - "hdr-histogram-percentiles-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", - "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==" - }, "htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -2884,27 +2856,11 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, - "nice-napi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", - "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", - "optional": true, - "requires": { - "node-addon-api": "^3.0.0", - "node-gyp-build": "^4.2.2" - } - }, "nocache": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/nocache/-/nocache-3.0.1.tgz", "integrity": "sha512-Gh39xwJwBKy0OvFmWfBs/vDO4Nl7JhnJtkqNP76OUinQz7BiMoszHYrIDHHAaqVl/QKVxCEy4ZxC/XZninu7nQ==" }, - "node-addon-api": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", - "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", - "optional": true - }, "node-cleanup": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/node-cleanup/-/node-cleanup-2.1.2.tgz", @@ -2919,12 +2875,6 @@ "whatwg-url": "^5.0.0" } }, - "node-gyp-build": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.2.3.tgz", - "integrity": "sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg==", - "optional": true - }, "nodemon": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", @@ -3041,11 +2991,6 @@ "resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.0.3.tgz", "integrity": "sha512-RV2Zp2MY2aeYK5G+B/Sps8lW5NHAzE5QClbFP15j+PWmP+T9PxlJXBOOLoSAdgwFvS4t0aMR4vpedMkbHfh0nA==" }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, "parse5": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", @@ -3185,17 +3130,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" }, - "piscina": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-2.2.0.tgz", - "integrity": "sha512-CQb0DfyTdC9FBIMYkVV/00fXRLKDjmWKA8S0N1zDg2JGEc5z3P9qHXtoq8OkJQ+vjCfXySkVonTNMqskMFOW/w==", - "requires": { - "eventemitter-asyncresource": "^1.0.0", - "hdr-histogram-js": "^2.0.1", - "hdr-histogram-percentiles-obj": "^3.0.0", - "nice-napi": "^1.0.2" - } - }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -3921,15 +3855,6 @@ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" }, - "websoc-api": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/websoc-api/-/websoc-api-1.0.0.tgz", - "integrity": "sha512-tufybVgn/HnJLjJMQeMyv11PbisqqQvEdtSgg4fsPZY2mBCMfXUGXVLAElYLt7+ojl9IS4SRYvJ6bTmGKwHpVQ==", - "requires": { - "camaro": "^6.0.2", - "node-fetch": "^2.6.0" - } - }, "whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", diff --git a/api/package.json b/api/package.json index 5b4764c1..a0d799d2 100644 --- a/api/package.json +++ b/api/package.json @@ -11,6 +11,7 @@ "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", @@ -34,8 +35,7 @@ "request-promise": "^4.2.6", "serverless-http": "^2.7.0", "shortid": "^2.2.15", - "typescript": "^4.3.5", - "websoc-api": "^1.0.0" + "typescript": "^4.3.5" }, "devDependencies": { "@types/connect-mongodb-session": "^2.4.2", diff --git a/api/src/controllers/schedule.ts b/api/src/controllers/schedule.ts index 3a93d967..77663a26 100644 --- a/api/src/controllers/schedule.ts +++ b/api/src/controllers/schedule.ts @@ -5,8 +5,8 @@ import express from 'express'; import { getWeek } from '../helpers/week'; import { getCurrentQuarter } from '../helpers/currentQuarter'; +import fetch from 'node-fetch'; -const websoc = require('websoc-api'); var router = express.Router(); const TERM_SEASONS = ['Winter', 'Spring', 'Summer1', 'Summer10wk', 'Summer2', 'Fall'] @@ -43,10 +43,10 @@ router.get('/api/currentWeek', function (req, res, next) { /** - * Proxy for WebSOC + * Proxy for WebSOC, using PeterPortal API */ router.get('/api/:term/:department/:number', async function (req, res) { - const result = await websoc.callWebSocAPI({ + const result = await callPPAPIWebSoc({ term: req.params.term, department: req.params.department, courseNumber: req.params.number @@ -55,14 +55,20 @@ router.get('/api/:term/:department/:number', async function (req, res) { }); /** - * Proxy for WebSOC + * Proxy for WebSOC, using PeterPortal API */ router.get('/api/:term/:professor', async function (req, res) { - const result = await websoc.callWebSocAPI({ + const result = await callPPAPIWebSoc({ term: req.params.term, instructorName: req.params.professor }); res.send(result); }); +async function callPPAPIWebSoc(params: Record) { + const url: URL = new URL(process.env.PUBLIC_API_URL + 'schedule/soc?' + + new URLSearchParams(params)) + return await fetch(url).then(response => response.json()); +} + export default router; \ No newline at end of file diff --git a/api/src/helpers/week.ts b/api/src/helpers/week.ts index 7ec1c4ef..d91c1658 100644 --- a/api/src/helpers/week.ts +++ b/api/src/helpers/week.ts @@ -6,6 +6,20 @@ 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'; +import objectSupport from 'dayjs/plugin/objectSupport'; + + +dayjs.extend(utc) +dayjs.extend(timezone) +dayjs.extend(objectSupport) + +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. @@ -13,9 +27,9 @@ import { QuarterMapping, WeekData } from '../types/types'; export function getWeek(): Promise { return new Promise(async resolve => { // current date - let date = new Date(Date.now()); + let date = dayjs().tz(); // current year - let year = date.getFullYear(); + let year = date.year(); // check for current year to current year + 1 let quarterMapping1 = await getQuarterMapping(year) as QuarterMapping; @@ -49,15 +63,45 @@ export function getWeek(): Promise { * @param quarterMapping Maps a quarter to its start and end date * @returns Week description if it lies within the quarter */ -function findWeek(date: Date, quarterMapping: QuarterMapping): WeekData { +function findWeek(date: dayjs.Dayjs, quarterMapping: QuarterMapping): WeekData { let result: WeekData = undefined!; // iterate through each quarter Object.keys(quarterMapping).forEach(function (quarter) { - let begin = new Date(quarterMapping[quarter]['begin']) - let end = new Date(quarterMapping[quarter]['end']) + 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({ year: beginDate.getUTCFullYear(), month: beginDate.getUTCMonth(), date: beginDate.getUTCDate() }); + end = dayjs.tz({ year: endDate.getUTCFullYear(), month: endDate.getUTCMonth(), date: endDate.getUTCDate() }); + } 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 last ms of the day + end = end.add({ + hours: 23, + minutes: 59, + seconds: 59, + ms: 999 + }); + + // 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 week = Math.floor(dateSubtract(begin, date) / 7) + 1; + 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, @@ -65,8 +109,8 @@ function findWeek(date: Date, quarterMapping: QuarterMapping): WeekData { display: display } } - // check if date is 1 week after end - else if (date > end && date <= addDays(end, 7)) { + // 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, @@ -169,7 +213,7 @@ function processRow(row: Element, $: CheerioAPI, quarterToDayMapping: QuarterMap * processDate('Jan 17', 'Winter 2020', 2019) * @example * // returns Date(7/30/2021) - * processDate('July 30', 'Summer Session 10WK', 2020) + * 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 @@ -182,8 +226,50 @@ function processDate(dateEntry: string, dateLabel: string, year: number): Date { let labelYear = dateLabel.split(' ')[1]; // 'Winter 2020' => 2020, but 'Summer Session I' => Session // Exception for Summer Session - let correctYear = isInteger(labelYear) ? labelYear : year + 1; - return new Date(`${month}/${day}/${correctYear}`) + let correctYear = isInteger(labelYear) ? parseInt(labelYear) : year + 1; + + return dayjs.tz({ year: correctYear, month: processMonth(month), date: day }).toDate(); +} + +/** + * @example + * // returns 0 + * processMonth('Jan') + * @example + * // returns 6 + * processMonth('Jul') + * @param month Month name as it appears on registrar + * @returns Month index (0-11) + */ +function processMonth(month: string): number { + switch (month) { + case 'Jan': + return 0; + case 'Feb': + return 1; + case 'Mar': + return 2; + case 'Apr': + return 3; + case 'May': + return 4; + case 'Jun': + return 5; + case 'Jul': + return 6; + case 'Aug': + return 7; + case 'Sep': + return 8; + case 'Oct': + return 9; + case 'Nov': + return 10; + case 'Dec': + return 11; + } + + return -1; } /** @@ -202,28 +288,4 @@ function strip(str: string): string { */ function isInteger(num: string): boolean { return !isNaN(parseInt(num, 10)) -} - -/** - * Get the number of days between two dates - * @param date1 Earlier date - * @param date2 Later date - * @returns Number of days between date1 and date2 - */ -function dateSubtract(date1: Date, date2: Date): number { - // To calculate the time difference of two dates - let Difference_In_Time = date2.getTime() - date1.getTime(); - // To calculate the no. of days between two dates - return Difference_In_Time / (1000 * 3600 * 24); -} - -/** - * Add days to a date - * @param date Date to add days to - * @param days Number of days to add - * @returns Same date as the one passed in - */ -function addDays(date: Date, days: number): Date { - date.setDate(date.getDate() + days); - return date; } \ No newline at end of file diff --git a/pull_request_template.md b/pull_request_template.md index 64329416..5d838f19 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -21,6 +21,10 @@ ## Steps to verify/test this change: -## Todos: +## Final Checks: +- [ ] Verify successful deployment +- [ ] Delete branch + +(optional) - [ ] Write tests - [ ] Write documentation diff --git a/site/package.json b/site/package.json index ea3b852d..a6198e3b 100644 --- a/site/package.json +++ b/site/package.json @@ -46,7 +46,6 @@ "semantic-ui-react": "^1.2.1", "treeviz-react": "^0.4.0", "typescript": "^4.3.5", - "websoc-api": "^1.0.0", "websoc-fuzzy-search": "^0.8.0-rc.1" }, "scripts": { diff --git a/site/src/component/Schedule/Schedule.tsx b/site/src/component/Schedule/Schedule.tsx index 3129002a..9b52468a 100644 --- a/site/src/component/Schedule/Schedule.tsx +++ b/site/src/component/Schedule/Schedule.tsx @@ -6,7 +6,7 @@ import Table from 'react-bootstrap/Table'; import ProgressBar from 'react-bootstrap/ProgressBar'; import Button from 'react-bootstrap/Button'; -import { WebsocResponse, Section } from 'websoc-api'; +import { WebsocResponse, Section } from '../../types/types'; interface ScheduleProps { courseID?: string; diff --git a/site/src/component/SearchPopup/SearchPopup.scss b/site/src/component/SearchPopup/SearchPopup.scss index b04c609d..96e201b0 100644 --- a/site/src/component/SearchPopup/SearchPopup.scss +++ b/site/src/component/SearchPopup/SearchPopup.scss @@ -11,11 +11,11 @@ } .search-popup { - position: sticky; background-color: #ffffff; padding: 3rem; margin-left: 3vw; - height: 95%; + height: 85vh; + overflow-y: auto; border-radius: var(--border-radius); max-width: 40vw; } diff --git a/site/src/pages/RoadmapPage/Course.tsx b/site/src/pages/RoadmapPage/Course.tsx index 4a5500cc..3b26b67f 100644 --- a/site/src/pages/RoadmapPage/Course.tsx +++ b/site/src/pages/RoadmapPage/Course.tsx @@ -24,7 +24,7 @@ const Course: FC = (props) => {
{description}
{prerequisite_text &&
- Prequisite: {prerequisite_text} + Prerequisite: {prerequisite_text}
} {corequisite &&
Corequisite: {corequisite} diff --git a/site/src/pages/SearchPage/SearchPage.scss b/site/src/pages/SearchPage/SearchPage.scss index 706dda6b..2b1247a6 100644 --- a/site/src/pages/SearchPage/SearchPage.scss +++ b/site/src/pages/SearchPage/SearchPage.scss @@ -12,12 +12,25 @@ #search-list { width: 50vw; + display: flex; + flex-direction: column; + height: 85vh; + overflow-y: hidden; } #search-popup { flex-grow: 1; height: fit-content; } + + .search-hit-container { + margin-top: 2vh; // use margin instead of padding so the scroll bar isn't offset/above the first hit item + padding-top: 0; + + .hit-item:last-child { + margin-bottom: 0; // so scroll bar doesn't extend past the last hit item + } + } } @media only screen and (max-width: 800px) { diff --git a/site/src/types/types.ts b/site/src/types/types.ts index 95bfc084..c951790a 100644 --- a/site/src/types/types.ts +++ b/site/src/types/types.ts @@ -297,4 +297,57 @@ export interface SubCourse { 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; +} +export interface EnrollmentCount { + totalEnrolled: string; + sectionEnrolled: string; } \ No newline at end of file diff --git a/site/src/types/websoc-api.d.ts b/site/src/types/websoc-api.d.ts deleted file mode 100644 index 85ec3ce5..00000000 --- a/site/src/types/websoc-api.d.ts +++ /dev/null @@ -1,51 +0,0 @@ -declare module "websoc-api" { - 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; - } - export interface EnrollmentCount { - totalEnrolled: string; - sectionEnrolled: string; - } -} \ No newline at end of file diff --git a/site/tsconfig.json b/site/tsconfig.json index 318bb3a9..892b59df 100644 --- a/site/tsconfig.json +++ b/site/tsconfig.json @@ -24,7 +24,6 @@ "include": [ "src/", "src/types/react-twemoji.d.ts", - "src/types/websoc-api.d.ts", "src/types/types.ts" ] }