From f0138e9ce70ffe86640bbaab03dc05910c63a631 Mon Sep 17 00:00:00 2001 From: doprz <52579214+doprz@users.noreply.github.com> Date: Thu, 3 Oct 2024 12:43:43 -0500 Subject: [PATCH 01/23] feat: setup settings page boilerplate --- src/pages/options/{App.tsx => DevMode.tsx} | 4 +- src/pages/options/Settings.tsx | 194 +++++++++++++++++++++ src/pages/options/index.tsx | 4 +- 3 files changed, 199 insertions(+), 3 deletions(-) rename src/pages/options/{App.tsx => DevMode.tsx} (91%) create mode 100644 src/pages/options/Settings.tsx diff --git a/src/pages/options/App.tsx b/src/pages/options/DevMode.tsx similarity index 91% rename from src/pages/options/App.tsx rename to src/pages/options/DevMode.tsx index c3e7d27fe..50f938455 100644 --- a/src/pages/options/App.tsx +++ b/src/pages/options/DevMode.tsx @@ -3,9 +3,11 @@ import Link from '@views/components/common/Link'; import React from 'react'; /** + * Renders the DevMode component. * + * @returns The rendered DevMode component. */ -export default function App() { +export default function DevMode() { return (
diff --git a/src/pages/options/Settings.tsx b/src/pages/options/Settings.tsx new file mode 100644 index 000000000..f08a31cbf --- /dev/null +++ b/src/pages/options/Settings.tsx @@ -0,0 +1,194 @@ +import { Button } from '@views/components/common/Button'; +import Divider from '@views/components/common/Divider'; +import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot'; +import { SmallLogo } from '@views/components/common/LogoIcon'; +import SwitchButton from '@views/components/common/SwitchButton'; +import React, { useCallback, useEffect, useState } from 'react'; + +import App from './DevMode'; + +interface TeamMember { + name: string; + role: string; +} + +const useDevMode = (targetCount: number): [boolean, () => void] => { + const [count, setCount] = useState(0); + const [active, setActive] = useState(false); + const [lastClick, setLastClick] = useState(0); + + const incrementCount = useCallback(() => { + const now = Date.now(); + if (now - lastClick < 500) { + setCount(prevCount => { + const newCount = prevCount + 1; + if (newCount === targetCount) { + setActive(true); + } + return newCount; + }); + } else { + setCount(1); + } + setLastClick(now); + }, [lastClick, targetCount]); + + useEffect(() => { + const timer = setTimeout(() => setCount(0), 3000); + return () => clearTimeout(timer); + }, [count]); + + return [active, incrementCount]; +}; + +/** + * Renders the settings page for the UTRP (UT Registration Plus) extension. + * Allows customization options and displays credits for the development team. + * + * @returns The JSX element representing the settings page. + */ +export default function SettingsPage() { + const [showCourseStatus, setShowCourseStatus] = useState(true); + const [showTimeLocation, setShowTimeLocation] = useState(true); + const [highlightConflicts, setHighlightConflicts] = useState(true); + const [loadAllCourses, setLoadAllCourses] = useState(true); + + const [devMode, toggleDevMode] = useDevMode(10); + + const teamMembers = [ + { name: 'Sriram Hariharan', role: 'Founder' }, + { name: 'Elie Soloveichik', role: 'Senior Software Engineer' }, + { name: 'Diego Perez', role: 'Senior Software Engineer' }, + { name: 'Lukas Zenick', role: 'Senior Software Engineer' }, + { name: 'Isaiah Rodriguez', role: 'Chief Design Officer' }, + { name: 'Som Gupta', role: 'Lead Software Engineer' }, + { name: 'Abhinav Chadaga', role: 'Software Engineer' }, + { name: 'Samuel Gunter', role: 'Software Engineer' }, + { name: 'Casey Charleston', role: 'Software Engineer' }, + { name: 'Dhruv Arora', role: 'Software Engineer' }, + { name: 'Derek Chen', role: 'Software Engineer' }, + { name: 'Vinson Zheng', role: 'Software Engineer' }, + { name: 'Vivek Malle', role: 'Software Engineer' }, + ] as const satisfies TeamMember[]; + + if (devMode) { + return ; + } + + return ( + +
+
+
+ + +

UTRP SETTINGS & CREDITS PAGE

+
+ LD Icon +
+ +
+

CUSTOMIZATION OPTIONS

+
+
+
+

Show Course Status

+

+ Shows an indicator for waitlisted, cancelled, and closed courses. +

+
+ +
+ +
+
+

Show Time & Location in Popup

+

+ Shows the course's time and location in the extension's popup. +

+
+ +
+
+
+ + + +
+

ADVANCED SETTINGS

+
+
+
+

Refresh Data

+

+ Refreshes waitlist, course status, and other info with the latest data from + UT's site. +

+
+ +
+ + + +
+
+

Course Conflict Highlight

+

+ Adds a red strikethrough to courses that have conflicting times. +

+
+ +
+ + + +
+
+

+ Load All Courses in Course Schedule +

+

+ Loads all courses in the Course Schedule site by scrolling, instead of using + next/prev page buttons. +

+
+ +
+ + + +
+
+

Reset All Data

+

Erases all schedules and courses you have.

+
+ +
+
+
+ +
+

MEET THE TEAM BEHIND UTRP V2

+
+ {teamMembers.map(member => ( +
+

{member.name}

+

{member.role}

+
+ ))} +
+
+ +
+

+ Dev Mode +

+
+
+
+ ); +} diff --git a/src/pages/options/index.tsx b/src/pages/options/index.tsx index 8ba7b8def..12e3a78a3 100644 --- a/src/pages/options/index.tsx +++ b/src/pages/options/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; -import App from './App'; +import SettingsPage from './Settings'; -createRoot(document.getElementById('root')!).render(); +createRoot(document.getElementById('root')!).render(); From bbabe455cf1c35a3e26a0df927b1292caf6a6a78 Mon Sep 17 00:00:00 2001 From: doprz <52579214+doprz@users.noreply.github.com> Date: Thu, 3 Oct 2024 13:04:57 -0500 Subject: [PATCH 02/23] feat: split view into halves --- src/pages/options/Settings.tsx | 202 ++++++++++++++++++--------------- 1 file changed, 111 insertions(+), 91 deletions(-) diff --git a/src/pages/options/Settings.tsx b/src/pages/options/Settings.tsx index f08a31cbf..f34669fb1 100644 --- a/src/pages/options/Settings.tsx +++ b/src/pages/options/Settings.tsx @@ -87,107 +87,127 @@ export default function SettingsPage() { LD Icon -
-

CUSTOMIZATION OPTIONS

-
-
-
-

Show Course Status

-

- Shows an indicator for waitlisted, cancelled, and closed courses. -

+
+
+
+

CUSTOMIZATION OPTIONS

+
+
+
+

Show Course Status

+

+ Shows an indicator for waitlisted, cancelled, and closed courses. +

+
+ +
+ +
+
+

+ Show Time & Location in Popup +

+

+ Shows the course's time and location in the extension's popup. +

+
+ +
- -
- -
-
-

Show Time & Location in Popup

-

- Shows the course's time and location in the extension's popup. -

-
- -
-
-
- - - -
-

ADVANCED SETTINGS

-
-
-
-

Refresh Data

-

- Refreshes waitlist, course status, and other info with the latest data from - UT's site. -

-
- -
- - - -
-
-

Course Conflict Highlight

-

- Adds a red strikethrough to courses that have conflicting times. -

-
- -
+
-
-
-

- Load All Courses in Course Schedule -

-

- Loads all courses in the Course Schedule site by scrolling, instead of using - next/prev page buttons. -

+
+

ADVANCED SETTINGS

+
+
+
+

Refresh Data

+

+ Refreshes waitlist, course status, and other info with the latest data from + UT's site. +

+
+ +
+ + + +
+
+

Course Conflict Highlight

+

+ Adds a red strikethrough to courses that have conflicting times. +

+
+ +
+ + + +
+
+

+ Load All Courses in Course Schedule +

+

+ Loads all courses in the Course Schedule site by scrolling, instead of using + next/prev page buttons. +

+
+ +
+ + + +
+
+

Reset All Data

+

+ Erases all schedules and courses you have. +

+
+ +
- -
+ -
-
-

Reset All Data

-

Erases all schedules and courses you have.

-
- -
+
+

+ Dev Mode +

+
- - -
-

MEET THE TEAM BEHIND UTRP V2

-
- {teamMembers.map(member => ( -
-

{member.name}

-

{member.role}

-
- ))} -
-
-
-

- Dev Mode -

-
+
+
+

MEET THE TEAM BEHIND UTRP V2

+
+ {teamMembers.map(member => ( +
+

{member.name}

+

{member.role}

+
+ ))} +
+
+
+
); From f424f625baccd2ef4b39ceef4c960eec39fbfb86 Mon Sep 17 00:00:00 2001 From: doprz <52579214+doprz@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:41:13 -0500 Subject: [PATCH 03/23] feat: add preview for Customization Options section --- src/pages/options/Settings.tsx | 74 +++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 23 deletions(-) diff --git a/src/pages/options/Settings.tsx b/src/pages/options/Settings.tsx index f34669fb1..2ae05afce 100644 --- a/src/pages/options/Settings.tsx +++ b/src/pages/options/Settings.tsx @@ -1,9 +1,16 @@ +import { getCourseColors } from '@shared/util/colors'; +import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell'; import { Button } from '@views/components/common/Button'; import Divider from '@views/components/common/Divider'; import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot'; import { SmallLogo } from '@views/components/common/LogoIcon'; +import PopupCourseBlock from '@views/components/common/PopupCourseBlock'; import SwitchButton from '@views/components/common/SwitchButton'; import React, { useCallback, useEffect, useState } from 'react'; +import { ExampleCourse } from 'src/stories/components/ConflictsWithWarning.stories'; + +import DeleteForeverIcon from '~icons/material-symbols/delete-forever'; +import RefreshIcon from '~icons/material-symbols/refresh'; import App from './DevMode'; @@ -82,7 +89,7 @@ export default function SettingsPage() {
-

UTRP SETTINGS & CREDITS PAGE

+

UTRP SETTINGS & CREDITS PAGE

LD Icon @@ -91,27 +98,46 @@ export default function SettingsPage() {

CUSTOMIZATION OPTIONS

-
-
-
-

Show Course Status

-

- Shows an indicator for waitlisted, cancelled, and closed courses. -

+
+
+
+
+

Show Course Status

+

+ Shows an indicator for waitlisted, cancelled, and closed courses. +

+
+ +
+ +
+
+

+ Show Time & Location in Popup +

+

+ Shows the course's time and location in the extension's popup. +

+
+
-
- -
-
-

- Show Time & Location in Popup -

-

- Shows the course's time and location in the extension's popup. -

+
+
+
Preview
+
+
+ +
-
@@ -122,7 +148,7 @@ export default function SettingsPage() {

ADVANCED SETTINGS

-
+

Refresh Data

Refreshes waitlist, course status, and other info with the latest data from @@ -132,6 +158,7 @@ export default function SettingsPage() {

- LD Icon + LD Icon
@@ -107,9 +145,17 @@ export default function SettingsPage() { Shows an indicator for waitlisted, cancelled, and closed courses.

- + { + setShowCourseStatus(!showCourseStatus); + OptionsStore.set('enableCourseStatusChips', !showCourseStatus); + }} + />
+ +

@@ -119,26 +165,27 @@ export default function SettingsPage() { Shows the course's time and location in the extension's popup.

- -
-
-
-
-
Preview
-
-
- { + setShowTimeLocation(!showTimeLocation); + OptionsStore.set('enableTimeAndLocationInPopup', !showTimeLocation); + }} /> -
+ + + +
@@ -146,70 +193,98 @@ export default function SettingsPage() {

ADVANCED SETTINGS

-
-
-
-

Refresh Data

-

- Refreshes waitlist, course status, and other info with the latest data from - UT's site. -

+
+
+
+
+

Refresh Data

+

+ Refreshes waitlist, course status, and other info with the latest data + from UT's site. +

+
+
- -
- + -
-
-

Course Conflict Highlight

-

- Adds a red strikethrough to courses that have conflicting times. -

+
+
+

+ Course Conflict Highlight +

+

+ Adds a red strikethrough to courses that have conflicting times. +

+
+ { + setHighlightConflicts(!highlightConflicts); + OptionsStore.set('enableHighlightConflicts', !highlightConflicts); + }} + />
- -
- - -
-
-

- Load All Courses in Course Schedule -

-

- Loads all courses in the Course Schedule site by scrolling, instead of using - next/prev page buttons. -

+ + +
+
+

+ Load All Courses in Course Schedule +

+

+ Loads all courses in the Course Schedule site by scrolling, instead of + using next/prev page buttons. +

+
+ { + setLoadAllCourses(!loadAllCourses); + OptionsStore.set('enableScrollToLoad', !loadAllCourses); + }} + />
- -
- + -
-
-

Reset All Data

-

- Erases all schedules and courses you have. -

+
+
+

Reset All Data

+

+ Erases all schedules and courses you have. +

+
+
-
+ + + +
diff --git a/src/shared/storage/OptionsStore.ts b/src/shared/storage/OptionsStore.ts index 1fa61c436..12c148857 100644 --- a/src/shared/storage/OptionsStore.ts +++ b/src/shared/storage/OptionsStore.ts @@ -3,18 +3,27 @@ import { createSyncStore, debugStore } from 'chrome-extension-toolkit'; /** * A store that is used for storing user options */ -interface IOptionsStore { - /** whether we should automatically highlight conflicts on the course schedule page */ - shouldHighlightConflicts: boolean; +export interface IOptionsStore { + /** whether we should enable course status chips (indicator for waitlisted, cancelled, and closed courses) */ + enableCourseStatusChips: boolean; + + /** whether we should enable course's time and location in the extension's popup */ + enableTimeAndLocationInPopup: boolean; + + /** whether we should automatically highlight conflicts on the course schedule page (adds a red strikethrough to courses that have conflicting times) */ + enableHighlightConflicts: boolean; + /** whether we should automatically scroll to load more courses on the course schedule page (without having to click next) */ - shouldScrollToLoad: boolean; + enableScrollToLoad: boolean; // url: URL; } export const OptionsStore = createSyncStore({ - shouldHighlightConflicts: true, - shouldScrollToLoad: true, + enableCourseStatusChips: false, + enableTimeAndLocationInPopup: false, + enableHighlightConflicts: true, + enableScrollToLoad: true, }); // Clothing retailer right diff --git a/src/views/components/common/SwitchButton.tsx b/src/views/components/common/SwitchButton.tsx index ab8b4d16e..e3844e71d 100644 --- a/src/views/components/common/SwitchButton.tsx +++ b/src/views/components/common/SwitchButton.tsx @@ -34,7 +34,7 @@ const SwitchButton = ({ isChecked = true, onChange }: ToggleSwitchProps): JSX.El checked={enabled} onChange={handleChange} className={`${enabled ? 'bg-[#579D42]' : 'bg-gray-400'} - relative inline-flex items-center h-8 w-13 rounded-full transition-colors ease-in-out duration-200`} + relative inline-flex items-center h-8 w-13 rounded-full transition-colors ease-in-out duration-200 min-w-[52px]`} > Date: Thu, 3 Oct 2024 23:16:54 -0500 Subject: [PATCH 05/23] feat: add courseStatusChips functionality --- src/pages/options/Settings.tsx | 1 - .../components/common/PopupCourseBlock.tsx | 27 +++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/pages/options/Settings.tsx b/src/pages/options/Settings.tsx index d54b6d276..290e38cb8 100644 --- a/src/pages/options/Settings.tsx +++ b/src/pages/options/Settings.tsx @@ -1,7 +1,6 @@ import type { IOptionsStore } from '@shared/storage/OptionsStore'; import { OptionsStore } from '@shared/storage/OptionsStore'; import { getCourseColors } from '@shared/util/colors'; -import { enableCourseStatusChips } from '@shared/util/experimental'; import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell'; import { Button } from '@views/components/common/Button'; import Divider from '@views/components/common/Divider'; diff --git a/src/views/components/common/PopupCourseBlock.tsx b/src/views/components/common/PopupCourseBlock.tsx index 6ddbf13e1..988a7ab8b 100644 --- a/src/views/components/common/PopupCourseBlock.tsx +++ b/src/views/components/common/PopupCourseBlock.tsx @@ -1,14 +1,15 @@ import type { DraggableProvidedDragHandleProps } from '@hello-pangea/dnd'; import { background } from '@shared/messages'; +import { OptionsStore } from '@shared/storage/OptionsStore'; import type { Course } from '@shared/types/Course'; import { Status } from '@shared/types/Course'; import type { CourseColors } from '@shared/types/ThemeColors'; import { pickFontColor } from '@shared/util/colors'; -import { enableCourseStatusChips } from '@shared/util/experimental'; +// import { enableCourseStatusChips } from '@shared/util/experimental'; import { StatusIcon } from '@shared/util/icons'; import Text from '@views/components/common/Text/Text'; import clsx from 'clsx'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import DragIndicatorIcon from '~icons/material-symbols/drag-indicator'; @@ -22,6 +23,13 @@ export interface PopupCourseBlockProps { dragHandleProps?: DraggableProvidedDragHandleProps; } +/** + * Initializes the course status chips. + * + * @returns {Promise} A promise that resolves to a boolean indicating whether the course status chips are enabled. + */ +const initCourseStatusChips = async () => await OptionsStore.get('enableCourseStatusChips'); + /** * The "course block" to be used in the extension popup. * @@ -33,6 +41,21 @@ export default function PopupCourseBlock({ colors, dragHandleProps, }: PopupCourseBlockProps): JSX.Element { + const [enableCourseStatusChips, setEnableCourseStatusChips] = useState(false); + + useEffect(() => { + initCourseStatusChips().then(setEnableCourseStatusChips); + + const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => { + setEnableCourseStatusChips(newValue); + // console.log('enableCourseStatusChips', newValue); + }); + + return () => { + OptionsStore.removeListener(l1); + }; + }, []); + // text-white or text-black based on secondaryColor const fontColor = pickFontColor(colors.primaryColor); const formattedUniqueId = course.uniqueId.toString().padStart(5, '0'); From c9086eb8267e298f548c23df518b86b6df0feeb5 Mon Sep 17 00:00:00 2001 From: doprz <52579214+doprz@users.noreply.github.com> Date: Fri, 4 Oct 2024 13:46:48 -0500 Subject: [PATCH 06/23] feat: migrate experimental settings to proper settings --- src/pages/options/Settings.tsx | 100 +++++++++++++----- src/shared/storage/OptionsStore.ts | 17 ++- src/shared/util/experimental.ts | 3 - src/views/components/PopupMain.tsx | 29 ++++- .../calendar/CalendarCourseCell.tsx | 19 +++- .../components/calendar/CalenderHeader.tsx | 31 +++++- .../components/common/PopupCourseBlock.tsx | 12 +-- 7 files changed, 162 insertions(+), 49 deletions(-) delete mode 100644 src/shared/util/experimental.ts diff --git a/src/pages/options/Settings.tsx b/src/pages/options/Settings.tsx index 290e38cb8..25521f995 100644 --- a/src/pages/options/Settings.tsx +++ b/src/pages/options/Settings.tsx @@ -1,5 +1,5 @@ import type { IOptionsStore } from '@shared/storage/OptionsStore'; -import { OptionsStore } from '@shared/storage/OptionsStore'; +import { initSettings, OptionsStore } from '@shared/storage/OptionsStore'; import { getCourseColors } from '@shared/util/colors'; import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell'; import { Button } from '@views/components/common/Button'; @@ -8,6 +8,10 @@ import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot' import { SmallLogo } from '@views/components/common/LogoIcon'; import PopupCourseBlock from '@views/components/common/PopupCourseBlock'; import SwitchButton from '@views/components/common/SwitchButton'; +import Text from '@views/components/common/Text/Text'; +import useSchedules from '@views/hooks/useSchedules'; +import { getUpdatedAtDateTimeString } from '@views/lib/getUpdatedAtDateTimeString'; +import clsx from 'clsx'; import React, { useCallback, useEffect, useState } from 'react'; import { ExampleCourse } from 'src/stories/components/ConflictsWithWarning.stories'; @@ -73,18 +77,6 @@ const useDevMode = (targetCount: number): [boolean, () => void] => { return [active, incrementCount]; }; -/** - * Initializes the settings by retrieving the values from the OptionsStore. - * @returns {Promise} A promise that resolves to an object satisfying the IOptionsStore interface. - */ -const initSettings = async () => - ({ - enableCourseStatusChips: await OptionsStore.get('enableCourseStatusChips'), - enableTimeAndLocationInPopup: await OptionsStore.get('enableTimeAndLocationInPopup'), - enableHighlightConflicts: await OptionsStore.get('enableHighlightConflicts'), - enableScrollToLoad: await OptionsStore.get('enableScrollToLoad'), - }) satisfies IOptionsStore; - /** * Renders the settings page for the UTRP (UT Registration Plus) extension. * Allows customization options and displays credits for the development team. @@ -92,10 +84,14 @@ const initSettings = async () => * @returns The JSX element representing the settings page. */ export default function SettingsPage() { - const [showCourseStatus, setShowCourseStatus] = useState(false); + const [enableCourseStatusChips, setEnableCourseStatusChips] = useState(false); const [showTimeLocation, setShowTimeLocation] = useState(false); const [highlightConflicts, setHighlightConflicts] = useState(false); const [loadAllCourses, setLoadAllCourses] = useState(false); + const [enableDataRefreshing, setEnableDataRefreshing] = useState(false); + + const [activeSchedule, schedules] = useSchedules(); + const [isRefreshing, setIsRefreshing] = useState(false); useEffect(() => { initSettings().then( @@ -104,13 +100,50 @@ export default function SettingsPage() { enableTimeAndLocationInPopup, enableHighlightConflicts, enableScrollToLoad, + enableDataRefreshing, }) => { - setShowCourseStatus(enableCourseStatusChips); + setEnableCourseStatusChips(enableCourseStatusChips); setShowTimeLocation(enableTimeAndLocationInPopup); setHighlightConflicts(enableHighlightConflicts); setLoadAllCourses(enableScrollToLoad); + setEnableDataRefreshing(enableDataRefreshing); } ); + + // Listen for changes in the settings + const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => { + setEnableCourseStatusChips(newValue); + // console.log('enableCourseStatusChips', newValue); + }); + + const l2 = OptionsStore.listen('enableTimeAndLocationInPopup', async ({ newValue }) => { + setShowTimeLocation(newValue); + // console.log('enableTimeAndLocationInPopup', newValue); + }); + + const l3 = OptionsStore.listen('enableHighlightConflicts', async ({ newValue }) => { + setHighlightConflicts(newValue); + // console.log('enableHighlightConflicts', newValue); + }); + + const l4 = OptionsStore.listen('enableScrollToLoad', async ({ newValue }) => { + setLoadAllCourses(newValue); + // console.log('enableScrollToLoad', newValue); + }); + + const l5 = OptionsStore.listen('enableDataRefreshing', async ({ newValue }) => { + setEnableDataRefreshing(newValue); + // console.log('enableDataRefreshing', newValue); + }); + + // Remove listeners when the component is unmounted + return () => { + OptionsStore.removeListener(l1); + OptionsStore.removeListener(l2); + OptionsStore.removeListener(l3); + OptionsStore.removeListener(l4); + OptionsStore.removeListener(l5); + }; }, []); const [devMode, toggleDevMode] = useDevMode(10); @@ -128,6 +161,7 @@ export default function SettingsPage() {

UTRP SETTINGS & CREDITS PAGE

+ {/* TODO: this icon doesn't show up in prod builds */} LD Icon @@ -145,10 +179,10 @@ export default function SettingsPage() {

{ - setShowCourseStatus(!showCourseStatus); - OptionsStore.set('enableCourseStatusChips', !showCourseStatus); + setEnableCourseStatusChips(!enableCourseStatusChips); + OptionsStore.set('enableCourseStatusChips', !enableCourseStatusChips); }} /> @@ -207,6 +241,7 @@ export default function SettingsPage() { color='ut-black' icon={RefreshIcon} onClick={() => console.log('Refresh clicked')} + disabled={!enableDataRefreshing} > Refresh @@ -273,16 +308,25 @@ export default function SettingsPage() { - + + DATA LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)} + + - - - - -
-
-

- Course Conflict Highlight -

-

- Adds a red strikethrough to courses that have conflicting times. -

-
- { - setHighlightConflicts(!highlightConflicts); - OptionsStore.set('enableHighlightConflicts', !highlightConflicts); - }} - /> -
- - - -
-
-

- Load All Courses in Course Schedule -

-

- Loads all courses in the Course Schedule site by scrolling, instead of - using next/prev page buttons. -

-
- { - setLoadAllCourses(!loadAllCourses); - OptionsStore.set('enableScrollToLoad', !loadAllCourses); - }} - /> -
- - - -
-
-

Reset All Data

-

- Erases all schedules and courses you have. -

-
- -
- - -
- - DATA LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)} - -
- - 01234 MWF 10:00 AM - 11:00 AM UTC 1.234 - -
- - - - - -
-

- Developer Mode -

-
- - -
-
-

LONGHORN DEVELOPERS ADMINS

-
- {LONGHORN_DEVELOPERS_ADMINS.map(admin => ( -
-

- window.open(`https://github.com/${admin.githubUsername}`, '_blank') - } - > - {admin.name} -

-

{admin.role}

- {showGitHubStats && adminGitHubStats[admin.githubUsername] && ( -
-

GitHub Stats (UTRP repo):

-

- Merged PRs: {adminGitHubStats[admin.githubUsername]?.mergedPRs} -

-

- Commits: {adminGitHubStats[admin.githubUsername]?.commits} -

-

- {adminGitHubStats[admin.githubUsername]?.linesAdded} ++ -

-

- {adminGitHubStats[admin.githubUsername]?.linesDeleted} -- -

-
- )} -
- ))} -
-
-
-

UTRP CONTRIBUTERS

-
- {contributors.map(username => ( -
-

window.open(`https://github.com/${username}`, '_blank')} - > - @{username} -

-

Contributor

- {showGitHubStats && userGitHubStats[username] && ( -
-

GitHub Stats (UTRP repo):

-

- Merged PRs: {userGitHubStats[username]?.mergedPRs} -

-

Commits: {userGitHubStats[username]?.commits}

-

- {userGitHubStats[username]?.linesAdded} ++ -

-

- {userGitHubStats[username]?.linesDeleted} -- -

-
- )} -
- ))} -
-
-
- - + + + ); } diff --git a/src/views/components/Settings.tsx b/src/views/components/Settings.tsx deleted file mode 100644 index 9ded32bc9..000000000 --- a/src/views/components/Settings.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; - -type Props = { - className?: string; -}; - -/** - * Component to hold everything for the settings page - * @param props className - * @returns The content for the settings page - */ -export default function Settings({ className }: Props): JSX.Element { - // TODO: Implement the settings page - return
this will be finished laterrrrrrr
; -} diff --git a/src/pages/options/DevMode.tsx b/src/views/components/settings/DevMode.tsx similarity index 100% rename from src/pages/options/DevMode.tsx rename to src/views/components/settings/DevMode.tsx diff --git a/src/pages/options/Preview.tsx b/src/views/components/settings/Preview.tsx similarity index 100% rename from src/pages/options/Preview.tsx rename to src/views/components/settings/Preview.tsx diff --git a/src/views/components/settings/Settings.tsx b/src/views/components/settings/Settings.tsx new file mode 100644 index 000000000..8ff73836a --- /dev/null +++ b/src/views/components/settings/Settings.tsx @@ -0,0 +1,421 @@ +import { initSettings, OptionsStore } from '@shared/storage/OptionsStore'; +import { getCourseColors } from '@shared/util/colors'; +import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell'; +import { Button } from '@views/components/common/Button'; +import { usePrompt } from '@views/components/common/DialogProvider/DialogProvider'; +import Divider from '@views/components/common/Divider'; +import { SmallLogo } from '@views/components/common/LogoIcon'; +import PopupCourseBlock from '@views/components/common/PopupCourseBlock'; +import SwitchButton from '@views/components/common/SwitchButton'; +import Text from '@views/components/common/Text/Text'; +import { LONGHORN_DEVELOPERS_ADMINS, useGitHubStats } from '@views/hooks/useGitHubStats'; +import useSchedules from '@views/hooks/useSchedules'; +import { getUpdatedAtDateTimeString } from '@views/lib/getUpdatedAtDateTimeString'; +import clsx from 'clsx'; +import React, { useCallback, useEffect, useState } from 'react'; +import { ExampleCourse } from 'src/stories/components/ConflictsWithWarning.stories'; + +import DeleteForeverIcon from '~icons/material-symbols/delete-forever'; +import RefreshIcon from '~icons/material-symbols/refresh'; + +import DevMode from './DevMode'; +import Preview from './Preview'; + +/** + * Custom hook for enabling developer mode. + * + * @param targetCount - The target count to activate developer mode. + * @returns A tuple containing a boolean indicating if developer mode is active and a function to increment the count. + */ +const useDevMode = (targetCount: number): [boolean, () => void] => { + const [count, setCount] = useState(0); + const [active, setActive] = useState(false); + const [lastClick, setLastClick] = useState(0); + + const incrementCount = useCallback(() => { + const now = Date.now(); + if (now - lastClick < 500) { + setCount(prevCount => { + const newCount = prevCount + 1; + if (newCount === targetCount) { + setActive(true); + } + return newCount; + }); + } else { + setCount(1); + } + setLastClick(now); + }, [lastClick, targetCount]); + + useEffect(() => { + const timer = setTimeout(() => setCount(0), 3000); + return () => clearTimeout(timer); + }, [count]); + + return [active, incrementCount]; +}; + +/** + * Component for managing user settings and preferences. + * + * @returns The Settings component. + */ +export default function Settings(): JSX.Element { + const [enableCourseStatusChips, setEnableCourseStatusChips] = useState(false); + const [showTimeLocation, setShowTimeLocation] = useState(false); + const [highlightConflicts, setHighlightConflicts] = useState(false); + const [loadAllCourses, setLoadAllCourses] = useState(false); + const [enableDataRefreshing, setEnableDataRefreshing] = useState(false); + + // Toggle GitHub stats when the user presses the 'S' key + const [showGitHubStats, setShowGitHubStats] = useState(false); + const { adminGitHubStats, userGitHubStats, contributors } = useGitHubStats(showGitHubStats); + + const [activeSchedule] = useSchedules(); + // const [isRefreshing, setIsRefreshing] = useState(false); + + const showDialog = usePrompt(); + + useEffect(() => { + initSettings().then( + ({ + enableCourseStatusChips, + enableTimeAndLocationInPopup, + enableHighlightConflicts, + enableScrollToLoad, + enableDataRefreshing, + }) => { + setEnableCourseStatusChips(enableCourseStatusChips); + setShowTimeLocation(enableTimeAndLocationInPopup); + setHighlightConflicts(enableHighlightConflicts); + setLoadAllCourses(enableScrollToLoad); + setEnableDataRefreshing(enableDataRefreshing); + } + ); + + const handleKeyPress = (event: KeyboardEvent) => { + if (event.key === 'S' || event.key === 's') { + setShowGitHubStats(prev => !prev); + } + }; + + window.addEventListener('keydown', handleKeyPress); + + // Listen for changes in the settings + const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => { + setEnableCourseStatusChips(newValue); + // console.log('enableCourseStatusChips', newValue); + }); + + const l2 = OptionsStore.listen('enableTimeAndLocationInPopup', async ({ newValue }) => { + setShowTimeLocation(newValue); + // console.log('enableTimeAndLocationInPopup', newValue); + }); + + const l3 = OptionsStore.listen('enableHighlightConflicts', async ({ newValue }) => { + setHighlightConflicts(newValue); + // console.log('enableHighlightConflicts', newValue); + }); + + const l4 = OptionsStore.listen('enableScrollToLoad', async ({ newValue }) => { + setLoadAllCourses(newValue); + // console.log('enableScrollToLoad', newValue); + }); + + const l5 = OptionsStore.listen('enableDataRefreshing', async ({ newValue }) => { + setEnableDataRefreshing(newValue); + // console.log('enableDataRefreshing', newValue); + }); + + // Remove listeners when the component is unmounted + return () => { + OptionsStore.removeListener(l1); + OptionsStore.removeListener(l2); + OptionsStore.removeListener(l3); + OptionsStore.removeListener(l4); + OptionsStore.removeListener(l5); + + window.removeEventListener('keydown', handleKeyPress); + }; + }, []); + + const handleEraseAll = () => { + showDialog({ + title: 'Erase All Course/Schedule Data', + description: ( + <> +

