Compare commits

...

196 Commits

Author SHA1 Message Date
Casey Charleston
2c4dd36f27 feat: popup misaligned but clickable on calendar grid 2024-02-23 09:52:53 -06:00
Casey Charleston
5daccc8349 fix: injected course popup storybook 2024-02-21 23:51:12 -06:00
Casey Charleston
e4ce051086 Merging hackathon into hackathon 2024-02-21 23:28:32 -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
Casey Charleston
51b6799d73 dropdown and calendar schedules storybook 2024-02-17 18:35:17 -06:00
DhruvArora-03
f214ed7c01 Merge remote-tracking branch 'origin/hackathon' into feat-conflict-row 2024-02-17 18:21:39 -06:00
Casey Charleston
141f5cc70e merging 2024-02-17 18:00:06 -06:00
Casey Charleston
6883a0bc8d packages 2024-02-17 17:59:05 -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
72b7a9d7b1 Calendar Components 3rd Attempt at Merging 2024-01-30 21:17:19 -06:00
103 changed files with 9353 additions and 2040 deletions

108
.eslintrc
View File

@@ -4,12 +4,9 @@
"browser": true,
"es6": true,
"node": true,
"webextensions": true
"webextensions": true,
},
"ignorePatterns": [
"*.html",
"tsconfig.json"
],
"ignorePatterns": ["*.html", "tsconfig.json"],
"extends": [
"eslint:recommended",
"plugin:react/recommended",
@@ -21,18 +18,14 @@
"@unocss",
"prettier",
],
"plugins": [
"import",
"jsdoc",
"react-prefer-function-component"
],
"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
"JSX": true,
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
@@ -42,37 +35,35 @@
"ecmaFeatures": {
"jsx": true,
"modules": true,
"experimentalObjectRestSpread": true
}
"experimentalObjectRestSpread": true,
},
},
"settings": {
"react": {
"version": "detect"
"version": "detect",
},
"jsdoc": {
"mode": "typescript"
"mode": "typescript",
},
"import/parsers": {
"@typescript-eslint/parser": [
".ts",
".tsx"
]
"@typescript-eslint/parser": [".ts", ".tsx"],
},
"import/resolver": {
"typescript": {
"alwaysTryTypes": true,
"project": "./tsconfig.json"
}
}
"project": "./tsconfig.json",
},
},
},
"rules": {
"prefer-const": [
"off",
{
"destructuring": "any",
"ignoreReadBeforeAssign": false
}
"ignoreReadBeforeAssign": false,
},
],
"no-plusplus": "off",
"no-inner-declarations": "off",
"sort-imports": "off",
"no-case-declarations": "off",
@@ -82,20 +73,16 @@
"no-undef": "off",
"no-return-await": "off",
"@typescript-eslint/return-await": "off",
"@typescript-eslint/no-shadow": [
"off"
],
"@typescript-eslint/no-use-before-define": [
"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
}
"props": false,
},
],
"no-console": "off",
"consistent-return": "off",
@@ -109,8 +96,8 @@
"error",
{
"before": true,
"after": true
}
"after": true,
},
],
"no-continue": "off",
"space-before-blocks": [
@@ -118,24 +105,22 @@
{
"functions": "always",
"keywords": "always",
"classes": "always"
}
"classes": "always",
},
],
"react/jsx-filename-extension": [
1,
{
"extensions": [
".tsx"
]
}
"extensions": [".tsx"],
},
],
"react/no-deprecated": "warn",
"react/prop-types": "off",
"react-prefer-function-component/react-prefer-function-component": [
"warn",
{
"allowComponentDidCatch": false
}
"allowComponentDidCatch": false,
},
],
"react/function-component-definition": "off",
"react/button-has-type": "off",
@@ -153,7 +138,7 @@
"ArrowFunctionExpression": true,
"ClassDeclaration": true,
"ClassExpression": true,
"FunctionExpression": true
"FunctionExpression": true,
},
"contexts": [
"MethodDefinition:not([key.name=\"componentDidMount\"]):not([key.name=\"render\"])",
@@ -168,9 +153,9 @@
"TSInterfaceDeclaration",
"TSMethodSignature",
"TSModuleDeclaration",
"TSTypeAliasDeclaration"
]
}
"TSTypeAliasDeclaration",
],
},
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "warn",
@@ -185,31 +170,36 @@
{
"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!"
"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!"
"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."
}
]
}
"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"
]
}
"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",
},
}

43
.github/workflows/best-practices.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: Best Practices
on: [push, pull_request]
jobs:
lint:
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: 8
- name: Install dependencies
run: pnpm install
- name: Run ESLint
run: pnpm run lint
format:
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: 8
- name: Install dependencies
run: pnpm install
- name: Run Prettier
run: pnpm run prettier

View File

@@ -1,26 +1,26 @@
name: "Chromatic"
name: 'Chromatic'
on: [push, pull_request_target]
on: [push, pull_request]
jobs:
chromatic:
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: 8
chromatic:
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: 8
- name: Install dependencies
run: pnpm install
- name: Install dependencies
run: pnpm install
- name: Publish to Chromatic
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitZeroOnChanges: true
autoAcceptChanges: "main"
- name: Publish to Chromatic
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
exitZeroOnChanges: true
autoAcceptChanges: 'main'

24
.github/workflows/tests.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Tests
on: [push, pull_request]
jobs:
test:
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: 8
- name: Install dependencies
run: pnpm install
- name: Run tests
run: pnpm test

1
.husky/commit-msg Normal file
View File

@@ -0,0 +1 @@
npx --no -- commitlint --edit $1

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,5 +1,8 @@
{
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
@@ -34,4 +37,7 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"typescript.tsdk": "node_modules/typescript/lib",
"tailwindCSS.includeLanguages": {
"plaintext": "javascript"
}
}

View File

@@ -2,17 +2,29 @@
## Built Using
- React 18
- TypeScript
- Vite 5
- ESLint
- Prettier
- Semantic-Release
- Custom Messaging & Storage Wrappers
- React 18
- TypeScript
- Vite 5
- ESLint
- Prettier
- Semantic-Release
- Custom Messaging & Storage Wrappers
## Getting Started
1. Clone this repo
2. Run `pnpm install` to install and patch all the required dependencies
3. Run `pnpm run dev` to start the development server
4. Run `pnpm build` to build the extension for production
- If you want to run the development build:
- Run `pnpm run dev`
- If you want to build the extension for production:
- Run `pnpm build`
You may have to rename the `__uno.css.js` to `uno.css.js` in dist
Go to chrome://extensions, ensure you have "Developer Mode" enabled, and click 'Load unpacked'
Navigate to the 'dist' folder and click 'select' to import the extension

123
commitlint.config.ts Normal file
View File

@@ -0,0 +1,123 @@
import { RuleConfigCondition, RuleConfigSeverity, TargetCaseType } from '@commitlint/types';
export default {
parserPreset: 'conventional-changelog-conventionalcommits',
rules: {
'body-leading-blank': [RuleConfigSeverity.Warning, 'always'] as const,
'body-max-line-length': [RuleConfigSeverity.Error, 'always', 100] as const,
'footer-leading-blank': [RuleConfigSeverity.Warning, 'always'] as const,
'footer-max-line-length': [RuleConfigSeverity.Error, 'always', 100] as const,
'header-max-length': [RuleConfigSeverity.Error, 'always', 100] as const,
'header-trim': [RuleConfigSeverity.Error, 'always'] as const,
'subject-case': [
RuleConfigSeverity.Error,
'never',
['sentence-case', 'start-case', 'pascal-case', 'upper-case'],
] as [RuleConfigSeverity, RuleConfigCondition, TargetCaseType[]],
'subject-empty': [RuleConfigSeverity.Error, 'never'] as const,
'subject-full-stop': [RuleConfigSeverity.Error, 'never', '.'] as const,
'type-case': [RuleConfigSeverity.Error, 'always', 'lower-case'] as const,
'type-empty': [RuleConfigSeverity.Error, 'never'] as const,
'type-enum': [
RuleConfigSeverity.Error,
'always',
['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test'],
] as [RuleConfigSeverity, RuleConfigCondition, string[]],
},
prompt: {
questions: {
type: {
description: "Select the type of change that you're committing",
enum: {
feat: {
description: 'A new feature',
title: 'Features',
emoji: '✨',
},
fix: {
description: 'A bug fix',
title: 'Bug Fixes',
emoji: '🐛',
},
docs: {
description: 'Documentation only changes',
title: 'Documentation',
emoji: '📚',
},
style: {
description:
'Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)',
title: 'Styles',
emoji: '💎',
},
refactor: {
description: 'A code change that neither fixes a bug nor adds a feature',
title: 'Code Refactoring',
emoji: '📦',
},
perf: {
description: 'A code change that improves performance',
title: 'Performance Improvements',
emoji: '🚀',
},
test: {
description: 'Adding missing tests or correcting existing tests',
title: 'Tests',
emoji: '🚨',
},
build: {
description:
'Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)',
title: 'Builds',
emoji: '🛠',
},
ci: {
description:
'Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)',
title: 'Continuous Integrations',
emoji: '⚙️',
},
chore: {
description: "Other changes that don't modify src or test files",
title: 'Chores',
emoji: '♻️',
},
revert: {
description: 'Reverts a previous commit',
title: 'Reverts',
emoji: '🗑',
},
},
},
scope: {
description: 'What is the scope of this change (e.g. component or file name)',
},
subject: {
description: 'Write a short, imperative tense description of the change',
},
body: {
description: 'Provide a longer description of the change',
},
isBreaking: {
description: 'Are there any breaking changes?',
},
breakingBody: {
description:
'A BREAKING CHANGE commit requires a body. Please enter a longer description of the commit itself',
},
breaking: {
description: 'Describe the breaking changes',
},
isIssueAffected: {
description: 'Does this change affect any open issues?',
},
issuesBody: {
description:
'If issues are closed, the commit requires a body. Please enter a longer description of the commit itself',
},
issues: {
description: 'Add issue references (e.g. "fix #123", "re #123".)',
},
},
},
};

3267
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,60 +9,77 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"prettier": "prettier src --check",
"prettier:fix": "prettier src --write",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"lint:fix": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0 --fix",
"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"
"build-storybook": "storybook build",
"prepare": "husky"
},
"dependencies": {
"@headlessui/react": "^1.7.18",
"@headlessui/tailwindcss": "^0.2.0",
"@hello-pangea/dnd": "^16.5.0",
"@types/sql.js": "^1.4.9",
"@vitejs/plugin-react": "^4.2.1",
"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",
"react": "^18.2.0",
"react-devtools-core": "^5.0.0",
"react-dom": "^18.2.0",
"sass": "^1.70.0",
"react-window": "^1.8.10",
"sass": "^1.71.1",
"sql.js": "1.10.2",
"styled-components": "^6.1.8",
"uuid": "^9.0.1"
},
"devDependencies": {
"@commitlint/cli": "^18.6.1",
"@commitlint/config-conventional": "^18.6.2",
"@crxjs/vite-plugin": "2.0.0-beta.21",
"@iconify-json/material-symbols": "^1.1.72",
"@iconify-json/material-symbols": "^1.1.73",
"@storybook/addon-designs": "^7.0.9",
"@storybook/addon-essentials": "^7.6.12",
"@storybook/addon-links": "^7.6.12",
"@storybook/blocks": "^7.6.12",
"@storybook/react": "^7.6.12",
"@storybook/react-vite": "^7.6.12",
"@storybook/test": "^7.6.12",
"@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.260",
"@types/node": "^20.11.16",
"@types/node": "^20.11.19",
"@types/prompts": "^2.4.9",
"@types/react": "^18.2.51",
"@types/react-dom": "^18.2.18",
"@types/semver": "^7.5.6",
"@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.20.0",
"@typescript-eslint/parser": "^6.20.0",
"@unocss/eslint-config": "^0.58.4",
"@unocss/postcss": "^0.58.4",
"@unocss/preset-uno": "^0.58.4",
"@unocss/preset-web-fonts": "^0.58.4",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@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.4",
"@unocss/transformer-variant-group": "^0.58.4",
"@unocss/transformer-directives": "^0.58.5",
"@unocss/transformer-variant-group": "^0.58.5",
"@vitejs/plugin-react-swc": "^3.6.0",
"chromatic": "^10.9.1",
"@vitest/coverage-v8": "^1.3.1",
"@vitest/ui": "^1.3.1",
"chromatic": "^10.9.6",
"cssnano": "^6.0.3",
"cssnano-preset-advanced": "^6.0.3",
"dotenv": "^16.4.1",
"dotenv": "^16.4.5",
"es-module-lexer": "^1.4.1",
"eslint": "^8.56.0",
"eslint-config-airbnb": "^19.0.4",
@@ -71,24 +88,27 @@
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsdoc": "^48.0.4",
"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.6.15",
"husky": "^9.0.11",
"path": "^0.12.7",
"postcss": "^8.4.33",
"prettier": "^3.2.4",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"react-dev-utils": "^12.0.1",
"react-devtools": "^5.0.0",
"storybook": "^7.6.12",
"storybook": "^7.6.17",
"typescript": "^5.3.3",
"unocss": "^0.58.4",
"unplugin-icons": "^0.18.3",
"vite": "^5.0.12",
"vite-plugin-inspect": "^0.8.3"
"unocss": "^0.58.5",
"unplugin-icons": "^0.18.5",
"vite": "^5.1.4",
"vite-plugin-inspect": "^0.8.3",
"vitest": "^1.3.1"
},
"pnpm": {
"patchedDependencies": {

2999
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

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

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -14,6 +14,7 @@ export default async function createSchedule(scheduleName: string): Promise<stri
schedules.push({
name: scheduleName,
courses: [],
hours: 0
});
await UserScheduleStore.set('schedules', schedules);

View File

@@ -1,10 +1,15 @@
import React from 'react';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import { Calendar } from 'src/views/components/calendar/Calendar/Calendar';
/**
* Calendar page
* @returns entire page
*/
export default function CalendarMain() {
return (
<ExtensionRoot>
<div>Calendar Placeholder</div>
<Calendar />
</ExtensionRoot>
);
}

View File

@@ -14,6 +14,7 @@ export const UserScheduleStore = createLocalStore<IUserScheduleStore>({
new UserSchedule({
courses: [],
name: 'Schedule 1',
hours: 0,
}),
],
activeIndex: 0,

View File

@@ -2,6 +2,7 @@ import { Serialized } from 'chrome-extension-toolkit';
/**
* a map of the days of the week that a class is taught, and the corresponding abbreviation
* Don't modify the keys
*/
export const DAY_MAP = {
M: 'Monday',
@@ -14,7 +15,7 @@ export const DAY_MAP = {
} as const;
/** A day of the week that a class is taught */
export type Day = typeof DAY_MAP[keyof typeof DAY_MAP];
export type Day = (typeof DAY_MAP)[keyof typeof DAY_MAP];
/** A physical room that a class is taught in */
export type Location = {

View File

@@ -7,10 +7,15 @@ import { Course } from './Course';
export class UserSchedule {
courses: Course[];
name: string;
hours: number;
constructor(schedule: Serialized<UserSchedule>) {
this.courses = schedule.courses.map(c => new Course(c));
this.name = schedule.name;
this.hours = 0;
for (const course of this.courses) {
this.hours += course.creditHours;
}
}
containsCourse(course: Course): boolean {

View File

@@ -6,7 +6,7 @@ export interface CourseColors {
}
// calculates luminance of a hex string
function getLuminance(hex: string): number {
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);
@@ -42,9 +42,9 @@ export function pickFontColor(bgColor: string): 'text-white' | 'text-black' {
* Get primary and secondary colors from a tailwind colorway
* @param colorway the tailwind colorway ex. "emerald"
*/
export function getCourseColors(colorway: keyof typeof theme.colors): CourseColors {
export function getCourseColors(colorway: keyof typeof theme.colors, index = 600, offset = 200): CourseColors {
return {
primaryColor: theme.colors[colorway][600] as string,
secondaryColor: theme.colors[colorway][800] as string,
primaryColor: theme.colors[colorway][index] as string,
secondaryColor: theme.colors[colorway][index + offset] as string,
};
}

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

@@ -0,0 +1,26 @@
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
expect(generateRandomId(5)).toHaveLength(5);
// Test case 2: Length 10
expect(generateRandomId(10)).toHaveLength(10);
// Test case 3: Length 15
expect(generateRandomId(15)).toHaveLength(15);
});
it('should generate a unique ID each time', () => {
// Generate 100 IDs and check if they are all unique
const ids = new Set<string>();
for (let i = 0; i < 100; i += 1) {
const id = generateRandomId();
expect(ids.has(id)).toBe(false);
ids.add(id);
}
});
});

View File

@@ -0,0 +1,39 @@
import { describe, expect, it } from 'vitest';
import { capitalize } from '../string';
// TODO: Fix `string.ts` and `string.test.ts` to make the tests pass
// `capitalize` is adding an extra space at the end of the word.
describe('capitalize', () => {
it('should capitalize the first letter of each word', () => {
// Debug
const word = 'hello world';
const capitalized = capitalize(word);
console.log(capitalize(word));
console.log(capitalized.length);
console.log(capitalized.split(''));
// Test case 1: Single word
expect(capitalize('hello')).toBe('Hello');
// Test case 2: Multiple words
expect(capitalize('hello world')).toBe('Hello World');
// Test case 3: Words with hyphens
expect(capitalize('hello-world')).toBe('Hello-World');
// Test case 4: Words with hyphens and spaces
expect(capitalize('hello-world test')).toBe('Hello-World Test');
});
it('should not change the capitalization of the remaining letters', () => {
// Test case 1: All lowercase
expect(capitalize('hello')).toBe('Hello');
// Test case 2: All uppercase
expect(capitalize('WORLD')).toBe('WORLD');
// Test case 3: Mixed case
expect(capitalize('HeLLo WoRLd')).toBe('Hello World');
});
});

View File

@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest';
import { sleep } from '../time';
describe('sleep', () => {
it('should resolve after the specified number of milliseconds', async () => {
const start = Date.now();
const milliseconds = 1000;
await sleep(milliseconds);
const end = Date.now();
const elapsed = end - start;
expect(elapsed).toBeGreaterThanOrEqual(milliseconds);
});
});

View File

@@ -0,0 +1,75 @@
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: '#af2e2d',
black: '#1a2024',
},
gradeDistribution: {
a: '#22c55e',
aminus: '#a3e635',
bplus: '#84CC16',
b: '#FDE047',
bminus: '#FACC15',
cplus: '#F59E0B',
c: '#FB923C',
cminus: '#F97316',
dplus: '#EA580C', // TODO (achadaga): copilot generated, get actual color from Isaiah
d: '#DC2626',
dminus: '#B91C1C',
f: '#B91C1C',
},
} as const;
type NestedKeys<T> = {
[K in keyof T]: T[K] extends Record<string, any> ? `${string & K}-${string & keyof T[K]}` : never;
}[keyof T];
/**
* A union of all colors in the theme
*/
export type ThemeColor = NestedKeys<typeof colors>;
export const colorsFlattened = Object.entries(colors).reduce(
(acc, [prefix, group]) => {
for (const [name, hex] of Object.entries(group)) {
acc[`${prefix}-${name}`] = hex;
}
return acc;
},
{} as Record<ThemeColor, string>
);
const hexToRgb = (hex: string) =>
hex.match(/[0-9a-f]{2}/gi).map(partialHex => parseInt(partialHex, 16)) as [number, number, number];
const colorsFlattenedRgb = Object.fromEntries(
Object.entries(colorsFlattened).map(([name, hex]) => [name, hexToRgb(hex)])
) as Record<ThemeColor, ReturnType<typeof hexToRgb>>;
/**
* Retrieves the hexadecimal color value by name from the theme.
*
* @param name - The name of the theme color.
* @returns The hexadecimal color value.
*/
export const getThemeColorHexByName = (name: ThemeColor): string => colorsFlattened[name];
/**
*
* @param name - The name of the theme color.
* @returns An array of the red, green, and blue values, respectively
*/
export const getThemeColorRgbByName = (name: ThemeColor) => colorsFlattenedRgb[name];

View File

@@ -1,24 +1,33 @@
import { Button } from 'src/views/components/common/Button/Button';
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';
import { colorsFlattened } from 'src/shared/util/themeColors';
import { Button } from 'src/views/components/common/Button/Button';
import AddIcon from '~icons/material-symbols/add';
import CalendarMonthIcon from '~icons/material-symbols/calendar-month';
import DescriptionIcon from '~icons/material-symbols/description';
import ImagePlaceholderIcon from '~icons/material-symbols/image';
import HappyFaceIcon from '~icons/material-symbols/mood';
import RemoveIcon from '~icons/material-symbols/remove';
import ReviewsIcon from '~icons/material-symbols/reviews';
// More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export
const meta = {
title: 'Components/Common/Button',
component: Button,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
args: {
children: 'Button',
},
argTypes: {
children: { control: 'text' },
},
title: 'Components/Common/Button',
component: Button,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
tags: ['autodocs'],
// More on argTypes: https://storybook.js.org/docs/api/argtypes
args: {
children: 'Button',
icon: ImagePlaceholderIcon,
},
argTypes: {
children: { control: 'text' },
},
} satisfies Meta<typeof Button>;
export default meta;
@@ -26,72 +35,112 @@ type Story = StoryObj<typeof meta>;
// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Default: Story = {
args: {},
args: {
variant: 'filled',
color: 'ut-black',
},
};
export const Disabled: Story = {
args: {
disabled: true,
},
};
export const Grid: Story = {
render: props => (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex' }}>
<Button {...props} type='primary' />
<Button {...props} type='secondary' />
<Button {...props} type='tertiary' />
<Button {...props} type='danger' />
<Button {...props} type='warning' />
<Button {...props} type='success' />
<Button {...props} type='info' />
</div>
<div style={{ display: 'flex' }}>
<Button {...props} type='primary' disabled />
<Button {...props} type='secondary' disabled />
<Button {...props} type='tertiary' disabled />
<Button {...props} type='danger' disabled />
<Button {...props} type='warning' disabled />
<Button {...props} type='success' disabled />
<Button {...props} type='info' disabled />
</div>
</div>
),
};
// TODO: Actually show the buttons as they appear in the design
export const CourseButtons: Story = {
args: {
children: 'Add Course',
},
render: props => (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex' }}>
<Button {...props} type='primary' />
<Button {...props} type='secondary' />
<Button {...props} type='tertiary' />
<Button {...props} type='danger' />
<Button {...props} type='warning' />
<Button {...props} type='success' />
<Button {...props} type='info' />
</div>
<div style={{ display: 'flex' }}>
<Button {...props} type='primary' disabled />
<Button {...props} type='secondary' disabled />
<Button {...props} type='tertiary' disabled />
<Button {...props} type='danger' disabled />
<Button {...props} type='warning' disabled />
<Button {...props} type='success' disabled />
<Button {...props} type='info' disabled />
</div>
</div>
),
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/8tsCay2FRqctrdcZ3r9Ahw/UTRP?type=design&node-id=324-389&mode=design&t=BoS5xBrpSsjgQXqv-4',
args: {
variant: 'filled',
color: 'ut-black',
disabled: true,
},
},
};
// @ts-ignore
export const Grid: Story = {
render: props => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
<div style={{ display: 'flex', gap: '15px' }}>
<Button {...props} variant='filled' color='ut-black' />
<Button {...props} variant='outline' color='ut-black' />
<Button {...props} variant='single' color='ut-black' />
</div>
<hr />
<div style={{ display: 'flex', gap: '15px' }}>
<Button {...props} variant='filled' color='ut-black' disabled />
<Button {...props} variant='outline' color='ut-black' disabled />
<Button {...props} variant='single' color='ut-black' disabled />
</div>
</div>
),
};
export const PrettyColors: Story = {
// @ts-ignore
args: {
children: '',
},
render: props => {
const colorsNames = Object.keys(colorsFlattened) as (keyof typeof colorsFlattened)[];
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px' }}>
{colorsNames.map(color => (
<div style={{ display: 'flex', gap: '15px' }} key={color}>
<Button {...props} variant='filled' color={color}>
Button
</Button>
<Button {...props} variant='outline' color={color}>
Button
</Button>
<Button {...props} variant='single' color={color}>
Button
</Button>
<Button {...props} variant='filled' color={color} />
<Button {...props} variant='outline' color={color} />
<Button {...props} variant='single' color={color} />
</div>
))}
</div>
);
},
};
// @ts-ignore
export const CourseButtons: Story = {
render: props => (
<div style={{ display: 'flex', flexDirection: 'column', gap: '15px', alignItems: 'center' }}>
<Button {...props} variant='filled' color='ut-green' icon={AddIcon}>
Add Course
</Button>
<Button {...props} variant='filled' color='theme-red' icon={RemoveIcon}>
Remove Course
</Button>
</div>
),
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/8tsCay2FRqctrdcZ3r9Ahw/UTRP?type=design&node-id=324-389&mode=design&t=BoS5xBrpSsjgQXqv-4',
},
},
};
export const CourseCatalogActionButtons: Story = {
// @ts-ignore
args: {
children: '',
},
render: props => (
<div style={{ display: 'flex', gap: '15px' }}>
<Button {...props} variant='filled' color='ut-burntorange' icon={CalendarMonthIcon} />
<Button {...props} variant='outline' color='ut-blue' icon={ReviewsIcon}>
RateMyProf
</Button>
<Button {...props} variant='outline' color='ut-teal' icon={HappyFaceIcon}>
CES
</Button>
<Button {...props} variant='outline' color='ut-yellow' icon={DescriptionIcon}>
Past Syllabi
</Button>
<Button {...props} variant='filled' color='ut-green' icon={AddIcon}>
Add Course
</Button>
</div>
),
};

