Compare commits

..

195 Commits

Author SHA1 Message Date
DhruvArora-03
514fbc6081 fix: comment/fix build errors 2024-02-24 21:05:22 -06:00
DhruvArora-03
0dcf1f2c04 Merge branch 'dhruv/fix-popup-main-icons' into dhruv/change-schedule-calendar-page 2024-02-24 20:59:27 -06:00
DhruvArora-03
dee20c6523 feat: fix icons on PopupMain 2024-02-24 20:52:28 -06:00
doprz
e4a368fbb6 fix: vitest path alias bug 2024-02-22 23:25:08 -06:00
doprz
29247d5dfa chore: lint-format-docs-tests-bugfixes (#105)
* docs: add jsdoc

* feat: change enums to as const objects

* chore(test): add themeColors.test.ts

* fix: fix tests and bugs with strings.ts util

* fix: path alias imports and tsconfig file bug

* fix: remove --max-warnings 0
2024-02-22 22:42:58 -06:00
Samuel Gunter
bb2efc0b46 feat: updated divider component (#99)
* feat: updated divider component

* refactor: inlined Divider's classes, simplified stories

* fix: style to unocss in story

* style: renamed variant to orientation for buttons

* docs: updated comments in Divider after prop name change
2024-02-22 21:24:41 -06:00
doprz
d5a04c745f feat: Best Practices (#102)
* feat: best practices

* feat: add tests workflow

* feat: add best-practices workflow

* fix: wrong indentation in workflow
2024-02-21 15:54:21 -06:00
doprz
f01cb070b3 feat: Conventional Commits (#103)
* feat: add commitlint and husky hook

* chore: fix indentation
2024-02-21 13:36:25 -06:00
b2eac59ae7 Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-20 22:06:52 -06:00
e99664fdae Old icon removed in .tsx 2024-02-20 22:06:48 -06:00
49c0d63f0b Icon added successfully 2024-02-20 22:06:26 -06:00
knownotunknown
8c82282467 Update CalendarGrid.tsx 2024-02-20 21:51:42 -06:00
knownotunknown
0a3c31ec09 Fixed some bugs 2024-02-20 21:39:11 -06:00
knownotunknown
ce7917b474 Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-20 21:20:40 -06:00
knownotunknown
a5fe6ec06b Can open tabs, updated injected popup heading. basically done 2024-02-20 21:20:37 -06:00
Samuel Gunter
5a2ee0d19a fix: change Chromatic action to build current branch, not base branch (#100) 2024-02-20 17:20:28 -06:00
knownotunknown
9ec0d106f5 Chrome extension works 2024-02-19 23:03:53 -06:00
knownotunknown
70a3f14e0a Squashed commit of the following:
commit c46e4a51c9
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Mon Feb 19 21:37:46 2024 -0600

    change from reducer pattern to state variables, remove chartData from state

commit 36bcdd2522
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Mon Feb 19 21:15:41 2024 -0600

    change grade distribution colors to match updated figma

commit 11a50df88d
Merge: c16b301 b4c96a9
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Mon Feb 19 17:57:13 2024 -0600

    Merge branch 'hackathon' into abhinavchadaga/course-catalog-popup

commit c16b301ff0
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Mon Feb 19 17:47:21 2024 -0600

    Kinda complete the handlers

commit 1ac1d9095a
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 17:36:59 2024 -0600

    Bunch of renaming

commit 925829ad41
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 17:24:53 2024 -0600

    Fix syllabi url

    Remove unused variable and unnecessary args to url

commit f2e5d51eb3
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 17:24:22 2024 -0600

    Add TODO

    replace current grade colors with a tailwind palette

commit 747ee44440
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 01:26:51 2024 -0600

    Minor tweaks

    change style in header

commit ddfe952a32
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 01:26:38 2024 -0600

    Add Grade Distribution Stuff

commit c27bf3c390
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sun Feb 18 01:26:13 2024 -0600

    Modify story to use proper course info

commit 7afdbac1b8
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 16:37:01 2024 -0600

    description stuff done

commit 1a89432276
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 15:26:32 2024 -0600

    Rename CoursePopup

    Old one to "Old", remove "2" from new one

commit 4c2b31e61a
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 15:23:01 2024 -0600

    add todo for calendar button

commit 11b7a51ded
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 15:22:18 2024 -0600

    add course button onclick handlers

commit f2dfcec838
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 14:52:38 2024 -0600

    some unocss updates

commit f9f375514b
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 13:00:46 2024 -0600

    Add rmp callback

commit 122fc6dbdd
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 13:00:16 2024 -0600

    Change test course to 314

commit 19b124b3bd
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 12:19:21 2024 -0600

    complete CourseHeaderAndActions Component

    added course buttons, using proper subcomponents now.

commit 2eea01fc74
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 11:22:12 2024 -0600

    use chip component in header

commit 9cb13c8fd1
Merge: a62b718 9392085
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 11:21:12 2024 -0600

    Merge branch 'hackathon' into abhinavchadaga/course-catalog-popup

commit a62b718c43
Merge: 43d2675 7b7b858
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 10:57:24 2024 -0600

    Merge branch 'hackathon' into abhinavchadaga/course-catalog-popup

commit 43d2675be5
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Sat Feb 17 10:54:49 2024 -0600

    some work on course popup

    update the stories and create the header component

commit 31bcef3099
Merge: 874f8d5 fa1d737
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Wed Feb 14 14:33:16 2024 -0600

    Merge branch 'main' into abhinavchadaga/course-catalog-popup

    pulling from main

commit 874f8d56cb
Author: Abhinav Chadaga <abhinav.chadaga@utexas.edu>
Date:   Wed Feb 14 14:30:24 2024 -0600

    some work
2024-02-19 22:39:26 -06:00
knownotunknown
d69707b8e8 Squashed commit of the following:
commit f6896e37e2
Author: knownotunknown <78577376+knownotunknown@users.noreply.github.com>
Date:   Mon Feb 19 20:46:57 2024 -0600

    Calendar Page mostly styled

commit a28422e6b0
Merge: 297601e 41e6d77
Author: knownotunknown <78577376+knownotunknown@users.noreply.github.com>
Date:   Mon Feb 19 18:46:59 2024 -0600

    Merge branch 'hackathon' into Som

commit 297601e715
Author: knownotunknown <78577376+knownotunknown@users.noreply.github.com>
Date:   Sun Feb 18 16:28:29 2024 -0600

    Grid works cleanly with up to two course conflicts. Prob needs refactoring

commit 313a9648c9
Merge: b0a95a6 0acd0b7
Author: knownotunknown <78577376+knownotunknown@users.noreply.github.com>
Date:   Sun Feb 18 15:45:57 2024 -0600

    Merge branch 'hackathon' into Som

commit b0a95a6153
Author: knownotunknown <78577376+knownotunknown@users.noreply.github.com>
Date:   Sun Feb 18 14:10:13 2024 -0600

    Made CourseCells and CalendarGridCells more responsive. CourseCells now rendering onto grid. Still, need to make course cells more responsive, and add edge cases

commit a1a0f00514
Merge: 7479004 ac71b83
Author: knownotunknown <78577376+knownotunknown@users.noreply.github.com>
Date:   Sat Feb 17 17:07:06 2024 -0600

    Merge branch 'hackathon' into Som

commit 7479004a65
Author: knownotunknown <78577376+knownotunknown@users.noreply.github.com>
Date:   Sat Feb 17 16:59:34 2024 -0600

    Need to add sorting
2024-02-19 20:48:00 -06:00
knownotunknown
41e6d77d02 Fixed import error 2024-02-19 18:34:42 -06:00
knownotunknown
11fece0595 Merged in finished CalendarGrid 2024-02-19 18:27:06 -06:00
knownotunknown
c676be4765 Fixed build errors and restructured Calendar page 2024-02-19 18:26:09 -06:00
knownotunknown
b4c96a9a10 Fixed build errors and merging in Casey's branch (driodiwb) 2024-02-19 12:03:23 -06:00
knownotunknown
adba5e1bbc Merge remote-tracking branch 'origin/PopupMain' into hackathon 2024-02-19 11:52:48 -06:00
DhruvArora-03
58dc706ece Merge branch 'feat-conflict-row' into hackathon 2024-02-19 11:29:56 -06:00
DhruvArora-03
70c75ff481 add button to the rows, use new ConflictsWithWarning component 2024-02-19 11:29:24 -06:00
DhruvArora-03
7f76af7ab3 fix ConflictsWithWarning 2024-02-19 11:28:33 -06:00
DhruvArora-03
a538f20aad Merge remote-tracking branch 'origin/hackathon' into feat-conflict-row 2024-02-19 11:11:37 -06:00
Lukas Zenick
0acd0b722c html2canvas -> htmlToImage
also fixed derick's bugs
2024-02-18 12:46:35 -06:00
ae32d0b645 feat: Derek/export png (#95)
* Attempting to use more lightweight version

* Did not work.

* This is not what I wanted

* The image saves correctly. Needs padding

* Padding !!

* Removed downloadjs

* Padding more
2024-02-17 19:09:41 -06:00
DhruvArora-03
f214ed7c01 Merge remote-tracking branch 'origin/hackathon' into feat-conflict-row 2024-02-17 18:21:39 -06:00
Lukas Zenick
e44b0c0e45 FIX README 2024-02-17 17:24:27 -06:00
Lukas Zenick
206c97c5b5 fixed README 2024-02-17 17:23:04 -06:00
ac71b838db feat: Derek vinson/calendar header (#94)
* CalendarHeader alignment progress

* Boom

* css

* Between

* Lol

* Gap fix

* whitespace-nowrap

* Gaps

* Finished alignment of CourseStatus and Buttons

* Colors

* ESLint auto format

* Color UT Registration Plus text

* Reverting vscode

---------

Co-authored-by: Vinson Zheng <vinsonzheng499@gmail.com>
2024-02-17 17:02:36 -06:00
Abhinav Chadaga
4f5753917b sort the flattened course schedule (#93)
first by dayIndex, then startIndex, then endIndex.
2024-02-17 16:57:51 -06:00
DhruvArora-03
478ab31706 Merge branch 'testing-useFlattenedCourseSchedule' into hackathon 2024-02-17 16:37:58 -06:00
DhruvArora-03
79c5c97d98 fix extra space 2024-02-17 16:37:35 -06:00
Samuel Gunter
42420f5502 feat: bottom bar for the calendar page (#91) 2024-02-17 16:25:50 -06:00
DhruvArora-03
0ced198f01 remove empty file 2024-02-17 16:23:17 -06:00
vivek12311
ff4ee494b6 Using React-icons. Probably need to take out later 2024-02-17 16:03:28 -06:00
Lukas Zenick
a03bcf17b8 broken file 2024-02-17 15:49:52 -06:00
DhruvArora-03
314ea09b41 nit: reorder classnames 2024-02-17 15:45:56 -06:00
DhruvArora-03
1891bd941e Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-17 15:45:34 -06:00
Lukas Zenick
af991e6609 broken close bracket 2024-02-17 15:42:30 -06:00
Lukas Zenick
fe3521dec7 Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-17 15:40:47 -06:00
Lukas Zenick
a363efb2d2 Save as PNG functionality 2024-02-17 15:40:42 -06:00
knownotunknown
e7ba9c54a6 Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-17 15:33:29 -06:00
knownotunknown
d69e3435cf WIP 2024-02-17 15:32:42 -06:00
vivek12311
b554b4344d Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-17 15:31:05 -06:00
DhruvArora-03
14f241d603 Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-17 15:30:16 -06:00
DhruvArora-03
419e3066f1 cleanup useFlattenedCourseSchedule 2024-02-17 15:30:14 -06:00
vivek12311
ed3ff846d8 Merge branch 'PopupMain' into hackathon 2024-02-17 15:26:10 -06:00
Lukas Zenick
7132bcf572 adjust location of svg files 2024-02-17 15:26:06 -06:00
vivek12311
0bc30d3d1e test 2024-02-17 15:25:58 -06:00
Lukas Zenick
3878c7104e cal save buttons (no functionality) 2024-02-17 15:15:24 -06:00
knownotunknown
851947db0b Update 2024-02-17 15:03:18 -06:00
Casey Charleston
724e1a1d19 Calendar schedules component (clicking on storybook not working) 2024-02-17 14:40:20 -06:00
Abhinav Chadaga
66fea21abd Merge branch 'hackathon' of https://github.com/ut-developers/UT-Registration-Plus into hackathon 2024-02-17 14:37:59 -06:00
Abhinav Chadaga
7550b55b9b Update CalendarCourseCell.tsx 2024-02-17 14:37:48 -06:00
Abhinav Chadaga
65f0cb27af Merge branch 'hackathon' of https://github.com/ut-developers/UT-Registration-Plus into hackathon 2024-02-17 14:33:51 -06:00
DhruvArora-03
9ce69c2f2e Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-17 14:32:34 -06:00
DhruvArora-03
a46526fa40 working on course popup - stopping because abhinav already doing 2024-02-17 14:32:28 -06:00
Abhinav Chadaga
c5968f3f11 Update CalendarCourseCell.tsx 2024-02-17 14:31:34 -06:00
Abhinav Chadaga
bba067f591 implement flatten course schedule helper function
takes a course schedule and returns an array of objects that can be used to render all the individual course
2024-02-17 14:30:59 -06:00
knownotunknown
71d8ac7486 Update CalendarGrid.stories.tsx 2024-02-17 14:30:15 -06:00
knownotunknown
2997cb87d4 Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-17 14:29:06 -06:00
knownotunknown
8b2d07033c update 2024-02-17 14:27:32 -06:00
Vinson Zheng
b8fe5109a9 Added CalendarHeader and its Storybook, need to resize 2024-02-17 14:22:24 -06:00
Razboy20
2cffb794db update course block names 2024-02-17 13:32:06 -06:00
Razboy20
73fe14e17a fix calendar course cell spacing 2024-02-17 13:27:07 -06:00
knownotunknown
33ca937d12 Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-17 13:08:43 -06:00
knownotunknown
c0968ef7a0 Revert "Revert "update""
This reverts commit 3c7b35d5f3.
2024-02-17 13:07:43 -06:00
knownotunknown
3c7b35d5f3 Revert "update"
This reverts commit 8edd062588.
2024-02-17 13:06:46 -06:00
knownotunknown
8edd062588 update 2024-02-17 13:02:24 -06:00
DhruvArora-03
42d24f6367 create empty settings component - waiting on design 2024-02-17 13:02:06 -06:00
Razboy20
2f19c57553 fix margins on list component 2024-02-17 12:59:43 -06:00
Razboy20
e3b8e4c270 fix calendar course cell colors 2024-02-17 12:54:10 -06:00
DhruvArora-03
5aef43496f create important links component 2024-02-17 12:44:05 -06:00
Razboy20
e1224e11af Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-17 12:24:49 -06:00
Razboy20
7f826c2a78 eslint on save 2024-02-17 12:24:46 -06:00
knownotunknown
89a8e42059 Made draggable only by handle 2024-02-17 12:23:52 -06:00
knownotunknown
ed8915bcd1 Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-17 12:09:10 -06:00
knownotunknown
1168584b8a Update 2024-02-17 12:09:01 -06:00
Razboy20
a0496fea18 Merge branch 'hackathon' of github.com:UT-Developers/UT-Registration-Plus into hackathon 2024-02-17 12:07:56 -06:00
Lukas Zenick
fd747e9e8e Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-17 12:04:42 -06:00
Lukas Zenick
27fdd9c559 fix undefined color case 2024-02-17 12:04:41 -06:00
Vinson Zheng
ba22ba427c Reverted CalendarGrid and CalendarGridCell back to SCSS from Tailwind 2024-02-17 11:54:50 -06:00
Razboy20
3e4cec43d1 update button stylings 2024-02-17 11:41:48 -06:00
Razboy20
633c9ba593 fix unocss theme color namings 2024-02-17 11:41:36 -06:00
knownotunknown
0f14b8ce1b Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-17 11:26:51 -06:00
knownotunknown
bee506b79b Merge branch 'pr/85' into hackathon 2024-02-17 11:25:33 -06:00
DhruvArora-03
d8a4c91a80 Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-17 11:24:41 -06:00
DhruvArora-03
14ab537db1 add JSdoc to calendar page 2024-02-17 11:24:40 -06:00
knownotunknown
939208532b Merge branch 'hackathon' of https://github.com/UT-Developers/UT-Registration-Plus into hackathon 2024-02-17 11:07:35 -06:00
knownotunknown
8e3502593e Converted all LabelsAndDetails Components to Tailwind 2024-02-17 11:06:36 -06:00
DhruvArora-03
7b7b858cf5 undo dummy commit 2024-02-17 10:55:44 -06:00
DhruvArora-03
82b467a223 dummy commit 2024-02-17 10:53:44 -06:00
knownotunknown
f049a25aac Merge branch 'pr/68' into hackathon 2024-02-17 10:52:02 -06:00
knownotunknown
39b4396f88 Merge branch 'NonVirtualList' into hackathon 2024-02-17 10:49:39 -06:00
knownotunknown
854d24bf0d Changed Chip to tailwind css. Fixed eslint for ConflictsWithWarning 2024-02-17 10:48:25 -06:00
knownotunknown
c47320e9a3 Merge branch 'LabelsAndDetails' into hackathon 2024-02-17 10:40:35 -06:00
knownotunknown
982b7de50e Merge branch 'ScheduleTotalHoursAndCourses' into hackathon 2024-02-17 10:40:16 -06:00
knownotunknown
17efb94e68 Merge branch 'InfoCard' into hackathon 2024-02-17 10:40:01 -06:00
knownotunknown
40631e2421 Merge branch 'ConflictsWithWarningComponent' into hackathon 2024-02-17 10:39:51 -06:00
knownotunknown
81e02c8187 Build without errors 2024-02-17 10:36:28 -06:00
vivek12311
d907a43552 Merge branch 'pr/90' into PopupMain 2024-02-16 17:16:49 -06:00
Vinson Zheng
3e1a20a9f2 Deleted scss files 2024-02-16 16:34:56 -06:00
Vinson Zheng
d21843468b Tailwind conversion halfway done 2024-02-16 16:34:06 -06:00
vinsonzheng499
6b7e45b5f4 Merge branch 'UT-Developers:main' into master 2024-02-16 16:05:29 -06:00
Razboy20
36b9189a6c fix non-virtual dnd 2024-02-14 19:25:04 -06:00
knownotunknown
80148ba0ff Early iteration of non-virtual list 2024-02-14 17:55:19 -06:00
knownotunknown
776cab9297 Merge branch 'main' into ListComponentContinuation 2024-02-14 14:23:45 -06:00
knownotunknown
f95736339a Update CalendarCourse.stories.tsx 2024-02-14 13:24:55 -06:00
knownotunknown
3c83a3c064 Eslint, removed React-beautiful-dnd 2024-02-14 13:23:22 -06:00
Samuel Gunter
0928e8d033 chore: merge branch 'main' into sgunter/button-component 2024-02-13 16:54:36 -06:00
vivek12311
3b9ea8ebd5 Popup 2024-02-13 12:53:51 -06:00
Samuel Gunter
b2e7af64b3 fix: only show button hover effects when not disabled 2024-02-12 01:51:46 -06:00
Samuel Gunter
11ce6be578 chore: suppress useless TS warnings 2024-02-11 23:38:24 -06:00
Samuel Gunter
524e3b46d3 chore: removed unused Button.module.scss 2024-02-11 23:29:42 -06:00
Samuel Gunter
000682b4db feat: buttons with icons in tailwind
i am not proud of some of this code
2024-02-11 23:24:24 -06:00
147d38d5c5 Lined em up 2024-02-10 18:21:18 -06:00
knownotunknown
14a90f3dc0 Made List more extensible 2024-02-10 15:09:49 -06:00
dcc7c731c6 Merge branch 'sghsri:main' into master 2024-02-09 18:40:39 -06:00
ad77d73363 Warning 2024-02-09 17:57:48 -06:00
80ee5bfdda Wat 2024-02-09 17:56:29 -06:00
e93c5d3167 Brought back coursemeeting.ts 2024-02-09 17:54:25 -06:00
5ec0f19c53 Merge branch 'master' of https://github.com/DereC4/UT-Registration-Plus 2024-02-09 17:47:24 -06:00
24da9baffe Y 2024-02-09 17:47:12 -06:00
de16ece2ea Merge branch 'sghsri:main' into master 2024-02-09 17:40:52 -06:00
d42ba72170 Correcting text 2024-02-09 17:35:52 -06:00
64563b2f40 Merge branch 'sghsri:main' into master 2024-02-09 17:27:14 -06:00
18dcf8a139 Cool looking tickmarks 2024-02-09 17:26:26 -06:00
72ecb314e8 Final polishing up 2024-02-09 17:20:46 -06:00
0ce6de8b13 ...now WE GOT IT 2024-02-09 17:13:02 -06:00
fa30c526b9 WE GOT IT 2024-02-09 17:12:39 -06:00
c44fd014e9 ARRAY.FROM PAIN 2024-02-09 17:07:21 -06:00
d44f5216f8 Fixing alignment 2024-02-09 16:55:18 -06:00
Vinson Zheng
f1e8485eb5 Small style adjustments 2024-02-09 16:00:38 -06:00
knownotunknown
b57415c846 Updated packages installed 2024-02-09 15:59:31 -06:00
knownotunknown
ff06d05857 Merge branch 'main' into ListComponentContinuation 2024-02-09 15:54:44 -06:00
knownotunknown
1a4d22ccf0 Updated alignment 2024-02-09 14:33:11 -06:00
knownotunknown
279ac076b0 Updated with Tailwind 2024-02-09 12:05:02 -06:00
knownotunknown
e3301cc200 Done, but might want to update font size 2024-02-09 11:57:22 -06:00
knownotunknown
ccf2c68340 Update ConflictsWithWarning.tsx 2024-02-09 11:01:42 -06:00
knownotunknown
dc19d3975a Implemented ConflictsWithWarning 2024-02-09 10:53:41 -06:00
Samuel Gunter
2eaf1b3c36 chore: merge branch 'main' into sgunter/button-component 2024-02-08 14:08:44 -06:00
Samuel Gunter
16cc2f071e chore: merge branch 'main' into sgunter/button-component
for clsx, and other features
2024-02-07 00:27:55 -06:00
Samuel Gunter
ae08ee02f4 feat: tailwind version of Button component
still WIP, need to figure out icon spacing exactly
2024-02-06 23:56:35 -06:00
4f3ccd9c90 stories for grid cell 2024-02-05 23:22:56 -06:00
aaccd9b562 Combine Days with Hours 2024-02-05 23:12:51 -06:00
349e1e5332 Merge branch 'master' of https://github.com/DereC4/UT-Registration-Plus 2024-02-05 23:12:12 -06:00
08e66c95e3 Now the Day labels are aligned and part of the grid 2024-02-05 23:05:15 -06:00
873be55b8b Filter Function replacing double pop(); 2024-02-05 23:00:42 -06:00
c20fb0827f Space bar 2024-02-05 22:54:23 -06:00
1c0ad51914 Reverted settings.json 2024-02-05 22:54:04 -06:00
d81ec5c2fc Exited Suez Canal 2024-02-05 22:45:04 -06:00
4bc65ec171 Got Delayed in the Suez Canal 2024-02-05 22:24:20 -06:00
e630bc82ee Time Column Being Transported via Crane 2024-02-05 22:23:03 -06:00
36314e0e3b Preparing to re-add hours to the left side 2024-02-05 22:11:58 -06:00
cdc1fc9224 Merge branch 'master' of https://github.com/DereC4/UT-Registration-Plus 2024-02-05 22:11:40 -06:00
be76f4bcc7 Merge branch 'sghsri:main' into master 2024-02-05 22:10:53 -06:00
a71bc0f9c3 THEY'RE EVOLVING CUBES 2024-02-05 22:05:20 -06:00
3d95d03813 THE CUBES HAVE LISTENED TO ME 2024-02-05 22:02:29 -06:00
d1720f3356 Retrying 2024-02-05 21:58:40 -06:00
8a317c590b Tactically Retreating 2024-02-05 21:47:10 -06:00
9ab86cfab1 Off center now 2024-02-05 21:38:34 -06:00
01a3918502 Reverts in Stylesheets 2024-02-05 21:33:59 -06:00
7a014a7aab Screams in Stylesheets 2024-02-05 21:33:40 -06:00
618089b17e Format 2024-02-05 21:15:38 -06:00
96dbe40637 Merge branch 'sghsri:main' into master 2024-02-05 21:03:37 -06:00
Samuel Gunter
13bc06cc6d chore: merge branch 'main' into sgunter/button-component 2024-02-03 15:22:27 -06:00
knownotunknown
429b49111a InfoCard implemented 2024-02-03 15:14:07 -06:00
knownotunknown
6e595d21aa Update Chip.stories.tsx 2024-02-03 14:29:07 -06:00
knownotunknown
80043dc652 Deleted unnecessary imports 2024-02-03 14:20:48 -06:00
knownotunknown
b470bf6996 Chip Implemented
Styled inline since I'll need to rewrite in TailwindCSS anyways
2024-02-03 14:15:18 -06:00
Samuel Gunter
e0bf48805a chore: merge branch 'main' into sgunter/button-component 2024-02-03 13:29:43 -06:00
bb2caa2dda Path alias 2024-02-03 00:47:07 -06:00
knownotunknown
dad74ddf4e Drag only on vertical axis
Made this change a while ago, but didn't push since it looks like we might switch to Elie's DnD. Pushing since it looks like there might still be reviewers.
2024-02-03 00:11:46 -06:00
cbf31aecd5 Storybook Story 2024-02-02 22:14:35 -06:00
6aab174618 Imported DAY_MAP 2024-02-02 21:52:46 -06:00
8f2b61d28a Ok 2024-02-02 21:32:38 -06:00
fb6781e17f Colors modules switch 2024-02-02 20:44:52 -06:00
1683d8c48b Docs 2024-02-02 20:42:11 -06:00
e9f95ad3d8 Docs and needed changes 2024-02-02 20:41:14 -06:00
745b83e945 CalendarGridCell 2024-02-02 20:38:28 -06:00
b0dec80364 Camelcase conversion for css modules 2024-02-02 20:27:18 -06:00
Samuel Gunter
03e4ab9d60 chore: merge branch 'main' into sgunter/button-component 2024-02-02 20:26:02 -06:00
9854c9aafa Merge branch 'sghsri:main' into master 2024-02-02 20:25:58 -06:00
b161e319bd Merge branch 'sghsri:main' into master 2024-02-02 20:09:22 -06:00
knownotunknown
ae0d1a3c67 Revert "rename to course block and fix line height for styling"
This reverts commit 77a1d67af3.
2024-02-02 19:59:52 -06:00
knownotunknown
3da4a395dd Implemented List component
Used CalendarCourseMeeting component in story for demonstration purposes
2024-02-02 13:08:55 -06:00
abhinavchadaga
77a1d67af3 rename to course block and fix line height for styling 2024-02-01 20:55:13 -06:00
abhinavchadaga
856d6cda24 change the default course 2024-01-31 13:10:29 -06:00
abhinavchadaga
cbbea7f810 basic component laid out - missing Text and Right Icon, can add more stories 2024-01-31 03:27:37 -06:00
Samuel Gunter
cedaa27a2c feat: updated Button to v2 design
wip commit
2024-01-31 00:11:01 -06:00
243 changed files with 39156 additions and 15642 deletions

View File

@@ -1,9 +0,0 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_size = 4
indent_style = space

1
.env.development Normal file
View File

@@ -0,0 +1 @@
MANIFEST_KEY=

0
.env.production Normal file
View File

205
.eslintrc Normal file
View File

@@ -0,0 +1,205 @@
{
"root": true,
"env": {
"browser": true,
"es6": true,
"node": true,
"webextensions": true,
},
"ignorePatterns": ["*.html", "tsconfig.json"],
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:storybook/recommended",
"airbnb-base",
"airbnb/rules/react",
"airbnb-typescript",
"@unocss",
"prettier",
],
"plugins": ["import", "jsdoc", "react-prefer-function-component", "@typescript-eslint", "simple-import-sort"],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly",
"debugger": true,
"browser": true,
"context": true,
"JSX": true,
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json",
"ecmaVersion": 2022,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true,
"modules": true,
"experimentalObjectRestSpread": true,
},
},
"settings": {
"react": {
"version": "detect",
},
"jsdoc": {
"mode": "typescript",
},
"import/parsers": {
"@typescript-eslint/parser": [".ts", ".tsx"],
},
"import/resolver": {
"typescript": {
"alwaysTryTypes": true,
"project": "./tsconfig.json",
},
},
},
"rules": {
"prefer-const": [
"off",
{
"destructuring": "any",
"ignoreReadBeforeAssign": false,
},
],
"no-plusplus": "off",
"no-inner-declarations": "off",
"sort-imports": "off",
"no-case-declarations": "off",
"no-unreachable": "warn",
"no-constant-condition": "error",
"space-before-function-paren": "off",
"no-undef": "off",
"no-return-await": "off",
"@typescript-eslint/return-await": "off",
"@typescript-eslint/no-shadow": ["off"],
"@typescript-eslint/no-use-before-define": ["off"],
"class-methods-use-this": "off",
"react-hooks/exhaustive-deps": "warn",
"@typescript-eslint/lines-between-class-members": "off",
"no-param-reassign": [
"error",
{
"props": false,
},
],
"no-console": "off",
"consistent-return": "off",
"react/destructuring-assignment": "off",
"import/prefer-default-export": "off",
"no-promise-executor-return": "off",
"import/no-cycle": "off",
"import/no-extraneous-dependencies": "off",
"react/jsx-props-no-spreading": "off",
"keyword-spacing": [
"error",
{
"before": true,
"after": true,
},
],
"no-continue": "off",
"space-before-blocks": [
"error",
{
"functions": "always",
"keywords": "always",
"classes": "always",
},
],
"react/jsx-filename-extension": [
1,
{
"extensions": [".tsx"],
},
],
"react/no-deprecated": "warn",
"react/prop-types": "off",
"react-prefer-function-component/react-prefer-function-component": [
"warn",
{
"allowComponentDidCatch": false,
},
],
"react/function-component-definition": "off",
"react/button-has-type": "off",
"jsdoc/require-param-type": "off",
"jsdoc/require-returns-type": "off",
"jsdoc/newline-after-description": "off",
"react/require-default-props": "off",
"jsdoc/require-jsdoc": [
"warn",
{
"enableFixer": false,
"publicOnly": true,
"checkConstructors": false,
"require": {
"ArrowFunctionExpression": true,
"ClassDeclaration": true,
"ClassExpression": true,
"FunctionExpression": true,
},
"contexts": [
"MethodDefinition:not([key.name=\"componentDidMount\"]):not([key.name=\"render\"])",
"ArrowFunctionExpression",
"ClassDeclaration",
"ClassExpression",
"ClassProperty:not([key.name=\"state\"]):not([key.name=\"componentDidMount\"])",
"FunctionDeclaration",
"FunctionExpression",
"TSDeclareFunction",
"TSEnumDeclaration",
"TSInterfaceDeclaration",
"TSMethodSignature",
"TSModuleDeclaration",
"TSTypeAliasDeclaration",
],
},
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/space-before-function-paren": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-interface": "warn",
"import/no-restricted-paths": [
"error",
{
"zones": [
{
"target": "./src/background",
"from": "./src/views",
"message": "You cannot import into the `background` directory from the `views` directory (i.e. content script files) because it will break the build!",
},
{
"target": "./src/views",
"from": "./src/background",
"message": "You cannot import into the `views` directory from the `background` directory (i.e. background script files) because it will break the build!",
},
{
"target": "./src/shared",
"from": "./",
"except": ["./src/shared", "./node_modules"],
"message": "You cannot import into `shared` from an external directory.",
},
],
},
],
"import/extensions": "off",
"no-restricted-syntax": [
"error",
"ForInStatement",
"LabeledStatement",
"WithStatement",
{
"selector": "TSEnumDeclaration",
"message": "Don't declare enums",
},
],
"@typescript-eslint/consistent-type-exports": "error",
"@typescript-eslint/consistent-type-imports": "error",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
},
}

View File

@@ -1,217 +0,0 @@
module.exports = {
root: true,
env: {
browser: true,
es6: true,
node: true,
webextensions: true,
},
ignorePatterns: ['*.html', 'tsconfig.json'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react/recommended',
'plugin:react-hooks/recommended',
'plugin:storybook/recommended',
'airbnb-base',
'airbnb/rules/react',
'airbnb-typescript',
'@unocss',
'prettier',
],
plugins: [
'import',
'import-essentials',
'jsdoc',
'react-prefer-function-component',
'@typescript-eslint',
'simple-import-sort',
],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
debugger: true,
browser: true,
context: true,
JSX: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
ecmaVersion: 2022,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
modules: true,
experimentalObjectRestSpread: true,
},
},
settings: {
react: {
version: 'detect',
},
jsdoc: {
mode: 'typescript',
},
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {
alwaysTryTypes: true,
project: './tsconfig.json',
},
},
},
rules: {
'prefer-const': [
'off',
{
destructuring: 'any',
ignoreReadBeforeAssign: false,
},
],
'no-plusplus': 'off',
'no-inner-declarations': 'off',
'sort-imports': 'off',
'no-case-declarations': 'off',
'no-unreachable': 'warn',
'no-constant-condition': 'error',
'space-before-function-paren': 'off',
'no-undef': 'off',
'no-return-await': 'off',
'@typescript-eslint/return-await': 'off',
'@typescript-eslint/no-shadow': ['off'],
'@typescript-eslint/no-use-before-define': ['off'],
'class-methods-use-this': 'off',
'react-hooks/exhaustive-deps': 'warn',
'@typescript-eslint/lines-between-class-members': 'off',
'no-param-reassign': [
'error',
{
props: false,
},
],
'no-console': 'off',
'consistent-return': 'off',
'react/destructuring-assignment': 'off',
'import/prefer-default-export': 'off',
'no-promise-executor-return': 'off',
'import/no-cycle': 'off',
'import/no-extraneous-dependencies': 'off',
'react/jsx-props-no-spreading': 'off',
'keyword-spacing': [
'error',
{
before: true,
after: true,
},
],
'no-continue': 'off',
'space-before-blocks': [
'error',
{
functions: 'always',
keywords: 'always',
classes: 'always',
},
],
'react/jsx-filename-extension': [
1,
{
extensions: ['.tsx'],
},
],
'react/no-deprecated': 'warn',
'react/prop-types': 'off',
'react-prefer-function-component/react-prefer-function-component': [
'warn',
{
allowComponentDidCatch: false,
},
],
'react/function-component-definition': 'off',
'react/button-has-type': 'off',
'jsdoc/require-param-type': 'off',
'jsdoc/require-returns-type': 'off',
'jsdoc/newline-after-description': 'off',
'react/require-default-props': 'off',
'jsdoc/require-jsdoc': [
'warn',
{
enableFixer: false,
publicOnly: true,
checkConstructors: false,
require: {
ArrowFunctionExpression: true,
ClassDeclaration: true,
ClassExpression: true,
FunctionExpression: true,
},
contexts: [
'MethodDefinition:not([key.name="componentDidMount"]):not([key.name="render"])',
'ArrowFunctionExpression',
'ClassDeclaration',
'ClassExpression',
'ClassProperty:not([key.name="state"]):not([key.name="componentDidMount"])',
'FunctionDeclaration',
'FunctionExpression',
'TSDeclareFunction',
'TSEnumDeclaration',
'TSInterfaceDeclaration',
'TSMethodSignature',
'TSModuleDeclaration',
'TSTypeAliasDeclaration',
],
},
],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/space-before-function-paren': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-empty-interface': 'warn',
'import/no-restricted-paths': [
'error',
{
zones: [
{
target: './src/background',
from: './src/views',
message:
'You cannot import into the `background` directory from the `views` directory (i.e. content script files) because it will break the build!',
},
{
target: './src/views',
from: './src/background',
message:
'You cannot import into the `views` directory from the `background` directory (i.e. background script files) because it will break the build!',
},
{
target: './src/shared',
from: './',
except: ['./src/shared', './node_modules'],
message: 'You cannot import into `shared` from an external directory.',
},
],
},
],
'import/extensions': 'off',
'no-restricted-syntax': [
'error',
'ForInStatement',
'LabeledStatement',
'WithStatement',
{
selector: 'TSEnumDeclaration',
message: "Don't declare enums",
},
],
'@typescript-eslint/consistent-type-exports': 'error',
'@typescript-eslint/consistent-type-imports': 'error',
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
'import-essentials/restrict-import-depth': 'error',
'import-essentials/check-path-alias': 'error',
},
};

View File

@@ -15,7 +15,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
version: 8
- name: Install dependencies
run: pnpm install
@@ -34,7 +34,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
version: 8
- name: Install dependencies
run: pnpm install

View File

@@ -1,24 +0,0 @@
name: Type Check
on: [push, pull_request]
jobs:
type-check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
- name: Install dependencies
run: pnpm install
- name: Run tests
run: pnpm run check-types

View File

@@ -13,7 +13,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
version: 8
- name: Install dependencies
run: pnpm install
@@ -21,6 +21,6 @@ jobs:
- name: Publish to Chromatic
uses: chromaui/action@latest
with:
projectToken: chpt_e8bd07b0b27d8eb
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitZeroOnChanges: true
autoAcceptChanges: 'main'

View File

@@ -15,7 +15,7 @@ jobs:
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: 9
version: 8
- name: Install dependencies
run: pnpm install

3
.gitignore vendored
View File

@@ -208,5 +208,4 @@ sketch
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,macos,node,react,storybookjs
# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option)
package-lock.json
storybook-static/

View File

@@ -2,13 +2,7 @@ import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-designs',
'@storybook/test',
'@chromatic-com/storybook',
],
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-designs'],
framework: {
name: '@storybook/react-vite',
options: {

View File

@@ -1,178 +1,24 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import type { Preview } from '@storybook/react';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import React from 'react';
import ExtensionRoot from 'src/views/components/common/ExtensionRoot/ExtensionRoot';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
decorators: [
Story => (
<React.StrictMode>
<ExtensionRoot>
<Story />
</ExtensionRoot>
</React.StrictMode>
),
],
},
decorators: [
Story => (
<ExtensionRoot>
<Story />
</ExtensionRoot>
),
],
};
let localData = {};
type ListenerFunction = (
changes: { [key: string]: chrome.storage.StorageChange },
areaName: chrome.storage.AreaName
) => void;
const localDataListeners = new Map<
ListenerFunction, // key to remove listener
(changes: { [key: string]: chrome.storage.StorageChange }) => void
>();
// mock chrome api
globalThis.chrome = {
storage: {
local: {
/**
* Removes all items from storage.
* @param callback Optional.
* Callback on success, or on failure (in which case runtime.lastError will be set).
*/
async clear() {
localData = {};
},
/**
* Gets one or more items from storage.
* @param keys A single key to get, list of keys to get, or a dictionary specifying default values.
* An empty list or object will return an empty result object. Pass in null to get the entire contents of storage.
* @return A Promise that resolves with an object containing items
*/
async get(keys?: string | string[] | { [key: string]: any } | null) {
if (keys === null) {
return localData;
}
if (Array.isArray(keys)) {
return keys.reduce((acc, key) => {
acc[key] = localData[key];
return acc;
}, {} as string); // funny types
}
if (typeof keys === 'string') {
return { [keys]: localData[keys] };
}
return keys;
},
/**
* Gets the amount of space (in bytes) being used by one or more items.
* @param keys Optional. A single key or list of keys to get the total usage for. An empty list will return 0. Pass in null to get the total usage of all of storage.
* @param callback Callback with the amount of space being used by storage, or on failure (in which case runtime.lastError will be set).
* Parameter bytesInUse: Amount of space being used in storage, in bytes.
*/
async getBytesInUse() {
return 0;
},
/**
* Removes one or more items from storage.
* @param keys A single key or a list of keys for items to remove.
* @param callback Optional.
* Callback on success, or on failure (in which case runtime.lastError will be set).
*/
async remove(keys: string | string[]) {
if (Array.isArray(keys)) {
keys.forEach(key => {
for (const listener of localDataListeners.values()) {
listener({ [key]: { oldValue: localData[key], newValue: undefined } });
}
delete localData[key];
});
} else {
for (const listener of localDataListeners.values()) {
listener({ [keys]: { oldValue: localData[keys], newValue: undefined } });
}
delete localData[keys];
}
},
/**
* Sets multiple items.
* @param items An object which gives each key/value pair to update storage with. Any other key/value pairs in storage will not be affected.
* Primitive values such as numbers will serialize as expected. Values with a typeof "object" and "function" will typically serialize to {}, with the exception of Array (serializes as expected), Date, and Regex (serialize using their String representation).
* @param callback Optional.
* Callback on success, or on failure (in which case runtime.lastError will be set).
*/
async set(items: { [key: string]: any }) {
for (const key in items) {
const oldValue = localData[key];
localData[key] = JSON.parse(JSON.stringify(items[key]));
for (const listener of localDataListeners.values()) {
listener({ [key]: { oldValue: oldValue, newValue: localData[key] } });
}
}
},
},
onChanged: {
/**
* Registers an event listener callback to an event.
* @param callback Called when an event occurs. The parameters of this function depend on the type of event.
*/
addListener(
listener: (
changes: { [key: string]: chrome.storage.StorageChange },
areaName: chrome.storage.AreaName
) => void
) {
localDataListeners.set(listener, (changes: { [key: string]: chrome.storage.StorageChange }) => {
listener(changes, 'local');
});
},
/**
* Deregisters an event listener callback from an event.
* @param callback Listener that shall be unregistered.
*/
removeListener(listener: ListenerFunction) {
localDataListeners.delete(listener);
},
},
},
runtime: {
id: 'fake-id',
getManifest(): chrome.runtime.Manifest {
return {
manifest_version: 3,
name: 'fake-name',
version: '0.0.0',
};
},
onMessage: {
/**
* Registers an event listener callback to an event.
* @param callback Called when an event occurs. The parameters of this function depend on the type of event.
*/
addListener<T extends Function>(callback: T) {},
/**
* Deregisters an event listener callback from an event.
* @param callback Listener that shall be unregistered.
*/
removeListener<T extends Function>(callback: T) {},
},
},
} as typeof chrome;
// set updatedAt dates to be fixed
UserScheduleStore.get('schedules').then(schedules => {
schedules.forEach(schedule => {
schedule.updatedAt = new Date('2024-01-01 12:00').getTime();
});
UserScheduleStore.set('schedules', schedules);
});
export default preview;

View File

@@ -0,0 +1,30 @@
// .storybook/vite-storybook.config.ts
import react from "file:///C:/Users/somgu/OneDrive/Desktop/UT-Registration-Plus/node_modules/.pnpm/@vitejs+plugin-react-swc@3.6.0_vite@5.1.2/node_modules/@vitejs/plugin-react-swc/index.mjs";
import { resolve } from "path";
import UnoCSS from "file:///C:/Users/somgu/OneDrive/Desktop/UT-Registration-Plus/node_modules/.pnpm/unocss@0.58.5_postcss@8.4.35_vite@5.1.2/node_modules/unocss/dist/vite.mjs";
import Icons from "file:///C:/Users/somgu/OneDrive/Desktop/UT-Registration-Plus/node_modules/.pnpm/unplugin-icons@0.18.5_@svgr+core@8.1.0/node_modules/unplugin-icons/dist/vite.js";
import { defineConfig } from "file:///C:/Users/somgu/OneDrive/Desktop/UT-Registration-Plus/node_modules/.pnpm/vite@5.1.2_@types+node@20.11.17_sass@1.70.0/node_modules/vite/dist/node/index.js";
var __vite_injected_original_dirname = "C:\\Users\\somgu\\OneDrive\\Desktop\\UT-Registration-Plus\\.storybook";
var root = resolve(__vite_injected_original_dirname, "../src");
var pagesDir = resolve(root, "pages");
var assetsDir = resolve(root, "assets");
var publicDir = resolve(__vite_injected_original_dirname, "../public");
console.log(root);
var vite_storybook_config_default = defineConfig({
plugins: [react(), UnoCSS(), Icons({ compiler: "jsx", jsx: "react" })],
resolve: {
alias: {
src: root,
"@assets": assetsDir,
"@pages": pagesDir,
"@public": publicDir,
"@shared": resolve(root, "shared"),
"@background": resolve(pagesDir, "background"),
"@views": resolve(root, "views")
}
}
});
export {
vite_storybook_config_default as default
};
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiLnN0b3J5Ym9vay92aXRlLXN0b3J5Ym9vay5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJDOlxcXFxVc2Vyc1xcXFxzb21ndVxcXFxPbmVEcml2ZVxcXFxEZXNrdG9wXFxcXFVULVJlZ2lzdHJhdGlvbi1QbHVzXFxcXC5zdG9yeWJvb2tcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIkM6XFxcXFVzZXJzXFxcXHNvbWd1XFxcXE9uZURyaXZlXFxcXERlc2t0b3BcXFxcVVQtUmVnaXN0cmF0aW9uLVBsdXNcXFxcLnN0b3J5Ym9va1xcXFx2aXRlLXN0b3J5Ym9vay5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL0M6L1VzZXJzL3NvbWd1L09uZURyaXZlL0Rlc2t0b3AvVVQtUmVnaXN0cmF0aW9uLVBsdXMvLnN0b3J5Ym9vay92aXRlLXN0b3J5Ym9vay5jb25maWcudHNcIjtpbXBvcnQgcmVhY3QgZnJvbSAnQHZpdGVqcy9wbHVnaW4tcmVhY3Qtc3djJztcbmltcG9ydCB7IHJlc29sdmUgfSBmcm9tICdwYXRoJztcbmltcG9ydCBVbm9DU1MgZnJvbSAndW5vY3NzL3ZpdGUnO1xuaW1wb3J0IEljb25zIGZyb20gJ3VucGx1Z2luLWljb25zL3ZpdGUnO1xuaW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSc7XG5cbmNvbnN0IHJvb3QgPSByZXNvbHZlKF9fZGlybmFtZSwgJy4uL3NyYycpO1xuY29uc3QgcGFnZXNEaXIgPSByZXNvbHZlKHJvb3QsICdwYWdlcycpO1xuY29uc3QgYXNzZXRzRGlyID0gcmVzb2x2ZShyb290LCAnYXNzZXRzJyk7XG5jb25zdCBwdWJsaWNEaXIgPSByZXNvbHZlKF9fZGlybmFtZSwgJy4uL3B1YmxpYycpO1xuXG5jb25zb2xlLmxvZyhyb290KTtcblxuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gICAgcGx1Z2luczogW3JlYWN0KCksIFVub0NTUygpLCBJY29ucyh7IGNvbXBpbGVyOiAnanN4JywganN4OiAncmVhY3QnIH0pXSxcbiAgICByZXNvbHZlOiB7XG4gICAgICAgIGFsaWFzOiB7XG4gICAgICAgICAgICBzcmM6IHJvb3QsXG4gICAgICAgICAgICAnQGFzc2V0cyc6IGFzc2V0c0RpcixcbiAgICAgICAgICAgICdAcGFnZXMnOiBwYWdlc0RpcixcbiAgICAgICAgICAgICdAcHVibGljJzogcHVibGljRGlyLFxuICAgICAgICAgICAgJ0BzaGFyZWQnOiByZXNvbHZlKHJvb3QsICdzaGFyZWQnKSxcbiAgICAgICAgICAgICdAYmFja2dyb3VuZCc6IHJlc29sdmUocGFnZXNEaXIsICdiYWNrZ3JvdW5kJyksXG4gICAgICAgICAgICAnQHZpZXdzJzogcmVzb2x2ZShyb290LCAndmlld3MnKSxcbiAgICAgICAgfSxcbiAgICB9LFxufSk7XG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQWlaLE9BQU8sV0FBVztBQUNuYSxTQUFTLGVBQWU7QUFDeEIsT0FBTyxZQUFZO0FBQ25CLE9BQU8sV0FBVztBQUNsQixTQUFTLG9CQUFvQjtBQUo3QixJQUFNLG1DQUFtQztBQU16QyxJQUFNLE9BQU8sUUFBUSxrQ0FBVyxRQUFRO0FBQ3hDLElBQU0sV0FBVyxRQUFRLE1BQU0sT0FBTztBQUN0QyxJQUFNLFlBQVksUUFBUSxNQUFNLFFBQVE7QUFDeEMsSUFBTSxZQUFZLFFBQVEsa0NBQVcsV0FBVztBQUVoRCxRQUFRLElBQUksSUFBSTtBQUdoQixJQUFPLGdDQUFRLGFBQWE7QUFBQSxFQUN4QixTQUFTLENBQUMsTUFBTSxHQUFHLE9BQU8sR0FBRyxNQUFNLEVBQUUsVUFBVSxPQUFPLEtBQUssUUFBUSxDQUFDLENBQUM7QUFBQSxFQUNyRSxTQUFTO0FBQUEsSUFDTCxPQUFPO0FBQUEsTUFDSCxLQUFLO0FBQUEsTUFDTCxXQUFXO0FBQUEsTUFDWCxVQUFVO0FBQUEsTUFDVixXQUFXO0FBQUEsTUFDWCxXQUFXLFFBQVEsTUFBTSxRQUFRO0FBQUEsTUFDakMsZUFBZSxRQUFRLFVBQVUsWUFBWTtBQUFBLE1BQzdDLFVBQVUsUUFBUSxNQUFNLE9BQU87QUFBQSxJQUNuQztBQUFBLEVBQ0o7QUFDSixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=

View File

@@ -1,9 +1,5 @@
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"antfu.unocss",
"editorconfig.editorconfig",
"figma.figma-vscode-extension"
"clinyong.vscode-css-modules"
]
}
}