+ Are you sure you want to erase all schedules and courses you have? This action is permanent and + cannot be undone. +

+
+

Note: This will not erase your settings and preferences.

+ + ), + // eslint-disable-next-line react/no-unstable-nested-components + buttons: close => ( + + ), + }); + }; + + const [devMode, toggleDevMode] = useDevMode(10); + + if (devMode) { + return ; + } + + return ( +
+
+
+ + +

UTRP SETTINGS & CREDITS PAGE

+
+ {/* TODO: this icon doesn't show up in prod builds */} + LD Icon +
+ +
+
+
+

CUSTOMIZATION OPTIONS

+
+
+
+
+

Show Course Status

+

+ Shows an indicator for waitlisted, cancelled, and closed courses. +

+
+ { + setEnableCourseStatusChips(!enableCourseStatusChips); + OptionsStore.set('enableCourseStatusChips', !enableCourseStatusChips); + }} + /> +
+ + + +
+
+

+ Show Time & Location in Popup +

+

+ Shows the course's time and location in the extension's popup. +

+
+ { + setShowTimeLocation(!showTimeLocation); + OptionsStore.set('enableTimeAndLocationInPopup', !showTimeLocation); + }} + /> +
+
+ + + + +
+
+ + + +
+

ADVANCED SETTINGS

