diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c708e858..aac78b504 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,6 @@ jobs: backend_tests: name: Backend Unit Tests runs-on: ubuntu-latest - if: false permissions: # required for artifacts checks: write pull-requests: write @@ -111,7 +110,7 @@ jobs: docker_build: name: Build docker containers runs-on: ubuntu-latest - needs: [backend_lint, backend_typecheck, frontend_lint, frontend_typecheck] # TODO: add test jobs when they get unskipped + needs: [backend_lint, backend_typecheck, backend_tests, frontend_lint, frontend_typecheck] # TODO: add test jobs when they get unskipped steps: - name: Checking out repository uses: actions/checkout@v4 diff --git a/backend/server/tests/user/utility.py b/backend/server/tests/user/utility.py index 09d25ef9b..f69d56991 100644 --- a/backend/server/tests/user/utility.py +++ b/backend/server/tests/user/utility.py @@ -1,14 +1,16 @@ +# pylint: disable=wrong-import-position import os import requests from dotenv import load_dotenv +load_dotenv("../env/backend.env") # must be before the connection imports as they require the env +os.environ["SESSIONSDB_SERVICE_HOSTNAME"] = "localhost" +os.environ["MONGODB_SERVICE_HOSTNAME"] = "localhost" + from server.db.mongo.setup import setup_user_related_collections from server.db.redis.setup import setup_redis_sessionsdb -load_dotenv("../env/backend.env") -os.environ["MONGODB_SERVICE_HOSTNAME"] = "localhost" -os.environ["SESSIONSDB_SERVICE_HOSTNAME"] = "localhost" diff --git a/frontend/src/components/Auth/IdentityProvider.tsx b/frontend/src/components/Auth/IdentityProvider.tsx index bd1993e6d..f87f2551c 100644 --- a/frontend/src/components/Auth/IdentityProvider.tsx +++ b/frontend/src/components/Auth/IdentityProvider.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { Outlet } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import { isAxiosError } from 'axios'; -import { refreshTokens } from 'utils/api/auth'; +import { refreshTokens } from 'utils/api/authApi'; import PageLoading from 'components/PageLoading'; import { useAppDispatch, useAppSelector } from 'hooks'; import { selectIdentity, unsetIdentity, updateIdentityWithAPIRes } from 'reducers/identitySlice'; diff --git a/frontend/src/components/CourseDescriptionPanel/CourseDescriptionPanel.tsx b/frontend/src/components/CourseDescriptionPanel/CourseDescriptionPanel.tsx index c3a0fbd65..ad5d48529 100644 --- a/frontend/src/components/CourseDescriptionPanel/CourseDescriptionPanel.tsx +++ b/frontend/src/components/CourseDescriptionPanel/CourseDescriptionPanel.tsx @@ -2,24 +2,30 @@ import React from 'react'; import { useLocation } from 'react-router-dom'; import { useQuery } from '@tanstack/react-query'; import { Typography } from 'antd'; -import axios from 'axios'; -import { Course, CoursePathFrom, CoursesUnlockedWhenTaken } from 'types/api'; -import { CourseTimetable } from 'types/courseCapacity'; import { CoursesResponse, DegreeResponse, PlannerResponse } from 'types/userResponse'; +import { getCourseInfo, getCoursePrereqs, getCoursesUnlockedWhenTaken } from 'utils/api/coursesApi'; +import { getCourseTimetable } from 'utils/api/timetableApi'; import getEnrolmentCapacity from 'utils/getEnrolmentCapacity'; -import prepareUserPayload from 'utils/prepareUserPayload'; +import { unwrapSettledPromise } from 'utils/queryUtils'; import { LoadingCourseDescriptionPanel, LoadingCourseDescriptionPanelSidebar } from 'components/LoadingSkeleton'; import PlannerButton from 'components/PlannerButton'; -import { TIMETABLE_API_URL } from 'config/constants'; import CourseAttributes from './CourseAttributes'; import CourseInfoDrawers from './CourseInfoDrawers'; import S from './styles'; const { Title, Text } = Typography; +const getCourseExtendedInfo = async (courseCode: string) => { + return Promise.allSettled([ + getCourseInfo(courseCode), + getCoursePrereqs(courseCode), + getCourseTimetable(courseCode) + ]); +}; + type CourseDescriptionPanelProps = { className?: string; courseCode: string; @@ -37,45 +43,18 @@ const CourseDescriptionPanel = ({ courses, degree }: CourseDescriptionPanelProps) => { - const getCoursesUnlocked = React.useCallback(async () => { - if (!degree || !planner || !courses) - return Promise.reject(new Error('degree, planner or courses undefined')); - const res = await axios.post( - `/courses/coursesUnlockedWhenTaken/${courseCode}`, - JSON.stringify(prepareUserPayload(degree, planner, courses)) - ); - return res.data; - }, [courseCode, degree, planner, courses]); - - const getCourseInfo = React.useCallback(async () => { - return Promise.allSettled([ - axios.get(`/courses/getCourse/${courseCode}`), - axios.get(`/courses/getPathFrom/${courseCode}`), - axios.get(`${TIMETABLE_API_URL}/${courseCode}`) - ]); - }, [courseCode]); - const coursesUnlockedQuery = useQuery({ queryKey: ['coursesUnlocked', courseCode, degree, planner, courses], - queryFn: getCoursesUnlocked, + queryFn: () => getCoursesUnlockedWhenTaken(degree!, planner!, courses!, courseCode), enabled: !!degree && !!planner && !!courses }); const { pathname } = useLocation(); const sidebar = pathname === '/course-selector'; - function unwrap(res: PromiseSettledResult): T | undefined { - if (res.status === 'rejected') { - // eslint-disable-next-line no-console - console.error('Rejected request at unwrap', res.reason); - return undefined; - } - return res.value; - } - const courseInfoQuery = useQuery({ queryKey: ['courseInfo', courseCode], - queryFn: getCourseInfo + queryFn: () => getCourseExtendedInfo(courseCode) }); const loadingWrapper = ( @@ -87,9 +66,9 @@ const CourseDescriptionPanel = ({ if (courseInfoQuery.isPending || !courseInfoQuery.isSuccess) return loadingWrapper; const [courseRes, pathFromRes, courseCapRes] = courseInfoQuery.data; - const course = unwrap(courseRes)?.data; - const coursesPathFrom = unwrap(pathFromRes)?.data.courses; - const courseCapacity = getEnrolmentCapacity(unwrap(courseCapRes)?.data); + const course = unwrapSettledPromise(courseRes); + const coursesPathFrom = unwrapSettledPromise(pathFromRes)?.courses; + const courseCapacity = getEnrolmentCapacity(unwrapSettledPromise(courseCapRes)); // course wasn't fetchable (fatal; should do proper error handling instead of indefinitely loading) if (!course) return loadingWrapper; diff --git a/frontend/src/components/CourseSearchBar/CourseSearchBar.tsx b/frontend/src/components/CourseSearchBar/CourseSearchBar.tsx index 023c55d9e..60b955b45 100644 --- a/frontend/src/components/CourseSearchBar/CourseSearchBar.tsx +++ b/frontend/src/components/CourseSearchBar/CourseSearchBar.tsx @@ -4,7 +4,7 @@ import { Flex, Select, Spin, Typography } from 'antd'; import { SearchCourse } from 'types/api'; import { CoursesResponse } from 'types/userResponse'; import { useDebounce } from 'use-debounce'; -import { searchCourse } from 'utils/api/courseApi'; +import { searchCourse } from 'utils/api/coursesApi'; import { addToUnplanned, removeCourse } from 'utils/api/plannerApi'; import QuickAddCartButton from 'components/QuickAddCartButton'; import useToken from 'hooks/useToken'; diff --git a/frontend/src/components/PrerequisiteTree/PrerequisiteTree.tsx b/frontend/src/components/PrerequisiteTree/PrerequisiteTree.tsx index f29bd073d..52086e400 100644 --- a/frontend/src/components/PrerequisiteTree/PrerequisiteTree.tsx +++ b/frontend/src/components/PrerequisiteTree/PrerequisiteTree.tsx @@ -1,8 +1,7 @@ import React, { useEffect, useRef, useState } from 'react'; import type { Item, TreeGraph, TreeGraphData } from '@antv/g6'; -import axios from 'axios'; -import { CourseChildren, CoursePathFrom } from 'types/api'; -import { CourseList } from 'types/courses'; +import { useQuery } from '@tanstack/react-query'; +import { getCourseChildren, getCoursePrereqs } from 'utils/api/coursesApi'; import Spinner from 'components/Spinner'; import GRAPH_STYLE from './config'; import TREE_CONSTANTS from './constants'; @@ -15,15 +14,25 @@ type Props = { }; const PrerequisiteTree = ({ courseCode, onCourseClick }: Props) => { - const [loading, setLoading] = useState(true); + const [initGraph, setInitGraph] = useState(true); const graphRef = useRef(null); - const [courseUnlocks, setCourseUnlocks] = useState([]); - const [coursesRequires, setCoursesRequires] = useState([]); const ref = useRef(null); + const childrenQuery = useQuery({ + queryKey: ['course', 'children', courseCode], // TODO: make this key reasonable when we rework all keys + queryFn: () => getCourseChildren(courseCode), + select: (data) => data.courses + }); + + const prereqsQuery = useQuery({ + queryKey: ['course', 'prereqs', courseCode], + queryFn: () => getCoursePrereqs(courseCode), + select: (data) => data.courses + }); + useEffect(() => { - // if the course code changes, force a reload - setLoading(true); + // if the course code changes, force a reload (REQUIRED) + setInitGraph(true); }, [courseCode]); useEffect(() => { @@ -56,76 +65,56 @@ const PrerequisiteTree = ({ courseCode, onCourseClick }: Props) => { const node = event.item as Item; if (onCourseClick) onCourseClick(node.getModel().label as string); }); + + setInitGraph(false); }; - // NOTE: This is for hot reloading in development as new graph will instantiate every time const updateTreeGraph = (graphData: TreeGraphData) => { + // TODO: fix sizing change here (will need to use graph.changeSize with the scroll height/width) + // Doing so has weird interactions where it grows in width each update, + // which gets influenced by the margins from here and Collapsible, and by the loading spinner size (= 142px) if (!graphRef.current) return; graphRef.current.changeData(graphData); bringEdgeLabelsToFront(graphRef.current); - }; - - /* REQUESTS */ - const getCourseUnlocks = async (code: string) => { - try { - const res = await axios.get(`/courses/courseChildren/${code}`); - return res.data.courses; - } catch (e) { - // eslint-disable-next-line no-console - console.error('Error at getCourseUnlocks', e); - return []; - } - }; - const getCoursePrereqs = async (code: string) => { - try { - const res = await axios.get(`/courses/getPathFrom/${code}`); - return res.data.courses; - } catch (e) { - // eslint-disable-next-line no-console - console.error('Error at getCoursePrereqs', e); - return []; - } + setInitGraph(false); }; - /* MAIN */ - const setupGraph = async (c: string) => { - setLoading(true); - - const unlocks = await getCourseUnlocks(c); - if (unlocks) setCourseUnlocks(unlocks); - const prereqs = await getCoursePrereqs(c); - if (prereqs) setCoursesRequires(prereqs); - - // create graph data + if (initGraph && !childrenQuery.isPending && !prereqsQuery.isPending) { + const unlocks = childrenQuery.data ?? []; + const prereqs = prereqsQuery.data ?? []; const graphData = { id: 'root', label: courseCode, children: prereqs - ?.map((child) => handleNodeData(child, TREE_CONSTANTS.PREREQ)) - .concat(unlocks?.map((child) => handleNodeData(child, TREE_CONSTANTS.UNLOCKS))) + .map((child) => handleNodeData(child, TREE_CONSTANTS.PREREQ)) + .concat(unlocks.map((child) => handleNodeData(child, TREE_CONSTANTS.UNLOCKS))) }; - // render graph if (!graphRef.current && graphData.children.length !== 0) { generateTreeGraph(graphData); } else { // NOTE: This is for hot reloading in development as new graph will instantiate every time updateTreeGraph(graphData); } - setLoading(false); - }; - - if (loading) { - setupGraph(courseCode); - setLoading(false); } - }, [courseCode, loading, onCourseClick]); + }, [ + courseCode, + initGraph, + onCourseClick, + childrenQuery.data, + prereqsQuery.data, + childrenQuery.isPending, + prereqsQuery.isPending + ]); + + const loading = initGraph || childrenQuery.isPending || prereqsQuery.isPending; + const height = calcHeight(prereqsQuery.data ?? [], childrenQuery.data ?? []); return ( - + {loading && } - {!loading && graphRef.current && !graphRef.current.getEdges().length && ( + {!loading && graphRef.current && graphRef.current.getEdges().length === 0 && (

