-
Notifications
You must be signed in to change notification settings - Fork 78
feat: implement Course Statuses feature #444
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import { UserScheduleStore } from "src/shared/storage/UserScheduleStore"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use path alias. |
||
import { CourseCatalogScraper } from '@views/lib/CourseCatalogScraper'; | ||
import { Course, StatusType } from '@shared/types/Course'; | ||
import { resolve } from "path"; | ||
|
||
|
||
export default async function checkCourseStatusChanges() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Needs jsdoc. |
||
const activeIndex = await UserScheduleStore.get('activeIndex'); | ||
const schedules = await UserScheduleStore.get('schedules'); | ||
const currentSchedule = schedules[activeIndex]?.courses; | ||
|
||
|
||
if (!currentSchedule || currentSchedule.length === 0) return; | ||
|
||
let newStatus = currentSchedule[0]?.status; | ||
for (const course of currentSchedule) { | ||
// Get the latest status from the course catalog or API | ||
// You might need to import your CourseCatalogScraper here | ||
const uniqueId = course.uniqueId; | ||
// gets the new status | ||
// console.log(uniqueId, 'UNIQUE ID'); | ||
try { | ||
const response = await fetch(`https://utdirect.utexas.edu/apps/registrar/course_schedule/20252/${uniqueId}/`); | ||
const html = await response.text(); | ||
|
||
const parser = new DOMParser(); | ||
const doc = parser.parseFromString(html, 'text/html'); | ||
const scraper = new CourseCatalogScraper('COURSE_CATALOG_LIST', doc); | ||
const rows = doc.querySelectorAll('tr'); | ||
rows.forEach(row => { | ||
try { | ||
const id = scraper.getUniqueId(row); | ||
if (id && id === uniqueId) { | ||
const scrapedStatus = scraper.getStatus(row)[0]; | ||
newStatus = scrapedStatus as StatusType; | ||
Comment on lines
+34
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ideally we should have some validation here. |
||
} | ||
console.log('Unique ID:', uniqueId); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should have some further context if needed for logging. Otherwise this is just verbose and should be removed. |
||
} catch (error) { | ||
console.error(error); | ||
} | ||
}); | ||
|
||
} | ||
catch (error) { | ||
console.error(`Failed to check status for course ${uniqueId}:`, error); | ||
} | ||
|
||
if (newStatus && newStatus !== currentSchedule[0]?.status) { | ||
const updatedCourses = currentSchedule.map(c => { | ||
if (c.uniqueId === uniqueId) { | ||
return { ...c, status: newStatus } as Course; | ||
} | ||
return c as Course; | ||
Comment on lines
+50
to
+53
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should try to prevent the use of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
}); | ||
console.log(updatedCourses); | ||
const updatedSchedules = [...schedules]; | ||
console.log("UPDATED", updatedSchedules) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A bit verbose. Let's refactor this to be both helpful for logging and brief. |
||
{ | ||
...updatedSchedules[activeIndex], | ||
courses: updatedCourses, | ||
}; | ||
|
||
|
||
await UserScheduleStore.set('schedules', updatedSchedules); | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,27 +8,40 @@ import switchSchedule from '@pages/background/lib/switchSchedule'; | |
import type { UserScheduleMessages } from '@shared/messages/UserScheduleMessages'; | ||
import { Course } from '@shared/types/Course'; | ||
import type { MessageHandler } from 'chrome-extension-toolkit'; | ||
import { UserScheduleStore } from 'src/shared/storage/UserScheduleStore'; | ||
import { CourseCatalogScraper } from '@views/lib/CourseCatalogScraper'; | ||
import { StatusType } from '@shared/types/Course'; | ||
import { Serialized } from 'chrome-extension-toolkit'; | ||
|
||
|
||
const userScheduleHandler: MessageHandler<UserScheduleMessages> = { | ||
addCourse({ data, sendResponse }) { | ||
checkCourseStatusChanges(); | ||
addCourse(data.scheduleId, new Course(data.course)).then(sendResponse); | ||
}, | ||
removeCourse({ data, sendResponse }) { | ||
console.log("TEST") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. extra console.log |
||
checkCourseStatusChanges(); | ||
removeCourse(data.scheduleId, new Course(data.course)).then(sendResponse); | ||
}, | ||
clearCourses({ data, sendResponse }) { | ||
checkCourseStatusChanges(); | ||
clearCourses(data.scheduleId).then(sendResponse); | ||
}, | ||
switchSchedule({ data, sendResponse }) { | ||
checkCourseStatusChanges(); | ||
switchSchedule(data.scheduleId).then(sendResponse); | ||
}, | ||
createSchedule({ data, sendResponse }) { | ||
checkCourseStatusChanges(); | ||
createSchedule(data.scheduleName).then(sendResponse); | ||
}, | ||
deleteSchedule({ data, sendResponse }) { | ||
checkCourseStatusChanges(); | ||
deleteSchedule(data.scheduleId).then(sendResponse); | ||
}, | ||
renameSchedule({ data, sendResponse }) { | ||
checkCourseStatusChanges(); | ||
renameSchedule(data.scheduleId, data.newName).then(sendResponse); | ||
}, | ||
// proxy so we can add courses | ||
|
@@ -42,4 +55,99 @@ const userScheduleHandler: MessageHandler<UserScheduleMessages> = { | |
}, | ||
}; | ||
|
||
async function checkCourseStatusChanges() { | ||
const activeIndex = await UserScheduleStore.get('activeIndex'); | ||
const schedules = await UserScheduleStore.get('schedules'); | ||
const currentSchedule = schedules[activeIndex]?.courses; | ||
|
||
|
||
if (!currentSchedule || currentSchedule.length === 0) return; | ||
|
||
let newStatus = currentSchedule[0]?.status; | ||
for (const course of currentSchedule) { | ||
// Get the latest status from the course catalog or API | ||
// You might need to import your CourseCatalogScraper here | ||
const uniqueId = course.uniqueId; | ||
// gets the new status | ||
try { | ||
const response = await fetch(`https://utdirect.utexas.edu/apps/registrar/course_schedule/20252/${uniqueId}/`); | ||
const html = await response.text(); | ||
|
||
// Parse the HTML using DOMParser | ||
const parser = new DOMParser(); | ||
const doc = parser.parseFromString(html, 'text/html'); | ||
|
||
const scraper = new CourseCatalogScraper('COURSE_CATALOG_LIST', doc); | ||
const rows = Array.from(doc.querySelectorAll('tr')); | ||
|
||
for (const row of rows) { | ||
const id = scraper.getUniqueId(row); | ||
if (id === uniqueId) { | ||
const scrapedStatus = scraper.getStatus(row)[0]; | ||
newStatus = scrapedStatus as StatusType; | ||
} | ||
} | ||
|
||
} | ||
catch (error) { | ||
console.error(`Failed to check status for course ${uniqueId}:`, error); | ||
} | ||
|
||
if (newStatus && newStatus !== currentSchedule[0]?.status) { | ||
const updatedCourses = currentSchedule.map(c => { | ||
if (c.uniqueId === uniqueId) { | ||
return { ...c, status: newStatus } as Serialized<Course>; // Explicitly cast here | ||
} | ||
return c as Serialized<Course>; // Ensure the rest match expected type | ||
}); | ||
|
||
const updatedSchedules = [...schedules]; | ||
// updatedSchedules[activeIndex] = { | ||
// ...updatedSchedules[activeIndex], | ||
// courses: updatedCourses, | ||
// }; | ||
|
||
|
||
await UserScheduleStore.set('schedules', updatedSchedules); | ||
} | ||
|
||
|
||
// if (updatedCourseInfo && updatedCourseInfo.status !== course.status) { | ||
// // Status has changed | ||
// const updatedCourses = currentSchedule.map(c => | ||
// c.uniqueNumber === course.uniqueNumber | ||
// ? { ...c, status: updatedCourseInfo.status } | ||
// : c | ||
// ); | ||
|
||
// // Update the store with new course status | ||
// const updatedSchedules = [...schedules]; | ||
// updatedSchedules[activeIndex] = { | ||
// ...schedules[activeIndex], | ||
// courses: updatedCourses | ||
// }; | ||
|
||
// await UserScheduleStore.set('schedules', updatedSchedules); | ||
|
||
// // Notify user of status change | ||
// chrome.notifications.create({ | ||
// type: 'basic', | ||
// title: 'Course Status Change', | ||
// message: `${course.courseName} status changed from ${course.status} to ${updatedCourseInfo.status}`, | ||
// iconUrl: '/icons/icon48.png' // Make sure this path is correct | ||
//}); | ||
} | ||
} | ||
Comment on lines
+58
to
+140
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is defined previously. |
||
|
||
// You'll need to implement this function to fetch the latest course status | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please write a proper jsdoc that uses tsdoc standards. |
||
async function fetchLatestCourseStatus(course: Course) { | ||
// Implement the logic to fetch the latest course status | ||
// This might involve using your CourseCatalogScraper or making an API call | ||
// Return the updated course information | ||
return null; // Replace with actual implementation | ||
} | ||
Comment on lines
+143
to
+148
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO |
||
|
||
|
||
|
||
|
||
export default userScheduleHandler; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,7 +21,7 @@ export interface IOptionsStore { | |
} | ||
|
||
export const OptionsStore = createSyncStore<IOptionsStore>({ | ||
enableCourseStatusChips: false, | ||
enableCourseStatusChips: true, // true for dev purposes, will make switch in settings - ali v | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO: remove comment later |
||
enableTimeAndLocationInPopup: false, | ||
enableHighlightConflicts: true, | ||
enableScrollToLoad: true, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -45,7 +45,10 @@ export default function CalendarCourseCell({ | |
|
||
useEffect(() => { | ||
initSettings().then(({ enableCourseStatusChips }) => setEnableCourseStatusChips(enableCourseStatusChips)); | ||
|
||
console.log("useEffect"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. debug console.log. Should be removed later on. |
||
initSettings().then((res) => { | ||
console.log(res); | ||
}) | ||
Comment on lines
+49
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ? |
||
const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => { | ||
setEnableCourseStatusChips(newValue); | ||
// console.log('enableCourseStatusChips', newValue); | ||
|
@@ -57,6 +60,8 @@ export default function CalendarCourseCell({ | |
}, []); | ||
|
||
let rightIcon: React.ReactNode | null = null; | ||
console.log("enabledCourseStatusChips", enableCourseStatusChips); | ||
console.log("status", status) | ||
Comment on lines
+63
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. debug console.log. Should be removed later on. |
||
if (enableCourseStatusChips) { | ||
if (status === Status.WAITLISTED) { | ||
rightIcon = <WaitlistIcon className='h-5 w-5' />; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,12 +7,15 @@ import ScheduleTotalHoursAndCourses from '@views/components/common/ScheduleTotal | |
import Text from '@views/components/common/Text/Text'; | ||
import useSchedules from '@views/hooks/useSchedules'; | ||
import { getUpdatedAtDateTimeString } from '@views/lib/getUpdatedAtDateTimeString'; | ||
|
||
import { openTabFromContentScript } from '@views/lib/openNewTabFromContentScript'; | ||
|
||
import React, { useEffect, useState } from 'react'; | ||
|
||
import MenuIcon from '~icons/material-symbols/menu'; | ||
// import RefreshIcon from '~icons/material-symbols/refresh'; | ||
import RefreshIcon from '~icons/material-symbols/refresh'; | ||
import SettingsIcon from '~icons/material-symbols/settings'; | ||
import checkCourseStatusChanges from 'src/pages/background/handler/courseStatusChange'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use path alias. |
||
|
||
/** | ||
* Opens the options page in a new tab. | ||
|
@@ -81,9 +84,12 @@ export default function CalendarHeader({ onSidebarToggle }: CalendarHeaderProps) | |
<Text variant='mini' className='text-nowrap text-ut-gray font-normal!'> | ||
LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)} | ||
</Text> | ||
{/* <button className='inline-block h-4 w-4 bg-transparent p-0 btn'> | ||
<button | ||
className='inline-block h-4 w-4 bg-transparent p-0 btn' | ||
onClick={async () => await checkCourseStatusChanges()} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit. I would extract this into a dedicated function. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. how is this any different from |
||
> | ||
<RefreshIcon className='h-4 w-4 animate-duration-800 text-ut-black' /> | ||
</button> */} | ||
</button> | ||
</div> | ||
)} | ||
</div> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -81,7 +81,7 @@ const useDevMode = (targetCount: number): [boolean, () => void] => { | |
* @returns The Settings component. | ||
*/ | ||
export default function Settings(): JSX.Element { | ||
const [_enableCourseStatusChips, setEnableCourseStatusChips] = useState<boolean>(false); | ||
const [enableCourseStatusChips, setEnableCourseStatusChips] = useState<boolean>(false); | ||
const [_showTimeLocation, setShowTimeLocation] = useState<boolean>(false); | ||
const [highlightConflicts, setHighlightConflicts] = useState<boolean>(false); | ||
const [loadAllCourses, setLoadAllCourses] = useState<boolean>(false); | ||
|
@@ -101,6 +101,9 @@ export default function Settings(): JSX.Element { | |
const showDialog = usePrompt(); | ||
const handleChangelogOnClick = useChangelog(); | ||
|
||
useEffect(() => { | ||
console.log(enableCourseStatusChips, "settings"); | ||
},[enableCourseStatusChips]) | ||
Comment on lines
+104
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. debug console.log. Should be removed later on. |
||
useEffect(() => { | ||
const fetchGitHubStats = async () => { | ||
try { | ||
|
@@ -140,7 +143,7 @@ export default function Settings(): JSX.Element { | |
// Listen for changes in the settings | ||
const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => { | ||
setEnableCourseStatusChips(newValue); | ||
// console.log('enableCourseStatusChips', newValue); | ||
console.log('enableCourseStatusChips', newValue); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. debug console.log. Should be removed later on. |
||
}); | ||
|
||
const l2 = OptionsStore.listen('enableTimeAndLocationInPopup', async ({ newValue }) => { | ||
|
@@ -364,6 +367,24 @@ export default function Settings(): JSX.Element { | |
|
||
<Divider size='auto' orientation='horizontal' /> | ||
|
||
<div className='flex items-center justify-between'> | ||
<div className='max-w-xs'> | ||
<h3 className='text-ut-burntorange font-semibold'>Show Course Status</h3> | ||
<p className='text-sm text-gray-600'> | ||
Shows an indicator for waitlisted, cancelled, and closed courses. | ||
</p> | ||
</div> | ||
<SwitchButton | ||
isChecked={enableCourseStatusChips} | ||
onChange={() => { | ||
setEnableCourseStatusChips(!enableCourseStatusChips); | ||
OptionsStore.set('enableCourseStatusChips', !enableCourseStatusChips); | ||
}} | ||
/> | ||
</div> | ||
|
||
<Divider size='auto' orientation='horizontal' /> | ||
|
||
<div className='flex items-center justify-between'> | ||
<div className='max-w-xs'> | ||
<Text variant='h4' className='text-ut-burntorange font-semibold'> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -222,7 +222,7 @@ export class CourseCatalogScraper { | |
*/ | ||
getInstructionMode(row: HTMLTableRowElement): InstructionMode { | ||
const text = (row.querySelector(TableDataSelector.INSTRUCTION_MODE)?.textContent || '').toLowerCase(); | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Random indentation. |
||
if (text.includes('internet')) { | ||
return 'Online'; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not related to this PR in addition that we shouldn't include random links. Please remove.