View File

@@ -1,19 +0,0 @@
// Calendar.stories.tsx
import React from 'react';
import Calendar from '@views/components/common/CalendarGrid/CalendarGrid';
import type { Meta, StoryObj } from '@storybook/react';
const meta = {
title: 'Components/Common/Calendar',
component: Calendar,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
tags: ['autodocs'],
}
} satisfies Meta<typeof Calendar>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};

View File

@@ -0,0 +1,23 @@
import { Meta, StoryObj } from '@storybook/react';
import { Chip } from 'src/views/components/common/Chip/Chip';
const meta = {
title: 'Components/Common/Chip',
component: Chip,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
label: { control: 'text' },
},
} satisfies Meta<typeof Chip>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
label: 'QR',
},
};

View File

@@ -0,0 +1,117 @@
import { Meta, StoryObj } from '@storybook/react';
import ConflictsWithWarning from '@views/components/common/ConflictsWithWarning/ConflictsWithWarning';
import { Course, Status } from 'src/shared/types/Course';
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
import Instructor from 'src/shared/types/Instructor';
export const ExampleCourse: Course = new Course({
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
creditHours: 3,
department: 'C S',
description: [
'Problem solving and fundamental algorithms for various applications in science, business, and on the World Wide Web, and introductory programming in a modern object-oriented programming language.',
'Only one of the following may be counted: Computer Science 303E, 312, 312H. Credit for Computer Science 303E may not be earned after a student has received credit for Computer Science 314, or 314H. May not be counted toward a degree in computer science.',
'May be counted toward the Quantitative Reasoning flag requirement.',
'Designed to accommodate 100 or more students.',
'Taught as a Web-based course.',
],
flags: ['Quantitative Reasoning'],
fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB',
instructionMode: 'Online',
instructors: [
new Instructor({
firstName: 'Bevo',
lastName: 'Bevo',
fullName: 'Bevo Bevo',
}),
],
isReserved: false,
number: '303E',
schedule: {
meetings: [
new CourseMeeting({
days: ['Tuesday', 'Thursday'],
endTime: 660,
startTime: 570,
}),
],
},
semester: {
code: '12345',
season: 'Spring',
year: 2024,
},
status: Status.WAITLISTED,
uniqueId: 12345,
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
});
export const ExampleCourse2: Course = new Course({
courseName: 'PRINCIPLES OF COMPUTER SYSTEMS',
creditHours: 3,
department: 'C S',
description: [
'Restricted to computer science majors.',
'An introduction to computer systems software abstractions with an emphasis on the connection of these abstractions to underlying computer hardware. Key abstractions include threads, virtual memory, protection, and I/O. Requires writing of synchronized multithreaded programs and pieces of an operating system.',
'Computer Science 439 and 439H may not both be counted.',
'Prerequisite: Computer Science 429, or 429H with a grade of at least C-.',
'May be counted toward the Independent Inquiry flag requirement.',
],
flags: ['Independent Inquiry'],
fullName: 'C S 439 PRINCIPLES OF COMPUTER SYSTEMS',
instructionMode: 'In Person',
instructors: [
new Instructor({
firstName: 'Allison',
lastName: 'Norman',
fullName: 'Allison Norman',
}),
],
isReserved: false,
number: '439',
schedule: {
meetings: [
new CourseMeeting({
days: ['Tuesday', 'Thursday'],
startTime: 930,
endTime: 1050,
}),
new CourseMeeting({
days: ['Friday'],
startTime: 600,
endTime: 720,
}),
],
},
semester: {
code: '12345',
season: 'Spring',
year: 2024,
},
status: Status.WAITLISTED,
uniqueId: 67890,
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
});
const meta = {
title: 'Components/Common/ConflictsWithWarning',
component: ConflictsWithWarning,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
conflicts: { control: 'object' },
},
args: {
conflicts: [ExampleCourse, ExampleCourse2],
},
} satisfies Meta<typeof ConflictsWithWarning>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
conflicts: [ExampleCourse, ExampleCourse2],
},
};

View File

@@ -0,0 +1,157 @@
import { Course, Status } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule';
import type { Meta, StoryObj } from '@storybook/react';
import type { Serialized } from 'chrome-extension-toolkit';
import React from 'react';
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting';
import { CourseSchedule } from 'src/shared/types/CourseSchedule';
import Instructor from 'src/shared/types/Instructor';
import Dropdown from 'src/views/components/common/Dropdown/Dropdown';
import ScheduleListItem from 'src/views/components/common/ScheduleListItem/ScheduleListItem';
const meta: Meta<typeof Dropdown> = {
title: 'Components/Common/Dropdown',
component: Dropdown,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
dummySchedules: { control: 'object' },
dummyActiveIndex: { control: 'number' },
scheduleComponents: { control: 'object' },
},
render: (args: any) => (
<div className='w-80'>
<Dropdown {...args} />
</div>
),
} satisfies Meta<typeof Dropdown>;
export default meta;
type Story = StoryObj<typeof meta>;
const schedules = [
new UserSchedule({
courses: [
new Course({
uniqueId: 123,
number: '311C',
fullName: "311C - Bevo's Default Course",
courseName: "Bevo's Default Course",
department: 'BVO',
creditHours: 3,
status: Status.WAITLISTED,
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
isReserved: false,
url: '',
flags: [],
schedule: new CourseSchedule({
meetings: [
new CourseMeeting({
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
startTime: 480,
endTime: 570,
location: {
building: 'UTC',
room: '123',
},
}),
],
}),
instructionMode: 'In Person',
semester: {
year: 2024,
season: 'Fall',
},
}),
],
name: 'Main Schedule',
hours: 0,
} as Serialized<UserSchedule>),
new UserSchedule({
courses: [
new Course({
uniqueId: 123,
number: '311C',
fullName: "311C - Bevo's Default Course",
courseName: "Bevo's Default Course",
department: 'BVO',
creditHours: 3,
status: Status.WAITLISTED,
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
isReserved: false,
url: '',
flags: [],
schedule: new CourseSchedule({
meetings: [
new CourseMeeting({
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
startTime: 480,
endTime: 570,
location: {
building: 'UTC',
room: '123',
},
}),
],
}),
instructionMode: 'In Person',
semester: {
year: 2024,
season: 'Fall',
},
}),
new Course({
uniqueId: 123,
number: '311C',
fullName: "311C - Bevo's Default Course",
courseName: "Bevo's Default Course",
department: 'BVO',
creditHours: 3,
status: Status.WAITLISTED,
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
isReserved: false,
url: '',
flags: [],
schedule: new CourseSchedule({
meetings: [
new CourseMeeting({
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
startTime: 480,
endTime: 570,
location: {
building: 'UTC',
room: '123',
},
}),
],
}),
instructionMode: 'In Person',
semester: {
year: 2024,
season: 'Fall',
},
}),
],
name: 'Backup #3',
hours: 0,
} as Serialized<UserSchedule>),
];
export const Hidden: Story = {
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/8tsCay2FRqctrdcZ3r9Ahw/UTRP?type=design&node-id=1579-5083&mode=dev',
},
},
args: {
dummySchedules: schedules,
dummyActiveIndex: 0,
scheduleComponents: schedules.map((schedule, index) => (
<ScheduleListItem active={index === 0} name={schedule.name} />
)),
},
};

View File

@@ -0,0 +1,19 @@
import { Meta, StoryObj } from '@storybook/react';
import ImportantLinks from 'src/views/components/calendar/ImportantLinks';
const meta = {
title: 'Components/Common/ImportantLinks',
component: ImportantLinks,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {},
} satisfies Meta<typeof ImportantLinks>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {},
};

View File

@@ -0,0 +1,25 @@
import { Meta, StoryObj } from '@storybook/react';
import { InfoCard } from 'src/views/components/common/InfoCard/InfoCard';
const meta = {
title: 'Components/Common/InfoCard',
component: InfoCard,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
titleText: { control: 'text' },
bodyText: { control: 'text' },
},
} satisfies Meta<typeof InfoCard>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
titleText: 'WAITLIST SIZE',
bodyText: '14 Students',
},
};

View File

@@ -0,0 +1,96 @@
import { Course, Status } from '@shared/types/Course';
import { CourseMeeting } from '@shared/types/CourseMeeting';
import Instructor from '@shared/types/Instructor';
import { Meta, StoryObj } from '@storybook/react';
import List from '@views/components/common/List/List';
import PopupCourseBlock from '@views/components/common/PopupCourseBlock/PopupCourseBlock';
import React from 'react';
import { test_colors } from './PopupCourseBlock.stories';
const numberOfCourses = 5;
export const generateCourses = count => {
const courses = [];
for (let i = 0; i < count; i++) {
const course = new Course({
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
creditHours: 3,
department: 'C S',
description: [
'Problem solving and fundamental algorithms for various applications in science, business, and on the World Wide Web, and introductory programming in a modern object-oriented programming language.',
'Only one of the following may be counted: Computer Science 303E, 312, 312H. Credit for Computer Science 303E may not be earned after a student has received credit for Computer Science 314, or 314H. May not be counted toward a degree in computer science.',
'May be counted toward the Quantitative Reasoning flag requirement.',
'Designed to accommodate 100 or more students.',
'Taught as a Web-based course.',
],
flags: ['Quantitative Reasoning'],
fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB',
instructionMode: 'Online',
instructors: [
new Instructor({
firstName: 'Bevo',
lastName: 'Bevo',
fullName: 'Bevo Bevo',
}),
],
isReserved: false,
number: '303E',
schedule: {
meetings: [
new CourseMeeting({
days: ['Tuesday', 'Thursday'],
endTime: 660,
startTime: 570,
}),
],
},
semester: {
code: '12345',
season: 'Spring',
year: 2024,
},
status: Status.WAITLISTED,
uniqueId: 12345 + i, // Make uniqueId different for each course
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
});
courses.push(course);
}
return courses;
};
const exampleCourses = generateCourses(numberOfCourses);
const generateCourseBlocks = (exampleCourses, colors) =>
exampleCourses.map((course, i) => <PopupCourseBlock key={course.uniqueId} course={course} colors={colors[i]} />);
export const exampleCourseBlocks = generateCourseBlocks(exampleCourses, test_colors);
const meta = {
title: 'Components/Common/List',
component: List,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
draggableElements: { control: 'object' },
itemHeight: { control: 'number' },
listHeight: { control: 'number' },
listWidth: { control: 'number' },
gap: { control: 'number' },
},
} satisfies Meta<typeof List>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
draggableElements: exampleCourseBlocks,
itemHeight: 55,
listHeight: 300,
listWidth: 300,
gap: 12,
},
};

View File