View File

@@ -37,4 +37,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"typescript.tsdk": "node_modules/typescript/lib",
"tailwindCSS.includeLanguages": {
"plaintext": "javascript"
}
}

13
@types/css-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,13 @@
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.module.scss' {
const classes: { [key: string]: string };
export default classes;
}
declare module '*.mp3' {
const src: string;
export default src;
}

22
@types/environment.d.ts vendored Normal file
View File

@@ -0,0 +1,22 @@
declare global {
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production';
CI?: string;
/** set this to make sure the extension id is the same for unpacked extensions
* @see https://developer.chrome.com/docs/apps/app_identity/#copy_key */
MANIFEST_KEY?: string;
/**
* The Node semantic versioning-compatible version of the extension. For preview-style releases, this variable
* converts versions like 1.0.0.100 to 1.0.0-beta.1.
*/
SEMANTIC_VERSION?: string;
}
}
type Environment = typeof process.env.NODE_ENV;
}
// If this file has no import/export statements (i.e. is a script)
// convert it into a module by adding an empty export statement.
export {};

6
@types/svg-import.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
declare module "*.svg" {
import { ReactElement, SVGProps } from "react";
const ReactComponent: (props: SVGProps<SVGElement>) => ReactElement;
export default ReactComponent;
}

10
@types/vite-env.d.ts vendored
View File