+
+
+
+
+

Refresh Data

+

+ Refreshes waitlist, course status, and other info with the latest data from + UT's site. +

+
+ +
+ + + +
+
+

Course Conflict Highlight

+

+ Adds a red strikethrough to courses that have conflicting times. +

+
+ { + setHighlightConflicts(!highlightConflicts); + OptionsStore.set('enableHighlightConflicts', !highlightConflicts); + }} + /> +
+ + + +
+
+

+ Load All Courses in Course Schedule +

+

+ Loads all courses in the Course Schedule site by scrolling, instead of using + next/prev page buttons. +

+
+ { + setLoadAllCourses(!loadAllCourses); + OptionsStore.set('enableScrollToLoad', !loadAllCourses); + }} + /> +
+ + + +
+
+

Reset All Data

+

+ Erases all schedules and courses you have. +

+
+ +
+
+ +
+ + DATA LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)} + +
+ + 01234 MWF 10:00 AM - 11:00 AM UTC 1.234 + +
+
+
+ + + +
+

+ Developer Mode +

+
+
+ +
+
+

LONGHORN DEVELOPERS ADMINS

+
+ {LONGHORN_DEVELOPERS_ADMINS.map(admin => ( +
+

+ window.open(`https://github.com/${admin.githubUsername}`, '_blank') + } + > + {admin.name} +

+

{admin.role}

+ {showGitHubStats && adminGitHubStats[admin.githubUsername] && ( +
+

GitHub Stats (UTRP repo):

+

+ Merged PRs: {adminGitHubStats[admin.githubUsername]?.mergedPRs} +

+

+ Commits: {adminGitHubStats[admin.githubUsername]?.commits} +

+

+ {adminGitHubStats[admin.githubUsername]?.linesAdded} ++ +

+

+ {adminGitHubStats[admin.githubUsername]?.linesDeleted} -- +

+
+ )} +
+ ))} +
+
+
+