@@ -1,13 +1,13 @@
import type { Meta, StoryObj } from '@storybook/react';
import PopupCourseBlock from '@views/components/common/PopupCourseBlock/PopupCourseBlock';
import React from 'react';
import { Course, Status } from 'src/shared/types/Course';
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
import Instructor from 'src/shared/types/Instructor';
import PopupCourseBlock from '@views/components/common/PopupCourseBlock/PopupCourseBlock';
import { getCourseColors } from 'src/shared/util/colors';
import { theme } from 'unocss/preset-mini';
const exampleCourse: Course = new Course({
export const exampleCourse: Course = new Course({
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
creditHours: 3,
department: 'C S',
@@ -95,7 +95,7 @@ export const Variants: Story = {
),
};
const colors = Object.keys(theme.colors)
export const test_colors = Object.keys(theme.colors)
// check that the color is a colorway (is an object)
.filter(color => typeof theme.colors[color] === 'object')
.slice(0, 17)
@@ -103,8 +103,8 @@ const colors = Object.keys(theme.colors)
export const AllColors: Story = {
render: props => (
<div className='grid grid-rows-9 grid-cols-2 grid-flow-col max-w-2xl w-90vw gap-x-4 gap-y-2'>
{colors.map((color, i) => (
<div className='grid grid-flow-col grid-cols-2 grid-rows-9 max-w-2xl w-90vw gap-x-4 gap-y-2'>
{test_colors.map((color, i) => (
<PopupCourseBlock key={color.primaryColor} course={exampleCourse} colors={color} />
))}
</div>

View File

@@ -0,0 +1,23 @@
import { Meta, StoryObj } from '@storybook/react';
import PopupMain from '@views/components/PopupMain';
const meta = {
title: 'Components/Common/PopupMain',
component: PopupMain,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
},
} satisfies Meta<typeof PopupMain>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
},
};

View File

@@ -0,0 +1,27 @@
import { Meta, StoryObj } from '@storybook/react';
import ScheduleTotalHoursAndCourses from '@views/components/common/ScheduleTotalHoursAndCourses/ScheduleTotalHoursAndCourses';
const meta = {
title: 'Components/Common/ScheduleTotalHoursAndCourses',
component: ScheduleTotalHoursAndCourses,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
scheduleName: { control: 'text' },
totalHours: { control: 'number' },
totalCourses: { control: 'number' }
},
} satisfies Meta<typeof ScheduleTotalHoursAndCourses>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
scheduleName: 'SCHEDULE',
totalHours: 22,
totalCourses: 8
},
};

View File

@@ -0,0 +1,36 @@
import React from 'react';
import ScheduleListItem from 'src/views/components/common/ScheduleListItem/ScheduleListItem';
export default {
title: 'Components/Common/ScheduleListItem',
component: ScheduleListItem,
parameters: {
layout: 'centered',
tags: ['autodocs'],
},
argTypes: {
active: { control: 'boolean' },
name: { control: 'text' },
},
};
export const Default = (args) => <ScheduleListItem {...args} />;
Default.args = {
name: 'My Schedule',
active: true,
};
export const Active = (args) => <ScheduleListItem {...args} />;
Active.args = {
name: 'My Schedule',
active: true,
};
export const Inactive = (args) => <ScheduleListItem {...args} />;
Inactive.args = {
name: 'My Schedule',
active: false,
};

View File

@@ -0,0 +1,19 @@
import { Meta, StoryObj } from '@storybook/react';
import Settings from 'src/views/components/Settings';
const meta = {
title: 'Components/Common/Settings',
component: Settings,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {},
} satisfies Meta<typeof Settings>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {},
};

View File

@@ -0,0 +1,23 @@
import { Meta, StoryObj } from '@storybook/react';
import { Calendar } from 'src/views/components/calendar/Calendar/Calendar';
const meta = {
title: 'Components/Calendar/Calendar',
component: Calendar,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
},
} satisfies Meta<typeof Calendar>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
},
};

View File

@@ -0,0 +1,101 @@
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import { Course, Status } from '@shared/types/Course';
import Instructor from '@shared/types/Instructor';
import { CalendarBottomBar } from 'src/views/components/calendar/CalendarBottomBar/CalendarBottomBar';
import { getCourseColors } from '../../../shared/util/colors';
const exampleGovCourse: Course = new Course({
courseName: 'Nope',
creditHours: 3,
department: 'GOV',
description: ['nah', 'aint typing this', 'corndog'],
flags: ['no flag for you >:)'],
fullName: 'GOV 312L Something something',
instructionMode: 'Online',
instructors: [
new Instructor({
firstName: 'Bevo',
lastName: 'Barrymore',
fullName: 'Bevo Barrymore',
}),
],
isReserved: false,
number: '312L',
schedule: {
meetings: [],
},
semester: {
code: '12345',
season: 'Spring',
year: 2024,
},
status: Status.OPEN,
uniqueId: 12345,
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
});
const examplePsyCourse: Course = new Course({
courseName: 'Nope Again',
creditHours: 3,
department: 'PSY',
description: ['nah', 'aint typing this', 'corndog'],
flags: ['no flag for you >:)'],
fullName: 'PSY 317L Yada yada',
instructionMode: 'Online',
instructors: [
new Instructor({
firstName: 'Bevo',
lastName: 'Etz',
fullName: 'Bevo Etz',
}),
],
isReserved: false,
number: '317L',
schedule: {
meetings: [],
},
semester: {
code: '12346',
season: 'Spring',
year: 2024,
},
status: Status.CLOSED,
uniqueId: 12346,
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
});
const meta = {
title: 'Components/Calendar/CalendarBottomBar',
component: CalendarBottomBar,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {},
} satisfies Meta<typeof CalendarBottomBar>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
courses: [
{
colors: getCourseColors('pink', 200),
courseDeptAndInstr: `${exampleGovCourse.department} ${exampleGovCourse.number} ${exampleGovCourse.instructors[0].lastName}`,
status: exampleGovCourse.status,
},
{
colors: getCourseColors('slate', 500),
courseDeptAndInstr: `${examplePsyCourse.department} ${examplePsyCourse.number} ${examplePsyCourse.instructors[0].lastName}`,
status: examplePsyCourse.status,
},
],
},
render: props => (
<div className='outline-red outline w-292.5!'>
<CalendarBottomBar {...props} />
</div>
),
};

View File

@@ -1,14 +1,13 @@
import { Meta, StoryObj } from '@storybook/react';
import React from 'react';
import { Course, Status } from 'src/shared/types/Course';
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting';
import { CourseSchedule } from 'src/shared/types/CourseSchedule';
import Instructor from 'src/shared/types/Instructor';
import CalendarCourseCell from 'src/views/components/common/CalendarCourseCell/CalendarCourseCell';
import { Course, Status } from '@shared/types/Course';
import { CourseMeeting, DAY_MAP } from '@shared/types/CourseMeeting';
import { CourseSchedule } from '@shared/types/CourseSchedule';
import Instructor from '@shared/types/Instructor';
import CalendarCourse from 'src/views/components/calendar/CalendarCourseBlock/CalendarCourseMeeting';
const meta = {
title: 'Components/Common/CalendarCourseCell',
component: CalendarCourseCell,
title: 'Components/Calendar/CalendarCourseMeeting',
component: CalendarCourse,
parameters: {
layout: 'centered',
},
@@ -17,13 +16,9 @@ const meta = {
course: { control: 'object' },
meetingIdx: { control: 'number' },
color: { control: 'color' },
rightIcon: { control: 'object' },
},
render: (args: any) => (
<div className="w-45">
<CalendarCourseCell {...args} />
</div>
),
} satisfies Meta<typeof CalendarCourseCell>;
} satisfies Meta<typeof CalendarCourse>;
export default meta;
type Story = StoryObj<typeof meta>;
@@ -37,7 +32,7 @@ export const Default: Story = {
courseName: "Bevo's Default Course",
department: 'BVO',
creditHours: 3,
status: Status.WAITLISTED,
status: Status.OPEN,
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
isReserved: false,
url: '',

View File

@@ -0,0 +1,114 @@
import { Course, Status } from '@shared/types/Course';
import { getCourseColors } from '@shared/util/colors';
import { Meta, StoryObj } from '@storybook/react';
import CalendarCourseCell from 'src/views/components/calendar/CalendarCourseCell/CalendarCourseCell';
import React from 'react';
import { exampleCourse } from '../PopupCourseBlock.stories';
const meta = {
title: 'Components/Calendar/CalendarCourseCell',
component: CalendarCourseCell,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
courseDeptAndInstr: { control: { type: 'text' } },
className: { control: { type: 'text' } },
status: { control: { type: 'select', options: Object.values(Status) } },
timeAndLocation: { control: { type: 'text' } },
colors: { control: { type: 'object' } },
},
render: (args: any) => (
<div className='w-45'>
<CalendarCourseCell {...args} />
</div>
),
args: {
courseDeptAndInstr: exampleCourse.department,
className: exampleCourse.number,
status: exampleCourse.status,
timeAndLocation: exampleCourse.schedule.meetings[0].getTimeString({ separator: '-' }),
colors: getCourseColors('emerald', 500),
},
} satisfies Meta<typeof CalendarCourseCell>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Variants: Story = {
render: props => (
<div className='grid grid-cols-2 h-40 max-w-60 w-90vw gap-x-4 gap-y-2'>
<CalendarCourseCell
{...props}
// course={new Course({ ...exampleCourse, status: Status.OPEN })}
// Course = new Course({
// courseName: 'PRINCIPLES OF COMPUTER SYSTEMS',
// creditHours: 3,
// department: 'C S',
// description: [
// 'Restricted to computer science majors.',
// 'An introduction to computer systems software abstractions with an emphasis on the connection of these abstractions to underlying computer hardware. Key abstractions include threads, virtual memory, protection, and I/O. Requires writing of synchronized multithreaded programs and pieces of an operating system.',
// 'Computer Science 439 and 439H may not both be counted.',
// 'Prerequisite: Computer Science 429, or 429H with a grade of at least C-.',
// 'May be counted toward the Independent Inquiry flag requirement.',
// ],
// flags: ['Independent Inquiry'],
// fullName: 'C S 439 PRINCIPLES OF COMPUTER SYSTEMS',
// instructionMode: 'In Person',
// instructors: [
// new Instructor({
// firstName: 'Allison',
// lastName: 'Norman',
// fullName: 'Allison Norman',
// }),
// ],
// isReserved: false,
// number: '439',
// schedule: {
// meetings: [
// new CourseMeeting({
// days: ['Tuesday', 'Thursday'],
// startTime: 930,
// endTime: 1050,
// }),
// new CourseMeeting({
// days: ['Friday'],
// startTime: 600,
// endTime: 720,
// }),
// ],
// },
// semester: {
// code: '12345',
// season: 'Spring',
// year: 2024,
// },
// status: Status.WAITLISTED,
// uniqueId: 67890,
// url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
// });
colors={getCourseColors('green', 500)}
/>
<CalendarCourseCell
{...props}
// course={new Course({ ...exampleCourse, status: Status.CLOSED })}
colors={getCourseColors('teal', 400)}
/>
<CalendarCourseCell
{...props}
// course={new Course({ ...exampleCourse, status: Status.WAITLISTED })}
colors={getCourseColors('indigo', 400)}
/>
<CalendarCourseCell
{...props}
// course={new Course({ ...exampleCourse, status: Status.CANCELLED })}
colors={getCourseColors('red', 500)}
/>
</div>
),
};

View File

@@ -0,0 +1,212 @@
import { Course, Status } from '@shared/types/Course';
import { getCourseColors } from '@shared/util/colors';
import type { Meta, StoryObj } from '@storybook/react';
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
import { Serialized } from 'chrome-extension-toolkit';
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting';
import { CourseSchedule } from 'src/shared/types/CourseSchedule';
import Instructor from 'src/shared/types/Instructor';
import { UserSchedule } from 'src/shared/types/UserSchedule';
import CalendarGrid from 'src/views/components/calendar/CalendarGrid/CalendarGrid';
const meta = {
title: 'Components/Calendar/CalendarGrid',
component: CalendarGrid,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
saturdayClass: { control: 'boolean' },
},
} satisfies Meta<typeof CalendarGrid>;
export default meta;
const exampleCourse: Course = new Course({
uniqueId: 50805,
number: '314',
fullName: 'C S 314 DATA STRUCTURES',
courseName: 'DATA STRUCTURES',
department: 'C S',
creditHours: 3,
status: Status.OPEN,
instructors: [
new Instructor({ fullName: 'SCOTT, MICHAEL', firstName: 'MICHAEL', lastName: 'SCOTT', middleInitial: 'D' }),
],
isReserved: true,
description: [
'Second part of a two-part sequence in programming. Introduction to specifications, simple unit testing, and debugging; building and using canonical data structures; algorithm analysis and reasoning techniques such as assertions and invariants.',
'Computer Science 314 and 314H may not both be counted.',
'BVO 311C and 312H may not both be counted.',
'Prerequisite: Computer Science 312 or 312H with a grade of at least C-.',
'May be counted toward the Quantitative Reasoning flag requirement.',
],
schedule: new CourseSchedule({
meetings: [
new CourseMeeting({
days: [DAY_MAP.T, DAY_MAP.TH],
startTime: 480,
endTime: 570,
location: { building: 'UTC', room: '123' },
}),
new CourseMeeting({
days: [DAY_MAP.TH],
startTime: 570,
endTime: 630,
location: { building: 'JES', room: '123' },
}),
],
}),
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
flags: ['Writing', 'Independent Inquiry'],
instructionMode: 'In Person',
semester: {
code: '12345',
year: 2024,
season: 'Spring',
},
});
const exampleSchedule = new UserSchedule({
name: 'Example Schedule',
courses: [exampleCourse],
hours: 3,
} as Serialized<UserSchedule>);
const testData: CalendarGridCourse[] = [
{
calendarGridPoint: {
dayIndex: 4,
startIndex: 10,
endIndex: 11,
},
componentProps: {
courseDeptAndInstr: 'Course 1',
timeAndLocation: '9:00 AM - 10:00 AM, Room 101',
status: Status.OPEN,
colors: getCourseColors('emerald', 500),
},
popupProps: {
course: exampleCourse,
activeSchedule: exampleSchedule,
onClose: () => {},
},
},
{
calendarGridPoint: {
dayIndex: 2,
startIndex: 5,
endIndex: 6,
},
componentProps: {
courseDeptAndInstr: 'Course 1',
timeAndLocation: '9:00 AM - 10:00 AM, Room 101',
status: Status.OPEN,
colors: getCourseColors('emerald', 500),
},
popupProps: {
course: exampleCourse,
activeSchedule: exampleSchedule,
onClose: () => {},
},
},
{
calendarGridPoint: {
dayIndex: 1,
startIndex: 10,
endIndex: 12,
},
componentProps: {
courseDeptAndInstr: 'Course 2',
timeAndLocation: '10:00 AM - 11:00 AM, Room 102',
status: Status.CLOSED,
colors: getCourseColors('emerald', 500),
},
popupProps: {
course: exampleCourse,
activeSchedule: exampleSchedule,
onClose: () => {},
},
},
{
calendarGridPoint: {
dayIndex: 4,
startIndex: 10,
endIndex: 11,
},
componentProps: {
courseDeptAndInstr: 'Course 1',
timeAndLocation: '9:00 AM - 10:00 AM, Room 101',
status: Status.OPEN,
colors: getCourseColors('emerald', 500),
},
popupProps: {
course: exampleCourse,
activeSchedule: exampleSchedule,
onClose: () => {},
},
},
{
calendarGridPoint: {
dayIndex: 1,
startIndex: 10,
endIndex: 12,
},
componentProps: {
courseDeptAndInstr: 'Course 2',
timeAndLocation: '10:00 AM - 11:00 AM, Room 102',
status: Status.CLOSED,
colors: getCourseColors('emerald', 500),
},
popupProps: {
course: exampleCourse,
activeSchedule: exampleSchedule,
onClose: () => {},
},
},
{
calendarGridPoint: {
dayIndex: 1,
startIndex: 10,
endIndex: 12,
},
componentProps: {
courseDeptAndInstr: 'Course 3',
timeAndLocation: '10:00 AM - 11:00 AM, Room 102',
status: Status.CLOSED,
colors: getCourseColors('emerald', 500),
},
popupProps: {
course: exampleCourse,
activeSchedule: exampleSchedule,
onClose: () => {},
},
},
{
calendarGridPoint: {
dayIndex: 1,
startIndex: 10,
endIndex: 12,
},
componentProps: {
courseDeptAndInstr: 'Course 4',
timeAndLocation: '10:00 AM - 11:00 AM, Room 102',
status: Status.CLOSED,
colors: getCourseColors('emerald', 500),
},
popupProps: {
course: exampleCourse,
activeSchedule: exampleSchedule,
onClose: () => {},
},
},
];
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
saturdayClass: true,
courseCells: testData,
},
};

View File

@@ -0,0 +1,19 @@
// Calendar.stories.tsx
import React from 'react';
import CalendarCell from 'src/views/components/calendar/CalendarGridCell/CalendarGridCell';
import type { Meta, StoryObj } from '@storybook/react';
const meta = {
title: 'Components/Calendar/CalendarGridCell',
component: CalendarCell,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/configure/story-layout
layout: 'centered',
tags: ['autodocs'],
},
} satisfies Meta<typeof CalendarCell>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { Meta, StoryObj } from '@storybook/react';
import CalendarHeader from 'src/views/components/calendar/CalendarHeader/CalenderHeader';
const meta = {
title: 'Components/Calendar/CalendarHeader',
component: CalendarHeader,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
} satisfies Meta<typeof CalendarHeader>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};

View File

@@ -0,0 +1,146 @@
import { Course, Status } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule';
import type { Meta, StoryObj } from '@storybook/react';
import React from 'react';
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting';
import { CourseSchedule } from 'src/shared/types/CourseSchedule';
import Instructor from 'src/shared/types/Instructor';
import { CalendarSchedules } from 'src/views/components/calendar/CalendarSchedules/CalendarSchedules';
const meta = {
title: 'Components/Calendar/CalendarSchedules',
component: CalendarSchedules,
parameters: {
layout: 'centered',
tags: ['autodocs'],
},
argTypes: {
dummySchedules: { control: 'object' },
dummyActiveIndex: { control: 'number' },
},
render: (args: any) => (
<div>
<CalendarSchedules {...args} />
</div>
),
} satisfies Meta<typeof CalendarSchedules>;
export default meta;
type Story = StoryObj<typeof meta>;
const schedules = [
new UserSchedule({
courses: [
new Course({
uniqueId: 123,
number: '311C',
fullName: "311C - Bevo's Default Course",
courseName: "Bevo's Default Course",
department: 'BVO',
creditHours: 3,
status: Status.WAITLISTED,
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
isReserved: false,
url: '',
flags: [],
schedule: new CourseSchedule({
meetings: [
new CourseMeeting({
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
startTime: 480,
endTime: 570,
location: {
building: 'UTC',
room: '123',
},
}),
],
}),
instructionMode: 'In Person',
semester: {
year: 2024,
season: 'Fall',
},
}),
],
name: 'Main Schedule',
hours: 0, // Add the missing 'hours' property
}),
new UserSchedule({
courses: [
new Course({
uniqueId: 123,
number: '311C',
fullName: "311C - Bevo's Default Course",
courseName: "Bevo's Default Course",
department: 'BVO',
creditHours: 3,
status: Status.WAITLISTED,
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
isReserved: false,
url: '',
flags: [],
schedule: new CourseSchedule({
meetings: [
new CourseMeeting({
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
startTime: 480,
endTime: 570,
location: {
building: 'UTC',
room: '123',
},
}),
],
}),
instructionMode: 'In Person',
semester: {
year: 2024,
season: 'Spring',
},
}),
new Course({
uniqueId: 123,
number: '311C',
fullName: "311C - Bevo's Default Course",
courseName: "Bevo's Default Course",
department: 'BVO',
creditHours: 3,
status: Status.WAITLISTED,
instructors: [new Instructor({ firstName: '', lastName: 'Bevo', fullName: 'Bevo' })],
isReserved: false,
url: '',
flags: [],
schedule: new CourseSchedule({
meetings: [
new CourseMeeting({
days: [DAY_MAP.M, DAY_MAP.W, DAY_MAP.F],
startTime: 480,
endTime: 570,
location: {
building: 'UTC',
room: '123',
},
}),
],
}),
instructionMode: 'In Person',
semester: {
year: 2024,
season: 'Fall',
},
}),
],
name: 'Backup #3',
hours: 0, // Add the missing 'hours' property
}),
];
export const Default: Story = {
args: {
dummySchedules: schedules,
dummyActiveIndex: 0,
},
};

