Compare commits

..

21 Commits

Author SHA1 Message Date
doprz
4f2a29f268 chore: fix lint errors 2024-09-30 19:31:17 -05:00
doprz
d5c3d32170 Merge branch 'derek/experimental' of https://github.com/DereC4/UT-Registration-Plus into DereC4-derek/experimental 2024-09-30 19:20:03 -05:00
9356357545 Merge branch 'Longhorn-Developers:main' into derek/experimental 2024-09-29 14:59:26 -05:00
0d63c66356 fix: styling 2024-09-08 12:46:26 -05:00
703a0fa596 feat: button custom function prop 2024-09-04 13:05:31 -05:00
7acc91f4ee chore: fix lint 2024-09-04 12:50:06 -05:00
5d9980cac3 feat: using type now 2024-09-04 12:43:45 -05:00
57832b0dd4 fix: story 2024-09-04 12:43:08 -05:00
b9358803dd feat: look at how this switch goes back and forth very mindful very demure 2024-09-04 12:27:12 -05:00
4397dfb1c5 feat: switch button initial test 2024-09-04 12:25:16 -05:00
0e70bbd52f Merge branch 'Longhorn-Developers:main' into main 2024-06-04 01:14:35 -05:00
79f2ffa4cc Merge branch 'Longhorn-Developers:main' into main 2024-05-27 18:26:05 -05:00
c330e3123a Merge branch 'Longhorn-Developers:main' into master 2024-05-21 21:46:06 -05:00
38d09e4aae Merge branch 'Longhorn-Developers:main' into master 2024-04-16 15:15:38 -05:00
98a7cc618b Merge branch 'Longhorn-Developers:main' into master 2024-04-09 14:04:09 -05:00
74cc6e6da1 fix: importance (tailwind wise) 2024-03-29 00:51:25 -05:00
c68b017462 Merge branch 'Longhorn-Developers:main' into master 2024-03-29 00:44:18 -05:00
aeb19cff54 fix: text-ut-burntorange 2024-03-25 15:50:59 -05:00
5a73c8ee6c fix: change color again 2024-03-25 15:17:40 -05:00
35b0d01c51 fix: change color hex 2024-03-25 15:16:30 -05:00
b84cf7c15d fix: font weight added 2024-03-25 15:14:52 -05:00
98 changed files with 6905 additions and 10453 deletions

View File

