Compare commits

..

40 Commits

Author SHA1 Message Date
doprz
234f3d627d feat(release): v2.2.1 2025-06-03 19:46:36 -05:00
Ethan Lanting
be1dccfcb9 feat: add dining app promo (#598)
* feat: add DiningAppPromo component and integrate it into Calendar

* feat: update WhatsNewPopup with new features and app download link

* fix: remove outdated links

* chore: run lint

* chore: run prettier

* feat: enhance DiningAppPromo with close button and integrate user preference for promo visibility

* chore: run lint

* chore: run check types

* fix: correct promo visibility logic in Calendar component

* feat: centralize app store URLs in appUrls.ts

* chore: run lint

* feat: integrate UT Dining promo image

* chore: run lint

* fix: update logo in WhatsNew popup to use LD icon

* fix: convert URLs to URL objects for consistency

* fix: update LD icon in WhatsNew popup to new version

* fix: update description for Coffee Shops feature to clarify operating times

* fix: rename promo state and storage key to showUTDiningPromo for clarity

---------

Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
2025-05-28 20:13:45 -05:00
Razboy20
454e5e807a fix: hide sentry instrumentation on debug builds (#604)
* fix: hide sentry instrumentation on debug builds

* fix: amend documentation to reference zip:to-public rather than zip
2025-05-27 23:35:11 -05:00
Razboy20
29d20d5c5a chore: setup dependabot configuration (#603)
* chore: setup dependabot configuration
2025-05-27 22:55:11 -05:00
doprz
e29546c727 chore: update issue templates 2025-05-27 21:55:36 -05:00
doprz
5a89be6238 chore: update issue templates 2025-05-27 21:53:42 -05:00
Samuel Gunter
cfb5faa09b fix: course columns on calendar (#587)
* fix: course columns on calendar

* test: new tests for course cell columns, extract calls to help function

* fix: gracefully handle async courses

* refactor: split course cell column logic into multiple functions, add comments, and derive fields

* chore: comments, for-index to for-of

* chore: fix typo

* fix: don't use sentry in storybook contexts

* fix: silence, brand

* refactor: don't rely on calculating columns, find as you go

---------

Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
2025-05-06 16:07:27 -05:00
37471efb74 feat: inside jokes005 (#590)
* feat: moved some of my lines together removed one of mine and added jack black

* feat: just ctrl x ctrl v a few lines

* Update insideJokes.tsx
2025-04-13 21:33:52 -05:00
doprz
7a4f40a765 feat(release): v2.2.0 2025-04-06 18:11:27 -05:00
doprz
d11d55db66 chore: update exec list (#580)
* chore: update exec list

* chore: update roles

* chore: update h2

* fix: key

* chore: caps
2025-04-05 14:33:44 -05:00
Samuel Gunter
76b6aa7c15 fix: include logo in screenshot, fix screenshots on small/zoomed windows (#579)
* fix: include logo in screenshot

* fix: screenshots on small/zoomed windows, screenshots with no async/other

---------

Co-authored-by: Razboy20 <razboy20@gmail.com>
2025-04-05 11:21:37 -05:00
Samuel Gunter
70d4fecad6 feat: recruitment banner for designer (#578) 2025-04-04 09:56:37 -05:00
Samuel Gunter
c3fa91752c chore: bump migration dialog message number, remove unused UpdateText component (#577) 2025-04-02 10:00:05 -05:00
Ethan Lanting
7c2beef193 feat: auto create empty schedule when deleted all schedules (#552)
* feat: enhance schedule deletion to create a new schedule if none remain

* feat: set active index to new schedule if only one exists

* chore: run lint

* feat: enhance schedule deletion to create a new schedule if none remain

* feat: set active index to new schedule if only one exists

* chore: run lint

* feat: reset schedules on update, refactor invariant to within deleteSchedule

* chore: pnpm lint

---------

Co-authored-by: Samuel Gunter <29130894+Samathingamajig@users.noreply.github.com>
Co-authored-by: Samuel Gunter <sgunter@utexas.edu>
2025-04-01 13:24:08 -05:00
Samuel Gunter
630d0d80d2 chore: professionalism in jokes (#572) 2025-03-26 14:16:06 -05:00
Samuel Gunter
695743104c feat: persist sidebar toggle state (#569) 2025-03-26 13:50:34 -05:00
Samuel Gunter
d014244b28 chore: pin volta pnpm version (#573) 2025-03-25 23:00:34 -05:00
Samuel Gunter
5cd56259f7 test: fix flaky sleep test (#571) 2025-03-24 02:28:41 -05:00
Samuel Gunter
fa9f78b46e feat: sticky calendar header and days (#568)
* feat: sticky calendar days

* feat: partial height borders for day labels

* feat: make calendar header actually sticky

* fix: remove unneeded gap

* refactor: add preston as co-author

Co-authored-by: Preston-Cook <preston.l.cook@gmail.com>

* fix: z-index issues with export sub-buttons

---------

Co-authored-by: Preston-Cook <preston.l.cook@gmail.com>
Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
2025-03-23 19:49:11 -05:00
Samuel Gunter
4a5f67f0fd fix: ics calendar export dates (#535)
* feat: academicCalendars object

* feat: seemingly working start, end, and until dates

* feat: seemingly working everything

* style: removed unnecessary deps, reorganized code

* style: code comments yay

* chore: old version of pnpm?

* ci: force github actions to rerun

* feat: list instructors in ics string, basic tests

* feat: testable code for ICS, tests for ICS, filter excluded dates

* style: eslint autofix

* test: check for graceful handling of errors in ICS

* fix: actually use scheduleToIcsString

* chore: eslint didn't include a space where it should've

* fix: ensure tz everywhere

* refactor: move string util to string util file

* feat: em dash in calendar event title

* feat: academic calendars 22-23 and 23-24

* fix: en dash instead of em dash
2025-03-22 22:55:16 -05:00
Yash Kukrecha
3bed9cc27f fix(schedule): truncate long schedule names in popup (#564) 2025-03-22 21:10:21 -05:00
ishita778
0dcae25b93 chore: removed extra space at calendar footer (#557)
* chore: removed extra space at calendar footer

* chore: fixed eslint issues

* chore: changed return type to react node

* chore: displaycourses true fixes and checks fixed

* chore: prettier fix
2025-03-18 04:00:46 -05:00
beastgwert
ca734dcd39 feat: rework start time to checkboxes (#553)
* feat: replace dropdown with checkbox

* refactor: remove console logs

* refactor: eslint happy

* refactor: change daysValue from string to array

* style: match course schedule page styling

* style: remove label font-normal

* style: finalize course schedule page style match

---------

Co-authored-by: Samuel Gunter <29130894+Samathingamajig@users.noreply.github.com>
2025-03-15 22:37:51 -05:00
Samuel Gunter
9448072112 feat: ensure unique splash text on schedule change (#554) 2025-03-10 18:57:54 -05:00
ishita778
b1e98ca9d7 chore: Modify the schedule creation prompt (#550)
* chore: modify the schedule creation prompt

* chore: changed border color to offwhite

---------

Co-authored-by: Samuel Gunter <29130894+Samathingamajig@users.noreply.github.com>
2025-03-08 21:01:46 -06:00
Abdulrahman Alshahrani
f036d409e6 feat: implement a What's New prompt (#539)
* feat: create whats new initial component

* feat: create initial whats-new hook

* feat: create whats-new story

* feat: add button to open dialog in storybook

* feat: complete popup ui

* feat: add check for new updates or installs

* fix: fix linter issues

* fix: use proper features and add video

* fix: properly fetch version from manifest

* feat: add a link to open the popup

* fix: update spacing and features' content

* fix: update UTRP Map name

* fix: increase icon size and display version correctly

* feat: update the features video

* fix: update offwhite color

* fix: color typo

* fix: fixing colors again

* feat: use numbers instead of boolean

* fix: typo in import

* feat: add type safety to features array

* feat: cdn video url

* fix: delete mp4 video

* feat: handle video failure to load

* fix: make border outline tight to video

* feat: make design responsive

* fix: make features array readonly

---------

Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
Co-authored-by: Derek Chen <derex1987@gmail.com>
2025-03-08 15:41:09 -06:00
ishita778
5493c63f18 chore: Change the highlight color on-hover over grade distribution bars to off-white/50 (#547)
* chore: opacity changed to 50 percent

* chore: fix pnpm lock file

* chore: pnpm checks pass attempt

* chore: pnpm lock checks should work

* fix: opacity 50 and pnpm version

* chore: pnpm lock file fix
2025-03-07 13:31:23 -06:00
ishita778
6c3139bf0f fix: merge course labels across pages (#541)
* fix: merge course labels across pages

* fix: merge same course

* fix: all checks pass

* fix: updated addrows function

* fix: prettier check

* fix: all checks

* fix: all checks pass

* fix: moved query tbody outside

* fix: uses row element

* fix: checks pass now

---------

Co-authored-by: Samuel Gunter <29130894+Samathingamajig@users.noreply.github.com>
2025-03-05 15:08:33 -06:00
Krish Patel
28ebb69612 style: update delete schedule prompt (#546)
Co-authored-by: Samuel Gunter <29130894+Samathingamajig@users.noreply.github.com>
2025-03-05 14:15:59 -06:00
Samuel Gunter
008cb40cb8 style: move input click style to central place (#532)
Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
2025-03-05 14:05:34 -06:00
doprz
195d3a219a chore(release): v2.1.1 2025-03-02 21:01:18 -06:00
doprz
c6452c4f2b feat: add isDeveloper 2025-03-02 20:47:31 -06:00
doprz
3dbacf0d65 chore: pin node@20.9.0 (#540) 2025-03-01 12:54:31 -06:00
doprz
56a82baf40 chore: update lock file and set engine to use pnpm v10 (#536)
* chore: update lock file and set engine to use pnpm v10.5.2

* chore: set pnpm version to v10.x.x

* chore(ci): use pnpm v10

* chore: update Dockerfile to use pnpm@latest-10

* chore(ci): use pnpm/action-setup@v4
2025-02-28 22:54:46 -06:00
ishita778
20d210245b chore: rename unscheduled text at calendar footer (#538) 2025-02-28 11:46:19 -06:00
ishita778
21cbcf677a chore: changed text to data source (#537) 2025-02-28 11:07:19 -06:00
Samuel Gunter
8fb355d03b chore(cal): update course schedule links for fall and summer 2025 (#534) 2025-02-27 23:37:38 -06:00
doprz
218477404f feat: map page (#390)
* feat: add boilerplate

* feat: add working paths

* feat: improve building selection controls and add week schedule

Signed-off-by: doprz <52579214+doprz@users.noreply.github.com>

* fix: sort week schedule

Signed-off-by: doprz <52579214+doprz@users.noreply.github.com>

* feat(testing): improve pathfinding

* Revert "feat(testing): improve pathfinding"

This reverts commit 87998cedbf.

* feat: add pathfinding with building selection controls

Signed-off-by: doprz <52579214+doprz@users.noreply.github.com>

* feat: improve path finding algorithm thresholds

* feat: add DaySelector, PathStats, and WeekSchedule components

* feat: add working stats and daily schedule

* chore: refactor everything

* feat: add linear walkway node generation

* feat: add bezier curve walkway node generation

* feat: add circular walkway node generation

* docs: add docs

* feat: add individual path selection and bump version

* fix: tsdoc and updated components/utils

* chore(deps): update deps

* feat: add UTRP Map and Debug Page to Settings > Developer Mode

* chore: fix pr review comments

* chore: add showDebugNodes

* chore: add all buildings around the UT tower

* chore: add stadium POIs

* chore: add east mall buildings

* chore: update DaySelector to use proper button styling

* chore: add university ave walkway

* feat: add zoom, pan, and dev controls functionality

- Fix SVG Overlay Alignment
- Use SVG for map
- Add Dev Controls
- Fix day selector position
- Update the SVG's `preserveAspectRatio` attribute to `xMidYMid` meet to
ensure proper scaling
- Use `useCallback` for event handlers to prevent unnecessary re-renders
- Remove old PNG map

* feat: add dynamic rendering"

* feat: add dynamicRendering dev toggle and fullscreen support

* chore: update deps

* chore: disable viewport svg overlay culling if dynamic rendering is off

* chore: update pnpm-lock.yaml

* chore: add north mall buildings

* chore: add buildings next to JES

* refactor: map components into individual files

* fix: missing import

---------

Signed-off-by: doprz <52579214+doprz@users.noreply.github.com>
2025-02-27 19:44:03 -06:00
Ethan Lanting
766c0bc1b4 fix: import schedule file upload button (#515)
* fix: add active scale style and accept file prop

* chore: improve type safety using MIMEType

* fix: update FileUpload component to support multiple MIME types in accept prop

* chore: run lint

---------

Co-authored-by: doprz <52579214+doprz@users.noreply.github.com>
2025-02-24 20:17:49 -06:00
Samuel Gunter
4a8d0666c2 chore: update grades as of fall 2024 (#508) 2025-02-21 16:09:53 -06:00
86 changed files with 13898 additions and 2054 deletions

View File

@@ -1,9 +1,10 @@
--- ---
name: Bug report name: Bug report
about: Create a report to help us improve about: Create a report to help us improve
title: '[BUG] ' title: "[BUG] "
labels: 'bug' labels: ''
assignees: '' assignees: ''
--- ---
**Pre-submission Checklist** **Pre-submission Checklist**

View File

@@ -1,9 +1,10 @@
--- ---
name: Feature Request name: Feature Request
about: Suggest an idea for this project about: Suggest an idea for this project
title: '[FEATURE] ' title: "[FEATURE] "
labels: 'feature' labels: feature
assignees: '' assignees: ''
--- ---
**Pre-submission Checklist** **Pre-submission Checklist**

View File

@@ -0,0 +1,12 @@
---
name: Updating Build Dependencies
about: Updating Build Dependencies
title: ''
labels: build, dependencies
assignees: doprz, Razboy20
---
- [ ] Updated Nix Flake
- [ ] Update Dockerfile
- [ ] Update Docs

21
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
version: 2
updates:
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'weekly'
day: 'monday'
time: '09:00'
timezone: 'America/Chicago'
groups:
minor-and-patch-updates:
update-types:
- 'minor'
- 'patch'
major-updates:
update-types:
- 'major'
ignore:
- dependency-name: '@crxjs/vite-plugin'
- dependency-name: '@unocss/vite'

View File

@@ -13,9 +13,9 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v3 uses: pnpm/action-setup@v4
with: with:
version: 9 version: 10
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
@@ -32,9 +32,9 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v3 uses: pnpm/action-setup@v4
with: with:
version: 9 version: 10
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install

View File

@@ -13,9 +13,9 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v3 uses: pnpm/action-setup@v4
with: with:
version: 9 version: 10
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install

View File

@@ -11,9 +11,9 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v3 uses: pnpm/action-setup@v4
with: with:
version: 9 version: 10
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install

View File

@@ -13,9 +13,9 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v3 uses: pnpm/action-setup@v4
with: with:
version: 9 version: 10
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install

View File

@@ -1,335 +1,372 @@
## [2.2.1](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.2.0...v2.2.1) (2025-06-04)
### Features
* add dining app promo ([#598](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/598)) ([be1dccf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/be1dccfcb9d052c6b291b50cc53418d6bb645beb))
* inside jokes005 ([#590](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/590)) ([37471ef](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/37471efb740c7a5828cf3b54bac70954694359d7))
### Bug Fixes
* course columns on calendar ([#587](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/587)) ([cfb5faa](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cfb5faa09bb0788e270d100f1f36536a53bcff75))
* hide sentry instrumentation on debug builds ([#604](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/604)) ([454e5e8](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/454e5e807af29ae0384cc3a3b8b691df5edc69d1))
## [2.2.0](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.1.1...v2.2.0) (2025-04-06)
### Features
* auto create empty schedule when deleted all schedules ([#552](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/552)) ([7c2beef](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7c2beef1930fbc887e8ec1aea789016b3150cd21))
* ensure unique splash text on schedule change ([#554](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/554)) ([9448072](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/94480721124e052426c1f3236e8605c7088df79c))
* implement a What's New prompt ([#539](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/539)) ([f036d40](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f036d409e60a39fd1d3cb2f0db53a6056615f336))
* persist sidebar toggle state ([#569](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/569)) ([6957431](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/695743104c57951ba1957258c60c843f8fae793f))
* recruitment banner for designer ([#578](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/578)) ([70d4fec](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/70d4fecad61ec3cd3ba839de302fd851e075d073))
* **release:** v2.2.0 ([7a4f40a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7a4f40a765d704bf32a3b515d695916ed84f9397))
* rework start time to checkboxes ([#553](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/553)) ([ca734dc](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ca734dcd39a433cfd2e930ea04adeba959b32c36))
* sticky calendar header and days ([#568](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/568)) ([fa9f78b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/fa9f78b46e3a2270a44d4cc0691195a7c695cb93))
### Bug Fixes
* ics calendar export dates ([#535](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/535)) ([4a5f67f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4a5f67f0fda9f0ef57f821e4b7a55d63f099f579))
* include logo in screenshot, fix screenshots on small/zoomed windows ([#579](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/579)) ([76b6aa7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/76b6aa7c150299dfcfa4b3dc00ce2de32f90f75c))
* merge course labels across pages ([#541](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/541)) ([6c3139b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6c3139bf0f324c9a7be826b6c24e8bf142fc53b1))
* **schedule:** truncate long schedule names in popup ([#564](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/564)) ([3bed9cc](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3bed9cc27febfe795af0766a913c4845e74cc2da))
## [2.1.1](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.1.0...v2.1.1) (2025-03-03)
### Features
* add isDeveloper ([c6452c4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c6452c4f2b174487e6f51ad546ca1c3f8b4dbc1f))
* map page ([#390](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/390)) ([2184774](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/218477404fdeacda7b39cd233e4e1e65995935d4))
### Bug Fixes
* import schedule file upload button ([#515](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/515)) ([766c0bc](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/766c0bc1b4a75f8146a922cb2eca8871032c1dc9))
## [2.1.0](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.0.2...v2.1.0) (2025-02-20) ## [2.1.0](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.0.2...v2.1.0) (2025-02-20)
### Features ### Features
- add 'new search' link to the course catalog page ([#456](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/456)) ([ca5e4c1](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ca5e4c13d31aeb603660972536712ea161c6f870)) * add 'new search' link to the course catalog page ([#456](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/456)) ([ca5e4c1](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ca5e4c13d31aeb603660972536712ea161c6f870))
- add 404 page ([#426](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/426)) ([46c76b1](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/46c76b1703ea2344c3330c4cfa82560663be1c4c)) * add 404 page ([#426](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/426)) ([46c76b1](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/46c76b1703ea2344c3330c4cfa82560663be1c4c))
- add CacheStore for GitHub stats and use names instead of usernames ([#405](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/405)) ([b732a80](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b732a80eaa6f6cbf5b627ffc31ab321de3a8e8f4)) * add CacheStore for GitHub stats and use names instead of usernames ([#405](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/405)) ([b732a80](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b732a80eaa6f6cbf5b627ffc31ab321de3a8e8f4))
- add eslint-plugin-tsdoc ([#430](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/430)) ([e987fbb](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e987fbbe8e733de6767d62811c2d4d6eaccf2d24)) * add eslint-plugin-tsdoc ([#430](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/430)) ([e987fbb](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e987fbbe8e733de6767d62811c2d4d6eaccf2d24))
- add explanation to grade distribution ([#325](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/325)) ([9ad3239](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9ad32390d13e1d413ca01b1bc45d02c695bc23dd)) * add explanation to grade distribution ([#325](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/325)) ([9ad3239](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9ad32390d13e1d413ca01b1bc45d02c695bc23dd))
- add open calendar button ([#457](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/457)) ([93733e3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/93733e37214f00543479e77209ca03864776a1a6)) * add open calendar button ([#457](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/457)) ([93733e3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/93733e37214f00543479e77209ca03864776a1a6))
- add spacing system ([#474](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/474)) ([e61ab56](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e61ab565c35a197609d205adcaac9c7ffe0fc6da)) * add spacing system ([#474](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/474)) ([e61ab56](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e61ab565c35a197609d205adcaac9c7ffe0fc6da))
- **build:** add Docker support ([#322](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/322)) ([a5e921f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a5e921fd75109a50f17d05c9682e73ba246f3dd1)) * **build:** add Docker support ([#322](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/322)) ([a5e921f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a5e921fd75109a50f17d05c9682e73ba246f3dd1))
- **build:** add vite-build-logger ([#507](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/507)) ([1aa4e8c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/1aa4e8c5fb2d1aea308e4cccea3e2818a9c946ec)) * **build:** add vite-build-logger ([#507](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/507)) ([1aa4e8c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/1aa4e8c5fb2d1aea308e4cccea3e2818a9c946ec))
- **build:** refactor gulpfile to use gulp-execa ([#323](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/323)) ([db04bbb](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/db04bbb52e4ab19730dd66f417e68ab013ce2f79)) * **build:** refactor gulpfile to use gulp-execa ([#323](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/323)) ([db04bbb](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/db04bbb52e4ab19730dd66f417e68ab013ce2f79))
- export/import functionality (backup/restore/share with friends) + a new input component ([#433](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/433)) ([7dbffc6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7dbffc6eef346747042f1596da627ad0a2fcae1a)) * export/import functionality (backup/restore/share with friends) + a new input component ([#433](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/433)) ([7dbffc6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7dbffc6eef346747042f1596da627ad0a2fcae1a))
- injected button - add all courses from MyUT AND passing URL to handler ([#291](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/291)) ([c41467c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c41467c6176d31f70cd3ecdcf56eeb73696c6c23)) * injected button - add all courses from MyUT AND passing URL to handler ([#291](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/291)) ([c41467c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c41467c6176d31f70cd3ecdcf56eeb73696c6c23))
- modify Course Block text style and time and location text ([#409](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/409)) ([0d51cae](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0d51cae4c8b27201b09c0898f4d7a6e7abb3c100)) * modify Course Block text style and time and location text ([#409](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/409)) ([0d51cae](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0d51cae4c8b27201b09c0898f4d7a6e7abb3c100))
- **settings:** add option to always open calendar in new tab ([#488](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/488)) ([009de62](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/009de628285ce9c6571e492f8e3f52cdeeed4459)) * **settings:** add option to always open calendar in new tab ([#488](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/488)) ([009de62](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/009de628285ce9c6571e492f8e3f52cdeeed4459))
- **settings:** allow disabling of auto-loading courses ([#489](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/489)) ([b74c698](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b74c698866c1074ce2236ede549c94555667e6a5)) * **settings:** allow disabling of auto-loading courses ([#489](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/489)) ([b74c698](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b74c698866c1074ce2236ede549c94555667e6a5))
- **ui:** add schedule list icons ([#500](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/500)) ([f0b257a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f0b257aa124a8fff0bb31e3396715aecb09948d5)) * **ui:** add schedule list icons ([#500](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/500)) ([f0b257a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f0b257aa124a8fff0bb31e3396715aecb09948d5))
- **ui:** added shadows to popup buttons and course blocks ([#378](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/378)) ([a20332e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a20332e53da9bda873fabb8395afe6ff6303799c)) * **ui:** added shadows to popup buttons and course blocks ([#378](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/378)) ([a20332e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a20332e53da9bda873fabb8395afe6ff6303799c))
- **ui:** calendar header redesign ([#479](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/479)) ([9c766c2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9c766c26959673389e372c3a440c1a73d5887b1d)) * **ui:** calendar header redesign ([#479](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/479)) ([9c766c2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9c766c26959673389e372c3a440c1a73d5887b1d))
- **ui:** calendar sidebar redesign ([#464](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/464)) ([843cb5b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/843cb5b4430885527592feee33656dfae50c95c2)) * **ui:** calendar sidebar redesign ([#464](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/464)) ([843cb5b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/843cb5b4430885527592feee33656dfae50c95c2))
- **ui:** change icons to phosphor-icons [#467](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/467) ([#469](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/469)) ([37bd7e7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/37bd7e79d9202d55c7d0f566518526b0aed53c68)) * **ui:** change icons to phosphor-icons [#467](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/467) ([#469](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/469)) ([37bd7e7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/37bd7e79d9202d55c7d0f566518526b0aed53c68))
- **ui:** change red text when instructor not found ([#483](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/483)) ([52347fd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/52347fd56dff95190915d9f375dbebe1383e76c8)) * **ui:** change red text when instructor not found ([#483](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/483)) ([52347fd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/52347fd56dff95190915d9f375dbebe1383e76c8))
- **ui:** color picker final touches ([#491](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/491)) ([c2328e4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c2328e461ea0a846f399a22bc525540da2796dcd)) * **ui:** color picker final touches ([#491](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/491)) ([c2328e4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c2328e461ea0a846f399a22bc525540da2796dcd))
- **ui:** course color picker ([#382](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/382)) ([1f635d2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/1f635d2515fc403ad7f08fc3a244a17d262e3f7b)) * **ui:** course color picker ([#382](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/382)) ([1f635d2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/1f635d2515fc403ad7f08fc3a244a17d262e3f7b))
- **ui:** course unique number copy button ([#490](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/490)) ([501f506](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/501f50667725aa1515391869b133908b8383d868)) * **ui:** course unique number copy button ([#490](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/490)) ([501f506](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/501f50667725aa1515391869b133908b8383d868))
- **ui:** Modify Calendar Footer design and Unscheduled courses ([#503](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/503)) ([b171f01](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b171f01d01f47c75e7df5e5c8e4e7b15b7397c52)) * **ui:** Modify Calendar Footer design and Unscheduled courses ([#503](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/503)) ([b171f01](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b171f01d01f47c75e7df5e5c8e4e7b15b7397c52))
- **ui:** redesign grade distribution tooltip ([#485](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/485)) ([a61bddf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a61bddf0e8a76c89d4963d9e580fb063309bee92)) * **ui:** redesign grade distribution tooltip ([#485](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/485)) ([a61bddf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a61bddf0e8a76c89d4963d9e580fb063309bee92))
- **ui:** update button variants following figma ([#482](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/482)) ([0aa469a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0aa469af81e7912a6b26ee1b80c5920f677b5fbd)) * **ui:** update button variants following figma ([#482](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/482)) ([0aa469a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0aa469af81e7912a6b26ee1b80c5920f677b5fbd))
- **ui:** update popup and course blocks ([#506](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/506)) ([ee4c6ce](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ee4c6ce6999ad35821b9be5d657790f2dee017b3)) * **ui:** update popup and course blocks ([#506](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/506)) ([ee4c6ce](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ee4c6ce6999ad35821b9be5d657790f2dee017b3))
- **ui:** update theme colors [#466](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/466) ([#473](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/473)) ([0d73b13](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0d73b13b288bf8b18444f26455da24c2f4acedf6)) * **ui:** update theme colors [#466](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/466) ([#473](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/473)) ([0d73b13](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0d73b13b288bf8b18444f26455da24c2f4acedf6))
- update SWE list ([aa29bcf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/aa29bcf9fe4bad7812336d55d7575a6032aea91c)) * update SWE list ([aa29bcf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/aa29bcf9fe4bad7812336d55d7575a6032aea91c))
- update text styles ([#468](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/468)) ([918f4e4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/918f4e419cf5fa71bda1112597e9a373daca69ea)), closes [#465](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/465) * update text styles ([#468](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/468)) ([918f4e4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/918f4e419cf5fa71bda1112597e9a373daca69ea)), closes [#465](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/465)
- validate login passed to background and implemented into add all injected button ([#443](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/443)) ([cd05e5e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cd05e5e7fcaa02dab30f230c178f189d8052a7c9)) * validate login passed to background and implemented into add all injected button ([#443](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/443)) ([cd05e5e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cd05e5e7fcaa02dab30f230c178f189d8052a7c9))
### Bug Fixes ### Bug Fixes
- 4th attempt for: now able to delete schedule even if active ([#435](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/435)) ([2425679](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/24256798ba7b877dd170d77ce4970b13b8a69a68)) * 4th attempt for: now able to delete schedule even if active ([#435](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/435)) ([2425679](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/24256798ba7b877dd170d77ce4970b13b8a69a68))
- instructor formatting errors ([#425](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/425)) ([8b92208](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8b922082a79ebdbf50a09f37ecb9dfea1be6e1f3)) * instructor formatting errors ([#425](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/425)) ([8b92208](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8b922082a79ebdbf50a09f37ecb9dfea1be6e1f3))
- place hours and courses under schedule name ([#388](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/388)) ([7dd9369](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7dd93690d6dbd206cb50442f42ec85d6bbfc1da8)) * place hours and courses under schedule name ([#388](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/388)) ([7dd9369](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7dd93690d6dbd206cb50442f42ec85d6bbfc1da8))
- transition added ([#381](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/381)) ([598bafe](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/598bafe67f47ad57b3801a425e61f86093ba2be8)) * transition added ([#381](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/381)) ([598bafe](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/598bafe67f47ad57b3801a425e61f86093ba2be8))
- **ui:** fix longstanding drag-and-drop duplication issue ([#502](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/502)) ([4752f58](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4752f5860a96e08a1177c0ddf57a0fa269d89072)) * **ui:** fix longstanding drag-and-drop duplication issue ([#502](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/502)) ([4752f58](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4752f5860a96e08a1177c0ddf57a0fa269d89072))
- **ui:** reduce left side grade distribution margin/padding ([#427](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/427)) ([91fa78e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/91fa78e2d0532a73e24ecc013d2ba6c0f62c1fcd)) * **ui:** reduce left side grade distribution margin/padding ([#427](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/427)) ([91fa78e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/91fa78e2d0532a73e24ecc013d2ba6c0f62c1fcd))
- **ui:** stop import button dropdown from squishing ([#504](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/504)) ([846070e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/846070ebb5e13e0fc0df94666dbce75194100049)) * **ui:** stop import button dropdown from squishing ([#504](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/504)) ([846070e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/846070ebb5e13e0fc0df94666dbce75194100049))
- updated text when time/location not provided ([#289](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/289)) ([ebcc0aa](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ebcc0aa76a89f0d1a9e90cfc50d70f017c9bed42)) * updated text when time/location not provided ([#289](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/289)) ([ebcc0aa](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ebcc0aa76a89f0d1a9e90cfc50d70f017c9bed42))
## [2.0.2](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.0.1...v2.0.2) (2024-11-05) ## [2.0.2](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.0.1...v2.0.2) (2024-11-05)
### Features ### Features
- add core curriculum chips to injected popup ([#372](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/372)) ([6f1afc5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6f1afc5b25441c6a1fbfdf57b3c8b5b74e36f5a0)) * add core curriculum chips to injected popup ([#372](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/372)) ([6f1afc5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6f1afc5b25441c6a1fbfdf57b3c8b5b74e36f5a0))
- Add linkedin social to calendar ([#368](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/368)) ([b6eccac](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b6eccaca6a2cdba9b57d2f49f064ae8504bbd5cb)) * Add linkedin social to calendar ([#368](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/368)) ([b6eccac](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b6eccaca6a2cdba9b57d2f49f064ae8504bbd5cb))
- add more relevant links to the From the Team section ([#380](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/380)) ([643ea13](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/643ea1320798aabb7783d267f5e6fd7c00fc2e3f)) * add more relevant links to the From the Team section ([#380](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/380)) ([643ea13](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/643ea1320798aabb7783d267f5e6fd7c00fc2e3f))
- bold course number in grade distribution chart, change text to ut-black ([#406](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/406)) ([638ee88](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/638ee88c96510a779c157b524903caaeffc9ef19)) * bold course number in grade distribution chart, change text to ut-black ([#406](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/406)) ([638ee88](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/638ee88c96510a779c157b524903caaeffc9ef19))
- disable/some actions when no instructor ([#319](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/319)) ([839f9c6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/839f9c6d6afd4a1eae1a0bdf8893ab2e19b9fdff)) * disable/some actions when no instructor ([#319](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/319)) ([839f9c6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/839f9c6d6afd4a1eae1a0bdf8893ab2e19b9fdff))
- **ui:** changed popup close icon to ut-black ([#394](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/394)) ([0077ae7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0077ae70d22f24549c4c3b243188d19adbfbac14)), closes [#333F48](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/333F48) * **ui:** changed popup close icon to ut-black ([#394](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/394)) ([0077ae7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0077ae70d22f24549c4c3b243188d19adbfbac14)), closes [#333F48](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/333F48)
- update senior swe admins ([#326](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/326)) ([b967240](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b967240f8fbb7a790a78f4aa256f0a77a491abb8)) * update senior swe admins ([#326](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/326)) ([b967240](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b967240f8fbb7a790a78f4aa256f0a77a491abb8))
- update useful links ([#367](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/367)) ([cef99c2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cef99c2d72d3a2800f8a918d01cb116f8795d0c8)) * update useful links ([#367](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/367)) ([cef99c2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cef99c2d72d3a2800f8a918d01cb116f8795d0c8))
- use "copy of" for duplicated schedules and place them under the original schedule [#358](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/358) ([#397](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/397)) ([94744e0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/94744e01b94819fb4f5d64616ea56857b906c2dd)) * use "copy of" for duplicated schedules and place them under the original schedule [#358](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/358) ([#397](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/397)) ([94744e0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/94744e01b94819fb4f5d64616ea56857b906c2dd))
### Bug Fixes ### Bug Fixes
- added descending sort for commits on contributor section in settings page ([#365](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/365)) ([a715bbd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a715bbd933a87742e7bce3a44e8ba1bd419ad5eb)), closes [#363](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/363) [#363](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/363) [#363](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/363) * added descending sort for commits on contributor section in settings page ([#365](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/365)) ([a715bbd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a715bbd933a87742e7bce3a44e8ba1bd419ad5eb)), closes [#363](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/363) [#363](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/363) [#363](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/363)
- change schedule total courses text color to UTRP black ([#369](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/369)) ([b00bf6c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b00bf6c180f1c6c3a61c5ef855e160ddf4af3ea4)) * change schedule total courses text color to UTRP black ([#369](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/369)) ([b00bf6c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b00bf6c180f1c6c3a61c5ef855e160ddf4af3ea4))
- changed the font-weight of h1-course ([#370](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/370)) ([4f609ae](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4f609aeec797c1f99f0a57e5aeef7b82756ea4bc)), closes [#347](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/347) * changed the font-weight of h1-course ([#370](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/370)) ([4f609ae](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4f609aeec797c1f99f0a57e5aeef7b82756ea4bc)), closes [#347](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/347)
- ensure input elements take full width of parent ([#364](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/364)) ([c2007ef](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c2007ef090aab3bbfcb8bca1ebc476255d09cb90)) * ensure input elements take full width of parent ([#364](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/364)) ([c2007ef](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c2007ef090aab3bbfcb8bca1ebc476255d09cb90))
- remove screenshot padding class for png download for [#344](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/344) ([#376](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/376)) ([768ac77](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/768ac776ed4d5ca2113a032a93c2dc7432915aa1)), closes [#334](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/334) * remove screenshot padding class for png download for [#344](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/344) ([#376](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/376)) ([768ac77](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/768ac776ed4d5ca2113a032a93c2dc7432915aa1)), closes [#334](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/334)
- sentry issues ([#389](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/389)) ([2d0804f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2d0804f90e5d7a9ff83f7fd5c5acfdc7c1b1cc84)) * sentry issues ([#389](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/389)) ([2d0804f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2d0804f90e5d7a9ff83f7fd5c5acfdc7c1b1cc84))
- typo in settings page ([#386](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/386)) ([d357735](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d3577358d0d1fb60f2c776ae4b01e255fcf9109e)) * typo in settings page ([#386](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/386)) ([d357735](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d3577358d0d1fb60f2c776ae4b01e255fcf9109e))
- **ui:** add space before/after forward slash in "ASYNC/OTHER" text ([#366](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/366)) ([86792eb](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/86792eb56f04b615f7d52b2f417b88f4cb9a82ec)) * **ui:** add space before/after forward slash in "ASYNC/OTHER" text ([#366](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/366)) ([86792eb](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/86792eb56f04b615f7d52b2f417b88f4cb9a82ec))
- **ui:** duplicate schedule warning ([#295](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/295)) ([7346720](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/73467208947e0116ce8538052ee75dea1d8038f9)) * **ui:** duplicate schedule warning ([#295](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/295)) ([7346720](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/73467208947e0116ce8538052ee75dea1d8038f9))
- **ui:** main popup now shows 0 for empty schedule ([#395](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/395)) ([8de88d6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8de88d6ad7d4c2b5c3aa08e1efc59f7226b40c6b)) * **ui:** main popup now shows 0 for empty schedule ([#395](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/395)) ([8de88d6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8de88d6ad7d4c2b5c3aa08e1efc59f7226b40c6b))
- **ui:** multiple instructors are formatted properly, displays last name only, and are capitalized in all course blocks ([#342](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/342)) ([#403](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/403)) ([50e88fa](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/50e88fa015e0290fbe0dab8a19f8fcdbc4dd02b0)) * **ui:** multiple instructors are formatted properly, displays last name only, and are capitalized in all course blocks ([#342](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/342)) ([#403](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/403)) ([50e88fa](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/50e88fa015e0290fbe0dab8a19f8fcdbc4dd02b0))
- **ui:** placeholder text for no instructor course [#400](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/400) ([#402](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/402)) ([b3ae91d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b3ae91d8f3cebb89e5e5cea7f1200d28326afb4d)) * **ui:** placeholder text for no instructor course [#400](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/400) ([#402](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/402)) ([b3ae91d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b3ae91d8f3cebb89e5e5cea7f1200d28326afb4d))
## [2.0.1](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.0.0...v2.0.1) (2024-10-17) ## [2.0.1](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/v2.0.0...v2.0.1) (2024-10-17)
### Features ### Features
- spring 2024 instructors db ([#317](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/317)) ([79dd29c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/79dd29cfc9e849b09e7d91bd0eed51c1c93b3352)) * spring 2024 instructors db ([#317](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/317)) ([79dd29c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/79dd29cfc9e849b09e7d91bd0eed51c1c93b3352))
### Bug Fixes ### Bug Fixes
- add a little error checking to settings ([#315](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/315)) ([e261641](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e261641e5985d8bd5047d8a0be5d1caae844e40f)) * add a little error checking to settings ([#315](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/315)) ([e261641](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e261641e5985d8bd5047d8a0be5d1caae844e40f))
- gulp zip ([#314](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/314)) ([05f00b2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/05f00b23d26b90f564710db4364426e90c8d6831)) * gulp zip ([#314](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/314)) ([05f00b2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/05f00b23d26b90f564710db4364426e90c8d6831))
- migration loop ([aeff5e0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/aeff5e09a238503293c3882d97d40270da1e4883)) * migration loop ([aeff5e0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/aeff5e09a238503293c3882d97d40270da1e4883))
- show calendar in active window ([#312](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/312)) ([ceba38b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ceba38b1ac74ec9e6630222183bd466a8d12c27d)) * show calendar in active window ([#312](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/312)) ([ceba38b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ceba38b1ac74ec9e6630222183bd466a8d12c27d))
## [2.0.0](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/b4e8c7589e53f1064d70703459cc6d66fae1b04c...v2.0.0) (2024-10-15) ## [2.0.0](https://github.com/Longhorn-Developers/UT-Registration-Plus/compare/b4e8c7589e53f1064d70703459cc6d66fae1b04c...v2.0.0) (2024-10-15)
### Features ### Features
- abhinavchadaga/course-catalog-popup ([#128](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/128)) ([745f9dd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/745f9dd6fb96ccc0eace7189b87abf9a38f03828)) * abhinavchadaga/course-catalog-popup ([#128](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/128)) ([745f9dd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/745f9dd6fb96ccc0eace7189b87abf9a38f03828))
- abhinavchadaga/reusable-popup-prompt ([#148](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/148)) ([44af9e1](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/44af9e16e4bf3b92c9a4fa0e779197f4f9ecb237)) * abhinavchadaga/reusable-popup-prompt ([#148](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/148)) ([44af9e1](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/44af9e16e4bf3b92c9a4fa0e779197f4f9ecb237))
- actually sum for duplicate semesters (different uniques) ([#202](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/202)) ([d3f64ec](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d3f64ec79eb64ebe72ae28991127dbe521823842)) * actually sum for duplicate semesters (different uniques) ([#202](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/202)) ([d3f64ec](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d3f64ec79eb64ebe72ae28991127dbe521823842))
- add button to the rows, use new ConflictsWithWarning component ([bec2649](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bec2649fc1244bffb5cb03809db62cc0ea477260)) * add button to the rows, use new ConflictsWithWarning component ([bec2649](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bec2649fc1244bffb5cb03809db62cc0ea477260))
- add buttons with icons in tailwind ([93b65ac](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/93b65ac2edef3fc728305cc51d1a32ecf5073274)) * add buttons with icons in tailwind ([93b65ac](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/93b65ac2edef3fc728305cc51d1a32ecf5073274))
- add cal save buttons (no functionality) ([53e7c7f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/53e7c7fedaf1f268d2d7d06ff1a5df9e2af2306d)) * add cal save buttons (no functionality) ([53e7c7f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/53e7c7fedaf1f268d2d7d06ff1a5df9e2af2306d))
- add Calendar Component ([e0212d5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e0212d510928e5e95d3ba75e391c34714f1bb035)) * add Calendar Component ([e0212d5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e0212d510928e5e95d3ba75e391c34714f1bb035))
- add Calendar schedules component (clicking on storybook not working) ([d1a336e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d1a336e903f09b04b89f11ba7ede2f7d26e0ac3c)) * add Calendar schedules component (clicking on storybook not working) ([d1a336e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d1a336e903f09b04b89f11ba7ede2f7d26e0ac3c))
- add CalendarHeader and its Storybook, need to resize ([23276e5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/23276e5c7c907261b1d02e9006d8021d99b43984)) * add CalendarHeader and its Storybook, need to resize ([23276e5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/23276e5c7c907261b1d02e9006d8021d99b43984))
- add check-path-alias custom ESLint rule ([#123](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/123)) ([208103d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/208103d7081abcf5b31bf36335320aaf7d213464)) * add check-path-alias custom ESLint rule ([#123](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/123)) ([208103d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/208103d7081abcf5b31bf36335320aaf7d213464))
- add chrome.storage api mocks for storybook use ([#141](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/141)) ([3839bff](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3839bff29ef6f0ecfad1e975e685df9a64d96c92)) * add chrome.storage api mocks for storybook use ([#141](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/141)) ([3839bff](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3839bff29ef6f0ecfad1e975e685df9a64d96c92))
- add CSS for calendarCell div ([6cdcf49](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6cdcf4930d9868c0ed45820f66102d89a644cfb6)) * add CSS for calendarCell div ([6cdcf49](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6cdcf4930d9868c0ed45820f66102d89a644cfb6))
- add CSS for hourLine div ([0ba6153](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0ba61534cbfc83408cb64eb1145ecacef1ee8f04)) * add CSS for hourLine div ([0ba6153](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0ba61534cbfc83408cb64eb1145ecacef1ee8f04))
- add CSS for timeLabelContainer div ([56f6456](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/56f6456ce85f77edb9d6fa64ecb82a8388feec41)) * add CSS for timeLabelContainer div ([56f6456](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/56f6456ce85f77edb9d6fa64ecb82a8388feec41))
- add custom ESLint rule restrict-import-depth ([#110](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/110)) ([8c069b7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8c069b7ad339ed8db97bb5e3e6e88ab8d332489b)) * add custom ESLint rule restrict-import-depth ([#110](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/110)) ([8c069b7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8c069b7ad339ed8db97bb5e3e6e88ab8d332489b))
- add downloadBlob util ([2af351e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2af351efa85264da5abeee27fbd091d4ce637041)) * add downloadBlob util ([2af351e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2af351efa85264da5abeee27fbd091d4ce637041))
- add empty settings component - waiting on design ([9d0f210](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9d0f2105481cc3d5affdb4bd69d428379ea92de3)) * add empty settings component - waiting on design ([9d0f210](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9d0f2105481cc3d5affdb4bd69d428379ea92de3))
- add ImportantLinks Component ([5dbee6f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5dbee6f0c39e620c3b65a5e0156fd59ec8217ca7)) * add ImportantLinks Component ([5dbee6f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5dbee6f0c39e620c3b65a5e0156fd59ec8217ca7))
- add List component ([e6b4049](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e6b40494031ca88894b2f918bb18448b70124481)) * add List component ([e6b4049](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e6b40494031ca88894b2f918bb18448b70124481))
- add MIMEType ([0c76052](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0c760524781556cf089f1885dc4ff3fd567bfc2b)) * add MIMEType ([0c76052](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0c760524781556cf089f1885dc4ff3fd567bfc2b))
- add new db powered by UT_Grade_Parser ([#163](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/163)) ([60d1f48](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/60d1f48bd95132f0517258f017a7644d8aff5101)) * add new db powered by UT_Grade_Parser ([#163](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/163)) ([60d1f48](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/60d1f48bd95132f0517258f017a7644d8aff5101))
- add react-loading-skeleton package ([#244](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/244)) ([b4dd91a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b4dd91ad2595d9583a60530c2c2ff6d829d2d5e5)) * add react-loading-skeleton package ([#244](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/244)) ([b4dd91a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b4dd91ad2595d9583a60530c2c2ff6d829d2d5e5))
- add skeleton loader for course description + distribution ([#267](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/267)) ([c2cab40](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c2cab407f3a07c1d3073e936c9d23cb1cecc9cb9)) * add skeleton loader for course description + distribution ([#267](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/267)) ([c2cab40](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c2cab407f3a07c1d3073e936c9d23cb1cecc9cb9))
- add story for CalendarGridCell ([fd91c3b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/fd91c3b12ef5670563e689d190be75ef1f695d1c)) * add story for CalendarGridCell ([fd91c3b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/fd91c3b12ef5670563e689d190be75ef1f695d1c))
- add Storybook story ([cb3cb5d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cb3cb5d5fc18a68ca5a825f97b808b535aa1fe29)) * add Storybook story ([cb3cb5d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cb3cb5d5fc18a68ca5a825f97b808b535aa1fe29))
- add tailwind version of Button component ([28d93b3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/28d93b3c25e1cbca3baea992a299d626ca823570)) * add tailwind version of Button component ([28d93b3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/28d93b3c25e1cbca3baea992a299d626ca823570))
- add tickmarks to day div ([8b9cb06](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8b9cb065c28b5dd38b7e1681839a81f55691a3c6)) * add tickmarks to day div ([8b9cb06](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8b9cb065c28b5dd38b7e1681839a81f55691a3c6))
- add time column to Calendar Component ([9d68211](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9d6821127e7ff9bf63cef659932ee7408de4c969)) * add time column to Calendar Component ([9d68211](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9d6821127e7ff9bf63cef659932ee7408de4c969))
- add timeAndGrid div ([95e0544](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/95e0544b7301d382439b74038fb1b3e25dfe6d30)) * add timeAndGrid div ([95e0544](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/95e0544b7301d382439b74038fb1b3e25dfe6d30))
- added flag ;-; ([#195](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/195)) ([9b4d61c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9b4d61c2b0d8806b5d4a10296c1cc1266ac4229f)) * added flag ;-; ([#195](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/195)) ([9b4d61c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9b4d61c2b0d8806b5d4a10296c1cc1266ac4229f))
- added scrapedAt property ([#149](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/149)) ([8e181b3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8e181b3010dfe25ea1e4b4ba181cbe9944d6e92e)) * added scrapedAt property ([#149](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/149)) ([8e181b3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8e181b3010dfe25ea1e4b4ba181cbe9944d6e92e))
- additional changes to [#201](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/201) ([#224](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/224)) ([bc354f3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bc354f3798a02e6ebc3c66a8b5eee553c25a19d4)) * additional changes to [#201](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/201) ([#224](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/224)) ([bc354f3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bc354f3798a02e6ebc3c66a8b5eee553c25a19d4))
- aesthetically pleasing squishier course blocks when compressed ([#232](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/232)) ([438c82b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/438c82bfb5db0f570b261d15368a05447939b0fb)) * aesthetically pleasing squishier course blocks when compressed ([#232](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/232)) ([438c82b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/438c82bfb5db0f570b261d15368a05447939b0fb))
- align day labels and add to grid ([1bb6191](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/1bb6191244f40220a9af66cced4fe8f0c1e546a0)) * align day labels and add to grid ([1bb6191](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/1bb6191244f40220a9af66cced4fe8f0c1e546a0))
- alignment on calendar and header ([#109](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/109)) ([07ec5ab](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/07ec5abc3e1b40b801b7c977d21399b31222a4d3)) * alignment on calendar and header ([#109](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/109)) ([07ec5ab](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/07ec5abc3e1b40b801b7c977d21399b31222a4d3))
- async course adding and async/other course block adjustments ([#273](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/273)) ([668c8d0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/668c8d00756a5e3e3daaade46380967c74eec1ae)) * async course adding and async/other course block adjustments ([#273](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/273)) ([668c8d0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/668c8d00756a5e3e3daaade46380967c74eec1ae))
- async text hiding on Calendar's Bottom Bar when there are no async courses ([#152](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/152)) ([0dff122](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0dff12232c29415fa7bc5ac3771393a406ab738d)) * async text hiding on Calendar's Bottom Bar when there are no async courses ([#152](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/152)) ([0dff122](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0dff12232c29415fa7bc5ac3771393a406ab738d))
- basic CalendarCourseMeeting component laid out - missing Text and Right Icon ([da9e7aa](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/da9e7aac4135afa5184e4949c36a288ef6ab62a7)) * basic CalendarCourseMeeting component laid out - missing Text and Right Icon ([da9e7aa](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/da9e7aac4135afa5184e4949c36a288ef6ab62a7))
- Best Practices ([#102](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/102)) ([5eb7be2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5eb7be246cd2a1db27c7ca777d81074ae7bb3b82)) * Best Practices ([#102](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/102)) ([5eb7be2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5eb7be246cd2a1db27c7ca777d81074ae7bb3b82))
- beta builds ([#187](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/187)) ([8a5e12e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8a5e12ec63539ae7199f0f1561f642cb9cf19302)) * beta builds ([#187](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/187)) ([8a5e12e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8a5e12ec63539ae7199f0f1561f642cb9cf19302))
- better discord icon ([#205](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/205)) ([a3f5e0f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a3f5e0f27ffdfcb4e3ddd47140979aca379f9f42)) * better discord icon ([#205](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/205)) ([a3f5e0f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a3f5e0f27ffdfcb4e3ddd47140979aca379f9f42))
- bold red refresh error ([#220](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/220)) ([229a8a2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/229a8a29e8d04dfbe15de200ceffc53b4f758413)) * bold red refresh error ([#220](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/220)) ([229a8a2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/229a8a29e8d04dfbe15de200ceffc53b4f758413))
- bottom bar for the calendar page ([#91](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/91)) ([0f730d6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0f730d6c50462c9f7d0428ebb379742305212ef6)) * bottom bar for the calendar page ([#91](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/91)) ([0f730d6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0f730d6c50462c9f7d0428ebb379742305212ef6))
- build without errors ([babc925](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/babc925967b01f1064b8d8e067e7bc792dd57b0a)) * build without errors ([babc925](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/babc925967b01f1064b8d8e067e7bc792dd57b0a))
- calendar components 3rd attempt at merging ([#60](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/60)) ([4faca8c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4faca8c43befe19c9b05ff4f3afba420e016bb59)) * calendar components 3rd attempt at merging ([#60](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/60)) ([4faca8c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4faca8c43befe19c9b05ff4f3afba420e016bb59))
- Calendar Components 3rd Attempt at Merging ([#60](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/60)) ([ab2cd68](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ab2cd688fae616742b39c0637291a65e508e23bd)) * Calendar Components 3rd Attempt at Merging ([#60](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/60)) ([ab2cd68](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ab2cd688fae616742b39c0637291a65e508e23bd))
- calendar course block component ([#75](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/75)) ([00e0019](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/00e00197efb807d420196f2b030fc4dc71e9cd42)) * calendar course block component ([#75](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/75)) ([00e0019](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/00e00197efb807d420196f2b030fc4dc71e9cd42))
- calendar course block component ([#75](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/75)) ([a41cb3e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a41cb3ed8711eff9a2fbbe435e867c073904f452)) * calendar course block component ([#75](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/75)) ([a41cb3e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a41cb3ed8711eff9a2fbbe435e867c073904f452))
- calendar grid and grid cells ([#81](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/81)) ([bfeb239](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bfeb2398aacf9a1fba892737b6ef0286acbc326f)) * calendar grid and grid cells ([#81](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/81)) ([bfeb239](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bfeb2398aacf9a1fba892737b6ef0286acbc326f))
- Calendar Grid and Grid Cells ([#81](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/81)) ([dd2f696](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/dd2f696f8de2c0b291e917ef4d4cb2171b76712c)) * Calendar Grid and Grid Cells ([#81](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/81)) ([dd2f696](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/dd2f696f8de2c0b291e917ef4d4cb2171b76712c))
- calendar header formatting and data displaying ([#160](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/160)) ([5cce1c7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5cce1c79fc2b50843426ed290a1b53849375e4eb)) * calendar header formatting and data displaying ([#160](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/160)) ([5cce1c7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5cce1c79fc2b50843426ed290a1b53849375e4eb))
- calendar matchings ([#173](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/173)) ([791a42b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/791a42bcd49478024b6f1a237a2d2a9f6211749e)) * calendar matchings ([#173](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/173)) ([791a42b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/791a42bcd49478024b6f1a237a2d2a9f6211749e))
- Calendar Schedule component finished, fix: list didn't allow updates when adding a new schedule ([#115](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/115)) ([a99a557](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a99a55788a0b9791ff048c8bbbaad5bada47c41f)) * Calendar Schedule component finished, fix: list didn't allow updates when adding a new schedule ([#115](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/115)) ([a99a557](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a99a55788a0b9791ff048c8bbbaad5bada47c41f))
- calendar-course-cell-color-picker ([#157](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/157)) ([df18491](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/df1849180de2c793c10e3a49d2c862d236456ec2)) * calendar-course-cell-color-picker ([#157](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/157)) ([df18491](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/df1849180de2c793c10e3a49d2c862d236456ec2))
- can open tabs, updated injected popup heading. basically done ([35fab34](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/35fab3444582b436f63c7b16a26ebd5989c4bf3e)) * can open tabs, updated injected popup heading. basically done ([35fab34](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/35fab3444582b436f63c7b16a26ebd5989c4bf3e))
- change Chip to tailwind css. Fixed eslint for ConflictsWithWarning ([3568b8e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3568b8eb5ee9ce3fe17f77f5641c0583bacc17a4)) * change Chip to tailwind css. Fixed eslint for ConflictsWithWarning ([3568b8e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3568b8eb5ee9ce3fe17f77f5641c0583bacc17a4))
- check-path-alias autofix ([#124](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/124)) ([265652c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/265652c4200a56b1ae62e6e94e0609fb4604c188)) * check-path-alias autofix ([#124](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/124)) ([265652c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/265652c4200a56b1ae62e6e94e0609fb4604c188))
- chrome extension works ([35f3c72](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/35f3c7225049d760c6061ed827aa796514291e62)) * chrome extension works ([35f3c72](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/35f3c7225049d760c6061ed827aa796514291e62))
- color palette for calendar ([#118](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/118)) ([471e55d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/471e55dcea1ae439658d00ed41570e2f218f0c3d)) * color palette for calendar ([#118](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/118)) ([471e55d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/471e55dcea1ae439658d00ed41570e2f218f0c3d))
- Conventional Commits ([#103](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/103)) ([fe599df](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/fe599dfe7514ead880596d3b75c8370bde796e09)) * Conventional Commits ([#103](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/103)) ([fe599df](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/fe599dfe7514ead880596d3b75c8370bde796e09))
- convert all LabelsAndDetails Components to Tailwind ([9e0f9df](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9e0f9df9de1a5c99657ddcef3b2eb101dd825b84)) * convert all LabelsAndDetails Components to Tailwind ([9e0f9df](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9e0f9df9de1a5c99657ddcef3b2eb101dd825b84))
- course color generation ([#179](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/179)) ([5ed81e4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5ed81e4be99a8b7f3c68a3ba70358e4cbe5cc613)) * course color generation ([#179](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/179)) ([5ed81e4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5ed81e4be99a8b7f3c68a3ba70358e4cbe5cc613))
- course colors ([#175](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/175)) ([dc77cc2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/dc77cc27da7ab521e290a8c9ba810573e677b6b8)) * course colors ([#175](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/175)) ([dc77cc2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/dc77cc27da7ab521e290a8c9ba810573e677b6b8))
- Course Flag Tooltips ([#178](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/178)) ([ba2bc60](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ba2bc60add2b87ccd4e9894c454d7a8d8382f0dd)) * Course Flag Tooltips ([#178](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/178)) ([ba2bc60](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ba2bc60add2b87ccd4e9894c454d7a8d8382f0dd))
- course-catalog-injected-popup ([#98](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/98)) ([89d03f4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/89d03f4244d324915cadadaf75a827221525d43f)) * course-catalog-injected-popup ([#98](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/98)) ([89d03f4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/89d03f4244d324915cadadaf75a827221525d43f))
- CourseStatus Component implemented ([#83](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/83)) ([58d7df4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/58d7df499ca9a8dfd4dc3aa47bc9d76837f21bf7)) * CourseStatus Component implemented ([#83](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/83)) ([58d7df4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/58d7df499ca9a8dfd4dc3aa47bc9d76837f21bf7))
- CourseStatus Component implemented ([#83](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/83)) ([fa1d737](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/fa1d7374bcafbe19673dc31f33b3f8075e45f37a)) * CourseStatus Component implemented ([#83](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/83)) ([fa1d737](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/fa1d7374bcafbe19673dc31f33b3f8075e45f37a))
- create empty Popup story ([ad83ba4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ad83ba4cdc67f86096d0cbc2cb6659aa9be98a0b)) * create empty Popup story ([ad83ba4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ad83ba4cdc67f86096d0cbc2cb6659aa9be98a0b))
- Create icon helper ([#77](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/77)) ([ccea0f4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ccea0f4bd1e8d66dc0717d92c9e970040d9fbe9e)) * Create icon helper ([#77](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/77)) ([ccea0f4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ccea0f4bd1e8d66dc0717d92c9e970040d9fbe9e))
- Create icon helper ([#77](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/77)) ([1b51d65](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/1b51d65c89a4b544f4f2a9c60106e14300f9a3b0)) * Create icon helper ([#77](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/77)) ([1b51d65](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/1b51d65c89a4b544f4f2a9c60106e14300f9a3b0))
- Derek vinson/calendar header ([#94](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/94)) ([12d09b5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/12d09b54cbf840993d17323b96cf274ec24c67ce)) * Derek vinson/calendar header ([#94](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/94)) ([12d09b5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/12d09b54cbf840993d17323b96cf274ec24c67ce))
- Derek/disable updating ([#239](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/239)) ([0ab83ef](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0ab83efd47d75fb6c278994c7dc8a6d22b4b6b83)) * Derek/disable updating ([#239](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/239)) ([0ab83ef](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0ab83efd47d75fb6c278994c7dc8a6d22b4b6b83))
- Derek/export png ([#95](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/95)) ([58c2b46](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/58c2b4634a9d065ca156d27c4af69a504b7c2d2d)) * Derek/export png ([#95](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/95)) ([58c2b46](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/58c2b4634a9d065ca156d27c4af69a504b7c2d2d))
- DialogProvider component ([#198](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/198)) ([d1b921a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d1b921a5b000693d7f3dabaf84d8b9580c361941)) * DialogProvider component ([#198](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/198)) ([d1b921a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d1b921a5b000693d7f3dabaf84d8b9580c361941))
- dividers in calendar bottom bar ([#120](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/120)) ([eba5d9f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/eba5d9f508576d2648bd2501e5d5aaff32592566)) * dividers in calendar bottom bar ([#120](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/120)) ([eba5d9f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/eba5d9f508576d2648bd2501e5d5aaff32592566))
- **docs:** add extra acknowledgements ([e2cbfa3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e2cbfa3f48cb0b10a43134cf9784a44b13aa542d)) * **docs:** add extra acknowledgements ([e2cbfa3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e2cbfa3f48cb0b10a43134cf9784a44b13aa542d))
- drag only on vertical axis ([34a6449](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/34a644952995d4aedd18d11d327bd8589f5b2610)) * drag only on vertical axis ([34a6449](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/34a644952995d4aedd18d11d327bd8589f5b2610))
- early iteration of non-virtual list ([677aa62](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/677aa624d7fb2bbb922aaf8806abe979c3399e4e)) * early iteration of non-virtual list ([677aa62](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/677aa624d7fb2bbb922aaf8806abe979c3399e4e))
- enable TS strict mode ([#168](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/168)) ([efed1c0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/efed1c0edbf93987c551952a03c9c4b3c461d819)) * enable TS strict mode ([#168](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/168)) ([efed1c0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/efed1c0edbf93987c551952a03c9c4b3c461d819))
- experimental toggle for icons, left off for now ([#237](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/237)) ([f34dd95](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f34dd95d7765322b6cecb2e817a46e0549d929a5)) * experimental toggle for icons, left off for now ([#237](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/237)) ([f34dd95](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f34dd95d7765322b6cecb2e817a46e0549d929a5))
- fall 2023 grades ([#226](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/226)) ([863d980](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/863d980b2d1c706b1fc648f1a0de2a31515e339e)) * fall 2023 grades ([#226](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/226)) ([863d980](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/863d980b2d1c706b1fc648f1a0de2a31515e339e))
- finally fix grid JSX.Element generation ([e49fc29](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e49fc295ba40a37f9bf59686adbd8b4f523cd9c6)) * finally fix grid JSX.Element generation ([e49fc29](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e49fc295ba40a37f9bf59686adbd8b4f523cd9c6))
- finish ScheduleTotalHoursAndCourses ([12f680d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/12f680d7e9aef826281c8f769825408dc6f9f9de)) * finish ScheduleTotalHoursAndCourses ([12f680d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/12f680d7e9aef826281c8f769825408dc6f9f9de))
- fix icons on PopupMain and convert to tailwind ([#108](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/108)) ([87799d8](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/87799d8f02aa5986025982f768639afdb1b0267a)) * fix icons on PopupMain and convert to tailwind ([#108](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/108)) ([87799d8](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/87799d8f02aa5986025982f768639afdb1b0267a))
- fix save as button dividers ([#153](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/153)) ([7986549](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7986549fdd40834a85bedbfd0862cdb21c9ae657)) * fix save as button dividers ([#153](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/153)) ([7986549](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7986549fdd40834a85bedbfd0862cdb21c9ae657))
- grades by professor ([#225](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/225)) ([78d749a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/78d749a8a96f63c32663b464e5b663f932c28ed0)) * grades by professor ([#225](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/225)) ([78d749a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/78d749a8a96f63c32663b464e5b663f932c28ed0))
- html2canvas -> htmlToImage and fix derick's bugs ([bda0282](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bda02826b14b96fd65a6f24218950e510c5aa3d8)) * html2canvas -> htmlToImage and fix derick's bugs ([bda0282](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bda02826b14b96fd65a6f24218950e510c5aa3d8))
- icon added successfully ([3b588c2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3b588c20394523ddf90456e1ca58546e5b820c74)) * icon added successfully ([3b588c2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3b588c20394523ddf90456e1ca58546e5b820c74))
- implement Chip component ([23e881f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/23e881f14cc80100d8f8deef6a25daaa72d83f07)) * implement Chip component ([23e881f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/23e881f14cc80100d8f8deef6a25daaa72d83f07))
- implement flatten course schedule helper function ([e54f488](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e54f488b170d5a9af646c1ac82e8b33579fef61d)) * implement flatten course schedule helper function ([e54f488](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e54f488b170d5a9af646c1ac82e8b33579fef61d))
- implemented ConflictsWithWarning ([93f3a30](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/93f3a307b4acce6cc55fc411f1ea2d7576006b00)) * implemented ConflictsWithWarning ([93f3a30](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/93f3a307b4acce6cc55fc411f1ea2d7576006b00))
- implemented InfoCard ([21b6430](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/21b643000d884442e025534802be6bb1f6ba68e2)) * implemented InfoCard ([21b6430](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/21b643000d884442e025534802be6bb1f6ba68e2))
- Initial Splash Text Commit ([#208](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/208)) ([0534f60](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0534f607a51f1dd0905f6f45f08519cba65ea05a)) * Initial Splash Text Commit ([#208](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/208)) ([0534f60](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0534f607a51f1dd0905f6f45f08519cba65ea05a))
- limit schedules to 10 ([#272](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/272)) ([290b841](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/290b8415170bca631e5d71b08d88db80b02163f0)) * limit schedules to 10 ([#272](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/272)) ([290b841](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/290b8415170bca631e5d71b08d88db80b02163f0))
- link to your registered courses ([#228](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/228)) ([f83e012](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f83e012d62e268d95dce19a30e54e34f3779142b)) * link to your registered courses ([#228](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/228)) ([f83e012](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f83e012d62e268d95dce19a30e54e34f3779142b))
- list reordering ([#154](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/154)) ([038ebaa](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/038ebaa2689c14bc9906afb542ef62c21cb13177)) * list reordering ([#154](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/154)) ([038ebaa](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/038ebaa2689c14bc9906afb542ef62c21cb13177))
- listed versioning for beta builds ([#192](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/192)) ([0c42979](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0c429794230a91c4fd949715b7eea210ac18fee3)) * listed versioning for beta builds ([#192](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/192)) ([0c42979](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0c429794230a91c4fd949715b7eea210ac18fee3))
- lowercase instructor! ([#268](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/268)) ([9ec05ef](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9ec05ef764b2451c05e2d084d92074fb1e984268)) * lowercase instructor! ([#268](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/268)) ([9ec05ef](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9ec05ef764b2451c05e2d084d92074fb1e984268))
- made List more extensible ([cd34601](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cd34601379d5cd2b53d967afbc954178834823b9)) * made List more extensible ([cd34601](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cd34601379d5cd2b53d967afbc954178834823b9))
- match calendar designs & add functionality ([#176](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/176)) ([8027c3d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8027c3d1bf228053acbad3e232b030c9ddbaca6a)) * match calendar designs & add functionality ([#176](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/176)) ([8027c3d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8027c3d1bf228053acbad3e232b030c9ddbaca6a))
- migrate styles to TailwindCSS ([7e2f5ea](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7e2f5eaed7d782bc886545f7241e0891d55cfae0)) * migrate styles to TailwindCSS ([7e2f5ea](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7e2f5eaed7d782bc886545f7241e0891d55cfae0))
- migration update showing ([#293](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/293)) ([aede681](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/aede681d4bb5b01723eb9d68527f4e64991adfab)) * migration update showing ([#293](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/293)) ([aede681](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/aede681d4bb5b01723eb9d68527f4e64991adfab))
- missed one chip toggle there ([#245](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/245)) ([5ca24da](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5ca24dab82509d34cf43af4359940476159d052f)) * missed one chip toggle there ([#245](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/245)) ([5ca24da](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5ca24dab82509d34cf43af4359940476159d052f))
- newer grades and parameterized queries ([#238](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/238)) ([75ad416](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/75ad4167b6639bbcf58e2dc6dafc80ed4656a899)) * newer grades and parameterized queries ([#238](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/238)) ([75ad416](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/75ad4167b6639bbcf58e2dc6dafc80ed4656a899))
- one single exclamation mark did all that ([#235](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/235)) ([be87e41](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/be87e4181430e0df41683be5c9d0a1ce992b2c61)) * one single exclamation mark did all that ([#235](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/235)) ([be87e41](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/be87e4181430e0df41683be5c9d0a1ce992b2c61))
- open an injected course page on course block click in popup main ([#146](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/146)) ([2709484](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/27094846f70feb8d83c8d464ab0fa8dcd99b3e71)) * open an injected course page on course block click in popup main ([#146](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/146)) ([2709484](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/27094846f70feb8d83c8d464ab0fa8dcd99b3e71))
- pad unique ids to 5 digits ([#170](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/170)) ([b4ad687](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b4ad6870bfc88f68bdd7f87fe1d94c23d0a02b95)) * pad unique ids to 5 digits ([#170](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/170)) ([b4ad687](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b4ad6870bfc88f68bdd7f87fe1d94c23d0a02b95))
- parallelize initializeDB.ts promises ([9f1dcc6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9f1dcc667df9857e3d0469fe7112a9f985aad490)) * parallelize initializeDB.ts promises ([9f1dcc6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9f1dcc667df9857e3d0469fe7112a9f985aad490))
- popout icon for ccip in calendar ([#221](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/221)) ([6812d68](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6812d685d08902f7b3d9d2e5293350c8170c6f06)) * popout icon for ccip in calendar ([#221](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/221)) ([6812d68](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6812d685d08902f7b3d9d2e5293350c8170c6f06))
- popup schedule select ([#126](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/126)) ([7f2a589](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7f2a5893d4dd82a0f077dca6dfd28e79f929766f)) * popup schedule select ([#126](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/126)) ([7f2a589](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7f2a5893d4dd82a0f077dca6dfd28e79f929766f))
- PopupCourseBlock Component ([#79](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/79)) ([9accd17](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9accd17bd418f8c9334e80b0f7e38673092ba208)) * PopupCourseBlock Component ([#79](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/79)) ([9accd17](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9accd17bd418f8c9334e80b0f7e38673092ba208))
- PopupCourseBlock Component ([#79](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/79)) ([f045b40](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f045b400a56569f7353eb00ecd0d081b68660fc5)) * PopupCourseBlock Component ([#79](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/79)) ([f045b40](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f045b400a56569f7353eb00ecd0d081b68660fc5))
- proper injected styles ([#164](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/164)) ([e919e96](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e919e96c53bc9bec7db3d09693d1ff0684b56e87)) * proper injected styles ([#164](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/164)) ([e919e96](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e919e96c53bc9bec7db3d09693d1ff0684b56e87))
- readme animation ([#281](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/281)) ([da6d86c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/da6d86c785f92f70f52ef0b4cab3dc14e0bd9790)) * readme animation ([#281](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/281)) ([da6d86c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/da6d86c785f92f70f52ef0b4cab3dc14e0bd9790))
- refactor all components in common ([e544312](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e5443122b422266a126de497b475020e298b2f1c)) * refactor all components in common ([e544312](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e5443122b422266a126de497b475020e298b2f1c))
- refactor all components in injected ([0c44849](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0c44849e15333c3a5cbdf8972eb31ada0e830b0a)) * refactor all components in injected ([0c44849](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0c44849e15333c3a5cbdf8972eb31ada0e830b0a))
- refactor calendar ([28f1924](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/28f192472b7f244253b6a0b5339491b9740880f7)) * refactor calendar ([28f1924](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/28f192472b7f244253b6a0b5339491b9740880f7))
- Refactor database initialization code ([5e98f45](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5e98f45210346f0e7bb69af5cf25e336c76416ca)) * Refactor database initialization code ([5e98f45](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5e98f45210346f0e7bb69af5cf25e336c76416ca))
- release notes ([#283](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/283)) ([bd17e33](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bd17e3353792df70ca2e33c8b7a32b5fb9acc7d2)) * release notes ([#283](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/283)) ([bd17e33](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bd17e3353792df70ca2e33c8b7a32b5fb9acc7d2))
- report issue popup ([#261](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/261)) ([65ff6bf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/65ff6bfbbfc6b696621628e448865155d0405f7f)) * report issue popup ([#261](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/261)) ([65ff6bf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/65ff6bfbbfc6b696621628e448865155d0405f7f))
- rerouted directory to syllabus when click on professor name ([#211](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/211)) ([8959e0d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8959e0d9f7fdd82125f391f1ec51aa390ba9450d)) * rerouted directory to syllabus when click on professor name ([#211](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/211)) ([8959e0d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8959e0d9f7fdd82125f391f1ec51aa390ba9450d))
- sam's jokes ([#278](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/278)) ([895cd31](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/895cd31d8eb40e7e99fc0969b543e95bd070f6b7)) * sam's jokes ([#278](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/278)) ([895cd31](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/895cd31d8eb40e7e99fc0969b543e95bd070f6b7))
- save as PNG functionality ([ad18fbd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ad18fbd16235be0c75121b527867fac708670300)) * save as PNG functionality ([ad18fbd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ad18fbd16235be0c75121b527867fac708670300))
- schedule list item action menu ([#230](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/230)) ([15fc369](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/15fc3699cfda3ad56e5a262bd37dca166e2685dd)) * schedule list item action menu ([#230](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/230)) ([15fc369](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/15fc3699cfda3ad56e5a262bd37dca166e2685dd))
- screenshot whole page, hide certain elements, screenshot fixed size ([#180](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/180)) ([7d4c5d7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7d4c5d7be8b266b5f3caf2135c4f3fecb96d75be)) * screenshot whole page, hide certain elements, screenshot fixed size ([#180](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/180)) ([7d4c5d7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7d4c5d7be8b266b5f3caf2135c4f3fecb96d75be))
- settings page ([#260](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/260)) ([7a5c3a2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7a5c3a2e62c3a726735f9fe921f9e08d4092d0f9)) * settings page ([#260](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/260)) ([7a5c3a2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7a5c3a2e62c3a726735f9fe921f9e08d4092d0f9))
- show async courses in the bottom bar ([#204](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/204)) ([227de53](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/227de53e8453804f0791f269dae64ec388136390)) * show async courses in the bottom bar ([#204](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/204)) ([227de53](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/227de53e8453804f0791f269dae64ec388136390))
- some small changes for colors and font ([#201](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/201)) ([bae1da4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bae1da43d3373e12d593682e9fb69e19557b5749)) * some small changes for colors and font ([#201](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/201)) ([bae1da4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bae1da43d3373e12d593682e9fb69e19557b5749))
- splash text additions before v2 release ([#296](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/296)) ([e774f31](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e774f316e3f92c03e79274a55f1f729178c9a14a)) * splash text additions before v2 release ([#296](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/296)) ([e774f31](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e774f316e3f92c03e79274a55f1f729178c9a14a))
- splash text has arrived! ([#246](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/246)) ([9971435](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9971435716dec11e954dc26c14bf7d1505563d0d)) * splash text has arrived! ([#246](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/246)) ([9971435](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9971435716dec11e954dc26c14bf7d1505563d0d))
- Storybook for Vite ([#52](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/52)) ([9cc299c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9cc299ced6132644a5c2375b95a8a16a3482601b)) * Storybook for Vite ([#52](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/52)) ([9cc299c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9cc299ced6132644a5c2375b95a8a16a3482601b))
- swe title updates ([#310](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/310)) ([4629626](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4629626a31934c491758ddca874e5018d9c22e57)) * swe title updates ([#310](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/310)) ([4629626](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4629626a31934c491758ddca874e5018d9c22e57))
- switch button ([#229](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/229)) ([abae7a5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/abae7a5c22ab944262d66ae897298b008a03c5f1)) * switch button ([#229](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/229)) ([abae7a5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/abae7a5c22ab944262d66ae897298b008a03c5f1))
- temporary removal of waitlist etc ([#236](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/236)) ([d424ccc](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d424ccce49ba05305b24cb61aeb4ffeada5bab33)) * temporary removal of waitlist etc ([#236](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/236)) ([d424ccc](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d424ccce49ba05305b24cb61aeb4ffeada5bab33))
- UnoCSS (TailwindCSS) (Storybook only) ([#61](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/61)) ([85c7f78](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/85c7f7817c58f13f6a4e8bec13a45f6412ad87db)) * UnoCSS (TailwindCSS) (Storybook only) ([#61](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/61)) ([85c7f78](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/85c7f7817c58f13f6a4e8bec13a45f6412ad87db))
- UnoCSS (TailwindCSS) (Storybook only) ([#61](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/61)) ([6521a4b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6521a4b2a90af06e182c779f2fc7cb1a3b1e55c1)) * UnoCSS (TailwindCSS) (Storybook only) ([#61](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/61)) ([6521a4b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6521a4b2a90af06e182c779f2fc7cb1a3b1e55c1))
- unplugin-icons ([#62](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/62)) ([2d67b12](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2d67b1218ff852c59901b77909615c5f54794a67)) * unplugin-icons ([#62](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/62)) ([2d67b12](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2d67b1218ff852c59901b77909615c5f54794a67))
- unplugin-icons ([#62](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/62)) ([945e09b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/945e09b49e4ffa9bd4b02442f6567bb99831923e)) * unplugin-icons ([#62](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/62)) ([945e09b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/945e09b49e4ffa9bd4b02442f6567bb99831923e))
- update admin titles ([4cf8c3f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4cf8c3f9645cd8fda07244413cdc406105b12fec)) * update admin titles ([4cf8c3f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4cf8c3f9645cd8fda07244413cdc406105b12fec))
- update badge count when schedule changes ([#150](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/150)) ([a5e9e3c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a5e9e3c2145d61d8cc5788eb50fa19718e6d13bf)) * update badge count when schedule changes ([#150](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/150)) ([a5e9e3c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a5e9e3c2145d61d8cc5788eb50fa19718e6d13bf))
- update Button to v2 design ([863521f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/863521fb3bb268131bd3f369064ae10a442b4fbc)) * update Button to v2 design ([863521f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/863521fb3bb268131bd3f369064ae10a442b4fbc))
- update dialog component to headlessui ([#159](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/159)) ([442be8c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/442be8cbee06ec403467b100d8d5300dd4a7ea72)) * update dialog component to headlessui ([#159](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/159)) ([442be8c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/442be8cbee06ec403467b100d8d5300dd4a7ea72))
- update with TailwindCSS ([f3a8a7d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f3a8a7db560a6b28f96a2e976c8ed33af97fe77a)) * update with TailwindCSS ([f3a8a7d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f3a8a7db560a6b28f96a2e976c8ed33af97fe77a))
- updated calendar page and recruitment banner links ([#219](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/219)) ([a2303ee](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a2303ee35f10aba5fa7ff5e307d0092a39f18830)) * updated calendar page and recruitment banner links ([#219](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/219)) ([a2303ee](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a2303ee35f10aba5fa7ff5e307d0092a39f18830))
- updated divider component ([#99](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/99)) ([8ab60c9](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8ab60c9f018143df594e7bfd0d321666289cc28c)) * updated divider component ([#99](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/99)) ([8ab60c9](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8ab60c9f018143df594e7bfd0d321666289cc28c))
- updated Text component to latest design specification ([#70](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/70)) ([8b8433d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8b8433deaf8fe66f917fd66fb56cb0a748bc6e6e)) * updated Text component to latest design specification ([#70](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/70)) ([8b8433d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8b8433deaf8fe66f917fd66fb56cb0a748bc6e6e))
- updated Text component to latest design specification ([#70](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/70)) ([bb727f7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bb727f70bea5a4c2ff5dba7e9b4428c0c84de7b2)) * updated Text component to latest design specification ([#70](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/70)) ([bb727f7](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bb727f70bea5a4c2ff5dba7e9b4428c0c84de7b2))
- updating joke styling and updating jokes array ([#277](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/277)) ([0da27e2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0da27e2c46e9fc32efb80808715053c1f9f5165a)) * updating joke styling and updating jokes array ([#277](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/277)) ([0da27e2](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0da27e2c46e9fc32efb80808715053c1f9f5165a))
- use display: grid for calendarGrid ([b535a6e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b535a6eb32ac1091c8b5309ca3cfb6bab0b62526)) * use display: grid for calendarGrid ([b535a6e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b535a6eb32ac1091c8b5309ca3cfb6bab0b62526))
- use downloadBlob util ([#186](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/186)) ([2dfb10e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2dfb10e57b51a08fcde2dc6638a81b5ec9bbc7ab)) * use downloadBlob util ([#186](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/186)) ([2dfb10e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2dfb10e57b51a08fcde2dc6638a81b5ec9bbc7ab))
- use filter() instead of pop() ([063349d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/063349d96d0ad008fa0dc1f0a8c7a27cf3f108ce)) * use filter() instead of pop() ([063349d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/063349d96d0ad008fa0dc1f0a8c7a27cf3f108ce))
- use React-icons ([8df9ea5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8df9ea55a9f0bcb04c90314cb311bba9d1ebf2e3)) * use React-icons ([8df9ea5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8df9ea55a9f0bcb04c90314cb311bba9d1ebf2e3))
- UTRP v2 migration ([#292](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/292)) ([d22237d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d22237d561ddf6a2f8ed699ca4c11088a8b408e8)) * UTRP v2 migration ([#292](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/292)) ([d22237d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d22237d561ddf6a2f8ed699ca4c11088a8b408e8))
- working PNG and CAL downloads ([#119](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/119)) ([d9ee23c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d9ee23c5bbe27240a0e2849aae2eae81d7960bb5)) * working PNG and CAL downloads ([#119](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/119)) ([d9ee23c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d9ee23c5bbe27240a0e2849aae2eae81d7960bb5))
### Bug Fixes ### Bug Fixes
- add margin-top: -10px to p ([18406b0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/18406b0c94604b1b524de03bf97e78cffe7a5bbf)) * add margin-top: -10px to p ([18406b0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/18406b0c94604b1b524de03bf97e78cffe7a5bbf))
- added room number to course popup ([#231](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/231)) ([9eaff24](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9eaff24cbb6d78d7f28521a07c32630a90a96f82)) * added room number to course popup ([#231](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/231)) ([9eaff24](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9eaff24cbb6d78d7f28521a07c32630a90a96f82))
- align timeBlock div ([4dc8957](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4dc8957c459d0ee57e40c67fa61194ace832327e)) * align timeBlock div ([4dc8957](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4dc8957c459d0ee57e40c67fa61194ace832327e))
- async course display size ([#181](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/181)) ([949bbb0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/949bbb0835bfb55a81f0800850aa084b4ed1bfc4)) * async course display size ([#181](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/181)) ([949bbb0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/949bbb0835bfb55a81f0800850aa084b4ed1bfc4))
- broken close bracket ([b34aacb](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b34aacb06772af6f83d1f7bafe828ba43a10db85)) * broken close bracket ([b34aacb](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b34aacb06772af6f83d1f7bafe828ba43a10db85))
- broken file ([92462cf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/92462cf0dffa222d75965885ee0e63181bce9890)) * broken file ([92462cf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/92462cf0dffa222d75965885ee0e63181bce9890))
- bugs ([2a01506](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2a0150600f26af5b7394f878e13c4d19611f3f64)) * bugs ([2a01506](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2a0150600f26af5b7394f878e13c4d19611f3f64))
- build ([60ab140](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/60ab140c556ec56eca78b1f4e280dfb6e9dbb538)) * build ([60ab140](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/60ab140c556ec56eca78b1f4e280dfb6e9dbb538))
- build errors and merge in Casey's branch (driodiwb) ([39947b3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/39947b3694f42f9d5ad2f21d044585aa3d12c407)) * build errors and merge in Casey's branch (driodiwb) ([39947b3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/39947b3694f42f9d5ad2f21d044585aa3d12c407))
- build errors and restructure Calendar page ([c6a48dd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c6a48dd3f6025fe531c18e12b8730683d63f67ed)) * build errors and restructure Calendar page ([c6a48dd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c6a48dd3f6025fe531c18e12b8730683d63f67ed))
- calendar course cell colors ([74be880](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/74be880f9d2dc340ba48416429711fb28915baa2)) * calendar course cell colors ([74be880](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/74be880f9d2dc340ba48416429711fb28915baa2))
- calendar storybook issue ([#125](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/125)) ([0c5bec8](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0c5bec8002022cb4702f3879f43fcd32b9fa6b5a)) * calendar storybook issue ([#125](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/125)) ([0c5bec8](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0c5bec8002022cb4702f3879f43fcd32b9fa6b5a))
- Calendar View/Scaling Issues ([#144](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/144)) ([4c61ebd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4c61ebd3fc93240903f1224f9dd3057a23d30046)) * Calendar View/Scaling Issues ([#144](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/144)) ([4c61ebd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4c61ebd3fc93240903f1224f9dd3057a23d30046))
- CalendarCourseCell spacing ([13c69ef](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/13c69ef862f0214dfa6c9368fc3463519aa8f2f0)) * CalendarCourseCell spacing ([13c69ef](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/13c69ef862f0214dfa6c9368fc3463519aa8f2f0))
- change Chromatic action to build current branch, not base branch ([#100](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/100)) ([e73c9fe](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e73c9fe417169e73c395cde1aeb841eee8add579)) * change Chromatic action to build current branch, not base branch ([#100](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/100)) ([e73c9fe](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e73c9fe417169e73c395cde1aeb841eee8add579))
- change material icons to material symbols ([#71](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/71)) ([52e34cb](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/52e34cb830ff1f12659703776bd3d9a24815ee00)) * change material icons to material symbols ([#71](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/71)) ([52e34cb](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/52e34cb830ff1f12659703776bd3d9a24815ee00))
- chromatic build ([11303da](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/11303daebcc1e4de340463e6266082dc0920582d)) * chromatic build ([11303da](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/11303daebcc1e4de340463e6266082dc0920582d))
- chromatic builds on PRs ([#140](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/140)) ([78a6939](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/78a69399292767a1f8eac66e4c86cbcb8aad019d)) * chromatic builds on PRs ([#140](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/140)) ([78a6939](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/78a69399292767a1f8eac66e4c86cbcb8aad019d))
- chromatic storybook - CourseCatalogInjectedPopup ([#106](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/106)) ([ced2997](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ced29975b20a2a5beac51ea875627a2b1042e5c2)) * chromatic storybook - CourseCatalogInjectedPopup ([#106](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/106)) ([ced2997](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ced29975b20a2a5beac51ea875627a2b1042e5c2))
- clean up [#173](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/173) ([#174](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/174)) ([afa634f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/afa634f0853dc16cf313c129434d49bf132388d0)) * clean up [#173](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/173) ([#174](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/174)) ([afa634f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/afa634f0853dc16cf313c129434d49bf132388d0))
- cleanup imports ([#112](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/112)) ([b17c3fa](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b17c3fae6d9a8fdb2f6de84dc28d00bc31e7bcc8)) * cleanup imports ([#112](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/112)) ([b17c3fa](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b17c3fae6d9a8fdb2f6de84dc28d00bc31e7bcc8))
- conflict row bug ([#130](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/130)) ([a8ea3bc](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a8ea3bc683f88fc33191a8cacf649be83f6758f8)) * conflict row bug ([#130](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/130)) ([a8ea3bc](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a8ea3bc683f88fc33191a8cacf649be83f6758f8))
- ConflictsWithWarning ([1599e48](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/1599e48d75d4877b3515c943120d83b59d084a55)) * ConflictsWithWarning ([1599e48](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/1599e48d75d4877b3515c943120d83b59d084a55))
- correct parsing of noon ([#155](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/155)) ([91f62e1](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/91f62e1943633eaef7dc8d437e6a7397456a9123)) * correct parsing of noon ([#155](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/155)) ([91f62e1](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/91f62e1943633eaef7dc8d437e6a7397456a9123))
- db with proper insertion order ([8e79d6a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8e79d6a6a87210e0a30584c0c9ceb60bd3a33665)) * db with proper insertion order ([8e79d6a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8e79d6a6a87210e0a30584c0c9ceb60bd3a33665))
- delete storybook timestamp file ([f93a98e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f93a98e46ae76e9b1474c8143bfc3f5076e8b91f)) * delete storybook timestamp file ([f93a98e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f93a98e46ae76e9b1474c8143bfc3f5076e8b91f))
- dialog movement ([#227](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/227)) ([bc5d68c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bc5d68ce18f818c285ef7f5eda03f499365ee2e8)) * dialog movement ([#227](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/227)) ([bc5d68c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bc5d68ce18f818c285ef7f5eda03f499365ee2e8))
- disabled [object Object] hover message on dialog popups ([#284](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/284)) ([dcc1d81](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/dcc1d81a74922ba27ad2a6859f978f3f20136178)) * disabled [object Object] hover message on dialog popups ([#284](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/284)) ([dcc1d81](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/dcc1d81a74922ba27ad2a6859f978f3f20136178))
- divider usage in HeaderAndActions ([#113](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/113)) ([84e8320](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/84e8320e8fce5961877a5745e89419a722055ed5)) * divider usage in HeaderAndActions ([#113](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/113)) ([84e8320](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/84e8320e8fce5961877a5745e89419a722055ed5))
- doesn't autoload on pages that don't have pages to load ([#270](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/270)) ([88eeb62](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/88eeb620aec52d6e4172cc908843339412ade90b)) * doesn't autoload on pages that don't have pages to load ([#270](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/270)) ([88eeb62](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/88eeb620aec52d6e4172cc908843339412ade90b))
- don't crash on cultural diversity flag ([#196](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/196)) ([5b1e451](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5b1e4513e29203a54ed9f4913c227d6c45d2e67a)) * don't crash on cultural diversity flag ([#196](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/196)) ([5b1e451](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5b1e4513e29203a54ed9f4913c227d6c45d2e67a))
- eslint and remove React-beautiful-dnd ([6af805b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6af805ba3a7b04b7e5705d3d597b5ace686d61e0)) * eslint and remove React-beautiful-dnd ([6af805b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6af805ba3a7b04b7e5705d3d597b5ace686d61e0))
- extra space ([0f43796](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0f43796cd864acd012fc1242cbdf34844df62406)) * extra space ([0f43796](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0f43796cd864acd012fc1242cbdf34844df62406))
- Fix popup drag hitbox ([#216](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/216)) ([7a40008](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7a40008c1e3846f09ac4eeadc4a50f810ffc0d90)) * Fix popup drag hitbox ([#216](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/216)) ([7a40008](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7a40008c1e3846f09ac4eeadc4a50f810ffc0d90))
- fixed bug where activeSchedule doesn't update correctly ([#158](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/158)) ([a409090](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a409090b904686b76a5db52af980e81108b97082)) * fixed bug where activeSchedule doesn't update correctly ([#158](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/158)) ([a409090](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a409090b904686b76a5db52af980e81108b97082))
- fixed bug with course cells after 12 PM extending past midnight ([#122](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/122)) ([f22a3cd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f22a3cd7c0df138ca6ecd4b4a4efaa7ce845c3dd)) * fixed bug with course cells after 12 PM extending past midnight ([#122](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/122)) ([f22a3cd](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/f22a3cd7c0df138ca6ecd4b4a4efaa7ce845c3dd))
- fixed issues involving course meeting objects not being recognized as course meeting objects ([#132](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/132)) ([3406e9a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3406e9a0e2644218fc06cbd0b05046df5339264c)) * fixed issues involving course meeting objects not being recognized as course meeting objects ([#132](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/132)) ([3406e9a](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3406e9a0e2644218fc06cbd0b05046df5339264c))
- Fixed typescript error ([#161](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/161)) ([df7a7c6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/df7a7c65d695cb5c5a825ac9f5ecc48438735b65)) * Fixed typescript error ([#161](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/161)) ([df7a7c6](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/df7a7c65d695cb5c5a825ac9f5ecc48438735b65))
- grade dist when no instructor ([#269](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/269)) ([6a363ae](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6a363aeb5ccb1c11e15db317b34c4f04a3c9aafc)) * grade dist when no instructor ([#269](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/269)) ([6a363ae](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6a363aeb5ccb1c11e15db317b34c4f04a3c9aafc))
- grid JSX.Element generation ([b691bf3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b691bf3231d5002a631687f26c7409e311237662)) * grid JSX.Element generation ([b691bf3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b691bf3231d5002a631687f26c7409e311237662))
- icon library resolution ([#74](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/74)) ([bb3b313](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bb3b313fd272bba12f99270c912e046df5358fed)) * icon library resolution ([#74](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/74)) ([bb3b313](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bb3b313fd272bba12f99270c912e046df5358fed))
- idk why that comment was there ([#177](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/177)) ([c5fc621](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c5fc6219e1540e1fb889d8bedc7296190103a56b)) * idk why that comment was there ([#177](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/177)) ([c5fc621](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c5fc6219e1540e1fb889d8bedc7296190103a56b))
- import error ([152bc45](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/152bc4577685605ae95f39d9e670b5449e32e0e9)) * import error ([152bc45](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/152bc4577685605ae95f39d9e670b5449e32e0e9))
- improper list data propagation ([#240](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/240)) ([149fda3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/149fda3b721dcc3d8ffa6c2398c28e39dcb62aaa)) * improper list data propagation ([#240](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/240)) ([149fda3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/149fda3b721dcc3d8ffa6c2398c28e39dcb62aaa))
- improve dialog handling and error management in list items ([#257](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/257)) ([1942508](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/1942508d8d11f51b62806894ae13162b6404753b)) * improve dialog handling and error management in list items ([#257](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/257)) ([1942508](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/1942508d8d11f51b62806894ae13162b6404753b))
- injection not working from som/elie commit ([#145](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/145)) ([591687e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/591687eee8731b3649a5f8c1357a416eb2992a93)) * injection not working from som/elie commit ([#145](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/145)) ([591687e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/591687eee8731b3649a5f8c1357a416eb2992a93))
- list component fixed ([#162](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/162)) ([5714ed1](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5714ed16d72c5442f58366ac51f40cc32ea406ab)) * list component fixed ([#162](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/162)) ([5714ed1](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5714ed16d72c5442f58366ac51f40cc32ea406ab))
- made list draggable only by handle ([cbb190b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cbb190bf4b3ad694c87b650489e1fca5aa6b7826)) * made list draggable only by handle ([cbb190b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cbb190bf4b3ad694c87b650489e1fca5aa6b7826))
- margins on list component ([cc71389](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cc7138949cf77d1bcf1520633d669e939540575e)) * margins on list component ([cc71389](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/cc7138949cf77d1bcf1520633d669e939540575e))
- non-determinstic options page generation ([#137](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/137)) ([d700110](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d70011016a45f5de62b06839a31c634637024f4a)) * non-determinstic options page generation ([#137](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/137)) ([d700110](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d70011016a45f5de62b06839a31c634637024f4a))
- non-virtual dnd ([837fddf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/837fddf8048350b0b25a58bbd16c1957fbad6edb)) * non-virtual dnd ([837fddf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/837fddf8048350b0b25a58bbd16c1957fbad6edb))
- old icon removed in .tsx ([4d387e8](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4d387e8063314c7c6d7452b746728e2cf85a1787)) * old icon removed in .tsx ([4d387e8](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4d387e8063314c7c6d7452b746728e2cf85a1787))
- only show button hover effects when not disabled ([8e3aa7e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8e3aa7ef3380836fd36a12698b1f6982e110c0f7)) * only show button hover effects when not disabled ([8e3aa7e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/8e3aa7ef3380836fd36a12698b1f6982e110c0f7))
- openTabFromContentScript TS issue ([3a48859](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3a48859dddc0e85649ff6a949e9afd22c962825d)) * openTabFromContentScript TS issue ([3a48859](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3a48859dddc0e85649ff6a949e9afd22c962825d))
- options page ([#131](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/131)) ([dc100b5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/dc100b5d3ac1db5f1dc011ea2536c286f56fa55b)) * options page ([#131](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/131)) ([dc100b5](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/dc100b5d3ac1db5f1dc011ea2536c286f56fa55b))
- reactivity ([#188](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/188)) ([4f4f34e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4f4f34e281e8aabcdbf8b06652d05bca3554b11f)) * reactivity ([#188](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/188)) ([4f4f34e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/4f4f34e281e8aabcdbf8b06652d05bca3554b11f))
- README.md ([a30fecf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a30fecf8ec76ecae03be9bb4d813803b9ffdb490)) * README.md ([a30fecf](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/a30fecf8ec76ecae03be9bb4d813803b9ffdb490))
- README.md ([b27b21b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b27b21bbfcc8fbdc130c3fa85776479451745b7b)) * README.md ([b27b21b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b27b21bbfcc8fbdc130c3fa85776479451745b7b))
- refactor AST parsing for custom ESLint rule ([62f0851](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/62f0851406bf82d3d0481fed40462bdd30ffcf1b)) * refactor AST parsing for custom ESLint rule ([62f0851](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/62f0851406bf82d3d0481fed40462bdd30ffcf1b))
- remote react-window fully (from List component) ([#114](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/114)) ([5f1c023](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5f1c0231e483bc571ef944bbc0b950ab20d31028)) * remote react-window fully (from List component) ([#114](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/114)) ([5f1c023](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/5f1c0231e483bc571ef944bbc0b950ab20d31028))
- remove course name from the syllabi lookup ([#200](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/200)) ([bcb5a8c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bcb5a8c469c4b5c1909880ab6789d67d04487ed2)) * remove course name from the syllabi lookup ([#200](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/200)) ([bcb5a8c](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/bcb5a8c469c4b5c1909880ab6789d67d04487ed2))
- remove extra spacing ([#121](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/121)) ([6ba8b68](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6ba8b68654dc182bd2a084f93b0f4f7d5c132862)) * remove extra spacing ([#121](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/121)) ([6ba8b68](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/6ba8b68654dc182bd2a084f93b0f4f7d5c132862))
- remove white space after duplicate schedule regex matching ([#286](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/286)) ([d73615e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d73615e28129ee7d78604c6b295ec572b11be46e)) * remove white space after duplicate schedule regex matching ([#286](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/286)) ([d73615e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/d73615e28129ee7d78604c6b295ec572b11be46e))
- rename to course block and fix line height for styling ([b602b0b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b602b0b895416a26214e934f9650a8ec0a10711e)) * rename to course block and fix line height for styling ([b602b0b](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b602b0b895416a26214e934f9650a8ec0a10711e))
- revert "chore: add default story" ([aef8c3d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/aef8c3d9873c6df79f6a54ab4c669ee1849b5ea5)) * revert "chore: add default story" ([aef8c3d](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/aef8c3d9873c6df79f6a54ab4c669ee1849b5ea5))
- revert "rename to course block and fix line height for styling" ([0273a23](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0273a239139c0f3c0ad773ed8ab1cd4ad8d22fa3)) * revert "rename to course block and fix line height for styling" ([0273a23](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/0273a239139c0f3c0ad773ed8ab1cd4ad8d22fa3))
- revert CalendarGrid and CalendarGridCell back to SCSS from Tailwind ([56306ab](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/56306ab9440baf5ca1158f95d788de7ba42d73d8)) * revert CalendarGrid and CalendarGridCell back to SCSS from Tailwind ([56306ab](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/56306ab9440baf5ca1158f95d788de7ba42d73d8))
- revert last commit ([27d945f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/27d945f57c76a1c2f31fee9f98c324fab6183bb6)) * revert last commit ([27d945f](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/27d945f57c76a1c2f31fee9f98c324fab6183bb6))
- Schedule Switching Bugs ([#138](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/138)) ([c51e688](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c51e6881d186e8bef056e483e2bca95585d5eac6)) * Schedule Switching Bugs ([#138](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/138)) ([c51e688](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/c51e6881d186e8bef056e483e2bca95585d5eac6))
- support classes with no location ([#242](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/242)) ([3ff06e0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3ff06e043b112bc232c9c173e72b2ea99609cf7a)) * support classes with no location ([#242](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/242)) ([3ff06e0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/3ff06e043b112bc232c9c173e72b2ea99609cf7a))
- theme colors ([2f537b4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2f537b4f3e3946241f2e287d9ae075e5a2aa7087)) * theme colors ([2f537b4](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/2f537b4f3e3946241f2e287d9ae075e5a2aa7087))
- type issues by using correct import ([#111](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/111)) ([19fe070](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/19fe070491be9e2d40f12918561f6f6ac5921237)) * type issues by using correct import ([#111](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/111)) ([19fe070](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/19fe070491be9e2d40f12918561f6f6ac5921237))
- **ui:** unhid settings button ([#288](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/288)) ([db1eac3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/db1eac33a2786a3db87f0e6b3679233a85ab923f)) * **ui:** unhid settings button ([#288](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/288)) ([db1eac3](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/db1eac33a2786a3db87f0e6b3679233a85ab923f))
- undefined color case ([9aa78a0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9aa78a02a5d4f68daa062e4892875cf5f568306c)) * undefined color case ([9aa78a0](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/9aa78a02a5d4f68daa062e4892875cf5f568306c))
- unocss theme color namings ([79d7832](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/79d7832d0987b8a7c9650b984f72ffd702682423)) * unocss theme color namings ([79d7832](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/79d7832d0987b8a7c9650b984f72ffd702682423))
- update alignment ([7eb3113](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7eb3113adaa0917c0275576c3366c387bfe77440)) * update alignment ([7eb3113](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/7eb3113adaa0917c0275576c3366c387bfe77440))
- update daysOfWeek with new DAY_MAP keys ([ecdaadb](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ecdaadb83eb21b144d07d573613fd8971f0a5ce2)) * update daysOfWeek with new DAY_MAP keys ([ecdaadb](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ecdaadb83eb21b144d07d573613fd8971f0a5ce2))
- visual overflow bug when editing schedule name ([#251](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/251)) ([e8d2c2e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e8d2c2e1429072fcc1173ee770f06b07e9ddda20)) * visual overflow bug when editing schedule name ([#251](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/251)) ([e8d2c2e](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/e8d2c2e1429072fcc1173ee770f06b07e9ddda20))
- vitest path alias bug ([ee37897](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ee37897df4f65cd2d285e2ad628be43aebb21ea1)) * vitest path alias bug ([ee37897](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/ee37897df4f65cd2d285e2ad628be43aebb21ea1))
### Reverts ### Reverts
- Revert "individual bug fix" ([b4e8c75](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b4e8c7589e53f1064d70703459cc6d66fae1b04c)) * Revert "individual bug fix" ([b4e8c75](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/b4e8c7589e53f1064d70703459cc6d66fae1b04c))
- color palette for calendar ([#118](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/118)) ([51bbd65](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/51bbd6590bf284ce54644b014466c8b2c73b8925)) * color palette for calendar ([#118](https://github.com/Longhorn-Developers/UT-Registration-Plus/issues/118)) ([51bbd65](https://github.com/Longhorn-Developers/UT-Registration-Plus/commit/51bbd6590bf284ce54644b014466c8b2c73b8925))

View File

@@ -2,7 +2,7 @@
FROM node:20.9.0-alpine AS base FROM node:20.9.0-alpine AS base
# Install pnpm # Install pnpm
RUN npm install -g pnpm RUN npm install -g pnpm@latest-10
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app

View File

@@ -35,9 +35,13 @@ function removeExtraDatabaseDir(cb) {
// Instrument with Sentry // Instrument with Sentry
// Make sure sentry is configured https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/typescript/#2-configure-sentry-cli // Make sure sentry is configured https://docs.sentry.io/platforms/javascript/sourcemaps/uploading/typescript/#2-configure-sentry-cli
async function instrumentWithSentry(cb) { async function instrumentWithSentry(cb) {
if (process.env.SENTRY_ENV && process.env.SENTRY_ENV !== 'development') {
await exec(`sentry-cli sourcemaps inject ${DIST_DIR}`); await exec(`sentry-cli sourcemaps inject ${DIST_DIR}`);
await exec(`sentry-cli sourcemaps upload ${DIST_DIR}`); await exec(`sentry-cli sourcemaps upload ${DIST_DIR}`);
log('Sentry instrumentation completed.'); log('Sentry instrumentation completed.');
} else {
logWarn('Skipping uploading/creating Sentry source maps. (development build)');
}
cb(); cb();
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "ut-registration-plus", "name": "ut-registration-plus",
"displayName": "UT Registration Plus", "displayName": "UT Registration Plus",
"version": "2.1.0", "version": "2.2.1",
"description": "UT Registration Plus is a Chrome extension that allows students to easily register for classes.", "description": "UT Registration Plus is a Chrome extension that allows students to easily register for classes.",
"private": true, "private": true,
"homepage": "https://github.com/Longhorn-Developers/UT-Registration-Plus", "homepage": "https://github.com/Longhorn-Developers/UT-Registration-Plus",
@@ -9,8 +9,10 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"build:dev": "tsc && NODE_ENV='development' vite build --mode development",
"build:watch": "NODE_ENV='development' vite build --mode development -w", "build:watch": "NODE_ENV='development' vite build --mode development -w",
"zip": "PROD=true pnpm build && pnpm gulp zipProdBuild", "zip": "pnpm build && pnpm gulp zipProdBuild",
"zip:to-publish": "SENTRY_ENV='production' pnpm zip",
"prettier": "prettier src --check", "prettier": "prettier src --check",
"prettier:fix": "prettier src --write", "prettier:fix": "prettier src --write",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives",
@@ -27,66 +29,69 @@
"prepare": "husky" "prepare": "husky"
}, },
"dependencies": { "dependencies": {
"@date-fns/tz": "^1.2.0",
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@headlessui/react": "^2.2.0", "@headlessui/react": "^2.2.0",
"@octokit/rest": "^21.0.2", "@octokit/rest": "^21.1.1",
"@phosphor-icons/react": "^2.1.7", "@phosphor-icons/react": "^2.1.7",
"@sentry/react": "^8.38.0", "@sentry/react": "^8.55.0",
"@tanstack/react-query": "^5.69.0",
"@unocss/vite": "^0.63.6", "@unocss/vite": "^0.63.6",
"@vitejs/plugin-react": "^4.3.3", "@vitejs/plugin-react": "^4.3.4",
"chrome-extension-toolkit": "^0.0.54", "chrome-extension-toolkit": "^0.0.54",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"conventional-changelog": "^6.0.0", "conventional-changelog": "^6.0.0",
"date-fns": "^4.1.0",
"highcharts": "^11.4.8", "highcharts": "^11.4.8",
"highcharts-react-official": "^3.2.1", "highcharts-react-official": "^3.2.1",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.13",
"husky": "^9.1.6", "husky": "^9.1.7",
"kc-dabr-wasm": "^0.1.2", "kc-dabr-wasm": "^0.1.2",
"nanoid": "^5.0.8", "nanoid": "^5.1.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-loading-skeleton": "^3.5.0", "react-loading-skeleton": "^3.5.0",
"react-markdown": "^9.0.1", "react-markdown": "^9.1.0",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.1",
"sass": "^1.81.0", "sass": "^1.85.1",
"simple-git": "^3.27.0", "simple-git": "^3.27.0",
"sql.js": "1.11.0" "sql.js": "1.11.0"
}, },
"devDependencies": { "devDependencies": {
"@chromatic-com/storybook": "^2.0.2", "@chromatic-com/storybook": "^2.0.2",
"@commitlint/cli": "^19.5.0", "@commitlint/cli": "^19.7.1",
"@commitlint/config-conventional": "^19.5.0", "@commitlint/config-conventional": "^19.7.1",
"@commitlint/types": "^19.5.0", "@commitlint/types": "^19.5.0",
"@crxjs/vite-plugin": "2.0.0-beta.21", "@crxjs/vite-plugin": "2.0.0-beta.21",
"@iconify-json/bi": "^1.2.1", "@iconify-json/bi": "^1.2.2",
"@iconify-json/ic": "^1.2.1", "@iconify-json/ic": "^1.2.2",
"@iconify-json/iconoir": "^1.2.4", "@iconify-json/iconoir": "^1.2.7",
"@iconify-json/material-symbols": "^1.2.6", "@iconify-json/material-symbols": "^1.2.14",
"@iconify-json/ri": "^1.2.3", "@iconify-json/ri": "^1.2.5",
"@iconify-json/streamline": "^1.2.1", "@iconify-json/streamline": "^1.2.2",
"@semantic-release/exec": "^6.0.3", "@semantic-release/exec": "^6.0.3",
"@sentry/types": "^8.38.0", "@sentry/types": "^8.55.0",
"@storybook/addon-designs": "^8.0.4", "@storybook/addon-designs": "^8.2.0",
"@storybook/addon-essentials": "^8.4.4", "@storybook/addon-essentials": "^8.6.0",
"@storybook/addon-links": "^8.4.4", "@storybook/addon-links": "^8.6.0",
"@storybook/blocks": "^8.4.4", "@storybook/blocks": "^8.6.0",
"@storybook/react": "^8.4.4", "@storybook/react": "^8.6.0",
"@storybook/react-vite": "^8.4.4", "@storybook/react-vite": "^8.6.0",
"@storybook/test": "^8.4.4", "@storybook/test": "^8.6.0",
"@svgr/core": "^8.1.0", "@svgr/core": "^8.1.0",
"@svgr/plugin-jsx": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0",
"@types/chrome": "^0.0.273", "@types/chrome": "^0.0.273",
"@types/conventional-changelog": "^3.1.5", "@types/conventional-changelog": "^3.1.5",
"@types/gulp": "^4.0.17", "@types/gulp": "^4.0.17",
"@types/gulp-zip": "^4.0.4", "@types/gulp-zip": "^4.0.4",
"@types/node": "^22.9.0", "@types/node": "^22.13.5",
"@types/prompts": "^2.4.9", "@types/prompts": "^2.4.9",
"@types/react": "^18.3.12", "@types/react": "^18.3.18",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.5",
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"@types/semantic-release": "^20.0.6", "@types/semantic-release": "^20.0.6",
"@types/semver": "^7.5.8", "@types/semver": "^7.5.8",
@@ -100,48 +105,52 @@
"@unocss/reset": "^0.63.6", "@unocss/reset": "^0.63.6",
"@unocss/transformer-directives": "^0.63.6", "@unocss/transformer-directives": "^0.63.6",
"@unocss/transformer-variant-group": "^0.63.6", "@unocss/transformer-variant-group": "^0.63.6",
"@vitejs/plugin-react-swc": "^3.7.1", "@vitejs/plugin-react-swc": "^3.8.0",
"@vitest/coverage-v8": "^2.1.5", "@vitest/coverage-v8": "^2.1.9",
"@vitest/ui": "^2.1.5", "@vitest/ui": "^2.1.9",
"chalk": "^5.3.0", "chalk": "^5.4.1",
"chromatic": "^11.18.1", "chromatic": "^11.26.0",
"cssnano": "^7.0.6", "cssnano": "^7.0.6",
"cssnano-preset-advanced": "^7.0.6", "cssnano-preset-advanced": "^7.0.6",
"dotenv": "^16.4.5", "dotenv": "^16.4.7",
"es-module-lexer": "^1.5.4", "es-module-lexer": "^1.6.0",
"eslint": "^8.57.1", "eslint": "^8.57.1",
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-base": "^15.0.0",
"eslint-config-airbnb-typescript": "^18.0.0", "eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.3", "eslint-import-resolver-typescript": "^3.8.3",
"eslint-plugin-import": "^2.31.0", "eslint-plugin-import": "^2.31.0",
"eslint-plugin-import-essentials": "^0.2.1", "eslint-plugin-import-essentials": "^0.2.1",
"eslint-plugin-jsdoc": "^50.5.0", "eslint-plugin-jsdoc": "^50.6.3",
"eslint-plugin-prettier": "^5.2.1", "eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.37.2", "eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-prefer-function-component": "^3.3.0", "eslint-plugin-react-prefer-function-component": "^3.4.0",
"eslint-plugin-react-refresh": "^0.4.14", "eslint-plugin-react-refresh": "^0.4.19",
"eslint-plugin-simple-import-sort": "^12.1.1", "eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-storybook": "^0.9.0", "eslint-plugin-storybook": "^0.9.0",
"eslint-plugin-tsdoc": "^0.3.0", "eslint-plugin-tsdoc": "^0.3.0",
"gulp": "^5.0.0", "gulp": "^5.0.0",
"gulp-execa": "^7.0.1", "gulp-execa": "^7.0.1",
"gulp-zip": "^6.0.0", "gulp-zip": "^6.1.0",
"path": "^0.12.7", "path": "^0.12.7",
"postcss": "^8.4.49", "postcss": "^8.5.3",
"prettier": "^3.3.3", "prettier": "^3.5.2",
"react-dev-utils": "^12.0.1", "react-dev-utils": "^12.0.1",
"semantic-release": "^24.2.0", "semantic-release": "^24.2.3",
"storybook": "^8.4.4", "storybook": "^8.6.0",
"typescript": "^5.6.3", "typescript": "^5.7.3",
"unocss": "^0.63.6", "unocss": "^0.63.6",
"unocss-preset-primitives": "0.0.2-beta.1", "unocss-preset-primitives": "0.0.2-beta.1",
"unplugin-icons": "^0.19.3", "unplugin-icons": "^0.19.3",
"vite": "^5.4.11", "vite": "^5.4.14",
"vite-plugin-inspect": "^0.8.7", "vite-plugin-inspect": "^0.8.9",
"vitest": "^2.1.5" "vitest": "^2.1.9"
},
"engineStrict": true,
"engines": {
"pnpm": "^10"
}, },
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {
@@ -151,5 +160,9 @@
"overrides": { "overrides": {
"es-module-lexer": "^1.5.4" "es-module-lexer": "^1.5.4"
} }
},
"volta": {
"node": "20.9.0",
"pnpm": "10.6.5"
} }
} }

2789
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,8 +27,8 @@ interface Props {
/** /**
* Generates a changelog using the conventional-changelog command. * Generates a changelog using the conventional-changelog command.
* *
* @returns {Promise<void>} A promise that resolves when the changelog is generated. * @returns A promise that resolves when the changelog is generated.
* @throws {Error} If there is an error generating the changelog. * @throws If there is an error generating the changelog.
*/ */
async function generateChangelog({ preset, outFile = 'CHANGELOG.md', releaseCount = 1 }: Props): Promise<void> { async function generateChangelog({ preset, outFile = 'CHANGELOG.md', releaseCount = 1 }: Props): Promise<void> {
try { try {

BIN
src/assets/LD-icon-new.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

5075
src/assets/UT-Map.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 347 KiB

View File

@@ -8,15 +8,27 @@ const splashText: string[] = [
'The squirrels, they have mastered begging for food better than students. Impressive... but worrying.', 'The squirrels, they have mastered begging for food better than students. Impressive... but worrying.',
"Do you study often? Ha! What am I saying? Of course you don't.", "Do you study often? Ha! What am I saying? Of course you don't.",
"Hey, you, you're finally awake. You were trying to skip class right?", "Hey, you, you're finally awake. You were trying to skip class right?",
'Mmm... Brutalist architecture...',
'The course syllabus: more than meets the eye',
'Pain is temporary, GPA is forever.',
"You've Yee'd Your Last Haw.",
'lol everything is already waitlisted.',
'Could be worse. Could be A&M.',
"Should you major in Compsci? well, here's a better question. do you wanna have a bad time?",
'A pen and paper is old fashioned, but sometimes old ways are best',
'A heart is like bedrock, destroyable only by cheating',
'You may not rest now, there are Canvas assignments nearby',
'You are filled with DETERMINATION',
'60k+ users!',
'Almost Turing complete',
'#BF5700',
'The waitlist is a lie!',
'BEVO JOCKEY 🗣️🗣️🗣️',
'RIP Domino, you beloved campus feline.', 'RIP Domino, you beloved campus feline.',
"The year is 2055 and Welch still isn't finished.", "The year is 2055 and Welch still isn't finished.",
'Motivation dropping faster than ur GPA', 'Motivation dropping faster than ur GPA',
'No Work Happens On PCL 5th Floor.', 'No Work Happens On PCL 5th Floor.',
'I may be a sophomore in name, but my credit count screams freshman!', 'I may be a sophomore in name, but my credit count screams freshman!',
'Pain is temporary, GPA is forever.',
"You've Yee'd Your Last Haw.",
'lol everything is already waitlisted.',
'Could be worse. Could be A&M.',
// 'TeXAs iS BaCK GuYZ', // 'TeXAs iS BaCK GuYZ',
'mAke iT yOuR tExAS', 'mAke iT yOuR tExAS',
'change yOur slogan', 'change yOur slogan',
@@ -24,7 +36,7 @@ const splashText: string[] = [
'Does McCombs teach Parseltongue?', 'Does McCombs teach Parseltongue?',
'No Cruce Enfrente Del Bus.', 'No Cruce Enfrente Del Bus.',
'Omae Wa Mou Shindeiru...', 'Omae Wa Mou Shindeiru...',
'Every day another brick disappears from Speedway', "They say each day, another brick disappears from Speedway. No one's sure where to.",
'The GDC will annex the EER one day', 'The GDC will annex the EER one day',
'To hike to Kins or not to hike to Kins...', 'To hike to Kins or not to hike to Kins...',
"C'mon you Longhorns! You want to study forever?", "C'mon you Longhorns! You want to study forever?",
@@ -34,15 +46,11 @@ const splashText: string[] = [
"The Block of Butter incident of '22", "The Block of Butter incident of '22",
'Begun, the midterms have.', 'Begun, the midterms have.',
'You must construct additional schedules', 'You must construct additional schedules',
"Arrows of Christ vs Church of Scientology was the crossover we didn't know we needed",
'THE WALK SIGN IS ON TO CROSS GUADALUPE AND 21ST', 'THE WALK SIGN IS ON TO CROSS GUADALUPE AND 21ST',
'Pay attention. Might learn something.', 'Pay attention. Might learn something.',
'Long ago, apartment rates lived together in harmony. Then, everything changed when American Campus Communities Inc attacked.',
'Roll for Initiative!', 'Roll for Initiative!',
'The line at the on-campus Starbucks is longer than your course waitlist.', 'The line at the on-campus Starbucks is longer than your course waitlist.',
'The weather changes more often than your class schedule.', 'The weather changes more often than your class schedule.',
'Mmm... Brutalist architecture...',
'The course syllabus: more than meets the eye',
"'studying' often means refreshing Canvas every five minutes to see if the professor posted lecture slides.", "'studying' often means refreshing Canvas every five minutes to see if the professor posted lecture slides.",
"It's over Bevo! I have the high ground!", "It's over Bevo! I have the high ground!",
"I'll just skip this lecture and watch the recording later. What's the worst that could happen?", "I'll just skip this lecture and watch the recording later. What's the worst that could happen?",
@@ -59,23 +67,15 @@ const splashText: string[] = [
'follow @sghsri!', 'follow @sghsri!',
'Officially part of the SEC', 'Officially part of the SEC',
'Planner is now acquired by Plus', 'Planner is now acquired by Plus',
'Longhorn-Developers is the best UT Student Org', 'Longhorn Developers is the best UT Student Org',
'The Eiffel Tower is the UT Tower of Paris', 'The Eiffel Tower is the UT Tower of Paris',
'A pen and paper is old fashioned, but sometimes old ways are best', "He's a CS Major, but he showers regularly. 🧢",
'A heart is like bedrock, destroyable only by cheating',
'You may not rest now, there are Canvas assignments nearby',
'You are filled with DETERMINATION',
'60k+ users!',
'Almost Turing complete',
'#BF5700',
'The waitlist is a lie!',
`He's a CS Major, but he showers regularly. 🧢`,
'A CS major walks into a bar. The bar is empty because it is a CS major.', 'A CS major walks into a bar. The bar is empty because it is a CS major.',
'UT Registration Plus - The only thing that can make registration worse is not having it', 'UT Registration Plus - The only thing that can make registration worse is not having it',
'UT Registration Plus - We make registration slightly less painful. Slightly.', 'UT Registration Plus - We make registration slightly less painful. Slightly.',
'UT Registration Plus - Do you really want to figure out which professors will ruin your GPA by yourself?', 'UT Registration Plus - Do you really want to figure out which professors will ruin your GPA by yourself?',
'Ayo tf is a memory leak', "Ayo what's is a memory leak",
"lowkey we never thought we'd get this far, how tf are 60k of you people using this", "lowkey we never thought we'd get this far, how are 60k of you people using this",
"dang we're really out here making a splash", "dang we're really out here making a splash",
"We'd make a joke about A&M, but we're not sure they can read", "We'd make a joke about A&M, but we're not sure they can read",
"We've only caused one or two outages, we swear!", "We've only caused one or two outages, we swear!",
@@ -113,8 +113,7 @@ const splashText: string[] = [
"Stop trying to make UTRP happen, it's not going to happen!", "Stop trying to make UTRP happen, it's not going to happen!",
'Befriend the raccoons on campus', 'Befriend the raccoons on campus',
`It's ${new Date().toLocaleString('en-US', { month: 'long', day: 'numeric' })} and OU still sucks`, `It's ${new Date().toLocaleString('en-US', { month: 'long', day: 'numeric' })} and OU still sucks`,
'As seen on TV! ', 'As seen on TV!',
"Should you major in Compsci? well, here's a better question. do you wanna have a bad time?",
]; ];
export default splashText; export default splashText;

View File

@@ -1,4 +1,7 @@
import { ExtensionStore } from '@shared/storage/ExtensionStore'; import { ExtensionStore } from '@shared/storage/ExtensionStore';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import createSchedule from '../lib/createSchedule';
/** /**
* Called when the extension is updated (or when the extension is reloaded in development mode) * Called when the extension is updated (or when the extension is reloaded in development mode)
@@ -8,4 +11,11 @@ export default async function onUpdate() {
version: chrome.runtime.getManifest().version, version: chrome.runtime.getManifest().version,
lastUpdate: Date.now(), lastUpdate: Date.now(),
}); });
const schedules = await UserScheduleStore.get('schedules');
// By invariant, there must always be at least one schedule
if (schedules.length === 0) {
createSchedule('Schedule 1');
}
} }

View File

@@ -36,5 +36,11 @@ export default async function createSchedule(scheduleName: string) {
schedules.push(newSchedule); schedules.push(newSchedule);
await UserScheduleStore.set('schedules', schedules); await UserScheduleStore.set('schedules', schedules);
// If there is only one schedule, set the active index to the new schedule
if (schedules.length <= 1) {
await UserScheduleStore.set('activeIndex', 0);
}
return newSchedule.id; return newSchedule.id;
} }

View File

@@ -22,6 +22,11 @@ export default async function deleteSchedule(scheduleId: string): Promise<string
schedules.splice(scheduleIndex, 1); schedules.splice(scheduleIndex, 1);
await UserScheduleStore.set('schedules', schedules); await UserScheduleStore.set('schedules', schedules);
// By invariant, there must always be at least one schedule
if (schedules.length === 0) {
createSchedule('Schedule 1');
}
let newActiveIndex = activeIndex; let newActiveIndex = activeIndex;
if (scheduleIndex < activeIndex) { if (scheduleIndex < activeIndex) {
newActiveIndex = activeIndex - 1; newActiveIndex = activeIndex - 1;

View File

@@ -3,6 +3,7 @@ import Calendar from '@views/components/calendar/Calendar';
import DialogProvider from '@views/components/common/DialogProvider/DialogProvider'; import DialogProvider from '@views/components/common/DialogProvider/DialogProvider';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot'; import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import { MigrationDialog } from '@views/components/common/MigrationDialog'; import { MigrationDialog } from '@views/components/common/MigrationDialog';
import { WhatsNewDialog } from '@views/components/common/WhatsNewPopup';
import SentryProvider from '@views/contexts/SentryContext'; import SentryProvider from '@views/contexts/SentryContext';
import { MessageListener } from 'chrome-extension-toolkit'; import { MessageListener } from 'chrome-extension-toolkit';
import useKC_DABR_WASM from 'kc-dabr-wasm'; import useKC_DABR_WASM from 'kc-dabr-wasm';
@@ -34,6 +35,7 @@ export default function CalendarMain() {
<ExtensionRoot className='h-full w-full'> <ExtensionRoot className='h-full w-full'>
<DialogProvider> <DialogProvider>
<MigrationDialog /> <MigrationDialog />
<WhatsNewDialog />
<Calendar /> <Calendar />
</DialogProvider> </DialogProvider>
</ExtensionRoot> </ExtensionRoot>

View File

@@ -1,5 +1,6 @@
import CourseCatalogMain from '@views/components/CourseCatalogMain'; import CourseCatalogMain from '@views/components/CourseCatalogMain';
import InjectedButton from '@views/components/injected/AddAllButton'; import InjectedButton from '@views/components/injected/AddAllButton';
import DaysCheckbox from '@views/components/injected/DaysCheckbox';
import getSiteSupport, { SiteSupport } from '@views/lib/getSiteSupport'; import getSiteSupport, { SiteSupport } from '@views/lib/getSiteSupport';
import React from 'react'; import React from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
@@ -25,3 +26,7 @@ if (support === SiteSupport.COURSE_CATALOG_DETAILS || support === SiteSupport.CO
if (support === SiteSupport.MY_UT) { if (support === SiteSupport.MY_UT) {
renderComponent(InjectedButton); renderComponent(InjectedButton);
} }
if (support === SiteSupport.COURSE_CATALOG_SEARCH) {
renderComponent(DaysCheckbox);
}

19
src/pages/map/Map.tsx Normal file
View File

@@ -0,0 +1,19 @@
import DialogProvider from '@views/components/common/DialogProvider/DialogProvider';
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import Map from '@views/components/map/Map';
import useKC_DABR_WASM from 'kc-dabr-wasm';
import React from 'react';
/**
* Renders the map page for the UTRP (UT Registration Plus) extension.
*/
export default function MapPage() {
useKC_DABR_WASM();
return (
<ExtensionRoot>
<DialogProvider>
<Map />
</DialogProvider>
</ExtensionRoot>
);
}

16
src/pages/map/index.html Normal file
View File

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

6
src/pages/map/index.tsx Normal file
View File

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

View File

@@ -4,6 +4,8 @@ import { createLocalStore, debugStore } from 'chrome-extension-toolkit';
* A store that is used to store data that is only relevant during development * A store that is used to store data that is only relevant during development
*/ */
interface IDevStore { interface IDevStore {
/** whether the user is a developer */
isDeveloper: boolean;
/** the tabId for the debug tab */ /** the tabId for the debug tab */
debugTabId?: number; debugTabId?: number;
/** whether the debug tab is visible */ /** whether the debug tab is visible */
@@ -17,6 +19,7 @@ interface IDevStore {
} }
export const DevStore = createLocalStore<IDevStore>({ export const DevStore = createLocalStore<IDevStore>({
isDeveloper: false,
debugTabId: undefined, debugTabId: undefined,
isTabReloading: true, isTabReloading: true,
wasDebugTabVisible: false, wasDebugTabVisible: false,

View File

@@ -8,11 +8,14 @@ interface IExtensionStore {
version: string; version: string;
/** When was the last update */ /** When was the last update */
lastUpdate: number; lastUpdate: number;
/** The last version of the "What's New" popup that was shown to the user */
lastWhatsNewPopupVersion: number;
} }
export const ExtensionStore = createLocalStore<IExtensionStore>({ export const ExtensionStore = createLocalStore<IExtensionStore>({
version: chrome.runtime.getManifest().version, version: chrome.runtime.getManifest().version,
lastUpdate: Date.now(), lastUpdate: Date.now(),
lastWhatsNewPopupVersion: 0,
}); });
debugStore({ ExtensionStore }); debugStore({ ExtensionStore });

View File

@@ -18,6 +18,12 @@ export interface IOptionsStore {
/** whether we should open the calendar in a new tab; default is to focus an existing calendar tab */ /** whether we should open the calendar in a new tab; default is to focus an existing calendar tab */
alwaysOpenCalendarInNewTab: boolean; alwaysOpenCalendarInNewTab: boolean;
/** whether the calendar sidebar should be shown when the calendar is opened */
showCalendarSidebar: boolean;
/** whether the promo should be shown */
showUTDiningPromo: boolean;
} }
export const OptionsStore = createSyncStore<IOptionsStore>({ export const OptionsStore = createSyncStore<IOptionsStore>({
@@ -26,6 +32,8 @@ export const OptionsStore = createSyncStore<IOptionsStore>({
enableScrollToLoad: true, enableScrollToLoad: true,
enableDataRefreshing: false, enableDataRefreshing: false,
alwaysOpenCalendarInNewTab: false, alwaysOpenCalendarInNewTab: false,
showCalendarSidebar: true,
showUTDiningPromo: true,
}); });
/** /**
@@ -40,6 +48,8 @@ export const initSettings = async () =>
enableScrollToLoad: await OptionsStore.get('enableScrollToLoad'), enableScrollToLoad: await OptionsStore.get('enableScrollToLoad'),
enableDataRefreshing: await OptionsStore.get('enableDataRefreshing'), enableDataRefreshing: await OptionsStore.get('enableDataRefreshing'),
alwaysOpenCalendarInNewTab: await OptionsStore.get('alwaysOpenCalendarInNewTab'), alwaysOpenCalendarInNewTab: await OptionsStore.get('alwaysOpenCalendarInNewTab'),
showCalendarSidebar: await OptionsStore.get('showCalendarSidebar'),
showUTDiningPromo: await OptionsStore.get('showUTDiningPromo'),
}) satisfies IOptionsStore; }) satisfies IOptionsStore;
// Clothing retailer right // Clothing retailer right

View File

@@ -5,6 +5,7 @@ export const CRX_PAGES = {
DEBUG: '/debug.html', DEBUG: '/debug.html',
CALENDAR: '/calendar.html', CALENDAR: '/calendar.html',
OPTIONS: '/options.html', OPTIONS: '/options.html',
MAP: '/map.html',
REPORT: '/report.html', REPORT: '/report.html',
} as const; } as const;

View File

@@ -17,4 +17,9 @@ const MIMEType = {
*/ */
export type MIMETypeKey = keyof typeof MIMEType; export type MIMETypeKey = keyof typeof MIMEType;
/**
* Represents a value of the MIMEType object
*/
export type MIMETypeValue = (typeof MIMEType)[MIMETypeKey];
export default MIMEType; export default MIMEType;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
/**
* This file contains URLs for external applications and resources.
* Centralizing these URLs makes it easier to track, update, and manage them in a single place.
*/
/**
* URL to the UT Dining app on the App Store
*/
export const UT_DINING_APP_STORE_URL = new URL('https://apps.apple.com/us/app/ut-dining/id6743042002').toString();
/**
* URL to the UT Dining app on the Google Play Store (currently not available)
*/
export const UT_DINING_GOOGLE_PLAY_URL = ''; // Placeholder for Google Play URL, Android app not available yet
/**
* URL to the promo image
*/
export const UT_DINING_PROMO_IMAGE_URL = new URL('https://cdn.longhorns.dev/ut-dining-advert1.png').toString();

View File

@@ -48,3 +48,22 @@ export const ellipsify = (input: string, chars: number): string => {
} }
return ellipisifed; return ellipisifed;
}; };
/**
* Stringifies a list of items in English format.
*
* @param items - The list of items to stringify.
* @returns A string representation of the list in English format.
* @example
* englishStringifyList([]) // ''
* englishStringifyList(['Alice']) // 'Alice'
* englishStringifyList(['Alice', 'Bob']) // 'Alice and Bob'
* englishStringifyList(['Alice', 'Bob', 'Charlie']) // 'Alice, Bob, and Charlie'
*/
export const englishStringifyList = (items: readonly string[]): string => {
if (items.length === 0) return '';
if (items.length === 1) return items[0]!;
if (items.length === 2) return `${items[0]} and ${items[1]}`;
return `${items.slice(0, -1).join(', ')}, and ${items.at(-1)}`;
};

View File

@@ -1,4 +1,4 @@
import { capitalize, capitalizeFirstLetter, ellipsify } from '@shared/util/string'; import { capitalize, capitalizeFirstLetter, ellipsify, englishStringifyList } from '@shared/util/string';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
// TODO: Fix `string.ts` and `string.test.ts` to make the tests pass // TODO: Fix `string.ts` and `string.test.ts` to make the tests pass
@@ -54,3 +54,49 @@ describe('ellipsify', () => {
expect(ellipsify('', 5)).toBe(''); expect(ellipsify('', 5)).toBe('');
}); });
}); });
describe('englishStringifyList', () => {
it('should handle an empty array', () => {
const data = [] satisfies string[];
const result = englishStringifyList(data);
const expected = '';
expect(result).toBe(expected);
});
it('should handle 1 element', () => {
const data = ['Alice'] satisfies string[];
const result = englishStringifyList(data);
const expected = 'Alice';
expect(result).toBe(expected);
});
it('should handle 2 elements', () => {
const data = ['Alice', 'Bob'] satisfies string[];
const result = englishStringifyList(data);
const expected = 'Alice and Bob';
expect(result).toBe(expected);
});
it('should handle 3 elements', () => {
const data = ['Alice', 'Bob', 'Charlie'] satisfies string[];
const result = englishStringifyList(data);
const expected = 'Alice, Bob, and Charlie';
expect(result).toBe(expected);
});
it('should handle n elements', () => {
const testcases = [
{ data: [], expected: '' },
{ data: ['foo'], expected: 'foo' },
{ data: ['foo', 'bar'], expected: 'foo and bar' },
{ data: ['foo', 'bar', 'baz'], expected: 'foo, bar, and baz' },
{ data: ['a', 'b', 'c', 'd'], expected: 'a, b, c, and d' },
{ data: 'abcdefghijk'.split(''), expected: 'a, b, c, d, e, f, g, h, i, j, and k' },
] satisfies { data: string[]; expected: string }[];
for (const { data, expected } of testcases) {
const result = englishStringifyList(data);
expect(result).toBe(expected);
}
});
});

View File

@@ -3,11 +3,13 @@ import { describe, expect, it } from 'vitest';
describe('sleep', () => { describe('sleep', () => {
it('should resolve after the specified number of milliseconds', async () => { it('should resolve after the specified number of milliseconds', async () => {
const start = Date.now(); const start = performance.now();
const milliseconds = 1000; const milliseconds = 1000;
await sleep(milliseconds); await sleep(milliseconds);
const end = Date.now(); const end = performance.now();
const elapsed = end - start; const elapsed = end - start;
expect(elapsed).toBeGreaterThanOrEqual(milliseconds); // Flaky test due to JS's lack of precision in setTimeout,
// so we allow for a 1ms difference
expect(elapsed).toBeGreaterThanOrEqual(milliseconds - 1);
}); });
}); });

View File

@@ -0,0 +1,14 @@
import type { Meta, StoryObj } from '@storybook/react';
import DiningAppPromo from '@views/components/calendar/DiningAppPromo';
export default {
title: 'Components/Calendar/DiningAppPromo',
component: DiningAppPromo,
parameters: {
layout: 'centered',
},
} satisfies Meta<typeof DiningAppPromo>;
type Story = StoryObj<typeof DiningAppPromo>;
export const Default: Story = {};

View File

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

View File

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

View File

@@ -174,3 +174,137 @@ export const mikeScottCS314Schedule: UserSchedule = new UserSchedule({
hours: 3, hours: 3,
updatedAt: Date.now(), updatedAt: Date.now(),
}); });
export const multiMeetingMultiInstructorCourse: Course = new Course({
colors: {
primaryColor: '#ef4444',
secondaryColor: '#991b1b',
},
core: [],
courseName: '44-REPORTING TEXAS',
creditHours: 3,
department: 'J',
description: [
"Contemporary social, professional, and intellectual concerns with the practice of journalism. Students work as online reporters, photographers, and editors for the School of Journalism's Reporting Texas Web site.",
'Prerequisite: Graduate standing; additional prerequisites vary with the topic.',
'Designed to accommodate 35 or fewer students. Course number may be repeated for credit when the topics vary.',
],
flags: [],
fullName: 'J 395 44-REPORTING TEXAS',
instructionMode: 'In Person',
instructors: [
{
firstName: 'JOHN',
fullName: 'SCHWARTZ, JOHN R',
lastName: 'SCHWARTZ',
middleInitial: 'R',
},
{
firstName: 'JOHN',
fullName: 'BRIDGES, JOHN A III',
lastName: 'BRIDGES',
middleInitial: 'A',
},
],
isReserved: true,
number: '395',
schedule: {
meetings: [
{
days: ['Tuesday', 'Thursday'],
endTime: 660,
location: {
building: 'CMA',
room: '6.146',
},
startTime: 570,
},
{
days: ['Friday'],
endTime: 960,
location: {
building: 'DMC',
room: '3.208',
},
startTime: 780,
},
],
},
scrapedAt: 1742491957535,
semester: {
code: '20259',
season: 'Fall',
year: 2025,
},
status: 'OPEN',
uniqueId: 10335,
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20259/10335/',
});
export const multiMeetingMultiInstructorSchedule: UserSchedule = new UserSchedule({
courses: [multiMeetingMultiInstructorCourse],
id: 'mmmis',
name: 'Multi Meeting Multi Instructor Schedule',
hours: 3,
updatedAt: Date.now(),
});
export const chatterjeeCS429Course: Course = new Course({
colors: {
primaryColor: '#0284c7',
secondaryColor: '#0c4a6e',
},
core: [],
courseName: 'COMP ORGANIZATN AND ARCH',
creditHours: 4,
department: 'C S',
description: [
'Restricted to computer science majors.',
'An introduction to low-level computer design ranging from the basics of digital design to the hardware/software interface for application programs. Includes basic systems principles of pipelining and caching, and requires writing and understanding programs at multiple levels.',
'Computer Science 429 and 429H may not both be counted.',
'Prerequisite: The following courses with a grade of at least C-: Computer Science 311 or 311H; and Computer Science 314 or 314H.',
],
flags: [],
fullName: 'C S 429 COMP ORGANIZATN AND ARCH',
instructionMode: 'In Person',
instructors: [
{
firstName: 'SIDDHARTHA',
fullName: 'CHATTERJEE, SIDDHARTHA',
lastName: 'CHATTERJEE',
},
],
isReserved: true,
number: '429',
schedule: {
meetings: [
{
days: ['Monday', 'Tuesday', 'Wednesday', 'Thursday'],
endTime: 1020,
location: {
building: 'UTC',
room: '3.102',
},
startTime: 960,
},
{
days: ['Friday'],
endTime: 660,
location: {
building: 'GSB',
room: '2.122',
},
startTime: 540,
},
],
},
scrapedAt: 1742496630445,
semester: {
code: '20259',
season: 'Fall',
year: 2025,
},
status: 'OPEN',
uniqueId: 54795,
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20259/54795/',
});

View File

@@ -13,7 +13,7 @@ import { CourseCatalogScraper } from '@views/lib/CourseCatalogScraper';
import getCourseTableRows from '@views/lib/getCourseTableRows'; import getCourseTableRows from '@views/lib/getCourseTableRows';
import type { SiteSupportType } from '@views/lib/getSiteSupport'; import type { SiteSupportType } from '@views/lib/getSiteSupport';
import { populateSearchInputs } from '@views/lib/populateSearchInputs'; import { populateSearchInputs } from '@views/lib/populateSearchInputs';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
interface Props { interface Props {
support: Extract<SiteSupportType, 'COURSE_CATALOG_DETAILS' | 'COURSE_CATALOG_LIST'>; support: Extract<SiteSupportType, 'COURSE_CATALOG_DETAILS' | 'COURSE_CATALOG_LIST'>;
@@ -27,6 +27,8 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element | nul
const [selectedCourse, setSelectedCourse] = useState<Course | null>(null); const [selectedCourse, setSelectedCourse] = useState<Course | null>(null);
const [showPopup, setShowPopup] = useState(false); const [showPopup, setShowPopup] = useState(false);
const [enableScrollToLoad, setEnableScrollToLoad] = useState<boolean>(false); const [enableScrollToLoad, setEnableScrollToLoad] = useState<boolean>(false);
const prevCourseTitleRef = useRef<string | null>(null);
const tbody = document.querySelector('table tbody')!;
useEffect(() => { useEffect(() => {
populateSearchInputs(); populateSearchInputs();
@@ -43,6 +45,9 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element | nul
const ccs = new CourseCatalogScraper(support); const ccs = new CourseCatalogScraper(support);
const scrapedRows = ccs.scrape(tableRows, true); const scrapedRows = ccs.scrape(tableRows, true);
setRows(scrapedRows); setRows(scrapedRows);
prevCourseTitleRef.current =
scrapedRows.findLast(row => row.course === null)?.element.querySelector('.course_header')?.textContent ??
null;
}, [support]); }, [support]);
useEffect(() => { useEffect(() => {
@@ -51,8 +56,17 @@ export default function CourseCatalogMain({ support }: Props): JSX.Element | nul
const addRows = (newRows: ScrapedRow[]) => { const addRows = (newRows: ScrapedRow[]) => {
newRows.forEach(row => { newRows.forEach(row => {
document.querySelector('table tbody')!.appendChild(row.element); const courseTitle = row.element.querySelector('.course_header')?.textContent ?? null;
if (row.course === null) {
if (courseTitle !== prevCourseTitleRef.current) {
tbody.appendChild(row.element);
prevCourseTitleRef.current = courseTitle;
}
} else {
tbody.appendChild(row.element);
}
}); });
setRows([...rows, ...newRows]); setRows([...rows, ...newRows]);
}; };

View File

@@ -66,11 +66,19 @@ export default function PopupMain(): JSX.Element {
}; };
useEffect(() => { useEffect(() => {
const randomIndex = Math.floor(Math.random() * splashText.length); setFunny(prevFunny => {
setFunny( // Ensure that the next splash text is not the same as the previous one
splashText[randomIndex] ?? 'If you are seeing this, something has gone horribly wrong behind the scenes.' const splashTextWithoutCurrent = splashText.filter(text => text !== prevFunny);
const randomIndex = Math.floor(Math.random() * splashTextWithoutCurrent.length);
return (
splashTextWithoutCurrent[randomIndex] ??
'If you are seeing this, something has gone horribly wrong behind the scenes.'
); );
}, []); });
// Generate a new splash text every time the active schedule changes
}, [activeSchedule.id]);
const handleOpenOptions = async () => { const handleOpenOptions = async () => {
const url = chrome.runtime.getURL('/options.html'); const url = chrome.runtime.getURL('/options.html');

View File

@@ -1,8 +1,10 @@
import { Sidebar } from '@phosphor-icons/react'; import { Sidebar } from '@phosphor-icons/react';
import type { CalendarTabMessages } from '@shared/messages/CalendarMessages'; import type { CalendarTabMessages } from '@shared/messages/CalendarMessages';
import { OptionsStore } from '@shared/storage/OptionsStore';
import type { Course } from '@shared/types/Course'; import type { Course } from '@shared/types/Course';
import { CRX_PAGES } from '@shared/types/CRXPages'; import { CRX_PAGES } from '@shared/types/CRXPages';
import { openReportWindow } from '@shared/util/openReportWindow'; import { openReportWindow } from '@shared/util/openReportWindow';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import CalendarBottomBar from '@views/components/calendar/CalendarBottomBar'; import CalendarBottomBar from '@views/components/calendar/CalendarBottomBar';
import CalendarGrid from '@views/components/calendar/CalendarGrid'; import CalendarGrid from '@views/components/calendar/CalendarGrid';
import CalendarHeader from '@views/components/calendar/CalendarHeader/CalendarHeader'; import CalendarHeader from '@views/components/calendar/CalendarHeader/CalendarHeader';
@@ -13,8 +15,10 @@ import CourseCatalogInjectedPopup from '@views/components/injected/CourseCatalog
import { CalendarContext } from '@views/contexts/CalendarContext'; import { CalendarContext } from '@views/contexts/CalendarContext';
import useCourseFromUrl from '@views/hooks/useCourseFromUrl'; import useCourseFromUrl from '@views/hooks/useCourseFromUrl';
import { useFlattenedCourseSchedule } from '@views/hooks/useFlattenedCourseSchedule'; import { useFlattenedCourseSchedule } from '@views/hooks/useFlattenedCourseSchedule';
import useWhatsNewPopUp from '@views/hooks/useWhatsNew';
import { MessageListener } from 'chrome-extension-toolkit'; import { MessageListener } from 'chrome-extension-toolkit';
import clsx from 'clsx'; import clsx from 'clsx';
import type { ReactNode } from 'react';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import OutwardArrowIcon from '~icons/material-symbols/arrow-outward'; import OutwardArrowIcon from '~icons/material-symbols/arrow-outward';
@@ -23,17 +27,39 @@ import { Button } from '../common/Button';
import { LargeLogo } from '../common/LogoIcon'; import { LargeLogo } from '../common/LogoIcon';
import Text from '../common/Text/Text'; import Text from '../common/Text/Text';
import CalendarFooter from './CalendarFooter'; import CalendarFooter from './CalendarFooter';
import DiningAppPromo from './DiningAppPromo';
/** /**
* Calendar page component * Calendar page component
*/ */
export default function Calendar(): JSX.Element { export default function Calendar(): ReactNode {
const { courseCells, activeSchedule } = useFlattenedCourseSchedule(); const { courseCells, activeSchedule } = useFlattenedCourseSchedule();
const asyncCourseCells = courseCells.filter(block => block.async);
const displayBottomBar = asyncCourseCells && asyncCourseCells.length > 0;
const [course, setCourse] = useState<Course | null>(useCourseFromUrl()); const [course, setCourse] = useState<Course | null>(useCourseFromUrl());
const [showPopup, setShowPopup] = useState<boolean>(course !== null); const [showPopup, setShowPopup] = useState<boolean>(course !== null);
const [showSidebar, setShowSidebar] = useState<boolean>(true); const showWhatsNewDialog = useWhatsNewPopUp();
const [showUTDiningPromo, setShowUTDiningPromo] = useState<boolean>(false);
const queryClient = useQueryClient();
const { data: showSidebar, isPending: isSidebarStatePending } = useQuery({
queryKey: ['settings', 'showCalendarSidebar'],
queryFn: () => OptionsStore.get('showCalendarSidebar'),
staleTime: Infinity, // Prevent loading state on refocus
});
const { mutate: setShowSidebar } = useMutation({
mutationKey: ['settings', 'showCalendarSidebar'],
mutationFn: async (showSidebar: boolean) => {
OptionsStore.set('showCalendarSidebar', showSidebar);
},
onSuccess: (_, showSidebar) => {
queryClient.setQueryData(['settings', 'showCalendarSidebar'], showSidebar);
},
});
useEffect(() => { useEffect(() => {
const listener = new MessageListener<CalendarTabMessages>({ const listener = new MessageListener<CalendarTabMessages>({
@@ -59,10 +85,19 @@ export default function Calendar(): JSX.Element {
if (course) setShowPopup(true); if (course) setShowPopup(true);
}, [course]); }, [course]);
useEffect(() => {
// Load the user's preference for the promo
OptionsStore.get('showUTDiningPromo').then(show => {
setShowUTDiningPromo(show);
});
}, []);
if (isSidebarStatePending) return null;
return ( return (
<CalendarContext.Provider value> <CalendarContext.Provider value>
<div className='h-full w-full flex flex-col'> <div className='h-full w-full flex flex-col'>
<div className='h-screen flex overflow-auto'> <div className='screenshot:calendar-target h-screen flex overflow-auto'>
<div <div
className={clsx( className={clsx(
'py-spacing-6 relative h-full min-h-screen w-full flex flex-none flex-col justify-between overflow-clip whitespace-nowrap border-r border-ut-offwhite/50 shadow-[2px_0_10px,rgba(214_210_196_/_.1)] motion-safe:duration-300 motion-safe:ease-out-expo motion-safe:transition-[max-width] screenshot:hidden', 'py-spacing-6 relative h-full min-h-screen w-full flex flex-none flex-col justify-between overflow-clip whitespace-nowrap border-r border-ut-offwhite/50 shadow-[2px_0_10px,rgba(214_210_196_/_.1)] motion-safe:duration-300 motion-safe:ease-out-expo motion-safe:transition-[max-width] screenshot:hidden',
@@ -97,8 +132,17 @@ export default function Calendar(): JSX.Element {
<CalendarSchedules /> <CalendarSchedules />
<Divider orientation='horizontal' size='100%' /> <Divider orientation='horizontal' size='100%' />
<ResourceLinks /> <ResourceLinks />
<Divider orientation='horizontal' size='100%' />
{/* <TeamLinks /> */} {/* <TeamLinks /> */}
<Divider orientation='horizontal' size='100%' />
{showUTDiningPromo && (
<DiningAppPromo
onClose={() => {
setShowUTDiningPromo(false);
OptionsStore.set('showUTDiningPromo', false);
}}
/>
)}
<div className='flex flex-col gap-spacing-3'>
<a <a
href={CRX_PAGES.REPORT} href={CRX_PAGES.REPORT}
className='flex items-center gap-spacing-2 text-ut-burntorange underline-offset-2 hover:underline' className='flex items-center gap-spacing-2 text-ut-burntorange underline-offset-2 hover:underline'
@@ -112,6 +156,20 @@ export default function Calendar(): JSX.Element {
<Text variant='p'>Send us Feedback!</Text> <Text variant='p'>Send us Feedback!</Text>
<OutwardArrowIcon className='h-4 w-4' /> <OutwardArrowIcon className='h-4 w-4' />
</a> </a>
<a
href=''
className='flex items-center gap-spacing-2 text-ut-burntorange underline-offset-2 hover:underline'
target='_blank'
rel='noreferrer'
onClick={event => {
event.preventDefault();
showWhatsNewDialog();
}}
>
<Text variant='p'>What&apos;s New!</Text>
<OutwardArrowIcon className='h-4 w-4' />
</a>
</div>
</div> </div>
<CalendarFooter /> <CalendarFooter />
@@ -123,7 +181,7 @@ export default function Calendar(): JSX.Element {
// scrollbarGutter: 'stable', // scrollbarGutter: 'stable',
} }
} }
className='h-full flex flex-grow flex-col overflow-x-scroll px-spacing-5' className='z-1 h-full flex flex-grow flex-col overflow-x-scroll [&>*]:px-spacing-5'
> >
<CalendarHeader <CalendarHeader
sidebarOpen={showSidebar} sidebarOpen={showSidebar}
@@ -131,7 +189,11 @@ export default function Calendar(): JSX.Element {
setShowSidebar(!showSidebar); setShowSidebar(!showSidebar);
}} }}
/> />
<div className='min-h-2xl min-w-5xl flex-grow overflow-auto pl-spacing-3 pt-spacing-3 screenshot:min-h-xl'> <div
className={clsx('min-h-2xl min-w-5xl flex-grow gap-0 pl-spacing-3 screenshot:min-h-xl', {
'screenshot:flex-grow-0': displayBottomBar, // html-to-image seems to have a bug with flex-grow
})}
>
<CalendarGrid courseCells={courseCells} setCourse={setCourse} /> <CalendarGrid courseCells={courseCells} setCourse={setCourse} />
</div> </div>
<CalendarBottomBar courseCells={courseCells} setCourse={setCourse} /> <CalendarBottomBar courseCells={courseCells} setCourse={setCourse} />

View File

@@ -3,6 +3,7 @@ import Text from '@views/components/common/Text/Text';
import { ColorPickerProvider } from '@views/contexts/ColorPickerContext'; import { ColorPickerProvider } from '@views/contexts/ColorPickerContext';
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule'; import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
import clsx from 'clsx'; import clsx from 'clsx';
import type { ReactNode } from 'react';
import React from 'react'; import React from 'react';
import CalendarCourseBlock from './CalendarCourseCell'; import CalendarCourseBlock from './CalendarCourseCell';
@@ -18,21 +19,17 @@ type CalendarBottomBarProps = {
* @param courses - The list of courses to display in the calendar. * @param courses - The list of courses to display in the calendar.
* @returns The rendered bottom bar component. * @returns The rendered bottom bar component.
*/ */
export default function CalendarBottomBar({ courseCells, setCourse }: CalendarBottomBarProps): JSX.Element { export default function CalendarBottomBar({ courseCells, setCourse }: CalendarBottomBarProps): ReactNode {
const asyncCourseCells = courseCells?.filter(block => block.async); const asyncCourseCells = courseCells?.filter(block => block.async);
const displayCourses = asyncCourseCells && asyncCourseCells.length > 0; const displayCourses = asyncCourseCells && asyncCourseCells.length > 0;
if (!displayCourses) return null;
return ( return (
<div className='w-full flex pl-spacing-7 pr-spacing-3 pt-spacing-4'> <div className='w-full flex pl-spacing-7 pr-spacing-3 pt-spacing-4'>
<div <div className='flex flex-grow items-center gap-1 text-nowrap'>
className={clsx('flex flex-grow items-center gap-1 text-nowrap', {
'py-7.5': !displayCourses,
})}
>
{displayCourses && (
<>
<Text variant='p' className='text-ut-black uppercase'> <Text variant='p' className='text-ut-black uppercase'>
Unscheduled Async / Other
</Text> </Text>
<Text variant='h4' className='text-theme-offwhite/50'> <Text variant='h4' className='text-theme-offwhite/50'>
@@ -54,8 +51,6 @@ export default function CalendarBottomBar({ courseCells, setCourse }: CalendarBo
})} })}
</ColorPickerProvider> </ColorPickerProvider>
</div> </div>
</>
)}
</div> </div>
</div> </div>
); );

View File

@@ -2,14 +2,18 @@ import type { Course } from '@shared/types/Course';
import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell'; import CalendarCourseCell from '@views/components/calendar/CalendarCourseCell';
import Text from '@views/components/common/Text/Text'; import Text from '@views/components/common/Text/Text';
import { ColorPickerProvider } from '@views/contexts/ColorPickerContext'; import { ColorPickerProvider } from '@views/contexts/ColorPickerContext';
import { useSentryScope } from '@views/contexts/SentryContext';
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule'; import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
import React from 'react'; import React, { Fragment } from 'react';
import CalendarCell from './CalendarGridCell'; import CalendarCell from './CalendarGridCell';
import { calculateCourseCellColumns } from './utils';
const daysOfWeek = ['MON', 'TUE', 'WED', 'THU', 'FRI']; const daysOfWeek = ['MON', 'TUE', 'WED', 'THU', 'FRI'];
const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8); const hoursOfDay = Array.from({ length: 14 }, (_, index) => index + 8);
const IS_STORYBOOK = import.meta.env.STORYBOOK;
interface Props { interface Props {
courseCells?: CalendarGridCourse[]; courseCells?: CalendarGridCourse[];
saturdayClass?: boolean; saturdayClass?: boolean;
@@ -30,13 +34,13 @@ function makeGridRow(row: number, cols: number): JSX.Element {
const hour = hoursOfDay[row]!; const hour = hoursOfDay[row]!;
return ( return (
<> <Fragment key={row}>
<CalendarHour hour={hour} /> <CalendarHour hour={hour} />
<div className='grid-row-span-2 w-4 border-b border-r border-gray-300' /> <div className='grid-row-span-2 w-4 border-b border-r border-gray-300' />
{[...Array(cols).keys()].map(col => ( {[...Array(cols).keys()].map(col => (
<CalendarCell key={`${row}${col}`} row={row} col={col} /> <CalendarCell key={`${row}${col}`} row={row} col={col} />
))} ))}
</> </Fragment>
); );
} }
@@ -56,23 +60,40 @@ export default function CalendarGrid({
setCourse, setCourse,
}: React.PropsWithChildren<Props>): JSX.Element { }: React.PropsWithChildren<Props>): JSX.Element {
return ( return (
<div className='grid grid-cols-[auto_auto_repeat(5,1fr)] grid-rows-[auto_repeat(26,1fr)] h-full'> <div className='grid grid-cols-[auto_auto_repeat(5,1fr)] grid-rows-[auto_auto_repeat(27,1fr)] h-full'>
{/* Cover top left corner of grid, so time gets cut off at the top of the partial border */}
<div className='sticky top-[85px] z-10 col-span-2 h-3 bg-white' />
{/* Displaying day labels */} {/* Displaying day labels */}
<div />
<div className='w-4 border-b border-r border-gray-300' />
{daysOfWeek.map(day => ( {daysOfWeek.map(day => (
<div className='h-4 flex items-end justify-center border-b border-r border-gray-300 pb-1.5'> <div
<Text key={day} variant='small' className='text-center text-ut-burntorange' as='div'> // Full height with background to prevent grid lines from showing behind
className='sticky top-[85px] z-10 row-span-2 h-7 flex flex-col items-end self-start justify-end bg-white'
key={day}
>
{/* Partial border height because that's what Isaiah wants */}
<div className='h-4 w-full flex items-end border-b border-r border-gray-300'>
{/* Alignment for text */}
<div className='h-[calc(1.75rem_-_1px)] w-full flex items-center justify-center'>
<Text variant='small' className='text-center text-ut-burntorange' as='div'>
{day} {day}
</Text> </Text>
</div> </div>
</div>
</div>
))} ))}
{/* empty slot, for alignment */}
<div />
{/* time tick for the first hour */}
<div className='h-4 w-4 self-end border-b border-r border-gray-300' />
{[...Array(13).keys()].map(i => makeGridRow(i, 5))} {[...Array(13).keys()].map(i => makeGridRow(i, 5))}
<CalendarHour hour={21} /> <CalendarHour hour={21} />
{Array(6) {Array(6)
.fill(1) .fill(1)
.map(() => ( .map((_, i) => (
<div className='h-4 flex items-end justify-center border-r border-gray-300' /> // Key suppresses warning about duplicate keys,
// and index is fine because it doesn't change between renders
// eslint-disable-next-line react/no-array-index-key
<div key={i} className='h-4 flex items-end justify-center border-r border-gray-300' />
))} ))}
<ColorPickerProvider> <ColorPickerProvider>
{courseCells && <AccountForCourseConflicts courseCells={courseCells} setCourse={setCourse} />} {courseCells && <AccountForCourseConflicts courseCells={courseCells} setCourse={setCourse} />}
@@ -89,6 +110,12 @@ interface AccountForCourseConflictsProps {
// TODO: Possibly refactor to be more concise // TODO: Possibly refactor to be more concise
// TODO: Deal with react strict mode (wacky movements) // TODO: Deal with react strict mode (wacky movements)
function AccountForCourseConflicts({ courseCells, setCourse }: AccountForCourseConflictsProps): JSX.Element[] { function AccountForCourseConflicts({ courseCells, setCourse }: AccountForCourseConflictsProps): JSX.Element[] {
// Sentry is not defined in storybook.
// This is a valid use case for a condition hook, since IS_STORYBOOK is determined at build time,
// it doesn't change between renders.
// eslint-disable-next-line react-hooks/rules-of-hooks
const [sentryScope] = IS_STORYBOOK ? [undefined] : useSentryScope();
// Groups by dayIndex to identify overlaps // Groups by dayIndex to identify overlaps
const days = courseCells.reduce( const days = courseCells.reduce(
(acc, cell: CalendarGridCourse) => { (acc, cell: CalendarGridCourse) => {
@@ -103,31 +130,15 @@ function AccountForCourseConflicts({ courseCells, setCourse }: AccountForCourseC
); );
// Check for overlaps within each day and adjust gridColumnIndex and totalColumns // Check for overlaps within each day and adjust gridColumnIndex and totalColumns
Object.values(days).forEach((dayCells: CalendarGridCourse[]) => { Object.values(days).forEach((dayCells: CalendarGridCourse[], idx) => {
// Sort by start time to ensure proper columnIndex assignment try {
dayCells.sort((a, b) => a.calendarGridPoint.startIndex - b.calendarGridPoint.startIndex); calculateCourseCellColumns(dayCells);
} catch (error) {
dayCells.forEach((cell, _, arr) => { console.error(`Error calculating course cell columns ${idx}`, error);
let columnIndex = 1; if (sentryScope) {
cell.totalColumns = 1; sentryScope.captureException(error);
// 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) {
// Adjust columnIndex to not overlap with the otherCell
if (otherCell.gridColumnStart && otherCell.gridColumnStart >= columnIndex) {
columnIndex = otherCell.gridColumnStart + 1;
}
cell.totalColumns += 1;
} }
} }
}
cell.gridColumnStart = columnIndex;
cell.gridColumnEnd = columnIndex + 1;
});
}); });
return courseCells return courseCells

View File

@@ -17,8 +17,8 @@ function CalendarCell({ row, col }: Props): JSX.Element {
<div <div
className='h-full w-full flex items-center border-b border-r border-gray-300' className='h-full w-full flex items-center border-b border-r border-gray-300'
style={{ style={{
gridColumn: col + 3, gridColumn: col + 3, // start in the 3rd 1-index column
gridRow: `${2 * row + 2} / ${2 * row + 4}`, gridRow: `${2 * row + 3} / ${2 * row + 5}`, // Span 2 rows, skip 2 header rows
}} }}
> >
<div className='h-0 w-full border-t border-gray-300/25' /> <div className='h-0 w-full border-t border-gray-300/25' />

View File

@@ -5,6 +5,7 @@ import { Button } from '@views/components/common/Button';
import DialogProvider from '@views/components/common/DialogProvider/DialogProvider'; import DialogProvider from '@views/components/common/DialogProvider/DialogProvider';
import Divider from '@views/components/common/Divider'; import Divider from '@views/components/common/Divider';
import { ExtensionRootWrapper, styleResetClass } from '@views/components/common/ExtensionRoot/ExtensionRoot'; import { ExtensionRootWrapper, styleResetClass } from '@views/components/common/ExtensionRoot/ExtensionRoot';
import { LargeLogo } from '@views/components/common/LogoIcon';
import ScheduleTotalHoursAndCourses from '@views/components/common/ScheduleTotalHoursAndCourses'; import ScheduleTotalHoursAndCourses from '@views/components/common/ScheduleTotalHoursAndCourses';
import useSchedules from '@views/hooks/useSchedules'; import useSchedules from '@views/hooks/useSchedules';
import clsx from 'clsx'; import clsx from 'clsx';
@@ -27,7 +28,7 @@ export default function CalendarHeader({ sidebarOpen, onSidebarToggle }: Calenda
return ( return (
<div <div
style={{ scrollbarGutter: 'stable' }} style={{ scrollbarGutter: 'stable' }}
className='sticky left-0 right-0 min-h-[85px] flex items-center gap-5 overflow-x-scroll overflow-y-hidden pl-spacing-7 pt-spacing-5' className='sticky left-0 right-0 top-0 z-10 min-h-[85px] flex items-center gap-5 overflow-x-scroll overflow-y-hidden bg-white pl-spacing-7 pt-spacing-5'
> >
{!sidebarOpen && ( {!sidebarOpen && (
<Button <Button
@@ -39,6 +40,9 @@ export default function CalendarHeader({ sidebarOpen, onSidebarToggle }: Calenda
/> />
)} )}
<LargeLogo className='hidden! screenshot:flex!' />
<Divider className='self-center hidden! screenshot:block!' size='2.5rem' orientation='vertical' />
<div className='min-w-[11.5rem] screenshot:transform-origin-left screenshot:scale-120'> <div className='min-w-[11.5rem] screenshot:transform-origin-left screenshot:scale-120'>
<ScheduleTotalHoursAndCourses <ScheduleTotalHoursAndCourses
scheduleName={activeSchedule.name} scheduleName={activeSchedule.name}
@@ -62,7 +66,7 @@ export default function CalendarHeader({ sidebarOpen, onSidebarToggle }: Calenda
className={clsx([ className={clsx([
styleResetClass, styleResetClass,
'mt-spacing-3', 'mt-spacing-3',
'min-w-max cursor-pointer origin-top-right rounded bg-white p-1 text-black shadow-lg transition border border-ut-offwhite/50 focus:outline-none', 'min-w-max cursor-pointer origin-top-right rounded bg-white p-1 text-black shadow-lg transition border border-ut-offwhite/50 focus:outline-none z-20',
'data-[closed]:(opacity-0 scale-95)', 'data-[closed]:(opacity-0 scale-95)',
'data-[enter]:(ease-out-expo duration-150)', 'data-[enter]:(ease-out-expo duration-150)',
'data-[leave]:(ease-out duration-50)', 'data-[leave]:(ease-out duration-50)',

View File

@@ -0,0 +1,69 @@
import { AppStoreLogo, ForkKnife, X as CloseIcon } from '@phosphor-icons/react';
import { UT_DINING_APP_STORE_URL, UT_DINING_GOOGLE_PLAY_URL } from '@shared/util/appUrls';
import { Button } from '@views/components/common/Button';
import Text from '@views/components/common/Text/Text';
import React from 'react';
interface DiningAppPromoProps {
onClose: () => void;
}
/**
* Promotional component for the UT Dining app
*/
export default function DiningAppPromo({ onClose }: DiningAppPromoProps) {
return (
<div className='relative min-w-[16.25rem] w-full flex items-center gap-spacing-3 border border-ut-offwhite/50 rounded p-spacing-4'>
<div className='flex items-center justify-center'>
<ForkKnife className='h-6 w-6 text-ut-black' />
</div>
<div className='flex flex-col gap-spacing-1'>
<Text as='p' variant='small' className='whitespace-normal text-ut-black'>
Download our new{' '}
<a
href={UT_DINING_APP_STORE_URL}
target='_blank'
rel='noreferrer'
aria-label='UT Dining app'
className='text-ut-burntorange underline'
>
UT Dining app
</a>{' '}
to explore all dining options on campus!
</Text>
<div className='mt-spacing-2 flex items-center gap-spacing-2'>
<Text variant='mini' className='text-ut-black'>
Available on
</Text>
<a
href={UT_DINING_APP_STORE_URL}
target='_blank'
rel='noreferrer'
aria-label='Download on App Store'
className='text-theme-black transition-colors hover:text-ut-burntorange'
>
<AppStoreLogo className='h-4.5 w-4.5' />
</a>
{/* <a
href={UT_DINING_GOOGLE_PLAY_URL}
target='_blank'
rel='noreferrer'
aria-label='Download on Google Play'
className='text-theme-black hover:text-ut-burntorange transition-colors'
>
<GooglePlayLogo className='h-4.5 w-4.5' />
</a> */}
</div>
</div>
<Button
variant='minimal'
color='theme-black'
onClick={onClose}
className='absolute right-1 top-1 h-5 w-5 p-0'
icon={CloseIcon}
aria-label='Close dining app promo'
title='Close'
/>
</div>
);
}

View File

@@ -13,10 +13,10 @@ interface LinkItem {
} }
const links: LinkItem[] = [ const links: LinkItem[] = [
{ // {
text: "Spring '25 Course Schedule", // text: "Spring '25 Course Schedule",
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20252/', // url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20252/',
}, // },
{ {
text: 'Course Schedule Archives', text: 'Course Schedule Archives',
url: 'https://registrar.utexas.edu/schedules/archive', url: 'https://registrar.utexas.edu/schedules/archive',

View File

@@ -15,9 +15,17 @@ interface LinkItem {
const links: LinkItem[] = [ const links: LinkItem[] = [
{ {
text: "Spring '25 Course Schedule", text: "Fall '25 Course Schedule",
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20252/', url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20259/',
}, },
{
text: "Summer '25 Course Schedule",
url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20256/',
},
// {
// text: "Spring '25 Course Schedule",
// url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20252/',
// },
{ {
text: 'Course Schedule Archives', text: 'Course Schedule Archives',
url: 'https://registrar.utexas.edu/schedules/archive', url: 'https://registrar.utexas.edu/schedules/archive',
@@ -27,10 +35,6 @@ const links: LinkItem[] = [
url: 'https://utdirect.utexas.edu/apps/degree/audits/', url: 'https://utdirect.utexas.edu/apps/degree/audits/',
}, },
// { // {
// text: "Summer '24 Course Schedule",
// url: 'https://utdirect.utexas.edu/apps/registrar/course_schedule/20246/',
// },
// {
// text: "'24-'25 Academic Calendar", // text: "'24-'25 Academic Calendar",
// url: 'https://registrar.utexas.edu/calendars/24-25', // url: 'https://registrar.utexas.edu/calendars/24-25',
// }, // },

View File

@@ -0,0 +1,200 @@
type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type Year = `20${Digit}${Digit}`;
type Month = `0${Exclude<Digit, 0>}` | `1${'0' | '1' | '2'}`;
type Day = `0${Exclude<Digit, 0>}` | `${1 | 2}${Digit}` | '30' | '31';
type DateStr = `${Year}-${Month}-${Day}`;
type SemesterDigit = 2 | 6 | 9;
type SemesterIdentifier = `20${Digit}${Digit}${SemesterDigit}`;
type AcademicCalendarSemester = {
year: number;
semester: 'Fall' | 'Spring' | 'Summer';
firstClassDate: DateStr;
lastClassDate: DateStr;
breakDates: (DateStr | [DateStr, DateStr])[];
};
/**
* UT Austin's academic calendars, split by semester.
*
* See https://registrar.utexas.edu/calendars for future years.
*/
export const academicCalendars = {
'20229': {
year: 2022,
semester: 'Fall',
firstClassDate: '2022-08-22',
lastClassDate: '2022-12-05',
breakDates: [
'2022-09-05', // Labor Day holiday
['2022-11-21', '2022-11-26'], // Fall break / Thanksgiving
],
},
'20232': {
year: 2023,
semester: 'Spring',
firstClassDate: '2023-01-09',
lastClassDate: '2023-04-24',
breakDates: [
'2023-01-16', // Martin Luther King, Jr. Day
['2023-03-13', '2023-03-18'], // Spring Break
],
},
'20236': {
year: 2023,
semester: 'Summer',
firstClassDate: '2023-06-01',
lastClassDate: '2023-08-11',
breakDates: [
'2023-06-19', // Juneteenth holiday
'2023-07-04', // Independence Day holiday
],
},
'20239': {
year: 2023,
semester: 'Fall',
firstClassDate: '2023-08-21',
lastClassDate: '2023-12-04',
breakDates: [
'2023-09-04', // Labor Day holiday
['2023-11-20', '2023-11-25'], // Fall break / Thanksgiving
],
},
'20242': {
year: 2024,
semester: 'Spring',
firstClassDate: '2024-01-16',
lastClassDate: '2024-04-29',
breakDates: [
'2024-01-15', // Martin Luther King, Jr. Day
['2024-03-11', '2024-03-16'], // Spring Break
],
},
'20246': {
year: 2024,
semester: 'Summer',
firstClassDate: '2024-06-06',
lastClassDate: '2024-08-16',
breakDates: [
'2024-06-19', // Juneteenth holiday
'2024-07-04', // Independence Day holiday
],
},
'20249': {
year: 2024,
semester: 'Fall',
firstClassDate: '2024-08-26',
lastClassDate: '2024-12-09',
breakDates: [
'2024-09-02', // Labor Day holiday
['2024-11-25', '2024-11-30'], // Fall break / Thanksgiving
],
},
'20252': {
year: 2025,
semester: 'Spring',
firstClassDate: '2025-01-13',
lastClassDate: '2025-04-28',
breakDates: [
'2025-01-20', // Martin Luther King, Jr. Day
['2025-03-17', '2025-03-22'], // Spring Break
],
},
'20256': {
year: 2025,
semester: 'Summer',
firstClassDate: '2025-06-05',
lastClassDate: '2025-08-15',
breakDates: [
'2025-06-19', // Juneteenth holiday
'2025-07-04', // Independence Day holiday
],
},
'20259': {
year: 2025,
semester: 'Fall',
firstClassDate: '2025-08-25',
lastClassDate: '2025-12-08',
breakDates: [
'2025-09-01', // Labor Day holiday
['2025-11-24', '2025-11-29'], // Fall break / Thanksgiving
],
},
'20262': {
year: 2026,
semester: 'Spring',
firstClassDate: '2026-01-12',
lastClassDate: '2026-04-27',
breakDates: [
'2026-01-19', // Martin Luther King, Jr. Day
['2026-03-16', '2026-03-21'], // Spring Break
],
},
'20266': {
year: 2026,
semester: 'Summer',
firstClassDate: '2026-06-04',
lastClassDate: '2026-08-14',
breakDates: [
'2026-06-19', // Juneteenth holiday
'2026-07-04', // Independence Day holiday
],
},
'20269': {
year: 2026,
semester: 'Fall',
firstClassDate: '2026-08-24',
lastClassDate: '2026-12-07',
breakDates: [
'2026-09-07', // Labor Day holiday
['2026-11-23', '2026-11-28'], // Fall break / Thanksgiving
],
},
'20272': {
year: 2027,
semester: 'Spring',
firstClassDate: '2027-01-11',
lastClassDate: '2027-04-26',
breakDates: [
'2027-01-18', // Martin Luther King, Jr. Day
['2027-03-15', '2027-03-20'], // Spring Break
],
},
'20276': {
year: 2027,
semester: 'Summer',
firstClassDate: '2027-06-03',
lastClassDate: '2027-08-13',
breakDates: [
'2027-07-04', // Independence Day holiday
],
},
'20279': {
year: 2027,
semester: 'Fall',
firstClassDate: '2027-08-23',
lastClassDate: '2027-12-06',
breakDates: [
'2027-09-06', // Labor Day holiday
['2027-11-22', '2027-11-27'], // Fall break / Thanksgiving
],
},
'20282': {
year: 2028,
semester: 'Spring',
firstClassDate: '2028-01-18',
lastClassDate: '2028-05-01',
breakDates: [
['2028-03-13', '2028-03-18'], // Spring Break
],
},
'20286': {
year: 2028,
semester: 'Summer',
firstClassDate: '2028-06-08',
lastClassDate: '2028-08-18',
breakDates: [
'2028-07-04', // Independence Day holiday
],
},
} as const satisfies Partial<Record<SemesterIdentifier, AcademicCalendarSemester>>;

View File

@@ -1,6 +1,40 @@
import { describe, expect, it } from 'vitest'; import { tz } from '@date-fns/tz';
import { Course } from '@shared/types/Course';
import { UserSchedule } from '@shared/types/UserSchedule';
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
import type { Serialized } from 'chrome-extension-toolkit';
import { format as formatDate, parseISO } from 'date-fns';
import {
chatterjeeCS429Course,
multiMeetingMultiInstructorCourse,
multiMeetingMultiInstructorSchedule,
} from 'src/stories/injected/mocked';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { formatToHHMMSS } from './utils'; import type { CalendarCourseCellProps } from './CalendarCourseCell';
import {
allDatesInRanges,
calculateCourseCellColumns,
formatToHHMMSS,
meetingToIcsString,
nextDayInclusive,
scheduleToIcsString,
} from './utils';
// Do all timezone calculations relative to UT's timezone
const TIMEZONE = 'America/Chicago';
const TZ = tz(TIMEZONE);
// Date and datetime formats used by iCal
const ISO_DATE_FORMAT = 'yyyy-MM-dd';
const ISO_BASIC_DATETIME_FORMAT = "yyyyMMdd'T'HHmmss";
/**
* Simulate serialized class instance, without the class's methods
*
* serde &lt;-- Serialize, Deserialize
*/
const serde = <T>(data: T) => JSON.parse(JSON.stringify(data)) as Serialized<T>;
describe('formatToHHMMSS', () => { describe('formatToHHMMSS', () => {
it('should format minutes to HHMMSS format', () => { it('should format minutes to HHMMSS format', () => {
@@ -24,3 +58,624 @@ describe('formatToHHMMSS', () => {
expect(result).toBe(expected); expect(result).toBe(expected);
}); });
}); });
describe('nextDayInclusive', () => {
it('should return the same date if the given day is the same as the target day', () => {
const date = parseISO('2024-01-01', { in: TZ }); // Monday
const day = 1; // Monday
const result = nextDayInclusive(date, day);
expect(formatDate(result, ISO_DATE_FORMAT)).toBe('2024-01-01');
});
it('should return the next day if the given day is not the same as the target day', () => {
const date = parseISO('2024-07-18', { in: TZ }); // Thursday
const day = 2; // Tuesday
const result = nextDayInclusive(date, day);
expect(formatDate(result, ISO_DATE_FORMAT)).toBe('2024-07-23');
});
it('should wrap around years', () => {
const date = parseISO('2025-12-28', { in: TZ }); // Sunday
const day = 5; // Friday
const result = nextDayInclusive(date, day);
expect(formatDate(result, ISO_DATE_FORMAT)).toBe('2026-01-02');
});
it('should handle leap day', () => {
const date = parseISO('2024-02-27', { in: TZ }); // Tuesday
const day = 4; // Thursday
const result = nextDayInclusive(date, day);
expect(formatDate(result, ISO_DATE_FORMAT)).toBe('2024-02-29');
});
it('should handle an entire week of inputs', () => {
const date = parseISO('2024-08-20', { in: TZ }); // Tuesday
const days = [0, 1, 2, 3, 4, 5, 6] as const;
const results = days.map(day => nextDayInclusive(date, day));
const resultsFormatted = results.map(result => formatDate(result, ISO_DATE_FORMAT));
const expectedResults = [
'2024-08-25',
'2024-08-26',
'2024-08-20', // Same date
'2024-08-21',
'2024-08-22',
'2024-08-23',
'2024-08-24',
];
for (let i = 0; i < days.length; i++) {
expect(resultsFormatted[i]).toBe(expectedResults[i]);
}
});
it('should maintain hours/minutes/seconds', () => {
const date = parseISO('20250115T143021', { in: TZ }); // Wednesday
const days = [0, 1, 2, 3, 4, 5, 6] as const;
const results = days.map(day => nextDayInclusive(date, day));
const resultsFormatted = results.map(result => formatDate(result, ISO_BASIC_DATETIME_FORMAT));
const expectedResults = [
'20250119T143021',
'20250120T143021',
'20250121T143021',
'20250115T143021',
'20250116T143021',
'20250117T143021',
'20250118T143021',
];
for (let i = 0; i < days.length; i++) {
expect(resultsFormatted[i]).toBe(expectedResults[i]);
}
});
});
describe('allDatesInRanges', () => {
it('should handle empty array', () => {
const dateRanges = [] satisfies string[];
const result = allDatesInRanges(dateRanges);
const expected = [] satisfies Date[];
expect(result).toEqual(expected);
});
it('should handle a single date', () => {
const dateRanges = ['2025-03-14'] satisfies (string | [string, string])[];
const result = allDatesInRanges(dateRanges);
const expected = ['2025-03-14'].map(dateStr => parseISO(dateStr, { in: TZ })) satisfies Date[];
expect(result).toEqual(expected);
});
it('should handle a single date', () => {
const dateRanges = ['2025-03-14'] satisfies string[];
const result = allDatesInRanges(dateRanges);
const expected = ['2025-03-14'].map(dateStr => parseISO(dateStr, { in: TZ })) satisfies Date[];
expect(result).toEqual(expected);
});
it('should handle a single date range', () => {
const dateRanges = [['2025-03-14', '2025-03-19']] satisfies (string | [string, string])[];
const result = allDatesInRanges(dateRanges);
const expected = ['2025-03-14', '2025-03-15', '2025-03-16', '2025-03-17', '2025-03-18', '2025-03-19'].map(
dateStr => parseISO(dateStr, { in: TZ })
) satisfies Date[];
expect(result).toEqual(expected);
});
it('should handle multiple dates/date ranges', () => {
const dateRanges = [
'2025-02-14',
['2025-03-14', '2025-03-19'],
'2026-12-01',
['2026-12-03', '2026-12-05'],
] satisfies (string | [string, string])[];
const result = allDatesInRanges(dateRanges);
const expected = [
'2025-02-14', // '2025-02-14'
'2025-03-14', // ['2025-03-14', '2025-03-19']
'2025-03-15',
'2025-03-16',
'2025-03-17',
'2025-03-18',
'2025-03-19',
'2026-12-01', // '2026-12-01'
'2026-12-03', // ['2026-12-03', '2026-12-05'
'2026-12-04',
'2026-12-05',
].map(dateStr => parseISO(dateStr, { in: TZ })) satisfies Date[];
expect(result).toEqual(expected);
});
it('should handle month-/year-spanning ranges', () => {
const dateRanges = [
['2023-02-27', '2023-03-02'],
['2023-12-27', '2024-01-03'],
] satisfies (string | [string, string])[];
const result = allDatesInRanges(dateRanges);
const expected = [
'2023-02-27', // ['2023-02-27', '2023-03-2']
'2023-02-28',
'2023-03-01',
'2023-03-02',
'2023-12-27', // ['2023-12-27', '2024-01-3']
'2023-12-28',
'2023-12-29',
'2023-12-30',
'2023-12-31',
'2024-01-01',
'2024-01-02',
'2024-01-03',
].map(dateStr => parseISO(dateStr, { in: TZ })) satisfies Date[];
expect(result).toEqual(expected);
});
it('should handle leap years', () => {
const dateRanges = [
['2023-02-27', '2023-03-02'],
['2024-02-27', '2024-03-02'],
['2025-02-27', '2025-03-02'],
] satisfies (string | [string, string])[];
const result = allDatesInRanges(dateRanges);
const expected = [
'2023-02-27', // ['2023-02-27', '2023-03-2']
'2023-02-28',
'2023-03-01',
'2023-03-02',
'2024-02-27', // ['2024-02-27', '2024-03-2']
'2024-02-28',
'2024-02-29',
'2024-03-01',
'2024-03-02',
'2025-02-27', // ['2025-02-27', '2025-03-2']
'2025-02-28',
'2025-03-01',
'2025-03-02',
].map(dateStr => parseISO(dateStr, { in: TZ })) satisfies Date[];
expect(result).toEqual(expected);
});
});
describe('meetingToIcsString', () => {
it('should handle a one-day meeting with one instructor', () => {
const course = serde(multiMeetingMultiInstructorCourse);
course.instructors = course.instructors.slice(0, 1);
const meeting = course.schedule.meetings[1]!;
const result = meetingToIcsString(course, meeting);
const expected = (
`BEGIN:VEVENT
DTSTART;TZID=America/Chicago:20250829T130000
DTEND;TZID=America/Chicago:20250829T160000
RRULE:FREQ=WEEKLY;BYDAY=FR;UNTIL=20251209T060000Z
EXDATE;TZID=America/Chicago:20251128T130000
` +
// Only skips one Thanksgiving break day
`SUMMARY:J 395 44-REPORTING TEXAS
LOCATION:DMC 3.208
DESCRIPTION:Unique number: 10335\\nTaught by John Schwartz
END:VEVENT`
).replaceAll(/^\s+/gm, '');
expect(result).toBe(expected);
});
it('should handle unique numbers below 5 digits', () => {
const course = serde(multiMeetingMultiInstructorCourse);
course.instructors = course.instructors.slice(0, 1);
course.uniqueId = 4269;
const meeting = course.schedule.meetings[1]!;
const result = meetingToIcsString(course, meeting);
const expected = `BEGIN:VEVENT
DTSTART;TZID=America/Chicago:20250829T130000
DTEND;TZID=America/Chicago:20250829T160000
RRULE:FREQ=WEEKLY;BYDAY=FR;UNTIL=20251209T060000Z
EXDATE;TZID=America/Chicago:20251128T130000
SUMMARY:J 395 44-REPORTING TEXAS
LOCATION:DMC 3.208
DESCRIPTION:Unique number: 04269\\nTaught by John Schwartz
END:VEVENT`.replaceAll(/^\s+/gm, '');
expect(result).toBe(expected);
});
it('should handle a one-day meeting with multiple instructors', () => {
const course = serde(multiMeetingMultiInstructorCourse);
const meeting = course.schedule.meetings[1]!;
const result = meetingToIcsString(course, meeting);
const expected = `BEGIN:VEVENT
DTSTART;TZID=America/Chicago:20250829T130000
DTEND;TZID=America/Chicago:20250829T160000
RRULE:FREQ=WEEKLY;BYDAY=FR;UNTIL=20251209T060000Z
EXDATE;TZID=America/Chicago:20251128T130000
SUMMARY:J 395 44-REPORTING TEXAS
LOCATION:DMC 3.208
DESCRIPTION:Unique number: 10335\\nTaught by John Schwartz and John Bridges
END:VEVENT`.replaceAll(/^\s+/gm, '');
expect(result).toBe(expected);
});
it('should gracefully error on an out of range semester code', () => {
const course = serde(multiMeetingMultiInstructorCourse);
const meeting = course.schedule.meetings[0]!;
vi.spyOn(console, 'error').mockReturnValue(undefined);
course.semester = {
season: 'Fall',
year: 2010,
code: '20109',
};
const result = meetingToIcsString(course, meeting);
expect(result).toBeNull();
expect(console.error).toBeCalledWith(
`No academic calendar found for semester code: 20109; course uniqueId: ${course.uniqueId}`
);
});
it('should handle a multi-day meeting with multiple instructors', () => {
const course = serde(multiMeetingMultiInstructorCourse);
const meeting = course.schedule.meetings[0]!;
const result = meetingToIcsString(course, meeting);
const expected = `BEGIN:VEVENT
DTSTART;TZID=America/Chicago:20250826T093000
DTEND;TZID=America/Chicago:20250826T110000
RRULE:FREQ=WEEKLY;BYDAY=TU,TH;UNTIL=20251209T060000Z
EXDATE;TZID=America/Chicago:20251125T093000,20251127T093000
SUMMARY:J 395 44-REPORTING TEXAS
LOCATION:CMA 6.146
DESCRIPTION:Unique number: 10335\\nTaught by John Schwartz and John Bridges
END:VEVENT`.replaceAll(/^\s+/gm, '');
expect(result).toBe(expected);
});
afterEach(() => {
vi.restoreAllMocks();
});
});
describe('scheduleToIcsString', () => {
it('should handle an empty schedule', () => {
const schedule = serde(
new UserSchedule({
courses: [],
hours: 0,
id: 'fajowe',
name: 'fajowe',
updatedAt: Date.now(),
})
);
const result = scheduleToIcsString(schedule);
const expected = `BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
X-WR-CALNAME:My Schedule
END:VCALENDAR`.replaceAll(/^\s+/gm, '');
expect(result).toBe(expected);
});
it('should handle a schedule with courses but no meetings', () => {
const schedule = serde(
new UserSchedule({
courses: [
new Course({
...multiMeetingMultiInstructorCourse,
schedule: {
meetings: [],
},
}),
],
hours: 0,
id: 'fajowe',
name: 'fajowe',
updatedAt: Date.now(),
})
);
const result = scheduleToIcsString(schedule);
const expected = `BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
X-WR-CALNAME:My Schedule
END:VCALENDAR`.replaceAll(/^\s+/gm, '');
expect(result).toBe(expected);
});
it('should handle a schedule with courses but out-of-range semester', () => {
vi.spyOn(console, 'error').mockReturnValue(undefined);
const schedule = serde(
new UserSchedule({
courses: [
new Course({
...multiMeetingMultiInstructorCourse,
semester: {
season: 'Fall',
year: 2010,
code: '20109',
},
}),
],
hours: 0,
id: 'fajowe',
name: 'fajowe',
updatedAt: Date.now(),
})
);
const result = scheduleToIcsString(schedule);
const expected = `BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
X-WR-CALNAME:My Schedule
END:VCALENDAR`.replaceAll(/^\s+/gm, '');
expect(result).toBe(expected);
});
it('should handle a single course with multiple meetings', () => {
const schedule = serde(multiMeetingMultiInstructorSchedule);
const result = scheduleToIcsString(schedule);
const expected = `BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
X-WR-CALNAME:My Schedule
BEGIN:VEVENT
DTSTART;TZID=America/Chicago:20250826T093000
DTEND;TZID=America/Chicago:20250826T110000
RRULE:FREQ=WEEKLY;BYDAY=TU,TH;UNTIL=20251209T060000Z
EXDATE;TZID=America/Chicago:20251125T093000,20251127T093000
SUMMARY:J 395 44-REPORTING TEXAS
LOCATION:CMA 6.146
DESCRIPTION:Unique number: 10335\\nTaught by John Schwartz and John Bridges
END:VEVENT
BEGIN:VEVENT
DTSTART;TZID=America/Chicago:20250829T130000
DTEND;TZID=America/Chicago:20250829T160000
RRULE:FREQ=WEEKLY;BYDAY=FR;UNTIL=20251209T060000Z
EXDATE;TZID=America/Chicago:20251128T130000
SUMMARY:J 395 44-REPORTING TEXAS
LOCATION:DMC 3.208
DESCRIPTION:Unique number: 10335\\nTaught by John Schwartz and John Bridges
END:VEVENT
END:VCALENDAR`.replaceAll(/^\s+/gm, '');
expect(result).toBe(expected);
});
it('should handle a complex schedule', () => {
const schedule = serde(multiMeetingMultiInstructorSchedule);
schedule.courses.push(chatterjeeCS429Course);
const result = scheduleToIcsString(schedule);
const expected = (
`BEGIN:VCALENDAR
VERSION:2.0
CALSCALE:GREGORIAN
X-WR-CALNAME:My Schedule
BEGIN:VEVENT
DTSTART;TZID=America/Chicago:20250826T093000
DTEND;TZID=America/Chicago:20250826T110000
RRULE:FREQ=WEEKLY;BYDAY=TU,TH;UNTIL=20251209T060000Z
EXDATE;TZID=America/Chicago:20251125T093000,20251127T093000
SUMMARY:J 395 44-REPORTING TEXAS
LOCATION:CMA 6.146
DESCRIPTION:Unique number: 10335\\nTaught by John Schwartz and John Bridges
END:VEVENT
BEGIN:VEVENT
DTSTART;TZID=America/Chicago:20250829T130000
DTEND;TZID=America/Chicago:20250829T160000
RRULE:FREQ=WEEKLY;BYDAY=FR;UNTIL=20251209T060000Z
EXDATE;TZID=America/Chicago:20251128T130000
SUMMARY:J 395 44-REPORTING TEXAS
LOCATION:DMC 3.208
DESCRIPTION:Unique number: 10335\\nTaught by John Schwartz and John Bridges
END:VEVENT
BEGIN:VEVENT
DTSTART;TZID=America/Chicago:20250825T160000
DTEND;TZID=America/Chicago:20250825T170000
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH;UNTIL=20251209T060000Z
` +
// Skips Labor Day and only relevant days of Thanksgiving
`EXDATE;TZID=America/Chicago:20250901T160000,20251124T160000,20251125T160000,20251126T160000,20251127T160000
SUMMARY:C S 429 COMP ORGANIZATN AND ARCH
LOCATION:UTC 3.102
DESCRIPTION:Unique number: 54795\\nTaught by Siddhartha Chatterjee
END:VEVENT
BEGIN:VEVENT
DTSTART;TZID=America/Chicago:20250829T090000
DTEND;TZID=America/Chicago:20250829T110000
RRULE:FREQ=WEEKLY;BYDAY=FR;UNTIL=20251209T060000Z
EXDATE;TZID=America/Chicago:20251128T090000
SUMMARY:C S 429 COMP ORGANIZATN AND ARCH
LOCATION:GSB 2.122
DESCRIPTION:Unique number: 54795\\nTaught by Siddhartha Chatterjee
END:VEVENT
END:VCALENDAR`
).replaceAll(/^\s+/gm, '');
expect(result).toBe(expected);
});
afterEach(() => {
vi.restoreAllMocks();
});
});
describe('calculateCourseCellColumns', () => {
let testIdCounter = 0;
const makeCell = (startIndex: number, endIndex: number): CalendarGridCourse => {
if (endIndex <= startIndex && !(startIndex === -1 && endIndex === -1)) {
throw new Error('Test writer error: startIndex must be strictly less than endIndex');
}
const cell = {
calendarGridPoint: {
dayIndex: 1,
startIndex,
endIndex,
},
componentProps: {} as unknown as CalendarCourseCellProps,
course: {} as unknown as Course,
async: false,
} satisfies CalendarGridCourse;
/* eslint no-underscore-dangle: ["error", { "allow": ["__test_id"] }] */
(cell as unknown as { __test_id: number }).__test_id = testIdCounter++;
return cell;
};
/**
* Creates test cases for calculateCourseCellColumns
* @param cellConfigs - Array of [startIndex, endIndex, totalColumns, gridColumnStart, gridColumnEnd]
* @returns Tuple of [cells, expectedCells]
*/
const makeCellsTest = (
cellConfigs: Array<[number, number, number, number, number]>
): [CalendarGridCourse[], CalendarGridCourse[]] => {
// Create cells with only start/end indices
const cells = cellConfigs.map(([startIndex, endIndex]) => makeCell(startIndex, endIndex));
// Create expected cells with all properties set
const expectedCells = structuredClone<CalendarGridCourse[]>(cells);
cellConfigs.forEach((config, index) => {
const [, , totalColumns, gridColumnStart, gridColumnEnd] = config;
expectedCells[index]!.totalColumns = totalColumns;
expectedCells[index]!.gridColumnStart = gridColumnStart;
expectedCells[index]!.gridColumnEnd = gridColumnEnd;
});
return [cells, expectedCells];
};
beforeEach(() => {
// Ensure independence between tests
testIdCounter = 0;
});
it('should do nothing to an empty array if no courses are present', () => {
const cells: CalendarGridCourse[] = [];
calculateCourseCellColumns(cells);
expect(cells).toEqual([]);
});
it('should set the right values for one course cell', () => {
const [cells, expectedCells] = makeCellsTest([[13, 15, 1, 1, 2]]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it('should handle two separated courses', () => {
// These two cells can share a column, because they aren't concurrent
const [cells, expectedCells] = makeCellsTest([
[13, 15, 1, 1, 2],
[16, 18, 1, 1, 2],
]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it('should handle two back-to-back courses', () => {
// These two cells can share a column, because they aren't concurrent
const [cells, expectedCells] = makeCellsTest([
[13, 15, 1, 1, 2],
[15, 17, 1, 1, 2],
]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it('should handle two concurrent courses', () => {
// These two cells must be in separate columns, because they are concurrent
const [cells, expectedCells] = makeCellsTest([
[13, 15, 2, 1, 2],
[14, 16, 2, 2, 3],
]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it('should handle a simple grid', () => {
// Two columns
const [cells, expectedCells] = makeCellsTest([
[13, 15, 2, 1, 2], // start in left-most column
[15, 17, 2, 1, 2], // compact into left column
[13, 17, 2, 2, 3], // take up second column
]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it('should handle a simple grid, flipped', () => {
// Ensures `totalColumns` is calculated correctly
const [cells, expectedCells] = makeCellsTest([
[13, 17, 2, 1, 2],
[15, 17, 2, 2, 3],
[13, 15, 2, 2, 3],
]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it('should handle a weird grid', () => {
// Three columns
const [cells, expectedCells] = makeCellsTest([
[13, 15, 3, 1, 2],
[14, 18, 3, 2, 3],
[14, 16, 3, 3, 4],
[15, 17, 3, 1, 2], // compacted into left-most columns
]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it('should handle many clean concurrent courses', () => {
// All cells here are concurrent, 8 columns
const [cells, expectedCells] = makeCellsTest([
[10, 16, 8, 1, 2],
[12, 16, 8, 2, 3],
[13, 16, 8, 3, 4],
[13, 16, 8, 4, 5],
[13, 19, 8, 5, 6],
[13, 19, 8, 6, 7],
[14, 18, 8, 7, 8],
[15, 19, 8, 8, 9],
]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it('should handle many clean concurrent courses with one partially-concurrent', () => {
// Despite adding another course, we don't need to increase
// the number of columns, because we can compact
const [cells, expectedCells] = makeCellsTest([
[10, 16, 8, 1, 2],
[11, 15, 8, 2, 3], // new course, only overlaps with some
[12, 16, 8, 3, 4],
[13, 16, 8, 4, 5],
[13, 16, 8, 5, 6],
[13, 19, 8, 6, 7],
[13, 19, 8, 7, 8],
[14, 18, 8, 8, 9],
[15, 19, 8, 2, 3], // compacts to be under new course
]);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
it("shouldn't crash on courses without times", () => {
const cells = [makeCell(-1, -1), makeCell(-1, -1)];
cells[1]!.async = true; // see if we can ignore async and non-async courses without times
const expectedCells = structuredClone<CalendarGridCourse[]>(cells);
calculateCourseCellColumns(cells);
expect(cells).toEqual(expectedCells);
});
});

View File

@@ -1,9 +1,37 @@
import { tz, TZDate } from '@date-fns/tz';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import type { Course } from '@shared/types/Course';
import type { CourseMeeting } from '@shared/types/CourseMeeting';
import Instructor from '@shared/types/Instructor';
import type { UserSchedule } from '@shared/types/UserSchedule'; import type { UserSchedule } from '@shared/types/UserSchedule';
import { downloadBlob } from '@shared/util/downloadBlob'; import { downloadBlob } from '@shared/util/downloadBlob';
import { englishStringifyList } from '@shared/util/string';
import type { CalendarGridCourse } from '@views/hooks/useFlattenedCourseSchedule';
import type { Serialized } from 'chrome-extension-toolkit'; import type { Serialized } from 'chrome-extension-toolkit';
import type { DateArg, Day } from 'date-fns';
import {
addDays,
eachDayOfInterval,
format as formatDate,
formatISO,
getDay,
nextDay,
parseISO,
set as setMultiple,
} from 'date-fns';
import { toBlob } from 'html-to-image'; import { toBlob } from 'html-to-image';
import { academicCalendars } from './academic-calendars';
// Do all timezone calculations relative to UT's timezone
const TIMEZONE_ID = 'America/Chicago';
const TZ = tz(TIMEZONE_ID);
// Datetime format used by iCal, not directly supported by date-fns
// (date-fns adds the timezone to the end, but iCal doesn't want it)
const ISO_BASIC_DATETIME_FORMAT = "yyyyMMdd'T'HHmmss";
// iCal uses two-letter codes for days of the week
export const CAL_MAP = { export const CAL_MAP = {
Sunday: 'SU', Sunday: 'SU',
Monday: 'MO', Monday: 'MO',
@@ -14,6 +42,17 @@ export const CAL_MAP = {
Saturday: 'SA', Saturday: 'SA',
} as const satisfies Record<string, string>; } as const satisfies Record<string, string>;
// Date objects' day field goes by index like this
const DAY_NAME_TO_NUMBER = {
Sunday: 0,
Monday: 1,
Tuesday: 2,
Wednesday: 3,
Thursday: 4,
Friday: 5,
Saturday: 6,
} as const satisfies Record<string, number>;
/** /**
* Retrieves the schedule from the UserScheduleStore based on the active index. * Retrieves the schedule from the UserScheduleStore based on the active index.
* @returns A promise that resolves to the retrieved schedule. * @returns A promise that resolves to the retrieved schedule.
@@ -38,50 +77,186 @@ export const formatToHHMMSS = (minutes: number) => {
return `${hours}${mins}00`; return `${hours}${mins}00`;
}; };
/**
* Formats a date in the format YYYYMMDD'T'HHmmss, which is the format used by iCal.
*
* @param date - The date to format.
* @returns The formatted date string.
*/
const iCalDateFormat = <DateType extends Date>(date: DateArg<DateType>) =>
formatDate(date, ISO_BASIC_DATETIME_FORMAT, { in: TZ });
/**
* Returns the next day of the given date, inclusive of the given day.
*
* If the given date is the given day, the same date is returned.
*
* For example, a Monday targeting a Wednesday will return the next Wednesday,
* but if it was targeting a Monday it would return the same date.
*
* @param date - The date to increment.
* @param day - The day to increment to. (0 = Sunday, 1 = Monday, etc.)
* @returns The next day of the given date, inclusive of the given day.
*/
export const nextDayInclusive = (date: Date, day: Day): TZDate => {
if (getDay(date, { in: TZ }) === day) {
return new TZDate(date, TIMEZONE_ID);
}
return nextDay(date, day, { in: TZ });
};
/**
* Returns an array of all the dates (as Date objects) in the given date ranges.
*
* @param dateRanges - An array of date ranges.
* Each date range can be a string (in which case it is interpreted as a single date)
* or an array of two strings (in which case it is interpreted as a date range, inclusive).
* @returns An array of all the dates (as Date objects) in the given date ranges.
*
* @example
* allDatesInRanges(['2025-01-01', ['2025-03-14', '2025-03-16']]) // ['2025-01-01', '2025-03-14', '2025-03-15', '2025-03-16'] (as Date objects)
*
* @remarks Does not remove duplicate dates.
*/
export const allDatesInRanges = (dateRanges: readonly (string | [string, string])[]): Date[] =>
dateRanges.flatMap(breakDate => {
if (Array.isArray(breakDate)) {
return eachDayOfInterval({
start: parseISO(breakDate[0], { in: TZ }),
end: parseISO(breakDate[1], { in: TZ }),
});
}
return parseISO(breakDate, { in: TZ });
});
/**
* Creates a VEVENT string for a meeting of a course.
*
* @param course - The course object
* @param meeting - The meeting object
* @returns A string representation of the meeting in the iCalendar format (ICS)
*/
export const meetingToIcsString = (course: Serialized<Course>, meeting: Serialized<CourseMeeting>): string | null => {
const { startTime, endTime, days, location } = meeting;
if (!course.semester.code) {
console.error(`No semester found for course uniqueId: ${course.uniqueId}`);
return null;
}
if (days.length === 0) {
console.error(`No days found for course uniqueId: ${course.uniqueId}`);
return null;
}
if (!Object.prototype.hasOwnProperty.call(academicCalendars, course.semester.code)) {
console.error(
`No academic calendar found for semester code: ${course.semester.code}; course uniqueId: ${course.uniqueId}`
);
return null;
}
const academicCalendar = academicCalendars[course.semester.code as keyof typeof academicCalendars];
const startDate = nextDayInclusive(
parseISO(academicCalendar.firstClassDate, { in: TZ }),
DAY_NAME_TO_NUMBER[days[0]!]
);
const startTimeHours = Math.floor(startTime / 60);
const startTimeMinutes = startTime % 60;
const startTimeDate = setMultiple(startDate, { hours: startTimeHours, minutes: startTimeMinutes }, { in: TZ });
const endTimeHours = Math.floor(endTime / 60);
const endTimeMinutes = endTime % 60;
const endTimeDate = setMultiple(startDate, { hours: endTimeHours, minutes: endTimeMinutes }, { in: TZ });
const untilDate = addDays(parseISO(academicCalendar.lastClassDate, { in: TZ }), 1);
const daysNumSet = new Set(days.map(d => DAY_NAME_TO_NUMBER[d]));
const excludedDates = allDatesInRanges(academicCalendar.breakDates)
// Don't need to exclude Tues/Thurs if it's a MWF class, etc.
.filter(date => daysNumSet.has(getDay(date, { in: TZ }) as Day))
.map(date => setMultiple(date, { hours: startTimeHours, minutes: startTimeMinutes }, { in: TZ }));
const startDateFormatted = iCalDateFormat(startTimeDate);
const endDateFormatted = iCalDateFormat(endTimeDate);
// Convert days to ICS compatible format, e.g. MO,WE,FR
const icsDays = days.map(day => CAL_MAP[day]).join(',');
// per spec, UNTIL must be in UTC
const untilDateFormatted = formatISO(untilDate, { format: 'basic', in: tz('utc') });
const excludedDatesFormatted = excludedDates.map(date => iCalDateFormat(date));
const uniqueNumberFormatted = course.uniqueId.toString().padStart(5, '0');
// The list part of "Taught by Michael Scott and Siddhartha Chatterjee Beasley"
const instructorsFormatted = englishStringifyList(
course.instructors
.map(instructor => Instructor.prototype.toString.call(instructor, { format: 'first_last' }))
.filter(name => name !== '')
);
// Construct event string
let icsString = 'BEGIN:VEVENT\n';
icsString += `DTSTART;TZID=${TIMEZONE_ID}:${startDateFormatted}\n`;
icsString += `DTEND;TZID=${TIMEZONE_ID}:${endDateFormatted}\n`;
icsString += `RRULE:FREQ=WEEKLY;BYDAY=${icsDays};UNTIL=${untilDateFormatted}\n`;
icsString += `EXDATE;TZID=${TIMEZONE_ID}:${excludedDatesFormatted.join(',')}\n`;
icsString += `SUMMARY:${course.department} ${course.number} \u2013 ${course.courseName}\n`;
if (location?.building || location?.building) {
const locationFormatted = `${location?.building ?? ''} ${location?.room ?? ''}`.trim();
icsString += `LOCATION:${locationFormatted}\n`;
}
icsString += `DESCRIPTION:Unique number: ${uniqueNumberFormatted}`;
if (instructorsFormatted) {
// Newlines need to be double-escaped
icsString += `\\nTaught by ${instructorsFormatted}`;
}
icsString += '\n';
icsString += 'END:VEVENT';
return icsString;
};
/**
* Creates a VCALENDAR string for a schedule of a user.
* @param schedule - The schedule object
* @returns A string representation of the schedule in the iCalendar format (ICS)
*/
export const scheduleToIcsString = (schedule: Serialized<UserSchedule>) => {
let icsString = 'BEGIN:VCALENDAR\nVERSION:2.0\nCALSCALE:GREGORIAN\nX-WR-CALNAME:My Schedule\n';
const vevents = schedule.courses
.flatMap(course => course.schedule.meetings.map(meeting => meetingToIcsString(course, meeting)))
.filter(event => event !== null)
.join('\n');
if (vevents.length > 0) {
icsString += `${vevents}\n`;
}
icsString += 'END:VCALENDAR';
return icsString;
};
/** /**
* Saves the current schedule as a calendar file in the iCalendar format (ICS). * Saves the current schedule as a calendar file in the iCalendar format (ICS).
* Fetches the current active schedule and converts it into an ICS string. * Fetches the current active schedule and converts it into an ICS string.
* Downloads the ICS file to the user's device. * Downloads the ICS file to the user's device.
*/ */
export const saveAsCal = async () => { export const saveAsCal = async () => {
const schedule = await getSchedule(); // Assumes this fetches the current active schedule const schedule = await getSchedule();
let icsString = 'BEGIN:VCALENDAR\nVERSION:2.0\nCALSCALE:GREGORIAN\nX-WR-CALNAME:My Schedule\n';
if (!schedule) { if (!schedule) {
throw new Error('No schedule found'); throw new Error('No schedule found');
} }
schedule.courses.forEach(course => { const icsString = scheduleToIcsString(schedule);
course.schedule.meetings.forEach(meeting => {
const { startTime, endTime, days, location } = meeting;
// Format start and end times to HHMMSS
const formattedStartTime = formatToHHMMSS(startTime);
const formattedEndTime = formatToHHMMSS(endTime);
// Map days to ICS compatible format
console.log(days);
const icsDays = days.map(day => CAL_MAP[day]).join(',');
console.log(icsDays);
// Assuming course has date started and ended, adapt as necessary
// const year = new Date().getFullYear(); // Example year, adapt accordingly
// Example event date, adapt startDate according to your needs
const startDate = `20240101T${formattedStartTime}`;
const endDate = `20240101T${formattedEndTime}`;
icsString += `BEGIN:VEVENT\n`;
icsString += `DTSTART:${startDate}\n`;
icsString += `DTEND:${endDate}\n`;
icsString += `RRULE:FREQ=WEEKLY;BYDAY=${icsDays}\n`;
icsString += `SUMMARY:${course.fullName}\n`;
icsString += `LOCATION:${location?.building ?? ''} ${location?.room ?? ''}\n`;
icsString += `END:VEVENT\n`;
});
});
icsString += 'END:VCALENDAR';
downloadBlob(icsString, 'CALENDAR', 'schedule.ics'); downloadBlob(icsString, 'CALENDAR', 'schedule.ics');
}; };
@@ -92,31 +267,38 @@ export const saveAsCal = async () => {
* @param calendarRef - The reference to the calendar component. * @param calendarRef - The reference to the calendar component.
*/ */
export const saveCalAsPng = () => { export const saveCalAsPng = () => {
const WIDTH_PX = 1165;
const HEIGHT_PX = 754;
const SCALE = 2;
const rootNode = document.createElement('div'); const rootNode = document.createElement('div');
rootNode.style.backgroundColor = 'white'; rootNode.style.backgroundColor = 'white';
rootNode.style.position = 'fixed'; rootNode.style.position = 'fixed';
rootNode.style.zIndex = '1000'; rootNode.style.zIndex = '1000';
rootNode.style.top = '-10000px'; rootNode.style.top = '-10000px';
rootNode.style.left = '-10000px'; rootNode.style.left = '-10000px';
rootNode.style.width = '1165px'; rootNode.style.width = `${WIDTH_PX}px`;
rootNode.style.height = '754px'; rootNode.style.height = `${HEIGHT_PX}px`;
document.body.appendChild(rootNode); document.body.appendChild(rootNode);
const clonedNode = document.querySelector('#root')!.cloneNode(true) as HTMLDivElement; const clonedNode = document.querySelector('#root')!.cloneNode(true) as HTMLDivElement;
clonedNode.style.backgroundColor = 'white'; clonedNode.style.backgroundColor = 'white';
(clonedNode.firstChild as HTMLDivElement).classList.add('screenshot-in-progress'); (clonedNode.firstChild as HTMLDivElement).classList.add('screenshot-in-progress');
return new Promise<void>((resolve, reject) => { const calendarTarget = clonedNode.querySelector('.screenshot\\:calendar-target') as HTMLDivElement;
requestAnimationFrame(async () => { calendarTarget.style.width = `${WIDTH_PX}px`;
rootNode.appendChild(clonedNode); calendarTarget.style.height = `${HEIGHT_PX}px`;
return new Promise<void>((resolve, reject) => {
rootNode.appendChild(clonedNode);
requestAnimationFrame(async () => {
try { try {
const screenshotBlob = await toBlob(clonedNode, { const screenshotBlob = await toBlob(clonedNode, {
cacheBust: true, cacheBust: true,
canvasWidth: 1165 * 2, canvasWidth: WIDTH_PX * SCALE,
canvasHeight: 754 * 2, canvasHeight: HEIGHT_PX * SCALE,
skipAutoScale: true, skipAutoScale: true,
pixelRatio: 2, pixelRatio: SCALE,
}); });
if (!screenshotBlob) { if (!screenshotBlob) {
@@ -134,3 +316,136 @@ export const saveCalAsPng = () => {
}); });
}); });
}; };
/**
* Determines all the connected components in the list of cells, where two cells
* are "connected" if there is a path (potentially through other cells) where
* each neighboring cells have overlapping start/end times
*
* @param cells - An array of cells to go on the calendar grid
* @returns An array of connected components, where the inner array is a list of
* all cells that there's a path to (potentially through other intervals)
* without crossing a time gap
*
* @remarks The internal fields cell.concurrentCells and cell.hasParent are
* modified by this function
*
* @example [[8am, 9am), [8:30am, 10am), [9:30am, 11am)] // is all one connected component
* @example [[8am, 9am), [8:30am, 10am), [10am, 11am)] // has two connected components, [[8am, 9am), [8:30am, 10am)] and [[10am, 11am)]]
*/
const findConnectedComponents = (cells: CalendarGridCourse[]): CalendarGridCourse[][] => {
const connectedComponents: CalendarGridCourse[][] = [];
for (let i = 0; i < cells.length; i++) {
const cell = cells[i]!;
if (!cell.concurrentCells || cell.concurrentCells.length === 0) {
// If this cell isn't already part of an existing connected component,
// then we need to make a new one.
connectedComponents.push([]);
}
connectedComponents.at(-1)!.push(cell);
for (let j = i + 1; j < cells.length; j++) {
const otherCell = cells[j]!;
if (otherCell.calendarGridPoint.startIndex >= cell.calendarGridPoint.endIndex) {
break;
}
// By ordering of cells array, we know cell.startTime <= other.startTime
// By the if check above, we know cell.endTime > other.endTime
// So, they're concurrent
// Also, by initializing j to i + 1, we know we don't have duplicates
cell.concurrentCells!.push(otherCell);
otherCell.concurrentCells!.push(cell);
}
}
return connectedComponents;
};
/**
* Assigns column positions to each cell in a set of calendar grid cells.
* Ensures that overlapping cells are placed in different columns.
*
* Inspired by the Greedy Interval-Partitioning algorithm.
*
* @param cells - An array of calendar grid course cells to position, must be
* sorted in increasing order of start time
* @throws Error if there's no available column for a cell (should never happen if totalColumns is calculated correctly)
* @remarks The number of columns created is strictly equal to the minimum needed by a perfectly optimal algorithm.
* The minimum number of columns needed is the maximum number of events that happen concurrently.
* Research Interval Graphs for more info https://en.wikipedia.org/wiki/Interval_graph
*/
const assignColumns = (cells: CalendarGridCourse[]) => {
const availableColumns = [true];
for (const cell of cells) {
availableColumns.fill(true);
for (const otherCell of cell.concurrentCells!) {
if (otherCell.gridColumnStart !== undefined) {
availableColumns[otherCell.gridColumnStart - 1] = false;
}
}
// Find an available column, or create one if all columns are full
let column = availableColumns.indexOf(true);
if (column === -1) {
column = availableColumns.length;
availableColumns.push(true);
}
// CSS Grid uses 1-based indexing
cell.gridColumnStart = column + 1;
cell.gridColumnEnd = column + 2;
}
for (const cell of cells) {
cell.totalColumns = availableColumns.length;
}
};
/**
* Calculates the column positions for course cells in a calendar grid.
* This function handles the layout algorithm for displaying overlapping course meetings
* in a calendar view. It identifies connected components of overlapping courses,
* determines the number of columns needed for each component, and assigns appropriate
* column positions to each cell.
*
* @param dayCells - An array of calendar grid course cells for a specific day
*/
export const calculateCourseCellColumns = (dayCells: CalendarGridCourse[]) => {
// Sort by start time, increasing
// This is necessary for the correctness of the column assignment
const cells = dayCells
.filter(
cell =>
!cell.async &&
cell.calendarGridPoint &&
typeof cell.calendarGridPoint.startIndex === 'number' &&
cell.calendarGridPoint.startIndex >= 0
)
.toSorted((a, b) => a.calendarGridPoint.startIndex - b.calendarGridPoint.startIndex);
// Initialize metadata
for (const cell of cells) {
cell.concurrentCells = [];
cell.gridColumnStart = undefined;
cell.gridColumnEnd = undefined;
}
// Construct connected components, set concurrent neighbors
const connectedComponents = findConnectedComponents(cells);
// Assign columns for each connectedComponents
for (const cc of connectedComponents) {
assignColumns(cc);
}
// Clean up
for (const cell of cells) {
delete cell.concurrentCells;
}
};

View File

@@ -46,7 +46,7 @@ export function usePrompt(): (info: PromptInfo, options?: DialogOptions) => void
{info.description} {info.description}
</Text> </Text>
), ),
className: 'max-w-[400px] flex flex-col gap-2.5 p-6.25', className: 'max-w-[415px] flex flex-col gap-2.5 p-6.25 border border-ut-offwhite/50',
}, },
options options
); );

View File

@@ -1,6 +1,7 @@
// import '@unocss/reset/tailwind-compat.css'; // import '@unocss/reset/tailwind-compat.css';
import 'uno.css'; import 'uno.css';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import clsx from 'clsx'; import clsx from 'clsx';
import React, { forwardRef } from 'react'; import React, { forwardRef } from 'react';
@@ -8,6 +9,8 @@ import styles from './ExtensionRoot.module.scss';
export const styleResetClass = styles.extensionRoot; export const styleResetClass = styles.extensionRoot;
const queryClient = new QueryClient();
/** /**
* A wrapper component for the extension elements that adds some basic styling to them * A wrapper component for the extension elements that adds some basic styling to them
*/ */
@@ -16,7 +19,9 @@ export default function ExtensionRoot(props: React.HTMLProps<HTMLDivElement>): J
return ( return (
<React.StrictMode> <React.StrictMode>
<QueryClientProvider client={queryClient}>
<div className={clsx(styleResetClass, 'h-full', className)} {...others} /> <div className={clsx(styleResetClass, 'h-full', className)} {...others} />
</QueryClientProvider>
</React.StrictMode> </React.StrictMode>
); );
} }

View File

@@ -1,4 +1,5 @@
import type { Icon, IconProps } from '@phosphor-icons/react'; import type { Icon, IconProps } from '@phosphor-icons/react';
import type { MIMETypeValue } from '@shared/types/MIMEType';
import type { ThemeColor } from '@shared/types/ThemeColors'; import type { ThemeColor } from '@shared/types/ThemeColors';
import { getThemeColorHexByName, getThemeColorRgbByName } from '@shared/util/themeColors'; import { getThemeColorHexByName, getThemeColorRgbByName } from '@shared/util/themeColors';
import Text from '@views/components/common/Text/Text'; import Text from '@views/components/common/Text/Text';
@@ -16,6 +17,7 @@ interface Props {
disabled?: boolean; disabled?: boolean;
title?: string; title?: string;
color: ThemeColor; color: ThemeColor;
accept?: MIMETypeValue | MIMETypeValue[];
} }
/** /**
@@ -34,6 +36,7 @@ export default function FileUpload({
disabled, disabled,
title, title,
color, color,
accept,
children, children,
}: React.PropsWithChildren<Props>): JSX.Element { }: React.PropsWithChildren<Props>): JSX.Element {
const Icon = icon; const Icon = icon;
@@ -41,6 +44,9 @@ export default function FileUpload({
const colorHex = getThemeColorHexByName(color); const colorHex = getThemeColorHexByName(color);
const colorRgb = getThemeColorRgbByName(color)?.join(' '); const colorRgb = getThemeColorRgbByName(color)?.join(' ');
// Convert accept to a comma-separated string if it's an array
const acceptValue = Array.isArray(accept) ? accept.join(',') : accept;
return ( return (
<label <label
style={ style={
@@ -78,7 +84,13 @@ export default function FileUpload({
{children} {children}
</Text> </Text>
)} )}
<input type='file' className='hidden' disabled={disabled} onChange={disabled ? undefined : onChange} /> <input
type='file'
{...(accept ? { accept: acceptValue } : {})}
className='hidden'
disabled={disabled}
onChange={disabled ? undefined : onChange}
/>
</label> </label>
); );
} }

View File

@@ -102,7 +102,7 @@ export function useMigrationDialog() {
{ {
title: 'This extension has updated!', title: 'This extension has updated!',
description: description:
"You may have already began planning your Spring '25 schedule. Click the button below to transfer your saved schedules into a new schedule. (You may be required to login to the UT Registrar)", "You may have already began planning your Fall '25 schedule. Click the button below to transfer your saved schedules into a new schedule. (You may be required to login to the UT Registrar)",
buttons: close => <MigrationButtons close={close} />, buttons: close => <MigrationButtons close={close} />,
}, },

View File

@@ -24,8 +24,12 @@ export default function ScheduleDropdown({ defaultOpen, children }: ScheduleDrop
{({ open }) => ( {({ open }) => (
<> <>
<DisclosureButton className='w-full flex items-center border-none bg-transparent px-3.5 py-2.5 text-left'> <DisclosureButton className='w-full flex items-center border-none bg-transparent px-3.5 py-2.5 text-left'>
<div className='flex-1'> <div className='min-w-0 flex-1 overflow-hidden'>
<Text as='div' variant='h3' className='w-100% text-ut-burntorange normal-case!'> <Text
as='div'
variant='h3'
className='w-full truncate whitespace-nowrap text-ut-burntorange normal-case!'
>
{activeSchedule ? activeSchedule.name : 'Schedule'} {activeSchedule ? activeSchedule.name : 'Schedule'}
</Text> </Text>
<div className='flex gap-2.5 text-theme-black leading-[75%]!'> <div className='flex gap-2.5 text-theme-black leading-[75%]!'>

View File

@@ -109,12 +109,14 @@ export default function ScheduleListItem({ schedule, onClick }: ScheduleListItem
const handleDelete = () => { const handleDelete = () => {
showDialog({ showDialog({
title: 'Are you sure?', title: 'Delete schedule?',
description: ( description: (
<> <>
<Text>Deleting</Text> <Text>Deleting </Text>
<Text className='text-ut-burntorange'> {schedule.name} </Text> <Text className='text-ut-burntorange'>{schedule.name}</Text>
<Text>is permanent and will remove all added courses from that schedule.</Text> <Text> is permanent and will remove all added courses from </Text>
<Text className='text-ut-burntorange'>{schedule.name}</Text>
<Text>.</Text>
</> </>
), ),
// eslint-disable-next-line react/no-unstable-nested-components // eslint-disable-next-line react/no-unstable-nested-components
@@ -126,12 +128,13 @@ export default function ScheduleListItem({ schedule, onClick }: ScheduleListItem
<Button <Button
variant='filled' variant='filled'
color='theme-red' color='theme-red'
icon={Trash}
onClick={() => { onClick={() => {
close(); close();
deleteSchedule(schedule.id); deleteSchedule(schedule.id);
}} }}
> >
Delete Permanently Delete permanently
</Button> </Button>
</> </>
), ),

View File

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

View File

@@ -0,0 +1,139 @@
import type { IconProps } from '@phosphor-icons/react';
import { CloudX, Coffee, ForkKnife, MapTrifold, Storefront } from '@phosphor-icons/react';
import { ExtensionStore } from '@shared/storage/ExtensionStore';
import { UT_DINING_PROMO_IMAGE_URL } from '@shared/util/appUrls';
import Text from '@views/components/common/Text/Text';
import useWhatsNewPopUp from '@views/hooks/useWhatsNew';
import React, { useEffect, useState } from 'react';
/**
* This is the version of the 'What's New' features popup.
*
* It is used to check if the popup has already been shown to the user or not
*
* It should be incremented every time the "What's New" popup is updated.
*/
const WHATSNEW_POPUP_VERSION = 2;
const WHATSNEW_VIDEO_URL = 'https://cdn.longhorns.dev/whats-new-v2.1.2.mp4';
type Feature = {
id: string;
icon: React.ForwardRefExoticComponent<IconProps>;
title: string | JSX.Element;
description: string;
};
const NEW_FEATURES = [
{
id: 'dining-halls-info',
icon: ForkKnife,
title: 'Dining Halls Info',
description: 'See daily menus and nutritional deets for J2, JCL, and Kins',
},
{
id: 'coffee-shops',
icon: Coffee,
title: 'Coffee Shops',
description: 'Need a Coffee Fix? Check operating times for your favorite campus cafes.',
},
{
id: 'convenience-stores',
icon: Storefront,
title: 'Convenience Stores',
description: 'Find hours for quick snacks and essentials on campus.',
},
{
id: 'microwave-map',
icon: MapTrifold,
title: 'Microwave Map',
description: 'Need to heat up your lunch? Find microwaves across campus!',
},
] as const satisfies readonly Feature[];
/**
* WhatsNewPopupContent component.
*
* This component displays the content of the WhatsNew dialog.
* It shows the new features that have been added to the extension.
*
* @returns A JSX of WhatsNewPopupContent component.
*/
export default function WhatsNewPopupContent(): JSX.Element {
const [videoError, setVideoError] = useState(false);
return (
<div className='w-full flex flex-row justify-between'>
<div className='w-full flex flex-col-reverse items-center justify-between gap-spacing-7 md:flex-row'>
<div className='grid grid-cols-1 w-fit items-center gap-spacing-6 sm:grid-cols-2 md:w-[277px] md:flex md:flex-col md:flex-nowrap'>
{NEW_FEATURES.map(({ id, icon: Icon, title, description }) => (
<div key={id} className='w-full flex items-center justify-between gap-spacing-5'>
<Icon width='40' height='40' className='text-ut-burntorange' />
<div className='w-full flex flex-col gap-spacing-1'>
<Text variant='h4' className='text-ut-burntorange font-bold!'>
{title}
</Text>
<Text variant='p' className='text-ut-black'>
{description}
</Text>
</div>
</div>
))}
</div>
<div className='h-full max-w-[464px] w-full flex items-center justify-center'>
{videoError ? (
<div className='h-full w-full flex items-center justify-center border border-ut-offwhite/50 rounded'>
<div className='flex flex-col items-center justify-center p-spacing-2'>
<CloudX size={52} className='text-ut-black/50' />
<Text variant='h4' className='text-center text-ut-black/50'>
Failed to load video. Please try again later.
</Text>
</div>
</div>
) : (
<img
className='h-full w-full border border-ut-offwhite/50 rounded object-cover'
src={UT_DINING_PROMO_IMAGE_URL}
alt='UT Dining Promo'
/>
)}
</div>
</div>
</div>
);
}
/**
* WhatsNewDialog component.
*
* This component is responsible for checking if the extension has already been updated
* and if so, it displays the WhatsNew dialog. Then it updates the state to show that the
* dialog has been shown.
*
* @returns An empty fragment.
*
* @remarks
* The component uses the `useWhatsNew` hook to show the WhatsNew dialog and the
* `useEffect` hook to perform the check on component mount. It also uses the `ExtensionStore`
* to view the state of the dialog.
*/
export function WhatsNewDialog(): null {
const showPopUp = useWhatsNewPopUp();
useEffect(() => {
const checkUpdate = async () => {
const version = await ExtensionStore.get('lastWhatsNewPopupVersion');
if (version !== WHATSNEW_POPUP_VERSION) {
await ExtensionStore.set('lastWhatsNewPopupVersion', WHATSNEW_POPUP_VERSION);
showPopUp();
}
};
checkUpdate();
// This is on purpose
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return null;
}

View File

@@ -140,7 +140,7 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
tickWidth: 1, tickWidth: 1,
tickLength: 10, tickLength: 10,
tickColor: '#9CADB7', tickColor: '#9CADB7',
crosshair: { color: extendedColors.theme.offwhite }, crosshair: { color: `${extendedColors.theme.offwhite}50` },
lineColor: '#9CADB7', lineColor: '#9CADB7',
}, },
yAxis: { yAxis: {
@@ -260,7 +260,7 @@ export default function GradeDistribution({ course }: GradeDistributionProps): J
))} ))}
</select> </select>
<Link variant='small' href={UT_GRADE_DISTRIBUTION_URL} className='link'> <Link variant='small' href={UT_GRADE_DISTRIBUTION_URL} className='link'>
About the data Data Source
</Link> </Link>
</div> </div>
{distributions[semester] && !distributions[semester]!.instructorIncluded && ( {distributions[semester] && !distributions[semester]!.instructorIncluded && (

View File

@@ -0,0 +1,89 @@
import ExtensionRoot from '@views/components/common/ExtensionRoot/ExtensionRoot';
import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'] as const;
/**
* Component that transforms the days dropdown into a series of checkboxes
* on the course catalog search page
*
* @returns The rendered checkbox component or null if the container is not found.
*/
export default function DaysCheckbox(): JSX.Element | null {
const [container, setContainer] = useState<HTMLDivElement | null>(null);
const [daysValue, setDaysValue] = useState<number[]>([0, 0, 0, 0, 0, 0]);
useEffect(() => {
const daysDropdown = document.getElementById('mtg_days_st') as HTMLSelectElement | null;
if (!daysDropdown) {
console.error('Days dropdown not found');
return;
}
const formElement = daysDropdown.closest('.form_element')!;
const checkboxContainer = document.createElement('div');
// Create a hidden input to store the value
const hiddenInput = document.createElement('input');
hiddenInput.type = 'hidden';
hiddenInput.name = 'mtg_days_st';
hiddenInput.id = 'mtg_days_st_hidden';
hiddenInput.value = daysDropdown.value;
// Remove old dropdown
formElement.innerHTML = '';
// Add the label back
const newLabel = document.createElement('label');
newLabel.className = 'primary_label';
newLabel.htmlFor = 'mtg_days_st_hidden';
newLabel.textContent = 'AND days';
formElement.appendChild(newLabel);
formElement.appendChild(hiddenInput);
formElement.appendChild(checkboxContainer);
setContainer(checkboxContainer);
return () => {
checkboxContainer.remove();
};
}, []);
useEffect(() => {
// Update hidden input when daysValue changes
const hiddenInput = document.getElementById('mtg_days_st_hidden') as HTMLInputElement | null;
if (hiddenInput) {
hiddenInput.value = daysValue.join('');
}
}, [daysValue]);
const handleDayChange = (position: number, checked: boolean) => {
setDaysValue(prev => prev.with(position, checked ? 1 : 0));
};
if (!container) {
return null;
}
return ReactDOM.createPortal(
<ExtensionRoot>
<ul className='text-black font-[Verdana,_"Helvetica_Neue",_Helvetica,_Arial,_sans-serif]'>
{days.map((day, index) => (
<li key={day}>
<input
type='checkbox'
id={`day_${day}`}
checked={daysValue[index] === 1}
onChange={e => {
handleDayChange(index, e.target.checked);
}}
className='form-checkbox m-[3px_3px_3px_4px]'
/>{' '}
<label htmlFor={`day_${day}`}>{day}</label>
</li>
))}
</ul>
</ExtensionRoot>,
container
);
}

View File

@@ -1,14 +1,49 @@
import Link from '@views/components/common/Link'; import Link from '@views/components/common/Link';
import Text from '@views/components/common/Text/Text'; import Text from '@views/components/common/Text/Text';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import styles from './RecruitmentBanner.module.scss'; import styles from './RecruitmentBanner.module.scss';
const DISCORD_URL = 'https://discord.gg/7pQDBGdmb7'; const DISCORD_URL = 'https://discord.gg/7pQDBGdmb7';
const GITHUB_URL = 'https://github.com/Longhorn-Developers/UT-Registration-Plus'; const GITHUB_URL = 'https://github.com/Longhorn-Developers/UT-Registration-Plus';
const DESIGNER_APPLICATION_URL =
'https://docs.google.com/forms/d/e/1FAIpQLSdX1Bb37tW6s1bkdIW3GJoTGcM_Uc-2DzFOFMXxGdn1jZ3K1A/viewform';
const RECRUIT_FROM_DEPARTMENTS = ['C S', 'ECE', 'MIS', 'CSE', 'EE', 'ITD', 'DES']; // The lists below _must_ be mutually exclusive
const DEVELOPER_RECRUIT_FROM_DEPARTMENTS = new Set(['C S', 'ECE', 'MIS', 'CSE', 'EE', 'ITD']);
const DESIGNER_RECRUIT_FROM_DEPARTMENTS = new Set(['I', 'DES', 'AET']);
type RecruitmentType = 'DEVELOPER' | 'DESIGNER' | 'NONE';
const DeveloperRecruitmentBanner = () => (
<div className={styles.container}>
<Text className='text-white'>
Interested in helping us develop UT Registration Plus? Check out our{' '}
<Link className='text-ut-orange!' href={DISCORD_URL}>
Discord Server
</Link>{' '}
and{' '}
<Link className='text-ut-orange!' href={GITHUB_URL}>
GitHub
</Link>
!
</Text>
</div>
);
const DesignerRecruitmentBanner = () => (
<div className={styles.container}>
<Text className='text-white'>
Design for thousands of UT students through Longhorn Developers on real-world projects like UT Reg.
Plus.build your portfolio and collaborate in Figma. Apply{' '}
<Link className='text-ut-orange!' href={DESIGNER_APPLICATION_URL}>
here
</Link>
!
</Text>
</div>
);
/** /**
* This adds a new column to the course catalog table header. * This adds a new column to the course catalog table header.
@@ -17,47 +52,37 @@ const RECRUIT_FROM_DEPARTMENTS = ['C S', 'ECE', 'MIS', 'CSE', 'EE', 'ITD', 'DES'
*/ */
export default function RecruitmentBanner(): JSX.Element | null { export default function RecruitmentBanner(): JSX.Element | null {
const [container, setContainer] = useState<HTMLDivElement | null>(null); const [container, setContainer] = useState<HTMLDivElement | null>(null);
const recruitmentType = useMemo<RecruitmentType>(getRecruitmentType, []);
useEffect(() => { useEffect(() => {
if (!canRecruitFrom()) { if (recruitmentType === 'NONE') {
return; return;
} }
const container = document.createElement('div'); const container = document.createElement('div');
container.setAttribute('id', 'ut-registration-plus-table-head'); container.setAttribute('id', 'ut-registration-plus-table-head');
const table = document.querySelector('table'); const table = document.querySelector('table');
table!.before(container); table!.before(container);
setContainer(container); setContainer(container);
}, []); }, [recruitmentType]);
if (!container) { if (!container || recruitmentType === 'NONE') {
return null; return null;
} }
return createPortal( return createPortal(
<div className={styles.container}> recruitmentType === 'DEVELOPER' ? <DeveloperRecruitmentBanner /> : <DesignerRecruitmentBanner />,
<Text color='white'>
Interested in helping us develop UT Registration Plus? Check out our{' '}
<Link color='white' href={DISCORD_URL}>
Discord Server
</Link>{' '}
and{' '}
<Link color='white' href={GITHUB_URL}>
GitHub
</Link>
!
</Text>
</div>,
container container
); );
} }
/** /**
* Determines if recruitment can be done from the current department. * Determines what type of recruitment can be done from the current department.
* *
* @returns True if recruitment can be done from the current department, false otherwise. * @returns 'DEVELOPER' or 'DESIGNER' if the current department recruits for that respective type, otherwise 'NONE'
*/ */
export const canRecruitFrom = (): boolean => { export const getRecruitmentType = (): RecruitmentType => {
const params = ['fos_fl', 'fos_cn']; const params = ['fos_fl', 'fos_cn'];
let department = ''; let department = '';
params.forEach(p => { params.forEach(p => {
@@ -66,8 +91,18 @@ export const canRecruitFrom = (): boolean => {
department = param; department = param;
} }
}); });
if (!department) { if (!department) {
return false; return 'NONE';
} }
return RECRUIT_FROM_DEPARTMENTS.includes(department);
if (DEVELOPER_RECRUIT_FROM_DEPARTMENTS.has(department)) {
return 'DEVELOPER';
}
if (DESIGNER_RECRUIT_FROM_DEPARTMENTS.has(department)) {
return 'DESIGNER';
}
return 'NONE';
}; };

View File

@@ -0,0 +1,795 @@
/* eslint-disable no-nested-ternary */
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import DaySelector from './DaySelector';
import DevToggles from './DevToggles';
import FullscreenButton from './FullscreenButton';
import { graphNodes } from './graphNodes';
import type { ProcessInPersonMeetings } from './Map';
import { Path } from './Path';
import { calcDirectPathStats, PathStats } from './PathStats';
import TimeWarningLabel from './TimeWarningLabel';
import type { DayCode, NodeId, NodeType } from './types';
import { DAY_MAPPING } from './types';
import { getMidpoint } from './utils';
import ZoomPanControls from './ZoomPanControls';
// Image: 783x753
const UTMapURL = new URL('/src/assets/UT-Map.svg', import.meta.url).href;
const minZoom = 0.5;
const maxZoom = 5;
const zoomStep = 1.2;
// Define zoom level thresholds for showing different levels of detail
const ZOOM_LEVELS = {
LOW: 0.8, // Show minimal buildings at this zoom level and below
MEDIUM: 1.5, // Show moderate amount of buildings
HIGH: 2.5, // Show all buildings with full details
} as const;
type SelectedBuildings = {
start: NodeId | null;
end: NodeId | null;
};
type CampusMapProps = {
processedCourses: ProcessInPersonMeetings[];
};
/**
* Component representing the campus map with interactive features.
*
* @param processedCourses - Array of processed courses.
* @returns The rendered CampusMap component.
*
* @remarks
* This component renders a map of the campus with interactive features such as:
* - Selecting buildings to create a path.
* - Displaying daily paths between sequential classes.
* - Highlighting paths with less than 15 minutes transition time.
* - Zooming and panning the map.
* - Toggling visibility of different map elements.
*
* The rendered output includes:
* - An image of the campus map.
* - An SVG overlay with paths and buildings.
* - Controls for selecting the day and displaying path information.
* - Dev controls for toggling element visibility.
* - Zoom and pan controls.
*/
export default function CampusMap({ processedCourses }: CampusMapProps): JSX.Element {
// Core state
const [selected, setSelected] = useState<SelectedBuildings>({
start: null,
end: null,
});
const [selectedDay, setSelectedDay] = useState<DayCode | null>(null);
const [hoveredPathIndex, setHoveredPathIndex] = useState<number | null>(null);
const [toggledPathIndex, setToggledPathIndex] = useState<number | null>(null);
// Dev toggle state
const [dynamicRendering, setDynamicRendering] = useState<boolean>(true);
const [showBuildings, setShowBuildings] = useState<boolean>(true);
const [showBuildingText, setShowBuildingText] = useState<boolean>(true);
const [showPrioritizedOnly, setShowPrioritizedOnly] = useState<boolean>(false);
const [showIntersections, setShowIntersections] = useState<boolean>(false);
const [showWalkways, setShowWalkways] = useState<boolean>(false);
// Zoom and pan state
const [zoomLevel, setZoomLevel] = useState<number>(1);
const [panPosition, setPanPosition] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState<boolean>(false);
const [dragStart, setDragStart] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
// Refs
const mapContainerRef = useRef<HTMLDivElement | null>(null);
// Function to calculate the current viewport in SVG coordinates
const calculateViewport = useCallback(() => {
if (!mapContainerRef.current) return null;
const container = mapContainerRef.current;
const rect = container.getBoundingClientRect();
// SVG dimensions from viewBox
const svgWidth = 783;
const svgHeight = 753;
// Calculate visible area in SVG coordinates
const scaleFactor = 1 / zoomLevel;
const visibleWidth = rect.width * scaleFactor;
const visibleHeight = rect.height * scaleFactor;
// Calculate the center point in SVG coordinates after pan
const centerX = svgWidth / 2 - panPosition.x * scaleFactor;
const centerY = svgHeight / 2 - panPosition.y * scaleFactor;
return {
left: centerX - visibleWidth / 2,
right: centerX + visibleWidth / 2,
top: centerY - visibleHeight / 2,
bottom: centerY + visibleHeight / 2,
width: visibleWidth,
height: visibleHeight,
};
}, [zoomLevel, panPosition]);
// Check if a node is in the viewport
const isNodeInViewport = useCallback(
(
node: { x: number; y: number },
viewport: {
left: number;
right: number;
top: number;
bottom: number;
} | null
) => {
if (!dynamicRendering) return true;
if (!viewport) return true;
return (
node.x >= viewport.left &&
node.x <= viewport.right &&
node.y >= viewport.top &&
node.y <= viewport.bottom
);
},
[dynamicRendering]
);
// Path calculations
const getDailyPaths = useCallback((courses: ProcessInPersonMeetings[]) => {
const sortedCourses = [...courses].sort((a, b) => a.normalizedStartTime - b.normalizedStartTime);
const paths = [];
for (let i = 0; i < sortedCourses.length - 1; i++) {
const currentCourse = sortedCourses[i];
const nextCourse = sortedCourses[i + 1];
if (currentCourse && nextCourse && currentCourse.location?.building && nextCourse.location?.building) {
paths.push({
start: currentCourse.location.building,
end: nextCourse.location.building,
startTime: currentCourse.normalizedEndTime,
endTime: nextCourse.normalizedStartTime,
colors: currentCourse.colors,
startCourseName: currentCourse.fullName,
endCourseName: nextCourse.fullName,
});
}
}
return paths;
}, []);
const relevantPaths = useMemo(() => {
if (!selectedDay) return [];
const coursesForDay = processedCourses.filter(course => course.day === DAY_MAPPING[selectedDay]);
const paths = getDailyPaths(coursesForDay);
return paths.map(path => ({
...path,
timeBetweenClasses: Math.floor(path.endTime - path.startTime),
}));
}, [selectedDay, processedCourses, getDailyPaths]);
// Memoized set of important buildings - buildings in active paths or daily routes
const importantBuildings = useMemo(() => {
const result = new Set<NodeId>();
// Add selected buildings
if (selected.start) result.add(selected.start);
if (selected.end) result.add(selected.end);
// Add buildings in the daily paths
relevantPaths?.forEach(path => {
result.add(path.start);
result.add(path.end);
});
return result;
}, [selected.start, selected.end, relevantPaths]);
// Memoized set of buildings to show based on zoom level and grid clustering
const visibleBuildings = useMemo(() => {
// Start with important buildings (selected or in active paths)
const result = new Set<NodeId>(importantBuildings);
const viewport = calculateViewport();
if (!dynamicRendering) {
Object.entries(graphNodes).forEach(([id, node]) => {
if (node.type === 'building' && isNodeInViewport(node, viewport)) {
result.add(id);
}
});
return result;
}
// If showing prioritized buildings only, return just the important ones
if (showPrioritizedOnly) {
return result;
}
// If we're zoomed in enough, show all buildings in viewport
if (zoomLevel >= ZOOM_LEVELS.HIGH) {
Object.entries(graphNodes).forEach(([id, node]) => {
if (node.type === 'building' && isNodeInViewport(node, viewport)) {
result.add(id);
}
});
return result;
}
// At medium zoom, show more buildings but still cluster them
if (zoomLevel >= ZOOM_LEVELS.MEDIUM) {
// Create a grid-based clustering with medium density
const gridSize = 40;
const grid: Record<string, NodeId[]> = {};
Object.entries(graphNodes).forEach(([id, node]) => {
if (node.type === 'building' && isNodeInViewport(node, viewport)) {
const gridX = Math.floor(node.x / gridSize);
const gridY = Math.floor(node.y / gridSize);
const gridId = `${gridX}-${gridY}`;
if (!grid[gridId]) {
grid[gridId] = [];
}
grid[gridId].push(id);
}
});
// Select one building per grid cell
Object.values(grid).forEach(buildings => {
if (buildings.length > 0) {
// Sort to ensure consistent selection
const sorted = [...buildings].sort();
if (sorted[0]) {
result.add(sorted[0]);
}
}
});
return result;
}
// At low zoom, create a sparser grid
const gridSize = 70;
const grid: Record<string, NodeId[]> = {};
Object.entries(graphNodes).forEach(([id, node]) => {
if (node.type === 'building' && isNodeInViewport(node, viewport)) {
const gridX = Math.floor(node.x / gridSize);
const gridY = Math.floor(node.y / gridSize);
const gridId = `${gridX}-${gridY}`;
if (!grid[gridId]) {
grid[gridId] = [];
}
grid[gridId].push(id);
}
});
// Select one building per grid cell
Object.values(grid).forEach(buildings => {
if (buildings.length > 0) {
// Sort to ensure consistent selection
const sorted = [...buildings].sort();
if (sorted[0]) {
result.add(sorted[0]);
}
// For grid cells with many buildings, maybe show a second one too
if (sorted.length > 3 && zoomLevel > ZOOM_LEVELS.LOW && sorted[1]) {
result.add(sorted[1]);
}
}
});
return result;
}, [importantBuildings, calculateViewport, dynamicRendering, showPrioritizedOnly, zoomLevel, isNodeInViewport]);
// Determine which intersections to show based on zoom level
const visibleIntersections = useMemo(() => {
const result = new Set<NodeId>();
const viewport = calculateViewport();
// Only process if intersections should be shown
if (!showIntersections) return result;
if (!dynamicRendering) {
Object.entries(graphNodes).forEach(([id, node]) => {
if (node.type === 'intersection' && isNodeInViewport(node, viewport)) {
result.add(id);
}
});
return result;
}
// Show all intersections at high zoom
if (zoomLevel >= ZOOM_LEVELS.HIGH) {
Object.entries(graphNodes).forEach(([id, node]) => {
if (node.type === 'intersection' && isNodeInViewport(node, viewport)) {
result.add(id);
}
});
return result;
}
// At medium zoom, show a subset
if (zoomLevel >= ZOOM_LEVELS.MEDIUM) {
Object.entries(graphNodes).forEach(([id, node]) => {
if (node.type === 'intersection' && isNodeInViewport(node, viewport)) {
// Show every 2nd intersection
const nodeIndex = parseInt(id.replace(/\D/g, '') || '0', 10);
if (nodeIndex % 2 === 0) {
result.add(id);
}
}
});
return result;
}
// At low zoom, show very few intersections
Object.entries(graphNodes).forEach(([id, node]) => {
if (node.type === 'intersection' && isNodeInViewport(node, viewport)) {
// Show only every 4th intersection
const nodeIndex = parseInt(id.replace(/\D/g, '') || '0', 10);
if (nodeIndex % 4 === 0) {
result.add(id);
}
}
});
return result;
}, [calculateViewport, dynamicRendering, showIntersections, zoomLevel, isNodeInViewport]);
// Determine which walkways to show based on zoom level
const visibleWalkways = useMemo(() => {
const result = new Set<NodeId>();
const viewport = calculateViewport();
// Only process if walkways should be shown
if (!showWalkways) return result;
if (!dynamicRendering) {
Object.entries(graphNodes).forEach(([id, node]) => {
if (node.type === 'walkway' && isNodeInViewport(node, viewport)) {
result.add(id);
}
});
return result;
}
// Show all walkways at high zoom
if (zoomLevel >= ZOOM_LEVELS.HIGH) {
Object.entries(graphNodes).forEach(([id, node]) => {
if (node.type === 'walkway' && isNodeInViewport(node, viewport)) {
result.add(id);
}
});
return result;
}
// At medium zoom, show a subset
if (zoomLevel >= ZOOM_LEVELS.MEDIUM) {
Object.entries(graphNodes).forEach(([id, node]) => {
if (node.type === 'walkway' && isNodeInViewport(node, viewport)) {
// Show every 3rd walkway
const nodeIndex = parseInt(id.replace(/\D/g, '') || '0', 10);
if (nodeIndex % 3 === 0) {
result.add(id);
}
}
});
return result;
}
// At low zoom, show very few walkways
Object.entries(graphNodes).forEach(([id, node]) => {
if (node.type === 'walkway' && isNodeInViewport(node, viewport)) {
// Show only every 5th walkway
const nodeIndex = parseInt(id.replace(/\D/g, '') || '0', 10);
if (nodeIndex % 5 === 0) {
result.add(id);
}
}
});
return result;
}, [calculateViewport, dynamicRendering, showWalkways, zoomLevel, isNodeInViewport]);
// Determine if a node should be shown based on type and zoom level
const shouldShowNode = useCallback(
(type: NodeType, id: NodeId): boolean => {
// Always show selected buildings
if (id === selected.start || id === selected.end) return true;
switch (type) {
case 'building':
return showBuildings && visibleBuildings.has(id);
case 'intersection':
return visibleIntersections.has(id);
case 'walkway':
return visibleWalkways.has(id);
default:
return false;
}
},
[showBuildings, selected, visibleBuildings, visibleIntersections, visibleWalkways]
);
// Get the appropriate node size based on zoom level with maximum cap
const getNodeSize = useCallback(
(type: NodeType): number => {
const baseSize = type === 'building' ? 6 : 4;
const minSize = baseSize * 0.8; // Minimum size at low zoom
const maxSize = baseSize * 0.5; // Maximum size cap
// If below minimum zoom level
if (zoomLevel <= ZOOM_LEVELS.LOW) {
return minSize;
}
// If above maximum zoom level, cap the size
if (zoomLevel >= ZOOM_LEVELS.HIGH) {
return maxSize;
}
// Scale size gradually between LOW and HIGH zoom levels
const zoomRatio = (zoomLevel - ZOOM_LEVELS.LOW) / (ZOOM_LEVELS.HIGH - ZOOM_LEVELS.LOW);
return minSize + zoomRatio * (maxSize - minSize);
},
[zoomLevel]
);
// Get the appropriate text size based on zoom level with maximum cap
const getTextSize = useCallback((): number => {
const minSize = 12; // Minimum text size at low zoom
const maxSize = 8; // Maximum text size cap
// If below minimum zoom level
if (zoomLevel <= ZOOM_LEVELS.LOW) {
return minSize;
}
// If above maximum zoom level, cap the size
if (zoomLevel >= ZOOM_LEVELS.HIGH) {
return maxSize;
}
// Scale text size gradually between LOW and HIGH zoom levels
const zoomRatio = (zoomLevel - ZOOM_LEVELS.LOW) / (ZOOM_LEVELS.HIGH - ZOOM_LEVELS.LOW);
return minSize + zoomRatio * (maxSize - minSize);
}, [zoomLevel]);
// Determine if text should be shown for a node
const shouldShowText = useCallback(
(type: NodeType, id: NodeId): boolean => {
// If building text is disabled in dev controls, don't show any text
if (!showBuildingText) return false;
if (type !== 'building') return false;
// Always show text for selected buildings
if (id === selected.start || id === selected.end) return true;
// Show text based on zoom level
return zoomLevel >= ZOOM_LEVELS.LOW;
},
[zoomLevel, selected, showBuildingText]
);
// Zoom and pan handlers
const handleZoomIn = useCallback(() => {
setZoomLevel(prev => Math.min(prev * zoomStep, maxZoom));
}, []);
const handleZoomOut = useCallback(() => {
setZoomLevel(prev => Math.max(prev / zoomStep, minZoom));
}, []);
const handleResetZoomPan = useCallback(() => {
setZoomLevel(1);
setPanPosition({ x: 0, y: 0 });
}, []);
const handleWheel = useCallback((e: React.WheelEvent) => {
e.preventDefault();
if (e.deltaY < 0) {
setZoomLevel(prev => Math.min(prev * zoomStep, maxZoom));
} else {
setZoomLevel(prev => Math.max(prev / zoomStep, minZoom));
}
}, []);
const handleMouseDown = useCallback((e: React.MouseEvent) => {
if (e.button !== 0) return; // Only handle left mouse button
setIsDragging(true);
setDragStart({ x: e.clientX, y: e.clientY });
}, []);
const handleMouseMove = useCallback(
(e: React.MouseEvent) => {
if (!isDragging) return;
const dx = e.clientX - dragStart.x;
const dy = e.clientY - dragStart.y;
setPanPosition(prev => ({ x: prev.x + dx, y: prev.y + dy }));
setDragStart({ x: e.clientX, y: e.clientY });
},
[isDragging, dragStart]
);
const handleMouseUp = useCallback(() => {
setIsDragging(false);
}, []);
const handleMouseLeave = useCallback(() => {
setIsDragging(false);
}, []);
// Event handlers
const handleDaySelect = useCallback((day: DayCode) => {
setSelectedDay(prevDay => (prevDay === day ? null : day));
setHoveredPathIndex(null);
setToggledPathIndex(null);
}, []);
const handleBuildingSelect = useCallback((buildingId: NodeId) => {
setSelected(prev => {
if (!prev.start) return { ...prev, start: buildingId };
if (!prev.end) return { ...prev, end: buildingId };
return { start: buildingId, end: null };
});
}, []);
const handlePathClick = useCallback((index: number) => {
setToggledPathIndex(prevIndex => (prevIndex === index ? null : index));
}, []);
const shouldShowPath = useCallback(
(index: number) => {
if (hoveredPathIndex !== null) {
return hoveredPathIndex === index;
}
if (toggledPathIndex !== null) {
return toggledPathIndex === index;
}
return true;
},
[hoveredPathIndex, toggledPathIndex]
);
// Global mouse up handler
useEffect(() => {
const handleGlobalMouseUp = () => {
setIsDragging(false);
};
window.addEventListener('mouseup', handleGlobalMouseUp);
return () => {
window.removeEventListener('mouseup', handleGlobalMouseUp);
};
}, []);
return (
<div className='relative h-full w-full overflow-hidden' ref={mapContainerRef}>
{/* Map container with zoom and pan applied */}
<div
className={`relative h-full w-full ${isDragging ? 'cursor-grabbing' : 'cursor-grab'}`}
style={{
transform: `scale(${zoomLevel}) translate(${panPosition.x / zoomLevel}px, ${panPosition.y / zoomLevel}px)`,
transformOrigin: 'center center',
transition: isDragging ? 'none' : 'transform 0.1s ease-out',
}}
onWheel={handleWheel}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onMouseLeave={handleMouseLeave}
>
{/* Map Image */}
<img src={UTMapURL} alt='UT Campus Map' className='h-full w-full object-contain' draggable={false} />
{/* SVG Overlay - ensuring it matches the image dimensions */}
<svg
className='absolute left-0 top-0 h-full w-full'
viewBox='0 0 783 753'
preserveAspectRatio='xMidYMid meet'
>
{/* Render buildings, intersections, and walkways */}
{Object.entries(graphNodes).map(
([id, node]) =>
shouldShowNode(node.type, id) && (
<g key={id}>
<circle
cx={node.x}
cy={node.y}
r={getNodeSize(node.type)}
fill={
id === selected.start
? '#579D42'
: id === selected.end
? '#D10000'
: node.type === 'building'
? '#BF5700'
: node.type === 'intersection'
? '#9CADB7'
: '#D6D2C400'
}
stroke={node.type !== 'walkway' ? 'white' : 'green'}
strokeWidth={zoomLevel < ZOOM_LEVELS.MEDIUM ? '1.5' : '2'}
className='cursor-pointer opacity-90'
onClick={() => handleBuildingSelect(id)}
/>
{node.type === 'building' && shouldShowText(node.type, id) && (
<text
x={node.x + 12}
y={node.y + 4}
fill='#000000'
fontSize={getTextSize()}
className='font-bold'
style={{
// Fade in text based on zoom level for smooth transition
opacity: zoomLevel < ZOOM_LEVELS.LOW ? zoomLevel / ZOOM_LEVELS.LOW : 1,
}}
>
{id}
</text>
)}
</g>
)
)}
{/* Render daily schedule paths */}
{relevantPaths.map(
(path, index) =>
shouldShowPath(index) && (
// eslint-disable-next-line react/no-array-index-key
<g key={`${path.start}-${path.end}-${index}`}>
<Path
startId={path.start}
endId={path.end}
graph={graphNodes}
color={path.colors?.primaryColor || '#BF5700'}
className='stroke-4 opacity-50 transition-opacity duration-300 hover:opacity-80'
/>
{path.timeBetweenClasses < 15 &&
(() => {
const midpoint = getMidpoint(path.start, path.end);
return midpoint ? (
<TimeWarningLabel
x={midpoint.x}
y={midpoint.y}
minutes={path.timeBetweenClasses}
/>
) : null;
})()}
</g>
)
)}
{/* Render user-selected path */}
{selected.start && selected.end && (
<Path
startId={selected.start}
endId={selected.end}
graph={graphNodes}
color='#BF5700'
className='opacity-75 transition-opacity duration-300 hover:opacity-100'
/>
)}
</svg>
</div>
{/* Fixed position controls that don't move with zoom/pan */}
<div className='absolute left-8 top-8 z-10 flex flex-col gap-4'>
{/* Day Selector */}
<DaySelector selectedDay={selectedDay} onDaySelect={handleDaySelect} />
{/* Zoom and Pan Controls */}
<ZoomPanControls
zoomIn={handleZoomIn}
zoomOut={handleZoomOut}
resetZoomPan={handleResetZoomPan}
zoomLevel={zoomLevel}
/>
{/* Fullscreen Button */}
<FullscreenButton containerRef={mapContainerRef} />
{/* Dev Toggles */}
<DevToggles
dynamicRendering={dynamicRendering}
showBuildings={showBuildings}
showIntersections={showIntersections}
showWalkways={showWalkways}
showBuildingText={showBuildingText}
showPrioritizedOnly={showPrioritizedOnly}
onToggleDynamicRendering={() => setDynamicRendering(prev => !prev)}
onToggleBuildings={() => setShowBuildings(prev => !prev)}
onToggleIntersections={() => setShowIntersections(prev => !prev)}
onToggleWalkways={() => setShowWalkways(prev => !prev)}
onToggleBuildingText={() => setShowBuildingText(prev => !prev)}
onTogglePrioritizedOnly={() => setShowPrioritizedOnly(prev => !prev)}
/>
</div>
{/* Path information */}
<div className='absolute right-8 top-8 z-10 max-h-[calc(100vh-120px)] flex flex-col gap-4 overflow-y-auto'>
{/* Path Statistics - show when a path is selected */}
{selected.start && selected.end && <PathStats startId={selected.start} endId={selected.end} />}
{/* Daily Paths Statistics - show when day is selected */}
{relevantPaths.length > 0 && (
<div className='rounded-md bg-white/90 p-3 shadow-sm'>
<div className='mb-2'>
<p className='text-sm font-medium'>Daily Transitions</p>
<p className='text-xs text-gray-600'>
Total time:{' '}
{relevantPaths.reduce(
(total, path) =>
total +
(calcDirectPathStats({ startId: path.start, endId: path.end })
?.walkingTimeMinutes || 0),
0
)}{' '}
min
</p>
<p className='text-xs text-gray-600'>
Total distance:{' '}
{relevantPaths.reduce(
(total, path) =>
total +
(calcDirectPathStats({ startId: path.start, endId: path.end })
?.distanceInFeet || 0),
0
)}{' '}
ft
</p>
</div>
<div className='space-y-4'>
{relevantPaths.map((path, index) => (
<div
// eslint-disable-next-line react/no-array-index-key
key={`path-info-${index}`}
className={`cursor-pointer space-y-1 text-xs transition-colors duration-200 ${
toggledPathIndex === index ? 'bg-gray-100' : ''
}`}
style={{
borderLeft: `3px solid ${path.colors?.primaryColor || '#BF5700'}`,
}}
onMouseEnter={() => setHoveredPathIndex(index)}
onMouseLeave={() => setHoveredPathIndex(null)}
onClick={() => handlePathClick(index)}
>
<p className='ml-2'>{path.startCourseName}</p>
<p className='ml-2'>
(
{
calcDirectPathStats({ startId: path.start, endId: path.end })
?.walkingTimeMinutes
}{' '}
min,{' '}
{calcDirectPathStats({ startId: path.start, endId: path.end })?.distanceInFeet}{' '}
ft)
{' - '}
{path.timeBetweenClasses} min transition
{path.timeBetweenClasses < 15 && ' ⚠️'}
</p>
<p className='ml-2'>{path.endCourseName}</p>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { Button } from '../common/Button';
import type { DayCode } from './types';
import { DAY_MAPPING } from './types';
interface DaySelectorProps {
selectedDay: DayCode | null;
onDaySelect: (day: DayCode) => void;
}
/**
* DaySelector component allows users to select a day from a list of days.
*
* @param selectedDay - The currently selected day.
* @param onDaySelect - Callback function to handle day selection.
*
* @returns The rendered DaySelector component.
*/
// const DaySelector = ({ selectedDay, onDaySelect }: DaySelectorProps): JSX.Element => (
export default function DaySelector({ selectedDay, onDaySelect }: DaySelectorProps): JSX.Element {
return (
<div className='flex gap-2 rounded-md bg-white/90 p-2 shadow-sm'>
{(Object.keys(DAY_MAPPING) as DayCode[]).map(day => (
<Button
key={day}
onClick={() => onDaySelect(day)}
color='ut-burntorange'
variant={selectedDay === day ? 'filled' : 'minimal'}
size='mini'
className='px-3 py-1'
>
{day}
</Button>
))}
</div>
);
}

View File

@@ -0,0 +1,101 @@
import React, { useState } from 'react';
interface DevTogglesProps {
dynamicRendering: boolean;
showBuildings: boolean;
showIntersections: boolean;
showWalkways: boolean;
showBuildingText: boolean;
showPrioritizedOnly: boolean;
onToggleDynamicRendering: () => void;
onToggleBuildings: () => void;
onToggleIntersections: () => void;
onToggleWalkways: () => void;
onToggleBuildingText: () => void;
onTogglePrioritizedOnly: () => void;
}
/**
* DevToggles component allows developers to toggle visibility of map elements.
*
* @param dynamicRendering - Whether to enable dynamic rendering.
* @param showBuildings - Whether to show buildings on the map.
* @param showIntersections - Whether to show intersections on the map.
* @param showWalkways - Whether to show walkways on the map.
* @param onToggleDynamicRendering - Callback function to toggle dynamic rendering.
* @param onToggleBuildings - Callback function to toggle buildings visibility.
* @param onToggleIntersections - Callback function to toggle intersections visibility.
* @param onToggleWalkways - Callback function to toggle walkways visibility.
*
* @returns The rendered DevToggles component.
*/
export default function DevToggles({
dynamicRendering,
showBuildings,
showIntersections,
showWalkways,
showBuildingText,
showPrioritizedOnly,
onToggleDynamicRendering,
onToggleBuildings,
onToggleIntersections,
onToggleWalkways,
onToggleBuildingText,
onTogglePrioritizedOnly,
}: DevTogglesProps): JSX.Element {
const [isCollapsed, setIsCollapsed] = useState<boolean>(false);
return (
<div className='flex flex-col gap-2 rounded-md bg-white/90 p-2 shadow-sm'>
<div className='flex items-center justify-between text-xs text-gray-700 font-semibold'>
<span>Dev Controls</span>
<button
onClick={() => setIsCollapsed(prev => !prev)}
className='ml-2 p-1 text-gray-500 hover:text-gray-800'
>
<svg
xmlns='http://www.w3.org/2000/svg'
width='14'
height='14'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
{isCollapsed ? <polyline points='6 9 12 15 18 9' /> : <polyline points='18 15 12 9 6 15' />}
</svg>
</button>
</div>
{!isCollapsed && (
<div className='flex flex-col gap-1'>
<label className='flex cursor-pointer items-center gap-2 text-xs'>
<input type='checkbox' checked={dynamicRendering} onChange={onToggleDynamicRendering} />
Dynamic Rendering
</label>
<label className='flex cursor-pointer items-center gap-2 text-xs'>
<input type='checkbox' checked={showBuildings} onChange={onToggleBuildings} />
Show Buildings
</label>
<label className='flex cursor-pointer items-center gap-2 text-xs'>
<input type='checkbox' checked={showBuildingText} onChange={onToggleBuildingText} />
Show Building Text
</label>
<label className='flex cursor-pointer items-center gap-2 text-xs'>
<input type='checkbox' checked={showPrioritizedOnly} onChange={onTogglePrioritizedOnly} />
Prioritized Buildings Only
</label>
<label className='flex cursor-pointer items-center gap-2 text-xs'>
<input type='checkbox' checked={showIntersections} onChange={onToggleIntersections} />
Show Intersections
</label>
<label className='flex cursor-pointer items-center gap-2 text-xs'>
<input type='checkbox' checked={showWalkways} onChange={onToggleWalkways} />
Show Walkways
</label>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,72 @@
import React, { useEffect, useState } from 'react';
import { Button } from '../common/Button';
interface FullscreenButtonProps {
containerRef: React.RefObject<HTMLDivElement>;
}
/**
* FullscreenButton component provides a toggle for fullscreen mode
*
* @param containerRef - Reference to the container element to make fullscreen.
*
* @returns The rendered FullscreenButton component.
*/
export default function FullscreenButton({ containerRef }: FullscreenButtonProps): JSX.Element {
const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener('fullscreenchange', handleFullscreenChange);
return () => {
document.removeEventListener('fullscreenchange', handleFullscreenChange);
};
}, []);
const toggleFullscreen = () => {
if (!document.fullscreenElement && containerRef.current) {
containerRef.current.requestFullscreen().catch(err => {
console.error(`Error attempting to enable fullscreen: ${err.message}`);
});
} else if (document.fullscreenElement) {
document.exitFullscreen().catch(err => {
console.error(`Error attempting to exit fullscreen: ${err.message}`);
});
}
};
return (
<div className='rounded-md bg-white/90 p-2 shadow-sm'>
<Button
onClick={toggleFullscreen}
color='ut-burntorange'
variant='minimal'
size='mini'
className='flex items-center gap-1 px-3 py-1'
>
<svg
xmlns='http://www.w3.org/2000/svg'
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
{isFullscreen ? (
<path d='M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3' />
) : (
<path d='M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3' />
)}
</svg>
{isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'}
</Button>
</div>
);
}

View File

@@ -0,0 +1,287 @@
import type { Course, StatusType } from '@shared/types/Course';
import type { CourseMeeting } from '@shared/types/CourseMeeting';
import { Button } from '@views/components/common/Button';
import Divider from '@views/components/common/Divider';
import { LargeLogo } from '@views/components/common/LogoIcon';
import Text from '@views/components/common/Text/Text';
import useChangelog from '@views/hooks/useChangelog';
import useSchedules from '@views/hooks/useSchedules';
import React, { useCallback, useEffect } from 'react';
import IconoirGitFork from '~icons/iconoir/git-fork';
import CalendarFooter from '../calendar/CalendarFooter';
import { CalendarSchedules } from '../calendar/CalendarSchedules';
import ImportantLinks from '../calendar/ImportantLinks';
import TeamLinks from '../calendar/TeamLinks';
import CampusMap from './CampusMap';
import { type DAY, DAYS } from './types';
const manifest = chrome.runtime.getManifest();
const LDIconURL = new URL('/src/assets/LD-icon.png', import.meta.url).href;
const dayToNumber = {
Monday: 0,
Tuesday: 1,
Wednesday: 2,
Thursday: 3,
Friday: 4,
Saturday: 5,
Sunday: 6,
} as const satisfies Record<string, number>;
/**
* Represents the details of an in-person meeting process.
*
* day - The day of the meeting.
* dayIndex - The index of the day in the week.
* fullName - The full name of the person.
* uid - The unique identifier of the person.
* time - The time of the meeting.
* normalizedStartTime - The normalized start time of the meeting.
* normalizedEndTime - The normalized end time of the meeting.
* startIndex - The start index of the meeting.
* endIndex - The end index of the meeting.
* location - The location of the meeting.
* status - The status of the meeting.
* colors - The colors associated with the course.
* course - The course details.
*/
export type ProcessInPersonMeetings = {
day: DAY;
dayIndex: number;
fullName: string;
uid: number;
time: string;
normalizedStartTime: number;
normalizedEndTime: number;
startIndex: number;
endIndex: number;
location: CourseMeeting['location'];
status: StatusType;
colors: Course['colors'];
course: Course;
};
/**
* Converts minutes to an index value.
* @param minutes - The number of minutes.
* @returns The index value.
*/
const convertMinutesToIndex = (minutes: number): number => Math.floor((minutes - 420) / 30);
/**
* Renders the map component for the UTRP (UT Registration Plus) extension.
*/
export default function Map(): JSX.Element {
const handleChangelogOnClick = useChangelog();
const [activeSchedule] = useSchedules();
/**
* Function to extract and format basic course information
*/
function extractCourseInfo(course: Course) {
const {
status,
schedule: { meetings },
} = course;
let courseDeptAndInstr = `${course.department} ${course.number}`;
const mainInstructor = course.instructors[0];
if (mainInstructor) {
courseDeptAndInstr += ` ${mainInstructor.toString({ format: 'first_last' })}`;
}
return { status, courseDeptAndInstr, meetings, course };
}
// /**
// * Function to process each in-person class into its distinct meeting objects for calendar grid
// */
// function processAsyncCourses({
// courseDeptAndInstr,
// status,
// course,
// }: {
// courseDeptAndInstr: string;
// status: StatusType;
// course: Course;
// }): CalendarGridCourse[] {
// return [
// {
// calendarGridPoint: {
// dayIndex: -1,
// startIndex: -1,
// endIndex: -1,
// },
// componentProps: {
// courseDeptAndInstr,
// status,
// colors: course.colors,
// },
// course,
// async: true,
// },
// ];
// }
/**
* Function to process each in-person class into its distinct meeting objects for calendar grid
*/
function processInPersonMeetings(
meeting: CourseMeeting,
courseDeptAndInstr: string,
status: StatusType,
course: Course
) {
const { days, location, startTime, endTime } = meeting;
const time = meeting.getTimeString({ separator: '-' });
const timeAndLocation = `${time}${location ? ` - ${location.building} ${location.room}` : ''}`;
const midnightIndex = 1440;
const normalizingTimeFactor = 720;
const normalizedStartTime = startTime >= midnightIndex ? startTime - normalizingTimeFactor : startTime;
const normalizedEndTime = endTime >= midnightIndex ? endTime - normalizingTimeFactor : endTime;
return days.map(day => ({
day,
dayIndex: dayToNumber[day],
// fullName: `${courseDeptAndInstr} - ${timeAndLocation}`,
fullName: `${timeAndLocation} - ${courseDeptAndInstr}`,
uid: course.uniqueId,
time,
normalizedStartTime,
normalizedEndTime,
startIndex: convertMinutesToIndex(normalizedStartTime),
endIndex: convertMinutesToIndex(normalizedEndTime),
location,
status,
colors: course.colors,
course,
}));
}
const processedCourses: ProcessInPersonMeetings[] = activeSchedule.courses.flatMap(course => {
const { status, courseDeptAndInstr, meetings } = extractCourseInfo(course);
// if (meetings.length === 0) {
// return processAsyncCourses({ courseDeptAndInstr, status, course });
// }
return meetings.flatMap(meeting =>
// if (meeting.days.includes(DAY_MAP.S) || meeting.startTime < 480) {
// return processAsyncCourses({ courseDeptAndInstr, status, course });
// }
processInPersonMeetings(meeting, courseDeptAndInstr, status, course)
);
});
const generateWeekSchedule = useCallback((): Record<DAY, string[]> => {
const weekSchedule: Record<string, string[]> = {};
processedCourses.forEach(course => {
const { day } = course;
// Add the course to the day's schedule
if (!weekSchedule[day]) weekSchedule[day] = [];
weekSchedule[day].push(course.fullName);
});
// TODO: Not the best way to do this
// currently weekSchedule is an object with keys as days and values as an array of courses
// we want to display the days in order, so we create a new object with the days in order
const orderedWeekSchedule: Record<DAY, string[]> = {
Monday: [],
Tuesday: [],
Wednesday: [],
Thursday: [],
Friday: [],
Saturday: [],
Sunday: [],
};
DAYS.forEach(day => {
if (weekSchedule[day]) {
orderedWeekSchedule[day] = weekSchedule[day];
}
});
// Sort each day based on the start time of the course
Object.entries(orderedWeekSchedule).forEach(([day, courses]) => {
orderedWeekSchedule[day as DAY] = courses.sort((courseA, courseB) => {
const courseAStartTime = processedCourses.find(
course => course.fullName === courseA
)?.normalizedStartTime;
const courseBStartTime = processedCourses.find(
course => course.fullName === courseB
)?.normalizedStartTime;
return (courseAStartTime ?? 0) - (courseBStartTime ?? 0);
});
});
return orderedWeekSchedule;
}, [processedCourses]);
useEffect(() => {
console.log('Active Schedule: ', activeSchedule);
console.log('processedCourses:', processedCourses);
console.log('generateWeekSchedule():', generateWeekSchedule());
}, [activeSchedule, processedCourses, generateWeekSchedule]);
return (
<div>
<header className='flex items-center gap-5 overflow-x-auto overflow-y-hidden border-b border-ut-offwhite px-7 py-4 md:overflow-x-hidden'>
<LargeLogo />
<Divider className='mx-2 self-center md:mx-4' size='2.5rem' orientation='vertical' />
<Text variant='h1' className='flex-1 text-ut-burntorange'>
UTRP Map
</Text>
<div className='hidden flex-row items-center justify-end gap-6 screenshot:hidden lg:flex'>
<Button variant='minimal' color='theme-black' onClick={handleChangelogOnClick}>
<IconoirGitFork className='h-6 w-6 text-ut-gray' />
<Text variant='small' className='text-ut-gray font-normal'>
v{manifest.version} - {process.env.NODE_ENV}
</Text>
</Button>
<img src={LDIconURL} alt='LD Icon' className='h-10 w-10 rounded-lg' />
</div>
</header>
<div className='h-full flex flex-row'>
<div className='h-full flex flex-none flex-col justify-between pb-5 screenshot:hidden'>
<div className='mb-3 h-full w-fit flex flex-col overflow-auto pb-2 pl-4.5 pr-4 pt-5'>
<CalendarSchedules />
<Divider orientation='horizontal' size='100%' className='my-5' />
<ImportantLinks />
<Divider orientation='horizontal' size='100%' className='my-5' />
<TeamLinks />
</div>
<CalendarFooter />
</div>
<div className='flex p-12'>
<CampusMap processedCourses={processedCourses} />
</div>
{/* Show week schedule */}
<div className='flex flex-col py-12'>
<p className='text-lg font-medium'>Week Schedule:</p>
{Object.entries(generateWeekSchedule()).map(([day, courses]) => (
<div key={day} className='flex flex-col pb-4'>
<p className='text-sm font-medium'>{day}</p>
<ul>
{courses.map(course => (
<li key={course} className='text-xs'>
{course}
</li>
))}
</ul>
</div>
))}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,61 @@
import React from 'react';
import { PathFinder } from './PathFinder';
import { type Graph, isValidNode, type NodeId } from './types';
type PathProps = {
startId: NodeId;
endId: NodeId;
graph: Graph;
color: string;
className?: string;
};
/**
* Renders a path between two nodes in a graph.
*
* @param startId - The ID of the starting node.
* @param endId - The ID of the ending node.
* @param graph - The graph object containing nodes and edges.
* @param color - The color of the path.
* @param className - Additional CSS classes for the path.
*
* @returns The rendered path as a series of SVG lines, or null if an error occurs.
*/
export const Path = ({ startId, endId, graph, color, className = '' }: PathProps): JSX.Element | null => {
try {
const pathFinder = new PathFinder(graph);
const path = pathFinder.findPath(startId, endId);
return (
<>
{path.slice(0, -1).map((nodeId, index) => {
const nextNodeId = path[index + 1];
if (!nextNodeId) return null;
const start = graph[nodeId];
const end = graph[nextNodeId];
if (!isValidNode(start) || !isValidNode(end)) return null;
return (
<line
key={`${nodeId}-${nextNodeId}`}
x1={start.x}
y1={start.y}
x2={end.x}
y2={end.y}
strokeLinecap='round'
stroke={color}
// TODO: use clsx
className={`stroke-8 ${className} opacity-50`}
/>
);
})}
</>
);
} catch (error) {
console.error('Error rendering path:', error instanceof Error ? error.message : 'Unknown error');
return null;
}
};

View File

@@ -0,0 +1,193 @@
/* eslint-disable max-classes-per-file */
import {
DIRECT_PATH_THRESHOLD,
type DistanceMap,
type Graph,
isValidNode,
type MapNode,
NEIGHBOR_DISTANCE_THRESHOLD,
type NodeId,
type PreviousMap,
} from './types';
import { calculateDistance, getNeighbors } from './utils';
/**
* Custom error class for handling pathfinding errors.
*/
export class PathFindingError extends Error {
constructor(message: string) {
super(message);
this.name = 'PathFindingError';
}
}
/**
* The `PathFinder` class is responsible for finding the shortest path between nodes in a graph.
* It uses Dijkstra's algorithm to compute the shortest path and supports bidirectional connections.
*/
export class PathFinder {
private graph: Graph;
private nodeConnections: Map<NodeId, Set<NodeId>>;
constructor(graph: Graph) {
this.graph = graph;
this.nodeConnections = this.buildNodeConnections();
}
private buildNodeConnections(): Map<NodeId, Set<NodeId>> {
const connections = new Map<NodeId, Set<NodeId>>();
// Initialize connections for each node
Object.keys(this.graph).forEach(nodeId => {
connections.set(nodeId, new Set<NodeId>());
});
// Build bidirectional connections
Object.keys(this.graph).forEach(nodeId => {
const neighbors = getNeighbors(nodeId, this.graph);
neighbors.forEach(neighbor => {
connections.get(nodeId)?.add(neighbor);
connections.get(neighbor)?.add(nodeId);
});
});
return connections;
}
public findPath(startId: NodeId, endId: NodeId): NodeId[] {
const startNode = this.graph[startId];
const endNode = this.graph[endId];
// Validate input
if (!isValidNode(startNode)) {
throw new PathFindingError(`Invalid start node: ${startId}`);
}
if (!isValidNode(endNode)) {
throw new PathFindingError(`Invalid end node: ${endId}`);
}
// Check for direct path possibility
if (this.shouldUseDirectPath(startNode, endNode)) {
return [startId, endId];
}
// Initialize Dijkstra's algorithm data structures
const distances = Object.keys(this.graph).reduce<DistanceMap>((acc, nodeId) => {
acc[nodeId] = nodeId === startId ? 0 : Infinity;
return acc;
}, {});
const previous = Object.keys(this.graph).reduce<PreviousMap>((acc, nodeId) => {
acc[nodeId] = null;
return acc;
}, {});
const unvisited = new Set(Object.keys(this.graph));
// Main Dijkstra's algorithm loop
while (unvisited.size > 0) {
const current = this.getMinDistanceNode(distances, unvisited);
if (current === null) break;
if (current === endId) break;
unvisited.delete(current);
// Use pre-computed connections
const neighbors = this.nodeConnections.get(current) ?? new Set<NodeId>();
neighbors.forEach(neighbor => {
if (!unvisited.has(neighbor)) return;
const currentNode = this.graph[current];
const neighborNode = this.graph[neighbor];
if (!isValidNode(currentNode) || !isValidNode(neighborNode)) return;
const distance = calculateDistance(currentNode, neighborNode);
const totalDistance = distances[current]! + distance;
if (totalDistance < distances[neighbor]!) {
distances[neighbor] = totalDistance;
previous[neighbor] = current;
}
});
}
// Reconstruct and validate path
const path = this.reconstructPath(previous, startId, endId);
// Verify path distance is reasonable
const totalPathDistance = this.calculatePathDistance(path);
if (totalPathDistance > NEIGHBOR_DISTANCE_THRESHOLD * path.length) {
console.warn(`Long path detected (${totalPathDistance.toFixed(2)} units) from ${startId} to ${endId}`);
}
return path;
}
private calculatePathDistance(path: NodeId[]): number {
let totalDistance = 0;
for (let i = 0; i < path.length - 1; i++) {
const currentNode = this.graph[path[i]!];
const nextNode = this.graph[path[i + 1]!];
if (isValidNode(currentNode) && isValidNode(nextNode)) {
totalDistance += calculateDistance(currentNode, nextNode);
}
}
return totalDistance;
}
private shouldUseDirectPath(start: MapNode, end: MapNode): boolean {
const distance = calculateDistance(start, end);
return distance <= DIRECT_PATH_THRESHOLD;
}
private getMinDistanceNode(distances: DistanceMap, unvisited: Set<NodeId>): NodeId | null {
let minDistance = Infinity;
let minNode: NodeId | null = null;
unvisited.forEach(nodeId => {
const distance = distances[nodeId] ?? Infinity;
if (distance < minDistance) {
minDistance = distance;
minNode = nodeId;
}
});
return minNode;
}
private reconstructPath(previous: PreviousMap, startId: NodeId, endId: NodeId): NodeId[] {
const path: NodeId[] = [];
let currentNode: NodeId | null = endId;
// Keep track of visited nodes to prevent infinite loops
const visited = new Set<NodeId>();
while (currentNode !== null) {
// Prevent infinite loops
if (visited.has(currentNode)) {
throw new PathFindingError('Circular path detected during reconstruction');
}
visited.add(currentNode);
path.unshift(currentNode);
const prevNode: NodeId | null = previous[currentNode] ?? null;
// If we can't find the previous node and we haven't reached the start,
// then the path is broken
if (prevNode === undefined) {
throw new PathFindingError('Path reconstruction failed: broken path chain');
}
currentNode = prevNode;
}
// Verify that we actually found a path to the start
if (path[0] !== startId) {
throw new PathFindingError('No valid path found between the specified nodes');
}
return path;
}
}

View File

@@ -0,0 +1,86 @@
import React from 'react';
import { graphNodes } from './graphNodes';
import { isValidNode, PIXELS_TO_FEET, WALKING_SPEED } from './types';
type PathStatsProps = {
startId: string;
endId: string;
};
/**
* Calculates the direct path statistics between two nodes.
*
* @param startId - The ID of the starting node.
* @param endId - The ID of the ending node.
*
* @returns The distance in feet and walking time in minutes between the two nodes.
*/
export const calcDirectPathStats = ({ startId, endId }: PathStatsProps) => {
const startNode = graphNodes[startId];
const endNode = graphNodes[endId];
if (!isValidNode(startNode) || !isValidNode(endNode)) {
return null;
}
// Calculate distance in pixels
const distanceInPixels = Math.sqrt((endNode.x - startNode.x) ** 2 + (endNode.y - startNode.y) ** 2);
// Convert to feet and calculate time
const distanceInFeet = Math.round(distanceInPixels * PIXELS_TO_FEET);
const walkingTimeMinutes = Math.ceil(distanceInFeet / WALKING_SPEED);
return {
distanceInFeet,
walkingTimeMinutes,
};
};
/**
* Component to display statistics about a path between two nodes on a map.
*
* @param startId - The ID of the starting node.
* @param endId - The ID of the ending node.
*
* @returns A JSX element displaying the path statistics, or null if the nodes are invalid.
*/
export const PathStats = ({ startId, endId }: PathStatsProps): JSX.Element | null => {
const startNode = graphNodes[startId];
const endNode = graphNodes[endId];
if (!isValidNode(startNode) || !isValidNode(endNode)) {
return null;
}
// Calculate distance in pixels
const distanceInPixels = Math.sqrt((endNode.x - startNode.x) ** 2 + (endNode.y - startNode.y) ** 2);
// Convert to feet and calculate time
const distanceInFeet = Math.round(distanceInPixels * PIXELS_TO_FEET);
const walkingTimeMinutes = Math.ceil(distanceInFeet / WALKING_SPEED);
return (
<div className='rounded-md bg-white/90 p-3 shadow-sm space-y-2'>
<h3 className='text-sm font-medium'>Path Statistics</h3>
<div className='space-y-1'>
<div className='flex justify-between text-xs'>
<span>Distance:</span>
<span className='font-medium'>{distanceInFeet} ft</span>
</div>
<div className='flex justify-between text-xs'>
<span>Walking Time:</span>
<span className='font-medium'>{walkingTimeMinutes} min</span>
</div>
<div className='flex justify-between text-xs'>
<span>From:</span>
<span className='font-medium'>{startId}</span>
</div>
<div className='flex justify-between text-xs'>
<span>To:</span>
<span className='font-medium'>{endId}</span>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,36 @@
import React from 'react';
interface TimeWarningLabelProps {
x: number;
y: number;
minutes: number;
}
/**
* TimeWarningLabel component that renders a warning label on a map.
* The label consists of a circle with a text inside it, indicating the number of minutes.
*
* @param x - The x-coordinate for the center of the circle.
* @param y - The y-coordinate for the center of the circle.
* @param minutes - The number of minutes to display inside the circle.
*
* @returns A JSX element representing the warning label.
*/
export default function TimeWarningLabel({ x, y, minutes }: TimeWarningLabelProps): JSX.Element {
return (
<g>
<circle cx={x} cy={y} r={12} fill='white' stroke='#FF4444' strokeWidth={2} />
<text
x={x}
y={y}
textAnchor='middle'
dominantBaseline='middle'
fill='#FF4444'
fontSize='10'
fontWeight='bold'
>
{minutes}&apos;
</text>
</g>
);
}

View File

@@ -0,0 +1,84 @@
import React from 'react';
import { Button } from '../common/Button';
interface ZoomPanControlsProps {
zoomIn: () => void;
zoomOut: () => void;
resetZoomPan: () => void;
zoomLevel: number;
}
/**
* ZoomPanControls component provides buttons for zooming and panning.
*
* @param zoomIn - Function to zoom in the map.
* @param zoomOut - Function to zoom out the map.
* @param resetZoomPan - Function to reset zoom and pan to default.
* @param zoomLevel - Current zoom level.
*
* @returns The rendered ZoomPanControls component.
*/
export default function ZoomPanControls({
zoomIn,
zoomOut,
resetZoomPan,
zoomLevel,
}: ZoomPanControlsProps): JSX.Element {
return (
<div className='flex gap-2 rounded-md bg-white/90 p-2 shadow-sm'>
<Button onClick={zoomIn} color='ut-burntorange' variant='minimal' size='mini' className='px-3 py-1'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<circle cx='11' cy='11' r='8' />
<line x1='21' y1='21' x2='16.65' y2='16.65' />
<line x1='11' y1='8' x2='11' y2='14' />
<line x1='8' y1='11' x2='14' y2='11' />
</svg>
</Button>
<Button onClick={zoomOut} color='ut-burntorange' variant='minimal' size='mini' className='px-3 py-1'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<circle cx='11' cy='11' r='8' />
<line x1='21' y1='21' x2='16.65' y2='16.65' />
<line x1='8' y1='11' x2='14' y2='11' />
</svg>
</Button>
<Button onClick={resetZoomPan} color='ut-burntorange' variant='minimal' size='mini' className='px-3 py-1'>
<svg
xmlns='http://www.w3.org/2000/svg'
width='16'
height='16'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
>
<path d='M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z' />
<polyline points='9 22 9 12 15 12 15 22' />
</svg>
</Button>
<span className='flex items-center text-xs font-medium'>{Math.round(zoomLevel * 100)}%</span>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,124 @@
// Constants
export const WALKING_SPEED = 246.4; // ~2.8 mph in feet per minute
export const PIXELS_TO_FEET = 9.3895; // Approximate scale factor
export const DIRECT_PATH_THRESHOLD = 50; // Units for direct path calculation
export const NEIGHBOR_DISTANCE_THRESHOLD = 25; // Increased threshold for neighbor connections
/**
* Represents the type of a node in the map.
*
* - 'building': A node representing a building.
* - 'intersection': A node representing an intersection.
* - 'walkway': A node representing a walkway.
*/
export type NodeType = 'building' | 'intersection' | 'walkway';
/**
* Represents the coordinates of a node on a map.
*
* @typeparam x - The x-coordinate of the node.
* @typeparam y - The y-coordinate of the node.
*/
export type NodeCoordinates = {
x: number;
y: number;
};
/**
* Represents a map node with specific coordinates and a type.
*
* @typeparam type - The type of the node.
*/
export type MapNode = NodeCoordinates & {
type: NodeType;
};
/**
* Represents a graph structure where each key is a string identifier
* and the value is a MapNode. This type is used to define the overall
* structure of a graph in the application.
*/
export type Graph = Record<string, MapNode>;
/**
* Represents a distance measurement.
*/
export type Distance = number;
/**
* Represents the unique identifier for a node in the map.
*/
export type NodeId = string;
/**
* A map that associates a node identifier with a distance.
*
* @typeparam NodeId - The unique identifier for a node.
* @typeparam Distance - The distance associated with the node.
*/
export type DistanceMap = Record<NodeId, Distance>;
/**
* A type representing a mapping of node identifiers to their previous node identifiers.
*
* @typeparam NodeId - The identifier of the current node.
* @typeparam NodeId - The identifier of the previous node, or null if there is no previous node.
*/
export type PreviousMap = Record<NodeId, NodeId | null>;
/**
* type guard to check if the given node is a valid MapNode.
*
* A valid MapNode is defined as:
* - Not undefined
* - Has numeric `x` and `y` properties
* - Has a `type` property that is either 'building', 'intersection', or 'walkway'
*
* @param node - The node to validate.
* @returns True if the node is a valid MapNode, false otherwise.
*/
export const isValidNode = (node: MapNode | undefined): node is MapNode =>
node !== undefined &&
typeof node.x === 'number' &&
typeof node.y === 'number' &&
(node.type === 'building' || node.type === 'intersection' || node.type === 'walkway');
/**
* Represents the code for a day of the week.
*
* - 'M' : Monday
* - 'T' : Tuesday
* - 'W' : Wednesday
* - 'TH' : Thursday
* - 'F' : Friday
*/
export type DayCode = 'M' | 'T' | 'W' | 'TH' | 'F';
/**
* An array of strings representing the days of the week.
* The days are ordered starting from Monday to Sunday.
*/
export const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] as const;
/**
* Represents a day of the week.
*
* @remarks
* This type is derived from the `DAYS` array, representing one of its elements.
* It is used to ensure that only valid days of the week are assigned to variables of this type.
*/
export type DAY = (typeof DAYS)[number];
type DayMapping = Record<DayCode, DAY>;
/**
* A constant object that maps single-letter day abbreviations to their full names.
*/
export const DAY_MAPPING = {
M: 'Monday',
T: 'Tuesday',
W: 'Wednesday',
TH: 'Thursday',
F: 'Friday',
} as const satisfies DayMapping;

View File

@@ -0,0 +1,111 @@
import { graphNodes } from './graphNodes';
import type { Graph, MapNode, NodeCoordinates, NodeId } from './types';
import { isValidNode, NEIGHBOR_DISTANCE_THRESHOLD } from './types';
/**
* Calculates the Euclidean distance between two points.
*
* @param point1 - The coordinates of the first point.
* @param point2 - The coordinates of the second point.
* @returns The distance between the two points.
*/
export const calculateDistance = (point1: NodeCoordinates, point2: NodeCoordinates): number =>
Math.sqrt((point2.x - point1.x) ** 2 + (point2.y - point1.y) ** 2);
/**
* Finds the nearest nodes to a given node in a graph.
*
* @param nodeId - The ID of the node for which to find the nearest nodes.
* @param graph - The graph containing all nodes.
* @returns An array of node IDs representing the nearest nodes.
*
* The function first checks if the current node is valid. If not, it returns an empty array.
* It then calculates the distances to all other valid nodes in the graph.
* The nodes are sorted by distance, and the function first attempts to connect to the nearest intersections.
* If no intersections are found, it connects to the nearest buildings.
*/
const findNearestNodes = (nodeId: NodeId, graph: Graph): NodeId[] => {
const currentNode = graph[nodeId];
if (!isValidNode(currentNode)) return [];
// Calculate distances to all other nodes
const distances = Object.entries(graph)
.filter((entry): entry is [string, MapNode] => {
const [id, node] = entry;
return id !== nodeId && isValidNode(node);
})
.map(([id, node]) => ({
id,
distance: calculateDistance(currentNode, node),
isIntersection: node.type === 'intersection',
}))
.sort((a, b) => a.distance - b.distance);
// First try to connect to nearest intersections
const nearestIntersections = distances
.filter(node => node.isIntersection)
.slice(0, 2)
.map(node => node.id);
if (nearestIntersections.length > 0) {
return nearestIntersections;
}
// If no intersections found, connect to nearest buildings
return distances.slice(0, 2).map(node => node.id);
};
/**
* Retrieves the neighboring nodes of a given node within a graph.
*
* @param nodeId - The ID of the node for which neighbors are to be found.
* @param graph - The graph containing all nodes and their connections.
* @returns An array of node IDs representing the neighbors of the given node.
*
* This function first checks if the current node is valid. If not, it returns an empty array.
* It then filters the graph to find all valid neighboring nodes within a specified distance threshold.
* If no direct neighbors are found, it attempts to connect to the nearest intersection or building.
*/
export const getNeighbors = (nodeId: NodeId, graph: Graph): NodeId[] => {
const currentNode = graph[nodeId];
if (!isValidNode(currentNode)) return [];
// Get all possible neighbors within the increased threshold
const neighbors = Object.entries(graph)
.filter((entry): entry is [string, MapNode] => {
const [id, node] = entry;
if (!isValidNode(node) || id === nodeId) return false;
const distance = calculateDistance(currentNode, node);
return distance < NEIGHBOR_DISTANCE_THRESHOLD;
})
.map(([id]) => id);
// If no direct neighbors found, connect to the nearest intersection or building
if (neighbors.length === 0) {
const nearestNodes = findNearestNodes(nodeId, graph);
return nearestNodes;
}
return neighbors;
};
/**
* Calculates the midpoint between two nodes identified by their IDs.
*
* @param startId - The ID of the starting node.
* @param endId - The ID of the ending node.
* @returns An object containing the x and y coordinates of the midpoint, or null if either node is invalid.
*/
export const getMidpoint = (startId: string, endId: string) => {
const startNode = graphNodes[startId];
const endNode = graphNodes[endId];
if (!isValidNode(startNode) || !isValidNode(endNode)) {
return null;
}
return {
x: (startNode.x + endNode.x) / 2,
y: (startNode.y + endNode.y) / 2,
};
};

View File

@@ -5,8 +5,11 @@ import exportSchedule from '@pages/background/lib/exportSchedule';
import importSchedule from '@pages/background/lib/importSchedule'; import importSchedule from '@pages/background/lib/importSchedule';
import { CalendarDots, Trash } from '@phosphor-icons/react'; import { CalendarDots, Trash } from '@phosphor-icons/react';
import { background } from '@shared/messages'; import { background } from '@shared/messages';
import { DevStore } from '@shared/storage/DevStore';
import { initSettings, OptionsStore } from '@shared/storage/OptionsStore'; import { initSettings, OptionsStore } from '@shared/storage/OptionsStore';
import { UserScheduleStore } from '@shared/storage/UserScheduleStore'; import { UserScheduleStore } from '@shared/storage/UserScheduleStore';
import { CRX_PAGES } from '@shared/types/CRXPages';
import MIMEType from '@shared/types/MIMEType';
import { downloadBlob } from '@shared/util/downloadBlob'; import { downloadBlob } from '@shared/util/downloadBlob';
// import { addCourseByUrl } from '@shared/util/courseUtils'; // import { addCourseByUrl } from '@shared/util/courseUtils';
// import { getCourseColors } from '@shared/util/colors'; // import { getCourseColors } from '@shared/util/colors';
@@ -36,14 +39,14 @@ import { useMigrationDialog } from '../common/MigrationDialog';
import DevMode from './DevMode'; import DevMode from './DevMode';
import Preview from './Preview'; import Preview from './Preview';
const DISPLAY_PREVIEWS = false;
const PREVIEW_SECTION_DIV_CLASSNAME = DISPLAY_PREVIEWS ? 'w-1/2 space-y-4' : 'flex-grow space-y-4';
const manifest = chrome.runtime.getManifest(); const manifest = chrome.runtime.getManifest();
const gitHubStatsService = new GitHubStatsService(); const gitHubStatsService = new GitHubStatsService();
const includeMergedPRs = false; const includeMergedPRs = false;
const DISPLAY_PREVIEWS = false;
const PREVIEW_SECTION_DIV_CLASSNAME = DISPLAY_PREVIEWS ? 'w-1/2 space-y-4' : 'flex-grow space-y-4';
/** /**
* Custom hook for enabling developer mode. * Custom hook for enabling developer mode.
* *
@@ -103,6 +106,8 @@ export default function Settings(): JSX.Element {
const [activeSchedule] = useSchedules(); const [activeSchedule] = useSchedules();
// const [isRefreshing, setIsRefreshing] = useState<boolean>(false); // const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
const [isDeveloper, setIsDeveloper] = useState<boolean>(false);
const showDialog = usePrompt(); const showDialog = usePrompt();
const handleChangelogOnClick = useChangelog(); const handleChangelogOnClick = useChangelog();
@@ -132,6 +137,16 @@ export default function Settings(): JSX.Element {
setCalendarNewTab(alwaysOpenCalendarInNewTab); setCalendarNewTab(alwaysOpenCalendarInNewTab);
}; };
const initDS = async () => {
const isDev = await DevStore.get('isDeveloper');
setIsDeveloper(isDev);
};
const ds_l1 = DevStore.listen('isDeveloper', async ({ newValue }) => {
setIsDeveloper(newValue);
});
initDS();
fetchGitHubStats(); fetchGitHubStats();
initAndSetSettings(); initAndSetSettings();
@@ -182,6 +197,8 @@ export default function Settings(): JSX.Element {
OptionsStore.removeListener(l4); OptionsStore.removeListener(l4);
OptionsStore.removeListener(l5); OptionsStore.removeListener(l5);
DevStore.removeListener(ds_l1);
window.removeEventListener('keydown', handleKeyPress); window.removeEventListener('keydown', handleKeyPress);
}; };
}, []); }, []);
@@ -255,6 +272,7 @@ export default function Settings(): JSX.Element {
const [devMode, toggleDevMode] = useDevMode(10); const [devMode, toggleDevMode] = useDevMode(10);
if (devMode) { if (devMode) {
DevStore.set('isDeveloper', true);
return <DevMode />; return <DevMode />;
} }
@@ -397,7 +415,12 @@ export default function Settings(): JSX.Element {
</Text> </Text>
<p className='text-sm text-gray-600'>Import from a schedule file</p> <p className='text-sm text-gray-600'>Import from a schedule file</p>
</div> </div>
<FileUpload variant='filled' color='ut-burntorange' onChange={handleImportClick}> <FileUpload
variant='filled'
color='ut-burntorange'
onChange={handleImportClick}
accept={MIMEType.JSON}
>
Import Schedule Import Schedule
</FileUpload> </FileUpload>
</div> </div>
@@ -501,6 +524,64 @@ export default function Settings(): JSX.Element {
<h2 className='mb-4 text-xl text-ut-black font-semibold' onClick={toggleDevMode}> <h2 className='mb-4 text-xl text-ut-black font-semibold' onClick={toggleDevMode}>
Developer Mode Developer Mode
</h2> </h2>
<div className='flex items-center justify-between'>
<div className='max-w-xs'>
<Text variant='h4' className='text-ut-burntorange font-semibold'>
UTRP Map
</Text>
<span className='mx-2 border border-ut-burntorange rounded px-2 py-0.5 text-xs text-ut-burntorange font-medium'>
BETA
</span>
<p className='text-sm text-gray-600'>
Navigate campus efficiently with our interactive map tool that integrates with your
schedule
</p>
</div>
<Button
variant='outline'
color='ut-burntorange'
onClick={() => {
const mapPageUrl = chrome.runtime.getURL(CRX_PAGES.MAP);
background.openNewTab({ url: mapPageUrl });
}}
>
Try UTRP Map
</Button>
</div>
{isDeveloper && (
<>
<Divider size='auto' orientation='horizontal' />
<div className='flex items-center justify-between'>
<div className='max-w-xs'>
<Text variant='h4' className='text-ut-burntorange font-semibold'>
Debug Page
</Text>
<span className='mx-2 border border-ut-gray rounded px-2 py-0.5 text-xs text-ut-gray font-medium'>
DEV
</span>
<p className='text-sm text-gray-600'>
Open the developer debug page to view extension storage and debug logs
</p>
</div>
<Button
variant='outline'
color='ut-burntorange'
onClick={() => {
const debugPageUrl = chrome.runtime.getURL(CRX_PAGES.DEBUG);
background.openNewTab({ url: debugPageUrl });
}}
>
Open Debug Page
</Button>
</div>
</>
)}
<Divider size='auto' orientation='horizontal' />
<Button variant='filled' color='ut-black' onClick={() => addCourseByURL(activeSchedule)}> <Button variant='filled' color='ut-black' onClick={() => addCourseByURL(activeSchedule)}>
Add course by link Add course by link
</Button> </Button>
@@ -514,7 +595,9 @@ export default function Settings(): JSX.Element {
<section className='my-8 lg:my-0 lg:ml-4 lg:w-1/2'> <section className='my-8 lg:my-0 lg:ml-4 lg:w-1/2'>
<section> <section>
<h2 className='mb-4 text-xl text-ut-black font-semibold'>LONGHORN DEVELOPERS ADMINS</h2> <h2 className='mb-4 text-xl text-ut-black font-semibold'>
LONGHORN DEVELOPERS (LHD) EXECUTIVE BOARD
</h2>
<div className='grid grid-cols-2 gap-4 2xl:grid-cols-4 md:grid-cols-3'> <div className='grid grid-cols-2 gap-4 2xl:grid-cols-4 md:grid-cols-3'>
{LONGHORN_DEVELOPERS_ADMINS.map(admin => ( {LONGHORN_DEVELOPERS_ADMINS.map(admin => (
<div <div
@@ -530,7 +613,11 @@ export default function Settings(): JSX.Element {
> >
{admin.name} {admin.name}
</Text> </Text>
<p className='text-sm text-gray-600'>{admin.role}</p> {admin.role.map(role => (
<p key={admin.githubUsername} className='text-sm text-gray-600'>
{role}
</p>
))}
{showGitHubStats && githubStats && ( {showGitHubStats && githubStats && (
<div className='mt-2'> <div className='mt-2'>
<p className='text-xs text-gray-500'>GitHub Stats (UTRP repo):</p> <p className='text-xs text-gray-500'>GitHub Stats (UTRP repo):</p>
@@ -576,7 +663,11 @@ export default function Settings(): JSX.Element {
> >
{swe.name} {swe.name}
</Text> </Text>
<p className='text-sm text-gray-600'>{swe.role}</p> {swe.role.map(role => (
<p key={swe.githubUsername} className='text-sm text-gray-600'>
{role}
</p>
))}
{showGitHubStats && githubStats && ( {showGitHubStats && githubStats && (
<div className='mt-2'> <div className='mt-2'>
<p className='text-xs text-gray-500'>GitHub Stats (UTRP repo):</p> <p className='text-xs text-gray-500'>GitHub Stats (UTRP repo):</p>

View File

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

View File

@@ -21,19 +21,19 @@ export function useEnforceScheduleLimit(): () => boolean {
return useCallback(() => { return useCallback(() => {
if (schedules.length >= SCHEDULE_LIMIT) { if (schedules.length >= SCHEDULE_LIMIT) {
showDialog({ showDialog({
title: `You have ${SCHEDULE_LIMIT} active schedules!`, title: `You have too many schedules!`,
description: ( description: (
<> <>
To encourage organization,{' '} To encourage organization,{' '}
<span className='text-ut-burntorange'>please consider removing some unused schedules</span> you <span className='text-ut-burntorange'>please consider deleting any unused schedules</span> you
may have. may have.
</> </>
), ),
buttons: close => ( buttons: close => (
<Button variant='filled' color='ut-burntorange' onClick={close}> <Button variant='filled' color='ut-burntorange' onClick={close}>
I Understand I understand
</Button> </Button>
), ),
}); });

View File

@@ -36,6 +36,7 @@ export interface CalendarGridCourse {
gridColumnStart?: number; gridColumnStart?: number;
gridColumnEnd?: number; gridColumnEnd?: number;
totalColumns?: number; totalColumns?: number;
concurrentCells?: CalendarGridCourse[];
} }
/** /**
@@ -52,7 +53,9 @@ export interface FlattenedCourseSchedule {
* @param minutes - The number of minutes. * @param minutes - The number of minutes.
* @returns The index value. * @returns The index value.
*/ */
export const convertMinutesToIndex = (minutes: number): number => Math.floor((minutes - 420) / 30); export const convertMinutesToIndex = (minutes: number): number =>
// 480 = 8 a.m., 30 = 30 minute slots, 2 header rows, and grid rows start at 1
Math.floor((minutes - 480) / 30) + 2 + 1;
/** /**
* Get the active schedule, and convert it to be render-able into a calendar. * Get the active schedule, and convert it to be render-able into a calendar.

View File

@@ -0,0 +1,55 @@
import { UT_DINING_APP_STORE_URL } from '@shared/util/appUrls';
import { Button } from '@views/components/common/Button';
import Text from '@views/components/common/Text/Text';
import WhatsNewPopupContent from '@views/components/common/WhatsNewPopup';
import { useDialog } from '@views/contexts/DialogContext';
import React from 'react';
import { LogoIcon } from '../components/common/LogoIcon';
import useChangelog from './useChangelog';
const LDIconURL = new URL('/src/assets/LD-icon-new.png', import.meta.url).href;
/**
* Custom hook that provides a function to display a what's new dialog.
*
* @returns A function that, when called, shows a dialog with the changelog.
*/
export default function useWhatsNewPopUp(): () => void {
const showDialog = useDialog();
const showChangeLog = useChangelog();
const { version } = chrome.runtime.getManifest();
const showPopUp = () => {
showDialog(close => ({
className: 'w-[830px] flex flex-col items-center gap-spacing-7 p-spacing-8',
title: (
<div className='flex items-center justify-between gap-4'>
<img src={LDIconURL} alt='LD Icon' className='h-12 w-12 rounded-lg' />
<Text variant='h1' className='text-theme-black'>
Download our new UT Dining app!
</Text>
</div>
),
description: <WhatsNewPopupContent />,
buttons: (
<div className='flex flex-row items-end gap-spacing-4'>
<Button
onClick={() => {
window.open(UT_DINING_APP_STORE_URL, '_blank');
}}
variant='minimal'
color='ut-black'
>
Download UT Dining on iOS
</Button>
<Button onClick={close} color='ut-burntorange'>
Close
</Button>
</div>
),
}));
};
return showPopUp;
}

View File

@@ -5,7 +5,7 @@ import type { CachedData } from '@shared/types/CachedData';
// Types // Types
type TeamMember = { type TeamMember = {
name: string; name: string;
role: string; role: string[];
githubUsername: string; githubUsername: string;
}; };
@@ -40,21 +40,38 @@ const REPO_NAME = 'UT-Registration-Plus';
const CONTRIBUTORS_API_ROUTE = `/repos/${REPO_OWNER}/${REPO_NAME}/stats/contributors`; const CONTRIBUTORS_API_ROUTE = `/repos/${REPO_OWNER}/${REPO_NAME}/stats/contributors`;
export const LONGHORN_DEVELOPERS_ADMINS = [ export const LONGHORN_DEVELOPERS_ADMINS = [
{ name: 'Sriram Hariharan', role: 'Founder', githubUsername: 'sghsri' }, { name: 'Sriram Hariharan', role: ['LHD Co-Founder', 'UTRP Founder'], githubUsername: 'sghsri' },
{ name: 'Elie Soloveichik', role: 'Staff Engineer', githubUsername: 'Razboy20' }, {
{ name: 'Diego Perez', role: 'Staff Engineer', githubUsername: 'doprz' }, name: 'Elie Soloveichik',
{ name: 'Lukas Zenick', role: 'Senior Software Engineer', githubUsername: 'Lukas-Zenick' }, role: ['LHD Co-Founder', 'Learning and Development Director', 'UTRP Senior SWE'],
{ name: 'Isaiah Rodriguez', role: 'Chief Operations and Design Officer', githubUsername: 'IsaDavRod' }, githubUsername: 'Razboy20',
{ name: 'Samuel Gunter', role: 'Senior Software Engineer', githubUsername: 'Samathingamajig' }, },
{ name: 'Derek Chen', role: 'Senior Software Engineer', githubUsername: 'DereC4' }, {
name: 'Diego Perez',
role: ['LHD Co-Founder', 'Software Engineering Director', 'UTRP Senior SWE'],
githubUsername: 'doprz',
},
{ name: 'Isaiah Rodriguez', role: ['LHD Co-Founder', 'President and UI/UX Director'], githubUsername: 'IsaDavRod' },
{
name: 'Samuel Gunter',
role: ['Administrative Director', 'UTRP Co-Lead', 'UTRP Senior SWE'],
githubUsername: 'Samathingamajig',
},
{
name: 'Derek Chen',
role: ['Communications Director', 'UTRP Co-Lead', 'UTRP Senior SWE'],
githubUsername: 'DereC4',
},
{ name: 'Kabir Ramzan', role: ['Events Director'], githubUsername: 'CMEONE' },
] as const satisfies TeamMember[]; ] as const satisfies TeamMember[];
export const LONGHORN_DEVELOPERS_SWE = [ export const LONGHORN_DEVELOPERS_SWE = [
{ name: 'Preston Cook', role: 'Software Engineer', githubUsername: 'Preston-Cook' }, { name: 'Preston Cook', role: ['Software Engineer'], githubUsername: 'Preston-Cook' },
{ name: 'Ethan Lanting', role: 'Software Engineer', githubUsername: 'EthanL06' }, { name: 'Ethan Lanting', role: ['Software Engineer'], githubUsername: 'EthanL06' },
{ name: 'Casey Charleston', role: 'Software Engineer', githubUsername: 'caseycharleston' }, { name: 'Casey Charleston', role: ['Software Engineer'], githubUsername: 'caseycharleston' },
{ name: 'Vinson', role: 'Software Engineer', githubUsername: 'vinsonzheng499' }, { name: 'Lukas Zenick', role: ['LHD Alumni', 'Senior Software Engineer'], githubUsername: 'Lukas-Zenick' },
{ name: 'Vivek', role: 'Software Engineer', githubUsername: 'vivek12311' }, { name: 'Vinson', role: ['LHD Alumni', 'Software Engineer'], githubUsername: 'vinsonzheng499' },
{ name: 'Vivek', role: ['LHD Alumni', 'Software Engineer'], githubUsername: 'vivek12311' },
] as const satisfies TeamMember[]; ] as const satisfies TeamMember[];
/** /**

View File

@@ -13,6 +13,7 @@ export const SiteSupport = {
MY_CALENDAR: 'MY_CALENDAR', MY_CALENDAR: 'MY_CALENDAR',
REPORT_ISSUE: 'REPORT_ISSUE', REPORT_ISSUE: 'REPORT_ISSUE',
MY_UT: 'MY_UT', MY_UT: 'MY_UT',
COURSE_CATALOG_SEARCH: 'COURSE_CATALOG_SEARCH',
CLASSLIST: 'CLASSLIST', CLASSLIST: 'CLASSLIST',
} as const; } as const;
@@ -45,6 +46,7 @@ export default function getSiteSupport(url: string): SiteSupportType | null {
if (document.querySelector('#details')) { if (document.querySelector('#details')) {
return SiteSupport.COURSE_CATALOG_DETAILS; return SiteSupport.COURSE_CATALOG_DETAILS;
} }
return SiteSupport.COURSE_CATALOG_SEARCH;
} }
if (url.includes('utdirect.utexas.edu') && (url.includes('waitlist') || url.includes('classlist'))) { if (url.includes('utdirect.utexas.edu') && (url.includes('waitlist') || url.includes('classlist'))) {
return SiteSupport.WAITLIST; return SiteSupport.WAITLIST;

View File

@@ -26,7 +26,7 @@ export default defineConfig({
], ],
shortcuts: { shortcuts: {
focusable: 'outline-none ring-blue-500/50 dark:ring-blue-400/60 ring-0 focus-visible:ring-4', focusable: 'outline-none ring-blue-500/50 dark:ring-blue-400/60 ring-0 focus-visible:ring-4',
btn: 'h-10 w-auto flex cursor-pointer justify-center items-center gap-spacing-3 rounded-1 px-spacing-5 py-0 text-4.5 btn-transition disabled:(cursor-not-allowed opacity-50) active:enabled:scale-96 focusable', btn: 'h-10 w-auto flex cursor-pointer justify-center items-center gap-spacing-3 rounded-1 px-spacing-5 py-0 text-4.5 btn-transition disabled:(cursor-not-allowed opacity-50) active:enabled:scale-96 active:has-enabled:scale-96 focusable',
link: 'text-ut-burntorange link:text-ut-burntorange underline underline-offset-2 hover:text-ut-orange focus-visible:text-ut-orange focusable btn-transition ease-out-expo', link: 'text-ut-burntorange link:text-ut-burntorange underline underline-offset-2 hover:text-ut-orange focus-visible:text-ut-orange focusable btn-transition ease-out-expo',
linkanimate: linkanimate:
'relative cursor-pointer transition duration-100 ease-out after:(absolute left-0.4 right-0.4 h-2px scale-x-95 bg-ut-orange opacity-0 transition duration-250 ease-out-expo content-empty -bottom-0.75 -translate-y-0.5) active:scale-95 hover:text-ut-orange focus-visible:text-ut-orange hover:after:(opacity-100) !hover:after:translate-y-0 !hover:after:scale-x-100', 'relative cursor-pointer transition duration-100 ease-out after:(absolute left-0.4 right-0.4 h-2px scale-x-95 bg-ut-orange opacity-0 transition duration-250 ease-out-expo content-empty -bottom-0.75 -translate-y-0.5) active:scale-95 hover:text-ut-orange focus-visible:text-ut-orange hover:after:(opacity-100) !hover:after:translate-y-0 !hover:after:scale-x-100',

View File

@@ -24,16 +24,14 @@ const pagesDir = resolve(root, 'pages');
const assetsDir = resolve(root, 'assets'); const assetsDir = resolve(root, 'assets');
const publicDir = resolve(__dirname, 'public'); const publicDir = resolve(__dirname, 'public');
// Set default environment variables
process.env.PROD = process.env.NODE_ENV === 'production' ? 'true' : 'false';
const isBeta = !!process.env.BETA; const isBeta = !!process.env.BETA;
if (isBeta) { if (isBeta) {
process.env.VITE_BETA_BUILD = 'true'; process.env.VITE_BETA_BUILD = 'true';
} }
process.env.VITE_PACKAGE_VERSION = packageJson.version; process.env.VITE_PACKAGE_VERSION = packageJson.version;
// TODO: Debug this. If PROD is false, VITE_SENTRY_ENVIRONMENT is in production mode
if (process.env.PROD) { // special condition for production sentry instrumentation, as many of our devs like to use `pnpm build` directly. Production instrumentation is added and uploaded during `pnpm zip:to-publish`.
if (process.env.SENTRY_ENV === 'production') {
process.env.VITE_SENTRY_ENVIRONMENT = 'production'; process.env.VITE_SENTRY_ENVIRONMENT = 'production';
} else if (isBeta) { } else if (isBeta) {
process.env.VITE_SENTRY_ENVIRONMENT = 'beta'; process.env.VITE_SENTRY_ENVIRONMENT = 'beta';
@@ -168,6 +166,7 @@ export default defineConfig({
renameFile('src/pages/options/index.html', 'options.html'), renameFile('src/pages/options/index.html', 'options.html'),
renameFile('src/pages/calendar/index.html', 'calendar.html'), renameFile('src/pages/calendar/index.html', 'calendar.html'),
renameFile('src/pages/report/index.html', 'report.html'), renameFile('src/pages/report/index.html', 'report.html'),
renameFile('src/pages/map/index.html', 'map.html'),
renameFile('src/pages/404/index.html', '404.html'), renameFile('src/pages/404/index.html', '404.html'),
vitePluginRunCommandOnDemand({ vitePluginRunCommandOnDemand({
// afterServerStart: 'pnpm gulp forceDisableUseDynamicUrl', // afterServerStart: 'pnpm gulp forceDisableUseDynamicUrl',
@@ -225,6 +224,10 @@ export default defineConfig({
target: 'http://localhost:5173', target: 'http://localhost:5173',
rewrite: path => path.replace('report', 'src/pages/report/index'), rewrite: path => path.replace('report', 'src/pages/report/index'),
}, },
'/map.html': {
target: 'http://localhost:5173',
rewrite: path => path.replace('map', 'src/pages/map/index'),
},
'/404.html': { '/404.html': {
target: 'http://localhost:5173', target: 'http://localhost:5173',
rewrite: path => path.replace('404', 'src/pages/404/index'), rewrite: path => path.replace('404', 'src/pages/404/index'),
@@ -244,6 +247,7 @@ export default defineConfig({
calendar: 'src/pages/calendar/index.html', calendar: 'src/pages/calendar/index.html',
options: 'src/pages/options/index.html', options: 'src/pages/options/index.html',
report: 'src/pages/report/index.html', report: 'src/pages/report/index.html',
map: 'src/pages/map/index.html',
404: 'src/pages/404/index.html', 404: 'src/pages/404/index.html',
}, },
output: { output: {