UTRP CONTRIBUTERS

+
+ {contributors.map(username => ( +
+

window.open(`https://github.com/${username}`, '_blank')} + > + @{username} +

+

Contributor

+ {showGitHubStats && userGitHubStats[username] && ( +
+

GitHub Stats (UTRP repo):

+

+ Merged PRs: {userGitHubStats[username]?.mergedPRs} +

+

Commits: {userGitHubStats[username]?.commits}

+

+ {userGitHubStats[username]?.linesAdded} ++ +

+

+ {userGitHubStats[username]?.linesDeleted} -- +

+
+ )} +
+ ))} +
+
+
+
+
+ ); +} From 0a5cd2962c9d146089f288b7e0f1d789fc90ced4 Mon Sep 17 00:00:00 2001 From: doprz <52579214+doprz@users.noreply.github.com> Date: Sat, 5 Oct 2024 13:30:26 -0500 Subject: [PATCH 16/23] fix: import --- src/stories/components/Settings.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stories/components/Settings.stories.tsx b/src/stories/components/Settings.stories.tsx index 64ae5f82d..a3964bee1 100644 --- a/src/stories/components/Settings.stories.tsx +++ b/src/stories/components/Settings.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react'; -import Settings from '@views/components/Settings'; +import Settings from '@views/components/settings/Settings'; const meta = { title: 'Components/Common/Settings', From 698990dcb9dface7a99cf1d44deab0fb5ebf9e73 Mon Sep 17 00:00:00 2001 From: doprz <52579214+doprz@users.noreply.github.com> Date: Sat, 5 Oct 2024 17:38:23 -0500 Subject: [PATCH 17/23] test: this commit has issues --- src/pages/background/lib/deleteSchedule.ts | 10 ++++++++++ src/views/components/settings/Settings.tsx | 12 ++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/pages/background/lib/deleteSchedule.ts b/src/pages/background/lib/deleteSchedule.ts index 2f627fbab..b02da6dce 100644 --- a/src/pages/background/lib/deleteSchedule.ts +++ b/src/pages/background/lib/deleteSchedule.ts @@ -28,3 +28,13 @@ export default async function deleteSchedule(scheduleId: string): Promise { + await UserScheduleStore.set('schedules', []); + await UserScheduleStore.set('activeIndex', 0); +} diff --git a/src/views/components/settings/Settings.tsx b/src/views/components/settings/Settings.tsx index 8ff73836a..e3ed73494 100644 --- a/src/views/components/settings/Settings.tsx +++ b/src/views/components/settings/Settings.tsx @@ -1,3 +1,4 @@ +import { deleteAllSchedules } from '@pages/background/lib/deleteSchedule'; import { initSettings, OptionsStore } from '@shared/storage/OptionsStore'; import { getCourseColors } from '@shared/util/colors'; import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell'; @@ -154,8 +155,15 @@ export default function Settings(): JSX.Element { ), // eslint-disable-next-line react/no-unstable-nested-components - buttons: close => ( - ), From 505243e7570374695fea5693725fb2c001c9e615 Mon Sep 17 00:00:00 2001 From: doprz <52579214+doprz@users.noreply.github.com> Date: Sat, 5 Oct 2024 18:37:27 -0500 Subject: [PATCH 18/23] fix: no schedule bug --- src/pages/background/lib/deleteSchedule.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/background/lib/deleteSchedule.ts b/src/pages/background/lib/deleteSchedule.ts index b02da6dce..e62c5141f 100644 --- a/src/pages/background/lib/deleteSchedule.ts +++ b/src/pages/background/lib/deleteSchedule.ts @@ -1,5 +1,7 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; +import createSchedule from './createSchedule'; + /** * Deletes a schedule with the specified name. * @@ -37,4 +39,5 @@ export default async function deleteSchedule(scheduleId: string): Promise { await UserScheduleStore.set('schedules', []); await UserScheduleStore.set('activeIndex', 0); + await createSchedule('Schedule 1'); } From 39c59ff9242ecbef32bfb7fe5f4a11312713d281 Mon Sep 17 00:00:00 2001 From: doprz <52579214+doprz@users.noreply.github.com> Date: Sat, 5 Oct 2024 18:48:00 -0500 Subject: [PATCH 19/23] fix: longhorn developers icon not rendering in prod builds --- src/views/components/settings/Settings.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/views/components/settings/Settings.tsx b/src/views/components/settings/Settings.tsx index e3ed73494..c17783e12 100644 --- a/src/views/components/settings/Settings.tsx +++ b/src/views/components/settings/Settings.tsx @@ -22,6 +22,8 @@ import RefreshIcon from '~icons/material-symbols/refresh'; import DevMode from './DevMode'; import Preview from './Preview'; +const LDIconURL = new URL('/src/assets/LD-icon.png', import.meta.url).href; + /** * Custom hook for enabling developer mode. * @@ -184,8 +186,7 @@ export default function Settings(): JSX.Element {

UTRP SETTINGS & CREDITS PAGE