@@ -100,12 +100,6 @@ module.exports = {
'import/no-cycle': 'off',
'import/no-extraneous-dependencies': 'off',
'react/jsx-props-no-spreading': 'off',
'react/jsx-no-useless-fragment': [
'error',
{
allowExpressions: true,
},
],
'keyword-spacing': [
'error',
{

1
.gitignore vendored
View File

@@ -210,4 +210,3 @@ sketch
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
package-lock.json
storybook-static/
package/

2
.nvmrc
View File

@@ -1 +1 @@
v20.9.0
v18.12.1

View File

@@ -1,258 +0,0 @@
## [2.0.1](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.0.0...v2.0.1) (2024-10-17)
### Features
- spring 2024 instructors db ([#317](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/317)) ([79dd29c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/79dd29cfc9e849b09e7d91bd0eed51c1c93b3352))
### Bug Fixes
- add a little error checking to settings ([#315](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/315)) ([e261641](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e261641e5985d8bd5047d8a0be5d1caae844e40f))
- gulp zip ([#314](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/314)) ([05f00b2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/05f00b23d26b90f564710db4364426e90c8d6831))
- migration loop ([aeff5e0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/aeff5e09a238503293c3882d97d40270da1e4883))
- show calendar in active window ([#312](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/312)) ([ceba38b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ceba38b1ac74ec9e6630222183bd466a8d12c27d))
## [2.0.0](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/b4e8c7589e53f1064d70703459cc6d66fae1b04c...v2.0.0) (2024-10-15)
### Features
- abhinavchadaga/course-catalog-popup ([#128](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/128)) ([745f9dd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/745f9dd6fb96ccc0eace7189b87abf9a38f03828))
- abhinavchadaga/reusable-popup-prompt ([#148](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/148)) ([44af9e1](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/44af9e16e4bf3b92c9a4fa0e779197f4f9ecb237))
- actually sum for duplicate semesters (different uniques) ([#202](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/202)) ([d3f64ec](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d3f64ec79eb64ebe72ae28991127dbe521823842))
- add button to the rows, use new ConflictsWithWarning component ([bec2649](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bec2649fc1244bffb5cb03809db62cc0ea477260))
- add buttons with icons in tailwind ([93b65ac](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/93b65ac2edef3fc728305cc51d1a32ecf5073274))
- add cal save buttons (no functionality) ([53e7c7f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/53e7c7fedaf1f268d2d7d06ff1a5df9e2af2306d))
- add Calendar Component ([e0212d5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e0212d510928e5e95d3ba75e391c34714f1bb035))
- add Calendar schedules component (clicking on storybook not working) ([d1a336e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d1a336e903f09b04b89f11ba7ede2f7d26e0ac3c))
- add CalendarHeader and its Storybook, need to resize ([23276e5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/23276e5c7c907261b1d02e9006d8021d99b43984))
- add check-path-alias custom ESLint rule ([#123](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/123)) ([208103d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/208103d7081abcf5b31bf36335320aaf7d213464))
- add chrome.storage api mocks for storybook use ([#141](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/141)) ([3839bff](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3839bff29ef6f0ecfad1e975e685df9a64d96c92))
- add CSS for calendarCell div ([6cdcf49](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6cdcf4930d9868c0ed45820f66102d89a644cfb6))
- add CSS for hourLine div ([0ba6153](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0ba61534cbfc83408cb64eb1145ecacef1ee8f04))
- add CSS for timeLabelContainer div ([56f6456](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/56f6456ce85f77edb9d6fa64ecb82a8388feec41))
- add custom ESLint rule restrict-import-depth ([#110](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/110)) ([8c069b7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8c069b7ad339ed8db97bb5e3e6e88ab8d332489b))
- add downloadBlob util ([2af351e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2af351efa85264da5abeee27fbd091d4ce637041))
- add empty settings component - waiting on design ([9d0f210](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9d0f2105481cc3d5affdb4bd69d428379ea92de3))
- add ImportantLinks Component ([5dbee6f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5dbee6f0c39e620c3b65a5e0156fd59ec8217ca7))
- add List component ([e6b4049](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e6b40494031ca88894b2f918bb18448b70124481))
- add MIMEType ([0c76052](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0c760524781556cf089f1885dc4ff3fd567bfc2b))
- add new db powered by UT_Grade_Parser ([#163](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/163)) ([60d1f48](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/60d1f48bd95132f0517258f017a7644d8aff5101))
- add react-loading-skeleton package ([#244](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/244)) ([b4dd91a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b4dd91ad2595d9583a60530c2c2ff6d829d2d5e5))
- add skeleton loader for course description + distribution ([#267](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/267)) ([c2cab40](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c2cab407f3a07c1d3073e936c9d23cb1cecc9cb9))
- add story for CalendarGridCell ([fd91c3b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/fd91c3b12ef5670563e689d190be75ef1f695d1c))
- add Storybook story ([cb3cb5d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cb3cb5d5fc18a68ca5a825f97b808b535aa1fe29))
- add tailwind version of Button component ([28d93b3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/28d93b3c25e1cbca3baea992a299d626ca823570))
- add tickmarks to day div ([8b9cb06](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8b9cb065c28b5dd38b7e1681839a81f55691a3c6))
- add time column to Calendar Component ([9d68211](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9d6821127e7ff9bf63cef659932ee7408de4c969))
- add timeAndGrid div ([95e0544](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/95e0544b7301d382439b74038fb1b3e25dfe6d30))
- added flag ;-; ([#195](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/195)) ([9b4d61c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9b4d61c2b0d8806b5d4a10296c1cc1266ac4229f))
- added scrapedAt property ([#149](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/149)) ([8e181b3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8e181b3010dfe25ea1e4b4ba181cbe9944d6e92e))
- additional changes to [#201](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/201) ([#224](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/224)) ([bc354f3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bc354f3798a02e6ebc3c66a8b5eee553c25a19d4))
- aesthetically pleasing squishier course blocks when compressed ([#232](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/232)) ([438c82b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/438c82bfb5db0f570b261d15368a05447939b0fb))
- align day labels and add to grid ([1bb6191](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/1bb6191244f40220a9af66cced4fe8f0c1e546a0))
- alignment on calendar and header ([#109](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/109)) ([07ec5ab](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/07ec5abc3e1b40b801b7c977d21399b31222a4d3))
- async course adding and async/other course block adjustments ([#273](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/273)) ([668c8d0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/668c8d00756a5e3e3daaade46380967c74eec1ae))
- async text hiding on Calendar's Bottom Bar when there are no async courses ([#152](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/152)) ([0dff122](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0dff12232c29415fa7bc5ac3771393a406ab738d))
- basic CalendarCourseMeeting component laid out - missing Text and Right Icon ([da9e7aa](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/da9e7aac4135afa5184e4949c36a288ef6ab62a7))
- Best Practices ([#102](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/102)) ([5eb7be2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5eb7be246cd2a1db27c7ca777d81074ae7bb3b82))
- beta builds ([#187](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/187)) ([8a5e12e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8a5e12ec63539ae7199f0f1561f642cb9cf19302))
- better discord icon ([#205](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/205)) ([a3f5e0f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a3f5e0f27ffdfcb4e3ddd47140979aca379f9f42))
- bold red refresh error ([#220](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/220)) ([229a8a2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/229a8a29e8d04dfbe15de200ceffc53b4f758413))
- bottom bar for the calendar page ([#91](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/91)) ([0f730d6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0f730d6c50462c9f7d0428ebb379742305212ef6))
- build without errors ([babc925](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/babc925967b01f1064b8d8e067e7bc792dd57b0a))
- calendar components 3rd attempt at merging ([#60](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/60)) ([4faca8c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4faca8c43befe19c9b05ff4f3afba420e016bb59))
- Calendar Components 3rd Attempt at Merging ([#60](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/60)) ([ab2cd68](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ab2cd688fae616742b39c0637291a65e508e23bd))
- calendar course block component ([#75](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/75)) ([00e0019](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/00e00197efb807d420196f2b030fc4dc71e9cd42))
- calendar course block component ([#75](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/75)) ([a41cb3e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a41cb3ed8711eff9a2fbbe435e867c073904f452))
- calendar grid and grid cells ([#81](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/81)) ([bfeb239](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bfeb2398aacf9a1fba892737b6ef0286acbc326f))
- Calendar Grid and Grid Cells ([#81](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/81)) ([dd2f696](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/dd2f696f8de2c0b291e917ef4d4cb2171b76712c))
- calendar header formatting and data displaying ([#160](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/160)) ([5cce1c7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5cce1c79fc2b50843426ed290a1b53849375e4eb))
- calendar matchings ([#173](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/173)) ([791a42b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/791a42bcd49478024b6f1a237a2d2a9f6211749e))
- Calendar Schedule component finished, fix: list didn't allow updates when adding a new schedule ([#115](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/115)) ([a99a557](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a99a55788a0b9791ff048c8bbbaad5bada47c41f))
- calendar-course-cell-color-picker ([#157](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/157)) ([df18491](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/df1849180de2c793c10e3a49d2c862d236456ec2))
- can open tabs, updated injected popup heading. basically done ([35fab34](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/35fab3444582b436f63c7b16a26ebd5989c4bf3e))
- change Chip to tailwind css. Fixed eslint for ConflictsWithWarning ([3568b8e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3568b8eb5ee9ce3fe17f77f5641c0583bacc17a4))
- check-path-alias autofix ([#124](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/124)) ([265652c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/265652c4200a56b1ae62e6e94e0609fb4604c188))
- chrome extension works ([35f3c72](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/35f3c7225049d760c6061ed827aa796514291e62))
- color palette for calendar ([#118](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/118)) ([471e55d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/471e55dcea1ae439658d00ed41570e2f218f0c3d))
- Conventional Commits ([#103](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/103)) ([fe599df](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/fe599dfe7514ead880596d3b75c8370bde796e09))
- convert all LabelsAndDetails Components to Tailwind ([9e0f9df](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9e0f9df9de1a5c99657ddcef3b2eb101dd825b84))
- course color generation ([#179](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/179)) ([5ed81e4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5ed81e4be99a8b7f3c68a3ba70358e4cbe5cc613))
- course colors ([#175](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/175)) ([dc77cc2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/dc77cc27da7ab521e290a8c9ba810573e677b6b8))
- Course Flag Tooltips ([#178](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/178)) ([ba2bc60](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ba2bc60add2b87ccd4e9894c454d7a8d8382f0dd))
- course-catalog-injected-popup ([#98](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/98)) ([89d03f4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/89d03f4244d324915cadadaf75a827221525d43f))
- CourseStatus Component implemented ([#83](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/83)) ([58d7df4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/58d7df499ca9a8dfd4dc3aa47bc9d76837f21bf7))
- CourseStatus Component implemented ([#83](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/83)) ([fa1d737](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/fa1d7374bcafbe19673dc31f33b3f8075e45f37a))
- create empty Popup story ([ad83ba4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ad83ba4cdc67f86096d0cbc2cb6659aa9be98a0b))
- Create icon helper ([#77](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/77)) ([ccea0f4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ccea0f4bd1e8d66dc0717d92c9e970040d9fbe9e))
- Create icon helper ([#77](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/77)) ([1b51d65](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/1b51d65c89a4b544f4f2a9c60106e14300f9a3b0))
- Derek vinson/calendar header ([#94](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/94)) ([12d09b5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/12d09b54cbf840993d17323b96cf274ec24c67ce))
- Derek/disable updating ([#239](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/239)) ([0ab83ef](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0ab83efd47d75fb6c278994c7dc8a6d22b4b6b83))
- Derek/export png ([#95](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/95)) ([58c2b46](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/58c2b4634a9d065ca156d27c4af69a504b7c2d2d))
- DialogProvider component ([#198](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/198)) ([d1b921a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d1b921a5b000693d7f3dabaf84d8b9580c361941))
- dividers in calendar bottom bar ([#120](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/120)) ([eba5d9f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/eba5d9f508576d2648bd2501e5d5aaff32592566))
- **docs:** add extra acknowledgements ([e2cbfa3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e2cbfa3f48cb0b10a43134cf9784a44b13aa542d))
- drag only on vertical axis ([34a6449](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/34a644952995d4aedd18d11d327bd8589f5b2610))
- early iteration of non-virtual list ([677aa62](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/677aa624d7fb2bbb922aaf8806abe979c3399e4e))
- enable TS strict mode ([#168](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/168)) ([efed1c0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/efed1c0edbf93987c551952a03c9c4b3c461d819))
- experimental toggle for icons, left off for now ([#237](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/237)) ([f34dd95](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f34dd95d7765322b6cecb2e817a46e0549d929a5))
- fall 2023 grades ([#226](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/226)) ([863d980](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/863d980b2d1c706b1fc648f1a0de2a31515e339e))
- finally fix grid JSX.Element generation ([e49fc29](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e49fc295ba40a37f9bf59686adbd8b4f523cd9c6))
- finish ScheduleTotalHoursAndCourses ([12f680d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/12f680d7e9aef826281c8f769825408dc6f9f9de))
- fix icons on PopupMain and convert to tailwind ([#108](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/108)) ([87799d8](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/87799d8f02aa5986025982f768639afdb1b0267a))
- fix save as button dividers ([#153](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/153)) ([7986549](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7986549fdd40834a85bedbfd0862cdb21c9ae657))
- grades by professor ([#225](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/225)) ([78d749a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/78d749a8a96f63c32663b464e5b663f932c28ed0))
- html2canvas -> htmlToImage and fix derick's bugs ([bda0282](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bda02826b14b96fd65a6f24218950e510c5aa3d8))
- icon added successfully ([3b588c2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3b588c20394523ddf90456e1ca58546e5b820c74))
- implement Chip component ([23e881f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/23e881f14cc80100d8f8deef6a25daaa72d83f07))
- implement flatten course schedule helper function ([e54f488](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e54f488b170d5a9af646c1ac82e8b33579fef61d))
- implemented ConflictsWithWarning ([93f3a30](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/93f3a307b4acce6cc55fc411f1ea2d7576006b00))
- implemented InfoCard ([21b6430](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/21b643000d884442e025534802be6bb1f6ba68e2))
- Initial Splash Text Commit ([#208](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/208)) ([0534f60](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0534f607a51f1dd0905f6f45f08519cba65ea05a))
- limit schedules to 10 ([#272](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/272)) ([290b841](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/290b8415170bca631e5d71b08d88db80b02163f0))
- link to your registered courses ([#228](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/228)) ([f83e012](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f83e012d62e268d95dce19a30e54e34f3779142b))
- list reordering ([#154](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/154)) ([038ebaa](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/038ebaa2689c14bc9906afb542ef62c21cb13177))
- listed versioning for beta builds ([#192](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/192)) ([0c42979](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0c429794230a91c4fd949715b7eea210ac18fee3))
- lowercase instructor! ([#268](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/268)) ([9ec05ef](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9ec05ef764b2451c05e2d084d92074fb1e984268))
- made List more extensible ([cd34601](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cd34601379d5cd2b53d967afbc954178834823b9))
- match calendar designs & add functionality ([#176](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/176)) ([8027c3d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8027c3d1bf228053acbad3e232b030c9ddbaca6a))
- migrate styles to TailwindCSS ([7e2f5ea](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7e2f5eaed7d782bc886545f7241e0891d55cfae0))
- migration update showing ([#293](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/293)) ([aede681](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/aede681d4bb5b01723eb9d68527f4e64991adfab))
- missed one chip toggle there ([#245](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/245)) ([5ca24da](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5ca24dab82509d34cf43af4359940476159d052f))
- newer grades and parameterized queries ([#238](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/238)) ([75ad416](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/75ad4167b6639bbcf58e2dc6dafc80ed4656a899))
- one single exclamation mark did all that ([#235](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/235)) ([be87e41](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/be87e4181430e0df41683be5c9d0a1ce992b2c61))
- open an injected course page on course block click in popup main ([#146](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/146)) ([2709484](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/27094846f70feb8d83c8d464ab0fa8dcd99b3e71))
- pad unique ids to 5 digits ([#170](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/170)) ([b4ad687](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b4ad6870bfc88f68bdd7f87fe1d94c23d0a02b95))
- parallelize initializeDB.ts promises ([9f1dcc6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9f1dcc667df9857e3d0469fe7112a9f985aad490))
- popout icon for ccip in calendar ([#221](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/221)) ([6812d68](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6812d685d08902f7b3d9d2e5293350c8170c6f06))
- popup schedule select ([#126](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/126)) ([7f2a589](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7f2a5893d4dd82a0f077dca6dfd28e79f929766f))
- PopupCourseBlock Component ([#79](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/79)) ([9accd17](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9accd17bd418f8c9334e80b0f7e38673092ba208))
- PopupCourseBlock Component ([#79](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/79)) ([f045b40](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f045b400a56569f7353eb00ecd0d081b68660fc5))
- proper injected styles ([#164](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/164)) ([e919e96](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e919e96c53bc9bec7db3d09693d1ff0684b56e87))
- readme animation ([#281](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/281)) ([da6d86c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/da6d86c785f92f70f52ef0b4cab3dc14e0bd9790))
- refactor all components in common ([e544312](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e5443122b422266a126de497b475020e298b2f1c))
- refactor all components in injected ([0c44849](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0c44849e15333c3a5cbdf8972eb31ada0e830b0a))
- refactor calendar ([28f1924](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/28f192472b7f244253b6a0b5339491b9740880f7))
- Refactor database initialization code ([5e98f45](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5e98f45210346f0e7bb69af5cf25e336c76416ca))
- release notes ([#283](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/283)) ([bd17e33](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bd17e3353792df70ca2e33c8b7a32b5fb9acc7d2))
- report issue popup ([#261](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/261)) ([65ff6bf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/65ff6bfbbfc6b696621628e448865155d0405f7f))
- rerouted directory to syllabus when click on professor name ([#211](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/211)) ([8959e0d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8959e0d9f7fdd82125f391f1ec51aa390ba9450d))
- sam's jokes ([#278](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/278)) ([895cd31](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/895cd31d8eb40e7e99fc0969b543e95bd070f6b7))
- save as PNG functionality ([ad18fbd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ad18fbd16235be0c75121b527867fac708670300))
- schedule list item action menu ([#230](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/230)) ([15fc369](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/15fc3699cfda3ad56e5a262bd37dca166e2685dd))
- screenshot whole page, hide certain elements, screenshot fixed size ([#180](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/180)) ([7d4c5d7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7d4c5d7be8b266b5f3caf2135c4f3fecb96d75be))
- settings page ([#260](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/260)) ([7a5c3a2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7a5c3a2e62c3a726735f9fe921f9e08d4092d0f9))
- show async courses in the bottom bar ([#204](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/204)) ([227de53](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/227de53e8453804f0791f269dae64ec388136390))
- some small changes for colors and font ([#201](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/201)) ([bae1da4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bae1da43d3373e12d593682e9fb69e19557b5749))
- splash text additions before v2 release ([#296](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/296)) ([e774f31](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e774f316e3f92c03e79274a55f1f729178c9a14a))
- splash text has arrived! ([#246](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/246)) ([9971435](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9971435716dec11e954dc26c14bf7d1505563d0d))
- Storybook for Vite ([#52](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/52)) ([9cc299c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9cc299ced6132644a5c2375b95a8a16a3482601b))
- swe title updates ([#310](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/310)) ([4629626](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4629626a31934c491758ddca874e5018d9c22e57))
- switch button ([#229](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/229)) ([abae7a5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/abae7a5c22ab944262d66ae897298b008a03c5f1))
- temporary removal of waitlist etc ([#236](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/236)) ([d424ccc](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d424ccce49ba05305b24cb61aeb4ffeada5bab33))
- UnoCSS (TailwindCSS) (Storybook only) ([#61](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/61)) ([85c7f78](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/85c7f7817c58f13f6a4e8bec13a45f6412ad87db))
- UnoCSS (TailwindCSS) (Storybook only) ([#61](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/61)) ([6521a4b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6521a4b2a90af06e182c779f2fc7cb1a3b1e55c1))
- unplugin-icons ([#62](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/62)) ([2d67b12](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2d67b1218ff852c59901b77909615c5f54794a67))
- unplugin-icons ([#62](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/62)) ([945e09b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/945e09b49e4ffa9bd4b02442f6567bb99831923e))
- update admin titles ([4cf8c3f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4cf8c3f9645cd8fda07244413cdc406105b12fec))
- update badge count when schedule changes ([#150](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/150)) ([a5e9e3c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a5e9e3c2145d61d8cc5788eb50fa19718e6d13bf))
- update Button to v2 design ([863521f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/863521fb3bb268131bd3f369064ae10a442b4fbc))
- update dialog component to headlessui ([#159](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/159)) ([442be8c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/442be8cbee06ec403467b100d8d5300dd4a7ea72))
- update with TailwindCSS ([f3a8a7d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f3a8a7db560a6b28f96a2e976c8ed33af97fe77a))
- updated calendar page and recruitment banner links ([#219](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/219)) ([a2303ee](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a2303ee35f10aba5fa7ff5e307d0092a39f18830))
- updated divider component ([#99](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/99)) ([8ab60c9](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8ab60c9f018143df594e7bfd0d321666289cc28c))
- updated Text component to latest design specification ([#70](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/70)) ([8b8433d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8b8433deaf8fe66f917fd66fb56cb0a748bc6e6e))
- updated Text component to latest design specification ([#70](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/70)) ([bb727f7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bb727f70bea5a4c2ff5dba7e9b4428c0c84de7b2))
- updating joke styling and updating jokes array ([#277](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/277)) ([0da27e2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0da27e2c46e9fc32efb80808715053c1f9f5165a))
- use display: grid for calendarGrid ([b535a6e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b535a6eb32ac1091c8b5309ca3cfb6bab0b62526))
- use downloadBlob util ([#186](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/186)) ([2dfb10e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2dfb10e57b51a08fcde2dc6638a81b5ec9bbc7ab))
- use filter() instead of pop() ([063349d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/063349d96d0ad008fa0dc1f0a8c7a27cf3f108ce))
- use React-icons ([8df9ea5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8df9ea55a9f0bcb04c90314cb311bba9d1ebf2e3))
- UTRP v2 migration ([#292](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/292)) ([d22237d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d22237d561ddf6a2f8ed699ca4c11088a8b408e8))
- working PNG and CAL downloads ([#119](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/119)) ([d9ee23c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d9ee23c5bbe27240a0e2849aae2eae81d7960bb5))
### Bug Fixes
- add margin-top: -10px to p ([18406b0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/18406b0c94604b1b524de03bf97e78cffe7a5bbf))
- added room number to course popup ([#231](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/231)) ([9eaff24](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9eaff24cbb6d78d7f28521a07c32630a90a96f82))
- align timeBlock div ([4dc8957](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4dc8957c459d0ee57e40c67fa61194ace832327e))
- async course display size ([#181](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/181)) ([949bbb0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/949bbb0835bfb55a81f0800850aa084b4ed1bfc4))
- broken close bracket ([b34aacb](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b34aacb06772af6f83d1f7bafe828ba43a10db85))
- broken file ([92462cf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/92462cf0dffa222d75965885ee0e63181bce9890))
- bugs ([2a01506](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2a0150600f26af5b7394f878e13c4d19611f3f64))
- build ([60ab140](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/60ab140c556ec56eca78b1f4e280dfb6e9dbb538))
- build errors and merge in Casey's branch (driodiwb) ([39947b3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/39947b3694f42f9d5ad2f21d044585aa3d12c407))
- build errors and restructure Calendar page ([c6a48dd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c6a48dd3f6025fe531c18e12b8730683d63f67ed))
- calendar course cell colors ([74be880](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/74be880f9d2dc340ba48416429711fb28915baa2))
- calendar storybook issue ([#125](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/125)) ([0c5bec8](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0c5bec8002022cb4702f3879f43fcd32b9fa6b5a))
- Calendar View/Scaling Issues ([#144](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/144)) ([4c61ebd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4c61ebd3fc93240903f1224f9dd3057a23d30046))
- CalendarCourseCell spacing ([13c69ef](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/13c69ef862f0214dfa6c9368fc3463519aa8f2f0))
- change Chromatic action to build current branch, not base branch ([#100](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/100)) ([e73c9fe](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e73c9fe417169e73c395cde1aeb841eee8add579))
- change material icons to material symbols ([#71](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/71)) ([52e34cb](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/52e34cb830ff1f12659703776bd3d9a24815ee00))
- chromatic build ([11303da](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/11303daebcc1e4de340463e6266082dc0920582d))
- chromatic builds on PRs ([#140](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/140)) ([78a6939](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/78a69399292767a1f8eac66e4c86cbcb8aad019d))
- chromatic storybook - CourseCatalogInjectedPopup ([#106](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/106)) ([ced2997](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ced29975b20a2a5beac51ea875627a2b1042e5c2))
- clean up [#173](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/173) ([#174](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/174)) ([afa634f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/afa634f0853dc16cf313c129434d49bf132388d0))
- cleanup imports ([#112](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/112)) ([b17c3fa](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b17c3fae6d9a8fdb2f6de84dc28d00bc31e7bcc8))
- conflict row bug ([#130](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/130)) ([a8ea3bc](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a8ea3bc683f88fc33191a8cacf649be83f6758f8))
- ConflictsWithWarning ([1599e48](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/1599e48d75d4877b3515c943120d83b59d084a55))
- correct parsing of noon ([#155](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/155)) ([91f62e1](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/91f62e1943633eaef7dc8d437e6a7397456a9123))
- db with proper insertion order ([8e79d6a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8e79d6a6a87210e0a30584c0c9ceb60bd3a33665))
- delete storybook timestamp file ([f93a98e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f93a98e46ae76e9b1474c8143bfc3f5076e8b91f))
- dialog movement ([#227](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/227)) ([bc5d68c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bc5d68ce18f818c285ef7f5eda03f499365ee2e8))
- disabled [object Object] hover message on dialog popups ([#284](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/284)) ([dcc1d81](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/dcc1d81a74922ba27ad2a6859f978f3f20136178))
- divider usage in HeaderAndActions ([#113](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/113)) ([84e8320](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/84e8320e8fce5961877a5745e89419a722055ed5))
- doesn't autoload on pages that don't have pages to load ([#270](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/270)) ([88eeb62](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/88eeb620aec52d6e4172cc908843339412ade90b))
- don't crash on cultural diversity flag ([#196](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/196)) ([5b1e451](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5b1e4513e29203a54ed9f4913c227d6c45d2e67a))
- eslint and remove React-beautiful-dnd ([6af805b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6af805ba3a7b04b7e5705d3d597b5ace686d61e0))
- extra space ([0f43796](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0f43796cd864acd012fc1242cbdf34844df62406))
- Fix popup drag hitbox ([#216](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/216)) ([7a40008](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7a40008c1e3846f09ac4eeadc4a50f810ffc0d90))
- fixed bug where activeSchedule doesn't update correctly ([#158](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/158)) ([a409090](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a409090b904686b76a5db52af980e81108b97082))
- fixed bug with course cells after 12 PM extending past midnight ([#122](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/122)) ([f22a3cd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f22a3cd7c0df138ca6ecd4b4a4efaa7ce845c3dd))
- fixed issues involving course meeting objects not being recognized as course meeting objects ([#132](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/132)) ([3406e9a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3406e9a0e2644218fc06cbd0b05046df5339264c))
- Fixed typescript error ([#161](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/161)) ([df7a7c6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/df7a7c65d695cb5c5a825ac9f5ecc48438735b65))
- grade dist when no instructor ([#269](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/269)) ([6a363ae](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6a363aeb5ccb1c11e15db317b34c4f04a3c9aafc))
- grid JSX.Element generation ([b691bf3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b691bf3231d5002a631687f26c7409e311237662))
- icon library resolution ([#74](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/74)) ([bb3b313](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bb3b313fd272bba12f99270c912e046df5358fed))
- idk why that comment was there ([#177](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/177)) ([c5fc621](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c5fc6219e1540e1fb889d8bedc7296190103a56b))
- import error ([152bc45](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/152bc4577685605ae95f39d9e670b5449e32e0e9))
- improper list data propagation ([#240](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/240)) ([149fda3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/149fda3b721dcc3d8ffa6c2398c28e39dcb62aaa))
- improve dialog handling and error management in list items ([#257](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/257)) ([1942508](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/1942508d8d11f51b62806894ae13162b6404753b))
- injection not working from som/elie commit ([#145](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/145)) ([591687e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/591687eee8731b3649a5f8c1357a416eb2992a93))
- list component fixed ([#162](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/162)) ([5714ed1](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5714ed16d72c5442f58366ac51f40cc32ea406ab))
- made list draggable only by handle ([cbb190b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cbb190bf4b3ad694c87b650489e1fca5aa6b7826))
- margins on list component ([cc71389](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cc7138949cf77d1bcf1520633d669e939540575e))
- non-determinstic options page generation ([#137](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/137)) ([d700110](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d70011016a45f5de62b06839a31c634637024f4a))
- non-virtual dnd ([837fddf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/837fddf8048350b0b25a58bbd16c1957fbad6edb))
- old icon removed in .tsx ([4d387e8](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4d387e8063314c7c6d7452b746728e2cf85a1787))
- only show button hover effects when not disabled ([8e3aa7e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8e3aa7ef3380836fd36a12698b1f6982e110c0f7))
- openTabFromContentScript TS issue ([3a48859](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3a48859dddc0e85649ff6a949e9afd22c962825d))
- options page ([#131](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/131)) ([dc100b5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/dc100b5d3ac1db5f1dc011ea2536c286f56fa55b))
- reactivity ([#188](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/188)) ([4f4f34e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4f4f34e281e8aabcdbf8b06652d05bca3554b11f))
- README.md ([a30fecf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a30fecf8ec76ecae03be9bb4d813803b9ffdb490))
- README.md ([b27b21b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b27b21bbfcc8fbdc130c3fa85776479451745b7b))
- refactor AST parsing for custom ESLint rule ([62f0851](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/62f0851406bf82d3d0481fed40462bdd30ffcf1b))
- remote react-window fully (from List component) ([#114](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/114)) ([5f1c023](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5f1c0231e483bc571ef944bbc0b950ab20d31028))
- remove course name from the syllabi lookup ([#200](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/200)) ([bcb5a8c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bcb5a8c469c4b5c1909880ab6789d67d04487ed2))
- remove extra spacing ([#121](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/121)) ([6ba8b68](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6ba8b68654dc182bd2a084f93b0f4f7d5c132862))
- remove white space after duplicate schedule regex matching ([#286](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/286)) ([d73615e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d73615e28129ee7d78604c6b295ec572b11be46e))
- rename to course block and fix line height for styling ([b602b0b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b602b0b895416a26214e934f9650a8ec0a10711e))
- revert "chore: add default story" ([aef8c3d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/aef8c3d9873c6df79f6a54ab4c669ee1849b5ea5))
- revert "rename to course block and fix line height for styling" ([0273a23](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0273a239139c0f3c0ad773ed8ab1cd4ad8d22fa3))
- revert CalendarGrid and CalendarGridCell back to SCSS from Tailwind ([56306ab](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/56306ab9440baf5ca1158f95d788de7ba42d73d8))
- revert last commit ([27d945f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/27d945f57c76a1c2f31fee9f98c324fab6183bb6))
- Schedule Switching Bugs ([#138](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/138)) ([c51e688](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c51e6881d186e8bef056e483e2bca95585d5eac6))
- support classes with no location ([#242](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/242)) ([3ff06e0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3ff06e043b112bc232c9c173e72b2ea99609cf7a))
- theme colors ([2f537b4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2f537b4f3e3946241f2e287d9ae075e5a2aa7087))
- type issues by using correct import ([#111](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/111)) ([19fe070](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/19fe070491be9e2d40f12918561f6f6ac5921237))
- **ui:** unhid settings button ([#288](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/288)) ([db1eac3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/db1eac33a2786a3db87f0e6b3679233a85ab923f))
- undefined color case ([9aa78a0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9aa78a02a5d4f68daa062e4892875cf5f568306c))
- unocss theme color namings ([79d7832](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/79d7832d0987b8a7c9650b984f72ffd702682423))
- update alignment ([7eb3113](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7eb3113adaa0917c0275576c3366c387bfe77440))
- update daysOfWeek with new DAY_MAP keys ([ecdaadb](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ecdaadb83eb21b144d07d573613fd8971f0a5ce2))
- visual overflow bug when editing schedule name ([#251](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/251)) ([e8d2c2e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e8d2c2e1429072fcc1173ee770f06b07e9ddda20))
- vitest path alias bug ([ee37897](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ee37897df4f65cd2d285e2ad628be43aebb21ea1))
### Reverts
- Revert "individual bug fix" ([b4e8c75](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b4e8c7589e53f1064d70703459cc6d66fae1b04c))
- color palette for calendar ([#118](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/118)) ([51bbd65](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/51bbd6590bf284ce54644b014466c8b2c73b8925))

201
README.md
View File

@@ -1,196 +1,59 @@
# UT Registration Plus
![UTRP Social Preview](images/UTRP_Social-Preview_Prod.svg)
We've all been there. 20 tabs of Rate My Professor, Google Spreadsheet, and the UT Course Schedule open and you still don't know what classes to take.
This extension, UT Registration Plus (UTRP), tries to streamline most of the unnecessary steps and headaches of registering for classes at UT Austin.
![Chrome Web Store](https://img.shields.io/chrome-web-store/v/hboadpjkoaieogjimneceaahlppnipaa)
![Downloads](https://img.shields.io/chrome-web-store/d/hboadpjkoaieogjimneceaahlppnipaa)
![License](https://img.shields.io/github/license/Longhorn-Developers/UT-Registration-Plus)
- For each class in the UT Course Schedule site, UTRP provides a "breakdown" popup, with quick and easy links to the instructor's RateMyProfessor, Course Evaluation Survey (CES) and past syllabi.
**UT Registration Plus (UTRP)** streamlines the process of registering for classes at UT Austin by reducing the chaos of juggling multiple tabs like Rate My Professor, Google Sheets, and the UT Course Schedule. With UTRP, you can simplify class selection and schedule management. We've all been there. 20 tabs of Rate My Professor, Google Spreadsheet, and the UT Course Schedule open and you still don't know what classes to take. UT Registration Plus (UTRP), tries to streamline most of the unnecessary steps and headaches of registering for classes at UT Austin.
- Shows the course description with highlighted information on prerequisites, restrictions, etc.
## Demo
- Shows an aggregate and semesterly graph of the grade distributions for each course.
![UTRP v2 Demo](images/UTRP-Demo.gif)
- Gives you the ability to add "Add Course" and view them in the extension popup, a quick list of all the courses you have saved and an easy way to copy unique numbers.
## Features
- Highlights and crosses-out what courses on the UT Course Catalog would conflict with your currently saved courses, making selecting courses that fit with your schedule so much easier.
- **Quick Access to Class Info**: For each class in the UT Course Schedule, UTRP provides a "breakdown" popup with direct links to RateMyProfessor, Course Evaluation Surveys (CES), and past syllabi.
- **Prerequisite & Restriction Highlights**: Displays course descriptions with highlighted details on prerequisites, restrictions, and other important info.
- **Grade Distribution Graphs**: View an aggregate and semester-specific graph of grade distributions for each course.
- **Saved Courses List**: Easily add courses to a list and view them in the extension popup. Copy unique numbers with a single click.
- **Conflict Detection**: Automatically highlights and strikes out courses that conflict with your saved courses in the UT Course Catalog.
- **Weekly Schedule View**: Displays your saved courses in a weekly schedule format for easier planning.
- **Multiple Schedule Support**: Create multiple schedules to plan for different registration scenarios.
- **And much more!**
- Display's a weekly schedule based on your saved courses.
- Give you the ability to create multiple schedules to plan for different scenarios.
- ... and much more!
## Toolchain
- React v20.9.0 (LTS)
- TypeScript
- Vite 5
- UnoCSS
- ESLint
- Prettier
- Storybook
- Figma
- Semantic-Release
- Custom Messaging & Storage Wrappers
## VSCode Extensions
We recommend using the following VSCode extensions to improve your development experience:
- **[ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint)**: For identifying and fixing linting issues.
- **[Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode)**: For automatic code formatting.
- **[Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)**: For Tailwind CSS class autocomplete and IntelliSense.
- **[UnoCSS](https://marketplace.visualstudio.com/items?itemName=antfu.unocss)**: For better support with UnoCSS utilities.
## Contributing
Contributions are welcome and encouraged! To get started:
1. **Fork** the repository.
2. **Create a new branch**: `git checkout -b feature/your-feature`.
3. **Make changes** and ensure your code passes linting and formatting checks: `pnpm run lint` and `pnpm run prettier`.
4. **Commit your changes** following the [Conventional Commits](#conventional-commits--branch-naming-convention) specification.
5. **Push** your branch: `git push origin feature/your-feature`.
6. **Open a Pull Request**.
For significant changes, its recommended to open an issue first to discuss the proposed updates.
- React 18
- TypeScript
- Vite 5
- ESLint
- Prettier
- Storybook
- Semantic-Release
- Custom Messaging & Storage Wrappers
## Development: Getting Started
1. Clone this repository: `git clone https://github.com/Longhorn-Developers/UT-Registration-Plus.git`
2. **Node Version**: This project requires the Node.js version specified in `.nvmrc`. Use [nvm](https://github.com/nvm-sh/nvm) to install and manage the correct version:
```bash
nvm install
nvm use
```
_Note: Installing the wrong Node version can lead to errors during setup._
3. Install dependencies using `pnpm` (which manages and patches dependencies):
```bash
pnpm install
```
Once set up, the extension can be built to the `dist/` directory using the following methods:
1. Clone this repo
2. This project uses `pnpm` to manage and patch dependencies. Run `pnpm install` to configure the repository for building/development
3. Using either of the methods listed below, the extension will build to the `dist/` directory.
### Development Builds
- Run the development server:
```bash
pnpm dev
```
- Run `pnpm dev`
> **Note**: Injected content on UT pages may not display correctly in development mode. To develop with accurate styles, use the following command:
>
> ```bash
> NODE_ENV='development' pnpm run dev build --mode development -w
> ```
> [!NOTE]
> Injected content such as extension content on UT pages is not properly styled, and are missing class stylings. When developing for these pages, use `NODE_ENV='development' pnpm run dev build --mode development -w` to build and watch for changes. This will ensure you are seeing an accurate representation of the extension.
### Production Builds
- To generate production builds:
```bash
pnpm build
```
- Run `pnpm build`
<details>
<summary>Beta Builds</summary>
Use `BETA=true pnpm build` to generate a beta build.
<summary>Beta builds</summary>
Use `BETA=true pnpm build` to build a beta build.
</details>
## Loading the Extension Manually
## Development: Loading the Extension Manually
To load the extension manually in Chrome:
Open [chrome://extensions](chrome://extensions), ensure you have 'Developer Mode' enabled, and click 'Load unpacked'.
1. Open `chrome://extensions`.
2. Enable 'Developer Mode'.
3. Click 'Load unpacked'.
4. Navigate to the `dist/` directory and select it.
## Bug Reporting
If you encounter bugs or issues, please report them in the [Issues](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues) section, including:
- A clear, descriptive title
- Steps to reproduce the issue
- Expected behavior
- Screenshots or logs (if applicable)
We aim to address issues promptly.
## Conventional Commits & Branch Naming Convention
We follow the **Conventional Commits** specification for commit messages. This ensures a consistent commit history and enables automated versioning and changelog generation.
### Commit Messages
Follow this structure for commit messages:
```
<type>(<scope>): <subject>
```
Where:
- **type**: One of the following:
- `feat`: A new feature
- `fix`: A bug fix
- `docs`: Documentation updates
- `style`: Code formatting changes (whitespace, semicolons, etc.)
- `refactor`: Code restructuring (without adding features or fixing bugs)
- `test`: Adding or modifying tests
- `chore`: Maintenance tasks or build process changes
Example:
```
feat(auth): add login functionality
fix(ui): align buttons in navbar
```
### Branch Naming
Branch names should follow the format:
```
<type>/<short-description>
```
Examples:
- `feat/user-login`
- `fix/navbar-layout`
- `docs/update-readme`
## License
This project is licensed under the MIT License. See the [LICENSE](./LICENSE.md) for more details.
## Code of Conduct
We maintain a strict code of conduct. By contributing, you agree to adhere to the rules outlined in [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
## Acknowledgements
Special thanks to the developers and contributors behind these amazing tools and libraries:
- React v20.9.0 (LTS)
- TypeScript
- Vite 5
- UnoCSS
- ESLint
- Prettier
- Storybook
- Figma
- Semantic-Release
- [chrome-extension-toolkit](https://github.com/sghsri/chrome-extension-toolkit)
- [UT_Grade_Parser](https://github.com/doprz/UT_Grade_Parser)
- [eslint-plugin-import-essentials](https://github.com/doprz/eslint-plugin-import-essentials)
- [UT Registration Plus v1.2.2.7](https://github.com/Longhorn-Developers/UT-Registration-Plus/tree/legacy) by @sghsri
- [UT Registration Planner](https://github.com/doprz/UT-Registration-Planner) by @doprz
- [Figma Designs](https://www.figma.com/design/8tsCay2FRqctrdcZ3r9Ahw/UTRP) by @IsaDavRod
- [Longhorn Developers](https://github.com/Longhorn-Developers) - established in 2024
- The UTRP devs, beta testers, and all supporters of the project!
Navigate to the `dist/` folder, and click 'select' to import the extension.

View File

@@ -4,6 +4,7 @@ export default {
parserPreset: 'conventional-changelog-conventionalcommits',
rules: {
'body-leading-blank': [RuleConfigSeverity.Warning, 'always'] as const,
'body-max-line-length': [RuleConfigSeverity.Error, 'always', 100] as const,
'footer-leading-blank': [RuleConfigSeverity.Warning, 'always'] as const,
'footer-max-line-length': [RuleConfigSeverity.Error, 'always', 100] as const,
'header-max-length': [RuleConfigSeverity.Error, 'always', 100] as const,

View File

@@ -1,48 +0,0 @@
// (Thanks go to https://github.com/pnd280/complexity/blob/alpha/gulpfile.js)
import cp from 'child_process';
import fs from 'fs';
import gulp from 'gulp';
import gulpZip from 'gulp-zip';
import { createRequire } from 'module';
// Make sure sentry is configured https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/typescript/#2-configure-sentry-cli
function instrumentWithSentry() {
return cp.exec('sentry-cli sourcemaps inject dist/ && sentry-cli sourcemaps upload dist/');
}
function zipDist() {
const require = createRequire(import.meta.url);
const manifest = require('./package.json');
const zipFileName = `${manifest.name.replaceAll(' ', '-')}-${manifest.version}.zip`;
return gulp
.src('dist/**', {
encoding: false,
})
.pipe(gulpZip(zipFileName))
.pipe(gulp.dest('package'));
}
const zip = gulp.series(instrumentWithSentry, zipDist);
// Temp fix for CSP on Chrome 130
// Manually remove them because there is no option to disable use_dynamic_url on @crxjs/vite-plugin
function forceDisableUseDynamicUrl(done) {
const require = createRequire(import.meta.url);
const manifest = require('./dist/manifest.json');
manifest.web_accessible_resources.forEach(resource => {
delete resource.use_dynamic_url;
});
if (!fs.existsSync('./dist/manifest.json')) {
return done();
}
fs.writeFileSync('./dist/manifest.json', JSON.stringify(manifest, null, 2));
done();
}
export { forceDisableUseDynamicUrl, zip };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,7 +1,7 @@
{
"name": "ut-registration-plus",
"displayName": "UT Registration Plus",
"version": "2.0.1",
"version": "2.0.0-beta4",
"description": "UT Registration Plus is a Chrome extension that allows students to easily register for classes.",
"private": true,
"homepage": "https://github.com/Longhorn-Developers/UT-Registration-Plus",
@@ -9,8 +9,6 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build:watch": "NODE_ENV='development' vite build --mode development -w",
"zip": "pnpm build && pnpm gulp zip",
"prettier": "prettier src --check",
"prettier:fix": "prettier src --write",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives",
@@ -20,128 +18,106 @@
"test:ui": "vitest --ui",
"coverage": "vitest run --coverage",
"preview": "vite preview",
"generate-changelog": "bun run scripts/generateChangelog.ts",
"preinstall": "npx only-allow pnpm",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"prepare": "husky"
},
"dependencies": {
"@headlessui/react": "^2.1.10",
"@hello-pangea/dnd": "^17.0.0",
"@octokit/rest": "^21.0.2",
"@sentry/react": "^8.34.0",
"@unocss/vite": "^0.63.4",
"@vitejs/plugin-react": "^4.3.2",
"@headlessui/react": "^2.0.3",
"@hello-pangea/dnd": "^16.5.0",
"@unocss/vite": "^0.58.6",
"@vitejs/plugin-react": "^4.2.1",
"chrome-extension-toolkit": "^0.0.54",
"clsx": "^2.1.1",
"conventional-changelog": "^6.0.0",
"highcharts": "^11.4.8",
"clsx": "^2.1.0",
"highcharts": "^11.3.0",
"highcharts-react-official": "^3.2.1",
"html-to-image": "^1.11.11",
"husky": "^9.1.6",
"kc-dabr-wasm": "^0.1.2",
"nanoid": "^5.0.7",
"husky": "^9.0.11",
"nanoid": "^5.0.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-loading-skeleton": "^3.5.0",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.5.0",
"remark-gfm": "^4.0.0",
"sass": "^1.79.5",
"simple-git": "^3.27.0",
"sql.js": "1.11.0"
"sass": "^1.71.1",
"sql.js": "1.10.2"
},
"devDependencies": {
"@chromatic-com/storybook": "^2.0.2",
"@commitlint/cli": "^19.5.0",
"@commitlint/config-conventional": "^19.5.0",
"@commitlint/types": "^19.5.0",
"@chromatic-com/storybook": "^1.4.0",
"@commitlint/cli": "^18.6.1",
"@commitlint/config-conventional": "^18.6.2",
"@commitlint/types": "^19.0.3",
"@crxjs/vite-plugin": "2.0.0-beta.21",
"@iconify-json/bi": "^1.2.1",
"@iconify-json/iconoir": "^1.2.2",
"@iconify-json/material-symbols": "^1.2.4",
"@iconify-json/ri": "^1.2.1",
"@semantic-release/exec": "^6.0.3",
"@sentry/types": "^8.34.0",
"@storybook/addon-designs": "^8.0.3",
"@storybook/addon-essentials": "^8.3.5",
"@storybook/addon-links": "^8.3.5",
"@storybook/blocks": "^8.3.5",
"@storybook/react": "^8.3.5",
"@storybook/react-vite": "^8.3.5",
"@storybook/test": "^8.3.5",
"@iconify-json/bi": "^1.1.23",
"@iconify-json/material-symbols": "^1.1.73",
"@iconify-json/ri": "^1.1.20",
"@storybook/addon-designs": "^8.0.1",
"@storybook/addon-essentials": "^8.1.1",
"@storybook/addon-links": "^8.1.1",
"@storybook/blocks": "^8.1.1",
"@storybook/react": "^8.1.1",
"@storybook/react-vite": "^8.1.1",
"@storybook/test": "^8.1.1",
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@types/chrome": "^0.0.273",
"@types/conventional-changelog": "^3.1.5",
"@types/gulp": "^4.0.17",
"@types/gulp-zip": "^4.0.4",
"@types/node": "^22.7.5",
"@types/chrome": "^0.0.268",
"@types/node": "^20.12.12",
"@types/prompts": "^2.4.9",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/semantic-release": "^20.0.6",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"@types/semver": "^7.5.8",
"@types/sql.js": "^1.4.9",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@unocss/eslint-config": "^0.63.4",
"@unocss/postcss": "^0.63.4",
"@unocss/preset-uno": "^0.63.4",
"@unocss/preset-web-fonts": "^0.63.4",
"@unocss/reset": "^0.63.4",
"@unocss/transformer-directives": "^0.63.4",
"@unocss/transformer-variant-group": "^0.63.4",
"@vitejs/plugin-react-swc": "^3.7.1",
"@vitest/coverage-v8": "^2.1.2",
"@vitest/ui": "^2.1.2",
"chalk": "^5.3.0",
"chromatic": "^11.12.5",
"cssnano": "^7.0.6",
"cssnano-preset-advanced": "^7.0.6",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@unocss/eslint-config": "^0.58.6",
"@unocss/postcss": "^0.58.6",
"@unocss/preset-uno": "^0.58.6",
"@unocss/preset-web-fonts": "^0.58.6",
"@unocss/reset": "^0.58.6",
"@unocss/transformer-directives": "^0.58.6",
"@unocss/transformer-variant-group": "^0.58.6",
"@vitejs/plugin-react-swc": "^3.6.0",
"@vitest/coverage-v8": "^1.3.1",
"@vitest/ui": "^1.3.1",
"chromatic": "^11.3.5",
"cssnano": "^6.0.5",
"cssnano-preset-advanced": "^6.0.5",
"dotenv": "^16.4.5",
"es-module-lexer": "^1.5.4",
"eslint": "^8.57.1",
"es-module-lexer": "^1.4.1",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.31.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-import-essentials": "^0.2.1",
"eslint-plugin-jsdoc": "^50.3.2",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-jsdoc": "^48.2.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-prefer-function-component": "^3.3.0",
"eslint-plugin-react-refresh": "^0.4.12",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-storybook": "^0.9.0",
"gulp": "^5.0.0",
"gulp-zip": "^6.0.0",
"eslint-plugin-react-refresh": "^0.4.5",
"eslint-plugin-simple-import-sort": "^12.0.0",
"eslint-plugin-storybook": "^0.8.0",
"path": "^0.12.7",
"postcss": "^8.4.47",
"prettier": "^3.3.3",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"react-dev-utils": "^12.0.1",
"semantic-release": "^24.1.2",
"storybook": "^8.3.5",
"typescript": "^5.6.3",
"unocss": "^0.63.4",
"unocss-preset-primitives": "0.0.2-beta.1",
"unplugin-icons": "^0.19.3",
"vite": "^5.4.8",
"vite-plugin-inspect": "^0.8.7",
"vitest": "^2.1.2"
"storybook": "^8.1.1",
"typescript": "^5.4.3",
"unocss": "^0.58.6",
"unocss-preset-primitives": "0.0.2-beta.0",
"unplugin-icons": "^0.18.5",
"vite": "^5.1.4",
"vite-plugin-inspect": "^0.8.3",
"vitest": "^1.3.1"
},
"pnpm": {
"patchedDependencies": {
"@crxjs/vite-plugin@2.0.0-beta.21": "patches/@crxjs__vite-plugin@2.0.0-beta.21.patch",
"@unocss/vite": "patches/@unocss__vite.patch"
"@unocss/vite@0.58.6": "patches/@unocss__vite@0.58.6.patch"
},
"overrides": {
"es-module-lexer": "^1.5.4"
"es-module-lexer": "^1.4.1"
}
}
}

View File

@@ -1,50 +0,0 @@
diff --git a/dist/index.mjs b/dist/index.mjs
index 7210f5fd650a0b7bb36b467fff85feb0d8e4ec63..c8f98bd314daec0b91c514ea9d9fc2b79cea8502 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -369,15 +369,15 @@ const VIRTUAL_ENTRY_ALIAS = [
/^(?:virtual:)?uno(?::(.+))?\.css(\?.*)?$/
];
const LAYER_MARK_ALL = "__ALL__";
-const RESOLVED_ID_WITH_QUERY_RE = /[/\\]__uno(_.*?)?\.css(\?.*)?$/;
-const RESOLVED_ID_RE = /[/\\]__uno(?:_(.*?))?\.css$/;
+const RESOLVED_ID_WITH_QUERY_RE = /[/\\]uno(_.*?)?\.css(\?.*)?$/;
+const RESOLVED_ID_RE = /[/\\]uno(?:_(.*?))?\.css$/;
function resolveId(id) {
if (id.match(RESOLVED_ID_WITH_QUERY_RE))
return id;
for (const alias of VIRTUAL_ENTRY_ALIAS) {
const match = id.match(alias);
if (match) {
- return match[1] ? `/__uno_${match[1]}.css` : "/__uno.css";
+ return match[1] ? `/uno_${match[1]}.css` : "/uno.css";
}
}
}
@@ -652,7 +652,7 @@ function GlobalModeBuildPlugin(ctx) {
css = await applyCssTransform(css, fakeCssId, options.dir, this);
const transformHandler = "handler" in cssPost.transform ? cssPost.transform.handler : cssPost.transform;
if (isLegacy) {
- await transformHandler.call({}, css, "/__uno.css");
+ await transformHandler.call({}, css, "/uno.css");
} else {
const hash = getHash(css);
await transformHandler.call({}, getHashPlaceholder(hash), fakeCssId);
@@ -914,7 +914,7 @@ function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules,
const { hash, css } = await generateCSS(layer);
return {
// add hash to the chunk of CSS that it will send back to client to check if there is new CSS generated
- code: `${css}__uno_hash_${hash}{--:'';}`,
+ code: `${css}uno_hash_${hash}{--:'';}`,
map: { mappings: "" }
};
},
@@ -933,7 +933,7 @@ function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules,
if (layer && code.includes("import.meta.hot")) {
let hmr = `
try {
- let hash = __vite__css.match(/__uno_hash_(\\w{${HASH_LENGTH}})/)
+ let hash = __vite__css.match(/uno_hash_(\\w{${HASH_LENGTH}})/)
hash = hash && hash[1]
if (!hash)
console.warn('[unocss-hmr]', 'failed to get unocss hash, hmr might not work')

View File

@@ -0,0 +1,108 @@
diff --git a/dist/index.cjs b/dist/index.cjs
index 560f423a07f21b0c47abd494d77654de4c874481..35ae1fdca8bd5546f7e40a23edacb1dbbbd34b58 100644
--- a/dist/index.cjs
+++ b/dist/index.cjs
@@ -35,15 +35,15 @@ const VIRTUAL_ENTRY_ALIAS = [
/^(?:virtual:)?uno(?::(.+))?\.css(\?.*)?$/
];
const LAYER_MARK_ALL = "__ALL__";
-const RESOLVED_ID_WITH_QUERY_RE = /[\/\\]__uno(?:(_.*?))?\.css(\?.*)?$/;
-const RESOLVED_ID_RE = /[\/\\]__uno(?:_(.*?))?\.css$/;
+const RESOLVED_ID_WITH_QUERY_RE = /[\/\\]uno(?:(_.*?))?\.css(\?.*)?$/;
+const RESOLVED_ID_RE = /[\/\\]uno(?:_(.*?))?\.css$/;
function resolveId(id) {
if (id.match(RESOLVED_ID_WITH_QUERY_RE))
return id;
for (const alias of VIRTUAL_ENTRY_ALIAS) {
const match = id.match(alias);
if (match) {
- return match[1] ? `/__uno_${match[1]}.css` : "/__uno.css";
+ return match[1] ? `/uno_${match[1]}.css` : "/uno.css";
}
}
}
@@ -745,7 +745,7 @@ function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules,
const { hash, css } = await generateCSS(layer);
return {
// add hash to the chunk of CSS that it will send back to client to check if there is new CSS generated
- code: `__uno_hash_${hash}{--:'';}${css}`,
+ code: `uno_hash_${hash}{--:'';}${css}`,
map: { mappings: "" }
};
},
@@ -764,7 +764,7 @@ function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules,
if (layer && code.includes("import.meta.hot")) {
let hmr = `
try {
- let hash = __vite__css.match(/__uno_hash_(\\w{${HASH_LENGTH}})/)
+ let hash = __vite__css.match(/uno_hash_(\\w{${HASH_LENGTH}})/)
hash = hash && hash[1]
if (!hash)
console.warn('[unocss-hmr]', 'failed to get unocss hash, hmr might not work')
diff --git a/dist/index.mjs b/dist/index.mjs
index bbbccb7cad7421cbdb97223a451ec5853c0476cb..4bf6a08d94e562090a530308c0ab8337afdf8243 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -1,17 +1,17 @@
-import process$1 from 'node:process';
+import remapping from '@ampproject/remapping';
+import { createFilter } from '@rollup/pluginutils';
+import { loadConfig } from '@unocss/config';
+import { BetterMap, createGenerator, cssIdRE, notNull, toEscapedSelector } from '@unocss/core';
import UnocssInspector from '@unocss/inspector';
-import { resolve, isAbsolute, dirname } from 'node:path';
-import fs from 'node:fs/promises';
import fg from 'fast-glob';
import MagicString from 'magic-string';
-import remapping from '@ampproject/remapping';
-import { createHash } from 'node:crypto';
-import { cssIdRE, createGenerator, BetterMap, notNull, toEscapedSelector } from '@unocss/core';
import { Buffer } from 'node:buffer';
-import { createFilter } from '@rollup/pluginutils';
+import { createHash } from 'node:crypto';
import fs$1 from 'node:fs';
+import fs from 'node:fs/promises';
+import { dirname, isAbsolute, resolve } from 'node:path';
+import process$1 from 'node:process';
import { fileURLToPath } from 'node:url';
-import { loadConfig } from '@unocss/config';
const defaultPipelineExclude = [cssIdRE];
const defaultPipelineInclude = [/\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/];
@@ -20,15 +20,15 @@ const VIRTUAL_ENTRY_ALIAS = [
/^(?:virtual:)?uno(?::(.+))?\.css(\?.*)?$/
];
const LAYER_MARK_ALL = "__ALL__";
-const RESOLVED_ID_WITH_QUERY_RE = /[\/\\]__uno(?:(_.*?))?\.css(\?.*)?$/;
-const RESOLVED_ID_RE = /[\/\\]__uno(?:_(.*?))?\.css$/;
+const RESOLVED_ID_WITH_QUERY_RE = /[\/\\]uno(?:(_.*?))?\.css(\?.*)?$/;
+const RESOLVED_ID_RE = /[\/\\]uno(?:_(.*?))?\.css$/;
function resolveId(id) {
if (id.match(RESOLVED_ID_WITH_QUERY_RE))
return id;
for (const alias of VIRTUAL_ENTRY_ALIAS) {
const match = id.match(alias);
if (match) {
- return match[1] ? `/__uno_${match[1]}.css` : "/__uno.css";
+ return match[1] ? `/uno_${match[1]}.css` : "/uno.css";
}
}
}
@@ -730,7 +730,7 @@ function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules,
const { hash, css } = await generateCSS(layer);
return {
// add hash to the chunk of CSS that it will send back to client to check if there is new CSS generated
- code: `__uno_hash_${hash}{--:'';}${css}`,
+ code: `uno_hash_${hash}{--:'';}${css}`,
map: { mappings: "" }
};
},
@@ -749,7 +749,7 @@ function GlobalModeDevPlugin({ uno, tokens, tasks, flushTasks, affectedModules,
if (layer && code.includes("import.meta.hot")) {
let hmr = `
try {
- let hash = __vite__css.match(/__uno_hash_(\\w{${HASH_LENGTH}})/)
+ let hash = __vite__css.match(/uno_hash_(\\w{${HASH_LENGTH}})/)
hash = hash && hash[1]
if (!hash)
console.warn('[unocss-hmr]', 'failed to get unocss hash, hmr might not work')

13096
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,54 +0,0 @@
import { exec } from 'child_process';
import { resolve } from 'path';
import { promisify } from 'util';
const execPromise = promisify(exec);
interface Props {
preset:
| 'angular'
| 'atom'
| 'codemirror'
| 'conventionalcommits'
| 'ember'
| 'eslint'
| 'express'
| 'jquery'
| 'jshint';
// The file to write the changelog to
outFile?: string;
// How many releases to be generated from the latest
// If 0, the whole changelog will be regenerated and the outfile will be overwritten
releaseCount?: number;
}
/**
* Generates a changelog using the conventional-changelog command.
*
* @returns {Promise<void>} A promise that resolves when the changelog is generated.
* @throws {Error} If there is an error generating the changelog.
*/
async function generateChangelog({ preset, outFile = 'CHANGELOG.md', releaseCount = 1 }: Props): Promise<void> {
try {
// Run the conventional-changelog command to generate changelog
const { stdout, stderr } = await execPromise(
`conventional-changelog -p ${preset} -i ${outFile} -s -r ${releaseCount}`,
{
// Ensures it runs from the project root
cwd: resolve(process.cwd()),
}
);
// Log output and error if any
console.log(stdout);
if (stderr) {
console.error(stderr);
}
} catch (error) {
console.error('Error generating changelog:', error);
}
}
generateChangelog({ preset: 'conventionalcommits', releaseCount: 0 });

View File

@@ -1,53 +0,0 @@
import prompts from 'prompts';
import { simpleGit } from 'simple-git';
import { getSourceRef } from '../utils/git/getSourceRef';
import { error, info } from '../utils/log';
const git = simpleGit();
const status = await git.status();
if (status.files.length) {
console.log(error('Working directory is not clean, please commit or stash changes before releasing.'));
process.exit(1);
}
const { destinationBranch } = await prompts({
type: 'select',
name: 'destinationBranch',
message: 'What kind of release do you want to create?',
choices: ['preview', 'production'].map(releaseType => ({
title: releaseType,
value: releaseType,
})),
});
const sourceRef = await getSourceRef(destinationBranch);
const { confirm } = await prompts({
type: 'confirm',
name: 'confirm',
message: `Are you sure you want to create a ${destinationBranch} release from ${sourceRef}?`,
});
if (!confirm) {
console.log(error('Aborting release.'));
process.exit(1);
}
// we fetch the latest changes from the remote
await git.fetch();
// we checkout the source ref, pull the latest changes and then checkout the destination branch
console.info(`Checking out and updating ${sourceRef}...`);
await git.checkout(sourceRef);
await git.pull('origin', sourceRef);
console.info(`Checking out and updating ${destinationBranch}...`);
await git.checkout(destinationBranch);
await git.pull('origin', destinationBranch);
// we trigger the release github action by merging the source ref into the destination branch
console.info(`Merging ${sourceRef} into ${destinationBranch}...`);
await git.merge([sourceRef, '--ff-only']);
await git.push('origin', destinationBranch);
console.info(info(`Release to ${destinationBranch} created! Check github for status`));

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -4,10 +4,7 @@ const splashText: string[] = [
'Rec Sports fills up fast, even before the sun reaches its peak.',
"Ah, Jendy's! A taste ever so refined.",
'Fine dining at Jester City Limits, eh?',
'The road to Kinsolving is long, but their delicacies makes it worth every step.',
'The squirrels, they have mastered begging for food better than students. Impressive... but worrying.',
"Do you study often? Ha! What am I saying? Of course you don't.",
"Hey, you, you're finally awake. You were trying to skip class right?",
'Rec Sports fills up fast, even before the sun reaches its peak.',
'RIP Domino, you beloved campus feline.',
"The year is 2055 and Welch still isn't finished.",
'Motivation dropping faster than ur GPA',
@@ -16,105 +13,42 @@ const splashText: string[] = [
'Pain is temporary, GPA is forever.',
"You've Yee'd Your Last Haw.",
'lol everything is already waitlisted.',
'Could be worse. Could be A&M.',
// 'TeXAs iS BaCK GuYZ',
"At Least You're Not At A&M.",
'TeXAs iS BaCK GuYZ',
'mAke iT yOuR tExAS',
'change yOur slogan',
"'Academically Challenged'",
'Does McCombs teach Parseltongue?',
'No Cruce Enfrente Del Bus.',
'Omae Wa Mou Shindeiru...',
'Bevo Bucks are the new Bitcoin',
'Every day another brick disappears from Speedway',
'The GDC will annex the EER one day',
'Just you wait. Our CNS operatives will topple the EER regime',
'To hike to Kins or not to hike to Kins...',
'The road to Kinsolving is long, but their delicacies makes it worth every step.',
"C'mon you Longhorns! You want to study forever?",
'HOW BOUT A NICE CUP OF LIBER TEA',
"Block of Butter? Now, that's a name I haven't heard in a long time...",
"It's called the quiet floor of the PCL for a reason",
"'Whose car is this and why is it attempting to enter Welch?'",
"'I really like one of my TAs and I wanna ask her out after this semester ends'",
'CaN YoU aSk OuT a tA aFtEr tHe SeMeStEr Is oVeR AnD gRaDeS ArE DoNe?',
"The Block of Butter incident of '22",
'Begun, the midterms have.',
'You must construct additional schedules',
"Arrows of Christ vs Church of Scientology was the crossover we didn't know we needed",
'THE WALK SIGN IS ON TO CROSS GUADALUPE AND 21ST',
'Pay attention. Might learn something.',
'Long ago, apartment rates lived together in harmony. Then, everything changed when American Campus Communities Inc attacked.',
'Roll for Initiative!',
'Days since last GDC door alarm: 0',
'Finding a parking spot is like winning the lottery... if the lottery required parallel parking skills.',
'The squirrels are more ambitious than most freshmen during finals week.',
'The line at the on-campus Starbucks is longer than your course waitlist.',
'The weather changes more often than your class schedule.',
'Mmm... Brutalist architecture...',
'The course syllabus: more than meets the eye',
"'Sorry, the PCL is full' is the most heartbreaking message you'll ever receive.",
'Getting to class on time is like navigating a maze of construction zones.',
"'studying' often means refreshing Canvas every five minutes to see if the professor posted lecture slides.",
"It's over Bevo! I have the high ground!",
"I'll just skip this lecture and watch the recording later. What's the worst that could happen?",
'I hear the tower bells are ringing, PCL full of students dreaming.',
"I don't like sand",
"'I'll just skip this lecture' often turns into a semester-long habit.",
'The libraries are full of students pretending to study but actually napping with their eyes open.',
"The 'campus loop' refers to both the bus route and the endless cycle of trying to find your way around campus.",
'The squirrels have mastered the art of begging for food better than most students during finals week.',
"'going for a run' often means 'running to get food from food trucks'.",
'We got UTRPv2 before GTA 6',
'Midterm season: Finish the Fight!',
"There's a Bevo on your lawn!",
'Veer was here',
'follow @sghsri!',
'Officially part of the SEC',
'Planner is now acquired by Plus',
'Longhorn-Developers is the best UT Student Org',
'The Eiffel Tower is the UT Tower of Paris',
'A pen and paper is old fashioned, but sometimes old ways are best',
'A heart is like bedrock, destroyable only by cheating',
'You may not rest now, there are Canvas assignments nearby',
'You are filled with DETERMINATION',
'60k+ users!',
'Almost Turing complete',
'#BF5700',
'The waitlist is a lie!',
`He's a CS Major, but he showers regularly. 🧢`,
'A CS major walks into a bar. The bar is empty because it is a CS major.',
'UT Registration Plus - The only thing that can make registration worse is not having it',
'UT Registration Plus - We make registration slightly less painful. Slightly.',
'UT Registration Plus - Do you really want to figure out which professors will ruin your GPA by yourself?',
'Ayo tf is a memory leak',
"lowkey we never thought we'd get this far, how tf are 60k of you people using this",
"dang we're really out here making a splash",
"We'd make a joke about A&M, but we're not sure they can read",
"We've only caused one or two outages, we swear!",
'Reality has a slight CNS bias',
'That one time in 2019 we accidentally DDoSed the registration system. (really)',
'Best UT financial aid tip? Marry rich.',
'Bevo Bucks are legal tender',
"'iS iT fInAlS wEeK oR mY fInAl WeEk'",
'Do you have an iPhone charger? Wait... Which one?',
'Deep in the heart of Texas',
'TEXAS!!! FIGHT!!!',
'Launder your AP credits through a community college for free',
'Also try Minecraft!',
'Please register later so the dev team can get the upper divs',
'The outdoor warning siren will test around 11:50 a.m.',
'You may be seeing this page because you used the Back button while browsing a secure web site or application. Alternatively, you may have mistakenly bookmarked the web sign in form instead of the actual web site you wanted to bookmark or used a link created by somebody else who made the same mistake.',
'Flag on the play, procrastinating picking classes, 1 day penalty, still first down',
'Pro-fessionals at Pro-crastinating',
'Built Texas Tough',
'Beware of trucks on Speedway',
'Marked as duplicate',
'If we ever flood, we can use the canoes on Speedway',
'Days since last Jester West fire alarm: 0',
'Remember the Alamo!',
"Figure out next year's housing by October",
'Technoblade never dies!',
// eslint-disable-next-line no-template-curly-in-string
'Why is my ${major} class in the ${unrelatedMajor} build?', // This is on purpose as a joke
'"Will that be cash, card, or id?"',
'How much did PepsiCo pay UT to replace all the coke machines?',
"Do it... take those 8am's",
"I'm polylingual, if you count programming languages",
'Just put the classes in the schedule lil bro',
'The wait is finally over',
"Stop trying to make UTRP happen, it's not going to happen!",
'Befriend the raccoons on campus',
`It's ${new Date().toLocaleString('en-US', { month: 'long', day: 'numeric' })} and OU still sucks`,
'As seen on TV! ',
"Should you major in Compsci? well, here's a better question. do you wanna have a bad time?",
'The struggle to fit all your classes into one schedule is as real as the struggle to fit all your groceries into a mini-fridge.',
];
export default splashText;

View File

@@ -1,5 +1,4 @@
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';
@@ -78,7 +77,6 @@ function DevDashboard() {
const [localStorage, setLocalStorage] = React.useState<Record<string, unknown>>({});
const [syncStorage, setSyncStorage] = React.useState<Record<string, unknown>>({});
const [sessionStorage, setSessionStorage] = React.useState<Record<string, unknown>>({});
useKC_DABR_WASM();
useEffect(() => {
const onVisibilityChange = () => {

View File

@@ -31,20 +31,6 @@ chrome.runtime.onInstalled.addListener(details => {
}
});
// migration/login logic
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => {
// console.log(changeInfo);
if (changeInfo.url === 'https://utdirect.utexas.edu/apps/registrar/course_schedule/utrp_login/') {
function openPopupAction() {
chrome.tabs.onActivated.removeListener(openPopupAction);
chrome.action.openPopup();
}
chrome.tabs.onActivated.addListener(openPopupAction);
await chrome.tabs.remove(tabId);
}
});
// initialize the message listener that will listen for messages from the content script
const messageListener = new MessageListener<BACKGROUND_MESSAGES>({
...browserActionHandler,

View File

@@ -30,8 +30,7 @@ const calendarBackgroundHandler: MessageHandler<CalendarBackgroundMessages> = {
if (openCalendarTabInfo !== undefined) {
const tabid = openCalendarTabInfo.tab.id;
await chrome.tabs.update(tabid, { active: true });
await chrome.windows.update(openCalendarTabInfo.tab.windowId, { focused: true, drawAttention: true });
chrome.tabs.update(tabid, { active: true });
if (uniqueId !== undefined) await tabs.openCoursePopup({ uniqueId }, tabid);
sendResponse(openCalendarTabInfo.tab);

View File

@@ -1,39 +1,25 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import type { UserSchedule } from '@shared/types/UserSchedule';
import { generateRandomId } from '@shared/util/random';
import type { Serialized } from 'chrome-extension-toolkit';
/**
* Creates a new schedule with the given name
* @param scheduleName the name of the schedule to create
* @returns undefined if successful, otherwise an error message
*/
export default async function createSchedule(scheduleName: string) {
export default async function createSchedule(scheduleName: string): Promise<string | undefined> {
const schedules = await UserScheduleStore.get('schedules');
// get the number of schedules that either have the same name or have the same name with a number appended (e.g. "New Schedule (1)")
// this way we can prevent duplicate schedule names and increment the number if necessary
// if (schedules.find(schedule => schedule.name === scheduleName)) {
// return `Schedule ${scheduleName} already exists`;
// }
// Regex to match schedule names that follow the pattern "ScheduleName" or "ScheduleName (1)", "ScheduleName (2)", etc.
const regex = new RegExp(`^${scheduleName}( \\(\\d+\\))?$`);
// Find how many schedules match the base name or follow the pattern with a number
const count = schedules.filter(s => regex.test(s.name)).length;
// If any matches are found, append the next number to the schedule name
let name = scheduleName;
if (count > 0) {
name = `${scheduleName} (${count})`;
}
const newSchedule: Serialized<UserSchedule> = {
schedules.push({
id: generateRandomId(),
name,
name: scheduleName,
courses: [],
hours: 0,
updatedAt: Date.now(),
};
schedules.push(newSchedule);
});
await UserScheduleStore.set('schedules', schedules);
return newSchedule.id;
return undefined;
}

View File

@@ -1,7 +1,5 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import createSchedule from './createSchedule';
/**
* Deletes a schedule with the specified name.
*
@@ -19,29 +17,10 @@ export default async function deleteSchedule(scheduleId: string): Promise<string
throw new Error(`Schedule ${scheduleId} does not exist`);
}
if (scheduleIndex === activeIndex) {
throw new Error(`Cannot delete active schedule`);
}
if (scheduleIndex < activeIndex) {
await UserScheduleStore.set('activeIndex', activeIndex - 1);
throw new Error('Cannot delete active schedule');
}
schedules.splice(scheduleIndex, 1);
await UserScheduleStore.set('schedules', schedules);
if (activeIndex >= schedules.length) {
await UserScheduleStore.set('activeIndex', schedules.length - 1);
}
return undefined;
}
/**
* Deletes all schedules.
*
* @returns A promise that resolves when all schedules are deleted
*/
export async function deleteAllSchedules(): Promise<void> {
await UserScheduleStore.set('schedules', []);
await UserScheduleStore.set('activeIndex', 0);
await createSchedule('Schedule 1');
}

View File

@@ -1,31 +0,0 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { generateRandomId } from '@shared/util/random';
import handleDuplicate from './handleDuplicate';
/**
* Creates a new schedule with the given name
* @param scheduleName the name of the schedule to create
* @returns undefined if successful, otherwise an error message
*/
export default async function duplicateSchedule(scheduleId: string): Promise<string | undefined> {
const schedules = await UserScheduleStore.get('schedules');
const schedule = schedules.find(schedule => schedule.id === scheduleId);
if (schedule === undefined) {
throw new Error(`Schedule ${scheduleId} does not exist`);
}
const updatedName = await handleDuplicate(schedule.name);
schedules.push({
id: generateRandomId(),
name: updatedName,
courses: JSON.parse(JSON.stringify(schedule.courses)),
hours: schedule.hours,
updatedAt: Date.now(),
} satisfies typeof schedule);
await UserScheduleStore.set('schedules', schedules);
return undefined;
}

View File

@@ -1,37 +0,0 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
/**
* Duplicates a new schedule with the given name.
* Assumes that each schedule has a unique name.
* @param scheduleName the name of the schedule to handle duplication for
* @param schedules the list of UserSchedules to find existing names
* @returns the new name for the schedule, of the form `{baseName}({index})`
*/
export default async function handleDuplicate(scheduleName: string): Promise<string> {
const schedules = await UserScheduleStore.get('schedules');
// No point in checking for duplicates if the name is unique
if (schedules.find(schedule => schedule.name === scheduleName) === undefined) {
return scheduleName;
}
// Regex for matching `{baseName}({index})`, where match[1] = baseName, match[2] = (index)
const regex = /^(.+?)(\(\d+\))?$/;
// Extract base name and existing index
const match = scheduleName.match(regex);
const baseName = match && match[1] ? match[1].trim() : scheduleName;
// Extract number from parentheses and increment
let index = match && match[2] ? parseInt(match[2].slice(1, -1), 10) + 1 : 1;
let newName: string;
// Increment until an unused index is found
do {
newName = `${baseName} (${index++})`;
// eslint-disable-next-line @typescript-eslint/no-loop-func
} while (schedules.find(schedule => schedule.name === newName));
return newName;
}

View File

@@ -1,78 +0,0 @@
import { validateLoginStatus } from '@shared/util/checkLoginStatus';
import { getActiveSchedule } from '@views/hooks/useSchedules';
import { courseMigration } from '@views/lib/courseMigration';
import addCourse from './addCourse';
import createSchedule from './createSchedule';
import switchSchedule from './switchSchedule';
/**
* Retrieves the saved courses from the extension's chrome sync storage (old store) and returns an array of course links.
*
* @returns A promise that resolves to an array of course links.
*/
export async function getUTRPv1Courses(): Promise<string[]> {
const { savedCourses } = await chrome.storage.sync.get('savedCourses');
// Check if the savedCourses array is empty
if (!savedCourses || savedCourses.length === 0) {
return [];
}
// Extract the link property from each course object and return it as an array
return savedCourses.map((course: { link: string }) => course.link);
}
/**
* Handles the migration of UTRP v1 courses.
*
* This function checks if the user is logged into the UT course schedule page.
* If the user is not logged in, it logs a message and exits. If the user is
* logged in, it retrieves the UTRP v1 courses, creates a new schedule for the
* migration, switches to the new schedule, and migrates the courses to the
* active schedule.
*
* @returns A promise that resolves when the migration is complete.
*/
async function migrateUTRPv1Courses() {
const loggedInToUT = await validateLoginStatus(
'https://utdirect.utexas.edu/apps/registrar/course_schedule/utrp_login/'
);
if (!loggedInToUT) {
console.warn('Not logged in to UT Registrar.');
return false;
}
const oldCourses = await getUTRPv1Courses();
console.log(oldCourses);
const migratedCourses = await courseMigration(oldCourses);
if (migratedCourses.length > 0) {
console.log(oldCourses, migratedCourses);
const migrateSchedule = await createSchedule('Migrated Schedule');
await switchSchedule(migrateSchedule);
const activeSchedule = getActiveSchedule();
for (const course of migratedCourses) {
// Add the course if it doesn't already exist
if (activeSchedule.courses.every(c => c.uniqueId !== course.uniqueId)) {
// ignore eslint, as we *do* want to spend time on each iteration
// eslint-disable-next-line no-await-in-loop
await addCourse(activeSchedule.id, course);
}
}
// Remove the old courses from storage :>
await chrome.storage.sync.remove('savedCourses');
console.log('Successfully migrated UTRP v1 courses');
} else {
console.warn('No courses successfully found to migrate');
}
return true;
}
export default migrateUTRPv1Courses;

View File

@@ -1,41 +1,21 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import handleDuplicate from './handleDuplicate';
/**
* Renames a schedule with the specified name to a new name.
* @param scheduleId - The id of the schedule to be renamed.
* @param newName - The new name for the schedule.
* @returns A promise that resolves to the new name if successful, otherwise undefined.
* @returns A promise that resolves to a string if there is an error, or undefined if the schedule is renamed successfully.
*/
export default async function renameSchedule(scheduleId: string, newName: string): Promise<string | undefined> {
const schedules = await UserScheduleStore.get('schedules');
const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId);
if (scheduleIndex === -1) {
return undefined;
}
const schedule = schedules[scheduleIndex];
if (schedule === undefined) {
return undefined;
return `Schedule ${scheduleId} does not exist`;
}
// if old name is of the form `{baseName}{index}` and newName === baseName, do nothing.
const oldName = schedule.name;
const regex = /^(.+?)(\(\d+\))?$/;
const match = oldName?.match(regex);
const baseName = match?.[1] ?? '';
const baseNameOfNewName = newName.match(regex)?.[1];
if (baseName === baseNameOfNewName) {
return oldName;
}
const updatedName = await handleDuplicate(newName);
schedule.name = updatedName;
schedule.updatedAt = Date.now();
schedules[scheduleIndex]!.name = newName;
// schedules[scheduleIndex].updatedAt = Date.now();
await UserScheduleStore.set('schedules', schedules);
return newName;
return undefined;
}

View File

@@ -1,6 +1,3 @@
/**
* Represents a tab with an additional `id` property
*/
export type TabWithId = Omit<chrome.tabs.Tab, 'id'> & { id: number };
/**

View File

@@ -1,11 +1,7 @@
import type TabInfoMessages from '@shared/messages/TabInfoMessages';
import Calendar from '@views/components/calendar/Calendar';
import DialogProvider from '@views/components/common/DialogProvider/DialogProvider';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import { MigrationDialog } from '@views/components/common/MigrationDialog';
import SentryProvider from '@views/contexts/SentryContext';
import { MessageListener } from 'chrome-extension-toolkit';
import useKC_DABR_WASM from 'kc-dabr-wasm';
import React, { useEffect } from 'react';
/**
@@ -13,7 +9,6 @@ import React, { useEffect } from 'react';
* @returns entire page
*/
export default function CalendarMain() {
useKC_DABR_WASM();
useEffect(() => {
const tabInfoListener = new MessageListener<TabInfoMessages>({
getTabInfo: ({ sendResponse }) => {
@@ -30,13 +25,8 @@ export default function CalendarMain() {
}, []);
return (
<SentryProvider fullInit>
<ExtensionRoot className='h-full w-full'>
<DialogProvider>
<MigrationDialog />
<Calendar />
</DialogProvider>
</ExtensionRoot>
</SentryProvider>
<ExtensionRoot className='h-full w-full'>
<Calendar />
</ExtensionRoot>
);
}

View File

@@ -3,11 +3,9 @@ import Link from '@views/components/common/Link';
import React from 'react';
/**
* Renders the DevMode component.
*
* @returns The rendered DevMode component.
*/
export default function DevMode() {
export default function App() {
return (
<ExtensionRoot>
<div className='text-base'>

View File

@@ -1,25 +0,0 @@
import DialogProvider from '@views/components/common/DialogProvider/DialogProvider';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import Settings from '@views/components/settings/Settings';
import SentryProvider from '@views/contexts/SentryContext';
import useKC_DABR_WASM from 'kc-dabr-wasm';
import React from 'react';
/**
* 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() {
useKC_DABR_WASM();
return (
<SentryProvider fullInit>
<ExtensionRoot>
<DialogProvider>
<Settings />
</DialogProvider>
</ExtensionRoot>
</SentryProvider>
);
}

View File

@@ -7,7 +7,7 @@
<title>UTRP Options</title>
</head>
<body style="min-height: 100vh; height: 0; margin: 0">
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import SettingsPage from './Settings';
import App from './App';
createRoot(document.getElementById('root')!).render(<SettingsPage />);
createRoot(document.getElementById('root')!).render(<App />);

View File

View File

@@ -9,7 +9,7 @@
body {
margin: 0;
padding: 0;
width: 400px;
width: 360px;
height: 540px;
}
</style>

View File

@@ -1,20 +1,7 @@
import 'uno.css';
import DialogProvider from '@views/components/common/DialogProvider/DialogProvider';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import { MigrationDialog } from '@views/components/common/MigrationDialog';
import PopupMain from '@views/components/PopupMain';
import SentryProvider from '@views/contexts/SentryContext';
import React from 'react';
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')!).render(
<SentryProvider fullInit>
<ExtensionRoot>
<DialogProvider>
<MigrationDialog />
<PopupMain />
</DialogProvider>
</ExtensionRoot>
</SentryProvider>
);
createRoot(document.getElementById('root')!).render(<PopupMain />);

View File

@@ -1,16 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>UTRP Report Issue</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script src="./index.tsx" type="module"></script>
</body>
</html>

View File

@@ -1,10 +0,0 @@
import ReportIssueMain from '@views/components/ReportIssueMain';
import SentryProvider from '@views/contexts/SentryContext';
import React from 'react';
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')!).render(
<SentryProvider fullInit>
<ReportIssueMain />
</SentryProvider>
);

View File

@@ -3,44 +3,20 @@ import { createSyncStore, debugStore } from 'chrome-extension-toolkit';
/**
* A store that is used for storing user options
*/
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;
interface IOptionsStore {
/** whether we should automatically highlight conflicts on the course schedule page */
shouldHighlightConflicts: boolean;
/** whether we should automatically scroll to load more courses on the course schedule page (without having to click next) */
enableScrollToLoad: boolean;
shouldScrollToLoad: boolean;
/** whether we should automatically refresh the data for the waitlist, course status, and other info with the latest data from UT's site */
enableDataRefreshing: boolean;
// url: URL;
}
export const OptionsStore = createSyncStore<IOptionsStore>({
enableCourseStatusChips: false,
enableTimeAndLocationInPopup: false,
enableHighlightConflicts: true,
enableScrollToLoad: true,
enableDataRefreshing: true,
shouldHighlightConflicts: true,
shouldScrollToLoad: true,
});
/**
* Initializes the settings by retrieving the values from the OptionsStore.
* @returns {Promise<IOptionsStore>} A promise that resolves to an object satisfying the IOptionsStore interface.
*/
export const initSettings = async () =>
({
enableCourseStatusChips: await OptionsStore.get('enableCourseStatusChips'),
enableTimeAndLocationInPopup: await OptionsStore.get('enableTimeAndLocationInPopup'),
enableHighlightConflicts: await OptionsStore.get('enableHighlightConflicts'),
enableScrollToLoad: await OptionsStore.get('enableScrollToLoad'),
enableDataRefreshing: await OptionsStore.get('enableDataRefreshing'),
}) satisfies IOptionsStore;
// Clothing retailer right
debugStore({ OptionsStore });

View File

@@ -72,12 +72,10 @@ export class CourseSchedule {
days,
startTime,
endTime,
location: location[0]
? {
building: location[0],
room: location[1] ?? '',
}
: undefined,
location: {
building: location[0] ?? '',
room: location[1] ?? '',
},
} satisfies Serialized<CourseMeeting>);
} catch (e) {
throw new Error(`Failed to parse schedule: ${dayLine} ${timeLine} ${locLine}`);

View File

@@ -12,9 +12,6 @@ const MIMEType = {
ANY: '*/*',
} as const satisfies Record<string, string>;
/**
* Represents a key of the MIMEType object
*/
export type MIMETypeKey = keyof typeof MIMEType;
export default MIMEType;

View File

@@ -52,15 +52,11 @@ type NestedKeys<T> = {
export type ThemeColor = NestedKeys<typeof colors>;
/**
* Represents a Tailwind colorway: a colorway is a key in the theme.colors object that has an object as its value
* Represents a Tailwind colorway: a colorway is a key in the theme.colors object that has an object as its value.
*/
export type TWColorway = {
[K in keyof typeof theme.colors]: (typeof theme.colors)[K] extends Record<string, unknown> ? K : never;
}[keyof typeof theme.colors];
/**
* Represents the index type for accessing the theme colors based on the specified TWColorway
*/
export type TWIndex = keyof (typeof theme.colors)[TWColorway];
/**

View File

@@ -1,27 +0,0 @@
/**
* Checks the login status by making a request to the provided URL.
* If the response indicates that the user is not logged in (redirected to a login page or returns a 401/403 status),
* it opens a new tab with the login URL and returns `false`.
* If the user is logged in, it returns `true`.
*
* @param url - The URL to check the login status against.
* @returns A promise that resolves to `true` if the user is logged in, otherwise `false`.
*/
export async function validateLoginStatus(url: string) {
try {
const response = await fetch(url, { credentials: 'include' });
// Check if the response is redirecting to a login page or returning a 401/403
if (response.redirected || response.status === 401 || response.status === 403) {
// User is not logged in
chrome.tabs.create({ url });
return false;
}
// User is logged in
return true;
} catch (error) {
console.error('Error checking login status:', error);
return false;
}
}

View File

@@ -207,9 +207,9 @@ export function getUnusedColor(
}
// OKLab helper functions (https://github.com/bottosson/bottosson.github.io/blob/master/misc/colorpicker/colorconversion.js)
// function srgbTransferFunction(a: number): number {
// return a <= 0.0031308 ? 12.92 * a : 1.055 * a ** 0.4166666666666667 - 0.055;
// }
function srgbTransferFunction(a: number): number {
return a <= 0.0031308 ? 12.92 * a : 1.055 * a ** 0.4166666666666667 - 0.055;
}
function srgbTransferFunctionInv(a: number): number {
return a > 0.04045 ? ((a + 0.055) / 1.055) ** 2.4 : a / 12.92;

View File

@@ -0,0 +1,3 @@
export const enableCourseStatusChips: boolean = false;
export const constenableSettingsPage: boolean = false;
export const enableCourseRefreshing: boolean = false;

View File

@@ -1,28 +0,0 @@
/**
* Open the report window relative centered to the current window.
*/
export function openReportWindow() {
chrome.windows.getCurrent({ populate: false }, currentWindow => {
const width = 400;
const height = 600;
// Calculate the new window's position to center it relative to the current window
const left =
currentWindow.left && currentWindow.width
? Math.round(currentWindow.left + (currentWindow.width - width) / 2)
: undefined;
const top =
currentWindow.top && currentWindow.height
? Math.round(currentWindow.top + (currentWindow.height - height) / 2)
: undefined;
chrome.windows.create({
url: chrome.runtime.getURL(`report.html`),
type: 'popup',
width,
height,
left,
top,
});
});
}

View File

@@ -1,38 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from '@views/components/common/Button';
import ChangelogPopup from '@views/components/common/ChangelogPopup';
import DialogProvider from '@views/components/common/DialogProvider/DialogProvider';
import useChangelog from '@views/hooks/useChangelog';
import React from 'react';
const meta = {
title: 'Components/Common/ChangelogPopup',
component: ChangelogPopup,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
} satisfies Meta<typeof ChangelogPopup>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
render: () => (
<DialogProvider>
<InnerComponent />
</DialogProvider>
),
};
const InnerComponent = () => {
const handleOnClick = useChangelog();
return (
<Button variant='filled' color='ut-burntorange' onClick={handleOnClick}>
Open Changelog Popup
</Button>
);
};

View File

@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react';
import Settings from '@views/components/settings/Settings';
import Settings from '@views/components/Settings';
const meta = {
title: 'Components/Common/Settings',

View File

@@ -1,32 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react';
import type { UpdateTextProps } from '@views/components/common/UpdateText';
import UpdateText from '@views/components/common/UpdateText';
import React from 'react';
const meta = {
title: 'Components/Common/UpdateText',
component: UpdateText,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
courses: { control: 'object' },
},
} satisfies Meta<typeof UpdateText>;
export default meta;
type Story = StoryObj<typeof meta>;
const Template = (args: React.JSX.IntrinsicAttributes & UpdateTextProps) => <UpdateText {...args} />;
export const Default: Story = {
render: Template,
args: {
courses: ['12345', '23456', '34567', '45678', '56789'],
},
};
Default.args = {
courses: ['12345', '23456', '34567', '45678', '56789'],
};

View File

@@ -1,23 +1,20 @@
import splashText from '@assets/insideJokes';
import createSchedule from '@pages/background/lib/createSchedule';
import { background } from '@shared/messages';
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { openReportWindow } from '@shared/util/openReportWindow';
import { enableCourseRefreshing, enableCourseStatusChips } from '@shared/util/experimental';
import Divider from '@views/components/common/Divider';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import List from '@views/components/common/List';
import Text from '@views/components/common/Text/Text';
import useSchedules, { getActiveSchedule, replaceSchedule, switchSchedule } from '@views/hooks/useSchedules';
import { getUpdatedAtDateTimeString } from '@views/lib/getUpdatedAtDateTimeString';
import useKC_DABR_WASM from 'kc-dabr-wasm';
import React, { useEffect, useState } from 'react';
import { openTabFromContentScript } from '@views/lib/openNewTabFromContentScript';
import clsx from 'clsx';
import React, { useState } from 'react';
import AddSchedule from '~icons/material-symbols/add';
import CalendarIcon from '~icons/material-symbols/calendar-month';
import Feedback from '~icons/material-symbols/flag';
import RefreshIcon from '~icons/material-symbols/refresh';
import SettingsIcon from '~icons/material-symbols/settings';
import { Button } from './common/Button';
import CourseStatus from './common/CourseStatus';
import { SmallLogo } from './common/LogoIcon';
import PopupCourseBlock from './common/PopupCourseBlock';
@@ -29,49 +26,12 @@ import ScheduleListItem from './common/ScheduleListItem';
* This component displays the main schedule, courses, and options buttons.
*/
export default function PopupMain(): JSX.Element {
const [enableCourseStatusChips, setEnableCourseStatusChips] = useState<boolean>(false);
const [enableDataRefreshing, setEnableDataRefreshing] = useState<boolean>(false);
useKC_DABR_WASM();
useEffect(() => {
const initAllSettings = async () => {
const { enableCourseStatusChips, enableDataRefreshing } = await initSettings();
setEnableCourseStatusChips(enableCourseStatusChips);
setEnableDataRefreshing(enableDataRefreshing);
};
initAllSettings();
const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => {
setEnableCourseStatusChips(newValue);
// console.log('enableCourseStatusChips', newValue);
});
const l2 = OptionsStore.listen('enableDataRefreshing', async ({ newValue }) => {
setEnableDataRefreshing(newValue);
// console.log('enableDataRefreshing', newValue);
});
return () => {
OptionsStore.removeListener(l1);
OptionsStore.removeListener(l2);
};
}, []);
const [activeSchedule, schedules] = useSchedules();
const [isRefreshing, setIsRefreshing] = useState(false);
const [funny, setFunny] = useState<string>('');
useEffect(() => {
const randomIndex = Math.floor(Math.random() * splashText.length);
setFunny(
splashText[randomIndex] ?? 'If you are seeing this, something has gone horribly wrong behind the scenes.'
);
}, []);
const handleOpenOptions = async () => {
const url = chrome.runtime.getURL('/options.html');
background.openNewTab({ url });
await openTabFromContentScript(url);
};
const handleCalendarOpenOnClick = async () => {
@@ -80,123 +40,103 @@ export default function PopupMain(): JSX.Element {
};
return (
<div className='h-screen max-h-full flex flex-col bg-white'>
<div className='p-5 py-3.5'>
<div className='flex items-center justify-between bg-white'>
<SmallLogo />
<div className='flex items-center gap-2.5'>
<button className='bg-ut-burntorange px-2 py-1.25 btn' onClick={handleCalendarOpenOnClick}>
<CalendarIcon className='size-6 text-white' />
</button>
<button className='bg-transparent px-2 py-1.25 btn' onClick={handleOpenOptions}>
<SettingsIcon className='size-6 color-ut-black' />
</button>
<button className='bg-transparent px-2 py-1.25 btn' onClick={openReportWindow}>
<Feedback className='size-6 color-ut-black' />
</button>
<ExtensionRoot>
<div className='h-screen max-h-full flex flex-col bg-white'>
<div className='p-5 py-3.5'>
<div className='flex items-center justify-between bg-white'>
<SmallLogo />
<div className='flex items-center gap-2.5'>
<button className='bg-ut-burntorange px-2 py-1.25 btn' onClick={handleCalendarOpenOnClick}>
<CalendarIcon className='size-6 text-white' />
</button>
<button className='bg-transparent px-2 py-1.25 btn' onClick={handleOpenOptions}>
<SettingsIcon className='size-6 color-ut-black' />
</button>
</div>
</div>
</div>
</div>
<Divider orientation='horizontal' size='100%' />
<div className='px-5 pb-2.5 pt-3.75'>
<ScheduleDropdown>
<List
draggables={schedules}
itemKey={schedule => schedule.id}
onReordered={reordered => {
const activeSchedule = getActiveSchedule();
const activeIndex = reordered.findIndex(s => s.id === activeSchedule.id);
<Divider orientation='horizontal' size='100%' />
<div className='px-5 pb-2.5 pt-3.75'>
<ScheduleDropdown>
<List
draggables={schedules}
itemKey={schedule => schedule.id}
onReordered={reordered => {
const activeSchedule = getActiveSchedule();
const activeIndex = reordered.findIndex(s => s.id === activeSchedule.id);
// don't care about the promise
UserScheduleStore.set('schedules', reordered);
UserScheduleStore.set('activeIndex', activeIndex);
}}
gap={10}
>
{(schedule, handleProps) => (
<ScheduleListItem
schedule={schedule}
onClick={() => {
switchSchedule(schedule.id);
}}
dragHandleProps={handleProps}
/>
)}
</List>
<div className='bottom-0 right-0 mt-2.5 w-full flex justify-end'>
<Button
variant='filled'
color='ut-burntorange'
className='h-fit p-0 btn'
onClick={() => createSchedule('New Schedule')}
// don't care about the promise
UserScheduleStore.set('schedules', reordered);
UserScheduleStore.set('activeIndex', activeIndex);
}}
gap={10}
>
<AddSchedule className='h-6 w-6' />
</Button>
</div>
</ScheduleDropdown>
</div>
{activeSchedule?.courses?.length === 0 && (
<div className='max-w-64 flex flex-col items-center self-center gap-1.25 px-2 py-2 pt-24'>
<Text variant='p' className='text-center text-ut-gray !font-normal'>
{funny}
</Text>
<Text variant='small' className='text-center text-black'>
(No courses added)
</Text>
{(schedule, handleProps) => (
<ScheduleListItem
schedule={schedule}
onClick={() => {
switchSchedule(schedule.id);
}}
dragHandleProps={handleProps}
/>
)}
</List>
</ScheduleDropdown>
</div>
)}
<div className='flex-1 self-stretch overflow-y-auto px-5'>
{activeSchedule?.courses?.length > 0 && (
<List
draggables={activeSchedule.courses}
onReordered={reordered => {
activeSchedule.courses = reordered;
replaceSchedule(getActiveSchedule(), activeSchedule);
}}
itemKey={e => e.uniqueId}
gap={10}
>
{(course, handleProps) => (
<PopupCourseBlock
key={course.uniqueId}
course={course}
colors={course.colors}
dragHandleProps={handleProps}
/>
)}
</List>
)}
</div>
<div className='w-full flex flex-col items-center gap-1.25 p-5 pt-3.75'>
<div className='flex gap-2.5'>
{enableCourseStatusChips && (
<>
<CourseStatus status='WAITLISTED' size='mini' />
<CourseStatus status='CLOSED' size='mini' />
<CourseStatus status='CANCELLED' size='mini' />
</>
<div className='flex-1 self-stretch overflow-y-auto px-5'>
{activeSchedule?.courses?.length > 0 && (
<List
draggables={activeSchedule.courses}
onReordered={reordered => {
activeSchedule.courses = reordered;
replaceSchedule(getActiveSchedule(), activeSchedule);
}}
itemKey={e => e.uniqueId}
gap={10}
>
{(course, handleProps) => (
<PopupCourseBlock
key={course.uniqueId}
course={course}
colors={course.colors}
dragHandleProps={handleProps}
/>
)}
</List>
)}
</div>
{enableDataRefreshing && (
<div className='inline-flex items-center self-center gap-1'>
<Text variant='mini' className='text-ut-gray !font-normal'>
LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)}
</Text>
{/* <button
className='h-4 w-4 bg-transparent p-0 btn'
onClick={() => {
setIsRefreshing(true);
}}
>
<RefreshIcon
className={clsx('h-4 w-4 text-ut-black animate-duration-800', {
'animate-spin': isRefreshing,
})}
/>
</button> */}
<div className='w-full flex flex-col items-center gap-1.25 p-5 pt-3.75'>
<div className='flex gap-2.5'>
{enableCourseStatusChips && (
<>
<CourseStatus status='WAITLISTED' size='mini' />
<CourseStatus status='CLOSED' size='mini' />
<CourseStatus status='CANCELLED' size='mini' />
</>
)}
</div>
)}
{enableCourseRefreshing && (
<div className='inline-flex items-center self-center gap-1'>
<Text variant='mini' className='text-ut-gray !font-normal'>
DATA LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)}
</Text>
<button
className='h-4 w-4 bg-transparent p-0 btn'
onClick={() => {
setIsRefreshing(true);
}}
>
<RefreshIcon
className={clsx('h-4 w-4 text-ut-black animate-duration-800', {
'animate-spin': isRefreshing,
})}
/>
</button>
</div>
)}
</div>
</div>
</div>
</ExtensionRoot>
);
}

View File

@@ -1,120 +0,0 @@
import 'uno.css';
import { captureFeedback } from '@sentry/react';
import React, { useState } from 'react';
import { Button } from './common/Button';
import Text from './common/Text/Text';
/**
* ReportIssueMain component renders a feedback form for users to submit their email and feedback.
*
* @returns The rendered component.
*/
export default function ReportIssueMain(): JSX.Element {
const [email, setEmail] = useState('');
const [feedback, setFeedback] = useState('');
const [isSubmitted, setIsSubmitted] = useState(false);
const submitFeedback = async () => {
if (!email || !feedback) {
throw new Error('Email and feedback are required');
}
// Here you would typically send the feedback to a server
await captureFeedback(
{
message: feedback || 'No feedback provided',
email,
tags: {
version: chrome.runtime.getManifest().version,
},
},
{
includeReplay: false,
}
);
// Reset form fields and close the dialog
setEmail('');
setFeedback('');
setIsSubmitted(true);
};
if (isSubmitted) {
return (
<div className='w-80 flex flex-col rounded-lg bg-white p-6 shadow-lg'>
<Text variant='h2' className='mb-4'>
Thank you
</Text>
<Text variant='p' className='mb-6'>
Your feedback has been submitted. You may close this window.
</Text>
<Button variant='filled' color='ut-green' className='border-0' onClick={() => window.close()}>
Done
</Button>
</div>
);
}
if (isSubmitted) {
return (
<div className='w-80 bg-white p-6'>
<h2 className='mb-4 text-2xl text-orange font-bold'>{`Hook'em Horns!`}</h2>
<p className='mb-6 text-gray-600'>Your feedback is music to our ears. Thanks for helping us improve!</p>
<button
className='w-full rounded bg-orange-600 px-4 py-2 text-white font-bold transition duration-300 hover:bg-orange-700'
onClick={() => window.close()}
>
Close
</button>
</div>
);
}
return (
<div className='w-80 bg-white p-6'>
<h2 className='mb-4 text-2xl text-ut-burntorange font-bold'>Longhorn Feedback</h2>
<p className='mb-4 text-sm text-ut-black'>Help us make UT Registration Plus even better!</p>
<form onSubmit={submitFeedback}>
<div className='mb-4'>
<label htmlFor='email' className='mb-1 block text-sm text-ut-black font-medium'>
Your @utexas.edu email
</label>
<input
type='email'
id='email'
value={email}
onChange={e => setEmail(e.target.value)}
className='w-full border border-gray-300 rounded-md px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-orange-500'
placeholder='bevo@utexas.edu'
required
/>
</div>
<div className='mb-4'>
<label htmlFor='feedback' className='mb-1 block text-sm text-ut-black font-medium'>
Your feedback
</label>
<textarea
id='feedback'
value={feedback}
onChange={e => setFeedback(e.target.value)}
className='h-24 w-full resize-none border border-gray-300 rounded-md px-3 py-2 text-sm font-sans focus:outline-none focus:ring-2 focus:ring-orange-500'
placeholder='I wish UT Registration Plus could...'
required
/>
</div>
<Button
onClick={submitFeedback}
variant='filled'
color='ut-orange'
className='w-full border-0 rounded bg-orange px-4 py-2 text-white font-bold transition duration-300 hover:bg-orange-700'
>
Send Feedback
</Button>
</form>
</div>
);
}

View File

@@ -0,0 +1,15 @@
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 <div className={className}>this will be finished laterrrrrrr</div>;
}

View File

@@ -8,7 +8,6 @@ import ImportantLinks from '@views/components/calendar/ImportantLinks';
import Divider from '@views/components/common/Divider';
import CourseCatalogInjectedPopup from '@views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
import { CalendarContext } from '@views/contexts/CalendarContext';
import useCourseFromUrl from '@views/hooks/useCourseFromUrl';
import { useFlattenedCourseSchedule } from '@views/hooks/useFlattenedCourseSchedule';
import { MessageListener } from 'chrome-extension-toolkit';
import React, { useEffect, useState } from 'react';
@@ -22,7 +21,21 @@ import TeamLinks from './TeamLinks';
export default function Calendar(): JSX.Element {
const { courseCells, activeSchedule } = useFlattenedCourseSchedule();
const [course, setCourse] = useState<Course | null>(useCourseFromUrl());
const [course, setCourse] = useState<Course | null>((): Course | null => {
const urlParams = new URLSearchParams(window.location.search);
const uniqueIdRaw = urlParams.get('uniqueId');
if (uniqueIdRaw === null) return null;
const uniqueId = Number(uniqueIdRaw);
const course = activeSchedule.courses.find(course => course.uniqueId === uniqueId);
if (course === undefined) return null;
urlParams.delete('uniqueId');
const newUrl = `${window.location.pathname}?${urlParams}`.replace(/\?$/, '');
window.history.replaceState({}, '', newUrl);
return course;
});
const [showPopup, setShowPopup] = useState<boolean>(course !== null);
const [showSidebar, setShowSidebar] = useState<boolean>(true);
@@ -81,8 +94,6 @@ export default function Calendar(): JSX.Element {
</div>
<CourseCatalogInjectedPopup
// Ideally let's not use ! here, but it's fine since we know course is always defined when showPopup is true
// Let's try to refactor this
course={course!} // always defined when showPopup is true
onClose={() => setShowPopup(false)}
open={showPopup}

View File

@@ -36,12 +36,7 @@ export default function CalendarBottomBar({ courseCells, setCourse }: CalendarBo
>
{displayCourses && (
<>
<Text variant='p' className='text-ut-black'>
ASYNC/OTHER
</Text>
<Text variant='h4' className='text-gray-300'>
</Text>
<Text variant='h4'>Async/Other:</Text>
<div className='inline-flex gap-2.5'>
{asyncCourseCells.map(block => {
const { courseDeptAndInstr, status, colors, className } = block.componentProps;

View File

@@ -1,11 +1,11 @@
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
import type { StatusType } 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 Text from '@views/components/common/Text/Text';
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import React from 'react';
import ClosedIcon from '~icons/material-symbols/lock';
import WaitlistIcon from '~icons/material-symbols/timelapse';
@@ -43,21 +43,6 @@ export default function CalendarCourseCell({
className,
onClick,
}: CalendarCourseCellProps): JSX.Element {
const [enableCourseStatusChips, setEnableCourseStatusChips] = useState<boolean>(false);
useEffect(() => {
initSettings().then(({ enableCourseStatusChips }) => setEnableCourseStatusChips(enableCourseStatusChips));
const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => {
setEnableCourseStatusChips(newValue);
// console.log('enableCourseStatusChips', newValue);
});
return () => {
OptionsStore.removeListener(l1);
};
}, []);
let rightIcon: React.ReactNode | null = null;
if (enableCourseStatusChips) {
if (status === Status.WAITLISTED) {
@@ -76,7 +61,7 @@ export default function CalendarCourseCell({
return (
<div
className={clsx(
'h-full w-0 flex justify-center rounded p-x-2 p-y-1.2 cursor-pointer screenshot:p-1.5 hover:shadow-md transition-shadow-100 ease-out',
'h-full w-0 flex justify-center rounded p-2 cursor-pointer screenshot:p-1.5 hover:shadow-md transition-shadow-100 ease-out',
{
'min-w-full': timeAndLocation,
'w-full': !timeAndLocation,
@@ -89,7 +74,11 @@ export default function CalendarCourseCell({
}}
onClick={onClick}
>
<div className={clsx('flex flex-1 flex-col gap-0.25 overflow-hidden max-h-full')}>
<div
className={clsx('flex flex-1 flex-col gap-0.25 overflow-hidden max-h-full', {
'self-center': !timeAndLocation,
})}
>
<Text
variant='h1-course'
as='p'

View File

@@ -9,8 +9,6 @@ import React from 'react';
import AddSchedule from '~icons/material-symbols/add';
import { usePrompt } from '../common/DialogProvider/DialogProvider';
/**
* Renders a component that displays a list of schedules.
*
@@ -19,33 +17,6 @@ import { usePrompt } from '../common/DialogProvider/DialogProvider';
*/
export function CalendarSchedules() {
const [, schedules] = useSchedules();
const showDialog = usePrompt();
const handleAddSchedule = () => {
if (schedules.length >= 10) {
showDialog({
title: `You have 10 active schedules!`,
description: (
<>
To encourage organization,{' '}
<span className='text-ut-burntorange'>please consider removing some unused schedules</span> you
may have.
</>
),
// eslint-disable-next-line react/no-unstable-nested-components
buttons: close => (
<Button variant='filled' color='ut-burntorange' onClick={close}>
I Understand
</Button>
),
});
return;
}
createSchedule('New Schedule');
};
return (
<div className='min-w-full w-0 items-center'>
@@ -53,7 +24,12 @@ export function CalendarSchedules() {
<Text variant='h3' className='text-nowrap'>
MY SCHEDULES
</Text>
<Button variant='single' color='theme-black' className='h-fit p-0 btn' onClick={handleAddSchedule}>
<Button
variant='single'
color='theme-black'
className='h-fit p-0 btn'
onClick={() => createSchedule('New Schedule')}
>
<AddSchedule className='h-6 w-6' />
</Button>
</div>

View File

@@ -1,4 +1,5 @@
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
import { Status } from '@shared/types/Course';
import { enableCourseRefreshing, enableCourseStatusChips } from '@shared/util/experimental';
import { Button } from '@views/components/common/Button';
import CourseStatus from '@views/components/common/CourseStatus';
import Divider from '@views/components/common/Divider';
@@ -8,11 +9,10 @@ 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 React from 'react';
import MenuIcon from '~icons/material-symbols/menu';
// import RefreshIcon from '~icons/material-symbols/refresh';
import SettingsIcon from '~icons/material-symbols/settings';
import RefreshIcon from '~icons/material-symbols/refresh';
/**
* Opens the options page in a new tab.
@@ -32,33 +32,8 @@ interface CalendarHeaderProps {
* @returns The JSX element representing the calendar header.
*/
export default function CalendarHeader({ onSidebarToggle }: CalendarHeaderProps): JSX.Element {
const [enableCourseStatusChips, setEnableCourseStatusChips] = useState<boolean>(false);
const [enableDataRefreshing, setEnableDataRefreshing] = useState<boolean>(false);
const [activeSchedule] = useSchedules();
useEffect(() => {
initSettings().then(({ enableCourseStatusChips, enableDataRefreshing }) => {
setEnableCourseStatusChips(enableCourseStatusChips);
setEnableDataRefreshing(enableDataRefreshing);
});
const l1 = OptionsStore.listen('enableCourseStatusChips', async ({ newValue }) => {
setEnableCourseStatusChips(newValue);
// console.log('enableCourseStatusChips', newValue);
});
const l2 = OptionsStore.listen('enableDataRefreshing', async ({ newValue }) => {
setEnableDataRefreshing(newValue);
// console.log('enableDataRefreshing', newValue);
});
return () => {
OptionsStore.removeListener(l1);
OptionsStore.removeListener(l2);
};
}, []);
return (
<div className='flex items-center gap-5 overflow-x-auto overflow-y-hidden border-b border-ut-offwhite px-7 py-4 md:overflow-x-hidden'>
<Button
@@ -76,14 +51,14 @@ export default function CalendarHeader({ onSidebarToggle }: CalendarHeaderProps)
totalHours={activeSchedule.hours}
totalCourses={activeSchedule.courses.length}
/>
{enableDataRefreshing && (
{enableCourseRefreshing && (
<div className='flex items-center gap-1 screenshot:hidden'>
<Text variant='mini' className='text-nowrap text-ut-gray font-normal!'>
LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)}
DATA 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'>
<RefreshIcon className='h-4 w-4 animate-duration-800 text-ut-black' />
</button> */}
</button>
</div>
)}
</div>
@@ -97,8 +72,8 @@ export default function CalendarHeader({ onSidebarToggle }: CalendarHeaderProps)
)}
{/* <Button variant='single' icon={UndoIcon} color='ut-black' />
<Button variant='single' icon={RedoIcon} color='ut-black' /> */}
<Button variant='single' icon={SettingsIcon} color='ut-black' onClick={handleOpenOptions} />
<Button variant='single' icon={RedoIcon} color='ut-black' /> */}
{/* <Button variant='single' icon={SettingsIcon} color='ut-black' onClick={handleOpenOptions} /> */}
</div>
</div>
);

View File

@@ -14,18 +14,18 @@ interface LinkItem {
}
const links: LinkItem[] = [
{
text: "Spring '25 Course Schedule",
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20252/',
},
{
text: "Fall '24 Course Schedule",
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20249/',
},
// {
// text: "Summer '24 Course Schedule",
// url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20246/',
// },
{
text: "Summer '24 Course Schedule",
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20246/',
},
{
text: "Spring '24 Course Schedule",
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/',
},
{
text: 'Registration Info Sheet',
url: 'https://utdirect.utexas.edu/registrar/ris.WBX',

View File

@@ -65,7 +65,7 @@ export function Button({
>
{Icon && <Icon className='h-6 w-6' />}
{!isIconOnly && (
<Text variant='h4' className='inline-flex translate-y-0.08 items-center gap-2'>
<Text variant='h4' className='translate-y-0.08'>
{children}
</Text>
)}

View File

@@ -1,171 +0,0 @@
/* eslint-disable react/no-unstable-nested-components */
import React, { useEffect, useState } from 'react';
import type { Options as RMOptions } from 'react-markdown';
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import remarkGfm from 'remark-gfm';
const changelog = new URL('/CHANGELOG.md', import.meta.url).href;
/**
* Renders a popup component for displaying the changelog.
*
* @param isOpen - A boolean indicating whether the popup is open or not.
* @param onClose - A function to be called when the popup is closed.
*
* @returns The JSX element representing the ChangelogPopup component.
*/
export default function ChangelogPopup(): JSX.Element {
const [markdownContent, setMarkdownContent] = useState('');
useEffect(() => {
fetch(changelog)
.then(response => response.text())
.then(text => setMarkdownContent(text))
.catch(error => console.error('Error fetching changelog:', error));
}, []);
const MarkdownComponents: RMOptions['components'] = {
h1: ({ children, ...props }) => (
<h1 className='mb-4 mt-6 text-3xl font-bold' {...props}>
{children}
</h1>
),
h2: ({ children, ...props }) => (
<h2 className='mb-3 mt-5 text-2xl font-semibold' {...props}>
{children}
</h2>
),
h3: ({ children, ...props }) => (
<h3 className='mb-2 mt-4 text-xl font-medium' {...props}>
{children}
</h3>
),
h4: ({ children, ...props }) => (
<h4 className='mb-2 mt-3 text-lg font-medium' {...props}>
{children}
</h4>
),
h5: ({ children, ...props }) => (
<h5 className='mb-1 mt-2 text-base font-medium' {...props}>
{children}
</h5>
),
h6: ({ children, ...props }) => (
<h6 className='mb-1 mt-2 text-sm font-medium' {...props}>
{children}
</h6>
),
p: ({ children, ...props }) => (
<p className='mb-4' {...props}>
{children}
</p>
),
a: ({ children, ...props }) => (
<a className='text-blue-500 hover:underline' {...props}>
{children}
</a>
),
ul: ({ children, ...props }) => (
<ul className='mb-4 list-disc pl-6' {...props}>
{children}
</ul>
),
ol: ({ children, ...props }) => (
<ol className='mb-4 list-decimal pl-6' {...props}>
{children}
</ol>
),
li: ({ children, ...props }) => (
<li className='mb-1' {...props}>
{children}
</li>
),
blockquote: ({ children, ...props }) => (
<blockquote className='mb-4 border-l-4 border-gray-300 py-2 pl-4 italic' {...props}>
{children}
</blockquote>
),
hr: props => <hr className='my-4 border-t border-gray-300' {...props} />,
table: ({ children, ...props }) => (
<div className='mb-4 overflow-x-auto'>
<table className='min-w-full divide-y divide-gray-300' {...props}>
{children}
</table>
</div>
),
thead: ({ children, ...props }) => (
<thead className='bg-gray-50' {...props}>
{children}
</thead>
),
tbody: ({ children, ...props }) => (
<tbody className='divide-y divide-gray-200' {...props}>
{children}
</tbody>
),
tr: ({ children, ...props }) => (
<tr className='hover:bg-gray-50' {...props}>
{children}
</tr>
),
th: ({ children, ...props }) => (
<th className='px-6 py-3 text-left text-xs text-gray-500 font-medium tracking-wider uppercase' {...props}>
{children}
</th>
),
td: ({ children, ...props }) => (
<td className='whitespace-nowrap px-6 py-4' {...props}>
{children}
</td>
),
pre: ({ children, ...props }) => (
<pre className='mb-4' {...props}>
{children}
</pre>
),
code: ({ className, children, ...props }) => {
const match = /language-(\w+)/.exec(className || '');
return match ? (
<SyntaxHighlighter PreTag='div' style={vscDarkPlus} language={match[1]}>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className='rounded bg-gray-100 px-1 py-0.5 dark:bg-gray-800' {...props}>
{children}
</code>
);
},
em: ({ children, ...props }) => (
<em className='italic' {...props}>
{children}
</em>
),
strong: ({ children, ...props }) => (
<strong className='font-bold' {...props}>
{children}
</strong>
),
del: ({ children, ...props }) => (
<del className='line-through' {...props}>
{children}
</del>
),
img: ({ src, alt, ...props }) => (
<img src={src} alt={alt} className='my-4 h-auto max-w-full rounded-md' {...props} />
),
};
return (
<div className='px-4'>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={MarkdownComponents}
className='text-gray-800 dark:text-gray-200'
>
{markdownContent}
</ReactMarkdown>
</div>
);
}

View File

@@ -2,7 +2,7 @@ import Text from '@views/components/common/Text/Text';
import React from 'react';
/**
* A type that represents the flags that a course can have
* A type that represents the flags that a course can have.
*/
export type Flag = 'WR' | 'QR' | 'GC' | 'CD' | 'E' | 'II';
export const flagMap = {

View File

@@ -13,9 +13,6 @@ import React, { Fragment } from 'react';
import ExtensionRoot from './ExtensionRoot/ExtensionRoot';
/**
* Represents the props for the _Dialog component
*/
export interface _DialogProps {
className?: string;
title?: JSX.Element;
@@ -31,7 +28,7 @@ export type DialogProps = _DialogProps & Omit<TransitionRootProps<typeof HDialog
* A reusable popup component that can be used to display content on the page
*/
export default function Dialog(props: PropsWithChildren<DialogProps>): JSX.Element {
const { children, className, open, title, description, ...rest } = props;
const { children, className, open, ...rest } = props;
return (
<Transition show={open} as={HDialog} {...rest}>
@@ -47,7 +44,7 @@ export default function Dialog(props: PropsWithChildren<DialogProps>): JSX.Eleme
>
<div className={clsx('fixed inset-0 z-50 bg-slate-700/35')} />
</TransitionChild>
<div className='fixed inset-0 z-50 flex items-center justify-center p-2'>
<div className='fixed inset-0 z-50 flex items-center justify-center'>
<TransitionChild
as={Fragment}
enter='transition duration-375 motion-reduce:duration-0 ease-[cubic-bezier(0.05,0.4,0.2,1)]'
@@ -59,12 +56,12 @@ export default function Dialog(props: PropsWithChildren<DialogProps>): JSX.Eleme
>
<DialogPanel
className={clsx(
'z-99 max-h-[90vh] flex flex-col overflow-y-auto border border-solid border-ut-offwhite rounded bg-white shadow-xl ml-[calc(100vw-100%-1rem)]',
'z-99 max-h-[90vh] flex flex-col overflow-y-auto border border-solid border-ut-offwhite rounded bg-white shadow-xl ml-[calc(100vw-100%)]',
className
)}
>
{title && <DialogTitle as={Fragment}>{title}</DialogTitle>}
{description && <Description as={Fragment}>{description}</Description>}
{props.title && <DialogTitle as={Fragment}>{props.title}</DialogTitle>}
{props.description && <Description as={Fragment}>{props.description}</Description>}
{children}
</DialogPanel>
</TransitionChild>

View File

@@ -1,4 +1,4 @@
import type { CloseWrapper, DialogInfo, DialogOptions, ShowDialogFn } from '@views/contexts/DialogContext';
import type { CloseWrapper, DialogInfo, ShowDialogFn } from '@views/contexts/DialogContext';
import { DialogContext, useDialog } from '@views/contexts/DialogContext';
import type { ReactNode } from 'react';
import React, { useCallback, useRef, useState } from 'react';
@@ -7,10 +7,6 @@ import Dialog from '../Dialog';
import Text from '../Text/Text';
type DialogElement = (show: boolean) => ReactNode;
/**
* Represents information for a prompt dialog
*/
export interface PromptInfo extends Omit<DialogInfo, 'buttons' | 'className' | 'title' | 'description'> {
title: JSX.Element | string;
description: JSX.Element | string;
@@ -29,27 +25,24 @@ function unwrapCloseWrapper<T>(obj: T | CloseWrapper<T>, close: () => void): T {
/**
* Hook to show prompt with default stylings.
*/
export function usePrompt(): (info: PromptInfo, options?: DialogOptions) => void {
export function usePrompt(): (info: PromptInfo) => void {
const showDialog = useDialog();
return (info, options) => {
showDialog(
{
...info,
title: (
<Text variant='h2' as='h1' className='text-theme-black'>
{info.title}
</Text>
),
description: (
<Text variant='p' as='p' className='text-ut-black'>
{info.description}
</Text>
),
className: 'max-w-[400px] flex flex-col gap-2.5 p-6.25',
},
options
);
return (info: PromptInfo) => {
showDialog({
...info,
title: (
<Text variant='h2' as='h1' className='text-theme-black'>
{info.title}
</Text>
),
description: (
<Text variant='p' as='p' className='text-ut-black'>
{info.description}
</Text>
),
className: 'max-w-[400px] flex flex-col gap-2.5 p-6.25',
});
};
}
@@ -67,7 +60,7 @@ export default function DialogProvider(props: { children: ReactNode }): JSX.Elem
const [isOpen, setIsOpen] = useState(false);
const showDialog = useCallback<ShowDialogFn>((info, options) => {
const showDialog = useCallback<ShowDialogFn>(info => {
const id = nextId++;
const handleClose = () => {
@@ -92,11 +85,11 @@ export default function DialogProvider(props: { children: ReactNode }): JSX.Elem
const dialogElement = (show: boolean) => (
<Dialog
key={id}
onClose={(options?.closeOnClickOutside ?? true) ? handleClose : () => {}}
onClose={handleClose}
afterLeave={onLeave}
title=<>{infoUnwrapped.title}</>
description=<>{infoUnwrapped.description}</>
appear={!(options?.immediate ?? false)}
title={infoUnwrapped.title}
description={infoUnwrapped.description}
appear
show={show}
className={infoUnwrapped.className}
>

View File

@@ -11,7 +11,7 @@
}
.extensionRoot {
@apply font-sans;
@apply font-sans h-full;
color: #303030;
[data-rfd-drag-handle-context-id=':r1:'] {

View File

@@ -2,27 +2,24 @@
import 'uno.css';
import clsx from 'clsx';
import React, { forwardRef } from 'react';
import React from 'react';
import styles from './ExtensionRoot.module.scss';
export const styleResetClass = styles.extensionRoot;
interface Props {
testId?: string;
className?: string;
}
/**
* A wrapper component for the extension elements that adds some basic styling to them
*/
export default function ExtensionRoot(props: React.HTMLProps<HTMLDivElement>): JSX.Element {
const { className, ...others } = props;
export default function ExtensionRoot(props: React.PropsWithChildren<Props>): JSX.Element {
return (
<React.StrictMode>
<div className={clsx(styleResetClass, 'h-full', className)} {...others} />
<div className={clsx(styles.extensionRoot, props.className)} data-testid={props.testId}>
{props.children}
</div>
</React.StrictMode>
);
}
export const ExtensionRootWrapper = forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement>>((props, ref) => (
<div className={styleResetClass}>
<div {...props} ref={ref} />
</div>
));

View File

@@ -79,9 +79,6 @@ function List<T>({ draggables, itemKey, children, onReordered, gap }: ListProps<
useEffect(() => {
setItems(wrap(draggables, itemKey));
// This is on purpose
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [draggables]);
const onDragEnd: OnDragEndResponder = useCallback(
@@ -95,9 +92,6 @@ function List<T>({ draggables, itemKey, children, onReordered, gap }: ListProps<
setItems(reordered);
onReordered(reordered.map(item => item.content));
},
// This is on purpose
// eslint-disable-next-line react-hooks/exhaustive-deps
[items]
);

View File

@@ -1,162 +0,0 @@
import migrateUTRPv1Courses, { getUTRPv1Courses } from '@background/lib/migrateUTRPv1Courses';
import Text from '@views/components/common/Text/Text';
import { useSentryScope } from '@views/contexts/SentryContext';
import React, { useEffect, useState } from 'react';
import { Button } from './Button';
import { usePrompt } from './DialogProvider/DialogProvider';
import Spinner from './Spinner';
function MigrationButtons({ close }: { close: () => void }): JSX.Element {
const [processState, setProcessState] = useState(0);
const [error, setError] = useState<string | undefined>(undefined);
const [sentryScope] = useSentryScope();
useEffect(() => {
const handleMigration = async () => {
if (processState === 1) {
try {
await chrome.storage.session.set({ pendingMigration: true });
const successful = await migrateUTRPv1Courses();
if (successful) {
await chrome.storage.local.set({ finishedMigration: true });
await chrome.storage.session.remove('pendingMigration');
}
} catch (error) {
console.error(error);
const sentryId = sentryScope.captureException(error);
setError(sentryId);
await chrome.storage.session.remove('pendingMigration');
return;
}
setProcessState(2);
close();
} else if (processState === 0) {
const { pendingMigration } = await chrome.storage.session.get('pendingMigration');
if (pendingMigration) setProcessState(1);
}
};
handleMigration();
// This is on purpose
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [processState]);
return (
<>
{error && (
<Text variant='p' className='text-ut-red'>
An error occurred while migrating your courses. Please try again later in settings. (
{error.substring(0, 8)})
</Text>
)}
<Button
variant='single'
color='ut-black'
onClick={() => {
close();
if (!error) {
chrome.storage.local.set({ finishedMigration: true });
}
}}
>
Cancel
</Button>
<Button
variant='filled'
color='ut-green'
disabled={processState > 0}
onClick={() => {
setProcessState(1);
}}
>
{processState === 1 ? (
<>
Migrating... <Spinner className='ml-2.75 inline-block h-4! w-4! text-current!' />
</>
) : (
'Migrate courses'
)}
</Button>
</>
);
}
/**
* Custom hook that provides a function to show a migration dialog.
*
* The dialog prompts the user to migrate their saved schedules if there are any courses
* available to migrate. If there are no courses to migrate, it informs the user that
* migration has already been completed.
*
* @returns A function that, when called, checks for courses to migrate and shows the appropriate dialog.
*/
export function useMigrationDialog() {
const showDialog = usePrompt();
return async () => {
if ((await getUTRPv1Courses()).length > 0) {
showDialog(
{
title: 'This extension has updated!',
description:
"You may have already began planning your Spring '25 schedule. Click the button below to transfer your saved schedules into a new schedule. (You may be required to login to the UT Registrar)",
buttons: close => <MigrationButtons close={close} />,
},
{
closeOnClickOutside: false,
}
);
} else {
showDialog({
title: 'Already migrated!',
description:
'There are no courses to migrate. If you have any issues, please submit a feedback report by clicking the flag at the top right of the extension popup.',
buttons: close => (
<Button variant='filled' color='ut-burntorange' onClick={close}>
I Understand
</Button>
),
});
}
};
}
/**
* MigrationDialog component.
*
* This component is responsible for checking if a migration has already been attempted
* and if there are any courses from UTRPv1 that need to be migrated. If migration is needed,
* it triggers the migration dialog.
*
* @returns An empty fragment.
*
* @remarks
* The component uses the `useMigrationDialog` hook to show the migration dialog and the
* `useEffect` hook to perform the migration check on component mount.
*/
export function MigrationDialog(): JSX.Element {
const showMigrationDialog = useMigrationDialog();
useEffect(() => {
const checkMigration = async () => {
// check if migration was already attempted
if ((await chrome.storage.local.get('finishedMigration')).finishedMigration) return;
if ((await getUTRPv1Courses()).length > 0) showMigrationDialog();
};
checkMigration();
// This is on purpose
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// (not actually a useless fragment)
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}

View File

@@ -1,6 +1,5 @@
import type { DraggableProvidedDragHandleProps } from '@hello-pangea/dnd';
import { background } from '@shared/messages';
import { initSettings, 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';
@@ -8,7 +7,7 @@ import { pickFontColor } from '@shared/util/colors';
import { StatusIcon } from '@shared/util/icons';
import Text from '@views/components/common/Text/Text';
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import React from 'react';
import DragIndicatorIcon from '~icons/material-symbols/drag-indicator';
@@ -33,21 +32,6 @@ export default function PopupCourseBlock({
colors,
dragHandleProps,
}: PopupCourseBlockProps): JSX.Element {
const [enableCourseStatusChips, setEnableCourseStatusChips] = useState<boolean>(false);
useEffect(() => {
initSettings().then(({ enableCourseStatusChips }) => setEnableCourseStatusChips(enableCourseStatusChips));
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');
@@ -81,7 +65,7 @@ export default function PopupCourseBlock({
<span className='px-0.5 font-450'>{formattedUniqueId}</span> {course.department} {course.number} &ndash;{' '}
{course.instructors.length === 0 ? 'Unknown' : course.instructors.map(v => v.lastName)}
</Text>
{enableCourseStatusChips && course.status !== Status.OPEN && (
{course.status !== Status.OPEN && (
<div
style={{
backgroundColor: colors.secondaryColor,

View File

@@ -1,4 +1,4 @@
import { Description, Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react';
import { Dialog, Transition, TransitionChild } from '@headlessui/react';
import type { ReactElement } from 'react';
import React from 'react';
@@ -50,11 +50,11 @@ function PromptDialog({ isOpen, onClose, title, content, children }: PromptDialo
leaveTo='opacity-0 scale-95'
>
<div className='fixed inset-0 w-screen flex items-center justify-center'>
<DialogPanel className='h-[200] w-[431px] flex flex-col rounded bg-white p-6'>
<DialogTitle className='mb-[10px]'>{title}</DialogTitle>
<Description className='mb-[13px]'>{content}</Description>
<Dialog.Panel className='h-[200] w-[431px] flex flex-col rounded bg-white p-6'>
<Dialog.Title className='mb-[10px]'>{title}</Dialog.Title>
<Dialog.Description className='mb-[13px]'>{content}</Dialog.Description>
<div className='flex items-center justify-end gap-2'>{children}</div>
</DialogPanel>
</Dialog.Panel>
</div>
</TransitionChild>
</Dialog>

View File

@@ -1,6 +1,4 @@
import { Menu, MenuButton, MenuItem, MenuItems } from '@headlessui/react';
import deleteSchedule from '@pages/background/lib/deleteSchedule';
import duplicateSchedule from '@pages/background/lib/duplicateSchedule';
import renameSchedule from '@pages/background/lib/renameSchedule';
import type { UserSchedule } from '@shared/types/UserSchedule';
import Text from '@views/components/common/Text/Text';
@@ -8,12 +6,8 @@ import useSchedules from '@views/hooks/useSchedules';
import clsx from 'clsx';
import React, { useEffect, useMemo, useState } from 'react';
import XIcon from '~icons/material-symbols/close';
import DragIndicatorIcon from '~icons/material-symbols/drag-indicator';
import MoreActionsIcon from '~icons/material-symbols/more-vert';
import { Button } from './Button';
import DialogProvider, { usePrompt } from './DialogProvider/DialogProvider';
import { ExtensionRootWrapper, styleResetClass } from './ExtensionRoot/ExtensionRoot';
/**
* Props for the ScheduleListItem component.
@@ -33,11 +27,10 @@ export default function ScheduleListItem({ schedule, dragHandleProps, onClick }:
const [isEditing, setIsEditing] = useState(false);
const [editorValue, setEditorValue] = useState(schedule.name);
const showDialog = usePrompt();
const editorRef = React.useRef<HTMLInputElement>(null);
useEffect(() => {
const editor = editorRef.current;
setEditorValue(schedule.name);
if (isEditing && editor) {
@@ -48,78 +41,29 @@ export default function ScheduleListItem({ schedule, dragHandleProps, onClick }:
const isActive = useMemo(() => activeSchedule.id === schedule.id, [activeSchedule, schedule]);
const handleBlur = async () => {
if (editorValue.trim() !== '' && editorValue.trim() !== schedule.name) {
schedule.name = (await renameSchedule(schedule.id, editorValue.trim())) as string;
const handleBlur = () => {
if (editorValue.trim() !== '') {
schedule.name = editorValue.trim();
renameSchedule(schedule.id, schedule.name);
}
setIsEditing(false);
};
const handleDelete = () => {
if (schedule.id === activeSchedule.id) {
showDialog({
title: `Unable to delete active schedule.`,
description: (
<>
<Text>Deleting the active schedule</Text>
<Text className='text-ut-burntorange'> {schedule.name} </Text>
<Text>is not allowed. Please switch to another schedule and try again.</Text>
</>
),
// eslint-disable-next-line react/no-unstable-nested-components
buttons: close => (
<Button variant='filled' color='ut-burntorange' onClick={close}>
I Understand
</Button>
),
});
} else {
showDialog({
title: `Are you sure?`,
description: (
<>
<Text>Deleting</Text>
<Text className='text-ut-burntorange'> {schedule.name} </Text>
<Text>is permanent and will remove all added courses from that schedule.</Text>
</>
),
// eslint-disable-next-line react/no-unstable-nested-components
buttons: close => (
<>
<Button variant='single' color='ut-black' onClick={close}>
Cancel
</Button>
<Button
variant='filled'
color='ut-red'
onClick={() => {
close();
deleteSchedule(schedule.id);
}}
>
Delete Permanently
</Button>
</>
),
});
}
};
return (
<div className='rounded bg-white'>
<li className='w-full flex cursor-pointer items-center text-ut-burntorange'>
<div className='h-full cursor-move focusable' {...dragHandleProps}>
<DragIndicatorIcon className='h-6 w-6 cursor-move text-zinc-300 btn-transition -ml-1.5 hover:text-zinc-400' />
</div>
<div className='group relative flex flex-1 items-center overflow-x-hidden'>
<div className='group flex flex-1 items-center overflow-x-hidden'>
<div
className='group/circle flex flex-grow items-center gap-1.5 overflow-x-hidden'
className='flex flex-grow items-center gap-1.5 overflow-x-hidden'
onClick={(...e) => !isEditing && onClick?.(...e)}
>
<div
className={clsx(
'h-5.5 w-5.5 relative flex-shrink-0 border-2px border-current rounded-full btn-transition group-active/circle:scale-95 after:(absolute content-empty bg-current h-2.9 w-2.9 rounded-full transition transform-gpu scale-100 ease-out-expo duration-250 -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2)',
'h-5.5 w-5.5 relative border-2px border-current rounded-full btn-transition group-active:scale-95 after:(absolute content-empty bg-current h-2.9 w-2.9 rounded-full transition tansform-gpu scale-100 ease-out-expo duration-250 -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2)',
{
'after:(scale-0! opacity-0 ease-in-out! duration-200!)': !isActive,
}
@@ -129,7 +73,7 @@ export default function ScheduleListItem({ schedule, dragHandleProps, onClick }:
<Text
variant='p'
as='input'
className='mr-1 w-full flex-1 px-0.5 outline-blue-500 -ml-0.5'
className='mr-1 flex-1 px-0.5 outline-blue-500 -ml-0.5'
value={editorValue}
onChange={e => setEditorValue(e.target.value)}
onKeyDown={e => {
@@ -148,57 +92,12 @@ export default function ScheduleListItem({ schedule, dragHandleProps, onClick }:
</Text>
)}
</div>
<DialogProvider>
<Menu>
<MenuButton className='invisible h-fit bg-transparent p-0 text-ut-gray btn-transition data-[open]:visible group-hover:visible'>
<MoreActionsIcon className='h-6 w-6' />
</MenuButton>
<MenuItems
as={ExtensionRootWrapper}
className={clsx([
styleResetClass,
'w-30 cursor-pointer origin-top-right rounded bg-white p-1 text-black shadow-lg transition border border-ut-offwhite focus:outline-none',
'data-[closed]:(opacity-0 scale-95)',
'data-[enter]:(ease-out-expo duration-150)',
'data-[leave]:(ease-out duration-50)',
])}
transition
anchor='bottom end'
>
<MenuItem>
<Text
as='button'
variant='small'
onClick={() => setIsEditing(true)}
className='w-full rounded bg-transparent p-2 text-left data-[focus]:bg-gray-200/40'
>
Rename
</Text>
</MenuItem>
<MenuItem>
<Text
as='button'
variant='small'
onClick={() => duplicateSchedule(schedule.id)}
className='w-full rounded bg-transparent p-2 text-left data-[focus]:bg-gray-200/40'
>
Duplicate
</Text>
</MenuItem>
<MenuItem>
<Text
as='button'
variant='small'
onClick={handleDelete}
className='w-full rounded bg-transparent p-2 text-left text-ut-red data-[focus]:bg-red-200/40'
>
Delete
</Text>
</MenuItem>
</MenuItems>
</Menu>
</DialogProvider>
<div>
<XIcon
className='invisible h-5 w-5 text-ut-red group-hover:visible'
onClick={() => deleteSchedule(schedule.id)}
/>
</div>
</div>
</li>
</div>

View File

@@ -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 min-w-[52px]`}
relative inline-flex items-center h-8 w-13 rounded-full transition-colors ease-in-out duration-200`}
>
<span
className={`${enabled ? 'translate-x-6' : 'translate-x-1'}

View File

@@ -1,40 +0,0 @@
import Text from '@views/components/common/Text/Text';
import React from 'react';
/**
* Props for the Update Text
*/
export type UpdateTextProps = {
courses: string[];
};
/**
* UpdateText component displays a message indicating that the extension has been updated
* and lists the unique course numbers from the old version.
*
* @param props - The properties object.
* @param props.courses - An array of course unique numbers to be displayed.
* @returns The rendered UpdateText component.
*/
export default function UpdateText({ courses }: UpdateTextProps): JSX.Element {
return (
<div className='max-w-64 flex flex-col justify-center gap-2'>
<div className='flex flex-col gap-0 text-center'>
<Text variant='h4' className='text-ut-burntorange'>
This extension has updated!
</Text>
<Text variant='p' className='text-ut-black'>
You may have already began planning your Spring 2025 schedule. Here are the Unique Numbers you had
from the old version: (Please open each link and re-add course to your new schedule)
</Text>
</div>
<div className='flex flex-col gap-1 text-center'>
{courses.map(course => (
<Text key={course} variant='p' className='text-ut-orange underline'>
{course}
</Text>
))}
</div>
</div>
);
}

View File

@@ -1,5 +1,3 @@
import 'react-loading-skeleton/dist/skeleton.css';
import type { ScrapedRow } from '@shared/types/Course';
import useInfiniteScroll from '@views/hooks/useInfiniteScroll';
import { CourseCatalogScraper } from '@views/lib/CourseCatalogScraper';
@@ -7,13 +5,11 @@ import { SiteSupport } from '@views/lib/getSiteSupport';
import type { AutoLoadStatusType } from '@views/lib/loadNextCourseCatalogPage';
import {
AutoLoadStatus,
getNextButton,
loadNextCourseCatalogPage,
removePaginationButtons,
} from '@views/lib/loadNextCourseCatalogPage';
import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import Skeleton from 'react-loading-skeleton';
import styles from './AutoLoad.module.scss';
@@ -28,7 +24,6 @@ type Props = {
export default function AutoLoad({ addRows }: Props): JSX.Element | null {
const [container, setContainer] = useState<HTMLDivElement | null>(null);
const [status, setStatus] = useState<AutoLoadStatusType>(AutoLoadStatus.IDLE);
const [isSinglePage, setIsSinglePage] = useState(false);
useEffect(() => {
const portalContainer = document.createElement('div');
@@ -37,10 +32,6 @@ export default function AutoLoad({ addRows }: Props): JSX.Element | null {
setContainer(portalContainer);
}, []);
useEffect(() => {
setIsSinglePage(!getNextButton(document));
}, []);
useEffect(() => {
removePaginationButtons(document);
console.log(`AutoLoad is now ${status}`);
@@ -49,7 +40,6 @@ export default function AutoLoad({ addRows }: Props): JSX.Element | null {
// This hook will call the callback when the user scrolls to the bottom of the page.
useInfiniteScroll(async () => {
if (isSinglePage) return;
// fetch the next page of courses
const [status, nextRows] = await loadNextCourseCatalogPage();
setStatus(status);
@@ -62,19 +52,23 @@ export default function AutoLoad({ addRows }: Props): JSX.Element | null {
// add the scraped courses to the current page
addRows(scrapedRows);
}, [addRows, isSinglePage]);
}, [addRows]);
if (!container || status === AutoLoadStatus.DONE || isSinglePage) {
if (!container || status === AutoLoadStatus.DONE) {
return null;
}
return createPortal(
<div>
{status !== AutoLoadStatus.ERROR && (
<div className=''>
{Array.from({ length: 8 }).map(() => (
<Skeleton style={{ marginBottom: 30 }} height={40} />
))}
<div
style={{
height: '500px',
backgroundColor: '#f4f4f4',
}}
>
{/* <Spinner />
<h2>Loading Next Page...</h2> */}
</div>
)}
{status === AutoLoadStatus.ERROR && (

View File

@@ -1,10 +1,10 @@
import type { Course } from '@shared/types/Course';
import Spinner from '@views/components/common/Spinner';
import Text from '@views/components/common/Text/Text';
import { CourseCatalogScraper } from '@views/lib/CourseCatalogScraper';
import { SiteSupport } from '@views/lib/getSiteSupport';
import clsx from 'clsx';
import React from 'react';
import Skeleton from 'react-loading-skeleton';
interface DescriptionProps {
course: Course;
@@ -60,8 +60,8 @@ export default function Description({ course }: DescriptionProps): JSX.Element {
Please refresh the page and log back in using your UT EID and password.
</Text>
)}
{status === LoadStatus.LOADING &&
Array.from({ length: 5 }).map(() => <Skeleton style={{ marginBottom: 10 }} height={35} />)}
{/* TODO (achadaga): would be nice to have a new spinner here */}
{status === LoadStatus.LOADING && <Spinner />}
{status === LoadStatus.DONE && (
<ul className='ml-6 mt-1.5 list-disc list-outside space-y-1.5'>
{description.map(line => {

View File

@@ -1,6 +1,7 @@
import type { Course } from '@shared/types/Course';
import type { Distribution, LetterGrade } from '@shared/types/Distribution';
import { extendedColors } from '@shared/types/ThemeColors';
import Spinner from '@views/components/common/Spinner';
import Text from '@views/components/common/Text/Text';
import {
NoDataError,
@@ -11,7 +12,6 @@ import Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import type { ChangeEvent } from 'react';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import Skeleton from 'react-loading-skeleton';
interface GradeDistributionProps {
course: Course;
@@ -193,7 +193,8 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
return (
<div className='pt-3'>
{status === DataStatus.LOADING && <Skeleton height={300} />}
{/* TODO (achadaga): again would be nice to have an updated spinner */}
{status === DataStatus.LOADING && <Spinner />}
{status === DataStatus.NOT_FOUND && (
<HighchartsReact
ref={ref}

View File

@@ -8,7 +8,7 @@ import styles from './RecruitmentBanner.module.scss';
const DISCORD_URL = 'https://discord.gg/7pQDBGdmb7';
const GITHUB_URL = 'https://github.com/Longhorn-Developers/UT-Registration-Plus';
const RECRUIT_FROM_DEPARTMENTS = ['C S', 'ECE', 'MIS', 'CSE', 'EE', 'ITD', 'DES'];
const RECRUIT_FROM_DEPARTMENTS = ['C S', 'ECE', 'MIS', 'CSE', 'EE', 'ITD'];
/**
* This adds a new column to the course catalog table header.

View File

@@ -86,10 +86,3 @@
text-decoration: line-through;
}
}
.isConflictNoLineThrough:not(.inActiveSchedule) {
> *:not(td:last-child) {
color: colors.$speedway_brick;
font-weight: normal;
}
}

View File

@@ -1,4 +1,3 @@
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';
@@ -26,26 +25,9 @@ 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<Course[]>([]);
const [highlightConflicts, setHighlightConflicts] = useState<boolean>(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');
@@ -90,23 +72,14 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
}
}
// 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!);
}
element.classList[conflicts.length ? 'add' : 'remove'](styles.isConflict!);
setConflicts(conflicts);
return () => {
element.classList.remove(styles.isConflict!);
setConflicts([]);
};
}, [activeSchedule, course, element.classList, highlightConflicts]);
}, [activeSchedule, course, element.classList]);
if (!container) {
return null;

View File

@@ -1,30 +0,0 @@
import clsx from 'clsx';
import type { PropsWithChildren } from 'react';
import React from 'react';
/**
* Props for the Preview component.
*/
export interface PreviewProps {
className?: string;
}
/**
* Renders a preview component.
*
* @param props - The component props.
* @returns The rendered preview component.
*/
export default function Preview(props: PropsWithChildren<PreviewProps>): JSX.Element {
const { children } = props;
return (
<div className='w-1/2 inline-flex flex-col items-start justify-start rounded-xl bg-ut-gray/10'>
<div className='m-4 inline-flex items-center self-stretch justify-end gap-2.5'>
<div className='text-center text-sm text-ut-gray font-medium'>Preview</div>
</div>
<div className={clsx('h-full flex flex-col self-stretch justify-center p-5 space-y-4', props.className)}>
{children}
</div>
</div>
);
}

View File

@@ -1,590 +0,0 @@
import addCourse from '@pages/background/lib/addCourse';
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 { Button } from '@views/components/common/Button';
import { usePrompt } from '@views/components/common/DialogProvider/DialogProvider';
import Divider from '@views/components/common/Divider';
import { LargeLogo } 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 useChangelog from '@views/hooks/useChangelog';
import useSchedules from '@views/hooks/useSchedules';
import { CourseCatalogScraper } from '@views/lib/CourseCatalogScraper';
import getCourseTableRows from '@views/lib/getCourseTableRows';
import { GitHubStatsService, LONGHORN_DEVELOPERS_ADMINS, LONGHORN_DEVELOPERS_SWE } from '@views/lib/getGitHubStats';
import { SiteSupport } from '@views/lib/getSiteSupport';
import { getUpdatedAtDateTimeString } from '@views/lib/getUpdatedAtDateTimeString';
import clsx from 'clsx';
import React, { useCallback, useEffect, useState } from 'react';
import IconoirGitFork from '~icons/iconoir/git-fork';
// import { ExampleCourse } from 'src/stories/components/ConflictsWithWarning.stories';
import DeleteForeverIcon from '~icons/material-symbols/delete-forever';
import { useMigrationDialog } from '../common/MigrationDialog';
// import RefreshIcon from '~icons/material-symbols/refresh';
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;
const gitHubStatsService = new GitHubStatsService();
const includeMergedPRs = false;
/**
* 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<boolean>(false);
const [showTimeLocation, setShowTimeLocation] = useState<boolean>(false);
const [highlightConflicts, setHighlightConflicts] = useState<boolean>(false);
const [loadAllCourses, setLoadAllCourses] = useState<boolean>(false);
const [enableDataRefreshing, setEnableDataRefreshing] = useState<boolean>(false);
const showMigrationDialog = useMigrationDialog();
// Toggle GitHub stats when the user presses the 'S' key
const [showGitHubStats, setShowGitHubStats] = useState<boolean>(false);
const [githubStats, setGitHubStats] = useState<Awaited<
ReturnType<typeof gitHubStatsService.fetchGitHubStats>
> | null>(null);
const [activeSchedule] = useSchedules();
// const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
const showDialog = usePrompt();
const handleChangelogOnClick = useChangelog();
useEffect(() => {
const fetchGitHubStats = async () => {
try {
const stats = await gitHubStatsService.fetchGitHubStats();
setGitHubStats(stats);
} catch (error) {
console.warn('Error fetching GitHub stats:', error);
}
};
const initAndSetSettings = async () => {
const {
enableCourseStatusChips,
enableTimeAndLocationInPopup,
enableHighlightConflicts,
enableScrollToLoad,
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') {
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: (
<>
<p>
Are you sure you want to erase all schedules and courses you have? This action is permanent and
cannot be undone.
</p>
<br />
<p className='text-sm text-gray-600'>Note: This will not erase your settings and preferences.</p>
</>
),
// eslint-disable-next-line react/no-unstable-nested-components
buttons: accept => (
<Button
variant='filled'
color='ut-burntorange'
onClick={() => {
deleteAllSchedules();
accept();
}}
>
I Understand
</Button>
),
});
};
// todo: move into a util/shared place, rather than specifically in settings
const handleAddCourseByUrl = async () => {
// todo: Use a proper modal instead of a prompt
// eslint-disable-next-line no-alert
const link: string | null = prompt('Enter course link');
// Exit if the user cancels the prompt
if (link === null) return;
try {
let response: Response;
try {
response = await fetch(link);
} catch (e) {
alert(`Failed to fetch url '${link}'`);
return;
}
const text = await response.text();
const doc = new DOMParser().parseFromString(text, 'text/html');
const scraper = new CourseCatalogScraper(SiteSupport.COURSE_CATALOG_DETAILS, doc, link);
const tableRows = getCourseTableRows(doc);
const courses = scraper.scrape(tableRows, false);
if (courses.length === 1) {
const description = scraper.getDescription(doc);
const row = courses[0]!;
const course = row.course!;
course.description = description;
// console.log(course);
if (activeSchedule.courses.every(c => c.uniqueId !== course.uniqueId)) {
console.log('adding course');
addCourse(activeSchedule.id, course);
} else {
console.log('course already exists');
}
} else {
console.log(courses);
}
} catch (error) {
console.error('Error scraping course:', error);
}
};
const [devMode, toggleDevMode] = useDevMode(10);
if (devMode) {
return <DevMode />;
}
return (
<div>
<header className='flex items-center gap-5 overflow-x-auto overflow-y-hidden border-b border-ut-offwhite px-7 py-4 md:overflow-x-hidden'>
<LargeLogo />
<Divider className='mx-2 self-center md:mx-4' size='2.5rem' orientation='vertical' />
<Text variant='h1' className='flex-1 text-ut-burntorange'>
UTRP SETTINGS & CREDITS PAGE
</Text>
<div className='hidden flex-row items-center justify-end gap-6 screenshot:hidden lg:flex'>
<Button variant='single' color='theme-black' onClick={handleChangelogOnClick}>
<IconoirGitFork className='h-6 w-6 text-ut-gray' />
<Text variant='small' className='text-ut-gray font-normal'>
v{manifest.version} - {process.env.NODE_ENV}
</Text>
</Button>
<img src={LDIconURL} alt='LD Icon' className='h-10 w-10 rounded-lg' />
</div>
</header>
<div className='p-6 lg:flex'>
<div className='mr-4 lg:w-1/2 xl:w-xl'>
{/* <section className='mb-8'>
<h2 className='mb-4 text-xl text-ut-black font-semibold'>CUSTOMIZATION OPTIONS</h2>
<div className='flex space-x-4'>
<div className='w-1/2 space-y-4'>
<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'>
<h3 className='text-ut-burntorange font-semibold'>
Show Time & Location in Popup
</h3>
<p className='text-sm text-gray-600'>
Shows the course&apos;s time and location in the extension&apos;s popup.
</p>
</div>
<SwitchButton
isChecked={showTimeLocation}
onChange={() => {
setShowTimeLocation(!showTimeLocation);
OptionsStore.set('enableTimeAndLocationInPopup', !showTimeLocation);
}}
/>
</div>
</div>
{DISPLAY_PREVIEWS && (
<Preview>
<CalendarCourseCell
colors={getCourseColors('orange')}
courseDeptAndInstr={ExampleCourse.department}
className={ExampleCourse.number}
status={ExampleCourse.status}
timeAndLocation={ExampleCourse.schedule.meetings[0]!.getTimeString({
separator: '-',
})}
/>
<PopupCourseBlock colors={getCourseColors('orange')} course={ExampleCourse} />
</Preview>
)}
</div>
</section>
<Divider size='auto' orientation='horizontal' /> */}
<section className='mb-8'>
<h2 className='mb-4 text-xl text-ut-black font-semibold'>ADVANCED SETTINGS</h2>
<div className='flex space-x-4'>
<div className={PREVIEW_SECTION_DIV_CLASSNAME}>
{/* <div className='flex items-center justify-between'>
<div className='max-w-xs'>
<h3 className='text-ut-burntorange font-semibold'>Refresh Data</h3>
<p className='text-sm text-gray-600'>
Refreshes waitlist, course status, and other info with the latest data from
UT&apos;s site.
</p>
</div>
<Button
variant='outline'
color='ut-black'
icon={RefreshIcon}
onClick={() => console.log('Refresh clicked')}
disabled={!enableDataRefreshing}
>
Refresh
</Button>
</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'>
Course Conflict Highlight
</Text>
<p className='text-sm text-gray-600'>
Adds a red strikethrough to courses that have conflicting times.
</p>
</div>
<SwitchButton
isChecked={highlightConflicts}
onChange={() => {
setHighlightConflicts(!highlightConflicts);
OptionsStore.set('enableHighlightConflicts', !highlightConflicts);
}}
/>
</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'>
Load All Courses in Course Schedule
</Text>
<p className='text-sm text-gray-600'>
Loads all courses in the Course Schedule site by scrolling, instead of using
next/prev page buttons.
</p>
</div>
<SwitchButton
isChecked={loadAllCourses}
onChange={() => {
setLoadAllCourses(!loadAllCourses);
OptionsStore.set('enableScrollToLoad', !loadAllCourses);
}}
/>
</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'>
Reset All Data
</Text>
<p className='text-sm text-gray-600'>
Erases all schedules and courses you have.
</p>
</div>
<Button
variant='outline'
color='ut-red'
icon={DeleteForeverIcon}
onClick={handleEraseAll}
>
Erase All
</Button>
</div>
</div>
{DISPLAY_PREVIEWS && (
<Preview>
<div className='inline-flex items-center self-center gap-1'>
<Text variant='small' className='text-ut-gray !font-normal'>
LAST UPDATED: {getUpdatedAtDateTimeString(activeSchedule.updatedAt)}
</Text>
</div>
<Text
variant='h2-course'
className={clsx('text-center text-ut-red !font-normal', {
'line-through': highlightConflicts,
})}
>
01234 MWF 10:00 AM - 11:00 AM UTC 1.234
</Text>
</Preview>
)}
</div>
</section>
<Divider size='auto' orientation='horizontal' />
<section className='my-8 space-y-4'>
<h2 className='mb-4 text-xl text-ut-black font-semibold' onClick={toggleDevMode}>
Developer Mode
</h2>
<Button variant='filled' color='ut-black' onClick={handleAddCourseByUrl}>
Add course by link
</Button>
<Button variant='filled' color='ut-burntorange' onClick={showMigrationDialog}>
Show Migration Dialog
</Button>
</section>
</div>
<Divider className='lg:hidden' size='auto' orientation='horizontal' />
<section className='my-8 lg:my-0 lg:ml-4 lg:w-1/2'>
<section>
<h2 className='mb-4 text-xl text-ut-black font-semibold'>LONGHORN DEVELOPERS ADMINS</h2>
<div className='grid grid-cols-2 gap-4 2xl:grid-cols-4 md:grid-cols-3'>
{LONGHORN_DEVELOPERS_ADMINS.map(admin => (
<div
key={admin.githubUsername}
className='border border-gray-300 rounded bg-ut-gray/10 p-4'
>
<Text
variant='p'
className='text-ut-burntorange font-semibold hover:cursor-pointer'
onClick={() =>
window.open(`https://github.com/${admin.githubUsername}`, '_blank')
}
>
{admin.name}
</Text>
<p className='text-sm text-gray-600'>{admin.role}</p>
{showGitHubStats && githubStats && (
<div className='mt-2'>
<p className='text-xs text-gray-500'>GitHub Stats (UTRP repo):</p>
{includeMergedPRs && (
<p className='text-xs'>
Merged PRS:{' '}
{githubStats.adminGitHubStats[admin.githubUsername]?.mergedPRs}
</p>
)}
<p className='text-xs'>
Commits: {githubStats.adminGitHubStats[admin.githubUsername]?.commits}
</p>
<p className='text-xs text-ut-green'>
{githubStats.adminGitHubStats[admin.githubUsername]?.linesAdded} ++
</p>
<p className='text-xs text-ut-red'>
{githubStats.adminGitHubStats[admin.githubUsername]?.linesDeleted} --
</p>
</div>
)}
</div>
))}
</div>
</section>
<section className='my-8'>
<h2 className='mb-4 text-xl text-ut-black font-semibold'>UTRP CONTRIBUTERS</h2>
<div className='grid grid-cols-2 gap-4 2xl:grid-cols-4 md:grid-cols-3 xl:grid-cols-3'>
{LONGHORN_DEVELOPERS_SWE.map(swe => (
<div
key={swe.githubUsername}
className='border border-gray-300 rounded bg-ut-gray/10 p-4'
>
<Text
variant='p'
className='text-ut-burntorange font-semibold hover:cursor-pointer'
onClick={() =>
window.open(`https://github.com/${swe.githubUsername}`, '_blank')
}
>
{swe.name}
</Text>
<p className='text-sm text-gray-600'>{swe.role}</p>
{showGitHubStats && githubStats && (
<div className='mt-2'>
<p className='text-xs text-gray-500'>GitHub Stats (UTRP repo):</p>
{includeMergedPRs && (
<p className='text-xs'>
Merged PRS:{' '}
{githubStats.userGitHubStats[swe.githubUsername]?.mergedPRs}
</p>
)}
<p className='text-xs'>
Commits: {githubStats.userGitHubStats[swe.githubUsername]?.commits}
</p>
<p className='text-xs text-ut-green'>
{githubStats.userGitHubStats[swe.githubUsername]?.linesAdded} ++
</p>
<p className='text-xs text-ut-red'>
{githubStats.userGitHubStats[swe.githubUsername]?.linesDeleted} --
</p>
</div>
)}
</div>
))}
{githubStats &&
Object.keys(githubStats.userGitHubStats)
.filter(
username =>
!LONGHORN_DEVELOPERS_ADMINS.some(
admin => admin.githubUsername === username
) && !LONGHORN_DEVELOPERS_SWE.some(swe => swe.githubUsername === username)
)
.map(username => (
<div
key={username}
className='overflow-clip border border-gray-300 rounded bg-ut-gray/10 p-4'
>
<Text
variant='p'
className='text-ut-burntorange font-semibold hover:cursor-pointer'
onClick={() => window.open(`https://github.com/${username}`, '_blank')}
>
@{username}
</Text>
<p className='text-sm text-gray-600'>Contributor</p>
{showGitHubStats && (
<div className='mt-2'>
<p className='text-xs text-gray-500'>GitHub Stats (UTRP repo):</p>
{includeMergedPRs && (
<p className='text-xs'>
Merged PRs:{' '}
{githubStats.userGitHubStats[username]?.mergedPRs}
</p>
)}
<p className='text-xs'>
Commits: {githubStats.userGitHubStats[username]?.commits}
</p>
<p className='text-xs text-ut-green'>
{githubStats.userGitHubStats[username]?.linesAdded} ++
</p>
<p className='text-xs text-ut-red'>
{githubStats.userGitHubStats[username]?.linesDeleted} --
</p>
</div>
)}
</div>
))}
</div>
</section>
</section>
</div>
</div>
);
}

View File

@@ -9,39 +9,22 @@ export type CloseWrapper<T> = (close: () => void) => T;
* Information about a dialog.
*/
export interface DialogInfo {
title?: string | JSX.Element;
description?: string | JSX.Element;
title?: JSX.Element;
description?: JSX.Element;
className?: string;
buttons?: JSX.Element | CloseWrapper<JSX.Element>;
onClose?: () => void;
}
/**
* Options for configuring the behavior of a dialog.
*/
export interface DialogOptions {
/**
* Whether to show the dialog immediately.
*/
immediate?: boolean;
/**
* Whether to allow the user to close the dialog by clicking outside of it. (Defaults to true)
*/
closeOnClickOutside?: boolean;
}
/**
* Function to show a dialog.
*/
export type ShowDialogFn = (info: DialogInfo | CloseWrapper<DialogInfo>, options?: DialogOptions) => void;
export type ShowDialogFn = (info: DialogInfo | CloseWrapper<DialogInfo>) => void;
/**
* Context for the dialog provider.
*/
export const DialogContext = createContext<ShowDialogFn>(() => {
throw new Error('DialogContext not initialized.');
});
export const DialogContext = createContext<ShowDialogFn>(() => {});
/**
* @returns The dialog context for showing dialogs.

View File

@@ -1,97 +0,0 @@
import {
BrowserClient,
defaultStackParser,
ErrorBoundary,
getCurrentScope,
getDefaultIntegrations,
init,
makeFetchTransport,
Scope,
} from '@sentry/react';
import type { Client, ClientOptions } from '@sentry/types';
import React, { createContext, useContext, useMemo } from 'react';
/**
* Context for the sentry provider.
*/
export const SentryContext = createContext<[scope: Scope, client: Client]>(undefined!);
/**
* @returns The dialog context for showing dialogs.
*/
export const useSentryScope = () => useContext(SentryContext);
/**
* SentryProvider component initializes and provides Sentry error tracking context to its children.
* It ensures that Sentry is not initialized more than once and configures the Sentry client and scope.
*
* @param props - The properties object.
* @param props.children - The child components that will have access to the Sentry context.
* @param props.transactionName - Optional name for the Sentry transaction.
* @param props.fullInit - Flag to determine if full initialization of Sentry should be performed.
*
* @returns The Sentry context provider wrapping the children components.
*/
export default function SentryProvider({
children,
transactionName,
fullInit,
}: {
children: React.ReactNode;
transactionName?: string;
fullInit?: boolean;
}): JSX.Element {
// prevent accidentally initializing sentry twice
const parent = useSentryScope();
const providerValue = useMemo((): [scope: Scope, client: Client] => {
if (parent) {
const [parentScope, parentClient] = parent;
const scope = parentScope.clone();
if (transactionName) scope.setTransactionName(transactionName);
return [scope, parentClient];
}
// filter integrations that use the global variable
const integrations = getDefaultIntegrations({}).filter(
defaultIntegration =>
!['BrowserApiErrors', 'Breadcrumbs', 'GlobalHandlers'].includes(defaultIntegration.name)
);
const options: ClientOptions = {
dsn: 'https://ed1a50d8626ff6be35b98d7b1ec86d9d@o4508033820852224.ingest.us.sentry.io/4508033822490624',
integrations,
transport: makeFetchTransport,
stackParser: defaultStackParser,
// debug: true,
release: import.meta.env.VITE_PACKAGE_VERSION,
};
let client: Client;
let scope: Scope;
if (fullInit) {
client = init(options)!;
scope = getCurrentScope();
} else {
client = new BrowserClient(options);
scope = new Scope();
scope.setClient(client);
client.init();
}
return [scope, client];
// This is on purpose to only run once
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<ErrorBoundary>
<SentryContext.Provider value={providerValue}>{children}</SentryContext.Provider>
</ErrorBoundary>
);
}

View File

@@ -1,35 +0,0 @@
import ChangelogPopup from '@views/components/common/ChangelogPopup';
import Text from '@views/components/common/Text/Text';
import { useDialog } from '@views/contexts/DialogContext';
import React from 'react';
import MaterialSymbolsClose from '~icons/material-symbols/close';
import { Button } from '../components/common/Button';
/**
* Custom hook that provides a function to display a changelog dialog.
*
* @returns A function that, when called, shows a dialog with the changelog.
*/
export default function useChangelog(): () => void {
const showDialog = useDialog();
const handleOnClick = () => {
showDialog(close => ({
title: (
<div className='sticky top-0 flex items-center justify-between bg-white p-4'>
<Text variant='h1' className='text-theme-black'>
Changelog
</Text>
<Button variant='single' onClick={close} color='theme-black' className='p-1 text-gray-700'>
<MaterialSymbolsClose className='h-7 w-7' />
</Button>
</div>
),
description: <ChangelogPopup />,
}));
};
return handleOnClick;
}

View File

@@ -1,35 +0,0 @@
import type { Course } from '@shared/types/Course';
import { useFlattenedCourseSchedule } from './useFlattenedCourseSchedule';
/**
* Custom hook that retrieves a course from the URL parameters.
*
* This hook extracts the `uniqueId` parameter from the URL, finds the corresponding
* course in the active schedule, and returns it. If the `uniqueId` is not found or
* does not correspond to any course, it returns `null`. After retrieving the course,
* it removes the `uniqueId` parameter from the URL.
*
* @returns The course corresponding to the `uniqueId` in the URL, or `null` if not found.
*/
export default function useCourseFromUrl(): Course | null {
const { activeSchedule } = useFlattenedCourseSchedule();
const getCourseFromUrl = (): Course | null => {
const urlParams = new URLSearchParams(window.location.search);
const uniqueIdRaw = urlParams.get('uniqueId');
if (uniqueIdRaw === null) return null;
const uniqueId = Number(uniqueIdRaw);
const course = activeSchedule.courses.find(course => course.uniqueId === uniqueId);
if (course === undefined) return null;
urlParams.delete('uniqueId');
const newUrl = `${window.location.pathname}?${urlParams}`.replace(/\?$/, '');
window.history.replaceState({}, '', newUrl);
return course;
};
return getCourseFromUrl();
}

View File

@@ -1,5 +1,5 @@
import type { Course, StatusType } from '@shared/types/Course';
import { type CourseMeeting, DAY_MAP } from '@shared/types/CourseMeeting';
import type { CourseMeeting } from '@shared/types/CourseMeeting';
import type { UserSchedule } from '@shared/types/UserSchedule';
import type { CalendarCourseCellProps } from '@views/components/calendar/CalendarCourseCell';
@@ -68,13 +68,7 @@ export function useFlattenedCourseSchedule(): FlattenedCourseSchedule {
return processAsyncCourses({ courseDeptAndInstr, status, course });
}
return meetings.flatMap(meeting => {
if (meeting.days.includes(DAY_MAP.S) || meeting.startTime < 480) {
return processAsyncCourses({ courseDeptAndInstr, status, course });
}
return processInPersonMeetings(meeting, courseDeptAndInstr, status, course);
});
return meetings.flatMap(meeting => processInPersonMeetings(meeting, courseDeptAndInstr, status, course));
})
.sort(sortCourses);
@@ -92,13 +86,7 @@ function extractCourseInfo(course: Course) {
status,
schedule: { meetings },
} = course;
let courseDeptAndInstr = `${course.department} ${course.number}`;
const mainInstructor = course.instructors[0];
if (mainInstructor) {
courseDeptAndInstr += ` ${mainInstructor.toString({ format: 'first_last', case: 'capitalize' })}`;
}
const courseDeptAndInstr = `${course.department} ${course.number} ${course.instructors[0]?.lastName}`;
return { status, courseDeptAndInstr, meetings, course };
}

View File

@@ -9,7 +9,7 @@ let initialLoad = true;
const errorSchedule = new UserSchedule({
courses: [],
id: 'error',
name: 'No Schedule Selected',
name: 'An error has occurred',
hours: 0,
updatedAt: Date.now(),
});
@@ -93,7 +93,7 @@ export async function replaceSchedule(oldSchedule: UserSchedule, newSchedule: Us
}
/**
* Switches the active schedule to the one with the specified id.
* Switches the active schedule to the one with the specified name.
* @param id - The id of the schedule to switch to.
* @returns A promise that resolves when the active schedule has been switched.
*/
@@ -103,15 +103,3 @@ export async function switchSchedule(id: string): Promise<void> {
const activeIndex = schedules.findIndex(s => s.id === id);
await UserScheduleStore.set('activeIndex', activeIndex);
}
/**
* Switches the active schedule to the one with the specified name.
* @param name - The name of the schedule to switch to.
* @returns A promise that resolves when the active schedule has been switched.
*/
export async function switchScheduleByName(name: string): Promise<void> {
console.log('Switching schedule...');
const schedules = await UserScheduleStore.get('schedules');
const activeIndex = schedules.findIndex(s => s.name === name);
await UserScheduleStore.set('activeIndex', activeIndex);
}

View File

@@ -38,13 +38,9 @@ type DetailsSelectorType = (typeof DetailsSelector)[keyof typeof DetailsSelector
*/
export class CourseCatalogScraper {
support: SiteSupportType;
doc: Document;
url: string;
constructor(support: SiteSupportType, doc: Document = document, url: string = window.location.href) {
constructor(support: SiteSupportType) {
this.support = support;
this.doc = doc;
this.url = url;
}
/**
@@ -95,7 +91,7 @@ export class CourseCatalogScraper {
uniqueId: this.getUniqueId(row),
instructionMode: this.getInstructionMode(row),
instructors: this.getInstructors(row) as Instructor[],
description: this.getDescription(this.doc),
description: this.getDescription(document),
semester: this.getSemester(),
scrapedAt: Date.now(),
colors: getCourseColors('emerald', 500),
@@ -169,7 +165,7 @@ export class CourseCatalogScraper {
*/
getURL(row: HTMLTableRowElement): string {
const div = row.querySelector<HTMLAnchorElement>(`${TableDataSelector.UNIQUE_ID} a`);
return div?.href || this.url;
return div?.href || window.location.href;
}
/**
@@ -225,11 +221,11 @@ export class CourseCatalogScraper {
/**
* Scrapes the description of the course from the course details page and separates it into an array of cleaned up lines
* @param doc the document of the course details page to scrape
* @param document the document of the course details page to scrape
* @returns an array of lines of the course description
*/
getDescription(doc: Document): string[] {
const lines = doc.querySelectorAll(DetailsSelector.COURSE_DESCRIPTION);
getDescription(document: Document): string[] {
const lines = document.querySelectorAll(DetailsSelector.COURSE_DESCRIPTION);
return Array.from(lines)
.map(line => line.textContent || '')
.map(line => line.replace(/\s\s+/g, ' ').trim())
@@ -237,7 +233,7 @@ export class CourseCatalogScraper {
}
getSemester(): Semester {
const { pathname } = new URL(this.url);
const { pathname } = new URL(window.location.href);
const code = pathname.split('/')[4];
if (!code) {
@@ -280,7 +276,7 @@ export class CourseCatalogScraper {
*/
getFullName(row?: HTMLTableRowElement): string {
if (!row) {
return this.doc.querySelector(DetailsSelector.COURSE_NAME)?.textContent || '';
return document.querySelector(DetailsSelector.COURSE_NAME)?.textContent || '';
}
const div = row.querySelector(TableDataSelector.COURSE_HEADER);
return div?.textContent || '';

View File

@@ -1,51 +0,0 @@
import { CourseCatalogScraper } from '@views/lib/CourseCatalogScraper';
import getCourseTableRows from '@views/lib/getCourseTableRows';
import { SiteSupport } from '@views/lib/getSiteSupport';
/**
* Migrates courses from UTRP v1 to a new schedule.
*
* @param activeSchedule - The active schedule to migrate the courses to.
* @param links - An array of UTRP v1 course URLs.
* @returns A promise that resolves when the migration is complete.
*
* This hook performs the following steps:
* 1. Fetches the course details from the links.
* 2. Scrapes the course information from the fetched HTML.
* 3. Checks if the course was found and adds it to the active schedule if it doesn't already exist.
*
* Notes:
* - Chrome warns in the console that in the future, cookies will not work when we do a network request like how we are doing it now, so might need to open a new tab instead.
*/
export const courseMigration = async (links: string[]) => {
const migratedCourses = [];
// Loop over the links
for (const link of links) {
// eslint-disable-next-line no-await-in-loop
const response = await fetch(link);
// eslint-disable-next-line no-await-in-loop
const text = await response.text();
const doc = new DOMParser().parseFromString(text, 'text/html');
// Scrape the course
const scraper = new CourseCatalogScraper(SiteSupport.COURSE_CATALOG_DETAILS, doc, link);
const tableRows = getCourseTableRows(doc);
const courses = scraper.scrape(tableRows, false);
// Check if the course was found
if (courses.length === 1) {
const description = scraper.getDescription(doc);
const row = courses[0]!;
const course = row.course!;
course.description = description;
// Add the course to the migrated courses
migratedCourses.push(course);
} else {
console.warn('Invalid course link:', link);
}
}
return migratedCourses;
};

View File

@@ -1,6 +1,5 @@
import type { Course, Semester } from '@shared/types/Course';
import type { CourseSQLRow, Distribution } from '@shared/types/Distribution';
import type { QueryExecResult } from 'sql.js';
import { initializeDB } from './initializeDB';
@@ -21,15 +20,9 @@ export async function queryAggregateDistribution(course: Course): Promise<[Distr
const db = await initializeDB();
const [query, params] = generateQuery(course, null, true);
let res: QueryExecResult | undefined;
let instructorIncluded = params[':instructor_last'] !== undefined;
if (instructorIncluded) {
// Only check for data on an instructor if the course has an instructor
res = db.exec(query, params)?.[0];
}
if (!instructorIncluded || !res?.columns?.length) {
// Either no instructor for the class, or no data for the instructor
let res = db.exec(query, params)?.[0];
let instructorIncluded = true;
if (!res?.columns?.length) {
instructorIncluded = false;
const [queryWithoutInstructor, paramsWithoutInstructor] = generateQuery(course, null, false);
res = db.exec(queryWithoutInstructor, paramsWithoutInstructor)?.[0];

View File

@@ -1,279 +0,0 @@
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<T> = {
data: T;
dataFetched: Date;
};
type FetchResult<T> = {
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: 'Staff Engineer', githubUsername: 'Razboy20' },
{ name: 'Diego Perez', role: 'Staff Engineer', githubUsername: 'doprz' },
{ name: 'Lukas Zenick', role: 'Senior Software Engineer', githubUsername: 'Lukas-Zenick' },
{ name: 'Isaiah Rodriguez', role: 'Chief Operations and Design Officer', githubUsername: 'IsaDavRod' },
] as const satisfies TeamMember[];
export const LONGHORN_DEVELOPERS_SWE = [
{ name: 'Samuel Gunter', role: 'Software Engineer', githubUsername: 'Samathingamajig' },
{ name: 'Derek Chen', role: 'Software Engineer', githubUsername: 'DereC4' },
{ name: 'Casey Charleston', role: 'Software Engineer', githubUsername: 'caseycharleston' },
{ name: 'Vinson', role: 'Software Engineer', githubUsername: 'vinsonzheng499' },
{ name: 'Vivek', role: 'Software Engineer', githubUsername: 'vivek12311' },
] as const satisfies TeamMember[];
/**
* Represents the GitHub usernames of the SWEs in the LONGHORN_DEVELOPERS_SWE array.
*/
export type LD_SWE_GITHUB_USERNAMES = (typeof LONGHORN_DEVELOPERS_SWE)[number]['githubUsername'];
/**
* 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<string, CachedData<any>>;
constructor(githubToken?: string) {
this.octokit = githubToken ? new Octokit({ auth: githubToken }) : new Octokit();
this.cache = new Map();
}
private getCachedData<T>(key: string): CachedData<T> | null {
const cachedItem = this.cache.get(key);
if (cachedItem && Date.now() - cachedItem.dataFetched.getTime() < CACHE_TTL) {
return cachedItem;
}
return null;
}
private setCachedData<T>(key: string, data: T): void {
this.cache.set(key, { data, dataFetched: new Date() });
}
private async fetchWithRetry<T>(fetchFn: () => Promise<T>, retries: number = 3, delay: number = 5000): Promise<T> {
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<FetchResult<ContributorStats[]>> {
const cacheKey = `contributor_stats_${REPO_OWNER}_${REPO_NAME}`;
const cachedStats = this.getCachedData<ContributorStats[]>(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<ContributorStats[]> = {
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<FetchResult<number>> {
const cacheKey = `merged_prs_${username}`;
const cachedCount = this.getCachedData<number>(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<number> = {
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<string, GitHubStats>;
userGitHubStats: Record<string, GitHubStats>;
contributors: string[];
dataFetched: Date;
lastUpdated: Date;
isCached: boolean;
}> {
const { includeMergedPRs = false } = options;
const adminGitHubStats: Record<string, GitHubStats> = {};
const userGitHubStats: Record<string, GitHubStats> = {};
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();

View File

@@ -11,7 +11,6 @@ export const SiteSupport = {
WAITLIST: 'WAITLIST',
EXTENSION_POPUP: 'EXTENSION_POPUP',
MY_CALENDAR: 'MY_CALENDAR',
REPORT_ISSUE: 'REPORT_ISSUE',
} as const;
/**

View File

@@ -70,7 +70,7 @@ export async function loadNextCourseCatalogPage(): Promise<[AutoLoadStatusType,
* @param doc the document to get the next button from
* @returns the next button from the document
*/
export function getNextButton(doc: Document) {
function getNextButton(doc: Document) {
return doc.querySelector<HTMLAnchorElement>(NEXT_PAGE_BUTTON_SELECTOR);
}

View File

@@ -6,7 +6,10 @@
"module": "esnext",
"noEmit": true,
"jsx": "react",
"typeRoots": ["./node_modules/@types", "@types"],
"typeRoots": [
"./node_modules/@types",
"@types"
],
"skipLibCheck": true,
"esModuleInterop": true,
"resolveJsonModule": true,
@@ -31,7 +34,6 @@
"include": [
"src",
"utils",
"scripts",
"vite.config.ts",
"@types",
"node_modules/@types",
@@ -40,8 +42,8 @@
".eslintrc.cjs",
"postcss.config.cjs",
".storybook",
"unocss.config.ts",
"gulpfile.js",
"@types"
]
"unocss.config.ts"
,
"@types"
]
}

View File

@@ -1,27 +0,0 @@
import type { SimpleGit } from 'simple-git';
import { simpleGit } from 'simple-git';
/**
* Determines the source reference based on the destination branch.
* @param destinationBranch - The destination branch for the release.
* @returns A Promise that resolves to the source reference.
* @throws Error if an invalid destination branch is provided.
*/
export async function getSourceRef(destinationBranch: string): Promise<string> {
const git: SimpleGit = simpleGit();
switch (destinationBranch) {
case 'preview':
return 'develop';
case 'production':
// Get the latest tag from the repository
const tags = await git.tags();
const latestTag = tags.latest;
if (!latestTag) {
throw new Error('No tags found in the repository');
}
return latestTag;
default:
throw new Error(`Invalid destination branch: ${destinationBranch}`);
}
}

View File

@@ -1,3 +1,26 @@
type ColorType = 'success' | 'info' | 'error' | 'warning' | keyof typeof COLORS;
export default function colorLog(message: string, type?: ColorType) {
let color: string = type || COLORS.FgBlack;
switch (type) {
case 'success':
color = COLORS.FgGreen;
break;
case 'info':
color = COLORS.FgBlue;
break;
case 'error':
color = COLORS.FgRed;
break;
case 'warning':
color = COLORS.FgYellow;
break;
}
console.log(color, message);
}
const COLORS = {
Reset: '\x1b[0m',
Bright: '\x1b[1m',
@@ -23,49 +46,3 @@ const COLORS = {
BgCyan: '\x1b[46m',
BgWhite: '\x1b[47m',
} as const;
type ColorType = 'success' | 'info' | 'error' | 'warning' | keyof typeof COLORS;
/**
* Logs a message with an optional color type.
*
* @param message - The message to be logged.
* @param type - The color type of the log message. Defaults to undefined.
*/
export default function colorLog(message: string, type?: ColorType) {
let color: string = type || COLORS.FgBlack;
switch (type) {
case 'success':
color = COLORS.FgGreen;
break;
case 'info':
color = COLORS.FgBlue;
break;
case 'error':
color = COLORS.FgRed;
break;
case 'warning':
color = COLORS.FgYellow;
break;
default:
color = COLORS.FgWhite;
break;
}
console.log(color, message);
}
/**
* Logs an info message
*
* @param message - The message to be logged.
*/
export const info = (message: string) => colorLog(message, 'info');
/**
* Logs an error message.
*
* @param message - The message to be logged
*/
export const error = (message: string) => colorLog(message, 'error');

View File

@@ -1,80 +0,0 @@
// https://github.com/pnd280/complexity/blob/alpha/vite-plugins/vite-plugin-run-command-on-demand.ts
import chalk from 'chalk';
import { exec } from 'child_process';
import type { HmrContext, Plugin } from 'vite';
const pluginName = 'vite-plugin-run-command-on-demand';
const log = (message: string) => console.log(chalk.blue(`\n[${pluginName}]`), chalk.green(message));
const logError = (message: string) => console.error(chalk.blue(`\n[${pluginName}]`), chalk.red(message));
const runCommand = (command: string): Promise<void> =>
new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
logError(`Error executing command: ${command}\n${stderr}`);
reject(error);
} else {
log(`Command executed successfully: ${command}\n${stdout}`);
resolve();
}
});
});
type CustomCommandsPluginOptions = {
beforeServerStart?: string;
afterServerStart?: string;
onHotUpdate?: string;
beforeBuild?: string;
afterBuild?: string;
closeBundle?: string;
};
const executeCommand = async (command: string | undefined, errorMessage: string) => {
if (command) {
try {
await runCommand(command);
} catch {
logError(errorMessage);
}
}
};
export default function customCommandsPlugin(options: CustomCommandsPluginOptions = {}): Plugin {
return {
name: pluginName,
configureServer(server) {
server.httpServer?.once('listening', async () => {
await executeCommand(
options.beforeServerStart,
`Error running beforeServerStart command: ${options.beforeServerStart}`
);
await executeCommand(
options.afterServerStart,
`Error running afterServerStart command: ${options.afterServerStart}`
);
});
},
async handleHotUpdate(ctx: HmrContext) {
const isPageReload = ctx.modules.some(module => !module.isSelfAccepting);
if (!isPageReload) {
await executeCommand(options.onHotUpdate, `Error running onHotUpdate command: ${options.onHotUpdate}`);
}
return ctx.modules;
},
async buildStart() {
await executeCommand(options.beforeBuild, `Error running beforeBuild command: ${options.beforeBuild}`);
},
async buildEnd() {
await executeCommand(options.afterBuild, `Error running afterBuild command: ${options.afterBuild}`);
},
async closeBundle() {
await executeCommand(options.closeBundle, `Error running closeBundle command: ${options.closeBundle}`);
},
};
}

View File

@@ -10,7 +10,6 @@ import inspect from 'vite-plugin-inspect';
import packageJson from './package.json';
import manifest from './src/manifest';
import vitePluginRunCommandOnDemand from './utils/plugins/run-command-on-demand';
const root = resolve(__dirname, 'src');
const pagesDir = resolve(root, 'pages');
@@ -141,11 +140,6 @@ export default defineConfig({
renameFile('src/pages/debug/index.html', 'debug.html'),
renameFile('src/pages/options/index.html', 'options.html'),
renameFile('src/pages/calendar/index.html', 'calendar.html'),
renameFile('src/pages/report/index.html', 'report.html'),
vitePluginRunCommandOnDemand({
afterServerStart: 'pnpm gulp forceDisableUseDynamicUrl',
closeBundle: 'pnpm gulp forceDisableUseDynamicUrl',
}),
],
resolve: {
alias: {
@@ -177,28 +171,20 @@ export default defineConfig({
target: 'http://localhost:5173',
rewrite: path => path.replace('options', 'src/pages/options/index'),
},
'/report.html': {
target: 'http://localhost:5173',
rewrite: path => path.replace('report', 'src/pages/report/index'),
},
},
},
build: {
target: ['chrome120', 'edge120', 'firefox120'],
emptyOutDir: true,
reportCompressedSize: false,
sourcemap: true,
rollupOptions: {
input: {
debug: 'src/pages/debug/index.html',
calendar: 'src/pages/calendar/index.html',
options: 'src/pages/options/index.html',
report: 'src/pages/report/index.html',
},
output: {
chunkFileNames: `assets/[name]-[hash].js`,
assetFileNames: `assets/[name]-[hash][extname]`,
},
// output: {
// entryFileNames: `[name].js`, // otherwise it will add the hash
// chunkFileNames: `[name].js`,
// },
// external: ['/@react-refresh'],
},
},
test: {