No prerequisite visualisation is needed for this course

)}
diff --git a/frontend/src/config/migrations.ts b/frontend/src/config/migrations.ts index a55acda93..6a16ed371 100644 --- a/frontend/src/config/migrations.ts +++ b/frontend/src/config/migrations.ts @@ -3,10 +3,9 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck -import axios from 'axios'; import type { MigrationManifest } from 'redux-persist'; import { createMigrate } from 'redux-persist'; -import { Course } from 'types/api'; +import { getCourseInfo } from 'utils/api/coursesApi'; /** * IMPORTANT NOTE: @@ -43,7 +42,7 @@ const persistMigrations: MigrationManifest = { await Promise.all( courses.map(async (course) => { try { - const res = await axios.get(`/courses/getCourse/${course}`); + const res = await getCourseInfo(course); if (res.status === 200) { const courseData = res.data; newState.planner.courses[courseData.code].isMultiterm = courseData.is_multiterm; diff --git a/frontend/src/pages/CourseSelector/CourseBanner/CourseBanner.tsx b/frontend/src/pages/CourseSelector/CourseBanner/CourseBanner.tsx index 11ee282f3..1ef5c09bc 100644 --- a/frontend/src/pages/CourseSelector/CourseBanner/CourseBanner.tsx +++ b/frontend/src/pages/CourseSelector/CourseBanner/CourseBanner.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useQuery } from '@tanstack/react-query'; import { Typography } from 'antd'; import { CoursesResponse } from 'types/userResponse'; -import { fetchAllDegrees } from 'utils/api/programApi'; +import { fetchAllDegrees } from 'utils/api/programsApi'; import { getUserDegree } from 'utils/api/userApi'; import CourseSearchBar from 'components/CourseSearchBar'; import { useAppDispatch } from 'hooks'; diff --git a/frontend/src/pages/DegreeWizard/DegreeStep/DegreeStep.tsx b/frontend/src/pages/DegreeWizard/DegreeStep/DegreeStep.tsx index 468da4e20..fffff8906 100644 --- a/frontend/src/pages/DegreeWizard/DegreeStep/DegreeStep.tsx +++ b/frontend/src/pages/DegreeWizard/DegreeStep/DegreeStep.tsx @@ -1,10 +1,10 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { animated, useSpring } from '@react-spring/web'; +import { useQuery } from '@tanstack/react-query'; import { Input, Menu, Typography } from 'antd'; -import axios from 'axios'; import { fuzzy } from 'fast-fuzzy'; -import { Programs } from 'types/api'; import { DegreeWizardPayload } from 'types/degreeWizard'; +import { fetchAllDegrees } from 'utils/api/programsApi'; import springProps from '../common/spring'; import Steps from '../common/steps'; import CS from '../common/styles'; @@ -22,21 +22,13 @@ type Props = { const DegreeStep = ({ incrementStep, degreeInfo, setDegreeInfo }: Props) => { const [input, setInput] = useState(''); const [options, setOptions] = useState([]); - const [allDegrees, setAllDegrees] = useState>({}); - const fetchAllDegrees = async () => { - try { - const res = await axios.get('/programs/getPrograms'); - setAllDegrees(res.data.programs); - } catch (e) { - // eslint-disable-next-line no-console - console.error('Error at fetchAllDegrees', e); - } - }; - - useEffect(() => { - fetchAllDegrees(); - }, []); + const allDegreesQuery = useQuery({ + queryKey: ['programs'], + queryFn: fetchAllDegrees, + select: (data) => data.programs + }); + const allDegrees = allDegreesQuery.data ?? {}; const onDegreeChange = async ({ key }: { key: string }) => { setInput(key); diff --git a/frontend/src/pages/DegreeWizard/DegreeWizard.tsx b/frontend/src/pages/DegreeWizard/DegreeWizard.tsx index 5bc5bd223..60d756156 100644 --- a/frontend/src/pages/DegreeWizard/DegreeWizard.tsx +++ b/frontend/src/pages/DegreeWizard/DegreeWizard.tsx @@ -3,9 +3,8 @@ import { useNavigate } from 'react-router-dom'; import { scroller } from 'react-scroll'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Typography } from 'antd'; -import axios from 'axios'; -import { SpecialisationTypes } from 'types/api'; import { DegreeWizardPayload } from 'types/degreeWizard'; +import { getSpecialisationTypes } from 'utils/api/specsApi'; import { getUserIsSetup, resetUserDegree } from 'utils/api/userApi'; import openNotification from 'utils/openNotification'; import PageTemplate from 'components/PageTemplate'; @@ -20,10 +19,11 @@ import YearStep from './YearStep'; const { Title } = Typography; +const DEFAULT_SPEC_TYPES = ['majors', 'honours', 'minors']; + const DegreeWizard = () => { - const [specs, setSpecs] = useState(['majors', 'honours', 'minors']); - const stepList = ['year', 'degree'].concat(specs).concat(['start browsing']); const token = useToken(); + const [currStep, setCurrStep] = useState(Steps.YEAR); const [degreeInfo, setDegreeInfo] = useState({ programCode: '', @@ -39,6 +39,15 @@ const DegreeWizard = () => { }).data; const navigate = useNavigate(); + const specTypesQuery = useQuery({ + queryKey: ['specialisations', 'types', programCode], + queryFn: () => getSpecialisationTypes(programCode), + select: (data) => data.types, + enabled: programCode !== '' + }); + const specs = specTypesQuery.data ?? DEFAULT_SPEC_TYPES; + const stepList = ['year', 'degree'].concat(specs).concat(['start browsing']); + const queryClient = useQueryClient(); const resetDegree = useMutation({ @@ -66,24 +75,6 @@ const DegreeWizard = () => { }); }, []); - useEffect(() => { - const getSteps = async () => { - try { - const res = await axios.get( - `/specialisations/getSpecialisationTypes/${programCode}` - ); - setSpecs(res.data.types); - } catch (e) { - // eslint-disable-next-line no-console - console.error('Error at getSteps', e); - } - }; - if (programCode !== '') getSteps(); - // run when we actually add d programCode - }, [programCode]); - - const [currStep, setCurrStep] = useState(Steps.YEAR); - const incrementStep = (stepTo?: Steps) => { const step = stepTo ? stepList[stepTo] : stepList[currStep + 1]; // if spec, then that's chillll diff --git a/frontend/src/pages/DegreeWizard/SpecialisationStep/SpecialisationStep.tsx b/frontend/src/pages/DegreeWizard/SpecialisationStep/SpecialisationStep.tsx index 88f766aef..d4dd1a0d3 100644 --- a/frontend/src/pages/DegreeWizard/SpecialisationStep/SpecialisationStep.tsx +++ b/frontend/src/pages/DegreeWizard/SpecialisationStep/SpecialisationStep.tsx @@ -1,10 +1,10 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React from 'react'; import { animated, useSpring } from '@react-spring/web'; +import { useQuery } from '@tanstack/react-query'; import type { MenuProps } from 'antd'; import { Button, Typography } from 'antd'; -import axios from 'axios'; -import { Specialisations } from 'types/api'; import { DegreeWizardPayload } from 'types/degreeWizard'; +import { getSpecialisationsForProgram } from 'utils/api/specsApi'; import openNotification from 'utils/openNotification'; import Spinner from 'components/Spinner'; import springProps from '../common/spring'; @@ -24,14 +24,6 @@ type Props = { setDegreeInfo: SetState; }; -type Specialisation = { - [spec: string]: { - is_optional?: boolean; - specs: Record; - notes: string; - }; -}; - const SpecialisationStep = ({ incrementStep, currStep, @@ -40,7 +32,6 @@ const SpecialisationStep = ({ setDegreeInfo }: Props) => { const props = useSpring(springProps); - const [options, setOptions] = useState(null); const handleAddSpecialisation = (specialisation: string) => { setDegreeInfo((prev) => ({ @@ -56,21 +47,13 @@ const SpecialisationStep = ({ })); }; - const fetchAllSpecialisations = useCallback(async () => { - try { - const res = await axios.get( - `/specialisations/getSpecialisations/${degreeInfo.programCode}/${type}` - ); - setOptions(res.data.spec); - } catch (e) { - // eslint-disable-next-line no-console - console.error('Error at getSteps', e); - } - }, [degreeInfo.programCode, type]); - - useEffect(() => { - if (degreeInfo.programCode) fetchAllSpecialisations(); - }, [fetchAllSpecialisations, degreeInfo.programCode, type]); + const specsQuery = useQuery({ + queryKey: ['specialisations', degreeInfo.programCode, type], + queryFn: () => getSpecialisationsForProgram(degreeInfo.programCode, type), + select: (data) => data.spec, + enabled: degreeInfo.programCode !== '' + }); + const options = specsQuery.data; const menuItems: MenuProps['items'] = options ? Object.keys(options).map((program, index) => ({ diff --git a/frontend/src/pages/Login/Login.tsx b/frontend/src/pages/Login/Login.tsx index fae9c8584..ba1fd0622 100644 --- a/frontend/src/pages/Login/Login.tsx +++ b/frontend/src/pages/Login/Login.tsx @@ -1,8 +1,7 @@ import React, { useCallback } from 'react'; import { Link } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; -import { guestLogin as guestLoginRequest } from 'utils/api/auth'; -import { initiateCSEAuth } from 'utils/api/userApi'; +import { guestLogin as guestLoginRequest, initiateCSEAuth } from 'utils/api/authApi'; import BackButton from 'assets/back.svg'; import SplashArt from 'assets/splashart.svg'; import PageTemplate from 'components/PageTemplate'; diff --git a/frontend/src/pages/LoginSuccess/LoginSuccess.tsx b/frontend/src/pages/LoginSuccess/LoginSuccess.tsx index 270bc641e..f3a5bd110 100644 --- a/frontend/src/pages/LoginSuccess/LoginSuccess.tsx +++ b/frontend/src/pages/LoginSuccess/LoginSuccess.tsx @@ -2,7 +2,7 @@ import React, { useEffect } from 'react'; import { useDispatch } from 'react-redux'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; -import { CSELogin } from 'utils/api/auth'; +import { CSELogin } from 'utils/api/authApi'; import { getUserIsSetup } from 'utils/api/userApi'; import PageLoading from 'components/PageLoading'; import { unsetIdentity, updateIdentityWithAPIRes } from 'reducers/identitySlice'; diff --git a/frontend/src/pages/Logout/Logout.tsx b/frontend/src/pages/Logout/Logout.tsx index b0e703ba1..54f81f50a 100644 --- a/frontend/src/pages/Logout/Logout.tsx +++ b/frontend/src/pages/Logout/Logout.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; -import { logout } from 'utils/api/auth'; +import { logout } from 'utils/api/authApi'; import PageLoading from 'components/PageLoading'; import { useAppDispatch, useAppSelector } from 'hooks'; import { selectToken, unsetIdentity } from 'reducers/identitySlice'; diff --git a/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx b/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx index b6fc4585a..ee0944f71 100644 --- a/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx +++ b/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx @@ -6,7 +6,7 @@ import { useQuery } from '@tanstack/react-query'; import { Button, Typography } from 'antd'; import { ProgramStructure } from 'types/structure'; import { badCourses, badDegree } from 'types/userResponse'; -import { fetchAllDegrees } from 'utils/api/programApi'; +import { fetchAllDegrees } from 'utils/api/programsApi'; import { getUserCourses, getUserDegree } from 'utils/api/userApi'; import getNumTerms from 'utils/getNumTerms'; import LiquidProgressChart from 'components/LiquidProgressChart'; diff --git a/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx b/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx index ec37d8513..3ded9f2a3 100644 --- a/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx +++ b/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx @@ -15,7 +15,7 @@ import { ViewSubgroupCourse } from 'types/progressionViews'; import { ProgramStructure } from 'types/structure'; -import { badCourses, badDegree, badPlanner } from 'types/userResponse'; +import { badCourses, badPlanner } from 'types/userResponse'; import { getProgramStructure } from 'utils/api/programsApi'; import { getUserCourses, getUserDegree, getUserPlanner } from 'utils/api/userApi'; import getNumTerms from 'utils/getNumTerms'; @@ -33,40 +33,28 @@ import TableView from './TableView'; const { Title } = Typography; const ProgressionChecker = () => { - const [isLoading, setIsLoading] = useState(true); - const [structure, setStructure] = useState({}); - const [uoc, setUoc] = useState(0); const token = useToken(); - const degreeQuery = useQuery({ - queryKey: ['degree'], - queryFn: () => getUserDegree(token) - }); - const degree = degreeQuery.data || badDegree; - const { programCode, specs } = degree; - const plannerQuery = useQuery({ queryKey: ['planner'], queryFn: () => getUserPlanner(token) }); - const planner = plannerQuery.data || badPlanner; + const planner = plannerQuery.data ?? badPlanner; const { unplanned } = planner; - useEffect(() => { - // get structure of degree - const fetchStructure = async () => { - try { - const res = await getProgramStructure(programCode, specs); - setStructure(res.structure); - setUoc(res.uoc); - } catch (err) { - // eslint-disable-next-line no-console - console.error('Error at fetchStructure', err); - } - setIsLoading(false); - }; - if (programCode && specs.length > 0) fetchStructure(); - }, [programCode, specs]); + const degreeQuery = useQuery({ + queryKey: ['degree'], + queryFn: () => getUserDegree(token) + }); + const degree = degreeQuery.data; + + const structureQuery = useQuery({ + queryKey: ['structure', degree?.programCode, degree?.specs], + queryFn: () => getProgramStructure(degree!.programCode, degree!.specs), + enabled: degree !== undefined + }); + const structure: ProgramStructure = structureQuery.data?.structure ?? {}; + const uoc = structureQuery.data?.uoc ?? 0; useEffect(() => { openNotification({ @@ -82,7 +70,7 @@ const ProgressionChecker = () => { queryKey: ['courses'], queryFn: () => getUserCourses(token) }); - const courses = coursesQuery.data || badCourses; + const courses = coursesQuery.data ?? badCourses; const countedCourses: string[] = []; const newViewLayout: ProgressionViewStructure = {}; @@ -139,7 +127,7 @@ const ProgressionChecker = () => { const course: ViewSubgroupCourse = { courseCode, title: subgroupStructure.courses[courseCode], - UOC: courses[courseCode]?.uoc || 0, + UOC: courses[courseCode]?.uoc ?? 0, plannedFor: courses[courseCode]?.plannedFor ?? '', isUnplanned: unplanned.includes(courseCode), isMultiterm: !!courses[courseCode]?.isMultiterm, @@ -232,7 +220,7 @@ const ProgressionChecker = () => { acc + curr.UOC, 0)} diff --git a/frontend/src/pages/TermPlanner/ImportPlannerMenu/ImportPlannerMenu.tsx b/frontend/src/pages/TermPlanner/ImportPlannerMenu/ImportPlannerMenu.tsx index ab82ad006..b4cf5aa98 100644 --- a/frontend/src/pages/TermPlanner/ImportPlannerMenu/ImportPlannerMenu.tsx +++ b/frontend/src/pages/TermPlanner/ImportPlannerMenu/ImportPlannerMenu.tsx @@ -2,12 +2,16 @@ import React, { useRef, useState } from 'react'; import { LoadingOutlined } from '@ant-design/icons'; import { useQuery } from '@tanstack/react-query'; import { Spin } from 'antd'; -import axios from 'axios'; -import { Course } from 'types/api'; -import { JSONPlanner, Term, UnPlannedToTerm } from 'types/planner'; +import { JSONPlanner, Term } from 'types/planner'; import { badPlanner } from 'types/userResponse'; -import { withAuthorization } from 'utils/api/auth'; -import { getUserPlanner } from 'utils/api/userApi'; +import { getCourseInfo } from 'utils/api/coursesApi'; +import { addToUnplanned, setUnplannedCourseToTerm } from 'utils/api/plannerApi'; +import { + getUserPlanner, + toggleSummerTerm, + updateDegreeLength, + updateStartYear +} from 'utils/api/userApi'; import openNotification from 'utils/openNotification'; import useToken from 'hooks/useToken'; import CS from '../common/styles'; @@ -23,34 +27,10 @@ const ImportPlannerMenu = () => { const inputRef = useRef(null); const [loading, setLoading] = useState(false); - const handleSetUnplannedCourseToTerm = async (data: UnPlannedToTerm) => { - try { - await axios.post('planner/unPlannedToTerm', data, { - headers: withAuthorization(token) - }); - } catch (err) { - // eslint-disable-next-line no-console - console.error(`Error at handleSetUnplannedCourseToTerm:`, err); - } - }; - const upload = () => { inputRef.current?.click(); }; - const handleAddToUnplanned = async (code: string) => { - try { - await axios.post( - 'planner/addToUnplanned', - { courseCode: code }, - { headers: withAuthorization(token) } - ); - } catch (err) { - // eslint-disable-next-line no-console - console.error('Error at handleAddToUnplanned: ', err); - } - }; - const uploadedJSONFile = async (e: React.ChangeEvent) => { if (e.target.files === null) { return; @@ -100,16 +80,8 @@ const ImportPlannerMenu = () => { return; } try { - await axios.put( - '/user/updateDegreeLength', - { numYears: fileInJson.numYears }, - { headers: withAuthorization(token) } - ); - await axios.put( - '/user/updateStartYear', - { startYear: fileInJson.startYear }, - { headers: withAuthorization(token) } - ); + await updateDegreeLength(token, fileInJson.numYears); + await updateStartYear(token, fileInJson.startYear.toString()); } catch { openNotification({ type: 'error', @@ -120,7 +92,7 @@ const ImportPlannerMenu = () => { } if (planner.isSummerEnabled !== fileInJson.isSummerEnabled) { try { - await axios.post('/user/toggleSummerTerm', {}, { headers: withAuthorization(token) }); + await toggleSummerTerm(token); } catch { openNotification({ type: 'error', @@ -133,10 +105,10 @@ const ImportPlannerMenu = () => { fileInJson.years.forEach((year, yearIndex) => { Object.entries(year).forEach(([term, termCourses]) => { termCourses.forEach(async (code, index) => { - const { data: course } = await axios.get(`/courses/getCourse/${code}`); + const course = await getCourseInfo(code); if (plannedCourses.indexOf(course.code) === -1) { plannedCourses.push(course.code); - handleAddToUnplanned(course.code); + addToUnplanned(token, course.code); const destYear = Number(yearIndex) + Number(planner.startYear); const destTerm = term as Term; const destRow = destYear - planner.startYear; @@ -147,7 +119,7 @@ const ImportPlannerMenu = () => { destIndex, courseCode: code }; - handleSetUnplannedCourseToTerm(data); + setUnplannedCourseToTerm(token, data); } }); }); diff --git a/frontend/src/pages/TermPlanner/SettingsMenu/SettingsMenu.tsx b/frontend/src/pages/TermPlanner/SettingsMenu/SettingsMenu.tsx index 5615d53fa..6ccbe623f 100644 --- a/frontend/src/pages/TermPlanner/SettingsMenu/SettingsMenu.tsx +++ b/frontend/src/pages/TermPlanner/SettingsMenu/SettingsMenu.tsx @@ -3,10 +3,9 @@ import { useSelector } from 'react-redux'; import { CheckOutlined, CloseOutlined } from '@ant-design/icons'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { DatePicker, Modal, Select, Switch } from 'antd'; -import axios from 'axios'; import dayjs from 'dayjs'; import { PlannerResponse } from 'types/userResponse'; -import { withAuthorization } from 'utils/api/auth'; +import { toggleSummerTerm, updateDegreeLength, updateStartYear } from 'utils/api/userApi'; import openNotification from 'utils/openNotification'; import Spinner from 'components/Spinner'; import type { RootState } from 'config/store'; @@ -36,16 +35,8 @@ const SettingsMenu = ({ planner }: Props) => { return false; } - async function updateStartYear(year: string) { - await axios.put( - '/user/updateStartYear', - { startYear: parseInt(year, 10) }, - { headers: withAuthorization(token) } - ); - } - const updateStartYearMutation = useMutation({ - mutationFn: updateStartYear, + mutationFn: (year: string) => updateStartYear(token, year), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['planner'] @@ -69,16 +60,8 @@ const SettingsMenu = ({ planner }: Props) => { } }; - async function updateDegreeLength(numYears: number) { - await axios.put( - '/user/updateDegreeLength', - { numYears }, - { headers: withAuthorization(token) } - ); - } - const updateDegreeLengthMutation = useMutation({ - mutationFn: updateDegreeLength, + mutationFn: (numYears: number) => updateDegreeLength(token, numYears), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['planner'] @@ -106,37 +89,30 @@ const SettingsMenu = ({ planner }: Props) => { } }; - async function summerToggle() { - try { - await axios.post('/user/toggleSummerTerm', {}, { headers: withAuthorization(token) }); - } catch { - openNotification({ - type: 'error', - message: 'Error setting summer term', - description: 'An error occurred when toggling the summer term.' - }); - return; - } - if (planner && planner.isSummerEnabled) { - openNotification({ - type: 'info', - message: 'Your summer term courses have been unplanned', - description: - 'Courses that were planned during summer terms have been unplanned including courses that have been planned across different terms.' - }); - } - } - const summerToggleMutation = useMutation({ - mutationFn: summerToggle, + mutationFn: () => toggleSummerTerm(token), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['planner'] }); + + if (planner && planner.isSummerEnabled) { + openNotification({ + type: 'info', + message: 'Your summer term courses have been unplanned', + description: + 'Courses that were planned during summer terms have been unplanned including courses that have been planned across different terms.' + }); + } }, onError: (err) => { // eslint-disable-next-line no-console console.error('Error at summerToggleMutationMutation: ', err); + openNotification({ + type: 'error', + message: 'Error setting summer term', + description: 'An error occurred when toggling the summer term.' + }); } }); diff --git a/frontend/src/pages/TermPlanner/TermBox/TermBox.tsx b/frontend/src/pages/TermPlanner/TermBox/TermBox.tsx index 77ed0def9..6b9bd45be 100644 --- a/frontend/src/pages/TermPlanner/TermBox/TermBox.tsx +++ b/frontend/src/pages/TermPlanner/TermBox/TermBox.tsx @@ -2,13 +2,12 @@ import React, { Suspense } from 'react'; import { LockFilled, UnlockFilled } from '@ant-design/icons'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { Badge } from 'antd'; -import axios from 'axios'; import { useTheme } from 'styled-components'; import { Course } from 'types/api'; import { CourseTime } from 'types/courses'; import { Term } from 'types/planner'; import { ValidateResponse } from 'types/userResponse'; -import { withAuthorization } from 'utils/api/auth'; +import { toggleLockTerm } from 'utils/api/plannerApi'; import { getUserCourses, getUserPlanner } from 'utils/api/userApi'; import { courseHasOffering } from 'utils/getAllCourseOfferings'; import Spinner from 'components/Spinner'; @@ -44,19 +43,13 @@ const TermBox = ({ const theme = useTheme(); const queryClient = useQueryClient(); - const toggleLockTerm = async () => { - await axios.post( - '/planner/toggleTermLocked', - {}, - { params: { termyear: `${year}${term}` }, headers: withAuthorization(token) } - ); - }; const plannerQuery = useQuery({ queryKey: ['planner'], queryFn: () => getUserPlanner(token) }); + const toggleLockTermMutation = useMutation({ - mutationFn: toggleLockTerm, + mutationFn: () => toggleLockTerm(token, year, term), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['planner'] @@ -67,6 +60,7 @@ const TermBox = ({ console.error('Error at toggleLockTermMutation: ', err); } }); + const coursesQuery = useQuery({ queryKey: ['courses'], queryFn: () => getUserCourses(token) diff --git a/frontend/src/pages/TermPlanner/TermPlanner.tsx b/frontend/src/pages/TermPlanner/TermPlanner.tsx index 98612c514..dd7eb4143 100644 --- a/frontend/src/pages/TermPlanner/TermPlanner.tsx +++ b/frontend/src/pages/TermPlanner/TermPlanner.tsx @@ -13,7 +13,7 @@ import { PlannerResponse, ValidatesResponse } from 'types/userResponse'; -import { getCourseForYearsInfo } from 'utils/api/courseApi'; +import { getCourseForYearsInfo } from 'utils/api/coursesApi'; import { setPlannedCourseToTerm, setUnplannedCourseToTerm, diff --git a/frontend/src/pages/TermPlanner/ValidateCtfButton/ValidateCtfButton.tsx b/frontend/src/pages/TermPlanner/ValidateCtfButton/ValidateCtfButton.tsx index 7e1ce756b..a44b948ca 100644 --- a/frontend/src/pages/TermPlanner/ValidateCtfButton/ValidateCtfButton.tsx +++ b/frontend/src/pages/TermPlanner/ValidateCtfButton/ValidateCtfButton.tsx @@ -1,20 +1,11 @@ import React from 'react'; import { Typography } from 'antd'; -import axios from 'axios'; import styled from 'styled-components'; -import { withAuthorization } from 'utils/api/auth'; +import { CtfResult, validateCtf as validateCtfRequest } from 'utils/api/ctfApi'; import useToken from 'hooks/useToken'; import CS from '../common/styles'; import S from './styles'; -type CtfResult = { - valid: boolean; - failed: number; - passed: Array; - message: string; - flags: Array; -}; - const { Text, Title } = Typography; const TextBlock = styled(Text)` @@ -44,12 +35,8 @@ const ValidateCtfButton = () => { const validateCtf = async () => { setOpen(true); - const res = await axios.post( - '/ctf/validateCtf/', - {}, - { headers: withAuthorization(token) } - ); - setResult(res.data); + const res = await validateCtfRequest(token); + setResult(res); }; return ( diff --git a/frontend/src/reducers/identitySlice.ts b/frontend/src/reducers/identitySlice.ts index f03ef3e9b..25962144b 100644 --- a/frontend/src/reducers/identitySlice.ts +++ b/frontend/src/reducers/identitySlice.ts @@ -1,6 +1,6 @@ import type { PayloadAction } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit'; -import { IdentityResponse } from 'utils/api/auth'; +import { IdentityResponse } from 'utils/api/authApi'; type CompleteIdentity = { token: string; diff --git a/frontend/src/utils/api/auth.ts b/frontend/src/utils/api/authApi.ts similarity index 84% rename from frontend/src/utils/api/auth.ts rename to frontend/src/utils/api/authApi.ts index 138755bec..7471f4c71 100644 --- a/frontend/src/utils/api/auth.ts +++ b/frontend/src/utils/api/authApi.ts @@ -14,6 +14,13 @@ export const withAuthorization = (token: string) => { return { Authorization: `Bearer ${token}` }; }; +export const initiateCSEAuth = async (): Promise => { + // Login to redirect link + await axios.get('/auth/authorization_url', { withCredentials: true }).then((res) => { + window.location.href = res.data; + }); +}; + export const guestLogin = async (): Promise => { const res = await axios.post( '/auth/guest_login', diff --git a/frontend/src/utils/api/courseApi.ts b/frontend/src/utils/api/courseApi.ts deleted file mode 100644 index 728b51f31..000000000 --- a/frontend/src/utils/api/courseApi.ts +++ /dev/null @@ -1,37 +0,0 @@ -import axios from 'axios'; -import { Course, SearchCourse } from 'types/api'; -import { LIVE_YEAR } from 'config/constants'; -import { withAuthorization } from './auth'; - -// TODO: Should error handling be done here? -export const searchCourse = async (token: string, query: string): Promise => { - const res = await axios.post( - `/courses/searchCourse/${query}`, - {}, - { headers: withAuthorization(token) } - ); - return res.data as SearchCourse; -}; - -export const getCourseInfo = async (courseId: string) => { - const res = await axios.get(`courses/getCourse/${courseId}`); - return res.data; -}; - -export const getCourseForYearsInfo = async ( - courseId: string, - years: number[] -): Promise> => { - const promises = await Promise.allSettled( - years.map((year) => axios.get(`courses/getLegacyCourse/${year}/${courseId}`)) - ); - const legacy: Record = {}; - promises.forEach((promise, index) => { - if (promise.status === 'fulfilled') { - legacy[years[index]] = promise.value.data; - } - }); - const current = (await axios.get(`courses/getCourse/${courseId}`)).data; - legacy[LIVE_YEAR] = current; - return legacy; -}; diff --git a/frontend/src/utils/api/coursesApi.ts b/frontend/src/utils/api/coursesApi.ts index 700517dc8..138f2e598 100644 --- a/frontend/src/utils/api/coursesApi.ts +++ b/frontend/src/utils/api/coursesApi.ts @@ -1,30 +1,60 @@ import axios from 'axios'; -import { Course, CoursesAllUnlocked } from 'types/api'; +import { + Course, + CourseChildren, + CoursePathFrom, + CoursesAllUnlocked, + CoursesUnlockedWhenTaken, + SearchCourse +} from 'types/api'; import { CoursesResponse, DegreeResponse, PlannerResponse } from 'types/userResponse'; import prepareUserPayload from 'utils/prepareUserPayload'; -import { getUserPlanner } from './userApi'; - -export const getCoursesInfo = async (token: string): Promise> => { - const courses: Record = {}; - const planner = await getUserPlanner(token); - - planner.years - .flatMap((x) => Object.values(x)) - .flat() - .concat(planner.unplanned) - .forEach(async (id) => { - const res = await axios.get(`courses/getCourse/${id}`); - courses[id] = res.data; - }); - - return courses; +import { LIVE_YEAR } from 'config/constants'; +import { withAuthorization } from './authApi'; + +// TODO: Should error handling be done here? +export const searchCourse = async (token: string, query: string): Promise => { + const res = await axios.post( + `/courses/searchCourse/${query}`, + {}, + { headers: withAuthorization(token) } + ); + return res.data as SearchCourse; +}; + +export const getCourseInfo = async (courseCode: string) => { + const res = await axios.get(`courses/getCourse/${courseCode}`); + return res.data; +}; + +export const getCoursePrereqs = async (courseCode: string) => { + const res = await axios.get(`/courses/getPathFrom/${courseCode}`); + return res.data; +}; + +export const getCourseChildren = async (courseCode: string) => { + const res = await axios.get(`/courses/courseChildren/${courseCode}`); + return res.data; +}; + +export const getCoursesUnlockedWhenTaken = async ( + degree: DegreeResponse, + planner: PlannerResponse, + courses: CoursesResponse, + courseCode: string +) => { + const res = await axios.post( + `/courses/coursesUnlockedWhenTaken/${courseCode}`, + JSON.stringify(prepareUserPayload(degree, planner, courses)) + ); + return res.data; }; export const getAllUnlockedCourses = async ( degree: DegreeResponse, planner: PlannerResponse, courses: CoursesResponse -): Promise => { +) => { const res = await axios.post( '/courses/getAllUnlocked/', JSON.stringify(prepareUserPayload(degree, planner, courses)) @@ -32,3 +62,21 @@ export const getAllUnlockedCourses = async ( return res.data; }; + +export const getCourseForYearsInfo = async ( + courseCode: string, + years: number[] +): Promise> => { + const promises = await Promise.allSettled( + years.map((year) => axios.get(`courses/getLegacyCourse/${year}/${courseCode}`)) + ); + const legacy: Record = {}; + promises.forEach((promise, index) => { + if (promise.status === 'fulfilled') { + legacy[years[index]] = promise.value.data; + } + }); + const current = await getCourseInfo(courseCode); + legacy[LIVE_YEAR] = current; + return legacy; +}; diff --git a/frontend/src/utils/api/ctfApi.ts b/frontend/src/utils/api/ctfApi.ts new file mode 100644 index 000000000..a0697325e --- /dev/null +++ b/frontend/src/utils/api/ctfApi.ts @@ -0,0 +1,21 @@ +import axios from 'axios'; +import { withAuthorization } from './authApi'; + +export type CtfResult = { + valid: boolean; + failed: number; + passed: Array; + message: string; + flags: Array; +}; + +// lol +export const validateCtf = async (token: string): Promise => { + const res = await axios.post( + '/ctf/validateCtf/', + {}, + { headers: withAuthorization(token) } + ); + + return res.data; +}; diff --git a/frontend/src/utils/api/degreeApi.ts b/frontend/src/utils/api/degreeApi.ts index 15aef2af6..b2d66b50c 100644 --- a/frontend/src/utils/api/degreeApi.ts +++ b/frontend/src/utils/api/degreeApi.ts @@ -1,7 +1,6 @@ import axios from 'axios'; -import { Programs } from 'types/api'; import { DegreeWizardPayload } from 'types/degreeWizard'; -import { withAuthorization } from './auth'; +import { withAuthorization } from './authApi'; export const setupDegreeWizard = async (token: string, wizard: DegreeWizardPayload) => { try { @@ -13,8 +12,3 @@ export const setupDegreeWizard = async (token: string, wizard: DegreeWizardPaylo throw err; } }; - -export const getAllDegrees = async (): Promise> => { - const res = await axios.get('/programs/getPrograms'); - return res.data.programs; -}; diff --git a/frontend/src/utils/api/plannerApi.ts b/frontend/src/utils/api/plannerApi.ts index 20b5fab72..94d3eba03 100644 --- a/frontend/src/utils/api/plannerApi.ts +++ b/frontend/src/utils/api/plannerApi.ts @@ -2,7 +2,7 @@ import axios from 'axios'; import { CourseMark } from 'types/api'; import { PlannedToTerm, UnPlannedToTerm, UnscheduleCourse } from 'types/planner'; import { ValidatesResponse } from 'types/userResponse'; -import { withAuthorization } from './auth'; +import { withAuthorization } from './authApi'; export const addToUnplanned = async (token: string, courseId: string) => { try { @@ -100,3 +100,11 @@ export const updateCourseMark = async (token: string, courseMark: CourseMark) => console.error(e); } }; + +export const toggleLockTerm = async (token: string, year: string, term: string) => { + await axios.post( + '/planner/toggleTermLocked', + {}, + { params: { termyear: `${year}${term}` }, headers: withAuthorization(token) } + ); +}; diff --git a/frontend/src/utils/api/programApi.ts b/frontend/src/utils/api/programApi.ts deleted file mode 100644 index 77bf27340..000000000 --- a/frontend/src/utils/api/programApi.ts +++ /dev/null @@ -1,7 +0,0 @@ -import axios from 'axios'; -import { Programs } from 'types/api'; - -export const fetchAllDegrees = async (): Promise => { - const res = await axios.get('/programs/getPrograms'); - return res.data as Programs; -}; diff --git a/frontend/src/utils/api/programsApi.ts b/frontend/src/utils/api/programsApi.ts index 9e5b2333c..abe6010c1 100644 --- a/frontend/src/utils/api/programsApi.ts +++ b/frontend/src/utils/api/programsApi.ts @@ -1,5 +1,5 @@ import axios from 'axios'; -import { GraphPayload, Structure } from 'types/api'; +import { GraphPayload, Programs, Structure } from 'types/api'; export const getProgramGraph = async ( programCode: string, @@ -19,3 +19,8 @@ export const getProgramStructure = async ( return res.data; }; + +export const fetchAllDegrees = async (): Promise => { + const res = await axios.get('/programs/getPrograms'); + return res.data; +}; diff --git a/frontend/src/utils/api/specsApi.ts b/frontend/src/utils/api/specsApi.ts new file mode 100644 index 000000000..6a86b0637 --- /dev/null +++ b/frontend/src/utils/api/specsApi.ts @@ -0,0 +1,21 @@ +import axios from 'axios'; +import { Specialisations, SpecialisationTypes } from 'types/api'; + +export const getSpecialisationsForProgram = async ( + programCode: string, + specType: string +): Promise => { + const res = await axios.get( + `/specialisations/getSpecialisations/${programCode}/${specType}` + ); + + return res.data; +}; + +export const getSpecialisationTypes = async (programCode: string): Promise => { + const res = await axios.get( + `/specialisations/getSpecialisationTypes/${programCode}` + ); + + return res.data; +}; diff --git a/frontend/src/utils/api/timetableApi.ts b/frontend/src/utils/api/timetableApi.ts new file mode 100644 index 000000000..1f4e6b387 --- /dev/null +++ b/frontend/src/utils/api/timetableApi.ts @@ -0,0 +1,8 @@ +import axios from 'axios'; +import { CourseTimetable } from 'types/courseCapacity'; +import { TIMETABLE_API_URL } from 'config/constants'; + +export const getCourseTimetable = async (courseCode: string) => { + const res = await axios.get(`${TIMETABLE_API_URL}/${courseCode}`); + return res.data; +}; diff --git a/frontend/src/utils/api/userApi.ts b/frontend/src/utils/api/userApi.ts index 637024cb3..6e44237a4 100644 --- a/frontend/src/utils/api/userApi.ts +++ b/frontend/src/utils/api/userApi.ts @@ -1,13 +1,6 @@ import axios from 'axios'; import { CoursesResponse, DegreeResponse, PlannerResponse, UserResponse } from 'types/userResponse'; -import { withAuthorization } from './auth'; - -export const initiateCSEAuth = async (): Promise => { - // Login to redirect link - await axios.get('/auth/authorization_url', { withCredentials: true }).then((res) => { - window.location.href = res.data; - }); -}; +import { withAuthorization } from './authApi'; export const getUser = async (token: string): Promise => { const user = await axios.get(`user/data/all`, { headers: withAuthorization(token) }); @@ -44,3 +37,19 @@ export const getUserCourses = async (token: string): Promise => export const resetUserDegree = async (token: string): Promise => { await axios.post(`user/reset`, {}, { headers: withAuthorization(token) }); }; + +export const updateDegreeLength = async (token: string, numYears: number): Promise => { + await axios.put('/user/updateDegreeLength', { numYears }, { headers: withAuthorization(token) }); +}; + +export const toggleSummerTerm = async (token: string): Promise => { + await axios.post('/user/toggleSummerTerm', {}, { headers: withAuthorization(token) }); +}; + +export const updateStartYear = async (token: string, year: string): Promise => { + await axios.put( + '/user/updateStartYear', + { startYear: parseInt(year, 10) }, + { headers: withAuthorization(token) } + ); +}; diff --git a/frontend/src/utils/queryUtils.ts b/frontend/src/utils/queryUtils.ts index ca57bf0e5..769d2be3f 100644 --- a/frontend/src/utils/queryUtils.ts +++ b/frontend/src/utils/queryUtils.ts @@ -7,3 +7,12 @@ export const unwrapQuery = (data: T | undefined): T => { } return data; }; + +export const unwrapSettledPromise = (res: PromiseSettledResult): T | undefined => { + if (res.status === 'rejected') { + // eslint-disable-next-line no-console + console.error('Rejected request at unwrap', res.reason); + return undefined; + } + return res.value; +};