View File

@@ -0,0 +1,79 @@
import type { Meta, StoryObj } from '@storybook/react';
import type { Serialized } from 'chrome-extension-toolkit';
import { Course, Status } from 'src/shared/types/Course';
import { CourseMeeting, DAY_MAP } from 'src/shared/types/CourseMeeting';
import { CourseSchedule } from 'src/shared/types/CourseSchedule';
import Instructor from 'src/shared/types/Instructor';
import { UserSchedule } from 'src/shared/types/UserSchedule';
import CourseCatalogInjectedPopup from 'src/views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
const exampleCourse: Course = new Course({
uniqueId: 50805,
number: '314',
fullName: 'C S 314 DATA STRUCTURES',
courseName: 'DATA STRUCTURES',
department: 'C S',
creditHours: 3,
status: Status.OPEN,
instructors: [
new Instructor({ fullName: 'SCOTT, MICHAEL', firstName: 'MICHAEL', lastName: 'SCOTT', middleInitial: 'D' }),
],
isReserved: true,
description: [
'Second part of a two-part sequence in programming. Introduction to specifications, simple unit testing, and debugging; building and using canonical data structures; algorithm analysis and reasoning techniques such as assertions and invariants.',
'Computer Science 314 and 314H may not both be counted.',
'BVO 311C and 312H may not both be counted.',
'Prerequisite: Computer Science 312 or 312H with a grade of at least C-.',
'May be counted toward the Quantitative Reasoning flag requirement.',
],
schedule: new CourseSchedule({
meetings: [
new CourseMeeting({
days: [DAY_MAP.T, DAY_MAP.TH],
startTime: 480,
endTime: 570,
location: { building: 'UTC', room: '123' },
}),
new CourseMeeting({
days: [DAY_MAP.TH],
startTime: 570,
endTime: 630,
location: { building: 'JES', room: '123' },
}),
],
}),
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
flags: ['Writing', 'Independent Inquiry'],
instructionMode: 'In Person',
semester: {
code: '12345',
year: 2024,
season: 'Spring',
},
});
const exampleSchedule = new UserSchedule({
name: 'Example Schedule',
courses: [exampleCourse],
hours: 3,
} as Serialized<UserSchedule>);
const meta: Meta<typeof CourseCatalogInjectedPopup> = {
title: 'Components/Injected/CourseCatalogInjectedPopup',
component: CourseCatalogInjectedPopup,
argTypes: {
onClose: { action: 'onClose' },
activeSchedule: { control: 'object' },
course: { control: 'object' },
},
};
export default meta;
type Story = StoryObj<typeof CourseCatalogInjectedPopup>;
export const Default: Story = {
args: {
activeSchedule: exampleSchedule,
course: exampleCourse,
},
};

View File

@@ -1,99 +1,115 @@
import { Course, Status } from 'src/shared/types/Course';
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
import { UserSchedule } from 'src/shared/types/UserSchedule';
import CoursePopup from 'src/views/components/injected/CoursePopup/CoursePopup';
import CoursePopup from 'src/views/components/injected/CoursePopupOld/CoursePopup';
import type { Meta, StoryObj } from '@storybook/react';
import Instructor from 'src/shared/types/Instructor';
const exampleCourse: Course = new Course({
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
creditHours: 3,
department: 'C S',
description: [
'Problem solving and fundamental algorithms for various applications in science, business, and on the World Wide Web, and introductory programming in a modern object-oriented programming language.',
'Only one of the following may be counted: Computer Science 303E, 312, 312H. Credit for Computer Science 303E may not be earned after a student has received credit for Computer Science 314, or 314H. May not be counted toward a degree in computer science.',
'May be counted toward the Quantitative Reasoning flag requirement.',
'Designed to accommodate 100 or more students.',
'Taught as a Web-based course.',
],
flags: ['Quantitative Reasoning'],
fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB',
instructionMode: 'Online',
instructors: [],
isReserved: false,
number: '303E',
schedule: {
meetings: [
new CourseMeeting({
days: ['Tuesday', 'Thursday'],
endTime: 660,
startTime: 570,
}),
courseName: 'ELEMS OF COMPTRS/PROGRAMMNG-WB',
creditHours: 3,
department: 'C S',
description: [
'Problem solving and fundamental algorithms for various applications in science, business, and on the World Wide Web, and introductory programming in a modern object-oriented programming language.',
'Only one of the following may be counted: Computer Science 303E, 312, 312H. Credit for Computer Science 303E may not be earned after a student has received credit for Computer Science 314, or 314H. May not be counted toward a degree in computer science.',
'May be counted toward the Quantitative Reasoning flag requirement.',
'Designed to accommodate 100 or more students.',
'Taught as a Web-based course.',
],
},
semester: {
code: '12345',
season: 'Spring',
year: 2024,
},
status: Status.CANCELLED,
uniqueId: 12345,
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
flags: ['Quantitative Reasoning'],
fullName: 'C S 303E ELEMS OF COMPTRS/PROGRAMMNG-WB',
instructionMode: 'Online',
instructors: [
new Instructor({
firstName: 'William',
lastName: 'Young',
middleInitial: 'D',
fullName: 'William D Young',
}),
new Instructor({
firstName: 'William',
lastName: 'Young',
middleInitial: 'D',
fullName: 'William D Young',
}),
],
isReserved: false,
number: '303E',
schedule: {
meetings: [
new CourseMeeting({
days: ['Tuesday', 'Thursday'],
endTime: 660,
startTime: 570,
}),
],
},
semester: {
code: '12345',
season: 'Spring',
year: 2024,
},
status: Status.CANCELLED,
uniqueId: 12345,
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/12345/',
});
const exampleSchedule: UserSchedule = new UserSchedule({
courses: [exampleCourse],
name: 'Example Schedule',
courses: [exampleCourse],
name: 'Example Schedule',
hours: 0,
});
const meta = {
title: 'Components/Injected/CoursePopup',
component: CoursePopup,
// tags: ['autodocs'],
args: {
course: exampleCourse,
activeSchedule: exampleSchedule,
},
argTypes: {
course: {
control: {
type: 'other',
},
title: 'Components/Injected/CoursePopup',
component: CoursePopup,
// tags: ['autodocs'],
args: {
course: exampleCourse,
activeSchedule: exampleSchedule,
},
activeSchedule: {
control: {
type: 'other',
},
argTypes: {
course: {
control: {
type: 'other',
},
},
activeSchedule: {
control: {
type: 'other',
},
},
},
},
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/8tsCay2FRqctrdcZ3r9Ahw/UTRP?type=design&node-id=602-1879&mode=design&t=BoS5xBrpSsjgQXqv-11',
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/8tsCay2FRqctrdcZ3r9Ahw/UTRP?type=design&node-id=602-1879&mode=design&t=BoS5xBrpSsjgQXqv-11',
},
},
},
} satisfies Meta<typeof CoursePopup>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Open: Story = {
args: {
course: new Course({ ...exampleCourse, status: Status.OPEN }),
activeSchedule: new UserSchedule({
courses: [],
name: 'Example Schedule',
}),
},
args: {
course: new Course({ ...exampleCourse, status: Status.OPEN }),
activeSchedule: new UserSchedule({
courses: [],
name: 'Example Schedule',
hours: 0,
}),
},
};
export const Closed: Story = {
args: {
course: new Course({ ...exampleCourse, status: Status.CLOSED }),
},
args: {
course: new Course({ ...exampleCourse, status: Status.CLOSED }),
},
};
export const Cancelled: Story = {
args: {
course: new Course({ ...exampleCourse, status: Status.CANCELLED }),
},
args: {
course: new Course({ ...exampleCourse, status: Status.CANCELLED }),
},
};

View File

@@ -8,11 +8,12 @@ import { SiteSupport } from '../lib/getSiteSupport';
import { populateSearchInputs } from '../lib/populateSearchInputs';
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';
import AutoLoad from './injected/AutoLoad/AutoLoad';
import CoursePopup from './injected/CoursePopup/CoursePopup';
import CoursePopup from './injected/CoursePopupOld/CoursePopup';
import RecruitmentBanner from './injected/RecruitmentBanner/RecruitmentBanner';
import TableHead from './injected/TableHead';
import TableRow from './injected/TableRow/TableRow';
import TableSubheading from './injected/TableSubheading/TableSubheading';
import CourseCatalogInjectedPopup from './injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
interface Props {
support: SiteSupport.COURSE_CATALOG_DETAILS | SiteSupport.COURSE_CATALOG_LIST;
@@ -79,7 +80,7 @@ export default function CourseCatalogMain({ support }: Props) {
);
})}
{selectedCourse && (
<CoursePopup
<CourseCatalogInjectedPopup
course={selectedCourse}
activeSchedule={activeSchedule}
onClose={handleClearSelectedCourse}

View File

@@ -1,24 +1,157 @@
import { background } from '@shared/messages';
import { StatusIcon } from '@shared/util/icons';
import React from 'react';
// import { FaCalendarAlt, FaCog, FaRedo } from 'react-icons/fa'; // Added FaRedo for the refresh icon
import { Status } from 'src/shared/types/Course';
import { test_colors } from 'src/stories/components/PopupCourseBlock.stories';
import logoImage from '../../assets/logo.png'; // Adjust the path as necessary
import useSchedules from '../hooks/useSchedules';
import { Button } from './common/Button/Button';
import { openTabFromContentScript } from '../lib/openNewTabFromContentScript';
import Divider from './common/Divider/Divider';
import ExtensionRoot from './common/ExtensionRoot/ExtensionRoot';
import List from './common/List/List'; // Ensure this path is correctly pointing to your List component
import PopupCourseBlock from './common/PopupCourseBlock/PopupCourseBlock';
import Text from './common/Text/Text';
import { handleOpenCalendar } from './injected/CourseCatalogInjectedPopup/HeadingAndActions';
export default function PopupMain() {
const [activeSchedule, schedules] = useSchedules();
const [activeSchedule] = useSchedules();
// TODO: Add a button to to switch the active schedule
const draggableElements = activeSchedule?.courses.map((course, i) => (
<PopupCourseBlock key={course.uniqueId} course={course} colors={test_colors[i]} />
));
const handleOpenOptions = async () => {
// Not sure if it's bad practice to export this
const url = chrome.runtime.getURL('/src/pages/options/index.html');
await openTabFromContentScript(url);
};
return (
<ExtensionRoot>
<Button
onClick={() => {
if (!activeSchedule) return;
background.clearCourses({ scheduleName: activeSchedule?.name });
}}
>
Clear Courses
</Button>
<div className='mx-auto max-w-sm rounded-lg bg-white p-4 shadow-md'>
<div className='mb-2 flex items-center justify-between bg-white'>
<div className='flex items-center'>
<img src={logoImage} alt='Logo' style={{ width: '40px', height: '40px', marginRight: '8px' }} />
<div>
<Text as='div' variant='h1-course' style={{ color: '#bf5700', fontSize: '1.3rem' }}>
UT Registration
</Text>
<Text as='div' variant='h1-course' style={{ color: '#f8971f', fontSize: '1.3rem' }}>
Plus
</Text>
</div>
</div>
<div className='flex items-center'>
<button
style={{ backgroundColor: '#bf5700', borderRadius: '8px', padding: '8px' }}
onClick={handleOpenCalendar}
>
{/* <FaCalendarAlt color='white' /> */}
</button>
<button
style={{
backgroundColor: 'white',
marginLeft: '10px',
borderRadius: '8px',
padding: '8px',
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)',
}}
onClick={handleOpenOptions}
>
{/* <FaCog color='#C05621' /> */}
</button>
</div>
</div>
<Divider color='#E2E8F0' type='solid' style={{ margin: '1rem 0' }} />
<div
className='mb-4 rounded-lg bg-white p-2 text-left shadow-inner'
style={{ backgroundColor: 'white', border: '1px solid #FBD38D', borderRadius: '0.5rem' }}
>
<Text as='div' variant='h2-course' style={{ color: '#DD6B20', fontSize: '1.2rem' }}>
MAIN SCHEDULE:
</Text>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'start', color: '#333f48' }}>
<Text
as='div'
variant='h1'
style={{ fontSize: '1.2rem', fontWeight: 'bold', marginRight: '0.5rem' }}
>
22 HOURS
</Text>
<Text as='div' variant='h2-course' style={{ fontSize: '1.2rem' }}>
8 Courses
</Text>
</div>
</div>
{/* Integrate the List component here */}
{activeSchedule ? (
<List
draggableElements={draggableElements}
itemHeight={100} // Adjust based on your content size
listHeight={500} // Adjust based on total height you want for the list
listWidth={350} // Adjust based on your layout/design
gap={12} // Spacing between items
/>
) : null}
<div className='mt-4 flex justify-between border-t border-gray-200 p-4 text-xs'>
<div className='flex items-center'>
<div
style={{
backgroundColor: '#6B7280',
padding: '1px',
borderRadius: '4px',
marginRight: '3px',
marginLeft: '8px',
}}
>
<StatusIcon status={Status.WAITLISTED} className='h-5 w-5 text-white' />
</div>
<Text as='span' variant='mini'>
WAITLISTED
</Text>
</div>
<div className='flex items-center'>
<div
style={{
backgroundColor: '#6B7280',
padding: '1px',
borderRadius: '4px',
marginRight: '3px',
marginLeft: '8px',
}}
>
<StatusIcon status={Status.CLOSED} className='h-5 w-5 text-white' />
</div>
<Text as='span' variant='mini'>
CLOSED
</Text>
</div>
<div className='flex items-center'>
<div
style={{
backgroundColor: '#6B7280',
padding: '1px',
borderRadius: '4px',
marginRight: '3px',
marginLeft: '8px',
}}
>
<StatusIcon status={Status.CANCELLED} className='h-5 w-5 text-white' />
</div>
<Text as='span' variant='mini'>
CANCELLED
</Text>
</div>
</div>
<div className='mt-2 text-center text-xs'>
<div className='inline-flex items-center justify-center'>
<Text as='div' variant='mini'>
DATA UPDATED ON: 12:00 AM 02/01/2024
</Text>
{/* <FaRedo className='ml-2 h-4 w-4 text-gray-600' /> */}
</div>
</div>
</div>
</ExtensionRoot>
);
}

View File

@@ -0,0 +1,14 @@
import React from 'react';
type Props = {
className?: string;
};
/**
* Component to hold everything for the settings page
* @param props className
* @returns The content for the settings page
*/
export default function Settings({ className }: Props) {
return <div className={className}>this will be finished laterrrrrrr</div>;
}

View File

