From 693b898a6400067defab681b2c95a8658c0816ce Mon Sep 17 00:00:00 2001 From: Michael Siu Date: Wed, 18 Sep 2024 17:11:58 +1000 Subject: [PATCH 01/55] feat: year, term and classNo added to class data --- client/src/api/getCourseInfo.ts | 3 +- client/src/interfaces/Database.ts | 2 + client/src/interfaces/Periods.ts | 3 + client/src/utils/DbCourse.ts | 9 ++- client/src/utils/syncTimetables.ts | 100 +++++++++++++++++++++++++++++ 5 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 client/src/utils/syncTimetables.ts diff --git a/client/src/api/getCourseInfo.ts b/client/src/api/getCourseInfo.ts index 3bbda1d92..15cf63314 100644 --- a/client/src/api/getCourseInfo.ts +++ b/client/src/api/getCourseInfo.ts @@ -88,7 +88,6 @@ const getCourseInfo = async ( const COURSE_API_TIMEOUT = 2000; try { const data = await timeoutPromise(COURSE_API_TIMEOUT, fetch(`${baseURL}/courses/${courseCode}/`)); - // Remove any leftover courses from localStorage if they are not offered in the current term // which is why a 400 error is returned if (data.status === 400) { @@ -162,7 +161,7 @@ const getCourseInfo = async ( if (!json) throw new NetworkError('Internal server error'); - return dbCourseToCourseData(json, isConvertToLocalTimezone); + return dbCourseToCourseData(json, isConvertToLocalTimezone, year); } catch (error) { console.log(error); throw new NetworkError('Could not connect to server'); diff --git a/client/src/interfaces/Database.ts b/client/src/interfaces/Database.ts index af9d68d88..45fda3130 100644 --- a/client/src/interfaces/Database.ts +++ b/client/src/interfaces/Database.ts @@ -9,9 +9,11 @@ export interface DbCourse { export interface DbClass { activity: Activity; times: DbTimes[]; + classID: number; status: Status; courseEnrolment: DbCourseEnrolment; section: Section; + term: string; } export interface DbCourseEnrolment { diff --git a/client/src/interfaces/Periods.ts b/client/src/interfaces/Periods.ts index 27d2c8d21..c2adfc376 100644 --- a/client/src/interfaces/Periods.ts +++ b/client/src/interfaces/Periods.ts @@ -27,6 +27,7 @@ export interface TermData { export interface ClassData { id: string; + classNo: number; courseCode: CourseCode; courseName: string; activity: Activity; @@ -35,6 +36,8 @@ export interface ClassData { capacity: number; periods: ClassPeriod[]; section: Section; + term: string; + year: string; } export interface TimetableData { diff --git a/client/src/utils/DbCourse.ts b/client/src/utils/DbCourse.ts index bc28e1aec..15e569677 100644 --- a/client/src/utils/DbCourse.ts +++ b/client/src/utils/DbCourse.ts @@ -146,7 +146,11 @@ const dbTimesToPeriod = (dbTimes: DbTimes, classData: ClassData, isConvertToLoca * const json: DBCourse = await data.json() * const courseInfo = dbCourseToCourseData(json) */ -export const dbCourseToCourseData = (dbCourse: DbCourse, isConvertToLocalTimezone: boolean): CourseData => { +export const dbCourseToCourseData = ( + dbCourse: DbCourse, + isConvertToLocalTimezone: boolean, + year: string, +): CourseData => { const courseData: CourseData = { code: dbCourse.courseCode, name: dbCourse.name, @@ -159,6 +163,7 @@ export const dbCourseToCourseData = (dbCourse: DbCourse, isConvertToLocalTimezon dbCourse.classes.forEach((dbClass) => { const classData: ClassData = { id: uuidv4(), + classNo: dbClass.classID, courseCode: dbCourse.courseCode, courseName: dbCourse.name, activity: dbClass.activity, @@ -167,6 +172,8 @@ export const dbCourseToCourseData = (dbCourse: DbCourse, isConvertToLocalTimezon capacity: dbClass.courseEnrolment.capacity, periods: [], section: dbClass.section, + term: dbClass.term, + year, }; classData.periods = dbClass.times.map((dbTime) => dbTimesToPeriod(dbTime, classData, isConvertToLocalTimezone)); diff --git a/client/src/utils/syncTimetables.ts b/client/src/utils/syncTimetables.ts new file mode 100644 index 000000000..e6e3e404f --- /dev/null +++ b/client/src/utils/syncTimetables.ts @@ -0,0 +1,100 @@ +import { API_URL } from '../api/config'; +import { ClassData, CreatedEvents, SelectedClasses, TimetableData } from '../interfaces/Periods'; + +const convertClassToDTO = (selectedClasses: SelectedClasses) => { + const a = Object.values(selectedClasses); + const b = a.map((c) => { + const d = Object.values(c); + return d.map((c) => { + const { id, classNo, year, term, courseCode } = c as ClassData; + return { id, classNo, year, term, courseCode }; + }); + }); + + return b.reduce((prev, curr) => prev.concat(curr)); +}; + +// TODO: Events don't really make sense right now. Timetable takes datetime, but frontend can only select +// day + time of day, not specific date. Modify this later +const convertEventToDTO = (createdEvents: CreatedEvents) => { + return []; +}; + +const convertTimetableToDTO = (timetable: TimetableData) => { + return { + ...timetable, + selectedClasses: convertClassToDTO(timetable.selectedClasses), + createdEvents: convertEventToDTO(timetable.createdEvents), + }; +}; + +const syncAddTimetable = async (userId: string, newTimetable: TimetableData) => { + try { + if (!userId) { + console.log('User is not logged in'); + return; + } + const { selectedCourses, selectedClasses, createdEvents, name } = newTimetable; + console.log(createdEvents); + await fetch(`${API_URL.server}/user/timetable`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + userId, + selectedCourses, + selectedClasses: convertClassToDTO(selectedClasses), + createdEvents: convertEventToDTO(createdEvents), // TODO + name, + }), + }); + } catch (e) { + console.log('todo'); + } +}; + +const syncDeleteTimetable = async (timetableId: string) => { + try { + await fetch( + `${API_URL.server}/user/timetable` + + new URLSearchParams({ + timetableId, + }).toString(), + { + method: 'DELETE', + }, + ); + } catch (e) { + console.log('todo'); + } +}; + +const syncDeleteTimetables = async (timetables: TimetableData[]) => { + try { + await Promise.all(timetables.map((t) => t.id).map((id) => syncDeleteTimetable(id))); + } catch (e) { + console.log('todo'); + } +}; + +const syncEditTimetable = async (userId: string, editedTimetable: TimetableData) => { + try { + if (!userId) { + console.log('User is not logged in'); + return; + } + + await fetch(`${API_URL.server}/user/timetable`, { + method: 'PUT', + body: JSON.stringify({ + timetable: convertTimetableToDTO(editedTimetable), + }), + }); + } catch (e) { + console.log('todo'); + } +}; + +export { syncAddTimetable, syncDeleteTimetable, syncDeleteTimetables, syncEditTimetable }; From 556cda24426d8a17dd87c23cbc84c5430ef09590 Mon Sep 17 00:00:00 2001 From: Michael Siu Date: Wed, 18 Sep 2024 17:17:47 +1000 Subject: [PATCH 02/55] feat: add sync functions for add/remove/edit timetables. Create/duplicate/delete one timetable synced, and history sync started --- client/src/App.tsx | 4 ++-- client/src/components/controls/History.tsx | 7 ++++++- .../components/timetableTabs/TimetableTabContextMenu.tsx | 3 +++ client/src/components/timetableTabs/TimetableTabs.tsx | 3 ++- client/src/utils/syncTimetables.ts | 1 - 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 2c6f8de15..fd67ee9a0 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -266,9 +266,9 @@ const App: React.FC = () => { Object.keys(course.activities).forEach((activity) => { prev[course.code][activity] = isDefaultUnscheduled ? null - : course.activities[activity].find((x) => x.enrolments !== x.capacity && x.periods.length) ?? + : (course.activities[activity].find((x) => x.enrolments !== x.capacity && x.periods.length) ?? course.activities[activity].find((x) => x.periods.length) ?? - null; + null); }); return prev; diff --git a/client/src/components/controls/History.tsx b/client/src/components/controls/History.tsx index 2a1d9be25..52148de2c 100644 --- a/client/src/components/controls/History.tsx +++ b/client/src/components/controls/History.tsx @@ -5,6 +5,7 @@ import React, { useContext, useEffect, useRef, useState } from 'react'; import { AppContext } from '../../context/AppContext'; import { CourseContext } from '../../context/CourseContext'; import { CourseData, CreatedEvents, DisplayTimetablesMap, SelectedClasses } from '../../interfaces/Periods'; +import { syncAddTimetable, syncDeleteTimetables } from '../../utils/syncTimetables'; import { ActionsPointer, areIdenticalTimetables, @@ -199,9 +200,13 @@ const History: React.FC = () => { * Resets all timetables - leave one as default */ const clearAll = () => { + const newTimetables = createDefaultTimetable(); + syncDeleteTimetables(displayTimetables[term]); + syncAddTimetable('zTODO', newTimetables[0]); + const newDisplayTimetables: DisplayTimetablesMap = { ...displayTimetables, - [term]: createDefaultTimetable(), + [term]: newTimetables, }; setTimetableState([], {}, {}, newDisplayTimetables, 0); }; diff --git a/client/src/components/timetableTabs/TimetableTabContextMenu.tsx b/client/src/components/timetableTabs/TimetableTabContextMenu.tsx index 90d5dc3fb..987ff4170 100644 --- a/client/src/components/timetableTabs/TimetableTabContextMenu.tsx +++ b/client/src/components/timetableTabs/TimetableTabContextMenu.tsx @@ -27,6 +27,7 @@ import { import { ExecuteButton, RedDeleteIcon, RedListItemText, StyledMenu } from '../../styles/CustomEventStyles'; import { StyledSnackbar } from '../../styles/TimetableTabStyles'; import storage from '../../utils/storage'; +import { syncAddTimetable } from '../../utils/syncTimetables'; import { duplicateClasses, duplicateEvents } from '../../utils/timetableHelpers'; import StyledDialog from '../StyledDialog'; @@ -197,6 +198,8 @@ const TimetableTabContextMenu: React.FC = ({ ancho assignedColors: currentTimetable.assignedColors, }; + syncAddTimetable('zTODO', newTimetable); + const newTimetables = [ ...displayTimetables[term].slice(0, selectedTimetable + 1), newTimetable, diff --git a/client/src/components/timetableTabs/TimetableTabs.tsx b/client/src/components/timetableTabs/TimetableTabs.tsx index f56db7396..97822665a 100644 --- a/client/src/components/timetableTabs/TimetableTabs.tsx +++ b/client/src/components/timetableTabs/TimetableTabs.tsx @@ -26,6 +26,7 @@ import { tabThemeLight, } from '../../styles/TimetableTabStyles'; import storage from '../../utils/storage'; +import { syncAddTimetable } from '../../utils/syncTimetables'; import TimetableTabContextMenu from './TimetableTabContextMenu'; const TimetableTabs: React.FC = () => { @@ -101,9 +102,9 @@ const TimetableTabs: React.FC = () => { storage.set('timetables', addingNewTimetables); setDisplayTimetables(addingNewTimetables); - // Clearing the selected courses, classes and created events for the new timetable setTimetableState([], {}, {}, {}, nextIndex); + syncAddTimetable('zTODO', newTimetable); } }; diff --git a/client/src/utils/syncTimetables.ts b/client/src/utils/syncTimetables.ts index e6e3e404f..2f5702cb2 100644 --- a/client/src/utils/syncTimetables.ts +++ b/client/src/utils/syncTimetables.ts @@ -35,7 +35,6 @@ const syncAddTimetable = async (userId: string, newTimetable: TimetableData) => return; } const { selectedCourses, selectedClasses, createdEvents, name } = newTimetable; - console.log(createdEvents); await fetch(`${API_URL.server}/user/timetable`, { method: 'POST', headers: { From f78a65dce1533835a1b306175a37e8529f9056b5 Mon Sep 17 00:00:00 2001 From: hhuolu Date: Sun, 22 Sep 2024 16:25:58 +1000 Subject: [PATCH 03/55] feat: added routing for friends page --- client/src/components/friends/ActivityBar.tsx | 31 +++++++ client/src/components/friends/Friends.tsx | 89 +++++++++++++++++++ .../components/friends/FriendsActivity.tsx | 80 +++++++++++++++++ .../components/friends/FriendsTimetable.tsx | 79 ++++++++++++++++ client/src/components/sidebar/CustomModal.tsx | 16 +++- client/src/components/sidebar/Sidebar.tsx | 16 ++-- client/src/index.tsx | 2 + client/src/interfaces/PropTypes.ts | 4 +- 8 files changed, 301 insertions(+), 16 deletions(-) create mode 100644 client/src/components/friends/ActivityBar.tsx create mode 100644 client/src/components/friends/Friends.tsx create mode 100644 client/src/components/friends/FriendsActivity.tsx create mode 100644 client/src/components/friends/FriendsTimetable.tsx diff --git a/client/src/components/friends/ActivityBar.tsx b/client/src/components/friends/ActivityBar.tsx new file mode 100644 index 000000000..a16d4d3ad --- /dev/null +++ b/client/src/components/friends/ActivityBar.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { styled } from '@mui/system'; +import FriendsActivity from './FriendsActivity'; + +const ActivityBarContainer = styled('div')` + display: flex; + flex-direction: column; + width: 340px; + height: 100vh; + border-left: 1px solid #eee; + padding: 30px; + gap: 20px; + align-items: flex-start; +`; + +const StyledTitle = styled('p')` + font-size: 1.2rem; + font-weight: 700; + margin: 0; +`; + +const ActivityBar: React.FC = ({}) => { + return ( + + Your Friends Activity + + + ); +}; + +export default ActivityBar; diff --git a/client/src/components/friends/Friends.tsx b/client/src/components/friends/Friends.tsx new file mode 100644 index 000000000..68f72c7b3 --- /dev/null +++ b/client/src/components/friends/Friends.tsx @@ -0,0 +1,89 @@ +import { Box, Button, GlobalStyles, StyledEngineProvider, ThemeProvider } from '@mui/material'; +import { styled } from '@mui/system'; +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 } from 'react'; +import { Outlet } from 'react-router-dom'; + +import { contentPadding, darkTheme, leftContentPadding, lightTheme } from '../../constants/theme'; +import { AppContext } from '../../context/AppContext'; +import Sidebar from '../sidebar/Sidebar'; +import Sponsors from '../Sponsors'; +import Footer from '../footer/Footer'; +import Alerts from '../Alerts'; +import ActivityBar from './ActivityBar'; +import { TimetableTabs } from '../timetableTabs/TimetableTabs'; +import Timetable from '../timetable/Timetable'; +import Controls from '../controls/Controls'; +import { CourseContext } from '../../context/CourseContext'; +import FriendsTimetable from './FriendsTimetable'; + +const StyledApp = styled(Box)` + height: 100%; + width: 100%; +`; + +const ContentWrapper = styled(Box)` + text-align: center; + padding-top: ${contentPadding}px; + padding-left: ${leftContentPadding}px; +// padding-right: ${contentPadding}px; + transition: + background 0.2s, + color 0.2s; + min-height: 50vh; + box-sizing: border-box; + display: flex; + flex-direction: row-reverse; + justify-content: center; + color: ${({ theme }) => theme.palette.text.primary}; +`; + +const Content = styled(Box)` + width: 1400px; + max-width: 100%; + transition: width 0.2s; + display: grid; + grid-template-rows: min-content min-content auto; + grid-template-columns: auto; + text-align: center; +`; + +const Friends: React.FC = () => { + const { isDarkMode } = useContext(AppContext); + const { + selectedCourses, + setSelectedCourses, + selectedClasses, + setSelectedClasses, + createdEvents, + setCreatedEvents, + assignedColors, + setAssignedColors, + } = useContext(CourseContext); + const theme = isDarkMode ? darkTheme : lightTheme; + + return ( + + + {/* */} + {/* */} + + + + + + +