@@ -1,10 +0,0 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_PACKAGE_VERSION: string;
readonly VITE_BETA_BUILD?: 'true';
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@@ -1,59 +1,30 @@
# UT Registration Plus
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.
## Built Using
- 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.
- React 18
- TypeScript
- Vite 5
- ESLint
- Prettier
- Semantic-Release
- Custom Messaging & Storage Wrappers
- Shows the course description with highlighted information on prerequisites, restrictions, etc.
- Shows an aggregate and semesterly graph of the grade distributions for each course.
- 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.
- 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.
- 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 18
- TypeScript
- Vite 5
- ESLint
- Prettier
- Storybook
- Semantic-Release
- Custom Messaging & Storage Wrappers
## Development: Getting Started
## Getting Started
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.
2. Run `pnpm install` to install and patch all the required dependencies
### Development Builds
- If you want to run the development build:
- Run `pnpm dev`
- Run `pnpm run dev`
> [!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.
- If you want to build the extension for production:
### Production Builds
- Run `pnpm build`
- Run `pnpm build`
You may have to rename the `__uno.css.js` to `uno.css.js` in dist
<details>
<summary>Beta builds</summary>
Use `BETA=true pnpm build` to build a beta build.
</details>
Go to chrome://extensions, ensure you have "Developer Mode" enabled, and click 'Load unpacked'
## Development: Loading the Extension Manually
Open [chrome://extensions](chrome://extensions), ensure you have 'Developer Mode' enabled, and click 'Load unpacked'.
Navigate to the `dist/` folder, and click 'select' to import the extension.
Navigate to the 'dist' folder and click 'select' to import the extension

View File

@@ -1,5 +0,0 @@
{
"onlyChanged": true,
"projectId": "Project:65c5172964f36dcf207985bf",
"zip": true
}

23602
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
{
"name": "ut-registration-plus",
"displayName": "UT Registration Plus",
"version": "2.0.0-beta4",
"description": "UT Registration Plus is a Chrome extension that allows students to easily register for classes.",
"version": "0.0.1",
"description": "The UT Registration Plus extension is a Chrome extension that allows students to easily register for classes at The University of Texas at Austin.",
"private": true,
"homepage": "https://github.com/Longhorn-Developers/UT-Registration-Plus",
"homepage": "sriramhariharan.com",
"type": "module",
"scripts": {
"dev": "vite",
@@ -13,99 +13,97 @@
"prettier:fix": "prettier src --write",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives",
"lint:fix": "eslint src --ext ts,tsx --report-unused-disable-directives --fix",
"check-types": "tsc --noEmit",
"test": "vitest",
"test:ui": "vitest --ui",
"coverage": "vitest run --coverage",
"preview": "vite preview",
"devtools": "react-devtools",
"preinstall": "npx only-allow pnpm",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"prepare": "husky"
},
"dependencies": {
"@headlessui/react": "^2.0.3",
"@headlessui/react": "^1.7.18",
"@headlessui/tailwindcss": "^0.2.0",
"@hello-pangea/dnd": "^16.5.0",
"@unocss/vite": "^0.58.6",
"@types/sql.js": "^1.4.9",
"@vitejs/plugin-react": "^4.2.1",
"chrome-extension-toolkit": "^0.0.54",
"chrome-extension-toolkit": "^0.0.51",
"clsx": "^2.1.0",
"highcharts": "^11.3.0",
"highcharts-react-official": "^3.2.1",
"html-to-image": "^1.11.11",
"husky": "^9.0.11",
"nanoid": "^5.0.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^18.2.0",
"react-devtools-core": "^5.0.0",
"react-dom": "^18.2.0",
"sass": "^1.71.1",
"sql.js": "1.10.2"
"sql.js": "1.10.2",
"styled-components": "^6.1.8",
"uuid": "^9.0.1"
},
"devDependencies": {
"@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.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",
"@storybook/addon-designs": "^7.0.9",
"@storybook/addon-essentials": "^7.6.17",
"@storybook/addon-links": "^7.6.17",
"@storybook/blocks": "^7.6.17",
"@storybook/react": "^7.6.17",
"@storybook/react-vite": "^7.6.17",
"@storybook/test": "^7.6.17",
"@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0",
"@types/chrome": "^0.0.268",
"@types/node": "^20.12.12",
"@types/chrome": "^0.0.260",
"@types/node": "^20.11.19",
"@types/prompts": "^2.4.9",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"@types/semver": "^7.5.8",
"@types/sql.js": "^1.4.9",
"@types/react": "^18.2.57",
"@types/react-dom": "^18.2.19",
"@types/semver": "^7.5.7",
"@types/uuid": "^9.0.8",
"@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",
"@unocss/eslint-config": "^0.58.5",
"@unocss/postcss": "^0.58.5",
"@unocss/preset-uno": "^0.58.5",
"@unocss/preset-web-fonts": "^0.58.5",
"@unocss/reset": "^0.58.5",
"@unocss/transformer-directives": "^0.58.5",
"@unocss/transformer-variant-group": "^0.58.5",
"@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",
"chromatic": "^10.9.6",
"cssnano": "^6.0.3",
"cssnano-preset-advanced": "^6.0.3",
"dotenv": "^16.4.5",
"es-module-lexer": "^1.4.1",
"eslint": "^8.57.0",
"eslint": "^8.56.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^17.1.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-import-essentials": "^0.2.1",
"eslint-plugin-jsdoc": "^48.2.0",
"eslint-plugin-jsdoc": "^48.1.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.5",
"eslint-plugin-simple-import-sort": "^12.0.0",
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-storybook": "^0.6.15",
"husky": "^9.0.11",
"path": "^0.12.7",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"react-dev-utils": "^12.0.1",
"storybook": "^8.1.1",
"typescript": "^5.4.3",
"unocss": "^0.58.6",
"unocss-preset-primitives": "0.0.2-beta.0",
"react-devtools": "^5.0.0",
"storybook": "^7.6.17",
"typescript": "^5.3.3",
"unocss": "^0.58.5",
"unplugin-icons": "^0.18.5",
"vite": "^5.1.4",
"vite-plugin-inspect": "^0.8.3",
@@ -113,8 +111,7 @@
},
"pnpm": {
"patchedDependencies": {
"@crxjs/vite-plugin@2.0.0-beta.21": "patches/@crxjs__vite-plugin@2.0.0-beta.21.patch",
"@unocss/vite@0.58.6": "patches/@unocss__vite@0.58.6.patch"
"@crxjs/vite-plugin@2.0.0-beta.21": "patches/@crxjs__vite-plugin@2.0.0-beta.21.patch"
},
"overrides": {
"es-module-lexer": "^1.4.1"

View File

@@ -1,108 +0,0 @@
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')

18504
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

BIN
public/database/grades.db Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Beta" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<defs>
<style>
.cls-1 {
fill: none;
stroke: #fff;
stroke-width: 70.06px;
}
.cls-2 {
fill: #005f86;
}
.cls-2, .cls-3 {
stroke-width: 0px;
}
.cls-3 {
fill: #fff;
}
</style>
</defs>
<rect class="cls-2" width="1024" height="1024"/>
<g>
<circle class="cls-1" cx="512" cy="512" r="362"/>
<rect class="cls-3" x="466.29" y="283.46" width="91.41" height="457.07"/>
<rect class="cls-3" x="283.46" y="466.29" width="457.07" height="91.41"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 791 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Development" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<defs>
<style>
.cls-1 {
fill: none;
stroke: #fff;
stroke-width: 70.06px;
}
.cls-2 {
fill: #bf2178;
}
.cls-2, .cls-3 {
stroke-width: 0px;
}
.cls-3 {
fill: #fff;
}
</style>
</defs>
<rect class="cls-2" x="0" width="1024" height="1024"/>
<g>
<circle class="cls-1" cx="512" cy="512" r="362"/>
<rect class="cls-3" x="466.29" y="283.46" width="91.41" height="457.07"/>
<rect class="cls-3" x="283.46" y="466.29" width="457.07" height="91.41"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 697 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 B

After

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 729 B

After

Width:  |  Height:  |  Size: 849 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Production" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<defs>
<style>
.cls-1 {
fill: #bf5700;
}
.cls-1, .cls-2 {
stroke-width: 0px;
}
.cls-3 {
fill: none;
stroke: #fff;
stroke-width: 70.06px;
}
.cls-2 {
fill: #fff;
}
</style>
</defs>
<rect class="cls-1" width="1024" height="1024"/>
<g>
<circle class="cls-3" cx="512" cy="512" r="362"/>
<rect class="cls-2" x="466.29" y="283.46" width="91.41" height="457.07"/>
<rect class="cls-2" x="283.46" y="466.29" width="457.07" height="91.41"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 B

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 706 B

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 17 KiB

8
src/assets/icons/cal.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_3175_7842" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="25">
<rect y="0.5" width="24" height="24" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_3175_7842)">
<path d="M12 14.5C11.7167 14.5 11.4792 14.4042 11.2875 14.2125C11.0958 14.0208 11 13.7833 11 13.5C11 13.2167 11.0958 12.9792 11.2875 12.7875C11.4792 12.5958 11.7167 12.5 12 12.5C12.2833 12.5 12.5208 12.5958 12.7125 12.7875C12.9042 12.9792 13 13.2167 13 13.5C13 13.7833 12.9042 14.0208 12.7125 14.2125C12.5208 14.4042 12.2833 14.5 12 14.5ZM8 14.5C7.71667 14.5 7.47917 14.4042 7.2875 14.2125C7.09583 14.0208 7 13.7833 7 13.5C7 13.2167 7.09583 12.9792 7.2875 12.7875C7.47917 12.5958 7.71667 12.5 8 12.5C8.28333 12.5 8.52083 12.5958 8.7125 12.7875C8.90417 12.9792 9 13.2167 9 13.5C9 13.7833 8.90417 14.0208 8.7125 14.2125C8.52083 14.4042 8.28333 14.5 8 14.5ZM16 14.5C15.7167 14.5 15.4792 14.4042 15.2875 14.2125C15.0958 14.0208 15 13.7833 15 13.5C15 13.2167 15.0958 12.9792 15.2875 12.7875C15.4792 12.5958 15.7167 12.5 16 12.5C16.2833 12.5 16.5208 12.5958 16.7125 12.7875C16.9042 12.9792 17 13.2167 17 13.5C17 13.7833 16.9042 14.0208 16.7125 14.2125C16.5208 14.4042 16.2833 14.5 16 14.5ZM12 18.5C11.7167 18.5 11.4792 18.4042 11.2875 18.2125C11.0958 18.0208 11 17.7833 11 17.5C11 17.2167 11.0958 16.9792 11.2875 16.7875C11.4792 16.5958 11.7167 16.5 12 16.5C12.2833 16.5 12.5208 16.5958 12.7125 16.7875C12.9042 16.9792 13 17.2167 13 17.5C13 17.7833 12.9042 18.0208 12.7125 18.2125C12.5208 18.4042 12.2833 18.5 12 18.5ZM8 18.5C7.71667 18.5 7.47917 18.4042 7.2875 18.2125C7.09583 18.0208 7 17.7833 7 17.5C7 17.2167 7.09583 16.9792 7.2875 16.7875C7.47917 16.5958 7.71667 16.5 8 16.5C8.28333 16.5 8.52083 16.5958 8.7125 16.7875C8.90417 16.9792 9 17.2167 9 17.5C9 17.7833 8.90417 18.0208 8.7125 18.2125C8.52083 18.4042 8.28333 18.5 8 18.5ZM16 18.5C15.7167 18.5 15.4792 18.4042 15.2875 18.2125C15.0958 18.0208 15 17.7833 15 17.5C15 17.2167 15.0958 16.9792 15.2875 16.7875C15.4792 16.5958 15.7167 16.5 16 16.5C16.2833 16.5 16.5208 16.5958 16.7125 16.7875C16.9042 16.9792 17 17.2167 17 17.5C17 17.7833 16.9042 18.0208 16.7125 18.2125C16.5208 18.4042 16.2833 18.5 16 18.5ZM5 22.5C4.45 22.5 3.97917 22.3042 3.5875 21.9125C3.19583 21.5208 3 21.05 3 20.5V6.5C3 5.95 3.19583 5.47917 3.5875 5.0875C3.97917 4.69583 4.45 4.5 5 4.5H6V2.5H8V4.5H16V2.5H18V4.5H19C19.55 4.5 20.0208 4.69583 20.4125 5.0875C20.8042 5.47917 21 5.95 21 6.5V20.5C21 21.05 20.8042 21.5208 20.4125 21.9125C20.0208 22.3042 19.55 22.5 19 22.5H5ZM5 20.5H19V10.5H5V20.5Z" fill="#333F48"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

8
src/assets/icons/png.svg Normal file
View File

@@ -0,0 +1,8 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0_3211_5369" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="25">
<rect y="0.5" width="24" height="24" fill="#D9D9D9"/>
</mask>
<g mask="url(#mask0_3211_5369)">
<path d="M5 21.5C4.45 21.5 3.97917 21.3042 3.5875 20.9125C3.19583 20.5208 3 20.05 3 19.5V5.5C3 4.95 3.19583 4.47917 3.5875 4.0875C3.97917 3.69583 4.45 3.5 5 3.5H19C19.55 3.5 20.0208 3.69583 20.4125 4.0875C20.8042 4.47917 21 4.95 21 5.5V19.5C21 20.05 20.8042 20.5208 20.4125 20.9125C20.0208 21.3042 19.55 21.5 19 21.5H5ZM6 17.5H18L14.25 12.5L11.25 16.5L9 13.5L6 17.5Z" fill="#333F48"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 702 B

View File

@@ -1,54 +0,0 @@
const splashText: string[] = [
"You can't fail classes you're not in, that's for sure.",
'Steer clear of O-Chem, unless you fancy a challenge.',
'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?',
'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',
'No Work Happens On PCL 5th Floor.',
'I may be a sophomore in name, but my credit count screams freshman!',
'Pain is temporary, GPA is forever.',
"You've Yee'd Your Last Haw.",
'lol everything is already waitlisted.',
"At Least You're Not At A&M.",
'TeXAs iS BaCK GuYZ',
'mAke iT yOuR tExAS',
"'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',
"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",
"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',
'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.',
"'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.",
"'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'.",
'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;

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -4,19 +4,9 @@ import { createRoot } from 'react-dom/client';
const manifest = chrome.runtime.getManifest();
/**
* Handles editing the storage for a specific area.
*
* @param {string} areaName - The name of the storage area.
* @returns {Function} - A function that accepts changes and sets them in the storage.
*/
const handleEditStorage = (areaName: 'local' | 'sync' | 'session') => (changes: Record<string, unknown>) => {
chrome.storage[areaName].set(changes);
};
interface JSONEditorProps {
data: unknown;
onChange: ReturnType<typeof handleEditStorage>;
data: any;
onChange: (updates: any) => void;
}
function JSONEditor(props: JSONEditorProps) {
@@ -39,8 +29,6 @@ function JSONEditor(props: JSONEditorProps) {
setIsEditing(false);
} catch (e) {
console.error(e);
// eslint-disable-next-line no-alert
alert('Invalid JSON');
}
};
@@ -74,9 +62,9 @@ function JSONEditor(props: JSONEditorProps) {
// ));
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>>({});
const [localStorage, setLocalStorage] = React.useState<any>({});
const [syncStorage, setSyncStorage] = React.useState<any>({});
const [sessionStorage, setSessionStorage] = React.useState<any>({});
useEffect(() => {
const onVisibilityChange = () => {
@@ -105,8 +93,7 @@ function DevDashboard() {
// listen for changes to the chrome storage to update the local storage state displayed in the dashboard
useEffect(() => {
const onChanged = (changes: chrome.storage.StorageChange, areaName: chrome.storage.AreaName) => {
let copy: Record<string, unknown> = {};
let copy = {};
if (areaName === 'local') {
copy = { ...localStorage };
} else if (areaName === 'sync') {
@@ -115,8 +102,8 @@ function DevDashboard() {
copy = { ...sessionStorage };
}
Object.keys(changes).forEach((key: string) => {
copy[key] = changes[key as keyof typeof changes].newValue;
Object.keys(changes).forEach(key => {
copy[key] = changes[key].newValue;
});
if (areaName === 'local') {
@@ -137,6 +124,10 @@ function DevDashboard() {
};
}, [localStorage, syncStorage, sessionStorage]);
const handleEditStorage = (areaName: string) => (changes: Record<string, any>) => {
chrome.storage[areaName].set(changes);
};
return (
<div>
<h1>
@@ -154,4 +145,4 @@ function DevDashboard() {
);
}
createRoot(document.getElementById('root')!).render(<DevDashboard />);
createRoot(document.getElementById('root')).render(<DevDashboard />);

View File

@@ -0,0 +1,24 @@
// this is a custom wrapper around react-devtools
// that changes it so that we only send messages to the devtools when the current tab is active;
import { connectToDevTools } from 'react-devtools-core';
// connect to the devtools server
let ws = new WebSocket('ws://localhost:8097');
connectToDevTools({
websocket: ws,
});
// when the tab's visibile state changes, we connect or disconnect from the devtools
const onVisibilityChange = () => {
if (document.visibilityState === 'visible') {
ws = new WebSocket('ws://localhost:8097');
connectToDevTools({
websocket: ws,
});
} else {
ws.close();
}
};
document.addEventListener('visibilitychange', onVisibilityChange);

View File

@@ -9,12 +9,7 @@ const [major, minor, patch, label = '0'] = packageJson.version
// split into version parts
.split(/[.-]/);
const isBeta = !!process.env.BETA;
const mode = isBeta ? 'beta' : process.env.NODE_ENV;
if (isBeta && process.env.NODE_ENV !== 'production') throw new Error('Cannot have beta non-production build');
// eslint-disable-next-line no-nested-ternary
const nameSuffix = isBeta ? ' (beta)' : mode === 'development' ? ' (dev)' : '';
const mode = process.env.NODE_ENV;
const HOST_PERMISSIONS: string[] = [
'*://*.utdirect.utexas.edu/apps/registrar/course_schedule/*',
@@ -22,17 +17,16 @@ const HOST_PERMISSIONS: string[] = [
'*://*.catalog.utexas.edu/ribbit/',
'*://*.registrar.utexas.edu/schedules/*',
'*://*.login.utexas.edu/login/*',
'https://utexas.bluera.com/*',
];
const manifest = defineManifest(async () => ({
manifest_version: 3,
name: `${packageJson.displayName ?? packageJson.name}${nameSuffix}`,
name: `${packageJson.displayName ?? packageJson.name}${mode === 'development' ? ' (dev)' : ''}`,
version: `${major}.${minor}.${patch}.${label}`,
description: packageJson.description,
options_page: 'src/pages/options/index.html',
background: { service_worker: 'src/pages/background/background.ts' },
permissions: ['storage', 'unlimitedStorage', 'background', 'scripting'],
permissions: ['storage', 'unlimitedStorage', 'background'],
host_permissions: process.env.MODE === 'development' ? [...HOST_PERMISSIONS, '<all_urls>'] : HOST_PERMISSIONS,
action: {
default_popup: 'src/pages/popup/index.html',
@@ -56,9 +50,6 @@ const manifest = defineManifest(async () => ({
matches: ['*://*/*'],
},
],
content_security_policy: {
extension_pages: "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'",
},
}));
export default manifest;

View File

@@ -1,14 +1,10 @@
import type { BACKGROUND_MESSAGES } from '@shared/messages';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import updateBadgeText from '@shared/util/updateBadgeText';
import { MessageListener } from 'chrome-extension-toolkit';
import onInstall from './events/onInstall';
import onServiceWorkerAlive from './events/onServiceWorkerAlive';
import onUpdate from './events/onUpdate';
import browserActionHandler from './handler/browserActionHandler';
import calendarBackgroundHandler from './handler/calendarBackgroundHandler';
import CESHandler from './handler/CESHandler';
import tabManagementHandler from './handler/tabManagementHandler';
import userScheduleHandler from './handler/userScheduleHandler';
@@ -36,24 +32,6 @@ const messageListener = new MessageListener<BACKGROUND_MESSAGES>({
...browserActionHandler,
...tabManagementHandler,
...userScheduleHandler,
...CESHandler,
...calendarBackgroundHandler,
});
messageListener.listen();
UserScheduleStore.listen('schedules', async schedules => {
const index = await UserScheduleStore.get('activeIndex');
const numCourses = schedules.newValue[index]?.courses?.length;
if (!numCourses) return;
updateBadgeText(numCourses);
});
UserScheduleStore.listen('activeIndex', async ({ newValue }) => {
const schedules = await UserScheduleStore.get('schedules');
const numCourses = schedules[newValue]?.courses?.length;
if (!numCourses) return;
updateBadgeText(numCourses);
});

View File

@@ -1,4 +1,4 @@
import { openDebugTab } from '@pages/background/util/openDebugTab';
import { openDebugTab } from '../util/openDebugTab';
/**
* Called whenever the background service worker comes alive

View File

@@ -1,39 +0,0 @@
import openNewTab from '@background/util/openNewTab';
import type CESMessage from '@shared/messages/CESMessage';
import type { MessageHandler } from 'chrome-extension-toolkit';
const CESFall2023Url = 'https://utexas.bluera.com/utexas/rpvl.aspx?rid=d3db767b-049f-46c5-9a67-29c21c29c580&regl=en-US';
const CESHandler: MessageHandler<CESMessage> = {
openCESPage({ data, sendResponse }) {
const { instructorFirstName, instructorLastName } = data;
openNewTab(CESFall2023Url).then(tab => {
const instructorFirstAndLastName = [instructorFirstName, instructorLastName];
chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (...instructorFirstAndLastName: string[]) => {
const inputElement = document.getElementById(
'ctl00_ContentPlaceHolder1_ViewList_tbxValue'
) as HTMLInputElement | null;
const [instructorFirstName, instructorLastName] = instructorFirstAndLastName;
if (inputElement) {
inputElement.value = `${instructorFirstName} ${instructorLastName}`;
inputElement.focus();
const enterKeyEvent = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
which: 13,
bubbles: true,
});
inputElement.dispatchEvent(enterKeyEvent);
}
},
args: instructorFirstAndLastName,
});
sendResponse(tab);
});
},
};
export default CESHandler;

View File

@@ -1,48 +0,0 @@
import type { TabWithId } from '@background/util/openNewTab';
import openNewTab from '@background/util/openNewTab';
import { tabs } from '@shared/messages';
import type { CalendarBackgroundMessages } from '@shared/messages/CalendarMessages';
import type { MessageHandler } from 'chrome-extension-toolkit';
const getAllTabInfos = async () => {
const openTabs = (await chrome.tabs.query({})).filter((tab): tab is TabWithId => tab.id !== undefined);
const results = await Promise.allSettled(openTabs.map(tab => tabs.getTabInfo(undefined, tab.id)));
type TabInfo = PromiseFulfilledResult<Awaited<ReturnType<typeof tabs.getTabInfo>>>;
return results
.map((result, index) => ({ result, index }))
.filter((el): el is { result: TabInfo; index: number } => el.result.status === 'fulfilled')
.map(({ result, index }) => ({
...result.value,
tab: openTabs[index]!,
}));
};
const calendarBackgroundHandler: MessageHandler<CalendarBackgroundMessages> = {
async switchToCalendarTab({ data, sendResponse }) {
const { uniqueId } = data;
const calendarUrl = chrome.runtime.getURL(`calendar.html`);
const allTabs = await getAllTabInfos();
const openCalendarTabInfo = allTabs.find(tab => tab.url?.startsWith(calendarUrl));
if (openCalendarTabInfo !== undefined) {
const tabid = openCalendarTabInfo.tab.id;
chrome.tabs.update(tabid, { active: true });
if (uniqueId !== undefined) await tabs.openCoursePopup({ uniqueId }, tabid);
sendResponse(openCalendarTabInfo.tab);
} else {
const urlParams = new URLSearchParams();
if (uniqueId !== undefined) urlParams.set('uniqueId', uniqueId.toString());
const url = `${calendarUrl}?${urlParams.toString()}`.replace(/\?$/, '');
const tab = await openNewTab(url);
sendResponse(tab);
}
},
};
export default calendarBackgroundHandler;

View File

@@ -0,0 +1,24 @@
import type HotReloadingMessages from '@shared/messages/HotReloadingMessages';
import { DevStore } from '@shared/storage/DevStore';
import type { MessageHandler } from 'chrome-extension-toolkit';
const hotReloadingHandler: MessageHandler<HotReloadingMessages> = {
async reloadExtension({ sendResponse }) {
const [isExtensionReloading, isTabReloading] = await Promise.all([
DevStore.get('isExtensionReloading'),
DevStore.get('isTabReloading'),
]);
if (!isExtensionReloading) return sendResponse();
if (isTabReloading) {
const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
const tabToReload = tabs[0];
await DevStore.set('reloadTabId', tabToReload?.id);
}
chrome.runtime.reload();
},
};
export default hotReloadingHandler;

View File

@@ -1,7 +1,8 @@
import openNewTab from '@pages/background/util/openNewTab';
import type TabManagementMessages from '@shared/messages/TabManagementMessages';
import type { MessageHandler } from 'chrome-extension-toolkit';
import openNewTab from '../util/openNewTab';
const tabManagementHandler: MessageHandler<TabManagementMessages> = {
getTabId({ sendResponse, sender }) {
sendResponse(sender.tab?.id ?? -1);

View File

@@ -1,35 +1,36 @@
import addCourse from '@pages/background/lib/addCourse';
import clearCourses from '@pages/background/lib/clearCourses';
import createSchedule from '@pages/background/lib/createSchedule';
import deleteSchedule from '@pages/background/lib/deleteSchedule';
import removeCourse from '@pages/background/lib/removeCourse';
import renameSchedule from '@pages/background/lib/renameSchedule';
import switchSchedule from '@pages/background/lib/switchSchedule';
import type { UserScheduleMessages } from '@shared/messages/UserScheduleMessages';
import { Course } from '@shared/types/Course';
import type { MessageHandler } from 'chrome-extension-toolkit';
import addCourse from '../lib/addCourse';
import clearCourses from '../lib/clearCourses';
import createSchedule from '../lib/createSchedule';
import deleteSchedule from '../lib/deleteSchedule';
import removeCourse from '../lib/removeCourse';
import renameSchedule from '../lib/renameSchedule';
import switchSchedule from '../lib/switchSchedule';
const userScheduleHandler: MessageHandler<UserScheduleMessages> = {
addCourse({ data, sendResponse }) {
addCourse(data.scheduleId, new Course(data.course)).then(sendResponse);
addCourse(data.scheduleName, new Course(data.course)).then(sendResponse);
},
removeCourse({ data, sendResponse }) {
removeCourse(data.scheduleId, new Course(data.course)).then(sendResponse);
removeCourse(data.scheduleName, new Course(data.course)).then(sendResponse);
},
clearCourses({ data, sendResponse }) {
clearCourses(data.scheduleId).then(sendResponse);
clearCourses(data.scheduleName).then(sendResponse);
},
switchSchedule({ data, sendResponse }) {
switchSchedule(data.scheduleId).then(sendResponse);
switchSchedule(data.scheduleName).then(sendResponse);
},
createSchedule({ data, sendResponse }) {
createSchedule(data.scheduleName).then(sendResponse);
},
deleteSchedule({ data, sendResponse }) {
deleteSchedule(data.scheduleId).then(sendResponse);
deleteSchedule(data.scheduleName).then(sendResponse);
},
renameSchedule({ data, sendResponse }) {
renameSchedule(data.scheduleId, data.newName).then(sendResponse);
renameSchedule(data.scheduleName, data.newName).then(sendResponse);
},
};

View File

@@ -1,24 +1,17 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import type { Course } from '@shared/types/Course';
import { getUnusedColor } from '@shared/util/colors';
/**
* Adds a course to a user's schedule.
* @param scheduleId - The id of the schedule to add the course to.
* @param course - The course to add.
* @returns A promise that resolves to void.
* @throws An error if the schedule is not found.
*
*/
export default async function addCourse(scheduleId: string, course: Course): Promise<void> {
export default async function addCourse(scheduleName: string, course: Course): Promise<void> {
const schedules = await UserScheduleStore.get('schedules');
const activeSchedule = schedules.find(s => s.id === scheduleId);
const activeSchedule = schedules.find(s => s.name === scheduleName);
if (!activeSchedule) {
throw new Error('Schedule not found');
}
course.colors = getUnusedColor(activeSchedule, course);
activeSchedule.courses.push(course);
activeSchedule.updatedAt = Date.now();
await UserScheduleStore.set('schedules', schedules);
}

View File

@@ -2,17 +2,15 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
/**
* Clears the courses for a given schedule.
* @param scheduleId - The id of the schedule.
* @param scheduleName - The name of the schedule.
* @throws Error if the schedule does not exist.
*/
export default async function clearCourses(scheduleId: string): Promise<void> {
export default async function clearCourses(scheduleName: string): Promise<void> {
const schedules = await UserScheduleStore.get('schedules');
const schedule = schedules.find(schedule => schedule.id === scheduleId);
const schedule = schedules.find(schedule => schedule.name === scheduleName);
if (!schedule) {
throw new Error(`Schedule ${scheduleId} does not exist`);
throw new Error(`Schedule ${scheduleName} does not exist`);
}
schedule.courses = [];
schedule.updatedAt = Date.now();
await UserScheduleStore.set('schedules', schedules);
}

View File

@@ -1,5 +1,4 @@
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { generateRandomId } from '@shared/util/random';
/**
* Creates a new schedule with the given name
@@ -8,16 +7,14 @@ import { generateRandomId } from '@shared/util/random';
*/
export default async function createSchedule(scheduleName: string): Promise<string | undefined> {
const schedules = await UserScheduleStore.get('schedules');
// if (schedules.find(schedule => schedule.name === scheduleName)) {
// return `Schedule ${scheduleName} already exists`;
// }
if (schedules.find(schedule => schedule.name === scheduleName)) {
return `Schedule ${scheduleName} already exists`;
}
schedules.push({
id: generateRandomId(),
name: scheduleName,
courses: [],
hours: 0,
updatedAt: Date.now(),
});
await UserScheduleStore.set('schedules', schedules);

View File

@@ -3,21 +3,21 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
/**
* Deletes a schedule with the specified name.
*
* @param scheduleId - The id of the schedule to delete.
* @param scheduleName - The name of the schedule to delete.
* @returns A promise that resolves to a string if there is an error, or undefined if the schedule is deleted successfully.
*/
export default async function deleteSchedule(scheduleId: string): Promise<string | undefined> {
export default async function deleteSchedule(scheduleName: string): Promise<string | undefined> {
const [schedules, activeIndex] = await Promise.all([
UserScheduleStore.get('schedules'),
UserScheduleStore.get('activeIndex'),
]);
const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId);
const scheduleIndex = schedules.findIndex(schedule => schedule.name === scheduleName);
if (scheduleIndex === -1) {
throw new Error(`Schedule ${scheduleId} does not exist`);
return `Schedule ${scheduleName} does not exist`;
}
if (scheduleIndex === activeIndex) {
throw new Error('Cannot delete active schedule');
return 'Cannot delete active schedule';
}
schedules.splice(scheduleIndex, 1);

View File

@@ -4,15 +4,14 @@ import type { Course } from '@shared/types/Course';
/**
*
*/
export default async function removeCourse(scheduleId: string, course: Course): Promise<void> {
export default async function removeCourse(scheduleName: string, course: Course): Promise<void> {
const schedules = await UserScheduleStore.get('schedules');
const activeSchedule = schedules.find(s => s.id === scheduleId);
const activeSchedule = schedules.find(s => s.name === scheduleName);
if (!activeSchedule) {
throw new Error('Schedule not found');
}
activeSchedule.courses = activeSchedule.courses.filter(c => c.uniqueId !== course.uniqueId);
activeSchedule.updatedAt = Date.now();
await UserScheduleStore.set('schedules', schedules);
}

View File

@@ -2,19 +2,21 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
/**
* Renames a schedule with the specified name to a new name.
* @param scheduleId - The id of the schedule to be renamed.
* @param scheduleName - The name of the schedule to be renamed.
* @param newName - The new name for the schedule.
* @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> {
export default async function renameSchedule(scheduleName: string, newName: string): Promise<string | undefined> {
const schedules = await UserScheduleStore.get('schedules');
const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId);
const scheduleIndex = schedules.findIndex(schedule => schedule.name === scheduleName);
if (scheduleIndex === -1) {
return `Schedule ${scheduleId} does not exist`;
return `Schedule ${scheduleName} does not exist`;
}
if (schedules.find(schedule => schedule.name === newName)) {
return `Schedule ${newName} already exists`;
}
schedules[scheduleIndex]!.name = newName;
// schedules[scheduleIndex].updatedAt = Date.now();
schedules[scheduleIndex].name = newName;
await UserScheduleStore.set('schedules', schedules);
return undefined;

View File

@@ -3,18 +3,16 @@ import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
/**
* Switches the active schedule to the specified schedule name.
* Throws an error if the schedule does not exist.
* @param scheduleId - The id of the schedule to switch to.
* @param scheduleName - The name of the schedule to switch to.
* @returns A Promise that resolves when the active schedule is successfully switched.
*/
export default async function switchSchedule(scheduleId: string): Promise<void> {
export default async function switchSchedule(scheduleName: string): Promise<void> {
const schedules = await UserScheduleStore.get('schedules');
const scheduleIndex = schedules.findIndex(schedule => schedule.id === scheduleId);
const scheduleIndex = schedules.findIndex(schedule => schedule.name === scheduleName);
if (scheduleIndex === -1) {
throw new Error(`Schedule ${scheduleId} does not exist`);
throw new Error(`Schedule ${scheduleName} does not exist`);
}
schedules[scheduleIndex]!.updatedAt = Date.now();
await UserScheduleStore.set('activeIndex', scheduleIndex);
}

View File

@@ -0,0 +1,42 @@
import { DevStore } from '@shared/storage/DevStore';
/**
* A list of websites that we don't want to reload when the extension reloads (becuase it'd be hella annoying lmao)
*/
const HOT_RELOADING_WHITELIST = [
'youtube.com',
'twitch.tv',
'github.dev',
'figma.com',
'netflix.com',
'disneyplus.com',
'hbomax.com',
'spotify.com',
'localhost:6006',
'docs.google.com',
'reddit.com',
'gmail.com',
'photopea.com',
'chat.openai.com',
];
/**
* Reloads the tab that was open when the extension was reloaded
* @returns a promise that resolves when the tab is reloaded
*/
export async function hotReloadTab(): Promise<void> {
const [isTabReloading, reloadTabId] = await Promise.all([
DevStore.get('isTabReloading'),
DevStore.get('reloadTabId'),
]);
if (!isTabReloading || !reloadTabId) return;
chrome.tabs.get(reloadTabId, tab => {
if (!tab?.id) return;
if (!tab.url) return;
if (!HOT_RELOADING_WHITELIST.find(url => tab.url?.includes(url))) {
chrome.tabs.reload(tab.id);
}
});
}

View File

@@ -10,7 +10,7 @@ export async function openDebugTab() {
DevStore.get('wasDebugTabVisible'),
]);
const isAlreadyOpen = (await chrome.tabs.query({})).some(tab => tab.id === debugTabId);
const isAlreadyOpen = await (await chrome.tabs.query({})).some(tab => tab.id === debugTabId);
if (isAlreadyOpen) return;
const tab = await chrome.tabs.create({

View File

@@ -1,12 +1,10 @@
export type TabWithId = Omit<chrome.tabs.Tab, 'id'> & { id: number };
/**
* This is a helper function that opens a new tab in the current window, and focuses the window
* @param tabIndex - the index of the tab to open the new tab at (optional)
* @returns the tab that was opened
*/
export default async function openNewTab(url: string, tabIndex?: number): Promise<TabWithId> {
const tab = (await chrome.tabs.create({ url, index: tabIndex, active: true })) as TabWithId;
export default async function openNewTab(url: string, tabIndex?: number): Promise<chrome.tabs.Tab> {
const tab = await chrome.tabs.create({ url, index: tabIndex, active: true });
await chrome.windows.update(tab.windowId, { focused: true });
return tab;
}

View File

@@ -1,31 +1,14 @@
import type TabInfoMessages from '@shared/messages/TabInfoMessages';
import Calendar from '@views/components/calendar/Calendar';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import { MessageListener } from 'chrome-extension-toolkit';
import React, { useEffect } from 'react';
import React from 'react';
import { Calendar } from 'src/views/components/calendar/Calendar/Calendar';
/**
* Calendar page
* @returns entire page
*/
export default function CalendarMain() {
useEffect(() => {
const tabInfoListener = new MessageListener<TabInfoMessages>({
getTabInfo: ({ sendResponse }) => {
sendResponse({
url: window.location.href,
title: document.title,
});
},
});
tabInfoListener.listen();
return () => tabInfoListener.unlisten();
}, []);
return (
<ExtensionRoot className='h-full w-full'>
<ExtensionRoot>
<Calendar />
</ExtensionRoot>
);

View File

@@ -7,9 +7,9 @@
<title>Calendar</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" style="height: 100%"></div>
<div id="root"></div>
<script src="./index.tsx" type="module"></script>
</body>

View File

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

View File

@@ -1,5 +1,4 @@
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import Link from '@views/components/common/Link';
import React from 'react';
/**
@@ -8,28 +7,7 @@ import React from 'react';
export default function App() {
return (
<ExtensionRoot>
<div className='text-base'>
<div className='font-serif'>
<i>&ldquo;Real powerusers modify the sourcecode instead of using settings&rdquo;</i> - doprz
</div>
<div className='font-serif'>
<i>
&ldquo;become hackerman, go to{' '}
<Link
href='https://github.com/Longhorn-Developers/UT-Registration-Plus'
className='link font-serif! italic!'
>
github
</Link>{' '}
yay&rdquo;
</i>{' '}
- razboy20
</div>
<p className='mt-2.5 text-sm text-ut-gray'>
{import.meta.env.VITE_PACKAGE_VERSION} - {import.meta.env.MODE}{' '}
{import.meta.env.VITE_BETA_BUILD ? 'beta' : ''}
</p>
</div>
<div>hello how are you doing today.</div>
</ExtensionRoot>
);
}

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>UTRP Options</title>
<title>Popup</title>
</head>
<body>

View File

@@ -3,4 +3,4 @@ import { createRoot } from 'react-dom/client';
import App from './App';
createRoot(document.getElementById('root')!).render(<App />);
createRoot(document.getElementById('root')).render(<App />);

View File

@@ -5,14 +5,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<title>Popup</title>
<style>
body {
margin: 0;
padding: 0;
width: 360px;
height: 540px;
}
</style>
</head>
<body>

View File

@@ -1,7 +1,6 @@
import 'uno.css';
import PopupMain from '@views/components/PopupMain';
import React from 'react';
import { createRoot } from 'react-dom/client';
createRoot(document.getElementById('root')!).render(<PopupMain />);
import PopupMain from '../../views/components/PopupMain';
createRoot(document.getElementById('root')).render(<PopupMain />);

View File

@@ -1,10 +0,0 @@
interface CESMessage {
/**
* Opens the CES page for the specified instructor
*
* @param data first and last name of the instructor
*/
openCESPage: (data: { instructorFirstName: string; instructorLastName: string }) => chrome.tabs.Tab;
}
export default CESMessage;

View File

@@ -1,19 +0,0 @@
interface CalendarBackgroundMessages {
/**
* Opens the calendar page if it is not already open, focuses the tab, and optionally opens the calendar for a specific course
*
* @param data - The unique id of the course to open the calendar page for (optional)
*/
switchToCalendarTab: (data: { uniqueId?: number }) => chrome.tabs.Tab;
}
interface CalendarTabMessages {
/**
* Opens a popup for a specific course on the calendar page
*
* @param data - The unique id of the course to open on the calendar page
*/
openCoursePopup: (data: { uniqueId: number }) => chrome.tabs.Tab;
}
export type { CalendarBackgroundMessages, CalendarTabMessages };

View File

@@ -0,0 +1,4 @@
/* eslint-disable jsdoc/require-jsdoc */
export default interface HotReloadingMessages {
reloadExtension: () => void;
}

View File

@@ -1,15 +0,0 @@
type TabInfo = {
url: string;
title: string;
};
interface TabInfoMessages {
/**
* Gets the info for the tab receiving the message
*
* @returns The info for the tab receiving the message
*/
getTabInfo: () => TabInfo;
}
export default TabInfoMessages;

View File

@@ -0,0 +1,4 @@
/**
* This is a type with all the message definitions that can be sent TO specific tabs
*/
export default interface TAB_MESSAGES {}

View File

@@ -6,25 +6,25 @@ import type { Course } from '@shared/types/Course';
export interface UserScheduleMessages {
/**
* Add a course to a schedule
* @param data the schedule id and course to add
* @param data the schedule name and course to add
*/
addCourse: (data: { scheduleId: string; course: Course }) => void;
addCourse: (data: { scheduleName: string; course: Course }) => void;
/**
* Remove a course from a schedule
* @param data the schedule id and course to remove
* @param data the schedule name and course to remove
*/
removeCourse: (data: { scheduleId: string; course: Course }) => void;
removeCourse: (data: { scheduleName: string; course: Course }) => void;
/**
* Clears all courses from a schedule
* @param data the id of the schedule to clear
* @param data the name of the schedule to clear
*/
clearCourses: (data: { scheduleId: string }) => void;
clearCourses: (data: { scheduleName: string }) => void;
/**
* Switches the active schedule to the one specified
* @param data the id of the schedule to switch to
* @param data the name of the schedule to switch to
*/
switchSchedule: (data: { scheduleId: string }) => void;
switchSchedule: (data: { scheduleName: string }) => void;
/**
* Creates a new schedule with the specified name
@@ -34,14 +34,14 @@ export interface UserScheduleMessages {
createSchedule: (data: { scheduleName: string }) => string | undefined;
/**
* Deletes a schedule with the specified name
* @param data the id of the schedule to delete
* @param data the name of the schedule to delete
* @returns undefined if successful, otherwise an error message
*/
deleteSchedule: (data: { scheduleId: string }) => string | undefined;
deleteSchedule: (data: { scheduleName: string }) => string | undefined;
/**
* Renames a schedule with the specified name
* @param data the id of the schedule to rename and the new name
* @param data the name of the schedule to rename and the new name
* @returns undefined if successful, otherwise an error message
*/
renameSchedule: (data: { scheduleId: string; newName: string }) => string | undefined;
renameSchedule: (data: { scheduleName: string; newName: string }) => string | undefined;
}

View File

@@ -1,25 +1,14 @@
import { createMessenger } from 'chrome-extension-toolkit';
import type BrowserActionMessages from './BrowserActionMessages';
import type { CalendarBackgroundMessages, CalendarTabMessages } from './CalendarMessages';
import type CESMessage from './CESMessage';
import type TabInfoMessages from './TabInfoMessages';
import type TabManagementMessages from './TabManagementMessages';
import type TAB_MESSAGES from './TabMessages';
import type { UserScheduleMessages } from './UserScheduleMessages';
/**
* This is a type with all the message definitions that can be sent TO the background script
*/
export type BACKGROUND_MESSAGES = BrowserActionMessages &
TabManagementMessages &
UserScheduleMessages &
CESMessage &
CalendarBackgroundMessages;
/**
* This is a type with all the message definitions that can be sent TO specific tabs
*/
export type TAB_MESSAGES = CalendarTabMessages & TabInfoMessages;
export type BACKGROUND_MESSAGES = BrowserActionMessages & TabManagementMessages & UserScheduleMessages;
/**
* A utility object that can be used to send type-safe messages to the background script
@@ -29,4 +18,4 @@ export const background = createMessenger<BACKGROUND_MESSAGES>('background');
/**
* A utility object that can be used to send type-safe messages to specific tabs
*/
export const tabs = createMessenger<TAB_MESSAGES>('foreground');
export const tabs = createMessenger<TAB_MESSAGES>('tab');

View File

@@ -1,8 +1,6 @@
import { UserSchedule } from '@shared/types/UserSchedule';
import { createLocalStore, debugStore } from 'chrome-extension-toolkit';
import { generateRandomId } from '../util/random';
interface IUserScheduleStore {
schedules: UserSchedule[];
activeIndex: number;
@@ -15,10 +13,8 @@ export const UserScheduleStore = createLocalStore<IUserScheduleStore>({
schedules: [
new UserSchedule({
courses: [],
id: generateRandomId(),
name: 'Schedule 1',
hours: 0,
updatedAt: Date.now(),
}),
],
activeIndex: 0,

View File

@@ -1,27 +0,0 @@
/**
* Represents a hexadecimal color value.
*/
export type HexColor = `#${string}`;
/**
* Checks if a string is a valid hexadecimal color value.
*
* @param color - The color string to check.
* @returns A boolean indicating if the color is a valid hexadecimal color value.
*/
export const isHexColor = (color: string): color is HexColor => /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color);
/**
* Represents an RGB color value.
*/
export type RGB = [r: number, g: number, b: number];
/**
* Represents a linear sRGB color value.
*/
export type sRGB = [r: number, g: number, b: number];
/**
* Represents a Lab color value.
*/
export type Lab = [l: number, a: number, b: number];

View File

@@ -1,10 +1,8 @@
import { getCourseColors } from '@shared/util/colors';
import type { Serialized } from 'chrome-extension-toolkit';
import type { CourseMeeting } from './CourseMeeting';
import { CourseSchedule } from './CourseSchedule';
import Instructor from './Instructor';
import type { CourseColors } from './ThemeColors';
/**
* Whether the class is taught online, in person, or a hybrid of the two
@@ -43,51 +41,43 @@ export type Semester = {
*/
export class Course {
/** Every course has a uniqueId within UT's registrar system corresponding to each course section */
uniqueId!: number;
uniqueId: number;
/** This is the course number for a course, i.e CS 314 would be 314, MAL 306H would be 306H */
number!: string;
number: string;
/** The full name of the course, i.e. CS 314 Data Structures and Algorithms */
fullName!: string;
fullName: string;
/** Just the english name for a course, without the number and department */
courseName!: string;
courseName: string;
/** The unique identifier for which department that a course belongs to, i.e. CS, MAL, etc. */
department!: string;
department: string;
/** The number of credits that a course is worth */
creditHours!: number;
creditHours: number;
/** Is the course open, closed, waitlisted, or cancelled? */
status!: StatusType;
status: StatusType;
/** all the people that are teaching this course, and some metadata about their names */
instructors: Instructor[];
/** Some courses at UT are reserved for certain groups of people or people within a certain major, which makes it difficult for people outside of that group to register for the course. */
isReserved!: boolean;
isReserved: boolean;
/** The description of the course as an array of "lines". This will include important information as well as a short summary of the topics covered */
description?: string[];
/** The schedule for the course, which includes the days of the week that the course is taught, the time that the course is taught, and the location that the course is taught */
schedule: CourseSchedule;
/** the link to the course details page for this course */
url!: string;
url: string;
/** the link to the registration page for this course, for easy access when registering */
registerURL?: string;
/** At UT, some courses have certain "flags" which aid in graduation */
flags!: string[];
flags: string[];
/** How is the class being taught (online, hybrid, in person, etc) */
instructionMode!: InstructionMode;
instructionMode: InstructionMode;
/** Which semester is the course from */
semester!: Semester;
/** Unix timestamp of when the course was last scraped */
scrapedAt!: number;
/** The colors of the course when displayed */
colors: CourseColors;
semester: Semester;
constructor(course: Serialized<Course>) {
Object.assign(this, course);
this.schedule = new CourseSchedule(course.schedule);
this.instructors = course.instructors.map(i => new Instructor(i));
if (!course.scrapedAt) {
this.scrapedAt = Date.now();
}
this.colors = course.colors ? structuredClone(course.colors) : getCourseColors('emerald', 500);
}
/**

View File

@@ -30,15 +30,11 @@ export type Location = {
*/
export class CourseMeeting {
/** The day of the week that the course is taught */
days!: Day[];
/** NOTE: Times starting and after 12 PM have an additional 720 minutes (12 hrs) added to them
* The start time of the course, in minutes since midnight
* */
startTime!: number;
/** NOTE: Times starting and after 12 PM have an additional 720 minutes (12 hrs) added to them
* The end time of the course, in minutes since midnight
* */
endTime!: number;
days: Day[];
/** The start time of the course, in minutes since midnight */
startTime: number;
/** The end time of the course, in minutes since midnight */
endTime: number;
/** The location that the course is taught */
location?: Location;
@@ -108,7 +104,7 @@ export class CourseMeeting {
}
startTimeString += startMinute === 0 ? ':00' : `:${startMinute}`;
startTimeString += startHour >= 12 ? ' p.m.' : ' a.m.';
startTimeString += startHour >= 12 ? 'pm' : 'am';
if (endHour === 0) {
endTimeString = '12';
@@ -118,7 +114,7 @@ export class CourseMeeting {
endTimeString = `${endHour}`;
}
endTimeString += endMinute === 0 ? ':00' : `:${endMinute}`;
endTimeString += endHour >= 12 ? ' p.m.' : ' a.m.';
endTimeString += endHour >= 12 ? 'pm' : 'am';
if (options.capitalize) {
startTimeString = startTimeString.toUpperCase();

View File

@@ -10,12 +10,10 @@ export class CourseSchedule {
meetings: CourseMeeting[] = [];
constructor(courseSchedule?: Serialized<CourseSchedule>) {
if (!courseSchedule || courseSchedule.meetings === undefined) {
this.meetings = [];
if (!courseSchedule) {
return;
}
this.meetings = courseSchedule.meetings.map(meeting => new CourseMeeting(meeting));
Object.assign(this, courseSchedule);
}
/**
@@ -38,7 +36,7 @@ export class CourseSchedule {
if (char === 'S' && nextChar === 'U') {
day += nextChar;
}
return DAY_MAP[day as keyof typeof DAY_MAP];
return DAY_MAP[day];
})
.filter(Boolean) as Day[];
@@ -46,37 +44,28 @@ export class CourseSchedule {
.replaceAll('.', '')
.split('-')
.map(time => {
const [rawHour, rest] = time.split(':');
const [rawMinute, ampm] = rest?.split(' ') ?? ['', ''];
const hour = (rawHour === '12' ? 0 : Number(rawHour)) + (ampm === 'pm' ? 12 : 0);
const minute = Number(rawMinute);
const [hour, rest] = time.split(':');
const [minute, ampm] = rest.split(' ');
return hour * 60 + minute;
if (ampm === 'pm') {
return Number(hour) * 60 + Number(minute) + 12 * 60;
}
return Number(hour) * 60 + Number(minute);
});
const location = locLine.split(' ').filter(Boolean);
if (startTime === undefined || endTime === undefined) {
throw new Error('Failed to parse time');
}
if (startTime >= endTime) {
throw new Error('Start time must be before end time');
}
if (location === undefined) {
throw new Error('Failed to parse location');
}
return new CourseMeeting({
days,
startTime,
endTime,
location: {
building: location[0] ?? '',
room: location[1] ?? '',
},
} satisfies Serialized<CourseMeeting>);
location: location.length
? {
building: location[0],
room: location[1],
}
: undefined,
});
} catch (e) {
throw new Error(`Failed to parse schedule: ${dayLine} ${timeLine} ${locLine}`);
}

View File

@@ -1,7 +1,10 @@
import { PointOptionsObject } from 'highcharts';
import { Semester } from './Course';
/**
* Each of the possible letter grades that can be given in a course
*/
export type LetterGrade = 'A' | 'A-' | 'B' | 'B+' | 'B-' | 'C' | 'C+' | 'C-' | 'D' | 'D+' | 'D-' | 'F' | 'Other';
export type LetterGrade = 'A' | 'A-' | 'B' | 'B+' | 'B-' | 'C' | 'C+' | 'C-' | 'D' | 'D+' | 'D-' | 'F';
/**
* A distribution of grades for a course,
@@ -15,26 +18,23 @@ export type Distribution = {
* This is a object-ified version of a row in the SQL table that is used to store the distribution data.
*/
export type CourseSQLRow = {
Semester: string;
Section: number;
Department: string;
Department_Code: string;
Course_Number: string;
Course_Title: string;
Course_Full_Title: string;
Instructor_First: string | null;
Instructor_Last: string | null;
A: number;
A_Minus: number;
B_Plus: number;
B: number;
B_Minus: number;
C_Plus: number;
C: number;
C_Minus: number;
D_Plus: number;
D: number;
D_Minus: number;
F: number;
Other: number;
sem?: string;
prof?: string;
dept?: string;
course_nbr?: string;
course_name?: string;
a1?: number;
a2?: number;
a3?: number;
b1?: number;
b2?: number;
b3?: number;
c1?: number;
c2?: number;
c3?: number;
d1?: number;
d2?: number;
d3?: number;
f?: number;
semesters?: string;
};

View File

@@ -1,13 +1,14 @@
import { capitalize } from '@shared/util/string';
import type { Serialized } from 'chrome-extension-toolkit';
import { capitalize } from '../util/string';
/**
* A type representing an instructor for a course (who teaches it)
*/
export default class Instructor {
fullName?: string;
firstName?: string;
lastName?: string;
fullName: string;
firstName: string;
lastName: string;
middleInitial?: string;
constructor(instructor: Serialized<Instructor>) {
@@ -53,16 +54,16 @@ export default class Instructor {
return capitalize(str);
};
if (format === 'abbr' && firstName && lastName && firstName[0]) {
if (format === 'abbr') {
return `${process(firstName[0])}. ${process(lastName)}`;
}
if (format === 'full_name' && fullName) {
if (format === 'full_name') {
return process(fullName);
}
if (format === 'first_last' && firstName && lastName) {
if (format === 'first_last') {
return `${process(firstName)} ${process(lastName)}`;
}
if (format === 'last' && lastName) {
if (format === 'last') {
return process(lastName);
}

View File

@@ -1,17 +0,0 @@
const MIMEType = {
JSON: 'application/json',
HTML: 'text/html',
TEXT: 'text/plain',
FORM: 'application/x-www-form-urlencoded',
MULTIPART: 'multipart/form-data',
PDF: 'application/pdf',
IMAGE: 'image/*',
AUDIO: 'audio/*',
VIDEO: 'video/*',
CALENDAR: 'text/calendar',
ANY: '*/*',
} as const satisfies Record<string, string>;
export type MIMETypeKey = keyof typeof MIMEType;
export default MIMEType;

View File

@@ -1,80 +0,0 @@
import type { theme } from 'unocss/preset-mini';
import type { HexColor } from './Color';
export const colors = {
ut: {
burntorange: '#BF5700',
black: '#333F48',
orange: '#F8971F',
yellow: '#FFD600',
lightgreen: '#A6CD57',
green: '#579D42',
teal: '#00A9B7',
blue: '#005F86',
gray: '#9CADB7',
offwhite: '#D6D2C4',
concrete: '#95A5A6',
red: '#B91C1C', // Not sure if this should be here, but it's used for remove course, and add course is ut-green
},
theme: {
red: '#BF0000',
black: '#1A2024',
},
} as const satisfies Record<string, Record<string, string>>;
export const extendedColors = {
...colors,
gradeDistribution: {
a: '#22C55E',
aminus: '#A3E635',
bplus: '#84CC16',
b: '#FDE047',
bminus: '#FACC15',
cplus: '#F59E0B',
c: '#FB923C',
cminus: '#F97316',
dplus: '#EF4444',
d: '#DC2626',
dminus: '#B91C1C',
f: '#B91C1C',
other: '#6B7280',
},
} as const;
type NestedKeys<T> = {
[K in keyof T]: T[K] extends Record<string, unknown> ? `${string & K}-${string & keyof T[K]}` : never;
}[keyof T];
/**
* A union of all colors in the theme
*/
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.
*/
export type TWColorway = {
[K in keyof typeof theme.colors]: (typeof theme.colors)[K] extends Record<string, unknown> ? K : never;
}[keyof typeof theme.colors];
export type TWIndex = keyof (typeof theme.colors)[TWColorway];
/**
* Represents the colors for a course.
*/
export interface CourseColors {
primaryColor: HexColor;
secondaryColor: HexColor;
}
/**
* Adjusted colorway indexes for better *quality*
*/
export const colorwayIndexes = {
yellow: 300,
amber: 400,
emerald: 400,
lime: 400,
orange: 400,
sky: 600,
} as const satisfies Record<string, number>;

View File

@@ -1,6 +1,5 @@
import type { Serialized } from 'chrome-extension-toolkit';
import { generateRandomId } from '../util/random';
import { Course } from './Course';
/**
@@ -8,18 +7,16 @@ import { Course } from './Course';
*/
export class UserSchedule {
courses: Course[];
id: string;
name: string;
hours: number;
/** Unix timestamp of when the schedule was last updated */
updatedAt: number;
constructor(schedule: Serialized<UserSchedule>) {
this.courses = schedule.courses.map(c => new Course(c));
this.id = schedule.id ?? generateRandomId();
this.name = schedule.name;
this.hours = this.courses.reduce((acc, c) => acc + c.creditHours, 0);
this.updatedAt = schedule.updatedAt ?? 0;
this.hours = 0;
for (const course of this.courses) {
this.hours += course.creditHours;
}
}
containsCourse(course: Course): boolean {

View File

@@ -1,252 +1,58 @@
import type { Serialized } from 'chrome-extension-toolkit';
import { theme } from 'unocss/preset-mini';
import type { HexColor, Lab, RGB, sRGB } from '../types/Color';
import { isHexColor } from '../types/Color';
import type { Course } from '../types/Course';
import type { CourseColors, TWColorway, TWIndex } from '../types/ThemeColors';
import { colorwayIndexes } from '../types/ThemeColors';
import type { UserSchedule } from '../types/UserSchedule';
/**
* Represents the colors for a course.
*/
export interface CourseColors {
primaryColor: string;
secondaryColor: string;
}
/**
* Converts a hexadecimal color value to RGB format. (adapted from https://stackoverflow.com/a/5624139/8022866)
* Calculates the luminance of a given hexadecimal color.
*
* @param hex - The hexadecimal color value.
* @returns An array containing the RGB values.
* @returns The luminance value between 0 and 1.
*/
export function hexToRGB(hex: HexColor): RGB | undefined {
// Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
let shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
const parsedHex = hex.replace(shorthandRegex, (m, r, g, b) => r + r + g + g + b + b);
export function getLuminance(hex: string): number {
let r = parseInt(hex.substring(1, 3), 16);
let g = parseInt(hex.substring(3, 5), 16);
let b = parseInt(hex.substring(5, 7), 16);
let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(parsedHex);
[r, g, b] = [r, g, b].map(color => {
let c = color / 255;
if (!result || !(result.length > 3)) return undefined;
c = c > 0.03928 ? ((c + 0.055) / 1.055) ** 2.4 : (c /= 12.92);
return [parseInt(result[1]!, 16), parseInt(result[2]!, 16), parseInt(result[3]!, 16)];
return c;
});
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
export const useableColorways = Object.keys(theme.colors)
// check that the color is a colorway (is an object)
.filter(color => typeof theme.colors[color as keyof typeof theme.colors] === 'object')
.slice(0, 17) as TWColorway[];
// calculates contrast ratio between two hex strings
function contrastRatioPair(hex1: string, hex2: string) {
const lum1 = getLuminance(hex1);
const lum2 = getLuminance(hex2);
/**
* Generate a Tailwind classname for the font color based on the background color
* @param bgColor the hex color of the background
*/
export function pickFontColor(bgColor: HexColor): 'text-white' | 'text-black' | 'text-theme-black' {
const coefficients = [0.2126729, 0.7151522, 0.072175] as const;
const flipYs = 0.342; // based on APCA™ 0.98G middle contrast BG color
const trc = 2.4; // 2.4 exponent for emulating actual monitor perception
const rgb = hexToRGB(bgColor);
if (!rgb) throw new Error('bgColor: Invalid hex.');
// coefficients and rgb are both 3 elements long, so this is safe
let Ys = rgb.reduce((acc, c, i) => acc + (c / 255.0) ** trc * coefficients[i]!, 0);
if (Ys < flipYs) {
return 'text-white';
}
return Ys < 0.365 ? 'text-black' : 'text-theme-black';
return (Math.max(lum1, lum2) + 0.05) / (Math.min(lum1, lum2) + 0.05);
}
/**
* Get primary and secondary colors from a Tailwind colorway
* @param colorway the Tailwind colorway ex. "emerald"
* Generate a tailwind classname for the font color based on the background color
* @param bgColor the tailwind classname for background ex. "bg-emerald-500"
*/
export function getCourseColors(colorway: TWColorway, index?: number, offset: number = 300): CourseColors {
if (index === undefined) {
// eslint-disable-next-line no-param-reassign
index = colorway in colorwayIndexes ? colorwayIndexes[colorway as keyof typeof colorwayIndexes] : 500;
}
export function pickFontColor(bgColor: string): 'text-white' | 'text-black' {
return contrastRatioPair(bgColor, '#606060') > contrastRatioPair(bgColor, '#ffffff') ? 'text-black' : 'text-white';
}
/**
* Get primary and secondary colors from a tailwind colorway
* @param colorway the tailwind colorway ex. "emerald"
*/
export function getCourseColors(colorway: keyof typeof theme.colors, index = 600, offset = 200): CourseColors {
return {
primaryColor: theme.colors[colorway][index as TWIndex] as HexColor,
secondaryColor: theme.colors[colorway][(index + offset) as TWIndex] as HexColor,
primaryColor: theme.colors[colorway][index] as string,
secondaryColor: theme.colors[colorway][index + offset] as string,
};
}
/**
* Get the Tailwind colorway from a given color.
*
* @param color - The hexadecimal color value.
* @returns The Tailwind colorway.
*/
export function getColorwayFromColor(color: HexColor): TWColorway {
for (const colorway of useableColorways) {
if (Object.values(theme.colors[colorway]).includes(color)) {
return colorway as TWColorway;
}
}
// not a direct match, get the closest color
let closestColor = '';
let closestDistance = Infinity;
for (const colorway of useableColorways) {
for (const [shade, shadeColor] of Object.entries(theme.colors[colorway])) {
// type guard
if (!isHexColor(shadeColor)) {
continue;
}
const shadeColorRGB = hexToRGB(shadeColor);
if (!shadeColorRGB) {
continue;
}
const distance = oklabDistance(rgbToOKlab(shadeColorRGB), rgbToOKlab(shadeColorRGB));
if (distance < closestDistance) {
closestDistance = distance;
closestColor = shade;
}
}
}
// type guard
if (!isHexColor(closestColor)) {
throw new Error("closestColor isn't a valid hex color");
}
return getColorwayFromColor(closestColor);
}
/**
* Get next unused color in a tailwind colorway for a given schedule
* @param schedule the schedule which the course is in
* @param course the course to get the color for
*/
export function getUnusedColor(
schedule: Serialized<UserSchedule>,
course: Course,
index?: number,
offset?: number
): CourseColors {
// strategy: First, check if any of the course's in schedule have the same department as the current course,
// if so, use a colorway near the color of that course if possible.
// Otherwise, find a colorway that is not used in the schedule and at least two hues away from any other color in the schedule.
// wrapping helper functions
function getPreviousColorway(colorway: TWColorway): TWColorway {
const colorwayIndex = useableColorways.indexOf(colorway);
return useableColorways[(colorwayIndex - 1 + useableColorways.length) % useableColorways.length] as TWColorway;
}
function getNextColorway(colorway: TWColorway): TWColorway {
const colorwayIndex = useableColorways.indexOf(colorway);
return useableColorways[(colorwayIndex + 1) % useableColorways.length] as TWColorway;
}
const scheduleCourses = schedule.courses.map(c => ({
...c,
colorway: getColorwayFromColor(c.colors.primaryColor),
}));
const usedColorways = new Set(scheduleCourses.map(c => c.colorway));
const availableColorways = new Set(useableColorways.filter(c => !usedColorways.has(c)));
if (availableColorways.size > 0) {
let sameDepartment = scheduleCourses.filter(c => c.department === course.department);
sameDepartment.sort((a, b) => {
const aIndex = useableColorways.indexOf(a.colorway);
const bIndex = useableColorways.indexOf(b.colorway);
return aIndex - bIndex;
});
if (sameDepartment.length > 0) {
// check to see if any adjacent colorways are available
const centerCourse = sameDepartment[Math.floor(Math.random() * sameDepartment.length)]!;
let nextColorway = getNextColorway(centerCourse.colorway);
let prevColorway = getPreviousColorway(centerCourse.colorway);
// eslint-disable-next-line no-constant-condition
while (true) {
if (availableColorways.has(nextColorway)) {
return getCourseColors(nextColorway, index, offset);
}
if (availableColorways.has(prevColorway)) {
return getCourseColors(prevColorway, index, offset);
}
nextColorway = getNextColorway(nextColorway);
prevColorway = getPreviousColorway(prevColorway);
}
}
const shortenedColorways = new Set(availableColorways);
// no courses in the same department, restrict colorways to those which are at least 2 indexes away from any used colors
for (const colorway of usedColorways) {
shortenedColorways.delete(getPreviousColorway(colorway));
shortenedColorways.delete(getNextColorway(colorway));
}
if (shortenedColorways.size > 0) {
// TODO: make this go by 3's to leave future spaces open
const randomColorway = Array.from(shortenedColorways)[Math.floor(Math.random() * shortenedColorways.size)]!;
return getCourseColors(randomColorway, index, offset);
}
// no colorways are at least 2 indexes away from any used colors, just get a random colorway
const randomColorway: TWColorway | undefined =
Array.from(availableColorways)[Math.floor(Math.random() * availableColorways.size)];
if (!randomColorway) {
throw new Error('randomColorway is undefined');
}
return getCourseColors(randomColorway, index, offset);
}
// TODO: get just a random color idk
return getCourseColors('emerald', index, offset);
}
// 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 srgbTransferFunctionInv(a: number): number {
return a > 0.04045 ? ((a + 0.055) / 1.055) ** 2.4 : a / 12.92;
}
function rgbToSrgb(rgb: RGB): sRGB {
return rgb.map(c => srgbTransferFunctionInv(c / 255)) as sRGB;
}
/**
* Convert an RGB color to the OKLab color space
* @param rgb the RGB color
* @returns the color in the OKLab color space
*/
function srgbToOKlab([r, g, b]: sRGB): Lab {
let l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b;
let m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b;
let s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b;
let lc = Math.cbrt(l);
let mc = Math.cbrt(m);
let sc = Math.cbrt(s);
return [
0.2104542553 * lc + 0.793617785 * mc - 0.0040720468 * sc,
1.9779984951 * lc - 2.428592205 * mc + 0.4505937099 * sc,
0.0259040371 * lc + 0.7827717662 * mc - 0.808675766 * sc,
];
}
function rgbToOKlab(rgb: RGB): Lab {
return srgbToOKlab(rgbToSrgb(rgb));
}
/**
* Calculate the distance between two colors in the OKLab color space
*/
function oklabDistance([l1, a1, b1]: Lab, [l2, a2, b2]: Lab): number {
return Math.sqrt((l2 - l1) ** 2 + (a2 - a1) ** 2 + (b2 - b1) ** 2);
}

View File

@@ -1,30 +0,0 @@
import type { MIMETypeKey } from '../types/MIMEType';
import MIMEType from '../types/MIMEType';
/**
* Downloads a blob by creating a temporary URL and triggering a download.
* @param blobPart The blob data to be downloaded.
* @param type The MIME type of the blob.
* @param fileName The name of the file to be downloaded.
* @returns A promise that resolves when the download is successful, or rejects with an error if the download fails.
*/
export function downloadBlob(blobPart: BlobPart, type: MIMETypeKey, fileName: string): Promise<void> {
return new Promise((resolve, reject) => {
const blob: Blob = new Blob([blobPart], { type: MIMEType[type] });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
link.addEventListener('click', () => {
resolve();
});
link.addEventListener('error', () => {
URL.revokeObjectURL(url);
reject(new Error('Download failed'));
});
link.click();
URL.revokeObjectURL(url);
});
}

View File

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

View File

@@ -10,9 +10,9 @@ import CancelledIcon from '~icons/material-symbols/warning';
/**
* Get Icon component based on status
* @param props.status status
* @returns the icon component
* @returns React.ReactElement - the icon component
*/
export function StatusIcon(props: SVGProps<SVGSVGElement> & { status: StatusType }): JSX.Element | null {
export function StatusIcon(props: SVGProps<SVGSVGElement> & { status: StatusType }): React.ReactElement {
const { status, ...rest } = props;
switch (props.status) {
@@ -23,6 +23,5 @@ export function StatusIcon(props: SVGProps<SVGSVGElement> & { status: StatusType
case Status.CANCELLED:
return <CancelledIcon {...rest} />;
default:
return null;
}
}

View File

@@ -1,9 +1,25 @@
import { customAlphabet } from 'nanoid';
/**
* Generate a random ID
*
* @returns string of size 10 made up of random numbers and letters
* @param length the length of the ID to generate
* @example "cdtl9l88pj"
*/
export function generateRandomId(length: number = 10): string {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < length; i += 1) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
/**
* Generate secure URL-friendly unique ID.
*
* @param size Size of the ID. The default size is 12.
* @returns A random string.
* Generate a random number between min and max
* @param min the minimum number
* @param max the maximum number
* @returns a random number between min and max
*/
export const generateRandomId = customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 12);
export function rangeRandom(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1)) + min;
}

View File

@@ -1,3 +0,0 @@
import { getCourseColors, useableColorways } from '@shared/util/colors';
export const tailwindColorways = useableColorways.map(color => getCourseColors(color));

View File

@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import { getLuminance } from '../colors';
describe('getLuminance', () => {
it('should return the correct luminance value for a given hex color', () => {
// Test case 1: Hex color #FFFFFF (white)
expect(getLuminance('#FFFFFF')).toBeCloseTo(1);
// Test case 2: Hex color #000000 (black)
expect(getLuminance('#000000')).toBeCloseTo(0);
// Test case 3: Hex color #FF0000 (red)
expect(getLuminance('#FF0000')).toBeCloseTo(0.2126);
// Test case 4: Hex color #00FF00 (green)
expect(getLuminance('#00FF00')).toBeCloseTo(0.7152);
// Test case 5: Hex color #0000FF (blue)
expect(getLuminance('#0000FF')).toBeCloseTo(0.0722);
});
});

View File

@@ -1,6 +1,7 @@
import { generateRandomId } from '@shared/util/random';
import { describe, expect, it } from 'vitest';
import { generateRandomId } from '../random';
describe('generateRandomId', () => {
it('should generate a random ID with the specified length', () => {
// Test case 1: Length 5

Some files were not shown because too many files have changed in this diff Show More