@@ -0,0 +1,40 @@
import React from 'react';
import CalendarHeader from 'src/views/components/calendar/CalendarHeader/CalenderHeader';
import { CalendarBottomBar } from '../CalendarBottomBar/CalendarBottomBar';
import { CalendarSchedules } from '../CalendarSchedules/CalendarSchedules';
import ImportantLinks from '../ImportantLinks';
import CalendarGrid from '../CalendarGrid/CalendarGrid';
export const flags = ['WR', 'QR', 'GC', 'CD', 'E', 'II'];
interface Props {
label: string;
}
/**
* A reusable chip component that follows the design system of the extension.
* @returns
*/
export function Calendar(): JSX.Element {
return (
<>
<CalendarHeader />
<div className='h-screen w-full flex flex-col md:flex-row'>
<div className='min-h-[30%] flex flex-col items-start gap-2.5 p-5 pl-7'>
<div className='min-h-[30%]'>
<CalendarSchedules />
</div>
<ImportantLinks />
</div>
<div className='flex flex-grow flex-col gap-4 overflow-hidden'>
<div className='flex-grow overflow-auto'>
<CalendarGrid />
</div>
<div>
<CalendarBottomBar />
</div>
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,44 @@
import React from 'react';
import clsx from 'clsx';
import Text from '../../common/Text/Text';
import CalendarCourseBlock, { CalendarCourseCellProps } from '../CalendarCourseCell/CalendarCourseCell';
import { Button } from '../../common/Button/Button';
import ImageIcon from '~icons/material-symbols/image';
import CalendarMonthIcon from '~icons/material-symbols/calendar-month';
type CalendarBottomBarProps = {
courses?: CalendarCourseCellProps[];
};
/**
*
*/
export const CalendarBottomBar = ({ courses }: CalendarBottomBarProps): JSX.Element => {
if (courses?.length === -1) console.log('foo'); // dumb line to make eslint happy
return (
<div className='w-full flex py-1.25'>
<div className='flex flex-grow items-center gap-3.75 pl-7.5 pr-2.5'>
<Text variant='h4'>Async. and Other:</Text>
<div className='h-14 inline-flex gap-2.5'>
{courses?.map(course => (
<CalendarCourseBlock
courseDeptAndInstr={course.courseDeptAndInstr}
status={course.status}
colors={course.colors}
key={course.courseDeptAndInstr}
className={clsx(course.className, 'w-35!')}
/>
))}
</div>
</div>
<div className='flex items-center pl-2.5 pr-7.5'>
<Button variant='single' color='ut-black' icon={CalendarMonthIcon}>
Save as .CAL
</Button>
<Button variant='single' color='ut-black' icon={ImageIcon}>
Save as .PNG
</Button>
</div>
</div>
);
};

View File

@@ -0,0 +1,34 @@
.component {
display: flex;
padding: 7px 7px 9px 7px;
flex-direction: column;
align-items: flex-start;
gap: 5px;
flex: 1 0 0;
align-self: stretch;
border-radius: 4px;
background: #cbd5e1;
.content {
display: flex;
gap: 7px;
.course-detail {
display: flex;
flex-direction: column;
gap: 5px;
width: 154px;
.course {
font-size: 16px;
line-height: 16px;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.time-and-location {
font-size: 11px;
line-height: 11px;
font-weight: 400;
}
}
}
}

View File

@@ -0,0 +1,49 @@
import React from 'react';
import { Course } from 'src/shared/types/Course';
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
import styles from './CalendarCourseMeeting.module.scss';
/**
* Props for the CalendarCourseMeeting component.
*/
export interface CalendarCourseMeetingProps {
/** The Course that the meeting is for. */
course: Course;
/* index into course meeting array to display */
meetingIdx?: number;
/** The background color for the course. */
color: string;
/** The icon to display on the right side of the course. This is optional. */
rightIcon?: React.ReactNode;
}
/**
* `CalendarCourseMeeting` is a functional component that displays a course meeting.
*
* @example
* <CalendarCourseMeeting course={course} meeting={meeting} color="red" rightIcon={<Icon />} />
*/
const CalendarCourseMeeting: React.FC<CalendarCourseMeetingProps> = ({
course,
meetingIdx,
}: CalendarCourseMeetingProps) => {
let meeting: CourseMeeting | null = meetingIdx !== undefined ? course.schedule.meetings[meetingIdx] : null;
return (
<div className={styles.component}>
<div className={styles.content}>
<div className={styles['course-detail']}>
<div className={styles.course}>
{course.department} {course.number} - {course.instructors[0].lastName}
</div>
<div className={styles['time-and-location']}>
{`${meeting.getTimeString({ separator: '-', capitalize: true })}${
meeting.location ? ` - ${meeting.location.building}` : ''
}`}
</div>
</div>
</div>
</div>
);
};
export default CalendarCourseMeeting;

View File

@@ -0,0 +1,91 @@
import { Status } from '@shared/types/Course';
import clsx from 'clsx';
import React from 'react';
import type { CourseColors } from 'src/shared/util/colors';
import { pickFontColor } from 'src/shared/util/colors';
import ClosedIcon from '~icons/material-symbols/lock';
import WaitlistIcon from '~icons/material-symbols/timelapse';
import CancelledIcon from '~icons/material-symbols/warning';
import Text from '../../common/Text/Text';
export interface CalendarCourseCellProps {
courseDeptAndInstr: string;
timeAndLocation?: string;
status: Status;
colors: CourseColors;
className?: string;
popup?: any;
}
const CalendarCourseCell: React.FC<CalendarCourseCellProps> = ({
courseDeptAndInstr,
timeAndLocation,
status,
colors,
className,
popup,
}: CalendarCourseCellProps) => {
const [showPopup, setShowPopup] = React.useState(false);
let rightIcon: React.ReactNode | null = null;
if (status === Status.WAITLISTED) {
rightIcon = <WaitlistIcon className='h-5 w-5' />;
} else if (status === Status.CLOSED) {
rightIcon = <ClosedIcon className='h-5 w-5' />;
} else if (status === Status.CANCELLED) {
rightIcon = <CancelledIcon className='h-5 w-5' />;
}
// popup.onClose = () => setShowPopup(false);
const handleClick = () => {
setShowPopup(true);
};
// whiteText based on secondaryColor
const fontColor = pickFontColor(colors.primaryColor);
return (
<div
className={clsx(
'h-full w-full flex justify-center rounded p-2 overflow-x-hidden cursor-pointer',
fontColor,
className
)}
style={{
backgroundColor: colors.primaryColor,
}}
onClick={handleClick}
>
<div className='flex flex-1 flex-col gap-1'>
<Text
variant='h1-course'
className={clsx('-my-0.8 leading-tight', {
truncate: timeAndLocation,
})}
>
{courseDeptAndInstr}
</Text>
{timeAndLocation && (
<Text variant='h3-course' className='-mb-0.5'>
{timeAndLocation}
</Text>
)}
</div>
{rightIcon && (
<div
className='h-fit flex items-center justify-center justify-self-start rounded p-0.5 text-white'
style={{
backgroundColor: colors.secondaryColor,
}}
>
{rightIcon}
</div>
)}
<div>{showPopup ? popup : null}</div>
</div>
);
};
export default CalendarCourseCell;

View File

@@ -0,0 +1,140 @@
.dayLabelContainer {
display: flex;
flex-direction: row;
height: 13px;
min-width: 40px;
min-height: 13px;
padding-bottom: 15px;
justify-content: center;
align-items: center;
gap: 10px;
flex: 1 0 0;
}
.calendarGrid {
display: grid;
grid-template-columns: auto repeat(5, 6fr);
grid-template-rows: repeat(26, 1fr);
width: 100%;
height: 100%;
}
.calendarRow {
display: flex;
}
.calendar {
display: flex;
flex-direction: column;
gap: 10px;
position: relative;
width: 100%;
height: 100%;
}
.day {
gap: 5px;
color: #bf5700;
text-align: center;
font-size: 14.22px;
font-style: normal;
font-weight: 500;
line-height: normal;
margin-top: 20px;
border-right: 1px solid #dadce0;
border-bottom: 1px solid #dadce0;
border-left: 1px solid #dadce0;
}
.timeAndGrid {
display: flex;
}
.timeColumn {
display: flex;
min-height: 573px;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
flex: 1 0 0;
border-radius: 0px;
}
.timeBlock {
display: flex;
width: auto;
height: auto;
flex-direction: column;
align-items: flex-end;
.timeLabelContainer {
display: flex;
max-height: 20px;
flex-direction: column;
align-items: flex-end;
gap: 17px;
flex: 1 0 0;
border-radius: var(--border-radius-none, 0px);
}
p {
color: #1a2024;
text-align: left;
height: 6.6px;
align-self: stretch;
margin-top: -10px;
margin-right: 10px;
margin-bottom: 0;
/* Type scale/small */
font-family: 'Roboto Flex';
font-size: 14.22px;
font-style: normal;
font-weight: 500;
line-height: normal;
}
}
.buttonContainer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
padding: 10px;
margin-top: auto;
}
.calendarButton {
display: flex;
align-items: center;
gap: 5px;
padding: 6px 6px;
border: none;
background-color: transparent;
color: #333;
cursor: pointer;
&:hover {
background-color: rgba(0, 0, 0, 0.1);
}
}
.buttonIcon {
height: 24px;
fill: currentColor;
}
.divider {
height: 30px;
width: 1px;
background-color: grey;
}
.dot {
height: 75%; /* 75% of the container's height */
width: 75%; /* 75% of the container's width */
background-color: #000000; /* Color of the dot */
border-radius: 50%; /* Rounds the corners into a circle */
top: 12.5%; /* Centers the dot vertically */
left: 12.5%; /* Centers the dot horizontally */
}

View File

@@ -0,0 +1,226 @@
import React, { useEffect, useRef, useState } from 'react';
// import html2canvas from 'html2canvas';
import { DAY_MAP } from 'src/shared/types/CourseMeeting';
import CourseCatalogInjectedPopup from 'src/views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
import type { CalendarGridCourse } from 'src/views/hooks/useFlattenedCourseSchedule';
import CalendarCourseCell from '../CalendarCourseCell/CalendarCourseCell';
/* import calIcon from 'src/assets/icons/cal.svg';
import pngIcon from 'src/assets/icons/png.svg';
*/
import CalendarCell from '../CalendarGridCell/CalendarGridCell';
import styles from './CalendarGrid.module.scss';
/* const daysOfWeek = Object.keys(DAY_MAP).filter(key => !['S', 'SU'].includes(key));
const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);
const grid = [];
for (let i = 0; i < 13; i++) {
const row = [];
let hour = hoursOfDay[i];
row.push(
<div key={hour} className={styles.timeBlock}>
<div className={styles.timeLabelContainer}>
<p>{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}</p>
</div>
</div>
);
row.push(Array.from({ length: 5 }, (_, j) => <CalendarCell key={j} />));
grid.push(row);
} */
interface Props {
courseCells?: CalendarGridCourse[];
saturdayClass?: boolean;
}
/**
* Grid of CalendarGridCell components forming the user's course schedule calendar view
* @param props
*/
function CalendarGrid({ courseCells, saturdayClass }: React.PropsWithChildren<Props>): JSX.Element {
const [grid, setGrid] = useState([]);
const calendarRef = useRef(null); // Create a ref for the calendar grid
const daysOfWeek = Object.keys(DAY_MAP).filter(key => !['S', 'SU'].includes(key));
const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);
/* const saveAsPNG = () => {
htmlToImage
.toPng(calendarRef.current, {
backgroundColor: 'white',
style: {
background: 'white',
marginTop: '20px',
marginBottom: '20px',
marginRight: '20px',
marginLeft: '20px',
},
})
.then(dataUrl => {
let img = new Image();
img.src = dataUrl;
fetch(dataUrl)
.then(response => response.blob())
.then(blob => {
const href = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = href;
link.download = 'my-schedule.png';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
})
.catch(error => console.error('Error downloading file:', error));
})
.catch(error => {
console.error('oops, something went wrong!', error);
});
}; */
useEffect(() => {
const newGrid = [];
for (let i = 0; i < 13; i++) {
const row = [];
let hour = hoursOfDay[i];
let styleProp = {
gridColumn: '1',
gridRow: `${2 * i + 2}`,
};
row.push(
<div key={hour} className={styles.timeBlock} style={styleProp}>
<div className={styles.timeLabelContainer}>
<p>{(hour % 12 === 0 ? 12 : hour % 12) + (hour < 12 ? ' AM' : ' PM')}</p>
</div>
</div>
);
for (let k = 0; k < 5; k++) {
// let shouldRender = false;
styleProp = {
gridColumn: `${k + 2}`,
gridRow: `${2 * i + 2} / ${2 * i + 4}`,
};
/* let shouldRenderChild = courseCells[iterator]?.calendarGridPoint &&
k === courseCells[iterator].calendarGridPoint.dayIndex && i === courseCells[iterator].calendarGridPoint.startIndex;
let childElement = <div className={styles.dot}/>; */
/* let completeGridCell = shouldRenderChild ? <CalendarCell key={k} children={childElement}/>
: <CalendarCell key={k} />; */
row.push(<CalendarCell key={k} styleProp={styleProp} />);
}
newGrid.push(row);
}
setGrid(newGrid);
}, []);
return (
<div className={styles.calendarGrid}>
{/* Displaying day labels */}
<div className={styles.timeBlock} />
{daysOfWeek.map(day => (
<div key={day} className={styles.day}>
{day}
</div>
))}
{grid.map((row, rowIndex) => row)}
{courseCells ? <AccountForCourseConflicts courseCells={courseCells} /> : null}
{/* courseCells.map((block: CalendarGridCourse) => (
<div
key={`${block}`}
style={{
gridColumn: `${block.calendarGridPoint.dayIndex + 1}`,
gridRow: `${block.calendarGridPoint.startIndex + 1} / ${block.calendarGridPoint.endIndex + 1}`,
}}
>
<CalendarCourseCell
courseDeptAndInstr={block.componentProps.courseDeptAndInstr}
timeAndLocation={block.componentProps.timeAndLocation}
status={block.componentProps.status}
colors={block.componentProps.colors}
/>
</div>
)) */}
</div>
);
}
export default CalendarGrid;
interface AccountForCourseConflictsProps {
courseCells: CalendarGridCourse[];
}
function AccountForCourseConflicts({ courseCells }: AccountForCourseConflictsProps): JSX.Element[] {
// Groups by dayIndex to identify overlaps
const days = courseCells.reduce((acc, cell: CalendarGridCourse) => {
const { dayIndex } = cell.calendarGridPoint;
if (!acc[dayIndex]) {
acc[dayIndex] = [];
}
acc[dayIndex].push(cell);
return acc;
}, {});
// Check for overlaps within each day and adjust gridColumnIndex and totalColumns
Object.values(days).forEach((dayCells: CalendarGridCourse[]) => {
// Sort by start time to ensure proper columnIndex assignment
dayCells.sort((a, b) => a.calendarGridPoint.startIndex - b.calendarGridPoint.startIndex);
dayCells.forEach((cell, _, arr) => {
let columnIndex = 1;
cell.totalColumns = 1;
// Check for overlaps and adjust columnIndex as needed
for (let otherCell of arr) {
if (otherCell !== cell) {
const isOverlapping =
otherCell.calendarGridPoint.startIndex < cell.calendarGridPoint.endIndex &&
otherCell.calendarGridPoint.endIndex > cell.calendarGridPoint.startIndex;
if (isOverlapping) {
console.log('Found overlapping element');
// Adjust columnIndex to not overlap with the otherCell
if (otherCell.gridColumnStart && otherCell.gridColumnStart >= columnIndex) {
columnIndex = otherCell.gridColumnStart + 1;
console.log(columnIndex);
}
cell.totalColumns += 1;
}
}
}
cell.gridColumnStart = columnIndex;
cell.gridColumnEnd = columnIndex + 1;
});
});
return courseCells.map(block => (
<div
key={`${block}`}
style={{
gridColumn: `${block.calendarGridPoint.dayIndex + 1}`,
gridRow: `${block.calendarGridPoint.startIndex + 1} / ${block.calendarGridPoint.endIndex + 1}`,
width: `calc(100% / ${block.totalColumns})`,
marginLeft: `calc(100% * ${(block.gridColumnStart - 1) / block.totalColumns})`,
padding: '0px 10px 4px 0px',
}}
>
<CalendarCourseCell
courseDeptAndInstr={block.componentProps.courseDeptAndInstr}
timeAndLocation={block.componentProps.timeAndLocation}
status={block.componentProps.status}
colors={block.componentProps.colors}
popup={<CourseCatalogInjectedPopup course={block.popupProps.course} activeSchedule={block.popupProps.activeSchedule} onClose={block.popupProps.onClose} />}
/>
</div>
));
}
/* <div className={styles.buttonContainer}>
<div className={styles.divider} />
<button className={styles.calendarButton}>
<img src={calIcon} className={styles.buttonIcon} alt='CAL' />
Save as .CAL
</button>
<div className={styles.divider} />
<button onClick={saveAsPNG} className={styles.calendarButton}>
<img src={pngIcon} className={styles.buttonIcon} alt='PNG' />
Save as .PNG
</button>
</div> */

View File

@@ -0,0 +1,18 @@
.calendarCell {
display: flex;
width: 100%;
height: 100%;
min-width: 45px;
min-height: 40px;
flex-direction: column;
justify-content: center;
align-items: flex-start;
border: 1px solid #dadce0;
}
.hourLine {
width: 100%;
height: 1px;
border-radius: 0px;
background: rgba(218, 220, 224, 0.25);
}

View File

@@ -0,0 +1,21 @@
import React from 'react';
import styles from './CalendarGridCell.module.scss';
interface Props {
styleProp: any;
}
/**
* Component representing each 1 hour time block of a calendar
* @param props
*/
function CalendarCell({ styleProp }: Props): JSX.Element {
return (
<div className={styles.calendarCell} style={styleProp}>
<div className={styles.hourLine} />
</div>
);
}
export default CalendarCell;

View File

@@ -0,0 +1,54 @@
import React from 'react';
import { Status } from '@shared/types/Course';
import Divider from '../../common/Divider/Divider';
import { Button } from '../../common/Button/Button';
import Text from '../../common/Text/Text';
import MenuIcon from '~icons/material-symbols/menu';
import UndoIcon from '~icons/material-symbols/undo';
import RedoIcon from '~icons/material-symbols/redo';
import SettingsIcon from '~icons/material-symbols/settings';
import ScheduleTotalHoursAndCourses from '../../common/ScheduleTotalHoursAndCourses/ScheduleTotalHoursAndCourses';
import CourseStatus from '../../common/CourseStatus/CourseStatus';
import calIcon from 'src/assets/logo.png';
const CalendarHeader = () => (
<div className='min-h-79px min-w-672px flex px-0 py-15'>
<div className='flex flex-row gap-20'>
<div className='flex gap-10'>
<div className='flex gap-1'>
<Button variant='single' icon={MenuIcon} color='ut-gray' />
<div className='flex items-center'>
<img src={calIcon} className='min-w-[48px] max-w-[48px]' alt='UT Registration Plus Logo' />
<div className='flex flex-col whitespace-nowrap'>
<Text className='leading-trim text-cap font-roboto text-base text-ut-burntorange font-medium'>
UT Registration
</Text>
<Text className='leading-trim text-cap font-roboto text-base text-ut-orange font-medium'>
Plus
</Text>
</div>
</div>
</div>
<div className='flex flex-col'>
<ScheduleTotalHoursAndCourses scheduleName='SCHEDULE' totalHours={22} totalCourses={8} />
DATA UPDATED ON: 12:00 AM 02/01/2024
</div>
</div>
<div className='flex flex-row items-center space-x-8'>
<div className='flex flex-row space-x-4'>
<CourseStatus size='small' status={Status.WAITLISTED} />
<CourseStatus size='small' status={Status.CLOSED} />
<CourseStatus size='small' status={Status.CANCELLED} />
</div>
<div className='flex flex-row'>
<Button variant='single' icon={UndoIcon} color='ut-black' />
<Button variant='single' icon={RedoIcon} color='ut-black' />
<Button variant='single' icon={SettingsIcon} color='ut-black' />
</div>
</div>
</div>
<Divider type='solid' />
</div>
);
export default CalendarHeader;

View File

@@ -0,0 +1,36 @@
import { UserSchedule } from '@shared/types/UserSchedule';
import React, { useState } from 'react';
import AddSchedule from '~icons/material-symbols/add';
import List from '../../common/List/List';
import ScheduleListItem from '../../common/ScheduleListItem/ScheduleListItem';
import Text from '../../common/Text/Text';
export type Props = {
style?: React.CSSProperties;
dummySchedules?: UserSchedule[];
dummyActiveIndex?: number;
};
export function CalendarSchedules(props: Props) {
const [activeScheduleIndex, setActiveScheduleIndex] = useState(props.dummyActiveIndex || 0);
const [schedules, setSchedules] = useState(props.dummySchedules || []);
const scheduleComponents = schedules.map((schedule, index) => (
<ScheduleListItem active={index === activeScheduleIndex} name={schedule.name} />
));
return (
<div style={{ ...props.style }} className='items-center'>
<div className='m0 m-b-2 w-full flex justify-between'>
<Text variant='h3'>MY SCHEDULES</Text>
<div className='cursor-pointer items-center justify-center btn-transition -ml-1.5 hover:text-zinc-400'>
<Text variant='h3'>
<AddSchedule />
</Text>
</div>
</div>
<List gap={10} draggableElements={scheduleComponents} itemHeight={30} listHeight={30} listWidth={240} />
</div>
);
}

View File

@@ -0,0 +1,65 @@
import React from 'react';
import clsx from 'clsx';
import Text from '../common/Text/Text';
import OutwardArrowIcon from '~icons/material-symbols/arrow-outward';
type Props = {
className?: string;
};
/**
* The "Important Links" section of the calendar website
* @returns
*/
export default function ImportantLinks({ className }: Props) {
return (
<article className={clsx(className, 'flex flex-col gap-2')}>
<Text variant='h3'>Important Links</Text>
<a
href='https://utdirect.utexas.edu/apps/registrar/course_schedule/20242/'
className='flex items-center gap-0.5 text-ut-burntorange'
target='_blank'
rel='noreferrer'
>
<Text variant='p'>Spring Course Schedule</Text>
<OutwardArrowIcon className='h-3 w-3' />
</a>
<a
href='https://utdirect.utexas.edu/apps/registrar/course_schedule/20236/'
className='flex items-center gap-0.5 text-ut-burntorange'
target='_blank'
rel='noreferrer'
>
<Text variant='p'>Summer Course Schedule</Text>
<OutwardArrowIcon className='h-3 w-3' />
</a>
<a
href='https://utdirect.utexas.edu/registrar/ris.WBX'
className='flex items-center gap-0.5 text-ut-burntorange'
target='_blank'
rel='noreferrer'
>
<Text variant='p'>Registration Info Sheet</Text>
<OutwardArrowIcon className='h-3 w-3' />
</a>
<a
href='https://utdirect.utexas.edu/registration/chooseSemester.WBX'
className='flex items-center gap-0.5 text-ut-burntorange'
target='_blank'
rel='noreferrer'
>
<Text variant='p'>Register For Courses</Text>
<OutwardArrowIcon className='h-3 w-3' />
</a>
<a
href='https://utdirect.utexas.edu/apps/degree/audits/'
className='flex items-center gap-0.5 text-ut-burntorange'
target='_blank'
rel='noreferrer'
>
<Text variant='p'>Degree Audit</Text>
<OutwardArrowIcon className='h-3 w-3' />
</a>
</article>
);
}

View File

@@ -1,67 +0,0 @@
@use 'sass:color';
@use 'src/views/styles/colors.module.scss';
.button {
background-color: #000;
color: #fff;
padding: 10px;
margin: 10px;
border-radius: 8px;
border: none;
cursor: pointer;
transition: all 0.1s ease-in-out;
font-family: 'Inter';
display: flex;
align-items: center;
justify-content: center;
&:hover {
background-color: #fff;
color: #000;
}
&:active {
transform: scale(0.96);
}
&.disabled {
cursor: not-allowed !important;
opacity: 0.5 !important;
&:active {
transform: unset;
}
}
@each $color,
$value
in (
primary: colors.$burnt_orange,
secondary: colors.$charcoal,
tertiary: colors.$bluebonnet,
danger: colors.$speedway_brick,
warning: colors.$tangerine,
success: colors.$turtle_pond,
info: colors.$turquoise
)
{
&.#{$color} {
background-color: $value;
color: #fff;
&:hover {
background-color: color.adjust($value, $lightness: 10%);
}
&:focus-visible,
&:active {
background-color: color.adjust($value, $lightness: -10%);
}
&.disabled {
background-color: $value !important;
}
}
}
}