- {/* TODO: this icon doesn't show up in prod builds */} - LD Icon + LD Icon
From a05ab175099a957095d56ab4ddfdf9429410ea60 Mon Sep 17 00:00:00 2001 From: doprz <52579214+doprz@users.noreply.github.com> Date: Sun, 6 Oct 2024 22:47:14 -0500 Subject: [PATCH 20/23] feat(pr-review): fix UI and comment out experimental code --- package.json | 2 + pnpm-lock.yaml | 54 ++++++++++----- src/debug/index.tsx | 2 + src/pages/calendar/CalendarMain.tsx | 2 + src/pages/options/Settings.tsx | 2 + src/views/components/PopupMain.tsx | 2 + .../injected/TableRow/TableRow.module.scss | 7 ++ .../components/injected/TableRow/TableRow.tsx | 31 ++++++++- src/views/components/settings/Settings.tsx | 67 ++++++++++++------- 9 files changed, 126 insertions(+), 43 deletions(-) diff --git a/package.json b/package.json index 737a9ff0c..7be669628 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "highcharts-react-official": "^3.2.1", "html-to-image": "^1.11.11", "husky": "^9.0.11", + "kc-dabr-wasm": "^0.1.2", "nanoid": "^5.0.6", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -48,6 +49,7 @@ "@commitlint/types": "^19.0.3", "@crxjs/vite-plugin": "2.0.0-beta.21", "@iconify-json/bi": "^1.1.23", + "@iconify-json/iconoir": "^1.2.1", "@iconify-json/material-symbols": "^1.1.73", "@iconify-json/ri": "^1.1.20", "@storybook/addon-designs": "^8.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index feec6902e..deb297c16 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,6 +49,9 @@ importers: husky: specifier: ^9.0.11 version: 9.0.11 + kc-dabr-wasm: + specifier: ^0.1.2 + version: 0.1.2 nanoid: specifier: ^5.0.6 version: 5.0.6 @@ -86,6 +89,9 @@ importers: '@iconify-json/bi': specifier: ^1.1.23 version: 1.1.23 + '@iconify-json/iconoir': + specifier: ^1.2.1 + version: 1.2.1 '@iconify-json/material-symbols': specifier: ^1.1.73 version: 1.1.73 @@ -112,7 +118,7 @@ importers: version: 8.1.1(prettier@3.2.5)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.13.0)(typescript@5.4.3)(vite@5.1.4(@types/node@20.12.12)(sass@1.71.1)(terser@5.28.1)) '@storybook/test': specifier: ^8.1.1 - version: 8.1.1(vitest@1.3.1(@types/node@20.12.12)(@vitest/ui@1.3.1)(sass@1.71.1)(terser@5.28.1)) + version: 8.1.1(vitest@1.3.1) '@svgr/core': specifier: ^8.1.0 version: 8.1.0(typescript@5.4.3) @@ -172,7 +178,7 @@ importers: version: 3.6.0(@swc/helpers@0.5.11)(vite@5.1.4(@types/node@20.12.12)(sass@1.71.1)(terser@5.28.1)) '@vitest/coverage-v8': specifier: ^1.3.1 - version: 1.3.1(vitest@1.3.1(@types/node@20.12.12)(@vitest/ui@1.3.1)(sass@1.71.1)(terser@5.28.1)) + version: 1.3.1(vitest@1.3.1) '@vitest/ui': specifier: ^1.3.1 version: 1.3.1(vitest@1.3.1) @@ -196,13 +202,13 @@ importers: version: 8.57.0 eslint-config-airbnb: specifier: ^19.0.4 - version: 19.0.4(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint-plugin-jsx-a11y@6.8.0(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.0(eslint@8.57.0))(eslint-plugin-react@7.33.2(eslint@8.57.0))(eslint@8.57.0) + version: 19.0.4(eslint-plugin-import@2.29.1)(eslint-plugin-jsx-a11y@6.8.0(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.0(eslint@8.57.0))(eslint-plugin-react@7.33.2(eslint@8.57.0))(eslint@8.57.0) eslint-config-airbnb-base: specifier: ^15.0.0 - version: 15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0) + version: 15.0.0(eslint-plugin-import@2.29.1)(eslint@8.57.0) eslint-config-airbnb-typescript: specifier: ^17.1.0 - version: 17.1.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint@8.57.0)(typescript@5.4.3))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0) + version: 17.1.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint@8.57.0)(typescript@5.4.3))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0) eslint-config-prettier: specifier: ^9.1.0 version: 9.1.0(eslint@8.57.0) @@ -1369,6 +1375,9 @@ packages: '@iconify-json/bi@1.1.23': resolution: {integrity: sha512-1te+g9ZzI+PU1Lv6Xerd3XPXf4DE6g3TvDL2buIopTAfrauPHyXCHPFQMrzoQVNrVPCpN3rv3vBtJMPyBwJ9IA==} + '@iconify-json/iconoir@1.2.1': + resolution: {integrity: sha512-x55gpORwMGkmmT9UO11rzfMOp40k0ggQnPiOoh9axbyuHrkFMN7pdoCbaXkzqAdShcoI1dLzARbdqXi2sAPJXQ==} + '@iconify-json/material-symbols@1.1.73': resolution: {integrity: sha512-3ioEMQvxUR0eWg3jCxeydowS7xlqJzUHqeKJg29Z5HN15ZBYniQhHYrDCRpmFjopgwxox9OIsoonBiBSLV4EMA==} @@ -4511,6 +4520,9 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + kc-dabr-wasm@0.1.2: + resolution: {integrity: sha512-RonEqnhqHLVo9b3E0fFlTFWgew3iOHUc5Qz67k4OVBwEbqRD4qjk06D+7jH6jLipChAEQ4pqJ6hUfu1Jdx+Naw==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -7897,6 +7909,10 @@ snapshots: dependencies: '@iconify/types': 2.0.0 + '@iconify-json/iconoir@1.2.1': + dependencies: + '@iconify/types': 2.0.0 + '@iconify-json/material-symbols@1.1.73': dependencies: '@iconify/types': 2.0.0 @@ -8843,14 +8859,14 @@ snapshots: - prettier - supports-color - '@storybook/test@8.1.1(vitest@1.3.1(@types/node@20.12.12)(@vitest/ui@1.3.1)(sass@1.71.1)(terser@5.28.1))': + '@storybook/test@8.1.1(vitest@1.3.1)': dependencies: '@storybook/client-logger': 8.1.1 '@storybook/core-events': 8.1.1 '@storybook/instrumenter': 8.1.1 '@storybook/preview-api': 8.1.1 '@testing-library/dom': 9.3.4 - '@testing-library/jest-dom': 6.4.2(vitest@1.3.1(@types/node@20.12.12)(@vitest/ui@1.3.1)(sass@1.71.1)(terser@5.28.1)) + '@testing-library/jest-dom': 6.4.2(vitest@1.3.1) '@testing-library/user-event': 14.5.2(@testing-library/dom@9.3.4) '@vitest/expect': 1.3.1 '@vitest/spy': 1.3.1 @@ -9022,7 +9038,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.2(vitest@1.3.1(@types/node@20.12.12)(@vitest/ui@1.3.1)(sass@1.71.1)(terser@5.28.1))': + '@testing-library/jest-dom@6.4.2(vitest@1.3.1)': dependencies: '@adobe/css-tools': 4.3.3 '@babel/runtime': 7.24.0 @@ -9599,7 +9615,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@1.3.1(vitest@1.3.1(@types/node@20.12.12)(@vitest/ui@1.3.1)(sass@1.71.1)(terser@5.28.1))': + '@vitest/coverage-v8@1.3.1(vitest@1.3.1)': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -10813,7 +10829,7 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.29.1)(eslint@8.57.0): dependencies: confusing-browser-globals: 1.0.11 eslint: 8.57.0 @@ -10822,18 +10838,18 @@ snapshots: object.entries: 1.1.7 semver: 6.3.1 - eslint-config-airbnb-typescript@17.1.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint@8.57.0)(typescript@5.4.3))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-config-airbnb-typescript@17.1.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint@8.57.0)(typescript@5.4.3))(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0): dependencies: '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint@8.57.0)(typescript@5.4.3) '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.4.3) eslint: 8.57.0 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) - eslint-config-airbnb@19.0.4(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint-plugin-jsx-a11y@6.8.0(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.0(eslint@8.57.0))(eslint-plugin-react@7.33.2(eslint@8.57.0))(eslint@8.57.0): + eslint-config-airbnb@19.0.4(eslint-plugin-import@2.29.1)(eslint-plugin-jsx-a11y@6.8.0(eslint@8.57.0))(eslint-plugin-react-hooks@4.6.0(eslint@8.57.0))(eslint-plugin-react@7.33.2(eslint@8.57.0))(eslint@8.57.0): dependencies: eslint: 8.57.0 - eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.29.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.57.0) eslint-plugin-react: 7.33.2(eslint@8.57.0) @@ -10858,7 +10874,7 @@ snapshots: debug: 4.3.4 enhanced-resolve: 5.15.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.2 @@ -10870,7 +10886,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -10895,7 +10911,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.4.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) hasown: 2.0.1 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -11901,6 +11917,10 @@ snapshots: object.assign: 4.1.5 object.values: 1.1.7 + kc-dabr-wasm@0.1.2: + dependencies: + react: 18.3.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 diff --git a/src/debug/index.tsx b/src/debug/index.tsx index 19cd36c4c..5b36ab724 100644 --- a/src/debug/index.tsx +++ b/src/debug/index.tsx @@ -1,4 +1,5 @@ import { DevStore } from '@shared/storage/DevStore'; +import useKC_DABR_WASM from 'kc-dabr-wasm'; import React, { useEffect } from 'react'; import { createRoot } from 'react-dom/client'; @@ -77,6 +78,7 @@ function DevDashboard() { const [localStorage, setLocalStorage] = React.useState>({}); const [syncStorage, setSyncStorage] = React.useState>({}); const [sessionStorage, setSessionStorage] = React.useState>({}); + useKC_DABR_WASM(); useEffect(() => { const onVisibilityChange = () => { diff --git a/src/pages/calendar/CalendarMain.tsx b/src/pages/calendar/CalendarMain.tsx index 9d529dc7b..8373b80bf 100644 --- a/src/pages/calendar/CalendarMain.tsx +++ b/src/pages/calendar/CalendarMain.tsx @@ -3,6 +3,7 @@ import Calendar from '@views/components/calendar/Calendar'; import DialogProvider from '@views/components/common/DialogProvider/DialogProvider'; import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot'; import { MessageListener } from 'chrome-extension-toolkit'; +import useKC_DABR_WASM from 'kc-dabr-wasm'; import React, { useEffect } from 'react'; /** @@ -10,6 +11,7 @@ import React, { useEffect } from 'react'; * @returns entire page */ export default function CalendarMain() { + useKC_DABR_WASM(); useEffect(() => { const tabInfoListener = new MessageListener({ getTabInfo: ({ sendResponse }) => { diff --git a/src/pages/options/Settings.tsx b/src/pages/options/Settings.tsx index 6dbde73ea..b59419d81 100644 --- a/src/pages/options/Settings.tsx +++ b/src/pages/options/Settings.tsx @@ -1,6 +1,7 @@ import DialogProvider from '@views/components/common/DialogProvider/DialogProvider'; import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot'; import Settings from '@views/components/settings/Settings'; +import useKC_DABR_WASM from 'kc-dabr-wasm'; import React from 'react'; /** @@ -10,6 +11,7 @@ import React from 'react'; * @returns The JSX element representing the settings page. */ export default function SettingsPage() { + useKC_DABR_WASM(); return ( diff --git a/src/views/components/PopupMain.tsx b/src/views/components/PopupMain.tsx index 05074697e..382c668e2 100644 --- a/src/views/components/PopupMain.tsx +++ b/src/views/components/PopupMain.tsx @@ -10,6 +10,7 @@ import useSchedules, { getActiveSchedule, replaceSchedule, switchSchedule } from import { getUpdatedAtDateTimeString } from '@views/lib/getUpdatedAtDateTimeString'; import { openTabFromContentScript } from '@views/lib/openNewTabFromContentScript'; import clsx from 'clsx'; +import useKC_DABR_WASM from 'kc-dabr-wasm'; import React, { useEffect, useState } from 'react'; import CalendarIcon from '~icons/material-symbols/calendar-month'; @@ -29,6 +30,7 @@ import ScheduleListItem from './common/ScheduleListItem'; export default function PopupMain(): JSX.Element { const [enableCourseStatusChips, setEnableCourseStatusChips] = useState(false); const [enableDataRefreshing, setEnableDataRefreshing] = useState(false); + useKC_DABR_WASM(); useEffect(() => { initSettings().then(({ enableCourseStatusChips, enableDataRefreshing }) => { diff --git a/src/views/components/injected/TableRow/TableRow.module.scss b/src/views/components/injected/TableRow/TableRow.module.scss index cdfadda37..1518abf82 100644 --- a/src/views/components/injected/TableRow/TableRow.module.scss +++ b/src/views/components/injected/TableRow/TableRow.module.scss @@ -86,3 +86,10 @@ text-decoration: line-through; } } + +.isConflictNoLineThrough:not(.inActiveSchedule) { + > *:not(td:last-child) { + color: colors.$speedway_brick; + font-weight: normal; + } +} diff --git a/src/views/components/injected/TableRow/TableRow.tsx b/src/views/components/injected/TableRow/TableRow.tsx index 8ef20cd02..2bd6e4bf3 100644 --- a/src/views/components/injected/TableRow/TableRow.tsx +++ b/src/views/components/injected/TableRow/TableRow.tsx @@ -8,6 +8,7 @@ import ReactDOM from 'react-dom'; import RowIcon from '~icons/material-symbols/bar-chart-rounded'; import styles from './TableRow.module.scss'; +import { initSettings, OptionsStore } from 'src/shared/storage/OptionsStore'; interface Props { isSelected: boolean; @@ -25,9 +26,26 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P // the courses in the active schedule that conflict with the course for this row const [conflicts, setConflicts] = useState([]); + const [highlightConflicts, setHighlightConflicts] = useState(false); const { element, course } = row; + useEffect(() => { + initSettings().then(({ enableHighlightConflicts }) => { + setHighlightConflicts(enableHighlightConflicts); + }); + + const l1 = OptionsStore.listen('enableHighlightConflicts', async ({ newValue }) => { + setHighlightConflicts(newValue); + // console.log('enableHighlightConflicts', newValue); + }); + + // Remove listeners when the component is unmounted + return () => { + OptionsStore.removeListener(l1); + }; + }, []); + useEffect(() => { element.classList.add(styles.row!); element.classList.add('group'); @@ -72,14 +90,23 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P } } - element.classList[conflicts.length ? 'add' : 'remove'](styles.isConflict!); + // Clear conflict styling + element.classList.remove(styles.isConflict!); + element.classList.remove(styles.isConflictNoLineThrough!); + + if (highlightConflicts) { + element.classList[conflicts.length ? 'add' : 'remove'](styles.isConflict!); + } else { + element.classList[conflicts.length ? 'add' : 'remove'](styles.isConflictNoLineThrough!); + } + setConflicts(conflicts); return () => { element.classList.remove(styles.isConflict!); setConflicts([]); }; - }, [activeSchedule, course, element.classList]); + }, [activeSchedule, course, element.classList, highlightConflicts]); if (!container) { return null; diff --git a/src/views/components/settings/Settings.tsx b/src/views/components/settings/Settings.tsx index c17783e12..abd0162ec 100644 --- a/src/views/components/settings/Settings.tsx +++ b/src/views/components/settings/Settings.tsx @@ -1,12 +1,12 @@ import { deleteAllSchedules } from '@pages/background/lib/deleteSchedule'; import { initSettings, OptionsStore } from '@shared/storage/OptionsStore'; -import { getCourseColors } from '@shared/util/colors'; -import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell'; +// import { getCourseColors } from '@shared/util/colors'; +// import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell'; import { Button } from '@views/components/common/Button'; import { usePrompt } from '@views/components/common/DialogProvider/DialogProvider'; import Divider from '@views/components/common/Divider'; import { SmallLogo } from '@views/components/common/LogoIcon'; -import PopupCourseBlock from '@views/components/common/PopupCourseBlock'; +// import PopupCourseBlock from '@views/components/common/PopupCourseBlock'; import SwitchButton from '@views/components/common/SwitchButton'; import Text from '@views/components/common/Text/Text'; import { LONGHORN_DEVELOPERS_ADMINS, useGitHubStats } from '@views/hooks/useGitHubStats'; @@ -14,14 +14,16 @@ import useSchedules from '@views/hooks/useSchedules'; import { getUpdatedAtDateTimeString } from '@views/lib/getUpdatedAtDateTimeString'; import clsx from 'clsx'; import React, { useCallback, useEffect, useState } from 'react'; -import { ExampleCourse } from 'src/stories/components/ConflictsWithWarning.stories'; +import IconoirGitFork from '~icons/iconoir/git-fork'; +// import { ExampleCourse } from 'src/stories/components/ConflictsWithWarning.stories'; import DeleteForeverIcon from '~icons/material-symbols/delete-forever'; -import RefreshIcon from '~icons/material-symbols/refresh'; +// import RefreshIcon from '~icons/material-symbols/refresh'; import DevMode from './DevMode'; import Preview from './Preview'; +const manifest = chrome.runtime.getManifest(); const LDIconURL = new URL('/src/assets/LD-icon.png', import.meta.url).href; /** @@ -179,19 +181,27 @@ export default function Settings(): JSX.Element { } return ( -
-
+
+

