diff --git a/client/src/App.tsx b/client/src/App.tsx index c84dcd7e4..5e1632e8c 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -183,7 +183,7 @@ const App: React.FC = () => { * Retrieves term data from the scraper backend */ const fetchTermData = async () => { - const { term, termName, year, firstDayOfTerm, termsData } = await getAvailableTermDetails(); + const { term, termName, year, firstDayOfTerm, termsData } = await getAvailableTermDetails(); setTerm(term); setTermName(termName); setYear(year); @@ -219,6 +219,7 @@ const App: React.FC = () => { const fetchCoursesList = async () => { const { courses } = await getCoursesList(term.substring(0, 2)); setCoursesList(courses); + }; if (year !== invalidYearFormat) fetchReliably(fetchCoursesList); diff --git a/client/src/components/controls/TermSelect.tsx b/client/src/components/controls/TermSelect.tsx index f0e8a97dd..69e2d864c 100644 --- a/client/src/components/controls/TermSelect.tsx +++ b/client/src/components/controls/TermSelect.tsx @@ -8,13 +8,14 @@ import { useTheme, } from '@mui/material'; import { styled } from '@mui/system'; -import React, { useContext, useState } from 'react'; +import { useContext, useState, useEffect } from 'react'; import { ThemeType } from '../../constants/theme'; import { convertToTermName } from '../../constants/timetable'; import { AppContext } from '../../context/AppContext'; import { CourseContext } from '../../context/CourseContext'; import { Term } from '../../interfaces/Periods'; +import { getTermsWithClassData } from '../../utils/getTermsWithClassData'; const StyledInputLabel = styled(InputLabel)(({ theme }) => ({ color: theme.palette.primary.main, @@ -86,16 +87,26 @@ const TermSelect: React.FC = () => { const termValue = e.target.value; const termInfo = termValue.split(', '); + let termPrefix = ''; if (termInfo[0].includes('Summer')) { termPrefix = 'U1'; } else { termPrefix = 'T' + termInfo[0].split(' ')[1]; } - + + const newYear = termInfo[1]; const termName = (termPrefix + newYear) as Term; // To get a string like T12024 + + localStorage.setItem( + 'currSelectedTerm', + JSON.stringify({ + term: termName, + }), + ); + setTerm(termName); setYear(newYear); setTermName(convertToTermName(termName!)); @@ -114,6 +125,23 @@ const TermSelect: React.FC = () => { const handleOpen = () => { setOpen(true); }; + + // Set up a list of terms with class data + const [termsWithClassData, setTermsWithClassData] = useState([]); + + useEffect(() => { + const loadTermsWithClassData = async () => { + const result = await getTermsWithClassData(termsData); + setTermsWithClassData(result); + }; + + if (termsData.length > 0) { + loadTermsWithClassData(); + } + }, [termsData]); // refresh list of terms if the year changes + + + const keysTerm = ["U1", "T1", "T2", "T3"].map(t => `${t}${year}`); return ( Select term @@ -129,11 +157,13 @@ const TermSelect: React.FC = () => { onChange={selectTerm} > {Array.from(termDataStrList).map((term, index) => { - return ( - - {term} - - ); + const isAvailable = termsWithClassData.includes(keysTerm[index]) + return ( + + {term} + + ) + })} diff --git a/client/src/constants/timetable.ts b/client/src/constants/timetable.ts index 72009fee1..7443621cc 100644 --- a/client/src/constants/timetable.ts +++ b/client/src/constants/timetable.ts @@ -1,6 +1,6 @@ import { gql } from '@apollo/client'; import { isWithinInterval, parse } from 'date-fns'; - +import { getTermsWithClassData } from '../utils/getTermsWithClassData'; import { client } from '../api/config'; import NetworkError from '../interfaces/NetworkError'; import { Term, TermDataList } from '../interfaces/Periods'; @@ -71,10 +71,21 @@ const get_current_term = async ( todaysDate: Date = new Date(), ): Promise => { const keys_term = sortTerms(Array.from(termInfoMap.keys())); + if (localStorage.getItem('currSelectedTerm')) { + const storedTerm = localStorage.getItem('currSelectedTerm'); + + if (storedTerm) { + const currSelectedTerm = JSON.parse(storedTerm) + console.log("localstorage" + currSelectedTerm.term) + return(currSelectedTerm.term) + } + } for (let currTermIndex = 0; currTermIndex < keys_term.length; currTermIndex++) { if (!keys_term[currTermIndex]) continue; const currTermVal = termInfoMap.get(keys_term[currTermIndex])!; + if (isWithinInterval(todaysDate, { start: currTermVal.startDate, end: currTermVal.endDate })) { + console.log("thist ype of data" + keys_term[currTermIndex]) return keys_term[currTermIndex]; } @@ -85,6 +96,7 @@ const get_current_term = async ( end: currTermVal.startDate, }) ) { + console.log("thist ype of data ", keys_term[currTermIndex]) return keys_term[currTermIndex]; } } @@ -101,6 +113,38 @@ export const convertToTermName = (termId: string) => { } }; +const getTermToDisplay = async ( + termInfoMap: Map, + +): Promise => { + + // (CURRENTLY BUGGED) Prioritise last selected term + if (localStorage.getItem('currSelectedTerm')) { + const storedTerm = localStorage.getItem('currSelectedTerm'); + + if (storedTerm) { + const currSelectedTerm = JSON.parse(storedTerm) + return currSelectedTerm.term + + } + + } + + // (TODO) If no selected term, display the latest term the user has timetable data for + + // If no class data, display the latest term with timetable data + + const keys_term = sortTerms(Array.from(termInfoMap.keys())); + const termsWithData = await getTermsWithClassData(keys_term) + + if (termsWithData && termsWithData.length > 0) { + return termsWithData[termsWithData.length - 1] + } + + // Default to T1 in case there are 0 terms with course data + return keys_term[1] +}; + /** * @returns The details of the latest term there is data for */ @@ -123,7 +167,7 @@ export const getAvailableTermDetails = async () => { try { const termMapInfo = await constructTermDetailsMap(); - const currTermId = await get_current_term(termMapInfo); + const currTermId = await getTermToDisplay(termMapInfo); firstDayOfTerm = termMapInfo.get(currTermId)?.startDate?.toLocaleDateString().split('/').reverse().join('-') || 'default-date'; @@ -237,4 +281,4 @@ export const weekdaysShort = ['Mo', 'Tu', 'We', 'Th', 'Fr']; export const unknownErrorMessage = 'An unknown error has occurred, please hard refresh the page'; -export const invalidYearFormat = '0000'; +export const invalidYearFormat = '0000'; \ No newline at end of file diff --git a/client/src/context/UserContext.tsx b/client/src/context/UserContext.tsx index 7710bc0de..58b41917a 100644 --- a/client/src/context/UserContext.tsx +++ b/client/src/context/UserContext.tsx @@ -152,6 +152,7 @@ const UserContextProvider = ({ children }: UserContextProviderProps) => { throw new NetworkError("Couldn't get response for user information!"); } } catch (error) { + setUser(undefinedUser); console.log(error); } }; diff --git a/client/src/utils/getTermsWithClassData.ts b/client/src/utils/getTermsWithClassData.ts new file mode 100644 index 000000000..8aeaf34cc --- /dev/null +++ b/client/src/utils/getTermsWithClassData.ts @@ -0,0 +1,67 @@ +import { gql } from '@apollo/client'; +import { client } from '../api/config'; + +const GET_CLASSES_WITH_TIMES = gql` + query MyQuery($term: String!) { + times_aggregate( + where: { + classe: { + course: { course_code: { _eq: "COMP2521" } }, + term: { _eq: $term } + } + } + limit: 1 + ) { + aggregate { + count + } + } + } +`; + + +/** + * Checks if a given term has any classes with timetable data + * + * @param term The term we want to check (e.g. "T1") + * @returns True if the term has class data, else False + * @example + * bool exists = await existsClassDataInTerm("T1") + */ + +const existsClassDataInTerm = async (term: string): Promise => { + try { + const { data } = await client.query({ + query: GET_CLASSES_WITH_TIMES, + variables: { term }, + }); + + return data?.times_aggregate?.aggregate?.count > 0; + } catch (error) { + console.error('Error fetching class data:', error); + return false; + } +}; + + +/** + * Returns a list of terms with class data, with the terms ordered by + * their appearance in the year - [U1, T1, T2, T3] + * + * @returns List of terms that have class data + * @example + * const termsWithData = await getTermsWithClassData(); + */ +export const getTermsWithClassData = async (terms: string[]): Promise => { + + const termsWithData: string[] = []; + + for (const term of terms) { + const hasClassData = await existsClassDataInTerm(term.substring(0, 2)); + if (hasClassData) { + termsWithData.push(term); + } + } + + return termsWithData; +}; \ No newline at end of file