View File

@@ -1,15 +1,18 @@
import clsx from 'clsx';
import React from 'react';
import styles from './Button.module.scss';
import type IconComponent from '~icons/material-symbols';
import { ThemeColor, getThemeColorHexByName, getThemeColorRgbByName } from '../../../../shared/util/themeColors';
import Text from '../Text/Text';
interface Props {
className?: string;
style?: React.CSSProperties;
variant: 'filled' | 'outline' | 'single';
onClick?: () => void;
type?: 'primary' | 'secondary' | 'tertiary' | 'danger' | 'warning' | 'success' | 'info';
icon?: typeof IconComponent;
disabled?: boolean;
title?: string;
testId?: string;
color: ThemeColor;
}
/**
@@ -17,26 +20,53 @@ interface Props {
* @returns
*/
export function Button({
style,
className,
type,
testId,
children,
style,
variant,
onClick,
icon,
disabled,
title,
onClick,
color,
children,
}: React.PropsWithChildren<Props>): JSX.Element {
const Icon = icon;
const isIconOnly = !children && !!icon;
const colorHex = getThemeColorHexByName(color);
const colorRgb = getThemeColorRgbByName(color)?.join(' ');
return (
<button
style={style}
data-testid={testId}
className={clsx(styles.button, className, styles[type ?? 'primary'], {
[styles.disabled]: disabled,
})}
style={
{
...style,
color: colorHex,
backgroundColor: `rgb(${colorRgb} / var(--un-bg-opacity)`,
} as React.CSSProperties
}
className={clsx(
'btn',
{
'text-white! bg-opacity-100 hover:enabled:shadow-md active:enabled:shadow-sm shadow-black/20':
variant === 'filled',
'bg-opacity-0 border-current hover:enabled:bg-opacity-8 border': variant === 'outline',
'bg-opacity-0 border-none hover:enabled:bg-opacity-8': variant === 'single', // settings is the only "single"
'px-2 py-1.25': isIconOnly && variant !== 'outline',
'px-1.75 py-1.25': isIconOnly && variant === 'outline',
'px-3.75': variant === 'outline' && !isIconOnly,
},
className
)}
title={title}
disabled={disabled}
onClick={disabled ? undefined : onClick}
>
{children}
{icon && <Icon className='h-6 w-6' />}
{!isIconOnly && (
<Text variant='h4' className='translate-y-0.08'>
{children}
</Text>
)}
</button>
);
}

View File

@@ -1,50 +0,0 @@
import React from 'react';
import { Course, Status } from 'src/shared/types/Course';
import { CourseMeeting } from 'src/shared/types/CourseMeeting';
import ClosedIcon from '~icons/material-symbols/lock';
import WaitlistIcon from '~icons/material-symbols/timelapse';
import CancelledIcon from '~icons/material-symbols/warning';
import Text from '../Text/Text';
export interface CalendarCourseBlockProps {
/** The Course that the meeting is for. */
course: Course;
/* index into course meeting array to display */
meetingIdx?: number;
/** The background color for the course. */
color: string;
}
const CalendarCourseBlock: React.FC<CalendarCourseBlockProps> = ({ course, meetingIdx }: CalendarCourseBlockProps) => {
let meeting: CourseMeeting | null = meetingIdx !== undefined ? course.schedule.meetings[meetingIdx] : null;
let rightIcon: React.ReactNode | null = null;
if (course.status === Status.WAITLISTED) {
rightIcon = <WaitlistIcon className='h-5 w-5' />;
} else if (course.status === Status.CLOSED) {
rightIcon = <ClosedIcon className='h-5 w-5' />;
} else if (course.status === Status.CANCELLED) {
rightIcon = <CancelledIcon className='h-5 w-5' />;
}
return (
<div className='w-full flex justify-center rounded bg-slate-300 p-2 text-ut-black'>
<div className='flex flex-1 flex-col gap-1'>
<Text variant='h1-course' className='leading-[75%]!'>
{course.department} {course.number} - {course.instructors[0].lastName}
</Text>
<Text variant='h3-course' className='leading-[75%]!'>
{`${meeting.getTimeString({ separator: '', capitalize: true })}${
meeting.location ? ` ${meeting.location.building}` : ''
}`}
</Text>
</div>
{rightIcon && (
<div className='h-fit flex items-center justify-center justify-self-start rounded bg-slate-700 p-0.5 text-white'>
{rightIcon}
</div>
)}
</div>
);
};
export default CalendarCourseBlock;

View File

@@ -1,94 +0,0 @@
@use 'sass:color';
@use 'src/views/styles/colors.module.scss';
.dayLabelContainer {
display: flex;
flex-direction: row;
height: 13px;
min-width: 40px;
min-height: 13px;
padding-bottom: 15px;
justify-content: center;
align-items: center;
gap: 10px;
flex: 1 0 0;
}
.calendarGrid {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(13, 1fr);
}
.calendarRow {
display: flex;
}
.calendar {
// display: grid;
// grid-template-columns: auto repeat(5, 1fr);
gap: 10px;
}
.day {
gap: 5px;
color: colors.$burnt_orange;
text-align: center;
font-size: 14.22px;
font-style: normal;
font-weight: 500;
line-height: normal;
}
.timeAndGrid {
display: flex;
}
.timeColumn {
display: flex;
min-height: 573px;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
flex: 1 0 0;
border-radius: var(--border-radius-none, 0px);
}
.timeBlock {
display: flex;
width: 50px;
height: 40.279px;
min-width: 50px;
max-width: 50px;
min-height: 40px;
flex-direction: column;
align-items: flex-end;
.timeLabelContainer {
display: flex;
max-height: 20px;
flex-direction: column;
align-items: flex-end;
gap: 17px;
flex: 1 0 0;
align-self: stretch;
border-radius: var(--border-radius-none, 0px);
}
p {
color: #1A2024;
text-align: left;
height: 6.6px;
align-self: stretch;
margin-top: 0;
margin-bottom: 0;
/* Type scale/small */
font-family: "Roboto Flex";
font-size: 14.22px;
font-style: normal;
font-weight: 500;
line-height: normal;
}
}

View File

@@ -1,52 +0,0 @@
import React from 'react';
import styles from './CalendarGrid.module.scss';
import CalendarCell from '../CalendarGridCell/CalendarGridCell';
import { DAY_MAP } from 'src/shared/types/CourseMeeting';
const daysOfWeek = Object.values(DAY_MAP).filter(d => d != "Saturday" && d != "Sunday")
const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);
const grid = Array.from({ length: 5 }, () =>
Array.from({ length: 13 }, (_, columnIndex) => (
<CalendarCell key={columnIndex} />
))
);
/**
* Grid of CalendarGridCell components forming the user's course schedule calendar view
* @param props
*/
const Calendar: React.FC = (props) => {
return (
<div className={styles.calendar}>
<div className={styles.dayLabelContainer}>
{/* Empty cell in the top-left corner */}
<div className={styles.day} />
{/* Displaying day labels */}
{daysOfWeek.map(day => (
<div key={day} className={styles.day}>
{day}
</div>
))}
</div>
{/* Displaying the rest of the calendar */}
<div className={styles.timeAndGrid}>
<div className={styles.timeColumn}>
{hoursOfDay.map((hour) => (
<div key={hour} className={styles.timeBlock}>
<div className={styles.timeLabelContainer}>
<p>{hour % 12 === 0 ? 12 : hour % 12}:00 {hour < 12 ? 'AM' : 'PM'}</p>
</div>
</div>
))}
</div>
<div className={styles.calendarGrid}>
{grid.map((row, rowIndex) => (
row
))}
</div>
</div>
</div>
)
};
export default Calendar;

View File

@@ -1,18 +0,0 @@
.calendarCell {
display: flex;
width: 165px;
height: 52.231px;
min-width: 45px;
min-height: 40px;
flex-direction: column;
justify-content: center;
align-items: flex-start;
border: 1px solid #DADCE0;
}
.hourLine {
width: 165px;
height: 1px;
border-radius: var(--border-radius-none, 0px);
background: rgba(218, 220, 224, 0.25);
}

View File

@@ -1,18 +0,0 @@
import React from 'react';
import styles from './CalendarGridCell.module.scss';
/**
* Component representing each 1 hour time block of a calendar
* @param props
*/
const CalendarCell: React.FC = (props) => {
return (
<div className={styles.calendarCell}>
<div className={styles.hourLine}>
</div>
</div>
);
};
export default CalendarCell;

View File

@@ -0,0 +1,38 @@
import React from 'react';
import Text from '../Text/Text';
/**
* A type that represents the flags that a course can have.
*/
export type Flag = 'WR' | 'QR' | 'GC' | 'CD' | 'E' | 'II';
export const flagMap: Record<string, Flag> = {
Writing: 'WR',
'Quantitative Reasoning': 'QR',
'Global Cultures': 'GC',
'Cultural Diversity in the United States': 'CD',
Ethics: 'E',
'Independent Inquiry': 'II',
};
interface Props {
label: Flag;
}
/**
* A reusable chip component that follows the design system of the extension.
* @returns
*/
export function Chip({ label }: React.PropsWithChildren<Props>): JSX.Element {
return (
<Text
as='div'
variant='h4'
className='min-w-5 inline-flex items-center justify-center gap-2.5 rounded-lg px-1 py-0.5'
style={{
backgroundColor: '#FFD600',
}}
>
{label}
</Text>
);
}

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { Course } from 'src/shared/types/Course';
import clsx from 'clsx';
import Text from '../Text/Text';
/**
* Props for ConflictWithWarningProps
*/
export interface ConflictsWithWarningProps {
className?: string;
conflicts: Course[];
}
/**
* The ConflictsWithWarning component is used to display a warning message when a course conflicts
* with another course as part of the labels and details section
*
* @param props ConflictsWithWarningProps
*/
export default function ConflictsWithWarning({ className, conflicts }: ConflictsWithWarningProps): JSX.Element {
return (
<Text
variant='mini'
className={clsx(
className,
'min-w-21 w-21 flex flex-col items-start gap-2.5 rounded bg-[#AF2E2D] p-2.5 text-white'
)}
>
<div>Conflicts With:</div>
{conflicts.map(course => (
<div>
{`${course.department} ${course.number} (${course.uniqueId})`}
</div>
))}
</Text>
);
}

View File

@@ -0,0 +1,110 @@
import { Disclosure, Transition } from '@headlessui/react';
import { UserSchedule } from '@shared/types/UserSchedule';
import React from 'react';
import userScheduleHandler from 'src/pages/background/handler/userScheduleHandler';
import DropdownArrowDown from '~icons/material-symbols/arrow-drop-down';
import DropdownArrowUp from '~icons/material-symbols/arrow-drop-up';
import List from '../List/List';
import Text from '../Text/Text';
export type Props = {
style?: React.CSSProperties;
// Dummy value solely for storybook
dummySchedules?: UserSchedule[];
dummyActiveIndex?: number;
dummyActiveSchedule?: UserSchedule;
scheduleComponents?: any[];
};
/**
* This is a reusable dropdown component that can be used to toggle the visiblity of information
*/
export default function Dropdown(props: Props) {
// Expand/Hide state for dropdown
let [expanded, toggle] = React.useState(false);
let [activeScheduleIndex, select] = React.useState(props.dummyActiveIndex);
let [activeSchedule, selectFrom] = React.useState(props.dummyActiveSchedule);
let [scheduleComponents, setScheduleComponents] = React.useState(props.scheduleComponents);
const schedules = props.dummySchedules;
if (schedules == null) {
// if no dummy values passed in
// useSchedules hook here
}
const toggleSwitch = () => {
toggle(!expanded);
};
// WIP function to swap schedules. Prefer to use the hook when in production
const switchSchedule = (index: number) => {
const scheduleToSwitchTo = schedules[index];
select(index);
selectFrom(scheduleToSwitchTo);
if (scheduleToSwitchTo != null && scheduleToSwitchTo.name != null) {
userScheduleHandler.switchSchedule({
data: {
scheduleName: scheduleToSwitchTo.name,
},
sender: null,
sendResponse: null,
});
}
};
return (
<div
style={{ ...props.style, height: expanded && schedules ? `${40 * schedules.length + 54}px` : '72px' }}
className='items-left absolute w-72 flex flex-col border'
>
<Disclosure>
<Disclosure.Button>
<div className='flex items-center border-none bg-white p-3 text-left'>
<div className='flex-1'>
<Text as='div' variant='h4' className='text-ut-burntorange mb-1 w-100%'>
MAIN SCHEDULE:
</Text>
<div>
<Text variant='h3' className='text-theme-black leading-[75%]!'>
{activeSchedule ? activeSchedule.hours : 0} HOURS
</Text>
<Text variant='h4' className='ml-2.5 text-ut-black leading-[75%]!'>
{activeSchedule ? activeSchedule.courses.length : 0} Courses
</Text>
</div>
</div>
<Text className='text-ut-burntorange text-2xl font-normal'>
{expanded ? <DropdownArrowDown /> : <DropdownArrowUp />}
</Text>
</div>
</Disclosure.Button>
<Transition
enter='transition duration-100 ease-out'
enterFrom='transform scale-95 opacity-0'
enterTo='transform scale-100 opacity-100'
leave='transition duration-75 ease-out'
leaveFrom='transform scale-100 opacity-100'
leaveTo='transform scale-95 opacity-0'
beforeEnter={() => {
toggleSwitch();
}}
afterLeave={() => {
toggleSwitch();
}}
>
<Disclosure.Panel>
<List
draggableElements={scheduleComponents}
itemHeight={30}
listHeight={30}
listWidth={240}
gap={10}
/>
</Disclosure.Panel>
</Transition>
</Disclosure>
</div>
);
}

View File

