From 7b057a7404039293f93baccc94806d2497608811 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 10 Oct 2025 07:13:21 +1100 Subject: [PATCH 01/16] WIP Rewrite frontend components to use React Query --- client/eslint.config.mjs | 5 +- client/src/App.tsx | 516 +---------- client/src/api/config.ts | 13 +- client/src/api/getAutoTimetable.ts | 27 - client/src/api/getCourseInfo.ts | 184 ---- client/src/api/getCoursesList.ts | 59 -- client/src/api/times/times.ts | 175 ++++ client/src/api/timetable/mutations.ts | 38 + client/src/api/timetable/queries.ts | 27 + client/src/api/timetable/routes.ts | 52 ++ client/src/api/user/routes.ts | 12 +- client/src/components/Alerts.tsx | 82 -- client/src/components/EventShareModal.tsx | 178 ---- .../src/components/controls/CourseSelect.tsx | 530 ----------- client/src/components/controls/TermSelect.tsx | 133 --- client/src/components/planner/Planner.tsx | 66 ++ .../{ => planner}/controls/Autotimetabler.tsx | 0 .../{ => planner}/controls/ColorOptions.tsx | 0 .../{ => planner}/controls/ColorPicker.tsx | 0 .../{ => planner}/controls/Controls.tsx | 39 +- .../planner/controls/CourseSelect.tsx | 433 +++++++++ .../{ => planner}/controls/CustomEvent.tsx | 0 .../controls/CustomEventGeneral.tsx | 0 .../controls/CustomEventTutoring.tsx | 0 .../{ => planner}/controls/History.tsx | 0 .../planner/controls/TermSelect.tsx | 90 ++ .../controls/customEventLink.tsx | 0 .../planner/timetableTabs/TimetableTabs.tsx | 135 +++ .../components/promotions/PromotionPopup.tsx | 23 +- .../components/promotions/SubcomPromotion.tsx | 6 +- .../components/sidebar/AddFriendsButton.tsx | 7 +- .../src/components/sidebar/CollapseButton.tsx | 7 +- .../components/sidebar/ColorThemePreview.tsx | 5 +- .../components/sidebar/CustomModalOpener.tsx | 6 +- .../src/components/sidebar/DarkModeButton.tsx | 5 +- .../src/components/sidebar/FriendsButton.tsx | 8 +- client/src/components/sidebar/Sidebar.tsx | 33 +- client/src/components/sidebar/UserAccount.tsx | 13 +- .../src/components/sidebar/friends/Friend.tsx | 12 +- .../sidebar/friends/FriendsList.tsx | 20 +- client/src/components/sidebar/friends/User.ts | 16 - .../sidebar/friends/UserProfile.tsx | 17 +- .../timetable/CreateEventPopover.tsx | 213 ----- .../components/timetable/DiscardDialog.tsx | 56 -- .../components/timetable/DropdownOption.tsx | 63 -- .../src/components/timetable/DroppedCards.tsx | 190 ---- .../src/components/timetable/DroppedClass.tsx | 270 ------ .../src/components/timetable/DroppedEvent.tsx | 267 ------ client/src/components/timetable/Dropzone.tsx | 73 -- client/src/components/timetable/Dropzones.tsx | 108 --- .../components/timetable/EventContextMenu.tsx | 102 --- .../timetable/ExpandedClassView.tsx | 267 ------ .../timetable/ExpandedEventView.tsx | 470 ---------- .../components/timetable/LocationDropdown.tsx | 27 - .../components/timetable/PeriodMetadata.tsx | 80 -- client/src/components/timetable/Timetable.tsx | 69 -- .../components/timetable/TimetableLayout.tsx | 331 ------- .../timetableTabs/TimetableTabContextMenu.tsx | 480 ---------- .../timetableTabs/TimetableTabs.tsx | 231 ----- client/src/constants/defaults.ts | 25 - client/src/constants/timetable.ts | 241 ----- client/src/context/AppContext.tsx | 223 ----- client/src/context/CourseContext.tsx | 58 -- client/src/hooks/useColorDecoder.ts | 42 - client/src/hooks/useColorMapper.ts | 38 - client/src/hooks/useUpdateEffect.ts | 16 - client/src/index.tsx | 53 +- client/src/interfaces/Courses.ts | 24 - client/src/interfaces/Database.ts | 34 - client/src/interfaces/GraphQLCourseInfo.ts | 35 - client/src/interfaces/NetworkError.ts | 5 - client/src/interfaces/Periods.ts | 202 ---- client/src/interfaces/PropTypes.ts | 232 ----- client/src/interfaces/TimeoutError.ts | 5 - client/src/service-worker.ts | 79 -- client/src/serviceWorkerRegistration.ts | 139 --- client/src/styles/DroppedCardStyles.tsx | 167 ---- client/src/styles/TimetableTabStyles.tsx | 4 +- client/src/utils/DbCourse.ts | 255 ------ client/src/utils/Drag.ts | 863 ------------------ client/src/utils/areDuplicatePeriods.ts | 12 - client/src/utils/cardsContextMenu.ts | 73 -- client/src/utils/clashes.ts | 214 ----- client/src/utils/colors.ts | 37 + client/src/utils/convertTo24Hour.ts | 29 - client/src/utils/createEvent.ts | 76 -- client/src/utils/eventTimes.ts | 30 - client/src/utils/generateICS.ts | 108 --- client/src/utils/getAllPeriods.ts | 9 - client/src/utils/getClassCourse.ts | 30 - client/src/utils/graphQLCourseToDbCourse.ts | 48 - client/src/utils/migrations/colourTheme.ts | 53 -- .../src/utils/migrations/primaryTimetables.ts | 15 - client/src/utils/oklchCovert.ts | 14 - client/src/utils/storage.ts | 103 --- client/src/utils/timeoutPromise.ts | 27 - client/src/utils/timetableHelpers.ts | 170 ---- client/src/utils/translateCard.ts | 90 -- server/codegen.ts | 2 +- server/src/auth/auth.service.ts | 4 +- server/src/graphql/graphql.service.ts | 5 +- server/src/graphql/queries.ts | 2 +- server/src/timetable/timetable.controller.ts | 4 +- server/src/timetable/timetable.service.ts | 9 +- 104 files changed, 1217 insertions(+), 8956 deletions(-) delete mode 100644 client/src/api/getAutoTimetable.ts delete mode 100644 client/src/api/getCourseInfo.ts delete mode 100644 client/src/api/getCoursesList.ts create mode 100644 client/src/api/times/times.ts delete mode 100644 client/src/components/Alerts.tsx delete mode 100644 client/src/components/EventShareModal.tsx delete mode 100644 client/src/components/controls/CourseSelect.tsx delete mode 100644 client/src/components/controls/TermSelect.tsx create mode 100644 client/src/components/planner/Planner.tsx rename client/src/components/{ => planner}/controls/Autotimetabler.tsx (100%) rename client/src/components/{ => planner}/controls/ColorOptions.tsx (100%) rename client/src/components/{ => planner}/controls/ColorPicker.tsx (100%) rename client/src/components/{ => planner}/controls/Controls.tsx (64%) create mode 100644 client/src/components/planner/controls/CourseSelect.tsx rename client/src/components/{ => planner}/controls/CustomEvent.tsx (100%) rename client/src/components/{ => planner}/controls/CustomEventGeneral.tsx (100%) rename client/src/components/{ => planner}/controls/CustomEventTutoring.tsx (100%) rename client/src/components/{ => planner}/controls/History.tsx (100%) create mode 100644 client/src/components/planner/controls/TermSelect.tsx rename client/src/components/{ => planner}/controls/customEventLink.tsx (100%) create mode 100644 client/src/components/planner/timetableTabs/TimetableTabs.tsx delete mode 100644 client/src/components/sidebar/friends/User.ts delete mode 100644 client/src/components/timetable/CreateEventPopover.tsx delete mode 100644 client/src/components/timetable/DiscardDialog.tsx delete mode 100644 client/src/components/timetable/DropdownOption.tsx delete mode 100644 client/src/components/timetable/DroppedCards.tsx delete mode 100644 client/src/components/timetable/DroppedClass.tsx delete mode 100644 client/src/components/timetable/DroppedEvent.tsx delete mode 100644 client/src/components/timetable/Dropzone.tsx delete mode 100644 client/src/components/timetable/Dropzones.tsx delete mode 100644 client/src/components/timetable/EventContextMenu.tsx delete mode 100644 client/src/components/timetable/ExpandedClassView.tsx delete mode 100644 client/src/components/timetable/ExpandedEventView.tsx delete mode 100644 client/src/components/timetable/LocationDropdown.tsx delete mode 100644 client/src/components/timetable/PeriodMetadata.tsx delete mode 100644 client/src/components/timetable/Timetable.tsx delete mode 100644 client/src/components/timetable/TimetableLayout.tsx delete mode 100644 client/src/components/timetableTabs/TimetableTabContextMenu.tsx delete mode 100644 client/src/components/timetableTabs/TimetableTabs.tsx delete mode 100644 client/src/constants/defaults.ts delete mode 100644 client/src/constants/timetable.ts delete mode 100644 client/src/context/AppContext.tsx delete mode 100644 client/src/context/CourseContext.tsx delete mode 100644 client/src/hooks/useColorDecoder.ts delete mode 100644 client/src/hooks/useColorMapper.ts delete mode 100644 client/src/hooks/useUpdateEffect.ts delete mode 100644 client/src/interfaces/Courses.ts delete mode 100644 client/src/interfaces/Database.ts delete mode 100644 client/src/interfaces/GraphQLCourseInfo.ts delete mode 100644 client/src/interfaces/NetworkError.ts delete mode 100644 client/src/interfaces/Periods.ts delete mode 100644 client/src/interfaces/PropTypes.ts delete mode 100644 client/src/interfaces/TimeoutError.ts delete mode 100644 client/src/service-worker.ts delete mode 100644 client/src/serviceWorkerRegistration.ts delete mode 100644 client/src/styles/DroppedCardStyles.tsx delete mode 100644 client/src/utils/DbCourse.ts delete mode 100644 client/src/utils/Drag.ts delete mode 100644 client/src/utils/areDuplicatePeriods.ts delete mode 100644 client/src/utils/cardsContextMenu.ts delete mode 100644 client/src/utils/clashes.ts create mode 100644 client/src/utils/colors.ts delete mode 100644 client/src/utils/convertTo24Hour.ts delete mode 100644 client/src/utils/createEvent.ts delete mode 100644 client/src/utils/eventTimes.ts delete mode 100644 client/src/utils/generateICS.ts delete mode 100644 client/src/utils/getAllPeriods.ts delete mode 100644 client/src/utils/getClassCourse.ts delete mode 100644 client/src/utils/graphQLCourseToDbCourse.ts delete mode 100644 client/src/utils/migrations/colourTheme.ts delete mode 100644 client/src/utils/migrations/primaryTimetables.ts delete mode 100644 client/src/utils/oklchCovert.ts delete mode 100644 client/src/utils/storage.ts delete mode 100644 client/src/utils/timeoutPromise.ts delete mode 100644 client/src/utils/timetableHelpers.ts delete mode 100644 client/src/utils/translateCard.ts diff --git a/client/eslint.config.mjs b/client/eslint.config.mjs index 0960ee35d..fd5b0586e 100644 --- a/client/eslint.config.mjs +++ b/client/eslint.config.mjs @@ -12,8 +12,9 @@ import tseslint from 'typescript-eslint'; export default tseslint.config( eslint.configs.recommended, - tseslint.configs.strictTypeChecked, - tseslint.configs.stylisticTypeChecked, + tseslint.configs.recommended, + // tseslint.configs.strictTypeChecked, + // tseslint.configs.stylisticTypeChecked, react.configs.flat.recommended, react.configs.flat['jsx-runtime'], reactHooks.configs['recommended-latest'], diff --git a/client/src/App.tsx b/client/src/App.tsx index 41b60acfb..6cffe30a4 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,54 +1,19 @@ -import { Box, Button, GlobalStyles, ThemeProvider } from '@mui/material'; +import { Box, GlobalStyles, ThemeProvider } from '@mui/material'; import { styled, StyledEngineProvider } from '@mui/material/styles'; import { LocalizationProvider } from '@mui/x-date-pickers'; import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; import * as Sentry from '@sentry/react'; -import React, { useContext, useEffect, useMemo } from 'react'; -import { Outlet } from 'react-router'; +import React, { useMemo } from 'react'; -import getCourseInfo from './api/getCourseInfo'; -import getCoursesList from './api/getCoursesList'; import { useGetUserSettingsQuery } from './api/user/queries'; import T3SelectGif from './assets/T3-select.gif'; -import Alerts from './components/Alerts'; -import Controls from './components/controls/Controls'; import Footer from './components/footer/Footer'; +import Planner from './components/planner/Planner'; import PromotionPopup from './components/promotions/PromotionPopup'; import SubcomPromotion from './components/promotions/SubcomPromotion'; import Sidebar from './components/sidebar/Sidebar'; import Sponsors from './components/Sponsors'; -import Timetable from './components/timetable/Timetable'; -import { TimetableTabs } from './components/timetableTabs/TimetableTabs'; import { contentPadding, darkTheme, lightTheme, rightContentPadding } from './constants/theme'; -import { - daysLong, - getAvailableTermDetails, - getDefaultEndTime, - getDefaultStartTime, - invalidYearFormat, - sortTerms, - unknownErrorMessage, -} from './constants/timetable'; -import { AppContext } from './context/AppContext'; -import { CourseContext } from './context/CourseContext'; -import { useColorsDecoder } from './hooks/useColorDecoder'; -import useColorMapper from './hooks/useColorMapper'; -import useUpdateEffect from './hooks/useUpdateEffect'; -import NetworkError from './interfaces/NetworkError'; -import { - Activity, - ClassData, - CourseCode, - CourseData, - DisplayTimetablesMap, - InInventory, - SelectedClasses, - TermDataList, -} from './interfaces/Periods'; -import { setDropzoneRange, useDrag } from './utils/Drag'; -import { downloadIcsFile } from './utils/generateICS'; -import storage from './utils/storage'; -import { createDefaultTimetable } from './utils/timetableHelpers'; const StyledApp = styled(Box)` height: 100%; @@ -85,460 +50,8 @@ const Content = styled(Box)` text-align: center; `; -const ICSButton = styled(Button)` - && { - min-width: 250px; - margin: 2vh auto; - background-color: ${({ theme }) => theme.palette.primary.main}; - color: #ffffff; - &:hover { - background-color: #598dff; - } - } -`; - const App: React.FC = () => { - const { - setAlertMsg, - setErrorVisibility, - days, - term, - year, - setDays, - earliestStartTime, - setEarliestStartTime, - latestEndTime, - setLatestEndTime, - setTerm, - setYear, - firstDayOfTerm, - setFirstDayOfTerm, - setTermName, - setTermsData, - setCoursesList, - selectedTimetable, - displayTimetables, - setDisplayTimetables, - courseData, - setCourseData, - } = useContext(AppContext); - - const { - selectedCourses, - setSelectedCourses, - selectedClasses, - setSelectedClasses, - createdEvents, - setCreatedEvents, - assignedColors, - setAssignedColors, - } = useContext(CourseContext); - - const { preferredTheme, isDarkMode, unscheduleClassesByDefault, convertToLocalTimezone } = useGetUserSettingsQuery(); - - const decodedAssignedColors = useColorsDecoder(assignedColors, preferredTheme); - - setDropzoneRange(days.length, earliestStartTime, latestEndTime); - - /** - * Attempts callback() several times before raising error. Intended for unreliable fetches - */ - const maxFetchAttempts = 6; - const fetchCooldown = 120; - const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); - const fetchReliably = async (callback: () => Promise) => { - for (let attempt = 1; attempt <= maxFetchAttempts; attempt++) { - try { - await callback(); - break; - } catch (e) { - if (attempt !== maxFetchAttempts) { - await sleep(fetchCooldown); // chill for a while before retrying - continue; - } - if (e instanceof NetworkError) { - setAlertMsg(e.message); - } else { - setAlertMsg(unknownErrorMessage); - } - setErrorVisibility(true); - } - } - }; - - useEffect(() => { - /** - * Retrieves term data from the scraper backend - */ - const fetchTermData = async () => { - const { term, termName, year, firstDayOfTerm, termsData } = await getAvailableTermDetails(); - setTerm(term); - setTermName(termName); - setYear(year); - setFirstDayOfTerm(firstDayOfTerm); - const termsSortedList: TermDataList = sortTerms(termsData); - setTermsData(termsSortedList); - - const oldData = storage.get('timetables'); - - let newTimetableTerms: DisplayTimetablesMap = {}; - for (const termId of termsData) { - newTimetableTerms = { - ...newTimetableTerms, - ...{ - [termId]: Object.prototype.hasOwnProperty.call(oldData, termId) - ? oldData[termId] - : createDefaultTimetable(undefined), - }, - }; - } - - setDisplayTimetables(newTimetableTerms); - storage.set('timetables', newTimetableTerms); - }; - - fetchReliably(fetchTermData); - }, []); - - useEffect(() => { - /** - * Retrieves the list of all courses from the scraper backend - */ - const fetchCoursesList = async () => { - const { courses } = await getCoursesList(term.substring(0, 2)); - setCoursesList(courses); - }; - - if (year !== invalidYearFormat) fetchReliably(fetchCoursesList); - }, [term, year]); - - // Fetching the saved timetables from local storage - useEffect(() => { - const savedTimetables: DisplayTimetablesMap = storage.get('timetables'); - if (savedTimetables) { - setDisplayTimetables(savedTimetables); - } - }, []); - - /** - * Update the class data for a particular course's activity e.g. when a class is dragged to another dropzone - * - * @param classData The data for the new class - */ - const handleSelectClass = (classData: ClassData) => { - setSelectedClasses((prev) => { - prev = { ...prev }; - - try { - prev[classData.courseCode][classData.activity] = classData; - } catch (err) { - setAlertMsg(unknownErrorMessage); - setErrorVisibility(true); - } - - return prev; - }); - }; - - /** - * Update the class data for a particular course's activity when it is moved to unscheduled - * - * @param classData The data for the unscheduled class - */ - const handleRemoveClass = (classData: ClassData) => { - setSelectedClasses((prev) => { - prev = { ...prev }; - prev[classData.courseCode][classData.activity] = null; - return prev; - }); - }; - - useDrag(handleSelectClass, handleRemoveClass); - - /** - * Initialise class data for a course when it is first selected - * - * @param course The data for the course which was selected - */ - const initCourse = (course: CourseData) => { - setSelectedClasses((prevRef) => { - const prev = { ...prevRef }; - - prev[course.code] = {}; - - // null means a class is unscheduled - Object.keys(course.activities).forEach((activity) => { - prev[course.code][activity] = unscheduleClassesByDefault - ? null - : (course.activities[activity].find((x) => x.enrolments !== x.capacity && x.periods.length) ?? - course.activities[activity].find((x) => x.periods.length) ?? - null); - }); - - return prev; - }); - }; - - /** - * Retrieves course info for a single course or a list of courses - * - * @param data The course code of the selected course (when selecting via the course selector) or a list of the course codes of the selected courses - * @param noInit Whether to initialise the data structure for the course data - * @param callback An optional callback function to be executed using the course data - */ - const handleSelectCourse = async ( - data: string | string[], - noInit?: boolean, - callback?: (_selectedCourses: CourseData[]) => void, - ) => { - const codes: string[] = Array.isArray(data) ? data : [data]; - Promise.all( - codes.map((code) => - getCourseInfo(term.substring(0, 2), code, term.substring(2), convertToLocalTimezone).catch((err) => { - return err; - }), - ), - ).then((result) => { - const addedCourses = result.filter((course) => course.code !== undefined) as CourseData[]; - - const newSelectedCourses = [...selectedCourses]; - const newCourseData = courseData; - - // Update the existing courses with the new data (for changing timezone). - addedCourses.forEach((addedCourse) => { - if (newSelectedCourses.find((x) => x.code === addedCourse.code)) { - const index = newSelectedCourses.findIndex((x) => x.code === addedCourse.code); - newSelectedCourses[index] = addedCourse; - if (!courseData.map.find((i) => i.code === addedCourse.code)) { - newCourseData.map.push(addedCourse); - } - } else { - newSelectedCourses.push(addedCourse); - } - if (!courseData.map.find((i) => i.code === addedCourse.code)) { - newCourseData.map.push(addedCourse); - } - }); - setSelectedCourses(newSelectedCourses); - setCourseData(newCourseData); - if (term && term in displayTimetables && displayTimetables[term].length > 0) { - setAssignedColors( - useColorMapper( - newSelectedCourses.map((course) => course.code), - assignedColors, - ), - ); - } - - if (!noInit) - addedCourses.forEach((course) => { - initCourse(course); - }); - if (callback) callback(newSelectedCourses); - }); - }; - - /** - * Handles removing a course from the currently selected courses - * - * @param courseCode The course code of the course which was removed - */ - const handleRemoveCourse = (courseCode: CourseCode) => { - const newSelectedCourses = selectedCourses.filter((course) => course.code !== courseCode); - setSelectedCourses(newSelectedCourses); - const newCourseData = courseData; - newCourseData.map = courseData.map.filter(() => { - for (const timetable of displayTimetables[term]) { - for (const course of timetable.selectedCourses) { - if (course.code.localeCompare(courseCode)) { - return true; - } - } - } - return false; - }); - setCourseData(newCourseData); - - setSelectedClasses((prev) => { - prev = { ...prev }; - delete prev[courseCode]; - return prev; - }); - }; - - type ClassId = string; - type SavedClasses = Record>; - - /** - * Populate selected courses, classes and created events with the data saved in local storage - */ - const updateTimetableEvents = () => { - if (!storage.get('timetables')[term]) { - // data stored in local storage not up to date with current term - const updatedWithTerms = { [term]: storage.get('timetables') }; - - storage.set('timetables', updatedWithTerms); - setDisplayTimetables(updatedWithTerms); - } - - if (!storage.get('timetables')?.[term][selectedTimetable]) return; - handleSelectCourse( - storage.get('timetables')[term][selectedTimetable].selectedCourses.map((course: CourseData) => course.code), - true, - (newSelectedCourses) => { - const timetableSelectedClasses: SelectedClasses = - storage.get('timetables')[term][selectedTimetable].selectedClasses; - - const savedClasses: SavedClasses = {}; - - Object.keys(timetableSelectedClasses).forEach((courseCode) => { - savedClasses[courseCode] = {}; - Object.keys(timetableSelectedClasses[courseCode]).forEach((activity) => { - const classData = timetableSelectedClasses[courseCode][activity]; - savedClasses[courseCode][activity] = classData ? classData.section : null; - }); - }); - - const newSelectedClasses: SelectedClasses = {}; - - Object.keys(savedClasses).forEach((courseCode) => { - newSelectedClasses[courseCode] = {}; - Object.keys(savedClasses[courseCode]).forEach((activity) => { - const classId = savedClasses[courseCode][activity]; - let classData: ClassData | null = null; - - if (classId) { - try { - const result = newSelectedCourses - .find((x) => x.code === courseCode) - ?.activities[activity].find((x) => x.section === classId); - if (result) classData = result; - } catch (err) { - setAlertMsg(unknownErrorMessage); - setErrorVisibility(true); - } - } - - // classData being null means the activity is unscheduled - newSelectedClasses[courseCode][activity] = classData; - }); - }); - setSelectedClasses(newSelectedClasses); - }, - ); - setCreatedEvents(storage.get('timetables')[term][selectedTimetable].createdEvents); - setAssignedColors(storage.get('timetables')[term][selectedTimetable].assignedColors); - }; - - useEffect(() => { - updateTimetableEvents(); - }, [year, convertToLocalTimezone]); - - // The following three useUpdateEffects update local storage whenever a change is made to the timetable - useUpdateEffect(() => { - displayTimetables[term][selectedTimetable].selectedCourses = selectedCourses; - const newCourseData = courseData; - storage.set('courseData', newCourseData); - - storage.set('timetables', displayTimetables); - setDisplayTimetables(displayTimetables); - }, [selectedCourses]); - - useUpdateEffect(() => { - displayTimetables[term][selectedTimetable].selectedClasses = selectedClasses; - - storage.set('timetables', displayTimetables); - setDisplayTimetables(displayTimetables); - }, [selectedClasses]); - - useUpdateEffect(() => { - displayTimetables[term][selectedTimetable].createdEvents = createdEvents; - - storage.set('timetables', displayTimetables); - setDisplayTimetables(displayTimetables); - }, [createdEvents]); - - useUpdateEffect(() => { - displayTimetables[term][selectedTimetable].assignedColors = assignedColors; - - storage.set('timetables', displayTimetables); - setDisplayTimetables(displayTimetables); - }, [assignedColors]); - - // Update storage when dragging timetables - useUpdateEffect(() => { - storage.set('timetables', displayTimetables); - }, [displayTimetables]); - - /** - * Get the latest day of the week a course has classes on - * The first day of the week is considered to be Monday - * - * @param courses The list of the currently selected courses - * @returns A number corresponding to the latest day of the week. Monday is 1, Tuesday is 2 and so on - */ - const getLatestDotW = (courses: CourseData[]) => { - let maxDay = 5; - for (const course of courses) { - const activities = Object.values(course.activities); - for (const activity of activities) { - for (const classData of activity) { - for (const period of classData.periods) { - maxDay = Math.max(maxDay, period.time.day); - } - } - } - } - - return maxDay; - }; - - /** - * Upon switching timetable, reset default bounds - */ - useEffect(() => { - setEarliestStartTime(getDefaultStartTime(convertToLocalTimezone)); - setLatestEndTime(getDefaultEndTime(convertToLocalTimezone)); - }, [selectedTimetable]); - - /** - * Update the bounds of the timetable (start time, end time, number of days) whenever a change is made to the timetable - */ - const updateTimetableDaysAndTimes = () => { - setEarliestStartTime((prev: number) => - Math.min( - ...selectedCourses.map((course) => course.earliestStartTime), - ...Object.entries(createdEvents).map(([_, eventPeriod]) => Math.floor(eventPeriod.time.start)), - getDefaultStartTime(convertToLocalTimezone), - prev, - ), - ); - - setLatestEndTime((prev: number) => - Math.max( - ...selectedCourses.map((course) => course.latestFinishTime), - ...Object.entries(createdEvents).map(([_, eventPeriod]) => Math.ceil(eventPeriod.time.end)), - getDefaultEndTime(convertToLocalTimezone), - prev, - ), - ); - - setDays( - daysLong.slice( - 0, - Math.max( - getLatestDotW(selectedCourses), - ...Object.entries(createdEvents).map(([_, eventPeriod]) => eventPeriod.time.day), - days.length, // Saturday and/or Sunday columns persist until the next reload even if they aren't needed anymore - 5, // default - ), - ), - ); - }; - - useUpdateEffect(() => { - updateTimetableDaysAndTimes(); - }, [createdEvents, selectedCourses, convertToLocalTimezone]); + const { preferredTheme, isDarkMode } = useGetUserSettingsQuery(); const themeObject = useMemo( () => (isDarkMode ? darkTheme(preferredTheme) : lightTheme(preferredTheme)), @@ -569,6 +82,8 @@ const App: React.FC = () => { }, }; + const [sidebarCollapsed, setSidebarCollapsed] = React.useState(false); + return ( @@ -576,26 +91,13 @@ const App: React.FC = () => { - + - - - - - downloadIcsFile(selectedCourses, createdEvents, selectedClasses, firstDayOfTerm)} - > - save to calendar - +