UTRP SETTINGS & CREDITS PAGE

- LD Icon +
+
+ + + v{manifest.version} - {process.env.NODE_ENV} + +
+ LD Icon +
-
+
-
+ {/*

CUSTOMIZATION OPTIONS

@@ -246,13 +256,13 @@ export default function Settings(): JSX.Element {
- + */} -
+

ADVANCED SETTINGS

-
+ {/*

Refresh Data

@@ -271,11 +281,13 @@ export default function Settings(): JSX.Element {

- + */}
-

Course Conflict Highlight

+ + Course Conflict Highlight +

Adds a red strikethrough to courses that have conflicting times.

@@ -293,9 +305,9 @@ export default function Settings(): JSX.Element {
-

+ Load All Courses in Course Schedule -

+

Loads all courses in the Course Schedule site by scrolling, instead of using next/prev page buttons. @@ -314,7 +326,9 @@ export default function Settings(): JSX.Element {

-

Reset All Data

+ + Reset All Data +

Erases all schedules and courses you have.

@@ -361,15 +375,19 @@ export default function Settings(): JSX.Element {

LONGHORN DEVELOPERS ADMINS

{LONGHORN_DEVELOPERS_ADMINS.map(admin => ( -
-

+ window.open(`https://github.com/${admin.githubUsername}`, '_blank') } > {admin.name} -

+

{admin.role}

{showGitHubStats && adminGitHubStats[admin.githubUsername] && (
@@ -396,13 +414,14 @@ export default function Settings(): JSX.Element {

UTRP CONTRIBUTERS

{contributors.map(username => ( -
-

+ window.open(`https://github.com/${username}`, '_blank')} > @{username} -

+

Contributor

{showGitHubStats && userGitHubStats[username] && (
From dae69642c862d3759e1404f80c63df1966ae53e2 Mon Sep 17 00:00:00 2001 From: doprz <52579214+doprz@users.noreply.github.com> Date: Sun, 6 Oct 2024 22:59:35 -0500 Subject: [PATCH 21/23] chore: run lint and prettier --- src/views/components/injected/TableRow/TableRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/components/injected/TableRow/TableRow.tsx b/src/views/components/injected/TableRow/TableRow.tsx index 2bd6e4bf3..0df22ceb4 100644 --- a/src/views/components/injected/TableRow/TableRow.tsx +++ b/src/views/components/injected/TableRow/TableRow.tsx @@ -1,3 +1,4 @@ +import { initSettings, OptionsStore } from '@shared/storage/OptionsStore'; import type { Course, ScrapedRow } from '@shared/types/Course'; import type { UserSchedule } from '@shared/types/UserSchedule'; import ConflictsWithWarning from '@views/components/common/ConflictsWithWarning'; @@ -8,7 +9,6 @@ import ReactDOM from 'react-dom'; import RowIcon from '~icons/material-symbols/bar-chart-rounded'; import styles from './TableRow.module.scss'; -import { initSettings, OptionsStore } from 'src/shared/storage/OptionsStore'; interface Props { isSelected: boolean; From efd5199cbf49ca1a93a5116f3b24b4c39de61c16 Mon Sep 17 00:00:00 2001 From: doprz <52579214+doprz@users.noreply.github.com> Date: Tue, 8 Oct 2024 22:14:38 -0500 Subject: [PATCH 22/23] feat: add responsive design --- src/views/components/settings/Preview.tsx | 5 +- src/views/components/settings/Settings.tsx | 80 +++++++++++++--------- 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/src/views/components/settings/Preview.tsx b/src/views/components/settings/Preview.tsx index eac4d7aaf..cd8a4b75e 100644 --- a/src/views/components/settings/Preview.tsx +++ b/src/views/components/settings/Preview.tsx @@ -12,9 +12,8 @@ export interface PreviewProps { /** * Renders a preview component. * - * @component - * @param {PropsWithChildren} props - The component props. - * @returns {JSX.Element} The rendered preview component. + * @param props - The component props. + * @returns The rendered preview component. */ export default function Preview(props: PropsWithChildren): JSX.Element { const { children } = props; diff --git a/src/views/components/settings/Settings.tsx b/src/views/components/settings/Settings.tsx index abd0162ec..696c34a7d 100644 --- a/src/views/components/settings/Settings.tsx +++ b/src/views/components/settings/Settings.tsx @@ -23,6 +23,9 @@ import DeleteForeverIcon from '~icons/material-symbols/delete-forever'; import DevMode from './DevMode'; import Preview from './Preview'; +const DISPLAY_PREVIEWS = false; +const PREVIEW_SECTION_DIV_CLASSNAME = DISPLAY_PREVIEWS ? 'w-1/2 space-y-4' : 'flex-grow space-y-4'; + const manifest = chrome.runtime.getManifest(); const LDIconURL = new URL('/src/assets/LD-icon.png', import.meta.url).href; @@ -181,7 +184,7 @@ export default function Settings(): JSX.Element { } return ( -
+
@@ -199,8 +202,8 @@ export default function Settings(): JSX.Element {
-
-
+
+
{/*

CUSTOMIZATION OPTIONS

@@ -241,18 +244,20 @@ export default function Settings(): JSX.Element { />
- - - - + {DISPLAY_PREVIEWS && ( + + + + + )}
@@ -261,7 +266,7 @@ export default function Settings(): JSX.Element {

ADVANCED SETTINGS

-
+
{/*

Refresh Data

@@ -343,21 +348,23 @@ export default function Settings(): JSX.Element {
- -
- - DATA LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)} + {DISPLAY_PREVIEWS && ( + +
+ + DATA LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)} + +
+ + 01234 MWF 10:00 AM - 11:00 AM UTC 1.234 -
- - 01234 MWF 10:00 AM - 11:00 AM UTC 1.234 - -
+ + )}
@@ -370,10 +377,12 @@ export default function Settings(): JSX.Element {
-
+ + +

LONGHORN DEVELOPERS ADMINS

-
+
{LONGHORN_DEVELOPERS_ADMINS.map(admin => (

UTRP CONTRIBUTERS

-
+
{contributors.map(username => ( -
+
Date: Wed, 9 Oct 2024 15:54:19 -0500 Subject: [PATCH 23/23] feat: use @octokit/rest and fix GitHub stats --- package.json | 1 + pnpm-lock.yaml | 126 ++++++++++ src/views/components/settings/Settings.tsx | 122 ++++++---- src/views/hooks/useGitHubStats.ts | 156 ------------ src/views/lib/getGitHubStats.ts | 266 +++++++++++++++++++++ 5 files changed, 467 insertions(+), 204 deletions(-) delete mode 100644 src/views/hooks/useGitHubStats.ts create mode 100644 src/views/lib/getGitHubStats.ts diff --git a/package.json b/package.json index 155b395b6..295f0b6bc 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "dependencies": { "@headlessui/react": "^2.1.8", "@hello-pangea/dnd": "^17.0.0", + "@octokit/rest": "^21.0.2", "@unocss/vite": "^0.63.2", "@vitejs/plugin-react": "^4.3.2", "chrome-extension-toolkit": "^0.0.54", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 38f20952a..c94b11564 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,9 @@ importers: '@hello-pangea/dnd': specifier: ^17.0.0 version: 17.0.0(@types/react@18.3.11)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@octokit/rest': + specifier: ^21.0.2 + version: 21.0.2 '@unocss/vite': specifier: ^0.63.2 version: 0.63.3(patch_hash=5ptgy7mbavmjf7zwexb7dph4ji)(rollup@4.24.0)(vite@5.4.8(@types/node@22.7.4)(sass@1.79.4)(terser@5.28.1)) @@ -1211,6 +1214,58 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@octokit/auth-token@5.1.1': + resolution: {integrity: sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==} + engines: {node: '>= 18'} + + '@octokit/core@6.1.2': + resolution: {integrity: sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==} + engines: {node: '>= 18'} + + '@octokit/endpoint@10.1.1': + resolution: {integrity: sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==} + engines: {node: '>= 18'} + + '@octokit/graphql@8.1.1': + resolution: {integrity: sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==} + engines: {node: '>= 18'} + + '@octokit/openapi-types@22.2.0': + resolution: {integrity: sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==} + + '@octokit/plugin-paginate-rest@11.3.5': + resolution: {integrity: sha512-cgwIRtKrpwhLoBi0CUNuY83DPGRMaWVjqVI/bGKsLJ4PzyWZNaEmhHroI2xlrVXkk6nFv0IsZpOp+ZWSWUS2AQ==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-request-log@5.3.1': + resolution: {integrity: sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/plugin-rest-endpoint-methods@13.2.6': + resolution: {integrity: sha512-wMsdyHMjSfKjGINkdGKki06VEkgdEldIGstIEyGX0wbYHGByOwN/KiM+hAAlUwAtPkP3gvXtVQA9L3ITdV2tVw==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=6' + + '@octokit/request-error@6.1.5': + resolution: {integrity: sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==} + engines: {node: '>= 18'} + + '@octokit/request@9.1.3': + resolution: {integrity: sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==} + engines: {node: '>= 18'} + + '@octokit/rest@21.0.2': + resolution: {integrity: sha512-+CiLisCoyWmYicH25y1cDfCrv41kRSvTq6pPWtRroRJzhsCZWZyCqGyI8foJT5LmScADSwRAnr/xo+eewL04wQ==} + engines: {node: '>= 18'} + + '@octokit/types@13.6.1': + resolution: {integrity: sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2372,6 +2427,9 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + before-after-hook@3.0.2: + resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + better-opn@3.0.2: resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==} engines: {node: '>=12.0.0'} @@ -5266,6 +5324,9 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universal-user-agent@7.0.2: + resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -6478,6 +6539,67 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@octokit/auth-token@5.1.1': {} + + '@octokit/core@6.1.2': + dependencies: + '@octokit/auth-token': 5.1.1 + '@octokit/graphql': 8.1.1 + '@octokit/request': 9.1.3 + '@octokit/request-error': 6.1.5 + '@octokit/types': 13.6.1 + before-after-hook: 3.0.2 + universal-user-agent: 7.0.2 + + '@octokit/endpoint@10.1.1': + dependencies: + '@octokit/types': 13.6.1 + universal-user-agent: 7.0.2 + + '@octokit/graphql@8.1.1': + dependencies: + '@octokit/request': 9.1.3 + '@octokit/types': 13.6.1 + universal-user-agent: 7.0.2 + + '@octokit/openapi-types@22.2.0': {} + + '@octokit/plugin-paginate-rest@11.3.5(@octokit/core@6.1.2)': + dependencies: + '@octokit/core': 6.1.2 + '@octokit/types': 13.6.1 + + '@octokit/plugin-request-log@5.3.1(@octokit/core@6.1.2)': + dependencies: + '@octokit/core': 6.1.2 + + '@octokit/plugin-rest-endpoint-methods@13.2.6(@octokit/core@6.1.2)': + dependencies: + '@octokit/core': 6.1.2 + '@octokit/types': 13.6.1 + + '@octokit/request-error@6.1.5': + dependencies: + '@octokit/types': 13.6.1 + + '@octokit/request@9.1.3': + dependencies: + '@octokit/endpoint': 10.1.1 + '@octokit/request-error': 6.1.5 + '@octokit/types': 13.6.1 + universal-user-agent: 7.0.2 + + '@octokit/rest@21.0.2': + dependencies: + '@octokit/core': 6.1.2 + '@octokit/plugin-paginate-rest': 11.3.5(@octokit/core@6.1.2) + '@octokit/plugin-request-log': 5.3.1(@octokit/core@6.1.2) + '@octokit/plugin-rest-endpoint-methods': 13.2.6(@octokit/core@6.1.2) + + '@octokit/types@13.6.1': + dependencies: + '@octokit/openapi-types': 22.2.0 + '@pkgjs/parseargs@0.11.0': optional: true @@ -7905,6 +8027,8 @@ snapshots: balanced-match@1.0.2: {} + before-after-hook@3.0.2: {} + better-opn@3.0.2: dependencies: open: 8.4.2 @@ -11112,6 +11236,8 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + universal-user-agent@7.0.2: {} + universalify@2.0.1: {} unocss-preset-primitives@0.0.2-beta.1: diff --git a/src/views/components/settings/Settings.tsx b/src/views/components/settings/Settings.tsx index 696c34a7d..81065a03a 100644 --- a/src/views/components/settings/Settings.tsx +++ b/src/views/components/settings/Settings.tsx @@ -9,8 +9,8 @@ import { SmallLogo } from '@views/components/common/LogoIcon'; // import PopupCourseBlock from '@views/components/common/PopupCourseBlock'; import SwitchButton from '@views/components/common/SwitchButton'; import Text from '@views/components/common/Text/Text'; -import { LONGHORN_DEVELOPERS_ADMINS, useGitHubStats } from '@views/hooks/useGitHubStats'; import useSchedules from '@views/hooks/useSchedules'; +import { GitHubStatsService, LONGHORN_DEVELOPERS_ADMINS } from '@views/lib/getGitHubStats'; import { getUpdatedAtDateTimeString } from '@views/lib/getUpdatedAtDateTimeString'; import clsx from 'clsx'; import React, { useCallback, useEffect, useState } from 'react'; @@ -29,6 +29,9 @@ const PREVIEW_SECTION_DIV_CLASSNAME = DISPLAY_PREVIEWS ? 'w-1/2 space-y-4' : 'fl const manifest = chrome.runtime.getManifest(); const LDIconURL = new URL('/src/assets/LD-icon.png', import.meta.url).href; +const gitHubStatsService = new GitHubStatsService(); +const includeMergedPRs = false; + /** * Custom hook for enabling developer mode. * @@ -78,7 +81,9 @@ export default function Settings(): JSX.Element { // Toggle GitHub stats when the user presses the 'S' key const [showGitHubStats, setShowGitHubStats] = useState(false); - const { adminGitHubStats, userGitHubStats, contributors } = useGitHubStats(showGitHubStats); + const [githubStats, setGitHubStats] = useState + > | null>(null); const [activeSchedule] = useSchedules(); // const [isRefreshing, setIsRefreshing] = useState(false); @@ -86,21 +91,28 @@ export default function Settings(): JSX.Element { const showDialog = usePrompt(); useEffect(() => { - initSettings().then( - ({ + const fetchGitHubStats = async () => { + const stats = await gitHubStatsService.fetchGitHubStats(); + setGitHubStats(stats); + }; + + const initAndSetSettings = async () => { + const { enableCourseStatusChips, enableTimeAndLocationInPopup, enableHighlightConflicts, enableScrollToLoad, enableDataRefreshing, - }) => { - setEnableCourseStatusChips(enableCourseStatusChips); - setShowTimeLocation(enableTimeAndLocationInPopup); - setHighlightConflicts(enableHighlightConflicts); - setLoadAllCourses(enableScrollToLoad); - setEnableDataRefreshing(enableDataRefreshing); - } - ); + } = await initSettings(); + setEnableCourseStatusChips(enableCourseStatusChips); + setShowTimeLocation(enableTimeAndLocationInPopup); + setHighlightConflicts(enableHighlightConflicts); + setLoadAllCourses(enableScrollToLoad); + setEnableDataRefreshing(enableDataRefreshing); + }; + + fetchGitHubStats(); + initAndSetSettings(); const handleKeyPress = (event: KeyboardEvent) => { if (event.key === 'S' || event.key === 's') { @@ -398,20 +410,23 @@ export default function Settings(): JSX.Element { {admin.name}

{admin.role}

- {showGitHubStats && adminGitHubStats[admin.githubUsername] && ( + {showGitHubStats && githubStats && (

GitHub Stats (UTRP repo):

+ {includeMergedPRs && ( +

+ Merged PRS:{' '} + {githubStats.adminGitHubStats[admin.githubUsername]?.mergedPRs} +

+ )}

- Merged PRs: {adminGitHubStats[admin.githubUsername]?.mergedPRs} -

-

- Commits: {adminGitHubStats[admin.githubUsername]?.commits} + Commits: {githubStats.adminGitHubStats[admin.githubUsername]?.commits}

- {adminGitHubStats[admin.githubUsername]?.linesAdded} ++ + {githubStats.adminGitHubStats[admin.githubUsername]?.linesAdded} ++

- {adminGitHubStats[admin.githubUsername]?.linesDeleted} -- + {githubStats.adminGitHubStats[admin.githubUsername]?.linesDeleted} --

)} @@ -422,36 +437,47 @@ export default function Settings(): JSX.Element {

UTRP CONTRIBUTERS

- {contributors.map(username => ( -
- window.open(`https://github.com/${username}`, '_blank')} - > - @{username} - -

Contributor

- {showGitHubStats && userGitHubStats[username] && ( -
-

GitHub Stats (UTRP repo):

-

- Merged PRs: {userGitHubStats[username]?.mergedPRs} -

-

Commits: {userGitHubStats[username]?.commits}

-

- {userGitHubStats[username]?.linesAdded} ++ -

-

- {userGitHubStats[username]?.linesDeleted} -- -

+ {githubStats && + Object.keys(githubStats.userGitHubStats) + .filter( + username => + !LONGHORN_DEVELOPERS_ADMINS.some(admin => admin.githubUsername === username) + ) + .map(username => ( +
+ window.open(`https://github.com/${username}`, '_blank')} + > + @{username} + +

Contributor

+ {showGitHubStats && ( +
+

GitHub Stats (UTRP repo):

+ {includeMergedPRs && ( +

+ Merged PRs:{' '} + {githubStats.userGitHubStats[username]?.mergedPRs} +

+ )} +

+ Commits: {githubStats.userGitHubStats[username]?.commits} +

+

+ {githubStats.userGitHubStats[username]?.linesAdded} ++ +

+

+ {githubStats.userGitHubStats[username]?.linesDeleted} -- +

+
+ )}
- )} -
- ))} + ))}
diff --git a/src/views/hooks/useGitHubStats.ts b/src/views/hooks/useGitHubStats.ts deleted file mode 100644 index 9e1f512d3..000000000 --- a/src/views/hooks/useGitHubStats.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { useEffect, useState } from 'react'; - -type TeamMember = { - name: string; - role: string; - githubUsername: string; -}; - -type GitHubStats = { - commits: number; - linesAdded: number; - linesDeleted: number; - mergedPRs: number; -}; - -type UserStat = { - author: { - login: string; - id: number; - node_id: string; - avatar_url: string; - gravatar_id: string; - url: string; - html_url: string; - followers_url: string; - following_url: string; - gists_url: string; - starred_url: string; - subscriptions_url: string; - organizations_url: string; - repos_url: string; - events_url: string; - received_events_url: string; - type: string; - site_admin: boolean; - }; - total: number; - weeks: { - w: number; - a: number; - d: number; - c: number; - }[]; -}; - -export const LONGHORN_DEVELOPERS_ADMINS = [ - { name: 'Sriram Hariharan', role: 'Founder', githubUsername: 'sghsri' }, - { name: 'Elie Soloveichik', role: 'Senior Software Engineer', githubUsername: 'Razboy20' }, - { name: 'Diego Perez', role: 'Senior Software Engineer', githubUsername: 'doprz' }, - { name: 'Lukas Zenick', role: 'Senior Software Engineer', githubUsername: 'Lukas-Zenick' }, - { name: 'Isaiah Rodriguez', role: 'Chief Design Officer', githubUsername: 'IsaDavRod' }, -] as const satisfies TeamMember[]; - -type LD_ADMIN_GITHUB_USERNAMES = (typeof LONGHORN_DEVELOPERS_ADMINS)[number]['githubUsername']; - -/** - * Custom hook that fetches GitHub user statistics for a given repository. - * - * @param showGitHubStats - A boolean indicating whether to fetch and display GitHub statistics. - * @param longhornDevelopersAdmins - An array of TeamMember objects representing the admins of the Longhorn Developers team. - * @returns An object containing the GitHub statistics for admins and users, as well as the list of contributors. - */ -export function useGitHubStats(showGitHubStats: boolean) { - const [adminGitHubStats, setAdminGitHubStats] = useState>({}); - const [userGitHubStats, setUserGitHubStats] = useState>({}); - const [contributors, setContributors] = useState([]); - - useEffect(() => { - const fetchContributors = async () => { - try { - const response = await fetch( - 'https://api.github.com/repos/Longhorn-Developers/UT-Registration-Plus/contributors' - ); - const data = await response.json(); - const adminUsernames = LONGHORN_DEVELOPERS_ADMINS.map(admin => admin.githubUsername); - const contributorNames = data - .map((contributor: { login: string }) => contributor.login) - .filter((name: string) => !adminUsernames.includes(name as LD_ADMIN_GITHUB_USERNAMES)); - setContributors(contributorNames); - } catch (error) { - console.error('Error fetching contributors:', error); - } - }; - - fetchContributors(); - }, []); - - useEffect(() => { - if (showGitHubStats) { - const fetchStats = async () => { - const adminStats: Record = {}; - const userStats: Record = {}; - - try { - const response = await fetch( - `https://api.github.com/repos/Longhorn-Developers/UT-Registration-Plus/stats/contributors` - ); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - if (!Array.isArray(data)) { - throw new Error('Unexpected response format'); - } - - for (const l_stat of data) { - // narrow type to UserStat - const stat = l_stat as UserStat; - - const isAdmin = LONGHORN_DEVELOPERS_ADMINS.some( - admin => admin.githubUsername === stat.author.login - ); - const statsObject = isAdmin ? adminStats : userStats; - - const totalLinesAdded = stat.weeks.reduce( - (total: number, week: { a: number }) => total + week.a, - 0 - ); - const totalLinesDeleted = stat.weeks.reduce( - (total: number, week: { d: number }) => total + week.d, - 0 - ); - - // eslint-disable-next-line no-await-in-loop - const prResponse = await fetch( - `https://api.github.com/search/issues?q=org:Longhorn-Developers%20author:${stat.author.login}%20type:pr%20is:merged` - ); - if (!prResponse.ok) { - console.error(`Error fetching PRs for GitHub user: ${stat.author.login}:`, prResponse); - throw new Error(`HTTP error! status: ${prResponse.status}`); - } - // eslint-disable-next-line no-await-in-loop - const prData = await prResponse.json(); - - statsObject[stat.author.login] = { - commits: stat.total, - linesAdded: totalLinesAdded, - linesDeleted: totalLinesDeleted, - mergedPRs: prData.total_count || 0, - }; - } - } catch (error) { - console.error('Error fetching stats:', error); - } - - setAdminGitHubStats(adminStats); - setUserGitHubStats(userStats); - }; - - fetchStats(); - } - }, [showGitHubStats]); - - return { adminGitHubStats, userGitHubStats, contributors }; -} diff --git a/src/views/lib/getGitHubStats.ts b/src/views/lib/getGitHubStats.ts new file mode 100644 index 000000000..152bf65c6 --- /dev/null +++ b/src/views/lib/getGitHubStats.ts @@ -0,0 +1,266 @@ +import { Octokit } from '@octokit/rest'; + +// Types +type TeamMember = { + name: string; + role: string; + githubUsername: string; +}; + +type GitHubStats = { + commits: number; + linesAdded: number; + linesDeleted: number; + mergedPRs?: number; +}; + +type ContributorStats = { + total: number; + weeks: { w: number; a: number; d: number; c: number }[]; + author: { login: string }; +}; + +type CachedData = { + data: T; + dataFetched: Date; +}; + +type FetchResult = { + data: T; + dataFetched: Date; + lastUpdated: Date; + isCached: boolean; +}; + +// Constants +const CACHE_TTL = 1 * 60 * 60 * 1000; // 1 hour in milliseconds +const REPO_OWNER = 'Longhorn-Developers'; +const REPO_NAME = 'UT-Registration-Plus'; + +export const LONGHORN_DEVELOPERS_ADMINS = [ + { name: 'Sriram Hariharan', role: 'Founder', githubUsername: 'sghsri' }, + { name: 'Elie Soloveichik', role: 'Senior Software Engineer', githubUsername: 'Razboy20' }, + { name: 'Diego Perez', role: 'Senior Software Engineer', githubUsername: 'doprz' }, + { name: 'Lukas Zenick', role: 'Senior Software Engineer', githubUsername: 'Lukas-Zenick' }, + { name: 'Isaiah Rodriguez', role: 'Chief Design Officer', githubUsername: 'IsaDavRod' }, +] as const satisfies TeamMember[]; + +/** + * Represents the GitHub usernames of the admins in the LONGHORN_DEVELOPERS_ADMINS array. + */ +export type LD_ADMIN_GITHUB_USERNAMES = (typeof LONGHORN_DEVELOPERS_ADMINS)[number]['githubUsername']; + +/** + * Service for fetching GitHub statistics. + */ +export class GitHubStatsService { + private octokit: Octokit; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private cache: Map>; + + constructor(githubToken?: string) { + this.octokit = githubToken ? new Octokit({ auth: githubToken }) : new Octokit(); + this.cache = new Map(); + } + + private getCachedData(key: string): CachedData | null { + const cachedItem = this.cache.get(key); + if (cachedItem && Date.now() - cachedItem.dataFetched.getTime() < CACHE_TTL) { + return cachedItem; + } + return null; + } + + private setCachedData(key: string, data: T): void { + this.cache.set(key, { data, dataFetched: new Date() }); + } + + private async fetchWithRetry(fetchFn: () => Promise, retries: number = 3, delay: number = 5000): Promise { + try { + return await fetchFn(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (error: any) { + if (retries > 0 && error.status === 202) { + await new Promise(resolve => setTimeout(resolve, delay)); + return this.fetchWithRetry(fetchFn, retries - 1, delay); + } + throw error; + } + } + + private async fetchContributorStats(): Promise> { + const cacheKey = `contributor_stats_${REPO_OWNER}_${REPO_NAME}`; + const cachedStats = this.getCachedData(cacheKey); + + if (cachedStats) { + return { + data: cachedStats.data, + dataFetched: cachedStats.dataFetched, + lastUpdated: new Date(), + isCached: true, + }; + } + + const { data } = await this.fetchWithRetry(() => + this.octokit.repos.getContributorsStats({ + owner: REPO_OWNER, + repo: REPO_NAME, + }) + ); + + if (Array.isArray(data)) { + const fetchResult: FetchResult = { + data: data as ContributorStats[], + dataFetched: new Date(), + lastUpdated: new Date(), + isCached: false, + }; + this.setCachedData(cacheKey, fetchResult.data); + return fetchResult; + } + + throw new Error('Invalid response format'); + } + + private async fetchMergedPRsCount(username: string): Promise> { + const cacheKey = `merged_prs_${username}`; + const cachedCount = this.getCachedData(cacheKey); + + if (cachedCount !== null) { + return { + data: cachedCount.data, + dataFetched: cachedCount.dataFetched, + lastUpdated: new Date(), + isCached: true, + }; + } + + const { data } = await this.octokit.search.issuesAndPullRequests({ + q: `org:${REPO_OWNER} author:${username} type:pr is:merged`, + }); + + const fetchResult: FetchResult = { + data: data.total_count, + dataFetched: new Date(), + lastUpdated: new Date(), + isCached: false, + }; + this.setCachedData(cacheKey, fetchResult.data); + return fetchResult; + } + + private processContributorStats(stats: ContributorStats): GitHubStats { + return { + commits: stats.total, + linesAdded: stats.weeks.reduce((total, week) => total + week.a, 0), + linesDeleted: stats.weeks.reduce((total, week) => total + week.d, 0), + }; + } + + public async fetchGitHubStats(options: { includeMergedPRs?: boolean } = {}): Promise<{ + adminGitHubStats: Record; + userGitHubStats: Record; + contributors: string[]; + dataFetched: Date; + lastUpdated: Date; + isCached: boolean; + }> { + const { includeMergedPRs = false } = options; + const adminGitHubStats: Record = {}; + const userGitHubStats: Record = {}; + const contributors: string[] = []; + let oldestDataFetch = new Date(); + let newestDataFetch = new Date(0); + let allCached = true; + + try { + const contributorStatsResult = await this.fetchContributorStats(); + oldestDataFetch = contributorStatsResult.dataFetched; + newestDataFetch = contributorStatsResult.dataFetched; + allCached = contributorStatsResult.isCached; + + await Promise.all( + contributorStatsResult.data.map(async stat => { + const { login } = stat.author; + contributors.push(login); + + const isAdmin = LONGHORN_DEVELOPERS_ADMINS.some(admin => admin.githubUsername === login); + const statsObject = isAdmin ? adminGitHubStats : userGitHubStats; + + statsObject[login] = this.processContributorStats(stat); + + if (includeMergedPRs) { + try { + const mergedPRsResult = await this.fetchMergedPRsCount(login); + statsObject[login].mergedPRs = mergedPRsResult.data; + + if (mergedPRsResult.dataFetched < oldestDataFetch) { + oldestDataFetch = mergedPRsResult.dataFetched; + } + if (mergedPRsResult.dataFetched > newestDataFetch) { + newestDataFetch = mergedPRsResult.dataFetched; + } + allCached = allCached && mergedPRsResult.isCached; + } catch (error) { + console.error(`Error fetching merged PRs for ${login}:`, error); + statsObject[login].mergedPRs = 0; + } + } + }) + ); + + return { + adminGitHubStats, + userGitHubStats, + contributors, + dataFetched: oldestDataFetch, + lastUpdated: new Date(), + isCached: allCached, + }; + } catch (error) { + console.error('Error fetching GitHub stats:', error); + throw error; + } + } +} + +// /** +// * Runs an example that fetches GitHub stats using the GitHubStatsService. +// * +// * @returns A promise that resolves when the example is finished running. +// * @throws If there is an error fetching the GitHub stats. +// */ +// async function runExample() { +// // Token is now optional +// // const githubToken = process.env.GITHUB_TOKEN; +// const gitHubStatsService = new GitHubStatsService(); + +// try { +// console.log('Fetching stats without merged PRs...'); +// const statsWithoutPRs = await gitHubStatsService.fetchGitHubStats(); +// console.log('Data fetched:', statsWithoutPRs.dataFetched.toLocaleString()); +// console.log('Last updated:', statsWithoutPRs.lastUpdated.toLocaleString()); +// console.log('Is cached:', statsWithoutPRs.isCached); + +// console.log(statsWithoutPRs); + +// // console.log('\nFetching stats with merged PRs...'); +// // const statsWithPRs = await gitHubStatsService.fetchGitHubStats({ includeMergedPRs: true }); +// // console.log('Data fetched:', statsWithPRs.dataFetched.toLocaleString()); +// // console.log('Last updated:', statsWithPRs.lastUpdated.toLocaleString()); +// // console.log('Is cached:', statsWithPRs.isCached); + +// // wait 5 seconds +// // await new Promise(resolve => setTimeout(resolve, 5000)); + +// // console.log('\nFetching stats again (should be cached)...'); +// // const cachedStats = await gitHubStatsService.fetchGitHubStats(); +// // console.log('Data fetched:', cachedStats.dataFetched.toLocaleString()); +// // console.log('Last updated:', cachedStats.lastUpdated.toLocaleString()); +// // console.log('Is cached:', cachedStats.isCached); +// } catch (error) { +// console.error('Failed to fetch GitHub stats:', error); +// } +// } + +// runExample();