@@ -1,7 +1,7 @@
@import 'src/views/styles/base.module.scss';
.extensionRoot {
font-family: 'Inter' !important;
@apply font-sans;
color: #303030;
-webkit-box-sizing: border-box;
box-sizing: border-box;

View File

@@ -0,0 +1,40 @@
import React from 'react';
import Text from '../Text/Text';
interface Props {
titleText: string;
bodyText: string;
}
/**
* A maybe reusable InfoCard component that follows the design system of the extension.
* @returns
*/
export function InfoCard({
titleText,
bodyText
}: React.PropsWithChildren<Props>): JSX.Element {
return (
<div
className = 'w-50 flex flex-col items-start justify-center border rounded p-4'
style = {{
border: "1px solid #D6D2C4",
background: "#FFF" // White
}}>
<div className="flex flex-col items-start self-stretch gap-1.5">
<Text variant = "h4" as = 'span'
style = {{
color: '#F8971F', // Orange
}}>
{titleText}
</Text>
<Text variant = "small" as = 'span'
style = {{
color: '#333F48', // Black
}}>
{bodyText}
</Text>
</ div>
</div>
);
}

View File

@@ -28,7 +28,7 @@ export default function Link(props: PropsWithChildren<Props>) {
<Text
color='bluebonnet'
{...passedProps}
span
as='span'
className={clsx(
styles.link,
{

View File

@@ -0,0 +1,169 @@
import { DragDropContext, Draggable, Droppable } from '@hello-pangea/dnd';
import type { ReactElement } from 'react';
import React, { useCallback, useState } from 'react';
import { areEqual } from 'react-window';
/*
* Ctrl + f dragHandleProps on PopupCourseBlock.tsx for example implementation of drag handle (two lines of code)
*
*/
/**
* Props for the List component.
*/
export interface ListProps {
draggableElements: any[]; // Will later define draggableElements based on what types
// of components are draggable.
itemHeight: number;
listHeight: number;
listWidth: number;
gap: number; // Impacts the spacing between items in the list
}
function initial(draggableElements: any[] = []) {
return draggableElements.map((element, index) => ({
id: `id:${index}`,
content: element as ReactElement,
}));
}
function reorder(list: { id: string; content: ReactElement }[], startIndex: number, endIndex: number) {
const result = Array.from(list);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
return result;
}
function getStyle({ provided, style /* , isDragging, gap */ }) {
const combined = {
...style,
...provided.draggableProps.style,
};
return combined;
}
function Item({ provided, item, style, isDragging /* , gap */ }) {
return (
<div
{...provided.draggableProps}
ref={provided.innerRef}
style={getStyle({ provided, style /* , isDragging, gap */ })}
className={`item ${isDragging ? 'is-dragging' : ''}`}
>
{React.cloneElement(item.content, { dragHandleProps: provided.dragHandleProps })}
</div>
);
}
interface RowProps {
data: any; // DraggableElements[]; Need to define DraggableElements interface once those components are ready
index: number;
style: React.CSSProperties;
}
const Row: React.FC<RowProps> = React.memo(({ data: { items, gap }, index, style }) => {
const item = items[index];
const adjustedStyle = {
...style,
height: `calc(${style.height}px - ${gap}px)`, // Reduce the height by gap to accommodate the margin
marginBottom: `${gap}px`, // Add gap as bottom margin
};
return (
<Draggable draggableId={item.id} index={index} key={item.id}>
{/* @ts-ignore */}
{provided => <Item provided={provided} item={item} style={adjustedStyle} gap={gap} />}
</Draggable>
);
}, areEqual);
/**
* `List` is a functional component that displays a course meeting.
*
* @example
* <List draggableElements={elements} />
*/
const List: React.FC<ListProps> = ({ draggableElements, itemHeight, listHeight, listWidth, gap = 12 }: ListProps) => {
const [items, setItems] = useState(() => initial(draggableElements));
const onDragEnd = useCallback(
result => {
if (!result.destination) {
return;
}
if (result.source.index === result.destination.index) {
return;
}
const newItems = reorder(items, result.source.index, result.destination.index);
setItems(newItems as { id: string; content: React.ReactElement }[]);
},
[items]
);
return (
<div style={{ overflow: 'hidden', width: listWidth }}>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable
droppableId='droppable'
direction='vertical'
renderClone={(provided, snapshot, rubric) => {
let { style } = provided.draggableProps;
const transform = style?.transform;
if (snapshot.isDragging && transform) {
console.log(transform);
let [, _x, y] = transform.match(/translate\(([-\d]+)px, ([-\d]+)px\)/) || [];
style.transform = `translate3d(0px, ${y}px, 0px)`; // Apply constrained y value
}
return (
<Item
provided={provided}
isDragging={snapshot.isDragging}
item={items[rubric.source.index]}
style={{
style,
}}
/>
);
}}
>
{provided => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
style={{ width: `${listWidth}px`, marginBottom: `-${gap}px` }}
>
{items.map((item, index) => (
<Draggable key={item.id} draggableId={item.id} index={index}>
{draggableProvided => (
<div
ref={draggableProvided.innerRef}
{...draggableProvided.draggableProps}
style={{
...draggableProvided.draggableProps.style,
// if last item, don't add margin
marginBottom: `${gap}px`,
}}
>
{React.cloneElement(item.content, {
dragHandleProps: draggableProvided.dragHandleProps,
})}
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
);
};
export default List;

View File

@@ -1,5 +1,7 @@
import clsx from 'clsx';
import React, { PropsWithChildren, useCallback } from 'react';
import type { PropsWithChildren } from 'react';
import React, { useCallback } from 'react';
import styles from './Popup.module.scss';
interface Props {
@@ -19,8 +21,6 @@ export default function Popup({ onClose, children, className, style, testId, ove
const containerRef = React.useRef<HTMLDivElement>(null);
const bodyRef = React.useRef<HTMLDivElement>(null);
const handleClickOutside = useCallback(
(event: MouseEvent) => {
if (!bodyRef.current) return;

View File

@@ -1,8 +1,8 @@
import clsx from 'clsx';
import React, { useState } from 'react';
import { Course, Status } from '@shared/types/Course';
import { CourseColors, pickFontColor } from '@shared/util/colors';
import { StatusIcon } from '@shared/util/icons';
import { CourseColors, getCourseColors, pickFontColor } from '@shared/util/colors';
import clsx from 'clsx';
import React from 'react';
import DragIndicatorIcon from '~icons/material-symbols/drag-indicator';
import Text from '../Text/Text';
@@ -13,6 +13,7 @@ export interface PopupCourseBlockProps {
className?: string;
course: Course;
colors: CourseColors;
dragHandleProps?: any;
}
/**
@@ -20,7 +21,12 @@ export interface PopupCourseBlockProps {
*
* @param props PopupCourseBlockProps
*/
export default function PopupCourseBlock({ className, course, colors }: PopupCourseBlockProps): JSX.Element {
export default function PopupCourseBlock({
className,
course,
colors,
dragHandleProps,
}: PopupCourseBlockProps): JSX.Element {
// whiteText based on secondaryColor
const fontColor = pickFontColor(colors.primaryColor);
@@ -36,13 +42,11 @@ export default function PopupCourseBlock({ className, course, colors }: PopupCou
backgroundColor: colors.secondaryColor,
}}
className='flex cursor-move items-center self-stretch rounded rounded-r-0'
{...dragHandleProps}
>
<DragIndicatorIcon className='h-6 w-6 text-white' />
</div>
<Text
className={clsx('flex-1 py-3.5 text-ellipsis whitespace-nowrap overflow-hidden', fontColor)}
variant='h1-course'
>
<Text className={clsx('flex-1 py-3.5 truncate', fontColor)} variant='h1-course'>
<span className='px-0.5 font-450'>{course.uniqueId}</span> {course.department} {course.number} &ndash;{' '}
{course.instructors.length === 0 ? 'Unknown' : course.instructors.map(v => v.lastName)}
</Text>

View File

@@ -0,0 +1,46 @@
import clsx from 'clsx';
import React from 'react';
import DragIndicatorIcon from '~icons/material-symbols/drag-indicator';
import Text from '../Text/Text';
export type Props = {
style?: React.CSSProperties;
active?: boolean;
name: string;
dragHandleProps?: any;
};
/**
* This is a reusable dropdown component that can be used to toggle the visiblity of information
*/
export default function ScheduleListItem(props: Props) {
const { dragHandleProps } = props;
console.log(props);
return (
<div style={{ ...props.style }} className='items-center'>
<li className='text-ut-burntorange w-100% flex cursor-pointer items-center self-stretch justify-left'>
<div className='group flex justify-center'>
<div
className='flex cursor-move items-center self-stretch rounded rounded-r-0'
{...dragHandleProps}
>
<DragIndicatorIcon className='h-6 w-6 cursor-move text-zinc-300 btn-transition -ml-1.5 hover:text-zinc-400' />
</div>
<div className='inline-flex items-center justify-center gap-1.5'>
<div className='h-5.5 w-5.5 flex items-center justify-center border-2px border-current rounded-full btn-transition group-active:scale-95'>
<div
className={clsx(
'bg-current h-3 w-3 rounded-full transition tansform scale-100 ease-out-expo duration-250',
{
'scale-0! opacity-0 ease-in-out! duration-200!': !props.active,
}
)}
/>
</div>
<Text variant='p'>{props.name}</Text>
</div>
</div>
</li>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import React from 'react';
import Text from '../Text/Text';
/**
* Props for ScheduleTotalHoursAndCourses
*/
export interface ScheduleTotalHoursAndCoursesProps {
scheduleName: string;
totalHours: number;
totalCourses: number;
}
/**
* The ScheduleTotalHoursAndCourses as per the Labels and Details Figma section
*
* @param props ScheduleTotalHoursAndCoursesProps
*/
export default function ScheduleTotalHoursAndCourses({
scheduleName,
totalHours,
totalCourses,
}: ScheduleTotalHoursAndCoursesProps): JSX.Element {
return (
<div className='min-w-64 flex whitespace-nowrap content-center items-baseline gap-2 uppercase'>
<Text className='text-[#BF5700]' variant='h1' as='span'>
{`${scheduleName}: `}
</Text>
<Text variant='h3' as='div' className='flex flex-row items-center gap-2 text-[#1A2024]'>
{`${totalHours} HOURS`}
<Text variant='h4' as='span' className='text-[#333F48]'>
{`${totalCourses} courses`}
</Text>
</Text>
</div>
);
}

View File

@@ -0,0 +1,26 @@
import Popup from '@views/components/common/Popup/Popup';
import React from 'react';
import type { Course } from 'src/shared/types/Course';
import type { UserSchedule } from 'src/shared/types/UserSchedule';
import Description from './Description';
import GradeDistribution from './GradeDistribution';
import HeadingAndActions from './HeadingAndActions';
export interface CourseCatalogInjectedPopupProps {
course: Course;
activeSchedule?: UserSchedule;
onClose: () => void;
}
const CourseCatalogInjectedPopup: React.FC<CourseCatalogInjectedPopupProps> = ({ course, activeSchedule, onClose }) => (
<Popup overlay className='max-w-[780px] px-6' onClose={onClose}>
<div className='flex flex-col'>
<HeadingAndActions course={course} onClose={onClose} activeSchedule={activeSchedule} />
<Description lines={course.description} />
<GradeDistribution course={course} />
</div>
</Popup>
);
export default CourseCatalogInjectedPopup;

View File

@@ -0,0 +1,31 @@
import clsx from 'clsx';
import React from 'react';
import Text from '../../common/Text/Text';
interface DescriptionProps {
lines: string[];
}
const Description: React.FC<DescriptionProps> = ({ lines }: DescriptionProps) => {
const keywords = ['prerequisite', 'restricted'];
return (
<ul className='my-[5px] space-y-1.5 children:marker:text-ut-burntorange'>
{lines.map(line => {
const isKeywordPresent = keywords.some(keyword => line.toLowerCase().includes(keyword));
return (
<div className='flex gap-2'>
<span className='text-ut-burntorange'></span>
<li key={line}>
<Text variant='p' className={clsx({ 'font-bold text-ut-burntorange': isKeywordPresent })}>
{line}
</Text>
</li>
</div>
);
})}
</ul>
);
};
export default Description;

View File

@@ -0,0 +1,170 @@
import Spinner from '@views/components/common/Spinner/Spinner';
import Text from '@views/components/common/Text/Text';
import Highcharts from 'highcharts';
import HighchartsReact from 'highcharts-react-official';
import React from 'react';
import type { Course } from 'src/shared/types/Course';
import type { Distribution, LetterGrade } from 'src/shared/types/Distribution';
import { colors } from 'src/shared/util/themeColors';
import {
NoDataError,
queryAggregateDistribution,
querySemesterDistribution,
} from 'src/views/lib/database/queryDistribution';
interface GradeDistributionProps {
course: Course;
}
enum DataStatus {
LOADING = 'LOADING',
FOUND = 'FOUND',
NOT_FOUND = 'NOT_FOUND',
ERROR = 'ERROR',
}
const GRADE_COLORS: Record<LetterGrade, string> = {
A: colors.gradeDistribution.a,
'A-': colors.gradeDistribution.aminus,
'B+': colors.gradeDistribution.bplus,
B: colors.gradeDistribution.b,
'B-': colors.gradeDistribution.bminus,
'C+': colors.gradeDistribution.cplus,
C: colors.gradeDistribution.c,
'C-': colors.gradeDistribution.cminus,
'D+': colors.gradeDistribution.dplus,
D: colors.gradeDistribution.d,
'D-': colors.gradeDistribution.dminus,
F: colors.gradeDistribution.f,
};
const GradeDistribution: React.FC<GradeDistributionProps> = ({ course }) => {
const [semester, setSemester] = React.useState('Aggregate');
const [distributions, setDistributions] = React.useState<Record<string, Distribution>>({});
const [status, setStatus] = React.useState(DataStatus.LOADING);
const ref = React.useRef<HighchartsReact.RefObject>(null);
const chartData = React.useMemo(() => {
if (status === DataStatus.FOUND && distributions[semester]) {
return Object.entries(distributions[semester]).map(([grade, count]) => ({
y: count,
color: GRADE_COLORS[grade as LetterGrade],
}));
}
return [];
}, [distributions, semester, status]);
React.useEffect(() => {
const fetchInitialData = async () => {
try {
const [aggregateDist, semesters] = await queryAggregateDistribution(course);
const initialDistributions: Record<string, Distribution> = { Aggregate: aggregateDist };
const semesterPromises = semesters.map(semester => querySemesterDistribution(course, semester));
const semesterDistributions = await Promise.all(semesterPromises);
semesters.forEach((semester, i) => {
initialDistributions[`${semester.season} ${semester.year}`] = semesterDistributions[i];
});
setDistributions(initialDistributions);
setStatus(DataStatus.FOUND);
} catch (e) {
console.error(e);
if (e instanceof NoDataError) {
setStatus(DataStatus.NOT_FOUND);
} else {
setStatus(DataStatus.ERROR);
}
}
};
fetchInitialData();
}, [course]);
const handleSelectSemester = (event: React.ChangeEvent<HTMLSelectElement>) => {
setSemester(event.target.value);
};
const chartOptions: Highcharts.Options = {
title: { text: undefined },
subtitle: { text: undefined },
legend: { enabled: false },
xAxis: {
title: { text: 'Grade' },
categories: ['A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F'],
crosshair: true,
},
yAxis: {
min: 0,
title: { text: 'Number of Students' },
},
chart: {
style: { fontFamily: 'Roboto Flex', fontWeight: '600' },
spacingBottom: 25,
spacingTop: 25,
height: 250,
},
credits: { enabled: false },
accessibility: { enabled: true },
tooltip: {
headerFormat: '<span style="font-size:small; font-weight:bold">{point.key}</span><table>',
pointFormat:
'<td style="color:{black};padding:0;font-size:small; font-weight:bold;"><b>{point.y:.0f} Students</b></td>',
footerFormat: '</table>',
shared: true,
useHTML: true,
},
plotOptions: {
bar: { pointPadding: 0.2, borderWidth: 0 },
series: { animation: { duration: 700 } },
},
series: [
{
type: 'column',
name: 'Grades',
data: chartData,
},
],
};
return (
<div className='pb-[25px] pt-[12px]'>
{status === DataStatus.LOADING && <Spinner />}
{status === DataStatus.NOT_FOUND && <Text variant='p'>No grade distribution data found</Text>}
{status === DataStatus.ERROR && <Text variant='p'>Error fetching grade distribution data</Text>}
{status === DataStatus.FOUND && (
<>
<div className='w-full flex items-center justify-center gap-[12px]'>
<Text variant='p'>Grade distribution for {`${course.department} ${course.number}`}</Text>
<select
className='border border rounded-[4px] border-solid px-[12px] py-[8px]'
onChange={handleSelectSemester}
>
{Object.keys(distributions)
.sort((k1, k2) => {
if (k1 === 'Aggregate') {
return -1;
}
if (k2 === 'Aggregate') {
return 1;
}
const [season1, year1] = k1.split(' ');
const [, year2] = k2.split(' ');
if (year1 !== year2) {
return parseInt(year2, 10) - parseInt(year1, 10);
}
return season1 === 'Fall' ? 1 : -1;
})
.map(semester => (
<option key={semester} value={semester}>
{semester}
</option>
))}
</select>
</div>
<HighchartsReact ref={ref} highcharts={Highcharts} options={chartOptions} />
</>
)}
</div>
);
};
export default GradeDistribution;

View File

@@ -0,0 +1,155 @@
import { Button } from '@views/components/common/Button/Button';
import { Chip, flagMap } from '@views/components/common/Chip/Chip';
import Divider from '@views/components/common/Divider/Divider';
import Text from '@views/components/common/Text/Text';
import React, { useState } from 'react';
import addCourse from 'src/pages/background/lib/addCourse';
import removeCourse from 'src/pages/background/lib/removeCourse';
import type { Course } from 'src/shared/types/Course';
import type { UserSchedule } from 'src/shared/types/UserSchedule';
import { openTabFromContentScript } from 'src/views/lib/openNewTabFromContentScript';
import Add from '~icons/material-symbols/add';
import CalendarMonth from '~icons/material-symbols/calendar-month';
import CloseIcon from '~icons/material-symbols/close';
import Copy from '~icons/material-symbols/content-copy';
import Description from '~icons/material-symbols/description';
import Mood from '~icons/material-symbols/mood';
import Remove from '~icons/material-symbols/remove';
import Reviews from '~icons/material-symbols/reviews';
interface HeadingAndActionProps {
/* The course to display */
course: Course;
/* The active schedule */
activeSchedule: UserSchedule;
/* The function to call when the popup should be closed */
onClose: () => void;
}
export const handleOpenCalendar = async () => {
// Not sure if it's bad practice to export this
const url = chrome.runtime.getURL('calendar.html');
await openTabFromContentScript(url);
};
/**
* Renders the heading component for the CoursePopup component.
*
* @param {HeadingAndActionProps} props - The component props.
* @returns {JSX.Element} The rendered component.
*/
const HeadingAndActions: React.FC<HeadingAndActionProps> = ({ course, onClose, activeSchedule }) => {
const { courseName, department, number: courseNumber, uniqueId, instructors, flags, schedule } = course;
const [courseAdded, setCourseAdded] = useState<boolean>(
activeSchedule.courses.some(course => course.uniqueId === uniqueId)
);
const instructorString = instructors
.map(instructor => {
const { firstName, lastName } = instructor;
if (firstName === '') return lastName;
return `${firstName} ${lastName}`;
})
.join(', ');
const handleCopy = () => {
navigator.clipboard.writeText(uniqueId.toString());
};
const handleOpenRateMyProf = async () => {
const openTabs = instructors.map(instructor => {
const { fullName } = instructor;
const url = `https://www.ratemyprofessors.com/search/professors/1255?q=${fullName}`;
return openTabFromContentScript(url);
});
await Promise.all(openTabs);
};
const handleOpenCES = async () => {
// TODO: does not look up the professor just takes you to the page
const cisUrl = 'https://utexas.bluera.com/utexas/rpvl.aspx?rid=d3db767b-049f-46c5-9a67-29c21c29c580&regl=en-US';
await openTabFromContentScript(cisUrl);
};
const handleOpenPastSyllabi = async () => {
// not specific to professor
const url = `https://utdirect.utexas.edu/apps/student/coursedocs/nlogon/?year=&semester=&department=${department}&course_number=${courseNumber}&course_title=${courseName}&unique=&instructor_first=&instructor_last=&course_type=In+Residence&search=Search`;
await openTabFromContentScript(url);
};
const handleAddOrRemoveCourse = async () => {
if (!courseAdded) {
await addCourse(activeSchedule.name, course);
} else {
await removeCourse(activeSchedule.name, course);
}
setCourseAdded(!courseAdded);
};
return (
<div className='w-full pb-3 pt-6'>
<div className='flex flex-col gap-1'>
<div className='flex items-center gap-1'>
<Text variant='h1' className='truncate'>
{courseName}
</Text>
<Text variant='h1' className='flex-1 whitespace-nowrap'>
{' '}
({department} {courseNumber})
</Text>
<Button color='ut-burntorange' variant='single' icon={Copy} onClick={handleCopy}>
{uniqueId}
</Button>
<button className='bg-transparent p-0 btn' onClick={onClose}>
<CloseIcon className='h-7 w-7' />
</button>
</div>
<div className='flex gap-2.5 flex-content-center'>
<Text variant='h4' className='inline-flex items-center justify-center'>
with {instructorString}
</Text>
<div className='flex-content-centr flex gap-1'>
{flags.map(flag => (
<Chip label={flagMap[flag]} />
))}
</div>
</div>
<div className='flex flex-col'>
{schedule.meetings.map(meeting => (
<Text variant='h4'>
{meeting.getDaysString({ format: 'long', separator: 'long' })}{' '}
{meeting.getTimeString({ separator: ' to ', capitalize: false })}
{meeting.location && (
<>
{` in `}
<Text variant='h4' className='text-ut-burntorange underline'>
{meeting.location.building}
</Text>
</>
)}
</Text>
))}
</div>
</div>
<div className='my-3 flex flex-wrap items-center gap-[15px]'>
<Button variant='filled' color='ut-burntorange' icon={CalendarMonth} onClick={handleOpenCalendar} />
<Divider type='solid' color='ut-offwhite' className='h-7' />
<Button variant='outline' color='ut-blue' icon={Reviews} onClick={handleOpenRateMyProf}>
RateMyProf
</Button>
<Button variant='outline' color='ut-teal' icon={Mood} onClick={handleOpenCES}>
CES
</Button>
<Button variant='outline' color='ut-orange' icon={Description} onClick={handleOpenPastSyllabi}>
Past Syllabi
</Button>
<Button
variant='filled'
color={!courseAdded ? 'ut-green' : 'ut-red'}
icon={!courseAdded ? Add : Remove}
onClick={handleAddOrRemoveCourse}
>
{!courseAdded ? 'Add Course' : 'Remove Course'}
</Button>
</div>
<Divider />
</div>
);
};
export default HeadingAndActions;

View File

@@ -1,104 +0,0 @@
import { Course } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule';
import React from 'react';
import Card from '@views/components/common/Card/Card';
import Icon from '@views/components/common/Icon/Icon';
import Link from '@views/components/common/Link/Link';
import Text from '@views/components/common/Text/Text';
import CourseButtons from './CourseButtons/CourseButtons';
import styles from './CourseHeader.module.scss';
type Props = {
course: Course;
activeSchedule?: UserSchedule;
onClose: () => void;
};
/**
* This component displays the header of the course info popup.
* It displays the course name, unique id, instructors, and schedule, all formatted nicely.
*/
export default function CourseHeader({ course, activeSchedule, onClose }: Props) {
const getBuildingUrl = (building?: string): string | undefined => {
if (!building) return undefined;
return `https://utdirect.utexas.edu/apps/campus/buildings/nlogon/maps/UTM/${building}/`;
};
return (
<Card className={styles.header}>
<Icon className={styles.close} size='large' name='close' onClick={onClose} />
<div className={styles.title}>
<Text className={styles.courseName} size='large' weight='bold' color='black'>
{course.courseName} ({course.department} {course.number})
</Text>
<Link
url={course.url}
className={styles.uniqueId}
size='medium'
weight='semi_bold'
color='burnt_orange'
title='View course details on UT Course Schedule'
>
#{course.uniqueId}
</Link>
</div>
<Text size='medium' className={styles.instructors}>
{`with ${!course.instructors.length ? 'TBA' : ''}`}
{course.instructors.map((instructor, index) => {
const name = instructor.toString({
format: 'first_last',
case: 'capitalize',
});
const url = instructor.getDirectoryUrl();
const numInstructors = course.instructors.length;
const isLast = course.instructors.length > 1 && index === course.instructors.length - 1;
return (
<span key={name}>
{numInstructors > 1 && index === course.instructors.length - 1 ? '& ' : ''}
<Link
key={name}
size='medium'
weight='normal'
url={url}
title="View instructor's directory page"
>
{name}
</Link>
{numInstructors > 2 && !isLast ? ', ' : ''}
</span>
);
})}
</Text>
{course.schedule.meetings.map(meeting => (
<Text size='medium' className={styles.meeting} key={meeting.startTime}>
<Text span size='medium' weight='bold' color='black'>
{meeting.getDaysString({
format: 'long',
separator: 'short',
})}
</Text>
{' at '}
<Text span size='medium'>
{meeting.getTimeString({
separator: 'to',
capitalize: true,
})}
</Text>
{' in '}
<Link
size='medium'
weight='normal'
title='View building on UT Map'
url={getBuildingUrl(meeting.location?.building)}
disabled={!meeting.location?.building}
>
{meeting.location?.building ?? 'TBA'}
</Link>
</Text>
))}
<CourseButtons course={course} activeSchedule={activeSchedule} />
</Card>
);
}

View File

@@ -39,7 +39,7 @@ export default function CourseDescription({ course }: Props) {
return (
<Card className={styles.container}>
{status === LoadStatus.ERROR && (
<Text color='speedway_brick' size='medium' weight='bold' align='center'>
<Text color='speedway_brick' /* size='medium' weight='bold' align='center' */>
Please refresh the page and log back in using your UT EID and password
</Text>
)}
@@ -72,7 +72,7 @@ function DescriptionLine({ line }: LineProps) {
});
return (
<Text className={className} size='medium'>
<Text className={className} /* size='medium' */>
{line}
</Text>
);

View File

@@ -1,11 +1,11 @@
import { background } from '@shared/messages';
import { Course } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule';
import React from 'react';
import { Button } from '@views/components/common/Button/Button';
import Card from '@views/components/common/Card/Card';
import Icon from '@views/components/common/Icon/Icon';
import Text from '@views/components/common/Text/Text';
import React from 'react';
import styles from './CourseButtons.module.scss';
type Props = {
@@ -83,47 +83,43 @@ export default function CourseButtons({ course, activeSchedule }: Props) {
<Button
onClick={openRateMyProfessorURL}
disabled={!course.instructors.length}
type='primary'
variant='filled'
className={styles.button}
color='ut-black'
title='Search for this professor on RateMyProfessor'
>
<Text size='medium' weight='regular' color='white'>
RateMyProf
</Text>
<Text /* size='medium' weight='regular' */ color='white'>RateMyProf</Text>
<Icon className={styles.icon} color='white' name='school' size='medium' />
</Button>
<Button
onClick={openSyllabiURL}
type='secondary'
variant='filled'
className={styles.button}
color='ut-black'
title='Search for syllabi for this course'
>
<Text size='medium' weight='regular' color='white'>
Syllabi
</Text>
<Text /* size='medium' weight='regular' */ color='white'>Syllabi</Text>
<Icon className={styles.icon} color='white' name='grading' size='medium' />
</Button>
<Button
onClick={openTextbookURL}
type='tertiary'
variant='filled'
className={styles.button}
color='ut-black'
title='Search for textbooks for this course'
>
<Text size='medium' weight='regular' color='white'>
Textbook
</Text>
<Text /* size='medium' weight='regular' color='white' */>Textbook</Text>
<Icon className={styles.icon} color='white' name='collections_bookmark' size='medium' />
</Button>
<Button
disabled={!activeSchedule}
onClick={isCourseSaved ? handleRemoveCourse : handleSaveCourse}
title={isCourseSaved ? 'Remove this course from your schedule' : 'Add this course to your schedule'}
type={isCourseSaved ? 'danger' : 'success'}
variant='filled'
className={styles.button}
color='ut-black'
>
<Text size='medium' weight='regular' color='white'>
{isCourseSaved ? 'Remove' : 'Add'}
</Text>
<Text /* size='medium' weight='regular' color='white' */>{isCourseSaved ? 'Remove' : 'Add'}</Text>
<Icon className={styles.icon} color='white' name={isCourseSaved ? 'remove' : 'add'} size='medium' />
</Button>
</Card>

View File

@@ -0,0 +1,133 @@
import { Course } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule';
import React from 'react';
import Card from '@views/components/common/Card/Card';
import Icon from '@views/components/common/Icon/Icon';
import Link from '@views/components/common/Link/Link';
import Text from '@views/components/common/Text/Text';
import { Button } from 'src/views/components/common/Button/Button';
import CourseButtons from './CourseButtons/CourseButtons';
import styles from './CourseHeader.module.scss';
import CopyIcon from '~icons/material-symbols/content-copy';
import CloseIcon from '~icons/material-symbols/close';
type Props = {
course: Course;
activeSchedule?: UserSchedule;
onClose: () => void;
};
/**
* This component displays the header of the course info popup.
* It displays the course name, unique id, instructors, and schedule, all formatted nicely.
*/
export default function CourseHeader({ course, activeSchedule, onClose }: Props) {
// const getBuildingUrl = (building?: string): string | undefined => {
// if (!building) return undefined;
// return `https://utdirect.utexas.edu/apps/campus/buildings/nlogon/maps/UTM/${building}/`;
// };
return (
<div className='mx-6 my-5'>
<div className='flex items-center justify-start'>
<Text variant='h1' className='shrink truncate'>
{course.courseName}
</Text>
<Text variant='h1' className='ml-1 shrink-0'>
{`(${course.department} ${course.number})`}
</Text>
<div className='ml-auto min-w-fit flex shrink-0 gap-0'>
<Button icon={CopyIcon} variant='single' className='mr-1 px-2' color='ut-burntorange'>
{course.uniqueId}
</Button>
<button className='btn bg-transparent p-0'>
<CloseIcon className='h-7 w-7' />
</button>
</div>
</div>
<div>
<Text variant='p'>
with{' '}
{course.instructors.map(instructor => (
<span className=''>{instructor.lastName}</span>
))}
</Text>
</div>
</div>
// <Card className={styles.header}>
// <Icon className={styles.close} /* size='large' */ name='close' onClick={onClose} />
// <div className={styles.title}>
// <Text className={styles.courseName} /* size='large' weight='bold' color='black' */>
// {course.courseName} ({course.department} {course.number}) blahhhhh
// </Text>
// <Link
// url={course.url}
// className={styles.uniqueId}
// /* size='medium'
// weight='semi_bold' */
// color='burnt_orange'
// title='View course details on UT Course Schedule'
// >
// #{course.uniqueId}
// </Link>
// </div>
// <Text /* size='medium' className={styles.instructors} */>
// {`with ${!course.instructors.length ? 'TBA' : ''}`}
// {course.instructors.map((instructor, index) => {
// const name = instructor.toString({
// format: 'first_last',
// case: 'capitalize',
// });
// const url = instructor.getDirectoryUrl();
// const numInstructors = course.instructors.length;
// const isLast = course.instructors.length > 1 && index === course.instructors.length - 1;
// return (
// <span key={name}>
// {numInstructors > 1 && index === course.instructors.length - 1 ? '& ' : ''}
// <Link
// key={name}
// /* size='medium'
// weight='normal' */
// url={url}
// title="View instructor's directory page"
// >
// {name}
// </Link>
// {numInstructors > 2 && !isLast ? ', ' : ''}
// </span>
// );
// })}
// </Text>
// {course.schedule.meetings.map(meeting => (
// <Text /* size='medium' */ className={styles.meeting} key={meeting.startTime}>
// <Text as='span' /* size='medium' weight='bold' */ color='black'>
// {meeting.getDaysString({
// format: 'long',
// separator: 'short',
// })}
// </Text>
// {' at '}
// <Text as='span' /* size='medium' */>
// {meeting.getTimeString({
// separator: 'to',
// capitalize: true,
// })}
// </Text>
// {' in '}
// <Link
// /* size='medium'
// weight='normal' */
// title='View building on UT Map'
// url={getBuildingUrl(meeting.location?.building)}
// disabled={!meeting.location?.building}
// >
// {meeting.location?.building ?? 'TBA'}
// </Link>
// </Text>
// ))}
// <CourseButtons course={course} activeSchedule={activeSchedule} />
// </Card>
);
}

View File

@@ -1,6 +1,7 @@
import { Course } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule';
import type { Course } from '@shared/types/Course';
import type { UserSchedule } from '@shared/types/UserSchedule';
import React from 'react';
import Popup from '../../common/Popup/Popup';
import CourseDescription from './CourseDescription/CourseDescription';
import CourseHeader from './CourseHeader/CourseHeader';

View File

@@ -203,18 +203,18 @@ export default function GradeDistribution({ course }: Props) {
{status === DataStatus.LOADING && <Spinner />}
{status === DataStatus.ERROR && (
<Card className={styles.text}>
<Text color='speedway_brick' size='medium' weight='semi_bold'>
<Text color='speedway_brick' /* size='medium' weight='semi_bold' */>
There was an error fetching the grade distribution data
</Text>
<Icon color='speedway_brick' size='large' name='sentiment_dissatisfied' />
<Icon color='speedway_brick' /* size='large' */ name='sentiment_dissatisfied' />
</Card>
)}
{status === DataStatus.NOT_FOUND && (
<Card className={styles.text}>
<Text color='charcoal' size='medium' weight='semi_bold'>
<Text color='charcoal' /* size='medium' weight='semi_bold' */>
No grade distribution data was found for this course
</Text>
<Icon color='charcoal' size='x_large' name='search_off' />
<Icon color='charcoal' /* size='x_large' */ name='search_off' />
</Card>
)}
</Card>

View File

@@ -3,9 +3,9 @@ import { UserSchedule } from '@shared/types/UserSchedule';
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
import { Button } from '../../common/Button/Button';
import Icon from '../../common/Icon/Icon';
import Text from '../../common/Text/Text';
import styles from './TableRow.module.scss';
import ConflictsWithWarning from '../../common/ConflictsWithWarning/ConflictsWithWarning';
import AddIcon from '~icons/material-symbols/add-circle';
interface Props {
isSelected: boolean;
@@ -54,7 +54,7 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
return () => {
element.classList.remove(styles.inActiveSchedule);
};
}, [activeSchedule, element.classList]);
}, [activeSchedule, course, element.classList]);
useEffect(() => {
if (!activeSchedule || !course) {
@@ -76,7 +76,7 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
element.classList.remove(styles.isConflict);
setConflicts([]);
};
}, [activeSchedule, course]);
}, [activeSchedule, course, element.classList]);
if (!container) {
return null;
@@ -84,20 +84,14 @@ export default function TableRow({ row, isSelected, activeSchedule, onClick }: P
return ReactDOM.createPortal(
<>
<Button className={styles.rowButton} onClick={onClick} type='secondary'>
<Icon name='bar_chart' color='white' size='medium' />
</Button>
{conflicts.length > 0 && (
<div className={styles.conflictTooltip}>
<div className={styles.body}>
{conflicts.map(c => (
<Text size='small' key={c.uniqueId}>
{c.department} {c.number} ({c.uniqueId})
</Text>
))}
</div>
</div>
)}
<Button
icon={AddIcon}
className={styles.rowButton}
color='ut-burntorange'
onClick={onClick}
variant='single'
/>
{conflicts.length > 0 && <ConflictsWithWarning className={styles.conflictTooltip} conflicts={conflicts} />}
</>,
container
);

View File

@@ -0,0 +1,12 @@
import { describe, expect, it } from 'vitest';
import { convertMinutesToIndex } from '../useFlattenedCourseSchedule';
describe('useFlattenedCourseSchedule', () => {
it('should convert minutes to index correctly', () => {
const minutes = 480; // 8:00 AM
const expectedIndex = 2; // (480 - 420) / 30 = 2
const result = convertMinutesToIndex(minutes);
expect(result).toBe(expectedIndex);
});
});

View File

@@ -0,0 +1,119 @@
import type { CalendarCourseCellProps } from 'src/views/components/calendar/CalendarCourseCell/CalendarCourseCell';
import type { CourseCatalogInjectedPopupProps } from 'src/views/components/injected/CourseCatalogInjectedPopup/CourseCatalogInjectedPopup';
import useSchedules from './useSchedules';
const dayToNumber: { [day: string]: number } = {
Monday: 0,
Tuesday: 1,
Wednesday: 2,
Thursday: 3,
Friday: 4,
};
interface CalendarGridPoint {
dayIndex: number;
startIndex: number;
endIndex: number;
}
/**
* Return type of useFlattenedCourseSchedule
*/
export interface CalendarGridCourse {
calendarGridPoint: CalendarGridPoint;
componentProps: CalendarCourseCellProps;
gridColumnStart?: number;
gridColumnEnd?: number;
totalColumns?: number;
popupProps: CourseCatalogInjectedPopupProps;
}
export const convertMinutesToIndex = (minutes: number): number => Math.floor(minutes - 420 / 30);
/**
* Get the active schedule, and convert it to be render-able into a calendar.
* @returns CalendarGridCourse
*/
export function useFlattenedCourseSchedule(): CalendarGridCourse[] {
const [activeSchedule] = useSchedules();
const { courses } = activeSchedule;
return courses
.flatMap(course => {
const {
status,
department,
instructors,
schedule: { meetings },
} = course;
const courseDeptAndInstr = `${department} ${instructors[0].lastName}`;
if (meetings.length === 0) {
// asynch, online course
return [
{
calendarGridPoint: {
dayIndex: 0,
startIndex: 0,
endIndex: 0,
},
componentProps: {
courseDeptAndInstr,
timeAndLocation: 'Asynchronous',
status,
colors: {
// TODO: figure out colors - these are defaults
primaryColor: 'ut-gray',
secondaryColor: 'ut-gray',
},
},
popupProps: {
course,
activeSchedule,
onClose: () => {},
},
},
];
}
// in-person
return meetings.flatMap(meeting => {
const { days, startTime, endTime, location } = meeting;
const time = meeting.getTimeString({ separator: '-', capitalize: true });
const timeAndLocation = `${time} - ${location ? location.building : 'WB'}`;
return days.map(d => ({
calendarGridPoint: {
dayIndex: dayToNumber[d],
startIndex: convertMinutesToIndex(startTime),
endIndex: convertMinutesToIndex(endTime),
},
componentProps: {
courseDeptAndInstr,
timeAndLocation,
status,
colors: {
// TODO: figure out colors - these are defaults
primaryColor: 'ut-orange',
secondaryColor: 'ut-orange',
},
},
popupProps: {
course,
activeSchedule,
onClose: () => {}, // Add onClose property here
},
}));
});
})
.sort((a: CalendarGridCourse, b: CalendarGridCourse) => {
if (a.calendarGridPoint.dayIndex !== b.calendarGridPoint.dayIndex) {
return a.calendarGridPoint.dayIndex - b.calendarGridPoint.dayIndex;
}
if (a.calendarGridPoint.startIndex !== b.calendarGridPoint.startIndex) {
return a.calendarGridPoint.startIndex - b.calendarGridPoint.startIndex;
}
return a.calendarGridPoint.endIndex - b.calendarGridPoint.endIndex;
});
}

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